ioeric created this revision.
ioeric added reviewers: sammccall, ilya-biryukov, hokein.
Herald added subscribers: cfe-commits, jkorous, MaskRay.

For completion items that would trigger include insertions (i.e. index symbols
that are not #included yet), add a visual indicator "+" before the completion
label. The inserted headers will appear in the completion detail.

Open to suggestions for better visual indicators; "+" was picked because it
seems cleaner than a few other candidates I've tried (*, #, @ ...).

The displayed header would be like a/b/c.h (without quote) or <vector> for 
system
headers. I didn't add quotation or "#include" because they can take up limited
space and do not provide additional information after users know what the
headers are. I think a header alone should be obvious for users to infer that
this is an include header..

To align indentation, also prepend ' ' to labels of candidates that would not
trigger include insertions (only for completions where index results are
possible).

(Placeholder: add screenshots)


Repository:
  rCTE Clang Tools Extra

https://reviews.llvm.org/D48163

Files:
  clangd/CodeComplete.cpp
  clangd/Headers.cpp
  clangd/Headers.h
  test/clangd/completion-snippets.test
  unittests/clangd/CodeCompleteTests.cpp
  unittests/clangd/HeadersTests.cpp

Index: unittests/clangd/HeadersTests.cpp
===================================================================
--- unittests/clangd/HeadersTests.cpp
+++ unittests/clangd/HeadersTests.cpp
@@ -106,7 +106,7 @@
     } else {
       EXPECT_FALSE(ExpectError);
     }
-    return std::move(*Path);
+    return Path->second ? std::move(Path->first) : "";
   }
 
   Expected<Optional<TextEdit>>
@@ -123,9 +123,13 @@
     for (const auto &Inc : ExistingInclusions)
       Inserter.addExisting(Inc);
 
-    auto Edit = Inserter.insert(DeclaringHeader, InsertedHeader);
+    auto HeaderAndEdit = Inserter.insert(DeclaringHeader, InsertedHeader);
     Action.EndSourceFile();
-    return Edit;
+    if (!HeaderAndEdit)
+      return HeaderAndEdit.takeError();
+    if (!HeaderAndEdit->second)
+      return llvm::None;
+    return *(HeaderAndEdit->second);
   }
 
   MockFSProvider FS;
Index: unittests/clangd/CodeCompleteTests.cpp
===================================================================
--- unittests/clangd/CodeCompleteTests.cpp
+++ unittests/clangd/CodeCompleteTests.cpp
@@ -566,17 +566,17 @@
           int main() { ns::^ }
       )cpp",
                              {Sym});
