diff options
| -rw-r--r-- | clang-tools-extra/clangd/ClangdLSPServer.cpp | 74 | ||||
| -rw-r--r-- | clang-tools-extra/clangd/ClangdLSPServer.h | 1 | ||||
| -rw-r--r-- | clang-tools-extra/clangd/JSONRPCDispatcher.cpp | 7 | ||||
| -rw-r--r-- | clang-tools-extra/clangd/JSONRPCDispatcher.h | 2 | ||||
| -rw-r--r-- | clang-tools-extra/clangd/Protocol.cpp | 160 | ||||
| -rw-r--r-- | clang-tools-extra/clangd/Protocol.h | 41 | ||||
| -rw-r--r-- | clang-tools-extra/clangd/ProtocolHandlers.cpp | 1 | ||||
| -rw-r--r-- | clang-tools-extra/clangd/ProtocolHandlers.h | 1 | ||||
| -rw-r--r-- | clang-tools-extra/clangd/clients/clangd-vscode/src/extension.ts | 20 | ||||
| -rw-r--r-- | clang-tools-extra/test/clangd/execute-command.test | 67 | ||||
| -rw-r--r-- | clang-tools-extra/test/clangd/fixits.test | 11 | ||||
| -rw-r--r-- | clang-tools-extra/test/clangd/initialize-params-invalid.test | 5 | ||||
| -rw-r--r-- | clang-tools-extra/test/clangd/initialize-params.test | 5 |
13 files changed, 346 insertions, 49 deletions
diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp index 1689a5fda69..4f70acb8692 100644 --- a/clang-tools-extra/clangd/ClangdLSPServer.cpp +++ b/clang-tools-extra/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<TextEdit> replacementsToEdits(StringRef Code, const std::vector<tooling::Replacement> &Replacements) { + std::vector<TextEdit> 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 @@ void ClangdLSPServer::onInitialize(Ctx C, InitializeParams &Params) { "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 @@ void ClangdLSPServer::onFileEvent(Ctx C, DidChangeWatchedFilesParams &Params) { 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 @@ void ClangdLSPServer::onDocumentOnTypeFormatting( 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 @@ void ClangdLSPServer::onCodeAction(Ctx C, CodeActionParams &Params) { for (Diagnostic &D : Params.context.diagnostics) { std::vector<clang::tooling::Replacement> 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(); diff --git a/clang-tools-extra/clangd/ClangdLSPServer.h b/clang-tools-extra/clangd/ClangdLSPServer.h index 261ff611863..22f73cb5874 100644 --- a/clang-tools-extra/clangd/ClangdLSPServer.h +++ b/clang-tools-extra/clangd/ClangdLSPServer.h @@ -69,6 +69,7 @@ private: 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<clang::tooling::Replacement> getFixIts(StringRef File, const clangd::Diagnostic &D); diff --git a/clang-tools-extra/clangd/JSONRPCDispatcher.cpp b/clang-tools-extra/clangd/JSONRPCDispatcher.cpp index 121ddb9bc8f..74d1dc81fb3 100644 --- a/clang-tools-extra/clangd/JSONRPCDispatcher.cpp +++ b/clang-tools-extra/clangd/JSONRPCDispatcher.cpp @@ -66,6 +66,13 @@ void RequestContext::replyError(int code, const llvm::StringRef &Message) { } } +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); diff --git a/clang-tools-extra/clangd/JSONRPCDispatcher.h b/clang-tools-extra/clangd/JSONRPCDispatcher.h index 9071e4267a0..9a682147411 100644 --- a/clang-tools-extra/clangd/JSONRPCDispatcher.h +++ b/clang-tools-extra/clangd/JSONRPCDispatcher.h @@ -58,6 +58,8 @@ public: 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; diff --git a/clang-tools-extra/clangd/Protocol.cpp b/clang-tools-extra/clangd/Protocol.cpp index 509e5964052..698c9416468 100644 --- a/clang-tools-extra/clangd/Protocol.cpp +++ b/clang-tools-extra/clangd/Protocol.cpp @@ -287,6 +287,19 @@ std::string TextEdit::unparse(const TextEdit &P) { return Result; } +std::string TextEdit::unparse(const std::vector<TextEdit> &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,153 @@ CodeActionParams::parse(llvm::yaml::MappingNode *Params, return Result; } +llvm::Optional<std::map<std::string, std::vector<TextEdit>>> +parseWorkspaceEditChange(llvm::yaml::MappingNode *Params, + clangd::Logger &Logger) { + std::map<std::string, std::vector<TextEdit>> Result; + for (auto &NextKeyValue : *Params) { + auto *KeyString = dyn_cast<llvm::yaml::ScalarNode>(NextKeyValue.getKey()); + if (!KeyString) + return llvm::None; + + llvm::SmallString<10> KeyStorage; + StringRef KeyValue = KeyString->getValue(KeyStorage); + if (Result.count(KeyValue)) { + logIgnoredField(KeyValue, Logger); + continue; + } + + auto *Value = + dyn_cast_or_null<llvm::yaml::SequenceNode>(NextKeyValue.getValue()); + if (!Value) + return llvm::None; + for (auto &Item : *Value) { + auto *ItemValue = dyn_cast_or_null<llvm::yaml::MappingNode>(&Item); + if (!ItemValue) + return llvm::None; + auto Parsed = TextEdit::parse(ItemValue, Logger); + if (!Parsed) + return llvm::None; + + Result[KeyValue].push_back(*Parsed); + } + } + + return Result; +} + +llvm::Optional<WorkspaceEdit> +WorkspaceEdit::parse(llvm::yaml::MappingNode *Params, clangd::Logger &Logger) { + WorkspaceEdit Result; + for (auto &NextKeyValue : *Params) { + auto *KeyString = dyn_cast<llvm::yaml::ScalarNode>(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<llvm::yaml::MappingNode>(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> +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<bool(llvm::yaml::MappingNode * Params)> ArgParser = nullptr; + + for (auto &NextKeyValue : *Params) { + auto *KeyString = dyn_cast<llvm::yaml::ScalarNode>(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<llvm::yaml::ScalarNode>(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 { + return llvm::None; + } + } else if (KeyValue == "arguments") { + auto *Value = NextKeyValue.getValue(); + auto *Seq = dyn_cast<llvm::yaml::SequenceNode>(Value); + if (!Seq) + return llvm::None; + for (auto &Item : *Seq) { + auto *ItemValue = dyn_cast_or_null<llvm::yaml::MappingNode>(&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 Changes; + for (auto &Change : *WE.changes) { + Changes += llvm::formatv(R"("{0}": {1})", Change.first, + TextEdit::unparse(Change.second)); + Changes += ','; + } + if (!Changes.empty()) + Changes.pop_back(); + + std::string Result; + llvm::raw_string_ostream(Result) + << llvm::format(R"({"changes": {%s}})", 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> TextDocumentPositionParams::parse(llvm::yaml::MappingNode *Params, clangd::Logger &Logger) { diff --git a/clang-tools-extra/clangd/Protocol.h b/clang-tools-extra/clangd/Protocol.h index 421fa028efb..4a7c8bf04d8 100644 --- a/clang-tools-extra/clangd/Protocol.h +++ b/clang-tools-extra/clangd/Protocol.h @@ -141,6 +141,7 @@ struct TextEdit { static llvm::Optional<TextEdit> parse(llvm::yaml::MappingNode *Params, clangd::Logger &Logger); static std::string unparse(const TextEdit &P); + static std::string unparse(const std::vector<TextEdit> &TextEdits); }; struct TextDocumentItem { @@ -382,6 +383,46 @@ struct CodeActionParams { clangd::Logger &Logger); }; +struct WorkspaceEdit { + /// Holds changes to existing resources. + llvm::Optional<std::map<std::string, std::vector<TextEdit>>> changes; + + /// Note: "documentChanges" is not currently used because currently there is + /// no support for versioned edits. + + static llvm::Optional<WorkspaceEdit> 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> workspaceEdit; + + static llvm::Optional<ExecuteCommandParams> + 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; diff --git a/clang-tools-extra/clangd/ProtocolHandlers.cpp b/clang-tools-extra/clangd/ProtocolHandlers.cpp index 507fc421d6a..4ca6ee0b6b7 100644 --- a/clang-tools-extra/clangd/ProtocolHandlers.cpp +++ b/clang-tools-extra/clangd/ProtocolHandlers.cpp @@ -72,4 +72,5 @@ void clangd::registerCallbackHandlers(JSONRPCDispatcher &Dispatcher, Register("textDocument/switchSourceHeader", &ProtocolCallbacks::onSwitchSourceHeader); Register("workspace/didChangeWatchedFiles", &ProtocolCallbacks::onFileEvent); + Register("workspace/executeCommand", &ProtocolCallbacks::onCommand); } diff --git a/clang-tools-extra/clangd/ProtocolHandlers.h b/clang-tools-extra/clangd/ProtocolHandlers.h index bf307c8434a..f82bd29142b 100644 --- a/clang-tools-extra/clangd/ProtocolHandlers.h +++ b/clang-tools-extra/clangd/ProtocolHandlers.h @@ -52,6 +52,7 @@ public: 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, diff --git a/clang-tools-extra/clangd/clients/clangd-vscode/src/extension.ts b/clang-tools-extra/clangd/clients/clangd-vscode/src/extension.ts index f89ddc97ffe..47c13a18f96 100644 --- a/clang-tools-extra/clangd/clients/clangd-vscode/src/extension.ts +++ b/clang-tools-extra/clangd/clients/clangd-vscode/src/extension.ts @@ -40,27 +40,7 @@ export function activate(context: vscode.ExtensionContext) { }; 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)); } diff --git a/clang-tools-extra/test/clangd/execute-command.test b/clang-tools-extra/test/clangd/execute-command.test new file mode 100644 index 00000000000..7e326d7a49c --- /dev/null +++ b/clang-tools-extra/test/clangd/execute-command.test @@ -0,0 +1,67 @@ +# RUN: clangd -run-synchronously < %s | FileCheck %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
+Content-Length: 85
+
+{"jsonrpc":"2.0","id":4,"method":"workspace/executeCommand","params":{"command": {}}}
+# Invalid, non-scalar command name
+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":")"}]}}]}}
+Content-Length: 117
+
+{"jsonrpc":"2.0","id":6,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","arguments":"foo"}}
+# Arguments not a sequence.
+Content-Length: 93
+
+{"jsonrpc":"2.0","id":7,"method":"workspace/executeCommand","params":{"command":"mycommand"}}
+# Unknown command.
+Content-Length: 132
+
+{"jsonrpc":"2.0","id":8,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","custom":"foo", "arguments":[""]}}
+# ApplyFix argument not a mapping node.
+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
+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
+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
+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
+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
+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
+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
+Content-Length: 44
+
+{"jsonrpc":"2.0","id":3,"method":"shutdown"}
diff --git a/clang-tools-extra/test/clangd/fixits.test b/clang-tools-extra/test/clangd/fixits.test index ab5b71e9cc8..38e086cf9b4 100644 --- a/clang-tools-extra/test/clangd/fixits.test +++ b/clang-tools-extra/test/clangd/fixits.test @@ -13,16 +13,21 @@ Content-Length: 180 #
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"}
diff --git a/clang-tools-extra/test/clangd/initialize-params-invalid.test b/clang-tools-extra/test/clangd/initialize-params-invalid.test index 77520d652fe..8c1f442c7d8 100644 --- a/clang-tools-extra/test/clangd/initialize-params-invalid.test +++ b/clang-tools-extra/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 @@ Content-Length: 142 # 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
diff --git a/clang-tools-extra/test/clangd/initialize-params.test b/clang-tools-extra/test/clangd/initialize-params.test index 57562323e61..f2d185e263f 100644 --- a/clang-tools-extra/test/clangd/initialize-params.test +++ b/clang-tools-extra/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 @@ Content-Length: 143 # 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
|

