Index: clangd/ClangdLSPServer.cpp =================================================================== --- clangd/ClangdLSPServer.cpp +++ clangd/ClangdLSPServer.cpp @@ -178,15 +178,15 @@ } void ClangdLSPServer::onCompletion(Ctx C, TextDocumentPositionParams &Params) { - auto Items = Server - .codeComplete(Params.textDocument.uri.file, - Position{Params.position.line, - Params.position.character}) - .get() // FIXME(ibiryukov): This could be made async if we - // had an API that would allow to attach callbacks to - // futures returned by ClangdServer. - .Value; - C.reply(json::ary(Items)); + auto List = Server + .codeComplete( + Params.textDocument.uri.file, + Position{Params.position.line, Params.position.character}) + .get() // FIXME(ibiryukov): This could be made async if we + // had an API that would allow to attach callbacks to + // futures returned by ClangdServer. + .Value; + C.reply(List); } void ClangdLSPServer::onSignatureHelp(Ctx C, Index: clangd/ClangdServer.h =================================================================== --- clangd/ClangdServer.h +++ clangd/ClangdServer.h @@ -251,18 +251,17 @@ /// This method should only be called for currently tracked files. However, it /// is safe to call removeDocument for \p File after this method returns, even /// while returned future is not yet ready. - std::future>> + std::future> codeComplete(PathRef File, Position Pos, llvm::Optional OverridenContents = llvm::None, IntrusiveRefCntPtr *UsedFS = nullptr); /// A version of `codeComplete` that runs \p Callback on the processing thread /// when codeComplete results become available. - void codeComplete( - UniqueFunction>)> Callback, - PathRef File, Position Pos, - llvm::Optional OverridenContents = llvm::None, - IntrusiveRefCntPtr *UsedFS = nullptr); + void codeComplete(UniqueFunction)> Callback, + PathRef File, Position Pos, + llvm::Optional OverridenContents = llvm::None, + IntrusiveRefCntPtr *UsedFS = nullptr); /// Provide signature help for \p File at \p Pos. If \p OverridenContents is /// not None, they will used only for signature help, i.e. no diagnostics Index: clangd/ClangdServer.cpp =================================================================== --- clangd/ClangdServer.cpp +++ clangd/ClangdServer.cpp @@ -198,11 +198,11 @@ std::move(TaggedFS)); } -std::future>> +std::future> ClangdServer::codeComplete(PathRef File, Position Pos, llvm::Optional OverridenContents, IntrusiveRefCntPtr *UsedFS) { - using ResultType = Tagged>; + using ResultType = Tagged; std::promise ResultPromise; @@ -218,11 +218,10 @@ } void ClangdServer::codeComplete( - UniqueFunction>)> Callback, - PathRef File, Position Pos, llvm::Optional OverridenContents, + UniqueFunction)> Callback, PathRef File, + Position Pos, llvm::Optional OverridenContents, IntrusiveRefCntPtr *UsedFS) { - using CallbackType = - UniqueFunction>)>; + using CallbackType = UniqueFunction)>; std::string Contents; if (OverridenContents) { @@ -259,7 +258,7 @@ // FIXME(ibiryukov): even if Preamble is non-null, we may want to check // both the old and the new version in case only one of them matches. - std::vector Result = clangd::codeComplete( + CompletionList Result = clangd::codeComplete( File, Resources->getCompileCommand(), Preamble ? &Preamble->Preamble : nullptr, Contents, Pos, TaggedFS.Value, PCHs, CodeCompleteOpts, Logger); Index: clangd/ClangdUnit.h =================================================================== --- clangd/ClangdUnit.h +++ clangd/ClangdUnit.h @@ -263,7 +263,7 @@ bool IncludeBriefComments); /// Returns options that can be passed to clang's completion engine. - clang::CodeCompleteOptions getClangCompleteOpts(); + clang::CodeCompleteOptions getClangCompleteOpts() const; /// When true, completion items will contain expandable code snippets in /// completion (e.g. `return ${1:expression}` or `foo(${1:int a}, ${2:int @@ -285,10 +285,14 @@ /// FIXME(ibiryukov): it looks like turning this option on significantly slows /// down completion, investigate if it can be made faster. bool IncludeBriefComments = true; + + /// Limit the number of results returned (0 means no limit). + /// If more results are available, we set CompletionList.isIncomplete. + size_t Limit = 0; }; /// Get code completions at a specified \p Pos in \p FileName. -std::vector +CompletionList codeComplete(PathRef FileName, const tooling::CompileCommand &Command, PrecompiledPreamble const *Preamble, StringRef Contents, Position Pos, IntrusiveRefCntPtr VFS, Index: clangd/ClangdUnit.cpp =================================================================== --- clangd/ClangdUnit.cpp +++ clangd/ClangdUnit.cpp @@ -368,28 +368,90 @@ return Result; } +/// A scored code completion result. +/// It may be promoted to a CompletionItem if it's among the top-ranked results. +struct CompletionCandidate { + CompletionCandidate(CodeCompletionResult &Result) + : Result(&Result), Score(score(Result)) {} + + CodeCompletionResult *Result; + // Higher score is worse. + // FIXME: use a more natural score, e.g. double 0-1, higher is better. + int Score; + + // Comparison: better candidates are bigger. + bool operator<(const CompletionCandidate &C) const { + if (Score != C.Score) + return Score > C.Score; + return *Result > *C.Result; // Reversed so descending list is alpha-sorted. + } + + std::string sortText() const { + // Fill in the sortText of the CompletionItem. + assert(Score <= 999999 && "Expecting score to have at most 6-digits"); + std::string S, NameStorage; + StringRef Name = Result->orderedName(NameStorage); + llvm::raw_string_ostream(S) + << llvm::format("%06d%.*s", Score, Name.size(), Name.data()); + return S; + } + +private: + static int score(const CodeCompletionResult &Result) { + int Score = Result.Priority; + // Fill in the sortText of the CompletionItem. + assert(Score <= 99999 && "Expecting code completion result " + "priority to have at most 5-digits"); + + const int Penalty = 100000; + switch (static_cast(Result.Availability)) { + case CXAvailability_Available: + // No penalty. + break; + case CXAvailability_Deprecated: + Score += Penalty; + break; + case CXAvailability_NotAccessible: + Score += 2 * Penalty; + break; + case CXAvailability_NotAvailable: + Score += 3 * Penalty; + break; + } + return Score; + } +}; + class CompletionItemsCollector : public CodeCompleteConsumer { public: - CompletionItemsCollector(const clang::CodeCompleteOptions &CodeCompleteOpts, - std::vector &Items) - : CodeCompleteConsumer(CodeCompleteOpts, /*OutputIsBinary=*/false), - Items(Items), + CompletionItemsCollector(const clangd::CodeCompleteOptions &CodeCompleteOpts, + CompletionList &Items) + : CodeCompleteConsumer(CodeCompleteOpts.getClangCompleteOpts(), + /*OutputIsBinary=*/false), + ClangdOpts(CodeCompleteOpts), Items(Items), Allocator(std::make_shared()), CCTUInfo(Allocator) {} void ProcessCodeCompleteResults(Sema &S, CodeCompletionContext Context, CodeCompletionResult *Results, unsigned NumResults) override final { - Items.reserve(NumResults); + std::priority_queue Candidates; for (unsigned I = 0; I < NumResults; ++I) { - auto &Result = Results[I]; - const auto *CCS = Result.CreateCodeCompletionString( + Candidates.emplace(Results[I]); + if (ClangdOpts.Limit && Candidates.size() > ClangdOpts.Limit) { + Candidates.pop(); + Items.isIncomplete = true; + } + } + while (!Candidates.empty()) { + auto &Candidate = Candidates.top(); + const auto *CCS = Candidate.Result->CreateCodeCompletionString( S, Context, *Allocator, CCTUInfo, CodeCompleteOpts.IncludeBriefComments); assert(CCS && "Expected the CodeCompletionString to be non-null"); - Items.push_back(ProcessCodeCompleteResult(Result, *CCS)); + Items.items.push_back(ProcessCodeCompleteResult(Candidate, *CCS)); + Candidates.pop(); } - std::sort(Items.begin(), Items.end()); } GlobalCodeCompletionAllocator &getAllocator() override { return *Allocator; } @@ -398,7 +460,7 @@ private: CompletionItem - ProcessCodeCompleteResult(const CodeCompletionResult &Result, + ProcessCodeCompleteResult(const CompletionCandidate &Candidate, const CodeCompletionString &CCS) const { // Adjust this to InsertTextFormat::Snippet iff we encounter a @@ -407,15 +469,14 @@ 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); // Fill in the kind field of the CompletionItem. - Item.kind = getKind(Result.Kind, Result.CursorKind); - - FillSortText(CCS, Item); + Item.kind = getKind(Candidate.Result->Kind, Candidate.Result->CursorKind); return Item; } @@ -423,42 +484,8 @@ virtual void ProcessChunks(const CodeCompletionString &CCS, CompletionItem &Item) const = 0; - static int GetSortPriority(const CodeCompletionString &CCS) { - int Score = CCS.getPriority(); - // Fill in the sortText of the CompletionItem. - assert(Score <= 99999 && "Expecting code completion result " - "priority to have at most 5-digits"); - - const int Penalty = 100000; - switch (static_cast(CCS.getAvailability())) { - case CXAvailability_Available: - // No penalty. - break; - case CXAvailability_Deprecated: - Score += Penalty; - break; - case CXAvailability_NotAccessible: - Score += 2 * Penalty; - break; - case CXAvailability_NotAvailable: - Score += 3 * Penalty; - break; - } - - return Score; - } - - static void FillSortText(const CodeCompletionString &CCS, - CompletionItem &Item) { - int Priority = GetSortPriority(CCS); - // Fill in the sortText of the CompletionItem. - assert(Priority <= 999999 && - "Expecting sort priority to have at most 6-digits"); - llvm::raw_string_ostream(Item.sortText) - << llvm::format("%06d%s", Priority, Item.filterText.c_str()); - } - - std::vector &Items; + clangd::CodeCompleteOptions ClangdOpts; + CompletionList &Items; std::shared_ptr Allocator; CodeCompletionTUInfo CCTUInfo; @@ -474,8 +501,8 @@ public: PlainTextCompletionItemsCollector( - const clang::CodeCompleteOptions &CodeCompleteOpts, - std::vector &Items) + const clangd::CodeCompleteOptions &CodeCompleteOpts, + CompletionList &Items) : CompletionItemsCollector(CodeCompleteOpts, Items) {} private: @@ -511,8 +538,8 @@ public: SnippetCompletionItemsCollector( - const clang::CodeCompleteOptions &CodeCompleteOpts, - std::vector &Items) + const clangd::CodeCompleteOptions &CodeCompleteOpts, + CompletionList &Items) : CompletionItemsCollector(CodeCompleteOpts, Items) {} private: @@ -795,7 +822,8 @@ IncludeMacros(IncludeMacros), IncludeGlobals(IncludeGlobals), IncludeBriefComments(IncludeBriefComments) {} -clang::CodeCompleteOptions clangd::CodeCompleteOptions::getClangCompleteOpts() { +clang::CodeCompleteOptions +clangd::CodeCompleteOptions::getClangCompleteOpts() const { clang::CodeCompleteOptions Result; Result.IncludeCodePatterns = EnableSnippets && IncludeCodePatterns; Result.IncludeMacros = IncludeMacros; @@ -805,25 +833,24 @@ return Result; } -std::vector +CompletionList clangd::codeComplete(PathRef FileName, const tooling::CompileCommand &Command, PrecompiledPreamble const *Preamble, StringRef Contents, Position Pos, IntrusiveRefCntPtr VFS, std::shared_ptr PCHs, clangd::CodeCompleteOptions Opts, clangd::Logger &Logger) { - std::vector Results; + CompletionList Results; std::unique_ptr Consumer; - clang::CodeCompleteOptions ClangCompleteOpts = Opts.getClangCompleteOpts(); if (Opts.EnableSnippets) { - Consumer = llvm::make_unique( - ClangCompleteOpts, Results); + Consumer = + llvm::make_unique(Opts, Results); } else { - Consumer = llvm::make_unique( - ClangCompleteOpts, Results); + Consumer = + llvm::make_unique(Opts, Results); } - invokeCodeComplete(std::move(Consumer), ClangCompleteOpts, FileName, Command, - Preamble, Contents, Pos, std::move(VFS), std::move(PCHs), - Logger); + invokeCodeComplete(std::move(Consumer), Opts.getClangCompleteOpts(), FileName, + Command, Preamble, Contents, Pos, std::move(VFS), + std::move(PCHs), Logger); return Results; } Index: clangd/Protocol.h =================================================================== --- clangd/Protocol.h +++ clangd/Protocol.h @@ -547,6 +547,18 @@ bool operator<(const CompletionItem &, const CompletionItem &); +/// Represents a collection of completion items to be presented in the editor. +struct CompletionList { + /// The list is not complete. Further typing should result in recomputing the + /// list. + bool isIncomplete = false; + + /// The completion items. + std::vector items; + + static json::Expr unparse(const CompletionList &); +}; + /// A single parameter of a particular signature. struct ParameterInformation { Index: clangd/Protocol.cpp =================================================================== --- clangd/Protocol.cpp +++ clangd/Protocol.cpp @@ -1043,6 +1043,13 @@ (R.sortText.empty() ? R.label : R.sortText); } +json::Expr CompletionList::unparse(const CompletionList &L) { + return json::obj{ + {"isIncomplete", L.isIncomplete}, + {"items", json::ary(L.items)}, + }; +} + json::Expr ParameterInformation::unparse(const ParameterInformation &PI) { assert(!PI.label.empty() && "parameter information label is required"); json::obj Result{{"label", PI.label}}; Index: test/clangd/authority-less-uri.test =================================================================== --- test/clangd/authority-less-uri.test +++ test/clangd/authority-less-uri.test @@ -17,14 +17,17 @@ # # CHECK: "id": 1, # CHECK-NEXT: "jsonrpc": "2.0", -# CHECK-NEXT: "result": [ -# CHECK: "filterText": "fake", -# CHECK-NEXT: "insertText": "fake", -# CHECK-NEXT: "insertTextFormat": 1, -# CHECK-NEXT: "kind": 7, -# CHECK-NEXT: "label": "fake::", -# CHECK-NEXT: "sortText": "000075fake" -# CHECK: ] +# CHECK-NEXT: "result": { +# CHECK-NEXT: "isIncomplete": false, +# CHECK-NEXT: "items": [ +# CHECK: "filterText": "fake", +# CHECK-NEXT: "insertText": "fake", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 7, +# CHECK-NEXT: "label": "fake::", +# CHECK-NEXT: "sortText": "000075fake" +# CHECK: ] +# CHECK-NEXT: } Content-Length: 172 {"jsonrpc":"2.0","id":2,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:///main.cpp"},"uri":"file:///main.cpp","position":{"line":3,"character":5}}} @@ -32,14 +35,17 @@ # # CHECK: "id": 2, # CHECK-NEXT: "jsonrpc": "2.0", -# CHECK-NEXT: "result": [ -# CHECK: "filterText": "fake", -# CHECK-NEXT: "insertText": "fake", -# CHECK-NEXT: "insertTextFormat": 1, -# CHECK-NEXT: "kind": 7, -# CHECK-NEXT: "label": "fake::", -# CHECK-NEXT: "sortText": "000075fake" -# CHECK: ] +# CHECK-NEXT: "result": { +# CHECK-NEXT: "isIncomplete": false, +# CHECK-NEXT: "items": [ +# CHECK: "filterText": "fake", +# CHECK-NEXT: "insertText": "fake", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 7, +# CHECK-NEXT: "label": "fake::", +# CHECK-NEXT: "sortText": "000075fake" +# CHECK: ] +# CHECK-NEXT: } Content-Length: 44 {"jsonrpc":"2.0","id":3,"method":"shutdown"} Index: test/clangd/completion-items-kinds.test =================================================================== --- test/clangd/completion-items-kinds.test +++ test/clangd/completion-items-kinds.test @@ -11,7 +11,7 @@ {"jsonrpc":"2.0","id":1,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:///main.cpp"},"position":{"line":4,"character":7}}} Content-Length: 58 -# CHECK: {"id":1,"jsonrpc":"2.0","result":[ +# CHECK: {"id":1,"jsonrpc":"2.0","result":{"isIncomplete":false,"items": # # Keyword # CHECK-DAG: {"filterText":"int","insertText":"int","insertTextFormat":1,"kind":14,"label":"int","sortText":"000050int"} @@ -31,6 +31,6 @@ # Function # CHECK-DAG: {"detail":"int","filterText":"function","insertText":"function()","insertTextFormat":1,"kind":3,"label":"function()","sortText":"000012function"} # -# CHECK-SAME: ]} +# CHECK-SAME: ]}} {"jsonrpc":"2.0","id":3,"method":"shutdown","params":null} Index: test/clangd/completion-priorities.test =================================================================== --- test/clangd/completion-priorities.test +++ test/clangd/completion-priorities.test @@ -15,69 +15,74 @@ {"jsonrpc":"2.0","id":2,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:///main.cpp"},"position":{"line":12,"character":8}}} # CHECK: "id": 2, # CHECK-NEXT: "jsonrpc": "2.0", -# CHECK-NEXT: "result": [ -# CHECK-NEXT: { -# CHECK-NEXT: "detail": "void", -# CHECK-NEXT: "filterText": "priv", -# CHECK-NEXT: "insertText": "priv", -# CHECK-NEXT: "insertTextFormat": 1, -# CHECK-NEXT: "kind": 2, -# CHECK-NEXT: "label": "priv()", -# CHECK-NEXT: "sortText": "000034priv" -# CHECK-NEXT: }, -# CHECK-NEXT: { -# CHECK-NEXT: "detail": "void", -# CHECK-NEXT: "filterText": "prot", -# CHECK-NEXT: "insertText": "prot", -# CHECK-NEXT: "insertTextFormat": 1, -# CHECK-NEXT: "kind": 2, -# CHECK-NEXT: "label": "prot()", -# CHECK-NEXT: "sortText": "000034prot" -# CHECK-NEXT: }, -# CHECK-NEXT: { -# CHECK-NEXT: "detail": "void", -# CHECK-NEXT: "filterText": "pub", -# CHECK-NEXT: "insertText": "pub", -# CHECK-NEXT: "insertTextFormat": 1, -# CHECK-NEXT: "kind": 2, -# CHECK-NEXT: "label": "pub()", -# CHECK-NEXT: "sortText": "000034pub" -# CHECK-NEXT: }, +# CHECK-NEXT: "result": { +# CHECK-NEXT: "isIncomplete": false, +# CHECK-NEXT: "items": [ +# CHECK-NEXT: { +# CHECK-NEXT: "detail": "void", +# CHECK-NEXT: "filterText": "priv", +# CHECK-NEXT: "insertText": "priv", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 2, +# CHECK-NEXT: "label": "priv()", +# CHECK-NEXT: "sortText": "000034priv" +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "detail": "void", +# CHECK-NEXT: "filterText": "prot", +# CHECK-NEXT: "insertText": "prot", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 2, +# CHECK-NEXT: "label": "prot()", +# CHECK-NEXT: "sortText": "000034prot" +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "detail": "void", +# CHECK-NEXT: "filterText": "pub", +# CHECK-NEXT: "insertText": "pub", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 2, +# CHECK-NEXT: "label": "pub()", +# CHECK-NEXT: "sortText": "000034pub" +# CHECK-NEXT: }, Content-Length: 151 {"jsonrpc":"2.0","id":3,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:///main.cpp"},"position":{"line":17,"character":4}}} # CHECK: "id": 3, # CHECK-NEXT: "jsonrpc": "2.0", -# CHECK-NEXT: "result": [ -# CHECK-NEXT: { -# CHECK-NEXT: "detail": "void", -# CHECK-NEXT: "filterText": "pub", -# CHECK-NEXT: "insertText": "pub", -# CHECK-NEXT: "insertTextFormat": 1, -# CHECK-NEXT: "kind": 2, -# CHECK-NEXT: "label": "pub()", -# CHECK-NEXT: "sortText": "000034pub" -# CHECK-NEXT: }, +# CHECK-NEXT: "result": { +# CHECK-NEXT: "isIncomplete": false, +# CHECK-NEXT: "items": [ +# CHECK-NEXT: { +# CHECK-NEXT: "detail": "void", +# CHECK-NEXT: "filterText": "pub", +# CHECK-NEXT: "insertText": "pub", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 2, +# CHECK-NEXT: "label": "pub()", +# CHECK-NEXT: "sortText": "000034pub" +# CHECK-NEXT: }, # priv() and prot() are at the end of the list -# CHECK-NEXT: { -# CHECK: "detail": "void", -# CHECK: "filterText": "priv", -# CHECK-NEXT: "insertText": "priv", -# CHECK-NEXT: "insertTextFormat": 1, -# CHECK-NEXT: "kind": 2, -# CHECK-NEXT: "label": "priv()", -# CHECK-NEXT: "sortText": "200034priv" -# CHECK-NEXT: }, -# CHECK-NEXT: { -# CHECK-NEXT: "detail": "void", -# CHECK-NEXT: "filterText": "prot", -# CHECK-NEXT: "insertText": "prot", -# CHECK-NEXT: "insertTextFormat": 1, -# CHECK-NEXT: "kind": 2, -# CHECK-NEXT: "label": "prot()", -# CHECK-NEXT: "sortText": "200034prot" -# CHECK-NEXT: } -# CHECK-NEXT: ] +# CHECK-NEXT: { +# CHECK: "detail": "void", +# CHECK: "filterText": "priv", +# CHECK-NEXT: "insertText": "priv", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 2, +# CHECK-NEXT: "label": "priv()", +# CHECK-NEXT: "sortText": "200034priv" +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "detail": "void", +# CHECK-NEXT: "filterText": "prot", +# CHECK-NEXT: "insertText": "prot", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 2, +# CHECK-NEXT: "label": "prot()", +# CHECK-NEXT: "sortText": "200034prot" +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } Content-Length: 58 {"jsonrpc":"2.0","id":4,"method":"shutdown","params":null} Index: test/clangd/completion-qualifiers.test =================================================================== --- test/clangd/completion-qualifiers.test +++ test/clangd/completion-qualifiers.test @@ -10,37 +10,40 @@ {"jsonrpc":"2.0","id":2,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:///main.cpp"},"position":{"line":11,"character":8}}} # CHECK: "id": 2, # CHECK-NEXT: "jsonrpc": "2.0", -# CHECK-NEXT: "result": [ +# CHECK-NEXT: "result": { +# CHECK-NEXT: "isIncomplete": false, +# CHECK-NEXT: "items": [ # Eligible const functions are at the top of the list. -# CHECK-NEXT: { -# CHECK-NEXT: "detail": "int", -# CHECK-NEXT: "filterText": "bar", -# CHECK-NEXT: "insertText": "bar", -# CHECK-NEXT: "insertTextFormat": 1, -# CHECK-NEXT: "kind": 2, -# CHECK-NEXT: "label": "bar() const", -# CHECK-NEXT: "sortText": "000037bar" -# CHECK-NEXT: }, -# CHECK-NEXT: { -# CHECK-NEXT: "detail": "int", -# CHECK-NEXT: "filterText": "foo", -# CHECK-NEXT: "insertText": "foo", -# CHECK-NEXT: "insertTextFormat": 1, -# CHECK-NEXT: "kind": 2, -# CHECK-NEXT: "label": "Foo::foo() const", -# CHECK-NEXT: "sortText": "000037foo" -# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "detail": "int", +# CHECK-NEXT: "filterText": "bar", +# CHECK-NEXT: "insertText": "bar", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 2, +# CHECK-NEXT: "label": "bar() const", +# CHECK-NEXT: "sortText": "000037bar" +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "detail": "int", +# CHECK-NEXT: "filterText": "foo", +# CHECK-NEXT: "insertText": "foo", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 2, +# CHECK-NEXT: "label": "Foo::foo() const", +# CHECK-NEXT: "sortText": "000037foo" +# CHECK-NEXT: }, # Ineligible non-const function is at the bottom of the list. -# CHECK-NEXT: { -# CHECK: "detail": "int", -# CHECK: "filterText": "foo", -# CHECK-NEXT: "insertText": "foo", -# CHECK-NEXT: "insertTextFormat": 1, -# CHECK-NEXT: "kind": 2, -# CHECK-NEXT: "label": "foo() const", -# CHECK-NEXT: "sortText": "200035foo" -# CHECK-NEXT: } -# CHECK-NEXT: ] +# CHECK-NEXT: { +# CHECK: "detail": "int", +# CHECK: "filterText": "foo", +# CHECK-NEXT: "insertText": "foo", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 2, +# CHECK-NEXT: "label": "foo() const", +# CHECK-NEXT: "sortText": "200035foo" +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } Content-Length: 44 {"jsonrpc":"2.0","id":4,"method":"shutdown"} Index: test/clangd/completion-snippet.test =================================================================== --- test/clangd/completion-snippet.test +++ test/clangd/completion-snippet.test @@ -14,71 +14,74 @@ {"jsonrpc":"2.0","id":1,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:///main.cpp"},"position":{"line":3,"character":5}}} # CHECK: "id": 1, # CHECK-NEXT: "jsonrpc": "2.0", -# CHECK-NEXT: "result": [ -# CHECK-NEXT: { -# CHECK-NEXT: "detail": "int", -# CHECK-NEXT: "filterText": "a", -# CHECK-NEXT: "insertText": "a", -# CHECK-NEXT: "insertTextFormat": 1, -# CHECK-NEXT: "kind": 5, -# CHECK-NEXT: "label": "a", -# CHECK-NEXT: "sortText": "000035a" -# CHECK-NEXT: }, -# CHECK-NEXT: { -# CHECK-NEXT: "detail": "int", -# CHECK-NEXT: "filterText": "bb", -# CHECK-NEXT: "insertText": "bb", -# CHECK-NEXT: "insertTextFormat": 1, -# CHECK-NEXT: "kind": 5, -# CHECK-NEXT: "label": "bb", -# CHECK-NEXT: "sortText": "000035bb" -# CHECK-NEXT: }, -# CHECK-NEXT: { -# CHECK-NEXT: "detail": "int", -# CHECK-NEXT: "filterText": "ccc", -# CHECK-NEXT: "insertText": "ccc", -# CHECK-NEXT: "insertTextFormat": 1, -# CHECK-NEXT: "kind": 5, -# CHECK-NEXT: "label": "ccc", -# CHECK-NEXT: "sortText": "000035ccc" -# CHECK-NEXT: }, -# CHECK-NEXT: { -# CHECK-NEXT: "detail": "int", -# CHECK-NEXT: "filterText": "f", -# CHECK-NEXT: "insertText": "f(${1:int i}, ${2:const float f})", -# CHECK-NEXT: "insertTextFormat": 2, -# CHECK-NEXT: "kind": 2, -# CHECK-NEXT: "label": "f(int i, const float f) const", -# CHECK-NEXT: "sortText": "000035f" -# CHECK-NEXT: }, -# CHECK-NEXT: { -# CHECK-NEXT: "filterText": "fake", -# CHECK-NEXT: "insertText": "fake::", -# CHECK-NEXT: "insertTextFormat": 1, -# CHECK-NEXT: "kind": 7, -# CHECK-NEXT: "label": "fake::", -# CHECK-NEXT: "sortText": "000075fake" -# CHECK-NEXT: }, -# CHECK-NEXT: { -# CHECK-NEXT: "detail": "fake &", -# CHECK-NEXT: "filterText": "operator=", -# CHECK-NEXT: "insertText": "operator=(${1:const fake &})", -# CHECK-NEXT: "insertTextFormat": 2, -# CHECK-NEXT: "kind": 2, -# CHECK-NEXT: "label": "operator=(const fake &)", -# CHECK-NEXT: "sortText": "000079operator=" -# CHECK-NEXT: }, +# CHECK-NEXT: "result": { +# CHECK-NEXT: "isIncomplete": false, +# CHECK-NEXT: "items": [ +# CHECK-NEXT: { +# CHECK-NEXT: "detail": "int", +# CHECK-NEXT: "filterText": "a", +# CHECK-NEXT: "insertText": "a", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 5, +# CHECK-NEXT: "label": "a", +# CHECK-NEXT: "sortText": "000035a" +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "detail": "int", +# CHECK-NEXT: "filterText": "bb", +# CHECK-NEXT: "insertText": "bb", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 5, +# CHECK-NEXT: "label": "bb", +# CHECK-NEXT: "sortText": "000035bb" +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "detail": "int", +# CHECK-NEXT: "filterText": "ccc", +# CHECK-NEXT: "insertText": "ccc", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 5, +# CHECK-NEXT: "label": "ccc", +# CHECK-NEXT: "sortText": "000035ccc" +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "detail": "int", +# CHECK-NEXT: "filterText": "f", +# CHECK-NEXT: "insertText": "f(${1:int i}, ${2:const float f})", +# CHECK-NEXT: "insertTextFormat": 2, +# CHECK-NEXT: "kind": 2, +# CHECK-NEXT: "label": "f(int i, const float f) const", +# CHECK-NEXT: "sortText": "000035f" +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "filterText": "fake", +# CHECK-NEXT: "insertText": "fake::", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 7, +# CHECK-NEXT: "label": "fake::", +# CHECK-NEXT: "sortText": "000075fake" +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "detail": "fake &", +# CHECK-NEXT: "filterText": "operator=", +# CHECK-NEXT: "insertText": "operator=(${1:const fake &})", +# CHECK-NEXT: "insertTextFormat": 2, +# CHECK-NEXT: "kind": 2, +# CHECK-NEXT: "label": "operator=(const fake &)", +# CHECK-NEXT: "sortText": "000079operator=" +# CHECK-NEXT: }, # FIXME: Why do some buildbots show an extra operator==(fake&&) here? -# CHECK: { -# CHECK: "detail": "void", -# CHECK-NEXT: "filterText": "~fake", -# CHECK-NEXT: "insertText": "~fake()", -# CHECK-NEXT: "insertTextFormat": 1, -# CHECK-NEXT: "kind": 4, -# CHECK-NEXT: "label": "~fake()", -# CHECK-NEXT: "sortText": "000079~fake" -# CHECK-NEXT: } -# CHECK-NEXT: ] +# CHECK: { +# CHECK: "detail": "void", +# CHECK-NEXT: "filterText": "~fake", +# CHECK-NEXT: "insertText": "~fake()", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 4, +# CHECK-NEXT: "label": "~fake()", +# CHECK-NEXT: "sortText": "000079~fake" +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } # Update the source file and check for completions again. Content-Length: 226 @@ -89,16 +92,18 @@ {"jsonrpc":"2.0","id":3,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:///main.cpp"},"position":{"line":3,"character":5}}} # CHECK: "id": 3, # CHECK-NEXT: "jsonrpc": "2.0", -# CHECK-NEXT: "result": [ -# CHECK-NEXT: { -# CHECK-NEXT: "detail": "int (*)(int, int)", -# CHECK-NEXT: "filterText": "func", -# CHECK-NEXT: "insertText": "func()", -# CHECK-NEXT: "insertTextFormat": 1, -# CHECK-NEXT: "kind": 2, -# CHECK-NEXT: "label": "func()", -# CHECK-NEXT: "sortText": "000034func" -# CHECK-NEXT: }, +# CHECK-NEXT: "result": { +# CHECK-NEXT: "isIncomplete": false, +# CHECK-NEXT: "items": [ +# CHECK-NEXT: { +# CHECK-NEXT: "detail": "int (*)(int, int)", +# CHECK-NEXT: "filterText": "func", +# CHECK-NEXT: "insertText": "func()", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 2, +# CHECK-NEXT: "label": "func()", +# CHECK-NEXT: "sortText": "000034func" +# CHECK-NEXT: }, Content-Length: 44 {"jsonrpc":"2.0","id":4,"method":"shutdown"} Index: test/clangd/completion.test =================================================================== --- test/clangd/completion.test +++ test/clangd/completion.test @@ -14,7 +14,9 @@ {"jsonrpc":"2.0","id":1,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:///main.cpp"},"position":{"line":3,"character":5}}} # CHECK: "id": 1 # CHECK-NEXT: "jsonrpc": "2.0", -# CHECK-NEXT: "result": [ +# CHECK-NEXT: "result": { +# CHECK-NEXT: "isIncomplete": false, +# CHECK-NEXT: "items": [ # CHECK-NEXT: { # CHECK-NEXT: "detail": "int", # CHECK-NEXT: "filterText": "a", @@ -84,7 +86,9 @@ {"jsonrpc":"2.0","id":2,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:///main.cpp"},"position":{"line":3,"character":5}}} # CHECK: "id": 2 # CHECK-NEXT: "jsonrpc": "2.0", -# CHECK-NEXT: "result": [ +# CHECK-NEXT: "result": { +# CHECK-NEXT: "isIncomplete": false, +# CHECK-NEXT: "items": [ # CHECK-NEXT: { # CHECK-NEXT: "detail": "int", # CHECK-NEXT: "filterText": "a", @@ -158,7 +162,9 @@ {"jsonrpc":"2.0","id":3,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:///main.cpp"},"position":{"line":3,"character":5}}} # CHECK: "id": 3, # CHECK-NEXT: "jsonrpc": "2.0", -# CHECK-NEXT: "result": [ +# CHECK-NEXT: "result": { +# CHECK-NEXT: "isIncomplete": false, +# CHECK-NEXT: "items": [ # CHECK-NEXT: { # CHECK-NEXT: "detail": "int (*)(int, int)", # CHECK-NEXT: "filterText": "func", Index: test/clangd/protocol.test =================================================================== --- test/clangd/protocol.test +++ test/clangd/protocol.test @@ -31,14 +31,17 @@ # # CHECK: "id": 1, # CHECK-NEXT: "jsonrpc": "2.0", -# CHECK-NEXT: "result": [ -# CHECK: "filterText": "fake", -# CHECK-NEXT: "insertText": "fake", -# CHECK-NEXT: "insertTextFormat": 1, -# CHECK-NEXT: "kind": 7, -# CHECK-NEXT: "label": "fake::", -# CHECK-NEXT: "sortText": "000075fake" -# CHECK: ] +# CHECK-NEXT: "result": { +# CHECK-NEXT: "isIncomplete": false, +# CHECK-NEXT: "items": [ +# CHECK: "filterText": "fake", +# CHECK-NEXT: "insertText": "fake", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 7, +# CHECK-NEXT: "label": "fake::", +# CHECK-NEXT: "sortText": "000075fake" +# CHECK: ] +# CHECK-NEXT: } X-Test: Testing Content-Type: application/vscode-jsonrpc; charset-utf-8 @@ -57,14 +60,17 @@ # # CHECK: "id": 3, # CHECK-NEXT: "jsonrpc": "2.0", -# CHECK-NEXT: "result": [ -# CHECK: "filterText": "fake", -# CHECK-NEXT: "insertText": "fake", -# CHECK-NEXT: "insertTextFormat": 1, -# CHECK-NEXT: "kind": 7, -# CHECK-NEXT: "label": "fake::", -# CHECK-NEXT: "sortText": "000075fake" -# CHECK: ] +# CHECK-NEXT: "result": { +# CHECK-NEXT: "isIncomplete": false, +# CHECK-NEXT: "items": [ +# CHECK: "filterText": "fake", +# CHECK-NEXT: "insertText": "fake", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 7, +# CHECK-NEXT: "label": "fake::", +# CHECK-NEXT: "sortText": "000075fake" +# CHECK: ] +# CHECK-NEXT: } # STDERR: Warning: Duplicate Content-Length header received. The previous value for this message (10) was ignored. Content-Type: application/vscode-jsonrpc; charset-utf-8 @@ -83,14 +89,17 @@ # # CHECK: "id": 5, # CHECK-NEXT: "jsonrpc": "2.0", -# CHECK-NEXT: "result": [ -# CHECK: "filterText": "fake", -# CHECK-NEXT: "insertText": "fake", -# CHECK-NEXT: "insertTextFormat": 1, -# CHECK-NEXT: "kind": 7, -# CHECK-NEXT: "label": "fake::", -# CHECK-NEXT: "sortText": "000075fake" -# CHECK: ] +# CHECK-NEXT: "result": { +# CHECK-NEXT: "isIncomplete": false, +# CHECK-NEXT: "items": [ +# CHECK: "filterText": "fake", +# CHECK-NEXT: "insertText": "fake", +# CHECK-NEXT: "insertTextFormat": 1, +# CHECK-NEXT: "kind": 7, +# CHECK-NEXT: "label": "fake::", +# CHECK-NEXT: "sortText": "000075fake" +# CHECK: ] +# CHECK-NEXT: } Content-Length: 1024 {"jsonrpc":"2.0","id":5,"method":"textDocument/completion","params":{"textDocument":{"uri":"file:/main.cpp"},"position":{"line":3,"character":5}}} Index: unittests/clangd/ClangdTests.cpp =================================================================== --- unittests/clangd/ClangdTests.cpp +++ unittests/clangd/ClangdTests.cpp @@ -619,16 +619,15 @@ class ClangdCompletionTest : public ClangdVFSTest { protected: template - bool ContainsItemPred(std::vector const &Items, - Predicate Pred) { - for (const auto &Item : Items) { + bool ContainsItemPred(CompletionList const &Items, Predicate Pred) { + for (const auto &Item : Items.items) { if (Pred(Item)) return true; } return false; } - bool ContainsItem(std::vector const &Items, StringRef Name) { + bool ContainsItem(CompletionList const &Items, StringRef Name) { return ContainsItemPred(Items, [Name](clangd::CompletionItem Item) { return Item.insertText == Name; }); @@ -694,6 +693,44 @@ } } +TEST_F(ClangdCompletionTest, Limit) { + MockFSProvider FS; + MockCompilationDatabase CDB(/*AddFreestandingFlag=*/true); + CDB.ExtraClangFlags.push_back("-xc++"); + ErrorCheckingDiagConsumer DiagConsumer; + clangd::CodeCompleteOptions Opts; + Opts.Limit = 2; + ClangdServer Server(CDB, DiagConsumer, FS, getDefaultAsyncThreadsCount(), + Opts, EmptyLogger::getInstance()); + + auto FooCpp = getVirtualTestFilePath("foo.cpp"); + FS.Files[FooCpp] = ""; + FS.ExpectedFile = FooCpp; + StringWithPos Completion = parseTextMarker(R"cpp( +struct ClassWithMembers { + int AAA(); + int BBB(); + int CCC(); +} +int main() { ClassWithMembers().{complete} } + )cpp", + "complete"); + Server.addDocument(FooCpp, Completion.Text); + + /// For after-dot completion we must always get consistent results. + auto Results = Server + .codeComplete(FooCpp, Completion.MarkerPos, + StringRef(Completion.Text)) + .get() + .Value; + + EXPECT_TRUE(Results.isIncomplete); + EXPECT_EQ(Opts.Limit, Results.items.size()); + EXPECT_TRUE(ContainsItem(Results, "AAA")); + EXPECT_TRUE(ContainsItem(Results, "BBB")); + EXPECT_FALSE(ContainsItem(Results, "CCC")); +} + TEST_F(ClangdCompletionTest, CompletionOptions) { MockFSProvider FS; ErrorCheckingDiagConsumer DiagConsumer;