Index: clangd/CodeComplete.h =================================================================== --- clangd/CodeComplete.h +++ clangd/CodeComplete.h @@ -19,6 +19,7 @@ #include "Logger.h" #include "Path.h" #include "Protocol.h" +#include "index/Index.h" #include "clang/Frontend/PrecompiledPreamble.h" #include "clang/Sema/CodeCompleteOptions.h" #include "clang/Tooling/CompilationDatabase.h" @@ -59,6 +60,11 @@ /// Limit the number of results returned (0 means no limit). /// If more results are available, we set CompletionList.isIncomplete. size_t Limit = 0; + + // Populated internally by clangd, do not set. + /// If `Index` is set, it is used to augment the code completion + /// results. + const SymbolIndex *Index = nullptr; }; /// Get code completions at a specified \p Pos in \p FileName. Index: clangd/CodeComplete.cpp =================================================================== --- clangd/CodeComplete.cpp +++ clangd/CodeComplete.cpp @@ -16,6 +16,8 @@ #include "CodeComplete.h" #include "Compiler.h" +#include "Logger.h" +#include "index/Index.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/Frontend/FrontendActions.h" #include "clang/Sema/CodeCompleteConsumer.h" @@ -26,26 +28,28 @@ namespace clangd { namespace { -CompletionItemKind getKindOfDecl(CXCursorKind CursorKind) { +CompletionItemKind toCompletionItemKind(CXCursorKind CursorKind) { switch (CursorKind) { case CXCursor_MacroInstantiation: case CXCursor_MacroDefinition: return CompletionItemKind::Text; case CXCursor_CXXMethod: + case CXCursor_Destructor: return CompletionItemKind::Method; case CXCursor_FunctionDecl: case CXCursor_FunctionTemplate: return CompletionItemKind::Function; case CXCursor_Constructor: - case CXCursor_Destructor: return CompletionItemKind::Constructor; case CXCursor_FieldDecl: return CompletionItemKind::Field; case CXCursor_VarDecl: case CXCursor_ParmDecl: return CompletionItemKind::Variable; - case CXCursor_ClassDecl: + // FIXME(ioeric): use LSP struct instead of class when it is suppoted in the + // protocol. case CXCursor_StructDecl: + case CXCursor_ClassDecl: case CXCursor_UnionDecl: case CXCursor_ClassTemplate: case CXCursor_ClassTemplatePartialSpecialization: @@ -58,6 +62,7 @@ return CompletionItemKind::Value; case CXCursor_EnumDecl: return CompletionItemKind::Enum; + // FIXME(ioeric): figure out whether reference is the right type for aliases. case CXCursor_TypeAliasDecl: case CXCursor_TypeAliasTemplateDecl: case CXCursor_TypedefDecl: @@ -69,11 +74,12 @@ } } -CompletionItemKind getKind(CodeCompletionResult::ResultKind ResKind, - CXCursorKind CursorKind) { +CompletionItemKind +toCompletionItemKind(CodeCompletionResult::ResultKind ResKind, + CXCursorKind CursorKind) { switch (ResKind) { case CodeCompletionResult::RK_Declaration: - return getKindOfDecl(CursorKind); + return toCompletionItemKind(CursorKind); case CodeCompletionResult::RK_Keyword: return CompletionItemKind::Keyword; case CodeCompletionResult::RK_Macro: @@ -85,6 +91,59 @@ llvm_unreachable("Unhandled CodeCompletionResult::ResultKind."); } +CompletionItemKind toCompletionItemKind(index::SymbolKind Kind) { + using SK = index::SymbolKind; + switch (Kind) { + case SK::Unknown: + return CompletionItemKind::Missing; + case SK::Module: + case SK::Namespace: + case SK::NamespaceAlias: + return CompletionItemKind::Module; + case SK::Macro: + return CompletionItemKind::Text; + case SK::Enum: + return CompletionItemKind::Enum; + // FIXME(ioeric): use LSP struct instead of class when it is suppoted in the + // protocol. + case SK::Struct: + case SK::Class: + case SK::Protocol: + case SK::Extension: + case SK::Union: + return CompletionItemKind::Class; + // FIXME(ioeric): figure out whether reference is the right type for aliases. + case SK::TypeAlias: + case SK::Using: + return CompletionItemKind::Reference; + case SK::Function: + // FIXME(ioeric): this should probably be an operator. This should be fixed + // when `Operator` is support type in the protocol. + case SK::ConversionFunction: + return CompletionItemKind::Function; + case SK::Variable: + case SK::Parameter: + return CompletionItemKind::Variable; + case SK::Field: + return CompletionItemKind::Field; + // FIXME(ioeric): use LSP enum constant when it is supported in the protocol. + case SK::EnumConstant: + return CompletionItemKind::Value; + case SK::InstanceMethod: + case SK::ClassMethod: + case SK::StaticMethod: + case SK::Destructor: + return CompletionItemKind::Method; + case SK::InstanceProperty: + case SK::ClassProperty: + case SK::StaticProperty: + return CompletionItemKind::Property; + case SK::Constructor: + return CompletionItemKind::Constructor; + } + llvm_unreachable("Unhandled clang::index::SymbolKind."); +} + std::string escapeSnippet(const llvm::StringRef Text) { std::string Result; Result.reserve(Text.size()); // Assume '$', '}' and '\\' are rare. @@ -228,20 +287,101 @@ } }; +/// \brief Information about the scope specifier in the qualified-id code +/// completion (e.g. "ns::ab?"). +struct SpecifiedScope { + /// The scope specifier as written. For example, for completion "ns::ab?", the + /// written scope specifier is "ns". + std::string Written; + // If this scope specifier is recognized in Sema (e.g. as a namespace + // context), this will be set to the fully qualfied name of the corresponding + // context. + std::string Resolved; +}; + +CompletionItem indexCompletionItem(const Symbol &Sym, llvm::StringRef Filter, + const SpecifiedScope &SSInfo) { + CompletionItem Item; + Item.kind = toCompletionItemKind(Sym.SymInfo.Kind); + Item.label = Sym.Name; + // FIXME(ioeric): support inserting/replacing scope qualifiers. + Item.insertText = Sym.Name; + // FIXME(ioeric): support snippets. + Item.insertTextFormat = InsertTextFormat::PlainText; + Item.filterText = Filter; + + // FIXME(ioeric): sort symbols appropriately. + Item.sortText = ""; + + // FIXME(ioeric): use more symbol information (e.g. documentation, label) to + // populate the completion item. + + return Item; +} + +void completeWithIndex(const Context &Ctx, const SymbolIndex &Index, + llvm::StringRef Code, const SpecifiedScope &SSInfo, + llvm::StringRef Filter, CompletionList *Items) { + FuzzyFindRequest Req; + Req.Query = Filter; + // FIXME(ioeric): add more possible scopes based on using namespaces and + // containing namespaces. + StringRef Scope = SSInfo.Resolved.empty() ? SSInfo.Written : SSInfo.Resolved; + Req.Scopes = {Scope.trim(':').str()}; + + Items->isIncomplete = !Index.fuzzyFind(Ctx, Req, [&](const Symbol &Sym) { + Items->items.push_back(indexCompletionItem(Sym, Filter, SSInfo)); + }); +} + +/// \brief Information from sema about (parital) symbol names to be completed. +/// For example, for completion "ns::ab^", this stores the scope specifier +/// "ns::" and the completion filter text "ab". +struct NameToComplete { + // The partial identifier being completed, without qualifier. + std::string Filter; + + /// This is set if the completion is for qualified IDs, e.g. "abc::x^". + llvm::Optional SSInfo; +}; + +SpecifiedScope extraCompletionScope(Sema &S, const CXXScopeSpec &SS) { + SpecifiedScope Info; + auto &SM = S.getSourceManager(); + auto SpecifierRange = SS.getRange(); + Info.Written = Lexer::getSourceText( + CharSourceRange::getCharRange(SpecifierRange), SM, clang::LangOptions()); + if (SS.isValid()) { + DeclContext *DC = S.computeDeclContext(SS); + if (auto *NS = llvm::dyn_cast(DC)) { + Info.Resolved = NS->getQualifiedNameAsString(); + } else if (auto *TU = llvm::dyn_cast(DC)) { + Info.Resolved = "::"; + // Sema does not include the suffix "::" in the range of SS, so we add + // it back here. + Info.Written = "::"; + } + } + return Info; +} + class CompletionItemsCollector : public CodeCompleteConsumer { public: CompletionItemsCollector(const CodeCompleteOptions &CodeCompleteOpts, - CompletionList &Items) + CompletionList &Items, NameToComplete &CompletedName) : CodeCompleteConsumer(CodeCompleteOpts.getClangCompleteOpts(), /*OutputIsBinary=*/false), ClangdOpts(CodeCompleteOpts), Items(Items), Allocator(std::make_shared()), - CCTUInfo(Allocator) {} + CCTUInfo(Allocator), CompletedName(CompletedName) {} void ProcessCodeCompleteResults(Sema &S, CodeCompletionContext Context, CodeCompletionResult *Results, unsigned NumResults) override final { - StringRef Filter = S.getPreprocessor().getCodeCompletionFilter(); + if (auto SS = Context.getCXXScopeSpecifier()) + CompletedName.SSInfo = extraCompletionScope(S, **SS); + + CompletedName.Filter = S.getPreprocessor().getCodeCompletionFilter(); std::priority_queue Candidates; for (unsigned I = 0; I < NumResults; ++I) { auto &Result = Results[I]; @@ -249,7 +389,8 @@ (Result.Availability == CXAvailability_NotAvailable || Result.Availability == CXAvailability_NotAccessible)) continue; - if (!Filter.empty() && !fuzzyMatch(S, Context, Filter, Result)) + if (!CompletedName.Filter.empty() && + !fuzzyMatch(S, Context, CompletedName.Filter, Result)) continue; Candidates.emplace(Result); if (ClangdOpts.Limit && Candidates.size() > ClangdOpts.Limit) { @@ -324,7 +465,8 @@ ProcessChunks(CCS, Item); // Fill in the kind field of the CompletionItem. - Item.kind = getKind(Candidate.Result->Kind, Candidate.Result->CursorKind); + Item.kind = toCompletionItemKind(Candidate.Result->Kind, + Candidate.Result->CursorKind); return Item; } @@ -336,7 +478,7 @@ CompletionList &Items; std::shared_ptr Allocator; CodeCompletionTUInfo CCTUInfo; - + NameToComplete &CompletedName; }; // CompletionItemsCollector bool isInformativeQualifierChunk(CodeCompletionString::Chunk const &Chunk) { @@ -349,8 +491,9 @@ public: PlainTextCompletionItemsCollector(const CodeCompleteOptions &CodeCompleteOpts, - CompletionList &Items) - : CompletionItemsCollector(CodeCompleteOpts, Items) {} + CompletionList &Items, + NameToComplete &CompletedName) + : CompletionItemsCollector(CodeCompleteOpts, Items, CompletedName) {} private: void ProcessChunks(const CodeCompletionString &CCS, @@ -385,8 +528,9 @@ public: SnippetCompletionItemsCollector(const CodeCompleteOptions &CodeCompleteOpts, - CompletionList &Items) - : CompletionItemsCollector(CodeCompleteOpts, Items) {} + CompletionList &Items, + NameToComplete &CompletedName) + : CompletionItemsCollector(CodeCompleteOpts, Items, CompletedName) {} private: void ProcessChunks(const CodeCompletionString &CCS, @@ -657,6 +801,9 @@ Result.IncludeGlobals = IncludeGlobals; Result.IncludeBriefComments = IncludeBriefComments; + // Enable index-based code completion when Index is provided. + Result.IncludeNamespaceLevelDecls = !Index; + return Result; } @@ -669,16 +816,24 @@ CodeCompleteOptions Opts) { CompletionList Results; std::unique_ptr Consumer; + NameToComplete CompletedName; if (Opts.EnableSnippets) { - Consumer = - llvm::make_unique(Opts, Results); + Consumer = llvm::make_unique( + Opts, Results, CompletedName); } else { - Consumer = - llvm::make_unique(Opts, Results); + Consumer = llvm::make_unique( + Opts, Results, CompletedName); } invokeCodeComplete(Ctx, std::move(Consumer), Opts.getClangCompleteOpts(), FileName, Command, Preamble, Contents, Pos, std::move(VFS), std::move(PCHs)); + if (Opts.Index && CompletedName.SSInfo) { + log(Ctx, "WARNING: Got completion results from sema for completion on " + "qualified ID while symbol index is provided."); + Results.items.clear(); + completeWithIndex(Ctx, *Opts.Index, Contents, *CompletedName.SSInfo, + CompletedName.Filter, &Results); + } return Results; } Index: clangd/index/FileIndex.h =================================================================== --- clangd/index/FileIndex.h +++ clangd/index/FileIndex.h @@ -58,9 +58,9 @@ public: /// \brief Update symbols in \p Path with symbols in \p AST. If \p AST is /// nullptr, this removes all symbols in the file - void update(Context &Ctx, PathRef Path, ParsedAST *AST); + void update(const Context &Ctx, PathRef Path, ParsedAST *AST); - bool fuzzyFind(Context &Ctx, const FuzzyFindRequest &Req, + bool fuzzyFind(const Context &Ctx, const FuzzyFindRequest &Req, std::function Callback) const override; private: Index: clangd/index/FileIndex.cpp =================================================================== --- clangd/index/FileIndex.cpp +++ clangd/index/FileIndex.cpp @@ -63,7 +63,7 @@ return {std::move(Snap), Pointers}; } -void FileIndex::update(Context &Ctx, PathRef Path, ParsedAST *AST) { +void FileIndex::update(const Context &Ctx, PathRef Path, ParsedAST *AST) { if (!AST) { FSymbols.update(Path, nullptr); } else { @@ -74,7 +74,7 @@ Index.build(std::move(Symbols)); } -bool FileIndex::fuzzyFind(Context &Ctx, const FuzzyFindRequest &Req, +bool FileIndex::fuzzyFind(const Context &Ctx, const FuzzyFindRequest &Req, std::function Callback) const { return Index.fuzzyFind(Ctx, Req, std::move(Callback)); } Index: clangd/index/Index.h =================================================================== --- clangd/index/Index.h +++ clangd/index/Index.h @@ -153,7 +153,7 @@ /// Returns true if the result list is complete, false if it was truncated due /// to MaxCandidateCount virtual bool - fuzzyFind(Context &Ctx, const FuzzyFindRequest &Req, + fuzzyFind(const Context &Ctx, const FuzzyFindRequest &Req, std::function Callback) const = 0; // FIXME: add interfaces for more index use cases: Index: clangd/index/MemIndex.h =================================================================== --- clangd/index/MemIndex.h +++ clangd/index/MemIndex.h @@ -24,7 +24,7 @@ /// accessible as long as `Symbols` is kept alive. void build(std::shared_ptr> Symbols); - bool fuzzyFind(Context &Ctx, const FuzzyFindRequest &Req, + bool fuzzyFind(const Context &Ctx, const FuzzyFindRequest &Req, std::function Callback) const override; private: Index: clangd/index/MemIndex.cpp =================================================================== --- clangd/index/MemIndex.cpp +++ clangd/index/MemIndex.cpp @@ -26,7 +26,7 @@ } } -bool MemIndex::fuzzyFind(Context &Ctx, const FuzzyFindRequest &Req, +bool MemIndex::fuzzyFind(const Context &Ctx, const FuzzyFindRequest &Req, std::function Callback) const { assert(!StringRef(Req.Query).contains("::") && "There must be no :: in query."); Index: unittests/clangd/CodeCompleteTests.cpp =================================================================== --- unittests/clangd/CodeCompleteTests.cpp +++ unittests/clangd/CodeCompleteTests.cpp @@ -6,6 +6,7 @@ // License. See LICENSE.TXT for details. // //===----------------------------------------------------------------------===// + #include "ClangdServer.h" #include "Compiler.h" #include "Context.h" @@ -13,6 +14,7 @@ #include "Protocol.h" #include "SourceCode.h" #include "TestFS.h" +#include "index/MemIndex.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -83,6 +85,7 @@ MATCHER_P(Named, Name, "") { return arg.insertText == Name; } MATCHER_P(Labeled, Label, "") { return arg.label == Label; } MATCHER_P(Kind, K, "") { return arg.kind == K; } +MATCHER_P(Filter, F, "") { return arg.filterText == F; } MATCHER_P(PlainText, Text, "") { return arg.insertTextFormat == clangd::InsertTextFormat::PlainText && arg.insertText == Text; @@ -457,6 +460,104 @@ EXPECT_EQ(1, Results.activeParameter); } +std::unique_ptr simpleIndexFromSymbols( + std::vector> Symbols) { + auto I = llvm::make_unique(); + struct Snapshot { + SymbolSlab Slab; + std::vector Pointers; + }; + auto Snap = std::make_shared(); + for (const auto &Pair : Symbols) { + Symbol Sym; + Sym.ID = SymbolID(Pair.first); + llvm::StringRef QName = Pair.first; + size_t Pos = QName.rfind("::"); + if (Pos == llvm::StringRef::npos) { + Sym.Name = QName; + Sym.Scope = ""; + } else { + Sym.Name = QName.substr(Pos + 2); + Sym.Scope = QName.substr(0, Pos); + } + Sym.SymInfo.Kind = Pair.second; + Snap->Slab.insert(std::move(Sym)); + } + for (auto &Iter : Snap->Slab) + Snap->Pointers.push_back(&Iter.second); + auto S = std::shared_ptr>(std::move(Snap), + &Snap->Pointers); + I->build(std::move(S)); + return I; +} + +TEST(CompletionTest, NoIndex) { + clangd::CodeCompleteOptions Opts; + Opts.Index = nullptr; + + auto Results = completions(R"cpp( + namespace ns { class No {}; } + void f() { ns::^ } + )cpp", + Opts); + EXPECT_THAT(Results.items, Has("No")); +} + +TEST(CompletionTest, SimpleIndexBased) { + clangd::CodeCompleteOptions Opts; + auto I = simpleIndexFromSymbols({{"ns::XYZ", index::SymbolKind::Class}, + {"nx::XYZ", index::SymbolKind::Class}, + {"ns::foo", index::SymbolKind::Function}}); + Opts.Index = I.get(); + + auto Results = completions(R"cpp( + namespace ns { class No {}; } + void f() { ns::^ } + )cpp", + Opts); + EXPECT_THAT(Results.items, Has("XYZ", CompletionItemKind::Class)); + EXPECT_THAT(Results.items, Has("foo", CompletionItemKind::Function)); + EXPECT_THAT(Results.items, Not(Has("No"))); +} + +TEST(CompletionTest, IndexBasedWithFilter) { + clangd::CodeCompleteOptions Opts; + auto I = simpleIndexFromSymbols({{"ns::XYZ", index::SymbolKind::Class}, + {"ns::foo", index::SymbolKind::Function}}); + Opts.Index = I.get(); + + auto Results = completions(R"cpp( + void f() { ns::x^ } + )cpp", + Opts); + EXPECT_THAT(Results.items, Contains(AllOf(Named("XYZ"), Filter("x")))); + EXPECT_THAT(Results.items, Not(Has("foo"))); +} + +TEST(CompletionTest, GlobalQualified) { + clangd::CodeCompleteOptions Opts; + auto I = simpleIndexFromSymbols({{"XYZ", index::SymbolKind::Class}}); + Opts.Index = I.get(); + + auto Results = completions(R"cpp( + void f() { ::^ } + )cpp", + Opts); + EXPECT_THAT(Results.items, Has("XYZ", CompletionItemKind::Class)); +} + +TEST(CompletionTest, FullyQualifiedScope) { + clangd::CodeCompleteOptions Opts; + auto I = simpleIndexFromSymbols({{"ns::XYZ", index::SymbolKind::Class}}); + Opts.Index = I.get(); + + auto Results = completions(R"cpp( + void f() { ::ns::^ } + )cpp", + Opts); + EXPECT_THAT(Results.items, Has("XYZ", CompletionItemKind::Class)); +} + } // namespace } // namespace clangd } // namespace clang Index: unittests/clangd/IndexTests.cpp =================================================================== --- unittests/clangd/IndexTests.cpp +++ unittests/clangd/IndexTests.cpp @@ -143,7 +143,7 @@ I.build(generateSymbols({"a::xyz", "b::yz", "yz"})); FuzzyFindRequest Req; Req.Query = "y"; - Req.Scopes.push_back(""); + Req.Scopes = {""}; auto Matches = match(I, Req); EXPECT_THAT(match(I, Req), UnorderedElementsAre("yz")); } @@ -153,7 +153,7 @@ I.build(generateSymbols({"a::xyz", "a::yy", "a::xz", "b::yz", "yz"})); FuzzyFindRequest Req; Req.Query = "y"; - Req.Scopes.push_back("a"); + Req.Scopes = {"a"}; auto Matches = match(I, Req); EXPECT_THAT(match(I, Req), UnorderedElementsAre("a::xyz", "a::yy")); } @@ -163,8 +163,7 @@ I.build(generateSymbols({"a::xyz", "a::yy", "a::xz", "b::yz", "yz"})); FuzzyFindRequest Req; Req.Query = "y"; - Req.Scopes.push_back("a"); - Req.Scopes.push_back("b"); + Req.Scopes = {"a", "b"}; auto Matches = match(I, Req); EXPECT_THAT(match(I, Req), UnorderedElementsAre("a::xyz", "a::yy", "b::yz")); } @@ -174,7 +173,7 @@ I.build(generateSymbols({"a::xyz", "a::b::yy"})); FuzzyFindRequest Req; Req.Query = "y"; - Req.Scopes.push_back("a"); + Req.Scopes = {"a"}; auto Matches = match(I, Req); EXPECT_THAT(match(I, Req), UnorderedElementsAre("a::xyz")); }