Index: clangd/ClangdLSPServer.h =================================================================== --- clangd/ClangdLSPServer.h +++ clangd/ClangdLSPServer.h @@ -69,6 +69,7 @@ void onGoToDefinition(Ctx C, TextDocumentPositionParams &Params) override; void onSwitchSourceHeader(Ctx C, TextDocumentIdentifier &Params) override; void onFileEvent(Ctx C, DidChangeWatchedFilesParams &Params) override; + void onCommand(Ctx C, ExecuteCommandParams &Params) override; std::vector getFixIts(StringRef File, const clangd::Diagnostic &D); Index: clangd/ClangdLSPServer.cpp =================================================================== --- clangd/ClangdLSPServer.cpp +++ clangd/ClangdLSPServer.cpp @@ -10,27 +10,25 @@ #include "ClangdLSPServer.h" #include "JSONRPCDispatcher.h" +#include "llvm/Support/FormatVariadic.h" + using namespace clang::clangd; using namespace clang; namespace { -std::string +std::vector replacementsToEdits(StringRef Code, const std::vector &Replacements) { + std::vector Edits; // Turn the replacements into the format specified by the Language Server - // Protocol. Fuse them into one big JSON array. - std::string Edits; + // Protocol. for (auto &R : Replacements) { Range ReplacementRange = { offsetToPosition(Code, R.getOffset()), offsetToPosition(Code, R.getOffset() + R.getLength())}; - TextEdit TE = {ReplacementRange, R.getReplacementText()}; - Edits += TextEdit::unparse(TE); - Edits += ','; + Edits.push_back({ReplacementRange, R.getReplacementText()}); } - if (!Edits.empty()) - Edits.pop_back(); return Edits; } @@ -47,7 +45,9 @@ "codeActionProvider": true, "completionProvider": {"resolveProvider": false, "triggerCharacters": [".",">",":"]}, "signatureHelpProvider": {"triggerCharacters": ["(",","]}, - "definitionProvider": true + "definitionProvider": true, + "executeCommandProvider": {"commands": [")" + + ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND + R"("]} }})"); if (Params.rootUri && !Params.rootUri->file.empty()) Server.setRootPath(Params.rootUri->file); @@ -84,6 +84,34 @@ Server.onFileEvent(Params); } +void ClangdLSPServer::onCommand(Ctx C, ExecuteCommandParams &Params) { + if (Params.command == ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND && + Params.workspaceEdit) { + // The flow for "apply-fix" : + // 1. We publish a diagnostic, including fixits + // 2. The user clicks on the diagnostic, the editor asks us for code actions + // 3. We send code actions, with the fixit embedded as context + // 4. The user selects the fixit, the editor asks us to apply it + // 5. We unwrap the changes and send them back to the editor + // 6. The editor applies the changes (applyEdit), and sends us a reply (but + // we ignore it) + + ApplyWorkspaceEditParams ApplyEdit; + ApplyEdit.edit = *Params.workspaceEdit; + C.reply("\"Fix applied.\""); + // We don't need the response so id == 1 is OK. + // Ideally, we would wait for the response and if there is no error, we + // would reply success/failure to the original RPC. + C.call("workspace/applyEdit", ApplyWorkspaceEditParams::unparse(ApplyEdit)); + } else { + // We should not get here because ExecuteCommandParams would not have + // parsed in the first place and this handler should not be called. But if + // more commands are added, this will be here has a safe guard. + C.replyError( + 1, llvm::formatv("Unsupported command \"{0}\".", Params.command).str()); + } +} + void ClangdLSPServer::onDocumentDidClose(Ctx C, DidCloseTextDocumentParams &Params) { Server.removeDocument(Params.textDocument.uri.file); @@ -93,26 +121,27 @@ Ctx C, DocumentOnTypeFormattingParams &Params) { auto File = Params.textDocument.uri.file; std::string Code = Server.getDocument(File); - std::string Edits = - replacementsToEdits(Code, Server.formatOnType(File, Params.position)); - C.reply("[" + Edits + "]"); + std::string Edits = TextEdit::unparse( + replacementsToEdits(Code, Server.formatOnType(File, Params.position))); + C.reply(Edits); } void ClangdLSPServer::onDocumentRangeFormatting( Ctx C, DocumentRangeFormattingParams &Params) { auto File = Params.textDocument.uri.file; std::string Code = Server.getDocument(File); - std::string Edits = - replacementsToEdits(Code, Server.formatRange(File, Params.range)); - C.reply("[" + Edits + "]"); + std::string Edits = TextEdit::unparse( + replacementsToEdits(Code, Server.formatRange(File, Params.range))); + C.reply(Edits); } void ClangdLSPServer::onDocumentFormatting(Ctx C, DocumentFormattingParams &Params) { auto File = Params.textDocument.uri.file; std::string Code = Server.getDocument(File); - std::string Edits = replacementsToEdits(Code, Server.formatFile(File)); - C.reply("[" + Edits + "]"); + std::string Edits = + TextEdit::unparse(replacementsToEdits(Code, Server.formatFile(File))); + C.reply(Edits); } void ClangdLSPServer::onCodeAction(Ctx C, CodeActionParams &Params) { @@ -123,15 +152,16 @@ for (Diagnostic &D : Params.context.diagnostics) { std::vector Fixes = getFixIts(Params.textDocument.uri.file, D); - std::string Edits = replacementsToEdits(Code, Fixes); + auto Edits = replacementsToEdits(Code, Fixes); + WorkspaceEdit WE; + WE.changes = {llvm::yaml::escape(Params.textDocument.uri.uri), Edits}; if (!Edits.empty()) Commands += R"({"title":"Apply FixIt ')" + llvm::yaml::escape(D.message) + - R"('", "command": "clangd.applyFix", "arguments": [")" + - llvm::yaml::escape(Params.textDocument.uri.uri) + - R"(", [)" + Edits + - R"(]]},)"; + R"('", "command": ")" + + ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND + + R"(", "arguments": [)" + WorkspaceEdit::unparse(WE) + R"(]},)"; } if (!Commands.empty()) Commands.pop_back(); Index: clangd/JSONRPCDispatcher.h =================================================================== --- clangd/JSONRPCDispatcher.h +++ clangd/JSONRPCDispatcher.h @@ -58,6 +58,8 @@ void reply(const Twine &Result); /// Sends an error response to the client, and logs it. void replyError(int code, const llvm::StringRef &Message); + /// Sends a request to the client. + void call(llvm::StringRef Method, StringRef Params); private: JSONOutput &Out; Index: clangd/JSONRPCDispatcher.cpp =================================================================== --- clangd/JSONRPCDispatcher.cpp +++ clangd/JSONRPCDispatcher.cpp @@ -64,6 +64,13 @@ } } +void RequestContext::call(StringRef Method, StringRef Params) { + // FIXME: Generate/Increment IDs for every request so that we can get proper + // replies once we need to. + Out.writeMessage(llvm::Twine(R"({"jsonrpc":"2.0","id":1,"method":")" + + Method + R"(","params":)" + Params + R"(})")); +} + void JSONRPCDispatcher::registerHandler(StringRef Method, Handler H) { assert(!Handlers.count(Method) && "Handler already registered!"); Handlers[Method] = std::move(H); Index: clangd/Protocol.h =================================================================== --- clangd/Protocol.h +++ clangd/Protocol.h @@ -141,6 +141,7 @@ static llvm::Optional parse(llvm::yaml::MappingNode *Params, clangd::Logger &Logger); static std::string unparse(const TextEdit &P); + static std::string unparse(const std::vector &TextEdits); }; struct TextDocumentItem { @@ -382,6 +383,43 @@ clangd::Logger &Logger); }; +struct WorkspaceEdit { + /// Holds changes to existing resources. + llvm::Optional>> changes; + + static llvm::Optional parse(llvm::yaml::MappingNode *Params, + clangd::Logger &Logger); + static std::string unparse(const WorkspaceEdit &WE); +}; + +/// Exact commands are not specified in the protocol so we define the +/// ones supported by Clangd here. The protocol specifies the command arguments +/// to be "any[]" but to make this safer and more manageable, each command we +/// handle maps to a certain llvm::Optional of some struct to contain its +/// arguments. Different commands could reuse the same llvm::Optional as +/// arguments but a command that needs different arguments would simply add a +/// new llvm::Optional and not use any other ones. In practice this means only +/// one argument type will be parsed and set. +struct ExecuteCommandParams { + // Command to apply fix-its. Uses WorkspaceEdit as argument. + const static std::string CLANGD_APPLY_FIX_COMMAND; + + /// The command identifier, e.g. CLANGD_APPLY_FIX_COMMAND + std::string command; + + // Arguments + + llvm::Optional workspaceEdit; + + static llvm::Optional + parse(llvm::yaml::MappingNode *Params, clangd::Logger &Logger); +}; + +struct ApplyWorkspaceEditParams { + WorkspaceEdit edit; + static std::string unparse(const ApplyWorkspaceEditParams &Params); +}; + struct TextDocumentPositionParams { /// The text document. TextDocumentIdentifier textDocument; Index: clangd/Protocol.cpp =================================================================== --- clangd/Protocol.cpp +++ clangd/Protocol.cpp @@ -287,6 +287,19 @@ return Result; } +std::string TextEdit::unparse(const std::vector &TextEdits) { + // Fuse all edits into one big JSON array. + std::string Edits; + for (auto &TE : TextEdits) { + Edits += TextEdit::unparse(TE); + Edits += ','; + } + if (!Edits.empty()) + Edits.pop_back(); + + return "[" + Edits + "]"; +} + namespace { TraceLevel getTraceLevel(llvm::StringRef TraceLevelStr, clangd::Logger &Logger) { @@ -846,6 +859,147 @@ return Result; } +llvm::Optional>> +parseWorkspaceEditChange(llvm::yaml::MappingNode *Params, + clangd::Logger &Logger) { + std::pair> Result; + for (auto &NextKeyValue : *Params) { + auto *KeyString = dyn_cast(NextKeyValue.getKey()); + if (!KeyString) + return llvm::None; + + llvm::SmallString<10> KeyStorage; + StringRef KeyValue = KeyString->getValue(KeyStorage); + if (!Result.first.empty()) { + logIgnoredField(KeyValue, Logger); + continue; + } + + Result.first = KeyValue; + auto *Value = + dyn_cast_or_null(NextKeyValue.getValue()); + if (!Value) + return llvm::None; + for (auto &Item : *Value) { + auto *ItemValue = dyn_cast_or_null(&Item); + if (!ItemValue) + return llvm::None; + auto Parsed = TextEdit::parse(ItemValue, Logger); + if (!Parsed) + return llvm::None; + + Result.second.push_back(*Parsed); + } + } + + return Result; +} + +llvm::Optional +WorkspaceEdit::parse(llvm::yaml::MappingNode *Params, clangd::Logger &Logger) { + WorkspaceEdit Result; + for (auto &NextKeyValue : *Params) { + auto *KeyString = dyn_cast(NextKeyValue.getKey()); + if (!KeyString) + return llvm::None; + + llvm::SmallString<10> KeyStorage; + StringRef KeyValue = KeyString->getValue(KeyStorage); + + llvm::SmallString<10> Storage; + if (KeyValue == "changes") { + auto *Value = + dyn_cast_or_null(NextKeyValue.getValue()); + if (!Value) + return llvm::None; + auto Parsed = parseWorkspaceEditChange(Value, Logger); + if (!Parsed) + return llvm::None; + Result.changes = std::move(*Parsed); + } else { + logIgnoredField(KeyValue, Logger); + } + } + return Result; +} + +const std::string ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND = + "clangd.applyFix"; + +llvm::Optional +ExecuteCommandParams::parse(llvm::yaml::MappingNode *Params, + clangd::Logger &Logger) { + ExecuteCommandParams Result; + // Depending on which "command" we parse, we will use this function to parse + // the command "arguments". + std::function ArgParser = nullptr; + + for (auto &NextKeyValue : *Params) { + auto *KeyString = dyn_cast(NextKeyValue.getKey()); + if (!KeyString) + return llvm::None; + + llvm::SmallString<10> KeyStorage; + StringRef KeyValue = KeyString->getValue(KeyStorage); + + // Note that "commands" has to be parsed before "arguments" for this to + // work properly. + if (KeyValue == "command") { + auto *ScalarValue = + dyn_cast_or_null(NextKeyValue.getValue()); + if (!ScalarValue) + return llvm::None; + llvm::SmallString<10> Storage; + Result.command = ScalarValue->getValue(Storage); + if (Result.command == ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND) { + ArgParser = [&Result, &Logger](llvm::yaml::MappingNode *Params) { + auto WE = WorkspaceEdit::parse(Params, Logger); + if (WE) + Result.workspaceEdit = WE; + return WE.hasValue(); + }; + } else { + Logger.log(llvm::formatv("Unknown command \"{0}\".\n", Result.command)); + return llvm::None; + } + } else if (KeyValue == "arguments") { + auto *Value = NextKeyValue.getValue(); + auto *Seq = dyn_cast(Value); + if (!Seq) + return llvm::None; + for (auto &Item : *Seq) { + auto *ItemValue = dyn_cast_or_null(&Item); + if (!ItemValue || !ArgParser) + return llvm::None; + if (!ArgParser(ItemValue)) + return llvm::None; + } + } else { + logIgnoredField(KeyValue, Logger); + } + } + if (Result.command.empty()) + return llvm::None; + + return Result; +} + +std::string WorkspaceEdit::unparse(const WorkspaceEdit &WE) { + std::string Result; + llvm::raw_string_ostream(Result) + << llvm::format(R"({"changes": {"%s": %s}})", WE.changes->first.c_str(), + TextEdit::unparse(WE.changes->second).c_str()); + return Result; +} + +std::string +ApplyWorkspaceEditParams::unparse(const ApplyWorkspaceEditParams &Params) { + std::string Result; + llvm::raw_string_ostream(Result) << llvm::format( + R"({"edit": %s})", WorkspaceEdit::unparse(Params.edit).c_str()); + return Result; +} + llvm::Optional TextDocumentPositionParams::parse(llvm::yaml::MappingNode *Params, clangd::Logger &Logger) { Index: clangd/ProtocolHandlers.h =================================================================== --- clangd/ProtocolHandlers.h +++ clangd/ProtocolHandlers.h @@ -52,6 +52,7 @@ virtual void onGoToDefinition(Ctx C, TextDocumentPositionParams &Params) = 0; virtual void onSwitchSourceHeader(Ctx C, TextDocumentIdentifier &Params) = 0; virtual void onFileEvent(Ctx C, DidChangeWatchedFilesParams &Params) = 0; + virtual void onCommand(Ctx C, ExecuteCommandParams &Params) = 0; }; void registerCallbackHandlers(JSONRPCDispatcher &Dispatcher, JSONOutput &Out, Index: clangd/ProtocolHandlers.cpp =================================================================== --- clangd/ProtocolHandlers.cpp +++ clangd/ProtocolHandlers.cpp @@ -68,4 +68,5 @@ Register("textDocument/switchSourceHeader", &ProtocolCallbacks::onSwitchSourceHeader); Register("workspace/didChangeWatchedFiles", &ProtocolCallbacks::onFileEvent); + Register("workspace/executeCommand", &ProtocolCallbacks::onCommand); } Index: clangd/clients/clangd-vscode/src/extension.ts =================================================================== --- clangd/clients/clangd-vscode/src/extension.ts +++ clangd/clients/clangd-vscode/src/extension.ts @@ -40,27 +40,7 @@ }; const clangdClient = new vscodelc.LanguageClient('Clang Language Server', serverOptions, clientOptions); - - function applyTextEdits(uri: string, edits: vscodelc.TextEdit[]) { - let textEditor = vscode.window.activeTextEditor; - - // FIXME: vscode expects that uri will be percent encoded - if (textEditor && textEditor.document.uri.toString(true) === uri) { - textEditor.edit(mutator => { - for (const edit of edits) { - mutator.replace(clangdClient.protocol2CodeConverter.asRange(edit.range), edit.newText); - } - }).then((success) => { - if (!success) { - vscode.window.showErrorMessage('Failed to apply fixes to the document.'); - } - }); - } - } - console.log('Clang Language Server is now active!'); const disposable = clangdClient.start(); - - context.subscriptions.push(disposable, vscode.commands.registerCommand('clangd.applyFix', applyTextEdits)); } Index: test/clangd/execute-command.test =================================================================== --- /dev/null +++ test/clangd/execute-command.test @@ -0,0 +1,81 @@ +# RUN: clangd -run-synchronously < %s | FileCheck %s +# RUN: clangd -run-synchronously < %s 2>&1 | FileCheck -check-prefix=STDERR %s +# It is absolutely vital that this file has CRLF line endings. +# +Content-Length: 125 + +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +# +Content-Length: 180 + +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file:///foo.c","languageId":"c","version":1,"text":"int main(int i, char **a) { if (i = 2) {}}"}}} +# +# CHECK: {"jsonrpc":"2.0","method":"textDocument/publishDiagnostics","params":{"uri":"file:///foo.c","diagnostics":[{"range":{"start": {"line": 0, "character": 35}, "end": {"line": 0, "character": 35}},"severity":2,"message":"using the result of an assignment as a condition without parentheses"},{"range":{"start": {"line": 0, "character": 35}, "end": {"line": 0, "character": 35}},"severity":3,"message":"place parentheses around the assignment to silence this warning"},{"range":{"start": {"line": 0, "character": 35}, "end": {"line": 0, "character": 35}},"severity":3,"message":"use '==' to turn this assignment into an equality comparison"}]}} +# +Content-Length: 72 + +{"jsonrpc":"2.0","id":3,"method":"workspace/executeCommand","params":{}} +# No command name +# STDERR: Failed to decode workspace/executeCommand request. +Content-Length: 85 + +{"jsonrpc":"2.0","id":4,"method":"workspace/executeCommand","params":{"command": {}}} +# Invalid, non-scalar command name +# STDERR: Failed to decode workspace/executeCommand request. +Content-Length: 345 + +{"jsonrpc":"2.0","id":5,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","custom":"foo", "arguments":[{"changes":{"file:///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":")"}]}}]}} +# STDERR: Ignored unknown field "custom" +Content-Length: 117 + +{"jsonrpc":"2.0","id":6,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","arguments":"foo"}} +# Arguments not a sequence. +# STDERR: Failed to decode workspace/executeCommand request. +Content-Length: 93 + +{"jsonrpc":"2.0","id":7,"method":"workspace/executeCommand","params":{"command":"mycommand"}} +# Unknown command. +# STDERR: Unknown command "mycommand". +Content-Length: 132 + +{"jsonrpc":"2.0","id":8,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","custom":"foo", "arguments":[""]}} +# ApplyFix argument not a mapping node. +# STDERR: Failed to decode workspace/executeCommand request. +Content-Length: 345 + +{"jsonrpc":"2.0","id":9,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","arguments":[{"custom":"foo", "changes":{"file:///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":")"}]}}]}} +# Custom field in WorkspaceEdit +# STDERR: Ignored unknown field "custom" +Content-Length: 132 + +{"jsonrpc":"2.0","id":10,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","arguments":[{"changes":"foo"}]}} +# changes in WorkspaceEdit with no mapping node +# STDERR: Failed to decode workspace/executeCommand request. +Content-Length: 346 + +{"jsonrpc":"2.0","id":11,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","arguments":[{"changes":{"file:///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":")"}], "custom":"foo"}}]}} +# Custom field in WorkspaceEditChange +# STDERR: Ignored unknown field "custom" +Content-Length: 150 + +{"jsonrpc":"2.0","id":12,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","arguments":[{"changes":{"file:///foo.c":"bar"}}]}} +# No sequence node for TextEdits +# STDERR: Failed to decode workspace/executeCommand request. +Content-Length: 149 + +{"jsonrpc":"2.0","id":13,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","arguments":[{"changes":{"file:///foo.c":[""]}}]}} +# No mapping node for TextEdit +# STDERR: Failed to decode workspace/executeCommand request. +Content-Length: 265 + +{"jsonrpc":"2.0","id":14,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","arguments":[{"changes":{"file:///foo.c":[{"range":{"start":{"line":0,"character":32},"end":{"line":0,"character":32}},"newText":"("},{"range":"","newText":")"}]}}]}} +# TextEdit not decoded +# STDERR: Failed to decode workspace/executeCommand request. +Content-Length: 345 + +{"jsonrpc":"2.0","id":9,"method":"workspace/executeCommand","params":{"arguments":[{"custom":"foo", "changes":{"file:///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":")"}]}}],"command":"clangd.applyFix"}} +# Command name after arguments +# STDERR: Failed to decode workspace/executeCommand request. +Content-Length: 44 + +{"jsonrpc":"2.0","id":3,"method":"shutdown"} Index: test/clangd/fixits.test =================================================================== --- test/clangd/fixits.test +++ test/clangd/fixits.test @@ -13,16 +13,21 @@ # Content-Length: 746 - {"jsonrpc":"2.0","id":2,"method":"textDocument/codeAction","params":{"textDocument":{"uri":"file:///foo.c"},"range":{"start":{"line":104,"character":13},"end":{"line":0,"character":35}},"context":{"diagnostics":[{"range":{"start": {"line": 0, "character": 35}, "end": {"line": 0, "character": 35}},"severity":2,"message":"using the result of an assignment as a condition without parentheses"},{"range":{"start": {"line": 0, "character": 35}, "end": {"line": 0, "character": 35}},"severity":3,"message":"place parentheses around the assignment to silence this warning"},{"range":{"start": {"line": 0, "character": 35}, "end": {"line": 0, "character": 35}},"severity":3,"message":"use '==' to turn this assignment into an equality comparison"}]}}} +{"jsonrpc":"2.0","id":2,"method":"textDocument/codeAction","params":{"textDocument":{"uri":"file:///foo.c"},"range":{"start":{"line":104,"character":13},"end":{"line":0,"character":35}},"context":{"diagnostics":[{"range":{"start": {"line": 0, "character": 35}, "end": {"line": 0, "character": 35}},"severity":2,"message":"using the result of an assignment as a condition without parentheses"},{"range":{"start": {"line": 0, "character": 35}, "end": {"line": 0, "character": 35}},"severity":3,"message":"place parentheses around the assignment to silence this warning"},{"range":{"start": {"line": 0, "character": 35}, "end": {"line": 0, "character": 35}},"severity":3,"message":"use '==' to turn this assignment into an equality comparison"}]}}} # -# CHECK: {"jsonrpc":"2.0","id":2,"result":[{"title":"Apply FixIt 'place parentheses around the assignment to silence this warning'", "command": "clangd.applyFix", "arguments": ["file:///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": ")"}]]},{"title":"Apply FixIt 'use '==' to turn this assignment into an equality comparison'", "command": "clangd.applyFix", "arguments": ["file:///foo.c", [{"range": {"start": {"line": 0, "character": 34}, "end": {"line": 0, "character": 35}}, "newText": "=="}]]}] +# CHECK: {"jsonrpc":"2.0","id":2,"result":[{"title":"Apply FixIt 'place parentheses around the assignment to silence this warning'", "command": "clangd.applyFix", "arguments": [{"changes": {"file:///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": ")"}]}}]},{"title":"Apply FixIt 'use '==' to turn this assignment into an equality comparison'", "command": "clangd.applyFix", "arguments": [{"changes": {"file:///foo.c": [{"range": {"start": {"line": 0, "character": 34}, "end": {"line": 0, "character": 35}}, "newText": "=="}]}}]}]} # Content-Length: 771 {"jsonrpc":"2.0","id":2,"method":"textDocument/codeAction","params":{"textDocument":{"uri":"file:///foo.c"},"range":{"start":{"line":104,"character":13},"end":{"line":0,"character":35}},"context":{"diagnostics":[{"range":{"start": {"line": 0, "character": 35}, "end": {"line": 0, "character": 35}},"severity":2,"code":"1","source":"foo","message":"using the result of an assignment as a condition without parentheses"},{"range":{"start": {"line": 0, "character": 35}, "end": {"line": 0, "character": 35}},"severity":3,"message":"place parentheses around the assignment to silence this warning"},{"range":{"start": {"line": 0, "character": 35}, "end": {"line": 0, "character": 35}},"severity":3,"message":"use '==' to turn this assignment into an equality comparison"}]}}} # Make sure unused "code" and "source" fields ignored gracefully -# CHECK: {"jsonrpc":"2.0","id":2,"result":[{"title":"Apply FixIt 'place parentheses around the assignment to silence this warning'", "command": "clangd.applyFix", "arguments": ["file:///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": ")"}]]},{"title":"Apply FixIt 'use '==' to turn this assignment into an equality comparison'", "command": "clangd.applyFix", "arguments": ["file:///foo.c", [{"range": {"start": {"line": 0, "character": 34}, "end": {"line": 0, "character": 35}}, "newText": "=="}]]}] +# CHECK: {"jsonrpc":"2.0","id":2,"result":[{"title":"Apply FixIt 'place parentheses around the assignment to silence this warning'", "command": "clangd.applyFix", "arguments": [{"changes": {"file:///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": ")"}]}}]},{"title":"Apply FixIt 'use '==' to turn this assignment into an equality comparison'", "command": "clangd.applyFix", "arguments": [{"changes": {"file:///foo.c": [{"range": {"start": {"line": 0, "character": 34}, "end": {"line": 0, "character": 35}}, "newText": "=="}]}}]}]} # +Content-Length: 329 + +{"jsonrpc":"2.0","id":3,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","arguments":[{"changes":{"file:///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: {"jsonrpc":"2.0","id":3,"result":"Fix applied."} +# CHECK: {"jsonrpc":"2.0","id":1,"method":"workspace/applyEdit","params":{"edit": {"changes": {"file:///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": ")"}]}}}} Content-Length: 44 {"jsonrpc":"2.0","id":3,"method":"shutdown"} Index: test/clangd/initialize-params-invalid.test =================================================================== --- test/clangd/initialize-params-invalid.test +++ test/clangd/initialize-params-invalid.test @@ -5,7 +5,7 @@ Content-Length: 142 {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":"","rootUri":"file:///path/to/workspace","capabilities":{},"trace":"off"}} -# CHECK: Content-Length: 535 +# CHECK: Content-Length: 606 # CHECK: {"jsonrpc":"2.0","id":0,"result":{"capabilities":{ # CHECK: "textDocumentSync": 1, # CHECK: "documentFormattingProvider": true, @@ -14,7 +14,8 @@ # CHECK: "codeActionProvider": true, # CHECK: "completionProvider": {"resolveProvider": false, "triggerCharacters": [".",">",":"]}, # CHECK: "signatureHelpProvider": {"triggerCharacters": ["(",","]}, -# CHECK: "definitionProvider": true +# CHECK: "definitionProvider": true, +# CHECK: "executeCommandProvider": {"commands": ["clangd.applyFix"]} # CHECK: }}} # Content-Length: 44 Index: test/clangd/initialize-params.test =================================================================== --- test/clangd/initialize-params.test +++ test/clangd/initialize-params.test @@ -5,7 +5,7 @@ Content-Length: 143 {"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootUri":"file:///path/to/workspace","capabilities":{},"trace":"off"}} -# CHECK: Content-Length: 535 +# CHECK: Content-Length: 606 # CHECK: {"jsonrpc":"2.0","id":0,"result":{"capabilities":{ # CHECK: "textDocumentSync": 1, # CHECK: "documentFormattingProvider": true, @@ -14,7 +14,8 @@ # CHECK: "codeActionProvider": true, # CHECK: "completionProvider": {"resolveProvider": false, "triggerCharacters": [".",">",":"]}, # CHECK: "signatureHelpProvider": {"triggerCharacters": ["(",","]}, -# CHECK: "definitionProvider": true +# CHECK: "definitionProvider": true, +# CHECK: "executeCommandProvider": {"commands": ["clangd.applyFix"]} # CHECK: }}} # Content-Length: 44