/**************************************************************************/ /* find_in_files.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 "find_in_files.h" #include "core/config/project_settings.h" #include "core/io/dir_access.h" #include "core/object/callable_mp.h" #include "core/os/os.h" #include "editor/docks/editor_dock_manager.h" #include "editor/editor_node.h" #include "editor/editor_string_names.h" #include "editor/gui/editor_file_dialog.h" #include "editor/settings/editor_command_palette.h" #include "editor/themes/editor_scale.h" #include "scene/gui/box_container.h" #include "scene/gui/button.h" #include "scene/gui/check_box.h" #include "scene/gui/check_button.h" #include "scene/gui/grid_container.h" #include "scene/gui/label.h" #include "scene/gui/line_edit.h" #include "scene/gui/progress_bar.h" #include "scene/gui/tab_container.h" #include "scene/gui/tree.h" #include "scene/main/scene_tree.h" #include "servers/rendering/rendering_server.h" // TODO: Would be nice in Vector and Vectors. template inline void pop_back(T &r_container) { r_container.resize(r_container.size() - 1); } static bool find_next(const String &p_line, const String &p_pattern, int p_from, bool p_match_case, bool p_whole_words, int &r_out_begin, int &r_out_end) { int end = p_from; while (true) { int begin = p_match_case ? p_line.find(p_pattern, end) : p_line.findn(p_pattern, end); if (begin == -1) { return false; } end = begin + p_pattern.length(); r_out_begin = begin; r_out_end = end; if (p_whole_words) { if (begin > 0 && is_ascii_identifier_char(p_line[begin - 1])) { continue; } if (end < p_line.size() && is_ascii_identifier_char(p_line[end])) { continue; } } return true; } } //-------------------------------------------------------------------------------- void FindInFilesSearch::set_search_text(const String &p_pattern) { pattern = p_pattern; } void FindInFilesSearch::set_whole_words(bool p_whole_word) { whole_words = p_whole_word; } void FindInFilesSearch::set_match_case(bool p_match_case) { match_case = p_match_case; } void FindInFilesSearch::set_folder(const String &p_folder) { root_dir = p_folder; } void FindInFilesSearch::set_filter(const HashSet &p_exts) { extension_filter = p_exts; } void FindInFilesSearch::set_includes(const HashSet &p_include_wildcards) { include_wildcards = p_include_wildcards; } void FindInFilesSearch::set_excludes(const HashSet &p_exclude_wildcards) { exclude_wildcards = p_exclude_wildcards; } void FindInFilesSearch::_notification(int p_what) { switch (p_what) { case NOTIFICATION_PROCESS: { _process(); } break; } } void FindInFilesSearch::start() { if (pattern.is_empty()) { print_verbose("Nothing to search, pattern is empty"); emit_signal(SceneStringName(finished)); return; } if (extension_filter.is_empty()) { print_verbose("Nothing to search, filter matches no files"); emit_signal(SceneStringName(finished)); return; } // Init search. current_dir = ""; PackedStringArray init_folder; init_folder.push_back(root_dir); folders_stack.clear(); folders_stack.push_back(init_folder); initial_files_count = 0; searching = true; set_process(true); } void FindInFilesSearch::stop() { searching = false; current_dir = ""; set_process(false); } void FindInFilesSearch::_process() { // This part can be moved to a thread if needed. OS &os = *OS::get_singleton(); uint64_t time_before = os.get_ticks_msec(); while (is_processing()) { _iterate(); uint64_t elapsed = (os.get_ticks_msec() - time_before); if (elapsed > 8) { // Process again after waiting 8 ticks. break; } } } void FindInFilesSearch::_iterate() { if (folders_stack.size() != 0) { // Scan folders first so we can build a list of files and have progress info later. PackedStringArray &folders_to_scan = folders_stack.write[folders_stack.size() - 1]; if (folders_to_scan.size() != 0) { // Scan one folder below. String folder_name = folders_to_scan[folders_to_scan.size() - 1]; pop_back(folders_to_scan); current_dir = current_dir.path_join(folder_name); PackedStringArray sub_dirs; PackedStringArray new_files_to_scan; _scan_dir("res://" + current_dir, sub_dirs, new_files_to_scan); folders_stack.push_back(sub_dirs); files_to_scan.append_array(new_files_to_scan); } else { // Go back one level. pop_back(folders_stack); current_dir = current_dir.get_base_dir(); if (folders_stack.is_empty()) { // All folders scanned. initial_files_count = files_to_scan.size(); } } } else if (files_to_scan.size() != 0) { // Then scan files. String fpath = files_to_scan[files_to_scan.size() - 1]; pop_back(files_to_scan); _scan_file(fpath); } else { print_verbose("Search complete"); set_process(false); current_dir = ""; searching = false; emit_signal(SceneStringName(finished)); } } float FindInFilesSearch::get_progress() const { if (initial_files_count != 0) { return static_cast(initial_files_count - files_to_scan.size()) / static_cast(initial_files_count); } return 0; } void FindInFilesSearch::_scan_dir(const String &p_path, PackedStringArray &r_out_folders, PackedStringArray &r_out_files_to_scan) { Ref dir = DirAccess::open(p_path); if (dir.is_null()) { print_verbose("Cannot open directory! " + p_path); return; } dir->list_dir_begin(); // Limit to 100,000 iterations to avoid an infinite loop just in case // (this technically limits results to 100,000 files per folder). for (int i = 0; i < 100'000; ++i) { String file = dir->get_next(); if (file.is_empty()) { break; } // If there is a .gdignore file in the directory, clear all the files/folders // to be searched on this path and skip searching the directory. if (file == ".gdignore") { r_out_folders.clear(); r_out_files_to_scan.clear(); break; } // Ignore special directories (such as those beginning with . and the project data directory). String project_data_dir_name = ProjectSettings::get_singleton()->get_project_data_dir_name(); if (file.begins_with(".") || file == project_data_dir_name) { continue; } if (dir->current_is_hidden()) { continue; } if (dir->current_is_dir()) { r_out_folders.push_back(file); } else { String file_ext = file.get_extension(); if (extension_filter.has(file_ext)) { String file_path = p_path.path_join(file); bool case_sensitive = dir->is_case_sensitive(p_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)) { r_out_files_to_scan.push_back(file_path); } } } } } void FindInFilesSearch::_scan_file(const String &p_fpath) { Ref f = FileAccess::open(p_fpath, FileAccess::READ); if (f.is_null()) { print_verbose("Cannot open file " + p_fpath); return; } int line_number = 0; while (!f->eof_reached()) { // Line number starts at 1. ++line_number; int begin = 0; int end = 0; String line = f->get_line(); while (find_next(line, pattern, end, match_case, whole_words, begin, end)) { emit_signal(SNAME("result_found"), p_fpath, line_number, begin, end, line); } } } bool FindInFilesSearch::_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 FindInFilesSearch::_bind_methods() { ADD_SIGNAL(MethodInfo("result_found", PropertyInfo(Variant::STRING, "path"), PropertyInfo(Variant::INT, "line_number"), PropertyInfo(Variant::INT, "begin"), PropertyInfo(Variant::INT, "end"), PropertyInfo(Variant::STRING, "text"))); ADD_SIGNAL(MethodInfo("finished")); } //----------------------------------------------------------------------------- void FindInFilesDialog::set_search_text(const String &p_text) { if (!p_text.is_empty()) { search_text_line_edit->set_text(p_text); _on_search_text_modified(p_text); } if (replace_mode && !p_text.is_empty()) { callable_mp((Control *)replace_text_line_edit, &Control::grab_focus).call_deferred(false); replace_text_line_edit->select_all(); } else { callable_mp((Control *)search_text_line_edit, &Control::grab_focus).call_deferred(false); search_text_line_edit->select_all(); } } void FindInFilesDialog::set_replace_text(const String &p_text) { replace_text_line_edit->set_text(p_text); } void FindInFilesDialog::set_replace_mode(bool p_replace) { if (replace_mode == p_replace) { return; } replace_mode = p_replace; if (replace_mode) { set_title(TTRC("Replace in Files")); replace_label->show(); replace_text_line_edit->show(); } else { set_title(TTRC("Find in Files")); replace_label->hide(); replace_text_line_edit->hide(); } // Recalculate the dialog size after hiding child controls. set_size(Size2(get_size().x, 0)); } String FindInFilesDialog::get_search_text() const { return search_text_line_edit->get_text(); } String FindInFilesDialog::get_replace_text() const { return replace_text_line_edit->get_text(); } bool FindInFilesDialog::is_match_case() const { return match_case_checkbox->is_pressed(); } bool FindInFilesDialog::is_whole_words() const { return whole_words_checkbox->is_pressed(); } String FindInFilesDialog::get_folder() const { String p_text = folder_line_edit->get_text(); return p_text.strip_edges(); } HashSet FindInFilesDialog::get_filter() const { // Could check the filters_preferences but it might not have been generated yet. HashSet filters; for (int i = 0; i < filters_container->get_child_count(); ++i) { CheckBox *cb = static_cast(filters_container->get_child(i)); if (cb->is_pressed()) { filters.insert(cb->get_text()); } } return filters; } HashSet FindInFilesDialog::get_includes() const { HashSet includes; String p_text = includes_line_edit->get_text(); if (p_text.is_empty()) { return includes; } PackedStringArray wildcards = p_text.split(",", false); for (const String &wildcard : wildcards) { includes.insert(_validate_filter_wildcard(wildcard)); } return includes; } HashSet FindInFilesDialog::get_excludes() const { HashSet excludes; String p_text = excludes_line_edit->get_text(); if (p_text.is_empty()) { return excludes; } PackedStringArray wildcards = p_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: { if (is_visible()) { // Extensions might have changed in the meantime, we clean them and instance them again. for (int i = 0; i < filters_container->get_child_count(); i++) { filters_container->get_child(i)->queue_free(); } Array exts = GLOBAL_GET("editor/script/search_in_file_extensions"); for (int i = 0; i < exts.size(); ++i) { CheckBox *cb = memnew(CheckBox); cb->set_text(exts[i]); if (!filters_preferences.has(exts[i])) { filters_preferences[exts[i]] = true; } cb->set_pressed(filters_preferences[exts[i]]); filters_container->add_child(cb); } } } break; } } void FindInFilesDialog::_on_folder_button_pressed() { folder_dialog->popup_file_dialog(); } void FindInFilesDialog::custom_action(const String &p_action) { for (int i = 0; i < filters_container->get_child_count(); ++i) { CheckBox *cb = static_cast(filters_container->get_child(i)); filters_preferences[cb->get_text()] = cb->is_pressed(); } if (p_action == "find") { emit_signal(SNAME("find_requested")); } else if (p_action == "replace") { emit_signal(SNAME("replace_requested")); } hide(); } void FindInFilesDialog::_on_search_text_modified(const String &p_text) { ERR_FAIL_NULL(find_button); ERR_FAIL_NULL(replace_button); find_button->set_disabled(get_search_text().is_empty()); replace_button->set_disabled(get_search_text().is_empty()); } void FindInFilesDialog::_on_search_text_submitted(const String &p_text) { // This allows to trigger a global search without leaving the keyboard. if (!replace_mode && !find_button->is_disabled()) { custom_action("find"); } if (replace_mode && !replace_button->is_disabled()) { custom_action("replace"); } } void FindInFilesDialog::_on_replace_text_submitted(const String &p_text) { // This allows to trigger a global search without leaving the keyboard. if (replace_mode && !replace_button->is_disabled()) { custom_action("replace"); } } void FindInFilesDialog::_on_folder_selected(String p_path) { int i = p_path.find("://"); if (i != -1) { p_path = p_path.substr(i + 3); } folder_line_edit->set_text(p_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("find_requested")); ADD_SIGNAL(MethodInfo("replace_requested")); } FindInFilesDialog::FindInFilesDialog() { set_min_size(Size2(500 * EDSCALE, 0)); set_title(TTRC("Find in Files")); VBoxContainer *vbc = memnew(VBoxContainer); vbc->set_anchor_and_offset(SIDE_LEFT, Control::ANCHOR_BEGIN, 8 * EDSCALE); vbc->set_anchor_and_offset(SIDE_TOP, Control::ANCHOR_BEGIN, 8 * EDSCALE); vbc->set_anchor_and_offset(SIDE_RIGHT, Control::ANCHOR_END, -8 * EDSCALE); vbc->set_anchor_and_offset(SIDE_BOTTOM, Control::ANCHOR_END, -8 * EDSCALE); add_child(vbc); GridContainer *gc = memnew(GridContainer); gc->set_columns(2); vbc->add_child(gc); Label *find_label = memnew(Label); find_label->set_text(TTRC("Find:")); gc->add_child(find_label); search_text_line_edit = memnew(LineEdit); search_text_line_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL); search_text_line_edit->set_accessibility_name(TTRC("Find:")); search_text_line_edit->connect(SceneStringName(text_changed), callable_mp(this, &FindInFilesDialog::_on_search_text_modified)); search_text_line_edit->connect(SceneStringName(text_submitted), callable_mp(this, &FindInFilesDialog::_on_search_text_submitted)); gc->add_child(search_text_line_edit); replace_label = memnew(Label); replace_label->set_text(TTRC("Replace:")); replace_label->hide(); gc->add_child(replace_label); replace_text_line_edit = memnew(LineEdit); replace_text_line_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL); replace_text_line_edit->set_accessibility_name(TTRC("Replace:")); replace_text_line_edit->connect(SceneStringName(text_submitted), callable_mp(this, &FindInFilesDialog::_on_replace_text_submitted)); replace_text_line_edit->hide(); gc->add_child(replace_text_line_edit); gc->add_child(memnew(Control)); // Space to maintain the grid alignment. { HBoxContainer *hbc = memnew(HBoxContainer); whole_words_checkbox = memnew(CheckBox); whole_words_checkbox->set_text(TTRC("Whole Words")); hbc->add_child(whole_words_checkbox); match_case_checkbox = memnew(CheckBox); match_case_checkbox->set_text(TTRC("Match Case")); hbc->add_child(match_case_checkbox); gc->add_child(hbc); } Label *folder_label = memnew(Label); folder_label->set_text(TTRC("Folder:")); gc->add_child(folder_label); { HBoxContainer *hbc = memnew(HBoxContainer); Label *prefix_label = memnew(Label); prefix_label->set_text("res://"); prefix_label->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); hbc->add_child(prefix_label); folder_line_edit = memnew(LineEdit); folder_line_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL); folder_line_edit->connect(SceneStringName(text_submitted), callable_mp(this, &FindInFilesDialog::_on_search_text_submitted)); folder_line_edit->set_accessibility_name(TTRC("Folder:")); hbc->add_child(folder_line_edit); Button *folder_button = memnew(Button); folder_button->set_accessibility_name(TTRC("Select Folder")); folder_button->set_text("..."); folder_button->connect(SceneStringName(pressed), callable_mp(this, &FindInFilesDialog::_on_folder_button_pressed)); hbc->add_child(folder_button); folder_dialog = memnew(EditorFileDialog); folder_dialog->set_file_mode(FileDialog::FILE_MODE_OPEN_DIR); folder_dialog->connect("dir_selected", callable_mp(this, &FindInFilesDialog::_on_folder_selected)); add_child(folder_dialog); gc->add_child(hbc); } Label *includes_label = memnew(Label); includes_label->set_text(TTRC("Includes:")); includes_label->set_tooltip_text(TTRC("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(TTRC("example: scripts,scenes/*/test.gd")); includes_line_edit->set_accessibility_name(TTRC("Includes:")); 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(TTRC("Excludes:")); excludes_label->set_tooltip_text(TTRC("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(TTRC("example: res://addons,scenes/test/*.gd")); excludes_line_edit->set_accessibility_name(TTRC("Excludes:")); 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(TTRC("Filters:")); filter_label->set_tooltip_text(TTRC("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); gc->add_child(filters_container); find_button = add_button(TTRC("Find..."), false, "find"); find_button->set_disabled(true); replace_button = add_button(TTRC("Replace..."), false, "replace"); replace_button->set_disabled(true); Button *cancel_button = get_ok_button(); cancel_button->set_text(TTRC("Cancel")); } //----------------------------------------------------------------------------- void FindInFilesPanel::set_with_replace(bool p_with_replace) { with_replace = p_with_replace; replace_container->set_visible(p_with_replace); if (with_replace) { // Results show checkboxes on their left so they can be opted out. results_display->set_columns(2); results_display->set_column_expand(0, false); results_display->set_column_custom_minimum_width(0, 48 * EDSCALE); } else { // Results are single-cell items. results_display->set_column_expand(0, true); results_display->set_columns(1); } } void FindInFilesPanel::set_replace_text(const String &p_text) { replace_line_edit->set_text(p_text); } bool FindInFilesPanel::is_keep_results() const { return keep_results_button->is_pressed(); } void FindInFilesPanel::set_search_labels_visibility(bool p_visible) { find_label->set_visible(p_visible); search_text_label->set_visible(p_visible); close_button->set_visible(p_visible); } void FindInFilesPanel::_clear() { file_items.clear(); file_items_results_count.clear(); result_items.clear(); results_display->clear(); results_display->create_item(); // Root } void FindInFilesPanel::start_search() { _clear(); status_label->set_text(TTRC("Searching...")); search_text_label->set_text(finder->get_search_text()); search_text_label->set_tooltip_text(finder->get_search_text()); int label_min_width = search_text_label->get_minimum_size().x + search_text_label->get_character_bounds(0).size.x; search_text_label->set_custom_minimum_size(Size2(label_min_width, 0)); set_process(true); progress_bar->set_visible(true); finder->start(); _update_replace_buttons(); refresh_button->hide(); cancel_button->show(); } void FindInFilesPanel::stop_search() { finder->stop(); status_label->set_text(""); _update_replace_buttons(); progress_bar->set_visible(false); refresh_button->show(); cancel_button->hide(); } void FindInFilesPanel::update_layout(EditorDock::DockLayout p_layout) { bool new_floating = (p_layout == EditorDock::DOCK_LAYOUT_FLOATING); if (floating == new_floating) { return; } floating = new_floating; if (floating) { results_mc->set_theme_type_variation("NoBorderHorizontalBottom"); results_display->set_scroll_hint_mode(Tree::SCROLL_HINT_MODE_TOP); } else { results_mc->set_theme_type_variation("NoBorderHorizontal"); results_display->set_scroll_hint_mode(Tree::SCROLL_HINT_MODE_BOTH); } } void FindInFilesPanel::_notification(int p_what) { switch (p_what) { case NOTIFICATION_THEME_CHANGED: { _on_theme_changed(); } break; case NOTIFICATION_TRANSLATION_CHANGED: { _update_matches_text(); TreeItem *file_item = results_display->get_root()->get_first_child(); while (file_item) { if (with_replace) { file_item->set_button_tooltip_text(0, file_item->get_button_by_id(0, FIND_BUTTON_REPLACE), TTR("Replace all matches in file")); } file_item->set_button_tooltip_text(0, file_item->get_button_by_id(0, FIND_BUTTON_REMOVE), TTR("Remove result")); TreeItem *result_item = file_item->get_first_child(); while (result_item) { if (with_replace) { result_item->set_button_tooltip_text(1, file_item->get_button_by_id(0, FIND_BUTTON_REPLACE), TTR("Replace")); result_item->set_button_tooltip_text(1, file_item->get_button_by_id(0, FIND_BUTTON_REMOVE), TTR("Remove result")); } else { result_item->set_button_tooltip_text(0, file_item->get_button_by_id(0, FIND_BUTTON_REMOVE), TTR("Remove result")); } result_item = result_item->get_next(); } file_item = file_item->get_next(); } } break; case NOTIFICATION_PROCESS: { progress_bar->set_as_ratio(finder->get_progress()); } break; } } void FindInFilesPanel::_on_result_found(const String &p_fpath, int p_line_number, int p_begin, int p_end, const String &p_text) { TreeItem *file_item; Ref remove_texture = get_editor_theme_icon(SNAME("Close")); Ref replace_texture = get_editor_theme_icon(SNAME("ReplaceText")); HashMap::Iterator E = file_items.find(p_fpath); if (!E) { file_item = results_display->create_item(); file_item->set_text(0, p_fpath); file_item->set_metadata(0, p_fpath); if (with_replace) { file_item->add_button(0, replace_texture, FIND_BUTTON_REPLACE, false, TTR("Replace all matches in file")); } file_item->add_button(0, remove_texture, FIND_BUTTON_REMOVE, false, TTR("Remove result")); // The width of this column is restrained to checkboxes, // but that doesn't make sense for the parent items, // so we override their width so they can expand to full width. file_item->set_expand_right(0, true); file_items[p_fpath] = file_item; file_items_results_count[file_item] = 1; } else { file_item = E->value; file_items_results_count[file_item]++; } Color file_item_color = results_display->get_theme_color(SceneStringName(font_color)) * Color(1, 1, 1, 0.67); file_item->set_custom_color(0, file_item_color); file_item->set_selectable(0, false); int text_index = with_replace ? 1 : 0; TreeItem *item = results_display->create_item(file_item); // Do this first because it resets properties of the cell... item->set_cell_mode(text_index, TreeItem::CELL_MODE_CUSTOM); // Trim result item line. String trimmed_text = p_text.strip_edges(true, false); int chars_removed = p_text.size() - trimmed_text.size(); String start = vformat("%3s: ", p_line_number); item->set_text(text_index, start + trimmed_text); item->set_custom_draw_callback(text_index, callable_mp(this, &FindInFilesPanel::_draw_result_text)); Result r; r.line_number = p_line_number; r.begin = p_begin; r.end = p_end; r.begin_trimmed = p_begin - chars_removed + start.size() - 1; result_items[item] = r; if (with_replace) { item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK); item->set_checked(0, true); item->set_editable(0, true); item->add_button(1, replace_texture, FIND_BUTTON_REPLACE, false, TTR("Replace")); item->add_button(1, remove_texture, FIND_BUTTON_REMOVE, false, TTR("Remove result")); } else { item->add_button(0, remove_texture, FIND_BUTTON_REMOVE, false, TTR("Remove result")); } } void FindInFilesPanel::_on_theme_changed() { results_display->add_theme_font_override(SceneStringName(font), get_theme_font(SNAME("source"), EditorStringName(EditorFonts))); results_display->add_theme_font_size_override(SceneStringName(font_size), get_theme_font_size(SNAME("source_size"), EditorStringName(EditorFonts))); Color file_item_color = results_display->get_theme_color(SceneStringName(font_color)) * Color(1, 1, 1, 0.67); Ref remove_texture = get_editor_theme_icon(SNAME("Close")); Ref replace_texture = get_editor_theme_icon(SNAME("ReplaceText")); TreeItem *file_item = results_display->get_root()->get_first_child(); while (file_item) { file_item->set_custom_color(0, file_item_color); if (with_replace) { file_item->set_button(0, file_item->get_button_by_id(0, FIND_BUTTON_REPLACE), replace_texture); } file_item->set_button(0, file_item->get_button_by_id(0, FIND_BUTTON_REMOVE), remove_texture); TreeItem *result_item = file_item->get_first_child(); while (result_item) { if (with_replace) { result_item->set_button(1, result_item->get_button_by_id(1, FIND_BUTTON_REPLACE), replace_texture); result_item->set_button(1, result_item->get_button_by_id(1, FIND_BUTTON_REMOVE), remove_texture); } else { result_item->set_button(0, result_item->get_button_by_id(0, FIND_BUTTON_REMOVE), remove_texture); } result_item = result_item->get_next(); } file_item = file_item->get_next(); } } void FindInFilesPanel::_draw_result_text(Object *p_item_obj, const Rect2 &p_rect) { TreeItem *item = Object::cast_to(p_item_obj); if (!item) { return; } HashMap::Iterator E = result_items.find(item); if (!E) { return; } Result r = E->value; String item_text = item->get_text(with_replace ? 1 : 0); Ref font = results_display->get_theme_font(SceneStringName(font)); int font_size = results_display->get_theme_font_size(SceneStringName(font_size)); Rect2 match_rect = p_rect; match_rect.position.x += font->get_string_size(item_text.left(r.begin_trimmed), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x - 1; match_rect.size.x = font->get_string_size(search_text_label->get_text(), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x + 1; match_rect.position.y += 1 * EDSCALE; match_rect.size.y -= 2 * EDSCALE; RID ci = item->get_tree()->get_custom_drawing_canvas_item(); Vector points; points.resize(5); points.write[0] = match_rect.position; points.write[1] = match_rect.position + Vector2(match_rect.size.x, 0); points.write[2] = match_rect.position + match_rect.size; points.write[3] = match_rect.position + Vector2(0, match_rect.size.y); points.write[4] = match_rect.position; Color accent_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor)); Vector colors = { Color(accent_color, 0.33) }; RenderingServer::get_singleton()->canvas_item_add_polyline(ci, points, colors, 2.0); RenderingServer::get_singleton()->canvas_item_add_rect(ci, match_rect, Color(accent_color, 0.17)); // Text is drawn by Tree already. } void FindInFilesPanel::_on_item_edited() { TreeItem *item = results_display->get_selected(); // Change opacity to half if checkbox is checked, otherwise full. Color use_color = results_display->get_theme_color(SceneStringName(font_color)); if (!item->is_checked(0)) { use_color.a *= 0.5; } item->set_custom_color(1, use_color); } void FindInFilesPanel::_on_finished() { _update_matches_text(); _update_replace_buttons(); progress_bar->set_visible(false); refresh_button->show(); cancel_button->hide(); } void FindInFilesPanel::_on_refresh_button_clicked() { start_search(); } void FindInFilesPanel::_on_cancel_button_clicked() { stop_search(); } void FindInFilesPanel::_on_close_button_clicked() { emit_signal(SNAME("close_button_clicked")); } void FindInFilesPanel::_on_result_selected() { TreeItem *item = results_display->get_selected(); HashMap::Iterator E = result_items.find(item); if (!E) { return; } Result r = E->value; TreeItem *file_item = item->get_parent(); String fpath = file_item->get_metadata(0); emit_signal(SNAME("result_selected"), fpath, r.line_number, r.begin, r.end); } void FindInFilesPanel::_on_replace_text_changed(const String &p_text) { _update_replace_buttons(); } void FindInFilesPanel::_on_replace_all_clicked() { String replace_text = _get_replace_text(); for (KeyValue &E : file_items) { TreeItem *file_item = E.value; String fpath = file_item->get_metadata(0); Vector locations; for (TreeItem *item = file_item->get_first_child(); item; item = item->get_next()) { if (!item->is_checked(0)) { continue; } HashMap::Iterator F = result_items.find(item); ERR_FAIL_COND(!F); locations.push_back(F->value); } if (locations.size() != 0) { // Results are sorted by file, so we can batch replaces. _apply_replaces_in_file(fpath, locations, replace_text); } } // Hide replace bar so we can't trigger the action twice without doing a new search. replace_container->hide(); emit_signal(SNAME("files_modified")); } void FindInFilesPanel::_on_button_clicked(TreeItem *p_item, int p_column, int p_id, int p_mouse_button_index) { const String file_path = p_item->get_metadata(0); if (p_id == FIND_BUTTON_REPLACE) { const String replace_text = _get_replace_text(); Vector locations; if (file_items.has(file_path)) { for (TreeItem *item = p_item->get_first_child(); item; item = item->get_next()) { HashMap::Iterator F = result_items.find(item); ERR_FAIL_COND(!F); locations.push_back(F->value); } _apply_replaces_in_file(file_path, locations, replace_text); } else { locations.push_back(result_items.find(p_item)->value); const String path = p_item->get_parent()->get_metadata(0); _apply_replaces_in_file(path, locations, replace_text); } emit_signal(SNAME("files_modified")); } result_items.erase(p_item); if (file_items_results_count.has(p_item)) { int match_count = p_item->get_child_count(); for (int i = 0; i < match_count; i++) { TreeItem *child_item = p_item->get_child(i); result_items.erase(child_item); } p_item->clear_children(); file_items.erase(file_path); file_items_results_count.erase(p_item); } TreeItem *item_parent = p_item->get_parent(); if (item_parent) { if (file_items_results_count.has(item_parent)) { file_items_results_count[item_parent]--; } if (item_parent->get_child_count() < 2 && item_parent != results_display->get_root()) { file_items.erase(item_parent->get_metadata(0)); get_tree()->queue_delete(item_parent); } } get_tree()->queue_delete(p_item); _update_matches_text(); } // Same as get_line, but preserves line ending characters. class ConservativeGetLine { public: String get_line(Ref p_file) { _line_buffer.clear(); char32_t c = p_file->get_8(); while (!p_file->eof_reached()) { if (c == '\n') { _line_buffer.push_back(c); _line_buffer.push_back(0); return String::utf8(_line_buffer.ptr()); } else if (c == '\0') { _line_buffer.push_back(c); return String::utf8(_line_buffer.ptr()); } else if (c != '\r') { _line_buffer.push_back(c); } c = p_file->get_8(); } _line_buffer.push_back(0); return String::utf8(_line_buffer.ptr()); } private: Vector _line_buffer; }; void FindInFilesPanel::_apply_replaces_in_file(const String &p_fpath, const Vector &p_locations, const String &p_new_text) { // If the file is already open, I assume the editor will reload it. // If there are unsaved changes, the user will be asked on focus, // however that means either losing changes or losing replaces. Ref f = FileAccess::open(p_fpath, FileAccess::READ); ERR_FAIL_COND_MSG(f.is_null(), "Cannot open file from path '" + p_fpath + "'."); String buffer; int current_line = 1; ConservativeGetLine conservative; String line = conservative.get_line(f); String search_text = finder->get_search_text(); int offset = 0; for (int i = 0; i < p_locations.size(); ++i) { int repl_line_number = p_locations[i].line_number; while (current_line < repl_line_number) { buffer += line; line = conservative.get_line(f); ++current_line; offset = 0; } int repl_begin = p_locations[i].begin + offset; int repl_end = p_locations[i].end + offset; int _; if (!find_next(line, search_text, repl_begin, finder->is_match_case(), finder->is_whole_words(), _, _)) { // Make sure the replace is still valid in case the file was tampered with. print_verbose(vformat(R"(Occurrence no longer matches, replace will be ignored in "%s": line %d, col %d.)", p_fpath, repl_line_number, repl_begin)); continue; } line = line.left(repl_begin) + p_new_text + line.substr(repl_end); // Keep an offset in case there are successive replaces in the same line. offset += p_new_text.length() - (repl_end - repl_begin); } buffer += line; while (!f->eof_reached()) { buffer += conservative.get_line(f); } // Now the modified contents are in the buffer, rewrite the file with our changes. Error err = f->reopen(p_fpath, FileAccess::WRITE); ERR_FAIL_COND_MSG(err != OK, "Cannot create file in path '" + p_fpath + "'."); f->store_string(buffer); } String FindInFilesPanel::_get_replace_text() { return replace_line_edit->get_text(); } void FindInFilesPanel::_update_replace_buttons() { bool disabled = finder->is_searching(); replace_all_button->set_disabled(disabled); } void FindInFilesPanel::_update_matches_text() { String results_text; int result_count = result_items.size(); int file_count = file_items.size(); if (result_count == 1 && file_count == 1) { results_text = vformat(TTR("%d match in %d file"), result_count, file_count); } else if (result_count != 1 && file_count == 1) { results_text = vformat(TTR("%d matches in %d file"), result_count, file_count); } else { results_text = vformat(TTR("%d matches in %d files"), result_count, file_count); } status_label->set_text(results_text); TreeItem *file_item = results_display->get_root()->get_first_child(); while (file_item) { int file_matches_count = file_items_results_count[file_item]; file_item->set_text(0, (String)file_item->get_metadata(0) + " (" + vformat(TTRN("%d match", "%d matches", file_matches_count), file_matches_count) + ")"); file_item = file_item->get_next(); } } void FindInFilesPanel::_bind_methods() { ADD_SIGNAL(MethodInfo("result_selected", PropertyInfo(Variant::STRING, "path"), PropertyInfo(Variant::INT, "line_number"), PropertyInfo(Variant::INT, "begin"), PropertyInfo(Variant::INT, "end"))); ADD_SIGNAL(MethodInfo("files_modified")); ADD_SIGNAL(MethodInfo("close_button_clicked")); } FindInFilesPanel::FindInFilesPanel() { finder = memnew(FindInFilesSearch); finder->connect("result_found", callable_mp(this, &FindInFilesPanel::_on_result_found)); finder->connect(SceneStringName(finished), callable_mp(this, &FindInFilesPanel::_on_finished)); add_child(finder); VBoxContainer *vbc = memnew(VBoxContainer); vbc->set_anchor_and_offset(SIDE_LEFT, ANCHOR_BEGIN, 0); vbc->set_anchor_and_offset(SIDE_TOP, ANCHOR_BEGIN, 0); vbc->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, 0); vbc->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, 0); add_child(vbc); { HBoxContainer *hbc = memnew(HBoxContainer); hbc->set_alignment(BoxContainer::ALIGNMENT_END); find_label = memnew(Label); find_label->set_text(TTRC("Find:")); hbc->add_child(find_label); search_text_label = memnew(Label); search_text_label->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS); search_text_label->set_h_size_flags(Control::SIZE_EXPAND_FILL); search_text_label->set_focus_mode(FOCUS_ACCESSIBILITY); search_text_label->set_mouse_filter(Control::MOUSE_FILTER_PASS); search_text_label->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); hbc->add_child(search_text_label); progress_bar = memnew(ProgressBar); progress_bar->set_h_size_flags(SIZE_EXPAND_FILL); progress_bar->set_v_size_flags(SIZE_SHRINK_CENTER); progress_bar->set_stretch_ratio(2.0); progress_bar->set_visible(false); hbc->add_child(progress_bar); status_label = memnew(Label); status_label->set_focus_mode(FOCUS_ACCESSIBILITY); hbc->add_child(status_label); keep_results_button = memnew(CheckButton); keep_results_button->set_text(TTRC("Keep Results")); keep_results_button->set_tooltip_text(TTRC("Keep these results and show subsequent results in a new window")); keep_results_button->set_pressed(false); hbc->add_child(keep_results_button); refresh_button = memnew(Button); refresh_button->set_text(TTRC("Refresh")); refresh_button->connect(SceneStringName(pressed), callable_mp(this, &FindInFilesPanel::_on_refresh_button_clicked)); refresh_button->hide(); hbc->add_child(refresh_button); cancel_button = memnew(Button); cancel_button->set_text(TTRC("Cancel")); cancel_button->connect(SceneStringName(pressed), callable_mp(this, &FindInFilesPanel::_on_cancel_button_clicked)); cancel_button->hide(); hbc->add_child(cancel_button); close_button = memnew(Button); close_button->set_text(TTRC("Close")); close_button->connect(SceneStringName(pressed), callable_mp(this, &FindInFilesPanel::_on_close_button_clicked)); hbc->add_child(close_button); vbc->add_child(hbc); } results_mc = memnew(MarginContainer); results_mc->set_theme_type_variation("NoBorderHorizontal"); results_mc->set_v_size_flags(SIZE_EXPAND_FILL); vbc->add_child(results_mc); results_display = memnew(Tree); results_display->set_accessibility_name(TTRC("Search Results")); results_display->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); results_display->set_scroll_hint_mode(Tree::SCROLL_HINT_MODE_BOTH); results_display->connect(SceneStringName(item_selected), callable_mp(this, &FindInFilesPanel::_on_result_selected)); results_display->connect("item_edited", callable_mp(this, &FindInFilesPanel::_on_item_edited)); results_display->connect("button_clicked", callable_mp(this, &FindInFilesPanel::_on_button_clicked)); results_display->set_hide_root(true); results_display->set_select_mode(Tree::SELECT_ROW); results_display->set_allow_rmb_select(true); results_display->set_allow_reselect(true); results_display->add_theme_constant_override("inner_item_margin_left", 0); results_display->add_theme_constant_override("inner_item_margin_right", 0); results_display->create_item(); // Root results_mc->add_child(results_display); { replace_container = memnew(HBoxContainer); Label *replace_label = memnew(Label); replace_label->set_text(TTRC("Replace:")); replace_container->add_child(replace_label); replace_line_edit = memnew(LineEdit); replace_line_edit->set_accessibility_name(TTRC("Replace:")); replace_line_edit->set_h_size_flags(SIZE_EXPAND_FILL); replace_line_edit->connect(SceneStringName(text_changed), callable_mp(this, &FindInFilesPanel::_on_replace_text_changed)); replace_container->add_child(replace_line_edit); replace_all_button = memnew(Button); replace_all_button->set_text(TTRC("Replace all (no undo)")); replace_all_button->connect(SceneStringName(pressed), callable_mp(this, &FindInFilesPanel::_on_replace_all_clicked)); replace_container->add_child(replace_all_button); replace_container->hide(); vbc->add_child(replace_container); } } //----------------------------------------------------------------------------- FindInFilesPanel *FindInFilesContainer::_create_new_panel() { int index = tabs->get_current_tab(); FindInFilesPanel *panel = memnew(FindInFilesPanel); tabs->add_child(panel); tabs->move_child(panel, index + 1); // New panel is added after the current activated panel. tabs->set_current_tab(index + 1); _update_bar_visibility(); panel->connect("result_selected", callable_mp(this, &FindInFilesContainer::_result_selected)); panel->connect("files_modified", callable_mp(this, &FindInFilesContainer::_files_modified)); panel->connect("close_button_clicked", callable_mp(this, &FindInFilesContainer::_close_panel).bind(panel)); return panel; } FindInFilesPanel *FindInFilesContainer::_get_current_panel() { return Object::cast_to(tabs->get_current_tab_control()); } FindInFilesPanel *FindInFilesContainer::get_panel_for_results(const String &p_label) { FindInFilesPanel *panel = nullptr; // Prefer the current panel. if (_get_current_panel() && !_get_current_panel()->is_keep_results()) { panel = _get_current_panel(); } else { // Find the first panel which does not keep results. for (int i = 0; i < tabs->get_tab_count(); i++) { FindInFilesPanel *p = Object::cast_to(tabs->get_tab_control(i)); if (p && !p->is_keep_results()) { panel = p; tabs->set_current_tab(i); break; } } if (!panel) { panel = _create_new_panel(); } } tabs->set_tab_title(tabs->get_current_tab(), p_label); return panel; } void FindInFilesContainer::_notification(int p_what) { switch (p_what) { case NOTIFICATION_POSTINITIALIZE: { connect("closed", callable_mp(this, &FindInFilesContainer::_on_dock_closed)); } break; } } void FindInFilesContainer::_on_theme_changed() { const Ref bottom_panel_style = EditorNode::get_singleton()->get_editor_theme()->get_stylebox(SNAME("BottomPanel"), EditorStringName(EditorStyles)); if (bottom_panel_style.is_valid()) { begin_bulk_theme_override(); add_theme_constant_override("margin_top", -bottom_panel_style->get_margin(SIDE_TOP)); add_theme_constant_override("margin_left", -bottom_panel_style->get_margin(SIDE_LEFT)); add_theme_constant_override("margin_right", -bottom_panel_style->get_margin(SIDE_RIGHT)); add_theme_constant_override("margin_bottom", -bottom_panel_style->get_margin(SIDE_BOTTOM)); end_bulk_theme_override(); } } void FindInFilesContainer::_result_selected(const String &p_fpath, int p_line_number, int p_begin, int p_end) { emit_signal(SNAME("result_selected"), p_fpath, p_line_number, p_begin, p_end); } void FindInFilesContainer::_files_modified() { emit_signal(SNAME("files_modified")); } void FindInFilesContainer::_close_panel(FindInFilesPanel *p_panel) { ERR_FAIL_COND_MSG(p_panel->get_parent() != tabs, "This panel is not a child!"); tabs->remove_child(p_panel); p_panel->queue_free(); _update_bar_visibility(); if (tabs->get_tab_count() == 0) { close(); } } void FindInFilesContainer::_on_dock_closed() { while (tabs->get_tab_count() > 0) { Control *tab = tabs->get_tab_control(0); tabs->remove_child(tab); tab->queue_free(); } _update_bar_visibility(); } void FindInFilesContainer::_on_tab_close_pressed(int p_tab) { FindInFilesPanel *panel = Object::cast_to(tabs->get_tab_control(p_tab)); if (panel) { _close_panel(panel); } } void FindInFilesContainer::_update_bar_visibility() { if (!update_bar) { return; } // If tab count <= 1, behaves like this is not a TabContainer and the bar is hidden. bool bar_visible = tabs->get_tab_count() > 1; tabs->set_tabs_visible(bar_visible); // Hide or show the search labels based on the visibility of the bar, as the search terms are displayed in the title of each tab. for (int i = 0; i < tabs->get_tab_count(); i++) { FindInFilesPanel *panel = Object::cast_to(tabs->get_tab_control(i)); if (panel) { panel->set_search_labels_visibility(!bar_visible); } } } void FindInFilesContainer::_bar_menu_option(int p_option) { int tab_index = tabs->get_current_tab(); switch (p_option) { case PANEL_CLOSE: { _on_tab_close_pressed(tab_index); } break; case PANEL_CLOSE_OTHERS: { update_bar = false; FindInFilesPanel *panel = Object::cast_to(tabs->get_tab_control(tab_index)); for (int i = tabs->get_tab_count() - 1; i >= 0; i--) { FindInFilesPanel *p = Object::cast_to(tabs->get_tab_control(i)); if (p != panel) { _close_panel(p); } } update_bar = true; _update_bar_visibility(); } break; case PANEL_CLOSE_RIGHT: { update_bar = false; for (int i = tabs->get_tab_count() - 1; i > tab_index; i--) { _on_tab_close_pressed(i); } update_bar = true; _update_bar_visibility(); } break; case PANEL_CLOSE_ALL: { update_bar = false; for (int i = tabs->get_tab_count() - 1; i >= 0; i--) { _on_tab_close_pressed(i); } update_bar = true; } break; } } void FindInFilesContainer::_bar_input(const Ref &p_input) { int tab_id = tabs->get_tab_bar()->get_hovered_tab(); Ref mb = p_input; if (tab_id >= 0 && mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) { tabs_context_menu->set_item_disabled(tabs_context_menu->get_item_index(PANEL_CLOSE_RIGHT), tab_id == tabs->get_tab_count() - 1); tabs_context_menu->set_position(tabs->get_tab_bar()->get_screen_position() + mb->get_position()); tabs_context_menu->reset_size(); tabs_context_menu->popup(); } } void FindInFilesContainer::update_layout(EditorDock::DockLayout p_layout) { for (Node *node : tabs->iterate_children()) { FindInFilesPanel *panel = Object::cast_to(node); if (panel) { panel->update_layout(p_layout); } } } void FindInFilesContainer::_bind_methods() { ADD_SIGNAL(MethodInfo("result_selected", PropertyInfo(Variant::STRING, "path"), PropertyInfo(Variant::INT, "line_number"), PropertyInfo(Variant::INT, "begin"), PropertyInfo(Variant::INT, "end"))); ADD_SIGNAL(MethodInfo("files_modified")); } FindInFilesContainer::FindInFilesContainer() { set_name(TTRC("Search Results")); set_icon_name("Search"); set_dock_shortcut(ED_SHORTCUT_AND_COMMAND("bottom_panels/toggle_search_results_bottom_panel", TTRC("Toggle Search Results Bottom Panel"))); set_default_slot(EditorDock::DOCK_SLOT_BOTTOM); set_available_layouts(EditorDock::DOCK_LAYOUT_HORIZONTAL | EditorDock::DOCK_LAYOUT_FLOATING); set_global(false); set_transient(true); set_closable(true); set_custom_minimum_size(Size2(0, 200 * EDSCALE)); tabs = memnew(TabContainer); tabs->set_tabs_visible(false); add_child(tabs); tabs->set_drag_to_rearrange_enabled(true); tabs->get_tab_bar()->set_select_with_rmb(true); tabs->get_tab_bar()->set_tab_close_display_policy(TabBar::CLOSE_BUTTON_SHOW_ACTIVE_ONLY); tabs->get_tab_bar()->connect("tab_close_pressed", callable_mp(this, &FindInFilesContainer::_on_tab_close_pressed)); tabs->get_tab_bar()->connect(SceneStringName(gui_input), callable_mp(this, &FindInFilesContainer::_bar_input)); tabs_context_menu = memnew(PopupMenu); add_child(tabs_context_menu); tabs_context_menu->add_item(TTRC("Close Tab"), PANEL_CLOSE); tabs_context_menu->add_item(TTRC("Close Other Tabs"), PANEL_CLOSE_OTHERS); tabs_context_menu->add_item(TTRC("Close Tabs to the Right"), PANEL_CLOSE_RIGHT); tabs_context_menu->add_item(TTRC("Close All Tabs"), PANEL_CLOSE_ALL); tabs_context_menu->connect(SceneStringName(id_pressed), callable_mp(this, &FindInFilesContainer::_bar_menu_option)); EditorNode::get_singleton()->get_gui_base()->connect(SceneStringName(theme_changed), callable_mp(this, &FindInFilesContainer::_on_theme_changed)); } void FindInFiles::_start_search(bool p_with_replace) { FindInFilesPanel *panel = container->get_panel_for_results((p_with_replace ? TTR("Replace:") : TTR("Find:")) + " " + dialog->get_search_text()); FindInFilesSearch *search = panel->get_finder(); search->set_search_text(dialog->get_search_text()); search->set_match_case(dialog->is_match_case()); search->set_whole_words(dialog->is_whole_words()); search->set_folder(dialog->get_folder()); search->set_filter(dialog->get_filter()); search->set_includes(dialog->get_includes()); search->set_excludes(dialog->get_excludes()); panel->set_with_replace(p_with_replace); panel->set_replace_text(dialog->get_replace_text()); panel->start_search(); container->make_visible(); } void FindInFiles::open_dialog(const String &p_initial_text, bool p_replace) { dialog->set_replace_mode(p_replace); dialog->set_search_text(p_initial_text); if (p_replace) { dialog->set_replace_text(String()); } dialog->popup_centered(); } void FindInFiles::_result_selected(const String &p_fpath, int p_line_number, int p_begin, int p_end) { emit_signal(SNAME("result_selected"), p_fpath, p_line_number, p_begin, p_end); } void FindInFiles::_files_modified() { emit_signal(SNAME("files_modified")); } void FindInFiles::_bind_methods() { ADD_SIGNAL(MethodInfo("result_selected", PropertyInfo(Variant::STRING, "path"), PropertyInfo(Variant::INT, "line_number"), PropertyInfo(Variant::INT, "begin"), PropertyInfo(Variant::INT, "end"))); ADD_SIGNAL(MethodInfo("files_modified")); } FindInFiles::FindInFiles() { dialog = memnew(FindInFilesDialog); dialog->connect("find_requested", callable_mp(this, &FindInFiles::_start_search).bind(false)); dialog->connect("replace_requested", callable_mp(this, &FindInFiles::_start_search).bind(true)); EditorNode::get_singleton()->get_gui_base()->add_child(dialog); container = memnew(FindInFilesContainer); EditorDockManager::get_singleton()->add_dock(container); container->close(); container->connect("result_selected", callable_mp(this, &FindInFiles::_result_selected)); container->connect("files_modified", callable_mp(this, &FindInFiles::_files_modified)); }