Simplify and optimize completion.

This commit is contained in:
Fangrui Song 2018-03-18 13:04:59 -07:00
parent 02542b1e69
commit ad88f707f7
6 changed files with 44 additions and 108 deletions

View File

@ -156,7 +156,7 @@ TEST_SUITE("fuzzy_match") {
Ranks("ma", {"map", "many", "maximum"}); Ranks("ma", {"map", "many", "maximum"});
Ranks("print", {"printf", "sprintf"}); Ranks("print", {"printf", "sprintf"});
// score(PRINT) = kMinScore // score(PRINT) = kMinScore
Ranks("int", {"int", "INT", "PRINT"}); Ranks("ast", {"ast", "AST", "INT_FAST16_MAX"});
// score(PRINT) > kMinScore // score(PRINT) > kMinScore
Ranks("Int", {"int", "INT", "PRINT"}); Ranks("Int", {"int", "INT", "PRINT"});
} }

View File

@ -180,23 +180,10 @@ std::string_view LexIdentifierAroundPos(lsPosition position,
return content.substr(start, end - start); return content.substr(start, end - start);
} }
bool SubsequenceMatchIgnoreCase(std::string_view search, std::string_view content) {
size_t j = 0;
for (size_t i = 0; i < search.size(); i++) {
char search_char = tolower(search[i]);
while (j < content.size() && tolower(content[j]) != search_char)
j++;
if (j == content.size())
return false;
j++;
}
return true;
}
// Find discontinous |search| in |content|. // Find discontinous |search| in |content|.
// Return |found| and the count of skipped chars before found. // Return |found| and the count of skipped chars before found.
std::tuple<bool, int> SubsequenceCountSkip(std::string_view search, std::pair<bool, int> CaseFoldingSubsequenceMatch(std::string_view search,
std::string_view content) { std::string_view content) {
bool hasUppercaseLetter = std::any_of(search.begin(), search.end(), isupper); bool hasUppercaseLetter = std::any_of(search.begin(), search.end(), isupper);
int skip = 0; int skip = 0;
size_t j = 0; size_t j = 0;
@ -206,10 +193,10 @@ std::tuple<bool, int> SubsequenceCountSkip(std::string_view search,
: tolower(content[j]) != tolower(c))) : tolower(content[j]) != tolower(c)))
++j, ++skip; ++j, ++skip;
if (j == content.size()) if (j == content.size())
return std::make_tuple(false, skip); return {false, skip};
++j; ++j;
} }
return std::make_tuple(true, skip); return {true, skip};
} }
TEST_SUITE("Offset") { TEST_SUITE("Offset") {
@ -234,43 +221,20 @@ TEST_SUITE("Offset") {
} }
TEST_SUITE("Substring") { TEST_SUITE("Substring") {
TEST_CASE("match") {
// Empty string matches anything.
REQUIRE(SubsequenceMatchIgnoreCase("", ""));
REQUIRE(SubsequenceMatchIgnoreCase("", "aa"));
// Match in start/middle/end.
REQUIRE(SubsequenceMatchIgnoreCase("a", "abbbb"));
REQUIRE(SubsequenceMatchIgnoreCase("a", "bbabb"));
REQUIRE(SubsequenceMatchIgnoreCase("a", "bbbba"));
REQUIRE(SubsequenceMatchIgnoreCase("aa", "aabbb"));
REQUIRE(SubsequenceMatchIgnoreCase("aa", "bbaab"));
REQUIRE(SubsequenceMatchIgnoreCase("aa", "bbbaa"));
// Capitalization.
REQUIRE(SubsequenceMatchIgnoreCase("aa", "aA"));
REQUIRE(SubsequenceMatchIgnoreCase("aa", "Aa"));
REQUIRE(SubsequenceMatchIgnoreCase("aa", "AA"));
// Token skipping.
REQUIRE(SubsequenceMatchIgnoreCase("ad", "abcd"));
REQUIRE(SubsequenceMatchIgnoreCase("ad", "ABCD"));
// Ordering.
REQUIRE(!SubsequenceMatchIgnoreCase("ad", "dcba"));
}
TEST_CASE("skip") { TEST_CASE("skip") {
REQUIRE(SubsequenceCountSkip("a", "a") == std::make_tuple(true, 0)); REQUIRE(CaseFoldingSubsequenceMatch("a", "a") == std::make_pair(true, 0));
REQUIRE(SubsequenceCountSkip("b", "a") == std::make_tuple(false, 1)); REQUIRE(CaseFoldingSubsequenceMatch("b", "a") == std::make_pair(false, 1));
REQUIRE(SubsequenceCountSkip("", "") == std::make_tuple(true, 0)); REQUIRE(CaseFoldingSubsequenceMatch("", "") == std::make_pair(true, 0));
REQUIRE(SubsequenceCountSkip("a", "ba") == std::make_tuple(true, 1)); REQUIRE(CaseFoldingSubsequenceMatch("a", "ba") == std::make_pair(true, 1));
REQUIRE(SubsequenceCountSkip("aa", "aba") == std::make_tuple(true, 1)); REQUIRE(CaseFoldingSubsequenceMatch("aa", "aba") ==
REQUIRE(SubsequenceCountSkip("aa", "baa") == std::make_tuple(true, 1)); std::make_pair(true, 1));
REQUIRE(SubsequenceCountSkip("aA", "aA") == std::make_tuple(true, 0)); REQUIRE(CaseFoldingSubsequenceMatch("aa", "baa") ==
REQUIRE(SubsequenceCountSkip("aA", "aa") == std::make_tuple(false, 1)); std::make_pair(true, 1));
REQUIRE(SubsequenceCountSkip("incstdioh", "include <stdio.h>") == REQUIRE(CaseFoldingSubsequenceMatch("aA", "aA") == std::make_pair(true, 0));
std::make_tuple(true, 7)); REQUIRE(CaseFoldingSubsequenceMatch("aA", "aa") ==
std::make_pair(false, 1));
REQUIRE(CaseFoldingSubsequenceMatch("incstdioh", "include <stdio.h>") ==
std::make_pair(true, 7));
} }
} }

View File

@ -26,8 +26,5 @@ void LexFunctionDeclaration(const std::string& buffer_content,
std::string_view LexIdentifierAroundPos(lsPosition position, std::string_view LexIdentifierAroundPos(lsPosition position,
std::string_view content); std::string_view content);
// Case-insensitive subsequence matching. std::pair<bool, int> CaseFoldingSubsequenceMatch(std::string_view search,
bool SubsequenceMatchIgnoreCase(std::string_view search, std::string_view content); std::string_view content);
std::tuple<bool, int> SubsequenceCountSkip(std::string_view search,
std::string_view content);

View File

@ -72,8 +72,7 @@ struct lsCompletionItem {
optional<std::string> documentation; optional<std::string> documentation;
// Internal information to order candidates. // Internal information to order candidates.
bool found_; int score_;
std::string::size_type skip_;
unsigned priority_; unsigned priority_;
// Use <> or "" by default as include path. // Use <> or "" by default as include path.

View File

@ -1,5 +1,6 @@
#include "clang_complete.h" #include "clang_complete.h"
#include "code_complete_cache.h" #include "code_complete_cache.h"
#include "fuzzy_match.h"
#include "include_complete.h" #include "include_complete.h"
#include "message_handler.h" #include "message_handler.h"
#include "queue_manager.h" #include "queue_manager.h"
@ -70,19 +71,6 @@ struct Out_TextDocumentComplete
}; };
MAKE_REFLECT_STRUCT(Out_TextDocumentComplete, jsonrpc, id, result); MAKE_REFLECT_STRUCT(Out_TextDocumentComplete, jsonrpc, id, result);
bool CompareLsCompletionItem(const lsCompletionItem& lhs,
const lsCompletionItem& rhs) {
if (lhs.found_ != rhs.found_)
return !lhs.found_ < !rhs.found_;
if (lhs.skip_ != rhs.skip_)
return lhs.skip_ < rhs.skip_;
if (lhs.priority_ != rhs.priority_)
return lhs.priority_ < rhs.priority_;
if (lhs.filterText->length() != rhs.filterText->length())
return lhs.filterText->length() < rhs.filterText->length();
return *lhs.filterText < *rhs.filterText;
}
void DecorateIncludePaths(const std::smatch& match, void DecorateIncludePaths(const std::smatch& match,
std::vector<lsCompletionItem>* items) { std::vector<lsCompletionItem>* items) {
std::string spaces_after_include = " "; std::string spaces_after_include = " ";
@ -200,41 +188,29 @@ void FilterAndSortCompletionResponse(
item.filterText = item.label; item.filterText = item.label;
} }
// If the text doesn't start with underscore, remove all candidates that // Fuzzy match and remove awful candidates.
// start with underscore. FuzzyMatcher fuzzy(complete_text);
if (complete_text[0] != '_') {
auto filter = [](const lsCompletionItem& item) {
return (*item.filterText)[0] == '_';
};
items.erase(std::remove_if(items.begin(), items.end(), filter),
items.end());
}
// Fuzzy match. Remove any candidates that do not match.
bool found = false;
for (auto& item : items) { for (auto& item : items) {
std::tie(item.found_, item.skip_) = item.score_ =
SubsequenceCountSkip(complete_text, *item.filterText); CaseFoldingSubsequenceMatch(complete_text, *item.filterText).first
found = found || item.found_; ? fuzzy.Match(*item.filterText)
} : FuzzyMatcher::kMinScore;
if (found) {
auto filter = [](const lsCompletionItem& item) { return !item.found_; };
items.erase(std::remove_if(items.begin(), items.end(), filter),
items.end());
// Order all items and set |sortText|.
const size_t kMaxSortSize = 200u;
if (items.size() <= kMaxSortSize) {
std::sort(items.begin(), items.end(), CompareLsCompletionItem);
} else {
// Just place items that found the text before those not.
std::vector<lsCompletionItem> items_found, items_notfound;
for (auto& item : items)
(item.found_ ? items_found : items_notfound).push_back(item);
items = items_found;
items.insert(items.end(), items_notfound.begin(), items_notfound.end());
}
} }
items.erase(std::remove_if(items.begin(), items.end(),
[](const lsCompletionItem& item) {
return item.score_ <= FuzzyMatcher::kMinScore;
}),
items.end());
std::sort(items.begin(), items.end(),
[](const lsCompletionItem& lhs, const lsCompletionItem& rhs) {
if (lhs.score_ != rhs.score_)
return lhs.score_ > rhs.score_;
if (lhs.priority_ != rhs.priority_)
return lhs.priority_ < rhs.priority_;
if (lhs.filterText->size() != rhs.filterText->size())
return lhs.filterText->size() < rhs.filterText->size();
return *lhs.filterText < *rhs.filterText;
});
// Trim result. // Trim result.
finalize(); finalize();

View File

@ -107,7 +107,7 @@ struct WorkspaceSymbolHandler : BaseMessageHandler<Ipc_WorkspaceSymbol> {
for (int i = 0; i < (int)db->symbols.size(); ++i) { for (int i = 0; i < (int)db->symbols.size(); ++i) {
std::string_view detailed_name = db->GetSymbolDetailedName(i); std::string_view detailed_name = db->GetSymbolDetailedName(i);
if (SubsequenceMatchIgnoreCase(query_without_space, detailed_name)) { if (CaseFoldingSubsequenceMatch(query_without_space, detailed_name).first) {
// Do not show the same entry twice. // Do not show the same entry twice.
if (!inserted_results.insert(std::string(detailed_name)).second) if (!inserted_results.insert(std::string(detailed_name)).second)
continue; continue;