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 @@ -10,6 +10,7 @@ #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_CLANGDLSPSERVER_H #include "ClangdServer.h" +#include "ConfigProvider.h" #include "DraftStore.h" #include "Features.inc" #include "FindSymbols.h" @@ -209,6 +210,22 @@ notify("$/progress", Params); } + /// Stores clangd config set over LSP. + class LSPConfigProvider : public config::Provider { + mutable std::mutex Mu; + mutable std::map Cached; + llvm::StringMap Pending; + + std::vector + getFragments(const config::Params &, + config::DiagnosticCallback) const override; + + public: + void update(const std::map &); + }; + LSPConfigProvider LSPConfig; + std::unique_ptr ConfigProviderStorage; + const ThreadsafeFS &TFS; /// Options used for code completion clangd::CodeCompleteOptions CCOpts; 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 @@ -8,6 +8,7 @@ #include "ClangdLSPServer.h" #include "CodeComplete.h" +#include "ConfigFragment.h" #include "Diagnostics.h" #include "DraftStore.h" #include "GlobalCompilationDatabase.h" @@ -1199,6 +1200,12 @@ void ClangdLSPServer::applyConfiguration( const ConfigurationSettings &Settings) { + bool ReparseAllFiles = false; + if (!Settings.fragments.empty()) { + LSPConfig.update(Settings.fragments); + ReparseAllFiles = true; // Config may affect any open file. + } + // Per-file update to the compilation database. llvm::StringSet<> ModifiedFiles; for (auto &Entry : Settings.compilationDatabaseChanges) { @@ -1214,8 +1221,9 @@ } } - reparseOpenFilesIfNeeded( - [&](llvm::StringRef File) { return ModifiedFiles.count(File) != 0; }); + reparseOpenFilesIfNeeded([&](llvm::StringRef File) { + return ReparseAllFiles || ModifiedFiles.count(File) != 0; + }); } void ClangdLSPServer::publishTheiaSemanticHighlighting( @@ -1234,6 +1242,35 @@ applyConfiguration(Params.settings); } +std::vector +ClangdLSPServer::LSPConfigProvider::getFragments( + const config::Params &P, config::DiagnosticCallback DC) const { + std::lock_guard Lock(Mu); + // Compile any pending configs, and merge them into the cache. + for (const auto &Entry : Pending) { + llvm::Optional Fragment; + if (!Entry.second.getAsNull()) + Fragment = config::Fragment::parseJSON( + Entry.second, ("LSP:" + Entry.first()).str(), DC); + if (Fragment) + Cached[Entry.first().str()] = std::move(*Fragment).compile(DC); + else + Cached.erase(Entry.first().str()); + } + // Now extract all fragments from the cache. + std::vector Results; + for (const auto &Entry : Cached) // Sorted by key, as desired. + Results.push_back(Entry.second); + return Results; +} + +void ClangdLSPServer::LSPConfigProvider::update( + const std::map &M) { + std::lock_guard Lock(Mu); + for (const auto &Entry : M) + Pending.insert_or_assign(Entry.first, Entry.second); +} + void ClangdLSPServer::onReference(const ReferenceParams &Params, Callback> Reply) { Server->findReferences(Params.textDocument.uri.file(), Params.position, @@ -1405,6 +1442,13 @@ if (Opts.FoldingRanges) MsgHandler->bind("textDocument/foldingRange", &ClangdLSPServer::onFoldingRange); // clang-format on + if (Opts.ConfigProvider) { + ConfigProviderStorage = + config::Provider::combine({&LSPConfig, Opts.ConfigProvider}); + ClangdServerOpts.ConfigProvider = ConfigProviderStorage.get(); + } else { + ClangdServerOpts.ConfigProvider = &LSPConfig; + } } ClangdLSPServer::~ClangdLSPServer() { diff --git a/clang-tools-extra/clangd/ConfigFragment.h b/clang-tools-extra/clangd/ConfigFragment.h --- a/clang-tools-extra/clangd/ConfigFragment.h +++ b/clang-tools-extra/clangd/ConfigFragment.h @@ -36,6 +36,7 @@ #include "llvm/ADT/Optional.h" #include "llvm/ADT/STLExtras.h" #include "llvm/Support/Error.h" +#include "llvm/Support/JSON.h" #include "llvm/Support/SMLoc.h" #include "llvm/Support/SourceMgr.h" #include @@ -69,6 +70,13 @@ llvm::StringRef BufferName, DiagnosticCallback); + /// Parses a fragment from a JSON value (an Object). + /// Returns None if the value contained a fatal error and could not be parsed. + /// Locations are not reported with the diagnostics. + static llvm::Optional parseJSON(const llvm::json::Value &JSON, + llvm::StringRef SourceName, + DiagnosticCallback); + /// Analyzes and consumes this fragment, possibly yielding more diagnostics. /// This always produces a usable result (errors are recovered). /// diff --git a/clang-tools-extra/clangd/ConfigYAML.cpp b/clang-tools-extra/clangd/ConfigYAML.cpp --- a/clang-tools-extra/clangd/ConfigYAML.cpp +++ b/clang-tools-extra/clangd/ConfigYAML.cpp @@ -11,6 +11,7 @@ #include "llvm/ADT/SmallSet.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/ScopedPrinter.h" #include "llvm/Support/SourceMgr.h" #include "llvm/Support/YAMLParser.h" #include @@ -225,6 +226,22 @@ return Result; } +llvm::Optional Fragment::parseJSON(const llvm::json::Value &Value, + llvm::StringRef Source, + DiagnosticCallback Diags) { + // This is a horrible hack: JSON is a subset of YAML, so use the YAML parser. + // FIXME: The diagnostics produced will use YAML terms etc. Do this properly. + auto NoLocDiags = [&](const llvm::SMDiagnostic &D) { + Diags(llvm::SMDiagnostic(Source, D.getKind(), D.getMessage())); + }; + auto Result = parseYAML(llvm::to_string(Value), "", NoLocDiags); + + assert(Result.size() <= 1 && "JSON yielded multiple YAML documents?"); + if (Result.empty()) + return llvm::None; + return std::move(Result.front()); +} + } // namespace config } // namespace clangd } // namespace clang 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 @@ -488,6 +488,11 @@ // Changes to the in-memory compilation database. // The key of the map is a file name. std::map compilationDatabaseChanges; + + // clangd configuration fragments: see https://clangd.llvm.org/config.html. + // Each value is an Object or null, and replaces any fragment with that key. + // Fragments are used in key order ("!foo" is low-priority, "~foo" is high). + std::map fragments; }; bool fromJSON(const llvm::json::Value &, ConfigurationSettings &); @@ -527,9 +532,6 @@ /// `rootUri` wins. llvm::Optional rootUri; - // User provided initialization options. - // initializationOptions?: any; - /// The capabilities provided by the client (editor or tool) ClientCapabilities capabilities; @@ -537,6 +539,7 @@ llvm::Optional trace; /// User-provided initialization options. + /// LSP defines this type as `any`. InitializationOptions initializationOptions; }; bool fromJSON(const llvm::json::Value &, InitializeParams &); 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 @@ -1077,6 +1077,9 @@ if (!O) return true; // 'any' type in LSP. O.map("compilationDatabaseChanges", S.compilationDatabaseChanges); + if (auto *Clangd = Params.getAsObject()->getObject("fragments")) + for (const auto &Entry : *Clangd) + S.fragments.emplace(Entry.first.str(), llvm::json::Value(Entry.second)); return true; } diff --git a/clang-tools-extra/clangd/test/config.test b/clang-tools-extra/clangd/test/config.test new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clangd/test/config.test @@ -0,0 +1,65 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +# Set some config fragments on startup. +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","initializationOptions": { + "fragments": { + "!global": { + "CompileFlags": {"Add": "-DFOO=A"} + }, + "bar": { + "If": {"PathMatch":".*/bar.c"}, + "CompileFlags": {"Add": "-DFOO=B"} + } + } +}}} +--- +# foo.c gets the global configuration. +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///foo.c","languageId":"c", + "text":"int main() { return FOO; }" +}}} +# CHECK: "method": "textDocument/publishDiagnostics", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "diagnostics": [ +# CHECK-NEXT: { +# CHECK-NEXT: "code": "undeclared_var_use", +# CHECK-NEXT: "message": "Use of undeclared identifier 'A'", +# CHECK: } +# CHECK: ], +# CHECK-NEXT: "uri": "file://{{.*}}/foo.c", +# CHECK-NEXT: "version": 0 +# CHECK-NEXT: } +--- +# bar.c (also) gets the conditional configuration. +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///bar.c","languageId":"c", + "text":"int main() { return FOO; }" +}}} +# CHECK: "method": "textDocument/publishDiagnostics", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "diagnostics": [ +# CHECK-NEXT: { +# CHECK-NEXT: "code": "undeclared_var_use", +# CHECK-NEXT: "message": "Use of undeclared identifier 'B'", +# CHECK: } +# CHECK: ], +# CHECK-NEXT: "uri": "file://{{.*}}/bar.c", +# CHECK-NEXT: "version": 0 +# CHECK-NEXT: } +--- +# Replace global configuration. +{"jsonrpc":"2.0","method":"workspace/didChangeConfiguration","params":{"settings":{ + "fragments": { + "!global": { + "CompileFlags": {"Add": "-DFOO=C"} + } + } +}}} +# Both files get rebuilt, and use the new config. +# CHECK: "method": "textDocument/publishDiagnostics", +# CHECK-DAG: "message": "Use of undeclared identifier 'C'", +# CHECK-DAG: "message": "Use of undeclared identifier 'B'", +--- +{"jsonrpc":"2.0","id":5,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} + + + diff --git a/clang-tools-extra/clangd/unittests/ConfigYAMLTests.cpp b/clang-tools-extra/clangd/unittests/ConfigYAMLTests.cpp --- a/clang-tools-extra/clangd/unittests/ConfigYAMLTests.cpp +++ b/clang-tools-extra/clangd/unittests/ConfigYAMLTests.cpp @@ -119,6 +119,25 @@ ASSERT_THAT(Results, IsEmpty()); } +TEST(ParseYAML, JSON) { + CapturedDiags Diags; + llvm::json::Object Config{ + {"If", llvm::json::Object{{"UnknownCondition", "foo"}}}, + {"CompileFlags", llvm::json::Object{{"Add", "first"}}}, + }; + auto Result = + Fragment::parseJSON(std::move(Config), "config.json", Diags.callback()); + + ASSERT_THAT(Diags.Diagnostics, + ElementsAre(AllOf(DiagMessage("Unknown If key UnknownCondition"), + DiagKind(llvm::SourceMgr::DK_Warning)))); + EXPECT_EQ(Diags.Diagnostics.front().Rng, llvm::None); + + ASSERT_NE(Result, llvm::None); + EXPECT_TRUE(Result->If.HasUnrecognizedCondition); + EXPECT_THAT(Result->CompileFlags.Add, ElementsAre(Val("first"))); +} + } // namespace } // namespace config } // namespace clangd