Index: clangd/CMakeLists.txt =================================================================== --- clangd/CMakeLists.txt +++ clangd/CMakeLists.txt @@ -9,6 +9,7 @@ ClangdUnitStore.cpp CodeComplete.cpp Context.cpp + CompletionString.cpp Compiler.cpp DraftStore.cpp FuzzyMatch.cpp Index: clangd/CodeComplete.cpp =================================================================== --- clangd/CodeComplete.cpp +++ clangd/CodeComplete.cpp @@ -16,6 +16,7 @@ #include "CodeComplete.h" #include "Compiler.h" +#include "CompletionString.h" #include "Logger.h" #include "index/Index.h" #include "clang/Frontend/CompilerInstance.h" @@ -144,46 +145,6 @@ llvm_unreachable("Unhandled clang::index::SymbolKind."); } -std::string escapeSnippet(const llvm::StringRef Text) { - std::string Result; - Result.reserve(Text.size()); // Assume '$', '}' and '\\' are rare. - for (const auto Character : Text) { - if (Character == '$' || Character == '}' || Character == '\\') - Result.push_back('\\'); - Result.push_back(Character); - } - return Result; -} - -std::string getDocumentation(const CodeCompletionString &CCS) { - // Things like __attribute__((nonnull(1,3))) and [[noreturn]]. Present this - // information in the documentation field. - std::string Result; - const unsigned AnnotationCount = CCS.getAnnotationCount(); - if (AnnotationCount > 0) { - Result += "Annotation"; - if (AnnotationCount == 1) { - Result += ": "; - } else /* AnnotationCount > 1 */ { - Result += "s: "; - } - for (unsigned I = 0; I < AnnotationCount; ++I) { - Result += CCS.getAnnotation(I); - Result.push_back(I == AnnotationCount - 1 ? '\n' : ' '); - } - } - // Add brief documentation (if there is any). - if (CCS.getBriefComment() != nullptr) { - if (!Result.empty()) { - // This means we previously added annotations. Add an extra newline - // character to make the annotations stand out. - Result.push_back('\n'); - } - Result += CCS.getBriefComment(); - } - return Result; -} - /// Get the optional chunk as a string. This function is possibly recursive. /// /// The parameter info for each parameter is appended to the Parameters. @@ -320,7 +281,8 @@ /*OutputIsBinary=*/false), ClangdOpts(CodeCompleteOpts), Items(Items), Allocator(std::make_shared()), - CCTUInfo(Allocator), CompletedName(CompletedName) {} + CCTUInfo(Allocator), CompletedName(CompletedName), + EnableSnippets(CodeCompleteOpts.EnableSnippets) {} void ProcessCodeCompleteResults(Sema &S, CodeCompletionContext Context, CodeCompletionResult *Results, @@ -402,14 +364,20 @@ // Adjust this to InsertTextFormat::Snippet iff we encounter a // CK_Placeholder chunk in SnippetCompletionItemsCollector. CompletionItem Item; - Item.insertTextFormat = InsertTextFormat::PlainText; Item.documentation = getDocumentation(CCS); Item.sortText = Candidate.sortText(); - // Fill in the label, detail, insertText and filterText fields of the - // CompletionItem. - ProcessChunks(CCS, Item); + Item.detail = getDetail(CCS); + Item.filterText = getFilterText(CCS); + bool IsSnippet = false; + auto LabelAndInsertText = + getLabelAndInsertText(CCS, EnableSnippets ? &IsSnippet : nullptr); + Item.label = LabelAndInsertText.first; + Item.insertText = LabelAndInsertText.second; + + Item.insertTextFormat = + IsSnippet ? InsertTextFormat::Snippet : InsertTextFormat::PlainText; // Fill in the kind field of the CompletionItem. Item.kind = toCompletionItemKind(Candidate.Result->Kind, @@ -418,170 +386,14 @@ return Item; } - virtual void ProcessChunks(const CodeCompletionString &CCS, - CompletionItem &Item) const = 0; - CodeCompleteOptions ClangdOpts; CompletionList &Items; std::shared_ptr Allocator; CodeCompletionTUInfo CCTUInfo; NameToComplete &CompletedName; + bool EnableSnippets; }; // CompletionItemsCollector -bool isInformativeQualifierChunk(CodeCompletionString::Chunk const &Chunk) { - return Chunk.Kind == CodeCompletionString::CK_Informative && - StringRef(Chunk.Text).endswith("::"); -} - -class PlainTextCompletionItemsCollector final - : public CompletionItemsCollector { - -public: - PlainTextCompletionItemsCollector(const CodeCompleteOptions &CodeCompleteOpts, - CompletionList &Items, - NameToComplete &CompletedName) - : CompletionItemsCollector(CodeCompleteOpts, Items, CompletedName) {} - -private: - void ProcessChunks(const CodeCompletionString &CCS, - CompletionItem &Item) const override { - for (const auto &Chunk : CCS) { - // Informative qualifier chunks only clutter completion results, skip - // them. - if (isInformativeQualifierChunk(Chunk)) - continue; - - switch (Chunk.Kind) { - case CodeCompletionString::CK_TypedText: - // There's always exactly one CK_TypedText chunk. - Item.insertText = Item.filterText = Chunk.Text; - Item.label += Chunk.Text; - break; - case CodeCompletionString::CK_ResultType: - assert(Item.detail.empty() && "Unexpected extraneous CK_ResultType"); - Item.detail = Chunk.Text; - break; - case CodeCompletionString::CK_Optional: - break; - default: - Item.label += Chunk.Text; - break; - } - } - } -}; // PlainTextCompletionItemsCollector - -class SnippetCompletionItemsCollector final : public CompletionItemsCollector { - -public: - SnippetCompletionItemsCollector(const CodeCompleteOptions &CodeCompleteOpts, - CompletionList &Items, - NameToComplete &CompletedName) - : CompletionItemsCollector(CodeCompleteOpts, Items, CompletedName) {} - -private: - void ProcessChunks(const CodeCompletionString &CCS, - CompletionItem &Item) const override { - unsigned ArgCount = 0; - for (const auto &Chunk : CCS) { - // Informative qualifier chunks only clutter completion results, skip - // them. - if (isInformativeQualifierChunk(Chunk)) - continue; - - switch (Chunk.Kind) { - case CodeCompletionString::CK_TypedText: - // The piece of text that the user is expected to type to match - // the code-completion string, typically a keyword or the name of - // a declarator or macro. - Item.filterText = Chunk.Text; - LLVM_FALLTHROUGH; - case CodeCompletionString::CK_Text: - // A piece of text that should be placed in the buffer, - // e.g., parentheses or a comma in a function call. - Item.label += Chunk.Text; - Item.insertText += Chunk.Text; - break; - case CodeCompletionString::CK_Optional: - // A code completion string that is entirely optional. - // For example, an optional code completion string that - // describes the default arguments in a function call. - - // FIXME: Maybe add an option to allow presenting the optional chunks? - break; - case CodeCompletionString::CK_Placeholder: - // A string that acts as a placeholder for, e.g., a function call - // argument. - ++ArgCount; - Item.insertText += "${" + std::to_string(ArgCount) + ':' + - escapeSnippet(Chunk.Text) + '}'; - Item.label += Chunk.Text; - Item.insertTextFormat = InsertTextFormat::Snippet; - break; - case CodeCompletionString::CK_Informative: - // A piece of text that describes something about the result - // but should not be inserted into the buffer. - // For example, the word "const" for a const method, or the name of - // the base class for methods that are part of the base class. - Item.label += Chunk.Text; - // Don't put the informative chunks in the insertText. - break; - case CodeCompletionString::CK_ResultType: - // A piece of text that describes the type of an entity or, - // for functions and methods, the return type. - assert(Item.detail.empty() && "Unexpected extraneous CK_ResultType"); - Item.detail = Chunk.Text; - break; - case CodeCompletionString::CK_CurrentParameter: - // A piece of text that describes the parameter that corresponds to - // the code-completion location within a function call, message send, - // macro invocation, etc. - // - // This should never be present while collecting completion items, - // only while collecting overload candidates. - llvm_unreachable("Unexpected CK_CurrentParameter while collecting " - "CompletionItems"); - break; - case CodeCompletionString::CK_LeftParen: - // A left parenthesis ('('). - case CodeCompletionString::CK_RightParen: - // A right parenthesis (')'). - case CodeCompletionString::CK_LeftBracket: - // A left bracket ('['). - case CodeCompletionString::CK_RightBracket: - // A right bracket (']'). - case CodeCompletionString::CK_LeftBrace: - // A left brace ('{'). - case CodeCompletionString::CK_RightBrace: - // A right brace ('}'). - case CodeCompletionString::CK_LeftAngle: - // A left angle bracket ('<'). - case CodeCompletionString::CK_RightAngle: - // A right angle bracket ('>'). - case CodeCompletionString::CK_Comma: - // A comma separator (','). - case CodeCompletionString::CK_Colon: - // A colon (':'). - case CodeCompletionString::CK_SemiColon: - // A semicolon (';'). - case CodeCompletionString::CK_Equal: - // An '=' sign. - case CodeCompletionString::CK_HorizontalSpace: - // Horizontal whitespace (' '). - Item.insertText += Chunk.Text; - Item.label += Chunk.Text; - break; - case CodeCompletionString::CK_VerticalSpace: - // Vertical whitespace ('\n' or '\r\n', depending on the - // platform). - Item.insertText += Chunk.Text; - // Don't even add a space to the label. - break; - } - } - } -}; // SnippetCompletionItemsCollector - class SignatureHelpCollector final : public CodeCompleteConsumer { public: @@ -617,6 +429,8 @@ CodeCompletionTUInfo &getCodeCompletionTUInfo() override { return CCTUInfo; } private: + // FIXME(ioeric): consider moving CodeCompletionString logic here to + // CompletionString.h. SignatureInformation ProcessOverloadCandidate(const OverloadCandidate &Candidate, const CodeCompletionString &CCS) const { @@ -817,15 +631,9 @@ std::shared_ptr PCHs, CodeCompleteOptions Opts) { CompletionList Results; - std::unique_ptr Consumer; NameToComplete CompletedName; - if (Opts.EnableSnippets) { - Consumer = llvm::make_unique( - Opts, Results, CompletedName); - } else { - Consumer = llvm::make_unique( - Opts, Results, CompletedName); - } + auto Consumer = + llvm::make_unique(Opts, Results, CompletedName); invokeCodeComplete(Ctx, std::move(Consumer), Opts.getClangCompleteOpts(), FileName, Command, Preamble, Contents, Pos, std::move(VFS), std::move(PCHs)); Index: clangd/CompletionString.h =================================================================== --- /dev/null +++ clangd/CompletionString.h @@ -0,0 +1,48 @@ +//===--- CompletionString.h --------------------------------------*- C++-*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===---------------------------------------------------------------------===// +// +// Functions for retrieving code completion information from +// `CodeCompletionString`. +// +//===---------------------------------------------------------------------===// +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_COMPLETIONSTRING_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_COMPLETIONSTRING_H + +#include "clang/Sema/CodeCompleteConsumer.h" + +namespace clang { +namespace clangd { + +/// Gets label and insert text for a completion item. For example, for function +/// `Foo`, this returns <"Foo(int x, int y)", "Foo"> without snippts enabled. +/// +/// If \p IsSnippet is not nullptr, this will try to use snippet for the insert +/// text and sets `IsSnippet` to true when a snippet is created. Otherwise, the +/// insert text will always be plain text. +std::pair +getLabelAndInsertText(const CodeCompletionString &CCS, + bool *IsSnippet = nullptr); + +/// Gets the documentation for a completion item. For example, comment for the +/// a class declaration. +std::string getDocumentation(const CodeCompletionString &CCS); + +/// Gets detail to be used as the detail field in an LSP completion item. This +/// is usually the result of a function. +std::string getDetail(const CodeCompletionString &CCS); + +/// Gets the piece of text that the user is expected to type to match the +/// code-completion string, typically a keyword or the name of a declarator or +/// macro. +std::string getFilterText(const CodeCompletionString &CCS); + +} // namespace clangd +} // namespace clang + +#endif Index: clangd/CompletionString.cpp =================================================================== --- /dev/null +++ clangd/CompletionString.cpp @@ -0,0 +1,223 @@ +//===--- CompletionString.cpp ------------------------------------*- C++-*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===---------------------------------------------------------------------===// + +#include "CompletionString.h" +#include + +namespace clang { +namespace clangd { + +namespace { + +bool isInformativeQualifierChunk(CodeCompletionString::Chunk const &Chunk) { + return Chunk.Kind == CodeCompletionString::CK_Informative && + StringRef(Chunk.Text).endswith("::"); +} + +std::pair +processPlainTextChunks(const CodeCompletionString &CCS) { + std::string Label; + std::string InsertText; + for (const auto &Chunk : CCS) { + // Informative qualifier chunks only clutter completion results, skip + // them. + if (isInformativeQualifierChunk(Chunk)) + continue; + + switch (Chunk.Kind) { + case CodeCompletionString::CK_ResultType: + case CodeCompletionString::CK_Optional: + break; + case CodeCompletionString::CK_TypedText: + InsertText += Chunk.Text; + Label += Chunk.Text; + break; + default: + Label += Chunk.Text; + break; + } + } + return {Label, InsertText}; +} + +std::string escapeSnippet(const llvm::StringRef Text) { + std::string Result; + Result.reserve(Text.size()); // Assume '$', '}' and '\\' are rare. + for (const auto Character : Text) { + if (Character == '$' || Character == '}' || Character == '\\') + Result.push_back('\\'); + Result.push_back(Character); + } + return Result; +} + +std::pair +processSnippetChunks(const CodeCompletionString &CCS, bool *IsSnippet) { + std::string Label; + std::string InsertText; + + unsigned ArgCount = 0; + for (const auto &Chunk : CCS) { + // Informative qualifier chunks only clutter completion results, skip + // them. + if (isInformativeQualifierChunk(Chunk)) + continue; + + switch (Chunk.Kind) { + case CodeCompletionString::CK_TypedText: + // The piece of text that the user is expected to type to match + // the code-completion string, typically a keyword or the name of + // a declarator or macro. + case CodeCompletionString::CK_Text: + Label += Chunk.Text; + InsertText += Chunk.Text; + break; + case CodeCompletionString::CK_Optional: + // A code completion string that is entirely optional. + // For example, an optional code completion string that + // describes the default arguments in a function call. + + // FIXME: Maybe add an option to allow presenting the optional chunks? + break; + case CodeCompletionString::CK_Placeholder: + // A string that acts as a placeholder for, e.g., a function call + // argument. + ++ArgCount; + InsertText += "${" + std::to_string(ArgCount) + ':' + + escapeSnippet(Chunk.Text) + '}'; + Label += Chunk.Text; + *IsSnippet = true; + break; + case CodeCompletionString::CK_Informative: + // A piece of text that describes something about the result + // but should not be inserted into the buffer. + // For example, the word "const" for a const method, or the name of + // the base class for methods that are part of the base class. + Label += Chunk.Text; + // Don't put the informative chunks in the insertText. + break; + case CodeCompletionString::CK_ResultType: + // This is retrieved as detail. + break; + case CodeCompletionString::CK_CurrentParameter: + // A piece of text that describes the parameter that corresponds to + // the code-completion location within a function call, message send, + // macro invocation, etc. + // + // This should never be present while collecting completion items, + // only while collecting overload candidates. + llvm_unreachable("Unexpected CK_CurrentParameter while collecting " + "CompletionItems"); + break; + case CodeCompletionString::CK_LeftParen: + // A left parenthesis ('('). + case CodeCompletionString::CK_RightParen: + // A right parenthesis (')'). + case CodeCompletionString::CK_LeftBracket: + // A left bracket ('['). + case CodeCompletionString::CK_RightBracket: + // A right bracket (']'). + case CodeCompletionString::CK_LeftBrace: + // A left brace ('{'). + case CodeCompletionString::CK_RightBrace: + // A right brace ('}'). + case CodeCompletionString::CK_LeftAngle: + // A left angle bracket ('<'). + case CodeCompletionString::CK_RightAngle: + // A right angle bracket ('>'). + case CodeCompletionString::CK_Comma: + // A comma separator (','). + case CodeCompletionString::CK_Colon: + // A colon (':'). + case CodeCompletionString::CK_SemiColon: + // A semicolon (';'). + case CodeCompletionString::CK_Equal: + // An '=' sign. + case CodeCompletionString::CK_HorizontalSpace: + // Horizontal whitespace (' '). + InsertText += Chunk.Text; + Label += Chunk.Text; + break; + case CodeCompletionString::CK_VerticalSpace: + // Vertical whitespace ('\n' or '\r\n', depending on the + // platform). + InsertText += Chunk.Text; + // Don't even add a space to the label. + break; + } + } + return {std::move(Label), std::move(InsertText)}; +} + +} // namespace + +std::pair +getLabelAndInsertText(const CodeCompletionString &CCS, bool *IsSnippet) { + return IsSnippet ? processSnippetChunks(CCS, IsSnippet) + : processPlainTextChunks(CCS); +} + +std::string getDocumentation(const CodeCompletionString &CCS) { + // Things like __attribute__((nonnull(1,3))) and [[noreturn]]. Present this + // information in the documentation field. + std::string Result; + const unsigned AnnotationCount = CCS.getAnnotationCount(); + if (AnnotationCount > 0) { + Result += "Annotation"; + if (AnnotationCount == 1) { + Result += ": "; + } else /* AnnotationCount > 1 */ { + Result += "s: "; + } + for (unsigned I = 0; I < AnnotationCount; ++I) { + Result += CCS.getAnnotation(I); + Result.push_back(I == AnnotationCount - 1 ? '\n' : ' '); + } + } + // Add brief documentation (if there is any). + if (CCS.getBriefComment() != nullptr) { + if (!Result.empty()) { + // This means we previously added annotations. Add an extra newline + // character to make the annotations stand out. + Result.push_back('\n'); + } + Result += CCS.getBriefComment(); + } + return Result; +} + +std::string getDetail(const CodeCompletionString &CCS) { + for (const auto &Chunk : CCS) { + // Informative qualifier chunks only clutter completion results, skip + // them. + switch (Chunk.Kind) { + case CodeCompletionString::CK_ResultType: + return Chunk.Text; + default: + break; + } + } + return ""; +} + +std::string getFilterText(const CodeCompletionString &CCS) { + for (const auto &Chunk : CCS) { + switch (Chunk.Kind) { + case CodeCompletionString::CK_TypedText: + // There's always exactly one CK_TypedText chunk. + return Chunk.Text; + default: + break; + } + } + return ""; +} + +} // namespace clangd +} // namespace clang Index: unittests/clangd/CMakeLists.txt =================================================================== --- unittests/clangd/CMakeLists.txt +++ unittests/clangd/CMakeLists.txt @@ -11,6 +11,7 @@ add_extra_unittest(ClangdTests ClangdTests.cpp CodeCompleteTests.cpp + CompletionStringTests.cpp ContextTests.cpp FileIndexTests.cpp FuzzyMatchTests.cpp Index: unittests/clangd/CompletionStringTests.cpp =================================================================== --- /dev/null +++ unittests/clangd/CompletionStringTests.cpp @@ -0,0 +1,130 @@ +//===-- CompletionStringTests.cpp -------------------------------*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#include "CompletionString.h" +#include "clang/Sema/CodeCompleteConsumer.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { +namespace { + +class CompletionStringTest : public ::testing::Test { +public: + CompletionStringTest() + : Allocator(std::make_shared()), + CCTUInfo(Allocator), Builder(*Allocator, CCTUInfo) {} + +protected: + std::shared_ptr Allocator; + CodeCompletionTUInfo CCTUInfo; + CodeCompletionBuilder Builder; +}; + +TEST_F(CompletionStringTest, Detail) { + Builder.AddResultTypeChunk("result"); + Builder.AddResultTypeChunk("redundant result no no"); + EXPECT_EQ(getDetail(*Builder.TakeString()), "result"); +} + +TEST_F(CompletionStringTest, FilterText) { + Builder.AddTypedTextChunk("typed"); + Builder.AddTypedTextChunk("redundant typed no no"); + auto *S = Builder.TakeString(); + EXPECT_EQ(getFilterText(*S), "typed"); +} + +TEST_F(CompletionStringTest, Documentation) { + Builder.addBriefComment("Is this brief?"); + EXPECT_EQ(getDocumentation(*Builder.TakeString()), "Is this brief?"); +} + +TEST_F(CompletionStringTest, DocumentationWithAnnotation) { + Builder.addBriefComment("Is this brief?"); + Builder.AddAnnotation("Ano"); + EXPECT_EQ(getDocumentation(*Builder.TakeString()), + "Annotation: Ano\n\nIs this brief?"); +} + +TEST_F(CompletionStringTest, MultipleAnnotations) { + Builder.AddAnnotation("Ano1"); + Builder.AddAnnotation("Ano2"); + Builder.AddAnnotation("Ano3"); + + EXPECT_EQ(getDocumentation(*Builder.TakeString()), + "Annotations: Ano1 Ano2 Ano3\n"); +} + +TEST_F(CompletionStringTest, SimpleLabelAndInsert) { + Builder.AddTypedTextChunk("X"); + Builder.AddResultTypeChunk("result no no"); + auto Pair = getLabelAndInsertText(*Builder.TakeString()); + EXPECT_EQ(Pair.first, "X"); + EXPECT_EQ(Pair.second, "X"); +} + +TEST_F(CompletionStringTest, FunctionPlainText) { + Builder.AddResultTypeChunk("result no no"); + Builder.AddTypedTextChunk("Foo"); + Builder.AddChunk(CodeCompletionString::CK_LeftParen); + Builder.AddPlaceholderChunk("p1"); + Builder.AddChunk(CodeCompletionString::CK_Comma); + Builder.AddPlaceholderChunk("p2"); + Builder.AddChunk(CodeCompletionString::CK_RightParen); + Builder.AddChunk(CodeCompletionString::CK_HorizontalSpace); + Builder.AddInformativeChunk("const"); + + auto Pair = getLabelAndInsertText(*Builder.TakeString()); + EXPECT_EQ(Pair.first, "Foo(p1, p2) const"); + EXPECT_EQ(Pair.second, "Foo"); +} + +TEST_F(CompletionStringTest, FunctionSnippet) { + Builder.AddResultTypeChunk("result no no"); + Builder.AddTypedTextChunk("Foo"); + Builder.AddTextChunk("d"); + Builder.AddChunk(CodeCompletionString::CK_LeftParen); + Builder.AddPlaceholderChunk("p1"); + Builder.AddChunk(CodeCompletionString::CK_Comma); + Builder.AddPlaceholderChunk("p2"); + Builder.AddChunk(CodeCompletionString::CK_RightParen); + + bool IsSnippet = false; + auto Pair = getLabelAndInsertText(*Builder.TakeString(), &IsSnippet); + EXPECT_TRUE(IsSnippet); + EXPECT_EQ(Pair.first, "Food(p1, p2)"); + EXPECT_EQ(Pair.second, "Food(${1:p1}, ${2:p2})"); +} + +TEST_F(CompletionStringTest, EscapeSnippet) { + Builder.AddTypedTextChunk("Foo"); + Builder.AddChunk(CodeCompletionString::CK_LeftParen); + Builder.AddPlaceholderChunk("$p}1\\"); + Builder.AddChunk(CodeCompletionString::CK_RightParen); + + bool IsSnippet = false; + auto Pair = getLabelAndInsertText(*Builder.TakeString(), &IsSnippet); + EXPECT_TRUE(IsSnippet); + EXPECT_EQ(Pair.first, "Foo($p}1\\)"); + EXPECT_EQ(Pair.second, "Foo(${1:\\$p\\}1\\\\})"); +} + +TEST_F(CompletionStringTest, IgnoreInformativeQualifier) { + Builder.AddTypedTextChunk("X"); + Builder.AddInformativeChunk("info ok"); + Builder.AddInformativeChunk("info no no::"); + auto Pair = getLabelAndInsertText(*Builder.TakeString()); + EXPECT_EQ(Pair.first, "Xinfo ok"); + EXPECT_EQ(Pair.second, "X"); +} + +} // namespace +} // namespace clangd +} // namespace clang