Explorar el Código

Add fuzzy string matching to quick open search

Co-authored-by: sam <[email protected]>
Adam Johnston hace 9 meses
padre
commit
3ac043c508

+ 349 - 0
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);
+}

+ 101 - 0
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

+ 14 - 1
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;

+ 3 - 1
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;

+ 12 - 0
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].

+ 4 - 0
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")
 

+ 308 - 270
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) {

+ 53 - 25
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:

+ 143 - 201
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();
 }
 

+ 5 - 0
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

+ 83 - 0
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

+ 19 - 0
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"));

+ 999 - 0
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

+ 1 - 0
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"