CompletionItem build(StringRef FileName, const CompletionItemScores &Scores,
const CodeCompleteOptions &Opts,
CodeCompletionString *SemaCCS,
- const IncludeInserter *Includes,
+ const IncludeInserter &Includes,
llvm::StringRef SemaDocComment) const {
assert(bool(SemaResult) == bool(SemaCCS));
CompletionItem I;
+ bool InsertingInclude = false; // Whether a new #include will be added.
if (SemaResult) {
I.kind = toCompletionItemKind(SemaResult->Kind, SemaResult->Declaration);
getLabelAndInsertText(*SemaCCS, &I.label, &I.insertText,
if (I.detail.empty())
I.detail = D->CompletionDetail;
if (auto Inserted = headerToInsertIfNotPresent()) {
- auto Edit = [&]() -> Expected<Optional<TextEdit>> {
+ auto IncludePath = [&]() -> Expected<std::string> {
auto ResolvedDeclaring = toHeaderFile(
IndexResult->CanonicalDeclaration.FileURI, FileName);
if (!ResolvedDeclaring)
auto ResolvedInserted = toHeaderFile(*Inserted, FileName);
if (!ResolvedInserted)
return ResolvedInserted.takeError();
- return Includes->insert(*ResolvedDeclaring, *ResolvedInserted);
+ if (!Includes.shouldInsertInclude(*ResolvedDeclaring,
+ *ResolvedInserted))
+ return "";
+ return Includes.calculateIncludePath(*ResolvedDeclaring,
+ *ResolvedInserted);
}();
- if (!Edit) {
+ if (!IncludePath) {
std::string ErrMsg =
("Failed to generate include insertion edits for adding header "
"(FileURI=\"" +
"\", IncludeHeader=\"" + D->IncludeHeader + "\") into " +
FileName)
.str();
- log(ErrMsg + ":" + llvm::toString(Edit.takeError()));
- } else if (*Edit) {
- I.additionalTextEdits = {std::move(**Edit)};
+ log(ErrMsg + ":" + llvm::toString(IncludePath.takeError()));
+ } else if (!IncludePath->empty()) {
+ // FIXME: consider what to show when there is no #include insertion,
+ // and for sema results, for consistency.
+ if (auto Edit = Includes.insert(*IncludePath)) {
+ I.detail += ("\n" + StringRef(*IncludePath).trim('"')).str();
+ I.additionalTextEdits = {std::move(*Edit)};
+ InsertingInclude = true;
+ }
}
}
}
}
+ I.label = (InsertingInclude ? Opts.IncludeIndicator.Insert
+ : Opts.IncludeIndicator.NoInsert) +
+ I.label;
I.scoreInfo = Scores;
I.sortText = sortText(Scores.finalScore, Name);
I.insertTextFormat = Opts.EnableSnippets ? InsertTextFormat::Snippet
llvm::StringRef Name = Bundle.front().Name;
First.insertText =
Opts.EnableSnippets ? (Name + "(${0})").str() : Name.str();
- First.label = (Name + "(…)").str();
+ // Keep the visual indicator of the original label.
+ bool InsertingInclude =
+ StringRef(First.label).startswith(Opts.IncludeIndicator.Insert);
+ First.label = (Twine(InsertingInclude ? Opts.IncludeIndicator.Insert
+ : Opts.IncludeIndicator.NoInsert) +
+ Name + "(…)")
+ .str();
First.detail = llvm::formatv("[{0} overloads]", Bundle.size());
return First;
}
// explicitly request symbols corresponding to Sema results.
// 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 = queryIndex();
+ auto IndexResults = (Opts.Index && allowIndex(Recorder->CCContext))
+ ? queryIndex()
+ : SymbolSlab();
// Merge Sema and Index results, score them, and pick the winners.
auto Top = mergeResults(Recorder->Results, IndexResults);
// Convert the results to the desired LSP structs.
}
SymbolSlab queryIndex() {
- if (!Opts.Index || !allowIndex(Recorder->CCContext))
- return SymbolSlab();
trace::Span Tracer("Query index");
SPAN_ATTACH(Tracer, "limit", Opts.Limit);
}
return CompletionCandidate::build(
Bundle,
- Bundle.front().build(FileName, Scores, Opts, SemaCCS, Includes.get(),
+ Bundle.front().build(FileName, Scores, Opts, SemaCCS, *Includes,
FrontDocComment),
Opts);
}
/// If more results are available, we set CompletionList.isIncomplete.
size_t Limit = 0;
+ /// A visual indicator to prepend to the completion label to indicate whether
+ /// completion result would trigger an #include insertion or not.
+ struct IncludeInsertionIndicator {
+ std::string Insert = "•";
+ std::string NoInsert = " ";
+ } IncludeIndicator;
+
// Populated internally by clangd, do not set.
/// If `Index` is set, it is used to augment the code completion
/// results.
/// FIXME(ioeric): we might not want to insert an absolute include path if the
/// path is not shortened.
-llvm::Expected<std::string> calculateIncludePath(
- PathRef File, StringRef BuildDir, HeaderSearch &HeaderSearchInfo,
- const std::vector<Inclusion> &Inclusions, const HeaderFile &DeclaringHeader,
- const HeaderFile &InsertedHeader) {
+bool IncludeInserter::shouldInsertInclude(
+ const HeaderFile &DeclaringHeader, const HeaderFile &InsertedHeader) const {
assert(DeclaringHeader.valid() && InsertedHeader.valid());
- if (File == DeclaringHeader.File || File == InsertedHeader.File)
- return "";
+ if (FileName == DeclaringHeader.File || FileName == InsertedHeader.File)
+ return false;
llvm::StringSet<> IncludedHeaders;
for (const auto &Inc : Inclusions) {
IncludedHeaders.insert(Inc.Written);
auto Included = [&](llvm::StringRef Header) {
return IncludedHeaders.find(Header) != IncludedHeaders.end();
};
- if (Included(DeclaringHeader.File) || Included(InsertedHeader.File))
- return "";
-
- bool IsSystem = false;
+ return !Included(DeclaringHeader.File) && !Included(InsertedHeader.File);
+}
+std::string
+IncludeInserter::calculateIncludePath(const HeaderFile &DeclaringHeader,
+ const HeaderFile &InsertedHeader) const {
+ assert(DeclaringHeader.valid() && InsertedHeader.valid());
if (InsertedHeader.Verbatim)
return InsertedHeader.File;
-
+ bool IsSystem = false;
std::string Suggested = HeaderSearchInfo.suggestPathToFileForDiagnostics(
InsertedHeader.File, BuildDir, &IsSystem);
if (IsSystem)
Suggested = "<" + Suggested + ">";
else
Suggested = "\"" + Suggested + "\"";
-
- log("Suggested #include for " + InsertedHeader.File + " is: " + Suggested);
return Suggested;
}
-Expected<Optional<TextEdit>>
-IncludeInserter::insert(const HeaderFile &DeclaringHeader,
- const HeaderFile &InsertedHeader) const {
- auto Validate = [](const HeaderFile &Header) {
- return Header.valid()
- ? llvm::Error::success()
- : llvm::make_error<llvm::StringError>(
- "Invalid HeaderFile: " + Header.File +
- " (verbatim=" + std::to_string(Header.Verbatim) + ").",
- llvm::inconvertibleErrorCode());
- };
- if (auto Err = Validate(DeclaringHeader))
- return std::move(Err);
- if (auto Err = Validate(InsertedHeader))
- return std::move(Err);
- auto Include =
- calculateIncludePath(FileName, BuildDir, HeaderSearchInfo, Inclusions,
- DeclaringHeader, InsertedHeader);
- if (!Include)
- return Include.takeError();
- if (Include->empty())
- return llvm::None;
- StringRef IncludeRef = *Include;
- auto Insertion =
- Inserter.insert(IncludeRef.trim("\"<>"), IncludeRef.startswith("<"));
- if (!Insertion)
- return llvm::None;
- return replacementToEdit(Code, *Insertion);
+Optional<TextEdit> IncludeInserter::insert(StringRef VerbatimHeader) const {
+ Optional<TextEdit> Edit = None;
+ if (auto Insertion = Inserter.insert(VerbatimHeader.trim("\"<>"),
+ VerbatimHeader.startswith("<")))
+ Edit = replacementToEdit(Code, *Insertion);
+ return Edit;
}
} // namespace clangd
collectInclusionsInMainFileCallback(const SourceManager &SM,
std::function<void(Inclusion)> Callback);
-/// Determines the preferred way to #include a file, taking into account the
-/// search path. Usually this will prefer a shorter representation like
-/// 'Foo/Bar.h' over a longer one like 'Baz/include/Foo/Bar.h'.
-///
-/// \param File is an absolute file path.
-/// \param Inclusions Existing inclusions in the main file.
-/// \param DeclaringHeader is the original header corresponding to \p
-/// InsertedHeader e.g. the header that declares a symbol.
-/// \param InsertedHeader The preferred header to be inserted. This could be the
-/// same as DeclaringHeader but must be provided.
-// \return A quoted "path" or <path>. This returns an empty string if:
-/// - Either \p DeclaringHeader or \p InsertedHeader is already (directly)
-/// in \p Inclusions (including those included via different paths).
-/// - \p DeclaringHeader or \p InsertedHeader is the same as \p File.
-llvm::Expected<std::string> calculateIncludePath(
- PathRef File, StringRef BuildDir, HeaderSearch &HeaderSearchInfo,
- const std::vector<Inclusion> &Inclusions, const HeaderFile &DeclaringHeader,
- const HeaderFile &InsertedHeader);
-
// Calculates insertion edit for including a new header in a file.
class IncludeInserter {
public:
void addExisting(Inclusion Inc) { Inclusions.push_back(std::move(Inc)); }
- /// Returns a TextEdit that inserts a new header; if the header is not
- /// inserted e.g. it's an existing header, this returns None. If any header is
- /// invalid, this returns error.
+ /// Checks whether to add an #include of the header into \p File.
+ /// An #include will not be added if:
+ /// - Either \p DeclaringHeader or \p InsertedHeader is already (directly)
+ /// in \p Inclusions (including those included via different paths).
+ /// - \p DeclaringHeader or \p InsertedHeader is the same as \p File.
+ ///
+ /// \param DeclaringHeader is the original header corresponding to \p
+ /// InsertedHeader e.g. the header that declares a symbol.
+ /// \param InsertedHeader The preferred header to be inserted. This could be
+ /// the same as DeclaringHeader but must be provided. \param Inclusions
+ /// Existing includes in the main file.
+ bool shouldInsertInclude(const HeaderFile &DeclaringHeader,
+ const HeaderFile &InsertedHeader) const;
+
+ /// Determines the preferred way to #include a file, taking into account the
+ /// search path. Usually this will prefer a shorter representation like
+ /// 'Foo/Bar.h' over a longer one like 'Baz/include/Foo/Bar.h'.
///
/// \param DeclaringHeader is the original header corresponding to \p
/// InsertedHeader e.g. the header that declares a symbol.
/// \param InsertedHeader The preferred header to be inserted. This could be
/// the same as DeclaringHeader but must be provided.
- Expected<Optional<TextEdit>> insert(const HeaderFile &DeclaringHeader,
- const HeaderFile &InsertedHeader) const;
+ ///
+ /// \return A quoted "path" or <path> to be included.
+ std::string calculateIncludePath(const HeaderFile &DeclaringHeader,
+ const HeaderFile &InsertedHeader) const;
+
+ /// Calculates an edit that inserts \p VerbatimHeader into code. If the header
+ /// is already included, this returns None.
+ llvm::Optional<TextEdit> insert(llvm::StringRef VerbatimHeader) const;
private:
StringRef FileName;
# CHECK-NEXT: "insertText": "func_with_args(${1:int a}, ${2:int b})",
# CHECK-NEXT: "insertTextFormat": 2,
# CHECK-NEXT: "kind": 3,
-# CHECK-NEXT: "label": "func_with_args(int a, int b)",
+# CHECK-NEXT: "label": " func_with_args(int a, int b)",
# CHECK-NEXT: "sortText": "{{.*}}func_with_args"
# CHECK-NEXT: }
# CHECK-NEXT: ]
# CHECK-NEXT: "insertText": "a",
# CHECK-NEXT: "insertTextFormat": 1,
# CHECK-NEXT: "kind": 5,
-# CHECK-NEXT: "label": "a",
+# CHECK-NEXT: "label": " a",
# CHECK-NEXT: "sortText": "{{.*}}a"
# CHECK-NEXT: }
# CHECK-NEXT: ]
# CHECK-NEXT: "insertText": "b",
# CHECK-NEXT: "insertTextFormat": 1,
# CHECK-NEXT: "kind": 5,
-# CHECK-NEXT: "label": "b",
+# CHECK-NEXT: "label": " b",
# CHECK-NEXT: "sortText": "{{.*}}b"
# CHECK-NEXT: }
# CHECK-NEXT: ]
# CHECK-NEXT: "insertText": "a",\r
# CHECK-NEXT: "insertTextFormat": 1,\r
# CHECK-NEXT: "kind": 5,\r
-# CHECK-NEXT: "label": "a",\r
+# CHECK-NEXT: "label": " a",\r
# CHECK-NEXT: "sortText": "{{.*}}"\r
# CHECK: ]\r
# CHECK-NEXT: }\r
# CHECK-NEXT: "insertText": "a",\r
# CHECK-NEXT: "insertTextFormat": 1,\r
# CHECK-NEXT: "kind": 5,\r
-# CHECK-NEXT: "label": "a",\r
+# CHECK-NEXT: "label": " a",\r
# CHECK-NEXT: "sortText": "{{.*}}"\r
# CHECK: ]\r
# CHECK-NEXT: }\r
# CHECK-NEXT: "insertText": "a",\r
# CHECK-NEXT: "insertTextFormat": 1,\r
# CHECK-NEXT: "kind": 5,\r
-# CHECK-NEXT: "label": "a",\r
+# CHECK-NEXT: "label": " a",\r
# CHECK-NEXT: "sortText": "{{.*}}"\r
# CHECK: ]\r
# CHECK-NEXT: }\r
// GMock helpers for matching completion items.
MATCHER_P(Named, Name, "") { return arg.insertText == Name; }
-MATCHER_P(Labeled, Label, "") { return arg.label == Label; }
+MATCHER_P(Labeled, Label, "") {
+ std::string Indented;
+ if (!StringRef(Label).startswith(
+ CodeCompleteOptions().IncludeIndicator.Insert) &&
+ !StringRef(Label).startswith(
+ CodeCompleteOptions().IncludeIndicator.NoInsert))
+ Indented =
+ (Twine(CodeCompleteOptions().IncludeIndicator.NoInsert) + Label).str();
+ else
+ Indented = Label;
+ return arg.label == Indented;
+}
+MATCHER_P(SigHelpLabeled, Label, "") { return arg.label == Label; }
MATCHER_P(Kind, K, "") { return arg.kind == K; }
MATCHER_P(Filter, F, "") { return arg.filterText == F; }
MATCHER_P(Doc, D, "") { return arg.documentation == D; }
)cpp",
{Sym});
EXPECT_THAT(Results.items,
- ElementsAre(AllOf(Named("X"), InsertInclude("\"bar.h\""))));
+ ElementsAre(AllOf(
+ Named("X"),
+ Labeled(CodeCompleteOptions().IncludeIndicator.Insert + "X"),
+ InsertInclude("\"bar.h\""))));
// Duplicate based on inclusions in preamble.
Results = completions(Server,
R"cpp(
int main() { ns::^ }
)cpp",
{Sym});
- EXPECT_THAT(Results.items,
- ElementsAre(AllOf(Named("X"), Not(HasAdditionalEdits()))));
+ EXPECT_THAT(Results.items, ElementsAre(AllOf(Named("X"), Labeled("X"),
+ Not(HasAdditionalEdits()))));
}
TEST(CompletionTest, NoIncludeInsertionWhenDeclFoundInFile) {
Matcher<SignatureInformation> Sig(std::string Label,
std::vector<std::string> Params) {
- return AllOf(Labeled(Label), ParamsAre(Params));
+ return AllOf(SigHelpLabeled(Label), ParamsAre(Params));
}
TEST(SignatureHelpTest, Overloads) {
NoArgsGFunc.Detail = &Detail;
EXPECT_THAT(
completions(Context + "int y = GFunc^", {NoArgsGFunc}, Opts).items,
- UnorderedElementsAre(AllOf(Named("GFuncC"), InsertInclude("<foo>")),
- Labeled("GFuncC(int)"), Labeled("GFuncD(int)")));
+ UnorderedElementsAre(
+ AllOf(
+ Named("GFuncC"),
+ Labeled(CodeCompleteOptions().IncludeIndicator.Insert + "GFuncC"),
+ InsertInclude("<foo>")),
+ Labeled("GFuncC(int)"), Labeled("GFuncD(int)")));
// Examine a bundled completion in detail.
auto A = completions(Context + "int y = X().a^", {}, Opts).items.front();
- EXPECT_EQ(A.label, "a(…)");
+ EXPECT_EQ(A.label, " a(…)");
EXPECT_EQ(A.detail, "[2 overloads]");
EXPECT_EQ(A.insertText, "a");
EXPECT_EQ(A.kind, CompletionItemKind::Method);
return Inclusions;
}
- // Calculates the include path, or returns "" on error.
+ // Calculates the include path, or returns "" on error or header should not be
+ // inserted.
std::string calculate(PathRef Original, PathRef Preferred = "",
- const std::vector<Inclusion> &Inclusions = {},
- bool ExpectError = false) {
+ const std::vector<Inclusion> &Inclusions = {}) {
auto Clang = setupClang();
PreprocessOnlyAction Action;
EXPECT_TRUE(
/*Verbatim=*/!llvm::sys::path::is_absolute(Header)};
};
- auto Path = calculateIncludePath(
- MainFile, CDB.getCompileCommand(MainFile)->Directory,
- Clang->getPreprocessor().getHeaderSearchInfo(), Inclusions,
- ToHeaderFile(Original), ToHeaderFile(Preferred));
+ IncludeInserter Inserter(MainFile, /*Code=*/"", format::getLLVMStyle(),
+ CDB.getCompileCommand(MainFile)->Directory,
+ Clang->getPreprocessor().getHeaderSearchInfo());
+ for (const auto &Inc : Inclusions)
+ Inserter.addExisting(Inc);
+ auto Declaring = ToHeaderFile(Original);
+ auto Inserted = ToHeaderFile(Preferred);
+ if (!Inserter.shouldInsertInclude(Declaring, Inserted))
+ return "";
+ std::string Path = Inserter.calculateIncludePath(Declaring, Inserted);
Action.EndSourceFile();
- if (!Path) {
- llvm::consumeError(Path.takeError());
- EXPECT_TRUE(ExpectError);
- return std::string();
- } else {
- EXPECT_FALSE(ExpectError);
- }
- return std::move(*Path);
+ return Path;
}
- Expected<Optional<TextEdit>>
- insert(const HeaderFile &DeclaringHeader, const HeaderFile &InsertedHeader,
- const std::vector<Inclusion> &ExistingInclusions = {}) {
+ Optional<TextEdit> insert(StringRef VerbatimHeader) {
auto Clang = setupClang();
PreprocessOnlyAction Action;
EXPECT_TRUE(
IncludeInserter Inserter(MainFile, /*Code=*/"", format::getLLVMStyle(),
CDB.getCompileCommand(MainFile)->Directory,
Clang->getPreprocessor().getHeaderSearchInfo());
- for (const auto &Inc : ExistingInclusions)
- Inserter.addExisting(Inc);
-
- auto Edit = Inserter.insert(DeclaringHeader, InsertedHeader);
+ auto Edit = Inserter.insert(VerbatimHeader);
Action.EndSourceFile();
return Edit;
}
EXPECT_EQ(calculate(BarHeader, "\"BAR.h\"", Inclusions), "");
}
-HeaderFile literal(StringRef Include) {
- HeaderFile H{Include, /*Verbatim=*/true};
- assert(H.valid());
- return H;
-}
-
TEST_F(HeadersTest, PreferInserted) {
- auto Edit = insert(literal("<x>"), literal("<y>"));
- EXPECT_TRUE(static_cast<bool>(Edit));
- EXPECT_TRUE(llvm::StringRef((*Edit)->newText).contains("<y>"));
-}
-
-TEST_F(HeadersTest, ExistingInclusion) {
- Inclusion Existing{Range(), /*Written=*/"<c.h>", /*Resolved=*/""};
- auto Edit = insert(literal("<c.h>"), literal("<c.h>"), {Existing});
- EXPECT_TRUE(static_cast<bool>(Edit));
- EXPECT_FALSE(static_cast<bool>(*Edit));
-}
-
-TEST_F(HeadersTest, ValidateHeaders) {
- HeaderFile InvalidHeader{"a.h", /*Verbatim=*/true};
- assert(!InvalidHeader.valid());
- auto Edit = insert(InvalidHeader, literal("\"c.h\""));
- EXPECT_FALSE(static_cast<bool>(Edit));
- llvm::consumeError(Edit.takeError());
+ auto Edit = insert("<y>");
+ EXPECT_TRUE(Edit.hasValue());
+ EXPECT_TRUE(llvm::StringRef(Edit->newText).contains("<y>"));
}
} // namespace