summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--core/string/fuzzy_search.cpp349
-rw-r--r--core/string/fuzzy_search.h101
-rw-r--r--core/string/ustring.cpp15
-rw-r--r--core/string/ustring.h4
-rw-r--r--doc/classes/EditorSettings.xml12
-rw-r--r--editor/editor_settings.cpp4
-rw-r--r--editor/gui/editor_quick_open_dialog.cpp578
-rw-r--r--editor/gui/editor_quick_open_dialog.h78
-rw-r--r--scene/gui/label.cpp344
-rw-r--r--scene/gui/label.h5
-rw-r--r--tests/core/string/test_fuzzy_search.h83
-rw-r--r--tests/core/string/test_string.h19
-rw-r--r--tests/data/fuzzy_search/project_dir_tree.txt999
-rw-r--r--tests/test_main.cpp1
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 4e9eb922f6..8816a7da81 100644
--- a/core/string/ustring.cpp
+++ b/core/string/ustring.cpp
@@ -3372,7 +3372,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);
}
@@ -3609,6 +3609,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();
@@ -3822,6 +3826,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 aa62c9cb18..017b12e04c 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 a5097521dc..fd36c60e90 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 94a5ff94a3..dc4d1e8f15 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.
@@ -562,11 +574,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() {
@@ -574,41 +590,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);
@@ -627,16 +643,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) {
@@ -664,32 +671,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() {
@@ -743,36 +745,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) {
@@ -825,6 +826,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);
@@ -852,13 +869,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);
@@ -866,18 +883,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 {
@@ -888,9 +916,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) {
@@ -919,10 +949,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);
@@ -930,16 +960,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 {
@@ -949,8 +986,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 8d6137cf62..343519e713 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"