Index: clangd/CMakeLists.txt =================================================================== --- clangd/CMakeLists.txt +++ clangd/CMakeLists.txt @@ -14,6 +14,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" @@ -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; @@ -102,6 +105,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", true}, {"executeCommandProvider", json::obj{ {"commands", @@ -245,6 +250,17 @@ } } +void ClangdLSPServer::onWorkspaceSymbol(WorkspaceSymbolParams &Params) { + Server.workspaceSymbols( + 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); @@ -417,11 +433,12 @@ } } -ClangdLSPServer::ClangdLSPServer(JSONOutput &Out, - const clangd::CodeCompleteOptions &CCOpts, - llvm::Optional CompileCommandsDir, - const ClangdServer::Options &Opts) +ClangdLSPServer::ClangdLSPServer( + JSONOutput &Out, const clangd::CodeCompleteOptions &CCOpts, + const clangd::WorkspaceSymbolOptions &WorkspaceSymOpts, + llvm::Optional CompileCommandsDir, const ClangdServer::Options &Opts) : Out(Out), 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,10 @@ /// Get code hover for a given position. void findHover(PathRef File, Position Pos, Callback CB); + void workspaceSymbols(StringRef Query, + const clangd::WorkspaceSymbolOptions &Opts, + 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" @@ -492,6 +493,13 @@ // invalidating other caches. } +void ClangdServer::workspaceSymbols( + StringRef Query, const clangd::WorkspaceSymbolOptions &Opts, + Callback> CB) { + CB(clangd::getWorkspaceSymbols(Query, Opts, Index, + FSProvider.getFileSystem())); +} + std::vector> ClangdServer::getUsedBytesPerFile() const { return WorkScheduler.getUsedBytesPerFile(); Index: clangd/FindSymbols.h =================================================================== --- /dev/null +++ clangd/FindSymbols.h @@ -0,0 +1,41 @@ +//===--- 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_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/FindSymbols.cpp =================================================================== --- /dev/null +++ clangd/FindSymbols.cpp @@ -0,0 +1,169 @@ +//===--- 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/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. + // FIXME: If the index stored line/col directly, we woudln't have to read all + // the files 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}); + } + }); + return Result; +} + +} // namespace clangd +} // namespace clang Index: clangd/Protocol.h =================================================================== --- clangd/Protocol.h +++ clangd/Protocol.h @@ -237,6 +237,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 { @@ -247,8 +300,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 +577,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) @@ -351,6 +386,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/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/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,16 +81,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; @@ -299,6 +290,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; 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,10 +230,13 @@ clangd::CodeCompleteOptions CCOpts; CCOpts.IncludeIneligibleResults = IncludeIneligibleResults; - CCOpts.Limit = LimitCompletionResult; + CCOpts.Limit = LimitResults; + clangd::WorkspaceSymbolOptions WorkspaceSymOpts; + WorkspaceSymOpts.Limit = LimitResults; // 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": 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: 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 HeadersTests.cpp IndexTests.cpp Index: unittests/clangd/FindSymbolsTests.cpp =================================================================== --- /dev/null +++ unittests/clangd/FindSymbolsTests.cpp @@ -0,0 +1,380 @@ +//===-- 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 { + +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.h", R"cpp( + struct ClassWithMembers { + int method(); + protected: + int field; + private: + int private_field; + };)cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT(getSymbols("method"), IsEmpty()); + EXPECT_THAT(getSymbols("ClassWithMembers::method"), IsEmpty()); + EXPECT_THAT(getSymbols("ClassWithMembers::met"), IsEmpty()); + EXPECT_THAT(getSymbols("field"), IsEmpty()); + EXPECT_THAT(getSymbols("ClassWithMembers::"), IsEmpty()); + EXPECT_THAT(getSymbols("ClassWithMembers:"), IsEmpty()); + EXPECT_THAT(getSymbols("ClassWithMembers"), + ElementsAre(AllOf(Named("ClassWithMembers"), InContainer("")))); +} + +TEST_F(WorkspaceSymbolsTest, ClassInNamespaceWithMembers) { + addFile("foo.h", R"cpp( + namespace ns1 { + struct ClassWithMembers { + int method(); + }; + })cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT(getSymbols("ns1::ClassWithMembers::method"), IsEmpty()); +} + +TEST_F(WorkspaceSymbolsTest, Globals) { + addFile("foo.h", R"cpp( + int global_var; + + int global_func(); + + struct GlobalClass {};)cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT( + getSymbols("global"), + UnorderedElementsAre(AllOf(Named("GlobalClass"), InContainer("")), + AllOf(Named("global_func"), InContainer("")), + AllOf(Named("global_var"), InContainer("")))); +} + +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(Named("UnnamedStruct"))); + 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, 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; + }; + )cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT(getSymbols("::"), + UnorderedElementsAre(AllOf(Named("Foo"), InContainer("")), + AllOf(Named("foo"), InContainer("")))); + 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("")), + 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.h", R"cpp( + enum { + Red + }; + enum Color1 { + Green + }; + enum class Color2 { + Yellow + }; + namespace ns { + enum { + Black + }; + } + )cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )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"), IsEmpty()); + EXPECT_THAT(getSymbols("Color2::Yellow"), IsEmpty()); + 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.h", R"cpp( + union U { + int x; + bool y; + }; + )cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )cpp"); + EXPECT_THAT(getSymbols("U"), ElementsAre(AllOf(Named("U"), InContainer("")))); + EXPECT_THAT(getSymbols("x"), IsEmpty()); + EXPECT_THAT(getSymbols("y"), IsEmpty()); +} + +TEST_F(WorkspaceSymbolsTest, InlineNamespace) { + addFile("foo.h", R"cpp( + namespace na { + inline namespace nb { + class Foo {}; + } + } + namespace na { + // This is still inlined. + namespace nb { + class Bar {}; + } + } + )cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )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.h", R"cpp( + struct Foo {}; + class Foo2 {}; + enum { + FOO_VAL + } + )cpp"); + addFile("foo.cpp", R"cpp( + #include "foo.h" + )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 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.workspaceSymbols(Query, Opts, capture(Result)); + return std::move(*Result); +} + } // namespace clangd } // namespace clang