Index: clangd/CMakeLists.txt =================================================================== --- clangd/CMakeLists.txt +++ clangd/CMakeLists.txt @@ -19,6 +19,7 @@ Context.cpp Diagnostics.cpp DraftStore.cpp + FindSymbols.cpp FuzzyMatch.cpp GlobalCompilationDatabase.cpp Headers.cpp Index: clangd/ClangdLSPServer.h =================================================================== --- clangd/ClangdLSPServer.h +++ clangd/ClangdLSPServer.h @@ -12,6 +12,7 @@ #include "ClangdServer.h" #include "DraftStore.h" +#include "FindSymbols.h" #include "GlobalCompilationDatabase.h" #include "Path.h" #include "Protocol.h" @@ -69,6 +70,7 @@ void onDocumentHighlight(TextDocumentPositionParams &Params) override; void onFileEvent(DidChangeWatchedFilesParams &Params) override; void onCommand(ExecuteCommandParams &Params) override; + void onWorkspaceSymbol(WorkspaceSymbolParams &Params) override; void onRename(RenameParams &Parames) override; void onHover(TextDocumentPositionParams &Params) override; void onChangeConfiguration(DidChangeConfigurationParams &Params) override; @@ -102,6 +104,8 @@ RealFileSystemProvider FSProvider; /// Options used for code completion clangd::CodeCompleteOptions CCOpts; + /// The supported kinds of the client. + SymbolKindBitset SupportedSymbolKinds; // Store of the current versions of the open documents. DraftStore DraftMgr; Index: clangd/ClangdLSPServer.cpp =================================================================== --- clangd/ClangdLSPServer.cpp +++ clangd/ClangdLSPServer.cpp @@ -86,6 +86,14 @@ return Edits; } +SymbolKindBitset defaultSymbolKinds() { + SymbolKindBitset Defaults; + for (size_t I = SymbolKindMin; I <= static_cast(SymbolKind::Array); + ++I) + Defaults.set(I); + return Defaults; +} + } // namespace void ClangdLSPServer::onInitialize(InitializeParams &Params) { @@ -97,6 +105,14 @@ CCOpts.EnableSnippets = Params.capabilities.textDocument.completion.completionItem.snippetSupport; + if (Params.capabilities.workspace && Params.capabilities.workspace->symbol && + Params.capabilities.workspace->symbol->symbolKind) { + for (SymbolKind Kind : + *Params.capabilities.workspace->symbol->symbolKind->valueSet) { + SupportedSymbolKinds.set(static_cast(Kind)); + } + } + reply(json::obj{ {{"capabilities", json::obj{ @@ -122,6 +138,7 @@ {"documentHighlightProvider", true}, {"hoverProvider", true}, {"renameProvider", true}, + {"workspaceSymbolProvider", true}, {"executeCommandProvider", json::obj{ {"commands", @@ -245,6 +262,20 @@ } } +void ClangdLSPServer::onWorkspaceSymbol(WorkspaceSymbolParams &Params) { + Server.workspaceSymbols( + Params.query, CCOpts.Limit, + [this](llvm::Expected> Items) { + if (!Items) + return replyError(ErrorCode::InternalError, + llvm::toString(Items.takeError())); + for (auto &Sym : *Items) + Sym.kind = adjustKindToCapability(Sym.kind, SupportedSymbolKinds); + + reply(json::ary(*Items)); + }); +} + void ClangdLSPServer::onRename(RenameParams &Params) { Path File = Params.textDocument.uri.file(); llvm::Optional Code = DraftMgr.getDraft(File); @@ -422,6 +453,7 @@ llvm::Optional CompileCommandsDir, const ClangdServer::Options &Opts) : Out(Out), CDB(std::move(CompileCommandsDir)), CCOpts(CCOpts), + SupportedSymbolKinds(defaultSymbolKinds()), Server(CDB, FSProvider, /*DiagConsumer=*/*this, Opts) {} bool ClangdLSPServer::run(std::istream &In, JSONStreamStyle InputStyle) { Index: clangd/ClangdServer.h =================================================================== --- clangd/ClangdServer.h +++ clangd/ClangdServer.h @@ -157,6 +157,10 @@ /// Get code hover for a given position. void findHover(PathRef File, Position Pos, Callback CB); + /// Retrieve the top symbols from the workspace matching a query. + void workspaceSymbols(StringRef Query, int Limit, + Callback> CB); + /// Run formatting for \p Rng inside \p File with content \p Code. llvm::Expected formatRange(StringRef Code, PathRef File, Range Rng); Index: clangd/ClangdServer.cpp =================================================================== --- clangd/ClangdServer.cpp +++ clangd/ClangdServer.cpp @@ -9,6 +9,7 @@ #include "ClangdServer.h" #include "CodeComplete.h" +#include "FindSymbols.h" #include "Headers.h" #include "SourceCode.h" #include "XRefs.h" @@ -499,6 +500,11 @@ // invalidating other caches. } +void ClangdServer::workspaceSymbols( + StringRef Query, int Limit, Callback> CB) { + CB(clangd::getWorkspaceSymbols(Query, Limit, Index)); +} + std::vector> ClangdServer::getUsedBytesPerFile() const { return WorkScheduler.getUsedBytesPerFile(); Index: clangd/FindSymbols.h =================================================================== --- clangd/FindSymbols.h +++ clangd/FindSymbols.h @@ -0,0 +1,37 @@ +//===--- FindSymbols.h --------------------------------------*- C++-*------===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// +// +// Queries that provide a list of symbols matching a string. +// +//===----------------------------------------------------------------------===// +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_FINDSYMBOLS_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_FINDSYMBOLS_H + +#include "Protocol.h" +#include "llvm/ADT/StringRef.h" + +namespace clang { +namespace clangd { +class SymbolIndex; + +/// Searches for the symbols matching \p Query. The syntax of \p Query can be +/// the non-qualified name or fully qualified of a symbol. For example, "vector" +/// will match the symbol std::vector and "std::vector" would also match it. +/// Direct children of scopes (namepaces, etc) can be listed with a trailing +/// "::". For example, "std::" will list all children of the std namespace and +/// "::" alone will list all children of the global namespace. +/// \p Limit limits the number of results returned (0 means no limit). +llvm::Expected> +getWorkspaceSymbols(llvm::StringRef Query, int Limit, + const SymbolIndex *const Index); + +} // namespace clangd +} // namespace clang + +#endif Index: clangd/FindSymbols.cpp =================================================================== --- clangd/FindSymbols.cpp +++ clangd/FindSymbols.cpp @@ -0,0 +1,141 @@ +//===--- FindSymbols.cpp ------------------------------------*- C++-*------===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// +#include "FindSymbols.h" + +#include "Logger.h" +#include "SourceCode.h" +#include "index/Index.h" +#include "clang/Index/IndexSymbol.h" +#include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/Path.h" + +namespace clang { +namespace clangd { + +namespace { + +// Convert a index::SymbolKind to clangd::SymbolKind (LSP) +// Note, some are not perfect matches and should be improved when this LSP +// issue is addressed: +// https://github.com/Microsoft/language-server-protocol/issues/344 +SymbolKind indexSymbolKindToSymbolKind(index::SymbolKind Kind) { + switch (Kind) { + case index::SymbolKind::Unknown: + return SymbolKind::Variable; + case index::SymbolKind::Module: + return SymbolKind::Module; + case index::SymbolKind::Namespace: + return SymbolKind::Namespace; + case index::SymbolKind::NamespaceAlias: + return SymbolKind::Namespace; + case index::SymbolKind::Macro: + return SymbolKind::String; + case index::SymbolKind::Enum: + return SymbolKind::Enum; + case index::SymbolKind::Struct: + return SymbolKind::Struct; + case index::SymbolKind::Class: + return SymbolKind::Class; + case index::SymbolKind::Protocol: + return SymbolKind::Interface; + case index::SymbolKind::Extension: + return SymbolKind::Interface; + case index::SymbolKind::Union: + return SymbolKind::Class; + case index::SymbolKind::TypeAlias: + return SymbolKind::Class; + case index::SymbolKind::Function: + return SymbolKind::Function; + case index::SymbolKind::Variable: + return SymbolKind::Variable; + case index::SymbolKind::Field: + return SymbolKind::Field; + case index::SymbolKind::EnumConstant: + return SymbolKind::EnumMember; + case index::SymbolKind::InstanceMethod: + case index::SymbolKind::ClassMethod: + case index::SymbolKind::StaticMethod: + return SymbolKind::Method; + case index::SymbolKind::InstanceProperty: + case index::SymbolKind::ClassProperty: + case index::SymbolKind::StaticProperty: + return SymbolKind::Property; + case index::SymbolKind::Constructor: + case index::SymbolKind::Destructor: + return SymbolKind::Method; + case index::SymbolKind::ConversionFunction: + return SymbolKind::Function; + case index::SymbolKind::Parameter: + return SymbolKind::Variable; + case index::SymbolKind::Using: + return SymbolKind::Namespace; + } + llvm_unreachable("invalid symbol kind"); +} + +} // namespace + +llvm::Expected> +getWorkspaceSymbols(StringRef Query, int Limit, + const SymbolIndex *const Index) { + std::vector Result; + if (Query.empty() || !Index) + return Result; + + auto Names = splitQualifiedName(Query); + + FuzzyFindRequest Req; + Req.Query = Names.second; + + // FuzzyFind doesn't want leading :: qualifier + bool IsGlobalQuery = Names.first.consume_front("::"); + // Restrict results to the scope in the query string if present (global or + // not). + if (IsGlobalQuery || !Names.first.empty()) + Req.Scopes = {Names.first}; + if (Limit) + Req.MaxCandidateCount = Limit; + Index->fuzzyFind(Req, [&Result](const Symbol &Sym) { + // Prefer the definition over e.g. a function declaration in a header + auto &CD = Sym.Definition ? Sym.Definition : Sym.CanonicalDeclaration; + auto Uri = URI::parse(CD.FileURI); + if (!Uri) { + log(llvm::formatv( + "Workspace symbol: Could not parse URI '{0}' for symbol '{1}'.", + CD.FileURI, Sym.Name)); + return; + } + // FIXME: Passing no HintPath here will work for "file" and "test" schemes + // because they don't use it but this might not work for other custom ones. + auto Path = URI::resolve(*Uri); + if (!Path) { + log(llvm::formatv("Workspace symbol: Could not resolve path for URI " + "'{0}' for symbol '{1}'.", + (*Uri).toString(), Sym.Name.str())); + return; + } + Location L; + L.uri = URIForFile((*Path)); + Position Start, End; + Start.line = CD.Start.Line; + Start.character = CD.Start.Column; + End.line = CD.End.Line; + End.character = CD.End.Column; + L.range = {Start, End}; + SymbolKind SK = indexSymbolKindToSymbolKind(Sym.SymInfo.Kind); + std::string Scope = Sym.Scope; + StringRef ScopeRef = Scope; + ScopeRef.consume_back("::"); + Result.push_back({Sym.Name, SK, L, ScopeRef}); + }); + return Result; +} + +} // namespace clangd +} // namespace clang Index: clangd/Protocol.h =================================================================== --- clangd/Protocol.h +++ clangd/Protocol.h @@ -27,6 +27,7 @@ #include "JSONExpr.h" #include "URI.h" #include "llvm/ADT/Optional.h" +#include #include #include @@ -237,6 +238,67 @@ }; bool fromJSON(const json::Expr &, CompletionClientCapabilities &); +/// A symbol kind. +enum class SymbolKind { + File = 1, + Module = 2, + Namespace = 3, + Package = 4, + Class = 5, + Method = 6, + Property = 7, + Field = 8, + Constructor = 9, + Enum = 10, + Interface = 11, + Function = 12, + Variable = 13, + Constant = 14, + String = 15, + Number = 16, + Boolean = 17, + Array = 18, + Object = 19, + Key = 20, + Null = 21, + EnumMember = 22, + Struct = 23, + Event = 24, + Operator = 25, + TypeParameter = 26 +}; + +constexpr auto SymbolKindMin = static_cast(SymbolKind::File); +constexpr auto SymbolKindMax = static_cast(SymbolKind::TypeParameter); +using SymbolKindBitset = std::bitset; + +bool fromJSON(const json::Expr &, SymbolKind &); + +struct SymbolKindCapabilities { + /// The SymbolKinds that the client supports. If not set, the client only + /// supports <= SymbolKind::Array and will not fall back to a valid default + /// value. + llvm::Optional> valueSet; +}; +bool fromJSON(const json::Expr &, std::vector &); +bool fromJSON(const json::Expr &, SymbolKindCapabilities &); +SymbolKind adjustKindToCapability(SymbolKind Kind, + SymbolKindBitset &supportedSymbolKinds); + +struct WorkspaceSymbolCapabilities { + /// Capabilities SymbolKind. + llvm::Optional symbolKind; +}; +bool fromJSON(const json::Expr &, WorkspaceSymbolCapabilities &); + +// FIXME: most of the capabilities are missing from this struct. Only the ones +// used by clangd are currently there. +struct WorkspaceClientCapabilities { + /// Capabilities specific to `workspace/symbol`. + llvm::Optional symbol; +}; +bool fromJSON(const json::Expr &, WorkspaceClientCapabilities &); + // FIXME: most of the capabilities are missing from this struct. Only the ones // used by clangd are currently there. struct TextDocumentClientCapabilities { @@ -247,8 +309,7 @@ struct ClientCapabilities { // Workspace specific client capabilities. - // NOTE: not used by clangd at the moment. - // WorkspaceClientCapabilities workspace; + llvm::Optional workspace; // Text document specific client capabilities. TextDocumentClientCapabilities textDocument; @@ -525,6 +586,31 @@ json::Expr toJSON(const Command &C); +/// Represents information about programming constructs like variables, classes, +/// interfaces etc. +struct SymbolInformation { + /// The name of this symbol. + std::string name; + + /// The kind of this symbol. + SymbolKind kind; + + /// The location of this symbol. + Location location; + + /// The name of the symbol containing this symbol. + std::string containerName; +}; +json::Expr toJSON(const SymbolInformation &); +llvm::raw_ostream &operator<<(llvm::raw_ostream &, const SymbolInformation &); + +/// The parameters of a Workspace Symbol Request. +struct WorkspaceSymbolParams { + /// A non-empty query string + std::string query; +}; +bool fromJSON(const json::Expr &, WorkspaceSymbolParams &); + struct ApplyWorkspaceEditParams { WorkspaceEdit edit; }; Index: clangd/Protocol.cpp =================================================================== --- clangd/Protocol.cpp +++ clangd/Protocol.cpp @@ -176,6 +176,63 @@ return true; } +bool fromJSON(const json::Expr &E, SymbolKind &Out) { + if (auto T = E.asInteger()) { + if (*T < static_cast(SymbolKind::File) || + *T > static_cast(SymbolKind::TypeParameter)) + return false; + Out = static_cast(*T); + return true; + } + return false; +} + +bool fromJSON(const json::Expr &E, std::vector &Out) { + if (auto *A = E.asArray()) { + Out.clear(); + for (size_t I = 0; I < A->size(); ++I) { + SymbolKind KindOut; + if (fromJSON((*A)[I], KindOut)) + Out.push_back(KindOut); + } + return true; + } + return false; +} + +bool fromJSON(const json::Expr &Params, SymbolKindCapabilities &R) { + json::ObjectMapper O(Params); + return O && O.map("valueSet", R.valueSet); +} + +SymbolKind adjustKindToCapability(SymbolKind Kind, + SymbolKindBitset &supportedSymbolKinds) { + auto KindVal = static_cast(Kind); + if (KindVal >= SymbolKindMin && KindVal <= supportedSymbolKinds.size() && + supportedSymbolKinds[KindVal]) + return Kind; + + switch (Kind) { + // Provide some fall backs for common kinds that are close enough. + case SymbolKind::Struct: + return SymbolKind::Class; + case SymbolKind::EnumMember: + return SymbolKind::Enum; + default: + return SymbolKind::String; + } +} + +bool fromJSON(const json::Expr &Params, WorkspaceSymbolCapabilities &R) { + json::ObjectMapper O(Params); + return O && O.map("symbolKind", R.symbolKind); +} + +bool fromJSON(const json::Expr &Params, WorkspaceClientCapabilities &R) { + json::ObjectMapper O(Params); + return O && O.map("symbol", R.symbol); +} + bool fromJSON(const json::Expr &Params, TextDocumentClientCapabilities &R) { json::ObjectMapper O(Params); if (!O) @@ -189,6 +246,7 @@ if (!O) return false; O.map("textDocument", R.textDocument); + O.map("workspace", R.workspace); return true; } @@ -351,6 +409,26 @@ return false; // Unrecognized command. } +json::Expr toJSON(const SymbolInformation &P) { + return json::obj{ + {"name", P.name}, + {"kind", static_cast(P.kind)}, + {"location", P.location}, + {"containerName", P.containerName}, + }; +} + +llvm::raw_ostream &operator<<(llvm::raw_ostream &O, + const SymbolInformation &SI) { + O << SI.containerName << "::" << SI.name << " - " << toJSON(SI); + return O; +} + +bool fromJSON(const json::Expr &Params, WorkspaceSymbolParams &R) { + json::ObjectMapper O(Params); + return O && O.map("query", R.query); +} + json::Expr toJSON(const Command &C) { auto Cmd = json::obj{{"title", C.title}, {"command", C.command}}; if (C.workspaceEdit) Index: clangd/ProtocolHandlers.h =================================================================== --- clangd/ProtocolHandlers.h +++ clangd/ProtocolHandlers.h @@ -49,6 +49,7 @@ virtual void onSwitchSourceHeader(TextDocumentIdentifier &Params) = 0; virtual void onFileEvent(DidChangeWatchedFilesParams &Params) = 0; virtual void onCommand(ExecuteCommandParams &Params) = 0; + virtual void onWorkspaceSymbol(WorkspaceSymbolParams &Params) = 0; virtual void onRename(RenameParams &Parames) = 0; virtual void onDocumentHighlight(TextDocumentPositionParams &Params) = 0; virtual void onHover(TextDocumentPositionParams &Params) = 0; Index: clangd/ProtocolHandlers.cpp =================================================================== --- clangd/ProtocolHandlers.cpp +++ clangd/ProtocolHandlers.cpp @@ -72,4 +72,5 @@ &ProtocolCallbacks::onDocumentHighlight); Register("workspace/didChangeConfiguration", &ProtocolCallbacks::onChangeConfiguration); + Register("workspace/symbol", &ProtocolCallbacks::onWorkspaceSymbol); } Index: clangd/SourceCode.h =================================================================== --- clangd/SourceCode.h +++ clangd/SourceCode.h @@ -49,6 +49,11 @@ // Note that clang also uses closed source ranges, which this can't handle! Range halfOpenToRange(const SourceManager &SM, CharSourceRange R); +/// From "a::b::c", return {"a::b::", "c"}. Scope is empty if there's no +/// qualifier. +std::pair +splitQualifiedName(llvm::StringRef QName); + } // namespace clangd } // namespace clang #endif Index: clangd/SourceCode.cpp =================================================================== --- clangd/SourceCode.cpp +++ clangd/SourceCode.cpp @@ -76,5 +76,13 @@ return {Begin, End}; } +std::pair +splitQualifiedName(llvm::StringRef QName) { + size_t Pos = QName.rfind("::"); + if (Pos == llvm::StringRef::npos) + return {StringRef(), QName}; + return {QName.substr(0, Pos + 2), QName.substr(Pos + 2)}; +} + } // namespace clangd } // namespace clang Index: clangd/index/SymbolCollector.cpp =================================================================== --- clangd/index/SymbolCollector.cpp +++ clangd/index/SymbolCollector.cpp @@ -11,6 +11,7 @@ #include "../AST.h" #include "../CodeCompletionStrings.h" #include "../Logger.h" +#include "../SourceCode.h" #include "../URI.h" #include "CanonicalIncludes.h" #include "clang/AST/DeclCXX.h" @@ -89,16 +90,6 @@ return llvm::None; } -// "a::b::c", return {"a::b::", "c"}. Scope is empty if there's no qualifier. -std::pair -splitQualifiedName(llvm::StringRef QName) { - assert(!QName.startswith("::") && "Qualified names should not start with ::"); - size_t Pos = QName.rfind("::"); - if (Pos == llvm::StringRef::npos) - return {StringRef(), QName}; - return {QName.substr(0, Pos + 2), QName.substr(Pos + 2)}; -} - bool shouldFilterDecl(const NamedDecl *ND, ASTContext *ASTCtx, const SymbolCollector::Options &Opts) { using namespace clang::ast_matchers; @@ -321,6 +312,7 @@ Policy.SuppressUnwrittenScope = true; ND.printQualifiedName(OS, Policy); OS.flush(); + assert(!StringRef(QName).startswith("::")); Symbol S; S.ID = std::move(ID); Index: clangd/tool/ClangdMain.cpp =================================================================== --- clangd/tool/ClangdMain.cpp +++ clangd/tool/ClangdMain.cpp @@ -96,9 +96,9 @@ clEnumValN(PCHStorageFlag::Memory, "memory", "store PCHs in memory")), llvm::cl::init(PCHStorageFlag::Disk)); -static llvm::cl::opt LimitCompletionResult( - "completion-limit", - llvm::cl::desc("Limit the number of completion results returned by clangd. " +static llvm::cl::opt LimitResults( + "limit-results", + llvm::cl::desc("Limit the number of results returned by clangd. " "0 means no limit."), llvm::cl::init(100)); @@ -118,11 +118,11 @@ "Mirror all LSP input to the specified file. Useful for debugging."), llvm::cl::init(""), llvm::cl::Hidden); -static llvm::cl::opt EnableIndexBasedCompletion( - "enable-index-based-completion", - llvm::cl::desc( - "Enable index-based global code completion. " - "Clang uses an index built from symbols in opened files"), +static llvm::cl::opt EnableIndex( + "index", + llvm::cl::desc("Enable index-based features such as global code completion " + "and searching for symbols." + "Clang uses an index built from symbols in opened files"), llvm::cl::init(true)); static llvm::cl::opt YamlSymbolFile( @@ -220,9 +220,9 @@ } if (!ResourceDir.empty()) Opts.ResourceDir = ResourceDir; - Opts.BuildDynamicSymbolIndex = EnableIndexBasedCompletion; + Opts.BuildDynamicSymbolIndex = EnableIndex; std::unique_ptr StaticIdx; - if (EnableIndexBasedCompletion && !YamlSymbolFile.empty()) { + if (EnableIndex && !YamlSymbolFile.empty()) { StaticIdx = BuildStaticIndex(YamlSymbolFile); Opts.StaticIndex = StaticIdx.get(); } @@ -230,7 +230,7 @@ clangd::CodeCompleteOptions CCOpts; CCOpts.IncludeIneligibleResults = IncludeIneligibleResults; - CCOpts.Limit = LimitCompletionResult; + CCOpts.Limit = LimitResults; // Initialize and run ClangdLSPServer. ClangdLSPServer LSPServer(Out, CCOpts, CompileCommandsDirPath, Opts); Index: test/clangd/initialize-params-invalid.test =================================================================== --- test/clangd/initialize-params-invalid.test +++ test/clangd/initialize-params-invalid.test @@ -36,7 +36,8 @@ # CHECK-NEXT: "," # CHECK-NEXT: ] # CHECK-NEXT: }, -# CHECK-NEXT: "textDocumentSync": 2 +# CHECK-NEXT: "textDocumentSync": 2, +# CHECK-NEXT: "workspaceSymbolProvider": true # CHECK-NEXT: } # CHECK-NEXT: } --- Index: test/clangd/initialize-params.test =================================================================== --- test/clangd/initialize-params.test +++ test/clangd/initialize-params.test @@ -36,7 +36,8 @@ # CHECK-NEXT: "," # CHECK-NEXT: ] # CHECK-NEXT: }, -# CHECK-NEXT: "textDocumentSync": 2 +# CHECK-NEXT: "textDocumentSync": 2, +# CHECK-NEXT: "workspaceSymbolProvider": true # CHECK-NEXT: } # CHECK-NEXT: } --- Index: test/clangd/symbols.test =================================================================== --- test/clangd/symbols.test +++ test/clangd/symbols.test @@ -0,0 +1,33 @@ +# RUN: clangd -lit-test < %s | FileCheck %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{"workspace":{"symbol":{"symbolKind":{"valueSet": [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]}}}},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":"#include \nvoid foo(); int main() { foo(); }\n"}}} +--- +{"jsonrpc":"2.0","id":1,"method":"workspace/symbol","params":{"query":"std::basic_ostringstream"}} +# CHECK: "id": 1, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": [ +# CHECK-NEXT: { +# CHECK-NEXT: "containerName": "std", +# CHECK-NEXT: "kind": 5, +# CHECK-NEXT: "location": { +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": {{.*}}, +# CHECK-NEXT: "line": {{.*}} +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": {{.*}}, +# CHECK-NEXT: "line": {{.*}} +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "uri": "file://{{.*}}/sstream" +# CHECK-NEXT: }, +# CHECK-NEXT: "name": "basic_ostringstream" +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT:} +--- +{"jsonrpc":"2.0","id":3,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} Index: unittests/clangd/CMakeLists.txt =================================================================== --- unittests/clangd/CMakeLists.txt +++ unittests/clangd/CMakeLists.txt @@ -17,6 +17,7 @@ ContextTests.cpp DraftStoreTests.cpp FileIndexTests.cpp + FindSymbolsTests.cpp FuzzyMatchTests.cpp GlobalCompilationDatabaseTests.cpp HeadersTests.cpp Index: unittests/clangd/FindSymbolsTests.cpp =================================================================== --- unittests/clangd/FindSymbolsTests.cpp +++ unittests/clangd/FindSymbolsTests.cpp @@ -0,0 +1,247 @@ +//===-- FindSymbolsTests.cpp -------------------------*- C++ -*------------===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// +#include "Annotations.h" +#include "ClangdServer.h" +#include "FindSymbols.h" +#include "SyncAPI.h" +#include "TestFS.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { + +namespace { + +using ::testing::AllOf; +using ::testing::AnyOf; +using ::testing::ElementsAre; +using ::testing::IsEmpty; +using ::testing::UnorderedElementsAre; + +class IgnoreDiagnostics : public DiagnosticsConsumer { + void onDiagnosticsReady(PathRef File, + std::vector Diagnostics) override {} +}; + +// GMock helpers for matching SymbolInfos items. +MATCHER_P(Named, Name, "") { return arg.name == Name; } +MATCHER_P(InContainer, ContainerName, "") { + return arg.containerName == ContainerName; +} +MATCHER_P(WithKind, Kind, "") { return arg.kind == Kind; } + +ClangdServer::Options optsForTests() { + auto ServerOpts = ClangdServer::optsForTest(); + ServerOpts.BuildDynamicSymbolIndex = true; + return ServerOpts; +} + +class WorkspaceSymbolsTest : public ::testing::Test { +public: + WorkspaceSymbolsTest() + : Server(CDB, FSProvider, DiagConsumer, optsForTests()) {} + +protected: + MockFSProvider FSProvider; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + ClangdServer Server; + int Limit; + + std::vector getSymbols(StringRef Query) { + EXPECT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for preamble"; + auto SymbolInfos = runWorkspaceSymbols(Server, Query, Limit); + EXPECT_TRUE(bool(SymbolInfos)) << "workspaceSymbols returned an error"; + return *SymbolInfos; + } + + void addFile(StringRef FileName, StringRef Contents) { + auto Path = testPath(FileName); + FSProvider.Files[Path] = Contents; + Server.addDocument(Path, Contents); + } +}; + +} // namespace + +TEST_F(WorkspaceSymbolsTest, NoMacro) { + addFile("foo.cpp", R"cpp( + #define MACRO X + )cpp"); + + // Macros are not in the index. + EXPECT_THAT(getSymbols("macro"), IsEmpty()); +} + +TEST_F(WorkspaceSymbolsTest, NoLocals) { + addFile("foo.cpp", R"cpp( + void test(int FirstParam, int SecondParam) { + struct LocalClass {}; + int local_var; + })cpp"); + EXPECT_THAT(getSymbols("l"), IsEmpty()); + EXPECT_THAT(getSymbols("p"), IsEmpty()); +} + +TEST_F(WorkspaceSymbolsTest, Globals) { + addFile("foo.h", R"cpp( + int global_var; + + int global_func(); + + struct GlobalStruct {};)cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT(getSymbols("global"), + UnorderedElementsAre(AllOf(Named("GlobalStruct"), InContainer(""), + WithKind(SymbolKind::Struct)), + AllOf(Named("global_func"), InContainer(""), + WithKind(SymbolKind::Function)), + AllOf(Named("global_var"), InContainer(""), + WithKind(SymbolKind::Variable)))); +} + +TEST_F(WorkspaceSymbolsTest, Unnamed) { + addFile("foo.h", R"cpp( + struct { + int InUnnamed; + } UnnamedStruct;)cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT(getSymbols("UnnamedStruct"), + ElementsAre(AllOf(Named("UnnamedStruct"), + WithKind(SymbolKind::Variable)))); + EXPECT_THAT(getSymbols("InUnnamed"), IsEmpty()); +} + +TEST_F(WorkspaceSymbolsTest, InMainFile) { + addFile("foo.cpp", R"cpp( + int test() { + } + )cpp"); + EXPECT_THAT(getSymbols("test"), IsEmpty()); +} + +TEST_F(WorkspaceSymbolsTest, Namespaces) { + addFile("foo.h", R"cpp( + namespace ans1 { + int ai1; + namespace ans2 { + int ai2; + } + } + )cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT( + getSymbols("a"), + UnorderedElementsAre(AllOf(Named("ans1"), InContainer("")), + AllOf(Named("ai1"), InContainer("ans1")), + AllOf(Named("ans2"), InContainer("ans1")), + AllOf(Named("ai2"), InContainer("ans1::ans2")))); + EXPECT_THAT(getSymbols("::"), + ElementsAre(AllOf(Named("ans1"), InContainer("")))); + EXPECT_THAT(getSymbols("::a"), + ElementsAre(AllOf(Named("ans1"), InContainer("")))); + EXPECT_THAT(getSymbols("ans1::"), + UnorderedElementsAre(AllOf(Named("ai1"), InContainer("ans1")), + AllOf(Named("ans2"), InContainer("ans1")))); + EXPECT_THAT(getSymbols("::ans1"), + ElementsAre(AllOf(Named("ans1"), InContainer("")))); + EXPECT_THAT(getSymbols("::ans1::"), + UnorderedElementsAre(AllOf(Named("ai1"), InContainer("ans1")), + AllOf(Named("ans2"), InContainer("ans1")))); + EXPECT_THAT(getSymbols("::ans1::ans2"), + ElementsAre(AllOf(Named("ans2"), InContainer("ans1")))); + EXPECT_THAT(getSymbols("::ans1::ans2::"), + ElementsAre(AllOf(Named("ai2"), InContainer("ans1::ans2")))); +} + +TEST_F(WorkspaceSymbolsTest, AnonymousNamespace) { + addFile("foo.h", R"cpp( + namespace { + void test() {} + } + )cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT(getSymbols("test"), IsEmpty()); +} + +TEST_F(WorkspaceSymbolsTest, MultiFile) { + addFile("foo.h", R"cpp( + int foo() { + } + )cpp"); + addFile("foo2.h", R"cpp( + int foo2() { + } + )cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + #include "foo2.h" + )cpp"); + EXPECT_THAT(getSymbols("foo"), + UnorderedElementsAre(AllOf(Named("foo"), InContainer("")), + AllOf(Named("foo2"), InContainer("")))); +} + +TEST_F(WorkspaceSymbolsTest, GlobalNamespaceQueries) { + addFile("foo.h", R"cpp( + int foo() { + } + class Foo { + int a; + }; + namespace ns { + int foo2() { + } + } + )cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT( + getSymbols("::"), + UnorderedElementsAre( + AllOf(Named("Foo"), InContainer(""), WithKind(SymbolKind::Class)), + AllOf(Named("foo"), InContainer(""), WithKind(SymbolKind::Function)), + AllOf(Named("ns"), InContainer(""), + WithKind(SymbolKind::Namespace)))); + EXPECT_THAT(getSymbols(":"), IsEmpty()); + EXPECT_THAT(getSymbols(""), IsEmpty()); +} + +TEST_F(WorkspaceSymbolsTest, WithLimit) { + addFile("foo.h", R"cpp( + int foo; + int foo2; + )cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT(getSymbols("foo"), + ElementsAre(AllOf(Named("foo"), InContainer(""), + WithKind(SymbolKind::Variable)), + AllOf(Named("foo2"), InContainer(""), + WithKind(SymbolKind::Variable)))); + + Limit = 1; + EXPECT_THAT(getSymbols("foo"), + ElementsAre(AnyOf((Named("foo"), InContainer("")), + AllOf(Named("foo2"), InContainer(""))))); +} + +} // namespace clangd +} // namespace clang Index: unittests/clangd/SyncAPI.h =================================================================== --- unittests/clangd/SyncAPI.h +++ unittests/clangd/SyncAPI.h @@ -41,6 +41,9 @@ std::string runDumpAST(ClangdServer &Server, PathRef File); +llvm::Expected> +runWorkspaceSymbols(ClangdServer &Server, StringRef Query, int Limit); + } // namespace clangd } // namespace clang Index: unittests/clangd/SyncAPI.cpp =================================================================== --- unittests/clangd/SyncAPI.cpp +++ unittests/clangd/SyncAPI.cpp @@ -110,5 +110,12 @@ return std::move(*Result); } +llvm::Expected> +runWorkspaceSymbols(ClangdServer &Server, StringRef Query, int Limit) { + llvm::Optional>> Result; + Server.workspaceSymbols(Query, Limit, capture(Result)); + return std::move(*Result); +} + } // namespace clangd } // namespace clang