Index: clangd/ClangdLSPServer.h =================================================================== --- clangd/ClangdLSPServer.h +++ clangd/ClangdLSPServer.h @@ -35,7 +35,7 @@ /// for compile_commands.json in all parent directories of each file. ClangdLSPServer(JSONOutput &Out, const clangd::CodeCompleteOptions &CCOpts, llvm::Optional CompileCommandsDir, - const ClangdServer::Options &Opts); + bool ShouldUseInMemoryCDB, const ClangdServer::Options &Opts); /// Run LSP server loop, receiving input for it from \p In. \p In must be /// opened in binary mode. Output will be written using Out variable passed to @@ -99,10 +99,57 @@ /// Caches FixIts per file and diagnostics llvm::StringMap FixItsMap; + /// Encapsulates the directory-based or the in-memory compilation database + /// that's used by the LSP server. + class CompilationDB { + public: + static CompilationDB makeInMemory(); + static CompilationDB + makeDirectoryBased(llvm::Optional CompileCommandsDir); + + void invalidate(PathRef File); + + /// Sets the compilation command for a particular file. + /// Only valid for in-memory CDB, no-op and error log on DirectoryBasedCDB. + /// + /// \returns True if the File had no compilation command before. + bool + setCompilationCommandForFile(PathRef File, + tooling::CompileCommand CompilationCommand); + + /// Adds extra compilation flags to the compilation command for a particular + /// file. Only valid for directory-based CDB, no-op and error log on + /// InMemoryCDB; + void setExtraFlagsForFile(PathRef File, + std::vector ExtraFlags); + + /// Set the compile commands directory to \p P. + /// Only valid for directory-based CDB, no-op and error log on InMemoryCDB; + void setCompileCommandsDir(Path P); + + /// Returns a CDB that should be used to get compile commands for the + /// current instance of ClangdLSPServer. + GlobalCompilationDatabase &getCDB(); + + private: + CompilationDB(std::unique_ptr CDB, + std::unique_ptr CachingCDB, + bool IsDirectoryBased) + : CDB(std::move(CDB)), CachingCDB(std::move(CachingCDB)), + IsDirectoryBased(IsDirectoryBased) {} + + // if IsDirectoryBased is true, an instance of InMemoryCDB. + // If IsDirectoryBased is false, an instance of DirectoryBasedCDB. + // unique_ptr CDB; + std::unique_ptr CDB; + // Non-null only for directory-based CDB + std::unique_ptr CachingCDB; + bool IsDirectoryBased; + }; + // Various ClangdServer parameters go here. It's important they're created // before ClangdServer. - DirectoryBasedGlobalCompilationDatabase NonCachedCDB; - CachingCompilationDb CDB; + CompilationDB CDB; RealFileSystemProvider FSProvider; /// Options used for code completion Index: clangd/ClangdLSPServer.cpp =================================================================== --- clangd/ClangdLSPServer.cpp +++ clangd/ClangdLSPServer.cpp @@ -132,11 +132,8 @@ void ClangdLSPServer::onDocumentDidOpen(DidOpenTextDocumentParams &Params) { PathRef File = Params.textDocument.uri.file(); - if (Params.metadata && !Params.metadata->extraFlags.empty()) { - NonCachedCDB.setExtraFlagsForFile(File, - std::move(Params.metadata->extraFlags)); - CDB.invalidate(File); - } + if (Params.metadata && !Params.metadata->extraFlags.empty()) + CDB.setExtraFlagsForFile(File, std::move(Params.metadata->extraFlags)); std::string &Contents = Params.textDocument.text; @@ -247,6 +244,7 @@ PathRef File = Params.textDocument.uri.file(); DraftMgr.removeDraft(File); Server.removeDocument(File); + CDB.invalidate(File); } void ClangdLSPServer::onDocumentOnTypeFormatting( @@ -405,21 +403,42 @@ // Compilation database change. if (Settings.compilationDatabasePath.hasValue()) { - NonCachedCDB.setCompileCommandsDir( - Settings.compilationDatabasePath.getValue()); - CDB.clear(); + CDB.setCompileCommandsDir(Settings.compilationDatabasePath.getValue()); reparseOpenedFiles(); } + + // Update to the compilation database. + if (Settings.compilationDatabaseChanges) { + const auto &CompileCommandUpdates = *Settings.compilationDatabaseChanges; + bool ShouldReparseOpenFiles = false; + for (auto &Entry : CompileCommandUpdates) { + /// The opened files need to be reparsed only when some existing + /// entries are changed. + PathRef File = Entry.first; + if (!CDB.setCompilationCommandForFile( + File, tooling::CompileCommand( + std::move(Entry.second.workingDirectory), + llvm::sys::path::filename(File), + std::move(Entry.second.compilationCommand), + /*Output=*/""))) + ShouldReparseOpenFiles = true; + } + if (ShouldReparseOpenFiles) + reparseOpenedFiles(); + } } ClangdLSPServer::ClangdLSPServer(JSONOutput &Out, const clangd::CodeCompleteOptions &CCOpts, llvm::Optional CompileCommandsDir, + bool ShouldUseInMemoryCDB, const ClangdServer::Options &Opts) - : Out(Out), NonCachedCDB(std::move(CompileCommandsDir)), CDB(NonCachedCDB), + : Out(Out), CDB(ShouldUseInMemoryCDB ? CompilationDB::makeInMemory() + : CompilationDB::makeDirectoryBased( + std::move(CompileCommandsDir))), CCOpts(CCOpts), SupportedSymbolKinds(defaultSymbolKinds()), - Server(CDB, FSProvider, /*DiagConsumer=*/*this, Opts) {} + Server(CDB.getCDB(), FSProvider, /*DiagConsumer=*/*this, Opts) {} bool ClangdLSPServer::run(std::FILE *In, JSONStreamStyle InputStyle) { assert(!IsDone && "Run was called before"); @@ -498,3 +517,67 @@ Server.addDocument(FilePath, *DraftMgr.getDraft(FilePath), WantDiagnostics::Auto); } + +ClangdLSPServer::CompilationDB ClangdLSPServer::CompilationDB::makeInMemory() { + return CompilationDB(llvm::make_unique(), nullptr, + /*IsDirectoryBased=*/false); +} + +ClangdLSPServer::CompilationDB +ClangdLSPServer::CompilationDB::makeDirectoryBased( + llvm::Optional CompileCommandsDir) { + auto CDB = llvm::make_unique( + std::move(CompileCommandsDir)); + auto CachingCDB = llvm::make_unique(*CDB); + return CompilationDB(std::move(CDB), std::move(CachingCDB), + /*IsDirectoryBased=*/true); +} + +void ClangdLSPServer::CompilationDB::invalidate(PathRef File) { + if (!IsDirectoryBased) + static_cast(CDB.get())->invalidate(File); + else if (CachingCDB) + CachingCDB->invalidate(File); +} + +bool ClangdLSPServer::CompilationDB::setCompilationCommandForFile( + PathRef File, tooling::CompileCommand CompilationCommand) { + if (IsDirectoryBased) { + elog("Trying to set compile command for {0} while using directory-based " + "compilation database", + File); + return false; + } + return static_cast(CDB.get()) + ->setCompilationCommandForFile(File, std::move(CompilationCommand)); +} + +void ClangdLSPServer::CompilationDB::setExtraFlagsForFile( + PathRef File, std::vector ExtraFlags) { + if (!IsDirectoryBased) { + elog("Trying to set extra flags for {0} while using in-memory compilation " + "database", + File); + return; + } + static_cast(CDB.get()) + ->setExtraFlagsForFile(File, std::move(ExtraFlags)); + CachingCDB->invalidate(File); +} + +void ClangdLSPServer::CompilationDB::setCompileCommandsDir(Path P) { + if (!IsDirectoryBased) { + elog("Trying to set compile commands dir while using in-memory compilation " + "database"); + return; + } + static_cast(CDB.get()) + ->setCompileCommandsDir(P); + CachingCDB->clear(); +} + +GlobalCompilationDatabase &ClangdLSPServer::CompilationDB::getCDB() { + if (CachingCDB) + return *CachingCDB; + return *CDB; +} Index: clangd/GlobalCompilationDatabase.h =================================================================== --- clangd/GlobalCompilationDatabase.h +++ clangd/GlobalCompilationDatabase.h @@ -113,6 +113,29 @@ Cached; /* GUARDED_BY(Mut) */ }; +/// Gets compile args from an in-memory mapping based on a filepath. Typically +/// used by clients who provide the compile commands themselves. +class InMemoryCompilationDb : public GlobalCompilationDatabase { +public: + /// Gets compile command for \p File from the stored mapping. + llvm::Optional + getCompileCommand(PathRef File) const override; + + /// Sets the compilation command for a particular file. + /// + /// \returns True if the File had no compilation command before. + bool setCompilationCommandForFile(PathRef File, + tooling::CompileCommand CompilationCommand); + + /// Removes the compilation command for \p File if it's present in the + /// mapping. + void invalidate(PathRef File); + +private: + mutable std::mutex Mutex; + llvm::StringMap Commands; /* GUARDED_BY(Mut) */ +}; + } // namespace clangd } // namespace clang Index: clangd/GlobalCompilationDatabase.cpp =================================================================== --- clangd/GlobalCompilationDatabase.cpp +++ clangd/GlobalCompilationDatabase.cpp @@ -152,5 +152,29 @@ Cached.clear(); } +llvm::Optional +InMemoryCompilationDb::getCompileCommand(PathRef File) const { + std::lock_guard Lock(Mutex); + auto It = Commands.find(File); + if (It == Commands.end()) + return None; + return It->second; +} + +bool InMemoryCompilationDb::setCompilationCommandForFile( + PathRef File, tooling::CompileCommand CompilationCommand) { + std::unique_lock Lock(Mutex); + auto ItInserted = Commands.insert(std::make_pair(File, CompilationCommand)); + if (ItInserted.second) + return true; + ItInserted.first->setValue(std::move(CompilationCommand)); + return false; +} + +void InMemoryCompilationDb::invalidate(PathRef File) { + std::unique_lock Lock(Mutex); + Commands.erase(File); +} + } // namespace clangd } // namespace clang Index: clangd/Protocol.h =================================================================== --- clangd/Protocol.h +++ clangd/Protocol.h @@ -419,10 +419,24 @@ }; bool fromJSON(const llvm::json::Value &, DidChangeWatchedFilesParams &); +/// Clangd extension that's used in the 'compilationDatabaseChanges' in +/// workspace/didChangeConfiguration to record updates to the in-memory +/// compilation database. +struct ClangdCompileCommand { + std::string workingDirectory; + std::vector compilationCommand; +}; +bool fromJSON(const llvm::json::Value &, ClangdCompileCommand &); + /// Clangd extension to manage a workspace/didChangeConfiguration notification /// since the data received is described as 'any' type in LSP. struct ClangdConfigurationParamsChange { llvm::Optional compilationDatabasePath; + + // The changes that happened to the compilation database. + // The key of the map is a file name. + llvm::Optional> + compilationDatabaseChanges; }; bool fromJSON(const llvm::json::Value &, ClangdConfigurationParamsChange &); Index: clangd/Protocol.cpp =================================================================== --- clangd/Protocol.cpp +++ clangd/Protocol.cpp @@ -591,10 +591,18 @@ return O && O.map("settings", CCP.settings); } +bool fromJSON(const llvm::json::Value &Params, + ClangdCompileCommand &CDbUpdate) { + json::ObjectMapper O(Params); + return O && O.map("workingDirectory", CDbUpdate.workingDirectory) && + O.map("compilationCommand", CDbUpdate.compilationCommand); +} + bool fromJSON(const json::Value &Params, ClangdConfigurationParamsChange &CCPC) { json::ObjectMapper O(Params); - return O && O.map("compilationDatabasePath", CCPC.compilationDatabasePath); + return O && O.map("compilationDatabasePath", CCPC.compilationDatabasePath) && + O.map("compilationDatabaseChanges", CCPC.compilationDatabaseChanges); } } // namespace clangd Index: clangd/tool/ClangdMain.cpp =================================================================== --- clangd/tool/ClangdMain.cpp +++ clangd/tool/ClangdMain.cpp @@ -166,6 +166,18 @@ "eventually. Don't rely on it."), llvm::cl::init(""), llvm::cl::Hidden); +enum CompileArgsFrom { LSPCompileArgs, FilesystemCompileArgs }; + +static llvm::cl::opt CompileArgsFrom( + "compile_args_from", llvm::cl::desc("The source of compile commands"), + llvm::cl::values(clEnumValN(LSPCompileArgs, "lsp", + "All compile commands come from LSP and " + "'compile_commands.json' files are ignored"), + clEnumValN(FilesystemCompileArgs, "filesystem", + "All compile commands come from the " + "'compile_commands.json' files")), + llvm::cl::init(FilesystemCompileArgs), llvm::cl::Hidden); + int main(int argc, char *argv[]) { llvm::sys::PrintStackTraceOnErrorSignal(argv[0]); llvm::cl::SetVersionPrinter([](llvm::raw_ostream &OS) { @@ -278,7 +290,9 @@ CCOpts.ShowOrigins = ShowOrigins; // Initialize and run ClangdLSPServer. - ClangdLSPServer LSPServer(Out, CCOpts, CompileCommandsDirPath, Opts); + ClangdLSPServer LSPServer( + Out, CCOpts, CompileCommandsDirPath, + /*ShouldUseInMemoryCDB=*/CompileArgsFrom == LSPCompileArgs, Opts); constexpr int NoShutdownRequestErrorCode = 1; llvm::set_thread_name("clangd.main"); // Change stdin to binary to not lose \r\n on windows. Index: test/clangd/did-change-configuration-params.test =================================================================== --- /dev/null +++ test/clangd/did-change-configuration-params.test @@ -0,0 +1,51 @@ +# RUN: clangd -compile_args_from=lsp -lit-test < %s 2> %t | FileCheck -strict-whitespace %s +# RUN: cat %t | FileCheck --check-prefix=ERR %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"workspace/didChangeConfiguration","params":{"settings":{"compilationDatabaseChanges":{"/clangd-test/foo.c": {"workingDirectory":"/clangd-test", "compilationCommand": ["clang", "-c", "foo.c"]}}}}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///foo.c","languageId":"c","version":1,"text":"int main() { int i; return i; }"}}} +# CHECK: "method": "textDocument/publishDiagnostics", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "diagnostics": [], +# CHECK-NEXT: "uri": "file://{{.*}}/foo.c" +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///bar.c","languageId":"c","version":1,"text":"int main() { int i; return i; }"}}} +# CHECK: "method": "textDocument/publishDiagnostics", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "diagnostics": [], +# CHECK-NEXT: "uri": "file://{{.*}}/bar.c" +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","method":"workspace/didChangeConfiguration","params":{"settings":{"compilationDatabaseChanges":{"/clangd-test/foo.c": {"workingDirectory":"/clangd-test2", "compilationCommand": ["clang", "-c", "foo.c", "-Wall", "-Werror"]}}}}} +# CHECK: "method": "textDocument/publishDiagnostics", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "diagnostics": [ +# CHECK-NEXT: { +# CHECK-NEXT: "message": "variable 'i' is uninitialized when used here", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 28, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 27, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "severity": 1 +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "uri": "file://{{.*}}/foo.c" +# CHECK-NEXT: } +# +# ERR: Updating file /clangd-test/foo.c with command [/clangd-test2] clang -c foo.c -Wall -Werror +# Don't reparse the second file: +# ERR: Skipping rebuild of the AST for /clangd-test/bar.c +--- +{"jsonrpc":"2.0","id":5,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} + +