1/**************************************************************************/
2/* find_in_files.cpp */
3/**************************************************************************/
4/* This file is part of: */
5/* GODOT ENGINE */
6/* https://godotengine.org */
7/**************************************************************************/
8/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10/* */
11/* Permission is hereby granted, free of charge, to any person obtaining */
12/* a copy of this software and associated documentation files (the */
13/* "Software"), to deal in the Software without restriction, including */
14/* without limitation the rights to use, copy, modify, merge, publish, */
15/* distribute, sublicense, and/or sell copies of the Software, and to */
16/* permit persons to whom the Software is furnished to do so, subject to */
17/* the following conditions: */
18/* */
19/* The above copyright notice and this permission notice shall be */
20/* included in all copies or substantial portions of the Software. */
21/* */
22/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29/**************************************************************************/
30
31#include "find_in_files.h"
32
33#include "core/config/project_settings.h"
34#include "core/io/dir_access.h"
35#include "core/os/os.h"
36#include "editor/editor_node.h"
37#include "editor/editor_scale.h"
38#include "editor/editor_string_names.h"
39#include "scene/gui/box_container.h"
40#include "scene/gui/button.h"
41#include "scene/gui/check_box.h"
42#include "scene/gui/file_dialog.h"
43#include "scene/gui/grid_container.h"
44#include "scene/gui/label.h"
45#include "scene/gui/line_edit.h"
46#include "scene/gui/progress_bar.h"
47#include "scene/gui/tree.h"
48
49const char *FindInFiles::SIGNAL_RESULT_FOUND = "result_found";
50const char *FindInFiles::SIGNAL_FINISHED = "finished";
51
52// TODO: Would be nice in Vector and Vectors.
53template <typename T>
54inline void pop_back(T &container) {
55 container.resize(container.size() - 1);
56}
57
58static bool find_next(const String &line, String pattern, int from, bool match_case, bool whole_words, int &out_begin, int &out_end) {
59 int end = from;
60
61 while (true) {
62 int begin = match_case ? line.find(pattern, end) : line.findn(pattern, end);
63
64 if (begin == -1) {
65 return false;
66 }
67
68 end = begin + pattern.length();
69 out_begin = begin;
70 out_end = end;
71
72 if (whole_words) {
73 if (begin > 0 && (is_ascii_identifier_char(line[begin - 1]))) {
74 continue;
75 }
76 if (end < line.size() && (is_ascii_identifier_char(line[end]))) {
77 continue;
78 }
79 }
80
81 return true;
82 }
83}
84
85//--------------------------------------------------------------------------------
86
87void FindInFiles::set_search_text(String p_pattern) {
88 _pattern = p_pattern;
89}
90
91void FindInFiles::set_whole_words(bool p_whole_word) {
92 _whole_words = p_whole_word;
93}
94
95void FindInFiles::set_match_case(bool p_match_case) {
96 _match_case = p_match_case;
97}
98
99void FindInFiles::set_folder(String folder) {
100 _root_dir = folder;
101}
102
103void FindInFiles::set_filter(const HashSet<String> &exts) {
104 _extension_filter = exts;
105}
106
107void FindInFiles::_notification(int p_what) {
108 switch (p_what) {
109 case NOTIFICATION_PROCESS: {
110 _process();
111 } break;
112 }
113}
114
115void FindInFiles::start() {
116 if (_pattern.is_empty()) {
117 print_verbose("Nothing to search, pattern is empty");
118 emit_signal(SNAME(SIGNAL_FINISHED));
119 return;
120 }
121 if (_extension_filter.size() == 0) {
122 print_verbose("Nothing to search, filter matches no files");
123 emit_signal(SNAME(SIGNAL_FINISHED));
124 return;
125 }
126
127 // Init search.
128 _current_dir = "";
129 PackedStringArray init_folder;
130 init_folder.push_back(_root_dir);
131 _folders_stack.clear();
132 _folders_stack.push_back(init_folder);
133
134 _initial_files_count = 0;
135
136 _searching = true;
137 set_process(true);
138}
139
140void FindInFiles::stop() {
141 _searching = false;
142 _current_dir = "";
143 set_process(false);
144}
145
146void FindInFiles::_process() {
147 // This part can be moved to a thread if needed.
148
149 OS &os = *OS::get_singleton();
150 uint64_t time_before = os.get_ticks_msec();
151 while (is_processing()) {
152 _iterate();
153 uint64_t elapsed = (os.get_ticks_msec() - time_before);
154 if (elapsed > 8) { // Process again after waiting 8 ticks.
155 break;
156 }
157 }
158}
159
160void FindInFiles::_iterate() {
161 if (_folders_stack.size() != 0) {
162 // Scan folders first so we can build a list of files and have progress info later.
163
164 PackedStringArray &folders_to_scan = _folders_stack.write[_folders_stack.size() - 1];
165
166 if (folders_to_scan.size() != 0) {
167 // Scan one folder below.
168
169 String folder_name = folders_to_scan[folders_to_scan.size() - 1];
170 pop_back(folders_to_scan);
171
172 _current_dir = _current_dir.path_join(folder_name);
173
174 PackedStringArray sub_dirs;
175 _scan_dir("res://" + _current_dir, sub_dirs);
176
177 _folders_stack.push_back(sub_dirs);
178
179 } else {
180 // Go back one level.
181
182 pop_back(_folders_stack);
183 _current_dir = _current_dir.get_base_dir();
184
185 if (_folders_stack.size() == 0) {
186 // All folders scanned.
187 _initial_files_count = _files_to_scan.size();
188 }
189 }
190
191 } else if (_files_to_scan.size() != 0) {
192 // Then scan files.
193
194 String fpath = _files_to_scan[_files_to_scan.size() - 1];
195 pop_back(_files_to_scan);
196 _scan_file(fpath);
197
198 } else {
199 print_verbose("Search complete");
200 set_process(false);
201 _current_dir = "";
202 _searching = false;
203 emit_signal(SNAME(SIGNAL_FINISHED));
204 }
205}
206
207float FindInFiles::get_progress() const {
208 if (_initial_files_count != 0) {
209 return static_cast<float>(_initial_files_count - _files_to_scan.size()) / static_cast<float>(_initial_files_count);
210 }
211 return 0;
212}
213
214void FindInFiles::_scan_dir(String path, PackedStringArray &out_folders) {
215 Ref<DirAccess> dir = DirAccess::open(path);
216 if (dir.is_null()) {
217 print_verbose("Cannot open directory! " + path);
218 return;
219 }
220
221 dir->list_dir_begin();
222
223 for (int i = 0; i < 1000; ++i) {
224 String file = dir->get_next();
225
226 if (file.is_empty()) {
227 break;
228 }
229
230 // If there is a .gdignore file in the directory, skip searching the directory.
231 if (file == ".gdignore") {
232 break;
233 }
234
235 // Ignore special directories (such as those beginning with . and the project data directory).
236 String project_data_dir_name = ProjectSettings::get_singleton()->get_project_data_dir_name();
237 if (file.begins_with(".") || file == project_data_dir_name) {
238 continue;
239 }
240 if (dir->current_is_hidden()) {
241 continue;
242 }
243
244 if (dir->current_is_dir()) {
245 out_folders.push_back(file);
246
247 } else {
248 String file_ext = file.get_extension();
249 if (_extension_filter.has(file_ext)) {
250 _files_to_scan.push_back(path.path_join(file));
251 }
252 }
253 }
254}
255
256void FindInFiles::_scan_file(String fpath) {
257 Ref<FileAccess> f = FileAccess::open(fpath, FileAccess::READ);
258 if (f.is_null()) {
259 print_verbose(String("Cannot open file ") + fpath);
260 return;
261 }
262
263 int line_number = 0;
264
265 while (!f->eof_reached()) {
266 // Line number starts at 1.
267 ++line_number;
268
269 int begin = 0;
270 int end = 0;
271
272 String line = f->get_line();
273
274 while (find_next(line, _pattern, end, _match_case, _whole_words, begin, end)) {
275 emit_signal(SNAME(SIGNAL_RESULT_FOUND), fpath, line_number, begin, end, line);
276 }
277 }
278}
279
280void FindInFiles::_bind_methods() {
281 ADD_SIGNAL(MethodInfo(SIGNAL_RESULT_FOUND,
282 PropertyInfo(Variant::STRING, "path"),
283 PropertyInfo(Variant::INT, "line_number"),
284 PropertyInfo(Variant::INT, "begin"),
285 PropertyInfo(Variant::INT, "end"),
286 PropertyInfo(Variant::STRING, "text")));
287
288 ADD_SIGNAL(MethodInfo(SIGNAL_FINISHED));
289}
290
291//-----------------------------------------------------------------------------
292const char *FindInFilesDialog::SIGNAL_FIND_REQUESTED = "find_requested";
293const char *FindInFilesDialog::SIGNAL_REPLACE_REQUESTED = "replace_requested";
294
295FindInFilesDialog::FindInFilesDialog() {
296 set_min_size(Size2(500 * EDSCALE, 0));
297 set_title(TTR("Find in Files"));
298
299 VBoxContainer *vbc = memnew(VBoxContainer);
300 vbc->set_anchor_and_offset(SIDE_LEFT, Control::ANCHOR_BEGIN, 8 * EDSCALE);
301 vbc->set_anchor_and_offset(SIDE_TOP, Control::ANCHOR_BEGIN, 8 * EDSCALE);
302 vbc->set_anchor_and_offset(SIDE_RIGHT, Control::ANCHOR_END, -8 * EDSCALE);
303 vbc->set_anchor_and_offset(SIDE_BOTTOM, Control::ANCHOR_END, -8 * EDSCALE);
304 add_child(vbc);
305
306 GridContainer *gc = memnew(GridContainer);
307 gc->set_columns(2);
308 vbc->add_child(gc);
309
310 Label *find_label = memnew(Label);
311 find_label->set_text(TTR("Find:"));
312 gc->add_child(find_label);
313
314 _search_text_line_edit = memnew(LineEdit);
315 _search_text_line_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL);
316 _search_text_line_edit->connect("text_changed", callable_mp(this, &FindInFilesDialog::_on_search_text_modified));
317 _search_text_line_edit->connect("text_submitted", callable_mp(this, &FindInFilesDialog::_on_search_text_submitted));
318 gc->add_child(_search_text_line_edit);
319
320 _replace_label = memnew(Label);
321 _replace_label->set_text(TTR("Replace:"));
322 _replace_label->hide();
323 gc->add_child(_replace_label);
324
325 _replace_text_line_edit = memnew(LineEdit);
326 _replace_text_line_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL);
327 _replace_text_line_edit->connect("text_submitted", callable_mp(this, &FindInFilesDialog::_on_replace_text_submitted));
328 _replace_text_line_edit->hide();
329 gc->add_child(_replace_text_line_edit);
330
331 gc->add_child(memnew(Control)); // Space to maintain the grid alignment.
332
333 {
334 HBoxContainer *hbc = memnew(HBoxContainer);
335
336 _whole_words_checkbox = memnew(CheckBox);
337 _whole_words_checkbox->set_text(TTR("Whole Words"));
338 hbc->add_child(_whole_words_checkbox);
339
340 _match_case_checkbox = memnew(CheckBox);
341 _match_case_checkbox->set_text(TTR("Match Case"));
342 hbc->add_child(_match_case_checkbox);
343
344 gc->add_child(hbc);
345 }
346
347 Label *folder_label = memnew(Label);
348 folder_label->set_text(TTR("Folder:"));
349 gc->add_child(folder_label);
350
351 {
352 HBoxContainer *hbc = memnew(HBoxContainer);
353
354 Label *prefix_label = memnew(Label);
355 prefix_label->set_text("res://");
356 hbc->add_child(prefix_label);
357
358 _folder_line_edit = memnew(LineEdit);
359 _folder_line_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL);
360 hbc->add_child(_folder_line_edit);
361
362 Button *folder_button = memnew(Button);
363 folder_button->set_text("...");
364 folder_button->connect("pressed", callable_mp(this, &FindInFilesDialog::_on_folder_button_pressed));
365 hbc->add_child(folder_button);
366
367 _folder_dialog = memnew(FileDialog);
368 _folder_dialog->set_file_mode(FileDialog::FILE_MODE_OPEN_DIR);
369 _folder_dialog->connect("dir_selected", callable_mp(this, &FindInFilesDialog::_on_folder_selected));
370 add_child(_folder_dialog);
371
372 gc->add_child(hbc);
373 }
374
375 Label *filter_label = memnew(Label);
376 filter_label->set_text(TTR("Filters:"));
377 filter_label->set_tooltip_text(TTR("Include the files with the following extensions. Add or remove them in ProjectSettings."));
378 gc->add_child(filter_label);
379
380 _filters_container = memnew(HBoxContainer);
381 gc->add_child(_filters_container);
382
383 _find_button = add_button(TTR("Find..."), false, "find");
384 _find_button->set_disabled(true);
385
386 _replace_button = add_button(TTR("Replace..."), false, "replace");
387 _replace_button->set_disabled(true);
388
389 Button *cancel_button = get_ok_button();
390 cancel_button->set_text(TTR("Cancel"));
391
392 _mode = SEARCH_MODE;
393}
394
395void FindInFilesDialog::set_search_text(String text) {
396 _search_text_line_edit->set_text(text);
397 _on_search_text_modified(text);
398}
399
400void FindInFilesDialog::set_replace_text(String text) {
401 _replace_text_line_edit->set_text(text);
402}
403
404void FindInFilesDialog::set_find_in_files_mode(FindInFilesMode p_mode) {
405 if (_mode == p_mode) {
406 return;
407 }
408
409 _mode = p_mode;
410
411 if (p_mode == SEARCH_MODE) {
412 set_title(TTR("Find in Files"));
413 _replace_label->hide();
414 _replace_text_line_edit->hide();
415 } else if (p_mode == REPLACE_MODE) {
416 set_title(TTR("Replace in Files"));
417 _replace_label->show();
418 _replace_text_line_edit->show();
419 }
420
421 // Recalculate the dialog size after hiding child controls.
422 set_size(Size2(get_size().x, 0));
423}
424
425String FindInFilesDialog::get_search_text() const {
426 return _search_text_line_edit->get_text();
427}
428
429String FindInFilesDialog::get_replace_text() const {
430 return _replace_text_line_edit->get_text();
431}
432
433bool FindInFilesDialog::is_match_case() const {
434 return _match_case_checkbox->is_pressed();
435}
436
437bool FindInFilesDialog::is_whole_words() const {
438 return _whole_words_checkbox->is_pressed();
439}
440
441String FindInFilesDialog::get_folder() const {
442 String text = _folder_line_edit->get_text();
443 return text.strip_edges();
444}
445
446HashSet<String> FindInFilesDialog::get_filter() const {
447 // Could check the _filters_preferences but it might not have been generated yet.
448 HashSet<String> filters;
449 for (int i = 0; i < _filters_container->get_child_count(); ++i) {
450 CheckBox *cb = static_cast<CheckBox *>(_filters_container->get_child(i));
451 if (cb->is_pressed()) {
452 filters.insert(cb->get_text());
453 }
454 }
455 return filters;
456}
457
458void FindInFilesDialog::_notification(int p_what) {
459 switch (p_what) {
460 case NOTIFICATION_VISIBILITY_CHANGED: {
461 if (is_visible()) {
462 // Doesn't work more than once if not deferred...
463 _search_text_line_edit->call_deferred(SNAME("grab_focus"));
464 _search_text_line_edit->select_all();
465 // Extensions might have changed in the meantime, we clean them and instance them again.
466 for (int i = 0; i < _filters_container->get_child_count(); i++) {
467 _filters_container->get_child(i)->queue_free();
468 }
469 Array exts = GLOBAL_GET("editor/script/search_in_file_extensions");
470 for (int i = 0; i < exts.size(); ++i) {
471 CheckBox *cb = memnew(CheckBox);
472 cb->set_text(exts[i]);
473 if (!_filters_preferences.has(exts[i])) {
474 _filters_preferences[exts[i]] = true;
475 }
476 cb->set_pressed(_filters_preferences[exts[i]]);
477 _filters_container->add_child(cb);
478 }
479 }
480 } break;
481 }
482}
483
484void FindInFilesDialog::_on_folder_button_pressed() {
485 _folder_dialog->popup_file_dialog();
486}
487
488void FindInFilesDialog::custom_action(const String &p_action) {
489 for (int i = 0; i < _filters_container->get_child_count(); ++i) {
490 CheckBox *cb = static_cast<CheckBox *>(_filters_container->get_child(i));
491 _filters_preferences[cb->get_text()] = cb->is_pressed();
492 }
493
494 if (p_action == "find") {
495 emit_signal(SNAME(SIGNAL_FIND_REQUESTED));
496 hide();
497 } else if (p_action == "replace") {
498 emit_signal(SNAME(SIGNAL_REPLACE_REQUESTED));
499 hide();
500 }
501}
502
503void FindInFilesDialog::_on_search_text_modified(String text) {
504 ERR_FAIL_NULL(_find_button);
505 ERR_FAIL_NULL(_replace_button);
506
507 _find_button->set_disabled(get_search_text().is_empty());
508 _replace_button->set_disabled(get_search_text().is_empty());
509}
510
511void FindInFilesDialog::_on_search_text_submitted(String text) {
512 // This allows to trigger a global search without leaving the keyboard.
513 if (!_find_button->is_disabled()) {
514 if (_mode == SEARCH_MODE) {
515 custom_action("find");
516 }
517 }
518
519 if (!_replace_button->is_disabled()) {
520 if (_mode == REPLACE_MODE) {
521 custom_action("replace");
522 }
523 }
524}
525
526void FindInFilesDialog::_on_replace_text_submitted(String text) {
527 // This allows to trigger a global search without leaving the keyboard.
528 if (!_replace_button->is_disabled()) {
529 if (_mode == REPLACE_MODE) {
530 custom_action("replace");
531 }
532 }
533}
534
535void FindInFilesDialog::_on_folder_selected(String path) {
536 int i = path.find("://");
537 if (i != -1) {
538 path = path.substr(i + 3);
539 }
540 _folder_line_edit->set_text(path);
541}
542
543void FindInFilesDialog::_bind_methods() {
544 ADD_SIGNAL(MethodInfo(SIGNAL_FIND_REQUESTED));
545 ADD_SIGNAL(MethodInfo(SIGNAL_REPLACE_REQUESTED));
546}
547
548//-----------------------------------------------------------------------------
549const char *FindInFilesPanel::SIGNAL_RESULT_SELECTED = "result_selected";
550const char *FindInFilesPanel::SIGNAL_FILES_MODIFIED = "files_modified";
551
552FindInFilesPanel::FindInFilesPanel() {
553 _finder = memnew(FindInFiles);
554 _finder->connect(FindInFiles::SIGNAL_RESULT_FOUND, callable_mp(this, &FindInFilesPanel::_on_result_found));
555 _finder->connect(FindInFiles::SIGNAL_FINISHED, callable_mp(this, &FindInFilesPanel::_on_finished));
556 add_child(_finder);
557
558 VBoxContainer *vbc = memnew(VBoxContainer);
559 vbc->set_anchor_and_offset(SIDE_LEFT, ANCHOR_BEGIN, 0);
560 vbc->set_anchor_and_offset(SIDE_TOP, ANCHOR_BEGIN, 0);
561 vbc->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, 0);
562 vbc->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, 0);
563 add_child(vbc);
564
565 {
566 HBoxContainer *hbc = memnew(HBoxContainer);
567
568 Label *find_label = memnew(Label);
569 find_label->set_text(TTR("Find:"));
570 hbc->add_child(find_label);
571
572 _search_text_label = memnew(Label);
573 hbc->add_child(_search_text_label);
574
575 _progress_bar = memnew(ProgressBar);
576 _progress_bar->set_h_size_flags(SIZE_EXPAND_FILL);
577 _progress_bar->set_v_size_flags(SIZE_SHRINK_CENTER);
578 hbc->add_child(_progress_bar);
579 set_progress_visible(false);
580
581 _status_label = memnew(Label);
582 hbc->add_child(_status_label);
583
584 _refresh_button = memnew(Button);
585 _refresh_button->set_text(TTR("Refresh"));
586 _refresh_button->connect("pressed", callable_mp(this, &FindInFilesPanel::_on_refresh_button_clicked));
587 _refresh_button->hide();
588 hbc->add_child(_refresh_button);
589
590 _cancel_button = memnew(Button);
591 _cancel_button->set_text(TTR("Cancel"));
592 _cancel_button->connect("pressed", callable_mp(this, &FindInFilesPanel::_on_cancel_button_clicked));
593 _cancel_button->hide();
594 hbc->add_child(_cancel_button);
595
596 vbc->add_child(hbc);
597 }
598
599 _results_display = memnew(Tree);
600 _results_display->set_v_size_flags(SIZE_EXPAND_FILL);
601 _results_display->connect("item_selected", callable_mp(this, &FindInFilesPanel::_on_result_selected));
602 _results_display->connect("item_edited", callable_mp(this, &FindInFilesPanel::_on_item_edited));
603 _results_display->set_hide_root(true);
604 _results_display->set_select_mode(Tree::SELECT_ROW);
605 _results_display->set_allow_rmb_select(true);
606 _results_display->set_allow_reselect(true);
607 _results_display->create_item(); // Root
608 vbc->add_child(_results_display);
609
610 {
611 _replace_container = memnew(HBoxContainer);
612
613 Label *replace_label = memnew(Label);
614 replace_label->set_text(TTR("Replace:"));
615 _replace_container->add_child(replace_label);
616
617 _replace_line_edit = memnew(LineEdit);
618 _replace_line_edit->set_h_size_flags(SIZE_EXPAND_FILL);
619 _replace_line_edit->connect("text_changed", callable_mp(this, &FindInFilesPanel::_on_replace_text_changed));
620 _replace_container->add_child(_replace_line_edit);
621
622 _replace_all_button = memnew(Button);
623 _replace_all_button->set_text(TTR("Replace all (no undo)"));
624 _replace_all_button->connect("pressed", callable_mp(this, &FindInFilesPanel::_on_replace_all_clicked));
625 _replace_container->add_child(_replace_all_button);
626
627 _replace_container->hide();
628
629 vbc->add_child(_replace_container);
630 }
631}
632
633void FindInFilesPanel::set_with_replace(bool with_replace) {
634 _with_replace = with_replace;
635 _replace_container->set_visible(with_replace);
636
637 if (with_replace) {
638 // Results show checkboxes on their left so they can be opted out.
639 _results_display->set_columns(2);
640 _results_display->set_column_expand(0, false);
641 _results_display->set_column_custom_minimum_width(0, 48 * EDSCALE);
642 } else {
643 // Results are single-cell items.
644 _results_display->set_column_expand(0, true);
645 _results_display->set_columns(1);
646 }
647}
648
649void FindInFilesPanel::set_replace_text(String text) {
650 _replace_line_edit->set_text(text);
651}
652
653void FindInFilesPanel::clear() {
654 _file_items.clear();
655 _result_items.clear();
656 _results_display->clear();
657 _results_display->create_item(); // Root
658}
659
660void FindInFilesPanel::start_search() {
661 clear();
662
663 _status_label->set_text(TTR("Searching..."));
664 _search_text_label->set_text(_finder->get_search_text());
665
666 set_process(true);
667 set_progress_visible(true);
668
669 _finder->start();
670
671 update_replace_buttons();
672 _refresh_button->hide();
673 _cancel_button->show();
674}
675
676void FindInFilesPanel::stop_search() {
677 _finder->stop();
678
679 _status_label->set_text("");
680 update_replace_buttons();
681 set_progress_visible(false);
682 _refresh_button->show();
683 _cancel_button->hide();
684}
685
686void FindInFilesPanel::_notification(int p_what) {
687 switch (p_what) {
688 case NOTIFICATION_THEME_CHANGED: {
689 _search_text_label->add_theme_font_override("font", get_theme_font(SNAME("source"), EditorStringName(EditorFonts)));
690 _search_text_label->add_theme_font_size_override("font_size", get_theme_font_size(SNAME("source_size"), EditorStringName(EditorFonts)));
691 _results_display->add_theme_font_override("font", get_theme_font(SNAME("source"), EditorStringName(EditorFonts)));
692 _results_display->add_theme_font_size_override("font_size", get_theme_font_size(SNAME("source_size"), EditorStringName(EditorFonts)));
693
694 // Rebuild search tree.
695 if (!_finder->get_search_text().is_empty()) {
696 start_search();
697 }
698 } break;
699
700 case NOTIFICATION_PROCESS: {
701 _progress_bar->set_as_ratio(_finder->get_progress());
702 } break;
703 }
704}
705
706void FindInFilesPanel::_on_result_found(String fpath, int line_number, int begin, int end, String text) {
707 TreeItem *file_item;
708 HashMap<String, TreeItem *>::Iterator E = _file_items.find(fpath);
709
710 if (!E) {
711 file_item = _results_display->create_item();
712 file_item->set_text(0, fpath);
713 file_item->set_metadata(0, fpath);
714
715 // The width of this column is restrained to checkboxes,
716 // but that doesn't make sense for the parent items,
717 // so we override their width so they can expand to full width.
718 file_item->set_expand_right(0, true);
719
720 _file_items[fpath] = file_item;
721 } else {
722 file_item = E->value;
723 }
724
725 Color file_item_color = _results_display->get_theme_color(SNAME("font_color")) * Color(1, 1, 1, 0.67);
726 file_item->set_custom_color(0, file_item_color);
727 file_item->set_selectable(0, false);
728
729 int text_index = _with_replace ? 1 : 0;
730
731 TreeItem *item = _results_display->create_item(file_item);
732
733 // Do this first because it resets properties of the cell...
734 item->set_cell_mode(text_index, TreeItem::CELL_MODE_CUSTOM);
735
736 // Trim result item line.
737 int old_text_size = text.size();
738 text = text.strip_edges(true, false);
739 int chars_removed = old_text_size - text.size();
740 String start = vformat("%3s: ", line_number);
741
742 item->set_text(text_index, start + text);
743 item->set_custom_draw(text_index, this, "_draw_result_text");
744
745 Result r;
746 r.line_number = line_number;
747 r.begin = begin;
748 r.end = end;
749 r.begin_trimmed = begin - chars_removed + start.size() - 1;
750 _result_items[item] = r;
751
752 if (_with_replace) {
753 item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
754 item->set_checked(0, true);
755 item->set_editable(0, true);
756 }
757}
758
759void FindInFilesPanel::draw_result_text(Object *item_obj, Rect2 rect) {
760 TreeItem *item = Object::cast_to<TreeItem>(item_obj);
761 if (!item) {
762 return;
763 }
764
765 HashMap<TreeItem *, Result>::Iterator E = _result_items.find(item);
766 if (!E) {
767 return;
768 }
769 Result r = E->value;
770 String item_text = item->get_text(_with_replace ? 1 : 0);
771 Ref<Font> font = _results_display->get_theme_font(SNAME("font"));
772 int font_size = _results_display->get_theme_font_size(SNAME("font_size"));
773
774 Rect2 match_rect = rect;
775 match_rect.position.x += font->get_string_size(item_text.left(r.begin_trimmed), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x - 1;
776 match_rect.size.x = font->get_string_size(_search_text_label->get_text(), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x + 1;
777 match_rect.position.y += 1 * EDSCALE;
778 match_rect.size.y -= 2 * EDSCALE;
779
780 _results_display->draw_rect(match_rect, get_theme_color(SNAME("accent_color"), EditorStringName(Editor)) * Color(1, 1, 1, 0.33), false, 2.0);
781 _results_display->draw_rect(match_rect, get_theme_color(SNAME("accent_color"), EditorStringName(Editor)) * Color(1, 1, 1, 0.17), true);
782
783 // Text is drawn by Tree already.
784}
785
786void FindInFilesPanel::_on_item_edited() {
787 TreeItem *item = _results_display->get_selected();
788
789 // Change opacity to half if checkbox is checked, otherwise full.
790 Color use_color = _results_display->get_theme_color(SNAME("font_color"));
791 if (!item->is_checked(0)) {
792 use_color.a *= 0.5;
793 }
794 item->set_custom_color(1, use_color);
795}
796
797void FindInFilesPanel::_on_finished() {
798 String results_text;
799 int result_count = _result_items.size();
800 int file_count = _file_items.size();
801
802 if (result_count == 1 && file_count == 1) {
803 results_text = vformat(TTR("%d match in %d file"), result_count, file_count);
804 } else if (result_count != 1 && file_count == 1) {
805 results_text = vformat(TTR("%d matches in %d file"), result_count, file_count);
806 } else {
807 results_text = vformat(TTR("%d matches in %d files"), result_count, file_count);
808 }
809
810 _status_label->set_text(results_text);
811 update_replace_buttons();
812 set_progress_visible(false);
813 _refresh_button->show();
814 _cancel_button->hide();
815}
816
817void FindInFilesPanel::_on_refresh_button_clicked() {
818 start_search();
819}
820
821void FindInFilesPanel::_on_cancel_button_clicked() {
822 stop_search();
823}
824
825void FindInFilesPanel::_on_result_selected() {
826 TreeItem *item = _results_display->get_selected();
827 HashMap<TreeItem *, Result>::Iterator E = _result_items.find(item);
828
829 if (!E) {
830 return;
831 }
832 Result r = E->value;
833
834 TreeItem *file_item = item->get_parent();
835 String fpath = file_item->get_metadata(0);
836
837 emit_signal(SNAME(SIGNAL_RESULT_SELECTED), fpath, r.line_number, r.begin, r.end);
838}
839
840void FindInFilesPanel::_on_replace_text_changed(String text) {
841 update_replace_buttons();
842}
843
844void FindInFilesPanel::_on_replace_all_clicked() {
845 String replace_text = get_replace_text();
846
847 PackedStringArray modified_files;
848
849 for (KeyValue<String, TreeItem *> &E : _file_items) {
850 TreeItem *file_item = E.value;
851 String fpath = file_item->get_metadata(0);
852
853 Vector<Result> locations;
854 for (TreeItem *item = file_item->get_first_child(); item; item = item->get_next()) {
855 if (!item->is_checked(0)) {
856 continue;
857 }
858
859 HashMap<TreeItem *, Result>::Iterator F = _result_items.find(item);
860 ERR_FAIL_COND(!F);
861 locations.push_back(F->value);
862 }
863
864 if (locations.size() != 0) {
865 // Results are sorted by file, so we can batch replaces.
866 apply_replaces_in_file(fpath, locations, replace_text);
867 modified_files.push_back(fpath);
868 }
869 }
870
871 // Hide replace bar so we can't trigger the action twice without doing a new search.
872 _replace_container->hide();
873
874 emit_signal(SNAME(SIGNAL_FILES_MODIFIED), modified_files);
875}
876
877// Same as get_line, but preserves line ending characters.
878class ConservativeGetLine {
879public:
880 String get_line(Ref<FileAccess> f) {
881 _line_buffer.clear();
882
883 char32_t c = f->get_8();
884
885 while (!f->eof_reached()) {
886 if (c == '\n') {
887 _line_buffer.push_back(c);
888 _line_buffer.push_back(0);
889 return String::utf8(_line_buffer.ptr());
890
891 } else if (c == '\0') {
892 _line_buffer.push_back(c);
893 return String::utf8(_line_buffer.ptr());
894
895 } else if (c != '\r') {
896 _line_buffer.push_back(c);
897 }
898
899 c = f->get_8();
900 }
901
902 _line_buffer.push_back(0);
903 return String::utf8(_line_buffer.ptr());
904 }
905
906private:
907 Vector<char> _line_buffer;
908};
909
910void FindInFilesPanel::apply_replaces_in_file(String fpath, const Vector<Result> &locations, String new_text) {
911 // If the file is already open, I assume the editor will reload it.
912 // If there are unsaved changes, the user will be asked on focus,
913 // however that means either losing changes or losing replaces.
914
915 Ref<FileAccess> f = FileAccess::open(fpath, FileAccess::READ);
916 ERR_FAIL_COND_MSG(f.is_null(), "Cannot open file from path '" + fpath + "'.");
917
918 String buffer;
919 int current_line = 1;
920
921 ConservativeGetLine conservative;
922
923 String line = conservative.get_line(f);
924 String search_text = _finder->get_search_text();
925
926 int offset = 0;
927
928 for (int i = 0; i < locations.size(); ++i) {
929 int repl_line_number = locations[i].line_number;
930
931 while (current_line < repl_line_number) {
932 buffer += line;
933 line = conservative.get_line(f);
934 ++current_line;
935 offset = 0;
936 }
937
938 int repl_begin = locations[i].begin + offset;
939 int repl_end = locations[i].end + offset;
940
941 int _;
942 if (!find_next(line, search_text, repl_begin, _finder->is_match_case(), _finder->is_whole_words(), _, _)) {
943 // Make sure the replace is still valid in case the file was tampered with.
944 print_verbose(String("Occurrence no longer matches, replace will be ignored in {0}: line {1}, col {2}").format(varray(fpath, repl_line_number, repl_begin)));
945 continue;
946 }
947
948 line = line.left(repl_begin) + new_text + line.substr(repl_end);
949 // Keep an offset in case there are successive replaces in the same line.
950 offset += new_text.length() - (repl_end - repl_begin);
951 }
952
953 buffer += line;
954
955 while (!f->eof_reached()) {
956 buffer += conservative.get_line(f);
957 }
958
959 // Now the modified contents are in the buffer, rewrite the file with our changes.
960
961 Error err = f->reopen(fpath, FileAccess::WRITE);
962 ERR_FAIL_COND_MSG(err != OK, "Cannot create file in path '" + fpath + "'.");
963
964 f->store_string(buffer);
965}
966
967String FindInFilesPanel::get_replace_text() {
968 return _replace_line_edit->get_text();
969}
970
971void FindInFilesPanel::update_replace_buttons() {
972 bool disabled = _finder->is_searching();
973
974 _replace_all_button->set_disabled(disabled);
975}
976
977void FindInFilesPanel::set_progress_visible(bool p_visible) {
978 _progress_bar->set_self_modulate(Color(1, 1, 1, p_visible ? 1 : 0));
979}
980
981void FindInFilesPanel::_bind_methods() {
982 ClassDB::bind_method("_on_result_found", &FindInFilesPanel::_on_result_found);
983 ClassDB::bind_method("_on_finished", &FindInFilesPanel::_on_finished);
984 ClassDB::bind_method("_draw_result_text", &FindInFilesPanel::draw_result_text);
985
986 ADD_SIGNAL(MethodInfo(SIGNAL_RESULT_SELECTED,
987 PropertyInfo(Variant::STRING, "path"),
988 PropertyInfo(Variant::INT, "line_number"),
989 PropertyInfo(Variant::INT, "begin"),
990 PropertyInfo(Variant::INT, "end")));
991
992 ADD_SIGNAL(MethodInfo(SIGNAL_FILES_MODIFIED, PropertyInfo(Variant::STRING, "paths")));
993}
994