Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
godotengine
GitHub Repository: godotengine/godot
Path: blob/master/editor/script/find_in_files.cpp
20892 views
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_string_names.h"
38
#include "editor/gui/editor_file_dialog.h"
39
#include "editor/settings/editor_command_palette.h"
40
#include "editor/themes/editor_scale.h"
41
#include "scene/gui/box_container.h"
42
#include "scene/gui/button.h"
43
#include "scene/gui/check_box.h"
44
#include "scene/gui/check_button.h"
45
#include "scene/gui/grid_container.h"
46
#include "scene/gui/label.h"
47
#include "scene/gui/line_edit.h"
48
#include "scene/gui/progress_bar.h"
49
#include "scene/gui/tab_container.h"
50
#include "scene/gui/tree.h"
51
52
const char *FindInFiles::SIGNAL_RESULT_FOUND = "result_found";
53
54
// TODO: Would be nice in Vector and Vectors.
55
template <typename T>
56
inline void pop_back(T &container) {
57
container.resize(container.size() - 1);
58
}
59
60
static bool find_next(const String &line, const String &pattern, int from, bool match_case, bool whole_words, int &out_begin, int &out_end) {
61
int end = from;
62
63
while (true) {
64
int begin = match_case ? line.find(pattern, end) : line.findn(pattern, end);
65
66
if (begin == -1) {
67
return false;
68
}
69
70
end = begin + pattern.length();
71
out_begin = begin;
72
out_end = end;
73
74
if (whole_words) {
75
if (begin > 0 && (is_ascii_identifier_char(line[begin - 1]))) {
76
continue;
77
}
78
if (end < line.size() && (is_ascii_identifier_char(line[end]))) {
79
continue;
80
}
81
}
82
83
return true;
84
}
85
}
86
87
//--------------------------------------------------------------------------------
88
89
void FindInFiles::set_search_text(const String &p_pattern) {
90
_pattern = p_pattern;
91
}
92
93
void FindInFiles::set_whole_words(bool p_whole_word) {
94
_whole_words = p_whole_word;
95
}
96
97
void FindInFiles::set_match_case(bool p_match_case) {
98
_match_case = p_match_case;
99
}
100
101
void FindInFiles::set_folder(const String &folder) {
102
_root_dir = folder;
103
}
104
105
void FindInFiles::set_filter(const HashSet<String> &exts) {
106
_extension_filter = exts;
107
}
108
109
void FindInFiles::set_includes(const HashSet<String> &p_include_wildcards) {
110
_include_wildcards = p_include_wildcards;
111
}
112
113
void FindInFiles::set_excludes(const HashSet<String> &p_exclude_wildcards) {
114
_exclude_wildcards = p_exclude_wildcards;
115
}
116
117
void FindInFiles::_notification(int p_what) {
118
switch (p_what) {
119
case NOTIFICATION_PROCESS: {
120
_process();
121
} break;
122
}
123
}
124
125
void FindInFiles::start() {
126
if (_pattern.is_empty()) {
127
print_verbose("Nothing to search, pattern is empty");
128
emit_signal(SceneStringName(finished));
129
return;
130
}
131
if (_extension_filter.is_empty()) {
132
print_verbose("Nothing to search, filter matches no files");
133
emit_signal(SceneStringName(finished));
134
return;
135
}
136
137
// Init search.
138
_current_dir = "";
139
PackedStringArray init_folder;
140
init_folder.push_back(_root_dir);
141
_folders_stack.clear();
142
_folders_stack.push_back(init_folder);
143
144
_initial_files_count = 0;
145
146
_searching = true;
147
set_process(true);
148
}
149
150
void FindInFiles::stop() {
151
_searching = false;
152
_current_dir = "";
153
set_process(false);
154
}
155
156
void FindInFiles::_process() {
157
// This part can be moved to a thread if needed.
158
159
OS &os = *OS::get_singleton();
160
uint64_t time_before = os.get_ticks_msec();
161
while (is_processing()) {
162
_iterate();
163
uint64_t elapsed = (os.get_ticks_msec() - time_before);
164
if (elapsed > 8) { // Process again after waiting 8 ticks.
165
break;
166
}
167
}
168
}
169
170
void FindInFiles::_iterate() {
171
if (_folders_stack.size() != 0) {
172
// Scan folders first so we can build a list of files and have progress info later.
173
174
PackedStringArray &folders_to_scan = _folders_stack.write[_folders_stack.size() - 1];
175
176
if (folders_to_scan.size() != 0) {
177
// Scan one folder below.
178
179
String folder_name = folders_to_scan[folders_to_scan.size() - 1];
180
pop_back(folders_to_scan);
181
182
_current_dir = _current_dir.path_join(folder_name);
183
184
PackedStringArray sub_dirs;
185
PackedStringArray files_to_scan;
186
_scan_dir("res://" + _current_dir, sub_dirs, files_to_scan);
187
188
_folders_stack.push_back(sub_dirs);
189
_files_to_scan.append_array(files_to_scan);
190
191
} else {
192
// Go back one level.
193
194
pop_back(_folders_stack);
195
_current_dir = _current_dir.get_base_dir();
196
197
if (_folders_stack.is_empty()) {
198
// All folders scanned.
199
_initial_files_count = _files_to_scan.size();
200
}
201
}
202
203
} else if (_files_to_scan.size() != 0) {
204
// Then scan files.
205
206
String fpath = _files_to_scan[_files_to_scan.size() - 1];
207
pop_back(_files_to_scan);
208
_scan_file(fpath);
209
210
} else {
211
print_verbose("Search complete");
212
set_process(false);
213
_current_dir = "";
214
_searching = false;
215
emit_signal(SceneStringName(finished));
216
}
217
}
218
219
float FindInFiles::get_progress() const {
220
if (_initial_files_count != 0) {
221
return static_cast<float>(_initial_files_count - _files_to_scan.size()) / static_cast<float>(_initial_files_count);
222
}
223
return 0;
224
}
225
226
void FindInFiles::_scan_dir(const String &path, PackedStringArray &out_folders, PackedStringArray &out_files_to_scan) {
227
Ref<DirAccess> dir = DirAccess::open(path);
228
if (dir.is_null()) {
229
print_verbose("Cannot open directory! " + path);
230
return;
231
}
232
233
dir->list_dir_begin();
234
235
// Limit to 100,000 iterations to avoid an infinite loop just in case
236
// (this technically limits results to 100,000 files per folder).
237
for (int i = 0; i < 100'000; ++i) {
238
String file = dir->get_next();
239
240
if (file.is_empty()) {
241
break;
242
}
243
244
// If there is a .gdignore file in the directory, clear all the files/folders
245
// to be searched on this path and skip searching the directory.
246
if (file == ".gdignore") {
247
out_folders.clear();
248
out_files_to_scan.clear();
249
break;
250
}
251
252
// Ignore special directories (such as those beginning with . and the project data directory).
253
String project_data_dir_name = ProjectSettings::get_singleton()->get_project_data_dir_name();
254
if (file.begins_with(".") || file == project_data_dir_name) {
255
continue;
256
}
257
if (dir->current_is_hidden()) {
258
continue;
259
}
260
261
if (dir->current_is_dir()) {
262
out_folders.push_back(file);
263
264
} else {
265
String file_ext = file.get_extension();
266
if (_extension_filter.has(file_ext)) {
267
String file_path = path.path_join(file);
268
bool case_sensitive = dir->is_case_sensitive(path);
269
270
if (!_exclude_wildcards.is_empty() && _is_file_matched(_exclude_wildcards, file_path, case_sensitive)) {
271
continue;
272
}
273
274
if (_include_wildcards.is_empty() || _is_file_matched(_include_wildcards, file_path, case_sensitive)) {
275
out_files_to_scan.push_back(file_path);
276
}
277
}
278
}
279
}
280
}
281
282
void FindInFiles::_scan_file(const String &fpath) {
283
Ref<FileAccess> f = FileAccess::open(fpath, FileAccess::READ);
284
if (f.is_null()) {
285
print_verbose(String("Cannot open file ") + fpath);
286
return;
287
}
288
289
int line_number = 0;
290
291
while (!f->eof_reached()) {
292
// Line number starts at 1.
293
++line_number;
294
295
int begin = 0;
296
int end = 0;
297
298
String line = f->get_line();
299
300
while (find_next(line, _pattern, end, _match_case, _whole_words, begin, end)) {
301
emit_signal(SNAME(SIGNAL_RESULT_FOUND), fpath, line_number, begin, end, line);
302
}
303
}
304
}
305
306
bool FindInFiles::_is_file_matched(const HashSet<String> &p_wildcards, const String &p_file_path, bool p_case_sensitive) const {
307
const String file_path = "/" + p_file_path.replace_char('\\', '/') + "/";
308
309
for (const String &wildcard : p_wildcards) {
310
if (p_case_sensitive && file_path.match(wildcard)) {
311
return true;
312
} else if (!p_case_sensitive && file_path.matchn(wildcard)) {
313
return true;
314
}
315
}
316
return false;
317
}
318
319
void FindInFiles::_bind_methods() {
320
ADD_SIGNAL(MethodInfo(SIGNAL_RESULT_FOUND,
321
PropertyInfo(Variant::STRING, "path"),
322
PropertyInfo(Variant::INT, "line_number"),
323
PropertyInfo(Variant::INT, "begin"),
324
PropertyInfo(Variant::INT, "end"),
325
PropertyInfo(Variant::STRING, "text")));
326
327
ADD_SIGNAL(MethodInfo("finished"));
328
}
329
330
//-----------------------------------------------------------------------------
331
const char *FindInFilesDialog::SIGNAL_FIND_REQUESTED = "find_requested";
332
const char *FindInFilesDialog::SIGNAL_REPLACE_REQUESTED = "replace_requested";
333
334
FindInFilesDialog::FindInFilesDialog() {
335
set_min_size(Size2(500 * EDSCALE, 0));
336
set_title(TTRC("Find in Files"));
337
338
VBoxContainer *vbc = memnew(VBoxContainer);
339
vbc->set_anchor_and_offset(SIDE_LEFT, Control::ANCHOR_BEGIN, 8 * EDSCALE);
340
vbc->set_anchor_and_offset(SIDE_TOP, Control::ANCHOR_BEGIN, 8 * EDSCALE);
341
vbc->set_anchor_and_offset(SIDE_RIGHT, Control::ANCHOR_END, -8 * EDSCALE);
342
vbc->set_anchor_and_offset(SIDE_BOTTOM, Control::ANCHOR_END, -8 * EDSCALE);
343
add_child(vbc);
344
345
GridContainer *gc = memnew(GridContainer);
346
gc->set_columns(2);
347
vbc->add_child(gc);
348
349
Label *find_label = memnew(Label);
350
find_label->set_text(TTRC("Find:"));
351
gc->add_child(find_label);
352
353
_search_text_line_edit = memnew(LineEdit);
354
_search_text_line_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL);
355
_search_text_line_edit->set_accessibility_name(TTRC("Find:"));
356
_search_text_line_edit->connect(SceneStringName(text_changed), callable_mp(this, &FindInFilesDialog::_on_search_text_modified));
357
_search_text_line_edit->connect(SceneStringName(text_submitted), callable_mp(this, &FindInFilesDialog::_on_search_text_submitted));
358
gc->add_child(_search_text_line_edit);
359
360
_replace_label = memnew(Label);
361
_replace_label->set_text(TTRC("Replace:"));
362
_replace_label->hide();
363
gc->add_child(_replace_label);
364
365
_replace_text_line_edit = memnew(LineEdit);
366
_replace_text_line_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL);
367
_replace_text_line_edit->set_accessibility_name(TTRC("Replace:"));
368
_replace_text_line_edit->connect(SceneStringName(text_submitted), callable_mp(this, &FindInFilesDialog::_on_replace_text_submitted));
369
_replace_text_line_edit->hide();
370
gc->add_child(_replace_text_line_edit);
371
372
gc->add_child(memnew(Control)); // Space to maintain the grid alignment.
373
374
{
375
HBoxContainer *hbc = memnew(HBoxContainer);
376
377
_whole_words_checkbox = memnew(CheckBox);
378
_whole_words_checkbox->set_text(TTRC("Whole Words"));
379
hbc->add_child(_whole_words_checkbox);
380
381
_match_case_checkbox = memnew(CheckBox);
382
_match_case_checkbox->set_text(TTRC("Match Case"));
383
hbc->add_child(_match_case_checkbox);
384
385
gc->add_child(hbc);
386
}
387
388
Label *folder_label = memnew(Label);
389
folder_label->set_text(TTRC("Folder:"));
390
gc->add_child(folder_label);
391
392
{
393
HBoxContainer *hbc = memnew(HBoxContainer);
394
395
Label *prefix_label = memnew(Label);
396
prefix_label->set_text("res://");
397
prefix_label->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
398
hbc->add_child(prefix_label);
399
400
_folder_line_edit = memnew(LineEdit);
401
_folder_line_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL);
402
_folder_line_edit->connect(SceneStringName(text_submitted), callable_mp(this, &FindInFilesDialog::_on_search_text_submitted));
403
_folder_line_edit->set_accessibility_name(TTRC("Folder:"));
404
hbc->add_child(_folder_line_edit);
405
406
Button *folder_button = memnew(Button);
407
folder_button->set_accessibility_name(TTRC("Select Folder"));
408
folder_button->set_text("...");
409
folder_button->connect(SceneStringName(pressed), callable_mp(this, &FindInFilesDialog::_on_folder_button_pressed));
410
hbc->add_child(folder_button);
411
412
_folder_dialog = memnew(EditorFileDialog);
413
_folder_dialog->set_file_mode(FileDialog::FILE_MODE_OPEN_DIR);
414
_folder_dialog->connect("dir_selected", callable_mp(this, &FindInFilesDialog::_on_folder_selected));
415
add_child(_folder_dialog);
416
417
gc->add_child(hbc);
418
}
419
420
Label *includes_label = memnew(Label);
421
includes_label->set_text(TTRC("Includes:"));
422
includes_label->set_tooltip_text(TTRC("Include the files with the following expressions. Use \",\" to separate."));
423
includes_label->set_mouse_filter(Control::MOUSE_FILTER_PASS);
424
gc->add_child(includes_label);
425
426
_includes_line_edit = memnew(LineEdit);
427
_includes_line_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL);
428
_includes_line_edit->set_placeholder(TTRC("example: scripts,scenes/*/test.gd"));
429
_includes_line_edit->set_accessibility_name(TTRC("Includes:"));
430
_includes_line_edit->connect(SceneStringName(text_submitted), callable_mp(this, &FindInFilesDialog::_on_search_text_submitted));
431
gc->add_child(_includes_line_edit);
432
433
Label *excludes_label = memnew(Label);
434
excludes_label->set_text(TTRC("Excludes:"));
435
excludes_label->set_tooltip_text(TTRC("Exclude the files with the following expressions. Use \",\" to separate."));
436
excludes_label->set_mouse_filter(Control::MOUSE_FILTER_PASS);
437
gc->add_child(excludes_label);
438
439
_excludes_line_edit = memnew(LineEdit);
440
_excludes_line_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL);
441
_excludes_line_edit->set_placeholder(TTRC("example: res://addons,scenes/test/*.gd"));
442
_excludes_line_edit->set_accessibility_name(TTRC("Excludes:"));
443
_excludes_line_edit->connect(SceneStringName(text_submitted), callable_mp(this, &FindInFilesDialog::_on_search_text_submitted));
444
gc->add_child(_excludes_line_edit);
445
446
Label *filter_label = memnew(Label);
447
filter_label->set_text(TTRC("Filters:"));
448
filter_label->set_tooltip_text(TTRC("Include the files with the following extensions. Add or remove them in ProjectSettings."));
449
filter_label->set_mouse_filter(Control::MOUSE_FILTER_PASS);
450
gc->add_child(filter_label);
451
452
_filters_container = memnew(HBoxContainer);
453
gc->add_child(_filters_container);
454
455
_find_button = add_button(TTRC("Find..."), false, "find");
456
_find_button->set_disabled(true);
457
458
_replace_button = add_button(TTRC("Replace..."), false, "replace");
459
_replace_button->set_disabled(true);
460
461
Button *cancel_button = get_ok_button();
462
cancel_button->set_text(TTRC("Cancel"));
463
464
_mode = SEARCH_MODE;
465
}
466
467
void FindInFilesDialog::set_search_text(const String &text) {
468
if (_mode == SEARCH_MODE) {
469
if (!text.is_empty()) {
470
_search_text_line_edit->set_text(text);
471
_on_search_text_modified(text);
472
}
473
callable_mp((Control *)_search_text_line_edit, &Control::grab_focus).call_deferred(false);
474
_search_text_line_edit->select_all();
475
} else if (_mode == REPLACE_MODE) {
476
if (!text.is_empty()) {
477
_search_text_line_edit->set_text(text);
478
callable_mp((Control *)_replace_text_line_edit, &Control::grab_focus).call_deferred(false);
479
_replace_text_line_edit->select_all();
480
_on_search_text_modified(text);
481
} else {
482
callable_mp((Control *)_search_text_line_edit, &Control::grab_focus).call_deferred(false);
483
_search_text_line_edit->select_all();
484
}
485
}
486
}
487
488
void FindInFilesDialog::set_replace_text(const String &text) {
489
_replace_text_line_edit->set_text(text);
490
}
491
492
void FindInFilesDialog::set_find_in_files_mode(FindInFilesMode p_mode) {
493
if (_mode == p_mode) {
494
return;
495
}
496
497
_mode = p_mode;
498
499
if (p_mode == SEARCH_MODE) {
500
set_title(TTRC("Find in Files"));
501
_replace_label->hide();
502
_replace_text_line_edit->hide();
503
} else if (p_mode == REPLACE_MODE) {
504
set_title(TTRC("Replace in Files"));
505
_replace_label->show();
506
_replace_text_line_edit->show();
507
}
508
509
// Recalculate the dialog size after hiding child controls.
510
set_size(Size2(get_size().x, 0));
511
}
512
513
String FindInFilesDialog::get_search_text() const {
514
return _search_text_line_edit->get_text();
515
}
516
517
String FindInFilesDialog::get_replace_text() const {
518
return _replace_text_line_edit->get_text();
519
}
520
521
bool FindInFilesDialog::is_match_case() const {
522
return _match_case_checkbox->is_pressed();
523
}
524
525
bool FindInFilesDialog::is_whole_words() const {
526
return _whole_words_checkbox->is_pressed();
527
}
528
529
String FindInFilesDialog::get_folder() const {
530
String text = _folder_line_edit->get_text();
531
return text.strip_edges();
532
}
533
534
HashSet<String> FindInFilesDialog::get_filter() const {
535
// Could check the _filters_preferences but it might not have been generated yet.
536
HashSet<String> filters;
537
for (int i = 0; i < _filters_container->get_child_count(); ++i) {
538
CheckBox *cb = static_cast<CheckBox *>(_filters_container->get_child(i));
539
if (cb->is_pressed()) {
540
filters.insert(cb->get_text());
541
}
542
}
543
return filters;
544
}
545
546
HashSet<String> FindInFilesDialog::get_includes() const {
547
HashSet<String> includes;
548
String text = _includes_line_edit->get_text();
549
550
if (text.is_empty()) {
551
return includes;
552
}
553
554
PackedStringArray wildcards = text.split(",", false);
555
for (const String &wildcard : wildcards) {
556
includes.insert(validate_filter_wildcard(wildcard));
557
}
558
return includes;
559
}
560
561
HashSet<String> FindInFilesDialog::get_excludes() const {
562
HashSet<String> excludes;
563
String text = _excludes_line_edit->get_text();
564
565
if (text.is_empty()) {
566
return excludes;
567
}
568
569
PackedStringArray wildcards = text.split(",", false);
570
for (const String &wildcard : wildcards) {
571
excludes.insert(validate_filter_wildcard(wildcard));
572
}
573
return excludes;
574
}
575
576
void FindInFilesDialog::_notification(int p_what) {
577
switch (p_what) {
578
case NOTIFICATION_VISIBILITY_CHANGED: {
579
if (is_visible()) {
580
// Extensions might have changed in the meantime, we clean them and instance them again.
581
for (int i = 0; i < _filters_container->get_child_count(); i++) {
582
_filters_container->get_child(i)->queue_free();
583
}
584
Array exts = GLOBAL_GET("editor/script/search_in_file_extensions");
585
for (int i = 0; i < exts.size(); ++i) {
586
CheckBox *cb = memnew(CheckBox);
587
cb->set_text(exts[i]);
588
if (!_filters_preferences.has(exts[i])) {
589
_filters_preferences[exts[i]] = true;
590
}
591
cb->set_pressed(_filters_preferences[exts[i]]);
592
_filters_container->add_child(cb);
593
}
594
}
595
} break;
596
}
597
}
598
599
void FindInFilesDialog::_on_folder_button_pressed() {
600
_folder_dialog->popup_file_dialog();
601
}
602
603
void FindInFilesDialog::custom_action(const String &p_action) {
604
for (int i = 0; i < _filters_container->get_child_count(); ++i) {
605
CheckBox *cb = static_cast<CheckBox *>(_filters_container->get_child(i));
606
_filters_preferences[cb->get_text()] = cb->is_pressed();
607
}
608
609
if (p_action == "find") {
610
emit_signal(SNAME(SIGNAL_FIND_REQUESTED));
611
hide();
612
} else if (p_action == "replace") {
613
emit_signal(SNAME(SIGNAL_REPLACE_REQUESTED));
614
hide();
615
}
616
}
617
618
void FindInFilesDialog::_on_search_text_modified(const String &text) {
619
ERR_FAIL_NULL(_find_button);
620
ERR_FAIL_NULL(_replace_button);
621
622
_find_button->set_disabled(get_search_text().is_empty());
623
_replace_button->set_disabled(get_search_text().is_empty());
624
}
625
626
void FindInFilesDialog::_on_search_text_submitted(const String &text) {
627
// This allows to trigger a global search without leaving the keyboard.
628
if (!_find_button->is_disabled()) {
629
if (_mode == SEARCH_MODE) {
630
custom_action("find");
631
}
632
}
633
634
if (!_replace_button->is_disabled()) {
635
if (_mode == REPLACE_MODE) {
636
custom_action("replace");
637
}
638
}
639
}
640
641
void FindInFilesDialog::_on_replace_text_submitted(const String &text) {
642
// This allows to trigger a global search without leaving the keyboard.
643
if (!_replace_button->is_disabled()) {
644
if (_mode == REPLACE_MODE) {
645
custom_action("replace");
646
}
647
}
648
}
649
650
void FindInFilesDialog::_on_folder_selected(String path) {
651
int i = path.find("://");
652
if (i != -1) {
653
path = path.substr(i + 3);
654
}
655
_folder_line_edit->set_text(path);
656
}
657
658
String FindInFilesDialog::validate_filter_wildcard(const String &p_expression) const {
659
String ret = p_expression.replace_char('\\', '/');
660
if (ret.begins_with("./")) {
661
// Relative to the project root.
662
ret = "res://" + ret.trim_prefix("./");
663
}
664
665
if (ret.begins_with(".")) {
666
// To match extension.
667
ret = "*" + ret;
668
}
669
670
if (!ret.begins_with("*")) {
671
ret = "*/" + ret.trim_prefix("/");
672
}
673
674
if (!ret.ends_with("*")) {
675
ret = ret.trim_suffix("/") + "/*";
676
}
677
678
return ret;
679
}
680
681
void FindInFilesDialog::_bind_methods() {
682
ADD_SIGNAL(MethodInfo(SIGNAL_FIND_REQUESTED));
683
ADD_SIGNAL(MethodInfo(SIGNAL_REPLACE_REQUESTED));
684
}
685
686
//-----------------------------------------------------------------------------
687
const char *FindInFilesPanel::SIGNAL_RESULT_SELECTED = "result_selected";
688
const char *FindInFilesPanel::SIGNAL_FILES_MODIFIED = "files_modified";
689
const char *FindInFilesPanel::SIGNAL_CLOSE_BUTTON_CLICKED = "close_button_clicked";
690
691
FindInFilesPanel::FindInFilesPanel() {
692
_finder = memnew(FindInFiles);
693
_finder->connect(FindInFiles::SIGNAL_RESULT_FOUND, callable_mp(this, &FindInFilesPanel::_on_result_found));
694
_finder->connect(SceneStringName(finished), callable_mp(this, &FindInFilesPanel::_on_finished));
695
add_child(_finder);
696
697
VBoxContainer *vbc = memnew(VBoxContainer);
698
vbc->set_anchor_and_offset(SIDE_LEFT, ANCHOR_BEGIN, 0);
699
vbc->set_anchor_and_offset(SIDE_TOP, ANCHOR_BEGIN, 0);
700
vbc->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, 0);
701
vbc->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, 0);
702
add_child(vbc);
703
704
{
705
HBoxContainer *hbc = memnew(HBoxContainer);
706
hbc->set_alignment(BoxContainer::ALIGNMENT_END);
707
708
_find_label = memnew(Label);
709
_find_label->set_text(TTRC("Find:"));
710
hbc->add_child(_find_label);
711
712
_search_text_label = memnew(Label);
713
_search_text_label->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS);
714
_search_text_label->set_h_size_flags(Control::SIZE_EXPAND_FILL);
715
_search_text_label->set_focus_mode(FOCUS_ACCESSIBILITY);
716
_search_text_label->set_mouse_filter(Control::MOUSE_FILTER_PASS);
717
_search_text_label->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
718
hbc->add_child(_search_text_label);
719
720
_progress_bar = memnew(ProgressBar);
721
_progress_bar->set_h_size_flags(SIZE_EXPAND_FILL);
722
_progress_bar->set_v_size_flags(SIZE_SHRINK_CENTER);
723
_progress_bar->set_stretch_ratio(2.0);
724
_progress_bar->set_visible(false);
725
hbc->add_child(_progress_bar);
726
727
_status_label = memnew(Label);
728
_status_label->set_focus_mode(FOCUS_ACCESSIBILITY);
729
hbc->add_child(_status_label);
730
731
_keep_results_button = memnew(CheckButton);
732
_keep_results_button->set_text(TTRC("Keep Results"));
733
_keep_results_button->set_tooltip_text(TTRC("Keep these results and show subsequent results in a new window"));
734
_keep_results_button->set_pressed(false);
735
hbc->add_child(_keep_results_button);
736
737
_refresh_button = memnew(Button);
738
_refresh_button->set_text(TTRC("Refresh"));
739
_refresh_button->connect(SceneStringName(pressed), callable_mp(this, &FindInFilesPanel::_on_refresh_button_clicked));
740
_refresh_button->hide();
741
hbc->add_child(_refresh_button);
742
743
_cancel_button = memnew(Button);
744
_cancel_button->set_text(TTRC("Cancel"));
745
_cancel_button->connect(SceneStringName(pressed), callable_mp(this, &FindInFilesPanel::_on_cancel_button_clicked));
746
_cancel_button->hide();
747
hbc->add_child(_cancel_button);
748
749
_close_button = memnew(Button);
750
_close_button->set_text(TTRC("Close"));
751
_close_button->connect(SceneStringName(pressed), callable_mp(this, &FindInFilesPanel::_on_close_button_clicked));
752
hbc->add_child(_close_button);
753
754
vbc->add_child(hbc);
755
}
756
757
_results_mc = memnew(MarginContainer);
758
_results_mc->set_theme_type_variation("NoBorderHorizontal");
759
_results_mc->set_v_size_flags(SIZE_EXPAND_FILL);
760
vbc->add_child(_results_mc);
761
762
_results_display = memnew(Tree);
763
_results_display->set_accessibility_name(TTRC("Search Results"));
764
_results_display->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
765
_results_display->set_scroll_hint_mode(Tree::SCROLL_HINT_MODE_BOTH);
766
_results_display->connect(SceneStringName(item_selected), callable_mp(this, &FindInFilesPanel::_on_result_selected));
767
_results_display->connect("item_edited", callable_mp(this, &FindInFilesPanel::_on_item_edited));
768
_results_display->connect("button_clicked", callable_mp(this, &FindInFilesPanel::_on_button_clicked));
769
_results_display->set_hide_root(true);
770
_results_display->set_select_mode(Tree::SELECT_ROW);
771
_results_display->set_allow_rmb_select(true);
772
_results_display->set_allow_reselect(true);
773
_results_display->add_theme_constant_override("inner_item_margin_left", 0);
774
_results_display->add_theme_constant_override("inner_item_margin_right", 0);
775
_results_display->create_item(); // Root
776
_results_mc->add_child(_results_display);
777
778
{
779
_replace_container = memnew(HBoxContainer);
780
781
Label *replace_label = memnew(Label);
782
replace_label->set_text(TTRC("Replace:"));
783
_replace_container->add_child(replace_label);
784
785
_replace_line_edit = memnew(LineEdit);
786
_replace_line_edit->set_accessibility_name(TTRC("Replace:"));
787
_replace_line_edit->set_h_size_flags(SIZE_EXPAND_FILL);
788
_replace_line_edit->connect(SceneStringName(text_changed), callable_mp(this, &FindInFilesPanel::_on_replace_text_changed));
789
_replace_container->add_child(_replace_line_edit);
790
791
_replace_all_button = memnew(Button);
792
_replace_all_button->set_text(TTRC("Replace all (no undo)"));
793
_replace_all_button->connect(SceneStringName(pressed), callable_mp(this, &FindInFilesPanel::_on_replace_all_clicked));
794
_replace_container->add_child(_replace_all_button);
795
796
_replace_container->hide();
797
798
vbc->add_child(_replace_container);
799
}
800
}
801
802
void FindInFilesPanel::set_with_replace(bool with_replace) {
803
_with_replace = with_replace;
804
_replace_container->set_visible(with_replace);
805
806
if (with_replace) {
807
// Results show checkboxes on their left so they can be opted out.
808
_results_display->set_columns(2);
809
_results_display->set_column_expand(0, false);
810
_results_display->set_column_custom_minimum_width(0, 48 * EDSCALE);
811
} else {
812
// Results are single-cell items.
813
_results_display->set_column_expand(0, true);
814
_results_display->set_columns(1);
815
}
816
}
817
818
void FindInFilesPanel::set_replace_text(const String &text) {
819
_replace_line_edit->set_text(text);
820
}
821
822
bool FindInFilesPanel::is_keep_results() const {
823
return _keep_results_button->is_pressed();
824
}
825
826
void FindInFilesPanel::set_search_labels_visibility(bool p_visible) {
827
_find_label->set_visible(p_visible);
828
_search_text_label->set_visible(p_visible);
829
_close_button->set_visible(p_visible);
830
}
831
832
void FindInFilesPanel::clear() {
833
_file_items.clear();
834
_file_items_results_count.clear();
835
_result_items.clear();
836
_results_display->clear();
837
_results_display->create_item(); // Root
838
}
839
840
void FindInFilesPanel::start_search() {
841
clear();
842
843
_status_label->set_text(TTRC("Searching..."));
844
_search_text_label->set_text(_finder->get_search_text());
845
_search_text_label->set_tooltip_text(_finder->get_search_text());
846
847
int label_min_width = _search_text_label->get_minimum_size().x + _search_text_label->get_character_bounds(0).size.x;
848
_search_text_label->set_custom_minimum_size(Size2(label_min_width, 0));
849
850
set_process(true);
851
_progress_bar->set_visible(true);
852
853
_finder->start();
854
855
update_replace_buttons();
856
_refresh_button->hide();
857
_cancel_button->show();
858
}
859
860
void FindInFilesPanel::stop_search() {
861
_finder->stop();
862
863
_status_label->set_text("");
864
update_replace_buttons();
865
_progress_bar->set_visible(false);
866
_refresh_button->show();
867
_cancel_button->hide();
868
}
869
870
void FindInFilesPanel::update_layout(EditorDock::DockLayout p_layout) {
871
bool new_floating = (p_layout == EditorDock::DOCK_LAYOUT_FLOATING);
872
if (_floating == new_floating) {
873
return;
874
}
875
_floating = new_floating;
876
877
if (_floating) {
878
_results_mc->set_theme_type_variation("NoBorderHorizontalBottom");
879
_results_display->set_scroll_hint_mode(Tree::SCROLL_HINT_MODE_TOP);
880
} else {
881
_results_mc->set_theme_type_variation("NoBorderHorizontal");
882
_results_display->set_scroll_hint_mode(Tree::SCROLL_HINT_MODE_BOTH);
883
}
884
}
885
886
void FindInFilesPanel::_notification(int p_what) {
887
switch (p_what) {
888
case NOTIFICATION_THEME_CHANGED: {
889
_on_theme_changed();
890
} break;
891
case NOTIFICATION_TRANSLATION_CHANGED: {
892
update_matches_text();
893
894
TreeItem *file_item = _results_display->get_root()->get_first_child();
895
while (file_item) {
896
if (_with_replace) {
897
file_item->set_button_tooltip_text(0, file_item->get_button_by_id(0, FIND_BUTTON_REPLACE), TTR("Replace all matches in file"));
898
}
899
file_item->set_button_tooltip_text(0, file_item->get_button_by_id(0, FIND_BUTTON_REMOVE), TTR("Remove result"));
900
901
TreeItem *result_item = file_item->get_first_child();
902
while (result_item) {
903
if (_with_replace) {
904
result_item->set_button_tooltip_text(1, file_item->get_button_by_id(0, FIND_BUTTON_REPLACE), TTR("Replace"));
905
result_item->set_button_tooltip_text(1, file_item->get_button_by_id(0, FIND_BUTTON_REMOVE), TTR("Remove result"));
906
} else {
907
result_item->set_button_tooltip_text(0, file_item->get_button_by_id(0, FIND_BUTTON_REMOVE), TTR("Remove result"));
908
}
909
result_item = result_item->get_next();
910
}
911
912
file_item = file_item->get_next();
913
}
914
} break;
915
case NOTIFICATION_PROCESS: {
916
_progress_bar->set_as_ratio(_finder->get_progress());
917
} break;
918
}
919
}
920
921
void FindInFilesPanel::_on_result_found(const String &fpath, int line_number, int begin, int end, String text) {
922
TreeItem *file_item;
923
Ref<Texture2D> remove_texture = get_editor_theme_icon(SNAME("Close"));
924
Ref<Texture2D> replace_texture = get_editor_theme_icon(SNAME("ReplaceText"));
925
926
HashMap<String, TreeItem *>::Iterator E = _file_items.find(fpath);
927
if (!E) {
928
file_item = _results_display->create_item();
929
file_item->set_text(0, fpath);
930
file_item->set_metadata(0, fpath);
931
932
if (_with_replace) {
933
file_item->add_button(0, replace_texture, FIND_BUTTON_REPLACE, false, TTR("Replace all matches in file"));
934
}
935
file_item->add_button(0, remove_texture, FIND_BUTTON_REMOVE, false, TTR("Remove result"));
936
937
// The width of this column is restrained to checkboxes,
938
// but that doesn't make sense for the parent items,
939
// so we override their width so they can expand to full width.
940
file_item->set_expand_right(0, true);
941
942
_file_items[fpath] = file_item;
943
_file_items_results_count[file_item] = 1;
944
} else {
945
file_item = E->value;
946
_file_items_results_count[file_item]++;
947
}
948
949
Color file_item_color = _results_display->get_theme_color(SceneStringName(font_color)) * Color(1, 1, 1, 0.67);
950
file_item->set_custom_color(0, file_item_color);
951
file_item->set_selectable(0, false);
952
953
int text_index = _with_replace ? 1 : 0;
954
955
TreeItem *item = _results_display->create_item(file_item);
956
957
// Do this first because it resets properties of the cell...
958
item->set_cell_mode(text_index, TreeItem::CELL_MODE_CUSTOM);
959
960
// Trim result item line.
961
int old_text_size = text.size();
962
text = text.strip_edges(true, false);
963
int chars_removed = old_text_size - text.size();
964
String start = vformat("%3s: ", line_number);
965
966
item->set_text(text_index, start + text);
967
item->set_custom_draw_callback(text_index, callable_mp(this, &FindInFilesPanel::draw_result_text));
968
969
Result r;
970
r.line_number = line_number;
971
r.begin = begin;
972
r.end = end;
973
r.begin_trimmed = begin - chars_removed + start.size() - 1;
974
_result_items[item] = r;
975
976
if (_with_replace) {
977
item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK);
978
item->set_checked(0, true);
979
item->set_editable(0, true);
980
item->add_button(1, replace_texture, FIND_BUTTON_REPLACE, false, TTR("Replace"));
981
item->add_button(1, remove_texture, FIND_BUTTON_REMOVE, false, TTR("Remove result"));
982
} else {
983
item->add_button(0, remove_texture, FIND_BUTTON_REMOVE, false, TTR("Remove result"));
984
}
985
}
986
987
void FindInFilesPanel::_on_theme_changed() {
988
_results_display->add_theme_font_override(SceneStringName(font), get_theme_font(SNAME("source"), EditorStringName(EditorFonts)));
989
_results_display->add_theme_font_size_override(SceneStringName(font_size), get_theme_font_size(SNAME("source_size"), EditorStringName(EditorFonts)));
990
991
Color file_item_color = _results_display->get_theme_color(SceneStringName(font_color)) * Color(1, 1, 1, 0.67);
992
Ref<Texture2D> remove_texture = get_editor_theme_icon(SNAME("Close"));
993
Ref<Texture2D> replace_texture = get_editor_theme_icon(SNAME("ReplaceText"));
994
995
TreeItem *file_item = _results_display->get_root()->get_first_child();
996
while (file_item) {
997
file_item->set_custom_color(0, file_item_color);
998
if (_with_replace) {
999
file_item->set_button(0, file_item->get_button_by_id(0, FIND_BUTTON_REPLACE), replace_texture);
1000
}
1001
file_item->set_button(0, file_item->get_button_by_id(0, FIND_BUTTON_REMOVE), remove_texture);
1002
1003
TreeItem *result_item = file_item->get_first_child();
1004
while (result_item) {
1005
if (_with_replace) {
1006
result_item->set_button(1, result_item->get_button_by_id(1, FIND_BUTTON_REPLACE), replace_texture);
1007
result_item->set_button(1, result_item->get_button_by_id(1, FIND_BUTTON_REMOVE), remove_texture);
1008
} else {
1009
result_item->set_button(0, result_item->get_button_by_id(0, FIND_BUTTON_REMOVE), remove_texture);
1010
}
1011
1012
result_item = result_item->get_next();
1013
}
1014
1015
file_item = file_item->get_next();
1016
}
1017
}
1018
1019
void FindInFilesPanel::draw_result_text(Object *item_obj, Rect2 rect) {
1020
TreeItem *item = Object::cast_to<TreeItem>(item_obj);
1021
if (!item) {
1022
return;
1023
}
1024
1025
HashMap<TreeItem *, Result>::Iterator E = _result_items.find(item);
1026
if (!E) {
1027
return;
1028
}
1029
Result r = E->value;
1030
String item_text = item->get_text(_with_replace ? 1 : 0);
1031
Ref<Font> font = _results_display->get_theme_font(SceneStringName(font));
1032
int font_size = _results_display->get_theme_font_size(SceneStringName(font_size));
1033
1034
Rect2 match_rect = rect;
1035
match_rect.position.x += font->get_string_size(item_text.left(r.begin_trimmed), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x - 1;
1036
match_rect.size.x = font->get_string_size(_search_text_label->get_text(), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x + 1;
1037
match_rect.position.y += 1 * EDSCALE;
1038
match_rect.size.y -= 2 * EDSCALE;
1039
1040
_results_display->draw_rect(match_rect, get_theme_color(SNAME("accent_color"), EditorStringName(Editor)) * Color(1, 1, 1, 0.33), false, 2.0);
1041
_results_display->draw_rect(match_rect, get_theme_color(SNAME("accent_color"), EditorStringName(Editor)) * Color(1, 1, 1, 0.17), true);
1042
1043
// Text is drawn by Tree already.
1044
}
1045
1046
void FindInFilesPanel::_on_item_edited() {
1047
TreeItem *item = _results_display->get_selected();
1048
1049
// Change opacity to half if checkbox is checked, otherwise full.
1050
Color use_color = _results_display->get_theme_color(SceneStringName(font_color));
1051
if (!item->is_checked(0)) {
1052
use_color.a *= 0.5;
1053
}
1054
item->set_custom_color(1, use_color);
1055
}
1056
1057
void FindInFilesPanel::_on_finished() {
1058
update_matches_text();
1059
update_replace_buttons();
1060
_progress_bar->set_visible(false);
1061
_refresh_button->show();
1062
_cancel_button->hide();
1063
}
1064
1065
void FindInFilesPanel::_on_refresh_button_clicked() {
1066
start_search();
1067
}
1068
1069
void FindInFilesPanel::_on_cancel_button_clicked() {
1070
stop_search();
1071
}
1072
1073
void FindInFilesPanel::_on_close_button_clicked() {
1074
emit_signal(SNAME(SIGNAL_CLOSE_BUTTON_CLICKED));
1075
}
1076
1077
void FindInFilesPanel::_on_result_selected() {
1078
TreeItem *item = _results_display->get_selected();
1079
HashMap<TreeItem *, Result>::Iterator E = _result_items.find(item);
1080
1081
if (!E) {
1082
return;
1083
}
1084
Result r = E->value;
1085
1086
TreeItem *file_item = item->get_parent();
1087
String fpath = file_item->get_metadata(0);
1088
1089
emit_signal(SNAME(SIGNAL_RESULT_SELECTED), fpath, r.line_number, r.begin, r.end);
1090
}
1091
1092
void FindInFilesPanel::_on_replace_text_changed(const String &text) {
1093
update_replace_buttons();
1094
}
1095
1096
void FindInFilesPanel::_on_replace_all_clicked() {
1097
String replace_text = get_replace_text();
1098
1099
PackedStringArray modified_files;
1100
1101
for (KeyValue<String, TreeItem *> &E : _file_items) {
1102
TreeItem *file_item = E.value;
1103
String fpath = file_item->get_metadata(0);
1104
1105
Vector<Result> locations;
1106
for (TreeItem *item = file_item->get_first_child(); item; item = item->get_next()) {
1107
if (!item->is_checked(0)) {
1108
continue;
1109
}
1110
1111
HashMap<TreeItem *, Result>::Iterator F = _result_items.find(item);
1112
ERR_FAIL_COND(!F);
1113
locations.push_back(F->value);
1114
}
1115
1116
if (locations.size() != 0) {
1117
// Results are sorted by file, so we can batch replaces.
1118
apply_replaces_in_file(fpath, locations, replace_text);
1119
modified_files.push_back(fpath);
1120
}
1121
}
1122
1123
// Hide replace bar so we can't trigger the action twice without doing a new search.
1124
_replace_container->hide();
1125
1126
emit_signal(SNAME(SIGNAL_FILES_MODIFIED), modified_files);
1127
}
1128
1129
void FindInFilesPanel::_on_button_clicked(TreeItem *p_item, int p_column, int p_id, int p_mouse_button_index) {
1130
const String file_path = p_item->get_metadata(0);
1131
1132
if (p_id == FIND_BUTTON_REPLACE) {
1133
const String replace_text = get_replace_text();
1134
Vector<Result> locations;
1135
PackedStringArray modified_files;
1136
if (_file_items.has(file_path)) {
1137
for (TreeItem *item = p_item->get_first_child(); item; item = item->get_next()) {
1138
HashMap<TreeItem *, Result>::Iterator F = _result_items.find(item);
1139
ERR_FAIL_COND(!F);
1140
locations.push_back(F->value);
1141
}
1142
apply_replaces_in_file(file_path, locations, replace_text);
1143
modified_files.push_back(file_path);
1144
} else {
1145
locations.push_back(_result_items.find(p_item)->value);
1146
const String path = p_item->get_parent()->get_metadata(0);
1147
apply_replaces_in_file(path, locations, replace_text);
1148
modified_files.push_back(path);
1149
}
1150
emit_signal(SNAME(SIGNAL_FILES_MODIFIED), modified_files);
1151
}
1152
1153
_result_items.erase(p_item);
1154
if (_file_items_results_count.has(p_item)) {
1155
int match_count = p_item->get_child_count();
1156
1157
for (int i = 0; i < match_count; i++) {
1158
TreeItem *child_item = p_item->get_child(i);
1159
_result_items.erase(child_item);
1160
}
1161
1162
p_item->clear_children();
1163
_file_items.erase(file_path);
1164
_file_items_results_count.erase(p_item);
1165
}
1166
1167
TreeItem *item_parent = p_item->get_parent();
1168
if (item_parent) {
1169
if (_file_items_results_count.has(item_parent)) {
1170
_file_items_results_count[item_parent]--;
1171
}
1172
if (item_parent->get_child_count() < 2 && item_parent != _results_display->get_root()) {
1173
_file_items.erase(item_parent->get_metadata(0));
1174
get_tree()->queue_delete(item_parent);
1175
}
1176
}
1177
get_tree()->queue_delete(p_item);
1178
update_matches_text();
1179
}
1180
1181
// Same as get_line, but preserves line ending characters.
1182
class ConservativeGetLine {
1183
public:
1184
String get_line(Ref<FileAccess> f) {
1185
_line_buffer.clear();
1186
1187
char32_t c = f->get_8();
1188
1189
while (!f->eof_reached()) {
1190
if (c == '\n') {
1191
_line_buffer.push_back(c);
1192
_line_buffer.push_back(0);
1193
return String::utf8(_line_buffer.ptr());
1194
1195
} else if (c == '\0') {
1196
_line_buffer.push_back(c);
1197
return String::utf8(_line_buffer.ptr());
1198
1199
} else if (c != '\r') {
1200
_line_buffer.push_back(c);
1201
}
1202
1203
c = f->get_8();
1204
}
1205
1206
_line_buffer.push_back(0);
1207
return String::utf8(_line_buffer.ptr());
1208
}
1209
1210
private:
1211
Vector<char> _line_buffer;
1212
};
1213
1214
void FindInFilesPanel::apply_replaces_in_file(const String &fpath, const Vector<Result> &locations, const String &new_text) {
1215
// If the file is already open, I assume the editor will reload it.
1216
// If there are unsaved changes, the user will be asked on focus,
1217
// however that means either losing changes or losing replaces.
1218
1219
Ref<FileAccess> f = FileAccess::open(fpath, FileAccess::READ);
1220
ERR_FAIL_COND_MSG(f.is_null(), "Cannot open file from path '" + fpath + "'.");
1221
1222
String buffer;
1223
int current_line = 1;
1224
1225
ConservativeGetLine conservative;
1226
1227
String line = conservative.get_line(f);
1228
String search_text = _finder->get_search_text();
1229
1230
int offset = 0;
1231
1232
for (int i = 0; i < locations.size(); ++i) {
1233
int repl_line_number = locations[i].line_number;
1234
1235
while (current_line < repl_line_number) {
1236
buffer += line;
1237
line = conservative.get_line(f);
1238
++current_line;
1239
offset = 0;
1240
}
1241
1242
int repl_begin = locations[i].begin + offset;
1243
int repl_end = locations[i].end + offset;
1244
1245
int _;
1246
if (!find_next(line, search_text, repl_begin, _finder->is_match_case(), _finder->is_whole_words(), _, _)) {
1247
// Make sure the replace is still valid in case the file was tampered with.
1248
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)));
1249
continue;
1250
}
1251
1252
line = line.left(repl_begin) + new_text + line.substr(repl_end);
1253
// Keep an offset in case there are successive replaces in the same line.
1254
offset += new_text.length() - (repl_end - repl_begin);
1255
}
1256
1257
buffer += line;
1258
1259
while (!f->eof_reached()) {
1260
buffer += conservative.get_line(f);
1261
}
1262
1263
// Now the modified contents are in the buffer, rewrite the file with our changes.
1264
1265
Error err = f->reopen(fpath, FileAccess::WRITE);
1266
ERR_FAIL_COND_MSG(err != OK, "Cannot create file in path '" + fpath + "'.");
1267
1268
f->store_string(buffer);
1269
}
1270
1271
String FindInFilesPanel::get_replace_text() {
1272
return _replace_line_edit->get_text();
1273
}
1274
1275
void FindInFilesPanel::update_replace_buttons() {
1276
bool disabled = _finder->is_searching();
1277
1278
_replace_all_button->set_disabled(disabled);
1279
}
1280
1281
void FindInFilesPanel::update_matches_text() {
1282
String results_text;
1283
int result_count = _result_items.size();
1284
int file_count = _file_items.size();
1285
1286
if (result_count == 1 && file_count == 1) {
1287
results_text = vformat(TTR("%d match in %d file"), result_count, file_count);
1288
} else if (result_count != 1 && file_count == 1) {
1289
results_text = vformat(TTR("%d matches in %d file"), result_count, file_count);
1290
} else {
1291
results_text = vformat(TTR("%d matches in %d files"), result_count, file_count);
1292
}
1293
1294
_status_label->set_text(results_text);
1295
1296
TreeItem *file_item = _results_display->get_root()->get_first_child();
1297
while (file_item) {
1298
int file_matches_count = _file_items_results_count[file_item];
1299
file_item->set_text(0, (String)file_item->get_metadata(0) + " (" + vformat(TTRN("%d match", "%d matches", file_matches_count), file_matches_count) + ")");
1300
file_item = file_item->get_next();
1301
}
1302
}
1303
1304
void FindInFilesPanel::_bind_methods() {
1305
ClassDB::bind_method("_on_result_found", &FindInFilesPanel::_on_result_found);
1306
ClassDB::bind_method("_on_finished", &FindInFilesPanel::_on_finished);
1307
1308
ADD_SIGNAL(MethodInfo(SIGNAL_RESULT_SELECTED,
1309
PropertyInfo(Variant::STRING, "path"),
1310
PropertyInfo(Variant::INT, "line_number"),
1311
PropertyInfo(Variant::INT, "begin"),
1312
PropertyInfo(Variant::INT, "end")));
1313
1314
ADD_SIGNAL(MethodInfo(SIGNAL_FILES_MODIFIED, PropertyInfo(Variant::STRING, "paths")));
1315
1316
ADD_SIGNAL(MethodInfo(SIGNAL_CLOSE_BUTTON_CLICKED));
1317
}
1318
1319
//-----------------------------------------------------------------------------
1320
1321
FindInFilesContainer::FindInFilesContainer() {
1322
set_name(TTRC("Search Results"));
1323
set_icon_name("Search");
1324
set_dock_shortcut(ED_SHORTCUT_AND_COMMAND("bottom_panels/toggle_search_results_bottom_panel", TTRC("Toggle Search Results Bottom Panel")));
1325
set_default_slot(EditorDock::DOCK_SLOT_BOTTOM);
1326
set_available_layouts(EditorDock::DOCK_LAYOUT_HORIZONTAL | EditorDock::DOCK_LAYOUT_FLOATING);
1327
set_global(false);
1328
set_transient(true);
1329
set_closable(true);
1330
set_custom_minimum_size(Size2(0, 200 * EDSCALE));
1331
1332
_tabs = memnew(TabContainer);
1333
_tabs->set_tabs_visible(false);
1334
add_child(_tabs);
1335
1336
_tabs->set_drag_to_rearrange_enabled(true);
1337
_tabs->get_tab_bar()->set_select_with_rmb(true);
1338
_tabs->get_tab_bar()->set_tab_close_display_policy(TabBar::CLOSE_BUTTON_SHOW_ACTIVE_ONLY);
1339
_tabs->get_tab_bar()->connect("tab_close_pressed", callable_mp(this, &FindInFilesContainer::_on_tab_close_pressed));
1340
_tabs->get_tab_bar()->connect(SceneStringName(gui_input), callable_mp(this, &FindInFilesContainer::_bar_input));
1341
1342
_tabs_context_menu = memnew(PopupMenu);
1343
add_child(_tabs_context_menu);
1344
_tabs_context_menu->add_item(TTRC("Close Tab"), PANEL_CLOSE);
1345
_tabs_context_menu->add_item(TTRC("Close Other Tabs"), PANEL_CLOSE_OTHERS);
1346
_tabs_context_menu->add_item(TTRC("Close Tabs to the Right"), PANEL_CLOSE_RIGHT);
1347
_tabs_context_menu->add_item(TTRC("Close All Tabs"), PANEL_CLOSE_ALL);
1348
_tabs_context_menu->connect(SceneStringName(id_pressed), callable_mp(this, &FindInFilesContainer::_bar_menu_option));
1349
1350
EditorNode::get_singleton()->get_gui_base()->connect(SceneStringName(theme_changed), callable_mp(this, &FindInFilesContainer::_on_theme_changed));
1351
}
1352
1353
FindInFilesPanel *FindInFilesContainer::_create_new_panel() {
1354
int index = _tabs->get_current_tab();
1355
FindInFilesPanel *panel = memnew(FindInFilesPanel);
1356
_tabs->add_child(panel);
1357
_tabs->move_child(panel, index + 1); // New panel is added after the current activated panel.
1358
_tabs->set_current_tab(index + 1);
1359
_update_bar_visibility();
1360
1361
panel->connect(FindInFilesPanel::SIGNAL_RESULT_SELECTED, callable_mp(this, &FindInFilesContainer::_on_find_in_files_result_selected));
1362
panel->connect(FindInFilesPanel::SIGNAL_FILES_MODIFIED, callable_mp(this, &FindInFilesContainer::_on_find_in_files_modified_files));
1363
panel->connect(FindInFilesPanel::SIGNAL_CLOSE_BUTTON_CLICKED, callable_mp(this, &FindInFilesContainer::_on_find_in_files_close_button_clicked).bind(panel));
1364
return panel;
1365
}
1366
1367
FindInFilesPanel *FindInFilesContainer::_get_current_panel() {
1368
return Object::cast_to<FindInFilesPanel>(_tabs->get_current_tab_control());
1369
}
1370
1371
FindInFilesPanel *FindInFilesContainer::get_panel_for_results(const String &p_label) {
1372
FindInFilesPanel *panel = nullptr;
1373
// Prefer the current panel.
1374
if (_get_current_panel() && !_get_current_panel()->is_keep_results()) {
1375
panel = _get_current_panel();
1376
} else {
1377
// Find the first panel which does not keep results.
1378
for (int i = 0; i < _tabs->get_tab_count(); i++) {
1379
FindInFilesPanel *p = Object::cast_to<FindInFilesPanel>(_tabs->get_tab_control(i));
1380
if (p && !p->is_keep_results()) {
1381
panel = p;
1382
_tabs->set_current_tab(i);
1383
break;
1384
}
1385
}
1386
1387
if (!panel) {
1388
panel = _create_new_panel();
1389
}
1390
}
1391
_tabs->set_tab_title(_tabs->get_current_tab(), p_label);
1392
return panel;
1393
}
1394
1395
void FindInFilesContainer::_bind_methods() {
1396
ADD_SIGNAL(MethodInfo("result_selected",
1397
PropertyInfo(Variant::STRING, "path"),
1398
PropertyInfo(Variant::INT, "line_number"),
1399
PropertyInfo(Variant::INT, "begin"),
1400
PropertyInfo(Variant::INT, "end")));
1401
1402
ADD_SIGNAL(MethodInfo("files_modified", PropertyInfo(Variant::STRING, "paths")));
1403
}
1404
1405
void FindInFilesContainer::_notification(int p_what) {
1406
switch (p_what) {
1407
case NOTIFICATION_POSTINITIALIZE: {
1408
connect("closed", callable_mp(this, &FindInFilesContainer::_on_dock_closed));
1409
} break;
1410
}
1411
}
1412
1413
void FindInFilesContainer::_on_theme_changed() {
1414
const Ref<StyleBox> bottom_panel_style = EditorNode::get_singleton()->get_editor_theme()->get_stylebox(SNAME("BottomPanel"), EditorStringName(EditorStyles));
1415
if (bottom_panel_style.is_valid()) {
1416
begin_bulk_theme_override();
1417
add_theme_constant_override("margin_top", -bottom_panel_style->get_margin(SIDE_TOP));
1418
add_theme_constant_override("margin_left", -bottom_panel_style->get_margin(SIDE_LEFT));
1419
add_theme_constant_override("margin_right", -bottom_panel_style->get_margin(SIDE_RIGHT));
1420
add_theme_constant_override("margin_bottom", -bottom_panel_style->get_margin(SIDE_BOTTOM));
1421
end_bulk_theme_override();
1422
}
1423
}
1424
1425
void FindInFilesContainer::_on_find_in_files_result_selected(const String &p_fpath, int p_line_number, int p_begin, int p_end) {
1426
emit_signal(SNAME("result_selected"), p_fpath, p_line_number, p_begin, p_end);
1427
}
1428
1429
void FindInFilesContainer::_on_find_in_files_modified_files(const PackedStringArray &p_paths) {
1430
emit_signal(SNAME("files_modified"), p_paths);
1431
}
1432
1433
void FindInFilesContainer::_on_find_in_files_close_button_clicked(FindInFilesPanel *p_panel) {
1434
ERR_FAIL_COND_MSG(p_panel->get_parent() != _tabs, "This panel is not a child!");
1435
_tabs->remove_child(p_panel);
1436
p_panel->queue_free();
1437
_update_bar_visibility();
1438
if (_tabs->get_tab_count() == 0) {
1439
close();
1440
}
1441
}
1442
1443
void FindInFilesContainer::update_layout(EditorDock::DockLayout p_layout) {
1444
for (Node *node : _tabs->iterate_children()) {
1445
FindInFilesPanel *panel = Object::cast_to<FindInFilesPanel>(node);
1446
if (panel) {
1447
panel->update_layout(p_layout);
1448
}
1449
}
1450
}
1451
1452
void FindInFilesContainer::_on_tab_close_pressed(int p_tab) {
1453
FindInFilesPanel *panel = Object::cast_to<FindInFilesPanel>(_tabs->get_tab_control(p_tab));
1454
if (panel) {
1455
_on_find_in_files_close_button_clicked(panel);
1456
}
1457
}
1458
1459
void FindInFilesContainer::_update_bar_visibility() {
1460
if (!_update_bar) {
1461
return;
1462
}
1463
1464
// If tab count <= 1, behaves like this is not a TabContainer and the bar is hidden.
1465
bool bar_visible = _tabs->get_tab_count() > 1;
1466
_tabs->set_tabs_visible(bar_visible);
1467
1468
// Hide or show the search labels based on the visibility of the bar, as the search terms are displayed in the title of each tab.
1469
for (int i = 0; i < _tabs->get_tab_count(); i++) {
1470
FindInFilesPanel *panel = Object::cast_to<FindInFilesPanel>(_tabs->get_tab_control(i));
1471
if (panel) {
1472
panel->set_search_labels_visibility(!bar_visible);
1473
}
1474
}
1475
}
1476
1477
void FindInFilesContainer::_bar_menu_option(int p_option) {
1478
int tab_index = _tabs->get_current_tab();
1479
switch (p_option) {
1480
case PANEL_CLOSE: {
1481
_on_tab_close_pressed(tab_index);
1482
} break;
1483
case PANEL_CLOSE_OTHERS: {
1484
_update_bar = false;
1485
FindInFilesPanel *panel = Object::cast_to<FindInFilesPanel>(_tabs->get_tab_control(tab_index));
1486
for (int i = _tabs->get_tab_count() - 1; i >= 0; i--) {
1487
FindInFilesPanel *p = Object::cast_to<FindInFilesPanel>(_tabs->get_tab_control(i));
1488
if (p != panel) {
1489
_on_find_in_files_close_button_clicked(p);
1490
}
1491
}
1492
_update_bar = true;
1493
_update_bar_visibility();
1494
} break;
1495
case PANEL_CLOSE_RIGHT: {
1496
_update_bar = false;
1497
for (int i = _tabs->get_tab_count() - 1; i > tab_index; i--) {
1498
_on_tab_close_pressed(i);
1499
}
1500
_update_bar = true;
1501
_update_bar_visibility();
1502
} break;
1503
case PANEL_CLOSE_ALL: {
1504
_update_bar = false;
1505
for (int i = _tabs->get_tab_count() - 1; i >= 0; i--) {
1506
_on_tab_close_pressed(i);
1507
}
1508
_update_bar = true;
1509
} break;
1510
}
1511
}
1512
1513
void FindInFilesContainer::_bar_input(const Ref<InputEvent> &p_input) {
1514
int tab_id = _tabs->get_tab_bar()->get_hovered_tab();
1515
Ref<InputEventMouseButton> mb = p_input;
1516
1517
if (tab_id >= 0 && mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) {
1518
_tabs_context_menu->set_item_disabled(_tabs_context_menu->get_item_index(PANEL_CLOSE_RIGHT), tab_id == _tabs->get_tab_count() - 1);
1519
_tabs_context_menu->set_position(_tabs->get_tab_bar()->get_screen_position() + mb->get_position());
1520
_tabs_context_menu->reset_size();
1521
_tabs_context_menu->popup();
1522
}
1523
}
1524
1525
void FindInFilesContainer::_on_dock_closed() {
1526
while (_tabs->get_tab_count() > 0) {
1527
Control *tab = _tabs->get_tab_control(0);
1528
_tabs->remove_child(tab);
1529
tab->queue_free();
1530
}
1531
_update_bar_visibility();
1532
}
1533
1534