Index: clangd/ClangdLSPServer.h =================================================================== --- clangd/ClangdLSPServer.h +++ clangd/ClangdLSPServer.h @@ -66,6 +66,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); @@ -78,6 +78,26 @@ Server.onFileEvent(Params); } +void ClangdLSPServer::onCommand(Ctx C, ExecuteCommandParams &Params) { + if (Params.command == ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND && + Params.workspaceEditParams) { + ApplyWorkspaceEditParams ApplyEdit; + ApplyEdit.edit = *Params.workspaceEditParams; + C.reply("\"Command executed.\""); + // We don't need the response so id == 1 is OK. + Out.writeMessage( + R"({"jsonrpc":"2.0","id":1,"method":"workspace/applyEdit","params":)" + + ApplyWorkspaceEditParams::unparse(ApplyEdit) + R"(})"); + } 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("Could not execute command \"{0}\".", Params.command) + .str()); + } +} + void ClangdLSPServer::onDocumentDidClose(Ctx C, DidCloseTextDocumentParams &Params) { Server.removeDocument(Params.textDocument.uri.file); @@ -87,26 +107,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) { @@ -117,15 +138,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/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 { @@ -381,6 +382,46 @@ clangd::Logger &Logger); }; +struct WorkspaceEdit { + // Not actually defined the protocol as such but used to adapt to C++ + // constructs. + struct WorkspaceEditChange { + std::string uri; + std::vector edits; + + static llvm::Optional + parse(llvm::yaml::MappingNode *Params, clangd::Logger &Logger); + static std::string unparse(const WorkspaceEditChange &WEC); + }; + + /// Holds changes to existing resources. + llvm::Optional changes; + + /// Note: Not currently used by clangd. + // llvm::Optional> documentChanges; + + static llvm::Optional parse(llvm::yaml::MappingNode *Params, + clangd::Logger &Logger); + static std::string unparse(const WorkspaceEdit &WE); +}; + +struct ExecuteCommandParams { + const static std::string CLANGD_APPLY_FIX_COMMAND; + + /// The identifier of the actual command handler. + std::string command; + + llvm::Optional workspaceEditParams; + + 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 @@ -92,6 +92,25 @@ return Result; } +namespace { +// Javascript/Typescript numbers can be floating points. +// Try integer first for precision then double. +bool getNumberAsInt(StringRef Str, int &Out) { + long long Val; + if (!llvm::getAsSignedInteger(Str, 0, Val)) { + Out = Val; + return false; + } + double DVal; + if (!Str.getAsDouble(DVal, false)) { + Out = static_cast(DVal); + return false; + } + + return true; +} +} // namespace + llvm::Optional Position::parse(llvm::yaml::MappingNode *Params, clangd::Logger &Logger) { Position Result; @@ -109,13 +128,13 @@ llvm::SmallString<10> Storage; if (KeyValue == "line") { - long long Val; - if (llvm::getAsSignedInteger(Value->getValue(Storage), 0, Val)) + int Val; + if (getNumberAsInt(Value->getValue(Storage), Val)) return llvm::None; Result.line = Val; } else if (KeyValue == "character") { - long long Val; - if (llvm::getAsSignedInteger(Value->getValue(Storage), 0, Val)) + int Val; + if (getNumberAsInt(Value->getValue(Storage), Val)) return llvm::None; Result.character = Val; } else { @@ -248,8 +267,13 @@ llvm::Optional TextEdit::parse(llvm::yaml::MappingNode *Params, clangd::Logger &Logger) { - TextEdit Result; + llvm::Optional Result = TextEdit(); for (auto &NextKeyValue : *Params) { + // We have to consume the whole MappingNode because it doesn't support + // skipping and we want to be able to parse further valid TextEdits. + if (!Result) + continue; + auto *KeyString = dyn_cast(NextKeyValue.getKey()); if (!KeyString) return llvm::None; @@ -261,17 +285,23 @@ llvm::SmallString<10> Storage; if (KeyValue == "range") { auto *Map = dyn_cast(Value); - if (!Map) - return llvm::None; + if (!Map) { + Result.reset(); + continue; + } auto Parsed = Range::parse(Map, Logger); - if (!Parsed) - return llvm::None; - Result.range = std::move(*Parsed); + if (!Parsed) { + Result.reset(); + continue; + } + Result->range = std::move(*Parsed); } else if (KeyValue == "newText") { auto *Node = dyn_cast(Value); - if (!Node) - return llvm::None; - Result.newText = Node->getValue(Storage); + if (!Node) { + Result.reset(); + continue; + } + Result->newText = Node->getValue(Storage); } else { logIgnoredField(KeyValue, Logger); } @@ -287,6 +317,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 +889,155 @@ return Result; } +llvm::Optional +WorkspaceEdit::WorkspaceEditChange::parse(llvm::yaml::MappingNode *Params, + clangd::Logger &Logger) { + WorkspaceEdit::WorkspaceEditChange 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.uri.empty()) { + logIgnoredField(KeyValue, Logger); + continue; + } + + Result.uri = 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) { + Logger.log("Failed to decode a TextEdit.\n"); + continue; + } + auto Parsed = TextEdit::parse(ItemValue, Logger); + if (Parsed) + Result.edits.push_back(*Parsed); + else + Logger.log("Failed to decode a TextEdit.\n"); + } + } + + 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 = WorkspaceEdit::WorkspaceEditChange::parse(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"; + +namespace { +bool isKnownCommand(StringRef Command) { + return ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND == Command; +} +} + +llvm::Optional +ExecuteCommandParams::parse(llvm::yaml::MappingNode *Params, + clangd::Logger &Logger) { + ExecuteCommandParams 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 (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 (!isKnownCommand(Result.command)) { + 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) { + if (Result.command == ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND) { + auto *ItemValue = dyn_cast_or_null(&Item); + if (!ItemValue) + return llvm::None; + auto WE = WorkspaceEdit::parse(ItemValue, Logger); + if (WE) + Result.workspaceEditParams = WE; + else + return llvm::None; + } + } + } else { + logIgnoredField(KeyValue, Logger); + } + } + if (Result.command.empty()) + return llvm::None; + + return Result; +} + +std::string +WorkspaceEdit::WorkspaceEditChange::unparse(const WorkspaceEditChange &WEC) { + std::string Result; + llvm::raw_string_ostream(Result) << llvm::format( + R"({"%s": %s})", WEC.uri.c_str(), TextEdit::unparse(WEC.edits).c_str()); + return Result; +} + +std::string WorkspaceEdit::unparse(const WorkspaceEdit &WE) { + std::string Result; + llvm::raw_string_ostream(Result) << llvm::format( + R"({"changes": %s})", + WorkspaceEdit::WorkspaceEditChange::unparse(*WE.changes).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 @@ -51,6 +51,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 @@ -67,4 +67,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,76 @@ +# 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 a TextEdit. +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 a TextEdit. +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,27 @@ # 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":"Command executed."} +# 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: 345 + +{"jsonrpc":"2.0","id":4,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","arguments":[{"changes":{"file:///foo.c":[{"range":{"start":{"line":0.0,"character":32.0},"end":{"line":0.0,"character":32.0}},"newText":"("},{"range":{"start":{"line":0.0,"character":37.0},"end":{"line":0.0,"character":37.0}},"newText":")"}]}}]}} +# Use floating point for numbers. +# CHECK: {"jsonrpc":"2.0","id":4,"result":"Command executed."} +# 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