#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;
}
"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);
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);
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) {
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();
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);
}
}
+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);
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;
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) {
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) {
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 {
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;
Register("textDocument/switchSourceHeader",
&ProtocolCallbacks::onSwitchSourceHeader);
Register("workspace/didChangeWatchedFiles", &ProtocolCallbacks::onFileEvent);
+ Register("workspace/executeCommand", &ProtocolCallbacks::onCommand);
}
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,
};
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));
}
--- /dev/null
+# RUN: clangd -run-synchronously < %s | FileCheck %s\r
+# It is absolutely vital that this file has CRLF line endings.\r
+#\r
+Content-Length: 125\r
+\r
+{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}}\r
+#\r
+Content-Length: 180\r
+\r
+{"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) {}}"}}}\r
+#\r
+# 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"}]}}\r
+#\r
+Content-Length: 72\r
+\r
+{"jsonrpc":"2.0","id":3,"method":"workspace/executeCommand","params":{}}\r
+# No command name\r
+Content-Length: 85\r
+\r
+{"jsonrpc":"2.0","id":4,"method":"workspace/executeCommand","params":{"command": {}}}\r
+# Invalid, non-scalar command name\r
+Content-Length: 345\r
+\r
+{"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":")"}]}}]}}\r
+Content-Length: 117\r
+\r
+{"jsonrpc":"2.0","id":6,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","arguments":"foo"}}\r
+# Arguments not a sequence.\r
+Content-Length: 93\r
+\r
+{"jsonrpc":"2.0","id":7,"method":"workspace/executeCommand","params":{"command":"mycommand"}}\r
+# Unknown command.\r
+Content-Length: 132\r
+\r
+{"jsonrpc":"2.0","id":8,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","custom":"foo", "arguments":[""]}}\r
+# ApplyFix argument not a mapping node.\r
+Content-Length: 345\r
+\r
+{"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":")"}]}}]}}\r
+# Custom field in WorkspaceEdit\r
+Content-Length: 132\r
+\r
+{"jsonrpc":"2.0","id":10,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","arguments":[{"changes":"foo"}]}}\r
+# changes in WorkspaceEdit with no mapping node\r
+Content-Length: 346\r
+\r
+{"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"}}]}}\r
+# Custom field in WorkspaceEditChange\r
+Content-Length: 150\r
+\r
+{"jsonrpc":"2.0","id":12,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","arguments":[{"changes":{"file:///foo.c":"bar"}}]}}\r
+# No sequence node for TextEdits\r
+Content-Length: 149\r
+\r
+{"jsonrpc":"2.0","id":13,"method":"workspace/executeCommand","params":{"command":"clangd.applyFix","arguments":[{"changes":{"file:///foo.c":[""]}}]}}\r
+# No mapping node for TextEdit\r
+Content-Length: 265\r
+\r
+{"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":")"}]}}]}}\r
+# TextEdit not decoded\r
+Content-Length: 345\r
+\r
+{"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"}}\r
+# Command name after arguments\r
+Content-Length: 44\r
+\r
+{"jsonrpc":"2.0","id":3,"method":"shutdown"}\r
#\r
Content-Length: 746\r
\r
- {"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"}]}}}\r
+{"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"}]}}}\r
#\r
-# 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": "=="}]]}]\r
+# 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": "=="}]}}]}]}\r
#\r
Content-Length: 771\r
\r
{"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"}]}}}\r
# Make sure unused "code" and "source" fields ignored gracefully\r
-# 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": "=="}]]}]\r
+# 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": "=="}]}}]}]}\r
#\r
+Content-Length: 329\r
+\r
+{"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":")"}]}}]}}\r
+# CHECK: {"jsonrpc":"2.0","id":3,"result":"Fix applied."}\r
+# 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": ")"}]}}}}\r
Content-Length: 44\r
\r
{"jsonrpc":"2.0","id":3,"method":"shutdown"}\r
Content-Length: 142\r
\r
{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":"","rootUri":"file:///path/to/workspace","capabilities":{},"trace":"off"}}\r
-# CHECK: Content-Length: 535\r
+# CHECK: Content-Length: 606\r
# CHECK: {"jsonrpc":"2.0","id":0,"result":{"capabilities":{\r
# CHECK: "textDocumentSync": 1,\r
# CHECK: "documentFormattingProvider": true,\r
# CHECK: "codeActionProvider": true,\r
# CHECK: "completionProvider": {"resolveProvider": false, "triggerCharacters": [".",">",":"]},\r
# CHECK: "signatureHelpProvider": {"triggerCharacters": ["(",","]},\r
-# CHECK: "definitionProvider": true\r
+# CHECK: "definitionProvider": true,\r
+# CHECK: "executeCommandProvider": {"commands": ["clangd.applyFix"]}\r
# CHECK: }}}\r
#\r
Content-Length: 44\r
Content-Length: 143\r
\r
{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootUri":"file:///path/to/workspace","capabilities":{},"trace":"off"}}\r
-# CHECK: Content-Length: 535\r
+# CHECK: Content-Length: 606\r
# CHECK: {"jsonrpc":"2.0","id":0,"result":{"capabilities":{\r
# CHECK: "textDocumentSync": 1,\r
# CHECK: "documentFormattingProvider": true,\r
# CHECK: "codeActionProvider": true,\r
# CHECK: "completionProvider": {"resolveProvider": false, "triggerCharacters": [".",">",":"]},\r
# CHECK: "signatureHelpProvider": {"triggerCharacters": ["(",","]},\r
-# CHECK: "definitionProvider": true\r
+# CHECK: "definitionProvider": true,\r
+# CHECK: "executeCommandProvider": {"commands": ["clangd.applyFix"]}\r
# CHECK: }}}\r
#\r
Content-Length: 44\r