Index: clangd/ClangdServer.cpp =================================================================== --- clangd/ClangdServer.cpp +++ clangd/ClangdServer.cpp @@ -23,7 +23,6 @@ #include "clang/Frontend/CompilerInstance.h" #include "clang/Frontend/CompilerInvocation.h" #include "clang/Lex/Preprocessor.h" -#include "clang/Sema/CodeCompleteConsumer.h" #include "clang/Tooling/CompilationDatabase.h" #include "clang/Tooling/Core/Replacement.h" #include "clang/Tooling/Refactoring/RefactoringResultConsumer.h" @@ -197,14 +196,10 @@ if (!IP->Preamble) { vlog("File {0} is not ready for code completion. Enter fallback mode.", File); - CodeCompleteResult CCR; - CCR.Context = CodeCompletionContext::CCC_Recovery; - // FIXME: perform simple completion e.g. using identifiers in the current - // file and symbols in the index. // FIXME: let clients know that we've entered fallback mode. - - return CB(std::move(CCR)); + return CB(clangd::codeCompleteNoCompile(File, IP->Contents, Pos, FS, + CodeCompleteOpts)); } assert(IP->Command.hasValue() && Index: clangd/ClangdUnit.cpp =================================================================== --- clangd/ClangdUnit.cpp +++ clangd/ClangdUnit.cpp @@ -313,7 +313,7 @@ auto Style = getFormatStyleForFile(MainInput.getFile(), Content, VFS.get()); auto Inserter = std::make_shared( MainInput.getFile(), Content, Style, BuildDir.get(), - Clang->getPreprocessor().getHeaderSearchInfo()); + &Clang->getPreprocessor().getHeaderSearchInfo()); if (Preamble) { for (const auto &Inc : Preamble->Includes.MainFileIncludes) Inserter->addExisting(Inc); Index: clangd/CodeComplete.h =================================================================== --- clangd/CodeComplete.h +++ clangd/CodeComplete.h @@ -236,6 +236,12 @@ CodeCompleteOptions Opts, SpeculativeFuzzyFind *SpecFuzzyFind = nullptr); +/// Performs simple code completion without compiling the file. +CodeCompleteResult +codeCompleteNoCompile(PathRef FileName, llvm::StringRef Content, Position Pos, + llvm::IntrusiveRefCntPtr VFS, + CodeCompleteOptions Opts); + /// Get signature help at a specified \p Pos in \p FileName. SignatureHelp signatureHelp(PathRef FileName, const tooling::CompileCommand &Command, Index: clangd/CodeComplete.cpp =================================================================== --- clangd/CodeComplete.cpp +++ clangd/CodeComplete.cpp @@ -34,14 +34,19 @@ #include "Trace.h" #include "URI.h" #include "index/Index.h" +#include "index/MemIndex.h" +#include "index/Ref.h" #include "index/Symbol.h" #include "clang/AST/Decl.h" #include "clang/AST/DeclBase.h" #include "clang/Basic/LangOptions.h" #include "clang/Basic/SourceLocation.h" +#include "clang/Basic/SourceManager.h" +#include "clang/Basic/TokenKinds.def" #include "clang/Format/Format.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/Frontend/FrontendActions.h" +#include "clang/Lex/Lexer.h" #include "clang/Lex/PreprocessorOptions.h" #include "clang/Sema/CodeCompleteConsumer.h" #include "clang/Sema/Sema.h" @@ -261,7 +266,8 @@ // computed from the first candidate, in the constructor. // Others vary per candidate, so add() must be called for remaining candidates. struct CodeCompletionBuilder { - CodeCompletionBuilder(ASTContext &ASTCtx, const CompletionCandidate &C, + // ASTCtx can be nullptr if not run with sema. + CodeCompletionBuilder(ASTContext *ASTCtx, const CompletionCandidate &C, CodeCompletionString *SemaCCS, llvm::ArrayRef QueryScopes, const IncludeInserter &Includes, @@ -272,6 +278,7 @@ EnableFunctionArgSnippets(Opts.EnableFunctionArgSnippets) { add(C, SemaCCS); if (C.SemaResult) { + assert(ASTCtx); Completion.Origin |= SymbolOrigin::AST; Completion.Name = llvm::StringRef(SemaCCS->getTypedText()); if (Completion.Scope.empty()) { @@ -290,8 +297,8 @@ Completion.Name.back() == '/') Completion.Kind = CompletionItemKind::Folder; for (const auto &FixIt : C.SemaResult->FixIts) { - Completion.FixIts.push_back( - toTextEdit(FixIt, ASTCtx.getSourceManager(), ASTCtx.getLangOpts())); + Completion.FixIts.push_back(toTextEdit( + FixIt, ASTCtx->getSourceManager(), ASTCtx->getLangOpts())); } llvm::sort(Completion.FixIts, [](const TextEdit &X, const TextEdit &Y) { return std::tie(X.range.start.line, X.range.start.character) < @@ -376,7 +383,7 @@ if (C.IndexResult) Completion.Documentation = C.IndexResult->Documentation; else if (C.SemaResult) - Completion.Documentation = getDocComment(ASTCtx, *C.SemaResult, + Completion.Documentation = getDocComment(*ASTCtx, *C.SemaResult, /*CommentsFromHeader=*/false); } } @@ -471,7 +478,7 @@ return "(…)"; } - ASTContext &ASTCtx; + ASTContext *ASTCtx; CodeCompletion Completion; llvm::SmallVector Bundled; bool ExtractDocumentation; @@ -611,6 +618,7 @@ case CodeCompletionContext::CCC_ObjCCategoryName: case CodeCompletionContext::CCC_Symbol: case CodeCompletionContext::CCC_SymbolOrNewName: + case CodeCompletionContext::CCC_Recovery: return true; case CodeCompletionContext::CCC_OtherWithMacros: case CodeCompletionContext::CCC_DotMemberAccess: @@ -628,7 +636,6 @@ // FIXME: Provide identifier based completions for the following contexts: case CodeCompletionContext::CCC_Other: // Be conservative. case CodeCompletionContext::CCC_NaturalLanguage: - case CodeCompletionContext::CCC_Recovery: case CodeCompletionContext::CCC_NewName: return false; } @@ -1123,6 +1130,41 @@ return CachedReq; } +// Indexes identifiers in the file. Identifiers are wrapped as fake symbols with +// symbol IDs like "identifier:". +std::unique_ptr +indexIdentifiers(llvm::StringRef FileName, llvm::StringRef Content, + const format::FormatStyle &Style) { + SourceManagerForFile FileSM(FileName, Content); + auto &SM = FileSM.get(); + auto FID = SM.getMainFileID(); + Lexer Lex(FID, SM.getBuffer(FID), SM, format::getFormattingLangOpts(Style)); + Token Tok; + SymbolSlab::Builder Syms; + std::vector IDs; + auto SymbolFromID = [](llvm::StringRef Identifier) { + Symbol Sym; + Sym.Name = Identifier; + Sym.ID = SymbolID(("identifier:" + Identifier).str()); + Sym.Flags |= Symbol::IndexedForCodeCompletion; + return Sym; + }; + while (!Lex.LexFromRawLexer(Tok)) { + switch (Tok.getKind()) { + case tok::identifier: + Syms.insert(SymbolFromID(Tok.getIdentifierInfo()->getName())); + break; + case tok::raw_identifier: + Syms.insert(SymbolFromID(Tok.getRawIdentifier())); + break; + default: + continue; + } + } + + return MemIndex::build(std::move(Syms).build(), RefSlab()); +} + // Runs Sema-based (AST) and Index-based completion, returns merged results. // // There are a few tricky considerations: @@ -1159,20 +1201,26 @@ const CodeCompleteOptions &Opts; // Sema takes ownership of Recorder. Recorder is valid until Sema cleanup. + // This can be nullptr in no-compile completion. CompletionRecorder *Recorder = nullptr; int NSema = 0, NIndex = 0, NBoth = 0; // Counters for logging. bool Incomplete = false; // Would more be available with a higher limit? - llvm::Optional Filter; // Initialized once Sema runs. - std::vector QueryScopes; // Initialized once Sema runs. - // Initialized once QueryScopes is initialized, if there are scopes. - llvm::Optional ScopeProximity; - llvm::Optional PreferredType; // Initialized once Sema runs. - // Whether to query symbols from any scope. Initialized once Sema runs. - bool AllScopes = false; + + // The following fields are initialized once Sema runs or run without compile. + CodeCompletionContext::Kind CCContextKind = CodeCompletionContext::CCC_Other; + llvm::Optional Filter; + Range TextEditRange; + std::vector QueryScopes; + llvm::Optional + ScopeProximity; // Initialized if QueryScopes is not empty. + llvm::Optional PreferredType; + bool AllScopes = false; // Whether to query symbols from any scope. // Include-insertion and proximity scoring rely on the include structure. // This is available after Sema has run. llvm::Optional Inserter; // Available during runWithSema. - llvm::Optional FileProximity; // Initialized once Sema runs. + + llvm::Optional FileProximity; + /// Speculative request based on the cached request and the filter text before /// the cursor. /// Initialized right before sema run. This is only set if `SpecFuzzyFind` is @@ -1203,6 +1251,7 @@ CodeCompleteResult Output; auto RecorderOwner = llvm::make_unique(Opts, [&]() { assert(Recorder && "Recorder is not set"); + CCContextKind = Recorder->CCContext.getKind(); auto Style = getFormatStyleForFile( SemaCCInput.FileName, SemaCCInput.Contents, SemaCCInput.VFS.get()); // If preprocessor was run, inclusions from preprocessor callback should @@ -1210,7 +1259,7 @@ Inserter.emplace( SemaCCInput.FileName, SemaCCInput.Contents, Style, SemaCCInput.Command.Directory, - Recorder->CCSema->getPreprocessor().getHeaderSearchInfo()); + &Recorder->CCSema->getPreprocessor().getHeaderSearchInfo()); for (const auto &Inc : Includes.MainFileIncludes) Inserter->addExisting(Inc); @@ -1236,10 +1285,10 @@ Output = runWithSema(); Inserter.reset(); // Make sure this doesn't out-live Clang. SPAN_ATTACH(Tracer, "sema_completion_kind", - getCompletionKindString(Recorder->CCContext.getKind())); + getCompletionKindString(CCContextKind)); log("Code complete: sema context {0}, query scopes [{1}] (AnyScope={2}), " "expected type {3}", - getCompletionKindString(Recorder->CCContext.getKind()), + getCompletionKindString(CCContextKind), llvm::join(QueryScopes.begin(), QueryScopes.end(), ","), AllScopes, PreferredType ? Recorder->CCContext.getPreferredType().getAsString() : ""); @@ -1265,13 +1314,55 @@ return Output; } + CodeCompleteResult + runWithoutCompile(llvm::StringRef Content, Position Pos, + llvm::IntrusiveRefCntPtr VFS) && { + auto CompletionFilter = speculateCompletionFilter(Content, Pos); + if (!CompletionFilter) { + elog("Failed to extract completion filter at {0}:{1} in code: {2}", + Pos.line, Pos.character, CompletionFilter.takeError()); + return CodeCompleteResult(); + } + // Initialize common data structures. + CCContextKind = CodeCompletionContext::CCC_Recovery; + Filter = FuzzyMatcher(*CompletionFilter); + TextEditRange.start = TextEditRange.end = Pos; + TextEditRange.start.character -= CompletionFilter->size(); + // FIXME: collect typed scope specifier and potentially parse the enclosing + // namespaces. + QueryScopes = {}; + // FIXME: initialize ScopeProximity when scopes are added. + AllScopes = true; // Identifiers have no scope. + auto Style = getFormatStyleForFile(FileName, Content, VFS.get()); + // This will only insert verbatim headers. + Inserter.emplace(FileName, Content, Style, + /*BuildDir=*/"", /*HeaderSearchInfo=*/nullptr); + + // Carve out the typed filter from the content so that we don't treat it as + // an identifier. + auto Offset = positionToOffset(Content, Pos); + if (!Offset) { + elog("{0}", Offset.takeError()); + return CodeCompleteResult(); + } + std::string ContentToIndex = + (Content.substr(0, *Offset - CompletionFilter->size()) + + Content.substr(*Offset)) + .str(); + + auto Idx = indexIdentifiers(FileName, ContentToIndex, Style); + // FIXME: merge with Opts.Index when we know more about scopes (e.g. typed + // scope specifier). + auto Results = queryIndex(*Idx); + return toCodeCompleteResult(mergeResults(/*SemaResults=*/{}, Results)); + } + private: // This is called by run() once Sema code completion is done, but before the // Sema data structures are torn down. It does all the real work. CodeCompleteResult runWithSema() { const auto &CodeCompletionRange = CharSourceRange::getCharRange( Recorder->CCSema->getPreprocessor().getCodeCompletionTokenRange()); - Range TextEditRange; // When we are getting completions with an empty identifier, for example // std::vector asdf; // asdf.^; @@ -1301,26 +1392,30 @@ // We can use their signals even if the index can't suggest them. // We must copy index results to preserve them, but there are at most Limit. auto IndexResults = (Opts.Index && allowIndex(Recorder->CCContext)) - ? queryIndex() + ? queryIndex(*Opts.Index) : SymbolSlab(); trace::Span Tracer("Populate CodeCompleteResult"); // Merge Sema and Index results, score them, and pick the winners. auto Top = mergeResults(Recorder->Results, IndexResults); + return toCodeCompleteResult(Top); + } + + CodeCompleteResult + toCodeCompleteResult(const std::vector &Scored) { CodeCompleteResult Output; // Convert the results to final form, assembling the expensive strings. - for (auto &C : Top) { + for (auto &C : Scored) { Output.Completions.push_back(toCodeCompletion(C.first)); Output.Completions.back().Score = C.second; Output.Completions.back().CompletionTokenRange = TextEditRange; } Output.HasMore = Incomplete; - Output.Context = Recorder->CCContext.getKind(); - + Output.Context = CCContextKind; return Output; } - SymbolSlab queryIndex() { + SymbolSlab queryIndex(const SymbolIndex &Index) { trace::Span Tracer("Query index"); SPAN_ATTACH(Tracer, "limit", int64_t(Opts.Limit)); @@ -1353,8 +1448,8 @@ // Run the query against the index. SymbolSlab::Builder ResultsBuilder; - if (Opts.Index->fuzzyFind( - Req, [&](const Symbol &Sym) { ResultsBuilder.insert(Sym); })) + if (Index.fuzzyFind(Req, + [&](const Symbol &Sym) { ResultsBuilder.insert(Sym); })) Incomplete = true; return std::move(ResultsBuilder).build(); } @@ -1400,7 +1495,7 @@ return nullptr; }; // Emit all Sema results, merging them with Index results if possible. - for (auto &SemaResult : Recorder->Results) + for (auto &SemaResult : SemaResults) AddToBundles(&SemaResult, CorrespondingIndexResult(SemaResult)); // Now emit any Index-only results. for (const auto &IndexResult : IndexResults) { @@ -1430,9 +1525,10 @@ CompletionCandidate::Bundle Bundle) { SymbolQualitySignals Quality; SymbolRelevanceSignals Relevance; - Relevance.Context = Recorder->CCContext.getKind(); + Relevance.Context = CCContextKind; Relevance.Query = SymbolRelevanceSignals::CodeComplete; - Relevance.FileProximityMatch = FileProximity.getPointer(); + if (FileProximity) + Relevance.FileProximityMatch = FileProximity.getPointer(); if (ScopeProximity) Relevance.ScopeProximityMatch = ScopeProximity.getPointer(); if (PreferredType) @@ -1500,9 +1596,9 @@ Item.SemaResult ? Recorder->codeCompletionString(*Item.SemaResult) : nullptr; if (!Builder) - Builder.emplace(Recorder->CCSema->getASTContext(), Item, SemaCCS, - QueryScopes, *Inserter, FileName, - Recorder->CCContext.getKind(), Opts); + Builder.emplace(Recorder ? &Recorder->CCSema->getASTContext() : nullptr, + Item, SemaCCS, QueryScopes, *Inserter, FileName, + CCContextKind, Opts); else Builder->add(Item, SemaCCS); } @@ -1543,9 +1639,7 @@ speculateCompletionFilter(llvm::StringRef Content, Position Pos) { auto Offset = positionToOffset(Content, Pos); if (!Offset) - return llvm::make_error( - "Failed to convert position to offset in content.", - llvm::inconvertibleErrorCode()); + return Offset.takeError(); if (*Offset == 0) return ""; @@ -1576,6 +1670,15 @@ .run({FileName, Command, Preamble, Contents, Pos, VFS, PCHs}); } +CodeCompleteResult +codeCompleteNoCompile(PathRef FileName, llvm::StringRef Content, Position Pos, + llvm::IntrusiveRefCntPtr VFS, + CodeCompleteOptions Opts) { + return CodeCompleteFlow(FileName, IncludeStructure(), + /*SpecFuzzyFind=*/nullptr, Opts) + .runWithoutCompile(Content, Pos, VFS); +} + SignatureHelp signatureHelp(PathRef FileName, const tooling::CompileCommand &Command, const PreambleData *Preamble, Index: clangd/Headers.h =================================================================== --- clangd/Headers.h +++ clangd/Headers.h @@ -119,9 +119,12 @@ // Calculates insertion edit for including a new header in a file. class IncludeInserter { public: + // If \p HeaderSearchInfo is nullptr (e.g. when compile command is + // infeasible), this will only try to insert verbatim headers, and + // include path of non-verbatim header will not be shortened. IncludeInserter(StringRef FileName, StringRef Code, const format::FormatStyle &Style, StringRef BuildDir, - HeaderSearch &HeaderSearchInfo) + HeaderSearch *HeaderSearchInfo) : FileName(FileName), Code(Code), BuildDir(BuildDir), HeaderSearchInfo(HeaderSearchInfo), Inserter(FileName, Code, Style.IncludeStyle) {} @@ -162,7 +165,7 @@ StringRef FileName; StringRef Code; StringRef BuildDir; - HeaderSearch &HeaderSearchInfo; + HeaderSearch *HeaderSearchInfo = nullptr; llvm::StringSet<> IncludedHeaders; // Both written and resolved. tooling::HeaderIncludes Inserter; // Computers insertion replacement. }; Index: clangd/Headers.cpp =================================================================== --- clangd/Headers.cpp +++ clangd/Headers.cpp @@ -177,6 +177,8 @@ assert(DeclaringHeader.valid() && InsertedHeader.valid()); if (FileName == DeclaringHeader.File || FileName == InsertedHeader.File) return false; + if (!HeaderSearchInfo && !InsertedHeader.Verbatim) + return false; auto Included = [&](llvm::StringRef Header) { return IncludedHeaders.find(Header) != IncludedHeaders.end(); }; @@ -190,7 +192,9 @@ if (InsertedHeader.Verbatim) return InsertedHeader.File; bool IsSystem = false; - std::string Suggested = HeaderSearchInfo.suggestPathToFileForDiagnostics( + if (!HeaderSearchInfo) + return "\"" + InsertedHeader.File + "\""; + std::string Suggested = HeaderSearchInfo->suggestPathToFileForDiagnostics( InsertedHeader.File, BuildDir, &IsSystem); if (IsSystem) Suggested = "<" + Suggested + ">"; Index: unittests/clangd/ClangdTests.cpp =================================================================== --- unittests/clangd/ClangdTests.cpp +++ unittests/clangd/ClangdTests.cpp @@ -535,12 +535,12 @@ EXPECT_ERROR(runLocateSymbolAt(Server, FooCpp, Position())); EXPECT_ERROR(runFindDocumentHighlights(Server, FooCpp, Position())); EXPECT_ERROR(runRename(Server, FooCpp, Position(), "new_name")); - // FIXME: codeComplete and signatureHelp should also return errors when they - // can't parse the file. + // Identifier-based fallback completion. EXPECT_THAT(cantFail(runCodeComplete(Server, FooCpp, Position(), clangd::CodeCompleteOptions())) .Completions, - IsEmpty()); + ElementsAre(Field(&CodeCompletion::Name, "int"), + Field(&CodeCompletion::Name, "main"))); auto SigHelp = runSignatureHelp(Server, FooCpp, Position()); ASSERT_TRUE(bool(SigHelp)) << "signatureHelp returned an error"; EXPECT_THAT(SigHelp->signatures, IsEmpty()); @@ -1089,23 +1089,32 @@ auto FooCpp = testPath("foo.cpp"); Annotations Code(R"cpp( + namespace ns { int xyz; } + using namespace ns; int main() { - int xyz; xy^ })cpp"); FS.Files[FooCpp] = FooCpp; Server.addDocument(FooCpp, Code.code()); auto Opts = clangd::CodeCompleteOptions(); Opts.AllowFallbackMode = true; - auto Res = cantFail(runCodeComplete(Server, FooCpp, Code.point(), Opts)); - EXPECT_THAT(Res.Completions, IsEmpty()); - EXPECT_EQ(Res.Context, CodeCompletionContext::CCC_Recovery); + + auto FallbackRes = cantFail(runCodeComplete(Server, FooCpp, Code.point(), Opts)); + EXPECT_EQ(FallbackRes.Context, CodeCompletionContext::CCC_Recovery); + // Identifier-based fallback completion doesn't know about "symbol" scope. + EXPECT_THAT(FallbackRes.Completions, + ElementsAre(AllOf(Field(&CodeCompletion::Name, "xyz"), + Field(&CodeCompletion::Scope, "")))); + + // Release compile command. CanReturnCommand.notify(); ASSERT_TRUE(Server.blockUntilIdleForTest()); + EXPECT_THAT(cantFail(runCodeComplete(Server, FooCpp, Code.point(), clangd::CodeCompleteOptions())) .Completions, - ElementsAre(Field(&CodeCompletion::Name, "xyz"))); + ElementsAre(AllOf(Field(&CodeCompletion::Name, "xyz"), + Field(&CodeCompletion::Scope, "ns::")))); } } // namespace Index: unittests/clangd/CodeCompleteTests.cpp =================================================================== --- unittests/clangd/CodeCompleteTests.cpp +++ unittests/clangd/CodeCompleteTests.cpp @@ -32,7 +32,6 @@ using ::llvm::Failed; using ::testing::AllOf; using ::testing::Contains; -using ::testing::Each; using ::testing::ElementsAre; using ::testing::Field; using ::testing::HasSubstr; @@ -139,6 +138,25 @@ FilePath); } +// Builds a server and runs code completion. +// If IndexSymbols is non-empty, an index will be built and passed to opts. +CodeCompleteResult completionsNoCompile(llvm::StringRef Text, + std::vector IndexSymbols = {}, + clangd::CodeCompleteOptions Opts = {}, + PathRef FilePath = "foo.cpp") { + std::unique_ptr OverrideIndex; + if (!IndexSymbols.empty()) { + assert(!Opts.Index && "both Index and IndexSymbols given!"); + OverrideIndex = memIndex(std::move(IndexSymbols)); + Opts.Index = OverrideIndex.get(); + } + + MockFSProvider FS; + Annotations Test(Text); + return codeCompleteNoCompile(FilePath, Test.code(), Test.point(), + FS.getFileSystem(), Opts); +} + Symbol withReferences(int N, Symbol S) { S.References = N; return S; @@ -2366,6 +2384,33 @@ UnorderedElementsAre(AllOf(Qualifier(""), Named("XYZ")))); } +TEST(NoCompileCompletionTest, Basic) { + auto Results = completionsNoCompile(R"cpp( + void func() { + int xyz; + int abc; + ^ + } + )cpp"); + EXPECT_THAT(Results.Completions, + UnorderedElementsAre(Named("void"), Named("func"), Named("int"), + Named("xyz"), Named("abc"))); +} + +TEST(NoCompileCompletionTest, WithFilter) { + auto Results = completionsNoCompile(R"cpp( + void func() { + int sym1; + int sym2; + int xyz1; + int xyz2; + sy^ + } + )cpp"); + EXPECT_THAT(Results.Completions, + UnorderedElementsAre(Named("sym1"), Named("sym2"))); +} + } // namespace } // namespace clangd } // namespace clang Index: unittests/clangd/HeadersTests.cpp =================================================================== --- unittests/clangd/HeadersTests.cpp +++ unittests/clangd/HeadersTests.cpp @@ -90,7 +90,7 @@ IncludeInserter Inserter(MainFile, /*Code=*/"", format::getLLVMStyle(), CDB.getCompileCommand(MainFile)->Directory, - Clang->getPreprocessor().getHeaderSearchInfo()); + &Clang->getPreprocessor().getHeaderSearchInfo()); for (const auto &Inc : Inclusions) Inserter.addExisting(Inc); auto Declaring = ToHeaderFile(Original); @@ -110,7 +110,7 @@ IncludeInserter Inserter(MainFile, /*Code=*/"", format::getLLVMStyle(), CDB.getCompileCommand(MainFile)->Directory, - Clang->getPreprocessor().getHeaderSearchInfo()); + &Clang->getPreprocessor().getHeaderSearchInfo()); auto Edit = Inserter.insert(VerbatimHeader); Action.EndSourceFile(); return Edit; @@ -252,6 +252,24 @@ EXPECT_TRUE(StringRef(Edit->newText).contains("")); } +TEST(Headers, NoHeaderSearchInfo) { + std::string MainFile = testPath("main.cpp"); + IncludeInserter Inserter(MainFile, /*Code=*/"", format::getLLVMStyle(), + /*BuildDir=*/"", /*HeaderSearchInfo=*/nullptr); + + auto HeaderPath = testPath("sub/bar.h"); + auto Declaring = HeaderFile{HeaderPath, /*Verbatim=*/false}; + auto Inserting = HeaderFile{HeaderPath, /*Verbatim=*/false}; + auto Verbatim = HeaderFile{"", /*Verbatim=*/true}; + + EXPECT_EQ(Inserter.calculateIncludePath(Declaring, Inserting), + "\"" + HeaderPath + "\""); + EXPECT_EQ(Inserter.shouldInsertInclude(Declaring, Inserting), false); + + EXPECT_EQ(Inserter.calculateIncludePath(Declaring, Verbatim), ""); + EXPECT_EQ(Inserter.shouldInsertInclude(Declaring, Verbatim), true); +} + } // namespace } // namespace clangd } // namespace clang