diff --git a/clang-tools-extra/clangd/CodeComplete.h b/clang-tools-extra/clangd/CodeComplete.h --- a/clang-tools-extra/clangd/CodeComplete.h +++ b/clang-tools-extra/clangd/CodeComplete.h @@ -155,6 +155,10 @@ struct CodeCompletion { // The unqualified name of the symbol or other completion item. std::string Name; + // The name of the symbol for filtering and sorting purposes. Typically the + // same as `Name`, but may be different e.g. for ObjC methods, `Name` is the + // first selector fragment but the `FilterText` is the entire selector. + std::string FilterText; // The scope qualifier for the symbol name. e.g. "ns1::ns2::" // Empty for non-symbol completions. Not inserted, but may be displayed. std::string Scope; diff --git a/clang-tools-extra/clangd/CodeComplete.cpp b/clang-tools-extra/clangd/CodeComplete.cpp --- a/clang-tools-extra/clangd/CodeComplete.cpp +++ b/clang-tools-extra/clangd/CodeComplete.cpp @@ -303,6 +303,7 @@ assert(ASTCtx); Completion.Origin |= SymbolOrigin::AST; Completion.Name = std::string(llvm::StringRef(SemaCCS->getTypedText())); + Completion.FilterText = SemaCCS->getAllTypedText(); if (Completion.Scope.empty()) { if ((C.SemaResult->Kind == CodeCompletionResult::RK_Declaration) || (C.SemaResult->Kind == CodeCompletionResult::RK_Pattern)) @@ -335,6 +336,8 @@ Completion.Kind = toCompletionItemKind(C.IndexResult->SymInfo.Kind); if (Completion.Name.empty()) Completion.Name = std::string(C.IndexResult->Name); + if (Completion.FilterText.empty()) + Completion.FilterText = Completion.Name; // If the completion was visible to Sema, no qualifier is needed. This // avoids unneeded qualifiers in cases like with `using ns::X`. if (Completion.RequiredQualifier.empty() && !C.SemaResult) { @@ -352,6 +355,7 @@ Completion.Origin |= SymbolOrigin::Identifier; Completion.Kind = CompletionItemKind::Text; Completion.Name = std::string(C.IdentifierResult->Name); + Completion.FilterText = Completion.Name; } // Turn absolute path into a literal string that can be #included. @@ -860,7 +864,10 @@ return Result.Pattern->getTypedText(); } auto *CCS = codeCompletionString(Result); - return CCS->getTypedText(); + if (!CCS->hasMultipleTypedTextChunks()) + return CCS->getTypedText(); + std::string Name = CCS->getAllTypedText(); + return CCAllocator->CopyString(Name); } // Build a CodeCompletion string for R, which must be from Results. @@ -2118,8 +2125,8 @@ Doc.append(*Documentation); LSP.documentation = renderDoc(Doc, Opts.DocumentationFormat); } - LSP.sortText = sortText(Score.Total, Name); - LSP.filterText = Name; + LSP.sortText = sortText(Score.Total, FilterText); + LSP.filterText = FilterText; LSP.textEdit = {CompletionTokenRange, RequiredQualifier + Name}; // Merge continuous additionalTextEdits into main edit. The main motivation // behind this is to help LSP clients, it seems most of them are confused when diff --git a/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp b/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp --- a/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp +++ b/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp @@ -58,6 +58,7 @@ MATCHER_P(nameStartsWith, Prefix, "") { return llvm::StringRef(arg.Name).startswith(Prefix); } +MATCHER_P(filterText, F, "") { return arg.FilterText == F; } MATCHER_P(scope, S, "") { return arg.Scope == S; } MATCHER_P(qualifier, Q, "") { return arg.RequiredQualifier == Q; } MATCHER_P(labeled, Label, "") { @@ -1918,6 +1919,7 @@ TEST(CompletionTest, Render) { CodeCompletion C; C.Name = "x"; + C.FilterText = "x"; C.Signature = "(bool) const"; C.SnippetSuffix = "(${0:bool})"; C.ReturnType = "int"; @@ -1950,6 +1952,11 @@ EXPECT_FALSE(R.deprecated); EXPECT_EQ(R.score, .5f); + C.FilterText = "xtra"; + R = C.render(Opts); + EXPECT_EQ(R.filterText, "xtra"); + EXPECT_EQ(R.sortText, sortText(1.0, "xtra")); + Opts.EnableSnippets = true; R = C.render(Opts); EXPECT_EQ(R.insertText, "Foo::x(${0:bool})"); @@ -3051,6 +3058,25 @@ EXPECT_THAT(C, ElementsAre(snippetSuffix("${1:(unsigned int)}"))); } +TEST(CompletionTest, ObjectiveCMethodFilterOnEntireSelector) { + auto Results = completions(R"objc( + @interface Foo + + (id)player:(id)player willRun:(id)run; + @end + id val = [Foo wi^] + )objc", + /*IndexSymbols=*/{}, + /*Opts=*/{}, "Foo.m"); + + auto C = Results.Completions; + EXPECT_THAT(C, ElementsAre(named("player:"))); + EXPECT_THAT(C, ElementsAre(filterText("player:willRun:"))); + EXPECT_THAT(C, ElementsAre(kind(CompletionItemKind::Method))); + EXPECT_THAT(C, ElementsAre(returnType("id"))); + EXPECT_THAT(C, ElementsAre(signature("(id) willRun:(id)"))); + EXPECT_THAT(C, ElementsAre(snippetSuffix("${1:(id)} willRun:${2:(id)}"))); +} + TEST(CompletionTest, ObjectiveCSimpleMethodDeclaration) { auto Results = completions(R"objc( @interface Foo diff --git a/clang/include/clang/Sema/CodeCompleteConsumer.h b/clang/include/clang/Sema/CodeCompleteConsumer.h --- a/clang/include/clang/Sema/CodeCompleteConsumer.h +++ b/clang/include/clang/Sema/CodeCompleteConsumer.h @@ -606,9 +606,17 @@ return begin()[I]; } - /// Returns the text in the TypedText chunk. + /// Returns whether this completion string has multiple TypedText chunks. + /// Typically there is only, but some completions, like for Objective-C + /// methods, can have multiple. + bool hasMultipleTypedTextChunks() const; + + /// Returns the text in the first TypedText chunk. const char *getTypedText() const; + /// Returns the combined text from all TypedText chunks. + std::string getAllTypedText() const; + /// Retrieve the priority of this code completion result. unsigned getPriority() const { return Priority; } diff --git a/clang/lib/Sema/CodeCompleteConsumer.cpp b/clang/lib/Sema/CodeCompleteConsumer.cpp --- a/clang/lib/Sema/CodeCompleteConsumer.cpp +++ b/clang/lib/Sema/CodeCompleteConsumer.cpp @@ -338,6 +338,19 @@ return Result; } +bool CodeCompletionString::hasMultipleTypedTextChunks() const { + bool SeenTypedChunk = false; + for (const Chunk &C : *this) { + if (C.Kind == CK_TypedText) { + if (SeenTypedChunk) + return true; + else + SeenTypedChunk = true; + } + } + return false; +} + const char *CodeCompletionString::getTypedText() const { for (const Chunk &C : *this) if (C.Kind == CK_TypedText) @@ -346,6 +359,15 @@ return nullptr; } +std::string CodeCompletionString::getAllTypedText() const { + std::string Res; + for (const Chunk &C : *this) + if (C.Kind == CK_TypedText) + Res += C.Text; + + return Res; +} + const char *CodeCompletionAllocator::CopyString(const Twine &String) { SmallString<128> Data; StringRef Ref = String.toStringRef(Data);