Skip to content

Conversation

@DaanDeMeyer
Copy link
Contributor

When multiple clangd instances run in the same VS Code window (e.g. in a multi-root workspace), they all try to register the same command names like 'clangd.applyFix', causing 'command already exists' errors.

Fix this by appending a UUID suffix to command names, making them unique per instance (e.g. 'clangd.applyFix.A1B2C3D4...'). The base command names are still registered as handlers for backwards compatibility but are not advertised in capabilities.

Link: clangd/vscode-clangd#810

When multiple clangd instances run in the same VS Code window (e.g. in a
multi-root workspace), they all try to register the same command names
like 'clangd.applyFix', causing 'command already exists' errors.

Fix this by appending a UUID suffix to command names, making them unique
per instance (e.g. 'clangd.applyFix.A1B2C3D4...'). The base command names
are still registered as handlers for backwards compatibility but are not
advertised in capabilities.

Link: clangd/vscode-clangd#810
@llvmbot
Copy link
Member

llvmbot commented Nov 26, 2025

@llvm/pr-subscribers-clang-tools-extra

Author: Daan De Meyer (DaanDeMeyer)

Changes

When multiple clangd instances run in the same VS Code window (e.g. in a multi-root workspace), they all try to register the same command names like 'clangd.applyFix', causing 'command already exists' errors.

Fix this by appending a UUID suffix to command names, making them unique per instance (e.g. 'clangd.applyFix.A1B2C3D4...'). The base command names are still registered as handlers for backwards compatibility but are not advertised in capabilities.

Link: clangd/vscode-clangd#810


Full diff: https://github.com/llvm/llvm-project/pull/169734.diff

8 Files Affected:

  • (modified) clang-tools-extra/clangd/ClangdLSPServer.cpp (+36-15)
  • (modified) clang-tools-extra/clangd/ClangdLSPServer.h (+7)
  • (modified) clang-tools-extra/clangd/LSPBinder.h (+4-4)
  • (modified) clang-tools-extra/clangd/test/code-action-request.test (+1-1)
  • (modified) clang-tools-extra/clangd/test/fixits-command-documentchanges.test (+4-4)
  • (modified) clang-tools-extra/clangd/test/fixits-command.test (+4-4)
  • (modified) clang-tools-extra/clangd/test/include-cleaner-batch-fix.test (+6-6)
  • (modified) clang-tools-extra/clangd/test/initialize-params.test (+3-3)
diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp
index f8e6da73bbb1f..e22023c8e0815 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.cpp
+++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp
@@ -35,7 +35,7 @@
 #include "llvm/Support/Error.h"
 #include "llvm/Support/FormatVariadic.h"
 #include "llvm/Support/JSON.h"
-#include "llvm/Support/SHA1.h"
+#include "llvm/Support/RandomNumberGenerator.h"
 #include "llvm/Support/ScopedPrinter.h"
 #include "llvm/Support/raw_ostream.h"
 #include <chrono>
@@ -73,18 +73,15 @@ std::optional<int64_t> decodeVersion(llvm::StringRef Encoded) {
   return std::nullopt;
 }
 
