diff options
-rw-r--r-- | core/string/fuzzy_search.cpp | 349 | ||||
-rw-r--r-- | core/string/fuzzy_search.h | 101 | ||||
-rw-r--r-- | core/string/ustring.cpp | 15 | ||||
-rw-r--r-- | core/string/ustring.h | 4 | ||||
-rw-r--r-- | doc/classes/EditorSettings.xml | 12 | ||||
-rw-r--r-- | editor/editor_settings.cpp | 4 | ||||
-rw-r--r-- | editor/gui/editor_quick_open_dialog.cpp | 578 | ||||
-rw-r--r-- | editor/gui/editor_quick_open_dialog.h | 78 | ||||
-rw-r--r-- | scene/gui/label.cpp | 344 | ||||
-rw-r--r-- | scene/gui/label.h | 5 | ||||
-rw-r--r-- | tests/core/string/test_fuzzy_search.h | 83 | ||||
-rw-r--r-- | tests/core/string/test_string.h | 19 | ||||
-rw-r--r-- | tests/data/fuzzy_search/project_dir_tree.txt | 999 | ||||
-rw-r--r-- | tests/test_main.cpp | 1 |
14 files changed, 2094 insertions, 498 deletions
diff --git a/core/string/fuzzy_search.cpp b/core/string/fuzzy_search.cpp new file mode 100644 index 0000000000..2fd0d3995e --- /dev/null +++ b/core/string/fuzzy_search.cpp @@ -0,0 +1,349 @@ +/**************************************************************************/ +/* fuzzy_search.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "fuzzy_search.h" + +constexpr float cull_factor = 0.1f; +constexpr float cull_cutoff = 30.0f; +const String boundary_chars = "/\\-_."; + +static bool _is_valid_interval(const Vector2i &p_interval) { + // Empty intervals are represented as (-1, -1). + return p_interval.x >= 0 && p_interval.y >= p_interval.x; +} + +static Vector2i _extend_interval(const Vector2i &p_a, const Vector2i &p_b) { + if (!_is_valid_interval(p_a)) { + return p_b; + } + if (!_is_valid_interval(p_b)) { + return p_a; + } + return Vector2i(MIN(p_a.x, p_b.x), MAX(p_a.y, p_b.y)); +} + +static bool _is_word_boundary(const String &p_str, int p_index) { + if (p_index == -1 || p_index == p_str.size()) { + return true; + } + return boundary_chars.find_char(p_str[p_index]) != -1; +} + +bool FuzzySearchToken::try_exact_match(FuzzyTokenMatch &p_match, const String &p_target, int p_offset) const { + p_match.token_idx = idx; + p_match.token_length = string.length(); + int match_idx = p_target.find(string, p_offset); + if (match_idx == -1) { + return false; + } + p_match.add_substring(match_idx, string.length()); + return true; +} + +bool FuzzySearchToken::try_fuzzy_match(FuzzyTokenMatch &p_match, const String &p_target, int p_offset, int p_miss_budget) const { + p_match.token_idx = idx; + p_match.token_length = string.length(); + int run_start = -1; + int run_len = 0; + + // Search for the subsequence p_token in p_target starting from p_offset, recording each substring for + // later scoring and display. + for (int i = 0; i < string.length(); i++) { + int new_offset = p_target.find_char(string[i], p_offset); + if (new_offset < 0) { + p_miss_budget--; + if (p_miss_budget < 0) { + return false; + } + } else { + if (run_start == -1 || p_offset != new_offset) { + if (run_start != -1) { + p_match.add_substring(run_start, run_len); + } + run_start = new_offset; + run_len = 1; + } else { + run_len += 1; + } + p_offset = new_offset + 1; + } + } + + if (run_start != -1) { + p_match.add_substring(run_start, run_len); + } + + return true; +} + +void FuzzyTokenMatch::add_substring(int p_substring_start, int p_substring_length) { + substrings.append(Vector2i(p_substring_start, p_substring_length)); + matched_length += p_substring_length; + Vector2i substring_interval = { p_substring_start, p_substring_start + p_substring_length - 1 }; + interval = _extend_interval(interval, substring_interval); +} + +bool FuzzyTokenMatch::intersects(const Vector2i &p_other_interval) const { + if (!_is_valid_interval(interval) || !_is_valid_interval(p_other_interval)) { + return false; + } + return interval.y >= p_other_interval.x && interval.x <= p_other_interval.y; +} + +bool FuzzySearchResult::can_add_token_match(const FuzzyTokenMatch &p_match) const { + if (p_match.get_miss_count() > miss_budget) { + return false; + } + + if (p_match.intersects(match_interval)) { + if (token_matches.size() == 1) { + return false; + } + for (const FuzzyTokenMatch &existing_match : token_matches) { + if (existing_match.intersects(p_match.interval)) { + return false; + } + } + } + + return true; +} + +bool FuzzyTokenMatch::is_case_insensitive(const String &p_original, const String &p_adjusted) const { + for (const Vector2i &substr : substrings) { + const int end = substr.x + substr.y; + for (int i = substr.x; i < end; i++) { + if (p_original[i] != p_adjusted[i]) { + return true; + } + } + } + return false; +} + +void FuzzySearchResult::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. + + p_match.score = -20 * p_match.get_miss_count() - (p_case_insensitive ? 3 : 0); + + for (const Vector2i &substring : p_match.substrings) { + // Score longer substrings higher than short substrings. + int substring_score = substring.y * substring.y; + // Score matches deeper in path higher than shallower matches + if (substring.x > dir_index) { + substring_score *= 2; + } + // Score matches on a word boundary higher than matches within a word + if (_is_word_boundary(target, substring.x - 1) || _is_word_boundary(target, substring.x + substring.y)) { + substring_score += 4; + } + // Score exact query matches higher than non-compact subsequence matches + if (substring.y == p_match.token_length) { + substring_score += 100; + } + p_match.score += substring_score; + } +} + +void FuzzySearchResult::maybe_apply_score_bonus() { + // This adds a small bonus to results which match tokens in the same order they appear in the query. + 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]) { + return; + } + last = token_range_starts[i]; + } + + score += 1; +} + +void FuzzySearchResult::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<FuzzySearchResult> &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(); + + 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) { + j--; + } + while (i < j && results[i].score >= p_cull_score) { + i++; + } + if (i >= j) { + break; + } + results[i++] = results[j--]; + } + + p_results.resize(j + 1); +} + +void FuzzySearch::sort_and_filter(Vector<FuzzySearchResult> &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<FuzzySearchResult, 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_query(const String &p_query) { + tokens.clear(); + for (const String &string : p_query.split(" ", false)) { + tokens.append({ static_cast<int>(tokens.size()), string }); + } + + case_sensitive = !p_query.is_lowercase(); + + struct TokenComparator { + bool operator()(const FuzzySearchToken &A, const FuzzySearchToken &B) const { + if (A.string.length() == B.string.length()) { + return A.idx < B.idx; + } + return A.string.length() > B.string.length(); + } + }; + + // Prioritize matching longer tokens before shorter ones since match overlaps are not accepted. + tokens.sort_custom<TokenComparator>(); +} + +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; + + String adjusted_target = case_sensitive ? p_target : p_target.to_lower(); + + // For each token, eagerly generate subsequences starting from index 0 and keep the best scoring one + // 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) { + 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 (!token.try_exact_match(match, adjusted_target, offset)) { + break; + } + } + if (p_result.can_add_token_match(match)) { + p_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; + } + } + if (_is_valid_interval(match.interval)) { + offset = match.interval.x + 1; + } else { + break; + } + } + + if (best_match.token_idx == -1) { + return false; + } + + p_result.add_token_match(best_match); + } + + p_result.maybe_apply_score_bonus(); + return true; +} + +void FuzzySearch::search_all(const PackedStringArray &p_targets, Vector<FuzzySearchResult> &p_results) const { + p_results.clear(); + + for (const String &target : p_targets) { + FuzzySearchResult result; + if (search(target, result)) { + p_results.append(result); + } + } + + sort_and_filter(p_results); +} diff --git a/core/string/fuzzy_search.h b/core/string/fuzzy_search.h new file mode 100644 index 0000000000..5d8ed813c7 --- /dev/null +++ b/core/string/fuzzy_search.h @@ -0,0 +1,101 @@ +/**************************************************************************/ +/* fuzzy_search.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef FUZZY_SEARCH_H +#define FUZZY_SEARCH_H + +#include "core/variant/variant.h" + +class FuzzyTokenMatch; + +struct FuzzySearchToken { + int idx = -1; + String string; + + bool try_exact_match(FuzzyTokenMatch &p_match, const String &p_target, int p_offset) const; + bool try_fuzzy_match(FuzzyTokenMatch &p_match, const String &p_target, int p_offset, int p_miss_budget) const; +}; + +class FuzzyTokenMatch { + friend struct FuzzySearchToken; + friend class FuzzySearchResult; + friend class FuzzySearch; + + int matched_length = 0; + int token_length = 0; + int token_idx = -1; + Vector2i interval = Vector2i(-1, -1); // x and y are both inclusive indices. + + void add_substring(int p_substring_start, int p_substring_length); + bool intersects(const Vector2i &p_other_interval) const; + bool is_case_insensitive(const String &p_original, const String &p_adjusted) const; + int get_miss_count() const { return token_length - matched_length; } + +public: + int score = 0; + Vector<Vector2i> substrings; // x is start index, y is length. +}; + +class FuzzySearchResult { + 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 dir_index = -1; + Vector<FuzzyTokenMatch> token_matches; +}; + +class FuzzySearch { + Vector<FuzzySearchToken> tokens; + + void sort_and_filter(Vector<FuzzySearchResult> &p_results) const; + +public: + int start_offset = 0; + bool case_sensitive = false; + int max_results = 100; + int max_misses = 2; + bool allow_subsequences = true; + + void set_query(const String &p_query); + bool search(const String &p_target, FuzzySearchResult &p_result) const; + void search_all(const PackedStringArray &p_targets, Vector<FuzzySearchResult> &p_results) const; +}; + +#endif // FUZZY_SEARCH_H diff --git a/core/string/ustring.cpp b/core/string/ustring.cpp index a78f0ff5ff..521dfe0b8c 100644 --- a/core/string/ustring.cpp +++ b/core/string/ustring.cpp @@ -3387,7 +3387,7 @@ int String::find(const char *p_str, int p_from) const { return -1; } -int String::find_char(const char32_t &p_char, int p_from) const { +int String::find_char(char32_t p_char, int p_from) const { return _cowdata.find(p_char, p_from); } @@ -3624,6 +3624,10 @@ int String::rfind(const char *p_str, int p_from) const { return -1; } +int String::rfind_char(char32_t p_char, int p_from) const { + return _cowdata.rfind(p_char, p_from); +} + int String::rfindn(const String &p_str, int p_from) const { // establish a limit int limit = length() - p_str.length(); @@ -3837,6 +3841,15 @@ bool String::is_quoted() const { return is_enclosed_in("\"") || is_enclosed_in("'"); } +bool String::is_lowercase() const { + for (const char32_t *str = &operator[](0); *str; str++) { + if (is_unicode_upper_case(*str)) { + return false; + } + } + return true; +} + int String::_count(const String &p_string, int p_from, int p_to, bool p_case_insensitive) const { if (p_string.is_empty()) { return 0; diff --git a/core/string/ustring.h b/core/string/ustring.h index 11d187beb4..d6e563223a 100644 --- a/core/string/ustring.h +++ b/core/string/ustring.h @@ -287,11 +287,12 @@ public: String substr(int p_from, int p_chars = -1) const; int find(const String &p_str, int p_from = 0) const; ///< return <0 if failed int find(const char *p_str, int p_from = 0) const; ///< return <0 if failed - int find_char(const char32_t &p_char, int p_from = 0) const; ///< return <0 if failed + int find_char(char32_t p_char, int p_from = 0) const; ///< return <0 if failed int findn(const String &p_str, int p_from = 0) const; ///< return <0 if failed, case insensitive int findn(const char *p_str, int p_from = 0) const; ///< return <0 if failed int rfind(const String &p_str, int p_from = -1) const; ///< return <0 if failed int rfind(const char *p_str, int p_from = -1) const; ///< return <0 if failed + int rfind_char(char32_t p_char, int p_from = -1) const; ///< return <0 if failed int rfindn(const String &p_str, int p_from = -1) const; ///< return <0 if failed, case insensitive int rfindn(const char *p_str, int p_from = -1) const; ///< return <0 if failed int findmk(const Vector<String> &p_keys, int p_from = 0, int *r_key = nullptr) const; ///< return <0 if failed @@ -305,6 +306,7 @@ public: bool is_subsequence_of(const String &p_string) const; bool is_subsequence_ofn(const String &p_string) const; bool is_quoted() const; + bool is_lowercase() const; Vector<String> bigrams() const; float similarity(const String &p_string) const; String format(const Variant &values, const String &placeholder = "{_}") const; diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index be5175c4bc..ea5207e826 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -717,9 +717,21 @@ <member name="filesystem/quick_open_dialog/default_display_mode" type="int" setter="" getter=""> If set to [code]Adaptive[/code], the dialog opens in list view or grid view depending on the requested type. If set to [code]Last Used[/code], the display mode will always open the way you last used it. </member> + <member name="filesystem/quick_open_dialog/enable_fuzzy_matching" type="bool" setter="" getter=""> + If [code]true[/code], fuzzy matching of search tokens is allowed. + </member> <member name="filesystem/quick_open_dialog/include_addons" type="bool" setter="" getter=""> If [code]true[/code], results will include files located in the [code]addons[/code] folder. </member> + <member name="filesystem/quick_open_dialog/max_fuzzy_misses" type="int" setter="" getter=""> + The number of allowed missed query characters in a match, if fuzzy matching is enabled. For example, with the default value of 2, [code]foobar[/code] would match [code]foobur[/code] and [code]foob[/code] but not [code]foo[/code]. + </member> + <member name="filesystem/quick_open_dialog/max_results" type="int" setter="" getter=""> + Maximum number of matches to show in dialog. + </member> + <member name="filesystem/quick_open_dialog/show_search_highlight" type="bool" setter="" getter=""> + If [code]true[/code], results will be highlighted with their search matches. + </member> <member name="filesystem/tools/oidn/oidn_denoise_path" type="String" setter="" getter=""> The path to the directory containing the Open Image Denoise (OIDN) executable, used optionally for denoising lightmaps. It can be downloaded from [url=https://www.openimagedenoise.org/downloads.html]openimagedenoise.org[/url]. To enable this feature for your specific project, use [member ProjectSettings.rendering/lightmapping/denoising/denoiser]. diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp index 12a7c3a2ff..eeb8ceb1ed 100644 --- a/editor/editor_settings.cpp +++ b/editor/editor_settings.cpp @@ -602,6 +602,10 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) { EDITOR_SETTING(Variant::INT, PROPERTY_HINT_RANGE, "filesystem/file_dialog/thumbnail_size", 64, "32,128,16") // Quick Open dialog + EDITOR_SETTING_USAGE(Variant::INT, PROPERTY_HINT_RANGE, "filesystem/quick_open_dialog/max_results", 100, "0,10000,1", PROPERTY_USAGE_DEFAULT) + _initial_set("filesystem/quick_open_dialog/show_search_highlight", true); + _initial_set("filesystem/quick_open_dialog/enable_fuzzy_matching", true); + EDITOR_SETTING_USAGE(Variant::INT, PROPERTY_HINT_RANGE, "filesystem/quick_open_dialog/max_fuzzy_misses", 2, "0,10,1", PROPERTY_USAGE_DEFAULT) _initial_set("filesystem/quick_open_dialog/include_addons", false); EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "filesystem/quick_open_dialog/default_display_mode", 0, "Adaptive,Last Used") diff --git a/editor/gui/editor_quick_open_dialog.cpp b/editor/gui/editor_quick_open_dialog.cpp index 0128f1f54c..44e7b3e483 100644 --- a/editor/gui/editor_quick_open_dialog.cpp +++ b/editor/gui/editor_quick_open_dialog.cpp @@ -30,6 +30,7 @@ #include "editor_quick_open_dialog.h" +#include "core/string/fuzzy_search.h" #include "editor/editor_file_system.h" #include "editor/editor_node.h" #include "editor/editor_resource_preview.h" @@ -45,6 +46,55 @@ #include "scene/gui/texture_rect.h" #include "scene/gui/tree.h" +void HighlightedLabel::draw_substr_rects(const Vector2i &p_substr, Vector2 p_offset, int p_line_limit, int line_spacing) { + for (int i = get_lines_skipped(); i < p_line_limit; i++) { + RID line = get_line_rid(i); + Vector<Vector2> ranges = TS->shaped_text_get_selection(line, p_substr.x, p_substr.x + p_substr.y); + Rect2 line_rect = get_line_rect(i); + for (const Vector2 &range : ranges) { + Rect2 rect = Rect2(Point2(range.x, 0) + line_rect.position, Size2(range.y - range.x, line_rect.size.y)); + rect.position = p_offset + line_rect.position; + rect.position.x += range.x; + rect.size = Size2(range.y - range.x, line_rect.size.y); + rect.size.x = MIN(rect.size.x, line_rect.size.x - range.x); + if (rect.size.x > 0) { + draw_rect(rect, Color(1, 1, 1, 0.07), true); + draw_rect(rect, Color(0.5, 0.7, 1.0, 0.4), false, 1); + } + } + p_offset.y += line_spacing + TS->shaped_text_get_ascent(line) + TS->shaped_text_get_descent(line); + } +} + +void HighlightedLabel::add_highlight(const Vector2i &p_interval) { + if (p_interval.y > 0) { + highlights.append(p_interval); + queue_redraw(); + } +} + +void HighlightedLabel::reset_highlights() { + highlights.clear(); + queue_redraw(); +} + +void HighlightedLabel::_notification(int p_notification) { + if (p_notification == NOTIFICATION_DRAW) { + if (highlights.is_empty()) { + return; + } + + Vector2 offset; + int line_limit; + int line_spacing; + get_layout_data(offset, line_limit, line_spacing); + + for (const Vector2i &substr : highlights) { + draw_substr_rects(substr, offset, line_limit, line_spacing); + } + } +} + EditorQuickOpenDialog::EditorQuickOpenDialog() { VBoxContainer *vbc = memnew(VBoxContainer); vbc->add_theme_constant_override("separation", 0); @@ -100,7 +150,7 @@ void EditorQuickOpenDialog::popup_dialog(const Vector<StringName> &p_base_types, get_ok_button()->set_disabled(container->has_nothing_selected()); set_title(get_dialog_title(p_base_types)); - popup_centered_clamped(Size2(710, 650) * EDSCALE, 0.8f); + popup_centered_clamped(Size2(780, 650) * EDSCALE, 0.8f); search_box->grab_focus(); } @@ -119,13 +169,18 @@ void EditorQuickOpenDialog::cancel_pressed() { } void EditorQuickOpenDialog::_search_box_text_changed(const String &p_query) { - container->update_results(p_query.to_lower()); - + container->set_query_and_update(p_query); get_ok_button()->set_disabled(container->has_nothing_selected()); } //------------------------- Result Container +void style_button(Button *p_button) { + p_button->set_flat(true); + p_button->set_focus_mode(Control::FOCUS_NONE); + p_button->set_default_cursor_shape(Control::CURSOR_POINTING_HAND); +} + QuickOpenResultContainer::QuickOpenResultContainer() { set_h_size_flags(Control::SIZE_EXPAND_FILL); set_v_size_flags(Control::SIZE_EXPAND_FILL); @@ -175,91 +230,107 @@ QuickOpenResultContainer::QuickOpenResultContainer() { } { - // Bottom bar - HBoxContainer *bottom_bar = memnew(HBoxContainer); - add_child(bottom_bar); - + // Selected filepath file_details_path = memnew(Label); file_details_path->set_h_size_flags(Control::SIZE_EXPAND_FILL); file_details_path->set_horizontal_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER); file_details_path->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS); - bottom_bar->add_child(file_details_path); + add_child(file_details_path); + } - { - HBoxContainer *hbc = memnew(HBoxContainer); - hbc->add_theme_constant_override("separation", 3); - bottom_bar->add_child(hbc); - - include_addons_toggle = memnew(CheckButton); - include_addons_toggle->set_flat(true); - include_addons_toggle->set_focus_mode(Control::FOCUS_NONE); - include_addons_toggle->set_default_cursor_shape(CURSOR_POINTING_HAND); - include_addons_toggle->set_tooltip_text(TTR("Include files from addons")); - include_addons_toggle->connect(SceneStringName(toggled), callable_mp(this, &QuickOpenResultContainer::_toggle_include_addons)); - hbc->add_child(include_addons_toggle); - - VSeparator *vsep = memnew(VSeparator); - vsep->set_v_size_flags(Control::SIZE_SHRINK_CENTER); - vsep->set_custom_minimum_size(Size2i(0, 14 * EDSCALE)); - hbc->add_child(vsep); - - display_mode_toggle = memnew(Button); - display_mode_toggle->set_flat(true); - display_mode_toggle->set_focus_mode(Control::FOCUS_NONE); - display_mode_toggle->set_default_cursor_shape(CURSOR_POINTING_HAND); - display_mode_toggle->connect(SceneStringName(pressed), callable_mp(this, &QuickOpenResultContainer::_toggle_display_mode)); - hbc->add_child(display_mode_toggle); - } + { + // Bottom bar + HBoxContainer *bottom_bar = memnew(HBoxContainer); + bottom_bar->set_h_size_flags(Control::SIZE_EXPAND_FILL); + bottom_bar->set_alignment(ALIGNMENT_END); + bottom_bar->add_theme_constant_override("separation", 3); + add_child(bottom_bar); + + fuzzy_search_toggle = memnew(CheckButton); + style_button(fuzzy_search_toggle); + fuzzy_search_toggle->set_text(TTR("Fuzzy Search")); + fuzzy_search_toggle->set_tooltip_text(TTR("Enable fuzzy matching")); + fuzzy_search_toggle->connect(SceneStringName(toggled), callable_mp(this, &QuickOpenResultContainer::_toggle_fuzzy_search)); + bottom_bar->add_child(fuzzy_search_toggle); + + include_addons_toggle = memnew(CheckButton); + style_button(include_addons_toggle); + include_addons_toggle->set_text(TTR("Addons")); + include_addons_toggle->set_tooltip_text(TTR("Include files from addons")); + include_addons_toggle->connect(SceneStringName(toggled), callable_mp(this, &QuickOpenResultContainer::_toggle_include_addons)); + bottom_bar->add_child(include_addons_toggle); + + VSeparator *vsep = memnew(VSeparator); + vsep->set_v_size_flags(Control::SIZE_SHRINK_CENTER); + vsep->set_custom_minimum_size(Size2i(0, 14 * EDSCALE)); + bottom_bar->add_child(vsep); + + display_mode_toggle = memnew(Button); + style_button(display_mode_toggle); + display_mode_toggle->connect(SceneStringName(pressed), callable_mp(this, &QuickOpenResultContainer::_toggle_display_mode)); + bottom_bar->add_child(display_mode_toggle); } +} - // Creating and deleting nodes while searching is slow, so we allocate - // a bunch of result nodes and fill in the content based on result ranking. - result_items.resize(TOTAL_ALLOCATED_RESULT_ITEMS); - for (int i = 0; i < TOTAL_ALLOCATED_RESULT_ITEMS; i++) { +void QuickOpenResultContainer::_ensure_result_vector_capacity() { + int target_size = EDITOR_GET("filesystem/quick_open_dialog/max_results"); + int initial_size = result_items.size(); + for (int i = target_size; i < initial_size; i++) { + result_items[i]->queue_free(); + } + result_items.resize(target_size); + for (int i = initial_size; i < target_size; i++) { QuickOpenResultItem *item = memnew(QuickOpenResultItem); item->connect(SceneStringName(gui_input), callable_mp(this, &QuickOpenResultContainer::_item_input).bind(i)); result_items.write[i] = item; - } -} - -QuickOpenResultContainer::~QuickOpenResultContainer() { - if (never_opened) { - for (QuickOpenResultItem *E : result_items) { - memdelete(E); + if (!never_opened) { + _layout_result_item(item); } } } void QuickOpenResultContainer::init(const Vector<StringName> &p_base_types) { + _ensure_result_vector_capacity(); base_types = p_base_types; - never_opened = false; const int display_mode_behavior = EDITOR_GET("filesystem/quick_open_dialog/default_display_mode"); const bool adaptive_display_mode = (display_mode_behavior == 0); if (adaptive_display_mode) { _set_display_mode(get_adaptive_display_mode(p_base_types)); + } else if (never_opened) { + int last = EditorSettings::get_singleton()->get_project_metadata("quick_open_dialog", "last_mode", (int)QuickOpenDisplayMode::LIST); + _set_display_mode((QuickOpenDisplayMode)last); } + const bool fuzzy_matching = EDITOR_GET("filesystem/quick_open_dialog/enable_fuzzy_matching"); const bool include_addons = EDITOR_GET("filesystem/quick_open_dialog/include_addons"); + fuzzy_search_toggle->set_pressed_no_signal(fuzzy_matching); include_addons_toggle->set_pressed_no_signal(include_addons); + never_opened = false; + + const bool enable_highlights = EDITOR_GET("filesystem/quick_open_dialog/show_search_highlight"); + for (QuickOpenResultItem *E : result_items) { + E->enable_highlights = enable_highlights; + } - _create_initial_results(include_addons); + _create_initial_results(); } -void QuickOpenResultContainer::_create_initial_results(bool p_include_addons) { - file_type_icons.insert("__default_icon", get_editor_theme_icon(SNAME("Object"))); - _find_candidates_in_folder(EditorFileSystem::get_singleton()->get_filesystem(), p_include_addons); - max_total_results = MIN(candidates.size(), TOTAL_ALLOCATED_RESULT_ITEMS); +void QuickOpenResultContainer::_create_initial_results() { file_type_icons.clear(); - - update_results(query); + file_type_icons.insert("__default_icon", get_editor_theme_icon(SNAME("Object"))); + filepaths.clear(); + filetypes.clear(); + _find_filepaths_in_folder(EditorFileSystem::get_singleton()->get_filesystem(), include_addons_toggle->is_pressed()); + max_total_results = MIN(filepaths.size(), result_items.size()); + update_results(); } -void QuickOpenResultContainer::_find_candidates_in_folder(EditorFileSystemDirectory *p_directory, bool p_include_addons) { +void QuickOpenResultContainer::_find_filepaths_in_folder(EditorFileSystemDirectory *p_directory, bool p_include_addons) { for (int i = 0; i < p_directory->get_subdir_count(); i++) { if (p_include_addons || p_directory->get_name() != "addons") { - _find_candidates_in_folder(p_directory->get_subdir(i), p_include_addons); + _find_filepaths_in_folder(p_directory->get_subdir(i), p_include_addons); } } @@ -276,146 +347,91 @@ void QuickOpenResultContainer::_find_candidates_in_folder(EditorFileSystemDirect bool is_valid = ClassDB::is_parent_class(engine_type, parent_type) || (!is_engine_type && EditorNode::get_editor_data().script_class_is_parent(script_type, parent_type)); if (is_valid) { - Candidate c; - c.file_name = file_path.get_file(); - c.file_directory = file_path.get_base_dir(); - - EditorResourcePreview::PreviewItem item = EditorResourcePreview::get_singleton()->get_resource_preview_if_available(file_path); - if (item.preview.is_valid()) { - c.thumbnail = item.preview; - } else if (file_type_icons.has(actual_type)) { - c.thumbnail = *file_type_icons.lookup_ptr(actual_type); - } else if (has_theme_icon(actual_type, EditorStringName(EditorIcons))) { - c.thumbnail = get_editor_theme_icon(actual_type); - file_type_icons.insert(actual_type, c.thumbnail); - } else { - c.thumbnail = *file_type_icons.lookup_ptr("__default_icon"); - } - - candidates.push_back(c); - + filepaths.append(file_path); + filetypes.insert(file_path, actual_type); break; // Stop testing base types as soon as we get a match. } } } } -void QuickOpenResultContainer::update_results(const String &p_query) { +void QuickOpenResultContainer::set_query_and_update(const String &p_query) { query = p_query; - - int relevant_candidates = _sort_candidates(p_query); - _update_result_items(MIN(relevant_candidates, max_total_results), 0); -} - -int QuickOpenResultContainer::_sort_candidates(const String &p_query) { - if (p_query.is_empty()) { - return 0; + update_results(); +} + +void QuickOpenResultContainer::_setup_candidate(QuickOpenResultCandidate &candidate, const String &filepath) { + StringName actual_type = *filetypes.lookup_ptr(filepath); + candidate.file_path = filepath; + candidate.result = nullptr; + + EditorResourcePreview::PreviewItem item = EditorResourcePreview::get_singleton()->get_resource_preview_if_available(filepath); + if (item.preview.is_valid()) { + candidate.thumbnail = item.preview; + } else if (file_type_icons.has(actual_type)) { + candidate.thumbnail = *file_type_icons.lookup_ptr(actual_type); + } else if (has_theme_icon(actual_type, EditorStringName(EditorIcons))) { + candidate.thumbnail = get_editor_theme_icon(actual_type); + file_type_icons.insert(actual_type, candidate.thumbnail); + } else { + candidate.thumbnail = *file_type_icons.lookup_ptr("__default_icon"); } +} - const PackedStringArray search_tokens = p_query.to_lower().replace("/", " ").split(" ", false); +void QuickOpenResultContainer::_setup_candidate(QuickOpenResultCandidate &p_candidate, const FuzzySearchResult &p_result) { + _setup_candidate(p_candidate, p_result.target); + p_candidate.result = &p_result; +} - if (search_tokens.is_empty()) { - return 0; +void QuickOpenResultContainer::update_results() { + showing_history = false; + candidates.clear(); + if (query.is_empty()) { + _use_default_candidates(); + } else { + _score_and_sort_candidates(); } + _update_result_items(MIN(candidates.size(), max_total_results), 0); +} - // First, we assign a score to each candidate. - int num_relevant_candidates = 0; - for (Candidate &c : candidates) { - c.score = 0; - int prev_token_match_pos = -1; - - for (const String &token : search_tokens) { - const int file_pos = c.file_name.findn(token); - const int dir_pos = c.file_directory.findn(token); - - const bool file_match = file_pos > -1; - const bool dir_match = dir_pos > -1; - if (!file_match && !dir_match) { - c.score = -1.0f; - break; - } - - float token_score = file_match ? 0.6f : 0.1999f; - - // Add bias for shorter filenames/paths: they resemble the query more. - const String &matched_string = file_match ? c.file_name : c.file_directory; - int matched_string_token_pos = file_match ? file_pos : dir_pos; - token_score += 0.1f * (1.0f - ((float)matched_string_token_pos / (float)matched_string.length())); - - // Add bias if the match happened in the file name, not the extension. - if (file_match) { - int ext_pos = matched_string.rfind("."); - if (ext_pos == -1 || ext_pos > matched_string_token_pos) { - token_score += 0.1f; - } - } - - // Add bias if token is in order. - { - int candidate_string_token_pos = file_match ? (c.file_directory.length() + file_pos) : dir_pos; - - if (prev_token_match_pos != -1 && candidate_string_token_pos > prev_token_match_pos) { - token_score += 0.2f; - } - - prev_token_match_pos = candidate_string_token_pos; - } - - c.score += token_score; +void QuickOpenResultContainer::_use_default_candidates() { + if (filepaths.size() <= SHOW_ALL_FILES_THRESHOLD) { + candidates.resize(filepaths.size()); + QuickOpenResultCandidate *candidates_write = candidates.ptrw(); + for (const String &filepath : filepaths) { + _setup_candidate(*candidates_write++, filepath); } - - if (c.score > 0.0f) { - num_relevant_candidates++; + } else if (base_types.size() == 1) { + Vector<QuickOpenResultCandidate> *history = selected_history.lookup_ptr(base_types[0]); + if (history) { + showing_history = true; + candidates.append_array(*history); } } - - // Now we will sort the candidates based on score, resolving ties by favoring: - // 1. Shorter file length. - // 2. Shorter directory length. - // 3. Lower alphabetic order. - struct CandidateComparator { - _FORCE_INLINE_ bool operator()(const Candidate &p_a, const Candidate &p_b) const { - if (!Math::is_equal_approx(p_a.score, p_b.score)) { - return p_a.score > p_b.score; - } - - if (p_a.file_name.length() != p_b.file_name.length()) { - return p_a.file_name.length() < p_b.file_name.length(); - } - - if (p_a.file_directory.length() != p_b.file_directory.length()) { - return p_a.file_directory.length() < p_b.file_directory.length(); - } - - return p_a.file_name < p_b.file_name; - } - }; - candidates.sort_custom<CandidateComparator>(); - - return num_relevant_candidates; } -void QuickOpenResultContainer::_update_result_items(int p_new_visible_results_count, int p_new_selection_index) { - List<Candidate> *type_history = nullptr; - - showing_history = false; - - if (query.is_empty()) { - if (candidates.size() <= SHOW_ALL_FILES_THRESHOLD) { - p_new_visible_results_count = candidates.size(); - } else { - p_new_visible_results_count = 0; +void QuickOpenResultContainer::_update_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; + 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.search_all(filepaths, search_results); +} - if (base_types.size() == 1) { - type_history = selected_history.lookup_ptr(base_types[0]); - if (type_history) { - p_new_visible_results_count = type_history->size(); - showing_history = true; - } - } - } +void QuickOpenResultContainer::_score_and_sort_candidates() { + _update_fuzzy_search_results(); + candidates.resize(search_results.size()); + QuickOpenResultCandidate *candidates_write = candidates.ptrw(); + for (const FuzzySearchResult &result : search_results) { + _setup_candidate(*candidates_write++, result); } +} +void QuickOpenResultContainer::_update_result_items(int p_new_visible_results_count, int p_new_selection_index) { // Only need to update items that were not hidden in previous update. int num_items_needing_updates = MAX(num_visible_results, p_new_visible_results_count); num_visible_results = p_new_visible_results_count; @@ -424,13 +440,7 @@ void QuickOpenResultContainer::_update_result_items(int p_new_visible_results_co QuickOpenResultItem *item = result_items[i]; if (i < num_visible_results) { - if (type_history) { - const Candidate &c = type_history->get(i); - item->set_content(c.thumbnail, c.file_name, c.file_directory); - } else { - const Candidate &c = candidates[i]; - item->set_content(c.thumbnail, c.file_name, c.file_directory); - } + item->set_content(candidates[i]); } else { item->reset(); } @@ -443,7 +453,7 @@ void QuickOpenResultContainer::_update_result_items(int p_new_visible_results_co no_results_container->set_visible(!any_results); if (!any_results) { - if (candidates.is_empty()) { + if (filepaths.is_empty()) { no_results_label->set_text(TTR("No files found for this type")); } else if (query.is_empty()) { no_results_label->set_text(TTR("Start searching to find files...")); @@ -471,10 +481,12 @@ void QuickOpenResultContainer::handle_search_box_input(const Ref<InputEvent> &p_ } break; case Key::LEFT: case Key::RIGHT: { - // Both grid and the search box use left/right keys. By default, grid will take it. - // It would be nice if we could check for ALT to give the event to the searchbox cursor. - // However, if you press ALT, the searchbox also denies the input. - move_selection = (content_display_mode == QuickOpenDisplayMode::GRID); + if (content_display_mode == QuickOpenDisplayMode::GRID) { + // Maybe strip off the shift modifier to allow non-selecting navigation by character? + if (key_event->get_modifiers_mask() == 0) { + move_selection = true; + } + } } break; default: break; // Let the event through so it will reach the search box. @@ -566,11 +578,15 @@ void QuickOpenResultContainer::_item_input(const Ref<InputEvent> &p_ev, int p_in } } +void QuickOpenResultContainer::_toggle_fuzzy_search(bool p_pressed) { + EditorSettings::get_singleton()->set("filesystem/quick_open_dialog/enable_fuzzy_matching", p_pressed); + update_results(); +} + void QuickOpenResultContainer::_toggle_include_addons(bool p_pressed) { EditorSettings::get_singleton()->set("filesystem/quick_open_dialog/include_addons", p_pressed); - cleanup(); - _create_initial_results(p_pressed); + _create_initial_results(); } void QuickOpenResultContainer::_toggle_display_mode() { @@ -578,41 +594,41 @@ void QuickOpenResultContainer::_toggle_display_mode() { _set_display_mode(new_display_mode); } -void QuickOpenResultContainer::_set_display_mode(QuickOpenDisplayMode p_display_mode) { - content_display_mode = p_display_mode; +CanvasItem *QuickOpenResultContainer::_get_result_root() { + if (content_display_mode == QuickOpenDisplayMode::LIST) { + return list; + } else { + return grid; + } +} - const bool show_list = (content_display_mode == QuickOpenDisplayMode::LIST); - if ((show_list && list->is_visible()) || (!show_list && grid->is_visible())) { - return; +void QuickOpenResultContainer::_layout_result_item(QuickOpenResultItem *item) { + item->set_display_mode(content_display_mode); + Node *parent = item->get_parent(); + if (parent) { + parent->remove_child(item); } + _get_result_root()->add_child(item); +} - hide(); +void QuickOpenResultContainer::_set_display_mode(QuickOpenDisplayMode p_display_mode) { + CanvasItem *prev_root = _get_result_root(); - // Move result item nodes from one container to the other. - CanvasItem *prev_root; - CanvasItem *next_root; - if (content_display_mode == QuickOpenDisplayMode::LIST) { - prev_root = Object::cast_to<CanvasItem>(grid); - next_root = Object::cast_to<CanvasItem>(list); - } else { - prev_root = Object::cast_to<CanvasItem>(list); - next_root = Object::cast_to<CanvasItem>(grid); + if (prev_root->is_visible() && content_display_mode == p_display_mode) { + return; } - const bool first_time = !list->is_visible() && !grid->is_visible(); + content_display_mode = p_display_mode; + CanvasItem *next_root = _get_result_root(); - prev_root->hide(); - for (QuickOpenResultItem *item : result_items) { - item->set_display_mode(content_display_mode); + EditorSettings::get_singleton()->set_project_metadata("quick_open_dialog", "last_mode", (int)content_display_mode); - if (!first_time) { - prev_root->remove_child(item); - } + prev_root->hide(); + next_root->show(); - next_root->add_child(item); + for (QuickOpenResultItem *item : result_items) { + _layout_result_item(item); } - next_root->show(); - show(); _update_result_items(num_visible_results, selection_index); @@ -631,16 +647,7 @@ bool QuickOpenResultContainer::has_nothing_selected() const { String QuickOpenResultContainer::get_selected() const { ERR_FAIL_COND_V_MSG(has_nothing_selected(), String(), "Tried to get selected file, but nothing was selected."); - - if (showing_history) { - const List<Candidate> *type_history = selected_history.lookup_ptr(base_types[0]); - - const Candidate &c = type_history->get(selection_index); - return c.file_directory.path_join(c.file_name); - } else { - const Candidate &c = candidates[selection_index]; - return c.file_directory.path_join(c.file_name); - } + return candidates[selection_index].file_path; } QuickOpenDisplayMode QuickOpenResultContainer::get_adaptive_display_mode(const Vector<StringName> &p_base_types) { @@ -669,32 +676,27 @@ void QuickOpenResultContainer::save_selected_item() { return; } - if (showing_history) { - // Selecting from history, so already added. - return; - } - const StringName &base_type = base_types[0]; + const QuickOpenResultCandidate &selected = candidates[selection_index]; + Vector<QuickOpenResultCandidate> *type_history = selected_history.lookup_ptr(base_type); - List<Candidate> *type_history = selected_history.lookup_ptr(base_type); if (!type_history) { - selected_history.insert(base_type, List<Candidate>()); + selected_history.insert(base_type, Vector<QuickOpenResultCandidate>()); type_history = selected_history.lookup_ptr(base_type); } else { - const Candidate &selected = candidates[selection_index]; - - for (const Candidate &candidate : *type_history) { - if (candidate.file_directory == selected.file_directory && candidate.file_name == selected.file_name) { - return; + for (int i = 0; i < type_history->size(); i++) { + if (selected.file_path == type_history->get(i).file_path) { + type_history->remove_at(i); + break; } } - - if (type_history->size() > 8) { - type_history->pop_back(); - } } - type_history->push_front(candidates[selection_index]); + type_history->insert(0, selected); + type_history->ptrw()->result = nullptr; + if (type_history->size() > MAX_HISTORY_SIZE) { + type_history->resize(MAX_HISTORY_SIZE); + } } void QuickOpenResultContainer::cleanup() { @@ -748,36 +750,35 @@ QuickOpenResultItem::QuickOpenResultItem() { void QuickOpenResultItem::set_display_mode(QuickOpenDisplayMode p_display_mode) { if (p_display_mode == QuickOpenDisplayMode::LIST) { grid_item->hide(); + grid_item->reset(); list_item->show(); } else { list_item->hide(); + list_item->reset(); grid_item->show(); } queue_redraw(); } -void QuickOpenResultItem::set_content(const Ref<Texture2D> &p_thumbnail, const String &p_file, const String &p_file_directory) { +void QuickOpenResultItem::set_content(const QuickOpenResultCandidate &p_candidate) { _set_enabled(true); if (list_item->is_visible()) { - list_item->set_content(p_thumbnail, p_file, p_file_directory); + list_item->set_content(p_candidate, enable_highlights); } else { - grid_item->set_content(p_thumbnail, p_file); + grid_item->set_content(p_candidate, enable_highlights); } + + queue_redraw(); } void QuickOpenResultItem::reset() { _set_enabled(false); - is_hovering = false; is_selected = false; - - if (list_item->is_visible()) { - list_item->reset(); - } else { - grid_item->reset(); - } + list_item->reset(); + grid_item->reset(); } void QuickOpenResultItem::highlight_item(bool p_enabled) { @@ -830,6 +831,22 @@ void QuickOpenResultItem::_notification(int p_what) { //----------------- List item +static Vector2i _get_path_interval(const Vector2i &p_interval, int p_dir_index) { + if (p_interval.x >= p_dir_index || p_interval.y < 1) { + return { -1, -1 }; + } + return { p_interval.x, MIN(p_interval.x + p_interval.y, p_dir_index) - p_interval.x }; +} + +static Vector2i _get_name_interval(const Vector2i &p_interval, int p_dir_index) { + if (p_interval.x + p_interval.y <= p_dir_index || p_interval.y < 1) { + return { -1, -1 }; + } + int first_name_idx = p_dir_index + 1; + int start = MAX(p_interval.x, first_name_idx); + return { start - first_name_idx, p_interval.y - start + p_interval.x }; +} + QuickOpenResultListItem::QuickOpenResultListItem() { set_h_size_flags(Control::SIZE_EXPAND_FILL); add_theme_constant_override("separation", 4 * EDSCALE); @@ -857,13 +874,13 @@ QuickOpenResultListItem::QuickOpenResultListItem() { text_container->set_v_size_flags(Control::SIZE_FILL); add_child(text_container); - name = memnew(Label); + name = memnew(HighlightedLabel); name->set_h_size_flags(Control::SIZE_EXPAND_FILL); name->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS); name->set_horizontal_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_LEFT); text_container->add_child(name); - path = memnew(Label); + path = memnew(HighlightedLabel); path->set_h_size_flags(Control::SIZE_EXPAND_FILL); path->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS); path->add_theme_font_size_override(SceneStringName(font_size), 12 * EDSCALE); @@ -871,18 +888,29 @@ QuickOpenResultListItem::QuickOpenResultListItem() { } } -void QuickOpenResultListItem::set_content(const Ref<Texture2D> &p_thumbnail, const String &p_file, const String &p_file_directory) { - thumbnail->set_texture(p_thumbnail); - name->set_text(p_file); - path->set_text(p_file_directory); +void QuickOpenResultListItem::set_content(const QuickOpenResultCandidate &p_candidate, bool p_highlight) { + thumbnail->set_texture(p_candidate.thumbnail); + name->set_text(p_candidate.file_path.get_file()); + path->set_text(p_candidate.file_path.get_base_dir()); + name->reset_highlights(); + path->reset_highlights(); + + if (p_highlight && p_candidate.result != nullptr) { + for (const FuzzyTokenMatch &match : p_candidate.result->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)); + } + } + } const int max_size = 32 * EDSCALE; - bool uses_icon = p_thumbnail->get_width() < max_size; + bool uses_icon = p_candidate.thumbnail->get_width() < max_size; if (uses_icon) { - thumbnail->set_custom_minimum_size(p_thumbnail->get_size()); + thumbnail->set_custom_minimum_size(p_candidate.thumbnail->get_size()); - int margin_needed = (max_size - p_thumbnail->get_width()) / 2; + int margin_needed = (max_size - p_candidate.thumbnail->get_width()) / 2; image_container->add_theme_constant_override("margin_left", CONTAINER_MARGIN + margin_needed); image_container->add_theme_constant_override("margin_right", margin_needed); } else { @@ -893,9 +921,11 @@ void QuickOpenResultListItem::set_content(const Ref<Texture2D> &p_thumbnail, con } void QuickOpenResultListItem::reset() { - name->set_text(""); thumbnail->set_texture(nullptr); + name->set_text(""); path->set_text(""); + name->reset_highlights(); + path->reset_highlights(); } void QuickOpenResultListItem::highlight_item(const Color &p_color) { @@ -924,10 +954,10 @@ QuickOpenResultGridItem::QuickOpenResultGridItem() { thumbnail = memnew(TextureRect); thumbnail->set_h_size_flags(Control::SIZE_SHRINK_CENTER); thumbnail->set_v_size_flags(Control::SIZE_SHRINK_CENTER); - thumbnail->set_custom_minimum_size(Size2i(80 * EDSCALE, 64 * EDSCALE)); + thumbnail->set_custom_minimum_size(Size2i(120 * EDSCALE, 64 * EDSCALE)); add_child(thumbnail); - name = memnew(Label); + name = memnew(HighlightedLabel); name->set_h_size_flags(Control::SIZE_EXPAND_FILL); name->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS); name->set_horizontal_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER); @@ -935,16 +965,23 @@ QuickOpenResultGridItem::QuickOpenResultGridItem() { add_child(name); } -void QuickOpenResultGridItem::set_content(const Ref<Texture2D> &p_thumbnail, const String &p_file) { - thumbnail->set_texture(p_thumbnail); +void QuickOpenResultGridItem::set_content(const QuickOpenResultCandidate &p_candidate, bool p_highlight) { + thumbnail->set_texture(p_candidate.thumbnail); + name->set_text(p_candidate.file_path.get_file()); + name->set_tooltip_text(p_candidate.file_path); + name->reset_highlights(); - const String &file_name = p_file.get_basename(); - name->set_text(file_name); - name->set_tooltip_text(file_name); + if (p_highlight && p_candidate.result != nullptr) { + for (const FuzzyTokenMatch &match : p_candidate.result->token_matches) { + for (const Vector2i &interval : match.substrings) { + name->add_highlight(_get_name_interval(interval, p_candidate.result->dir_index)); + } + } + } - bool uses_icon = p_thumbnail->get_width() < (32 * EDSCALE); + bool uses_icon = p_candidate.thumbnail->get_width() < (32 * EDSCALE); - if (uses_icon || p_thumbnail->get_height() <= thumbnail->get_custom_minimum_size().y) { + if (uses_icon || p_candidate.thumbnail->get_height() <= thumbnail->get_custom_minimum_size().y) { thumbnail->set_expand_mode(TextureRect::EXPAND_KEEP_SIZE); thumbnail->set_stretch_mode(TextureRect::StretchMode::STRETCH_KEEP_CENTERED); } else { @@ -954,8 +991,9 @@ void QuickOpenResultGridItem::set_content(const Ref<Texture2D> &p_thumbnail, con } void QuickOpenResultGridItem::reset() { - name->set_text(""); thumbnail->set_texture(nullptr); + name->set_text(""); + name->reset_highlights(); } void QuickOpenResultGridItem::highlight_item(const Color &p_color) { diff --git a/editor/gui/editor_quick_open_dialog.h b/editor/gui/editor_quick_open_dialog.h index 49257aed6b..3b3f927527 100644 --- a/editor/gui/editor_quick_open_dialog.h +++ b/editor/gui/editor_quick_open_dialog.h @@ -48,6 +48,8 @@ class Texture2D; class TextureRect; class VBoxContainer; +class FuzzySearchResult; + class QuickOpenResultItem; enum class QuickOpenDisplayMode { @@ -55,13 +57,35 @@ enum class QuickOpenDisplayMode { LIST, }; +struct QuickOpenResultCandidate { + String file_path; + Ref<Texture2D> thumbnail; + const FuzzySearchResult *result = nullptr; +}; + +class HighlightedLabel : public Label { + GDCLASS(HighlightedLabel, Label) + + Vector<Vector2i> highlights; + + void draw_substr_rects(const Vector2i &p_substr, Vector2 p_offset, int p_line_limit, int line_spacing); + +public: + void add_highlight(const Vector2i &p_interval); + void reset_highlights(); + +protected: + void _notification(int p_notification); +}; + class QuickOpenResultContainer : public VBoxContainer { GDCLASS(QuickOpenResultContainer, VBoxContainer) public: void init(const Vector<StringName> &p_base_types); void handle_search_box_input(const Ref<InputEvent> &p_ie); - void update_results(const String &p_query); + void set_query_and_update(const String &p_query); + void update_results(); bool has_nothing_selected() const; String get_selected() const; @@ -70,27 +94,21 @@ public: void cleanup(); QuickOpenResultContainer(); - ~QuickOpenResultContainer(); protected: void _notification(int p_what); private: - static const int TOTAL_ALLOCATED_RESULT_ITEMS = 100; - static const int SHOW_ALL_FILES_THRESHOLD = 30; - - struct Candidate { - String file_name; - String file_directory; - - Ref<Texture2D> thumbnail; - float score = 0; - }; + static constexpr int SHOW_ALL_FILES_THRESHOLD = 30; + static constexpr int MAX_HISTORY_SIZE = 20; + Vector<FuzzySearchResult> search_results; Vector<StringName> base_types; - Vector<Candidate> candidates; + Vector<String> filepaths; + OAHashMap<String, StringName> filetypes; + Vector<QuickOpenResultCandidate> candidates; - OAHashMap<StringName, List<Candidate>> selected_history; + OAHashMap<StringName, Vector<QuickOpenResultCandidate>> selected_history; String query; int selection_index = -1; @@ -114,15 +132,21 @@ private: Label *file_details_path = nullptr; Button *display_mode_toggle = nullptr; CheckButton *include_addons_toggle = nullptr; + CheckButton *fuzzy_search_toggle = nullptr; OAHashMap<StringName, Ref<Texture2D>> file_type_icons; static QuickOpenDisplayMode get_adaptive_display_mode(const Vector<StringName> &p_base_types); - void _create_initial_results(bool p_include_addons); - void _find_candidates_in_folder(EditorFileSystemDirectory *p_directory, bool p_include_addons); + void _ensure_result_vector_capacity(); + void _create_initial_results(); + void _find_filepaths_in_folder(EditorFileSystemDirectory *p_directory, bool p_include_addons); - int _sort_candidates(const String &p_query); + void _setup_candidate(QuickOpenResultCandidate &p_candidate, const String &p_filepath); + void _setup_candidate(QuickOpenResultCandidate &p_candidate, const FuzzySearchResult &p_result); + void _update_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); void _move_selection_index(Key p_key); @@ -130,9 +154,12 @@ private: void _item_input(const Ref<InputEvent> &p_ev, int p_index); + CanvasItem *_get_result_root(); + void _layout_result_item(QuickOpenResultItem *p_item); void _set_display_mode(QuickOpenDisplayMode p_display_mode); void _toggle_display_mode(); void _toggle_include_addons(bool p_pressed); + void _toggle_fuzzy_search(bool p_pressed); static void _bind_methods(); }; @@ -143,14 +170,14 @@ class QuickOpenResultGridItem : public VBoxContainer { public: QuickOpenResultGridItem(); - void set_content(const Ref<Texture2D> &p_thumbnail, const String &p_file_name); void reset(); + void set_content(const QuickOpenResultCandidate &p_candidate, bool p_highlight); void highlight_item(const Color &p_color); void remove_highlight(); private: TextureRect *thumbnail = nullptr; - Label *name = nullptr; + HighlightedLabel *name = nullptr; }; class QuickOpenResultListItem : public HBoxContainer { @@ -159,8 +186,8 @@ class QuickOpenResultListItem : public HBoxContainer { public: QuickOpenResultListItem(); - void set_content(const Ref<Texture2D> &p_thumbnail, const String &p_file_name, const String &p_file_directory); void reset(); + void set_content(const QuickOpenResultCandidate &p_candidate, bool p_highlight); void highlight_item(const Color &p_color); void remove_highlight(); @@ -174,8 +201,8 @@ private: VBoxContainer *text_container = nullptr; TextureRect *thumbnail = nullptr; - Label *name = nullptr; - Label *path = nullptr; + HighlightedLabel *name = nullptr; + HighlightedLabel *path = nullptr; }; class QuickOpenResultItem : public HBoxContainer { @@ -184,10 +211,11 @@ class QuickOpenResultItem : public HBoxContainer { public: QuickOpenResultItem(); - void set_content(const Ref<Texture2D> &p_thumbnail, const String &p_file_name, const String &p_file_directory); - void set_display_mode(QuickOpenDisplayMode p_display_mode); - void reset(); + bool enable_highlights = true; + void reset(); + void set_content(const QuickOpenResultCandidate &p_candidate); + void set_display_mode(QuickOpenDisplayMode p_display_mode); void highlight_item(bool p_enabled); protected: diff --git a/scene/gui/label.cpp b/scene/gui/label.cpp index 42b4e56b48..7a0e5b8867 100644 --- a/scene/gui/label.cpp +++ b/scene/gui/label.cpp @@ -335,6 +335,121 @@ inline void draw_glyph_outline(const Glyph &p_gl, const RID &p_canvas, const Col } } +void Label::_ensure_shaped() const { + if (dirty || font_dirty || lines_dirty) { + const_cast<Label *>(this)->_shape(); + } +} + +RID Label::get_line_rid(int p_line) const { + return lines_rid[p_line]; +} + +Rect2 Label::get_line_rect(int p_line) const { + // Returns a rect providing the line's horizontal offset and total size. To determine the vertical + // offset, use r_offset and r_line_spacing from get_layout_data. + bool rtl = TS->shaped_text_get_inferred_direction(text_rid) == TextServer::DIRECTION_RTL; + bool rtl_layout = is_layout_rtl(); + Ref<StyleBox> style = theme_cache.normal_style; + Size2 size = get_size(); + Size2 line_size = TS->shaped_text_get_size(lines_rid[p_line]); + Vector2 offset; + + switch (horizontal_alignment) { + case HORIZONTAL_ALIGNMENT_FILL: + if (rtl && autowrap_mode != TextServer::AUTOWRAP_OFF) { + offset.x = int(size.width - style->get_margin(SIDE_RIGHT) - line_size.width); + } else { + offset.x = style->get_offset().x; + } + break; + case HORIZONTAL_ALIGNMENT_LEFT: { + if (rtl_layout) { + offset.x = int(size.width - style->get_margin(SIDE_RIGHT) - line_size.width); + } else { + offset.x = style->get_offset().x; + } + } break; + case HORIZONTAL_ALIGNMENT_CENTER: { + offset.x = int(size.width - line_size.width) / 2; + } break; + case HORIZONTAL_ALIGNMENT_RIGHT: { + if (rtl_layout) { + offset.x = style->get_offset().x; + } else { + offset.x = int(size.width - style->get_margin(SIDE_RIGHT) - line_size.width); + } + } break; + } + + return Rect2(offset, line_size); +} + +void Label::get_layout_data(Vector2 &r_offset, int &r_line_limit, int &r_line_spacing) const { + // Computes several common parameters involved in laying out and rendering text set to this label. + // Only vertical margin is considered in r_offset: use get_line_rect to get the horizontal offset + // for a given line of text. + Size2 size = get_size(); + Ref<StyleBox> style = theme_cache.normal_style; + int line_spacing = settings.is_valid() ? settings->get_line_spacing() : theme_cache.line_spacing; + + float total_h = 0.0; + int lines_visible = 0; + + // Get number of lines to fit to the height. + for (int64_t i = lines_skipped; i < lines_rid.size(); i++) { + total_h += TS->shaped_text_get_size(lines_rid[i]).y + line_spacing; + if (total_h > (get_size().height - style->get_minimum_size().height + line_spacing)) { + break; + } + lines_visible++; + } + + if (max_lines_visible >= 0 && lines_visible > max_lines_visible) { + lines_visible = max_lines_visible; + } + + r_line_limit = MIN(lines_rid.size(), lines_visible + lines_skipped); + + // Get real total height. + total_h = 0; + for (int64_t i = lines_skipped; i < r_line_limit; i++) { + total_h += TS->shaped_text_get_size(lines_rid[i]).y + line_spacing; + } + total_h += style->get_margin(SIDE_TOP) + style->get_margin(SIDE_BOTTOM); + + int vbegin = 0, vsep = 0; + if (lines_visible > 0) { + switch (vertical_alignment) { + case VERTICAL_ALIGNMENT_TOP: { + // Nothing. + } break; + case VERTICAL_ALIGNMENT_CENTER: { + vbegin = (size.y - (total_h - line_spacing)) / 2; + vsep = 0; + + } break; + case VERTICAL_ALIGNMENT_BOTTOM: { + vbegin = size.y - (total_h - line_spacing); + vsep = 0; + + } break; + case VERTICAL_ALIGNMENT_FILL: { + vbegin = 0; + if (lines_visible > 1) { + vsep = (size.y - (total_h - line_spacing)) / (lines_visible - 1); + } else { + vsep = 0; + } + + } break; + } + } + + r_offset = { 0, style->get_offset().y + vbegin }; + r_line_spacing = line_spacing + vsep; +} + PackedStringArray Label::get_configuration_warnings() const { PackedStringArray warnings = Control::get_configuration_warnings(); @@ -361,10 +476,7 @@ PackedStringArray Label::get_configuration_warnings() const { } if (font.is_valid()) { - if (dirty || font_dirty || lines_dirty) { - const_cast<Label *>(this)->_shape(); - } - + _ensure_shaped(); const Glyph *glyph = TS->shaped_text_get_glyphs(text_rid); int64_t glyph_count = TS->shaped_text_get_glyph_count(text_rid); for (int64_t i = 0; i < glyph_count; i++) { @@ -416,22 +528,17 @@ void Label::_notification(int p_what) { } } - if (dirty || font_dirty || lines_dirty) { - _shape(); - } + _ensure_shaped(); RID ci = get_canvas_item(); bool has_settings = settings.is_valid(); Size2 string_size; - Size2 size = get_size(); Ref<StyleBox> style = theme_cache.normal_style; - Ref<Font> font = (has_settings && settings->get_font().is_valid()) ? settings->get_font() : theme_cache.font; Color font_color = has_settings ? settings->get_font_color() : theme_cache.font_color; Color font_shadow_color = has_settings ? settings->get_shadow_color() : theme_cache.font_shadow_color; Point2 shadow_ofs = has_settings ? settings->get_shadow_offset() : theme_cache.font_shadow_offset; - int line_spacing = has_settings ? settings->get_line_spacing() : theme_cache.line_spacing; Color font_outline_color = has_settings ? settings->get_outline_color() : theme_cache.font_outline_color; int outline_size = has_settings ? settings->get_outline_size() : theme_cache.font_outline_size; int shadow_outline_size = has_settings ? settings->get_shadow_size() : theme_cache.font_shadow_outline_size; @@ -440,98 +547,28 @@ void Label::_notification(int p_what) { style->draw(ci, Rect2(Point2(0, 0), get_size())); - float total_h = 0.0; - int lines_visible = 0; - - // Get number of lines to fit to the height. - for (int64_t i = lines_skipped; i < lines_rid.size(); i++) { - total_h += TS->shaped_text_get_size(lines_rid[i]).y + line_spacing; - if (total_h > (get_size().height - style->get_minimum_size().height + line_spacing)) { - break; - } - lines_visible++; - } - - if (max_lines_visible >= 0 && lines_visible > max_lines_visible) { - lines_visible = max_lines_visible; - } - - int last_line = MIN(lines_rid.size(), lines_visible + lines_skipped); bool trim_chars = (visible_chars >= 0) && (visible_chars_behavior == TextServer::VC_CHARS_AFTER_SHAPING); bool trim_glyphs_ltr = (visible_chars >= 0) && ((visible_chars_behavior == TextServer::VC_GLYPHS_LTR) || ((visible_chars_behavior == TextServer::VC_GLYPHS_AUTO) && !rtl_layout)); bool trim_glyphs_rtl = (visible_chars >= 0) && ((visible_chars_behavior == TextServer::VC_GLYPHS_RTL) || ((visible_chars_behavior == TextServer::VC_GLYPHS_AUTO) && rtl_layout)); - // Get real total height. + Vector2 ofs; + int line_limit; + int line_spacing; + get_layout_data(ofs, line_limit, line_spacing); + + int processed_glyphs = 0; int total_glyphs = 0; - total_h = 0; - for (int64_t i = lines_skipped; i < last_line; i++) { - total_h += TS->shaped_text_get_size(lines_rid[i]).y + line_spacing; + + for (int64_t i = lines_skipped; i < line_limit; i++) { total_glyphs += TS->shaped_text_get_glyph_count(lines_rid[i]) + TS->shaped_text_get_ellipsis_glyph_count(lines_rid[i]); } - int visible_glyphs = total_glyphs * visible_ratio; - int processed_glyphs = 0; - total_h += style->get_margin(SIDE_TOP) + style->get_margin(SIDE_BOTTOM); - - int vbegin = 0, vsep = 0; - if (lines_visible > 0) { - switch (vertical_alignment) { - case VERTICAL_ALIGNMENT_TOP: { - // Nothing. - } break; - case VERTICAL_ALIGNMENT_CENTER: { - vbegin = (size.y - (total_h - line_spacing)) / 2; - vsep = 0; - - } break; - case VERTICAL_ALIGNMENT_BOTTOM: { - vbegin = size.y - (total_h - line_spacing); - vsep = 0; - - } break; - case VERTICAL_ALIGNMENT_FILL: { - vbegin = 0; - if (lines_visible > 1) { - vsep = (size.y - (total_h - line_spacing)) / (lines_visible - 1); - } else { - vsep = 0; - } - } break; - } - } + int visible_glyphs = total_glyphs * visible_ratio; - Vector2 ofs; - ofs.y = style->get_offset().y + vbegin; - for (int i = lines_skipped; i < last_line; i++) { - Size2 line_size = TS->shaped_text_get_size(lines_rid[i]); - ofs.x = 0; + for (int i = lines_skipped; i < line_limit; i++) { + Vector2 line_offset = get_line_rect(i).position; + ofs.x = line_offset.x; ofs.y += TS->shaped_text_get_ascent(lines_rid[i]); - switch (horizontal_alignment) { - case HORIZONTAL_ALIGNMENT_FILL: - if (rtl && autowrap_mode != TextServer::AUTOWRAP_OFF) { - ofs.x = int(size.width - style->get_margin(SIDE_RIGHT) - line_size.width); - } else { - ofs.x = style->get_offset().x; - } - break; - case HORIZONTAL_ALIGNMENT_LEFT: { - if (rtl_layout) { - ofs.x = int(size.width - style->get_margin(SIDE_RIGHT) - line_size.width); - } else { - ofs.x = style->get_offset().x; - } - } break; - case HORIZONTAL_ALIGNMENT_CENTER: { - ofs.x = int(size.width - line_size.width) / 2; - } break; - case HORIZONTAL_ALIGNMENT_RIGHT: { - if (rtl_layout) { - ofs.x = style->get_offset().x; - } else { - ofs.x = int(size.width - style->get_margin(SIDE_RIGHT) - line_size.width); - } - } break; - } const Glyph *glyphs = TS->shaped_text_get_glyphs(lines_rid[i]); int gl_size = TS->shaped_text_get_glyph_count(lines_rid[i]); @@ -621,7 +658,7 @@ void Label::_notification(int p_what) { } } } - ofs.y += TS->shaped_text_get_descent(lines_rid[i]) + vsep + line_spacing; + ofs.y += TS->shaped_text_get_descent(lines_rid[i]) + line_spacing; } } break; @@ -637,102 +674,16 @@ void Label::_notification(int p_what) { } Rect2 Label::get_character_bounds(int p_pos) const { - if (dirty || font_dirty || lines_dirty) { - const_cast<Label *>(this)->_shape(); - } - - bool has_settings = settings.is_valid(); - Size2 size = get_size(); - Ref<StyleBox> style = theme_cache.normal_style; - int line_spacing = has_settings ? settings->get_line_spacing() : theme_cache.line_spacing; - bool rtl = (TS->shaped_text_get_inferred_direction(text_rid) == TextServer::DIRECTION_RTL); - bool rtl_layout = is_layout_rtl(); - - float total_h = 0.0; - int lines_visible = 0; - - // Get number of lines to fit to the height. - for (int64_t i = lines_skipped; i < lines_rid.size(); i++) { - total_h += TS->shaped_text_get_size(lines_rid[i]).y + line_spacing; - if (total_h > (get_size().height - style->get_minimum_size().height + line_spacing)) { - break; - } - lines_visible++; - } - - if (max_lines_visible >= 0 && lines_visible > max_lines_visible) { - lines_visible = max_lines_visible; - } - - int last_line = MIN(lines_rid.size(), lines_visible + lines_skipped); - - // Get real total height. - total_h = 0; - for (int64_t i = lines_skipped; i < last_line; i++) { - total_h += TS->shaped_text_get_size(lines_rid[i]).y + line_spacing; - } - - total_h += style->get_margin(SIDE_TOP) + style->get_margin(SIDE_BOTTOM); - - int vbegin = 0, vsep = 0; - if (lines_visible > 0) { - switch (vertical_alignment) { - case VERTICAL_ALIGNMENT_TOP: { - // Nothing. - } break; - case VERTICAL_ALIGNMENT_CENTER: { - vbegin = (size.y - (total_h - line_spacing)) / 2; - vsep = 0; - - } break; - case VERTICAL_ALIGNMENT_BOTTOM: { - vbegin = size.y - (total_h - line_spacing); - vsep = 0; - - } break; - case VERTICAL_ALIGNMENT_FILL: { - vbegin = 0; - if (lines_visible > 1) { - vsep = (size.y - (total_h - line_spacing)) / (lines_visible - 1); - } else { - vsep = 0; - } - - } break; - } - } + _ensure_shaped(); Vector2 ofs; - ofs.y = style->get_offset().y + vbegin; - for (int i = lines_skipped; i < last_line; i++) { - Size2 line_size = TS->shaped_text_get_size(lines_rid[i]); - ofs.x = 0; - switch (horizontal_alignment) { - case HORIZONTAL_ALIGNMENT_FILL: - if (rtl && autowrap_mode != TextServer::AUTOWRAP_OFF) { - ofs.x = int(size.width - style->get_margin(SIDE_RIGHT) - line_size.width); - } else { - ofs.x = style->get_offset().x; - } - break; - case HORIZONTAL_ALIGNMENT_LEFT: { - if (rtl_layout) { - ofs.x = int(size.width - style->get_margin(SIDE_RIGHT) - line_size.width); - } else { - ofs.x = style->get_offset().x; - } - } break; - case HORIZONTAL_ALIGNMENT_CENTER: { - ofs.x = int(size.width - line_size.width) / 2; - } break; - case HORIZONTAL_ALIGNMENT_RIGHT: { - if (rtl_layout) { - ofs.x = style->get_offset().x; - } else { - ofs.x = int(size.width - style->get_margin(SIDE_RIGHT) - line_size.width); - } - } break; - } + int line_limit; + int line_spacing; + get_layout_data(ofs, line_limit, line_spacing); + + for (int i = lines_skipped; i < line_limit; i++) { + Rect2 line_rect = get_line_rect(i); + ofs.x = line_rect.position.x; int v_size = TS->shaped_text_get_glyph_count(lines_rid[i]); const Glyph *glyphs = TS->shaped_text_get_glyphs(lines_rid[i]); @@ -746,22 +697,19 @@ Rect2 Label::get_character_bounds(int p_pos) const { } Rect2 rect; rect.position = ofs + Vector2(gl_off, 0); - rect.size = Vector2(advance, TS->shaped_text_get_size(lines_rid[i]).y); + rect.size = Vector2(advance, line_rect.size.y); return rect; } } gl_off += glyphs[j].advance * glyphs[j].repeat; } - ofs.y += TS->shaped_text_get_ascent(lines_rid[i]) + TS->shaped_text_get_descent(lines_rid[i]) + vsep + line_spacing; + ofs.y += TS->shaped_text_get_ascent(lines_rid[i]) + TS->shaped_text_get_descent(lines_rid[i]) + line_spacing; } return Rect2(); } Size2 Label::get_minimum_size() const { - // don't want to mutable everything - if (dirty || font_dirty || lines_dirty) { - const_cast<Label *>(this)->_shape(); - } + _ensure_shaped(); Size2 min_size = minsize; @@ -798,10 +746,7 @@ int Label::get_line_count() const { if (!is_inside_tree()) { return 1; } - if (dirty || font_dirty || lines_dirty) { - const_cast<Label *>(this)->_shape(); - } - + _ensure_shaped(); return lines_rid.size(); } @@ -1104,10 +1049,7 @@ int Label::get_max_lines_visible() const { } int Label::get_total_character_count() const { - if (dirty || font_dirty || lines_dirty) { - const_cast<Label *>(this)->_shape(); - } - + _ensure_shaped(); return xl_text.length(); } diff --git a/scene/gui/label.h b/scene/gui/label.h index e0ebca944a..2576d21c33 100644 --- a/scene/gui/label.h +++ b/scene/gui/label.h @@ -91,11 +91,16 @@ private: int font_shadow_outline_size; } theme_cache; + void _ensure_shaped() const; void _update_visible(); void _shape(); void _invalidate(); protected: + RID get_line_rid(int p_line) const; + Rect2 get_line_rect(int p_line) const; + void get_layout_data(Vector2 &r_offset, int &r_line_limit, int &r_line_spacing) const; + void _notification(int p_what); static void _bind_methods(); #ifndef DISABLE_DEPRECATED diff --git a/tests/core/string/test_fuzzy_search.h b/tests/core/string/test_fuzzy_search.h new file mode 100644 index 0000000000..d647ebdd1a --- /dev/null +++ b/tests/core/string/test_fuzzy_search.h @@ -0,0 +1,83 @@ +/**************************************************************************/ +/* test_fuzzy_search.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef TEST_FUZZY_SEARCH_H +#define TEST_FUZZY_SEARCH_H + +#include "core/string/fuzzy_search.h" +#include "tests/test_macros.h" + +namespace TestFuzzySearch { + +struct FuzzySearchTestCase { + String query; + String expected; +}; + +// Ideally each of these test queries should represent a different aspect, and potentially bottleneck, of the search process. +const FuzzySearchTestCase test_cases[] = { + // Short query, many matches, few adjacent characters + { "///gd", "./menu/hud/hud.gd" }, + // Filename match with typo + { "sm.png", "./entity/blood_sword/sam.png" }, + // Multipart filename word matches + { "ham ", "./entity/game_trap/ha_missed_me.wav" }, + // Single word token matches + { "push background", "./entity/background_zone1/background/push.png" }, + // Long token matches + { "background_freighter background png", "./entity/background_freighter/background/background.png" }, + // Many matches, many short tokens + { "menu menu characters wav", "./menu/menu/characters/smoker/0.wav" }, + // Maximize total matches + { "entity gd", "./entity/entity_man.gd" } +}; + +Vector<String> load_test_data() { + Ref<FileAccess> fp = FileAccess::open(TestUtils::get_data_path("fuzzy_search/project_dir_tree.txt"), FileAccess::READ); + REQUIRE(fp.is_valid()); + return fp->get_as_utf8_string().split("\n"); +} + +TEST_CASE("[FuzzySearch] Test fuzzy search results") { + FuzzySearch search; + Vector<FuzzySearchResult> results; + Vector<String> targets = load_test_data(); + + for (FuzzySearchTestCase test_case : test_cases) { + search.set_query(test_case.query); + search.search_all(targets, results); + CHECK_GT(results.size(), 0); + CHECK_EQ(results[0].target, test_case.expected); + } +} + +} //namespace TestFuzzySearch + +#endif // TEST_FUZZY_SEARCH_H diff --git a/tests/core/string/test_string.h b/tests/core/string/test_string.h index 49afc55c64..9adc97e845 100644 --- a/tests/core/string/test_string.h +++ b/tests/core/string/test_string.h @@ -389,6 +389,19 @@ TEST_CASE("[String] Find") { MULTICHECK_STRING_INT_EQ(s, rfind, "", 15, -1); } +TEST_CASE("[String] Find character") { + String s = "racecar"; + CHECK_EQ(s.find_char('r'), 0); + CHECK_EQ(s.find_char('r', 1), 6); + CHECK_EQ(s.find_char('e'), 3); + CHECK_EQ(s.find_char('e', 4), -1); + + CHECK_EQ(s.rfind_char('r'), 6); + CHECK_EQ(s.rfind_char('r', 5), 0); + CHECK_EQ(s.rfind_char('e'), 3); + CHECK_EQ(s.rfind_char('e', 2), -1); +} + TEST_CASE("[String] Find case insensitive") { String s = "Pretty Whale Whale"; MULTICHECK_STRING_EQ(s, findn, "WHA", 7); @@ -1254,6 +1267,12 @@ TEST_CASE("[String] is_subsequence_of") { CHECK(String("Sub").is_subsequence_ofn(a)); } +TEST_CASE("[String] is_lowercase") { + CHECK(String("abcd1234 !@#$%^&*()_-=+,.<>/\\|[]{};':\"`~").is_lowercase()); + CHECK(String("").is_lowercase()); + CHECK(!String("abc_ABC").is_lowercase()); +} + TEST_CASE("[String] match") { CHECK(String("img1.png").match("*.png")); CHECK(!String("img1.jpeg").match("*.png")); diff --git a/tests/data/fuzzy_search/project_dir_tree.txt b/tests/data/fuzzy_search/project_dir_tree.txt new file mode 100644 index 0000000000..153dd8802a --- /dev/null +++ b/tests/data/fuzzy_search/project_dir_tree.txt @@ -0,0 +1,999 @@ +./menu/home/home_menu.tscn +./menu/tooltips/tooltip_server.tscn +./menu/tooltips/tooltip_server.gd +./menu/tooltips/tooltip.gd +./menu/menu/characters/smoker/4.wav +./menu/menu/characters/smoker/6.wav +./menu/menu/characters/smoker/10.wav +./menu/menu/characters/smoker/smoker.tscn +./menu/menu/characters/smoker/8.wav +./menu/menu/characters/smoker/type.gd +./menu/menu/characters/smoker/9.wav +./menu/menu/characters/smoker/5.wav +./menu/menu/characters/smoker/0.wav +./menu/menu/characters/smoker/back_light.png +./menu/menu/characters/smoker/glasses.png +./menu/menu/characters/smoker/smoker.gd +./menu/menu/characters/smoker/cig.gd +./menu/menu/characters/smoker/eyes.png +./menu/menu/characters/smoker/3.wav +./menu/menu/characters/smoker/to_pixelate.gd +./menu/menu/characters/smoker/7.wav +./menu/menu/characters/smoker/cig.png +./menu/menu/characters/smoker/2.wav +./menu/menu/characters/smoker/1.wav +./menu/menu/characters/smoke.png +./menu/menu/characters/space_bandit.tres +./menu/menu/characters/dead_guy/blood_texture.png +./menu/menu/characters/dead_guy/head_gibbed.png +./menu/menu/characters/dead_guy/back_light.png +./menu/menu/characters/dead_guy/smoker.gd +./menu/menu/characters/dead_guy/eyes.png +./menu/menu/characters/dead_guy/to_pixelate.gd +./menu/menu/characters/dead_guy/dead_guy.gd +./menu/menu/characters/dead_guy/eyes.gd +./menu/menu/characters/dead_guy/x.png +./menu/menu/characters/dead_guy/dead_guy.tscn +./menu/menu/characters/dead_guy/mouth.png +./menu/menu/characters/dead_guy/dead_guy.tres +./menu/menu/characters/Label.gd +./menu/menu/characters/guns2.png +./menu/menu/characters/c.gd +./menu/menu/characters/smoke.gd +./menu/menu/characters/character.gd +./menu/menu/characters/space_bandit/eyes.tres +./menu/menu/characters/space_bandit/space_bandit_face_happy.png +./menu/menu/characters/space_bandit/space_bandit.gd +./menu/menu/characters/space_bandit/space_bandit.tscn +./menu/menu/characters/boss/smoker.tscn +./menu/menu/characters/boss/back_light.png +./menu/menu/characters/boss/glasses.png +./menu/menu/characters/boss/smoker.gd +./menu/menu/characters/boss/cig.gd +./menu/menu/characters/boss/eyes.png +./menu/menu/characters/boss/to_pixelate.gd +./menu/menu/characters/boss/x.png +./menu/menu/characters/boss/cig.png +./menu/menu/characters/eye.gd +./menu/menu/characters/space_bandit_face_happy.png +./menu/menu/characters/face.gd +./menu/menu/characters/color.tres +./menu/menu/characters/space_bandit.tscn +./menu/menu/characters/space_bandit_face_bloody.png +./menu/menu/characters/guns.png +./menu/menu/characters/eyes2.tres +./menu/options/controls/use.tres +./menu/options/controls/input_map_button.gd +./menu/options/controls/swap.tres +./menu/options/controls/teleport.tres +./menu/options/controls/joy_controls.tscn +./menu/options/controls/mouse_and_keyboard_controls.tscn +./menu/options/controls/input_map_button.tscn +./menu/options/controls/special.tres +./menu/options/controls/throw.tres +./menu/options/controls/center.tres +./menu/options/controls/input_action.gd +./menu/options/controls/move.tres +./menu/options/controls/melee.tres +./menu/options/controls/controls.gd +./menu/options/options.gd +./menu/options/options.tscn +./menu/options/graphics/graphics.tscn +./menu/options/graphics/graphics.gd +./menu/options/audio/audio.gd +./menu/options/audio/audio.tscn +./menu/options/game/game.gd +./menu/options/game/game.tscn +./menu/circle.tres +./menu/fonts/keys.png +./menu/fonts/rainbow_font.tres +./menu/fonts/fallback_font.tres +./menu/fonts/taxi_Driver.png +./menu/fonts/NotoSansJP-Regular.ttf +./menu/fonts/taxi_Driver_noise.png +./menu/fonts/rainbow_font_shader.tres +./menu/fonts/m5x7.ttf +./menu/colors.gd +./menu/toast_enter.wav +./menu/ui_colors.tres +./menu/pause/pause.gd +./menu/pause/rainbow.tres +./menu/pause/Label.gd +./menu/pause/label.tscn +./menu/pause/pause.tscn +./menu/hoola.wav +./menu/in_game_fallback.tres +./menu/widgets/next_unlock.gd +./menu/widgets/slider.gd +./menu/widgets/fade.tscn +./menu/widgets/background_hint.gd +./menu/widgets/panel_container_smoke.gd +./menu/widgets/wishlist_sticker.gd +./menu/widgets/smoke.tres +./menu/widgets/color_grade.gd +./menu/widgets/rich_text_button.gd +./menu/widgets/panel_container_smok2.tscn +./menu/widgets/slider.tscn +./menu/widgets/rich_text_heading.gd +./menu/widgets/background_hint.tscn +./menu/widgets/tip.tscn +./menu/widgets/rich_text_button.tscn +./menu/widgets/toggle.tscn +./menu/widgets/heading.tscn +./menu/widgets/hover.tscn +./menu/widgets/toggle.gd +./menu/widgets/smoke_panel_material.tres +./menu/widgets/confirm.gd +./menu/widgets/tip.gd +./menu/widgets/panel.gd +./menu/widgets/modal.gd +./menu/widgets/NinePatchRect.gd +./menu/widgets/smoke.shader +./menu/widgets/9patch.png +./menu/widgets/big_hint.gd +./menu/widgets/TDVB1i.png +./menu/widgets/color_grade.tscn +./menu/widgets/text.gd +./menu/widgets/panel_container_smoke.tscn +./menu/widgets/1x1.png +./menu/widgets/confirm.tscn +./menu/widgets/RichTextPanel.tscn +./menu/hud/cursor.png +./menu/hud/inventory/draggable.gd +./menu/hud/inventory/menu/characters/color.tres +./menu/hud/inventory/drop_zone.tscn +./menu/hud/inventory/RichTextLabel.gd +./menu/hud/inventory/hud_icon_mutation.tscn +./menu/hud/inventory/use_count.gd +./menu/hud/inventory/draggable.tscn +./menu/hud/inventory/black_shadow_font.tres +./menu/hud/inventory/x.png +./menu/hud/inventory/hud_icon_mutation.gd +./menu/hud/inventory/flash_parent.gd +./menu/hud/inventory/TextureRect4.gd +./menu/hud/cursor.tscn +./menu/hud/hud.tscn +./menu/hud/cursor.gd +./menu/hud/hud.gd +./menu/metal_text.tres +./menu/rich_text_effects/RichTextType.gd +./menu/rich_text_effects/RichTextPanel.gd +./menu/rich_text_effects/RichTextFlash.gd +./menu/rich_text_effects/RichTextTranslate.gd +./menu/in_game.tres +./menu/lcd_screen_font.tres +./menu/toast_exit.wav +./menu/stack/ahses_material.tres +./menu/stack/home.kra +./menu/stack/fade.gd +./menu/stack/stack.tscn +./menu/stack/stack.gd +./menu/stack/version.gd +./menu/stack/art.kra +./entity/unlock_skin_classic/icon.png +./entity/use.gd +./entity/chair/entity.tscn +./entity/chair/icon.png +./entity/chair/data.gd +./entity/man_desert/entity.tscn +./entity/man_desert/icon.png +./entity/man_desert/teleprompts/need_medbay.wav +./entity/man_desert/teleprompts/me_too.wav +./entity/man_desert/teleprompts/get_up_alt.wav +./entity/man_desert/teleprompts/getting_a_medpack.wav +./entity/man_desert/teleprompts/firstaid-incoming.wav +./entity/man_desert/teleprompts/batch_name.py +./entity/man_desert/teleprompts/what.wav +./entity/man_desert/teleprompts/oo.wav +./entity/man_desert/teleprompts/yell.wav +./entity/man_desert/teleprompts/rushing.wav +./entity/man_desert/teleprompts/ooo.wav +./entity/man_desert/teleprompts/coming_to_heal_ya.wav +./entity/man_desert/teleprompts/where_is_the_medpack.wav +./entity/man_desert/teleprompts/ah.wav +./entity/man_desert/teleprompts/no.wav +./entity/man_desert/teleprompts/going_to_camp_medbay.wav +./entity/man_desert/teleprompts/aa.wav +./entity/man_desert/teleprompts/pirate_alt.wav +./entity/man_desert/teleprompts/take_morphine.wav +./entity/man_desert/teleprompts/ee.wav +./entity/man_desert/teleprompts/get_up.wav +./entity/man_desert/teleprompts/aw.wav +./entity/man_desert/teleprompts/easy.wav +./entity/man_desert/teleprompts/intruder.wav +./entity/man_desert/teleprompts/amateur.wav +./entity/man_desert/teleprompts/hes_not_moving.wav +./entity/man_desert/teleprompts/pirate.wav +./entity/man_desert/teleprompts/i_dont_know.wav +./entity/man_desert/teleprompts/index.txt +./entity/man_desert/teleprompts/move.wav +./entity/man_desert/teleprompts/hes_stuck.wav +./entity/man_desert/teleprompts/how.wav +./entity/man_desert/teleprompts/uu.wav +./entity/man_desert/teleprompts/where_is_the_gun.wav +./entity/man_desert/teleprompts/getting_a_gun.wav +./entity/man_desert/data.gd +./entity/man_desert/hand.png +./entity/barrel_side_smoke/entity.tscn +./entity/barrel_side_smoke/icon.png +./entity/barrel_side_smoke/data.gd +./entity/barrel_smoke/entity.tscn +./entity/barrel_smoke/icon.png +./entity/barrel_smoke/data.gd +./entity/project_box/entity.tscn +./entity/project_box/icon.png +./entity/project_box/data.gd +./entity/mutation_saw/entity.tscn +./entity/mutation_saw/icon.png +./entity/mutation_saw/special.gd +./entity/mutation_saw/data.gd +./entity/lift_entrance/entity.tscn +./entity/lift_entrance/icon.png +./entity/lift_entrance/special.gd +./entity/lift_entrance/data.gd +./entity/mutation_accuracy_boost_DELETE/entity.tscn +./entity/mutation_accuracy_boost_DELETE/icon.png +./entity/mutation_accuracy_boost_DELETE/special.gd +./entity/mutation_accuracy_boost_DELETE/data.gd +./entity/skin_ruffle/entity.tscn +./entity/skin_ruffle/icon.png +./entity/skin_ruffle/carried.png +./entity/skin_ruffle/data.gd +./entity/editor_only_icon.gd +./entity/console_dark/entity.tscn +./entity/console_dark/icon.png +./entity/console_dark/data.gd +./entity/console_dark/animation.png +./entity/smg2/entity.tscn +./entity/smg2/used.wav +./entity/smg2/icon.png +./entity/smg2/data.gd +./entity/smg2/debug.gd +./entity/grenade_launcher/entity.tscn +./entity/grenade_launcher/used.wav +./entity/grenade_launcher/icon.png +./entity/grenade_launcher/special.gd +./entity/grenade_launcher/data.gd +./entity/floor_tile_full_square/entity.tscn +./entity/floor_tile_full_square/icon.png +./entity/floor_tile_full_square/data.gd +./entity/grate_1/entity.tscn +./entity/grate_1/icon.png +./entity/grate_1/data.gd +./entity/bed_bunk_corner/entity.tscn +./entity/bed_bunk_corner/icon.png +./entity/bed_bunk_corner/data.gd +./entity/kill_streak_rail_gun_level_3/entity.tscn +./entity/kill_streak_rail_gun_level_3/data.gd +./entity/teleporter_random_weak/entity.tscn +./entity/teleporter_random_weak/teleporter_model.gd +./entity/teleporter_random_weak/used.wav +./entity/teleporter_random_weak/icon.png +./entity/teleporter_random_weak/special.gd +./entity/teleporter_random_weak/ray.gd +./entity/teleporter_random_weak/data.gd +./entity/teleporter_random_weak/flap.png +./entity/entities.kra +./entity/jerry_can/entity.tscn +./entity/jerry_can/icon.png +./entity/jerry_can/data.gd +./entity/kill_streak_helmet_full/entity.tscn +./entity/kill_streak_helmet_full/data.gd +./entity/background_derelict/background2.gd +./entity/background_derelict/entity.tscn +./entity/background_derelict/icon.png +./entity/background_derelict/background/space.png +./entity/background_derelict/background/line.png +./entity/background_derelict/background/overlay.png +./entity/background_derelict/background/background2.png +./entity/background_derelict/background/background.png +./entity/background_derelict/background/engine_glow.tscn +./entity/background_derelict/background/lines3.png +./entity/background_derelict/background/background.tscn +./entity/background_derelict/background/lines.tres +./entity/background_derelict/background/xx.gd +./entity/background_derelict/background/background.gd +./entity/background_derelict/background/bayer16tile2.png +./entity/background_derelict/background/push.png +./entity/background_derelict/background/palette_mono.png +./entity/background_derelict/background/stars.gd +./entity/background_derelict/background/lines2.png +./entity/background_derelict/background/lines.shader +./entity/background_derelict/background/ambience.gd +./entity/background_derelict/background/space_ship_ambience.ogg +./entity/background_derelict/background/stars.png +./entity/background_derelict/data.gd +./entity/smoker/entity.tscn +./entity/smoker/right_hand.png +./entity/smoker/eyes.png +./entity/smoker/data.gd +./entity/smoker/animate.gd +./entity/smoker/left_hand.png +./entity/EntityStatic.gd +./entity/level_model.gd +./entity/class_teleporter_drop_chance/entity.tscn +./entity/class_teleporter_drop_chance/icon.png +./entity/class_teleporter_drop_chance/special.gd +./entity/class_teleporter_drop_chance/data.gd +./entity/smg4/entity.tscn +./entity/smg4/used.wav +./entity/smg4/icon.png +./entity/smg4/data.gd +./entity/medpack/entity.tscn +./entity/medpack/icon.png +./entity/medpack/dead.png +./entity/medpack/data.gd +./entity/model.gd +./entity/doom_transition/entity.tscn +./entity/doom_transition/icon.png +./entity/doom_transition/special.gd +./entity/doom_transition/Screenshot from 2021-12-08 18-25-03.png +./entity/doom_transition/data.gd +./entity/glass_block_exploding/entity.tscn +./entity/glass_block_exploding/icon.png +./entity/glass_block_exploding/special.gd +./entity/glass_block_exploding/dead.png +./entity/glass_block_exploding/data.gd +./entity/floor_ting/entity.tscn +./entity/floor_ting/icon.png +./entity/floor_ting/data.gd +./entity/background_crashed_ship/entity.tscn +./entity/background_crashed_ship/icon.png +./entity/background_crashed_ship/background/background2.kra +./entity/background_crashed_ship/background/dust_storm_negative.png +./entity/background_crashed_ship/background/background2.png +./entity/background_crashed_ship/background/background2 (copy 1).png +./entity/background_crashed_ship/background/dust_bowl.ogg +./entity/background_crashed_ship/background/background.tscn +./entity/background_crashed_ship/background/background.kra +./entity/background_crashed_ship/data.gd +./entity/game_aim_hack_boss/entity.tscn +./entity/game_aim_hack_boss/icon.png +./entity/game_aim_hack_boss/special.gd +./entity/game_aim_hack_boss/give_my_arm_back.wav +./entity/game_aim_hack_boss/my_arm_came_off.wav +./entity/game_aim_hack_boss/data.gd +./entity/sink/entity.tscn +./entity/sink/icon.png +./entity/sink/data.gd +./entity/grate_2/entity.tscn +./entity/grate_2/icon.png +./entity/grate_2/data.gd +./entity/barrel_side/entity.tscn +./entity/barrel_side/icon.png +./entity/barrel_side/data.gd +./entity/oxygen/entity.tscn +./entity/oxygen/icon.png +./entity/oxygen/shadow.png +./entity/oxygen/data.gd +./entity/oxygen/normal.png +./entity/unlock_skin_robo/entity.tscn +./entity/unlock_skin_robo/icon.png +./entity/unlock_skin_robo/special.gd +./entity/unlock_skin_robo/data.gd +./entity/entity_agency_model.gd +./entity/floor_tile_wood/entity.tscn +./entity/floor_tile_wood/icon.png +./entity/floor_tile_wood/data.gd +./entity/qr_code/entity.tscn +./entity/qr_code/icon.png +./entity/qr_code/data.gd +./entity/background_sun/overlay.png +./entity/background_sun/entity.tscn +./entity/background_sun/c.gd +./entity/background_sun/kill.tscn +./entity/background_sun/icon.png +./entity/background_sun/special.gd +./entity/background_sun/wtf.tres +./entity/background_sun/background/background2.png +./entity/background_sun/background/background.tscn +./entity/background_sun/background/color2s.tres +./entity/background_sun/background/background_glow.png +./entity/background_sun/data.gd +./entity/background_sun/kill.gd +./entity/background_sun/stars.png +./entity/background_zone_intro/overlay.png +./entity/background_zone_intro/entity.tscn +./entity/background_zone_intro/icon.png +./entity/background_zone_intro/special.gd +./entity/background_zone_intro/background/space.png +./entity/background_zone_intro/background/line.png +./entity/background_zone_intro/background/background2.png +./entity/background_zone_intro/background/background.png +./entity/background_zone_intro/background/engine_glow.tscn +./entity/background_zone_intro/background/lines3.png +./entity/background_zone_intro/background/background.tscn +./entity/background_zone_intro/background/lines.tres +./entity/background_zone_intro/background/background.gd +./entity/background_zone_intro/background/bayer16tile2.png +./entity/background_zone_intro/background/push.png +./entity/background_zone_intro/background/palette_mono.png +./entity/background_zone_intro/background/stars.gd +./entity/background_zone_intro/background/lines2.png +./entity/background_zone_intro/background/lines.shader +./entity/background_zone_intro/background/ambience.gd +./entity/background_zone_intro/background/space_ship_ambience.ogg +./entity/background_zone_intro/background/stars.png +./entity/background_zone_intro/background_end.png +./entity/background_zone_intro/data.gd +./entity/background_zone_intro/tinge.png +./entity/closet_alt/entity.tscn +./entity/closet_alt/icon.png +./entity/closet_alt/data.gd +./entity/meta_random_sound/entity.tscn +./entity/meta_random_sound/giberish.wav +./entity/meta_random_sound/icon.png +./entity/meta_random_sound/special.gd +./entity/meta_random_sound/who.wav +./entity/meta_random_sound/data.gd +./entity/meta_random_sound/hoola_boola.wav +./entity/meta_random_sound/space_bandit.wav +./entity/lines/entity.tscn +./entity/lines/icon.png +./entity/lines/data.gd +./entity/teleporter_random_avoid_ray/entity.tscn +./entity/teleporter_random_avoid_ray/used.wav +./entity/teleporter_random_avoid_ray/icon.png +./entity/teleporter_random_avoid_ray/ray.gd +./entity/teleporter_random_avoid_ray/data.gd +./entity/teleporter_random_avoid_ray/flap.png +./entity/teleporter_random_avoid_ray/RayCast2D.gd +./entity/teleporter_random_avoid_ray/area.gd +./entity/teleporter_random_avoid_ray/flap.gd +./entity/saw/blades.gd +./entity/saw/entity.tscn +./entity/saw/used.wav +./entity/saw/icon.png +./entity/saw/special.gd +./entity/saw/carried.png +./entity/saw/data.gd +./entity/saw/used (copy 1).wav +./entity/saw/saw.wav +./entity/saw/carried_blades.png +./entity/floor_tile_checkerdboard/damage.png +./entity/floor_tile_checkerdboard/entity.tscn +./entity/floor_tile_checkerdboard/icon.png +./entity/floor_tile_checkerdboard/entity.tres +./entity/floor_tile_checkerdboard/data.gd +./entity/mutation_smoke_grenade_upgrade/entity.tscn +./entity/mutation_smoke_grenade_upgrade/icon.png +./entity/mutation_smoke_grenade_upgrade/special.gd +./entity/mutation_smoke_grenade_upgrade/data.gd +./entity/mutation_smoke_grenade_upgrade/mutation_model.gd +./entity/helmet_full/entity.tscn +./entity/helmet_full/pick_up.wav +./entity/helmet_full/icon.png +./entity/helmet_full/data.gd +./entity/helmet_full/helmet-ping.wav +./entity/barrel_explosive/entity.tscn +./entity/barrel_explosive/icon.png +./entity/barrel_explosive/data.gd +./entity/bank/entity.tscn +./entity/bank/icon.png +./entity/bank/special.gd +./entity/bank/data.gd +./entity/kick/entity.tscn +./entity/kick/swipe.png +./entity/kick/used.wav +./entity/kick/icon.png +./entity/kick/AnimatedSprite.gd +./entity/kick/data.gd +./entity/battery/entity.tscn +./entity/battery/icon.png +./entity/battery/data.gd +./entity/lift/entity.tscn +./entity/lift/opening.wav +./entity/lift/doors_open.png +./entity/lift/RichTextLabel.gd +./entity/lift/icon.png +./entity/lift/open.wav +./entity/lift/elevator_end.wav +./entity/lift/lift_model.gd +./entity/lift/label.tscn +./entity/lift/rumble.gd +./entity/lift/level_portal_model.gd +./entity/lift/data.gd +./entity/lift/doors.png +./entity/lift/area.gd +./entity/snes/entity.tscn +./entity/snes/icon.png +./entity/snes/data.gd +./entity/passive_disarm/entity.tscn +./entity/passive_disarm/icon.png +./entity/passive_disarm/special.gd +./entity/passive_disarm/data.gd +./entity/mutation_lots_of_shot/entity.tscn +./entity/mutation_lots_of_shot/icon.png +./entity/mutation_lots_of_shot/special.gd +./entity/mutation_lots_of_shot/data.gd +./entity/pallet2/entity.tscn +./entity/pallet2/icon.png +./entity/pallet2/data.gd +./entity/kill_streak_sword/entity.tscn +./entity/kill_streak_sword/data.gd +./entity/rain/entity.tscn +./entity/rain/icon.png +./entity/rain/special.gd +./entity/rain/rain.png +./entity/rain/rain.tscn +./entity/rain/data.gd +./entity/rain/rain.gd +./entity/white_line/entity.tscn +./entity/white_line/icon.png +./entity/white_line/data.gd +./entity/game_break_sword/entity.tscn +./entity/game_break_sword/icon.png +./entity/game_break_sword/special.gd +./entity/game_break_sword/data.gd +./entity/background_zone1/overlay.png +./entity/background_zone1/entity.tscn +./entity/background_zone1/icon.png +./entity/background_zone1/special.gd +./entity/background_zone1/background/space.png +./entity/background_zone1/background/line.png +./entity/background_zone1/background/background2.png +./entity/background_zone1/background/background.png +./entity/background_zone1/background/engine_glow.tscn +./entity/background_zone1/background/lines3.png +./entity/background_zone1/background/background.tscn +./entity/background_zone1/background/lines.tres +./entity/background_zone1/background/background.gd +./entity/background_zone1/background/bayer16tile2.png +./entity/background_zone1/background/push.png +./entity/background_zone1/background/palette_mono.png +./entity/background_zone1/background/stars.gd +./entity/background_zone1/background/lines2.png +./entity/background_zone1/background/lines.shader +./entity/background_zone1/background/ambience.gd +./entity/background_zone1/background/space_ship_ambience.ogg +./entity/background_zone1/background/stars.png +./entity/background_zone1/data.gd +./entity/background_zone1/tinge.png +./entity/mutation_throw_trap_DELETE/entity.tscn +./entity/mutation_throw_trap_DELETE/icon.png +./entity/mutation_throw_trap_DELETE/special.gd +./entity/mutation_throw_trap_DELETE/data.gd +./entity/agency.gd +./entity/skin_cheese/entity.tscn +./entity/skin_cheese/icon.png +./entity/skin_cheese/carried.png +./entity/skin_cheese/data.gd +./entity/toilet/entity.tscn +./entity/toilet/icon.png +./entity/toilet/special.gd +./entity/toilet/water.png +./entity/toilet/drink.wav +./entity/toilet/data.gd +./entity/smg3/entity.tscn +./entity/smg3/used.wav +./entity/smg3/icon.png +./entity/smg3/dead.png +./entity/smg3/data.gd +./entity/smg3/debug.gd +./entity/teleporter_super/entity.tscn +./entity/teleporter_super/icon.png +./entity/teleporter_super/data.gd +./entity/background_zone_end/overlay.png +./entity/background_zone_end/entity.tscn +./entity/background_zone_end/icon.png +./entity/background_zone_end/special.gd +./entity/background_zone_end/stars2.png +./entity/background_zone_end/background_end.png +./entity/background_zone_end/data.gd +./entity/background_zone_end/tinge.png +./entity/kill_streak_barricade/entity.tscn +./entity/kill_streak_barricade/data.gd +./entity/game_zone_4_boss_1/entity.tscn +./entity/game_zone_4_boss_1/icon.png +./entity/game_zone_4_boss_1/special.gd +./entity/game_zone_4_boss_1/data.gd +./entity/game_zone_4_boss_1/kill_me_and_explode_ship.wav +./entity/mutation_remove_melee/entity.tscn +./entity/mutation_remove_melee/icon.png +./entity/mutation_remove_melee/special.gd +./entity/mutation_remove_melee/data.gd +./entity/he_grenade_level_2/entity.tscn +./entity/he_grenade_level_2/icon.png +./entity/he_grenade_level_2/data.gd +./entity/background_zone_2/entity.tscn +./entity/background_zone_2/icon.png +./entity/background_zone_2/background/background2.kra +./entity/background_zone_2/background/grad.png +./entity/background_zone_2/background/background2.png +./entity/background_zone_2/background/background.png +./entity/background_zone_2/background/background2 (copy 1).png +./entity/background_zone_2/background/backgrounds.gd +./entity/background_zone_2/background/wall_overlay.png +./entity/background_zone_2/background/background.tscn +./entity/background_zone_2/background/Screenshot from 2022-07-07 10-58-48.png +./entity/background_zone_2/background/background.gd +./entity/background_zone_2/background/shadow.png +./entity/background_zone_2/background/engine smoke.png +./entity/background_zone_2/background/background.kra +./entity/background_zone_2/background/sea.ogg +./entity/background_zone_2/background/background2blur.png +./entity/background_zone_2/background/test.gd +./entity/background_zone_2/background/grad3.png +./entity/background_zone_2/background/lines2.png +./entity/background_zone_2/background/smoke.tscn +./entity/background_zone_2/background/left_water.tscn +./entity/background_zone_2/background/grad2.png +./entity/background_zone_2/background/para.png +./entity/background_zone_2/data.gd +./entity/pipe_corner/entity.tscn +./entity/pipe_corner/icon.png +./entity/pipe_corner/data.gd +./entity/floor_tile_metal_cow_trap/entity.tscn +./entity/floor_tile_metal_cow_trap/icon.png +./entity/floor_tile_metal_cow_trap/data.gd +./entity/skin_naked/entity.tscn +./entity/skin_naked/icon.png +./entity/skin_naked/carried.png +./entity/skin_naked/data.gd +./entity/valve/entity.tscn +./entity/valve/icon.png +./entity/valve/.icon.png-autosave.kra +./entity/valve/data.gd +./entity/bed/entity.tscn +./entity/bed/icon.png +./entity/bed/data.gd +./entity/game_invisible_guy/entity.tscn +./entity/game_invisible_guy/icon.png +./entity/game_invisible_guy/special.gd +./entity/game_invisible_guy/data.gd +./entity/smg/entity.tscn +./entity/smg/used.wav +./entity/smg/icon.png +./entity/smg/data.gd +./entity/skin_robo/entity.tscn +./entity/skin_robo/icon.png +./entity/skin_robo/carried.png +./entity/skin_robo/data.gd +./entity/bandana/entity.tscn +./entity/bandana/bob.gd +./entity/bandana/icon.png +./entity/bandana/special.gd +./entity/bandana/carried.png +./entity/bandana/data.gd +./entity/bandana/pixel.png +./entity/floor_plug/entity.tscn +./entity/floor_plug/icon.png +./entity/floor_plug/data.gd +./entity/bench/entity.tscn +./entity/bench/icon.png +./entity/bench/data.gd +./entity/meta_strip_items/entity.tscn +./entity/meta_strip_items/special.gd +./entity/meta_strip_items/meta_strip_items_model.gd +./entity/meta_strip_items/data.gd +./entity/crate_teleporter/entity.tscn +./entity/crate_teleporter/icon.png +./entity/crate_teleporter/data.gd +./entity/crate_teleporter/satellite.kra +./entity/crate_garbage/entity.tscn +./entity/crate_garbage/icon.png +./entity/crate_garbage/data.gd +./entity/crate_garbage/gibbed.png +./entity/meta_stats/entity.tscn +./entity/meta_stats/letters.tres +./entity/meta_stats/icon.png +./entity/meta_stats/special.gd +./entity/meta_stats/data.gd +./entity/meta_stats/meta_stats_model.gd +./entity/rail_gun/entity.tscn +./entity/rail_gun/used.wav +./entity/rail_gun/icon.png +./entity/rail_gun/special.gd +./entity/rail_gun/carried.png +./entity/rail_gun/data.gd +./entity/drop_ship_door/entity.tscn +./entity/drop_ship_door/icon.png +./entity/drop_ship_door/data.gd +./entity/floor_lines/entity.tscn +./entity/floor_lines/icon.png +./entity/floor_lines/data.gd +./entity/game_trap/entity.tscn +./entity/game_trap/you_blew_up_my_force_field.wav +./entity/game_trap/droped_my_grenade_2.wav +./entity/game_trap/icon.png +./entity/game_trap/special.gd +./entity/game_trap/droped_my_grenade_0.wav +./entity/game_trap/shock.wav +./entity/game_trap/uh_my_helmet.wav +./entity/game_trap/ha_missed_me.wav +./entity/game_trap/data.gd +./entity/game_trap/try_beat_this_force_field.wav +./entity/game_trap/droped_my_grenade_1.wav +./entity/blood_sword/entity.tscn +./entity/blood_sword/pick_up.wav +./entity/blood_sword/used.wav +./entity/blood_sword/sam2.png +./entity/blood_sword/icon.png +./entity/blood_sword/special.gd +./entity/blood_sword/hit_bar.gd +./entity/blood_sword/data.gd +./entity/blood_sword/sam.png +./entity/blood_sword/dead.wav +./entity/blood_sword/animation.png +./entity/auto_cables_thick/entity.tscn +./entity/auto_cables_thick/data.gd +./entity/auto_cables_thick/wires2.png +./entity/shield/entity.tscn +./entity/shield/pick_up.wav +./entity/shield/icon.png +./entity/shield/carried.png +./entity/shield/data.gd +./entity/shield/helmet-ping.wav +./entity/game_teleport_in/entity.tscn +./entity/game_teleport_in/icon.png +./entity/game_teleport_in/special.gd +./entity/game_teleport_in/data.gd +./entity/shotgun_super/entity.tscn +./entity/shotgun_super/icon.png +./entity/shotgun_super/data.gd +./entity/bottle/entity.tscn +./entity/bottle/icon.png +./entity/bottle/data.gd +./entity/bottle/normal.png +./entity/bottle/icon_shadow.png +./entity/kill_streak_p90/entity.tscn +./entity/kill_streak_p90/data.gd +./entity/drain/entity.tscn +./entity/drain/icon.png +./entity/drain/data.gd +./entity/auto_wires_three/entity.tscn +./entity/auto_wires_three/data.gd +./entity/light/entity.tscn +./entity/light/icon.png +./entity/light/special.gd +./entity/light/light.wav +./entity/light/data.gd +./entity/debris/entity.tscn +./entity/debris/icon.png +./entity/debris/data.gd +./entity/debris/gibbed.png +./entity/mutation_rail_gun_upgrade/entity.tscn +./entity/mutation_rail_gun_upgrade/icon.png +./entity/mutation_rail_gun_upgrade/special.gd +./entity/mutation_rail_gun_upgrade/data.gd +./entity/mutation_rail_gun_upgrade/mutation_model.gd +./entity/auto_cables/entity.tscn +./entity/auto_cables/data.gd +./entity/auto_cables/wires2.png +./entity/stealth_camo/entity.tscn +./entity/stealth_camo/special.gd +./entity/stealth_camo/data.gd +./entity/colt_45/entity.tscn +./entity/colt_45/used.wav +./entity/colt_45/icon.png +./entity/colt_45/dead.png +./entity/colt_45/data.gd +./entity/quantum_suicide_drive/entity.tscn +./entity/quantum_suicide_drive/heart.ogg +./entity/quantum_suicide_drive/icon.png +./entity/quantum_suicide_drive/special.gd +./entity/quantum_suicide_drive/qsd_model.gd +./entity/quantum_suicide_drive/multi.gd +./entity/quantum_suicide_drive/multi.tscn +./entity/quantum_suicide_drive/CenterContainer.gd +./entity/quantum_suicide_drive/carried.png +./entity/quantum_suicide_drive/data.gd +./entity/helmet/entity.tscn +./entity/helmet/pick_up.wav +./entity/helmet/icon.png +./entity/helmet/special.gd +./entity/helmet/die.wav +./entity/helmet/carried.png +./entity/helmet/data.gd +./entity/helmet/helmet-ping.wav +./entity/ammo_box/entity.tscn +./entity/ammo_box/icon.png +./entity/ammo_box/data.gd +./entity/rail_gun_level_2/entity.tscn +./entity/rail_gun_level_2/icon.png +./entity/rail_gun_level_2/data.gd +./entity/glass_block_backup/entity.tscn +./entity/glass_block_backup/icon.png +./entity/glass_block_backup/data.gd +./entity/closet/entity.tscn +./entity/closet/icon.png +./entity/closet/data.gd +./entity/little_boxes/entity.tscn +./entity/little_boxes/icon.png +./entity/little_boxes/data.gd +./entity/meta_health_bar/entity.tscn +./entity/meta_health_bar/health_bar_model.gd +./entity/meta_health_bar/icon.png +./entity/meta_health_bar/special.gd +./entity/meta_health_bar/invunerable.png +./entity/meta_health_bar/data.gd +./entity/night_stand/entity.tscn +./entity/night_stand/icon_normal.png +./entity/night_stand/icon.png +./entity/night_stand/shadow.png +./entity/night_stand/data.gd +./entity/fan/entity.tscn +./entity/fan/flap2.png +./entity/fan/flaps.gd +./entity/fan/icon.png +./entity/fan/data.gd +./entity/fan/flap.png +./entity/fan/icon_shadow.png +./entity/fan/animation.png +./entity/fan/gibbed.png +./entity/game_tutorial_end/entity.tscn +./entity/game_tutorial_end/icon.png +./entity/game_tutorial_end/special.gd +./entity/game_tutorial_end/data.gd +./entity/mutation_disarmament/entity.tscn +./entity/mutation_disarmament/icon.png +./entity/mutation_disarmament/special.gd +./entity/mutation_disarmament/data.gd +./entity/air_lock/icon_open.png +./entity/air_lock/entity.tscn +./entity/air_lock/door_close.wav +./entity/air_lock/icon.png +./entity/air_lock/special.gd +./entity/air_lock/air_lock_model.gd +./entity/air_lock/data.gd +./entity/scorpion/entity.tscn +./entity/scorpion/used.wav +./entity/scorpion/laser.gd +./entity/scorpion/icon.png +./entity/scorpion/data.gd +./entity/kill_streak_aim_hack/entity.tscn +./entity/kill_streak_aim_hack/data.gd +./entity/dungeon_proc_debug/entity.tscn +./entity/dungeon_proc_debug/icon.png +./entity/dungeon_proc_debug/data.gd +./entity/dungeon_proc_debug/debug.gd +./entity/dungeon_proc_debug/debug.tscn +./entity/tarp/entity.tscn +./entity/tarp/icon.png +./entity/tarp/data.gd +./entity/hit_indicator/entity.tscn +./entity/hit_indicator/data.gd +./entity/console_corner/entity.tscn +./entity/console_corner/animation2.tscn +./entity/console_corner/icon.png +./entity/console_corner/data.gd +./entity/console_corner/animation.tscn +./entity/icon.png +./entity/couch_corner/entity.tscn +./entity/couch_corner/icon.png +./entity/couch_corner/data.gd +./entity/m4/entity.tscn +./entity/m4/used.wav +./entity/m4/icon.png +./entity/m4/data.gd +./entity/game_hud/entity.tscn +./entity/game_hud/icon.png +./entity/game_hud/data.gd +./entity/game_hud/inventory_game.tscn +./entity/prototypes.gd +./entity/agent_chicken/emotes.png +./entity/agent_chicken/entity.tscn +./entity/agent_chicken/sound_board.gd +./entity/agent_chicken/bones.tscn +./entity/agent_chicken/bones.gd +./entity/agent_chicken/barks.gd +./entity/agent_chicken/emote.gd +./entity/agent_chicken/icon.png +./entity/agent_chicken/special.gd +./entity/agent_chicken/bark.gd +./entity/agent_chicken/deaad.png +./entity/agent_chicken/icon.gd +./entity/agent_chicken/data.gd +./entity/agent_chicken/animation.tscn +./entity/agent_chicken/emote.tscn +./entity/agent_chicken/hand.png +./entity/velocity/entity.tscn +./entity/velocity/icon.png +./entity/velocity/special.gd +./entity/velocity/data.gd +./entity/aircon/entity.tscn +./entity/aircon/grate.png +./entity/aircon/icon.png +./entity/aircon/data.gd +./entity/aircon/animation.png +./entity/floor_tile_bricks/entity.tscn +./entity/floor_tile_bricks/icon.png +./entity/floor_tile_bricks/data.gd +./entity/pallet/entity.tscn +./entity/pallet/icon.png +./entity/pallet/data.gd +./entity/barricade_deployed/debug.png +./entity/barricade_deployed/field.tscn +./entity/barricade_deployed/entity.tscn +./entity/barricade_deployed/ambience.ogg +./entity/barricade_deployed/icon.png +./entity/barricade_deployed/field.gd +./entity/barricade_deployed/field_material.tres +./entity/barricade_deployed/debug2.png +./entity/barricade_deployed/data.gd +./entity/barricade_deployed/field_material_invert.tres +./entity/barricade_deployed/field_material.gd +./entity/barricade_deployed/gibbed.png +./entity/helmet_nv/entity.tscn +./entity/helmet_nv/pick_up.wav +./entity/helmet_nv/icon.png +./entity/helmet_nv/special.gd +./entity/helmet_nv/carried.png +./entity/helmet_nv/eyes.png +./entity/helmet_nv/data.gd +./entity/helmet_nv/helmet-ping.wav +./entity/helmet_nv/eyes.gd +./entity/mutation_sword/entity.tscn +./entity/mutation_sword/icon.png +./entity/mutation_sword/special.gd +./entity/mutation_sword/data.gd +./entity/field_full_super/entity.tscn +./entity/field_full_super/icon.png +./entity/field_full_super/special.gd +./entity/field_full_super/carried.png +./entity/field_full_super/data.gd +./entity/entity_man.gd +./entity/couch/entity.tscn +./entity/couch/icon.png +./entity/couch/data.gd +./entity/teleporter_lil_hunter/entity.tscn +./entity/teleporter_lil_hunter/icon.png +./entity/teleporter_lil_hunter/tubes.png +./entity/teleporter_lil_hunter/osc_shader.tres +./entity/teleporter_lil_hunter/eyes.png +./entity/teleporter_lil_hunter/data.gd +./entity/teleporter_lil_hunter/osc.tres +./entity/game_tutorial_melee_zone/entity.tscn +./entity/game_tutorial_melee_zone/icon.png +./entity/game_tutorial_melee_zone/special.gd +./entity/game_tutorial_melee_zone/data.gd +./entity/kill_streak_glock/entity.tscn +./entity/kill_streak_glock/data.gd +./entity/skin_mime/entity.tscn +./entity/skin_mime/icon.png +./entity/skin_mime/special.gd +./entity/skin_mime/carried.png +./entity/skin_mime/data.gd +./entity/medpack_hard/entity.tscn +./entity/medpack_hard/icon.png +./entity/medpack_hard/data.gd +./entity/teleporter_overload/entity.tscn +./entity/teleporter_overload/icon.png +./entity/teleporter_overload/special.gd +./entity/teleporter_overload/carried.png +./entity/teleporter_overload/data.gd +./entity/background_freighter/overlay.png +./entity/background_freighter/entity.tscn +./entity/background_freighter/icon.png +./entity/background_freighter/Master.ogg +./entity/background_freighter/background/space.png +./entity/background_freighter/background/line.png +./entity/background_freighter/background/background2.gd +./entity/background_freighter/background/good create.png +./entity/background_freighter/background/backgip.png +./entity/background_freighter/background/background2.png +./entity/background_freighter/background/background.png +./entity/background_freighter/background/engine_glow.tscn +./entity/background_freighter/background/gra2d.png +./entity/background_freighter/background/lines3.png +./entity/background_freighter/background/background.tscn +./entity/background_freighter/background/lines.tres +./entity/background_freighter/background/background.gd +./entity/background_freighter/background/bayer16tile2.png +./entity/background_freighter/background/goodcrate.png +./entity/background_freighter/background/push.png +./entity/background_freighter/background/background_floor.png +./entity/background_freighter/background/palette_mono.png +./entity/background_freighter/background/stars.gd +./entity/background_freighter/background/lines2.png +./entity/background_freighter/background/lines.shader +./entity/background_freighter/background/ambience.gd +./entity/background_freighter/background/bacsdas.png +./entity/background_freighter/background/space_ship_ambience.ogg +./entity/background_freighter/background/stars.png +./entity/background_freighter/data.gd +./entity/auto_wires/entity.tscn +./entity/auto_wires/data.gd +./entity/kill_streak/entity.tscn +./entity/kill_streak/kill_streak_toast.tscn +./entity/kill_streak/icon.png diff --git a/tests/test_main.cpp b/tests/test_main.cpp index 979aee8001..65d45ae92f 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -82,6 +82,7 @@ #include "tests/core/object/test_object.h" #include "tests/core/object/test_undo_redo.h" #include "tests/core/os/test_os.h" +#include "tests/core/string/test_fuzzy_search.h" #include "tests/core/string/test_node_path.h" #include "tests/core/string/test_string.h" #include "tests/core/string/test_translation.h" |