[clangd] Handle clangd.applyFix server-side
authorMarc-Andre Laperle <marc-andre.laperle@ericsson.com>
Fri, 3 Nov 2017 13:39:15 +0000 (13:39 +0000)
committerMarc-Andre Laperle <marc-andre.laperle@ericsson.com>
Fri, 3 Nov 2017 13:39:15 +0000 (13:39 +0000)
Summary:
When the user selects a fix-it (or any code action with commands), it is
possible to let the client forward the selected command to the server.
When the clangd.applyFix command is handled on the server, it can send a
workspace/applyEdit request to the client. This has the advantage that
the client doesn't explicitly have to know how to handle
clangd.applyFix. Therefore, the code to handle clangd.applyFix in the VS
Code extension (and any other Clangd client) is not required anymore.

Reviewers: ilya-biryukov, sammccall, Nebiroth, hokein

Reviewed By: hokein

Subscribers: ioeric, hokein, rwols, puremourning, bkramer, ilya-biryukov

Tags: #clang-tools-extra

Differential Revision: https://reviews.llvm.org/D39276

llvm-svn: 317322

13 files changed:
clang-tools-extra/clangd/ClangdLSPServer.cpp
clang-tools-extra/clangd/ClangdLSPServer.h
clang-tools-extra/clangd/JSONRPCDispatcher.cpp
clang-tools-extra/clangd/JSONRPCDispatcher.h
clang-tools-extra/clangd/Protocol.cpp
clang-tools-extra/clangd/Protocol.h
clang-tools-extra/clangd/ProtocolHandlers.cpp
clang-tools-extra/clangd/ProtocolHandlers.h
clang-tools-extra/clangd/clients/clangd-vscode/src/extension.ts
clang-tools-extra/test/clangd/execute-command.test [new file with mode: 0644]
clang-tools-extra/test/clangd/fixits.test
clang-tools-extra/test/clangd/initialize-params-invalid.test
clang-tools-extra/test/clangd/initialize-params.test

index 1689a5f..4f70acb 100644 (file)
 #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();
index 261ff61..22f73cb 100644 (file)
@@ -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);
index 121ddb9..74d1dc8 100644 (file)
@@ -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);
index 9071e42..9a68214 100644 (file)
@@ -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;
index 509e596..698c941 100644 (file)
@@ -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) {
index 421fa02..4a7c8bf 100644 (file)
@@ -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;
index 507fc42..4ca6ee0 100644 (file)
@@ -72,4 +72,5 @@ void clangd::registerCallbackHandlers(JSONRPCDispatcher &Dispatcher,
   Register("textDocument/switchSourceHeader",
            &ProtocolCallbacks::onSwitchSourceHeader);
   Register("workspace/didChangeWatchedFiles", &ProtocolCallbacks::onFileEvent);
+  Register("workspace/executeCommand", &ProtocolCallbacks::onCommand);
 }
index bf307c8..f82bd29 100644 (file)
@@ -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,
index f89ddc9..47c13a1 100644 (file)
@@ -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 (file)
index 0000000..7e326d7
--- /dev/null
@@ -0,0 +1,67 @@
+# 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
index ab5b71e..38e086c 100644 (file)
@@ -13,16 +13,21 @@ Content-Length: 180
 #\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
index 77520d6..8c1f442 100644 (file)
@@ -5,7 +5,7 @@
 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
@@ -14,7 +14,8 @@ Content-Length: 142
 # 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
index 5756232..f2d185e 100644 (file)
@@ -5,7 +5,7 @@
 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
@@ -14,7 +14,8 @@ Content-Length: 143
 # 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