diff options
-rw-r--r-- | editor/find_in_files.cpp | 829 | ||||
-rw-r--r-- | editor/find_in_files.h | 184 | ||||
-rw-r--r-- | editor/plugins/script_editor_plugin.cpp | 172 | ||||
-rw-r--r-- | editor/plugins/script_editor_plugin.h | 13 | ||||
-rw-r--r-- | editor/plugins/script_text_editor.cpp | 23 | ||||
-rw-r--r-- | editor/plugins/script_text_editor.h | 2 |
6 files changed, 1155 insertions, 68 deletions
diff --git a/editor/find_in_files.cpp b/editor/find_in_files.cpp new file mode 100644 index 0000000000..9442bbc0e8 --- /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 <typename T> +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<String> &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<float>(_initial_files_count - _files_to_scan.size()) / static_cast<float>(_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<String> 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<String> FindInFilesDialog::get_filter() const { + Set<String> 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<char> _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 0000000000..d57184960b --- /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<String> &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<String> _extension_filter; + String _root_prefix; + String _root_dir; + bool _whole_words; + bool _match_case; + + // State + bool _searching; + String _current_dir; + Vector<PoolStringArray> _folders_stack; + Vector<String> _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<String> 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<CheckBox *> _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 d18422c0c0..879b9f5f19 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<Script> p_script) { void ScriptEditor::_goto_script_line2(int p_line) { - int selected = tab_container->get_current_tab(); - if (selected < 0 || selected >= tab_container->get_child_count()) - return; - - ScriptEditorBase *current = Object::cast_to<ScriptEditorBase>(tab_container->get_child(selected)); - if (!current) - return; - - current->goto_line(p_line); + ScriptEditorBase *current = _get_current_editor(); + if (current) + current->goto_line(p_line); } void ScriptEditor::_goto_script_line(REF p_script, int p_line) { @@ -318,19 +316,22 @@ void ScriptEditor::_goto_script_line(REF p_script, int p_line) { if (edit(p_script, p_line, 0)) { editor->push_item(p_script.ptr()); - int selected = tab_container->get_current_tab(); - if (selected < 0 || selected >= tab_container->get_child_count()) - return; - - ScriptEditorBase *current = Object::cast_to<ScriptEditorBase>(tab_container->get_child(selected)); - if (!current) - return; - - current->goto_line(p_line, true); + ScriptEditorBase *current = _get_current_editor(); + if (current) + current->goto_line(p_line, true); } } } +ScriptEditorBase *ScriptEditor::_get_current_editor() const { + + int selected = tab_container->get_current_tab(); + if (selected < 0 || selected >= tab_container->get_child_count()) + return NULL; + + return Object::cast_to<ScriptEditorBase>(tab_container->get_child(selected)); +} + void ScriptEditor::_update_history_arrows() { script_back->set_disabled(history_pos <= 0); @@ -583,7 +584,7 @@ void ScriptEditor::_close_docs_tab() { } void ScriptEditor::_copy_script_path() { - ScriptEditorBase *se = Object::cast_to<ScriptEditorBase>(tab_container->get_child(tab_container->get_current_tab())); + ScriptEditorBase *se = _get_current_editor(); Ref<Script> script = se->get_edited_script(); OS::get_singleton()->set_clipboard(script->get_path()); } @@ -815,11 +816,8 @@ void ScriptEditor::_file_dialog_action(String p_file) { Ref<Script> ScriptEditor::_get_current_script() { - int selected = tab_container->get_current_tab(); - if (selected < 0 || selected >= tab_container->get_child_count()) - return NULL; + ScriptEditorBase *current = _get_current_editor(); - ScriptEditorBase *current = Object::cast_to<ScriptEditorBase>(tab_container->get_child(selected)); if (current) { return current->get_edited_script(); } else { @@ -935,11 +933,7 @@ void ScriptEditor::_menu_option(int p_option) { } } - int selected = tab_container->get_current_tab(); - if (selected < 0 || selected >= tab_container->get_child_count()) - return; - - ScriptEditorBase *current = Object::cast_to<ScriptEditorBase>(tab_container->get_child(selected)); + ScriptEditorBase *current = _get_current_editor(); if (current) { switch (p_option) { @@ -1029,7 +1023,7 @@ void ScriptEditor::_menu_option(int p_option) { _copy_script_path(); } break; case SHOW_IN_FILE_SYSTEM: { - ScriptEditorBase *se = Object::cast_to<ScriptEditorBase>(tab_container->get_child(tab_container->get_current_tab())); + ScriptEditorBase *se = _get_current_editor(); Ref<Script> script = se->get_edited_script(); FileSystemDock *file_system_dock = EditorNode::get_singleton()->get_filesystem_dock(); file_system_dock->navigate_to_path(script->get_path()); @@ -1219,6 +1213,17 @@ void ScriptEditor::_notification(int p_what) { recent_scripts->set_as_minsize(); } break; + case CanvasItem::NOTIFICATION_VISIBILITY_CHANGED: { + + if (is_visible()) { + find_in_files_button->show(); + } else { + find_in_files->hide(); + find_in_files_button->hide(); + } + + } break; + default: break; } @@ -1226,15 +1231,11 @@ void ScriptEditor::_notification(int p_what) { bool ScriptEditor::can_take_away_focus() const { - int selected = tab_container->get_current_tab(); - if (selected < 0 || selected >= tab_container->get_child_count()) - return true; - - ScriptEditorBase *current = Object::cast_to<ScriptEditorBase>(tab_container->get_child(selected)); - if (!current) + ScriptEditorBase *current = _get_current_editor(); + if (current) + return current->can_lose_focus_on_node_selection(); + else return true; - - return current->can_lose_focus_on_node_selection(); } void ScriptEditor::close_builtin_scripts_from_scene(const String &p_scene) { @@ -1311,20 +1312,13 @@ void ScriptEditor::ensure_focus_current() { if (!is_inside_tree()) return; - int cidx = tab_container->get_current_tab(); - if (cidx < 0 || cidx >= tab_container->get_tab_count()) - return; - - Control *c = Object::cast_to<Control>(tab_container->get_child(cidx)); - ScriptEditorBase *se = Object::cast_to<ScriptEditorBase>(c); - if (!se) - return; - se->ensure_focus(); + ScriptEditorBase *current = _get_current_editor(); + if (current) + current->ensure_focus(); } void ScriptEditor::_members_overview_selected(int p_idx) { - Node *current = tab_container->get_child(tab_container->get_current_tab()); - ScriptEditorBase *se = Object::cast_to<ScriptEditorBase>(current); + ScriptEditorBase *se = _get_current_editor(); if (!se) { return; } @@ -1357,18 +1351,12 @@ void ScriptEditor::ensure_select_current() { if (tab_container->get_child_count() && tab_container->get_current_tab() >= 0) { - Node *current = tab_container->get_child(tab_container->get_current_tab()); - - ScriptEditorBase *se = Object::cast_to<ScriptEditorBase>(current); + ScriptEditorBase *se = _get_current_editor(); if (se) { - Ref<Script> script = se->get_edited_script(); - if (!grab_focus_block && is_visible_in_tree()) se->ensure_focus(); } - - EditorHelp *eh = Object::cast_to<EditorHelp>(current); } _update_selected_editor_menu(); @@ -1408,12 +1396,7 @@ struct _ScriptEditorItemData { void ScriptEditor::_update_members_overview_visibility() { - int selected = tab_container->get_current_tab(); - if (selected < 0 || selected >= tab_container->get_child_count()) - return; - - Node *current = tab_container->get_child(tab_container->get_current_tab()); - ScriptEditorBase *se = Object::cast_to<ScriptEditorBase>(current); + ScriptEditorBase *se = _get_current_editor(); if (!se) { members_overview->set_visible(false); return; @@ -1429,12 +1412,7 @@ void ScriptEditor::_update_members_overview_visibility() { void ScriptEditor::_update_members_overview() { members_overview->clear(); - int selected = tab_container->get_current_tab(); - if (selected < 0 || selected >= tab_container->get_child_count()) - return; - - Node *current = tab_container->get_child(tab_container->get_current_tab()); - ScriptEditorBase *se = Object::cast_to<ScriptEditorBase>(current); + ScriptEditorBase *se = _get_current_editor(); if (!se) { return; } @@ -1777,6 +1755,7 @@ bool ScriptEditor::edit(const Ref<Script> &p_script, int p_line, int p_col, bool se->connect("request_open_script_at_line", this, "_goto_script_line"); se->connect("go_to_help", this, "_help_class_goto"); se->connect("request_save_history", this, "_save_history"); + se->connect("search_in_files_requested", this, "_on_find_in_files_requested"); //test for modification, maybe the script was not edited but was loaded @@ -2483,6 +2462,48 @@ void ScriptEditor::_script_changed() { NodeDock::singleton->update_lists(); } +void ScriptEditor::_on_find_in_files_requested(String text) { + + find_in_files_dialog->set_search_text(text); + find_in_files_dialog->popup_centered_minsize(); +} + +void ScriptEditor::_on_find_in_files_result_selected(String fpath, int line_number, int begin, int end) { + + Ref<Resource> res = ResourceLoader::load(fpath); + edit(res); + + ScriptEditorBase *seb = _get_current_editor(); + + ScriptTextEditor *ste = Object::cast_to<ScriptTextEditor>(seb); + if (ste) { + ste->goto_line_selection(line_number - 1, begin, end); + } +} + +void ScriptEditor::_start_find_in_files(bool with_replace) { + + FindInFiles *f = find_in_files->get_finder(); + + f->set_search_text(find_in_files_dialog->get_search_text()); + f->set_match_case(find_in_files_dialog->is_match_case()); + f->set_whole_words(find_in_files_dialog->is_match_case()); + f->set_folder(find_in_files_dialog->get_folder()); + f->set_filter(find_in_files_dialog->get_filter()); + + find_in_files->set_with_replace(with_replace); + find_in_files->start_search(); + + find_in_files_button->set_pressed(true); + find_in_files->show(); +} + +void ScriptEditor::_on_find_in_files_modified_files(PoolStringArray paths) { + + _test_script_times_on_disk(); + _update_modified_scripts_for_external_editor(); +} + void ScriptEditor::_bind_methods() { ClassDB::bind_method("_file_dialog_action", &ScriptEditor::_file_dialog_action); @@ -2530,6 +2551,10 @@ void ScriptEditor::_bind_methods() { ClassDB::bind_method("_script_list_gui_input", &ScriptEditor::_script_list_gui_input); ClassDB::bind_method("_script_changed", &ScriptEditor::_script_changed); ClassDB::bind_method("_update_recent_scripts", &ScriptEditor::_update_recent_scripts); + ClassDB::bind_method("_on_find_in_files_requested", &ScriptEditor::_on_find_in_files_requested); + ClassDB::bind_method("_start_find_in_files", &ScriptEditor::_start_find_in_files); + ClassDB::bind_method("_on_find_in_files_result_selected", &ScriptEditor::_on_find_in_files_result_selected); + ClassDB::bind_method("_on_find_in_files_modified_files", &ScriptEditor::_on_find_in_files_modified_files); ClassDB::bind_method(D_METHOD("get_drag_data_fw", "point", "from"), &ScriptEditor::get_drag_data_fw); ClassDB::bind_method(D_METHOD("can_drop_data_fw", "point", "data", "from"), &ScriptEditor::can_drop_data_fw); @@ -2785,6 +2810,19 @@ ScriptEditor::ScriptEditor(EditorNode *p_editor) { add_child(help_index); help_index->connect("open_class", this, "_help_class_open"); + find_in_files_dialog = memnew(FindInFilesDialog); + find_in_files_dialog->connect(FindInFilesDialog::SIGNAL_FIND_REQUESTED, this, "_start_find_in_files", varray(false)); + find_in_files_dialog->connect(FindInFilesDialog::SIGNAL_REPLACE_REQUESTED, this, "_start_find_in_files", varray(true)); + add_child(find_in_files_dialog); + find_in_files = memnew(FindInFilesPanel); + find_in_files_button = editor->add_bottom_panel_item(TTR("Search results"), find_in_files); + find_in_files_button->set_tooltip(TTR("Search in files")); + find_in_files->set_custom_minimum_size(Size2(0, 200)); + find_in_files->connect(FindInFilesPanel::SIGNAL_RESULT_SELECTED, this, "_on_find_in_files_result_selected"); + find_in_files->connect(FindInFilesPanel::SIGNAL_FILES_MODIFIED, this, "_on_find_in_files_modified_files"); + find_in_files->hide(); + find_in_files_button->hide(); + history_pos = -1; //debugger_gui->hide(); diff --git a/editor/plugins/script_editor_plugin.h b/editor/plugins/script_editor_plugin.h index e60e4cf8c0..bb0271fa4b 100644 --- a/editor/plugins/script_editor_plugin.h +++ b/editor/plugins/script_editor_plugin.h @@ -115,6 +115,8 @@ public: typedef ScriptEditorBase *(*CreateScriptEditorFunc)(const Ref<Script> &p_script); class EditorScriptCodeCompletionCache; +class FindInFilesDialog; +class FindInFilesPanel; class ScriptEditor : public PanelContainer { @@ -211,6 +213,10 @@ class ScriptEditor : public PanelContainer { ToolButton *script_back; ToolButton *script_forward; + FindInFilesDialog *find_in_files_dialog; + FindInFilesPanel *find_in_files; + Button *find_in_files_button; + enum { SCRIPT_EDITOR_FUNC_MAX = 32 }; @@ -294,6 +300,8 @@ class ScriptEditor : public PanelContainer { void _update_window_menu(); void _script_created(Ref<Script> p_script); + ScriptEditorBase *_get_current_editor() const; + void _save_layout(); void _editor_settings_changed(); void _autosave_scripts(); @@ -349,6 +357,11 @@ class ScriptEditor : public PanelContainer { Ref<Script> _get_current_script(); Array _get_open_scripts() const; + void _on_find_in_files_requested(String text); + void _on_find_in_files_result_selected(String fpath, int line_number, int begin, int end); + void _start_find_in_files(bool with_replace); + void _on_find_in_files_modified_files(PoolStringArray paths); + static void _open_script_request(const String &p_path); static ScriptEditor *script_editor; diff --git a/editor/plugins/script_text_editor.cpp b/editor/plugins/script_text_editor.cpp index a3728a1d46..b6299c1777 100644 --- a/editor/plugins/script_text_editor.cpp +++ b/editor/plugins/script_text_editor.cpp @@ -523,6 +523,14 @@ void ScriptTextEditor::goto_line(int p_line, bool p_with_error) { tx->call_deferred("cursor_set_line", p_line); } +void ScriptTextEditor::goto_line_selection(int p_line, int p_begin, int p_end) { + TextEdit *tx = code_editor->get_text_edit(); + tx->unfold_line(p_line); + tx->call_deferred("cursor_set_line", p_line); + tx->call_deferred("cursor_set_column", p_begin); + tx->select(p_line, p_begin, p_line, p_end); +} + void ScriptTextEditor::ensure_focus() { code_editor->get_text_edit()->grab_focus(); @@ -1134,6 +1142,15 @@ void ScriptTextEditor::_edit_option(int p_op) { code_editor->get_find_replace_bar()->popup_replace(); } break; + case SEARCH_IN_FILES: { + + String selected_text = code_editor->get_text_edit()->get_selection_text(); + + // Yep, because it doesn't make sense to instance this dialog for every single script open... + // So this will be delegated to the ScriptEditor + emit_signal("search_in_files_requested", selected_text); + + } break; case SEARCH_LOCATE_FUNCTION: { quick_open->popup(get_functions()); @@ -1599,6 +1616,8 @@ ScriptTextEditor::ScriptTextEditor() { search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/find_previous"), SEARCH_FIND_PREV); search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/replace"), SEARCH_REPLACE); search_menu->get_popup()->add_separator(); + search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/find_in_files"), SEARCH_IN_FILES); + search_menu->get_popup()->add_separator(); search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_function"), SEARCH_LOCATE_FUNCTION); search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_line"), SEARCH_GOTO_LINE); search_menu->get_popup()->add_separator(); @@ -1670,7 +1689,9 @@ void ScriptTextEditor::register_editor() { ED_SHORTCUT("script_text_editor/find_previous", TTR("Find Previous"), KEY_MASK_SHIFT | KEY_F3); ED_SHORTCUT("script_text_editor/replace", TTR("Replace.."), KEY_MASK_CMD | KEY_R); - ED_SHORTCUT("script_text_editor/goto_function", TTR("Goto Function.."), KEY_MASK_SHIFT | KEY_MASK_CMD | KEY_F); + ED_SHORTCUT("script_text_editor/find_in_files", TTR("Find in files..."), KEY_MASK_CMD | KEY_MASK_SHIFT | KEY_F); + + ED_SHORTCUT("script_text_editor/goto_function", TTR("Goto Function.."), KEY_MASK_ALT | KEY_MASK_CMD | KEY_F); ED_SHORTCUT("script_text_editor/goto_line", TTR("Goto Line.."), KEY_MASK_CMD | KEY_L); ED_SHORTCUT("script_text_editor/contextual_help", TTR("Contextual Help"), KEY_MASK_SHIFT | KEY_F1); diff --git a/editor/plugins/script_text_editor.h b/editor/plugins/script_text_editor.h index 22e8fbce25..903fb0dffa 100644 --- a/editor/plugins/script_text_editor.h +++ b/editor/plugins/script_text_editor.h @@ -105,6 +105,7 @@ class ScriptTextEditor : public ScriptEditorBase { SEARCH_REPLACE, SEARCH_LOCATE_FUNCTION, SEARCH_GOTO_LINE, + SEARCH_IN_FILES, DEBUG_TOGGLE_BREAKPOINT, DEBUG_REMOVE_ALL_BREAKPOINTS, DEBUG_GOTO_NEXT_BREAKPOINT, @@ -163,6 +164,7 @@ public: virtual void tag_saved_version(); virtual void goto_line(int p_line, bool p_with_error = false); + void goto_line_selection(int p_line, int p_begin, int p_end); virtual void reload(bool p_soft); virtual void get_breakpoints(List<int> *p_breakpoints); |