mirror of
https://github.com/llvm/llvm-project.git
synced 2025-04-17 08:06:40 +00:00
[clangd] Add support for hierarchical documentSymbol
Reviewers: ioeric, sammccall, simark Reviewed By: sammccall Subscribers: MaskRay, jkorous, arphaman, kadircet, cfe-commits Differential Revision: https://reviews.llvm.org/D52311 llvm-svn: 347498
This commit is contained in:
parent
0fc5dcd1c8
commit
19d75608f8
@ -11,9 +11,12 @@
|
||||
|
||||
#include "clang/AST/ASTContext.h"
|
||||
#include "clang/AST/Decl.h"
|
||||
#include "clang/AST/DeclTemplate.h"
|
||||
#include "clang/Basic/SourceLocation.h"
|
||||
#include "clang/Basic/SourceManager.h"
|
||||
#include "clang/Index/USRGeneration.h"
|
||||
#include "llvm/Support/Casting.h"
|
||||
#include "llvm/Support/ScopedPrinter.h"
|
||||
|
||||
using namespace llvm;
|
||||
namespace clang {
|
||||
@ -61,6 +64,46 @@ std::string printQualifiedName(const NamedDecl &ND) {
|
||||
return QName;
|
||||
}
|
||||
|
||||
static const TemplateArgumentList *
|
||||
getTemplateSpecializationArgs(const NamedDecl &ND) {
|
||||
if (auto *Func = llvm::dyn_cast<FunctionDecl>(&ND))
|
||||
return Func->getTemplateSpecializationArgs();
|
||||
if (auto *Cls = llvm::dyn_cast<ClassTemplateSpecializationDecl>(&ND))
|
||||
return &Cls->getTemplateInstantiationArgs();
|
||||
if (auto *Var = llvm::dyn_cast<VarTemplateSpecializationDecl>(&ND))
|
||||
return &Var->getTemplateInstantiationArgs();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string printName(const ASTContext &Ctx, const NamedDecl &ND) {
|
||||
std::string Name;
|
||||
llvm::raw_string_ostream Out(Name);
|
||||
PrintingPolicy PP(Ctx.getLangOpts());
|
||||
// Handle 'using namespace'. They all have the same name - <using-directive>.
|
||||
if (auto *UD = llvm::dyn_cast<UsingDirectiveDecl>(&ND)) {
|
||||
Out << "using namespace ";
|
||||
if (auto *Qual = UD->getQualifier())
|
||||
Qual->print(Out, PP);
|
||||
UD->getNominatedNamespaceAsWritten()->printName(Out);
|
||||
return Out.str();
|
||||
}
|
||||
ND.getDeclName().print(Out, PP);
|
||||
if (!Out.str().empty()) {
|
||||
// FIXME(ibiryukov): do not show args not explicitly written by the user.
|
||||
if (auto *ArgList = getTemplateSpecializationArgs(ND))
|
||||
printTemplateArgumentList(Out, ArgList->asArray(), PP);
|
||||
return Out.str();
|
||||
}
|
||||
// The name was empty, so present an anonymous entity.
|
||||
if (auto *NS = llvm::dyn_cast<NamespaceDecl>(&ND))
|
||||
return "(anonymous namespace)";
|
||||
if (auto *Cls = llvm::dyn_cast<RecordDecl>(&ND))
|
||||
return ("(anonymous " + Cls->getKindName() + ")").str();
|
||||
if (auto *En = llvm::dyn_cast<EnumDecl>(&ND))
|
||||
return "(anonymous enum)";
|
||||
return "(anonymous)";
|
||||
}
|
||||
|
||||
std::string printNamespaceScope(const DeclContext &DC) {
|
||||
for (const auto *Ctx = &DC; Ctx != nullptr; Ctx = Ctx->getParent())
|
||||
if (const auto *NS = dyn_cast<NamespaceDecl>(Ctx))
|
||||
|
@ -42,6 +42,11 @@ std::string printQualifiedName(const NamedDecl &ND);
|
||||
/// Returns the first enclosing namespace scope starting from \p DC.
|
||||
std::string printNamespaceScope(const DeclContext &DC);
|
||||
|
||||
/// Prints unqualified name of the decl for the purpose of displaying it to the
|
||||
/// user. Anonymous decls return names of the form "(anonymous {kind})", e.g.
|
||||
/// "(anonymous struct)" or "(anonymous namespace)".
|
||||
std::string printName(const ASTContext &Ctx, const NamedDecl &ND);
|
||||
|
||||
/// Gets the symbol ID for a declaration, if possible.
|
||||
llvm::Optional<SymbolID> getSymbolID(const Decl *D);
|
||||
|
||||
|
@ -23,6 +23,14 @@ namespace clang {
|
||||
namespace clangd {
|
||||
namespace {
|
||||
|
||||
void adjustSymbolKinds(llvm::MutableArrayRef<DocumentSymbol> Syms,
|
||||
SymbolKindBitset Kinds) {
|
||||
for (auto &S : Syms) {
|
||||
S.kind = adjustKindToCapability(S.kind, Kinds);
|
||||
adjustSymbolKinds(S.children, Kinds);
|
||||
}
|
||||
}
|
||||
|
||||
SymbolKindBitset defaultSymbolKinds() {
|
||||
SymbolKindBitset Defaults;
|
||||
for (size_t I = SymbolKindMin; I <= static_cast<size_t>(SymbolKind::Array);
|
||||
@ -284,6 +292,8 @@ void ClangdLSPServer::onInitialize(const InitializeParams &Params,
|
||||
if (Params.capabilities.CompletionItemKinds)
|
||||
SupportedCompletionItemKinds |= *Params.capabilities.CompletionItemKinds;
|
||||
SupportsCodeAction = Params.capabilities.CodeActionStructure;
|
||||
SupportsHierarchicalDocumentSymbol =
|
||||
Params.capabilities.HierarchicalDocumentSymbol;
|
||||
|
||||
Reply(json::Object{
|
||||
{{"capabilities",
|
||||
@ -501,19 +511,48 @@ void ClangdLSPServer::onDocumentFormatting(
|
||||
Reply(ReplacementsOrError.takeError());
|
||||
}
|
||||
|
||||
void ClangdLSPServer::onDocumentSymbol(
|
||||
const DocumentSymbolParams &Params,
|
||||
Callback<std::vector<SymbolInformation>> Reply) {
|
||||
/// The functions constructs a flattened view of the DocumentSymbol hierarchy.
|
||||
/// Used by the clients that do not support the hierarchical view.
|
||||
static std::vector<SymbolInformation>
|
||||
flattenSymbolHierarchy(llvm::ArrayRef<DocumentSymbol> Symbols,
|
||||
const URIForFile &FileURI) {
|
||||
|
||||
std::vector<SymbolInformation> Results;
|
||||
std::function<void(const DocumentSymbol &, StringRef)> Process =
|
||||
[&](const DocumentSymbol &S, Optional<StringRef> ParentName) {
|
||||
SymbolInformation SI;
|
||||
SI.containerName = ParentName ? "" : *ParentName;
|
||||
SI.name = S.name;
|
||||
SI.kind = S.kind;
|
||||
SI.location.range = S.range;
|
||||
SI.location.uri = FileURI;
|
||||
|
||||
Results.push_back(std::move(SI));
|
||||
std::string FullName =
|
||||
!ParentName ? S.name : (ParentName->str() + "::" + S.name);
|
||||
for (auto &C : S.children)
|
||||
Process(C, /*ParentName=*/FullName);
|
||||
};
|
||||
for (auto &S : Symbols)
|
||||
Process(S, /*ParentName=*/"");
|
||||
return Results;
|
||||
}
|
||||
|
||||
void ClangdLSPServer::onDocumentSymbol(const DocumentSymbolParams &Params,
|
||||
Callback<json::Value> Reply) {
|
||||
URIForFile FileURI = Params.textDocument.uri;
|
||||
Server->documentSymbols(
|
||||
Params.textDocument.uri.file(),
|
||||
Bind(
|
||||
[this](decltype(Reply) Reply,
|
||||
Expected<std::vector<SymbolInformation>> Items) {
|
||||
[this, FileURI](decltype(Reply) Reply,
|
||||
Expected<std::vector<DocumentSymbol>> Items) {
|
||||
if (!Items)
|
||||
return Reply(Items.takeError());
|
||||
for (auto &Sym : *Items)
|
||||
Sym.kind = adjustKindToCapability(Sym.kind, SupportedSymbolKinds);
|
||||
Reply(std::move(*Items));
|
||||
adjustSymbolKinds(*Items, SupportedSymbolKinds);
|
||||
if (SupportsHierarchicalDocumentSymbol)
|
||||
return Reply(std::move(*Items));
|
||||
else
|
||||
return Reply(flattenSymbolHierarchy(*Items, FileURI));
|
||||
},
|
||||
std::move(Reply)));
|
||||
}
|
||||
|
@ -66,8 +66,11 @@ private:
|
||||
Callback<std::vector<TextEdit>>);
|
||||
void onDocumentFormatting(const DocumentFormattingParams &,
|
||||
Callback<std::vector<TextEdit>>);
|
||||
// The results are serialized 'vector<DocumentSymbol>' if
|
||||
// SupportsHierarchicalDocumentSymbol is true and 'vector<SymbolInformation>'
|
||||
// otherwise.
|
||||
void onDocumentSymbol(const DocumentSymbolParams &,
|
||||
Callback<std::vector<SymbolInformation>>);
|
||||
Callback<llvm::json::Value>);
|
||||
void onCodeAction(const CodeActionParams &, Callback<llvm::json::Value>);
|
||||
void onCompletion(const TextDocumentPositionParams &,
|
||||
Callback<CompletionList>);
|
||||
@ -128,6 +131,8 @@ private:
|
||||
CompletionItemKindBitset SupportedCompletionItemKinds;
|
||||
// Whether the client supports CodeAction response objects.
|
||||
bool SupportsCodeAction = false;
|
||||
/// From capabilities of textDocument/documentSymbol.
|
||||
bool SupportsHierarchicalDocumentSymbol = false;
|
||||
|
||||
// Store of the current versions of the open documents.
|
||||
DraftStore DraftMgr;
|
||||
|
@ -470,10 +470,10 @@ void ClangdServer::workspaceSymbols(
|
||||
std::move(CB)));
|
||||
}
|
||||
|
||||
void ClangdServer::documentSymbols(
|
||||
StringRef File, Callback<std::vector<SymbolInformation>> CB) {
|
||||
auto Action = [](Callback<std::vector<SymbolInformation>> CB,
|
||||
Expected<InputsAndAST> InpAST) {
|
||||
void ClangdServer::documentSymbols(StringRef File,
|
||||
Callback<std::vector<DocumentSymbol>> CB) {
|
||||
auto Action = [](Callback<std::vector<DocumentSymbol>> CB,
|
||||
llvm::Expected<InputsAndAST> InpAST) {
|
||||
if (!InpAST)
|
||||
return CB(InpAST.takeError());
|
||||
CB(clangd::getDocumentSymbols(InpAST->AST));
|
||||
|
@ -167,7 +167,7 @@ public:
|
||||
|
||||
/// Retrieve the symbols within the specified file.
|
||||
void documentSymbols(StringRef File,
|
||||
Callback<std::vector<SymbolInformation>> CB);
|
||||
Callback<std::vector<DocumentSymbol>> CB);
|
||||
|
||||
/// Retrieve locations for symbol references.
|
||||
void findReferences(PathRef File, Position Pos,
|
||||
|
@ -15,11 +15,13 @@
|
||||
#include "Quality.h"
|
||||
#include "SourceCode.h"
|
||||
#include "index/Index.h"
|
||||
#include "clang/AST/DeclTemplate.h"
|
||||
#include "clang/Index/IndexDataConsumer.h"
|
||||
#include "clang/Index/IndexSymbol.h"
|
||||
#include "clang/Index/IndexingAction.h"
|
||||
#include "llvm/Support/FormatVariadic.h"
|
||||
#include "llvm/Support/Path.h"
|
||||
#include "llvm/Support/ScopedPrinter.h"
|
||||
|
||||
#define DEBUG_TYPE "FindSymbols"
|
||||
|
||||
@ -178,104 +180,146 @@ getWorkspaceSymbols(StringRef Query, int Limit, const SymbolIndex *const Index,
|
||||
}
|
||||
|
||||
namespace {
|
||||
/// Finds document symbols in the main file of the AST.
|
||||
class DocumentSymbolsConsumer : public index::IndexDataConsumer {
|
||||
ASTContext &AST;
|
||||
std::vector<SymbolInformation> Symbols;
|
||||
// We are always list document for the same file, so cache the value.
|
||||
Optional<URIForFile> MainFileUri;
|
||||
llvm::Optional<DocumentSymbol> declToSym(ASTContext &Ctx, const NamedDecl &ND) {
|
||||
auto &SM = Ctx.getSourceManager();
|
||||
|
||||
SourceLocation NameLoc = findNameLoc(&ND);
|
||||
// getFileLoc is a good choice for us, but we also need to make sure
|
||||
// sourceLocToPosition won't switch files, so we call getSpellingLoc on top of
|
||||
// that to make sure it does not switch files.
|
||||
// FIXME: sourceLocToPosition should not switch files!
|
||||
SourceLocation BeginLoc = SM.getSpellingLoc(SM.getFileLoc(ND.getBeginLoc()));
|
||||
SourceLocation EndLoc = SM.getSpellingLoc(SM.getFileLoc(ND.getEndLoc()));
|
||||
if (NameLoc.isInvalid() || BeginLoc.isInvalid() || EndLoc.isInvalid())
|
||||
return llvm::None;
|
||||
|
||||
if (!SM.isWrittenInMainFile(NameLoc) || !SM.isWrittenInMainFile(BeginLoc) ||
|
||||
!SM.isWrittenInMainFile(EndLoc))
|
||||
return llvm::None;
|
||||
|
||||
Position NameBegin = sourceLocToPosition(SM, NameLoc);
|
||||
Position NameEnd = sourceLocToPosition(
|
||||
SM, Lexer::getLocForEndOfToken(NameLoc, 0, SM, Ctx.getLangOpts()));
|
||||
|
||||
index::SymbolInfo SymInfo = index::getSymbolInfo(&ND);
|
||||
// FIXME: this is not classifying constructors, destructors and operators
|
||||
// correctly (they're all "methods").
|
||||
SymbolKind SK = indexSymbolKindToSymbolKind(SymInfo.Kind);
|
||||
|
||||
DocumentSymbol SI;
|
||||
SI.name = printName(Ctx, ND);
|
||||
SI.kind = SK;
|
||||
SI.deprecated = ND.isDeprecated();
|
||||
SI.range =
|
||||
Range{sourceLocToPosition(SM, BeginLoc), sourceLocToPosition(SM, EndLoc)};
|
||||
SI.selectionRange = Range{NameBegin, NameEnd};
|
||||
if (!SI.range.contains(SI.selectionRange)) {
|
||||
// 'selectionRange' must be contained in 'range', so in cases where clang
|
||||
// reports unrelated ranges we need to reconcile somehow.
|
||||
SI.range = SI.selectionRange;
|
||||
}
|
||||
return SI;
|
||||
}
|
||||
|
||||
/// A helper class to build an outline for the parse AST. It traverse the AST
|
||||
/// directly instead of using RecursiveASTVisitor (RAV) for three main reasons:
|
||||
/// - there is no way to keep RAV from traversing subtrees we're not
|
||||
/// interested in. E.g. not traversing function locals or implicit template
|
||||
/// instantiations.
|
||||
/// - it's easier to combine results of recursive passes, e.g.
|
||||
/// - visiting decls is actually simple, so we don't hit the complicated
|
||||
/// cases that RAV mostly helps with (types and expressions, etc.)
|
||||
class DocumentOutline {
|
||||
public:
|
||||
DocumentSymbolsConsumer(ASTContext &AST) : AST(AST) {}
|
||||
std::vector<SymbolInformation> takeSymbols() { return std::move(Symbols); }
|
||||
DocumentOutline(ParsedAST &AST) : AST(AST) {}
|
||||
|
||||
void initialize(ASTContext &Ctx) override {
|
||||
// Compute the absolute path of the main file which we will use for all
|
||||
// results.
|
||||
const SourceManager &SM = AST.getSourceManager();
|
||||
const FileEntry *F = SM.getFileEntryForID(SM.getMainFileID());
|
||||
if (!F)
|
||||
/// Builds the document outline for the generated AST.
|
||||
std::vector<DocumentSymbol> build() {
|
||||
std::vector<DocumentSymbol> Results;
|
||||
for (auto &TopLevel : AST.getLocalTopLevelDecls())
|
||||
traverseDecl(TopLevel, Results);
|
||||
return Results;
|
||||
}
|
||||
|
||||
private:
|
||||
enum class VisitKind { No, OnlyDecl, DeclAndChildren };
|
||||
|
||||
void traverseDecl(Decl *D, std::vector<DocumentSymbol> &Results) {
|
||||
if (auto *Templ = llvm::dyn_cast<TemplateDecl>(D))
|
||||
D = Templ->getTemplatedDecl();
|
||||
auto *ND = llvm::dyn_cast<NamedDecl>(D);
|
||||
if (!ND)
|
||||
return;
|
||||
auto FilePath = getRealPath(F, SM);
|
||||
if (FilePath)
|
||||
MainFileUri = URIForFile(*FilePath);
|
||||
VisitKind Visit = shouldVisit(ND);
|
||||
if (Visit == VisitKind::No)
|
||||
return;
|
||||
llvm::Optional<DocumentSymbol> Sym = declToSym(AST.getASTContext(), *ND);
|
||||
if (!Sym)
|
||||
return;
|
||||
if (Visit == VisitKind::DeclAndChildren)
|
||||
traverseChildren(D, Sym->children);
|
||||
Results.push_back(std::move(*Sym));
|
||||
}
|
||||
|
||||
bool shouldIncludeSymbol(const NamedDecl *ND) {
|
||||
if (!ND || ND->isImplicit())
|
||||
return false;
|
||||
// Skip anonymous declarations, e.g (anonymous enum/class/struct).
|
||||
if (ND->getDeclName().isEmpty())
|
||||
return false;
|
||||
return true;
|
||||
void traverseChildren(Decl *D, std::vector<DocumentSymbol> &Results) {
|
||||
auto *Scope = llvm::dyn_cast<DeclContext>(D);
|
||||
if (!Scope)
|
||||
return;
|
||||
for (auto *C : Scope->decls())
|
||||
traverseDecl(C, Results);
|
||||
}
|
||||
|
||||
bool
|
||||
handleDeclOccurence(const Decl *, index::SymbolRoleSet Roles,
|
||||
ArrayRef<index::SymbolRelation> Relations,
|
||||
SourceLocation Loc,
|
||||
index::IndexDataConsumer::ASTNodeInfo ASTNode) override {
|
||||
assert(ASTNode.OrigD);
|
||||
// No point in continuing the index consumer if we could not get the
|
||||
// absolute path of the main file.
|
||||
if (!MainFileUri)
|
||||
return false;
|
||||
// We only want declarations and definitions, i.e. no references.
|
||||
if (!(Roles & static_cast<unsigned>(index::SymbolRole::Declaration) ||
|
||||
Roles & static_cast<unsigned>(index::SymbolRole::Definition)))
|
||||
return true;
|
||||
SourceLocation NameLoc = findNameLoc(ASTNode.OrigD);
|
||||
const SourceManager &SourceMgr = AST.getSourceManager();
|
||||
// We should be only be looking at "local" decls in the main file.
|
||||
if (!SourceMgr.isWrittenInMainFile(NameLoc)) {
|
||||
// Even thought we are visiting only local (non-preamble) decls,
|
||||
// we can get here when in the presence of "extern" decls.
|
||||
return true;
|
||||
VisitKind shouldVisit(NamedDecl *D) {
|
||||
if (D->isImplicit())
|
||||
return VisitKind::No;
|
||||
|
||||
if (auto Func = llvm::dyn_cast<FunctionDecl>(D)) {
|
||||
// Some functions are implicit template instantiations, those should be
|
||||
// ignored.
|
||||
if (auto *Info = Func->getTemplateSpecializationInfo()) {
|
||||
if (!Info->isExplicitInstantiationOrSpecialization())
|
||||
return VisitKind::No;
|
||||
}
|
||||
// Only visit the function itself, do not visit the children (i.e.
|
||||
// function parameters, etc.)
|
||||
return VisitKind::OnlyDecl;
|
||||
}
|
||||
const NamedDecl *ND = dyn_cast<NamedDecl>(ASTNode.OrigD);
|
||||
if (!shouldIncludeSymbol(ND))
|
||||
return true;
|
||||
|
||||
SourceLocation EndLoc =
|
||||
Lexer::getLocForEndOfToken(NameLoc, 0, SourceMgr, AST.getLangOpts());
|
||||
Position Begin = sourceLocToPosition(SourceMgr, NameLoc);
|
||||
Position End = sourceLocToPosition(SourceMgr, EndLoc);
|
||||
Range R = {Begin, End};
|
||||
Location L;
|
||||
L.uri = *MainFileUri;
|
||||
L.range = R;
|
||||
|
||||
std::string QName = printQualifiedName(*ND);
|
||||
StringRef Scope, Name;
|
||||
std::tie(Scope, Name) = splitQualifiedName(QName);
|
||||
Scope.consume_back("::");
|
||||
|
||||
index::SymbolInfo SymInfo = index::getSymbolInfo(ND);
|
||||
SymbolKind SK = indexSymbolKindToSymbolKind(SymInfo.Kind);
|
||||
|
||||
SymbolInformation SI;
|
||||
SI.name = Name;
|
||||
SI.kind = SK;
|
||||
SI.location = L;
|
||||
SI.containerName = Scope;
|
||||
Symbols.push_back(std::move(SI));
|
||||
return true;
|
||||
// Handle template instantiations. We have three cases to consider:
|
||||
// - explicit instantiations, e.g. 'template class std::vector<int>;'
|
||||
// Visit the decl itself (it's present in the code), but not the
|
||||
// children.
|
||||
// - implicit instantiations, i.e. not written by the user.
|
||||
// Do not visit at all, they are not present in the code.
|
||||
// - explicit specialization, e.g. 'template <> class vector<bool> {};'
|
||||
// Visit both the decl and its children, both are written in the code.
|
||||
if (auto *TemplSpec = llvm::dyn_cast<ClassTemplateSpecializationDecl>(D)) {
|
||||
if (TemplSpec->isExplicitInstantiationOrSpecialization())
|
||||
return TemplSpec->isExplicitSpecialization()
|
||||
? VisitKind::DeclAndChildren
|
||||
: VisitKind::OnlyDecl;
|
||||
return VisitKind::No;
|
||||
}
|
||||
if (auto *TemplSpec = llvm::dyn_cast<VarTemplateSpecializationDecl>(D)) {
|
||||
if (TemplSpec->isExplicitInstantiationOrSpecialization())
|
||||
return TemplSpec->isExplicitSpecialization()
|
||||
? VisitKind::DeclAndChildren
|
||||
: VisitKind::OnlyDecl;
|
||||
return VisitKind::No;
|
||||
}
|
||||
// For all other cases, visit both the children and the decl.
|
||||
return VisitKind::DeclAndChildren;
|
||||
}
|
||||
|
||||
ParsedAST &AST;
|
||||
};
|
||||
|
||||
std::vector<DocumentSymbol> collectDocSymbols(ParsedAST &AST) {
|
||||
return DocumentOutline(AST).build();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Expected<std::vector<SymbolInformation>> getDocumentSymbols(ParsedAST &AST) {
|
||||
DocumentSymbolsConsumer DocumentSymbolsCons(AST.getASTContext());
|
||||
|
||||
index::IndexingOptions IndexOpts;
|
||||
IndexOpts.SystemSymbolFilter =
|
||||
index::IndexingOptions::SystemSymbolFilterKind::DeclarationsOnly;
|
||||
IndexOpts.IndexFunctionLocals = false;
|
||||
indexTopLevelDecls(AST.getASTContext(), AST.getPreprocessor(),
|
||||
AST.getLocalTopLevelDecls(), DocumentSymbolsCons,
|
||||
IndexOpts);
|
||||
|
||||
return DocumentSymbolsCons.takeSymbols();
|
||||
llvm::Expected<std::vector<DocumentSymbol>> getDocumentSymbols(ParsedAST &AST) {
|
||||
return collectDocSymbols(AST);
|
||||
}
|
||||
|
||||
} // namespace clangd
|
||||
|
@ -36,8 +36,7 @@ getWorkspaceSymbols(llvm::StringRef Query, int Limit,
|
||||
|
||||
/// Retrieves the symbols contained in the "main file" section of an AST in the
|
||||
/// same order that they appear.
|
||||
llvm::Expected<std::vector<SymbolInformation>>
|
||||
getDocumentSymbols(ParsedAST &AST);
|
||||
llvm::Expected<std::vector<DocumentSymbol>> getDocumentSymbols(ParsedAST &AST);
|
||||
|
||||
} // namespace clangd
|
||||
} // namespace clang
|
||||
|
@ -18,6 +18,7 @@
|
||||
#include "llvm/ADT/SmallString.h"
|
||||
#include "llvm/Support/Format.h"
|
||||
#include "llvm/Support/FormatVariadic.h"
|
||||
#include "llvm/Support/JSON.h"
|
||||
#include "llvm/Support/Path.h"
|
||||
#include "llvm/Support/raw_ostream.h"
|
||||
|
||||
@ -222,6 +223,11 @@ bool fromJSON(const json::Value &Params, ClientCapabilities &R) {
|
||||
if (CodeAction->getObject("codeActionLiteralSupport"))
|
||||
R.CodeActionStructure = true;
|
||||
}
|
||||
if (auto *DocumentSymbol = TextDocument->getObject("documentSymbol")) {
|
||||
if (auto HierarchicalSupport =
|
||||
DocumentSymbol->getBoolean("hierarchicalDocumentSymbolSupport"))
|
||||
R.HierarchicalDocumentSymbol = *HierarchicalSupport;
|
||||
}
|
||||
}
|
||||
if (auto *Workspace = O->getObject("workspace")) {
|
||||
if (auto *Symbol = Workspace->getObject("symbol")) {
|
||||
@ -449,6 +455,25 @@ json::Value toJSON(const CodeAction &CA) {
|
||||
return std::move(CodeAction);
|
||||
}
|
||||
|
||||
llvm::raw_ostream &operator<<(llvm::raw_ostream &O, const DocumentSymbol &S) {
|
||||
return O << S.name << " - " << toJSON(S);
|
||||
}
|
||||
|
||||
llvm::json::Value toJSON(const DocumentSymbol &S) {
|
||||
json::Object Result{{"name", S.name},
|
||||
{"kind", static_cast<int>(S.kind)},
|
||||
{"range", S.range},
|
||||
{"selectionRange", S.selectionRange}};
|
||||
|
||||
if (!S.detail.empty())
|
||||
Result["detail"] = S.detail;
|
||||
if (!S.children.empty())
|
||||
Result["children"] = S.children;
|
||||
if (S.deprecated)
|
||||
Result["deprecated"] = true;
|
||||
return Result;
|
||||
}
|
||||
|
||||
json::Value toJSON(const WorkspaceEdit &WE) {
|
||||
if (!WE.changes)
|
||||
return json::Object{};
|
||||
|
@ -150,6 +150,9 @@ struct Range {
|
||||
}
|
||||
|
||||
bool contains(Position Pos) const { return start <= Pos && Pos < end; }
|
||||
bool contains(Range Rng) const {
|
||||
return start <= Rng.start && Rng.end <= end;
|
||||
}
|
||||
};
|
||||
bool fromJSON(const llvm::json::Value &, Range &);
|
||||
llvm::json::Value toJSON(const Range &);
|
||||
@ -331,6 +334,9 @@ struct ClientCapabilities {
|
||||
/// textDocument.completion.completionItem.snippetSupport
|
||||
bool CompletionSnippets = false;
|
||||
|
||||
/// Client supports hierarchical document symbols.
|
||||
bool HierarchicalDocumentSymbol = false;
|
||||
|
||||
/// The supported set of CompletionItemKinds for textDocument/completion.
|
||||
/// textDocument.completion.completionItemKind.valueSet
|
||||
llvm::Optional<CompletionItemKindBitset> CompletionItemKinds;
|
||||
@ -655,6 +661,39 @@ struct CodeAction {
|
||||
};
|
||||
llvm::json::Value toJSON(const CodeAction &);
|
||||
|
||||
/// Represents programming constructs like variables, classes, interfaces etc.
|
||||
/// that appear in a document. Document symbols can be hierarchical and they
|
||||
/// have two ranges: one that encloses its definition and one that points to its
|
||||
/// most interesting range, e.g. the range of an identifier.
|
||||
struct DocumentSymbol {
|
||||
/// The name of this symbol.
|
||||
std::string name;
|
||||
|
||||
/// More detail for this symbol, e.g the signature of a function.
|
||||
std::string detail;
|
||||
|
||||
/// The kind of this symbol.
|
||||
SymbolKind kind;
|
||||
|
||||
/// Indicates if this symbol is deprecated.
|
||||
bool deprecated;
|
||||
|
||||
/// The range enclosing this symbol not including leading/trailing whitespace
|
||||
/// but everything else like comments. This information is typically used to
|
||||
/// determine if the clients cursor is inside the symbol to reveal in the
|
||||
/// symbol in the UI.
|
||||
Range range;
|
||||
|
||||
/// The range that should be selected and revealed when this symbol is being
|
||||
/// picked, e.g the name of a function. Must be contained by the `range`.
|
||||
Range selectionRange;
|
||||
|
||||
/// Children of this symbol, e.g. properties of a class.
|
||||
std::vector<DocumentSymbol> children;
|
||||
};
|
||||
llvm::raw_ostream &operator<<(llvm::raw_ostream &O, const DocumentSymbol &S);
|
||||
llvm::json::Value toJSON(const DocumentSymbol &S);
|
||||
|
||||
/// Represents information about programming constructs like variables, classes,
|
||||
/// interfaces etc.
|
||||
struct SymbolInformation {
|
||||
|
@ -6,7 +6,7 @@
|
||||
"publisher": "llvm-vs-code-extensions",
|
||||
"homepage": "https://clang.llvm.org/extra/clangd.html",
|
||||
"engines": {
|
||||
"vscode": "^1.18.0"
|
||||
"vscode": "^1.27.0"
|
||||
},
|
||||
"categories": [
|
||||
"Programming Languages",
|
||||
@ -32,8 +32,8 @@
|
||||
"test": "node ./node_modules/vscode/bin/test"
|
||||
},
|
||||
"dependencies": {
|
||||
"vscode-languageclient": "^4.0.0",
|
||||
"vscode-languageserver": "^4.0.0"
|
||||
"vscode-languageclient": "^5.1.0",
|
||||
"vscode-languageserver": "^5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^2.0.3",
|
||||
|
@ -14,6 +14,8 @@
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace llvm;
|
||||
|
||||
namespace clang {
|
||||
namespace clangd {
|
||||
|
||||
@ -23,6 +25,7 @@ using ::testing::AllOf;
|
||||
using ::testing::AnyOf;
|
||||
using ::testing::ElementsAre;
|
||||
using ::testing::ElementsAreArray;
|
||||
using ::testing::Field;
|
||||
using ::testing::IsEmpty;
|
||||
using ::testing::UnorderedElementsAre;
|
||||
|
||||
@ -37,9 +40,17 @@ MATCHER_P(QName, Name, "") {
|
||||
return arg.name == Name;
|
||||
return (arg.containerName + "::" + arg.name) == Name;
|
||||
}
|
||||
MATCHER_P(WithName, N, "") { return arg.name == N; }
|
||||
MATCHER_P(WithKind, Kind, "") { return arg.kind == Kind; }
|
||||
MATCHER_P(SymRange, Range, "") { return arg.location.range == Range; }
|
||||
|
||||
// GMock helpers for matching DocumentSymbol.
|
||||
MATCHER_P(SymNameRange, Range, "") { return arg.selectionRange == Range; }
|
||||
template <class... ChildMatchers>
|
||||
testing::Matcher<DocumentSymbol> Children(ChildMatchers... ChildrenM) {
|
||||
return Field(&DocumentSymbol::children, ElementsAre(ChildrenM...));
|
||||
}
|
||||
|
||||
ClangdServer::Options optsForTests() {
|
||||
auto ServerOpts = ClangdServer::optsForTest();
|
||||
ServerOpts.WorkspaceRoot = testRoot();
|
||||
@ -300,7 +311,7 @@ protected:
|
||||
IgnoreDiagnostics DiagConsumer;
|
||||
ClangdServer Server;
|
||||
|
||||
std::vector<SymbolInformation> getSymbols(PathRef File) {
|
||||
std::vector<DocumentSymbol> getSymbols(PathRef File) {
|
||||
EXPECT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for preamble";
|
||||
auto SymbolInfos = runDocumentSymbols(Server, File);
|
||||
EXPECT_TRUE(bool(SymbolInfos)) << "documentSymbols returned an error";
|
||||
@ -363,31 +374,46 @@ TEST_F(DocumentSymbolsTest, BasicSymbols) {
|
||||
)");
|
||||
|
||||
addFile(FilePath, Main.code());
|
||||
EXPECT_THAT(getSymbols(FilePath),
|
||||
ElementsAreArray(
|
||||
{AllOf(QName("Foo"), WithKind(SymbolKind::Class)),
|
||||
AllOf(QName("Foo"), WithKind(SymbolKind::Class)),
|
||||
AllOf(QName("Foo::Foo"), WithKind(SymbolKind::Method)),
|
||||
AllOf(QName("Foo::Foo"), WithKind(SymbolKind::Method)),
|
||||
AllOf(QName("Foo::f"), WithKind(SymbolKind::Method)),
|
||||
AllOf(QName("f1"), WithKind(SymbolKind::Function)),
|
||||
AllOf(QName("Foo::operator="), WithKind(SymbolKind::Method)),
|
||||
AllOf(QName("Foo::~Foo"), WithKind(SymbolKind::Method)),
|
||||
AllOf(QName("Foo::Nested"), WithKind(SymbolKind::Class)),
|
||||
AllOf(QName("Foo::Nested::f"), WithKind(SymbolKind::Method)),
|
||||
AllOf(QName("Friend"), WithKind(SymbolKind::Class)),
|
||||
AllOf(QName("f1"), WithKind(SymbolKind::Function)),
|
||||
AllOf(QName("f2"), WithKind(SymbolKind::Function)),
|
||||
AllOf(QName("KInt"), WithKind(SymbolKind::Variable)),
|
||||
AllOf(QName("kStr"), WithKind(SymbolKind::Variable)),
|
||||
AllOf(QName("f1"), WithKind(SymbolKind::Function)),
|
||||
AllOf(QName("foo"), WithKind(SymbolKind::Namespace)),
|
||||
AllOf(QName("foo::int32"), WithKind(SymbolKind::Class)),
|
||||
AllOf(QName("foo::int32_t"), WithKind(SymbolKind::Class)),
|
||||
AllOf(QName("foo::v1"), WithKind(SymbolKind::Variable)),
|
||||
AllOf(QName("foo::bar"), WithKind(SymbolKind::Namespace)),
|
||||
AllOf(QName("foo::bar::v2"), WithKind(SymbolKind::Variable)),
|
||||
AllOf(QName("foo::baz"), WithKind(SymbolKind::Namespace))}));
|
||||
EXPECT_THAT(
|
||||
getSymbols(FilePath),
|
||||
ElementsAreArray(
|
||||
{AllOf(WithName("Foo"), WithKind(SymbolKind::Class), Children()),
|
||||
AllOf(WithName("Foo"), WithKind(SymbolKind::Class),
|
||||
Children(AllOf(WithName("Foo"), WithKind(SymbolKind::Method),
|
||||
Children()),
|
||||
AllOf(WithName("Foo"), WithKind(SymbolKind::Method),
|
||||
Children()),
|
||||
AllOf(WithName("f"), WithKind(SymbolKind::Method),
|
||||
Children()),
|
||||
AllOf(WithName("operator="),
|
||||
WithKind(SymbolKind::Method), Children()),
|
||||
AllOf(WithName("~Foo"), WithKind(SymbolKind::Method),
|
||||
Children()),
|
||||
AllOf(WithName("Nested"), WithKind(SymbolKind::Class),
|
||||
Children(AllOf(WithName("f"),
|
||||
WithKind(SymbolKind::Method),
|
||||
Children()))))),
|
||||
AllOf(WithName("Friend"), WithKind(SymbolKind::Class), Children()),
|
||||
AllOf(WithName("f1"), WithKind(SymbolKind::Function), Children()),
|
||||
AllOf(WithName("f2"), WithKind(SymbolKind::Function), Children()),
|
||||
AllOf(WithName("KInt"), WithKind(SymbolKind::Variable), Children()),
|
||||
AllOf(WithName("kStr"), WithKind(SymbolKind::Variable), Children()),
|
||||
AllOf(WithName("f1"), WithKind(SymbolKind::Function), Children()),
|
||||
AllOf(WithName("foo"), WithKind(SymbolKind::Namespace),
|
||||
Children(
|
||||
AllOf(WithName("int32"), WithKind(SymbolKind::Class),
|
||||
Children()),
|
||||
AllOf(WithName("int32_t"), WithKind(SymbolKind::Class),
|
||||
Children()),
|
||||
AllOf(WithName("v1"), WithKind(SymbolKind::Variable),
|
||||
Children()),
|
||||
AllOf(WithName("bar"), WithKind(SymbolKind::Namespace),
|
||||
Children(AllOf(WithName("v2"),
|
||||
WithKind(SymbolKind::Variable),
|
||||
Children()))),
|
||||
AllOf(WithName("baz"), WithKind(SymbolKind::Namespace),
|
||||
Children()),
|
||||
AllOf(WithName("v2"), WithKind(SymbolKind::Variable))))}));
|
||||
}
|
||||
|
||||
TEST_F(DocumentSymbolsTest, DeclarationDefinition) {
|
||||
@ -402,11 +428,12 @@ TEST_F(DocumentSymbolsTest, DeclarationDefinition) {
|
||||
|
||||
addFile(FilePath, Main.code());
|
||||
EXPECT_THAT(getSymbols(FilePath),
|
||||
ElementsAre(AllOf(QName("Foo"), WithKind(SymbolKind::Class)),
|
||||
AllOf(QName("Foo::f"), WithKind(SymbolKind::Method),
|
||||
SymRange(Main.range("decl"))),
|
||||
AllOf(QName("Foo::f"), WithKind(SymbolKind::Method),
|
||||
SymRange(Main.range("def")))));
|
||||
ElementsAre(AllOf(WithName("Foo"), WithKind(SymbolKind::Class),
|
||||
Children(AllOf(
|
||||
WithName("f"), WithKind(SymbolKind::Method),
|
||||
SymNameRange(Main.range("decl"))))),
|
||||
AllOf(WithName("f"), WithKind(SymbolKind::Method),
|
||||
SymNameRange(Main.range("def")))));
|
||||
}
|
||||
|
||||
TEST_F(DocumentSymbolsTest, ExternSymbol) {
|
||||
@ -429,7 +456,7 @@ TEST_F(DocumentSymbolsTest, NoLocals) {
|
||||
struct LocalClass {};
|
||||
int local_var;
|
||||
})cpp");
|
||||
EXPECT_THAT(getSymbols(FilePath), ElementsAre(QName("test")));
|
||||
EXPECT_THAT(getSymbols(FilePath), ElementsAre(WithName("test")));
|
||||
}
|
||||
|
||||
TEST_F(DocumentSymbolsTest, Unnamed) {
|
||||
@ -442,9 +469,12 @@ TEST_F(DocumentSymbolsTest, Unnamed) {
|
||||
)cpp");
|
||||
EXPECT_THAT(
|
||||
getSymbols(FilePath),
|
||||
ElementsAre(AllOf(QName("UnnamedStruct"), WithKind(SymbolKind::Variable)),
|
||||
AllOf(QName("(anonymous struct)::InUnnamed"),
|
||||
WithKind(SymbolKind::Field))));
|
||||
ElementsAre(
|
||||
AllOf(WithName("(anonymous struct)"), WithKind(SymbolKind::Struct),
|
||||
Children(AllOf(WithName("InUnnamed"),
|
||||
WithKind(SymbolKind::Field), Children()))),
|
||||
AllOf(WithName("UnnamedStruct"), WithKind(SymbolKind::Variable),
|
||||
Children())));
|
||||
}
|
||||
|
||||
TEST_F(DocumentSymbolsTest, InHeaderFile) {
|
||||
@ -461,23 +491,46 @@ TEST_F(DocumentSymbolsTest, InHeaderFile) {
|
||||
addFile("foo.cpp", R"cpp(
|
||||
#include "foo.h"
|
||||
)cpp");
|
||||
EXPECT_THAT(getSymbols(FilePath), ElementsAre(QName("test")));
|
||||
EXPECT_THAT(getSymbols(FilePath), ElementsAre(WithName("test")));
|
||||
}
|
||||
|
||||
TEST_F(DocumentSymbolsTest, Template) {
|
||||
std::string FilePath = testPath("foo.cpp");
|
||||
addFile(FilePath, R"(
|
||||
// Primary templates and specializations are included but instantiations
|
||||
// are not.
|
||||
template <class T> struct Tmpl {T x = 0;};
|
||||
template <> struct Tmpl<int> {};
|
||||
template <> struct Tmpl<int> {
|
||||
int y = 0;
|
||||
};
|
||||
extern template struct Tmpl<float>;
|
||||
template struct Tmpl<double>;
|
||||
|
||||
template <class T, class U, class Z = float>
|
||||
int funcTmpl(U a);
|
||||
template <>
|
||||
int funcTmpl<int>(double a);
|
||||
|
||||
template <class T, class U = double>
|
||||
int varTmpl = T();
|
||||
template <>
|
||||
double varTmpl<int> = 10.0;
|
||||
)");
|
||||
EXPECT_THAT(getSymbols(FilePath),
|
||||
ElementsAre(AllOf(QName("Tmpl"), WithKind(SymbolKind::Struct)),
|
||||
AllOf(QName("Tmpl::x"), WithKind(SymbolKind::Field)),
|
||||
AllOf(QName("Tmpl"), WithKind(SymbolKind::Struct))));
|
||||
EXPECT_THAT(
|
||||
getSymbols(FilePath),
|
||||
ElementsAre(
|
||||
AllOf(WithName("Tmpl"), WithKind(SymbolKind::Struct),
|
||||
Children(AllOf(WithName("x"), WithKind(SymbolKind::Field)))),
|
||||
AllOf(WithName("Tmpl<int>"), WithKind(SymbolKind::Struct),
|
||||
Children(WithName("y"))),
|
||||
AllOf(WithName("Tmpl<float>"), WithKind(SymbolKind::Struct),
|
||||
Children()),
|
||||
AllOf(WithName("Tmpl<double>"), WithKind(SymbolKind::Struct),
|
||||
Children()),
|
||||
AllOf(WithName("funcTmpl"), Children()),
|
||||
// FIXME(ibiryukov): template args should be <int> to match the code.
|
||||
AllOf(WithName("funcTmpl<int, double, float>"), Children()),
|
||||
AllOf(WithName("varTmpl"), Children()),
|
||||
// FIXME(ibiryukov): template args should be <int> to match the code.
|
||||
AllOf(WithName("varTmpl<int, double>"), Children())));
|
||||
}
|
||||
|
||||
TEST_F(DocumentSymbolsTest, Namespaces) {
|
||||
@ -507,10 +560,15 @@ TEST_F(DocumentSymbolsTest, Namespaces) {
|
||||
)cpp");
|
||||
EXPECT_THAT(
|
||||
getSymbols(FilePath),
|
||||
ElementsAreArray({QName("ans1"), QName("ans1::ai1"), QName("ans1::ans2"),
|
||||
QName("ans1::ans2::ai2"), QName("test"), QName("na"),
|
||||
QName("na::nb"), QName("na::Foo"), QName("na"),
|
||||
QName("na::nb"), QName("na::Bar")}));
|
||||
ElementsAreArray<testing::Matcher<DocumentSymbol>>(
|
||||
{AllOf(WithName("ans1"),
|
||||
Children(AllOf(WithName("ai1"), Children()),
|
||||
AllOf(WithName("ans2"), Children(WithName("ai2"))))),
|
||||
AllOf(WithName("(anonymous namespace)"), Children(WithName("test"))),
|
||||
AllOf(WithName("na"),
|
||||
Children(AllOf(WithName("nb"), Children(WithName("Foo"))))),
|
||||
AllOf(WithName("na"),
|
||||
Children(AllOf(WithName("nb"), Children(WithName("Bar")))))}));
|
||||
}
|
||||
|
||||
TEST_F(DocumentSymbolsTest, Enums) {
|
||||
@ -531,10 +589,14 @@ TEST_F(DocumentSymbolsTest, Enums) {
|
||||
};
|
||||
}
|
||||
)");
|
||||
EXPECT_THAT(getSymbols(FilePath),
|
||||
ElementsAre(QName("Red"), QName("Color"), QName("Green"),
|
||||
QName("Color2"), QName("Color2::Yellow"), QName("ns"),
|
||||
QName("ns::Black")));
|
||||
EXPECT_THAT(
|
||||
getSymbols(FilePath),
|
||||
ElementsAre(
|
||||
AllOf(WithName("(anonymous enum)"), Children(WithName("Red"))),
|
||||
AllOf(WithName("Color"), Children(WithName("Green"))),
|
||||
AllOf(WithName("Color2"), Children(WithName("Yellow"))),
|
||||
AllOf(WithName("ns"), Children(AllOf(WithName("(anonymous enum)"),
|
||||
Children(WithName("Black")))))));
|
||||
}
|
||||
|
||||
TEST_F(DocumentSymbolsTest, FromMacro) {
|
||||
@ -553,8 +615,43 @@ TEST_F(DocumentSymbolsTest, FromMacro) {
|
||||
addFile(FilePath, Main.code());
|
||||
EXPECT_THAT(
|
||||
getSymbols(FilePath),
|
||||
ElementsAre(AllOf(QName("abc_Test"), SymRange(Main.range("expansion"))),
|
||||
AllOf(QName("Test"), SymRange(Main.range("spelling")))));
|
||||
ElementsAre(
|
||||
AllOf(WithName("abc_Test"), SymNameRange(Main.range("expansion"))),
|
||||
AllOf(WithName("Test"), SymNameRange(Main.range("spelling")))));
|
||||
}
|
||||
|
||||
TEST_F(DocumentSymbolsTest, FuncTemplates) {
|
||||
std::string FilePath = testPath("foo.cpp");
|
||||
Annotations Source(R"cpp(
|
||||
template <class T>
|
||||
T foo() {}
|
||||
|
||||
auto x = foo<int>();
|
||||
auto y = foo<double>()
|
||||
)cpp");
|
||||
addFile(FilePath, Source.code());
|
||||
// Make sure we only see the template declaration, not instantiations.
|
||||
EXPECT_THAT(getSymbols(FilePath),
|
||||
ElementsAre(WithName("foo"), WithName("x"), WithName("y")));
|
||||
}
|
||||
|
||||
TEST_F(DocumentSymbolsTest, UsingDirectives) {
|
||||
std::string FilePath = testPath("foo.cpp");
|
||||
Annotations Source(R"cpp(
|
||||
namespace ns {
|
||||
int foo;
|
||||
}
|
||||
|
||||
namespace ns_alias = ns;
|
||||
|
||||
using namespace ::ns; // check we don't loose qualifiers.
|
||||
using namespace ns_alias; // and namespace aliases.
|
||||
)cpp");
|
||||
addFile(FilePath, Source.code());
|
||||
EXPECT_THAT(getSymbols(FilePath),
|
||||
ElementsAre(WithName("ns"), WithName("ns_alias"),
|
||||
WithName("using namespace ::ns"),
|
||||
WithName("using namespace ns_alias")));
|
||||
}
|
||||
|
||||
} // namespace clangd
|
||||
|
@ -120,9 +120,9 @@ runWorkspaceSymbols(ClangdServer &Server, StringRef Query, int Limit) {
|
||||
return std::move(*Result);
|
||||
}
|
||||
|
||||
Expected<std::vector<SymbolInformation>>
|
||||
runDocumentSymbols(ClangdServer &Server, PathRef File) {
|
||||
Optional<Expected<std::vector<SymbolInformation>>> Result;
|
||||
Expected<std::vector<DocumentSymbol>> runDocumentSymbols(ClangdServer &Server,
|
||||
PathRef File) {
|
||||
Optional<Expected<std::vector<DocumentSymbol>>> Result;
|
||||
Server.documentSymbols(File, capture(Result));
|
||||
return std::move(*Result);
|
||||
}
|
||||
|
@ -47,8 +47,8 @@ std::string runDumpAST(ClangdServer &Server, PathRef File);
|
||||
llvm::Expected<std::vector<SymbolInformation>>
|
||||
runWorkspaceSymbols(ClangdServer &Server, StringRef Query, int Limit);
|
||||
|
||||
llvm::Expected<std::vector<SymbolInformation>>
|
||||
runDocumentSymbols(ClangdServer &Server, PathRef File);
|
||||
Expected<std::vector<DocumentSymbol>> runDocumentSymbols(ClangdServer &Server,
|
||||
PathRef File);
|
||||
|
||||
SymbolSlab runFuzzyFind(const SymbolIndex &Index, StringRef Query);
|
||||
SymbolSlab runFuzzyFind(const SymbolIndex &Index, const FuzzyFindRequest &Req);
|
||||
|
Loading…
x
Reference in New Issue
Block a user