From f637ad84e3af9cecddcd0ff3c89609087ab9701e Mon Sep 17 00:00:00 2001 From: Marc Gilleron Date: Mon, 12 Feb 2018 02:36:15 +0100 Subject: [PATCH] Added find & replace in files --- editor/find_in_files.cpp | 829 ++++++++++++++++++++++++ editor/find_in_files.h | 184 ++++++ editor/plugins/script_editor_plugin.cpp | 172 +++-- editor/plugins/script_editor_plugin.h | 13 + editor/plugins/script_text_editor.cpp | 23 +- editor/plugins/script_text_editor.h | 2 + 6 files changed, 1155 insertions(+), 68 deletions(-) create mode 100644 editor/find_in_files.cpp create mode 100644 editor/find_in_files.h diff --git a/editor/find_in_files.cpp b/editor/find_in_files.cpp new file mode 100644 index 00000000000..9442bbc0e8d --- /dev/null +++ b/editor/find_in_files.cpp @@ -0,0 +1,829 @@ +/*************************************************************************/ +/* find_in_files.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2018 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2018 Godot Engine contributors (cf. AUTHORS.md) */ +/* */ +/* 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/os/dir_access.h" +#include "core/os/os.h" +#include "editor_scale.h" +#include "scene/gui/box_container.h" +#include "scene/gui/button.h" +#include "scene/gui/check_box.h" +#include "scene/gui/file_dialog.h" +#include "scene/gui/grid_container.h" +#include "scene/gui/item_list.h" +#include "scene/gui/label.h" +#include "scene/gui/line_edit.h" +#include "scene/gui/progress_bar.h" + +#define ROOT_PREFIX "res://" + +const char *FindInFiles::SIGNAL_RESULT_FOUND = "result_found"; +const char *FindInFiles::SIGNAL_FINISHED = "finished"; + +// TODO Would be nice in Vector and PoolVectors +template +inline void pop_back(T &container) { + container.resize(container.size() - 1); +} + +// TODO Copied from TextEdit private, would be nice to extract it in a single place +static bool is_text_char(CharType c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; +} + +FindInFiles::FindInFiles() { + _root_prefix = ROOT_PREFIX; + _extension_filter.insert("gd"); + _extension_filter.insert("cs"); + _searching = false; + _whole_words = true; + _match_case = true; +} + +void FindInFiles::set_search_text(String p_pattern) { + _pattern = p_pattern; +} + +void FindInFiles::set_whole_words(bool p_whole_word) { + _whole_words = p_whole_word; +} + +void FindInFiles::set_match_case(bool p_match_case) { + _match_case = p_match_case; +} + +void FindInFiles::set_folder(String folder) { + _root_dir = folder; +} + +void FindInFiles::set_filter(const Set &exts) { + _extension_filter = exts; +} + +void FindInFiles::_notification(int p_notification) { + if (p_notification == NOTIFICATION_PROCESS) { + _process(); + } +} + +void FindInFiles::start() { + if (_pattern == "") { + print_line("Nothing to search, pattern is empty"); + emit_signal(SIGNAL_FINISHED); + return; + } + if (_extension_filter.size() == 0) { + print_line("Nothing to search, filter matches no files"); + emit_signal(SIGNAL_FINISHED); + return; + } + + // Init search + _current_dir = ""; + PoolStringArray init_folder; + init_folder.append(_root_dir); + _folders_stack.push_back(init_folder); + + _initial_files_count = 0; + + _searching = true; + set_process(true); +} + +void FindInFiles::stop() { + _searching = false; + _current_dir = ""; + set_process(false); +} + +void FindInFiles::_process() { + // This part can be moved to a thread if needed + + OS &os = *OS::get_singleton(); + float duration = 0.0; + while (duration < 1.0 / 120.0) { + float time_before = os.get_ticks_msec(); + _iterate(); + duration += (os.get_ticks_msec() - time_before); + } +} + +void FindInFiles::_iterate() { + + if (_folders_stack.size() != 0) { + + // Scan folders first so we can build a list of files and have progress info later + + PoolStringArray &folders_to_scan = _folders_stack[_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.plus_file(folder_name); + + PoolStringArray sub_dirs; + _scan_dir(_root_prefix + _current_dir, sub_dirs); + + if (sub_dirs.size() != 0) { + _folders_stack.push_back(sub_dirs); + } + + } else { + // Go back one level + + pop_back(_folders_stack); + _current_dir = _current_dir.get_base_dir(); + + if (_folders_stack.size() == 0) { + // 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(_root_prefix + fpath); + + } else { + print_line("Search complete"); + set_process(false); + _current_dir = ""; + _searching = false; + emit_signal(SIGNAL_FINISHED); + } +} + +float FindInFiles::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 FindInFiles::_scan_dir(String path, PoolStringArray &out_folders) { + + DirAccess *dir = DirAccess::open(path); + if (dir == NULL) { + print_line("Cannot open directory! " + path); + return; + } + + //print_line(String("Scanning ") + path); + + dir->list_dir_begin(); + + for (int i = 0; i < 1000; ++i) { + String file = dir->get_next(); + + if (file == "") + break; + + // Ignore special dirs and hidden dirs (such as .git and .import) + if (file == "." || file == ".." || file.begins_with(".")) + continue; + + if (dir->current_is_dir()) + out_folders.append(file); + + else { + String file_ext = file.get_extension(); + if (_extension_filter.has(file_ext)) { + _files_to_scan.push_back(file); + } + } + } +} + +void FindInFiles::_scan_file(String fpath) { + + FileAccess *f = FileAccess::open(fpath, FileAccess::READ); + if (f == NULL) { + f->close(); + print_line(String("Cannot open file ") + 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(); + + // Find all occurrences in the current line + while (true) { + begin = _match_case ? line.find(_pattern, end) : line.findn(_pattern, end); + + if (begin == -1) + break; + + end = begin + _pattern.length(); + + if (_whole_words) { + if (begin > 0 && is_text_char(line[begin - 1])) { + continue; + } + if (end < line.size() && is_text_char(line[end])) { + continue; + } + } + + emit_signal(SIGNAL_RESULT_FOUND, fpath, line_number, begin, end, line); + } + } + + f->close(); +} + +void FindInFiles::_bind_methods() { + + ADD_SIGNAL(MethodInfo(SIGNAL_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(SIGNAL_FINISHED)); +} + +//----------------------------------------------------------------------------- +const char *FindInFilesDialog::SIGNAL_FIND_REQUESTED = "find_requested"; +const char *FindInFilesDialog::SIGNAL_REPLACE_REQUESTED = "replace_requested"; + +FindInFilesDialog::FindInFilesDialog() { + + set_custom_minimum_size(Size2(400, 190)); + set_resizable(true); + set_title(TTR("Find in files")); + + VBoxContainer *vbc = memnew(VBoxContainer); + vbc->set_anchor_and_margin(MARGIN_LEFT, ANCHOR_BEGIN, 8 * EDSCALE); + vbc->set_anchor_and_margin(MARGIN_TOP, ANCHOR_BEGIN, 8 * EDSCALE); + vbc->set_anchor_and_margin(MARGIN_RIGHT, ANCHOR_END, -8 * EDSCALE); + vbc->set_anchor_and_margin(MARGIN_BOTTOM, 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(TTR("Find: ")); + gc->add_child(find_label); + + _search_text_line_edit = memnew(LineEdit); + _search_text_line_edit->set_h_size_flags(SIZE_EXPAND_FILL); + _search_text_line_edit->connect("text_changed", this, "_on_search_text_modified"); + _search_text_line_edit->connect("text_entered", this, "_on_search_text_entered"); + gc->add_child(_search_text_line_edit); + + { + Control *placeholder = memnew(Control); + gc->add_child(placeholder); + } + + { + HBoxContainer *hbc = memnew(HBoxContainer); + + _whole_words_checkbox = memnew(CheckBox); + _whole_words_checkbox->set_text(TTR("Whole words")); + _whole_words_checkbox->set_pressed(true); + hbc->add_child(_whole_words_checkbox); + + _match_case_checkbox = memnew(CheckBox); + _match_case_checkbox->set_text(TTR("Match case")); + _match_case_checkbox->set_pressed(true); + hbc->add_child(_match_case_checkbox); + + gc->add_child(hbc); + } + + Label *folder_label = memnew(Label); + folder_label->set_text(TTR("Folder: ")); + gc->add_child(folder_label); + + { + HBoxContainer *hbc = memnew(HBoxContainer); + + Label *prefix_label = memnew(Label); + prefix_label->set_text(ROOT_PREFIX); + hbc->add_child(prefix_label); + + _folder_line_edit = memnew(LineEdit); + _folder_line_edit->set_h_size_flags(SIZE_EXPAND_FILL); + hbc->add_child(_folder_line_edit); + + Button *folder_button = memnew(Button); + folder_button->set_text("..."); + folder_button->connect("pressed", this, "_on_folder_button_pressed"); + hbc->add_child(folder_button); + + _folder_dialog = memnew(FileDialog); + _folder_dialog->set_mode(FileDialog::MODE_OPEN_DIR); + _folder_dialog->connect("dir_selected", this, "_on_folder_selected"); + add_child(_folder_dialog); + + gc->add_child(hbc); + } + + Label *filter_label = memnew(Label); + filter_label->set_text(TTR("Filter: ")); + gc->add_child(filter_label); + + { + HBoxContainer *hbc = memnew(HBoxContainer); + + Vector exts; + exts.push_back("gd"); + exts.push_back("cs"); + + for (int i = 0; i < exts.size(); ++i) { + CheckBox *cb = memnew(CheckBox); + cb->set_text(exts[i]); + cb->set_pressed(true); + hbc->add_child(cb); + _filters.push_back(cb); + } + + gc->add_child(hbc); + } + + { + Control *placeholder = memnew(Control); + placeholder->set_custom_minimum_size(Size2(0, EDSCALE * 16)); + vbc->add_child(placeholder); + } + + { + HBoxContainer *hbc = memnew(HBoxContainer); + hbc->set_alignment(HBoxContainer::ALIGN_CENTER); + + _find_button = memnew(Button); + _find_button->set_text(TTR("Find...")); + _find_button->connect("pressed", this, "_on_find_button_pressed"); + _find_button->set_disabled(true); + hbc->add_child(_find_button); + + { + Control *placeholder = memnew(Control); + placeholder->set_custom_minimum_size(Size2(EDSCALE * 16, 0)); + hbc->add_child(placeholder); + } + + _replace_button = memnew(Button); + _replace_button->set_text(TTR("Replace...")); + _replace_button->connect("pressed", this, "_on_replace_button_pressed"); + _replace_button->set_disabled(true); + hbc->add_child(_replace_button); + + { + Control *placeholder = memnew(Control); + placeholder->set_custom_minimum_size(Size2(EDSCALE * 16, 0)); + hbc->add_child(placeholder); + } + + Button *cancel_button = memnew(Button); + cancel_button->set_text(TTR("Cancel")); + cancel_button->connect("pressed", this, "hide"); + hbc->add_child(cancel_button); + + vbc->add_child(hbc); + } +} + +void FindInFilesDialog::set_search_text(String text) { + _search_text_line_edit->set_text(text); +} + +String FindInFilesDialog::get_search_text() const { + String text = _search_text_line_edit->get_text(); + return text.strip_edges(); +} + +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 text = _folder_line_edit->get_text(); + return text.strip_edges(); +} + +Set FindInFilesDialog::get_filter() const { + Set filters; + for (int i = 0; i < _filters.size(); ++i) { + CheckBox *cb = _filters[i]; + if (cb->is_pressed()) { + filters.insert(_filters[i]->get_text()); + } + } + return filters; +} + +void FindInFilesDialog::_notification(int p_what) { + if (p_what == NOTIFICATION_VISIBILITY_CHANGED) { + if (is_visible()) { + // Doesn't work more than once if not deferred... + _search_text_line_edit->call_deferred("grab_focus"); + _search_text_line_edit->select_all(); + } + } +} + +void FindInFilesDialog::_on_folder_button_pressed() { + _folder_dialog->popup_centered_ratio(); +} + +void FindInFilesDialog::_on_find_button_pressed() { + emit_signal(SIGNAL_FIND_REQUESTED); + hide(); +} + +void FindInFilesDialog::_on_replace_button_pressed() { + emit_signal(SIGNAL_REPLACE_REQUESTED); + hide(); +} + +void FindInFilesDialog::_on_search_text_modified(String text) { + + ERR_FAIL_COND(!_find_button); + ERR_FAIL_COND(!_replace_button); + + _find_button->set_disabled(get_search_text().empty()); + _replace_button->set_disabled(get_search_text().empty()); +} + +void FindInFilesDialog::_on_search_text_entered(String text) { + // This allows to trigger a global search without leaving the keyboard + if (!_find_button->is_disabled()) + _on_find_button_pressed(); +} + +void FindInFilesDialog::_on_folder_selected(String path) { + int i = path.find("://"); + if (i != -1) + path = path.right(i + 3); + _folder_line_edit->set_text(path); +} + +void FindInFilesDialog::_bind_methods() { + + ClassDB::bind_method("_on_folder_button_pressed", &FindInFilesDialog::_on_folder_button_pressed); + ClassDB::bind_method("_on_find_button_pressed", &FindInFilesDialog::_on_find_button_pressed); + ClassDB::bind_method("_on_replace_button_pressed", &FindInFilesDialog::_on_replace_button_pressed); + ClassDB::bind_method("_on_folder_selected", &FindInFilesDialog::_on_folder_selected); + ClassDB::bind_method("_on_search_text_modified", &FindInFilesDialog::_on_search_text_modified); + ClassDB::bind_method("_on_search_text_entered", &FindInFilesDialog::_on_search_text_entered); + + ADD_SIGNAL(MethodInfo(SIGNAL_FIND_REQUESTED)); + ADD_SIGNAL(MethodInfo(SIGNAL_REPLACE_REQUESTED)); +} + +//----------------------------------------------------------------------------- +const char *FindInFilesPanel::SIGNAL_RESULT_SELECTED = "result_selected"; +const char *FindInFilesPanel::SIGNAL_FILES_MODIFIED = "files_modified"; + +FindInFilesPanel::FindInFilesPanel() { + + _finder = memnew(FindInFiles); + _finder->connect(FindInFiles::SIGNAL_RESULT_FOUND, this, "_on_result_found"); + _finder->connect(FindInFiles::SIGNAL_FINISHED, this, "_on_finished"); + add_child(_finder); + + VBoxContainer *vbc = memnew(VBoxContainer); + vbc->set_anchor_and_margin(MARGIN_LEFT, ANCHOR_BEGIN, 0); + vbc->set_anchor_and_margin(MARGIN_TOP, ANCHOR_BEGIN, 0); + vbc->set_anchor_and_margin(MARGIN_RIGHT, ANCHOR_END, 0); + vbc->set_anchor_and_margin(MARGIN_BOTTOM, ANCHOR_END, 0); + add_child(vbc); + + { + HBoxContainer *hbc = memnew(HBoxContainer); + + Label *find_label = memnew(Label); + find_label->set_text(TTR("Find: ")); + hbc->add_child(find_label); + + _search_text_label = memnew(Label); + _search_text_label->add_font_override("font", get_font("source", "EditorFonts")); + hbc->add_child(_search_text_label); + + _progress_bar = memnew(ProgressBar); + _progress_bar->set_h_size_flags(SIZE_EXPAND_FILL); + hbc->add_child(_progress_bar); + set_progress_visible(false); + + _status_label = memnew(Label); + hbc->add_child(_status_label); + + _cancel_button = memnew(Button); + _cancel_button->set_text(TTR("Cancel")); + _cancel_button->connect("pressed", this, "_on_cancel_button_clicked"); + _cancel_button->set_disabled(true); + hbc->add_child(_cancel_button); + + vbc->add_child(hbc); + } + + // In the future, this should be replaced by a more specific list container, + // which can highlight text regions and change opacity for enabled/disabled states + _results_display = memnew(ItemList); + _results_display->add_font_override("font", get_font("source", "EditorFonts")); + _results_display->set_v_size_flags(SIZE_EXPAND_FILL); + _results_display->connect("item_selected", this, "_on_result_selected"); + vbc->add_child(_results_display); + + { + _replace_container = memnew(HBoxContainer); + + Label *replace_label = memnew(Label); + replace_label->set_text(TTR("Replace: ")); + _replace_container->add_child(replace_label); + + _replace_line_edit = memnew(LineEdit); + _replace_line_edit->set_h_size_flags(SIZE_EXPAND_FILL); + _replace_line_edit->connect("text_changed", this, "_on_replace_text_changed"); + _replace_container->add_child(_replace_line_edit); + + _replace_all_button = memnew(Button); + _replace_all_button->set_text(TTR("Replace all (no undo)")); + _replace_all_button->connect("pressed", this, "_on_replace_all_clicked"); + _replace_container->add_child(_replace_all_button); + + _replace_container->hide(); + + vbc->add_child(_replace_container); + } +} + +void FindInFilesPanel::set_with_replace(bool with_replace) { + + _replace_container->set_visible(with_replace); +} + +void FindInFilesPanel::start_search() { + + _results_display->clear(); + _status_label->set_text(TTR("Searching...")); + _search_text_label->set_text(_finder->get_search_text()); + + set_process(true); + set_progress_visible(true); + + _finder->start(); + + update_replace_buttons(); + _cancel_button->set_disabled(false); +} + +void FindInFilesPanel::stop_search() { + + _finder->stop(); + + _status_label->set_text(""); + update_replace_buttons(); + set_progress_visible(false); + _cancel_button->set_disabled(true); +} + +void FindInFilesPanel::_notification(int p_what) { + if (p_what == NOTIFICATION_PROCESS) { + _progress_bar->set_as_ratio(_finder->get_progress()); + } +} + +void FindInFilesPanel::_on_result_found(String fpath, int line_number, int begin, int end, String text) { + + int i = _results_display->get_item_count(); + _results_display->add_item(fpath + ": " + String::num(line_number) + ": " + text.replace("\t", " ")); + _results_display->set_item_metadata(i, varray(fpath, line_number, begin, end)); +} + +void FindInFilesPanel::_on_finished() { + + _status_label->set_text(TTR("Search complete")); + update_replace_buttons(); + set_progress_visible(false); + _cancel_button->set_disabled(true); +} + +void FindInFilesPanel::_on_cancel_button_clicked() { + stop_search(); +} + +void FindInFilesPanel::_on_result_selected(int i) { + + Array meta = _results_display->get_item_metadata(i); + emit_signal(SIGNAL_RESULT_SELECTED, meta[0], meta[1], meta[2], meta[3]); +} + +void FindInFilesPanel::_on_replace_text_changed(String text) { + update_replace_buttons(); +} + +void FindInFilesPanel::_on_replace_all_clicked() { + + String replace_text = get_replace_text(); + ERR_FAIL_COND(replace_text.empty()); + + String last_fpath; + PoolIntArray locations; + PoolStringArray modified_files; + + for (int i = 0; i < _results_display->get_item_count(); ++i) { + + Array meta = _results_display->get_item_metadata(i); + + String fpath = meta[0]; + + // Results are sorted by file, so we can batch replaces + if (fpath != last_fpath) { + if (locations.size() != 0) { + apply_replaces_in_file(last_fpath, locations, replace_text); + modified_files.append(last_fpath); + locations.resize(0); + } + } + + locations.append(meta[1]); // line_number + locations.append(meta[2]); // begin + locations.append(meta[3]); // end + + last_fpath = fpath; + } + + if (locations.size() != 0) { + apply_replaces_in_file(last_fpath, locations, replace_text); + modified_files.append(last_fpath); + } + + // Hide replace bar so we can't trigger the action twice without doing a new search + set_with_replace(false); + + emit_signal(SIGNAL_FILES_MODIFIED, modified_files); +} + +// Same as get_line, but preserves line ending characters +class ConservativeGetLine { +public: + String get_line(FileAccess *f) { + + _line_buffer.clear(); + + CharType c = f->get_8(); + + while (!f->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 = f->get_8(); + } + + _line_buffer.push_back(0); + return String::utf8(_line_buffer.ptr()); + } + +private: + Vector _line_buffer; +}; + +void FindInFilesPanel::apply_replaces_in_file(String fpath, PoolIntArray locations, String text) { + + ERR_FAIL_COND(locations.size() % 3 != 0); + + //print_line(String("Replacing {0} occurrences in {1}").format(varray(fpath, locations.size() / 3))); + + // 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 loosing changes or loosing replaces. + + FileAccess *f = FileAccess::open(fpath, FileAccess::READ); + ERR_FAIL_COND(f == NULL); + + String buffer; + int current_line = 1; + + ConservativeGetLine conservative; + + String line = conservative.get_line(f); + + PoolIntArray::Read locations_read = locations.read(); + for (int i = 0; i < locations.size(); i += 3) { + + int repl_line_number = locations_read[i]; + int repl_begin = locations_read[i + 1]; + int repl_end = locations_read[i + 2]; + + while (current_line < repl_line_number) { + buffer += line; + line = conservative.get_line(f); + ++current_line; + } + + line = line.left(repl_begin) + text + line.right(repl_end); + } + + 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(fpath, FileAccess::WRITE); + ERR_FAIL_COND(err != OK); + + f->store_string(buffer); + + f->close(); +} + +String FindInFilesPanel::get_replace_text() { + return _replace_line_edit->get_text().strip_edges(); +} + +void FindInFilesPanel::update_replace_buttons() { + + String text = get_replace_text(); + bool disabled = text.empty() || _finder->is_searching(); + + _replace_all_button->set_disabled(disabled); +} + +void FindInFilesPanel::set_progress_visible(bool visible) { + _progress_bar->set_self_modulate(Color(1, 1, 1, visible ? 1 : 0)); +} + +void FindInFilesPanel::_bind_methods() { + + ClassDB::bind_method("_on_result_found", &FindInFilesPanel::_on_result_found); + ClassDB::bind_method("_on_finished", &FindInFilesPanel::_on_finished); + ClassDB::bind_method("_on_cancel_button_clicked", &FindInFilesPanel::_on_cancel_button_clicked); + ClassDB::bind_method("_on_result_selected", &FindInFilesPanel::_on_result_selected); + ClassDB::bind_method("_on_replace_text_changed", &FindInFilesPanel::_on_replace_text_changed); + ClassDB::bind_method("_on_replace_all_clicked", &FindInFilesPanel::_on_replace_all_clicked); + + ADD_SIGNAL(MethodInfo(SIGNAL_RESULT_SELECTED, + PropertyInfo(Variant::STRING, "path"), + PropertyInfo(Variant::INT, "line_number"), + PropertyInfo(Variant::INT, "begin"), + PropertyInfo(Variant::INT, "end"))); + + ADD_SIGNAL(MethodInfo(SIGNAL_FILES_MODIFIED, PropertyInfo(Variant::STRING, "paths"))); +} diff --git a/editor/find_in_files.h b/editor/find_in_files.h new file mode 100644 index 00000000000..d57184960b4 --- /dev/null +++ b/editor/find_in_files.h @@ -0,0 +1,184 @@ +/*************************************************************************/ +/* find_in_files.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2018 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2018 Godot Engine contributors (cf. AUTHORS.md) */ +/* */ +/* 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 FIND_IN_FILES_H +#define FIND_IN_FILES_H + +#include "scene/gui/dialogs.h" + +// Performs the actual search +class FindInFiles : public Node { + GDCLASS(FindInFiles, Node) +public: + static const char *SIGNAL_RESULT_FOUND; + static const char *SIGNAL_FINISHED; + + FindInFiles(); + + void set_search_text(String p_pattern); + void set_whole_words(bool p_whole_word); + void set_match_case(bool p_match_case); + void set_folder(String folder); + void set_filter(const Set &exts); + + String get_search_text() const { return _pattern; } + + bool is_whole_words() const { return _whole_words; } + bool is_match_case() const { return _match_case; } + + void start(); + void stop(); + + bool is_searching() const { return _searching; } + float get_progress() const; + +protected: + void _notification(int p_notification); + + static void _bind_methods(); + +private: + void _process(); + void _iterate(); + void _scan_dir(String path, PoolStringArray &out_folders); + void _scan_file(String fpath); + + // Config + String _pattern; + Set _extension_filter; + String _root_prefix; + String _root_dir; + bool _whole_words; + bool _match_case; + + // State + bool _searching; + String _current_dir; + Vector _folders_stack; + Vector _files_to_scan; + int _initial_files_count; +}; + +class LineEdit; +class CheckBox; +class FileDialog; + +// Prompts search parameters +class FindInFilesDialog : public WindowDialog { + GDCLASS(FindInFilesDialog, WindowDialog) +public: + static const char *SIGNAL_FIND_REQUESTED; + static const char *SIGNAL_REPLACE_REQUESTED; + + FindInFilesDialog(); + + void set_search_text(String text); + + String get_search_text() const; + bool is_match_case() const; + bool is_whole_words() const; + String get_folder() const; + Set get_filter() const; + +protected: + static void _bind_methods(); + + void _notification(int p_what); + +private: + void _on_folder_button_pressed(); + void _on_find_button_pressed(); + void _on_replace_button_pressed(); + void _on_folder_selected(String path); + void _on_search_text_modified(String text); + void _on_search_text_entered(String text); + + LineEdit *_search_text_line_edit; + LineEdit *_folder_line_edit; + Vector _filters; + CheckBox *_match_case_checkbox; + CheckBox *_whole_words_checkbox; + Button *_find_button; + Button *_replace_button; + FileDialog *_folder_dialog; +}; + +class Button; +class ItemList; +class ProgressBar; + +// Display search results +class FindInFilesPanel : public Control { + GDCLASS(FindInFilesPanel, Control) +public: + static const char *SIGNAL_RESULT_SELECTED; + static const char *SIGNAL_FILES_MODIFIED; + + FindInFilesPanel(); + + FindInFiles *get_finder() const { return _finder; } + + void set_with_replace(bool with_replace); + + void start_search(); + void stop_search(); + +protected: + static void _bind_methods(); + + void _notification(int p_what); + +private: + void _on_result_found(String fpath, int line_number, int begin, int end, String text); + void _on_finished(); + void _on_cancel_button_clicked(); + void _on_result_selected(int i); + void _on_replace_text_changed(String text); + void _on_replace_all_clicked(); + + void apply_replaces_in_file(String fpath, PoolIntArray locations, String text); + + void update_replace_buttons(); + String get_replace_text(); + void set_progress_visible(bool visible); + + FindInFiles *_finder; + Label *_search_text_label; + ItemList *_results_display; + Label *_status_label; + Button *_cancel_button; + ProgressBar *_progress_bar; + + HBoxContainer *_replace_container; + LineEdit *_replace_line_edit; + Button *_replace_all_button; +}; + +#endif // FIND_IN_FILES_H diff --git a/editor/plugins/script_editor_plugin.cpp b/editor/plugins/script_editor_plugin.cpp index d18422c0c04..879b9f5f19c 100644 --- a/editor/plugins/script_editor_plugin.cpp +++ b/editor/plugins/script_editor_plugin.cpp @@ -39,9 +39,11 @@ #include "core/project_settings.h" #include "editor/editor_node.h" #include "editor/editor_settings.h" +#include "editor/find_in_files.h" #include "editor/node_dock.h" #include "editor/script_editor_debugger.h" #include "scene/main/viewport.h" +#include "script_text_editor.h" /*** SCRIPT EDITOR ****/ @@ -54,6 +56,8 @@ void ScriptEditorBase::_bind_methods() { ADD_SIGNAL(MethodInfo("request_open_script_at_line", PropertyInfo(Variant::OBJECT, "script"), PropertyInfo(Variant::INT, "line"))); ADD_SIGNAL(MethodInfo("request_save_history")); ADD_SIGNAL(MethodInfo("go_to_help", PropertyInfo(Variant::STRING, "what"))); + // TODO This signal is no use for VisualScript... + ADD_SIGNAL(MethodInfo("search_in_files_requested", PropertyInfo(Variant::STRING, "text"))); } static bool _can_open_in_editor(Script *p_script) { @@ -300,15 +304,9 @@ void ScriptEditor::_script_created(Ref