Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
godotengine
GitHub Repository: godotengine/godot
Path: blob/master/editor/gui/code_editor.cpp
20831 views
1
/**************************************************************************/
2
/* code_editor.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 "code_editor.h"
32
33
#include "core/input/input.h"
34
#include "core/os/keyboard.h"
35
#include "core/string/string_builder.h"
36
#include "editor/editor_node.h"
37
#include "editor/editor_string_names.h"
38
#include "editor/script/syntax_highlighters.h"
39
#include "editor/settings/editor_settings.h"
40
#include "editor/themes/editor_scale.h"
41
#include "editor/themes/editor_theme_manager.h"
42
#include "scene/gui/check_box.h"
43
#include "scene/gui/label.h"
44
#include "scene/gui/line_edit.h"
45
#include "scene/gui/menu_button.h"
46
#include "scene/gui/rich_text_label.h"
47
#include "scene/gui/separator.h"
48
#include "scene/main/timer.h"
49
#include "scene/resources/font.h"
50
51
void GotoLinePopup::popup_find_line(CodeTextEditor *p_text_editor) {
52
text_editor = p_text_editor;
53
54
original_state = text_editor->get_navigation_state();
55
56
// Add 1 because the TextEdit starts from 0, but the editor user interface starts from 1.
57
TextEdit *text_edit = text_editor->get_text_editor();
58
int original_line = text_edit->get_caret_line() + 1;
59
line_input->set_text(itos(original_line));
60
text_editor->set_preview_navigation_change(true);
61
62
Rect2i parent_rect = text_editor->get_global_rect();
63
Point2i centered_pos(parent_rect.get_center().x - get_contents_minimum_size().x / 2.0, parent_rect.position.y);
64
popup_on_parent(Rect2i(centered_pos, Size2()));
65
reset_size();
66
line_input->grab_focus();
67
}
68
69
void GotoLinePopup::_goto_line() {
70
if (line_input->get_text().is_empty()) {
71
return;
72
}
73
74
PackedStringArray line_col_strings = line_input->get_text().split(":");
75
// Subtract 1 because the editor user interface starts from 1, but the TextEdit starts from 0.
76
const int line_number = line_col_strings[0].to_int() - 1;
77
if (line_number < 0 || line_number >= text_editor->get_text_editor()->get_line_count()) {
78
return;
79
}
80
81
int column_number = 0;
82
if (line_col_strings.size() >= 2) {
83
column_number = line_col_strings[1].to_int() - 1;
84
}
85
text_editor->goto_line_centered(line_number, column_number);
86
}
87
88
void GotoLinePopup::_submit() {
89
_goto_line();
90
hide();
91
}
92
93
void GotoLinePopup::_notification(int p_what) {
94
switch (p_what) {
95
case NOTIFICATION_VISIBILITY_CHANGED: {
96
if (!is_visible()) {
97
text_editor->set_preview_navigation_change(false);
98
}
99
} break;
100
}
101
}
102
103
void GotoLinePopup::_input_from_window(const Ref<InputEvent> &p_event) {
104
if (p_event->is_action_pressed(SNAME("ui_cancel"), false, true)) {
105
// Cancelled, go back to original state.
106
text_editor->set_edit_state(original_state);
107
}
108
PopupPanel::_input_from_window(p_event);
109
}
110
111
GotoLinePopup::GotoLinePopup() {
112
set_title(TTRC("Go to Line"));
113
114
VBoxContainer *vbc = memnew(VBoxContainer);
115
vbc->set_anchor_and_offset(SIDE_LEFT, Control::ANCHOR_BEGIN, 8 * EDSCALE);
116
vbc->set_anchor_and_offset(SIDE_TOP, Control::ANCHOR_BEGIN, 8 * EDSCALE);
117
vbc->set_anchor_and_offset(SIDE_RIGHT, Control::ANCHOR_END, -8 * EDSCALE);
118
vbc->set_anchor_and_offset(SIDE_BOTTOM, Control::ANCHOR_END, -8 * EDSCALE);
119
add_child(vbc);
120
121
Label *l = memnew(Label);
122
l->set_text(TTRC("Line Number:"));
123
vbc->add_child(l);
124
125
line_input = memnew(LineEdit);
126
line_input->set_emoji_menu_enabled(false);
127
line_input->set_custom_minimum_size(Size2(100, 0) * EDSCALE);
128
line_input->set_select_all_on_focus(true);
129
line_input->connect(SceneStringName(text_changed), callable_mp(this, &GotoLinePopup::_goto_line).unbind(1));
130
line_input->connect(SceneStringName(text_submitted), callable_mp(this, &GotoLinePopup::_submit).unbind(1));
131
line_input->set_accessibility_name(TTRC("Line Number:"));
132
vbc->add_child(line_input);
133
}
134
135
void FindReplaceBar::_notification(int p_what) {
136
switch (p_what) {
137
case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
138
if (!EditorThemeManager::is_generated_theme_outdated()) {
139
break;
140
}
141
[[fallthrough]];
142
}
143
case NOTIFICATION_READY: {
144
find_prev->set_button_icon(get_editor_theme_icon(SNAME("MoveUp")));
145
find_next->set_button_icon(get_editor_theme_icon(SNAME("MoveDown")));
146
hide_button->set_button_icon(get_editor_theme_icon(SNAME("Close")));
147
_update_toggle_replace_button(replace_text->is_visible_in_tree());
148
} break;
149
150
case NOTIFICATION_TRANSLATION_CHANGED: {
151
if (matches_label->is_visible()) {
152
_update_matches_display();
153
}
154
[[fallthrough]];
155
}
156
case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: {
157
_update_toggle_replace_button(replace_text->is_visible_in_tree());
158
} break;
159
160
case NOTIFICATION_VISIBILITY_CHANGED: {
161
set_process_input(is_visible_in_tree());
162
} break;
163
164
case NOTIFICATION_THEME_CHANGED: {
165
matches_label->add_theme_color_override(SceneStringName(font_color), results_count > 0 ? get_theme_color(SceneStringName(font_color), SNAME("Label")) : get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
166
} break;
167
168
case NOTIFICATION_PREDELETE: {
169
if (base_text_editor) {
170
base_text_editor->remove_find_replace_bar();
171
base_text_editor = nullptr;
172
}
173
} break;
174
}
175
}
176
177
// Implemented in input(..) as the LineEdit consumes the Escape pressed key.
178
void FindReplaceBar::input(const Ref<InputEvent> &p_event) {
179
ERR_FAIL_COND(p_event.is_null());
180
181
Ref<InputEventKey> k = p_event;
182
if (k.is_valid() && k->is_action_pressed(SNAME("ui_cancel"), false, true)) {
183
Control *focus_owner = get_viewport()->gui_get_focus_owner();
184
185
if (text_editor->has_focus() || (focus_owner && is_ancestor_of(focus_owner))) {
186
_hide_bar();
187
accept_event();
188
}
189
}
190
}
191
192
void FindReplaceBar::_update_flags(bool p_direction_backwards) {
193
flags = 0;
194
195
if (is_whole_words()) {
196
flags |= TextEdit::SEARCH_WHOLE_WORDS;
197
}
198
if (is_case_sensitive()) {
199
flags |= TextEdit::SEARCH_MATCH_CASE;
200
}
201
if (p_direction_backwards) {
202
flags |= TextEdit::SEARCH_BACKWARDS;
203
}
204
}
205
206
bool FindReplaceBar::_search(uint32_t p_flags, int p_from_line, int p_from_col) {
207
if (!preserve_cursor) {
208
text_editor->remove_secondary_carets();
209
}
210
String text = get_search_text();
211
Point2i pos = text_editor->search(text, p_flags, p_from_line, p_from_col);
212
213
if (pos.x != -1) {
214
if (!preserve_cursor && !is_selection_only()) {
215
text_editor->unfold_line(pos.y);
216
text_editor->select(pos.y, pos.x, pos.y, pos.x + text.length());
217
text_editor->center_viewport_to_caret(0);
218
text_editor->set_code_hint("");
219
text_editor->cancel_code_completion();
220
221
line_col_changed_for_result = true;
222
}
223
224
text_editor->set_search_text(text);
225
text_editor->set_search_flags(p_flags);
226
227
result_line = pos.y;
228
result_col = pos.x;
229
230
_update_results_count();
231
} else {
232
results_count = 0;
233
result_line = -1;
234
result_col = -1;
235
text_editor->set_search_text("");
236
text_editor->set_search_flags(p_flags);
237
}
238
239
_update_matches_display();
240
241
return pos.x != -1;
242
}
243
244
void FindReplaceBar::_replace() {
245
text_editor->begin_complex_operation();
246
text_editor->remove_secondary_carets();
247
bool selection_enabled = text_editor->has_selection(0);
248
Point2i selection_begin, selection_end;
249
if (selection_enabled) {
250
selection_begin = Point2i(text_editor->get_selection_from_line(0), text_editor->get_selection_from_column(0));
251
selection_end = Point2i(text_editor->get_selection_to_line(0), text_editor->get_selection_to_column(0));
252
}
253
254
String repl_text = get_replace_text();
255
int search_text_len = get_search_text().length();
256
257
if (selection_enabled && is_selection_only()) {
258
// Restrict search_current() to selected region.
259
text_editor->set_caret_line(selection_begin.width, false, true, -1, 0);
260
text_editor->set_caret_column(selection_begin.height, true, 0);
261
}
262
263
if (search_current()) {
264
text_editor->unfold_line(result_line);
265
text_editor->select(result_line, result_col, result_line, result_col + search_text_len, 0);
266
267
if (selection_enabled && is_selection_only()) {
268
Point2i match_from(result_line, result_col);
269
Point2i match_to(result_line, result_col + search_text_len);
270
if (!(match_from < selection_begin || match_to > selection_end)) {
271
text_editor->insert_text_at_caret(repl_text, 0);
272
if (match_to.x == selection_end.x) {
273
// Adjust selection bounds if necessary.
274
selection_end.y += repl_text.length() - search_text_len;
275
}
276
}
277
} else {
278
text_editor->insert_text_at_caret(repl_text, 0);
279
}
280
}
281
text_editor->end_complex_operation();
282
results_count = -1;
283
results_count_to_current = -1;
284
needs_to_count_results = true;
285
286
if (selection_enabled && is_selection_only()) {
287
// Reselect in order to keep 'Replace' restricted to selection.
288
text_editor->select(selection_begin.x, selection_begin.y, selection_end.x, selection_end.y, 0);
289
} else {
290
text_editor->deselect(0);
291
}
292
}
293
294
void FindReplaceBar::_replace_all() {
295
text_editor->begin_complex_operation();
296
text_editor->remove_secondary_carets();
297
text_editor->disconnect(SceneStringName(text_changed), callable_mp(this, &FindReplaceBar::_editor_text_changed));
298
// Line as x so it gets priority in comparison, column as y.
299
Point2i orig_cursor(text_editor->get_caret_line(0), text_editor->get_caret_column(0));
300
Point2i prev_match = Point2(-1, -1);
301
302
bool selection_enabled = text_editor->has_selection(0);
303
if (!is_selection_only()) {
304
text_editor->deselect();
305
selection_enabled = false;
306
} else {
307
result_line = -1;
308
result_col = -1;
309
}
310
311
Point2i selection_begin, selection_end;
312
if (selection_enabled) {
313
selection_begin = Point2i(text_editor->get_selection_from_line(0), text_editor->get_selection_from_column(0));
314
selection_end = Point2i(text_editor->get_selection_to_line(0), text_editor->get_selection_to_column(0));
315
}
316
317
int vsval = text_editor->get_v_scroll();
318
319
String repl_text = get_replace_text();
320
int search_text_len = get_search_text().length();
321
322
int rc = 0;
323
324
replace_all_mode = true;
325
326
if (selection_enabled && is_selection_only()) {
327
text_editor->set_caret_line(selection_begin.width, false, true, -1, 0);
328
text_editor->set_caret_column(selection_begin.height, true, 0);
329
} else {
330
text_editor->set_caret_line(0, false, true, -1, 0);
331
text_editor->set_caret_column(0, true, 0);
332
}
333
334
if (search_current()) {
335
do {
336
// Replace area.
337
Point2i match_from(result_line, result_col);
338
Point2i match_to(result_line, result_col + search_text_len);
339
340
if (match_from < prev_match) {
341
break; // Done.
342
}
343
344
prev_match = Point2i(result_line, result_col + repl_text.length());
345
346
text_editor->unfold_line(result_line);
347
text_editor->select(result_line, result_col, result_line, match_to.y, 0);
348
349
if (selection_enabled) {
350
if (match_from < selection_begin || match_to > selection_end) {
351
break; // Done.
352
}
353
354
// Replace but adjust selection bounds.
355
text_editor->insert_text_at_caret(repl_text, 0);
356
if (match_to.x == selection_end.x) {
357
selection_end.y += repl_text.length() - search_text_len;
358
}
359
360
} else {
361
// Just replace.
362
text_editor->insert_text_at_caret(repl_text, 0);
363
}
364
365
rc++;
366
} while (search_next());
367
}
368
369
text_editor->end_complex_operation();
370
371
replace_all_mode = false;
372
373
// Restore editor state (selection, cursor, scroll).
374
text_editor->set_caret_line(orig_cursor.x, false, true, 0, 0);
375
text_editor->set_caret_column(orig_cursor.y, true, 0);
376
377
if (selection_enabled) {
378
// Reselect.
379
text_editor->select(selection_begin.x, selection_begin.y, selection_end.x, selection_end.y, 0);
380
}
381
382
text_editor->set_v_scroll(vsval);
383
matches_label->add_theme_color_override(SceneStringName(font_color), rc > 0 ? get_theme_color(SceneStringName(font_color), SNAME("Label")) : get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
384
matches_label->set_text(vformat(TTR("%d replaced."), rc));
385
386
callable_mp((Object *)text_editor, &Object::connect).call_deferred(SceneStringName(text_changed), callable_mp(this, &FindReplaceBar::_editor_text_changed), 0U);
387
results_count = -1;
388
results_count_to_current = -1;
389
needs_to_count_results = true;
390
}
391
392
void FindReplaceBar::_get_search_from(int &r_line, int &r_col, SearchMode p_search_mode) {
393
if (!text_editor->has_selection(0) || is_selection_only()) {
394
r_line = text_editor->get_caret_line(0);
395
r_col = text_editor->get_caret_column(0);
396
397
if (p_search_mode == SEARCH_PREV && r_line == result_line && r_col >= result_col && r_col <= result_col + get_search_text().length()) {
398
r_col = result_col;
399
}
400
return;
401
}
402
403
if (p_search_mode == SEARCH_NEXT) {
404
r_line = text_editor->get_selection_to_line();
405
r_col = text_editor->get_selection_to_column();
406
} else {
407
r_line = text_editor->get_selection_from_line();
408
r_col = text_editor->get_selection_from_column();
409
}
410
}
411
412
void FindReplaceBar::_update_results_count() {
413
int caret_line, caret_column;
414
_get_search_from(caret_line, caret_column, SEARCH_CURRENT);
415
bool match_selected = caret_line == result_line && caret_column == result_col && !is_selection_only() && text_editor->has_selection(0);
416
417
if (match_selected && !needs_to_count_results && result_line != -1 && results_count_to_current > 0) {
418
results_count_to_current += (flags & TextEdit::SEARCH_BACKWARDS) ? -1 : 1;
419
420
if (results_count_to_current > results_count) {
421
results_count_to_current = results_count_to_current - results_count;
422
} else if (results_count_to_current <= 0) {
423
results_count_to_current = results_count;
424
}
425
426
return;
427
}
428
429
String searched = get_search_text();
430
if (searched.is_empty()) {
431
return;
432
}
433
434
needs_to_count_results = !match_selected;
435
436
results_count = 0;
437
results_count_to_current = 0;
438
439
for (int i = 0; i < text_editor->get_line_count(); i++) {
440
String line_text = text_editor->get_line(i);
441
442
int col_pos = 0;
443
444
bool searched_start_is_symbol = is_symbol(searched[0]);
445
bool searched_end_is_symbol = is_symbol(searched[searched.length() - 1]);
446
447
while (true) {
448
col_pos = is_case_sensitive() ? line_text.find(searched, col_pos) : line_text.findn(searched, col_pos);
449
450
if (col_pos == -1) {
451
break;
452
}
453
454
if (is_whole_words()) {
455
if (!searched_start_is_symbol && col_pos > 0 && !is_symbol(line_text[col_pos - 1])) {
456
col_pos += searched.length();
457
continue;
458
}
459
if (!searched_end_is_symbol && col_pos + searched.length() < line_text.length() && !is_symbol(line_text[col_pos + searched.length()])) {
460
col_pos += searched.length();
461
continue;
462
}
463
}
464
465
results_count++;
466
467
if (i <= result_line && col_pos <= result_col) {
468
results_count_to_current = results_count;
469
}
470
if (i == result_line && col_pos < result_col && col_pos + searched.length() > result_col) {
471
// Searching forwards and backwards with repeating text can lead to different matches.
472
col_pos = result_col;
473
}
474
col_pos += searched.length();
475
}
476
}
477
if (!match_selected) {
478
// Current result should refer to the match before the caret, if the caret is not on a match.
479
if (caret_line != result_line || caret_column != result_col) {
480
results_count_to_current -= 1;
481
}
482
if (results_count_to_current == 0 && (caret_line > result_line || (caret_line == result_line && caret_column > result_col))) {
483
// Caret is after all matches.
484
results_count_to_current = results_count;
485
}
486
}
487
}
488
489
void FindReplaceBar::_update_matches_display() {
490
if (search_text->get_text().is_empty() || results_count == -1) {
491
matches_label->hide();
492
} else {
493
matches_label->show();
494
495
matches_label->add_theme_color_override(SceneStringName(font_color), results_count > 0 ? get_theme_color(SceneStringName(font_color), SNAME("Label")) : get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
496
497
if (results_count == 0) {
498
matches_label->set_text(TTR("No match"));
499
} else if (results_count_to_current == -1) {
500
matches_label->set_text(vformat(TTRN("%d match", "%d matches", results_count), results_count));
501
} else {
502
matches_label->set_text(vformat(TTRN("%d of %d match", "%d of %d matches", results_count), results_count_to_current, results_count));
503
}
504
}
505
find_prev->set_disabled(results_count < 1);
506
find_next->set_disabled(results_count < 1);
507
replace->set_disabled(search_text->get_text().is_empty());
508
replace_all->set_disabled(search_text->get_text().is_empty());
509
}
510
511
bool FindReplaceBar::search_current() {
512
_update_flags(false);
513
514
int line, col;
515
_get_search_from(line, col, SEARCH_CURRENT);
516
517
return _search(flags, line, col);
518
}
519
520
bool FindReplaceBar::search_prev() {
521
if (is_selection_only() && !replace_all_mode) {
522
return false;
523
}
524
525
if (!is_visible()) {
526
popup_search(true);
527
}
528
529
String text = get_search_text();
530
531
if ((flags & TextEdit::SEARCH_BACKWARDS) == 0) {
532
needs_to_count_results = true;
533
}
534
535
_update_flags(true);
536
537
int line, col;
538
_get_search_from(line, col, SEARCH_PREV);
539
540
col -= text.length();
541
if (col < 0) {
542
line -= 1;
543
if (line < 0) {
544
line = text_editor->get_line_count() - 1;
545
}
546
col = text_editor->get_line(line).length();
547
}
548
549
return _search(flags, line, col);
550
}
551
552
bool FindReplaceBar::search_next() {
553
if (is_selection_only() && !replace_all_mode) {
554
return false;
555
}
556
557
if (!is_visible()) {
558
popup_search(true);
559
}
560
561
if (flags & TextEdit::SEARCH_BACKWARDS) {
562
needs_to_count_results = true;
563
}
564
565
_update_flags(false);
566
567
int line, col;
568
_get_search_from(line, col, SEARCH_NEXT);
569
570
return _search(flags, line, col);
571
}
572
573
void FindReplaceBar::_hide_bar() {
574
if (replace_text->has_focus() || search_text->has_focus()) {
575
text_editor->grab_focus();
576
}
577
578
text_editor->set_search_text("");
579
result_line = -1;
580
result_col = -1;
581
hide();
582
}
583
584
void FindReplaceBar::_update_toggle_replace_button(bool p_replace_visible) {
585
String tooltip = p_replace_visible ? TTRC("Hide Replace") : TTRC("Show Replace");
586
String shortcut = ED_GET_SHORTCUT(p_replace_visible ? "script_text_editor/find" : "script_text_editor/replace")->get_as_text();
587
toggle_replace_button->set_tooltip_text(vformat("%s (%s)", tooltip, shortcut));
588
StringName rtl_compliant_arrow = is_layout_rtl() ? SNAME("GuiTreeArrowLeft") : SNAME("GuiTreeArrowRight");
589
toggle_replace_button->set_button_icon(get_editor_theme_icon(p_replace_visible ? SNAME("GuiTreeArrowDown") : rtl_compliant_arrow));
590
}
591
592
void FindReplaceBar::_show_search(bool p_with_replace, bool p_show_only) {
593
show();
594
if (p_show_only) {
595
return;
596
}
597
598
const bool on_one_line = text_editor->has_selection(0) && text_editor->get_selection_from_line(0) == text_editor->get_selection_to_line(0);
599
const bool focus_replace = p_with_replace && on_one_line;
600
601
if (focus_replace) {
602
search_text->deselect();
603
callable_mp((Control *)replace_text, &Control::grab_focus).call_deferred(false);
604
} else {
605
replace_text->deselect();
606
callable_mp((Control *)search_text, &Control::grab_focus).call_deferred(false);
607
}
608
609
if (on_one_line) {
610
search_text->set_text(text_editor->get_selected_text(0));
611
result_line = text_editor->get_selection_from_line();
612
result_col = text_editor->get_selection_from_column();
613
}
614
615
if (!get_search_text().is_empty()) {
616
if (focus_replace) {
617
replace_text->select_all();
618
replace_text->set_caret_column(replace_text->get_text().length());
619
} else {
620
search_text->select_all();
621
search_text->set_caret_column(search_text->get_text().length());
622
}
623
624
preserve_cursor = true;
625
_search_text_changed(get_search_text());
626
preserve_cursor = false;
627
}
628
}
629
630
void FindReplaceBar::popup_search(bool p_show_only) {
631
replace_text->hide();
632
hbc_button_replace->hide();
633
hbc_option_replace->hide();
634
selection_only->set_pressed(false);
635
_update_toggle_replace_button(false);
636
637
_show_search(false, p_show_only);
638
}
639
640
void FindReplaceBar::popup_replace() {
641
if (!replace_text->is_visible_in_tree()) {
642
replace_text->show();
643
hbc_button_replace->show();
644
hbc_option_replace->show();
645
_update_toggle_replace_button(true);
646
}
647
648
selection_only->set_pressed(text_editor->has_selection(0) && text_editor->get_selection_from_line(0) < text_editor->get_selection_to_line(0));
649
650
_show_search(true, false);
651
}
652
653
void FindReplaceBar::_search_options_changed(bool p_pressed) {
654
results_count = -1;
655
results_count_to_current = -1;
656
needs_to_count_results = true;
657
search_current();
658
}
659
660
void FindReplaceBar::_editor_text_changed() {
661
results_count = -1;
662
results_count_to_current = -1;
663
needs_to_count_results = true;
664
if (is_visible_in_tree()) {
665
preserve_cursor = true;
666
search_current();
667
preserve_cursor = false;
668
}
669
}
670
671
void FindReplaceBar::_search_text_changed(const String &p_text) {
672
results_count = -1;
673
results_count_to_current = -1;
674
needs_to_count_results = true;
675
search_current();
676
}
677
678
void FindReplaceBar::_search_text_submitted(const String &p_text) {
679
if (Input::get_singleton()->is_key_pressed(Key::SHIFT)) {
680
search_prev();
681
} else {
682
search_next();
683
}
684
}
685
686
void FindReplaceBar::_replace_text_submitted(const String &p_text) {
687
if (selection_only->is_pressed() && text_editor->has_selection(0)) {
688
_replace_all();
689
_hide_bar();
690
} else if (Input::get_singleton()->is_key_pressed(Key::SHIFT)) {
691
_replace();
692
search_prev();
693
} else {
694
_replace();
695
search_next();
696
}
697
}
698
699
void FindReplaceBar::_toggle_replace_pressed() {
700
bool replace_visible = replace_text->is_visible_in_tree();
701
replace_visible ? popup_search(true) : popup_replace();
702
}
703
704
String FindReplaceBar::get_search_text() const {
705
return search_text->get_text();
706
}
707
708
String FindReplaceBar::get_replace_text() const {
709
return replace_text->get_text();
710
}
711
712
bool FindReplaceBar::is_case_sensitive() const {
713
return case_sensitive->is_pressed();
714
}
715
716
bool FindReplaceBar::is_whole_words() const {
717
return whole_words->is_pressed();
718
}
719
720
bool FindReplaceBar::is_selection_only() const {
721
return selection_only->is_pressed();
722
}
723
724
void FindReplaceBar::set_text_edit(CodeTextEditor *p_text_editor) {
725
if (p_text_editor == base_text_editor) {
726
return;
727
}
728
729
if (base_text_editor) {
730
text_editor->set_search_text(String());
731
base_text_editor->remove_find_replace_bar();
732
base_text_editor = nullptr;
733
text_editor->disconnect(SceneStringName(text_changed), callable_mp(this, &FindReplaceBar::_editor_text_changed));
734
text_editor = nullptr;
735
}
736
737
if (!p_text_editor) {
738
return;
739
}
740
741
results_count = -1;
742
results_count_to_current = -1;
743
needs_to_count_results = true;
744
base_text_editor = p_text_editor;
745
text_editor = base_text_editor->get_text_editor();
746
text_editor->connect(SceneStringName(text_changed), callable_mp(this, &FindReplaceBar::_editor_text_changed));
747
748
_editor_text_changed();
749
}
750
751
void FindReplaceBar::_bind_methods() {
752
ClassDB::bind_method("_search_current", &FindReplaceBar::search_current);
753
}
754
755
FindReplaceBar::FindReplaceBar() {
756
toggle_replace_button = memnew(Button);
757
toggle_replace_button->set_theme_type_variation(SceneStringName(FlatButton));
758
add_child(toggle_replace_button);
759
toggle_replace_button->set_accessibility_name(TTRC("Replace Mode"));
760
toggle_replace_button->set_focus_mode(FOCUS_ACCESSIBILITY);
761
toggle_replace_button->connect(SceneStringName(pressed), callable_mp(this, &FindReplaceBar::_toggle_replace_pressed));
762
763
VBoxContainer *vbc_lineedit = memnew(VBoxContainer);
764
add_child(vbc_lineedit);
765
vbc_lineedit->set_alignment(BoxContainer::ALIGNMENT_CENTER);
766
vbc_lineedit->set_h_size_flags(SIZE_EXPAND_FILL);
767
VBoxContainer *vbc_button = memnew(VBoxContainer);
768
add_child(vbc_button);
769
VBoxContainer *vbc_option = memnew(VBoxContainer);
770
add_child(vbc_option);
771
772
HBoxContainer *hbc_button_search = memnew(HBoxContainer);
773
hbc_button_search->set_v_size_flags(SIZE_EXPAND_FILL);
774
hbc_button_search->set_alignment(BoxContainer::ALIGNMENT_END);
775
vbc_button->add_child(hbc_button_search);
776
hbc_button_replace = memnew(HBoxContainer);
777
hbc_button_replace->set_v_size_flags(SIZE_EXPAND_FILL);
778
hbc_button_replace->set_alignment(BoxContainer::ALIGNMENT_END);
779
vbc_button->add_child(hbc_button_replace);
780
781
HBoxContainer *hbc_option_search = memnew(HBoxContainer);
782
vbc_option->add_child(hbc_option_search);
783
hbc_option_replace = memnew(HBoxContainer);
784
vbc_option->add_child(hbc_option_replace);
785
786
// Search toolbar.
787
search_text = memnew(LineEdit);
788
search_text->set_keep_editing_on_text_submit(true);
789
vbc_lineedit->add_child(search_text);
790
search_text->set_placeholder(TTRC("Find"));
791
search_text->set_tooltip_text(TTRC("Find"));
792
search_text->set_accessibility_name(TTRC("Find"));
793
search_text->set_custom_minimum_size(Size2(100 * EDSCALE, 0));
794
search_text->connect(SceneStringName(text_changed), callable_mp(this, &FindReplaceBar::_search_text_changed));
795
search_text->connect(SceneStringName(text_submitted), callable_mp(this, &FindReplaceBar::_search_text_submitted));
796
797
matches_label = memnew(Label);
798
hbc_button_search->add_child(matches_label);
799
matches_label->set_focus_mode(FOCUS_ACCESSIBILITY);
800
matches_label->hide();
801
802
find_prev = memnew(Button);
803
find_prev->set_theme_type_variation(SceneStringName(FlatButton));
804
find_prev->set_disabled(results_count < 1);
805
find_prev->set_tooltip_text(TTRC("Previous Match"));
806
hbc_button_search->add_child(find_prev);
807
find_prev->set_focus_mode(FOCUS_ACCESSIBILITY);
808
find_prev->connect(SceneStringName(pressed), callable_mp(this, &FindReplaceBar::search_prev));
809
810
find_next = memnew(Button);
811
find_next->set_theme_type_variation(SceneStringName(FlatButton));
812
find_next->set_disabled(results_count < 1);
813
find_next->set_tooltip_text(TTRC("Next Match"));
814
hbc_button_search->add_child(find_next);
815
find_next->set_focus_mode(FOCUS_ACCESSIBILITY);
816
find_next->connect(SceneStringName(pressed), callable_mp(this, &FindReplaceBar::search_next));
817
818
case_sensitive = memnew(CheckBox);
819
hbc_option_search->add_child(case_sensitive);
820
case_sensitive->set_text(TTRC("Match Case"));
821
case_sensitive->set_focus_mode(FOCUS_ACCESSIBILITY);
822
case_sensitive->connect(SceneStringName(toggled), callable_mp(this, &FindReplaceBar::_search_options_changed));
823
824
whole_words = memnew(CheckBox);
825
hbc_option_search->add_child(whole_words);
826
whole_words->set_text(TTRC("Whole Words"));
827
whole_words->set_focus_mode(FOCUS_ACCESSIBILITY);
828
whole_words->connect(SceneStringName(toggled), callable_mp(this, &FindReplaceBar::_search_options_changed));
829
830
// Replace toolbar.
831
replace_text = memnew(LineEdit);
832
vbc_lineedit->add_child(replace_text);
833
replace_text->set_placeholder(TTRC("Replace"));
834
replace_text->set_tooltip_text(TTRC("Replace"));
835
replace_text->set_accessibility_name(TTRC("Replace"));
836
replace_text->set_custom_minimum_size(Size2(100 * EDSCALE, 0));
837
replace_text->connect(SceneStringName(text_submitted), callable_mp(this, &FindReplaceBar::_replace_text_submitted));
838
839
replace = memnew(Button);
840
hbc_button_replace->add_child(replace);
841
replace->set_text(TTRC("Replace"));
842
replace->connect(SceneStringName(pressed), callable_mp(this, &FindReplaceBar::_replace));
843
844
replace_all = memnew(Button);
845
hbc_button_replace->add_child(replace_all);
846
replace_all->set_text(TTRC("Replace All"));
847
replace_all->connect(SceneStringName(pressed), callable_mp(this, &FindReplaceBar::_replace_all));
848
849
selection_only = memnew(CheckBox);
850
hbc_option_replace->add_child(selection_only);
851
selection_only->set_text(TTRC("Selection Only"));
852
selection_only->set_focus_mode(FOCUS_ACCESSIBILITY);
853
selection_only->connect(SceneStringName(toggled), callable_mp(this, &FindReplaceBar::_search_options_changed));
854
855
hide_button = memnew(Button);
856
hide_button->set_theme_type_variation(SceneStringName(FlatButton));
857
hide_button->set_tooltip_text(TTRC("Hide"));
858
hide_button->set_focus_mode(FOCUS_ACCESSIBILITY);
859
hide_button->connect(SceneStringName(pressed), callable_mp(this, &FindReplaceBar::_hide_bar));
860
hide_button->set_v_size_flags(SIZE_SHRINK_CENTER);
861
add_child(hide_button);
862
}
863
864
/*** CODE EDITOR ****/
865
866
static constexpr float ZOOM_FACTOR_PRESETS[8] = { 0.5f, 0.75f, 0.9f, 1.0f, 1.1f, 1.25f, 1.5f, 2.0f };
867
868
// This function should be used to handle shortcuts that could otherwise
869
// be handled too late if they weren't handled here.
870
void CodeTextEditor::input(const Ref<InputEvent> &event) {
871
ERR_FAIL_COND(event.is_null());
872
873
const Ref<InputEventKey> key_event = event;
874
875
if (key_event.is_null()) {
876
return;
877
}
878
if (!key_event->is_pressed()) {
879
return;
880
}
881
882
if (!text_editor->has_focus()) {
883
if ((find_replace_bar != nullptr && find_replace_bar->is_visible()) && (find_replace_bar->has_focus() || (get_viewport()->gui_get_focus_owner() && find_replace_bar->is_ancestor_of(get_viewport()->gui_get_focus_owner())))) {
884
if (ED_IS_SHORTCUT("script_text_editor/find_next", key_event)) {
885
find_replace_bar->search_next();
886
accept_event();
887
return;
888
}
889
if (ED_IS_SHORTCUT("script_text_editor/find_previous", key_event)) {
890
find_replace_bar->search_prev();
891
accept_event();
892
return;
893
}
894
}
895
return;
896
}
897
898
if (ED_IS_SHORTCUT("script_text_editor/move_up", key_event)) {
899
text_editor->move_lines_up();
900
accept_event();
901
return;
902
}
903
if (ED_IS_SHORTCUT("script_text_editor/move_down", key_event)) {
904
text_editor->move_lines_down();
905
accept_event();
906
return;
907
}
908
if (ED_IS_SHORTCUT("script_text_editor/delete_line", key_event)) {
909
text_editor->delete_lines();
910
accept_event();
911
return;
912
}
913
if (ED_IS_SHORTCUT("script_text_editor/duplicate_selection", key_event)) {
914
text_editor->duplicate_selection();
915
accept_event();
916
return;
917
}
918
if (ED_IS_SHORTCUT("script_text_editor/duplicate_lines", key_event)) {
919
text_editor->duplicate_lines();
920
accept_event();
921
return;
922
}
923
}
924
925
void CodeTextEditor::_text_editor_gui_input(const Ref<InputEvent> &p_event) {
926
Ref<InputEventMouseButton> mb = p_event;
927
928
if (mb.is_valid()) {
929
if (mb->is_pressed() && mb->is_command_or_control_pressed()) {
930
if (mb->get_button_index() == MouseButton::WHEEL_UP) {
931
_zoom_in();
932
accept_event();
933
return;
934
}
935
if (mb->get_button_index() == MouseButton::WHEEL_DOWN) {
936
_zoom_out();
937
accept_event();
938
return;
939
}
940
}
941
}
942
943
#ifndef ANDROID_ENABLED
944
Ref<InputEventMagnifyGesture> magnify_gesture = p_event;
945
if (magnify_gesture.is_valid()) {
946
_zoom_to(zoom_factor * std::pow(magnify_gesture->get_factor(), 0.25f));
947
accept_event();
948
return;
949
}
950
#endif
951
952
Ref<InputEventKey> k = p_event;
953
954
if (k.is_valid()) {
955
if (k->is_pressed()) {
956
if (ED_IS_SHORTCUT("script_editor/zoom_in", p_event)) {
957
_zoom_in();
958
accept_event();
959
return;
960
}
961
if (ED_IS_SHORTCUT("script_editor/zoom_out", p_event)) {
962
_zoom_out();
963
accept_event();
964
return;
965
}
966
if (ED_IS_SHORTCUT("script_editor/reset_zoom", p_event)) {
967
_zoom_to(1);
968
accept_event();
969
return;
970
}
971
}
972
}
973
}
974
975
void CodeTextEditor::_line_col_changed() {
976
if (!code_complete_timer->is_stopped() && code_complete_timer_line != text_editor->get_caret_line()) {
977
code_complete_timer->stop();
978
}
979
980
String line = text_editor->get_line(text_editor->get_caret_line());
981
982
int positional_column = 0;
983
for (int i = 0; i < text_editor->get_caret_column(); i++) {
984
if (line[i] == '\t') {
985
positional_column += text_editor->get_indent_size(); // Tab size
986
} else {
987
positional_column += 1;
988
}
989
}
990
991
StringBuilder sb;
992
sb.append(itos(text_editor->get_caret_line() + 1).lpad(4));
993
sb.append(" : ");
994
sb.append(itos(positional_column + 1).lpad(3));
995
996
line_and_col_txt->set_text(sb.as_string());
997
998
if (find_replace_bar) {
999
if (!find_replace_bar->line_col_changed_for_result) {
1000
find_replace_bar->needs_to_count_results = true;
1001
}
1002
1003
find_replace_bar->line_col_changed_for_result = false;
1004
}
1005
}
1006
1007
void CodeTextEditor::_text_changed() {
1008
if (code_complete_enabled && text_editor->is_insert_text_operation()) {
1009
code_complete_timer_line = text_editor->get_caret_line();
1010
code_complete_timer->start();
1011
}
1012
1013
idle->start();
1014
1015
if (find_replace_bar) {
1016
find_replace_bar->needs_to_count_results = true;
1017
}
1018
}
1019
1020
void CodeTextEditor::_code_complete_timer_timeout() {
1021
if (!is_visible_in_tree()) {
1022
return;
1023
}
1024
text_editor->request_code_completion();
1025
}
1026
1027
void CodeTextEditor::_complete_request() {
1028
List<ScriptLanguage::CodeCompletionOption> entries;
1029
String ctext = text_editor->get_text_for_code_completion();
1030
_code_complete_script(ctext, &entries);
1031
bool forced = false;
1032
if (code_complete_func) {
1033
code_complete_func(code_complete_ud, ctext, &entries, forced);
1034
}
1035
1036
for (const ScriptLanguage::CodeCompletionOption &e : entries) {
1037
Color font_color = completion_font_color;
1038
if (!e.theme_color_name.is_empty() && EDITOR_GET("text_editor/completion/colorize_suggestions")) {
1039
font_color = get_theme_color(e.theme_color_name, SNAME("Editor"));
1040
} else if (e.insert_text.begins_with("\"") || e.insert_text.begins_with("\'")) {
1041
font_color = completion_string_color;
1042
} else if (e.insert_text.begins_with("##") || e.insert_text.begins_with("///")) {
1043
font_color = completion_doc_comment_color;
1044
} else if (e.insert_text.begins_with("&")) {
1045
font_color = completion_string_name_color;
1046
} else if (e.insert_text.begins_with("^")) {
1047
font_color = completion_node_path_color;
1048
} else if (e.insert_text.begins_with("#") || e.insert_text.begins_with("//")) {
1049
font_color = completion_comment_color;
1050
}
1051
text_editor->add_code_completion_option((CodeEdit::CodeCompletionKind)e.kind, e.display, e.insert_text, font_color, _get_completion_icon(e), e.default_value, e.location);
1052
}
1053
text_editor->update_code_completion_options(forced);
1054
}
1055
1056
Ref<Texture2D> CodeTextEditor::_get_completion_icon(const ScriptLanguage::CodeCompletionOption &p_option) {
1057
Ref<Texture2D> tex;
1058
switch (p_option.kind) {
1059
case ScriptLanguage::CODE_COMPLETION_KIND_CLASS: {
1060
if (has_theme_icon(p_option.display, EditorStringName(EditorIcons))) {
1061
tex = get_editor_theme_icon(p_option.display);
1062
} else {
1063
tex = EditorNode::get_singleton()->get_class_icon(p_option.display);
1064
if (tex.is_null()) {
1065
tex = get_editor_theme_icon(SNAME("Object"));
1066
}
1067
}
1068
} break;
1069
case ScriptLanguage::CODE_COMPLETION_KIND_ENUM:
1070
tex = get_editor_theme_icon(SNAME("Enum"));
1071
break;
1072
case ScriptLanguage::CODE_COMPLETION_KIND_FILE_PATH:
1073
tex = get_editor_theme_icon(SNAME("File"));
1074
break;
1075
case ScriptLanguage::CODE_COMPLETION_KIND_NODE_PATH:
1076
tex = get_editor_theme_icon(SNAME("NodePath"));
1077
break;
1078
case ScriptLanguage::CODE_COMPLETION_KIND_VARIABLE:
1079
tex = get_editor_theme_icon(SNAME("LocalVariable"));
1080
break;
1081
case ScriptLanguage::CODE_COMPLETION_KIND_CONSTANT:
1082
tex = get_editor_theme_icon(SNAME("MemberConstant"));
1083
break;
1084
case ScriptLanguage::CODE_COMPLETION_KIND_MEMBER:
1085
tex = get_editor_theme_icon(SNAME("MemberProperty"));
1086
break;
1087
case ScriptLanguage::CODE_COMPLETION_KIND_SIGNAL:
1088
tex = get_editor_theme_icon(SNAME("MemberSignal"));
1089
break;
1090
case ScriptLanguage::CODE_COMPLETION_KIND_FUNCTION:
1091
tex = get_editor_theme_icon(SNAME("MemberMethod"));
1092
break;
1093
case ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT:
1094
tex = get_editor_theme_icon(SNAME("BoxMesh"));
1095
break;
1096
default:
1097
tex = get_editor_theme_icon(SNAME("String"));
1098
break;
1099
}
1100
return tex;
1101
}
1102
1103
void CodeTextEditor::update_editor_settings() {
1104
// Theme: Highlighting
1105
completion_font_color = EDITOR_GET("text_editor/theme/highlighting/completion_font_color");
1106
completion_string_color = EDITOR_GET("text_editor/theme/highlighting/string_color");
1107
completion_string_name_color = EDITOR_GET("text_editor/theme/highlighting/gdscript/string_name_color");
1108
completion_node_path_color = EDITOR_GET("text_editor/theme/highlighting/gdscript/node_path_color");
1109
completion_comment_color = EDITOR_GET("text_editor/theme/highlighting/comment_color");
1110
completion_doc_comment_color = EDITOR_GET("text_editor/theme/highlighting/doc_comment_color");
1111
1112
// Appearance: Caret
1113
text_editor->set_caret_type((TextEdit::CaretType)EDITOR_GET("text_editor/appearance/caret/type").operator int());
1114
text_editor->set_caret_blink_enabled(EDITOR_GET("text_editor/appearance/caret/caret_blink"));
1115
text_editor->set_caret_blink_interval(EDITOR_GET("text_editor/appearance/caret/caret_blink_interval"));
1116
text_editor->set_highlight_current_line(EDITOR_GET("text_editor/appearance/caret/highlight_current_line"));
1117
text_editor->set_highlight_all_occurrences(EDITOR_GET("text_editor/appearance/caret/highlight_all_occurrences"));
1118
1119
// Appearance: Gutters
1120
text_editor->set_draw_line_numbers(EDITOR_GET("text_editor/appearance/gutters/show_line_numbers"));
1121
text_editor->set_line_numbers_zero_padded(EDITOR_GET("text_editor/appearance/gutters/line_numbers_zero_padded"));
1122
1123
// Appearance: Minimap
1124
text_editor->set_draw_minimap(EDITOR_GET("text_editor/appearance/minimap/show_minimap"));
1125
text_editor->set_minimap_width((int)EDITOR_GET("text_editor/appearance/minimap/minimap_width") * EDSCALE);
1126
1127
// Appearance: Lines
1128
text_editor->set_line_folding_enabled(EDITOR_GET("text_editor/appearance/lines/code_folding"));
1129
text_editor->set_draw_fold_gutter(EDITOR_GET("text_editor/appearance/lines/code_folding"));
1130
text_editor->set_line_wrapping_mode((TextEdit::LineWrappingMode)EDITOR_GET("text_editor/appearance/lines/word_wrap").operator int());
1131
text_editor->set_autowrap_mode((TextServer::AutowrapMode)EDITOR_GET("text_editor/appearance/lines/autowrap_mode").operator int());
1132
1133
// Appearance: Whitespace
1134
text_editor->set_draw_tabs(EDITOR_GET("text_editor/appearance/whitespace/draw_tabs"));
1135
text_editor->set_draw_spaces(EDITOR_GET("text_editor/appearance/whitespace/draw_spaces"));
1136
text_editor->add_theme_constant_override("line_spacing", EDITOR_GET("text_editor/appearance/whitespace/line_spacing"));
1137
1138
// Behavior: General
1139
text_editor->set_empty_selection_clipboard_enabled(EDITOR_GET("text_editor/behavior/general/empty_selection_clipboard"));
1140
1141
// Behavior: Navigation
1142
text_editor->set_scroll_past_end_of_file_enabled(EDITOR_GET("text_editor/behavior/navigation/scroll_past_end_of_file"));
1143
text_editor->set_smooth_scroll_enabled(EDITOR_GET("text_editor/behavior/navigation/smooth_scrolling"));
1144
text_editor->set_v_scroll_speed(EDITOR_GET("text_editor/behavior/navigation/v_scroll_speed"));
1145
text_editor->set_drag_and_drop_selection_enabled(EDITOR_GET("text_editor/behavior/navigation/drag_and_drop_selection"));
1146
text_editor->set_use_default_word_separators(EDITOR_GET("text_editor/behavior/navigation/use_default_word_separators"));
1147
text_editor->set_use_custom_word_separators(EDITOR_GET("text_editor/behavior/navigation/use_custom_word_separators"));
1148
text_editor->set_custom_word_separators(EDITOR_GET("text_editor/behavior/navigation/custom_word_separators"));
1149
1150
// Behavior: Indent
1151
set_indent_using_spaces(EDITOR_GET("text_editor/behavior/indent/type"));
1152
text_editor->set_indent_size(EDITOR_GET("text_editor/behavior/indent/size"));
1153
text_editor->set_auto_indent_enabled(EDITOR_GET("text_editor/behavior/indent/auto_indent"));
1154
text_editor->set_indent_wrapped_lines(EDITOR_GET("text_editor/behavior/indent/indent_wrapped_lines"));
1155
1156
// Completion
1157
text_editor->set_auto_brace_completion_enabled(EDITOR_GET("text_editor/completion/auto_brace_complete"));
1158
text_editor->set_code_hint_draw_below(EDITOR_GET("text_editor/completion/put_callhint_tooltip_below_current_line"));
1159
code_complete_enabled = EDITOR_GET("text_editor/completion/code_complete_enabled");
1160
code_complete_timer->set_wait_time(EDITOR_GET("text_editor/completion/code_complete_delay"));
1161
idle_time = EDITOR_GET("text_editor/completion/idle_parse_delay");
1162
idle_time_with_errors = EDITOR_GET("text_editor/completion/idle_parse_delay_with_errors_found");
1163
1164
// Appearance: Guidelines
1165
if (EDITOR_GET("text_editor/appearance/guidelines/show_line_length_guidelines")) {
1166
TypedArray<int> guideline_cols;
1167
guideline_cols.append(EDITOR_GET("text_editor/appearance/guidelines/line_length_guideline_hard_column"));
1168
if (EDITOR_GET("text_editor/appearance/guidelines/line_length_guideline_soft_column") != guideline_cols[0]) {
1169
guideline_cols.append(EDITOR_GET("text_editor/appearance/guidelines/line_length_guideline_soft_column"));
1170
}
1171
text_editor->set_line_length_guidelines(guideline_cols);
1172
} else {
1173
text_editor->set_line_length_guidelines(TypedArray<int>());
1174
}
1175
1176
set_zoom_factor(zoom_factor);
1177
}
1178
1179
void CodeTextEditor::set_find_replace_bar(FindReplaceBar *p_bar) {
1180
if (find_replace_bar) {
1181
return;
1182
}
1183
1184
find_replace_bar = p_bar;
1185
find_replace_bar->set_text_edit(this);
1186
}
1187
1188
void CodeTextEditor::remove_find_replace_bar() {
1189
if (!find_replace_bar) {
1190
return;
1191
}
1192
1193
find_replace_bar = nullptr;
1194
}
1195
1196
void CodeTextEditor::trim_trailing_whitespace() {
1197
bool trimmed_whitespace = false;
1198
for (int i = 0; i < text_editor->get_line_count(); i++) {
1199
String line = text_editor->get_line(i);
1200
if (line.ends_with(" ") || line.ends_with("\t")) {
1201
if (!trimmed_whitespace) {
1202
text_editor->begin_complex_operation();
1203
trimmed_whitespace = true;
1204
}
1205
1206
int end = 0;
1207
for (int j = line.length() - 1; j > -1; j--) {
1208
if (line[j] != ' ' && line[j] != '\t') {
1209
end = j + 1;
1210
break;
1211
}
1212
}
1213
text_editor->remove_text(i, end, i, line.length());
1214
}
1215
}
1216
1217
if (trimmed_whitespace) {
1218
text_editor->merge_overlapping_carets();
1219
text_editor->end_complex_operation();
1220
}
1221
}
1222
1223
void CodeTextEditor::trim_final_newlines() {
1224
int final_line = text_editor->get_line_count() - 1;
1225
int check_line = final_line;
1226
1227
String line = text_editor->get_line(check_line);
1228
1229
while (line.is_empty() && check_line > -1) {
1230
--check_line;
1231
1232
line = text_editor->get_line(check_line);
1233
}
1234
1235
++check_line;
1236
1237
if (check_line < final_line) {
1238
text_editor->begin_complex_operation();
1239
1240
text_editor->remove_text(check_line, 0, final_line, 0);
1241
1242
text_editor->merge_overlapping_carets();
1243
text_editor->end_complex_operation();
1244
text_editor->queue_redraw();
1245
}
1246
}
1247
1248
void CodeTextEditor::insert_final_newline() {
1249
int final_line = text_editor->get_line_count() - 1;
1250
String line = text_editor->get_line(final_line);
1251
1252
// Length 0 means it's already an empty line, no need to add a newline.
1253
if (line.length() > 0 && !line.ends_with("\n")) {
1254
text_editor->insert_text("\n", final_line, line.length(), false);
1255
}
1256
}
1257
1258
void CodeTextEditor::convert_case(CaseStyle p_case) {
1259
if (!text_editor->has_selection()) {
1260
return;
1261
}
1262
text_editor->begin_complex_operation();
1263
text_editor->begin_multicaret_edit();
1264
1265
for (int c = 0; c < text_editor->get_caret_count(); c++) {
1266
if (text_editor->multicaret_edit_ignore_caret(c)) {
1267
continue;
1268
}
1269
if (!text_editor->has_selection(c)) {
1270
continue;
1271
}
1272
1273
int begin = text_editor->get_selection_from_line(c);
1274
int end = text_editor->get_selection_to_line(c);
1275
int begin_col = text_editor->get_selection_from_column(c);
1276
int end_col = text_editor->get_selection_to_column(c);
1277
1278
for (int i = begin; i <= end; i++) {
1279
int len = text_editor->get_line(i).length();
1280
if (i == end) {
1281
len = end_col;
1282
}
1283
if (i == begin) {
1284
len -= begin_col;
1285
}
1286
String new_line = text_editor->get_line(i).substr(i == begin ? begin_col : 0, len);
1287
1288
switch (p_case) {
1289
case UPPER: {
1290
new_line = new_line.to_upper();
1291
} break;
1292
case LOWER: {
1293
new_line = new_line.to_lower();
1294
} break;
1295
case CAPITALIZE: {
1296
new_line = new_line.capitalize();
1297
} break;
1298
}
1299
1300
if (i == begin) {
1301
new_line = text_editor->get_line(i).left(begin_col) + new_line;
1302
}
1303
if (i == end) {
1304
new_line = new_line + text_editor->get_line(i).substr(end_col);
1305
}
1306
text_editor->set_line(i, new_line);
1307
}
1308
}
1309
text_editor->end_multicaret_edit();
1310
text_editor->end_complex_operation();
1311
}
1312
1313
void CodeTextEditor::set_indent_using_spaces(bool p_use_spaces) {
1314
text_editor->set_indent_using_spaces(p_use_spaces);
1315
indentation_txt->set_text(p_use_spaces ? TTR("Spaces", "Indentation") : TTR("Tabs", "Indentation"));
1316
}
1317
1318
void CodeTextEditor::toggle_inline_comment(const String &delimiter) {
1319
text_editor->begin_complex_operation();
1320
text_editor->begin_multicaret_edit();
1321
1322
Vector<Point2i> line_ranges = text_editor->get_line_ranges_from_carets();
1323
int folded_to = 0;
1324
for (Point2i line_range : line_ranges) {
1325
int from_line = line_range.x;
1326
int to_line = line_range.y;
1327
// If last line is folded, extends to the end of the folded section
1328
if (text_editor->is_line_folded(to_line)) {
1329
folded_to = text_editor->get_next_visible_line_offset_from(to_line + 1, 1) - 1;
1330
to_line += folded_to;
1331
}
1332
// Check first if there's any uncommented lines in selection.
1333
bool is_commented = true;
1334
bool is_all_empty = true;
1335
for (int line = from_line; line <= to_line; line++) {
1336
// `+ delimiter.length()` here because comment delimiter is not actually `in comment` so we check first character after it
1337
int delimiter_idx = text_editor->is_in_comment(line, text_editor->get_first_non_whitespace_column(line) + delimiter.length());
1338
// Empty lines should not be counted.
1339
bool is_empty = text_editor->get_line(line).strip_edges().is_empty();
1340
is_all_empty = is_all_empty && is_empty;
1341
// get_delimiter_start_key will return `##` instead of `#` when there is multiple comment delimiter in a line.
1342
if (!is_empty && (delimiter_idx == -1 || !text_editor->get_delimiter_start_key(delimiter_idx).begins_with(delimiter))) {
1343
is_commented = false;
1344
break;
1345
}
1346
}
1347
1348
// Special case for commenting empty lines, treat it/them as uncommented lines.
1349
is_commented = is_commented && !is_all_empty;
1350
1351
// Comment/uncomment.
1352
for (int line = from_line; line <= to_line; line++) {
1353
if (is_all_empty) {
1354
text_editor->insert_text(delimiter, line, 0);
1355
continue;
1356
}
1357
1358
if (is_commented) {
1359
int delimiter_column = text_editor->get_line(line).find(delimiter);
1360
if (delimiter_column != -1) {
1361
text_editor->remove_text(line, delimiter_column, line, delimiter_column + delimiter.length());
1362
}
1363
} else {
1364
text_editor->insert_text(delimiter, line, text_editor->get_first_non_whitespace_column(line));
1365
}
1366
}
1367
}
1368
1369
text_editor->end_multicaret_edit();
1370
text_editor->end_complex_operation();
1371
}
1372
1373
void CodeTextEditor::goto_line(int p_line, int p_column) {
1374
text_editor->remove_secondary_carets();
1375
text_editor->deselect();
1376
text_editor->unfold_line(CLAMP(p_line, 0, text_editor->get_line_count() - 1));
1377
text_editor->set_caret_line(p_line, false);
1378
text_editor->set_caret_column(p_column, false);
1379
text_editor->set_code_hint("");
1380
text_editor->cancel_code_completion();
1381
// Defer in case the CodeEdit was just created and needs to be resized.
1382
callable_mp((TextEdit *)text_editor, &TextEdit::adjust_viewport_to_caret).call_deferred(0);
1383
}
1384
1385
void CodeTextEditor::goto_line_selection(int p_line, int p_begin, int p_end) {
1386
text_editor->remove_secondary_carets();
1387
text_editor->unfold_line(CLAMP(p_line, 0, text_editor->get_line_count() - 1));
1388
text_editor->select(p_line, p_begin, p_line, p_end);
1389
text_editor->set_code_hint("");
1390
text_editor->cancel_code_completion();
1391
callable_mp((TextEdit *)text_editor, &TextEdit::adjust_viewport_to_caret).call_deferred(0);
1392
}
1393
1394
void CodeTextEditor::goto_line_centered(int p_line, int p_column) {
1395
text_editor->remove_secondary_carets();
1396
text_editor->deselect();
1397
text_editor->unfold_line(CLAMP(p_line, 0, text_editor->get_line_count() - 1));
1398
text_editor->set_caret_line(p_line, false);
1399
text_editor->set_caret_column(p_column, false);
1400
text_editor->set_code_hint("");
1401
text_editor->cancel_code_completion();
1402
callable_mp((TextEdit *)text_editor, &TextEdit::center_viewport_to_caret).call_deferred(0);
1403
}
1404
1405
void CodeTextEditor::set_executing_line(int p_line) {
1406
text_editor->set_line_as_executing(p_line, true);
1407
}
1408
1409
void CodeTextEditor::clear_executing_line() {
1410
text_editor->clear_executing_lines();
1411
}
1412
1413
Variant CodeTextEditor::get_edit_state() {
1414
Dictionary state;
1415
state.merge(get_navigation_state());
1416
1417
state["folded_lines"] = text_editor->get_folded_lines();
1418
state["breakpoints"] = text_editor->get_breakpointed_lines();
1419
state["bookmarks"] = text_editor->get_bookmarked_lines();
1420
1421
Ref<EditorSyntaxHighlighter> syntax_highlighter = text_editor->get_syntax_highlighter();
1422
state["syntax_highlighter"] = syntax_highlighter->_get_name();
1423
1424
return state;
1425
}
1426
1427
Variant CodeTextEditor::get_previous_state() {
1428
return previous_state;
1429
}
1430
1431
void CodeTextEditor::store_previous_state() {
1432
previous_state = get_navigation_state();
1433
}
1434
1435
bool CodeTextEditor::is_previewing_navigation_change() const {
1436
return preview_navigation_change;
1437
}
1438
1439
void CodeTextEditor::set_preview_navigation_change(bool p_preview) {
1440
if (preview_navigation_change == p_preview) {
1441
return;
1442
}
1443
preview_navigation_change = p_preview;
1444
if (!preview_navigation_change) {
1445
emit_signal("navigation_preview_ended");
1446
}
1447
}
1448
1449
void CodeTextEditor::set_edit_state(const Variant &p_state) {
1450
Dictionary state = p_state;
1451
1452
/* update the row first as it sets the column to 0 */
1453
text_editor->set_caret_line(state["row"]);
1454
text_editor->set_caret_column(state["column"]);
1455
if (int(state["scroll_position"]) == -1) {
1456
// Special case for previous state.
1457
text_editor->center_viewport_to_caret();
1458
} else {
1459
text_editor->set_v_scroll(state["scroll_position"]);
1460
}
1461
text_editor->set_h_scroll(state["h_scroll_position"]);
1462
1463
if (state.get("selection", false)) {
1464
text_editor->select(state["selection_from_line"], state["selection_from_column"], state["selection_to_line"], state["selection_to_column"]);
1465
} else {
1466
text_editor->deselect();
1467
}
1468
1469
if (state.has("folded_lines")) {
1470
const PackedInt32Array folded_lines = state["folded_lines"];
1471
for (const int &line : folded_lines) {
1472
text_editor->fold_line(line);
1473
}
1474
}
1475
1476
if (state.has("breakpoints")) {
1477
const PackedInt32Array breakpoints = state["breakpoints"];
1478
for (const int &line : breakpoints) {
1479
text_editor->set_line_as_breakpoint(line, true);
1480
}
1481
}
1482
1483
if (state.has("bookmarks")) {
1484
const PackedInt32Array bookmarks = state["bookmarks"];
1485
for (const int &line : bookmarks) {
1486
text_editor->set_line_as_bookmarked(line, true);
1487
}
1488
}
1489
1490
if (previous_state.is_empty()) {
1491
previous_state = p_state;
1492
}
1493
}
1494
1495
Variant CodeTextEditor::get_navigation_state() {
1496
Dictionary state;
1497
1498
state["scroll_position"] = text_editor->get_v_scroll();
1499
state["h_scroll_position"] = text_editor->get_h_scroll();
1500
state["column"] = text_editor->get_caret_column();
1501
state["row"] = text_editor->get_caret_line();
1502
1503
state["selection"] = get_text_editor()->has_selection();
1504
if (get_text_editor()->has_selection()) {
1505
state["selection_from_line"] = text_editor->get_selection_from_line();
1506
state["selection_from_column"] = text_editor->get_selection_from_column();
1507
state["selection_to_line"] = text_editor->get_selection_to_line();
1508
state["selection_to_column"] = text_editor->get_selection_to_column();
1509
}
1510
1511
return state;
1512
}
1513
1514
void CodeTextEditor::set_error(const String &p_error) {
1515
error->set_text(p_error);
1516
1517
_update_error_content_height();
1518
1519
if (p_error.is_empty()) {
1520
error->set_default_cursor_shape(CURSOR_ARROW);
1521
} else {
1522
error->set_default_cursor_shape(CURSOR_POINTING_HAND);
1523
}
1524
}
1525
1526
void CodeTextEditor::_update_error_content_height() {
1527
float margin_height = 0;
1528
const Ref<StyleBox> style = error->get_theme_stylebox(CoreStringName(normal));
1529
if (style.is_valid()) {
1530
margin_height += style->get_content_margin(SIDE_TOP) + style->get_content_margin(SIDE_BOTTOM);
1531
}
1532
1533
const float content_height = margin_height + error->get_content_height();
1534
1535
float content_max_height = margin_height;
1536
for (int i = 0; i < 3; i++) {
1537
if (i >= error->get_line_count()) {
1538
break;
1539
}
1540
content_max_height += error->get_line_height(i);
1541
}
1542
1543
error->set_custom_minimum_size(Size2(0, CLAMP(content_height, 0, content_max_height)));
1544
}
1545
1546
void CodeTextEditor::set_error_pos(int p_line, int p_column) {
1547
error_line = p_line;
1548
error_column = p_column;
1549
}
1550
1551
Point2i CodeTextEditor::get_error_pos() const {
1552
return Point2i(error_line, error_column);
1553
}
1554
1555
void CodeTextEditor::goto_error() {
1556
if (!error->get_text().is_empty()) {
1557
int corrected_column = error_column;
1558
1559
const String line_text = text_editor->get_line(error_line);
1560
const int indent_size = text_editor->get_indent_size();
1561
if (indent_size > 1) {
1562
const int tab_count = line_text.length() - line_text.lstrip("\t").length();
1563
corrected_column -= tab_count * (indent_size - 1);
1564
}
1565
1566
goto_line_centered(error_line, corrected_column);
1567
}
1568
}
1569
1570
void CodeTextEditor::_update_text_editor_theme() {
1571
emit_signal(SNAME("load_theme_settings"));
1572
1573
const Ref<Font> status_bar_font = get_theme_font(SNAME("status_source"), EditorStringName(EditorFonts));
1574
const int status_bar_font_size = get_theme_font_size(SNAME("status_source_size"), EditorStringName(EditorFonts));
1575
const Color &error_color = get_theme_color(SNAME("error_color"), EditorStringName(Editor));
1576
const Color &warning_color = get_theme_color(SNAME("warning_color"), EditorStringName(Editor));
1577
const Ref<StyleBox> label_stylebox = get_theme_stylebox(SNAME("normal"), SNAME("Label")); // Empty stylebox.
1578
1579
error->begin_bulk_theme_override();
1580
error->add_theme_font_override(SNAME("normal_font"), status_bar_font);
1581
error->add_theme_font_size_override(SNAME("normal_font_size"), status_bar_font_size);
1582
error->add_theme_color_override(SNAME("default_color"), error_color);
1583
error->add_theme_style_override(SNAME("normal"), label_stylebox);
1584
error->end_bulk_theme_override();
1585
1586
error_button->set_button_icon(get_editor_theme_icon(SNAME("StatusError")));
1587
error_button->add_theme_color_override(SceneStringName(font_color), error_color);
1588
1589
warning_button->set_button_icon(get_editor_theme_icon(SNAME("NodeWarning")));
1590
warning_button->add_theme_color_override(SceneStringName(font_color), warning_color);
1591
1592
const int child_count = status_bar->get_child_count();
1593
for (int i = 0; i < child_count; i++) {
1594
Control *child = Object::cast_to<Control>(status_bar->get_child(i));
1595
if (child) {
1596
child->begin_bulk_theme_override();
1597
child->add_theme_font_override(SceneStringName(font), status_bar_font);
1598
child->add_theme_font_size_override(SceneStringName(font_size), status_bar_font_size);
1599
child->end_bulk_theme_override();
1600
}
1601
}
1602
1603
_update_font_ligatures();
1604
}
1605
1606
void CodeTextEditor::_update_font_ligatures() {
1607
int ot_mode = EDITOR_GET("interface/editor/code_font_contextual_ligatures");
1608
1609
Ref<FontVariation> fc = text_editor->get_theme_font(SceneStringName(font));
1610
if (fc.is_valid()) {
1611
switch (ot_mode) {
1612
case 1: { // Disable ligatures.
1613
Dictionary ftrs;
1614
ftrs[TS->name_to_tag("calt")] = 0;
1615
fc->set_opentype_features(ftrs);
1616
} break;
1617
case 2: { // Custom.
1618
Vector<String> subtag = String(EDITOR_GET("interface/editor/code_font_custom_opentype_features")).split(",");
1619
Dictionary ftrs;
1620
for (int i = 0; i < subtag.size(); i++) {
1621
Vector<String> subtag_a = subtag[i].split("=");
1622
if (subtag_a.size() == 2) {
1623
ftrs[TS->name_to_tag(subtag_a[0])] = subtag_a[1].to_int();
1624
} else if (subtag_a.size() == 1) {
1625
ftrs[TS->name_to_tag(subtag_a[0])] = 1;
1626
}
1627
}
1628
fc->set_opentype_features(ftrs);
1629
} break;
1630
default: { // Enabled.
1631
Dictionary ftrs;
1632
ftrs[TS->name_to_tag("calt")] = 1;
1633
fc->set_opentype_features(ftrs);
1634
} break;
1635
}
1636
}
1637
}
1638
1639
void CodeTextEditor::_text_changed_idle_timeout() {
1640
_validate_script();
1641
emit_signal(SNAME("validate_script"));
1642
}
1643
1644
void CodeTextEditor::validate_script() {
1645
idle->start();
1646
}
1647
1648
void CodeTextEditor::_error_button_pressed() {
1649
_set_show_errors_panel(!is_errors_panel_opened);
1650
_set_show_warnings_panel(false);
1651
}
1652
1653
void CodeTextEditor::_warning_button_pressed() {
1654
_set_show_warnings_panel(!is_warnings_panel_opened);
1655
_set_show_errors_panel(false);
1656
}
1657
1658
void CodeTextEditor::_zoom_popup_id_pressed(int p_idx) {
1659
_zoom_to(zoom_button->get_popup()->get_item_metadata(p_idx));
1660
}
1661
1662
void CodeTextEditor::_set_show_errors_panel(bool p_show) {
1663
is_errors_panel_opened = p_show;
1664
emit_signal(SNAME("show_errors_panel"), p_show);
1665
}
1666
1667
void CodeTextEditor::_set_show_warnings_panel(bool p_show) {
1668
is_warnings_panel_opened = p_show;
1669
emit_signal(SNAME("show_warnings_panel"), p_show);
1670
}
1671
1672
void CodeTextEditor::_toggle_files_pressed() {
1673
ERR_FAIL_NULL(toggle_files_list);
1674
toggle_files_list->set_visible(!toggle_files_list->is_visible());
1675
EditorSettings::get_singleton()->set_project_metadata("files_panel", "show_files_panel", toggle_files_list->is_visible());
1676
update_toggle_files_button();
1677
}
1678
1679
void CodeTextEditor::_error_pressed(const Ref<InputEvent> &p_event) {
1680
Ref<InputEventMouseButton> mb = p_event;
1681
if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {
1682
goto_error();
1683
}
1684
}
1685
1686
void CodeTextEditor::_notification(int p_what) {
1687
switch (p_what) {
1688
case NOTIFICATION_READY: {
1689
set_error_count(0);
1690
set_warning_count(0);
1691
} break;
1692
1693
case NOTIFICATION_THEME_CHANGED: {
1694
if (toggle_files_button->is_visible()) {
1695
update_toggle_files_button();
1696
}
1697
_update_text_editor_theme();
1698
} break;
1699
1700
case NOTIFICATION_TRANSLATION_CHANGED: {
1701
set_indent_using_spaces(text_editor->is_indent_using_spaces());
1702
update_toggle_files_button();
1703
1704
zoom_button->set_tooltip_text(
1705
TTR("Zoom Factor") + "\n" +
1706
// TRANSLATORS: The placeholders are keyboard shortcuts. The first one is in the form of "Ctrl+"/"Cmd+".
1707
vformat(TTR("%s+Mouse Wheel, %s/%s: Finetune\n%s: Reset"), keycode_get_string((Key)KeyModifierMask::CMD_OR_CTRL), ED_GET_SHORTCUT("script_editor/zoom_in")->get_as_text(), ED_GET_SHORTCUT("script_editor/zoom_out")->get_as_text(), ED_GET_SHORTCUT("script_editor/reset_zoom")->get_as_text()));
1708
1709
[[fallthrough]];
1710
}
1711
case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: {
1712
if (toggle_files_button->is_visible()) {
1713
update_toggle_files_button();
1714
}
1715
} break;
1716
1717
case NOTIFICATION_VISIBILITY_CHANGED: {
1718
if (toggle_files_button->is_visible()) {
1719
update_toggle_files_button();
1720
}
1721
set_process_input(is_visible_in_tree());
1722
} break;
1723
1724
case NOTIFICATION_PREDELETE: {
1725
if (find_replace_bar) {
1726
find_replace_bar->set_text_edit(nullptr);
1727
}
1728
} break;
1729
}
1730
}
1731
1732
void CodeTextEditor::set_error_count(int p_error_count) {
1733
error_button->set_text(itos(p_error_count));
1734
error_button->set_visible(p_error_count > 0);
1735
if (p_error_count > 0) {
1736
idle->set_wait_time(idle_time_with_errors); // Parsing should happen sooner.
1737
} else {
1738
_set_show_errors_panel(false);
1739
idle->set_wait_time(idle_time);
1740
}
1741
}
1742
1743
void CodeTextEditor::set_warning_count(int p_warning_count) {
1744
warning_button->set_text(itos(p_warning_count));
1745
warning_button->set_visible(p_warning_count > 0);
1746
if (!p_warning_count) {
1747
_set_show_warnings_panel(false);
1748
}
1749
}
1750
1751
void CodeTextEditor::toggle_bookmark() {
1752
Vector<int> sorted_carets = text_editor->get_sorted_carets();
1753
int last_line = -1;
1754
for (const int &c : sorted_carets) {
1755
int from = text_editor->get_selection_from_line(c);
1756
from += from == last_line ? 1 : 0;
1757
int to = text_editor->get_selection_to_line(c);
1758
if (to < from) {
1759
continue;
1760
}
1761
// Check first if there's any bookmarked lines in the selection.
1762
bool selection_has_bookmarks = false;
1763
for (int line = from; line <= to; line++) {
1764
if (text_editor->is_line_bookmarked(line)) {
1765
selection_has_bookmarks = true;
1766
break;
1767
}
1768
}
1769
1770
// Set bookmark on caret or remove all bookmarks from the selection.
1771
if (!selection_has_bookmarks) {
1772
if (text_editor->get_caret_line(c) != last_line) {
1773
text_editor->set_line_as_bookmarked(text_editor->get_caret_line(c), true);
1774
}
1775
} else {
1776
for (int line = from; line <= to; line++) {
1777
text_editor->set_line_as_bookmarked(line, false);
1778
}
1779
}
1780
last_line = to;
1781
}
1782
}
1783
1784
void CodeTextEditor::goto_next_bookmark() {
1785
PackedInt32Array bmarks = text_editor->get_bookmarked_lines();
1786
if (bmarks.is_empty()) {
1787
return;
1788
}
1789
1790
int current_line = text_editor->get_caret_line();
1791
int bmark_idx = 0;
1792
if (current_line < (int)bmarks[bmarks.size() - 1]) {
1793
while (bmark_idx < bmarks.size() && bmarks[bmark_idx] <= current_line) {
1794
bmark_idx++;
1795
}
1796
}
1797
goto_line_centered(bmarks[bmark_idx]);
1798
}
1799
1800
void CodeTextEditor::goto_prev_bookmark() {
1801
PackedInt32Array bmarks = text_editor->get_bookmarked_lines();
1802
if (bmarks.is_empty()) {
1803
return;
1804
}
1805
1806
int current_line = text_editor->get_caret_line();
1807
int bmark_idx = bmarks.size() - 1;
1808
if (current_line > (int)bmarks[0]) {
1809
while (bmark_idx >= 0 && bmarks[bmark_idx] >= current_line) {
1810
bmark_idx--;
1811
}
1812
}
1813
goto_line_centered(bmarks[bmark_idx]);
1814
}
1815
1816
void CodeTextEditor::remove_all_bookmarks() {
1817
text_editor->clear_bookmarked_lines();
1818
}
1819
1820
void CodeTextEditor::_zoom_in() {
1821
int s = text_editor->get_theme_font_size(SceneStringName(font_size));
1822
_zoom_to(zoom_factor * (s + MAX(1.0f, EDSCALE)) / s);
1823
}
1824
1825
void CodeTextEditor::_zoom_out() {
1826
int s = text_editor->get_theme_font_size(SceneStringName(font_size));
1827
_zoom_to(zoom_factor * (s - MAX(1.0f, EDSCALE)) / s);
1828
}
1829
1830
void CodeTextEditor::_zoom_to(float p_zoom_factor) {
1831
if (zoom_factor == p_zoom_factor) {
1832
return;
1833
}
1834
1835
float old_zoom_factor = zoom_factor;
1836
1837
set_zoom_factor(p_zoom_factor);
1838
1839
if (old_zoom_factor != zoom_factor) {
1840
emit_signal(SNAME("zoomed"), zoom_factor);
1841
}
1842
}
1843
1844
void CodeTextEditor::set_zoom_factor(float p_zoom_factor) {
1845
zoom_factor = CLAMP(p_zoom_factor, 0.25f, 3.0f);
1846
int neutral_font_size = int(EDITOR_GET("interface/editor/code_font_size")) * EDSCALE;
1847
int new_font_size = Math::round(zoom_factor * neutral_font_size);
1848
1849
zoom_button->set_text(itos(Math::round(zoom_factor * 100)) + " %");
1850
1851
text_editor->add_theme_font_size_override(SceneStringName(font_size), new_font_size);
1852
}
1853
1854
float CodeTextEditor::get_zoom_factor() {
1855
return zoom_factor;
1856
}
1857
1858
void CodeTextEditor::_bind_methods() {
1859
ADD_SIGNAL(MethodInfo("validate_script"));
1860
ADD_SIGNAL(MethodInfo("load_theme_settings"));
1861
ADD_SIGNAL(MethodInfo("show_errors_panel"));
1862
ADD_SIGNAL(MethodInfo("show_warnings_panel"));
1863
ADD_SIGNAL(MethodInfo("navigation_preview_ended"));
1864
ADD_SIGNAL(MethodInfo("zoomed", PropertyInfo(Variant::FLOAT, "p_zoom_factor")));
1865
}
1866
1867
void CodeTextEditor::set_code_complete_func(CodeTextEditorCodeCompleteFunc p_code_complete_func, void *p_ud) {
1868
code_complete_func = p_code_complete_func;
1869
code_complete_ud = p_ud;
1870
}
1871
1872
void CodeTextEditor::set_toggle_list_control(Control *p_toggle_list_control) {
1873
toggle_files_list = p_toggle_list_control;
1874
}
1875
1876
void CodeTextEditor::show_toggle_files_button() {
1877
toggle_files_button->show();
1878
}
1879
1880
void CodeTextEditor::update_toggle_files_button() {
1881
ERR_FAIL_NULL(toggle_files_list);
1882
bool forward = toggle_files_list->is_visible() == is_layout_rtl();
1883
toggle_files_button->set_button_icon(get_editor_theme_icon(forward ? SNAME("Forward") : SNAME("Back")));
1884
toggle_files_button->set_tooltip_text(vformat("%s (%s)", TTR("Toggle Files Panel"), ED_GET_SHORTCUT("script_editor/toggle_files_panel")->get_as_text()));
1885
}
1886
1887
CodeTextEditor::CodeTextEditor() {
1888
code_complete_func = nullptr;
1889
ED_SHORTCUT("script_editor/zoom_in", TTRC("Zoom In"), KeyModifierMask::CMD_OR_CTRL | Key::EQUAL);
1890
ED_SHORTCUT("script_editor/zoom_out", TTRC("Zoom Out"), KeyModifierMask::CMD_OR_CTRL | Key::MINUS);
1891
ED_SHORTCUT_ARRAY("script_editor/reset_zoom", TTRC("Reset Zoom"),
1892
{ int32_t(KeyModifierMask::CMD_OR_CTRL | Key::KEY_0), int32_t(KeyModifierMask::CMD_OR_CTRL | Key::KP_0) });
1893
1894
text_editor = memnew(CodeEdit);
1895
add_child(text_editor);
1896
text_editor->set_v_size_flags(SIZE_EXPAND_FILL);
1897
text_editor->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_GDSCRIPT);
1898
text_editor->set_draw_bookmarks_gutter(true);
1899
1900
text_editor->set_virtual_keyboard_show_on_focus(false);
1901
text_editor->set_draw_line_numbers(true);
1902
text_editor->set_highlight_matching_braces_enabled(true);
1903
text_editor->set_auto_indent_enabled(true);
1904
text_editor->set_deselect_on_focus_loss_enabled(false);
1905
1906
status_bar = memnew(HBoxContainer);
1907
add_child(status_bar);
1908
status_bar->set_h_size_flags(SIZE_EXPAND_FILL);
1909
status_bar->set_custom_minimum_size(Size2(0, 24 * EDSCALE)); // Adjust for the height of the warning icon.
1910
idle = memnew(Timer);
1911
add_child(idle);
1912
idle->set_one_shot(true);
1913
1914
code_complete_enabled = EDITOR_GET("text_editor/completion/code_complete_enabled");
1915
code_complete_timer = memnew(Timer);
1916
add_child(code_complete_timer);
1917
code_complete_timer->set_one_shot(true);
1918
1919
error_line = 0;
1920
error_column = 0;
1921
1922
toggle_files_button = memnew(Button);
1923
toggle_files_button->set_theme_type_variation(SceneStringName(FlatButton));
1924
toggle_files_button->set_v_size_flags(SIZE_EXPAND | SIZE_SHRINK_CENTER);
1925
toggle_files_button->set_tooltip_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
1926
toggle_files_button->connect(SceneStringName(pressed), callable_mp(this, &CodeTextEditor::_toggle_files_pressed));
1927
toggle_files_button->set_accessibility_name(TTRC("Scripts"));
1928
status_bar->add_child(toggle_files_button);
1929
toggle_files_button->hide();
1930
1931
// Error
1932
error = memnew(RichTextLabel);
1933
error->set_use_bbcode(true);
1934
error->set_selection_enabled(true);
1935
error->set_context_menu_enabled(true);
1936
error->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
1937
error->set_h_size_flags(SIZE_EXPAND_FILL);
1938
error->set_v_size_flags(SIZE_SHRINK_CENTER);
1939
error->connect(SceneStringName(gui_input), callable_mp(this, &CodeTextEditor::_error_pressed));
1940
error->connect(SceneStringName(resized), callable_mp(this, &CodeTextEditor::_update_error_content_height));
1941
status_bar->add_child(error);
1942
1943
// Errors
1944
error_button = memnew(Button);
1945
error_button->set_flat(true);
1946
status_bar->add_child(error_button);
1947
error_button->set_v_size_flags(SIZE_EXPAND | SIZE_SHRINK_CENTER);
1948
error_button->set_default_cursor_shape(CURSOR_POINTING_HAND);
1949
error_button->connect(SceneStringName(pressed), callable_mp(this, &CodeTextEditor::_error_button_pressed));
1950
error_button->set_tooltip_text(TTRC("Errors"));
1951
1952
// Warnings
1953
warning_button = memnew(Button);
1954
warning_button->set_flat(true);
1955
status_bar->add_child(warning_button);
1956
warning_button->set_v_size_flags(SIZE_EXPAND | SIZE_SHRINK_CENTER);
1957
warning_button->set_default_cursor_shape(CURSOR_POINTING_HAND);
1958
warning_button->connect(SceneStringName(pressed), callable_mp(this, &CodeTextEditor::_warning_button_pressed));
1959
warning_button->set_tooltip_text(TTRC("Warnings"));
1960
1961
status_bar->add_child(memnew(VSeparator));
1962
1963
// Zoom
1964
zoom_button = memnew(MenuButton);
1965
status_bar->add_child(zoom_button);
1966
zoom_button->set_flat(false);
1967
zoom_button->set_theme_type_variation("FlatMenuButton");
1968
zoom_button->set_v_size_flags(SIZE_EXPAND | SIZE_SHRINK_CENTER);
1969
zoom_button->set_text("100 %");
1970
zoom_button->set_accessibility_name(TTRC("Zoom Factor"));
1971
1972
PopupMenu *zoom_menu = zoom_button->get_popup();
1973
constexpr int preset_count = std_size(ZOOM_FACTOR_PRESETS);
1974
1975
for (int i = 0; i < preset_count; i++) {
1976
float z = ZOOM_FACTOR_PRESETS[i];
1977
zoom_menu->add_item(itos(Math::round(z * 100)) + " %");
1978
zoom_menu->set_item_metadata(i, z);
1979
}
1980
1981
zoom_menu->connect(SceneStringName(id_pressed), callable_mp(this, &CodeTextEditor::_zoom_popup_id_pressed));
1982
1983
status_bar->add_child(memnew(VSeparator));
1984
1985
// Line and column
1986
line_and_col_txt = memnew(Label);
1987
status_bar->add_child(line_and_col_txt);
1988
line_and_col_txt->set_v_size_flags(SIZE_EXPAND | SIZE_SHRINK_CENTER);
1989
line_and_col_txt->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
1990
line_and_col_txt->set_tooltip_auto_translate_mode(AUTO_TRANSLATE_MODE_ALWAYS);
1991
line_and_col_txt->set_tooltip_text(TTRC("Line and column numbers."));
1992
line_and_col_txt->set_accessibility_name(TTRC("Line and column numbers."));
1993
line_and_col_txt->set_focus_mode(FOCUS_ACCESSIBILITY);
1994
line_and_col_txt->set_mouse_filter(MOUSE_FILTER_STOP);
1995
1996
status_bar->add_child(memnew(VSeparator));
1997
1998
// Indentation
1999
indentation_txt = memnew(Label);
2000
status_bar->add_child(indentation_txt);
2001
indentation_txt->set_v_size_flags(SIZE_EXPAND | SIZE_SHRINK_CENTER);
2002
indentation_txt->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
2003
indentation_txt->set_tooltip_auto_translate_mode(AUTO_TRANSLATE_MODE_ALWAYS);
2004
indentation_txt->set_tooltip_text(TTRC("Indentation"));
2005
indentation_txt->set_accessibility_name(TTRC("Indentation"));
2006
indentation_txt->set_focus_mode(FOCUS_ACCESSIBILITY);
2007
indentation_txt->set_mouse_filter(MOUSE_FILTER_STOP);
2008
2009
text_editor->connect(SceneStringName(gui_input), callable_mp(this, &CodeTextEditor::_text_editor_gui_input));
2010
text_editor->connect("caret_changed", callable_mp(this, &CodeTextEditor::_line_col_changed));
2011
text_editor->connect(SceneStringName(text_changed), callable_mp(this, &CodeTextEditor::_text_changed));
2012
text_editor->connect("code_completion_requested", callable_mp(this, &CodeTextEditor::_complete_request));
2013
TypedArray<String> cs = { ".", ",", "(", "=", "$", "@", "\"", "\'" };
2014
text_editor->set_code_completion_prefixes(cs);
2015
idle->connect("timeout", callable_mp(this, &CodeTextEditor::_text_changed_idle_timeout));
2016
2017
code_complete_timer->connect("timeout", callable_mp(this, &CodeTextEditor::_code_complete_timer_timeout));
2018
}
2019
2020