From 9d1369786fd534f1c8470c235d301ed35d729934 Mon Sep 17 00:00:00 2001 From: Jacob Dufault Date: Thu, 15 Jun 2017 19:28:49 -0700 Subject: [PATCH] Pre-filter completion results before sending to vscode. This results in a less laggy completion experience. Before, vscode would drop frames if handed too many completions (ie, by typing #include in a very large project). --- src/command_line.cc | 54 ++++++++++++++++++++++++++++++++++-- src/lex_utils.cc | 65 +++++++++++++++++++++++++++++++++++++++++++- src/lex_utils.h | 4 ++- src/working_files.cc | 25 ++++++++++++++--- src/working_files.h | 4 ++- 5 files changed, 142 insertions(+), 10 deletions(-) diff --git a/src/command_line.cc b/src/command_line.cc index e0ada0e0..6d889572 100644 --- a/src/command_line.cc +++ b/src/command_line.cc @@ -435,11 +435,53 @@ void EmitDiagnostics(WorkingFiles* working_files, std::string path, NonElidedVec }); } +// Pre-filters completion responses before sending to vscode. This results in a +// significantly snappier completion experience as vscode is easily overloaded +// when given 1000+ completion items. +void FilterCompletionResponse(Out_TextDocumentComplete* complete_response, + const std::string& complete_text) { + // Used to inject more completions. +#if false + const size_t kNumIterations = 250; + size_t size = complete_response->result.items.size(); + complete_response->result.items.reserve(size * (kNumIterations + 1)); + for (size_t iteration = 0; iteration < kNumIterations; ++iteration) { + for (size_t i = 0; i < size; ++i) { + auto item = complete_response->result.items[i]; + item.label += "#" + std::to_string(iteration); + complete_response->result.items.push_back(item); + } + } +#endif + const size_t kMaxResultSize = 100u; + if (complete_response->result.items.size() > kMaxResultSize) { + //std::cerr << "!!! Filtering " << complete_response->result.items.size() << " results using " << complete_text << std::endl; + complete_response->result.isIncomplete = true; + if (complete_text.empty()) { + complete_response->result.items.resize(kMaxResultSize); + } + else { + NonElidedVector filtered_result; + filtered_result.reserve(kMaxResultSize); + for (const lsCompletionItem& item : complete_response->result.items) { + if (SubstringMatch(complete_text, item.label)) { + //std::cerr << "!! emitting " << item.label << std::endl; + filtered_result.push_back(item); + if (filtered_result.size() >= kMaxResultSize) + break; + } + } + complete_response->result.items = filtered_result; + } + + //std::cerr << "!! Filtering resulted in " << complete_response->result.items.size() << " entries" << std::endl; + } +} @@ -1547,15 +1589,20 @@ bool QueryDbMainLoop( } std::cerr << "[complete] Returning " << complete_response.result.items.size() << " include completions" << std::endl; + FilterCompletionResponse(&complete_response, buffer_line); ipc->SendOutMessageToClient(IpcId::TextDocumentCompletion, complete_response); } else { bool is_global_completion = false; - if (file) - msg->params.position = file->FindStableCompletionSource(msg->params.position, &is_global_completion); + std::string existing_completion; + if (file) { + msg->params.position = file->FindStableCompletionSource(msg->params.position, &is_global_completion, &existing_completion); + } + + std::cerr << "[complete] Got existing completion " << existing_completion; ClangCompleteManager::OnComplete callback = std::bind( - [working_files, global_code_complete_cache, non_global_code_complete_cache, is_global_completion] + [working_files, global_code_complete_cache, non_global_code_complete_cache, is_global_completion, existing_completion] (Ipc_TextDocumentComplete* msg, NonElidedVector results) { Out_TextDocumentComplete complete_response; @@ -1564,6 +1611,7 @@ bool QueryDbMainLoop( complete_response.result.items = results; // Emit completion results. + FilterCompletionResponse(&complete_response, existing_completion); IpcManager::instance()->SendOutMessageToClient(IpcId::TextDocumentCompletion, complete_response); // Cache completion results. diff --git a/src/lex_utils.cc b/src/lex_utils.cc index a53cf3cf..13c26e8a 100644 --- a/src/lex_utils.cc +++ b/src/lex_utils.cc @@ -1,5 +1,7 @@ #include "lex_utils.h" +#include + int GetOffsetForPosition(lsPosition position, const std::string& content) { int offset = 0; @@ -173,4 +175,65 @@ std::string LexWordAroundPos(lsPosition position, const std::string& content) { } return content.substr(start, end - start + 1); -} \ No newline at end of file +} + +bool SubstringMatch(const std::string& search, const std::string& content) { + if (search.empty()) + return true; + + size_t search_index = 0; + char search_char = tolower(search[search_index]); + + size_t content_index = 0; + + while (true) { + char content_char = tolower(content[content_index]); + + if (content_char == search_char) { + search_index += 1; + if (search_index >= search.size()) + return true; + search_char = tolower(search[search_index]); + } + + content_index += 1; + if (content_index >= content.size()) + return false; + } + + return false; +} + +TEST_SUITE("Substring"); + +TEST_CASE("match") { + // Sanity. + REQUIRE(SubstringMatch("a", "aa")); + REQUIRE(SubstringMatch("aa", "aa")); + + // Empty string matches anything. + REQUIRE(SubstringMatch("", "")); + REQUIRE(SubstringMatch("", "aa")); + + // Match in start/middle/end. + REQUIRE(SubstringMatch("a", "abbbb")); + REQUIRE(SubstringMatch("a", "bbabb")); + REQUIRE(SubstringMatch("a", "bbbba")); + REQUIRE(SubstringMatch("aa", "aabbb")); + REQUIRE(SubstringMatch("aa", "bbaab")); + REQUIRE(SubstringMatch("aa", "bbbaa")); + + // Capitalization. + REQUIRE(SubstringMatch("aa", "aA")); + REQUIRE(SubstringMatch("aa", "Aa")); + REQUIRE(SubstringMatch("aa", "AA")); + + // Token skipping. + REQUIRE(SubstringMatch("ad", "abcd")); + REQUIRE(SubstringMatch("ad", "ABCD")); + + // Ordering. + REQUIRE(!SubstringMatch("ad", "dcba")); +} + +TEST_SUITE_END(); \ No newline at end of file diff --git a/src/lex_utils.h b/src/lex_utils.h index 76e005ab..ccb11416 100644 --- a/src/lex_utils.h +++ b/src/lex_utils.h @@ -17,4 +17,6 @@ optional ExtractQuotedRange(int line_number, const std::string& line); void LexFunctionDeclaration(const std::string& buffer_content, lsPosition declaration_spelling, optional type_name, std::string* insert_text, int* newlines_after_name); -std::string LexWordAroundPos(lsPosition position, const std::string& content); \ No newline at end of file +std::string LexWordAroundPos(lsPosition position, const std::string& content); + +bool SubstringMatch(const std::string& search, const std::string& content); \ No newline at end of file diff --git a/src/working_files.cc b/src/working_files.cc index 29c34d6e..38ee2523 100644 --- a/src/working_files.cc +++ b/src/working_files.cc @@ -214,12 +214,11 @@ std::string WorkingFile::FindClosestCallNameInBuffer(lsPosition position, int* a return buffer_content.substr(offset, start_offset - offset + 1); } -// Returns a position which contains the most recent ., ->, :, or ( for code -// completion purposes. -lsPosition WorkingFile::FindStableCompletionSource(lsPosition position, bool* is_global_completion) const { +lsPosition WorkingFile::FindStableCompletionSource(lsPosition position, bool* is_global_completion, std::string* existing_completion) const { *is_global_completion = true; - int offset = GetOffsetForPosition(position, buffer_content); + int start_offset = GetOffsetForPosition(position, buffer_content); + int offset = start_offset; while (offset > 0) { char c = buffer_content[offset - 1]; @@ -241,6 +240,7 @@ lsPosition WorkingFile::FindStableCompletionSource(lsPosition position, bool* is --offset; } + *existing_completion = buffer_content.substr(offset, start_offset - offset); return GetPositionForOffset(buffer_content, offset); } @@ -403,4 +403,21 @@ TEST_CASE("auto-insert )") { REQUIRE(active_param == 0); } +TEST_CASE("existing completion") { + WorkingFile f("foo.cc", "zzz.asdf"); + bool is_global_completion; + std::string existing_completion; + + f.FindStableCompletionSource(CharPos(f, '.'), &is_global_completion, &existing_completion); + REQUIRE(existing_completion == "zzz"); + f.FindStableCompletionSource(CharPos(f, 'a', 1), &is_global_completion, &existing_completion); + REQUIRE(existing_completion == "a"); + f.FindStableCompletionSource(CharPos(f, 's', 1), &is_global_completion, &existing_completion); + REQUIRE(existing_completion == "as"); + f.FindStableCompletionSource(CharPos(f, 'd', 1), &is_global_completion, &existing_completion); + REQUIRE(existing_completion == "asd"); + f.FindStableCompletionSource(CharPos(f, 'f', 1), &is_global_completion, &existing_completion); + REQUIRE(existing_completion == "asdf"); +} + TEST_SUITE_END(); \ No newline at end of file diff --git a/src/working_files.h b/src/working_files.h index bdea8456..ccf34000 100644 --- a/src/working_files.h +++ b/src/working_files.h @@ -61,7 +61,9 @@ struct WorkingFile { // // The out param |is_global_completion| is set to true if this looks like a // global completion. - lsPosition FindStableCompletionSource(lsPosition position, bool* is_global_completion) const; + // The out param |existing_completion| is set to any existing completion + // content the user has entered. + lsPosition FindStableCompletionSource(lsPosition position, bool* is_global_completion, std::string* existing_completion) const; CXUnsavedFile AsUnsavedFile() const; };