-
Notifications
You must be signed in to change notification settings - Fork 15.4k
[clangd] Use unique command names to fix multi-root workspace support #169734
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
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
|
@llvm/pr-subscribers-clang-tools-extra Author: Daan De Meyer (DaanDeMeyer) ChangesWhen 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:
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,
|
|
@llvm/pr-subscribers-clangd Author: Daan De Meyer (DaanDeMeyer) ChangesWhen 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:
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,
|
|
Ping |
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