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