From 132c9e5ce2a070501e74d31b8ae39eeb6a514d7a Mon Sep 17 00:00:00 2001 From: Adam Johnston Date: Sun, 8 Jun 2025 13:19:19 -0700 Subject: [PATCH] Expose fuzzy search classes in public api --- core/register_core_types.cpp | 4 + core/string/fuzzy_search.cpp | 274 ++++++++++++++++-------- core/string/fuzzy_search.h | 97 +++++++-- doc/classes/FuzzySearch.xml | 75 +++++++ doc/classes/FuzzySearchMatch.xml | 29 +++ editor/gui/editor_quick_open_dialog.cpp | 38 ++-- editor/gui/editor_quick_open_dialog.h | 10 +- editor/script/script_editor_plugin.cpp | 20 +- scene/gui/popup_menu.cpp | 13 +- tests/core/string/test_fuzzy_search.cpp | 6 +- 10 files changed, 411 insertions(+), 155 deletions(-) create mode 100644 doc/classes/FuzzySearch.xml create mode 100644 doc/classes/FuzzySearchMatch.xml diff --git a/core/register_core_types.cpp b/core/register_core_types.cpp index 80696814aa..1d522312c5 100644 --- a/core/register_core_types.cpp +++ b/core/register_core_types.cpp @@ -85,6 +85,7 @@ #include "core/os/main_loop.h" #include "core/os/os.h" #include "core/os/time.h" +#include "core/string/fuzzy_search.h" #include "core/string/optimized_translation.h" #include "core/string/translation.h" #include "core/string/translation_server.h" @@ -302,6 +303,9 @@ void register_core_types() { GDREGISTER_CLASS(EngineProfiler); + GDREGISTER_CLASS(FuzzySearch); + GDREGISTER_CLASS(FuzzySearchMatch); + resource_uid = memnew(ResourceUID); gdextension_manager = memnew(GDExtensionManager); diff --git a/core/string/fuzzy_search.cpp b/core/string/fuzzy_search.cpp index 418518d87e..a55f14df82 100644 --- a/core/string/fuzzy_search.cpp +++ b/core/string/fuzzy_search.cpp @@ -30,9 +30,10 @@ #include "fuzzy_search.h" -constexpr float cull_factor = 0.1f; -constexpr float cull_cutoff = 30.0f; -const String boundary_chars = "/\\-_."; +#include "core/object/class_db.h" +#include "core/variant/typed_array.h" + +static const String boundary_chars = "/\\-_. "; static bool _is_valid_interval(const Vector2i &p_interval) { // Empty intervals are represented as (-1, -1). @@ -117,7 +118,7 @@ bool FuzzyTokenMatch::intersects(const Vector2i &p_other_interval) const { return interval.y >= p_other_interval.x && interval.x <= p_other_interval.y; } -bool FuzzySearchResult::can_add_token_match(const FuzzyTokenMatch &p_match) const { +bool FuzzySearchMatch::_can_add_token_match(const FuzzyTokenMatch &p_match) const { if (p_match.get_miss_count() > miss_budget) { return false; } @@ -148,7 +149,7 @@ bool FuzzyTokenMatch::is_case_insensitive(const String &p_original, const String return false; } -void FuzzySearchResult::score_token_match(FuzzyTokenMatch &p_match, bool p_case_insensitive) const { +void FuzzySearchMatch::_score_token_match(FuzzyTokenMatch &p_match, bool p_case_insensitive) const { // This can always be tweaked more. The intuition is that exact matches should almost always // be prioritized over broken up matches, and other criteria more or less act as tie breakers. @@ -173,44 +174,80 @@ void FuzzySearchResult::score_token_match(FuzzyTokenMatch &p_match, bool p_case_ } } -void FuzzySearchResult::maybe_apply_score_bonus() { +void FuzzySearchMatch::_maybe_apply_token_order_score_bonus() { // This adds a small bonus to results which match tokens in the same order they appear in the query. + if (token_matches.is_empty()) { + return; + } + int *token_range_starts = (int *)alloca(sizeof(int) * token_matches.size()); for (const FuzzyTokenMatch &match : token_matches) { token_range_starts[match.token_idx] = match.interval.x; } - int last = token_range_starts[0]; for (int i = 1; i < token_matches.size(); i++) { - if (last > token_range_starts[i]) { + // Individual tokens can match without a range if the missed-character budget allows for it. If + // the i'th token matches in this manner, skip ahead so we check neither (i-1, i) nor (i, i+1). + // It's safe that this skips i=0 since any valid start will be > -1. + if (token_range_starts[i] == -1) { + i++; + continue; + } + if (token_range_starts[i - 1] > token_range_starts[i]) { return; } - last = token_range_starts[i]; } score += 1; } -void FuzzySearchResult::add_token_match(const FuzzyTokenMatch &p_match) { +void FuzzySearchMatch::_add_token_match(const FuzzyTokenMatch &p_match) { score += p_match.score; match_interval = _extend_interval(match_interval, p_match.interval); miss_budget -= p_match.get_miss_count(); token_matches.append(p_match); } -void remove_low_scores(Vector &p_results, float p_cull_score) { +void FuzzySearchMatch::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_target", "target"), &FuzzySearchMatch::set_target); + ClassDB::bind_method(D_METHOD("get_target"), &FuzzySearchMatch::get_target); + + ClassDB::bind_method(D_METHOD("set_score", "score"), &FuzzySearchMatch::set_score); + ClassDB::bind_method(D_METHOD("get_score"), &FuzzySearchMatch::get_score); + + ClassDB::bind_method(D_METHOD("set_original_index", "original_index"), &FuzzySearchMatch::set_original_index); + ClassDB::bind_method(D_METHOD("get_original_index"), &FuzzySearchMatch::get_original_index); + + ClassDB::bind_method(D_METHOD("get_matched_substrings"), &FuzzySearchMatch::get_matched_substrings); + + ADD_PROPERTY(PropertyInfo(Variant::STRING, "target"), "set_target", "get_target"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "score"), "set_score", "get_score"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "original_index"), "set_original_index", "get_original_index"); +} + +TypedArray FuzzySearchMatch::get_matched_substrings() const { + TypedArray substrings; + for (const FuzzyTokenMatch &match : token_matches) { + for (const Vector2i &substring : match.substrings) { + substrings.append(substring); + } + } + return substrings; +} + +static void remove_low_scores(Vector> &p_results, float p_cull_score) { // Removes all results with score < p_cull_score in-place. int i = 0; int j = p_results.size() - 1; - FuzzySearchResult *results = p_results.ptrw(); + Ref *results = p_results.ptrw(); while (true) { // Advances i to an element to remove and j to an element to keep. - while (j >= i && results[j].score < p_cull_score) { + while (j >= i && results[j]->get_score() < p_cull_score) { j--; } - while (i < j && results[i].score >= p_cull_score) { + while (i < j && results[i]->get_score() >= p_cull_score) { i++; } if (i >= j) { @@ -222,60 +259,13 @@ void remove_low_scores(Vector &p_results, float p_cull_score) p_results.resize(j + 1); } -void FuzzySearch::sort_and_filter(Vector &p_results) const { - if (p_results.is_empty()) { - return; - } - - float avg_score = 0; - float max_score = 0; - - for (const FuzzySearchResult &result : p_results) { - avg_score += result.score; - max_score = MAX(max_score, result.score); - } - - // TODO: Tune scoring and culling here to display fewer subsequence soup matches when good matches - // are available. - avg_score /= p_results.size(); - float cull_score = MIN(cull_cutoff, Math::lerp(avg_score, max_score, cull_factor)); - remove_low_scores(p_results, cull_score); - - struct FuzzySearchResultComparator { - bool operator()(const FuzzySearchResult &p_lhs, const FuzzySearchResult &p_rhs) const { - // Sort on (score, length, alphanumeric) to ensure consistent ordering. - if (p_lhs.score == p_rhs.score) { - if (p_lhs.target.length() == p_rhs.target.length()) { - return p_lhs.target < p_rhs.target; - } - return p_lhs.target.length() < p_rhs.target.length(); - } - return p_lhs.score > p_rhs.score; - } - }; - - SortArray sorter; - - if (p_results.size() > max_results) { - sorter.partial_sort(0, p_results.size(), max_results, p_results.ptrw()); - p_results.resize(max_results); - } else { - sorter.sort(p_results.ptrw(), p_results.size()); - } -} - -void FuzzySearch::set_query(const String &p_query) { - set_query(p_query, !p_query.is_lowercase()); -} - -void FuzzySearch::set_query(const String &p_query, bool p_case_sensitive) { - tokens.clear(); - case_sensitive = p_case_sensitive; +Vector FuzzySearch::_get_tokens(const String &p_query) const { + Vector tokens; for (const String &string : p_query.split(" ", false)) { tokens.append({ static_cast(tokens.size()), - p_case_sensitive ? string : string.to_lower(), + case_sensitive ? string : string.to_lower(), }); } @@ -290,12 +280,60 @@ void FuzzySearch::set_query(const String &p_query, bool p_case_sensitive) { // Prioritize matching longer tokens before shorter ones since match overlaps are not accepted. tokens.sort_custom(); + return tokens; } -bool FuzzySearch::search(const String &p_target, FuzzySearchResult &p_result) const { - p_result.target = p_target; - p_result.dir_index = p_target.rfind_char('/'); - p_result.miss_budget = max_misses; +void FuzzySearch::_sort_and_filter(Vector> &p_results) const { + if (p_results.is_empty()) { + return; + } + + if (filter_low_scores) { + float avg_score = 0; + float max_score = 0; + + for (const Ref &result : p_results) { + avg_score += result->get_score(); + max_score = MAX(max_score, result->get_score()); + } + + avg_score /= p_results.size(); + float cull_score = MIN(filter_cutoff, Math::lerp(avg_score, max_score, filter_factor)); + remove_low_scores(p_results, cull_score); + } + + struct FuzzySearchResultComparator { + bool operator()(const Ref &p_lhs, const Ref &p_rhs) const { + // Sort on (score, length, alphanumeric) to ensure consistent ordering. + if (p_lhs->score == p_rhs->score) { + if (p_lhs->target.length() == p_rhs->target.length()) { + return p_lhs->target < p_rhs->target; + } + return p_lhs->target.length() < p_rhs->target.length(); + } + return p_lhs->score > p_rhs->score; + } + }; + + SortArray, FuzzySearchResultComparator> sorter; + + if (p_results.size() > max_results) { + sorter.partial_sort(0, p_results.size(), max_results, p_results.ptrw()); + p_results.resize(max_results); + } else { + sorter.sort(p_results.ptrw(), p_results.size()); + } +} + +void FuzzySearch::set_case_sensitive(bool p_case_sensitive) { + case_sensitive = p_case_sensitive; +} + +bool FuzzySearch::_search_tokens(const Vector &p_tokens, const String &p_target, Ref &r_result) const { + r_result->target = p_target; + r_result->dir_index = p_target.rfind_char('/'); + r_result->miss_budget = max_misses; + r_result->token_matches.reserve(p_tokens.size()); String adjusted_target = case_sensitive ? p_target : p_target.to_lower(); @@ -303,23 +341,23 @@ bool FuzzySearch::search(const String &p_target, FuzzySearchResult &p_result) co // which does not conflict with prior token matches. This is not ensured to find the highest scoring // combination of matches, or necessarily the highest scoring single subsequence, as it only considers // eager subsequences for a given index, and likewise eagerly finds matches for each token in sequence. - for (const FuzzySearchToken &token : tokens) { + for (const FuzzySearchToken &token : p_tokens) { FuzzyTokenMatch best_match; int offset = start_offset; while (true) { FuzzyTokenMatch match; - if (allow_subsequences) { - if (!token.try_fuzzy_match(match, adjusted_target, offset, p_result.miss_budget)) { - break; - } - } else { + if (exact_token_matches) { if (!token.try_exact_match(match, adjusted_target, offset)) { break; } + } else { + if (!token.try_fuzzy_match(match, adjusted_target, offset, r_result->miss_budget)) { + break; + } } - if (p_result.can_add_token_match(match)) { - p_result.score_token_match(match, match.is_case_insensitive(p_target, adjusted_target)); + if (r_result->_can_add_token_match(match)) { + r_result->_score_token_match(match, match.is_case_insensitive(p_target, adjusted_target)); if (best_match.token_idx == -1 || best_match.score < match.score) { best_match = match; } @@ -335,23 +373,89 @@ bool FuzzySearch::search(const String &p_target, FuzzySearchResult &p_result) co return false; } - p_result.add_token_match(best_match); + r_result->_add_token_match(best_match); } - p_result.maybe_apply_score_bonus(); + if (r_result->match_interval.x == -1) { + // Reject matches which rely entirely on misses. + return false; + } + + r_result->_maybe_apply_token_order_score_bonus(); return true; } -void FuzzySearch::search_all(const PackedStringArray &p_targets, Vector &p_results) const { - p_results.clear(); +Ref FuzzySearch::search(const String &p_query, const String &p_target) const { + Ref result; + result.instantiate(); + if (_search_tokens(_get_tokens(p_query), p_target, result)) { + return result; + } + return nullptr; +} + +Vector> FuzzySearch::search_all(const String &p_query, const PackedStringArray &p_targets) const { + Vector> results; + const Vector tokens = _get_tokens(p_query); for (int i = 0; i < p_targets.size(); i++) { - FuzzySearchResult result; - result.original_index = i; - if (search(p_targets[i], result)) { - p_results.append(result); + Ref result; + result.instantiate(); + result->original_index = i; + if (_search_tokens(tokens, p_targets[i], result)) { + results.append(result); } } - sort_and_filter(p_results); + _sort_and_filter(results); + return results; +} + +TypedArray FuzzySearch::_search_all_bind(const String &p_query, const PackedStringArray &p_targets) const { + Vector> results = search_all(p_query, p_targets); + TypedArray wrapped_results; + wrapped_results.reserve(results.size()); + for (Ref &result : results) { + wrapped_results.append(result); + } + + return wrapped_results; +} + +void FuzzySearch::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_start_offset", "start_offset"), &FuzzySearch::set_start_offset); + ClassDB::bind_method(D_METHOD("get_start_offset"), &FuzzySearch::get_start_offset); + + ClassDB::bind_method(D_METHOD("set_max_results", "max_results"), &FuzzySearch::set_max_results); + ClassDB::bind_method(D_METHOD("get_max_results"), &FuzzySearch::get_max_results); + + ClassDB::bind_method(D_METHOD("set_max_misses", "max_misses"), &FuzzySearch::set_max_misses); + ClassDB::bind_method(D_METHOD("get_max_misses"), &FuzzySearch::get_max_misses); + + ClassDB::bind_method(D_METHOD("set_use_exact_tokens", "use_exact_tokens"), &FuzzySearch::set_use_exact_tokens); + ClassDB::bind_method(D_METHOD("get_use_exact_tokens"), &FuzzySearch::get_use_exact_tokens); + + ClassDB::bind_method(D_METHOD("set_case_sensitive", "case_sensitive"), &FuzzySearch::set_case_sensitive); + ClassDB::bind_method(D_METHOD("get_case_sensitive"), &FuzzySearch::get_case_sensitive); + + ClassDB::bind_method(D_METHOD("set_filter_low_scores", "filter_low_scores"), &FuzzySearch::set_filter_low_scores); + ClassDB::bind_method(D_METHOD("get_filter_low_scores"), &FuzzySearch::get_filter_low_scores); + + ClassDB::bind_method(D_METHOD("set_filter_factor", "filter_factor"), &FuzzySearch::set_filter_factor); + ClassDB::bind_method(D_METHOD("get_filter_factor"), &FuzzySearch::get_filter_factor); + + ClassDB::bind_method(D_METHOD("set_filter_cutoff", "filter_cutoff"), &FuzzySearch::set_filter_cutoff); + ClassDB::bind_method(D_METHOD("get_filter_cutoff"), &FuzzySearch::get_filter_cutoff); + + ClassDB::bind_method(D_METHOD("search", "query", "target"), &FuzzySearch::search); + ClassDB::bind_method(D_METHOD("search_all", "query", "targets"), &FuzzySearch::_search_all_bind); + + ADD_PROPERTY(PropertyInfo(Variant::INT, "start_offset"), "set_start_offset", "get_start_offset"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "max_results"), "set_max_results", "get_max_results"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "max_misses"), "set_max_misses", "get_max_misses"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_exact_tokens"), "set_use_exact_tokens", "get_use_exact_tokens"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "case_sensitive"), "set_case_sensitive", "get_case_sensitive"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "filter_low_scores"), "set_filter_low_scores", "get_filter_low_scores"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "filter_factor"), "set_filter_factor", "get_filter_factor"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "filter_cutoff"), "set_filter_cutoff", "get_filter_cutoff"); } diff --git a/core/string/fuzzy_search.h b/core/string/fuzzy_search.h index d1cd1dc257..873d970f0e 100644 --- a/core/string/fuzzy_search.h +++ b/core/string/fuzzy_search.h @@ -30,6 +30,7 @@ #pragma once +#include "core/object/ref_counted.h" #include "core/variant/variant.h" class FuzzyTokenMatch; @@ -44,7 +45,7 @@ struct FuzzySearchToken { class FuzzyTokenMatch { friend struct FuzzySearchToken; - friend class FuzzySearchResult; + friend class FuzzySearchMatch; friend class FuzzySearch; int matched_length = 0; @@ -62,39 +63,93 @@ public: Vector substrings; // x is start index, y is length. }; -class FuzzySearchResult { +class FuzzySearchMatch : public RefCounted { + GDCLASS(FuzzySearchMatch, RefCounted) + friend class FuzzySearch; - int miss_budget = 0; - Vector2i match_interval = Vector2i(-1, -1); - - bool can_add_token_match(const FuzzyTokenMatch &p_match) const; - void score_token_match(FuzzyTokenMatch &p_match, bool p_case_insensitive) const; - void add_token_match(const FuzzyTokenMatch &p_match); - void maybe_apply_score_bonus(); - -public: String target; int score = 0; int original_index = -1; int dir_index = -1; Vector token_matches; -}; + int miss_budget = 0; + Vector2i match_interval = Vector2i(-1, -1); -class FuzzySearch { - Vector tokens; + bool _can_add_token_match(const FuzzyTokenMatch &p_match) const; + void _score_token_match(FuzzyTokenMatch &p_match, bool p_case_insensitive) const; + void _add_token_match(const FuzzyTokenMatch &p_match); + void _maybe_apply_token_order_score_bonus(); - bool case_sensitive = false; - void sort_and_filter(Vector &p_results) const; +protected: + static void _bind_methods(); public: + void set_target(const String &p_target) { target = p_target; } + String get_target() const { return target; } + + void set_score(int p_score) { score = p_score; } + int get_score() const { return score; } + + void set_original_index(int p_original_index) { original_index = p_original_index; } + int get_original_index() const { return original_index; } + + void set_dir_index(int p_dir_index) { dir_index = p_dir_index; } + int get_dir_index() const { return dir_index; } + + Vector get_token_matches() { return token_matches; } + + TypedArray get_matched_substrings() const; +}; + +class FuzzySearch : public RefCounted { + GDCLASS(FuzzySearch, RefCounted) + int start_offset = 0; int max_results = 100; int max_misses = 2; - bool allow_subsequences = true; + bool exact_token_matches = false; + bool case_sensitive = false; + bool filter_low_scores = true; + float filter_factor = 0.1f; + float filter_cutoff = 30.0f; - void set_query(const String &p_query); - void set_query(const String &p_query, bool p_case_sensitive); - bool search(const String &p_target, FuzzySearchResult &p_result) const; - void search_all(const PackedStringArray &p_targets, Vector &p_results) const; + Vector _get_tokens(const String &p_query) const; + void _sort_and_filter(Vector> &p_results) const; + bool _search_tokens(const Vector &p_tokens, const String &p_target, Ref &r_result) const; + TypedArray _search_all_bind(const String &p_query, const PackedStringArray &p_targets) const; + +protected: + static void _bind_methods(); + +public: + void set_start_offset(int p_offset) { start_offset = p_offset; } + int get_start_offset() const { return start_offset; } + + void set_max_results(int p_max_results) { max_results = p_max_results; } + int get_max_results() const { return max_results; } + + void set_max_misses(int p_max_misses) { max_misses = p_max_misses; } + int get_max_misses() const { return max_misses; } + + void set_use_exact_tokens(bool p_use_exact_tokens) { exact_token_matches = p_use_exact_tokens; } + bool get_use_exact_tokens() const { return exact_token_matches; } + + void set_case_sensitive(bool p_case_sensitive); + bool get_case_sensitive() const { return case_sensitive; } + + void set_filter_low_scores(bool p_filter_low_scores) { filter_low_scores = p_filter_low_scores; } + bool get_filter_low_scores() const { return filter_low_scores; } + + void set_filter_factor(float p_filter_factor) { + ERR_FAIL_COND_MSG(p_filter_factor < 0.0f || p_filter_factor > 1.0f, "filter_factor should be in the range [0, 1]"); + filter_factor = p_filter_factor; + } + float get_filter_factor() const { return filter_factor; } + + void set_filter_cutoff(float p_filter_cutoff) { filter_cutoff = p_filter_cutoff; } + float get_filter_cutoff() const { return filter_cutoff; } + + Ref search(const String &p_query, const String &p_target) const; + Vector> search_all(const String &p_query, const PackedStringArray &p_targets) const; }; diff --git a/doc/classes/FuzzySearch.xml b/doc/classes/FuzzySearch.xml new file mode 100644 index 0000000000..fa8b3e4ff3 --- /dev/null +++ b/doc/classes/FuzzySearch.xml @@ -0,0 +1,75 @@ + + + + Provides fuzzy string searching and matching capabilities. + + + The fuzzy search algorithm is designed to find target strings which mostly match a query string while allowing for breaks, typos, and out of order matches. + [codeblocks] + [gdscript] + var items := ["Potion of Healing", "Greater Health Potion", "Poison Vial"] + var fuzzy := FuzzySearch.new() + + for result in fuzzy.search_all("health potion", items): + # Prints "Greater Health Potion", "Potion of Healing" + print(result.target) + [/gdscript] + [csharp] + string[] items = ["Potion of Healing", "Greater Health Potion", "Poison Vial"]; + FuzzySearch fuzzy = new(); + + foreach (var result in fuzzy.SearchAll("health potion", items)) + { + // Prints "Greater Health Potion", "Potion of Healing" + GD.Print(result.Target); + } + [/csharp] + [/codeblocks] + + + + + + + + + + Searches the [param target] for [param query], returning a [FuzzySearchMatch] instance on success or [code]null[/code] otherwise. + + + + + + + + Searches all of [param targets] for [param query] and returns up to the top [member max_results] scoring values. The results are sorted by score in descending order. Low quality results are removed if [member filter_low_scores] is [code]true[/code]. + + + + + + Whether the query character casing should be matched exactly or not. + + + Minimum score for filtering results returned by [method search_all]. + + + Biases the filtering cutoff score between the average score and max score. Value should be between [code]0[/code] and [code]1[/code]. + + + If [code]true[/code], lower quality matches are not returned by [method search_all]. The default filtering behavior is tuned to keep exact matches and reject significantly broken up matches. Setting this to [code]false[/code] can potentially improve results when searching a small number of items. + + + Maximum number of non-matched characters in the query before skipping a target as non-matching. This option is ignored if [member use_exact_tokens] is [code]true[/code]. + + + Maximum number of results which can be returned by [method search_all]. + + + Number of leading characters to omit from matching. For example, this can be used to skip a common prefix such as [code]res://[/code]. + + + If [code]true[/code], only targets which contain each token as a non-overlapping substring are returned. Gaps and missed characters are not considered valid matches, but [member case_sensitive] is still used. + + + diff --git a/doc/classes/FuzzySearchMatch.xml b/doc/classes/FuzzySearchMatch.xml new file mode 100644 index 0000000000..ac1287b399 --- /dev/null +++ b/doc/classes/FuzzySearchMatch.xml @@ -0,0 +1,29 @@ + + + + Result returned by [FuzzySearch] containing match information. + + + + + + + + + + Returns each matched substring interval of [member target]. The [member Vector2i.x] component is the start of the interval and the [member Vector2i.y] component is the length, corresponding to the arguments for [method String.substr]. + + + + + + The original index of [member target] in the provided array of targets when using [method FuzzySearch.search_all] otherwise [code]-1[/code]. + + + Match score determined by [FuzzySearch]. Scores are not normalized and generally should not be directly compared between different queries. + + + Target string which was matched against. + + + diff --git a/editor/gui/editor_quick_open_dialog.cpp b/editor/gui/editor_quick_open_dialog.cpp index c93a45b0bb..6433a7e6b5 100644 --- a/editor/gui/editor_quick_open_dialog.cpp +++ b/editor/gui/editor_quick_open_dialog.cpp @@ -718,15 +718,15 @@ QuickOpenResultCandidate QuickOpenResultCandidate::from_uid(const ResourceUID::I return candidate; } -QuickOpenResultCandidate QuickOpenResultCandidate::from_result(const FuzzySearchResult &p_result, bool &r_success) { - ResourceUID::ID uid = EditorFileSystem::get_singleton()->get_file_uid(p_result.target); +QuickOpenResultCandidate QuickOpenResultCandidate::from_result(Ref p_result, bool &r_success) { + ResourceUID::ID uid = EditorFileSystem::get_singleton()->get_file_uid(p_result->get_target()); QuickOpenResultCandidate candidate = from_uid(uid, r_success); if (!r_success) { return QuickOpenResultCandidate(); } - candidate.result = &p_result; + candidate.result = p_result; return candidate; } @@ -806,15 +806,15 @@ void QuickOpenResultContainer::_use_default_candidates() { } } -void QuickOpenResultContainer::_update_fuzzy_search_results() { +Vector> QuickOpenResultContainer::_get_fuzzy_search_results() { FuzzySearch fuzzy_search; - fuzzy_search.start_offset = 6; // Don't match against "res://" at the start of each filepath. - fuzzy_search.set_query(query); - fuzzy_search.max_results = max_total_results; + fuzzy_search.set_start_offset(6); // Don't match against "res://" at the start of each filepath. + fuzzy_search.set_case_sensitive(!query.is_lowercase()); + fuzzy_search.set_max_results(max_total_results); bool fuzzy_matching = EDITOR_GET("filesystem/quick_open_dialog/enable_fuzzy_matching"); int max_misses = EDITOR_GET("filesystem/quick_open_dialog/max_fuzzy_misses"); - fuzzy_search.allow_subsequences = fuzzy_matching; - fuzzy_search.max_misses = fuzzy_matching ? max_misses : 0; + fuzzy_search.set_use_exact_tokens(!fuzzy_matching); + fuzzy_search.set_max_misses(fuzzy_matching ? max_misses : 0); PackedStringArray paths; paths.reserve_exact(uids.size()); @@ -823,13 +823,11 @@ void QuickOpenResultContainer::_update_fuzzy_search_results() { paths.push_back(ResourceUID::get_singleton()->get_id_path(uid)); } - fuzzy_search.search_all(paths, search_results); + return fuzzy_search.search_all(query, paths); } void QuickOpenResultContainer::_score_and_sort_candidates() { - _update_fuzzy_search_results(); - - for (const FuzzySearchResult &result : search_results) { + for (const Ref &result : _get_fuzzy_search_results()) { bool success; QuickOpenResultCandidate candidate = QuickOpenResultCandidate::from_result(result, success); if (!success) { @@ -1361,11 +1359,11 @@ void QuickOpenResultListItem::set_content(const QuickOpenResultCandidate &p_cand name->reset_highlights(); path->reset_highlights(); - if (p_highlight && p_candidate.result != nullptr) { - for (const FuzzyTokenMatch &match : p_candidate.result->token_matches) { + if (p_highlight && p_candidate.result.is_valid()) { + for (const FuzzyTokenMatch &match : p_candidate.result->get_token_matches()) { for (const Vector2i &interval : match.substrings) { - path->add_highlight(_get_path_interval(interval, p_candidate.result->dir_index)); - name->add_highlight(_get_name_interval(interval, p_candidate.result->dir_index)); + path->add_highlight(_get_path_interval(interval, p_candidate.result->get_dir_index())); + name->add_highlight(_get_name_interval(interval, p_candidate.result->get_dir_index())); } } } @@ -1433,10 +1431,10 @@ void QuickOpenResultGridItem::set_content(const QuickOpenResultCandidate &p_cand name->set_tooltip_text(file_path); name->reset_highlights(); - if (p_highlight && p_candidate.result != nullptr) { - for (const FuzzyTokenMatch &match : p_candidate.result->token_matches) { + if (p_highlight && p_candidate.result.is_valid()) { + for (const FuzzyTokenMatch &match : p_candidate.result->get_token_matches()) { for (const Vector2i &interval : match.substrings) { - name->add_highlight(_get_name_interval(interval, p_candidate.result->dir_index)); + name->add_highlight(_get_name_interval(interval, p_candidate.result->get_dir_index())); } } } diff --git a/editor/gui/editor_quick_open_dialog.h b/editor/gui/editor_quick_open_dialog.h index ea5f7ff944..800bee71c3 100644 --- a/editor/gui/editor_quick_open_dialog.h +++ b/editor/gui/editor_quick_open_dialog.h @@ -30,6 +30,7 @@ #pragma once +#include "core/string/fuzzy_search.h" #include "core/templates/a_hash_map.h" #include "scene/gui/dialogs.h" #include "scene/gui/margin_container.h" @@ -50,8 +51,6 @@ class Texture2D; class TextureRect; class VBoxContainer; -class FuzzySearchResult; - class QuickOpenResultItem; enum class QuickOpenDisplayMode { @@ -62,10 +61,10 @@ enum class QuickOpenDisplayMode { struct QuickOpenResultCandidate { ResourceUID::ID uid; Ref thumbnail; - const FuzzySearchResult *result = nullptr; + Ref result; static QuickOpenResultCandidate from_uid(const ResourceUID::ID &p_uid, bool &r_success); - static QuickOpenResultCandidate from_result(const FuzzySearchResult &p_result, bool &r_success); + static QuickOpenResultCandidate from_result(const Ref p_result, bool &r_success); }; class HighlightedLabel : public Label { @@ -116,7 +115,6 @@ protected: private: static constexpr int MAX_HISTORY_SIZE = 20; - Vector search_results; Vector base_types; LocalVector uids; AHashMap filetypes; @@ -163,7 +161,7 @@ private: Vector *_get_history(); void _add_candidate(QuickOpenResultCandidate &p_candidate); - void _update_fuzzy_search_results(); + Vector> _get_fuzzy_search_results(); void _use_default_candidates(); void _score_and_sort_candidates(); void _update_result_items(int p_new_visible_results_count, int p_new_selection_index); diff --git a/editor/script/script_editor_plugin.cpp b/editor/script/script_editor_plugin.cpp index 05fa3494f0..e312c66045 100644 --- a/editor/script/script_editor_plugin.cpp +++ b/editor/script/script_editor_plugin.cpp @@ -1852,14 +1852,13 @@ void ScriptEditor::_update_members_overview() { search_names.append(functions[i].get_slicec(':', 0)); } - Vector results; FuzzySearch fuzzy; - fuzzy.set_query(filter, false); - fuzzy.search_all(search_names, results); + fuzzy.set_case_sensitive(false); + Vector> results = fuzzy.search_all(filter, search_names); - for (const FuzzySearchResult &res : results) { - String name = functions[res.original_index].get_slicec(':', 0); - int line = functions[res.original_index].get_slicec(':', 1).to_int() - 1; + for (const Ref &res : results) { + String name = functions[res->get_original_index()].get_slicec(':', 0); + int line = functions[res->get_original_index()].get_slicec(':', 1).to_int() - 1; members_overview->add_item(name); members_overview->set_item_metadata(-1, line); } @@ -2122,13 +2121,12 @@ void ScriptEditor::_update_script_names() { search_names.append(sedata[i].name); } - Vector results; FuzzySearch fuzzy; - fuzzy.set_query(filter, false); - fuzzy.search_all(search_names, results); + fuzzy.set_case_sensitive(false); + Vector> results = fuzzy.search_all(filter, search_names); - for (const FuzzySearchResult &res : results) { - sedata_filtered.push_back(sedata[res.original_index]); + for (const Ref &res : results) { + sedata_filtered.push_back(sedata[res->get_original_index()]); } } diff --git a/scene/gui/popup_menu.cpp b/scene/gui/popup_menu.cpp index 7b030aac0a..2fe492cf20 100644 --- a/scene/gui/popup_menu.cpp +++ b/scene/gui/popup_menu.cpp @@ -1180,16 +1180,13 @@ void PopupMenu::_filter_items(const String &p_query) { } } - Vector results; FuzzySearch fuzzy; - fuzzy.max_results = search_candidates.size(); - fuzzy.max_misses = search_bar_fuzzy_search_max_misses; - fuzzy.allow_subsequences = search_bar_fuzzy_search_enabled; - fuzzy.set_query(p_query, false); - fuzzy.search_all(search_candidates, results); + fuzzy.set_max_results(search_candidates.size()); + fuzzy.set_max_misses(search_bar_fuzzy_search_max_misses); + fuzzy.set_use_exact_tokens(!search_bar_fuzzy_search_enabled); - for (const FuzzySearchResult &result : results) { - PopupMenu::Item &item = items.write[search_candidate_to_item[result.original_index]]; + for (const Ref &result : fuzzy.search_all(p_query, search_candidates)) { + PopupMenu::Item &item = items.write[search_candidate_to_item[result->get_original_index()]]; item.visible = true; if (item.submenu) { for (PopupMenu::Item &submenu_item : item.submenu->items) { diff --git a/tests/core/string/test_fuzzy_search.cpp b/tests/core/string/test_fuzzy_search.cpp index 9d1536ef5b..0abd76fd9c 100644 --- a/tests/core/string/test_fuzzy_search.cpp +++ b/tests/core/string/test_fuzzy_search.cpp @@ -69,14 +69,12 @@ Vector load_test_data() { TEST_CASE("[FuzzySearch] Test fuzzy search results") { FuzzySearch search; - Vector results; Vector targets = load_test_data(); for (FuzzySearchTestCase test_case : test_cases) { - search.set_query(test_case.query); - search.search_all(targets, results); + Vector> results = search.search_all(test_case.query, targets); CHECK_GT(results.size(), 0); - CHECK_EQ(results[0].target, test_case.expected); + CHECK_EQ(results[0]->get_target(), test_case.expected); } }