-const llvm::StringLiteral ApplyFixCommand = "clangd.applyFix";
-const llvm::StringLiteral ApplyTweakCommand = "clangd.applyTweak";
-const llvm::StringLiteral ApplyRenameCommand = "clangd.applyRename";
-
 CodeAction toCodeAction(const ClangdServer::CodeActionResult::Rename &R,
-                        const URIForFile &File) {
+                        const URIForFile &File,
+                        const std::string &ApplyRenameCommand) {
   CodeAction CA;
   CA.title = R.FixMessage;
   CA.kind = std::string(CodeAction::QUICKFIX_KIND);
   CA.command.emplace();
   CA.command->title = R.FixMessage;
-  CA.command->command = std::string(ApplyRenameCommand);
+  CA.command->command = ApplyRenameCommand;
   RenameParams Params;
   Params.textDocument = TextDocumentIdentifier{File};
   Params.position = R.Diag.Range.start;
@@ -96,7 +93,7 @@ CodeAction toCodeAction(const ClangdServer::CodeActionResult::Rename &R,
 /// Transforms a tweak into a code action that would apply it if executed.
 /// EXPECTS: T.prepare() was called and returned true.
 CodeAction toCodeAction(const ClangdServer::TweakRef &T, const URIForFile &File,
-                        Range Selection) {
+                        Range Selection, const std::string &ApplyTweakCommand) {
   CodeAction CA;
   CA.title = T.Title;
   CA.kind = T.Kind.str();
@@ -107,7 +104,7 @@ CodeAction toCodeAction(const ClangdServer::TweakRef &T, const URIForFile &File,
   //        directly.
   CA.command.emplace();
   CA.command->title = T.Title;
-  CA.command->command = std::string(ApplyTweakCommand);
+  CA.command->command = ApplyTweakCommand;
   TweakArgs Args;
   Args.file = File;
   Args.tweakID = T.ID;
@@ -679,8 +676,15 @@ void ClangdLSPServer::onInitialize(const InitializeParams &Params,
           : llvm::json::Value(true);
 
   std::vector<llvm::StringRef> Commands;
+  // Advertise only instance-specific commands (those with a unique suffix).
+  // The base command names (clangd.applyFix, clangd.applyTweak,
+  // clangd.applyRename) are registered as handlers for backwards compatibility
+  // but are not advertised, to avoid conflicts when multiple clangd instances
+  // run in the same VS Code window.
   for (llvm::StringRef Command : Handlers.CommandHandlers.keys())
-    Commands.push_back(Command);
+    if (!Command.starts_with("clangd.apply") ||
+        Command.count('.') > 1) // Instance-specific commands have 2 dots
+      Commands.push_back(Command);
   llvm::sort(Commands);
   ServerCaps["executeCommandProvider"] =
       llvm::json::Object{{"commands", Commands}};
@@ -1044,14 +1048,15 @@ void ClangdLSPServer::onFoldingRange(
   Server->foldingRanges(Params.textDocument.uri.file(), std::move(Reply));
 }
 
-static std::optional<Command> asCommand(const CodeAction &Action) {
+static std::optional<Command> asCommand(const CodeAction &Action,
+                                        const std::string &ApplyFixCommand) {
   Command Cmd;
   if (Action.command && Action.edit)
     return std::nullopt; // Not representable. (We never emit these anyway).
   if (Action.command) {
     Cmd = *Action.command;
   } else if (Action.edit) {
-    Cmd.command = std::string(ApplyFixCommand);
+    Cmd.command = ApplyFixCommand;
     Cmd.argument = *Action.edit;
   } else {
     return std::nullopt;
@@ -1099,10 +1104,10 @@ void ClangdLSPServer::onCodeAction(const CodeActionParams &Params,
     }
 
     for (const auto &R : Fixits->Renames)
-      CAs.push_back(toCodeAction(R, File));
+      CAs.push_back(toCodeAction(R, File, ApplyRenameCommand));
 
     for (const auto &TR : Fixits->TweakRefs)
-      CAs.push_back(toCodeAction(TR, File, Selection));
+      CAs.push_back(toCodeAction(TR, File, Selection, ApplyTweakCommand));
 
     // If there's exactly one quick-fix, call it "preferred".
     // We never consider refactorings etc as preferred.
@@ -1127,7 +1132,7 @@ void ClangdLSPServer::onCodeAction(const CodeActionParams &Params,
       return Reply(llvm::json::Array(CAs));
     std::vector<Command> Commands;
     for (const auto &Action : CAs) {
-      if (auto Command = asCommand(Action))
+      if (auto Command = asCommand(Action, ApplyFixCommand))
         Commands.push_back(std::move(*Command));
     }
     return Reply(llvm::json::Array(Commands));
@@ -1663,6 +1668,16 @@ ClangdLSPServer::ClangdLSPServer(Transport &Transp, const ThreadsafeFS &TFS,
       MsgHandler(new MessageHandler(*this)), TFS(TFS),
       SupportedSymbolKinds(defaultSymbolKinds()),
       SupportedCompletionItemKinds(defaultCompletionItemKinds()), Opts(Opts) {
+  // Generate unique command names using a UUID to avoid collisions when
+  // multiple clangd instances run in the same editor (e.g., multi-root
+  // workspaces).
+  std::array<uint8_t, 16> UUID;
+  llvm::getRandomBytes(UUID.data(), UUID.size());
+  std::string Suffix = llvm::toHex(UUID);
+  ApplyFixCommand = "clangd.applyFix." + Suffix;
+  ApplyTweakCommand = "clangd.applyTweak." + Suffix;
+  ApplyRenameCommand = "clangd.applyRename." + Suffix;
+
   if (Opts.ConfigProvider) {
     assert(!Opts.ContextProvider &&
            "Only one of ConfigProvider and ContextProvider allowed!");
@@ -1724,9 +1739,15 @@ void ClangdLSPServer::bindMethods(LSPBinder &Bind,
   Bind.method("textDocument/inlayHint", this, &ClangdLSPServer::onInlayHint);
   Bind.method("$/memoryUsage", this, &ClangdLSPServer::onMemoryUsage);
   Bind.method("textDocument/foldingRange", this, &ClangdLSPServer::onFoldingRange);
+  // Register unique command names that will be advertised in capabilities.
   Bind.command(ApplyFixCommand, this, &ClangdLSPServer::onCommandApplyEdit);
   Bind.command(ApplyTweakCommand, this, &ClangdLSPServer::onCommandApplyTweak);
   Bind.command(ApplyRenameCommand, this, &ClangdLSPServer::onCommandApplyRename);
+  // Also register base command names for backwards compatibility (e.g., tests,
+  // manual command invocation). These are not advertised but will be accepted.
+  Bind.command("clangd.applyFix", this, &ClangdLSPServer::onCommandApplyEdit);
+  Bind.command("clangd.applyTweak", this, &ClangdLSPServer::onCommandApplyTweak);
+  Bind.command("clangd.applyRename", this, &ClangdLSPServer::onCommandApplyRename);
 
   ApplyWorkspaceEdit = Bind.outgoingMethod("workspace/applyEdit");
   PublishDiagnostics = Bind.outgoingNotification("textDocument/publishDiagnostics");
diff --git a/clang-tools-extra/clangd/ClangdLSPServer.h b/clang-tools-extra/clangd/ClangdLSPServer.h
index 6ada3fd9e6e47..787940f23acea 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.h
+++ b/clang-tools-extra/clangd/ClangdLSPServer.h
@@ -308,6 +308,13 @@ class ClangdLSPServer : private ClangdServer::Callbacks,
   /// Whether the client supports change annotations on text edits.
   bool SupportsChangeAnnotation = false;
 
+  /// Unique command names for this server instance, used to avoid conflicts
+  /// when multiple clangd instances run in the same editor (e.g., multi-root
+  /// workspaces).
+  std::string ApplyFixCommand;
+  std::string ApplyTweakCommand;
+  std::string ApplyRenameCommand;
+
   std::mutex BackgroundIndexProgressMutex;
   enum class BackgroundIndexProgress {
     // Client doesn't support reporting progress. No transitions possible.
diff --git a/clang-tools-extra/clangd/LSPBinder.h b/clang-tools-extra/clangd/LSPBinder.h
index 8542112681375..7c07faca9b892 100644
--- a/clang-tools-extra/clangd/LSPBinder.h
+++ b/clang-tools-extra/clangd/LSPBinder.h
@@ -74,7 +74,7 @@ class LSPBinder {
   /// Handler should be e.g. void load(const LoadParams&, Callback<LoadResult>);
   /// LoadParams must be JSON-parseable and LoadResult must be serializable.
   template <typename Param, typename Result, typename ThisT>
-  void command(llvm::StringLiteral Command, ThisT *This,
+  void command(llvm::StringRef Command, ThisT *This,
                void (ThisT::*Handler)(const Param &, Callback<Result>));
 
   template <typename P, typename R>
@@ -155,11 +155,11 @@ void LSPBinder::notification(llvm::StringLiteral Method, ThisT *This,
 }
 
 template <typename Param, typename Result, typename ThisT>
-void LSPBinder::command(llvm::StringLiteral Method, ThisT *This,
+void LSPBinder::command(llvm::StringRef Method, ThisT *This,
                         void (ThisT::*Handler)(const Param &,
                                                Callback<Result>)) {
-  Raw.CommandHandlers[Method] = [Method, Handler, This](JSON RawParams,
-                                                        Callback<JSON> Reply) {
+  Raw.CommandHandlers[Method] = [Method = Method.str(), Handler,
+                                 This](JSON RawParams, Callback<JSON> Reply) {
     auto P = LSPBinder::parse<Param>(RawParams, Method, "command");
     if (!P)
       return Reply(P.takeError());
diff --git a/clang-tools-extra/clangd/test/code-action-request.test b/clang-tools-extra/clangd/test/code-action-request.test
index f1511f58f561f..c4cf9e5893121 100644
--- a/clang-tools-extra/clangd/test/code-action-request.test
+++ b/clang-tools-extra/clangd/test/code-action-request.test
@@ -46,7 +46,7 @@
 # CHECK-NEXT:          "tweakID": "ExpandDeducedType"
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyTweak",
+# CHECK-NEXT:      "command": "clangd.applyTweak.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Replace with deduced type"
 # CHECK-NEXT:    }
 # CHECK-NEXT:  ]
diff --git a/clang-tools-extra/clangd/test/fixits-command-documentchanges.test b/clang-tools-extra/clangd/test/fixits-command-documentchanges.test
index cd636c4df387a..177e4276bdc54 100644
--- a/clang-tools-extra/clangd/test/fixits-command-documentchanges.test
+++ b/clang-tools-extra/clangd/test/fixits-command-documentchanges.test
@@ -71,7 +71,7 @@
 # CHECK-NEXT:          ]
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: place parentheses around the assignment to silence this warning"
 # CHECK-NEXT:    },
 # CHECK-NEXT:    {
@@ -102,7 +102,7 @@
 # CHECK-NEXT:          ]
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: use '==' to turn this assignment into an equality comparison"
 # CHECK-NEXT:    }
 # CHECK-NEXT:  ]
@@ -153,7 +153,7 @@
 # CHECK-NEXT:          ]
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: place parentheses around the assignment to silence this warning"
 # CHECK-NEXT:    },
 # CHECK-NEXT:    {
@@ -184,7 +184,7 @@
 # CHECK-NEXT:          ]
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: use '==' to turn this assignment into an equality comparison"
 # CHECK-NEXT:    }
 # CHECK-NEXT:  ]
diff --git a/clang-tools-extra/clangd/test/fixits-command.test b/clang-tools-extra/clangd/test/fixits-command.test
index 62b5a6152d2cf..60d492caee384 100644
--- a/clang-tools-extra/clangd/test/fixits-command.test
+++ b/clang-tools-extra/clangd/test/fixits-command.test
@@ -65,7 +65,7 @@
 # CHECK-NEXT:          }
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: place parentheses around the assignment to silence this warning"
 # CHECK-NEXT:    },
 # CHECK-NEXT:    {
@@ -90,7 +90,7 @@
 # CHECK-NEXT:          }
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: use '==' to turn this assignment into an equality comparison"
 # CHECK-NEXT:    }
 # CHECK-NEXT:  ]
@@ -135,7 +135,7 @@
 # CHECK-NEXT:          }
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: place parentheses around the assignment to silence this warning"
 # CHECK-NEXT:    },
 # CHECK-NEXT:    {
@@ -160,7 +160,7 @@
 # CHECK-NEXT:          }
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: use '==' to turn this assignment into an equality comparison"
 # CHECK-NEXT:    }
 # CHECK-NEXT:  ]
diff --git a/clang-tools-extra/clangd/test/include-cleaner-batch-fix.test b/clang-tools-extra/clangd/test/include-cleaner-batch-fix.test
index 07ebe1009a78f..59de0c50e029d 100644
--- a/clang-tools-extra/clangd/test/include-cleaner-batch-fix.test
+++ b/clang-tools-extra/clangd/test/include-cleaner-batch-fix.test
@@ -151,7 +151,7 @@
 # CHECK-NEXT:          ]
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: #include {{.*}}foo.h{{.*}}"
 # CHECK-NEXT:    },
 # CHECK-NEXT:    {
@@ -195,7 +195,7 @@
 # CHECK-NEXT:          ]
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: add all missing includes"
 # CHECK-NEXT:    },
 # CHECK-NEXT:    {
@@ -265,7 +265,7 @@
 # CHECK-NEXT:          ]
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: fix all includes"
 # CHECK-NEXT:    }
 # CHECK-NEXT:  ]
@@ -302,7 +302,7 @@
 # CHECK-NEXT:          ]
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: remove #include directive"
 # CHECK-NEXT:    },
 # CHECK-NEXT:    {
@@ -346,7 +346,7 @@
 # CHECK-NEXT:          ]
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: remove all unused includes"
 # CHECK-NEXT:    },
 # CHECK-NEXT:    {
@@ -416,7 +416,7 @@
 # CHECK-NEXT:          ]
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: fix all includes"
 # CHECK-NEXT:    }
 # CHECK-NEXT:  ]
diff --git a/clang-tools-extra/clangd/test/initialize-params.test b/clang-tools-extra/clangd/test/initialize-params.test
index d976b7d19fd0e..9c1351bb74c4f 100644
--- a/clang-tools-extra/clangd/test/initialize-params.test
+++ b/clang-tools-extra/clangd/test/initialize-params.test
@@ -41,9 +41,9 @@
 # CHECK-NEXT:      "documentSymbolProvider": true,
 # CHECK-NEXT:      "executeCommandProvider": {
 # CHECK-NEXT:        "commands": [
-# CHECK-NEXT:          "clangd.applyFix",
-# CHECK-NEXT:          "clangd.applyRename"
-# CHECK-NEXT:          "clangd.applyTweak"
+# CHECK-NEXT:          "clangd.applyFix.{{[0-9A-F]+}}",
+# CHECK-NEXT:          "clangd.applyRename.{{[0-9A-F]+}}",
+# CHECK-NEXT:          "clangd.applyTweak.{{[0-9A-F]+}}"
 # CHECK-NEXT:        ]
 # CHECK-NEXT:      },
 # CHECK-NEXT:      "foldingRangeProvider": true,

@llvmbot
Copy link
Member

llvmbot commented Nov 26, 2025

@llvm/pr-subscribers-clangd

Author: Daan De Meyer (DaanDeMeyer)

Changes

When multiple clangd instances run in the same VS Code window (e.g. in a multi-root workspace), they all try to register the same command names like 'clangd.applyFix', causing 'command already exists' errors.

Fix this by appending a UUID suffix to command names, making them unique per instance (e.g. 'clangd.applyFix.A1B2C3D4...'). The base command names are still registered as handlers for backwards compatibility but are not advertised in capabilities.

Link: clangd/vscode-clangd#810


Full diff: https://github.com/llvm/llvm-project/pull/169734.diff

8 Files Affected:

  • (modified) clang-tools-extra/clangd/ClangdLSPServer.cpp (+36-15)
  • (modified) clang-tools-extra/clangd/ClangdLSPServer.h (+7)
  • (modified) clang-tools-extra/clangd/LSPBinder.h (+4-4)
  • (modified) clang-tools-extra/clangd/test/code-action-request.test (+1-1)
  • (modified) clang-tools-extra/clangd/test/fixits-command-documentchanges.test (+4-4)
  • (modified) clang-tools-extra/clangd/test/fixits-command.test (+4-4)
  • (modified) clang-tools-extra/clangd/test/include-cleaner-batch-fix.test (+6-6)
  • (modified) clang-tools-extra/clangd/test/initialize-params.test (+3-3)
diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp
index f8e6da73bbb1f..e22023c8e0815 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.cpp
+++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp
@@ -35,7 +35,7 @@
 #include "llvm/Support/Error.h"
 #include "llvm/Support/FormatVariadic.h"
 #include "llvm/Support/JSON.h"
-#include "llvm/Support/SHA1.h"
+#include "llvm/Support/RandomNumberGenerator.h"
 #include "llvm/Support/ScopedPrinter.h"
 #include "llvm/Support/raw_ostream.h"
 #include <chrono>
@@ -73,18 +73,15 @@ std::optional<int64_t> decodeVersion(llvm::StringRef Encoded) {
   return std::nullopt;
 }
 
-const llvm::StringLiteral ApplyFixCommand = "clangd.applyFix";
-const llvm::StringLiteral ApplyTweakCommand = "clangd.applyTweak";
-const llvm::StringLiteral ApplyRenameCommand = "clangd.applyRename";
-
 CodeAction toCodeAction(const ClangdServer::CodeActionResult::Rename &R,
-                        const URIForFile &File) {
+                        const URIForFile &File,
+                        const std::string &ApplyRenameCommand) {
   CodeAction CA;
   CA.title = R.FixMessage;
   CA.kind = std::string(CodeAction::QUICKFIX_KIND);
   CA.command.emplace();
   CA.command->title = R.FixMessage;
-  CA.command->command = std::string(ApplyRenameCommand);
+  CA.command->command = ApplyRenameCommand;
   RenameParams Params;
   Params.textDocument = TextDocumentIdentifier{File};
   Params.position = R.Diag.Range.start;
@@ -96,7 +93,7 @@ CodeAction toCodeAction(const ClangdServer::CodeActionResult::Rename &R,
 /// Transforms a tweak into a code action that would apply it if executed.
 /// EXPECTS: T.prepare() was called and returned true.
 CodeAction toCodeAction(const ClangdServer::TweakRef &T, const URIForFile &File,
-                        Range Selection) {
+                        Range Selection, const std::string &ApplyTweakCommand) {
   CodeAction CA;
   CA.title = T.Title;
   CA.kind = T.Kind.str();
@@ -107,7 +104,7 @@ CodeAction toCodeAction(const ClangdServer::TweakRef &T, const URIForFile &File,
   //        directly.
   CA.command.emplace();
   CA.command->title = T.Title;
-  CA.command->command = std::string(ApplyTweakCommand);
+  CA.command->command = ApplyTweakCommand;
   TweakArgs Args;
   Args.file = File;
   Args.tweakID = T.ID;
@@ -679,8 +676,15 @@ void ClangdLSPServer::onInitialize(const InitializeParams &Params,
           : llvm::json::Value(true);
 
   std::vector<llvm::StringRef> Commands;
+  // Advertise only instance-specific commands (those with a unique suffix).
+  // The base command names (clangd.applyFix, clangd.applyTweak,
+  // clangd.applyRename) are registered as handlers for backwards compatibility
+  // but are not advertised, to avoid conflicts when multiple clangd instances
+  // run in the same VS Code window.
   for (llvm::StringRef Command : Handlers.CommandHandlers.keys())
-    Commands.push_back(Command);
+    if (!Command.starts_with("clangd.apply") ||
+        Command.count('.') > 1) // Instance-specific commands have 2 dots
+      Commands.push_back(Command);
   llvm::sort(Commands);
   ServerCaps["executeCommandProvider"] =
       llvm::json::Object{{"commands", Commands}};
@@ -1044,14 +1048,15 @@ void ClangdLSPServer::onFoldingRange(
   Server->foldingRanges(Params.textDocument.uri.file(), std::move(Reply));
 }
 
-static std::optional<Command> asCommand(const CodeAction &Action) {
+static std::optional<Command> asCommand(const CodeAction &Action,
+                                        const std::string &ApplyFixCommand) {
   Command Cmd;
   if (Action.command && Action.edit)
     return std::nullopt; // Not representable. (We never emit these anyway).
   if (Action.command) {
     Cmd = *Action.command;
   } else if (Action.edit) {
-    Cmd.command = std::string(ApplyFixCommand);
+    Cmd.command = ApplyFixCommand;
     Cmd.argument = *Action.edit;
   } else {
     return std::nullopt;
@@ -1099,10 +1104,10 @@ void ClangdLSPServer::onCodeAction(const CodeActionParams &Params,
     }
 
     for (const auto &R : Fixits->Renames)
-      CAs.push_back(toCodeAction(R, File));
+      CAs.push_back(toCodeAction(R, File, ApplyRenameCommand));
 
     for (const auto &TR : Fixits->TweakRefs)
-      CAs.push_back(toCodeAction(TR, File, Selection));
+      CAs.push_back(toCodeAction(TR, File, Selection, ApplyTweakCommand));
 
     // If there's exactly one quick-fix, call it "preferred".
     // We never consider refactorings etc as preferred.
@@ -1127,7 +1132,7 @@ void ClangdLSPServer::onCodeAction(const CodeActionParams &Params,
       return Reply(llvm::json::Array(CAs));
     std::vector<Command> Commands;
     for (const auto &Action : CAs) {
-      if (auto Command = asCommand(Action))
+      if (auto Command = asCommand(Action, ApplyFixCommand))
         Commands.push_back(std::move(*Command));
     }
     return Reply(llvm::json::Array(Commands));
@@ -1663,6 +1668,16 @@ ClangdLSPServer::ClangdLSPServer(Transport &Transp, const ThreadsafeFS &TFS,
       MsgHandler(new MessageHandler(*this)), TFS(TFS),
       SupportedSymbolKinds(defaultSymbolKinds()),
       SupportedCompletionItemKinds(defaultCompletionItemKinds()), Opts(Opts) {
+  // Generate unique command names using a UUID to avoid collisions when
+  // multiple clangd instances run in the same editor (e.g., multi-root
+  // workspaces).
+  std::array<uint8_t, 16> UUID;
+  llvm::getRandomBytes(UUID.data(), UUID.size());
+  std::string Suffix = llvm::toHex(UUID);
+  ApplyFixCommand = "clangd.applyFix." + Suffix;
+  ApplyTweakCommand = "clangd.applyTweak." + Suffix;
+  ApplyRenameCommand = "clangd.applyRename." + Suffix;
+
   if (Opts.ConfigProvider) {
     assert(!Opts.ContextProvider &&
            "Only one of ConfigProvider and ContextProvider allowed!");
@@ -1724,9 +1739,15 @@ void ClangdLSPServer::bindMethods(LSPBinder &Bind,
   Bind.method("textDocument/inlayHint", this, &ClangdLSPServer::onInlayHint);
   Bind.method("$/memoryUsage", this, &ClangdLSPServer::onMemoryUsage);
   Bind.method("textDocument/foldingRange", this, &ClangdLSPServer::onFoldingRange);
+  // Register unique command names that will be advertised in capabilities.
   Bind.command(ApplyFixCommand, this, &ClangdLSPServer::onCommandApplyEdit);
   Bind.command(ApplyTweakCommand, this, &ClangdLSPServer::onCommandApplyTweak);
   Bind.command(ApplyRenameCommand, this, &ClangdLSPServer::onCommandApplyRename);
+  // Also register base command names for backwards compatibility (e.g., tests,
+  // manual command invocation). These are not advertised but will be accepted.
+  Bind.command("clangd.applyFix", this, &ClangdLSPServer::onCommandApplyEdit);
+  Bind.command("clangd.applyTweak", this, &ClangdLSPServer::onCommandApplyTweak);
+  Bind.command("clangd.applyRename", this, &ClangdLSPServer::onCommandApplyRename);
 
   ApplyWorkspaceEdit = Bind.outgoingMethod("workspace/applyEdit");
   PublishDiagnostics = Bind.outgoingNotification("textDocument/publishDiagnostics");
diff --git a/clang-tools-extra/clangd/ClangdLSPServer.h b/clang-tools-extra/clangd/ClangdLSPServer.h
index 6ada3fd9e6e47..787940f23acea 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.h
+++ b/clang-tools-extra/clangd/ClangdLSPServer.h
@@ -308,6 +308,13 @@ class ClangdLSPServer : private ClangdServer::Callbacks,
   /// Whether the client supports change annotations on text edits.
   bool SupportsChangeAnnotation = false;
 
+  /// Unique command names for this server instance, used to avoid conflicts
+  /// when multiple clangd instances run in the same editor (e.g., multi-root
+  /// workspaces).
+  std::string ApplyFixCommand;
+  std::string ApplyTweakCommand;
+  std::string ApplyRenameCommand;
+
   std::mutex BackgroundIndexProgressMutex;
   enum class BackgroundIndexProgress {
     // Client doesn't support reporting progress. No transitions possible.
diff --git a/clang-tools-extra/clangd/LSPBinder.h b/clang-tools-extra/clangd/LSPBinder.h
index 8542112681375..7c07faca9b892 100644
--- a/clang-tools-extra/clangd/LSPBinder.h
+++ b/clang-tools-extra/clangd/LSPBinder.h
@@ -74,7 +74,7 @@ class LSPBinder {
   /// Handler should be e.g. void load(const LoadParams&, Callback<LoadResult>);
   /// LoadParams must be JSON-parseable and LoadResult must be serializable.
   template <typename Param, typename Result, typename ThisT>
-  void command(llvm::StringLiteral Command, ThisT *This,
+  void command(llvm::StringRef Command, ThisT *This,
                void (ThisT::*Handler)(const Param &, Callback<Result>));
 
   template <typename P, typename R>
@@ -155,11 +155,11 @@ void LSPBinder::notification(llvm::StringLiteral Method, ThisT *This,
 }
 
 template <typename Param, typename Result, typename ThisT>
-void LSPBinder::command(llvm::StringLiteral Method, ThisT *This,
+void LSPBinder::command(llvm::StringRef Method, ThisT *This,
                         void (ThisT::*Handler)(const Param &,
                                                Callback<Result>)) {
-  Raw.CommandHandlers[Method] = [Method, Handler, This](JSON RawParams,
-                                                        Callback<JSON> Reply) {
+  Raw.CommandHandlers[Method] = [Method = Method.str(), Handler,
+                                 This](JSON RawParams, Callback<JSON> Reply) {
     auto P = LSPBinder::parse<Param>(RawParams, Method, "command");
     if (!P)
       return Reply(P.takeError());
diff --git a/clang-tools-extra/clangd/test/code-action-request.test b/clang-tools-extra/clangd/test/code-action-request.test
index f1511f58f561f..c4cf9e5893121 100644
--- a/clang-tools-extra/clangd/test/code-action-request.test
+++ b/clang-tools-extra/clangd/test/code-action-request.test
@@ -46,7 +46,7 @@
 # CHECK-NEXT:          "tweakID": "ExpandDeducedType"
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyTweak",
+# CHECK-NEXT:      "command": "clangd.applyTweak.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Replace with deduced type"
 # CHECK-NEXT:    }
 # CHECK-NEXT:  ]
diff --git a/clang-tools-extra/clangd/test/fixits-command-documentchanges.test b/clang-tools-extra/clangd/test/fixits-command-documentchanges.test
index cd636c4df387a..177e4276bdc54 100644
--- a/clang-tools-extra/clangd/test/fixits-command-documentchanges.test
+++ b/clang-tools-extra/clangd/test/fixits-command-documentchanges.test
@@ -71,7 +71,7 @@
 # CHECK-NEXT:          ]
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: place parentheses around the assignment to silence this warning"
 # CHECK-NEXT:    },
 # CHECK-NEXT:    {
@@ -102,7 +102,7 @@
 # CHECK-NEXT:          ]
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: use '==' to turn this assignment into an equality comparison"
 # CHECK-NEXT:    }
 # CHECK-NEXT:  ]
@@ -153,7 +153,7 @@
 # CHECK-NEXT:          ]
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: place parentheses around the assignment to silence this warning"
 # CHECK-NEXT:    },
 # CHECK-NEXT:    {
@@ -184,7 +184,7 @@
 # CHECK-NEXT:          ]
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: use '==' to turn this assignment into an equality comparison"
 # CHECK-NEXT:    }
 # CHECK-NEXT:  ]
diff --git a/clang-tools-extra/clangd/test/fixits-command.test b/clang-tools-extra/clangd/test/fixits-command.test
index 62b5a6152d2cf..60d492caee384 100644
--- a/clang-tools-extra/clangd/test/fixits-command.test
+++ b/clang-tools-extra/clangd/test/fixits-command.test
@@ -65,7 +65,7 @@
 # CHECK-NEXT:          }
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: place parentheses around the assignment to silence this warning"
 # CHECK-NEXT:    },
 # CHECK-NEXT:    {
@@ -90,7 +90,7 @@
 # CHECK-NEXT:          }
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: use '==' to turn this assignment into an equality comparison"
 # CHECK-NEXT:    }
 # CHECK-NEXT:  ]
@@ -135,7 +135,7 @@
 # CHECK-NEXT:          }
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: place parentheses around the assignment to silence this warning"
 # CHECK-NEXT:    },
 # CHECK-NEXT:    {
@@ -160,7 +160,7 @@
 # CHECK-NEXT:          }
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: use '==' to turn this assignment into an equality comparison"
 # CHECK-NEXT:    }
 # CHECK-NEXT:  ]
diff --git a/clang-tools-extra/clangd/test/include-cleaner-batch-fix.test b/clang-tools-extra/clangd/test/include-cleaner-batch-fix.test
index 07ebe1009a78f..59de0c50e029d 100644
--- a/clang-tools-extra/clangd/test/include-cleaner-batch-fix.test
+++ b/clang-tools-extra/clangd/test/include-cleaner-batch-fix.test
@@ -151,7 +151,7 @@
 # CHECK-NEXT:          ]
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: #include {{.*}}foo.h{{.*}}"
 # CHECK-NEXT:    },
 # CHECK-NEXT:    {
@@ -195,7 +195,7 @@
 # CHECK-NEXT:          ]
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: add all missing includes"
 # CHECK-NEXT:    },
 # CHECK-NEXT:    {
@@ -265,7 +265,7 @@
 # CHECK-NEXT:          ]
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: fix all includes"
 # CHECK-NEXT:    }
 # CHECK-NEXT:  ]
@@ -302,7 +302,7 @@
 # CHECK-NEXT:          ]
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: remove #include directive"
 # CHECK-NEXT:    },
 # CHECK-NEXT:    {
@@ -346,7 +346,7 @@
 # CHECK-NEXT:          ]
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: remove all unused includes"
 # CHECK-NEXT:    },
 # CHECK-NEXT:    {
@@ -416,7 +416,7 @@
 # CHECK-NEXT:          ]
 # CHECK-NEXT:        }
 # CHECK-NEXT:      ],
-# CHECK-NEXT:      "command": "clangd.applyFix",
+# CHECK-NEXT:      "command": "clangd.applyFix.{{[0-9A-F]+}}",
 # CHECK-NEXT:      "title": "Apply fix: fix all includes"
 # CHECK-NEXT:    }
 # CHECK-NEXT:  ]
diff --git a/clang-tools-extra/clangd/test/initialize-params.test b/clang-tools-extra/clangd/test/initialize-params.test
index d976b7d19fd0e..9c1351bb74c4f 100644
--- a/clang-tools-extra/clangd/test/initialize-params.test
+++ b/clang-tools-extra/clangd/test/initialize-params.test
@@ -41,9 +41,9 @@
 # CHECK-NEXT:      "documentSymbolProvider": true,
 # CHECK-NEXT:      "executeCommandProvider": {
 # CHECK-NEXT:        "commands": [
-# CHECK-NEXT:          "clangd.applyFix",
-# CHECK-NEXT:          "clangd.applyRename"
-# CHECK-NEXT:          "clangd.applyTweak"
+# CHECK-NEXT:          "clangd.applyFix.{{[0-9A-F]+}}",
+# CHECK-NEXT:          "clangd.applyRename.{{[0-9A-F]+}}",
+# CHECK-NEXT:          "clangd.applyTweak.{{[0-9A-F]+}}"
 # CHECK-NEXT:        ]
 # CHECK-NEXT:      },
 # CHECK-NEXT:      "foldingRangeProvider": true,

@DaanDeMeyer
Copy link
Contributor Author

Ping

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants