Index: clangd/CodeCompletionStrings.cpp =================================================================== --- clangd/CodeCompletionStrings.cpp +++ clangd/CodeCompletionStrings.cpp @@ -76,6 +76,7 @@ void getSignature(const CodeCompletionString &CCS, std::string *Signature, std::string *Snippet, std::string *RequiredQualifiers) { unsigned ArgCount = 0; + bool HadObjCArguments = false; for (const auto &Chunk : CCS) { // Informative qualifier chunks only clutter completion results, skip // them. @@ -85,13 +86,36 @@ switch (Chunk.Kind) { case CodeCompletionString::CK_TypedText: // The typed-text chunk is the actual name. We don't record this chunk. - // In general our string looks like . - // So once we see the name, any text we recorded so far should be - // reclassified as qualifiers. - if (RequiredQualifiers) - *RequiredQualifiers = std::move(*Signature); - Signature->clear(); - Snippet->clear(); + // C++: + // In general our string looks like . + // So once we see the name, any text we recorded so far should be + // reclassified as qualifiers. + // + // Objective-C: + // Objective-C methods may have multiple typed-text chunks, so we must + // treat them carefully. For Objective-C methods, all typed-text chunks + // will end in ':' (unless there are no arguments, in which case we + // can safely treat them as C++). + if (!StringRef(Chunk.Text).endswith(":")) { // Treat as C++. + if (RequiredQualifiers) + *RequiredQualifiers = std::move(*Signature); + Signature->clear(); + Snippet->clear(); + } else { // Objective-C method with args. + // If this is the first TypedText to the Objective-C method, discard any + // text that we've previously seen (such as previous parameter selector, + // which will be marked as Informative text). + // + // TODO: Make previous parameters part of the signature for Objective-C + // methods. + if (!HadObjCArguments) { + HadObjCArguments = true; + Signature->clear(); + } else { // Subsequent argument, considered part of snippet/signature. + *Signature += Chunk.Text; + *Snippet += Chunk.Text; + } + } break; case CodeCompletionString::CK_Text: *Signature += Chunk.Text; Index: unittests/clangd/CodeCompleteTests.cpp =================================================================== --- unittests/clangd/CodeCompleteTests.cpp +++ unittests/clangd/CodeCompleteTests.cpp @@ -67,6 +67,7 @@ } MATCHER_P(SnippetSuffix, Text, "") { return arg.SnippetSuffix == Text; } MATCHER_P(Origin, OriginSet, "") { return arg.Origin == OriginSet; } +MATCHER_P(Signature, S, "") { return arg.Signature == S; } // Shorthand for Contains(Named(Name)). Matcher &> Has(std::string Name) { @@ -105,7 +106,8 @@ CodeCompleteResult completions(ClangdServer &Server, StringRef Text, std::vector IndexSymbols = {}, - clangd::CodeCompleteOptions Opts = {}) { + clangd::CodeCompleteOptions Opts = {}, + PathRef FilePath = "foo.cpp") { std::unique_ptr OverrideIndex; if (!IndexSymbols.empty()) { assert(!Opts.Index && "both Index and IndexSymbols given!"); @@ -113,7 +115,7 @@ Opts.Index = OverrideIndex.get(); } - auto File = testPath("foo.cpp"); + auto File = testPath(FilePath); Annotations Test(Text); runAddDocument(Server, File, Test.code()); auto CompletionList = @@ -125,12 +127,14 @@ // If IndexSymbols is non-empty, an index will be built and passed to opts. CodeCompleteResult completions(StringRef Text, std::vector IndexSymbols = {}, - clangd::CodeCompleteOptions Opts = {}) { + clangd::CodeCompleteOptions Opts = {}, + PathRef FilePath = "foo.cpp") { MockFSProvider FS; MockCompilationDatabase CDB; IgnoreDiagnostics DiagConsumer; ClangdServer Server(CDB, FS, DiagConsumer, ClangdServer::optsForTest()); - return completions(Server, Text, std::move(IndexSymbols), std::move(Opts)); + return completions(Server, Text, std::move(IndexSymbols), std::move(Opts), + FilePath); } std::string replace(StringRef Haystack, StringRef Needle, StringRef Repl) { @@ -2204,6 +2208,84 @@ {cls("naber"), cls("nx::naber")}, Opts); EXPECT_THAT(Results.Completions, UnorderedElementsAre()); } + +TEST(CompletionTest, ObjectiveCMethodNoArguments) { + auto Results = completions(R"objc( + @interface Foo + @property(nonatomic, setter=setXToIgnoreComplete:) int value; + @end + Foo *foo = [Foo new]; int y = [foo v^] + )objc", + /*IndexSymbols=*/{}, + /*Opts=*/{}, + "Foo.m"); + + auto C = Results.Completions; + EXPECT_THAT(C, ElementsAre(Named("value"))); + EXPECT_THAT(C, ElementsAre(Kind(CompletionItemKind::Method))); + EXPECT_THAT(C, ElementsAre(ReturnType("int"))); + EXPECT_THAT(C, ElementsAre(Signature(""))); + EXPECT_THAT(C, ElementsAre(SnippetSuffix(""))); +} + +TEST(CompletionTest, ObjectiveCMethodOneArgument) { + auto Results = completions(R"objc( + @interface Foo + - (int)valueForCharacter:(char)c; + @end + Foo *foo = [Foo new]; int y = [foo v^] + )objc", + /*IndexSymbols=*/{}, + /*Opts=*/{}, + "Foo.m"); + + auto C = Results.Completions; + EXPECT_THAT(C, ElementsAre(Named("valueForCharacter:"))); + EXPECT_THAT(C, ElementsAre(Kind(CompletionItemKind::Method))); + EXPECT_THAT(C, ElementsAre(ReturnType("int"))); + EXPECT_THAT(C, ElementsAre(Signature("(char)"))); + EXPECT_THAT(C, ElementsAre(SnippetSuffix("${1:(char)}"))); +} + +TEST(CompletionTest, ObjectiveCMethodTwoArgumentsFromBeginning) { + auto Results = completions(R"objc( + @interface Foo + + (id)fooWithValue:(int)value fooey:(unsigned int)fooey; + @end + id val = [Foo foo^] + )objc", + /*IndexSymbols=*/{}, + /*Opts=*/{}, + "Foo.m"); + + auto C = Results.Completions; + EXPECT_THAT(C, ElementsAre(Named("fooWithValue:"))); + EXPECT_THAT(C, ElementsAre(Kind(CompletionItemKind::Method))); + EXPECT_THAT(C, ElementsAre(ReturnType("id"))); + EXPECT_THAT(C, ElementsAre(Signature("(int) fooey:(unsigned int)"))); + EXPECT_THAT(C, + ElementsAre(SnippetSuffix("${1:(int)} fooey:${2:(unsigned int)}"))); +} + +TEST(CompletionTest, ObjectiveCMethodTwoArgumentsFromMiddle) { + auto Results = completions(R"objc( + @interface Foo + + (id)fooWithValue:(int)value fooey:(unsigned int)fooey; + @end + id val = [Foo fooWithValue:10 f^] + )objc", + /*IndexSymbols=*/{}, + /*Opts=*/{}, + "Foo.m"); + + auto C = Results.Completions; + EXPECT_THAT(C, ElementsAre(Named("fooey:"))); + EXPECT_THAT(C, ElementsAre(Kind(CompletionItemKind::Method))); + EXPECT_THAT(C, ElementsAre(ReturnType("id"))); + EXPECT_THAT(C, ElementsAre(Signature("(unsigned int)"))); + EXPECT_THAT(C, ElementsAre(SnippetSuffix("${1:(unsigned int)}"))); +} + } // namespace } // namespace clangd } // namespace clang Index: unittests/clangd/CodeCompletionStringsTests.cpp =================================================================== --- unittests/clangd/CodeCompletionStringsTests.cpp +++ unittests/clangd/CodeCompletionStringsTests.cpp @@ -109,6 +109,53 @@ EXPECT_EQ(Snippet, ""); } +TEST_F(CompletionStringTest, ObjectiveCMethodNoArguments) { + Builder.AddResultTypeChunk("void"); + Builder.AddTypedTextChunk("methodName"); + + auto *CCS = Builder.TakeString(); + computeSignature(*CCS); + EXPECT_EQ(Signature, ""); + EXPECT_EQ(Snippet, ""); +} + +TEST_F(CompletionStringTest, ObjectiveCMethodOneArgument) { + Builder.AddResultTypeChunk("void"); + Builder.AddTypedTextChunk("methodWithArg:"); + Builder.AddPlaceholderChunk("(type)"); + + auto *CCS = Builder.TakeString(); + computeSignature(*CCS); + EXPECT_EQ(Signature, "(type)"); + EXPECT_EQ(Snippet, "${1:(type)}"); +} + +TEST_F(CompletionStringTest, ObjectiveCMethodTwoArgumentsFromBeginning) { + Builder.AddResultTypeChunk("int"); + Builder.AddTypedTextChunk("withFoo:"); + Builder.AddPlaceholderChunk("(type)"); + Builder.AddChunk(CodeCompletionString::CK_HorizontalSpace); + Builder.AddTypedTextChunk("bar:"); + Builder.AddPlaceholderChunk("(type2)"); + + auto *CCS = Builder.TakeString(); + computeSignature(*CCS); + EXPECT_EQ(Signature, "(type) bar:(type2)"); + EXPECT_EQ(Snippet, "${1:(type)} bar:${2:(type2)}"); +} + +TEST_F(CompletionStringTest, ObjectiveCMethodTwoArgumentsFromMiddle) { + Builder.AddResultTypeChunk("int"); + Builder.AddInformativeChunk("withFoo:"); + Builder.AddTypedTextChunk("bar:"); + Builder.AddPlaceholderChunk("(type2)"); + + auto *CCS = Builder.TakeString(); + computeSignature(*CCS); + EXPECT_EQ(Signature, "(type2)"); + EXPECT_EQ(Snippet, "${1:(type2)}"); +} + } // namespace } // namespace clangd } // namespace clang