summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--clang-tools-extra/clangd/ClangdLSPServer.cpp74
-rw-r--r--clang-tools-extra/clangd/ClangdLSPServer.h1
-rw-r--r--clang-tools-extra/clangd/JSONRPCDispatcher.cpp7
-rw-r--r--clang-tools-extra/clangd/JSONRPCDispatcher.h2
-rw-r--r--clang-tools-extra/clangd/Protocol.cpp160
-rw-r--r--clang-tools-extra/clangd/Protocol.h41
-rw-r--r--clang-tools-extra/clangd/ProtocolHandlers.cpp1
-rw-r--r--clang-tools-extra/clangd/ProtocolHandlers.h1
-rw-r--r--clang-tools-extra/clangd/clients/clangd-vscode/src/extension.ts20
-rw-r--r--clang-tools-extra/test/clangd/execute-command.test67
-rw-r--r--clang-tools-extra/test/clangd/fixits.test11
-rw-r--r--clang-tools-extra/test/clangd/initialize-params-invalid.test5
-rw-r--r--clang-tools-extra/test/clangd/initialize-params.test5
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
OpenPOWER on IntegriCloud