Index: clangd/CMakeLists.txt =================================================================== --- clangd/CMakeLists.txt +++ clangd/CMakeLists.txt @@ -27,6 +27,7 @@ Trace.cpp TUScheduler.cpp URI.cpp + WorkspaceSymbols.cpp XRefs.cpp index/CanonicalIncludes.cpp index/FileIndex.cpp Index: clangd/ClangdLSPServer.h =================================================================== --- clangd/ClangdLSPServer.h +++ clangd/ClangdLSPServer.h @@ -16,6 +16,7 @@ #include "Path.h" #include "Protocol.h" #include "ProtocolHandlers.h" +#include "WorkspaceSymbols.h" #include "clang/Tooling/Core/Replacement.h" #include "llvm/ADT/Optional.h" @@ -33,6 +34,7 @@ /// loaded only from \p CompileCommandsDir. Otherwise, clangd will look /// for compile_commands.json in all parent directories of each file. ClangdLSPServer(JSONOutput &Out, const clangd::CodeCompleteOptions &CCOpts, + const clangd::WorkspaceSymbolOptions &WorkspaceSymOpts, llvm::Optional CompileCommandsDir, const ClangdServer::Options &Opts); @@ -69,6 +71,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; @@ -89,6 +92,8 @@ /// Language Server client. /// It's used to break out of the LSP parsing loop. bool IsDone = false; + /// Indicates whether or not the index is available. + bool SymbolIndexAvailable = false; std::mutex FixItsMutex; typedef std::map, LSPDiagnosticCompare> @@ -102,6 +107,8 @@ RealFileSystemProvider FSProvider; /// Options used for code completion clangd::CodeCompleteOptions CCOpts; + /// Options used for workspace symbol. + clangd::WorkspaceSymbolOptions WorkspaceSymbolOpts; // Store of the current versions of the open documents. DraftStore DraftMgr; Index: clangd/ClangdLSPServer.cpp =================================================================== --- clangd/ClangdLSPServer.cpp +++ clangd/ClangdLSPServer.cpp @@ -96,6 +96,10 @@ CCOpts.EnableSnippets = Params.capabilities.textDocument.completion.completionItem.snippetSupport; + if (Params.capabilities.workspace && Params.capabilities.workspace->symbol && + Params.capabilities.workspace->symbol->symbolKind) + WorkspaceSymbolOpts.supportedSymbolKinds = + Params.capabilities.workspace->symbol->symbolKind->valueSet; reply(json::obj{ {{"capabilities", @@ -122,6 +126,7 @@ {"documentHighlightProvider", true}, {"hoverProvider", true}, {"renameProvider", true}, + {"workspaceSymbolProvider", SymbolIndexAvailable}, {"executeCommandProvider", json::obj{ {"commands", @@ -240,6 +245,17 @@ } } +void ClangdLSPServer::onWorkspaceSymbol(WorkspaceSymbolParams &Params) { + Server.onWorkspaceSymbol( + Params.query, WorkspaceSymbolOpts, + [](llvm::Expected> Items) { + if (!Items) + return replyError(ErrorCode::InvalidParams, + llvm::toString(Items.takeError())); + reply(json::ary(*Items)); + }); +} + void ClangdLSPServer::onRename(RenameParams &Params) { Path File = Params.textDocument.uri.file(); llvm::Optional Code = DraftMgr.getDraft(File); @@ -412,11 +428,13 @@ } } -ClangdLSPServer::ClangdLSPServer(JSONOutput &Out, - const clangd::CodeCompleteOptions &CCOpts, - llvm::Optional CompileCommandsDir, - const ClangdServer::Options &Opts) - : Out(Out), CDB(std::move(CompileCommandsDir)), CCOpts(CCOpts), +ClangdLSPServer::ClangdLSPServer( + JSONOutput &Out, const clangd::CodeCompleteOptions &CCOpts, + const clangd::WorkspaceSymbolOptions &WorkspaceSymOpts, + llvm::Optional CompileCommandsDir, const ClangdServer::Options &Opts) + : Out(Out), SymbolIndexAvailable(Opts.BuildDynamicSymbolIndex), + CDB(std::move(CompileCommandsDir)), CCOpts(CCOpts), + WorkspaceSymbolOpts(WorkspaceSymOpts), Server(CDB, FSProvider, /*DiagConsumer=*/*this, Opts) {} bool ClangdLSPServer::run(std::istream &In, JSONStreamStyle InputStyle) { Index: clangd/ClangdServer.h =================================================================== --- clangd/ClangdServer.h +++ clangd/ClangdServer.h @@ -33,6 +33,7 @@ class PCHContainerOperations; namespace clangd { +struct WorkspaceSymbolOptions; class DiagnosticsConsumer { public: @@ -157,6 +158,11 @@ /// Get code hover for a given position. void findHover(PathRef File, Position Pos, Callback CB); + void onWorkspaceSymbol( + StringRef Query, const clangd::WorkspaceSymbolOptions &Opts, + UniqueFunction>)> + Callback); + /// 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 @@ -11,6 +11,7 @@ #include "CodeComplete.h" #include "Headers.h" #include "SourceCode.h" +#include "WorkspaceSymbols.h" #include "XRefs.h" #include "index/Merge.h" #include "clang/Format/Format.h" @@ -492,6 +493,14 @@ // invalidating other caches. } +void ClangdServer::onWorkspaceSymbol( + StringRef Query, const clangd::WorkspaceSymbolOptions &Opts, + UniqueFunction>)> + Callback) { + Callback(clangd::getWorkspaceSymbols(Query, Opts, Index, + FSProvider.getFileSystem())); +} + std::vector> ClangdServer::getUsedBytesPerFile() const { return WorkScheduler.getUsedBytesPerFile(); Index: clangd/CodeComplete.cpp =================================================================== --- clangd/CodeComplete.cpp +++ clangd/CodeComplete.cpp @@ -919,6 +919,7 @@ if (Opts.Limit) Req.MaxCandidateCount = Opts.Limit; Req.Query = Filter->pattern(); + Req.CompletionMatchesOnly = true; Req.Scopes = getQueryScopes(Recorder->CCContext, Recorder->CCSema->getSourceManager()); log(llvm::formatv("Code complete: fuzzyFind(\"{0}\", scopes=[{1}])", Index: clangd/Protocol.h =================================================================== --- clangd/Protocol.h +++ clangd/Protocol.h @@ -223,6 +223,59 @@ }; 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 +}; +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 &, SymbolKindCapabilities &); + +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 { @@ -233,8 +286,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; @@ -505,6 +557,30 @@ 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 &); + +/// 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,41 @@ 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 &Params, SymbolKindCapabilities &R) { + json::ObjectMapper O(Params); + if (!O) + return false; + O.map("valueSet", R.valueSet); + return true; +} + +bool fromJSON(const json::Expr &Params, WorkspaceSymbolCapabilities &R) { + json::ObjectMapper O(Params); + if (!O) + return false; + O.map("symbolKind", R.symbolKind); + return true; +} + +bool fromJSON(const json::Expr &Params, WorkspaceClientCapabilities &R) { + json::ObjectMapper O(Params); + if (!O) + return false; + O.map("symbol", R.symbol); + return true; +} + bool fromJSON(const json::Expr &Params, TextDocumentClientCapabilities &R) { json::ObjectMapper O(Params); if (!O) @@ -350,6 +385,20 @@ 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}, + }; +} + +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,22 @@ // Note that clang also uses closed source ranges, which this can't handle! Range halfOpenToRange(const SourceManager &SM, CharSourceRange R); +/// Turn a SourceRange into a Location. +llvm::Optional +sourceRangeToLocation(const SourceManager &SourceMgr, + const SourceRange &ValSourceRange); + +/// Turn a pair of offsets into a Location. +llvm::Optional offsetRangeToLocation(SourceManager &SourceMgr, + StringRef File, + size_t OffsetStart, + size_t OffsetEnd); + +/// 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 @@ -7,10 +7,12 @@ // //===----------------------------------------------------------------------===// #include "SourceCode.h" +#include "Logger.h" #include "clang/Basic/SourceManager.h" #include "llvm/Support/Errc.h" #include "llvm/Support/Error.h" +#include "llvm/Support/Path.h" namespace clang { namespace clangd { @@ -76,5 +78,62 @@ return {Begin, End}; } +llvm::Optional +sourceRangeToLocation(const SourceManager &SourceMgr, + const SourceRange &ValSourceRange) { + SourceLocation LocStart = ValSourceRange.getBegin(); + const FileEntry *FE = + SourceMgr.getFileEntryForID(SourceMgr.getFileID(LocStart)); + if (!FE) + return llvm::None; + SourceLocation LocEnd = ValSourceRange.getEnd(); + Position Begin = sourceLocToPosition(SourceMgr, LocStart); + Position End = sourceLocToPosition(SourceMgr, LocEnd); + Range R = {Begin, End}; + Location L; + + SmallString<64> FilePath = FE->tryGetRealPathName(); + if (FilePath.empty()) + FilePath = FE->getName(); + if (!llvm::sys::path::is_absolute(FilePath)) { + if (!SourceMgr.getFileManager().makeAbsolutePath(FilePath)) { + log("Could not turn relative path to absolute: " + FilePath); + return llvm::None; + } + } + + L.uri = URIForFile(FilePath.str()); + L.range = R; + return L; +} + +llvm::Optional offsetRangeToLocation(SourceManager &SourceMgr, + StringRef File, + size_t OffsetStart, + size_t OffsetEnd) { + const FileEntry *FE = SourceMgr.getFileManager().getFile(File); + if (!FE) + return llvm::None; + + FileID FID = SourceMgr.getOrCreateFileID(FE, SrcMgr::C_User); + + SourceLocation LocStart = SourceMgr.getComposedLoc(FID, OffsetStart); + SourceLocation LocEnd = SourceMgr.getComposedLoc(FID, OffsetEnd); + if (LocStart.isInvalid() || LocEnd.isInvalid()) + return llvm::None; + + return sourceRangeToLocation(SourceMgr, {LocStart, LocEnd}); +} + +/// From "a::b::c", return {"a::b::", "c"}. Scope is empty if there's no +/// qualifier. +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/WorkspaceSymbols.h =================================================================== --- /dev/null +++ clangd/WorkspaceSymbols.h @@ -0,0 +1,42 @@ +//===--- WorkspaceSymbols.h --------------------------------------*- C++-*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// +// +// Workspace symbols provides a list of symbols in the workspace matching a +// string query. +// +//===----------------------------------------------------------------------===// +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_WORKSPACESYMBOL_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_WORKSPACESYMBOL_H + +#include "Protocol.h" +#include "clang/Basic/VirtualFileSystem.h" +#include "llvm/ADT/IntrusiveRefCntPtr.h" +#include "llvm/ADT/StringRef.h" + +namespace clang { +namespace clangd { +class SymbolIndex; + +struct WorkspaceSymbolOptions { + /// Limit the number of results returned (0 means no limit). + size_t Limit = 0; + + /// Symbol kinds supported by the requester. + llvm::Optional> supportedSymbolKinds; +}; + +llvm::Expected> +getWorkspaceSymbols(llvm::StringRef Query, const WorkspaceSymbolOptions &Opts, + const SymbolIndex *const Index, + llvm::IntrusiveRefCntPtr VFS); + +} // namespace clangd +} // namespace clang + +#endif Index: clangd/WorkspaceSymbols.cpp =================================================================== --- /dev/null +++ clangd/WorkspaceSymbols.cpp @@ -0,0 +1,171 @@ +//===--- WorkspaceSymbols.cpp ------------------------------------*- C++-*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// +#include "WorkspaceSymbols.h" + +#include "Logger.h" +#include "SourceCode.h" +#include "index/Index.h" +#include "clang/Basic/SourceManager.h" +#include "clang/Frontend/CompilerInstance.h" +#include "clang/Index/IndexSymbol.h" +#include "llvm/Support/FormatVariadic.h" + +namespace clang { +namespace clangd { + +namespace { + +SymbolKind adjustKindToCapability( + SymbolKind Kind, + const llvm::Optional> &supportedSymbolKinds) { + // All clients should support those. + if (Kind >= SymbolKind::File && Kind <= SymbolKind::Array) + return Kind; + + if (supportedSymbolKinds && + std::find(supportedSymbolKinds->begin(), supportedSymbolKinds->end(), + Kind) != supportedSymbolKinds->end()) + return Kind; + + // Provide some fall backs for common kinds that are close enough. + if (Kind == SymbolKind::Struct) + return SymbolKind::Class; + if (Kind == SymbolKind::EnumMember) + return SymbolKind::Enum; + + if (!supportedSymbolKinds) { + // Provide some sensible default when all fails. + return SymbolKind::Variable; + } + return Kind; +} + +// 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, const WorkspaceSymbolOptions &Opts, + const SymbolIndex *const Index, + IntrusiveRefCntPtr VFS) { + std::vector Result; + if (Query.empty() || !Index) + return Result; + + auto Names = splitQualifiedName(Query); + + // We'll use a temporary SourceManager to do the offset -> line/col mapping. + // We don't have any context from which this query was launched (working dir), + // so use defaults here. + FileSystemOptions FileOpts; + FileManager FM(FileOpts, VFS); + IntrusiveRefCntPtr DE( + CompilerInstance::createDiagnostics(new DiagnosticOptions)); + SourceManager TempSM(*DE, FM); + + // Global scope is represented by "" in FuzzyFind. + if (Names.first.startswith("::")) + Names.first = Names.first.substr(2); + + FuzzyFindRequest Req; + Req.Query = Names.second; + if (!Names.first.empty()) + Req.Scopes.push_back(Names.first); + if (Opts.Limit) + Req.MaxCandidateCount = Opts.Limit; + Index->fuzzyFind(Req, [&TempSM, &Result, &Opts](const Symbol &Sym) { + 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; + } + 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; + } + auto L = + offsetRangeToLocation(TempSM, *Path, CD.StartOffset, CD.StartOffset); + if (L) { + SymbolKind SK = + adjustKindToCapability(indexSymbolKindToSymbolKind(Sym.SymInfo.Kind), + Opts.supportedSymbolKinds); + Result.push_back({Sym.Name, SK, *L, Sym.Scope}); + } + }); + std::sort(Result.begin(), Result.end(), + [](const SymbolInformation &A, const SymbolInformation &B) { + return A.name < B.name; + }); + return Result; +} + +} // namespace clangd +} // namespace clang Index: clangd/XRefs.cpp =================================================================== --- clangd/XRefs.cpp +++ clangd/XRefs.cpp @@ -143,38 +143,6 @@ } }; -llvm::Optional -makeLocation(ParsedAST &AST, const SourceRange &ValSourceRange) { - const SourceManager &SourceMgr = AST.getASTContext().getSourceManager(); - const LangOptions &LangOpts = AST.getASTContext().getLangOpts(); - SourceLocation LocStart = ValSourceRange.getBegin(); - - const FileEntry *F = - SourceMgr.getFileEntryForID(SourceMgr.getFileID(LocStart)); - if (!F) - return llvm::None; - SourceLocation LocEnd = Lexer::getLocForEndOfToken(ValSourceRange.getEnd(), 0, - SourceMgr, LangOpts); - Position Begin = sourceLocToPosition(SourceMgr, LocStart); - Position End = sourceLocToPosition(SourceMgr, LocEnd); - Range R = {Begin, End}; - Location L; - - SmallString<64> FilePath = F->tryGetRealPathName(); - if (FilePath.empty()) - FilePath = F->getName(); - if (!llvm::sys::path::is_absolute(FilePath)) { - if (!SourceMgr.getFileManager().makeAbsolutePath(FilePath)) { - log("Could not turn relative path to absolute: " + FilePath); - return llvm::None; - } - } - - L.uri = URIForFile(FilePath.str()); - L.range = R; - return L; -} - } // namespace std::vector findDefinitions(ParsedAST &AST, Position Pos) { @@ -182,6 +150,7 @@ const FileEntry *FE = SourceMgr.getFileEntryForID(SourceMgr.getMainFileID()); if (!FE) return {}; + const LangOptions &LangOpts = AST.getASTContext().getLangOpts(); SourceLocation SourceLocationBeg = getBeginningOfIdentifier(AST, Pos, FE); @@ -213,14 +182,16 @@ for (auto D : Decls) { auto Loc = findNameLoc(D); - auto L = makeLocation(AST, SourceRange(Loc, Loc)); + auto EndLoc = Lexer::getLocForEndOfToken(Loc, 0, SourceMgr, LangOpts); + auto L = sourceRangeToLocation(SourceMgr, SourceRange(Loc, EndLoc)); if (L) Result.push_back(*L); } for (auto Item : MacroInfos) { auto Loc = Item.Info->getDefinitionLoc(); - auto L = makeLocation(AST, SourceRange(Loc, Loc)); + auto EndLoc = Lexer::getLocForEndOfToken(Loc, 0, SourceMgr, LangOpts); + auto L = sourceRangeToLocation(SourceMgr, SourceRange(Loc, EndLoc)); if (L) Result.push_back(*L); } Index: clangd/index/Index.h =================================================================== --- clangd/index/Index.h +++ clangd/index/Index.h @@ -139,8 +139,11 @@ // file. This number is only meaningful if aggregated in an index. unsigned References = 0; + // Whether or not the symbol should be considered for completion. + bool ForCompletion = false; + /// A brief description of the symbol that can be displayed in the completion - /// candidate list. For example, "Foo(X x, Y y) const" is a labal for a + /// candidate list. For example, "Foo(X x, Y y) const" is a label for a /// function. llvm::StringRef CompletionLabel; /// The piece of text that the user is expected to type to match the @@ -250,6 +253,8 @@ /// \brief The number of top candidates to return. The index may choose to /// return more than this, e.g. if it doesn't know which candidates are best. size_t MaxCandidateCount = UINT_MAX; + /// A flag to restrict the results to completion matches. + bool CompletionMatchesOnly = false; }; struct LookupRequest { Index: clangd/index/MemIndex.cpp =================================================================== --- clangd/index/MemIndex.cpp +++ clangd/index/MemIndex.cpp @@ -45,6 +45,8 @@ // Exact match against all possible scopes. if (!Req.Scopes.empty() && !llvm::is_contained(Req.Scopes, Sym->Scope)) continue; + if (Req.CompletionMatchesOnly && !Sym->ForCompletion) + continue; if (auto Score = Filter.match(Sym->Name)) { Top.emplace(-*Score, Sym); 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" @@ -80,25 +81,8 @@ 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; - if (ND->isImplicit()) - return true; - // Skip anonymous declarations, e.g (anonymous enum/class/struct). - if (ND->getDeclName().isEmpty()) - return true; - // FIXME: figure out a way to handle internal linkage symbols (e.g. static // variables, function) defined in the .cc files. Also we skip the symbols // in anonymous namespace as the qualifier names of these symbols are like @@ -110,27 +94,41 @@ if (ND->isInAnonymousNamespace()) return true; + using namespace clang::ast_matchers; + if (ND->isImplicit()) + return true; + // Skip anonymous declarations, e.g (anonymous enum/class/struct). + if (ND->getDeclName().isEmpty()) + return true; + + // Don't index template specializations. + auto IsSpecialization = + anyOf(functionDecl(isExplicitTemplateSpecialization()), + cxxRecordDecl(isExplicitTemplateSpecialization()), + varDecl(isExplicitTemplateSpecialization())); + if (!match(decl(IsSpecialization), *ND, *ASTCtx).empty()) + return true; + + return false; +} + +bool isForCompletion(const NamedDecl *ND, ASTContext *ASTCtx) { + using namespace clang::ast_matchers; // We only want: // * symbols in namespaces or translation unit scopes (e.g. no class // members) // * enum constants in unscoped enum decl (e.g. "red" in "enum {red};") auto InTopLevelScope = hasDeclContext( anyOf(namespaceDecl(), translationUnitDecl(), linkageSpecDecl())); - // Don't index template specializations. - auto IsSpecialization = - anyOf(functionDecl(isExplicitTemplateSpecialization()), - cxxRecordDecl(isExplicitTemplateSpecialization()), - varDecl(isExplicitTemplateSpecialization())); if (match(decl(allOf(unless(isExpansionInMainFile()), anyOf(InTopLevelScope, hasDeclContext(enumDecl(InTopLevelScope, - unless(isScoped())))), - unless(IsSpecialization))), + unless(isScoped())))))), *ND, *ASTCtx) .empty()) - return true; + return false; - return false; + return true; } // We only collect #include paths for symbols that are suitable for global code @@ -299,6 +297,8 @@ Symbol S; S.ID = std::move(ID); + assert(!StringRef(QName).startswith("::") && + "Qualified names should not start with '::' here."); std::tie(S.Scope, S.Name) = splitQualifiedName(QName); S.SymInfo = index::getSymbolInfo(&ND); std::string FileURI; @@ -314,18 +314,8 @@ *ASTCtx, *PP, CodeCompletionContext::CCC_Name, *CompletionAllocator, *CompletionTUInfo, /*IncludeBriefComments*/ true); - std::string Label; - std::string SnippetInsertText; - std::string IgnoredLabel; - std::string PlainInsertText; - getLabelAndInsertText(*CCS, &Label, &SnippetInsertText, - /*EnableSnippets=*/true); - getLabelAndInsertText(*CCS, &IgnoredLabel, &PlainInsertText, - /*EnableSnippets=*/false); - std::string FilterText = getFilterText(*CCS); - std::string Documentation = getDocumentation(*CCS); - std::string CompletionDetail = getDetail(*CCS); + std::string Documentation = getDocumentation(*CCS); std::string Include; if (Opts.CollectIncludePath && shouldCollectIncludePath(S.SymInfo.Kind)) { // Use the expansion location to get the #include header since this is @@ -334,14 +324,29 @@ QName, SM, SM.getExpansionLoc(ND.getLocation()), Opts)) Include = std::move(*Header); } - S.CompletionFilterText = FilterText; - S.CompletionLabel = Label; - S.CompletionPlainInsertText = PlainInsertText; - S.CompletionSnippetInsertText = SnippetInsertText; + Symbol::Details Detail; Detail.Documentation = Documentation; - Detail.CompletionDetail = CompletionDetail; Detail.IncludeHeader = Include; + + std::string Label, SnippetInsertText, IgnoredLabel, PlainInsertText, + CompletionDetail, FilterText; + if (isForCompletion(&ND, ASTCtx)) { + S.ForCompletion = true; + getLabelAndInsertText(*CCS, &Label, &SnippetInsertText, + /*EnableSnippets=*/true); + getLabelAndInsertText(*CCS, &IgnoredLabel, &PlainInsertText, + /*EnableSnippets=*/false); + CompletionDetail = getDetail(*CCS); + FilterText = getFilterText(*CCS); + + Detail.CompletionDetail = CompletionDetail; + S.CompletionFilterText = FilterText; + S.CompletionLabel = Label; + S.CompletionPlainInsertText = PlainInsertText; + S.CompletionSnippetInsertText = SnippetInsertText; + } + S.Detail = &Detail; Symbols.insert(S); Index: clangd/index/SymbolYAML.cpp =================================================================== --- clangd/index/SymbolYAML.cpp +++ clangd/index/SymbolYAML.cpp @@ -101,6 +101,7 @@ SymbolLocation()); IO.mapOptional("Definition", Sym.Definition, SymbolLocation()); IO.mapOptional("References", Sym.References, 0u); + IO.mapOptional("ForCompletion", Sym.ForCompletion, false); IO.mapRequired("CompletionLabel", Sym.CompletionLabel); IO.mapRequired("CompletionFilterText", Sym.CompletionFilterText); IO.mapRequired("CompletionPlainInsertText", Sym.CompletionPlainInsertText); Index: clangd/tool/ClangdMain.cpp =================================================================== --- clangd/tool/ClangdMain.cpp +++ clangd/tool/ClangdMain.cpp @@ -102,6 +102,13 @@ "0 means no limit."), llvm::cl::init(100)); +static llvm::cl::opt LimitWorkspaceSymbolResult( + "workspace-symbol-limit", + llvm::cl::desc( + "Limit the number of workspace symbol results returned by clangd. " + "0 means no limit."), + llvm::cl::init(100)); + static llvm::cl::opt RunSynchronously( "run-synchronously", llvm::cl::desc("Parse on main thread. If set, -j is ignored"), @@ -118,11 +125,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 EnableIndexBasedFeatures( + "enable-index-based-features", + 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 +227,9 @@ } if (!ResourceDir.empty()) Opts.ResourceDir = ResourceDir; - Opts.BuildDynamicSymbolIndex = EnableIndexBasedCompletion; + Opts.BuildDynamicSymbolIndex = EnableIndexBasedFeatures; std::unique_ptr StaticIdx; - if (EnableIndexBasedCompletion && !YamlSymbolFile.empty()) { + if (EnableIndexBasedFeatures && !YamlSymbolFile.empty()) { StaticIdx = BuildStaticIndex(YamlSymbolFile); Opts.StaticIndex = StaticIdx.get(); } @@ -232,8 +239,11 @@ CCOpts.IncludeIneligibleResults = IncludeIneligibleResults; CCOpts.Limit = LimitCompletionResult; + clangd::WorkspaceSymbolOptions WorkspaceSymOpts; + WorkspaceSymOpts.Limit = LimitWorkspaceSymbolResult; // Initialize and run ClangdLSPServer. - ClangdLSPServer LSPServer(Out, CCOpts, CompileCommandsDirPath, Opts); + ClangdLSPServer LSPServer(Out, CCOpts, WorkspaceSymOpts, + CompileCommandsDirPath, 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/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": 1 +# CHECK-NEXT: "textDocumentSync": 1, +# 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": 1 +# CHECK-NEXT: "textDocumentSync": 1, +# CHECK-NEXT: "workspaceSymbolProvider": true # CHECK-NEXT: } # CHECK-NEXT: } --- Index: unittests/clangd/CMakeLists.txt =================================================================== --- unittests/clangd/CMakeLists.txt +++ unittests/clangd/CMakeLists.txt @@ -28,6 +28,7 @@ TraceTests.cpp TUSchedulerTests.cpp URITests.cpp + WorkspaceSymbolsTests.cpp XRefsTests.cpp ) Index: unittests/clangd/CodeCompleteTests.cpp =================================================================== --- unittests/clangd/CodeCompleteTests.cpp +++ unittests/clangd/CodeCompleteTests.cpp @@ -57,6 +57,7 @@ using ::testing::Each; using ::testing::ElementsAre; using ::testing::Field; +using ::testing::IsEmpty; using ::testing::Not; using ::testing::UnorderedElementsAre; @@ -156,6 +157,7 @@ } USR += Regex("^.*$").sub(USRFormat, Sym.Name); // e.g. func -> @F@func# Sym.ID = SymbolID(USR); + Sym.ForCompletion = true; Sym.CompletionPlainInsertText = Sym.Name; Sym.CompletionSnippetInsertText = Sym.Name; Sym.CompletionLabel = Sym.Name; @@ -645,6 +647,109 @@ EXPECT_THAT(Results.items, Not(Contains(Labeled("param_in_bar")))); } +TEST(CompletionTest, Enums) { + EXPECT_THAT(completions(R"cpp( + enum class Color2 { + Yellow + }; + void foo() { + Color2::^ + })cpp") + .items, + Has("Yellow", CompletionItemKind::Value)); + EXPECT_THAT(completions(R"cpp( + enum { + Red + }; + void foo() { + Re^ + })cpp") + .items, + Has("Red", CompletionItemKind::Value)); + EXPECT_THAT(completions(R"cpp( + enum Color { + Green + }; + void foo() { + Gr^ + })cpp") + .items, + Has("Green", CompletionItemKind::Value)); + EXPECT_THAT(completions(R"cpp( + namespace ns { + enum { + Black + }; + } + void foo() { + ns::B^ + })cpp") + .items, + Has("Black", CompletionItemKind::Value)); + EXPECT_THAT(completions(R"cpp( + void foo() { + ns::B^ + })cpp") + .items, + IsEmpty()); +} + +TEST(CompletionTest, AnonymousNamespace) { + + MockFSProvider FS; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + auto Opts = ClangdServer::optsForTest(); + Opts.BuildDynamicSymbolIndex = true; + ClangdServer Server(CDB, FS, DiagConsumer, Opts); + auto File = testPath("bar.cpp"); + Server.addDocument(File, R"( + namespace { + void inAnymous() { + } + } // namespace + )"); + + File = testPath("bar2.cpp"); + Annotations Test(R"( + void bar() { + inAnym^ + } + )"); + + Server.addDocument(File, Test.code()); + ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for preamble"; + auto Results = cantFail(runCodeComplete(Server, File, Test.point(), {})); + EXPECT_THAT(Results.items, IsEmpty()); +} + +TEST(CompletionTest, InMainFile) { + + MockFSProvider FS; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + auto Opts = ClangdServer::optsForTest(); + Opts.BuildDynamicSymbolIndex = true; + ClangdServer Server(CDB, FS, DiagConsumer, Opts); + auto File = testPath("main.cpp"); + Server.addDocument(File, R"( + void funcInMain() { + } + )"); + + File = testPath("bar2.cpp"); + Annotations Test(R"( + void bar() { + funcInMa^ + } + )"); + + Server.addDocument(File, Test.code()); + ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for preamble"; + auto Results = cantFail(runCodeComplete(Server, File, Test.point(), {})); + EXPECT_THAT(Results.items, IsEmpty()); +} + SignatureHelp signatures(StringRef Text) { MockFSProvider FS; MockCompilationDatabase CDB; Index: unittests/clangd/FileIndexTests.cpp =================================================================== --- unittests/clangd/FileIndexTests.cpp +++ unittests/clangd/FileIndexTests.cpp @@ -170,7 +170,7 @@ EXPECT_THAT(match(M, FuzzyFindRequest()), UnorderedElementsAre()); } -TEST(FileIndexTest, IgnoreClassMembers) { +TEST(FileIndexTest, ClassMembers) { FileIndex M; M.update("f1", build("f1", "class X { static int m1; int m2; static void f(); };") @@ -178,7 +178,8 @@ FuzzyFindRequest Req; Req.Query = ""; - EXPECT_THAT(match(M, Req), UnorderedElementsAre("X")); + EXPECT_THAT(match(M, Req), + UnorderedElementsAre("X", "X::m1", "X::m2", "X::f")); } TEST(FileIndexTest, NoIncludeCollected) { Index: unittests/clangd/SymbolCollectorTests.cpp =================================================================== --- unittests/clangd/SymbolCollectorTests.cpp +++ unittests/clangd/SymbolCollectorTests.cpp @@ -60,6 +60,9 @@ arg.Definition.EndOffset == Offsets.second; } MATCHER_P(Refs, R, "") { return int(arg.References) == R; } +MATCHER_P(ForCompletion, ForCompletion, "") { + return arg.ForCompletion == ForCompletion; +} namespace clang { namespace clangd { @@ -191,25 +194,31 @@ } // namespace foo )"; runSymbolCollector(Header, /*Main=*/""); - EXPECT_THAT(Symbols, - UnorderedElementsAreArray( - {QName("Foo"), QName("f1"), QName("f2"), QName("KInt"), - QName("kStr"), QName("foo"), QName("foo::bar"), - QName("foo::int32"), QName("foo::int32_t"), QName("foo::v1"), - QName("foo::bar::v2"), QName("foo::baz")})); + EXPECT_THAT( + Symbols, + UnorderedElementsAreArray( + {QName("Foo"), QName("Foo::f"), QName("f1"), QName("f2"), + QName("KInt"), QName("kStr"), QName("foo"), QName("foo::bar"), + QName("foo::int32"), QName("foo::int32_t"), QName("foo::v1"), + QName("foo::bar::v2"), QName("foo::baz")})); } +template struct Tmpl { T TmplField = 0; }; + TEST_F(SymbolCollectorTest, Template) { Annotations Header(R"( // Template is indexed, specialization and instantiation is not. - template struct [[Tmpl]] {T x = 0;}; + template struct [[Tmpl]] {T $xdecl[[x]] = 0;}; template <> struct Tmpl {}; extern template struct Tmpl; template struct Tmpl; )"); runSymbolCollector(Header.code(), /*Main=*/""); - EXPECT_THAT(Symbols, UnorderedElementsAreArray({AllOf( - QName("Tmpl"), DeclRange(Header.offsetRange()))})); + EXPECT_THAT( + Symbols, + UnorderedElementsAreArray( + {AllOf(QName("Tmpl"), DeclRange(Header.offsetRange())), + AllOf(QName("Tmpl::x"), DeclRange(Header.offsetRange("xdecl")))})); } TEST_F(SymbolCollectorTest, Locations) { @@ -228,7 +237,7 @@ void $printdef[[print]]() {} // Declared/defined in main only. - int Y; + int $ydecl[[Y]]; )cpp"); runSymbolCollector(Header.code(), Main.code()); EXPECT_THAT( @@ -240,7 +249,8 @@ DefRange(Main.offsetRange("clsdef"))), AllOf(QName("print"), DeclRange(Header.offsetRange("printdecl")), DefRange(Main.offsetRange("printdef"))), - AllOf(QName("Z"), DeclRange(Header.offsetRange("zdecl"))))); + AllOf(QName("Z"), DeclRange(Header.offsetRange("zdecl"))), + AllOf(QName("Y"), DeclRange(Main.offsetRange("ydecl"))))); } TEST_F(SymbolCollectorTest, References) { @@ -262,10 +272,11 @@ CollectorOpts.CountReferences = true; runSymbolCollector(Header, Main); EXPECT_THAT(Symbols, - UnorderedElementsAre(AllOf(QName("W"), Refs(1)), - AllOf(QName("X"), Refs(1)), - AllOf(QName("Y"), Refs(0)), - AllOf(QName("Z"), Refs(0)), QName("y"))); + UnorderedElementsAre( + AllOf(QName("W"), Refs(1)), AllOf(QName("X"), Refs(1)), + AllOf(QName("Y"), Refs(0)), AllOf(QName("Z"), Refs(0)), + QName("y"), QName("w"), QName("w2"), QName("x"), QName("V"), + QName("v"))); } TEST_F(SymbolCollectorTest, SymbolRelativeNoFallback) { @@ -320,7 +331,7 @@ Green }; enum class Color2 { - Yellow // ignore + Yellow }; namespace ns { enum { @@ -329,20 +340,22 @@ } )"; runSymbolCollector(Header, /*Main=*/""); - EXPECT_THAT(Symbols, UnorderedElementsAre(QName("Red"), QName("Color"), - QName("Green"), QName("Color2"), - QName("ns"), QName("ns::Black"))); + EXPECT_THAT(Symbols, + UnorderedElementsAre(QName("Red"), QName("Color"), QName("Green"), + QName("Color2"), QName("Color2::Yellow"), + QName("ns"), QName("ns::Black"))); } -TEST_F(SymbolCollectorTest, IgnoreNamelessSymbols) { +TEST_F(SymbolCollectorTest, NamelessSymbols) { const std::string Header = R"( struct { int a; } Foo; )"; runSymbolCollector(Header, /*Main=*/""); - EXPECT_THAT(Symbols, - UnorderedElementsAre(QName("Foo"))); + EXPECT_THAT(Symbols, UnorderedElementsAre( + QName("Foo"), AllOf(QName("(anonymous struct)::a"), + ForCompletion(false)))); } TEST_F(SymbolCollectorTest, SymbolFormedFromMacro) { @@ -384,7 +397,7 @@ DeclURI(TestHeaderURI)))); } -TEST_F(SymbolCollectorTest, IgnoreSymbolsInMainFile) { +TEST_F(SymbolCollectorTest, SymbolsInMainFile) { const std::string Header = R"( class Foo {}; void f1(); @@ -394,15 +407,16 @@ namespace { void ff() {} // ignore } - void main_f() {} // ignore + void main_f() {} // not for completion void f1() {} )"; runSymbolCollector(Header, Main); - EXPECT_THAT(Symbols, - UnorderedElementsAre(QName("Foo"), QName("f1"), QName("f2"))); + EXPECT_THAT(Symbols, UnorderedElementsAre( + QName("Foo"), QName("f1"), QName("f2"), + AllOf(QName("main_f"), ForCompletion(false)))); } -TEST_F(SymbolCollectorTest, IgnoreClassMembers) { +TEST_F(SymbolCollectorTest, ClassMembers) { const std::string Header = R"( class Foo { void f() {} @@ -417,7 +431,13 @@ void Foo::ssf() {} )"; runSymbolCollector(Header, Main); - EXPECT_THAT(Symbols, UnorderedElementsAre(QName("Foo"))); + EXPECT_THAT(Symbols, + UnorderedElementsAre( + QName("Foo"), AllOf(QName("Foo::f"), ForCompletion(false)), + AllOf(QName("Foo::g"), ForCompletion(false)), + AllOf(QName("Foo::sf"), ForCompletion(false)), + AllOf(QName("Foo::ssf"), ForCompletion(false)), + AllOf(QName("Foo::x"), ForCompletion(false)))); } TEST_F(SymbolCollectorTest, Scopes) { @@ -512,6 +532,7 @@ StartOffset: 0 EndOffset: 1 FileURI: file:///path/foo.h +ForCompletion: true CompletionLabel: 'Foo1-label' CompletionFilterText: 'filter' CompletionPlainInsertText: 'plain' @@ -532,6 +553,7 @@ StartOffset: 10 EndOffset: 12 FileURI: file:///path/bar.h +ForCompletion: true CompletionLabel: 'Foo2-label' CompletionFilterText: 'filter' CompletionPlainInsertText: 'plain' @@ -545,9 +567,10 @@ QName("clang::Foo1"), Labeled("Foo1-label"), Doc("Foo doc"), Detail("int"), DeclURI("file:///path/foo.h")))); auto Symbols2 = SymbolsFromYAML(YAML2); - EXPECT_THAT(Symbols2, UnorderedElementsAre(AllOf( - QName("clang::Foo2"), Labeled("Foo2-label"), - Not(HasDetail()), DeclURI("file:///path/bar.h")))); + EXPECT_THAT(Symbols2, + UnorderedElementsAre(AllOf( + QName("clang::Foo2"), Labeled("Foo2-label"), Not(HasDetail()), + DeclURI("file:///path/bar.h"), ForCompletion(true)))); std::string ConcatenatedYAML; { @@ -638,23 +661,30 @@ // Canonical declarations. class $cdecl[[C]] {}; struct $sdecl[[S]] {}; - union $udecl[[U]] {int x; bool y;}; + union $udecl[[U]] {int $xdecl[[x]]; bool $ydecl[[y]];}; )"); runSymbolCollector(Header.code(), /*Main=*/""); - EXPECT_THAT(Symbols, - UnorderedElementsAre( - AllOf(QName("C"), DeclURI(TestHeaderURI), - DeclRange(Header.offsetRange("cdecl")), - IncludeHeader(TestHeaderURI), DefURI(TestHeaderURI), - DefRange(Header.offsetRange("cdecl"))), - AllOf(QName("S"), DeclURI(TestHeaderURI), - DeclRange(Header.offsetRange("sdecl")), - IncludeHeader(TestHeaderURI), DefURI(TestHeaderURI), - DefRange(Header.offsetRange("sdecl"))), - AllOf(QName("U"), DeclURI(TestHeaderURI), - DeclRange(Header.offsetRange("udecl")), - IncludeHeader(TestHeaderURI), DefURI(TestHeaderURI), - DefRange(Header.offsetRange("udecl"))))); + EXPECT_THAT( + Symbols, + UnorderedElementsAre( + AllOf(QName("C"), DeclURI(TestHeaderURI), + DeclRange(Header.offsetRange("cdecl")), + IncludeHeader(TestHeaderURI), DefURI(TestHeaderURI), + DefRange(Header.offsetRange("cdecl"))), + AllOf(QName("S"), DeclURI(TestHeaderURI), + DeclRange(Header.offsetRange("sdecl")), + IncludeHeader(TestHeaderURI), DefURI(TestHeaderURI), + DefRange(Header.offsetRange("sdecl"))), + AllOf(QName("U"), DeclURI(TestHeaderURI), + DeclRange(Header.offsetRange("udecl")), + IncludeHeader(TestHeaderURI), DefURI(TestHeaderURI), + DefRange(Header.offsetRange("udecl"))), + AllOf(QName("U::x"), DeclURI(TestHeaderURI), + DeclRange(Header.offsetRange("xdecl")), DefURI(TestHeaderURI), + DefRange(Header.offsetRange("xdecl"))), + AllOf(QName("U::y"), DeclURI(TestHeaderURI), + DeclRange(Header.offsetRange("ydecl")), DefURI(TestHeaderURI), + DefRange(Header.offsetRange("ydecl"))))); } TEST_F(SymbolCollectorTest, ClassForwardDeclarationIsCanonical) { Index: unittests/clangd/SyncAPI.h =================================================================== --- unittests/clangd/SyncAPI.h +++ unittests/clangd/SyncAPI.h @@ -41,6 +41,10 @@ std::string runDumpAST(ClangdServer &Server, PathRef File); +llvm::Expected> +runWorkspaceSymbols(ClangdServer &Server, StringRef Query, + const WorkspaceSymbolOptions &Opts); + } // namespace clangd } // namespace clang Index: unittests/clangd/SyncAPI.cpp =================================================================== --- unittests/clangd/SyncAPI.cpp +++ unittests/clangd/SyncAPI.cpp @@ -110,5 +110,13 @@ return std::move(*Result); } +llvm::Expected> +runWorkspaceSymbols(ClangdServer &Server, StringRef Query, + const WorkspaceSymbolOptions &Opts) { + llvm::Optional>> Result; + Server.onWorkspaceSymbol(Query, Opts, capture(Result)); + return std::move(*Result); +} + } // namespace clangd } // namespace clang Index: unittests/clangd/WorkspaceSymbolsTests.cpp =================================================================== --- /dev/null +++ unittests/clangd/WorkspaceSymbolsTests.cpp @@ -0,0 +1,373 @@ +//===-- WorkspaceSymbolsTests.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 "SyncAPI.h" +#include "TestFS.h" +#include "WorkspaceSymbols.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { + +void PrintTo(const SymbolInformation &I, std::ostream *O) { + llvm::raw_os_ostream OS(*O); + OS << I.containerName << I.name << " - " << toJSON(I); +} + +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; } + +class WorkspaceSymbolsTest : public ::testing::Test { +protected: + std::unique_ptr FSProvider; + std::unique_ptr CDB; + std::unique_ptr DiagConsumer; + std::unique_ptr Server; + std::unique_ptr Opts; + + virtual void SetUp() override { + FSProvider = llvm::make_unique(); + CDB = llvm::make_unique(); + DiagConsumer = llvm::make_unique(); + auto ServerOpts = ClangdServer::optsForTest(); + ServerOpts.BuildDynamicSymbolIndex = true; + Server = llvm::make_unique(*CDB, *FSProvider, *DiagConsumer, + ServerOpts); + Opts = llvm::make_unique(); + } + + std::vector getSymbols(StringRef Query) { + EXPECT_TRUE(Server->blockUntilIdleForTest()) << "Waiting for preamble"; + auto SymbolInfos = runWorkspaceSymbols(*Server, Query, *Opts); + 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() { + struct LocalClass {}; + int local_var; + })cpp"); + EXPECT_THAT(getSymbols("local_var"), IsEmpty()); + EXPECT_THAT(getSymbols("LocalClass"), IsEmpty()); +} + +TEST_F(WorkspaceSymbolsTest, NoParams) { + addFile("foo.cpp", R"cpp( + void test(int FirstParam, int SecondParam) { + })cpp"); + EXPECT_THAT(getSymbols("FirstParam"), IsEmpty()); + EXPECT_THAT(getSymbols("SecondParam"), IsEmpty()); +} + +TEST_F(WorkspaceSymbolsTest, ClassWithMembers) { + addFile("foo.cpp", R"cpp( + struct ClassWithMembers { + int method(); + protected: + int field; + private: + int private_field; + };)cpp"); + EXPECT_THAT( + getSymbols("method"), + ElementsAre(AllOf(Named("method"), InContainer("ClassWithMembers::")))); + EXPECT_THAT( + getSymbols("ClassWithMembers::method"), + ElementsAre(AllOf(Named("method"), InContainer("ClassWithMembers::")))); + EXPECT_THAT( + getSymbols("ClassWithMembers::met"), + ElementsAre(AllOf(Named("method"), InContainer("ClassWithMembers::")))); + EXPECT_THAT( + getSymbols("field"), + ElementsAre( + AllOf(Named("field"), InContainer("ClassWithMembers::")), + AllOf(Named("private_field"), InContainer("ClassWithMembers::")))); + EXPECT_THAT( + getSymbols("ClassWithMembers::"), + ElementsAre( + AllOf(Named("field"), InContainer("ClassWithMembers::")), + AllOf(Named("method"), InContainer("ClassWithMembers::")), + AllOf(Named("private_field"), InContainer("ClassWithMembers::")))); + EXPECT_THAT(getSymbols("ClassWithMembers:"), IsEmpty()); + EXPECT_THAT(getSymbols("ClassWithMembers"), + ElementsAre(AllOf(Named("ClassWithMembers"), InContainer("")))); +} + +TEST_F(WorkspaceSymbolsTest, ClassInNamespaceWithMembers) { + addFile("foo.cpp", R"cpp( + namespace ns1 { + struct ClassWithMembers { + int method(); + }; + })cpp"); + EXPECT_THAT(getSymbols("ns1::ClassWithMembers::method"), + ElementsAre(AllOf(Named("method"), + InContainer("ns1::ClassWithMembers::")))); +} + +TEST_F(WorkspaceSymbolsTest, Globals) { + addFile("foo.cpp", R"cpp( + int global_var; + + int global_func(); + + struct GlobalClass {};)cpp"); + EXPECT_THAT(getSymbols("global"), + ElementsAre(AllOf(Named("GlobalClass"), InContainer("")), + AllOf(Named("global_func"), InContainer("")), + AllOf(Named("global_var"), InContainer("")))); +} + +TEST_F(WorkspaceSymbolsTest, Unnamed) { + addFile("foo.cpp", R"cpp( + struct { + int InUnnamed; + } UnnamedStruct;)cpp"); + EXPECT_THAT(getSymbols("UnnamedStruct"), ElementsAre(Named("UnnamedStruct"))); + EXPECT_THAT(getSymbols("InUnnamed"), + ElementsAre(AllOf(Named("InUnnamed"), + InContainer("(anonymous struct)::")))); +} + +TEST_F(WorkspaceSymbolsTest, InMainFile) { + addFile("foo.cpp", R"cpp( + int test() { + } + )cpp"); + EXPECT_THAT(getSymbols("test"), + ElementsAre(AllOf(Named("test"), InContainer("")))); +} + +TEST_F(WorkspaceSymbolsTest, AnonymousNamespace) { + addFile("foo.cpp", R"cpp( + namespace { + void test() {} + } + )cpp"); + + EXPECT_THAT(getSymbols("test"), IsEmpty() + // FIXME: This should work once SymbolCollector collects symbols + // in anonymous namespaces. + // ElementsAre(AllOf(Named("test"), InContainer(""))) + ); +} + +TEST_F(WorkspaceSymbolsTest, MultiFile) { + addFile("foo.cpp", R"cpp( + int foo() { + } + )cpp"); + addFile("foo2.cpp", R"cpp( + int foo2() { + } + )cpp"); + EXPECT_THAT(getSymbols("foo"), + ElementsAre(AllOf(Named("foo"), InContainer("")), + AllOf(Named("foo2"), InContainer("")))); +} + +TEST_F(WorkspaceSymbolsTest, GlobalNamespaceQueries) { + addFile("foo.cpp", R"cpp( + int foo() { + } + class Foo { + int a; + }; + )cpp"); + EXPECT_THAT(getSymbols("::"), + ElementsAre(AllOf(Named("Foo"), InContainer("")), + AllOf(Named("a"), InContainer("Foo::")), + AllOf(Named("foo"), InContainer("")))); + EXPECT_THAT(getSymbols(":"), IsEmpty()); + EXPECT_THAT(getSymbols(""), IsEmpty()); +} + +TEST_F(WorkspaceSymbolsTest, WithLimit) { + addFile("foo.cpp", R"cpp( + int foo; + int foo2; + )cpp"); + EXPECT_THAT(getSymbols("foo"), + ElementsAre(AllOf(Named("foo"), InContainer("")), + AllOf(Named("foo2"), InContainer("")))); + + Opts->Limit = 1; + EXPECT_THAT(getSymbols("foo"), + ElementsAre(AnyOf((Named("foo"), InContainer("")), + AllOf(Named("foo2"), InContainer(""))))); +} + +TEST_F(WorkspaceSymbolsTest, Enums) { + addFile("foo.cpp", R"cpp( + enum { + Red + }; + enum Color1 { + Green + }; + enum class Color2 { + Yellow + }; + namespace ns { + enum { + Black + }; + } + )cpp"); + EXPECT_THAT(getSymbols("Red"), + ElementsAre(AllOf(Named("Red"), InContainer("")))); + EXPECT_THAT(getSymbols("Color1"), + ElementsAre(AllOf(Named("Color1"), InContainer("")))); + EXPECT_THAT(getSymbols("Green"), + ElementsAre(AllOf(Named("Green"), InContainer("")))); + EXPECT_THAT(getSymbols("Color2"), + ElementsAre(AllOf(Named("Color2"), InContainer("")))); + EXPECT_THAT(getSymbols("Yellow"), + ElementsAre(AllOf(Named("Yellow"), InContainer("Color2::")))); + EXPECT_THAT(getSymbols("Color2::Yellow"), + ElementsAre(AllOf(Named("Yellow"), InContainer("Color2::")))); + EXPECT_THAT(getSymbols("ns"), + ElementsAre(AllOf(Named("ns"), InContainer("")))); + EXPECT_THAT(getSymbols("Black"), + ElementsAre(AllOf(Named("Black"), InContainer("ns::")))); + EXPECT_THAT(getSymbols("ns::Black"), + ElementsAre(AllOf(Named("Black"), InContainer("ns::")))); +} + +TEST_F(WorkspaceSymbolsTest, Union) { + addFile("foo.cpp", R"cpp( + union U { + int x; + bool y; + }; + )cpp"); + EXPECT_THAT(getSymbols("U"), ElementsAre(AllOf(Named("U"), InContainer("")))); + EXPECT_THAT(getSymbols("x"), + ElementsAre(AllOf(Named("x"), InContainer("U::")))); + EXPECT_THAT(getSymbols("y"), + ElementsAre(AllOf(Named("y"), InContainer("U::")))); +} + +TEST_F(WorkspaceSymbolsTest, InlineNamespace) { + addFile("foo.cpp", R"cpp( + namespace na { + inline namespace nb { + class Foo {}; + } + } + namespace na { + // This is still inlined. + namespace nb { + class Bar {}; + } + } + )cpp"); + EXPECT_THAT(getSymbols("na"), + ElementsAre(AllOf(Named("na"), InContainer("")))); + EXPECT_THAT(getSymbols("nb"), + ElementsAre(AllOf(Named("nb"), InContainer("na::")))); + EXPECT_THAT(getSymbols("Foo"), + ElementsAre(AllOf(Named("Foo"), InContainer("na::")))); + EXPECT_THAT(getSymbols("na::Foo"), + ElementsAre(AllOf(Named("Foo"), InContainer("na::")))); + // It would be good if it was possible to query with the inline namespace as + // well. + // EXPECT_THAT(getSymbols("na::nb::Foo"), ElementsAre(AllOf(Named("Foo"), + // InContainer("na::Foo")))); + EXPECT_THAT(getSymbols("Bar"), + ElementsAre(AllOf(Named("Bar"), InContainer("na::")))); + EXPECT_THAT(getSymbols("na::Bar"), + ElementsAre(AllOf(Named("Bar"), InContainer("na::")))); + EXPECT_THAT(getSymbols("nb::Bar"), IsEmpty()); +} + +TEST_F(WorkspaceSymbolsTest, SymbolKindCapabilities) { + addFile("foo.cpp", R"cpp( + struct Foo {}; + class Foo2 {}; + enum { + FOO_VAL + } + )cpp"); + + EXPECT_THAT( + getSymbols("Foo"), + UnorderedElementsAre( + AllOf(Named("Foo"), InContainer(""), WithKind(SymbolKind::Class)), + AllOf(Named("Foo2"), InContainer(""), WithKind(SymbolKind::Class)), + AllOf(Named("FOO_VAL"), InContainer(""), + WithKind(SymbolKind::Enum)))); + + using SymbolKindType = std::underlying_type::type; + std::vector BaseKinds; + for (SymbolKindType I = 0; I < SymbolKindType(SymbolKind::Array); ++I) + BaseKinds.push_back(SymbolKind(I)); + + Opts->supportedSymbolKinds = BaseKinds; + EXPECT_THAT( + getSymbols("Foo"), + UnorderedElementsAre( + AllOf(Named("Foo"), InContainer(""), WithKind(SymbolKind::Class)), + AllOf(Named("Foo2"), InContainer(""), WithKind(SymbolKind::Class)), + AllOf(Named("FOO_VAL"), InContainer(""), + WithKind(SymbolKind::Enum)))); + + Opts->supportedSymbolKinds->push_back(SymbolKind::Struct); + Opts->supportedSymbolKinds->push_back(SymbolKind::EnumMember); + EXPECT_THAT( + getSymbols("Foo"), + UnorderedElementsAre( + AllOf(Named("Foo"), InContainer(""), WithKind(SymbolKind::Struct)), + AllOf(Named("Foo2"), InContainer(""), WithKind(SymbolKind::Class)), + AllOf(Named("FOO_VAL"), InContainer(""), + WithKind(SymbolKind::EnumMember)))); +} + +} // namespace clangd +} // namespace clang