-  EXPECT_THAT(Results.items,
-              ElementsAre(AllOf(Named("X"), InsertInclude("\"bar.h\""))));
+  EXPECT_THAT(Results.items, ElementsAre(AllOf(Named("X"), Labeled("+X"),
+                                               InsertInclude("\"bar.h\""))));
   // Duplicate based on inclusions in preamble.
   Results = completions(Server,
                         R"cpp(
           #include "sub/bar.h"  // not shortest, so should only match resolved.
           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) {
Index: test/clangd/completion-snippets.test
===================================================================
--- test/clangd/completion-snippets.test
+++ test/clangd/completion-snippets.test
@@ -32,7 +32,7 @@
 # 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:    ]
Index: clangd/Headers.h
===================================================================
--- clangd/Headers.h
+++ clangd/Headers.h
@@ -60,11 +60,12 @@
 /// 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:
+/// \return A quoted "path" or <path> to be included and a boolean that is true
+/// if the include can be added, and false 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(
+llvm::Expected<std::pair<std::string, bool>> calculateIncludePath(
     PathRef File, StringRef BuildDir, HeaderSearch &HeaderSearchInfo,
     const std::vector<Inclusion> &Inclusions, const HeaderFile &DeclaringHeader,
     const HeaderFile &InsertedHeader);
@@ -81,16 +82,18 @@
 
   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.
+  /// Returns a <Header, Edit> pair where Header is verbatim
+  /// (e.g. <vector>) and Edit, if not None, inserts the entire include
+  /// directive. Edit is None is the header is not suitable to include e.g.
+  /// it's already included. If any header is invalid, this returns error.
   ///
   /// \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;
+  Expected<std::pair<std::string, Optional<TextEdit>>>
+  insert(const HeaderFile &DeclaringHeader,
+         const HeaderFile &InsertedHeader) const;
 
 private:
   StringRef FileName;
Index: clangd/Headers.cpp
===================================================================
--- clangd/Headers.cpp
+++ clangd/Headers.cpp
@@ -72,13 +72,13 @@
 
 /// FIXME(ioeric): we might not want to insert an absolute include path if the
 /// path is not shortened.
-llvm::Expected<std::string> calculateIncludePath(
+llvm::Expected<std::pair<std::string, bool>> calculateIncludePath(
     PathRef File, StringRef BuildDir, HeaderSearch &HeaderSearchInfo,
     const std::vector<Inclusion> &Inclusions, const HeaderFile &DeclaringHeader,
     const HeaderFile &InsertedHeader) {
   assert(DeclaringHeader.valid() && InsertedHeader.valid());
-  if (File == DeclaringHeader.File || File == InsertedHeader.File)
-    return "";
+  bool AddInclude =
+      File != DeclaringHeader.File && File != InsertedHeader.File;
   llvm::StringSet<> IncludedHeaders;
   for (const auto &Inc : Inclusions) {
     IncludedHeaders.insert(Inc.Written);
@@ -88,26 +88,24 @@
   auto Included = [&](llvm::StringRef Header) {
     return IncludedHeaders.find(Header) != IncludedHeaders.end();
   };
-  if (Included(DeclaringHeader.File) || Included(InsertedHeader.File))
-    return "";
+  AddInclude &=
+      !Included(DeclaringHeader.File) && !Included(InsertedHeader.File);
 
   bool IsSystem = false;
 
   if (InsertedHeader.Verbatim)
-    return InsertedHeader.File;
+    return std::make_pair(InsertedHeader.File, AddInclude);
 
   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;
+  return std::make_pair(Suggested, AddInclude);
 }
 
-Expected<Optional<TextEdit>>
+Expected<std::pair<std::string, Optional<TextEdit>>>
 IncludeInserter::insert(const HeaderFile &DeclaringHeader,
                         const HeaderFile &InsertedHeader) const {
   auto Validate = [](const HeaderFile &Header) {
@@ -127,14 +125,13 @@
                            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);
+  StringRef IncludeRef = Include->first;
+  Optional<TextEdit> Edit = None;
+  if (Include->second)
+    if (auto Insertion = Inserter.insert(IncludeRef.trim("\"<>"),
+                                         IncludeRef.startswith("<")))
+      Edit = replacementToEdit(Code, *Insertion);
+  return std::make_pair(IncludeRef, std::move(Edit));
 }
 
 } // namespace clangd
Index: clangd/CodeComplete.cpp
===================================================================
--- clangd/CodeComplete.cpp
+++ clangd/CodeComplete.cpp
@@ -183,16 +183,22 @@
   // We may have a result from Sema, from the index, or both.
   const CodeCompletionResult *SemaResult = nullptr;
   const Symbol *IndexResult = nullptr;
+  // Whether or not this candidate is in a completion where index is allowed.
+  // This can affect how items are built (e.g. indent label to align with visual
+  // indicator in index results).
+  bool AllowIndexCompletion = false;
 
   // Builds an LSP completion item.
   CompletionItem build(StringRef FileName, const CompletionItemScores &Scores,
                        const CodeCompleteOptions &Opts,
                        CodeCompletionString *SemaCCS,
                        const IncludeInserter *Includes,
                        llvm::StringRef SemaDocComment) const {
     assert(bool(SemaResult) == bool(SemaCCS));
+    assert((AllowIndexCompletion || !IndexResult));
     CompletionItem I;
     bool ShouldInsertInclude = true;
+    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,
@@ -236,7 +242,8 @@
         if (I.detail.empty())
           I.detail = D->CompletionDetail;
         if (ShouldInsertInclude && Includes && !D->IncludeHeader.empty()) {
-          auto Edit = [&]() -> Expected<Optional<TextEdit>> {
+          auto HeaderAndEdit =
+              [&]() -> Expected<std::pair<std::string, Optional<TextEdit>>> {
             auto ResolvedDeclaring = toHeaderFile(
                 IndexResult->CanonicalDeclaration.FileURI, FileName);
             if (!ResolvedDeclaring)
@@ -246,21 +253,29 @@
               return ResolvedInserted.takeError();
             return Includes->insert(*ResolvedDeclaring, *ResolvedInserted);
           }();
-          if (!Edit) {
+          if (!HeaderAndEdit) {
             std::string ErrMsg =
                 ("Failed to generate include insertion edits for adding header "
                  "(FileURI=\"" +
                  IndexResult->CanonicalDeclaration.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(HeaderAndEdit.takeError()));
+          } else {
+            InsertingInclude = static_cast<bool>(HeaderAndEdit->second);
+            if (InsertingInclude) {
+              I.detail += ((I.detail.empty() ? "" : "\n") +
+                           StringRef(HeaderAndEdit->first).trim('"'))
+                              .str();
+              I.additionalTextEdits = {std::move(*(HeaderAndEdit->second))};
+            }
           }
         }
       }
     }
+    if (AllowIndexCompletion)
+      I.label = (InsertingInclude ? "+" : " ") + I.label;
     I.scoreInfo = Scores;
     I.sortText = sortText(Scores.finalScore, Name);
     I.insertTextFormat = Opts.EnableSnippets ? InsertTextFormat::Snippet
@@ -848,6 +863,7 @@
   bool Incomplete = false; // Would more be available with a higher limit?
   llvm::Optional<FuzzyMatcher> Filter;       // Initialized once Sema runs.
   std::unique_ptr<IncludeInserter> Includes; // Initialized once compiler runs.
+  bool AllowIndexCompletion = false;         // Initialized once Sema runs.
 
 public:
   // A CodeCompleteFlow object is only useful for calling run() exactly once.
@@ -897,12 +913,13 @@
   CompletionList runWithSema() {
     Filter = FuzzyMatcher(
         Recorder->CCSema->getPreprocessor().getCodeCompletionFilter());
+    AllowIndexCompletion = Opts.Index && allowIndex(Recorder->CCContext);
     // Sema provides the needed context to query the index.
     // FIXME: in addition to querying for extra/overlapping symbols, we should
     //        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 = AllowIndexCompletion ? 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.
@@ -914,8 +931,6 @@
   }
 
   SymbolSlab queryIndex() {
-    if (!Opts.Index || !allowIndex(Recorder->CCContext))
-      return SymbolSlab();
     trace::Span Tracer("Query index");
     SPAN_ATTACH(Tracer, "limit", Opts.Limit);
 
@@ -986,6 +1001,7 @@
                     const CodeCompletionResult *SemaResult,
                     const Symbol *IndexResult) {
     CompletionCandidate C;
+    C.AllowIndexCompletion = AllowIndexCompletion;
     C.SemaResult = SemaResult;
     C.IndexResult = IndexResult;
     C.Name = IndexResult ? IndexResult->Name : Recorder->getName(*SemaResult);
_______________________________________________
cfe-commits mailing list
cfe-commits@lists.llvm.org
http://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to