Index: clangd/ClangdLSPServer.h =================================================================== --- clangd/ClangdLSPServer.h +++ clangd/ClangdLSPServer.h @@ -52,6 +52,7 @@ private: // Implement DiagnosticsConsumer. void onDiagnosticsReady(PathRef File, std::vector Diagnostics) override; + void onFileUpdated(PathRef File, const TUStatus &Status) override; // LSP methods. Notifications have signature void(const Params&). // Calls have signature void(const Params&, Callback). @@ -132,11 +133,12 @@ SymbolKindBitset SupportedSymbolKinds; /// The supported completion item kinds of the client. CompletionItemKindBitset SupportedCompletionItemKinds; - // Whether the client supports CodeAction response objects. + /// Whether the client supports CodeAction response objects. bool SupportsCodeAction = false; /// From capabilities of textDocument/documentSymbol. bool SupportsHierarchicalDocumentSymbol = false; - + /// Whether the client supports showing file status. + bool SupportFileStatus = false; // Store of the current versions of the open documents. DraftStore DraftMgr; Index: clangd/ClangdLSPServer.cpp =================================================================== --- clangd/ClangdLSPServer.cpp +++ clangd/ClangdLSPServer.cpp @@ -294,7 +294,7 @@ SupportsCodeAction = Params.capabilities.CodeActionStructure; SupportsHierarchicalDocumentSymbol = Params.capabilities.HierarchicalDocumentSymbol; - + SupportFileStatus = Params.initializationOptions.FileStatus; Reply(json::Object{ {{"capabilities", json::Object{ @@ -802,6 +802,19 @@ }); } +void ClangdLSPServer::onFileUpdated(PathRef File, const TUStatus &Status) { + if (!SupportFileStatus) + return; + // FIXME: we don't emit "BuildingFile" and `RunningAction`, as these + // two statuses are running faster in practice, which leads the UI constantly + // changing, and doesn't provide much value. We may want to emit status at a + // reasonable time interval (e.g. 0.5s). + if (Status.Action.S == TUAction::BuildingFile || + Status.Action.S == TUAction::RunningAction) + return; + notify("textDocument/clangd.fileStatus", Status.render(File)); +} + void ClangdLSPServer::reparseOpenedFiles() { for (const Path &FilePath : DraftMgr.getActiveFiles()) Server->addDocument(FilePath, *DraftMgr.getDraft(FilePath), Index: clangd/Protocol.h =================================================================== --- clangd/Protocol.h +++ clangd/Protocol.h @@ -397,6 +397,9 @@ // the compilation database doesn't describe an opened file. // The command used will be approximately `clang $FILE $fallbackFlags`. std::vector fallbackFlags; + + /// Clients supports show file status for textDocument/clangd.fileStatus. + bool FileStatus = false; }; bool fromJSON(const llvm::json::Value &, InitializationOptions &); @@ -973,6 +976,33 @@ }; bool fromJSON(const llvm::json::Value &, ReferenceParams &); +enum class MessageType { + Error = 1, + Warning = 2, + Info = 3, + Log = 4, +}; +struct ShowMessageParams { + /// The message type. + MessageType type; + /// The actual message. + std::string message; +}; +llvm::json::Value toJSON(const ShowMessageParams &); + +/// Clangd extension: indicates the current state of the file in clangd, +/// sent from server via the `textDocument/fileStatus` notification. +struct FileStatus { + /// The text document's URI. + URIForFile uri; + /// The human-readable string presents the current state of the file, can be + /// shown in the UI (e.g. status bar). + std::string state; + /// Details of the state that are worth sufacing to users. + std::vector details; +}; +llvm::json::Value toJSON(const FileStatus &FStatus); + } // namespace clangd } // namespace clang Index: clangd/Protocol.cpp =================================================================== --- clangd/Protocol.cpp +++ clangd/Protocol.cpp @@ -716,6 +716,20 @@ }; } +llvm::json::Value toJSON(const ShowMessageParams &S) { + return json::Object{ + {"type", static_cast(S.type)}, {"message", S.message}, + }; +} + +llvm::json::Value toJSON(const FileStatus &FStatus) { + return json::Object{ + {"uri", FStatus.uri}, + {"state", FStatus.state}, + {"details", FStatus.details}, + }; +} + raw_ostream &operator<<(raw_ostream &O, const DocumentHighlight &V) { O << V.range; if (V.kind == DocumentHighlightKind::Read) @@ -752,6 +766,7 @@ fromJSON(Params, Opts.ConfigSettings); O.map("compilationDatabasePath", Opts.compilationDatabasePath); O.map("fallbackFlags", Opts.fallbackFlags); + O.map("fileStatus", Opts.FileStatus); return true; } Index: clangd/TUScheduler.h =================================================================== --- clangd/TUScheduler.h +++ clangd/TUScheduler.h @@ -77,6 +77,8 @@ /// Indicates whether we reused the prebuilt AST. bool ReuseAST = false; }; + /// Serialize this to an LSP file status item. + FileStatus render(PathRef File) const; TUAction Action; BuildDetails Details; Index: clangd/TUScheduler.cpp =================================================================== --- clangd/TUScheduler.cpp +++ clangd/TUScheduler.cpp @@ -729,6 +729,33 @@ return wait(Lock, RequestsCV, Timeout, [&] { return Requests.empty(); }); } +// Render a TUAction to a user-facing string representation. +// TUAction represents clangd-internal states, we don't intend to expose them +// to users (say C++ programmers) directly to avoid confusion, we use terms that +// are familiar by C++ programmers. +std::string renderTUAction(const TUAction& Action) { + std::string Result; + raw_string_ostream OS(Result); + switch (Action.S) { + case TUAction::Queued: + OS << "file is queued"; + break; + case TUAction::RunningAction: + OS << "running " << Action.Name; + break; + case TUAction::BuildingPreamble: + OS << "parsing includes"; + break; + case TUAction::BuildingFile: + OS << "parsing main file"; + break; + case TUAction::Idle: + OS << "idle"; + break; + } + return OS.str(); +} + } // namespace unsigned getDefaultAsyncThreadsCount() { @@ -741,6 +768,17 @@ return HardwareConcurrency; } +FileStatus TUStatus::render(PathRef File) const { + FileStatus FStatus; + FStatus.uri = URIForFile::canonicalize(File, /*TUPath=*/File); + FStatus.state = renderTUAction(Action); + if (Details.BuildFailed) + FStatus.details.push_back({MessageType::Error, "failed to build the file"}); + if (Details.ReuseAST) + FStatus.details.push_back({MessageType::Info, "reused the AST"}); + return FStatus; +} + struct TUScheduler::FileData { /// Latest inputs, passed to TUScheduler::update(). std::string Contents; Index: clangd/index/Merge.cpp =================================================================== --- clangd/index/Merge.cpp +++ clangd/index/Merge.cpp @@ -134,6 +134,8 @@ S.Documentation = O.Documentation; if (S.ReturnType == "") S.ReturnType = O.ReturnType; + if (S.Type == "") + S.Type = O.Type; for (const auto &OI : O.IncludeHeaders) { bool Found = false; for (auto &SI : S.IncludeHeaders) { Index: test/clangd/filestatus.test =================================================================== --- /dev/null +++ test/clangd/filestatus.test @@ -0,0 +1,14 @@ +# RUN: clangd -lit-test < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"initializationOptions":{"fileStatus": true},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":"int x; int y = x;"}}} +# CHECK: "method": "textDocument/clangd.fileStatus", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "details": [], +# CHECK-NEXT: "state": "parsing includes", +# CHECK-NEXT: "uri": "{{.*}}/main.cpp" +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","id":3,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: unittests/clangd/IndexTests.cpp =================================================================== --- unittests/clangd/IndexTests.cpp +++ unittests/clangd/IndexTests.cpp @@ -219,6 +219,7 @@ R.Documentation = "--doc--"; L.Origin = SymbolOrigin::Dynamic; R.Origin = SymbolOrigin::Static; + R.Type = "expectedType"; Symbol M = mergeSymbol(L, R); EXPECT_EQ(M.Name, "Foo"); @@ -227,6 +228,7 @@ EXPECT_EQ(M.Signature, "()"); EXPECT_EQ(M.CompletionSnippetSuffix, "{$1:0}"); EXPECT_EQ(M.Documentation, "--doc--"); + EXPECT_EQ(M.Type, "expectedType"); EXPECT_EQ(M.Origin, SymbolOrigin::Dynamic | SymbolOrigin::Static | SymbolOrigin::Merge); }