diff --git a/clang-tools-extra/clangd/ClangdLSPServer.h b/clang-tools-extra/clangd/ClangdLSPServer.h --- a/clang-tools-extra/clangd/ClangdLSPServer.h +++ b/clang-tools-extra/clangd/ClangdLSPServer.h @@ -87,6 +87,7 @@ std::vector Diagnostics) override; void onFileUpdated(PathRef File, const TUStatus &Status) override; void onBackgroundIndexProgress(const BackgroundQueue::Stats &Stats) override; + void onClientInformationInvalidated(PathRef File) override; // LSP methods. Notifications have signature void(const Params&). // Calls have signature void(const Params&, Callback). @@ -181,11 +182,12 @@ ReportWorkDoneProgress; LSPBinder::OutgoingNotification> EndWorkDoneProgress; + LSPBinder::OutgoingMethod SemanticTokensRefresh; void applyEdit(WorkspaceEdit WE, llvm::json::Value Success, Callback Reply); - void bindMethods(LSPBinder &); + void bindMethods(LSPBinder &, const ClientCapabilities &Caps); std::vector getFixes(StringRef File, const clangd::Diagnostic &D); /// Checks if completion request should be ignored. We need this due to the diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp --- a/clang-tools-extra/clangd/ClangdLSPServer.cpp +++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp @@ -579,7 +579,7 @@ { LSPBinder Binder(Handlers, *this); - bindMethods(Binder); + bindMethods(Binder, Params.capabilities); if (Opts.Modules) for (auto &Mod : *Opts.Modules) Mod.initializeLSP(Binder, Params.rawCapabilities, ServerCaps); @@ -1452,7 +1452,8 @@ Bind.method("initialize", this, &ClangdLSPServer::onInitialize); } -void ClangdLSPServer::bindMethods(LSPBinder &Bind) { +void ClangdLSPServer::bindMethods(LSPBinder &Bind, + const ClientCapabilities &Caps) { // clang-format off Bind.notification("initialized", this, &ClangdLSPServer::onInitialized); Bind.method("shutdown", this, &ClangdLSPServer::onShutdown); @@ -1506,6 +1507,8 @@ BeginWorkDoneProgress = Bind.outgoingNotification("$/progress"); ReportWorkDoneProgress = Bind.outgoingNotification("$/progress"); EndWorkDoneProgress = Bind.outgoingNotification("$/progress"); + if(Caps.SemanticTokenRefreshSupport) + SemanticTokensRefresh = Bind.outgoingMethod("workspace/semanticTokens/refresh"); // clang-format on } @@ -1692,5 +1695,15 @@ WantDiagnostics::Auto); } +void ClangdLSPServer::onClientInformationInvalidated(PathRef File) { + if (SemanticTokensRefresh) { + SemanticTokensRefresh(NoParams{}, [](llvm::Expected E) { + if (E) + return; + elog("Failed to refresh semantic tokens: {0}", E.takeError()); + }); + } +} + } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/ClangdServer.h b/clang-tools-extra/clangd/ClangdServer.h --- a/clang-tools-extra/clangd/ClangdServer.h +++ b/clang-tools-extra/clangd/ClangdServer.h @@ -27,6 +27,7 @@ #include "support/Cancellation.h" #include "support/Function.h" #include "support/MemoryTree.h" +#include "support/Path.h" #include "support/ThreadsafeFS.h" #include "clang/Tooling/CompilationDatabase.h" #include "clang/Tooling/Core/Replacement.h" @@ -73,6 +74,12 @@ /// Not called concurrently. virtual void onBackgroundIndexProgress(const BackgroundQueue::Stats &Stats) {} + + /// Called when server triggers a state change without client requesting it. + /// For example a preamble build, which might happen on its own, + /// invalidating information like semantic tokens on client side. This is + /// used to notify clients that they might have stale information. + virtual void onClientInformationInvalidated(PathRef File) {} }; /// Creates a context provider that loads and installs config. /// Errors in loading config are reported as diagnostics via Callbacks. diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp --- a/clang-tools-extra/clangd/ClangdServer.cpp +++ b/clang-tools-extra/clangd/ClangdServer.cpp @@ -73,6 +73,8 @@ const CanonicalIncludes &CanonIncludes) override { if (FIndex) FIndex->updatePreamble(Path, Version, Ctx, std::move(PP), CanonIncludes); + if (ServerCallbacks) + ServerCallbacks->onClientInformationInvalidated(Path); } void onMainAST(PathRef Path, ParsedAST &AST, PublishFn Publish) override { diff --git a/clang-tools-extra/clangd/Protocol.h b/clang-tools-extra/clangd/Protocol.h --- a/clang-tools-extra/clangd/Protocol.h +++ b/clang-tools-extra/clangd/Protocol.h @@ -261,6 +261,7 @@ bool fromJSON(const llvm::json::Value &E, TraceLevel &Out, llvm::json::Path); struct NoParams {}; +inline llvm::json::Value toJSON(const NoParams &) { return nullptr; } inline bool fromJSON(const llvm::json::Value &, NoParams &, llvm::json::Path) { return true; } @@ -473,6 +474,10 @@ /// This is a clangd extension. /// window.implicitWorkDoneProgressCreate bool ImplicitProgressCreation = false; + + /// Whether the client implementation supports a refresh request sent from the + /// server to the client. + bool SemanticTokenRefreshSupport = false; }; bool fromJSON(const llvm::json::Value &, ClientCapabilities &, llvm::json::Path); diff --git a/clang-tools-extra/clangd/Protocol.cpp b/clang-tools-extra/clangd/Protocol.cpp --- a/clang-tools-extra/clangd/Protocol.cpp +++ b/clang-tools-extra/clangd/Protocol.cpp @@ -403,6 +403,10 @@ } } } + if (auto *SemanticTokens = Workspace->getObject("semanticTokens")) { + if (auto RefreshSupport = SemanticTokens->getBoolean("refreshSupport")) + R.SemanticTokenRefreshSupport = *RefreshSupport; + } } if (auto *Window = O->getObject("window")) { if (auto WorkDoneProgress = Window->getBoolean("workDoneProgress")) diff --git a/clang-tools-extra/clangd/test/semantic-tokens-refresh.test b/clang-tools-extra/clangd/test/semantic-tokens-refresh.test new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/test/semantic-tokens-refresh.test @@ -0,0 +1,42 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"capabilities":{ + "workspace":{"semanticTokens":{"refreshSupport":true}} +}}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{ + "uri": "test:///foo.cpp", + "languageId": "cpp", + "text": "int x = 2;" +}}} +# Expect a request after initial preamble build. +# CHECK: "method": "workspace/semanticTokens/refresh", +# CHECK-NEXT: "params": null +# CHECK-NEXT: } +--- +# Reply with success. +{"jsonrpc":"2.0","id":0} +--- +# Preamble stays the same, no refresh requests. +{"jsonrpc":"2.0","method":"textDocument/didChange","params":{ + "textDocument": {"uri":"test:///foo.cpp","version":2}, + "contentChanges":[{"text":"int x = 2;\nint y = 3;"}] +}} +# CHECK-NOT: "method": "workspace/semanticTokens/refresh" +--- +# Preamble changes +{"jsonrpc":"2.0","method":"textDocument/didChange","params":{ + "textDocument": {"uri":"test:///foo.cpp","version":2}, + "contentChanges":[{"text":"#define FOO"}] +}} +# Expect a request after initial preamble build. +# CHECK: "method": "workspace/semanticTokens/refresh", +# CHECK-NEXT: "params": null +# CHECK-NEXT: } +--- +# Reply with error, to make sure there are no crashes. +{"jsonrpc":"2.0","id":1,"error":{"code": 0, "message": "msg"}} +--- +{"jsonrpc":"2.0","id":3,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} +