Index: clangd/CMakeLists.txt =================================================================== --- clangd/CMakeLists.txt +++ clangd/CMakeLists.txt @@ -16,6 +16,7 @@ JSONExpr.cpp JSONRPCDispatcher.cpp Logger.cpp + Position.cpp Protocol.cpp ProtocolHandlers.cpp Trace.cpp Index: clangd/ClangdLSPServer.cpp =================================================================== --- clangd/ClangdLSPServer.cpp +++ clangd/ClangdLSPServer.cpp @@ -9,6 +9,7 @@ #include "ClangdLSPServer.h" #include "JSONRPCDispatcher.h" +#include "Position.h" #include "llvm/Support/FormatVariadic.h" using namespace clang::clangd; Index: clangd/ClangdServer.h =================================================================== --- clangd/ClangdServer.h +++ clangd/ClangdServer.h @@ -32,15 +32,8 @@ namespace clang { class PCHContainerOperations; - namespace clangd { -/// Turn a [line, column] pair into an offset in Code. -size_t positionToOffset(StringRef Code, Position P); - -/// Turn an offset in Code into a [line, column] pair. -Position offsetToPosition(StringRef Code, size_t Offset); - /// A tag supplied by the FileSytemProvider. typedef std::string VFSTag; Index: clangd/ClangdServer.cpp =================================================================== --- clangd/ClangdServer.cpp +++ clangd/ClangdServer.cpp @@ -8,6 +8,7 @@ //===-------------------------------------------------------------------===// #include "ClangdServer.h" +#include "Position.h" #include "clang/Format/Format.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/Frontend/CompilerInvocation.h" @@ -57,29 +58,6 @@ } // namespace -size_t clangd::positionToOffset(StringRef Code, Position P) { - size_t Offset = 0; - for (int I = 0; I != P.line; ++I) { - // FIXME: \r\n - // FIXME: UTF-8 - size_t F = Code.find('\n', Offset); - if (F == StringRef::npos) - return 0; // FIXME: Is this reasonable? - Offset = F + 1; - } - return (Offset == 0 ? 0 : (Offset - 1)) + P.character; -} - -/// Turn an offset in Code into a [line, column] pair. -Position clangd::offsetToPosition(StringRef Code, size_t Offset) { - StringRef JustBefore = Code.substr(0, Offset); - // FIXME: \r\n - // FIXME: UTF-8 - int Lines = JustBefore.count('\n'); - int Cols = JustBefore.size() - JustBefore.rfind('\n') - 1; - return {Lines, Cols}; -} - Tagged> RealFileSystemProvider::getTaggedFileSystem(PathRef File) { return make_tagged(vfs::getRealFileSystem(), VFSTag()); 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 "Position.h" +#include "index/Index.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/Frontend/FrontendActions.h" #include "clang/Sema/CodeCompleteConsumer.h" @@ -85,6 +87,53 @@ llvm_unreachable("Unhandled CodeCompletionResult::ResultKind."); } +CompletionItemKind getKindOfSymbol(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; + case SK::Struct: + case SK::Class: + case SK::Protocol: + case SK::Extension: + case SK::Union: + return CompletionItemKind::Class; + case SK::TypeAlias: + case SK::Using: + return CompletionItemKind::Reference; + case SK::Function: + case SK::ConversionFunction: + return CompletionItemKind::Function; + case SK::Variable: + case SK::Parameter: + return CompletionItemKind::Variable; + case SK::Field: + return CompletionItemKind::Field; + case SK::EnumConstant: + return CompletionItemKind::Value; + case SK::InstanceMethod: + case SK::ClassMethod: + case SK::StaticMethod: + return CompletionItemKind::Method; + case SK::InstanceProperty: + case SK::ClassProperty: + case SK::StaticProperty: + return CompletionItemKind::Property; + case SK::Constructor: + case SK::Destructor: + 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 +277,128 @@ } }; +/// \brief Information about the scope specifier in the qualfiied-id code +/// completion (e.g. "ns::ab?"). +struct ScopeSpecifierInfo { + static ScopeSpecifierInfo create(Sema &S, const CXXScopeSpec &SS) { + ScopeSpecifierInfo Info; + auto &SM = S.getSourceManager(); + auto SpecifierRange = SS.getRange(); + Info.WrittenSpecifier = + Lexer::getSourceText(CharSourceRange::getCharRange(SpecifierRange), SM, + clang::LangOptions()); + if (SS.isValid()) { + DeclContext *DC = S.computeDeclContext(SS); + if (auto *NS = llvm::dyn_cast(DC)) { + Info.SpecifiedContextName = NS->getQualifiedNameAsString(); + } else if (auto *TU = llvm::dyn_cast(DC)) { + Info.SpecifiedContextName = "::"; + // Sema does not include the suffix "::" in the range of SS, so we add + // it back here. + Info.WrittenSpecifier = "::"; + } + } + + Info.SpecifierBeginOffset = SM.getFileOffset(SpecifierRange.getBegin()); + Info.SpecifierEndOffset = SM.getFileOffset(SpecifierRange.getEnd()); + return Info; + } + + // The range of scope specifier as written. This does not include the + // filter text following the specifier. For example, for completion at + // "ns::ab?", the range will be "ns". + unsigned int SpecifierBeginOffset; + unsigned int SpecifierEndOffset; + + // The scope specifier as written. For example, for completion "ns::ab?", + // the written scope specifier is "ns". + std::string WrittenSpecifier; + // 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 SpecifiedContextName; +}; + +CompletionItem symbolToCompletionItem(const Symbol &Sym, llvm::StringRef Code, + const ScopeSpecifierInfo &SSInfo) { + CompletionItem Item; + bool FullyQualified = + llvm::StringRef(SSInfo.WrittenSpecifier).startswith("::"); + if (FullyQualified) + Item.label = "::"; + // FIXME: use more symbol information (e.g. documentation, label) to populate + // the completion item. + Item.label += Sym.QualifiedName; + Item.kind = getKindOfSymbol(Sym.SymInfo.Kind); + Item.insertTextFormat = InsertTextFormat::PlainText; + // FIXME: sort symbols appropriately. + Item.sortText = ""; + + TextEdit Edit; + Edit.newText = + FullyQualified ? ("::" + Sym.QualifiedName) : Sym.QualifiedName; + Edit.range.start = + offsetToPosition(Code, SSInfo.SpecifierBeginOffset); + Edit.range.end = offsetToPosition(Code, SSInfo.SpecifierEndOffset); + Item.textEdit = std::move(Edit); + return Item; +} + +void qualifiedIdCompletionWithIndex(const Context &Ctx, + const SymbolIndex &Index, + llvm::StringRef Code, + const ScopeSpecifierInfo &SSInfo, + llvm::StringRef Filter, + CompletionList *Items) { + FuzzyFindRequest Req; + Req.Query = SSInfo.SpecifiedContextName.empty() ? SSInfo.WrittenSpecifier + : SSInfo.SpecifiedContextName; + if (!llvm::StringRef(Req.Query).endswith("::")) + Req.Query += "::"; + // FIXME: for now we simply cancatenate specifier with the typed filter. We + // might want to fix the specifier prefix if it is a recognized context (e.g. + // a known namespace in the AST). + if (!Filter.empty()) + Req.Query += Filter; + + // Global qualifier means all symbols. + if (Req.Query == "::") + Req.Query = ""; + + Items->isIncomplete = !Index.fuzzyFind(Ctx, Req, [&](const Symbol &Sym) { + Items->items.push_back(symbolToCompletionItem(Sym, Code, SSInfo)); + }); +} + +/// brief Collects information about an invocation of sema code completion. +struct SemaCompletionInfo { + /// Code completion filter. + std::string Filter; + + /// This is set if the completion is for qualified IDs, e.g. "abc::x^". + llvm::Optional SSInfo; + // FIXME: add more information for other completion cases that we care about. + // For example, non-qualified id completion. +}; + class CompletionItemsCollector : public CodeCompleteConsumer { public: CompletionItemsCollector(const CodeCompleteOptions &CodeCompleteOpts, - CompletionList &Items) + CompletionList &Items, SemaCompletionInfo &SCInfo) : CodeCompleteConsumer(CodeCompleteOpts.getClangCompleteOpts(), /*OutputIsBinary=*/false), ClangdOpts(CodeCompleteOpts), Items(Items), Allocator(std::make_shared()), - CCTUInfo(Allocator) {} + CCTUInfo(Allocator), SCInfo(SCInfo) {} void ProcessCodeCompleteResults(Sema &S, CodeCompletionContext Context, CodeCompletionResult *Results, unsigned NumResults) override final { - StringRef Filter = S.getPreprocessor().getCodeCompletionFilter(); + if (llvm::Optional SS = + Context.getCXXScopeSpecifier()) + SCInfo.SSInfo = ScopeSpecifierInfo::create(S, **SS); + + SCInfo.Filter = S.getPreprocessor().getCodeCompletionFilter(); std::priority_queue Candidates; for (unsigned I = 0; I < NumResults; ++I) { auto &Result = Results[I]; @@ -249,7 +406,8 @@ (Result.Availability == CXAvailability_NotAvailable || Result.Availability == CXAvailability_NotAccessible)) continue; - if (!Filter.empty() && !fuzzyMatch(S, Context, Filter, Result)) + if (!SCInfo.Filter.empty() && + !fuzzyMatch(S, Context, SCInfo.Filter, Result)) continue; Candidates.emplace(Result); if (ClangdOpts.Limit && Candidates.size() > ClangdOpts.Limit) { @@ -336,7 +494,7 @@ CompletionList &Items; std::shared_ptr Allocator; CodeCompletionTUInfo CCTUInfo; - + SemaCompletionInfo &SCInfo; }; // CompletionItemsCollector bool isInformativeQualifierChunk(CodeCompletionString::Chunk const &Chunk) { @@ -349,8 +507,9 @@ public: PlainTextCompletionItemsCollector(const CodeCompleteOptions &CodeCompleteOpts, - CompletionList &Items) - : CompletionItemsCollector(CodeCompleteOpts, Items) {} + CompletionList &Items, + SemaCompletionInfo &SCInfo) + : CompletionItemsCollector(CodeCompleteOpts, Items, SCInfo) {} private: void ProcessChunks(const CodeCompletionString &CCS, @@ -385,8 +544,9 @@ public: SnippetCompletionItemsCollector(const CodeCompleteOptions &CodeCompleteOpts, - CompletionList &Items) - : CompletionItemsCollector(CodeCompleteOpts, Items) {} + CompletionList &Items, + SemaCompletionInfo &SCInfo) + : CompletionItemsCollector(CodeCompleteOpts, Items, SCInfo) {} private: void ProcessChunks(const CodeCompletionString &CCS, @@ -657,6 +817,9 @@ Result.IncludeGlobals = IncludeGlobals; Result.IncludeBriefComments = IncludeBriefComments; + // Enable index-based code completion when Index is provided. + Result.IncludeNamespaceLevelDecls = !Index; + return Result; } @@ -669,16 +832,24 @@ CodeCompleteOptions Opts) { CompletionList Results; std::unique_ptr Consumer; + SemaCompletionInfo SCInfo; if (Opts.EnableSnippets) { - Consumer = - llvm::make_unique(Opts, Results); + Consumer = llvm::make_unique(Opts, Results, + SCInfo); } else { - Consumer = - llvm::make_unique(Opts, Results); + Consumer = llvm::make_unique( + Opts, Results, SCInfo); } invokeCodeComplete(Ctx, std::move(Consumer), Opts.getClangCompleteOpts(), FileName, Command, Preamble, Contents, Pos, std::move(VFS), std::move(PCHs)); + if (Opts.Index && SCInfo.SSInfo) { + // FIXME: log warning with logger if sema code completion have collected + // results. + Results.items.clear(); + qualifiedIdCompletionWithIndex(Ctx, *Opts.Index, Contents, *SCInfo.SSInfo, + SCInfo.Filter, &Results); + } return Results; } Index: clangd/Position.h =================================================================== --- /dev/null +++ clangd/Position.h @@ -0,0 +1,27 @@ +//===--- Position.h - Positions in code. -------------------------*- C++-*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_POSITION_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_POSITION_H + +#include "Protocol.h" + +namespace clang { +namespace clangd { + +/// Turn a [line, column] pair into an offset in Code. +size_t positionToOffset(llvm::StringRef Code, Position P); + +/// Turn an offset in Code into a [line, column] pair. +Position offsetToPosition(llvm::StringRef Code, size_t Offset); + +} // namespace clangd +} // namespace clang + +#endif // LLVM_CLANG_TOOLS_EXTRA_CLANGD_POSITION_H Index: clangd/Position.cpp =================================================================== --- /dev/null +++ clangd/Position.cpp @@ -0,0 +1,40 @@ +//===--- Position.cpp - Positions in code. -------------------------*- +-*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#include "Position.h" + +namespace clang { +namespace clangd { + +size_t positionToOffset(llvm::StringRef Code, Position P) { + size_t Offset = 0; + for (int I = 0; I != P.line; ++I) { + // FIXME: \r\n + // FIXME: UTF-8 + size_t F = Code.find('\n', Offset); + if (F == llvm::StringRef::npos) + return 0; // FIXME: Is this reasonable? + Offset = F + 1; + } + return (Offset == 0 ? 0 : (Offset - 1)) + P.character; +} + +/// Turn an offset in Code into a [line, column] pair. +Position offsetToPosition(llvm::StringRef Code, size_t Offset) { + llvm::StringRef JustBefore = Code.substr(0, Offset); + // FIXME: \r\n + // FIXME: UTF-8 + int Lines = JustBefore.count('\n'); + int Cols = JustBefore.size() - JustBefore.rfind('\n') - 1; + return {Lines, Cols}; +} + + +} // namespace clangd +} // namespace clang 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 @@ -142,7 +142,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 @@ -25,7 +25,7 @@ } } -bool MemIndex::fuzzyFind(Context & /*Ctx*/, const FuzzyFindRequest &Req, +bool MemIndex::fuzzyFind(const Context & /*Ctx*/, const FuzzyFindRequest &Req, std::function Callback) const { std::string LoweredQuery = llvm::StringRef(Req.Query).lower(); unsigned Matched = 0; Index: unittests/clangd/CodeCompleteTests.cpp =================================================================== --- unittests/clangd/CodeCompleteTests.cpp +++ unittests/clangd/CodeCompleteTests.cpp @@ -10,8 +10,10 @@ #include "Compiler.h" #include "Context.h" #include "Matchers.h" +#include "Position.h" #include "Protocol.h" #include "TestFS.h" +#include "index/MemIndex.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -77,6 +79,13 @@ return arg.insertTextFormat == clangd::InsertTextFormat::Snippet && arg.insertText == Text; } +MATCHER_P(Edit, Text, "") { + return arg.textEdit.hasValue() && arg.textEdit.getValue().newText == Text; +} +MATCHER_P2(Edit, Text, Range, "") { + return arg.textEdit.hasValue() && arg.textEdit.getValue().newText == Text && + arg.textEdit.getValue().range == Range; +} // Shorthand for Contains(Named(Name)). Matcher &> Has(std::string Name) { return Contains(Named(std::move(Name))); @@ -85,6 +94,18 @@ CompletionItemKind K) { return Contains(AllOf(Named(std::move(Name)), Kind(K))); } +// Shorthand for Contains(Edit(Name)). +Matcher &> HasEdit(std::string Name) { + return Contains(Edit(std::move(Name))); +} +Matcher &> HasEdit(std::string Name, + CompletionItemKind K) { + return Contains(AllOf(Edit(std::move(Name)), Kind(K))); +} +Matcher &> +HasEditWithRange(std::string Name, Range R) { + return Contains(Edit(std::move(Name), std::move(R))); +} MATCHER(IsDocumented, "") { return !arg.documentation.empty(); } CompletionList completions(StringRef Text, @@ -368,6 +389,135 @@ EXPECT_THAT(Results.items, Has("namespace", CompletionItemKind::Snippet)); } +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); + Sym.QualifiedName = Pair.first; + 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}, + {"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, HasEdit("ns::XYZ", CompletionItemKind::Class)); + EXPECT_THAT(Results.items, HasEdit("ns::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, HasEdit("ns::XYZ", CompletionItemKind::Class)); + EXPECT_THAT(Results.items, Not(HasEdit("ns::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, HasEdit("::XYZ", CompletionItemKind::Class)); +} + +TEST(CompletionTest, EditRangeGlobal) { + 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, HasEditWithRange("::XYZ", {{1, 17}, {1, 17}})); +} + +TEST(CompletionTest, EditRangeQualified) { + 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); + // "::" is not in the buffer. + EXPECT_THAT(Results.items, HasEditWithRange("ns::XYZ", {{1, 17}, {1, 19}})); +} + +TEST(CompletionTest, EditRangeQualifiedWithFilter) { + clangd::CodeCompleteOptions Opts; + auto I = simpleIndexFromSymbols({{"ns::XYZ", index::SymbolKind::Class}}); + Opts.Index = I.get(); + + auto Results = completions(R"cpp( + void f() { ns::X^ } + )cpp", + Opts); + // Filter is not in the buffer. + EXPECT_THAT(Results.items, HasEditWithRange("ns::XYZ", {{1, 17}, {1, 19}})); +} + +TEST(CompletionTest, RecognizedSpecifierAndRange) { + clangd::CodeCompleteOptions Opts; + auto I = simpleIndexFromSymbols({{"ns::XYZ", index::SymbolKind::Class}}); + Opts.Index = I.get(); + + auto Results = completions(R"cpp( + namespace ns {} + void f() { ns::X^ } + )cpp", + Opts); + // Filter is not in the buffer. + EXPECT_THAT(Results.items, HasEditWithRange("ns::XYZ", {{2, 17}, {2, 19}})); +} + } // namespace } // namespace clangd } // namespace clang