Index: clangd/ClangdLSPServer.h =================================================================== --- clangd/ClangdLSPServer.h +++ clangd/ClangdLSPServer.h @@ -166,6 +166,8 @@ SymbolKindBitset SupportedSymbolKinds; /// The supported completion item kinds of the client. CompletionItemKindBitset SupportedCompletionItemKinds; + // Whether the client supports CodeAction response objects. + bool SupportsCodeAction = false; // Store of the current versions of the open documents. DraftStore DraftMgr; Index: clangd/ClangdLSPServer.cpp =================================================================== --- clangd/ClangdLSPServer.cpp +++ clangd/ClangdLSPServer.cpp @@ -95,6 +95,8 @@ Params.capabilities.textDocument.publishDiagnostics.clangdFixSupport; DiagOpts.SendDiagnosticCategory = Params.capabilities.textDocument.publishDiagnostics.categorySupport; + SupportsCodeAction = + Params.capabilities.textDocument.codeActionLiteralSupport; if (Params.capabilities.workspace && Params.capabilities.workspace->symbol && Params.capabilities.workspace->symbol->symbolKind && @@ -331,28 +333,53 @@ }); } +static Optional asCommand(const CodeAction &Action) { + Command Cmd; + if (Action.command && Action.edit) + return llvm::None; // Not representable. (We never emit these anyway). + if (Action.command) { + Cmd = *Action.command; + } else if (Action.edit) { + Cmd.command = Command::CLANGD_APPLY_FIX_COMMAND; + Cmd.workspaceEdit = *Action.edit; + } else { + return llvm::None; + } + Cmd.title = Action.title; + if (Action.kind && *Action.kind == CodeAction::QUICKFIX_KIND) + Cmd.title = "Apply fix: " + Cmd.title; + return Cmd; +} + void ClangdLSPServer::onCodeAction(CodeActionParams &Params) { - // We provide a code action for each diagnostic at the requested location - // which has FixIts available. - auto Code = DraftMgr.getDraft(Params.textDocument.uri.file()); - if (!Code) + // We provide a code action for Fixes on the specified diagnostics. + if (!DraftMgr.getDraft(Params.textDocument.uri.file())) return replyError(ErrorCode::InvalidParams, "onCodeAction called for non-added file"); - json::Array Commands; + std::vector Actions; for (Diagnostic &D : Params.context.diagnostics) { for (auto &F : getFixes(Params.textDocument.uri.file(), D)) { - WorkspaceEdit WE; - std::vector Edits(F.Edits.begin(), F.Edits.end()); - WE.changes = {{Params.textDocument.uri.uri(), std::move(Edits)}}; - Commands.push_back(json::Object{ - {"title", llvm::formatv("Apply fix: {0}", F.Message)}, - {"command", ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND}, - {"arguments", {WE}}, - }); + Actions.emplace_back(); + Actions.back().title = F.Message; + Actions.back().kind = CodeAction::QUICKFIX_KIND; + Actions.back().diagnostics = {D}; + Actions.back().edit.emplace(); + Actions.back().edit->changes.emplace(); + (*Actions.back().edit->changes)[Params.textDocument.uri.uri()] = { + F.Edits.begin(), F.Edits.end()}; } } - reply(std::move(Commands)); + + if (SupportsCodeAction) + reply(json::Array(Actions)); + else { + std::vector Commands; + for (const auto &Action : Actions) + if (auto Command = asCommand(Action)) + Commands.push_back(std::move(*Command)); + reply(json::Array(Commands)); + } } void ClangdLSPServer::onCompletion(TextDocumentPositionParams &Params) { Index: clangd/Protocol.h =================================================================== --- clangd/Protocol.h +++ clangd/Protocol.h @@ -385,6 +385,10 @@ /// Capabilities specific to the 'textDocument/publishDiagnostics' PublishDiagnosticsClientCapabilities publishDiagnostics; + + /// Flattened from codeAction.codeActionLiteralSupport. + // FIXME: flatten other properties in this way. + bool codeActionLiteralSupport = false; }; bool fromJSON(const llvm::json::Value &, TextDocumentClientCapabilities &); @@ -608,6 +612,7 @@ /// which the issue was produced, e.g. "Semantic Issue" or "Parse Issue". std::string category; }; +llvm::json::Value toJSON(const Diagnostic &); /// A LSP-specific comparator used to find diagnostic in a container like /// std:map. @@ -673,9 +678,34 @@ struct Command : public ExecuteCommandParams { std::string title; }; - llvm::json::Value toJSON(const Command &C); +/// A code action represents a change that can be performed in code, e.g. to fix +/// a problem or to refactor code. +/// +/// A CodeAction must set either `edit` and/or a `command`. If both are +/// supplied, the `edit` is applied first, then the `command` is executed. +struct CodeAction { + /// A short, human-readable, title for this code action. + std::string title; + + /// The kind of the code action. + /// Used to filter code actions. + llvm::Optional kind; + const static llvm::StringLiteral QUICKFIX_KIND; + + /// The diagnostics that this code action resolves. + llvm::Optional> diagnostics; + + /// The workspace edit this code action performs. + llvm::Optional edit; + + /// A command this code action executes. If a code action provides an edit + /// and a command, first the edit is executed and then the command. + llvm::Optional command; +}; +llvm::json::Value toJSON(const CodeAction &); + /// Represents information about programming constructs like variables, classes, /// interfaces etc. struct SymbolInformation { Index: clangd/Protocol.cpp =================================================================== --- clangd/Protocol.cpp +++ clangd/Protocol.cpp @@ -251,6 +251,9 @@ return false; O.map("completion", R.completion); O.map("publishDiagnostics", R.publishDiagnostics); + if (auto *CodeAction = Params.getAsObject()->getObject("codeAction")) + if (CodeAction->getObject("codeActionLiteralSupport")) + R.codeActionLiteralSupport = true; return true; } @@ -360,6 +363,17 @@ return O && O.map("textDocument", R.textDocument); } +llvm::json::Value toJSON(const Diagnostic &D) { + json::Object Diag{ + {"range", D.range}, + {"severity", D.severity}, + {"message", D.message}, + }; + // FIXME: this should be used for publishDiagnostics. + // FIXME: send category and fixes when appropriate. + return std::move(Diag); +} + bool fromJSON(const json::Value &Params, Diagnostic &R) { json::ObjectMapper O(Params); if (!O || !O.map("range", R.range) || !O.map("message", R.message)) @@ -448,6 +462,21 @@ return std::move(Cmd); } +const llvm::StringLiteral CodeAction::QUICKFIX_KIND = "quickfix"; + +llvm::json::Value toJSON(const CodeAction &CA) { + auto CodeAction = json::Object{{"title", CA.title}}; + if (CA.kind) + CodeAction["kind"] = *CA.kind; + if (CA.diagnostics) + CodeAction["diagnostics"] = json::Array(*CA.diagnostics); + if (CA.edit) + CodeAction["edit"] = *CA.edit; + if (CA.command) + CodeAction["command"] = *CA.command; + return std::move(CodeAction); +} + json::Value toJSON(const WorkspaceEdit &WE) { if (!WE.changes) return json::Object{}; Index: test/clangd/fixits-codeaction.test =================================================================== --- /dev/null +++ test/clangd/fixits-codeaction.test @@ -0,0 +1,126 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{"textDocument":{"codeAction":{"codeActionLiteralSupport":{}}}},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///foo.c","languageId":"c","version":1,"text":"int main(int i, char **a) { if (i = 2) {}}"}}} +# CHECK: "method": "textDocument/publishDiagnostics", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "diagnostics": [ +# CHECK-NEXT: { +# CHECK-NEXT: "message": "Using the result of an assignment as a condition without parentheses", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 37, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 32, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "severity": 2 +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "uri": "file://{{.*}}/foo.c" +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","id":2,"method":"textDocument/codeAction","params":{"textDocument":{"uri":"test:///foo.c"},"range":{"start":{"line":104,"character":13},"end":{"line":0,"character":35}},"context":{"diagnostics":[{"range":{"start": {"line": 0, "character": 32}, "end": {"line": 0, "character": 37}},"severity":2,"message":"Using the result of an assignment as a condition without parentheses"}]}}} +# CHECK: "id": 2, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": [ +# CHECK-NEXT: { +# CHECK-NEXT: "diagnostics": [ +# CHECK-NEXT: { +# CHECK-NEXT: "message": "Using the result of an assignment as a condition without parentheses", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 37, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 32, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "severity": 2 +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "edit": { +# CHECK-NEXT: "changes": { +# CHECK-NEXT: "file://{{.*}}/foo.c": [ +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "(", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 32, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 32, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "newText": ")", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 37, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 37, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "kind": "quickfix", +# CHECK-NEXT: "title": "place parentheses around the assignment to silence this warning" +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "diagnostics": [ +# CHECK-NEXT: { +# CHECK-NEXT: "message": "Using the result of an assignment as a condition without parentheses", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 37, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 32, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "severity": 2 +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "edit": { +# CHECK-NEXT: "changes": { +# CHECK-NEXT: "file://{{.*}}/foo.c": [ +# CHECK-NEXT: { +# CHECK-NEXT: "newText": "==", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 35, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 34, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "kind": "quickfix", +# CHECK-NEXT: "title": "use '==' to turn this assignment into an equality comparison" +# CHECK-NEXT: } +# CHECK-NEXT: ] +--- +{"jsonrpc":"2.0","id":4,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} + Index: test/clangd/fixits.test =================================================================== --- /dev/null +++ test/clangd/fixits.test @@ -1,210 +0,0 @@ -# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s -{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} ---- -{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///foo.c","languageId":"c","version":1,"text":"int main(int i, char **a) { if (i = 2) {}}"}}} -# CHECK: "method": "textDocument/publishDiagnostics", -# CHECK-NEXT: "params": { -# CHECK-NEXT: "diagnostics": [ -# CHECK-NEXT: { -# CHECK-NEXT: "message": "Using the result of an assignment as a condition without parentheses", -# CHECK-NEXT: "range": { -# CHECK-NEXT: "end": { -# CHECK-NEXT: "character": 37, -# CHECK-NEXT: "line": 0 -# CHECK-NEXT: }, -# CHECK-NEXT: "start": { -# CHECK-NEXT: "character": 32, -# CHECK-NEXT: "line": 0 -# CHECK-NEXT: } -# CHECK-NEXT: }, -# CHECK-NEXT: "severity": 2 -# CHECK-NEXT: } -# CHECK-NEXT: ], -# CHECK-NEXT: "uri": "file://{{.*}}/foo.c" -# CHECK-NEXT: } ---- -{"jsonrpc":"2.0","id":2,"method":"textDocument/codeAction","params":{"textDocument":{"uri":"test:///foo.c"},"range":{"start":{"line":104,"character":13},"end":{"line":0,"character":35}},"context":{"diagnostics":[{"range":{"start": {"line": 0, "character": 32}, "end": {"line": 0, "character": 37}},"severity":2,"message":"Using the result of an assignment as a condition without parentheses"}]}}} -# CHECK: "id": 2, -# CHECK-NEXT: "jsonrpc": "2.0", -# CHECK-NEXT: "result": [ -# CHECK-NEXT: { -# CHECK-NEXT: "arguments": [ -# CHECK-NEXT: { -# CHECK-NEXT: "changes": { -# CHECK-NEXT: "file://{{.*}}/foo.c": [ -# CHECK-NEXT: { -# CHECK-NEXT: "newText": "(", -# CHECK-NEXT: "range": { -# CHECK-NEXT: "end": { -# CHECK-NEXT: "character": 32, -# CHECK-NEXT: "line": 0 -# CHECK-NEXT: }, -# CHECK-NEXT: "start": { -# CHECK-NEXT: "character": 32, -# CHECK-NEXT: "line": 0 -# CHECK-NEXT: } -# CHECK-NEXT: } -# CHECK-NEXT: }, -# CHECK-NEXT: { -# CHECK-NEXT: "newText": ")", -# CHECK-NEXT: "range": { -# CHECK-NEXT: "end": { -# CHECK-NEXT: "character": 37, -# CHECK-NEXT: "line": 0 -# CHECK-NEXT: }, -# CHECK-NEXT: "start": { -# CHECK-NEXT: "character": 37, -# CHECK-NEXT: "line": 0 -# CHECK-NEXT: } -# CHECK-NEXT: } -# CHECK-NEXT: } -# CHECK-NEXT: ] -# CHECK-NEXT: } -# CHECK-NEXT: } -# CHECK-NEXT: ], -# CHECK-NEXT: "command": "clangd.applyFix", -# CHECK-NEXT: "title": "Apply fix: place parentheses around the assignment to silence this warning" -# CHECK-NEXT: }, -# CHECK-NEXT: { -# CHECK-NEXT: "arguments": [ -# CHECK-NEXT: { -# CHECK-NEXT: "changes": { -# CHECK-NEXT: "file://{{.*}}/foo.c": [ -# CHECK-NEXT: { -# CHECK-NEXT: "newText": "==", -# CHECK-NEXT: "range": { -# CHECK-NEXT: "end": { -# CHECK-NEXT: "character": 35, -# CHECK-NEXT: "line": 0 -# CHECK-NEXT: }, -# CHECK-NEXT: "start": { -# CHECK-NEXT: "character": 34, -# CHECK-NEXT: "line": 0 -# CHECK-NEXT: } -# CHECK-NEXT: } -# CHECK-NEXT: } -# CHECK-NEXT: ] -# CHECK-NEXT: } -# CHECK-NEXT: } -# CHECK-NEXT: ], -# CHECK-NEXT: "command": "clangd.applyFix", -# CHECK-NEXT: "title": "Apply fix: use '==' to turn this assignment into an equality comparison" -# CHECK-NEXT: } -# CHECK-NEXT: ] ---- -{"jsonrpc":"2.0","id":3,"method":"textDocument/codeAction","params":{"textDocument":{"uri":"test:///foo.c"},"range":{"start":{"line":104,"character":13},"end":{"line":0,"character":35}},"context":{"diagnostics":[{"range":{"start": {"line": 0, "character": 32}, "end": {"line": 0, "character": 37}},"severity":2,"message":"Using the result of an assignment as a condition without parentheses"}]}}} -# Make sure unused "code" and "source" fields ignored gracefully -# CHECK: "id": 3, -# CHECK-NEXT: "jsonrpc": "2.0", -# CHECK-NEXT: "result": [ -# CHECK-NEXT: { -# CHECK-NEXT: "arguments": [ -# CHECK-NEXT: { -# CHECK-NEXT: "changes": { -# CHECK-NEXT: "file://{{.*}}/foo.c": [ -# CHECK-NEXT: { -# CHECK-NEXT: "newText": "(", -# CHECK-NEXT: "range": { -# CHECK-NEXT: "end": { -# CHECK-NEXT: "character": 32, -# CHECK-NEXT: "line": 0 -# CHECK-NEXT: }, -# CHECK-NEXT: "start": { -# CHECK-NEXT: "character": 32, -# CHECK-NEXT: "line": 0 -# CHECK-NEXT: } -# CHECK-NEXT: } -# CHECK-NEXT: }, -# CHECK-NEXT: { -# CHECK-NEXT: "newText": ")", -# CHECK-NEXT: "range": { -# CHECK-NEXT: "end": { -# CHECK-NEXT: "character": 37, -# CHECK-NEXT: "line": 0 -# CHECK-NEXT: }, -# CHECK-NEXT: "start": { -# CHECK-NEXT: "character": 37, -# CHECK-NEXT: "line": 0 -# CHECK-NEXT: } -# CHECK-NEXT: } -# CHECK-NEXT: } -# CHECK-NEXT: ] -# CHECK-NEXT: } -# CHECK-NEXT: } -# CHECK-NEXT: ], -# CHECK-NEXT: "command": "clangd.applyFix", -# CHECK-NEXT: "title": "Apply fix: place parentheses around the assignment to silence this warning" -# CHECK-NEXT: }, -# CHECK-NEXT: { -# CHECK-NEXT: "arguments": [ -# CHECK-NEXT: { -# CHECK-NEXT: "changes": { -# CHECK-NEXT: "file://{{.*}}/foo.c": [ -# CHECK-NEXT: { -# CHECK-NEXT: "newText": "==", -# CHECK-NEXT: "range": { -# CHECK-NEXT: "end": { -# CHECK-NEXT: "character": 35, -# CHECK-NEXT: "line": 0 -# CHECK-NEXT: }, -# CHECK-NEXT: "start": { -# CHECK-NEXT: "character": 34, -# CHECK-NEXT: "line": 0 -# CHECK-NEXT: } -# CHECK-NEXT: } -# CHECK-NEXT: } -# CHECK-NEXT: ] -# CHECK-NEXT: } -# CHECK-NEXT: } -# CHECK-NEXT: ], -# CHECK-NEXT: "command": "clangd.applyFix", -# CHECK-NEXT: "title": "Apply fix: use '==' to turn this assignment into an equality comparison" -# CHECK-NEXT: } -# CHECK-NEXT: ] ---- -{"jsonrpc":"2.0","id":4,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","arguments":[{"changes":{"test:///foo.c":[{"range":{"start":{"line":0,"character":32},"end":{"line":0,"character":32}},"newText":"("},{"range":{"start":{"line":0,"character":37},"end":{"line":0,"character":37}},"newText":")"}]}}]}} -# CHECK: "id": 4, -# CHECK-NEXT: "jsonrpc": "2.0", -# CHECK-NEXT: "result": "Fix applied." -# -# CHECK: "id": 1, -# CHECK-NEXT: "jsonrpc": "2.0", -# CHECK-NEXT: "method": "workspace/applyEdit", -# CHECK-NEXT: "params": { -# CHECK-NEXT: "edit": { -# CHECK-NEXT: "changes": { -# CHECK-NEXT: "{{.*}}/foo.c": [ -# CHECK-NEXT: { -# CHECK-NEXT: "newText": "(", -# CHECK-NEXT: "range": { -# CHECK-NEXT: "end": { -# CHECK-NEXT: "character": 32, -# CHECK-NEXT: "line": 0 -# CHECK-NEXT: }, -# CHECK-NEXT: "start": { -# CHECK-NEXT: "character": 32, -# CHECK-NEXT: "line": 0 -# CHECK-NEXT: } -# CHECK-NEXT: } -# CHECK-NEXT: }, -# CHECK-NEXT: { -# CHECK-NEXT: "newText": ")", -# CHECK-NEXT: "range": { -# CHECK-NEXT: "end": { -# CHECK-NEXT: "character": 37, -# CHECK-NEXT: "line": 0 -# CHECK-NEXT: }, -# CHECK-NEXT: "start": { -# CHECK-NEXT: "character": 37, -# CHECK-NEXT: "line": 0 -# CHECK-NEXT: } -# CHECK-NEXT: } -# CHECK-NEXT: } -# CHECK-NEXT: ] -# CHECK-NEXT: } -# CHECK-NEXT: } -# CHECK-NEXT: } ---- -{"jsonrpc":"2.0","id":4,"method":"shutdown"} ---- -{"jsonrpc":"2.0","method":"exit"} Index: test/include-fixer/merge.test =================================================================== --- test/include-fixer/merge.test +++ test/include-fixer/merge.test @@ -6,7 +6,7 @@ Contexts: - ContextType: Namespace ContextName: a -FilePath: ../include/bar.h +FilePath: '../include/bar.h' Type: Class Seen: 1 Used: 1 @@ -16,7 +16,7 @@ Contexts: - ContextType: Namespace ContextName: a -FilePath: ../include/barbar.h +FilePath: '../include/barbar.h' Type: Class Seen: 1 Used: 0