Skip to content

Commit

Permalink
nixd-next: add diagnostic quick fix (code action)
Browse files Browse the repository at this point in the history
  • Loading branch information
inclyc committed Jan 22, 2024
1 parent a357f6d commit 6492070
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 12 deletions.
107 changes: 96 additions & 11 deletions nixd-next/tools/nixd/src/Controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,29 +39,67 @@ int getLSPSeverity(nixf::Diagnostic::DiagnosticKind Kind) {
assert(false && "Invalid severity");
}

/// Holds analyzed information about a document.
///
/// TU stands for "Translation Unit".
class NixTU {
std::vector<nixf::Diagnostic> Diagnostics;
std::shared_ptr<nixf::Node> AST;

public:
NixTU() = default;
NixTU(std::vector<nixf::Diagnostic> Diagnostics,
std::shared_ptr<nixf::Node> AST)
: Diagnostics(std::move(Diagnostics)), AST(std::move(AST)) {}

[[nodiscard]] const std::vector<nixf::Diagnostic> &diagnostics() const {
return Diagnostics;
}

[[nodiscard]] const std::shared_ptr<nixf::Node> &ast() const { return AST; }
};

class Controller : public LSPServer {
DraftStore Store;

llvm::unique_function<void(const lspserver::PublishDiagnosticsParams &)>
PublishDiagnostic;

llvm::StringMap<NixTU> TUs;

/// Action right after a document is added (including updates).
void actOnDocumentAdd(PathRef File, std::optional<int64_t> Version) {
auto Draft = Store.getDraft(File);
assert(Draft && "Added document is not in the store?");
std::vector<nixf::Diagnostic> Diagnostics;
nixf::parse(*Draft->Contents, Diagnostics);

std::shared_ptr<nixf::Node> AST =
nixf::parse(*Draft->Contents, Diagnostics);
std::vector<Diagnostic> LSPDiags;
LSPDiags.reserve(Diagnostics.size());
for (const nixf::Diagnostic &D : Diagnostics) {
// Format the message.
std::string Message = D.format();

// Add fix information.
if (!D.fixes().empty()) {
Message += " (";
if (D.fixes().size() == 1) {
Message += "fix available";
} else {
Message += std::to_string(D.fixes().size());
Message += " fixes available";
}
Message += ")";
}

LSPDiags.emplace_back(Diagnostic{
.range = toLSPRange(D.range()),
.severity = getLSPSeverity(D.kind()),
.code = D.sname(),
.source = "nixf",
.message = D.format(),
.message = Message,
});

for (const nixf::Note &N : D.notes()) {
LSPDiags.emplace_back(Diagnostic{
.range = toLSPRange(N.range()),
Expand All @@ -76,6 +114,7 @@ class Controller : public LSPServer {
.diagnostics = std::move(LSPDiags),
.version = Version,
});
TUs[File] = NixTU{std::move(Diagnostics), std::move(AST)};
}

void removeDocument(PathRef File) { Store.removeDraft(File); }
Expand All @@ -84,14 +123,19 @@ class Controller : public LSPServer {
[[maybe_unused]] const InitializeParams &Params, Callback<Value> Reply) {

Object ServerCaps{
{
{"textDocumentSync",
llvm::json::Object{
{"openClose", true},
{"change", (int)TextDocumentSyncKind::Incremental},
{"save", true},
}},
},
{{"textDocumentSync",
llvm::json::Object{
{"openClose", true},
{"change", (int)TextDocumentSyncKind::Incremental},
{"save", true},
}},
{
"codeActionProvider",
Object{
{"codeActionKinds", Array{CodeAction::QUICKFIX_KIND}},
{"resolveProvider", false},
},
}},
};

Object Result{{
Expand Down Expand Up @@ -146,6 +190,43 @@ class Controller : public LSPServer {
removeDocument(File);
}

void onCodeAction(const CodeActionParams &Params,
Callback<std::vector<CodeAction>> Reply) {
PathRef File = Params.textDocument.uri.file();
Range Range = Params.range;
const std::vector<nixf::Diagnostic> &Diagnostics = TUs[File].diagnostics();
std::vector<CodeAction> Actions;
Actions.reserve(Diagnostics.size());
for (const nixf::Diagnostic &D : Diagnostics) {
// Skip diagnostics that are not in the range.
if (!Range.contains(toLSPRange(D.range())))
continue;

// Add fixes.
for (const nixf::Fix &F : D.fixes()) {
std::vector<TextEdit> Edits;
Edits.reserve(F.edits().size());
for (const nixf::TextEdit &TE : F.edits()) {
Edits.emplace_back(TextEdit{
.range = toLSPRange(TE.oldRange()),
.newText = std::string(TE.newText()),
});
}
using Changes = std::map<std::string, std::vector<TextEdit>>;
std::string FileURI = URIForFile::canonicalize(File, File).uri();
WorkspaceEdit WE{.changes = Changes{
{std::move(FileURI), std::move(Edits)},
}};
Actions.emplace_back(CodeAction{
.title = F.message(),
.kind = std::string(CodeAction::QUICKFIX_KIND),
.edit = std::move(WE),
});
}
}
Reply(std::move(Actions));
}

public:
Controller(std::unique_ptr<InboundPort> In, std::unique_ptr<OutboundPort> Out)
: LSPServer(std::move(In), std::move(Out)) {
Expand All @@ -162,6 +243,10 @@ class Controller : public LSPServer {

Registry.addNotification("textDocument/didClose", this,
&Controller::onDocumentDidClose);

// Language Features
Registry.addMethod("textDocument/codeAction", this,
&Controller::onCodeAction);
}
};

Expand Down
101 changes: 101 additions & 0 deletions nixd-next/tools/nixd/test/code-action-quick-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@

# RUN: nixd-next --lit-test < %s | FileCheck %s

<-- initialize(0)

```json
{
"jsonrpc":"2.0",
"id":0,
"method":"initialize",
"params":{
"processId":123,
"rootPath":"",
"capabilities":{
},
"trace":"off"
}
}
```


<-- textDocument/didOpen

```json
{
"jsonrpc":"2.0",
"method":"textDocument/didOpen",
"params":{
"textDocument":{
"uri":"file:///basic.nix",
"languageId":"nix",
"version":1,
"text":"/*"
}
}
}
```

<-- textDocument/codeAction(2)


```json
{
"jsonrpc":"2.0",
"id":2,
"method":"textDocument/codeAction",
"params":{
"textDocument":{
"uri":"file:///basic.nix"
},
"range":{
"start":{
"line": 0,
"character":0
},
"end":{
"line":0,
"character":2
}
},
"context":{
"diagnostics":[],
"triggerKind":2
}
}
}
```

```
CHECK: "id": 2,
CHECK-NEXT: "jsonrpc": "2.0",
CHECK-NEXT: "result": [
CHECK-NEXT: {
CHECK-NEXT: "edit": {
CHECK-NEXT: "changes": {
CHECK-NEXT: "file:///basic.nix": [
CHECK-NEXT: {
CHECK-NEXT: "newText": "*/",
CHECK-NEXT: "range": {
CHECK-NEXT: "end": {
CHECK-NEXT: "character": 2,
CHECK-NEXT: "line": 0
CHECK-NEXT: },
CHECK-NEXT: "start": {
CHECK-NEXT: "character": 2,
CHECK-NEXT: "line": 0
CHECK-NEXT: }
CHECK-NEXT: }
CHECK-NEXT: }
CHECK-NEXT: ]
CHECK-NEXT: }
CHECK-NEXT: },
CHECK-NEXT: "kind": "quickfix",
CHECK-NEXT: "title": "insert */"
CHECK-NEXT: }
CHECK-NEXT: ]
```

```json
{"jsonrpc":"2.0","method":"exit"}
```
2 changes: 1 addition & 1 deletion nixd-next/tools/nixd/test/diagnostic.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ CHECK-NEXT: "params": {
CHECK-NEXT: "diagnostics": [
CHECK-NEXT: {
CHECK-NEXT: "code": "lex-unterminated-bcomment",
CHECK-NEXT: "message": "unterminated /* comment",
CHECK-NEXT: "message": "unterminated /* comment (fix available)",
CHECK-NEXT: "range": {
CHECK-NEXT: "end": {
CHECK-NEXT: "character": 2,
Expand Down
6 changes: 6 additions & 0 deletions nixd-next/tools/nixd/test/initialize.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ CHECK-NEXT: "id": 0,
CHECK-NEXT: "jsonrpc": "2.0",
CHECK-NEXT: "result": {
CHECK-NEXT: "capabilities": {
CHECK-NEXT: "codeActionProvider": {
CHECK-NEXT: "codeActionKinds": [
CHECK-NEXT: "quickfix"
CHECK-NEXT: ],
CHECK-NEXT: "resolveProvider": false
CHECK-NEXT: },
CHECK-NEXT: "textDocumentSync": {
CHECK-NEXT: "change": 2,
CHECK-NEXT: "openClose": true,
Expand Down

0 comments on commit 6492070

Please sign in to comment.