diff --git a/.gitmodules b/.gitmodules index 211365bfa..0172f592e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,6 @@ [submodule "third_party/reproc"] path = third_party/reproc url = https://github.com/DaanDeMeyer/reproc.git +[submodule "third_party/lib_fts"] + path = third_party/lib_fts + url = https://github.com/forrestthewoods/lib_fts.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 5c54eae41..5b5c0c3a4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -156,6 +156,7 @@ target_include_directories(cquery PRIVATE third_party/pugixml/src third_party/rapidjson/include third_party/sparsepp + third_party/lib_fts/code ) ### Install @@ -304,6 +305,8 @@ target_sources(cquery PRIVATE src/file_contents.cc src/file_types.cc src/fuzzy_match.cc + src/fts_match.cc + src/prefix_match.cc src/iindexer.cc src/import_manager.cc src/import_pipeline.cc diff --git a/src/completion_matcher.h b/src/completion_matcher.h new file mode 100644 index 000000000..d69c47742 --- /dev/null +++ b/src/completion_matcher.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +class CompletionMatcher { + public: + virtual ~CompletionMatcher() = default; + virtual int Match(std::string_view text) = 0; + virtual int MinScore() const = 0; +}; diff --git a/src/config.h b/src/config.h index 3867ffe5c..d90a6c331 100644 --- a/src/config.h +++ b/src/config.h @@ -156,6 +156,14 @@ struct Config { // For example, to hide all files in a /CACHE/ folder, use ".*/CACHE/.*" std::vector includeBlacklist; std::vector includeWhitelist; + + // Matcher type to filter completion candidates. + // Available matchers are: + // "cqueryMatcher": default cquery fuzzy matching algorithm + // "ftsMatcher": fuzzy matching algorithm powered by + // lib_fts "caseSensitivePrefixMatcher": simple case sensitive prefix + // matcher "caseInsensitivePrefixMatcher": simple case insensitive prefix + std::string matcherType = "cqueryMatcher"; }; Completion completion; @@ -261,7 +269,8 @@ MAKE_REFLECT_STRUCT(Config::Completion, includeMaxPathSize, includeSuffixWhitelist, includeBlacklist, - includeWhitelist); + includeWhitelist, + matcherType); MAKE_REFLECT_STRUCT(Config::Formatting, enabled) MAKE_REFLECT_STRUCT(Config::Diagnostics, blacklist, diff --git a/src/fts_match.cc b/src/fts_match.cc new file mode 100644 index 000000000..147d3e5db --- /dev/null +++ b/src/fts_match.cc @@ -0,0 +1,23 @@ +#include "fts_match.h" + +#define FTS_FUZZY_MATCH_IMPLEMENTATION 1 +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-function" +#include +#pragma clang diagnostic pop + +FtsMatcher::FtsMatcher(std::string_view pattern) { + original_pattern = pattern; +} + +int FtsMatcher::Match(std::string_view text) { + int result = 0; + if (fts::fuzzy_match(original_pattern.data(), text.data(), result)) { + return result; + } + return MinScore(); +} + +int FtsMatcher::MinScore() const { + return -100000; +} diff --git a/src/fts_match.h b/src/fts_match.h new file mode 100644 index 000000000..d98fcad7b --- /dev/null +++ b/src/fts_match.h @@ -0,0 +1,13 @@ +#pragma once + +#include "completion_matcher.h" + +class FtsMatcher : public CompletionMatcher { + public: + FtsMatcher(std::string_view pattern); + int Match(std::string_view text) override; + int MinScore() const override; + + private: + std::string_view original_pattern; +}; diff --git a/src/fuzzy_match.cc b/src/fuzzy_match.cc index 6e271665d..6f8670a57 100644 --- a/src/fuzzy_match.cc +++ b/src/fuzzy_match.cc @@ -6,6 +6,7 @@ #include #include #include +#include "lex_utils.h" enum CharClass { Other, Lower, Upper }; enum CharRole { None, Tail, Head }; @@ -73,6 +74,7 @@ int FuzzyMatcher::MatchScore(int i, int j, bool last) { } FuzzyMatcher::FuzzyMatcher(std::string_view pattern) { + original_pattern = pattern; CalculateRoles(pattern, pat_role, &pat_set); size_t n = 0; for (size_t i = 0; i < pattern.size(); i++) @@ -85,6 +87,9 @@ FuzzyMatcher::FuzzyMatcher(std::string_view pattern) { } int FuzzyMatcher::Match(std::string_view text) { + if (!CaseFoldingSubsequenceMatch(original_pattern, text).first) { + return MinScore(); + } int n = int(text.size()); if (n > kMaxText) return kMinScore + 1; @@ -123,6 +128,10 @@ int FuzzyMatcher::Match(std::string_view text) { return ret; } +int FuzzyMatcher::MinScore() const { + return kMinScore; +} + TEST_SUITE("fuzzy_match") { bool Ranks(std::string_view pat, std::vector texts) { FuzzyMatcher fuzzy(pat); diff --git a/src/fuzzy_match.h b/src/fuzzy_match.h index 336815b72..925b5c578 100644 --- a/src/fuzzy_match.h +++ b/src/fuzzy_match.h @@ -4,8 +4,9 @@ #include #include +#include "completion_matcher.h" -class FuzzyMatcher { +class FuzzyMatcher : public CompletionMatcher { public: constexpr static int kMaxPat = 100; constexpr static int kMaxText = 200; @@ -14,9 +15,11 @@ class FuzzyMatcher { constexpr static int kMinScore = INT_MIN / 4; FuzzyMatcher(std::string_view pattern); - int Match(std::string_view text); + int Match(std::string_view text) override; + int MinScore() const override; private: + std::string_view original_pattern; std::string pat; std::string_view text; int pat_set, text_set; diff --git a/src/messages/text_document_completion.cc b/src/messages/text_document_completion.cc index 7cd8a252b..2d7edb2ba 100644 --- a/src/messages/text_document_completion.cc +++ b/src/messages/text_document_completion.cc @@ -1,11 +1,14 @@ #include "clang_complete.h" #include "code_complete_cache.h" #include "fuzzy_match.h" +#include "fts_match.h" +#include "prefix_match.h" #include "include_complete.h" #include "message_handler.h" #include "queue_manager.h" #include "timer.h" #include "working_files.h" +#include "config.h" #include "lex_utils.h" @@ -221,17 +224,23 @@ void FilterAndSortCompletionResponse( item.filterText = item.label; } - // Fuzzy match and remove awful candidates. - FuzzyMatcher fuzzy(complete_text); + // Match and remove awful candidates. + std::unique_ptr matcher; + if (g_config->completion.matcherType == "cqueryMatcher") { + matcher = std::make_unique(complete_text); + } else if (g_config->completion.matcherType == "ftsMatcher") { + matcher = std::make_unique(complete_text); + } else if (g_config->completion.matcherType == "caseSensitivePrefixMatcher") { + matcher = std::make_unique(complete_text, true); + } else if (g_config->completion.matcherType == "caseInsensitivePrefixMatcher") { + matcher = std::make_unique(complete_text, false); + } for (auto& item : items) { - item.score_ = - CaseFoldingSubsequenceMatch(complete_text, *item.filterText).first - ? fuzzy.Match(*item.filterText) - : FuzzyMatcher::kMinScore; + item.score_ = matcher->Match(*item.filterText); } items.erase(std::remove_if(items.begin(), items.end(), - [](const lsCompletionItem& item) { - return item.score_ <= FuzzyMatcher::kMinScore; + [&matcher](const lsCompletionItem& item) { + return item.score_ <= matcher->MinScore(); }), items.end()); std::sort(items.begin(), items.end(), diff --git a/src/prefix_match.cc b/src/prefix_match.cc new file mode 100644 index 000000000..7b1f86d03 --- /dev/null +++ b/src/prefix_match.cc @@ -0,0 +1,22 @@ +#include "prefix_match.h" + +#include "utils.h" +#include +#include + +PrefixMatcher::PrefixMatcher(std::string_view pattern, bool case_sensitive) { + original_pattern = pattern; + this->case_sensitive = case_sensitive; +} + +int PrefixMatcher::Match(std::string_view text) { + if (case_sensitive) { + return ::StartsWith(text, original_pattern) ? 1 : MinScore(); + } else { + return ::StartsWithIgnoreCase(text, original_pattern) ? 1 : MinScore(); + } +} + +int PrefixMatcher::MinScore() const { + return -1; +} diff --git a/src/prefix_match.h b/src/prefix_match.h new file mode 100644 index 000000000..b23503e7e --- /dev/null +++ b/src/prefix_match.h @@ -0,0 +1,14 @@ +#pragma once + +#include "completion_matcher.h" + +class PrefixMatcher : public CompletionMatcher { + public: + PrefixMatcher(std::string_view pattern, bool case_sensitive); + int Match(std::string_view text) override; + int MinScore() const override; + + private: + std::string_view original_pattern; + bool case_sensitive; +}; diff --git a/src/utils.cc b/src/utils.cc index 1a07b980a..575a19b8d 100644 --- a/src/utils.cc +++ b/src/utils.cc @@ -79,6 +79,12 @@ bool StartsWith(std::string_view value, std::string_view start) { return std::equal(start.begin(), start.end(), value.begin()); } +bool StartsWithIgnoreCase(std::string_view value, std::string_view start) { + if (start.size() > value.size()) + return false; + return std::equal(start.begin(), start.end(), value.begin(), [](char ch1, char ch2) { return std::toupper(ch1) == std::toupper(ch2); }); +} + bool AnyStartsWith(const std::vector& values, const std::string& start) { return std::any_of( diff --git a/src/utils.h b/src/utils.h index d0f044f99..62a809292 100644 --- a/src/utils.h +++ b/src/utils.h @@ -24,6 +24,7 @@ uint64_t HashUsr(std::string_view s); // Returns true if |value| starts/ends with |start| or |ending|. bool StartsWith(std::string_view value, std::string_view start); +bool StartsWithIgnoreCase(std::string_view value, std::string_view start); bool EndsWith(std::string_view value, std::string_view ending); bool AnyStartsWith(const std::vector& values, const std::string& start); @@ -146,4 +147,4 @@ bool IsWindowsAbsolutePath(const std::string& path); bool IsDirectory(const std::string& path); -size_t HashArguments(const std::vector& args); \ No newline at end of file +size_t HashArguments(const std::vector& args); diff --git a/third_party/.clang-format b/third_party/.clang-format new file mode 100644 index 000000000..ef2ae21fa --- /dev/null +++ b/third_party/.clang-format @@ -0,0 +1,4 @@ +--- +DisableFormat: true +SortIncludes: false +... diff --git a/third_party/lib_fts b/third_party/lib_fts new file mode 160000 index 000000000..80f3f8c52 --- /dev/null +++ b/third_party/lib_fts @@ -0,0 +1 @@ +Subproject commit 80f3f8c52db53428247e741b9efe2cde9667050c