diff --git a/editor/find_in_files.cpp b/editor/find_in_files.cpp index 96f84cabf8e..220669fcbb2 100644 --- a/editor/find_in_files.cpp +++ b/editor/find_in_files.cpp @@ -103,6 +103,14 @@ void FindInFiles::set_filter(const HashSet &exts) { _extension_filter = exts; } +void FindInFiles::set_includes(const HashSet &p_include_wildcards) { + _include_wildcards = p_include_wildcards; +} + +void FindInFiles::set_excludes(const HashSet &p_exclude_wildcards) { + _exclude_wildcards = p_exclude_wildcards; +} + void FindInFiles::_notification(int p_what) { switch (p_what) { case NOTIFICATION_PROCESS: { @@ -253,7 +261,16 @@ void FindInFiles::_scan_dir(const String &path, PackedStringArray &out_folders, } else { String file_ext = file.get_extension(); if (_extension_filter.has(file_ext)) { - out_files_to_scan.push_back(path.path_join(file)); + String file_path = path.path_join(file); + bool case_sensitive = dir->is_case_sensitive(path); + + if (!_exclude_wildcards.is_empty() && _is_file_matched(_exclude_wildcards, file_path, case_sensitive)) { + continue; + } + + if (_include_wildcards.is_empty() || _is_file_matched(_include_wildcards, file_path, case_sensitive)) { + out_files_to_scan.push_back(file_path); + } } } } @@ -283,6 +300,19 @@ void FindInFiles::_scan_file(const String &fpath) { } } +bool FindInFiles::_is_file_matched(const HashSet &p_wildcards, const String &p_file_path, bool p_case_sensitive) const { + const String file_path = "/" + p_file_path.replace_char('\\', '/') + "/"; + + for (const String &wildcard : p_wildcards) { + if (p_case_sensitive && file_path.match(wildcard)) { + return true; + } else if (!p_case_sensitive && file_path.matchn(wildcard)) { + return true; + } + } + return false; +} + void FindInFiles::_bind_methods() { ADD_SIGNAL(MethodInfo(SIGNAL_RESULT_FOUND, PropertyInfo(Variant::STRING, "path"), @@ -381,9 +411,36 @@ FindInFilesDialog::FindInFilesDialog() { gc->add_child(hbc); } + Label *includes_label = memnew(Label); + includes_label->set_text(TTR("Includes:")); + includes_label->set_tooltip_text(TTR("Include the files with the following expressions. Use \",\" to separate.")); + includes_label->set_mouse_filter(Control::MOUSE_FILTER_PASS); + gc->add_child(includes_label); + + _includes_line_edit = memnew(LineEdit); + _includes_line_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL); + _includes_line_edit->set_placeholder(TTR("example: scripts,scenes/*/test.gd")); + _includes_line_edit->set_accessibility_name(TTRC("Include Files")); + _includes_line_edit->connect(SceneStringName(text_submitted), callable_mp(this, &FindInFilesDialog::_on_search_text_submitted)); + gc->add_child(_includes_line_edit); + + Label *excludes_label = memnew(Label); + excludes_label->set_text(TTR("Excludes:")); + excludes_label->set_tooltip_text(TTR("Exclude the files with the following expressions. Use \",\" to separate.")); + excludes_label->set_mouse_filter(Control::MOUSE_FILTER_PASS); + gc->add_child(excludes_label); + + _excludes_line_edit = memnew(LineEdit); + _excludes_line_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL); + _excludes_line_edit->set_placeholder(TTR("example: res://addons,scenes/test/*.gd")); + _excludes_line_edit->set_accessibility_name(TTRC("Exclude Files")); + _excludes_line_edit->connect(SceneStringName(text_submitted), callable_mp(this, &FindInFilesDialog::_on_search_text_submitted)); + gc->add_child(_excludes_line_edit); + Label *filter_label = memnew(Label); filter_label->set_text(TTR("Filters:")); filter_label->set_tooltip_text(TTR("Include the files with the following extensions. Add or remove them in ProjectSettings.")); + filter_label->set_mouse_filter(Control::MOUSE_FILTER_PASS); gc->add_child(filter_label); _filters_container = memnew(HBoxContainer); @@ -480,6 +537,36 @@ HashSet FindInFilesDialog::get_filter() const { return filters; } +HashSet FindInFilesDialog::get_includes() const { + HashSet includes; + String text = _includes_line_edit->get_text(); + + if (text.is_empty()) { + return includes; + } + + PackedStringArray wildcards = text.split(",", false); + for (const String &wildcard : wildcards) { + includes.insert(validate_filter_wildcard(wildcard)); + } + return includes; +} + +HashSet FindInFilesDialog::get_excludes() const { + HashSet excludes; + String text = _excludes_line_edit->get_text(); + + if (text.is_empty()) { + return excludes; + } + + PackedStringArray wildcards = text.split(",", false); + for (const String &wildcard : wildcards) { + excludes.insert(validate_filter_wildcard(wildcard)); + } + return excludes; +} + void FindInFilesDialog::_notification(int p_what) { switch (p_what) { case NOTIFICATION_VISIBILITY_CHANGED: { @@ -562,6 +649,29 @@ void FindInFilesDialog::_on_folder_selected(String path) { _folder_line_edit->set_text(path); } +String FindInFilesDialog::validate_filter_wildcard(const String &p_expression) const { + String ret = p_expression.replace_char('\\', '/'); + if (ret.begins_with("./")) { + // Relative to the project root. + ret = "res://" + ret.trim_prefix("./"); + } + + if (ret.begins_with(".")) { + // To match extension. + ret = "*" + ret; + } + + if (!ret.begins_with("*")) { + ret = "*/" + ret.trim_prefix("/"); + } + + if (!ret.ends_with("*")) { + ret = ret.trim_suffix("/") + "/*"; + } + + return ret; +} + void FindInFilesDialog::_bind_methods() { ADD_SIGNAL(MethodInfo(SIGNAL_FIND_REQUESTED)); ADD_SIGNAL(MethodInfo(SIGNAL_REPLACE_REQUESTED)); diff --git a/editor/find_in_files.h b/editor/find_in_files.h index 34c0297fbde..e383132355b 100644 --- a/editor/find_in_files.h +++ b/editor/find_in_files.h @@ -46,6 +46,8 @@ public: void set_match_case(bool p_match_case); void set_folder(const String &folder); void set_filter(const HashSet &exts); + void set_includes(const HashSet &p_include_wildcards); + void set_excludes(const HashSet &p_exclude_wildcards); String get_search_text() const { return _pattern; } @@ -69,9 +71,13 @@ private: void _scan_dir(const String &path, PackedStringArray &out_folders, PackedStringArray &out_files_to_scan); void _scan_file(const String &fpath); + bool _is_file_matched(const HashSet &p_wildcards, const String &p_file_path, bool p_case_sensitive) const; + // Config String _pattern; HashSet _extension_filter; + HashSet _include_wildcards; + HashSet _exclude_wildcards; String _root_dir; bool _whole_words = true; bool _match_case = true; @@ -115,6 +121,8 @@ public: bool is_whole_words() const; String get_folder() const; HashSet get_filter() const; + HashSet get_includes() const; + HashSet get_excludes() const; protected: void _notification(int p_what); @@ -130,6 +138,8 @@ private: void _on_search_text_submitted(const String &text); void _on_replace_text_submitted(const String &text); + String validate_filter_wildcard(const String &p_expression) const; + FindInFilesMode _mode; LineEdit *_search_text_line_edit = nullptr; @@ -143,6 +153,9 @@ private: Button *_replace_button = nullptr; FileDialog *_folder_dialog = nullptr; HBoxContainer *_filters_container = nullptr; + LineEdit *_includes_line_edit = nullptr; + LineEdit *_excludes_line_edit = nullptr; + HashMap _filters_preferences; }; diff --git a/editor/plugins/script_editor_plugin.cpp b/editor/plugins/script_editor_plugin.cpp index 1c3d3a0d28a..1963877703d 100644 --- a/editor/plugins/script_editor_plugin.cpp +++ b/editor/plugins/script_editor_plugin.cpp @@ -4035,6 +4035,8 @@ void ScriptEditor::_start_find_in_files(bool with_replace) { f->set_whole_words(find_in_files_dialog->is_whole_words()); f->set_folder(find_in_files_dialog->get_folder()); f->set_filter(find_in_files_dialog->get_filter()); + f->set_includes(find_in_files_dialog->get_includes()); + f->set_excludes(find_in_files_dialog->get_excludes()); find_in_files->set_with_replace(with_replace); find_in_files->set_replace_text(find_in_files_dialog->get_replace_text());