Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
godotengine
GitHub Repository: godotengine/godot
Path: blob/master/editor/script/script_text_editor.cpp
9903 views
1
/**************************************************************************/
2
/* script_text_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 "script_text_editor.h"
32
33
#include "core/config/project_settings.h"
34
#include "core/io/dir_access.h"
35
#include "core/io/json.h"
36
#include "core/math/expression.h"
37
#include "core/os/keyboard.h"
38
#include "editor/debugger/editor_debugger_node.h"
39
#include "editor/doc/editor_help.h"
40
#include "editor/docks/filesystem_dock.h"
41
#include "editor/editor_node.h"
42
#include "editor/editor_string_names.h"
43
#include "editor/gui/editor_toaster.h"
44
#include "editor/inspector/editor_context_menu_plugin.h"
45
#include "editor/settings/editor_command_palette.h"
46
#include "editor/settings/editor_settings.h"
47
#include "editor/themes/editor_scale.h"
48
#include "scene/gui/grid_container.h"
49
#include "scene/gui/menu_button.h"
50
#include "scene/gui/rich_text_label.h"
51
#include "scene/gui/slider.h"
52
#include "scene/gui/split_container.h"
53
54
void ConnectionInfoDialog::ok_pressed() {
55
}
56
57
void ConnectionInfoDialog::popup_connections(const String &p_method, const Vector<Node *> &p_nodes) {
58
method->set_text(p_method);
59
60
tree->clear();
61
TreeItem *root = tree->create_item();
62
63
for (int i = 0; i < p_nodes.size(); i++) {
64
List<Connection> all_connections;
65
p_nodes[i]->get_signals_connected_to_this(&all_connections);
66
67
for (const Connection &connection : all_connections) {
68
if (connection.callable.get_method() != p_method) {
69
continue;
70
}
71
72
TreeItem *node_item = tree->create_item(root);
73
74
node_item->set_text(0, Object::cast_to<Node>(connection.signal.get_object())->get_name());
75
node_item->set_icon(0, EditorNode::get_singleton()->get_object_icon(connection.signal.get_object(), "Node"));
76
node_item->set_selectable(0, false);
77
node_item->set_editable(0, false);
78
79
node_item->set_text(1, connection.signal.get_name());
80
Control *p = Object::cast_to<Control>(get_parent());
81
node_item->set_icon(1, p->get_editor_theme_icon(SNAME("Slot")));
82
node_item->set_selectable(1, false);
83
node_item->set_editable(1, false);
84
85
node_item->set_text(2, Object::cast_to<Node>(connection.callable.get_object())->get_name());
86
node_item->set_icon(2, EditorNode::get_singleton()->get_object_icon(connection.callable.get_object(), "Node"));
87
node_item->set_selectable(2, false);
88
node_item->set_editable(2, false);
89
}
90
}
91
92
popup_centered(Size2(600, 300) * EDSCALE);
93
}
94
95
ConnectionInfoDialog::ConnectionInfoDialog() {
96
set_title(TTRC("Connections to method:"));
97
98
VBoxContainer *vbc = memnew(VBoxContainer);
99
vbc->set_anchor_and_offset(SIDE_LEFT, Control::ANCHOR_BEGIN, 8 * EDSCALE);
100
vbc->set_anchor_and_offset(SIDE_TOP, Control::ANCHOR_BEGIN, 8 * EDSCALE);
101
vbc->set_anchor_and_offset(SIDE_RIGHT, Control::ANCHOR_END, -8 * EDSCALE);
102
vbc->set_anchor_and_offset(SIDE_BOTTOM, Control::ANCHOR_END, -8 * EDSCALE);
103
add_child(vbc);
104
105
method = memnew(Label);
106
method->set_focus_mode(Control::FOCUS_ACCESSIBILITY);
107
method->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
108
method->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
109
vbc->add_child(method);
110
111
tree = memnew(Tree);
112
tree->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
113
tree->set_columns(3);
114
tree->set_hide_root(true);
115
tree->set_column_titles_visible(true);
116
tree->set_column_title(0, TTRC("Source"));
117
tree->set_column_title(1, TTRC("Signal"));
118
tree->set_column_title(2, TTRC("Target"));
119
vbc->add_child(tree);
120
tree->set_v_size_flags(Control::SIZE_EXPAND_FILL);
121
tree->set_allow_rmb_select(true);
122
}
123
124
////////////////////////////////////////////////////////////////////////////////
125
126
Vector<String> ScriptTextEditor::get_functions() {
127
CodeEdit *te = code_editor->get_text_editor();
128
String text = te->get_text();
129
List<String> fnc;
130
131
if (script->get_language()->validate(text, script->get_path(), &fnc)) {
132
//if valid rewrite functions to latest
133
functions.clear();
134
for (const String &E : fnc) {
135
functions.push_back(E);
136
}
137
}
138
139
return functions;
140
}
141
142
void ScriptTextEditor::apply_code() {
143
if (script.is_null()) {
144
return;
145
}
146
script->set_source_code(code_editor->get_text_editor()->get_text());
147
script->update_exports();
148
code_editor->get_text_editor()->get_syntax_highlighter()->update_cache();
149
}
150
151
Ref<Resource> ScriptTextEditor::get_edited_resource() const {
152
return script;
153
}
154
155
void ScriptTextEditor::set_edited_resource(const Ref<Resource> &p_res) {
156
ERR_FAIL_COND(script.is_valid());
157
ERR_FAIL_COND(p_res.is_null());
158
159
script = p_res;
160
161
code_editor->get_text_editor()->set_text(script->get_source_code());
162
code_editor->get_text_editor()->clear_undo_history();
163
code_editor->get_text_editor()->tag_saved_version();
164
165
emit_signal(SNAME("name_changed"));
166
code_editor->update_line_and_column();
167
}
168
169
void ScriptTextEditor::enable_editor(Control *p_shortcut_context) {
170
if (editor_enabled) {
171
return;
172
}
173
174
editor_enabled = true;
175
176
_enable_code_editor();
177
178
_validate_script();
179
180
if (p_shortcut_context) {
181
for (int i = 0; i < edit_hb->get_child_count(); ++i) {
182
Control *c = cast_to<Control>(edit_hb->get_child(i));
183
if (c) {
184
c->set_shortcut_context(p_shortcut_context);
185
}
186
}
187
}
188
}
189
190
void ScriptTextEditor::_load_theme_settings() {
191
CodeEdit *text_edit = code_editor->get_text_editor();
192
193
Color updated_warning_line_color = EDITOR_GET("text_editor/theme/highlighting/warning_color");
194
Color updated_marked_line_color = EDITOR_GET("text_editor/theme/highlighting/mark_color");
195
Color updated_safe_line_number_color = EDITOR_GET("text_editor/theme/highlighting/safe_line_number_color");
196
Color updated_folded_code_region_color = EDITOR_GET("text_editor/theme/highlighting/folded_code_region_color");
197
198
bool warning_line_color_updated = updated_warning_line_color != warning_line_color;
199
bool marked_line_color_updated = updated_marked_line_color != marked_line_color;
200
bool safe_line_number_color_updated = updated_safe_line_number_color != safe_line_number_color;
201
bool folded_code_region_color_updated = updated_folded_code_region_color != folded_code_region_color;
202
if (safe_line_number_color_updated || warning_line_color_updated || marked_line_color_updated || folded_code_region_color_updated) {
203
safe_line_number_color = updated_safe_line_number_color;
204
for (int i = 0; i < text_edit->get_line_count(); i++) {
205
if (warning_line_color_updated && text_edit->get_line_background_color(i) == warning_line_color) {
206
text_edit->set_line_background_color(i, updated_warning_line_color);
207
}
208
209
if (marked_line_color_updated && text_edit->get_line_background_color(i) == marked_line_color) {
210
text_edit->set_line_background_color(i, updated_marked_line_color);
211
}
212
213
if (safe_line_number_color_updated && text_edit->get_line_gutter_item_color(i, line_number_gutter) != default_line_number_color) {
214
text_edit->set_line_gutter_item_color(i, line_number_gutter, safe_line_number_color);
215
}
216
217
if (folded_code_region_color_updated && text_edit->get_line_background_color(i) == folded_code_region_color) {
218
text_edit->set_line_background_color(i, updated_folded_code_region_color);
219
}
220
}
221
warning_line_color = updated_warning_line_color;
222
marked_line_color = updated_marked_line_color;
223
folded_code_region_color = updated_folded_code_region_color;
224
}
225
226
theme_loaded = true;
227
if (script.is_valid()) {
228
_set_theme_for_script();
229
}
230
}
231
232
void ScriptTextEditor::_set_theme_for_script() {
233
if (!theme_loaded) {
234
return;
235
}
236
237
CodeEdit *text_edit = code_editor->get_text_editor();
238
text_edit->get_syntax_highlighter()->update_cache();
239
240
Vector<String> strings = script->get_language()->get_string_delimiters();
241
text_edit->clear_string_delimiters();
242
for (const String &string : strings) {
243
String beg = string.get_slicec(' ', 0);
244
String end = string.get_slice_count(" ") > 1 ? string.get_slicec(' ', 1) : String();
245
if (!text_edit->has_string_delimiter(beg)) {
246
text_edit->add_string_delimiter(beg, end, end.is_empty());
247
}
248
249
if (!end.is_empty() && !text_edit->has_auto_brace_completion_open_key(beg)) {
250
text_edit->add_auto_brace_completion_pair(beg, end);
251
}
252
}
253
254
text_edit->clear_comment_delimiters();
255
256
for (const String &comment : script->get_language()->get_comment_delimiters()) {
257
String beg = comment.get_slicec(' ', 0);
258
String end = comment.get_slice_count(" ") > 1 ? comment.get_slicec(' ', 1) : String();
259
text_edit->add_comment_delimiter(beg, end, end.is_empty());
260
261
if (!end.is_empty() && !text_edit->has_auto_brace_completion_open_key(beg)) {
262
text_edit->add_auto_brace_completion_pair(beg, end);
263
}
264
}
265
266
for (const String &doc_comment : script->get_language()->get_doc_comment_delimiters()) {
267
String beg = doc_comment.get_slicec(' ', 0);
268
String end = doc_comment.get_slice_count(" ") > 1 ? doc_comment.get_slicec(' ', 1) : String();
269
text_edit->add_comment_delimiter(beg, end, end.is_empty());
270
271
if (!end.is_empty() && !text_edit->has_auto_brace_completion_open_key(beg)) {
272
text_edit->add_auto_brace_completion_pair(beg, end);
273
}
274
}
275
}
276
277
void ScriptTextEditor::_show_errors_panel(bool p_show) {
278
errors_panel->set_visible(p_show);
279
}
280
281
void ScriptTextEditor::_show_warnings_panel(bool p_show) {
282
warnings_panel->set_visible(p_show);
283
}
284
285
void ScriptTextEditor::_warning_clicked(const Variant &p_line) {
286
if (p_line.get_type() == Variant::INT) {
287
goto_line_centered(p_line.operator int64_t());
288
} else if (p_line.get_type() == Variant::DICTIONARY) {
289
Dictionary meta = p_line.operator Dictionary();
290
const int line = meta["line"].operator int64_t() - 1;
291
const String code = meta["code"].operator String();
292
const String quote_style = EDITOR_GET("text_editor/completion/use_single_quotes") ? "'" : "\"";
293
294
CodeEdit *text_editor = code_editor->get_text_editor();
295
String prev_line = line > 0 ? text_editor->get_line(line - 1) : "";
296
if (prev_line.contains("@warning_ignore")) {
297
const int closing_bracket_idx = prev_line.find_char(')');
298
const String text_to_insert = ", " + code.quote(quote_style);
299
text_editor->insert_text(text_to_insert, line - 1, closing_bracket_idx);
300
} else {
301
const int indent = text_editor->get_indent_level(line) / text_editor->get_indent_size();
302
String annotation_indent;
303
if (!text_editor->is_indent_using_spaces()) {
304
annotation_indent = String("\t").repeat(indent);
305
} else {
306
annotation_indent = String(" ").repeat(text_editor->get_indent_size() * indent);
307
}
308
text_editor->insert_line_at(line, annotation_indent + "@warning_ignore(" + code.quote(quote_style) + ")");
309
}
310
311
_validate_script();
312
}
313
}
314
315
void ScriptTextEditor::_error_clicked(const Variant &p_line) {
316
if (p_line.get_type() == Variant::INT) {
317
goto_line_centered(p_line.operator int64_t());
318
} else if (p_line.get_type() == Variant::DICTIONARY) {
319
Dictionary meta = p_line.operator Dictionary();
320
const String path = meta["path"].operator String();
321
const int line = meta["line"].operator int64_t();
322
const int column = meta["column"].operator int64_t();
323
if (path.is_empty()) {
324
goto_line_centered(line, column);
325
} else {
326
Ref<Resource> scr = ResourceLoader::load(path);
327
if (scr.is_null()) {
328
EditorNode::get_singleton()->show_warning(TTR("Could not load file at:") + "\n\n" + path, TTR("Error!"));
329
} else {
330
int corrected_column = column;
331
332
const String line_text = code_editor->get_text_editor()->get_line(line);
333
const int indent_size = code_editor->get_text_editor()->get_indent_size();
334
if (indent_size > 1) {
335
const int tab_count = line_text.length() - line_text.lstrip("\t").length();
336
corrected_column -= tab_count * (indent_size - 1);
337
}
338
339
ScriptEditor::get_singleton()->edit(scr, line, corrected_column);
340
}
341
}
342
}
343
}
344
345
void ScriptTextEditor::reload_text() {
346
ERR_FAIL_COND(script.is_null());
347
348
CodeEdit *te = code_editor->get_text_editor();
349
int column = te->get_caret_column();
350
int row = te->get_caret_line();
351
int h = te->get_h_scroll();
352
int v = te->get_v_scroll();
353
354
te->set_text(script->get_source_code());
355
te->set_caret_line(row);
356
te->set_caret_column(column);
357
te->set_h_scroll(h);
358
te->set_v_scroll(v);
359
360
te->tag_saved_version();
361
362
code_editor->update_line_and_column();
363
if (editor_enabled) {
364
_validate_script();
365
}
366
}
367
368
void ScriptTextEditor::add_callback(const String &p_function, const PackedStringArray &p_args) {
369
ScriptLanguage *language = script->get_language();
370
if (!language->can_make_function()) {
371
return;
372
}
373
code_editor->get_text_editor()->begin_complex_operation();
374
code_editor->get_text_editor()->remove_secondary_carets();
375
code_editor->get_text_editor()->deselect();
376
String code = code_editor->get_text_editor()->get_text();
377
int pos = language->find_function(p_function, code);
378
if (pos == -1) {
379
// Function does not exist, create it at the end of the file.
380
int last_line = code_editor->get_text_editor()->get_line_count() - 1;
381
String func = language->make_function("", p_function, p_args);
382
code_editor->get_text_editor()->insert_text("\n\n" + func, last_line, code_editor->get_text_editor()->get_line(last_line).length());
383
pos = last_line + 3;
384
}
385
// Put caret on the line after the function, after the indent.
386
int indent_column = 1;
387
if (EDITOR_GET("text_editor/behavior/indent/type")) {
388
indent_column = EDITOR_GET("text_editor/behavior/indent/size");
389
}
390
code_editor->get_text_editor()->set_caret_line(pos, true, true, -1);
391
code_editor->get_text_editor()->set_caret_column(indent_column);
392
code_editor->get_text_editor()->end_complex_operation();
393
}
394
395
bool ScriptTextEditor::show_members_overview() {
396
return true;
397
}
398
399
bool ScriptTextEditor::_is_valid_color_info(const Dictionary &p_info) {
400
if (p_info.get_valid("color").get_type() != Variant::COLOR) {
401
return false;
402
}
403
if (!p_info.get_valid("color_end").is_num() || !p_info.get_valid("color_mode").is_num()) {
404
return false;
405
}
406
return true;
407
}
408
409
Array ScriptTextEditor::_inline_object_parse(const String &p_text) {
410
Array result;
411
int i_end_previous = 0;
412
int i_start = p_text.find("Color");
413
414
while (i_start != -1) {
415
// Ignore words that just have "Color" in them.
416
if (i_start != 0 && ('_' + p_text.substr(i_start - 1, 1)).is_valid_ascii_identifier()) {
417
i_end_previous = MAX(i_end_previous, i_start);
418
i_start = p_text.find("Color", i_start + 1);
419
continue;
420
}
421
422
const int i_par_start = p_text.find_char('(', i_start + 5);
423
const int i_par_end = p_text.find_char(')', i_start + 5);
424
if (i_par_start == -1 || i_par_end == -1) {
425
i_end_previous = MAX(i_end_previous, i_start);
426
i_start = p_text.find("Color", i_start + 1);
427
continue;
428
}
429
430
Dictionary color_info;
431
color_info["column"] = i_start;
432
color_info["width_ratio"] = 1.0;
433
color_info["color_end"] = i_par_end;
434
435
const String fn_name = p_text.substr(i_start + 5, i_par_start - i_start - 5);
436
const String s_params = p_text.substr(i_par_start + 1, i_par_end - i_par_start - 1);
437
bool has_added_color = false;
438
439
if (fn_name.is_empty()) {
440
String stripped = s_params.strip_edges(true, true);
441
if (stripped.length() > 1 && (stripped[0] == '"' || stripped[0] == '\'')) {
442
// String constructor.
443
const char32_t string_delimiter = stripped[0];
444
if (stripped[stripped.length() - 1] == string_delimiter) {
445
const String color_string = stripped.substr(1, stripped.length() - 2);
446
if (!color_string.contains_char(string_delimiter)) {
447
color_info["color"] = Color::from_string(color_string, Color());
448
color_info["color_mode"] = MODE_STRING;
449
has_added_color = true;
450
}
451
}
452
} else if (stripped.length() == 10 && stripped.begins_with("0x")) {
453
// Hex constructor.
454
const String color_string = stripped.substr(2);
455
if (color_string.is_valid_hex_number(false)) {
456
color_info["color"] = Color::from_string(color_string, Color());
457
color_info["color_mode"] = MODE_HEX;
458
has_added_color = true;
459
}
460
} else if (stripped.is_empty()) {
461
// Empty Color() constructor.
462
color_info["color"] = Color();
463
color_info["color_mode"] = MODE_RGB;
464
has_added_color = true;
465
}
466
}
467
// Float & int parameters.
468
if (!has_added_color && s_params.size() > 0) {
469
const PackedStringArray s_params_split = s_params.split(",", false, 4);
470
PackedFloat64Array params;
471
bool valid_floats = true;
472
for (const String &s_param : s_params_split) {
473
// Only allow float literals, expressions won't be evaluated and could get replaced.
474
if (!s_param.strip_edges().is_valid_float()) {
475
valid_floats = false;
476
break;
477
}
478
params.push_back(s_param.to_float());
479
}
480
if (valid_floats && params.size() == 3) {
481
params.push_back(1.0);
482
}
483
if (valid_floats && params.size() == 4) {
484
has_added_color = true;
485
if (fn_name == ".from_ok_hsl") {
486
color_info["color"] = Color::from_ok_hsl(params[0], params[1], params[2], params[3]);
487
color_info["color_mode"] = MODE_OKHSL;
488
} else if (fn_name == ".from_hsv") {
489
color_info["color"] = Color::from_hsv(params[0], params[1], params[2], params[3]);
490
color_info["color_mode"] = MODE_HSV;
491
} else if (fn_name == ".from_rgba8") {
492
color_info["color"] = Color::from_rgba8(int(params[0]), int(params[1]), int(params[2]), int(params[3]));
493
color_info["color_mode"] = MODE_RGB8;
494
} else if (fn_name.is_empty()) {
495
color_info["color"] = Color(params[0], params[1], params[2], params[3]);
496
color_info["color_mode"] = MODE_RGB;
497
} else {
498
has_added_color = false;
499
}
500
}
501
}
502
503
if (has_added_color) {
504
result.push_back(color_info);
505
i_end_previous = i_par_end + 1;
506
}
507
i_end_previous = MAX(i_end_previous, i_start);
508
i_start = p_text.find("Color", i_start + 1);
509
}
510
return result;
511
}
512
513
void ScriptTextEditor::_inline_object_draw(const Dictionary &p_info, const Rect2 &p_rect) {
514
if (_is_valid_color_info(p_info)) {
515
Rect2 col_rect = p_rect.grow(-4);
516
if (color_alpha_texture.is_null()) {
517
color_alpha_texture = inline_color_picker->get_theme_icon("sample_bg", "ColorPicker");
518
}
519
code_editor->get_text_editor()->draw_texture_rect(color_alpha_texture, col_rect, false);
520
code_editor->get_text_editor()->draw_rect(col_rect, Color(p_info["color"]));
521
code_editor->get_text_editor()->draw_rect(col_rect, Color(1, 1, 1), false, 1);
522
}
523
}
524
525
void ScriptTextEditor::_inline_object_handle_click(const Dictionary &p_info, const Rect2 &p_rect) {
526
if (_is_valid_color_info(p_info)) {
527
inline_color_picker->set_pick_color(p_info["color"]);
528
inline_color_line = p_info["line"];
529
inline_color_start = p_info["column"];
530
inline_color_end = p_info["color_end"];
531
532
// Reset tooltip hover timer.
533
code_editor->get_text_editor()->set_symbol_tooltip_on_hover_enabled(false);
534
code_editor->get_text_editor()->set_symbol_tooltip_on_hover_enabled(true);
535
536
_update_color_constructor_options();
537
inline_color_options->select(p_info["color_mode"]);
538
EditorNode::get_singleton()->setup_color_picker(inline_color_picker);
539
540
// Move popup above the line if it's too low.
541
float_t view_h = get_viewport_rect().size.y;
542
float_t pop_h = inline_color_popup->get_contents_minimum_size().y;
543
float_t pop_y = p_rect.get_end().y;
544
float_t pop_x = p_rect.position.x;
545
if (pop_y + pop_h > view_h) {
546
pop_y = p_rect.position.y - pop_h;
547
}
548
// Move popup to the right if it's too high.
549
if (pop_y < 0) {
550
pop_x = p_rect.get_end().x;
551
}
552
553
inline_color_popup->popup(Rect2(pop_x, pop_y, 0, 0));
554
}
555
}
556
557
String ScriptTextEditor::_picker_color_stringify(const Color &p_color, COLOR_MODE p_mode) {
558
String result;
559
String fname;
560
Vector<String> str_params;
561
switch (p_mode) {
562
case ScriptTextEditor::MODE_STRING: {
563
str_params.push_back("\"" + p_color.to_html() + "\"");
564
} break;
565
case ScriptTextEditor::MODE_HEX: {
566
str_params.push_back("0x" + p_color.to_html());
567
} break;
568
case ScriptTextEditor::MODE_RGB: {
569
str_params = {
570
String::num(p_color.r, 3),
571
String::num(p_color.g, 3),
572
String::num(p_color.b, 3),
573
String::num(p_color.a, 3)
574
};
575
} break;
576
case ScriptTextEditor::MODE_HSV: {
577
str_params = {
578
String::num(p_color.get_h(), 3),
579
String::num(p_color.get_s(), 3),
580
String::num(p_color.get_v(), 3),
581
String::num(p_color.a, 3)
582
};
583
fname = ".from_hsv";
584
} break;
585
case ScriptTextEditor::MODE_OKHSL: {
586
str_params = {
587
String::num(p_color.get_ok_hsl_h(), 3),
588
String::num(p_color.get_ok_hsl_s(), 3),
589
String::num(p_color.get_ok_hsl_l(), 3),
590
String::num(p_color.a, 3)
591
};
592
fname = ".from_ok_hsl";
593
} break;
594
case ScriptTextEditor::MODE_RGB8: {
595
str_params = {
596
itos(p_color.get_r8()),
597
itos(p_color.get_g8()),
598
itos(p_color.get_b8()),
599
itos(p_color.get_a8())
600
};
601
fname = ".from_rgba8";
602
} break;
603
default: {
604
} break;
605
}
606
result = "Color" + fname + "(" + String(", ").join(str_params) + ")";
607
return result;
608
}
609
610
void ScriptTextEditor::_picker_color_changed(const Color &p_color) {
611
_update_color_constructor_options();
612
_update_color_text();
613
}
614
615
void ScriptTextEditor::_update_color_constructor_options() {
616
int item_count = inline_color_options->get_item_count();
617
// Update or add each constructor as an option.
618
for (int i = 0; i < MODE_MAX; i++) {
619
String option_text = _picker_color_stringify(inline_color_picker->get_pick_color(), (COLOR_MODE)i);
620
if (i >= item_count) {
621
inline_color_options->add_item(option_text);
622
} else {
623
inline_color_options->set_item_text(i, option_text);
624
}
625
}
626
}
627
628
void ScriptTextEditor::_update_background_color() {
629
// Clear background lines.
630
CodeEdit *te = code_editor->get_text_editor();
631
for (int i = 0; i < te->get_line_count(); i++) {
632
bool is_folded_code_region = te->is_line_code_region_start(i) && te->is_line_folded(i);
633
te->set_line_background_color(i, is_folded_code_region ? folded_code_region_color : Color(0, 0, 0, 0));
634
}
635
636
// Set the warning background.
637
if (warning_line_color.a != 0.0) {
638
for (const ScriptLanguage::Warning &warning : warnings) {
639
int warning_start_line = CLAMP(warning.start_line - 1, 0, te->get_line_count() - 1);
640
int warning_end_line = CLAMP(warning.end_line - 1, 0, te->get_line_count() - 1);
641
int folded_line_header = te->get_folded_line_header(warning_start_line);
642
643
// If the warning highlight is too long, only highlight the start line.
644
const int warning_max_lines = 20;
645
646
te->set_line_background_color(folded_line_header, warning_line_color);
647
if (warning_end_line - warning_start_line < warning_max_lines) {
648
for (int i = warning_start_line + 1; i <= warning_end_line; i++) {
649
te->set_line_background_color(i, warning_line_color);
650
}
651
}
652
}
653
}
654
655
// Set the error background.
656
if (marked_line_color.a != 0.0) {
657
for (const ScriptLanguage::ScriptError &error : errors) {
658
int error_line = CLAMP(error.line - 1, 0, te->get_line_count() - 1);
659
int folded_line_header = te->get_folded_line_header(error_line);
660
661
te->set_line_background_color(folded_line_header, marked_line_color);
662
}
663
}
664
}
665
666
void ScriptTextEditor::_update_color_text() {
667
if (inline_color_line < 0) {
668
return;
669
}
670
String result = inline_color_options->get_item_text(inline_color_options->get_selected_id());
671
code_editor->get_text_editor()->begin_complex_operation();
672
code_editor->get_text_editor()->remove_text(inline_color_line, inline_color_start, inline_color_line, inline_color_end + 1);
673
inline_color_end = inline_color_start + result.size() - 2;
674
code_editor->get_text_editor()->insert_text(result, inline_color_line, inline_color_start);
675
code_editor->get_text_editor()->end_complex_operation();
676
}
677
678
void ScriptTextEditor::update_settings() {
679
code_editor->get_text_editor()->set_gutter_draw(connection_gutter, EDITOR_GET("text_editor/appearance/gutters/show_info_gutter"));
680
if (EDITOR_GET("text_editor/appearance/enable_inline_color_picker")) {
681
code_editor->get_text_editor()->set_inline_object_handlers(
682
callable_mp(this, &ScriptTextEditor::_inline_object_parse),
683
callable_mp(this, &ScriptTextEditor::_inline_object_draw),
684
callable_mp(this, &ScriptTextEditor::_inline_object_handle_click));
685
} else {
686
code_editor->get_text_editor()->set_inline_object_handlers(Callable(), Callable(), Callable());
687
}
688
code_editor->update_editor_settings();
689
}
690
691
bool ScriptTextEditor::is_unsaved() {
692
const bool unsaved =
693
code_editor->get_text_editor()->get_version() != code_editor->get_text_editor()->get_saved_version() ||
694
script->get_path().is_empty(); // In memory.
695
return unsaved;
696
}
697
698
Variant ScriptTextEditor::get_edit_state() {
699
return code_editor->get_edit_state();
700
}
701
702
void ScriptTextEditor::set_edit_state(const Variant &p_state) {
703
code_editor->set_edit_state(p_state);
704
705
Dictionary state = p_state;
706
if (state.has("syntax_highlighter")) {
707
int idx = highlighter_menu->get_item_idx_from_text(state["syntax_highlighter"]);
708
if (idx >= 0) {
709
_change_syntax_highlighter(idx);
710
}
711
}
712
713
if (editor_enabled) {
714
#ifndef ANDROID_ENABLED
715
ensure_focus();
716
#endif
717
}
718
}
719
720
Variant ScriptTextEditor::get_navigation_state() {
721
return code_editor->get_navigation_state();
722
}
723
724
Variant ScriptTextEditor::get_previous_state() {
725
return code_editor->get_previous_state();
726
}
727
728
void ScriptTextEditor::store_previous_state() {
729
return code_editor->store_previous_state();
730
}
731
732
void ScriptTextEditor::_convert_case(CodeTextEditor::CaseStyle p_case) {
733
code_editor->convert_case(p_case);
734
}
735
736
void ScriptTextEditor::trim_trailing_whitespace() {
737
code_editor->trim_trailing_whitespace();
738
}
739
740
void ScriptTextEditor::trim_final_newlines() {
741
code_editor->trim_final_newlines();
742
}
743
744
void ScriptTextEditor::insert_final_newline() {
745
code_editor->insert_final_newline();
746
}
747
748
void ScriptTextEditor::convert_indent() {
749
code_editor->get_text_editor()->convert_indent();
750
}
751
752
void ScriptTextEditor::tag_saved_version() {
753
code_editor->get_text_editor()->tag_saved_version();
754
edited_file_data.last_modified_time = FileAccess::get_modified_time(edited_file_data.path);
755
}
756
757
void ScriptTextEditor::goto_line(int p_line, int p_column) {
758
code_editor->goto_line(p_line, p_column);
759
}
760
761
void ScriptTextEditor::goto_line_selection(int p_line, int p_begin, int p_end) {
762
code_editor->goto_line_selection(p_line, p_begin, p_end);
763
}
764
765
void ScriptTextEditor::goto_line_centered(int p_line, int p_column) {
766
code_editor->goto_line_centered(p_line, p_column);
767
}
768
769
void ScriptTextEditor::set_executing_line(int p_line) {
770
code_editor->set_executing_line(p_line);
771
}
772
773
void ScriptTextEditor::clear_executing_line() {
774
code_editor->clear_executing_line();
775
}
776
777
void ScriptTextEditor::ensure_focus() {
778
code_editor->get_text_editor()->grab_focus();
779
}
780
781
String ScriptTextEditor::get_name() {
782
String name;
783
784
name = script->get_path().get_file();
785
if (name.is_empty()) {
786
// This appears for newly created built-in scripts before saving the scene.
787
name = TTR("[unsaved]");
788
} else if (script->is_built_in()) {
789
const String &script_name = script->get_name();
790
if (!script_name.is_empty()) {
791
// If the built-in script has a custom resource name defined,
792
// display the built-in script name as follows: `ResourceName (scene_file.tscn)`
793
name = vformat("%s (%s)", script_name, name.get_slice("::", 0));
794
}
795
}
796
797
if (is_unsaved()) {
798
name += "(*)";
799
}
800
801
return name;
802
}
803
804
Ref<Texture2D> ScriptTextEditor::get_theme_icon() {
805
if (get_parent_control()) {
806
String icon_name = script->get_class();
807
if (script->is_built_in()) {
808
icon_name += "Internal";
809
}
810
811
if (get_parent_control()->has_theme_icon(icon_name, EditorStringName(EditorIcons))) {
812
return get_parent_control()->get_editor_theme_icon(icon_name);
813
} else if (get_parent_control()->has_theme_icon(script->get_class(), EditorStringName(EditorIcons))) {
814
return get_parent_control()->get_editor_theme_icon(script->get_class());
815
}
816
}
817
818
return Ref<Texture2D>();
819
}
820
821
void ScriptTextEditor::_validate_script() {
822
CodeEdit *te = code_editor->get_text_editor();
823
824
String text = te->get_text();
825
List<String> fnc;
826
827
warnings.clear();
828
errors.clear();
829
depended_errors.clear();
830
safe_lines.clear();
831
832
if (!script->get_language()->validate(text, script->get_path(), &fnc, &errors, &warnings, &safe_lines)) {
833
List<ScriptLanguage::ScriptError>::Element *E = errors.front();
834
while (E) {
835
List<ScriptLanguage::ScriptError>::Element *next_E = E->next();
836
if ((E->get().path.is_empty() && !script->get_path().is_empty()) || E->get().path != script->get_path()) {
837
depended_errors[E->get().path].push_back(E->get());
838
E->erase();
839
}
840
E = next_E;
841
}
842
843
if (errors.size() > 0) {
844
const int line = errors.front()->get().line;
845
const int column = errors.front()->get().column;
846
const String message = errors.front()->get().message.replace("[", "[lb]");
847
const String error_text = vformat(TTR("Error at ([hint=Line %d, column %d]%d, %d[/hint]):"), line, column, line, column) + " " + message;
848
code_editor->set_error(error_text);
849
code_editor->set_error_pos(line - 1, column - 1);
850
}
851
script_is_valid = false;
852
} else {
853
code_editor->set_error("");
854
if (!script->is_tool()) {
855
script->set_source_code(text);
856
script->update_exports();
857
te->get_syntax_highlighter()->update_cache();
858
}
859
860
functions.clear();
861
for (const String &E : fnc) {
862
functions.push_back(E);
863
}
864
script_is_valid = true;
865
}
866
_update_connected_methods();
867
_update_warnings();
868
_update_errors();
869
_update_background_color();
870
871
emit_signal(SNAME("name_changed"));
872
emit_signal(SNAME("edited_script_changed"));
873
}
874
875
void ScriptTextEditor::_update_warnings() {
876
int warning_nb = warnings.size();
877
warnings_panel->clear();
878
879
bool has_connections_table = false;
880
// Add missing connections.
881
if (GLOBAL_GET("debug/gdscript/warnings/enable")) {
882
Node *base = get_tree()->get_edited_scene_root();
883
if (base && missing_connections.size() > 0) {
884
has_connections_table = true;
885
warnings_panel->push_table(1);
886
for (const Connection &connection : missing_connections) {
887
String base_path = base->get_name();
888
String source_path = base == connection.signal.get_object() ? base_path : base_path + "/" + String(base->get_path_to(Object::cast_to<Node>(connection.signal.get_object())));
889
String target_path = base == connection.callable.get_object() ? base_path : base_path + "/" + String(base->get_path_to(Object::cast_to<Node>(connection.callable.get_object())));
890
891
warnings_panel->push_cell();
892
warnings_panel->push_color(warnings_panel->get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));
893
warnings_panel->add_text(vformat(TTR("Missing connected method '%s' for signal '%s' from node '%s' to node '%s'."), connection.callable.get_method(), connection.signal.get_name(), source_path, target_path));
894
warnings_panel->pop(); // Color.
895
warnings_panel->pop(); // Cell.
896
}
897
warnings_panel->pop(); // Table.
898
899
warning_nb += missing_connections.size();
900
}
901
}
902
903
code_editor->set_warning_count(warning_nb);
904
905
if (has_connections_table) {
906
warnings_panel->add_newline();
907
}
908
909
// Add script warnings.
910
warnings_panel->push_table(3);
911
for (const ScriptLanguage::Warning &w : warnings) {
912
Dictionary ignore_meta;
913
ignore_meta["line"] = w.start_line;
914
ignore_meta["code"] = w.string_code.to_lower();
915
warnings_panel->push_cell();
916
warnings_panel->push_meta(ignore_meta);
917
warnings_panel->push_color(
918
warnings_panel->get_theme_color(SNAME("accent_color"), EditorStringName(Editor)).lerp(warnings_panel->get_theme_color(SNAME("mono_color"), EditorStringName(Editor)), 0.5f));
919
warnings_panel->add_text(TTR("[Ignore]"));
920
warnings_panel->pop(); // Color.
921
warnings_panel->pop(); // Meta ignore.
922
warnings_panel->pop(); // Cell.
923
924
warnings_panel->push_cell();
925
warnings_panel->push_meta(w.start_line - 1);
926
warnings_panel->push_color(warnings_panel->get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));
927
warnings_panel->add_text(vformat(TTR("Line %d (%s):"), w.start_line, w.string_code));
928
warnings_panel->pop(); // Color.
929
warnings_panel->pop(); // Meta goto.
930
warnings_panel->pop(); // Cell.
931
932
warnings_panel->push_cell();
933
warnings_panel->add_text(w.message);
934
warnings_panel->add_newline();
935
warnings_panel->pop(); // Cell.
936
}
937
warnings_panel->pop(); // Table.
938
}
939
940
void ScriptTextEditor::_update_errors() {
941
code_editor->set_error_count(errors.size());
942
943
errors_panel->clear();
944
errors_panel->push_table(2);
945
for (const ScriptLanguage::ScriptError &err : errors) {
946
Dictionary click_meta;
947
click_meta["line"] = err.line;
948
click_meta["column"] = err.column;
949
950
errors_panel->push_cell();
951
errors_panel->push_meta(err.line - 1);
952
errors_panel->push_color(warnings_panel->get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
953
errors_panel->add_text(vformat(TTR("Line %d:"), err.line));
954
errors_panel->pop(); // Color.
955
errors_panel->pop(); // Meta goto.
956
errors_panel->pop(); // Cell.
957
958
errors_panel->push_cell();
959
errors_panel->add_text(err.message);
960
errors_panel->add_newline();
961
errors_panel->pop(); // Cell.
962
}
963
errors_panel->pop(); // Table
964
965
for (const KeyValue<String, List<ScriptLanguage::ScriptError>> &KV : depended_errors) {
966
Dictionary click_meta;
967
click_meta["path"] = KV.key;
968
click_meta["line"] = 1;
969
970
errors_panel->add_newline();
971
errors_panel->add_newline();
972
errors_panel->push_meta(click_meta);
973
errors_panel->add_text(vformat(R"(%s:)", KV.key));
974
errors_panel->pop(); // Meta goto.
975
errors_panel->add_newline();
976
977
errors_panel->push_indent(1);
978
errors_panel->push_table(2);
979
String filename = KV.key.get_file();
980
for (const ScriptLanguage::ScriptError &err : KV.value) {
981
click_meta["line"] = err.line;
982
click_meta["column"] = err.column;
983
984
errors_panel->push_cell();
985
errors_panel->push_meta(click_meta);
986
errors_panel->push_color(errors_panel->get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
987
errors_panel->add_text(vformat(TTR("Line %d:"), err.line));
988
errors_panel->pop(); // Color.
989
errors_panel->pop(); // Meta goto.
990
errors_panel->pop(); // Cell.
991
992
errors_panel->push_cell();
993
errors_panel->add_text(err.message);
994
errors_panel->pop(); // Cell.
995
}
996
errors_panel->pop(); // Table
997
errors_panel->pop(); // Indent.
998
}
999
1000
bool highlight_safe = EDITOR_GET("text_editor/appearance/gutters/highlight_type_safe_lines");
1001
bool last_is_safe = false;
1002
CodeEdit *te = code_editor->get_text_editor();
1003
1004
for (int i = 0; i < te->get_line_count(); i++) {
1005
if (highlight_safe) {
1006
if (safe_lines.has(i + 1)) {
1007
te->set_line_gutter_item_color(i, line_number_gutter, safe_line_number_color);
1008
last_is_safe = true;
1009
} else if (last_is_safe && (te->is_in_comment(i) != -1 || te->get_line(i).strip_edges().is_empty())) {
1010
te->set_line_gutter_item_color(i, line_number_gutter, safe_line_number_color);
1011
} else {
1012
te->set_line_gutter_item_color(i, line_number_gutter, default_line_number_color);
1013
last_is_safe = false;
1014
}
1015
} else {
1016
te->set_line_gutter_item_color(i, 1, default_line_number_color);
1017
}
1018
}
1019
}
1020
1021
void ScriptTextEditor::_update_bookmark_list() {
1022
bookmarks_menu->clear();
1023
bookmarks_menu->reset_size();
1024
1025
bookmarks_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_bookmark"), BOOKMARK_TOGGLE);
1026
bookmarks_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/remove_all_bookmarks"), BOOKMARK_REMOVE_ALL);
1027
bookmarks_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_next_bookmark"), BOOKMARK_GOTO_NEXT);
1028
bookmarks_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_previous_bookmark"), BOOKMARK_GOTO_PREV);
1029
1030
PackedInt32Array bookmark_list = code_editor->get_text_editor()->get_bookmarked_lines();
1031
if (bookmark_list.is_empty()) {
1032
return;
1033
}
1034
1035
bookmarks_menu->add_separator();
1036
1037
for (int i = 0; i < bookmark_list.size(); i++) {
1038
// Strip edges to remove spaces or tabs.
1039
// Also replace any tabs by spaces, since we can't print tabs in the menu.
1040
String line = code_editor->get_text_editor()->get_line(bookmark_list[i]).replace("\t", " ").strip_edges();
1041
1042
// Limit the size of the line if too big.
1043
if (line.length() > 50) {
1044
line = line.substr(0, 50);
1045
}
1046
1047
bookmarks_menu->add_item(String::num_int64(bookmark_list[i] + 1) + " - `" + line + "`");
1048
bookmarks_menu->set_item_metadata(-1, bookmark_list[i]);
1049
}
1050
}
1051
1052
void ScriptTextEditor::_bookmark_item_pressed(int p_idx) {
1053
if (p_idx < 4) { // Any item before the separator.
1054
_edit_option(bookmarks_menu->get_item_id(p_idx));
1055
} else {
1056
code_editor->goto_line_centered(bookmarks_menu->get_item_metadata(p_idx));
1057
}
1058
}
1059
1060
static Vector<Node *> _find_all_node_for_script(Node *p_base, Node *p_current, const Ref<Script> &p_script) {
1061
Vector<Node *> nodes;
1062
1063
if (p_current->get_owner() != p_base && p_base != p_current) {
1064
return nodes;
1065
}
1066
1067
Ref<Script> c = p_current->get_script();
1068
if (c == p_script) {
1069
nodes.push_back(p_current);
1070
}
1071
1072
for (int i = 0; i < p_current->get_child_count(); i++) {
1073
Vector<Node *> found = _find_all_node_for_script(p_base, p_current->get_child(i), p_script);
1074
nodes.append_array(found);
1075
}
1076
1077
return nodes;
1078
}
1079
1080
static Node *_find_node_for_script(Node *p_base, Node *p_current, const Ref<Script> &p_script) {
1081
if (p_current->get_owner() != p_base && p_base != p_current) {
1082
return nullptr;
1083
}
1084
Ref<Script> c = p_current->get_script();
1085
if (c == p_script) {
1086
return p_current;
1087
}
1088
for (int i = 0; i < p_current->get_child_count(); i++) {
1089
Node *found = _find_node_for_script(p_base, p_current->get_child(i), p_script);
1090
if (found) {
1091
return found;
1092
}
1093
}
1094
1095
return nullptr;
1096
}
1097
1098
static void _find_changed_scripts_for_external_editor(Node *p_base, Node *p_current, HashSet<Ref<Script>> &r_scripts) {
1099
if (p_current->get_owner() != p_base && p_base != p_current) {
1100
return;
1101
}
1102
Ref<Script> c = p_current->get_script();
1103
1104
if (c.is_valid()) {
1105
r_scripts.insert(c);
1106
}
1107
1108
for (int i = 0; i < p_current->get_child_count(); i++) {
1109
_find_changed_scripts_for_external_editor(p_base, p_current->get_child(i), r_scripts);
1110
}
1111
}
1112
1113
void ScriptEditor::_update_modified_scripts_for_external_editor(Ref<Script> p_for_script) {
1114
bool use_external_editor = bool(EDITOR_GET("text_editor/external/use_external_editor"));
1115
1116
ERR_FAIL_NULL(get_tree());
1117
1118
HashSet<Ref<Script>> scripts;
1119
1120
Node *base = get_tree()->get_edited_scene_root();
1121
if (base) {
1122
_find_changed_scripts_for_external_editor(base, base, scripts);
1123
}
1124
1125
for (const Ref<Script> &E : scripts) {
1126
Ref<Script> scr = E;
1127
1128
if (!use_external_editor && !scr->get_language()->overrides_external_editor()) {
1129
continue; // We're not using an external editor for this script.
1130
}
1131
1132
if (p_for_script.is_valid() && p_for_script != scr) {
1133
continue;
1134
}
1135
1136
if (scr->is_built_in()) {
1137
continue; //internal script, who cares, though weird
1138
}
1139
1140
uint64_t last_date = scr->get_last_modified_time();
1141
uint64_t date = FileAccess::get_modified_time(scr->get_path());
1142
1143
if (last_date != date) {
1144
Ref<Script> rel_scr = ResourceLoader::load(scr->get_path(), scr->get_class(), ResourceFormatLoader::CACHE_MODE_IGNORE);
1145
ERR_CONTINUE(rel_scr.is_null());
1146
scr->set_source_code(rel_scr->get_source_code());
1147
scr->set_last_modified_time(rel_scr->get_last_modified_time());
1148
scr->update_exports();
1149
1150
trigger_live_script_reload(scr->get_path());
1151
}
1152
}
1153
}
1154
1155
void ScriptTextEditor::_code_complete_scripts(void *p_ud, const String &p_code, List<ScriptLanguage::CodeCompletionOption> *r_options, bool &r_force) {
1156
ScriptTextEditor *ste = (ScriptTextEditor *)p_ud;
1157
ste->_code_complete_script(p_code, r_options, r_force);
1158
}
1159
1160
void ScriptTextEditor::_code_complete_script(const String &p_code, List<ScriptLanguage::CodeCompletionOption> *r_options, bool &r_force) {
1161
if (color_panel->is_visible()) {
1162
return;
1163
}
1164
Node *base = get_tree()->get_edited_scene_root();
1165
if (base) {
1166
base = _find_node_for_script(base, base, script);
1167
}
1168
String hint;
1169
Error err = script->get_language()->complete_code(p_code, script->get_path(), base, r_options, r_force, hint);
1170
1171
if (err == OK) {
1172
code_editor->get_text_editor()->set_code_hint(hint);
1173
}
1174
}
1175
1176
void ScriptTextEditor::_update_breakpoint_list() {
1177
breakpoints_menu->clear();
1178
breakpoints_menu->reset_size();
1179
1180
breakpoints_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_breakpoint"), DEBUG_TOGGLE_BREAKPOINT);
1181
breakpoints_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/remove_all_breakpoints"), DEBUG_REMOVE_ALL_BREAKPOINTS);
1182
breakpoints_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_next_breakpoint"), DEBUG_GOTO_NEXT_BREAKPOINT);
1183
breakpoints_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_previous_breakpoint"), DEBUG_GOTO_PREV_BREAKPOINT);
1184
1185
PackedInt32Array breakpoint_list = code_editor->get_text_editor()->get_breakpointed_lines();
1186
if (breakpoint_list.is_empty()) {
1187
return;
1188
}
1189
1190
breakpoints_menu->add_separator();
1191
1192
for (int i = 0; i < breakpoint_list.size(); i++) {
1193
// Strip edges to remove spaces or tabs.
1194
// Also replace any tabs by spaces, since we can't print tabs in the menu.
1195
String line = code_editor->get_text_editor()->get_line(breakpoint_list[i]).replace("\t", " ").strip_edges();
1196
1197
// Limit the size of the line if too big.
1198
if (line.length() > 50) {
1199
line = line.substr(0, 50);
1200
}
1201
1202
breakpoints_menu->add_item(String::num_int64(breakpoint_list[i] + 1) + " - `" + line + "`");
1203
breakpoints_menu->set_item_metadata(-1, breakpoint_list[i]);
1204
}
1205
}
1206
1207
void ScriptTextEditor::_breakpoint_item_pressed(int p_idx) {
1208
if (p_idx < 4) { // Any item before the separator.
1209
_edit_option(breakpoints_menu->get_item_id(p_idx));
1210
} else {
1211
code_editor->goto_line_centered(breakpoints_menu->get_item_metadata(p_idx));
1212
}
1213
}
1214
1215
void ScriptTextEditor::_breakpoint_toggled(int p_row) {
1216
EditorDebuggerNode::get_singleton()->set_breakpoint(script->get_path(), p_row + 1, code_editor->get_text_editor()->is_line_breakpointed(p_row));
1217
}
1218
1219
void ScriptTextEditor::_on_caret_moved() {
1220
if (code_editor->is_previewing_navigation_change()) {
1221
return;
1222
}
1223
int current_line = code_editor->get_text_editor()->get_caret_line();
1224
if (Math::abs(current_line - previous_line) >= 10) {
1225
Dictionary nav_state = get_navigation_state();
1226
nav_state["row"] = previous_line;
1227
nav_state["scroll_position"] = -1;
1228
emit_signal(SNAME("request_save_previous_state"), nav_state);
1229
store_previous_state();
1230
}
1231
previous_line = current_line;
1232
}
1233
1234
void ScriptTextEditor::_lookup_symbol(const String &p_symbol, int p_row, int p_column) {
1235
Node *base = get_tree()->get_edited_scene_root();
1236
if (base) {
1237
base = _find_node_for_script(base, base, script);
1238
}
1239
1240
ScriptLanguage::LookupResult result;
1241
String code_text = code_editor->get_text_editor()->get_text_with_cursor_char(p_row, p_column);
1242
Error lc_error = script->get_language()->lookup_code(code_text, p_symbol, script->get_path(), base, result);
1243
if (ScriptServer::is_global_class(p_symbol)) {
1244
EditorNode::get_singleton()->load_resource(ScriptServer::get_global_class_path(p_symbol));
1245
} else if (p_symbol.is_resource_file() || p_symbol.begins_with("uid://")) {
1246
if (DirAccess::dir_exists_absolute(p_symbol)) {
1247
FileSystemDock::get_singleton()->navigate_to_path(p_symbol);
1248
} else {
1249
EditorNode::get_singleton()->load_scene_or_resource(p_symbol);
1250
}
1251
} else if (lc_error == OK) {
1252
_goto_line(p_row);
1253
1254
if (!result.class_name.is_empty() && EditorHelp::get_doc_data()->class_list.has(result.class_name) && !EditorHelp::get_doc_data()->class_list[result.class_name].is_script_doc) {
1255
switch (result.type) {
1256
case ScriptLanguage::LOOKUP_RESULT_CLASS: {
1257
emit_signal(SNAME("go_to_help"), "class_name:" + result.class_name);
1258
} break;
1259
case ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT: {
1260
StringName cname = result.class_name;
1261
while (ClassDB::class_exists(cname)) {
1262
if (ClassDB::has_integer_constant(cname, result.class_member, true)) {
1263
result.class_name = cname;
1264
break;
1265
}
1266
cname = ClassDB::get_parent_class(cname);
1267
}
1268
emit_signal(SNAME("go_to_help"), "class_constant:" + result.class_name + ":" + result.class_member);
1269
} break;
1270
case ScriptLanguage::LOOKUP_RESULT_CLASS_PROPERTY: {
1271
StringName cname = result.class_name;
1272
while (ClassDB::class_exists(cname)) {
1273
if (ClassDB::has_property(cname, result.class_member, true)) {
1274
result.class_name = cname;
1275
break;
1276
}
1277
cname = ClassDB::get_parent_class(cname);
1278
}
1279
emit_signal(SNAME("go_to_help"), "class_property:" + result.class_name + ":" + result.class_member);
1280
} break;
1281
case ScriptLanguage::LOOKUP_RESULT_CLASS_METHOD: {
1282
StringName cname = result.class_name;
1283
while (ClassDB::class_exists(cname)) {
1284
if (ClassDB::has_method(cname, result.class_member, true)) {
1285
result.class_name = cname;
1286
break;
1287
}
1288
cname = ClassDB::get_parent_class(cname);
1289
}
1290
emit_signal(SNAME("go_to_help"), "class_method:" + result.class_name + ":" + result.class_member);
1291
} break;
1292
case ScriptLanguage::LOOKUP_RESULT_CLASS_SIGNAL: {
1293
StringName cname = result.class_name;
1294
while (ClassDB::class_exists(cname)) {
1295
if (ClassDB::has_signal(cname, result.class_member, true)) {
1296
result.class_name = cname;
1297
break;
1298
}
1299
cname = ClassDB::get_parent_class(cname);
1300
}
1301
emit_signal(SNAME("go_to_help"), "class_signal:" + result.class_name + ":" + result.class_member);
1302
} break;
1303
case ScriptLanguage::LOOKUP_RESULT_CLASS_ENUM: {
1304
StringName cname = result.class_name;
1305
while (ClassDB::class_exists(cname)) {
1306
if (ClassDB::has_enum(cname, result.class_member, true)) {
1307
result.class_name = cname;
1308
break;
1309
}
1310
cname = ClassDB::get_parent_class(cname);
1311
}
1312
emit_signal(SNAME("go_to_help"), "class_enum:" + result.class_name + ":" + result.class_member);
1313
} break;
1314
case ScriptLanguage::LOOKUP_RESULT_CLASS_ANNOTATION: {
1315
emit_signal(SNAME("go_to_help"), "class_annotation:" + result.class_name + ":" + result.class_member);
1316
} break;
1317
case ScriptLanguage::LOOKUP_RESULT_CLASS_TBD_GLOBALSCOPE: { // Deprecated.
1318
emit_signal(SNAME("go_to_help"), "class_global:" + result.class_name + ":" + result.class_member);
1319
} break;
1320
case ScriptLanguage::LOOKUP_RESULT_SCRIPT_LOCATION:
1321
case ScriptLanguage::LOOKUP_RESULT_LOCAL_CONSTANT:
1322
case ScriptLanguage::LOOKUP_RESULT_LOCAL_VARIABLE:
1323
case ScriptLanguage::LOOKUP_RESULT_MAX: {
1324
// Nothing to do.
1325
} break;
1326
}
1327
} else if (result.location >= 0) {
1328
if (result.script.is_valid()) {
1329
emit_signal(SNAME("request_open_script_at_line"), result.script, result.location - 1);
1330
} else {
1331
emit_signal(SNAME("request_save_history"));
1332
goto_line_centered(result.location - 1);
1333
}
1334
}
1335
} else if (ProjectSettings::get_singleton()->has_autoload(p_symbol)) {
1336
// Check for Autoload scenes.
1337
const ProjectSettings::AutoloadInfo &info = ProjectSettings::get_singleton()->get_autoload(p_symbol);
1338
if (info.is_singleton) {
1339
EditorNode::get_singleton()->load_scene(info.path);
1340
}
1341
} else if (p_symbol.is_relative_path()) {
1342
// Every symbol other than absolute path is relative path so keep this condition at last.
1343
String path = _get_absolute_path(p_symbol);
1344
if (FileAccess::exists(path)) {
1345
EditorNode::get_singleton()->load_scene_or_resource(path);
1346
}
1347
}
1348
}
1349
1350
void ScriptTextEditor::_validate_symbol(const String &p_symbol) {
1351
CodeEdit *text_edit = code_editor->get_text_editor();
1352
1353
Node *base = get_tree()->get_edited_scene_root();
1354
if (base) {
1355
base = _find_node_for_script(base, base, script);
1356
}
1357
1358
ScriptLanguage::LookupResult result;
1359
String lc_text = code_editor->get_text_editor()->get_text_for_symbol_lookup();
1360
Error lc_error = script->get_language()->lookup_code(lc_text, p_symbol, script->get_path(), base, result);
1361
bool is_singleton = ProjectSettings::get_singleton()->has_autoload(p_symbol) && ProjectSettings::get_singleton()->get_autoload(p_symbol).is_singleton;
1362
if (lc_error == OK || is_singleton || ScriptServer::is_global_class(p_symbol) || p_symbol.is_resource_file() || p_symbol.begins_with("uid://")) {
1363
text_edit->set_symbol_lookup_word_as_valid(true);
1364
} else if (p_symbol.is_relative_path()) {
1365
String path = _get_absolute_path(p_symbol);
1366
if (FileAccess::exists(path)) {
1367
text_edit->set_symbol_lookup_word_as_valid(true);
1368
} else {
1369
text_edit->set_symbol_lookup_word_as_valid(false);
1370
}
1371
} else {
1372
text_edit->set_symbol_lookup_word_as_valid(false);
1373
}
1374
}
1375
1376
void ScriptTextEditor::_show_symbol_tooltip(const String &p_symbol, int p_row, int p_column) {
1377
if (!EDITOR_GET("text_editor/behavior/documentation/enable_tooltips").booleanize()) {
1378
return;
1379
}
1380
1381
if (p_symbol.begins_with("res://") || p_symbol.begins_with("uid://")) {
1382
EditorHelpBitTooltip::show_tooltip(code_editor->get_text_editor(), "resource||" + p_symbol);
1383
return;
1384
}
1385
1386
Node *base = get_tree()->get_edited_scene_root();
1387
if (base) {
1388
base = _find_node_for_script(base, base, script);
1389
}
1390
1391
ScriptLanguage::LookupResult result;
1392
String doc_symbol;
1393
const String code_text = code_editor->get_text_editor()->get_text_with_cursor_char(p_row, p_column);
1394
const Error lc_error = script->get_language()->lookup_code(code_text, p_symbol, script->get_path(), base, result);
1395
if (lc_error == OK) {
1396
switch (result.type) {
1397
case ScriptLanguage::LOOKUP_RESULT_CLASS: {
1398
doc_symbol = "class|" + result.class_name + "|";
1399
} break;
1400
case ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT: {
1401
StringName cname = result.class_name;
1402
while (ClassDB::class_exists(cname)) {
1403
if (ClassDB::has_integer_constant(cname, result.class_member, true)) {
1404
result.class_name = cname;
1405
break;
1406
}
1407
cname = ClassDB::get_parent_class(cname);
1408
}
1409
doc_symbol = "constant|" + result.class_name + "|" + result.class_member;
1410
} break;
1411
case ScriptLanguage::LOOKUP_RESULT_CLASS_PROPERTY: {
1412
StringName cname = result.class_name;
1413
while (ClassDB::class_exists(cname)) {
1414
if (ClassDB::has_property(cname, result.class_member, true)) {
1415
result.class_name = cname;
1416
break;
1417
}
1418
cname = ClassDB::get_parent_class(cname);
1419
}
1420
doc_symbol = "property|" + result.class_name + "|" + result.class_member;
1421
} break;
1422
case ScriptLanguage::LOOKUP_RESULT_CLASS_METHOD: {
1423
StringName cname = result.class_name;
1424
while (ClassDB::class_exists(cname)) {
1425
if (ClassDB::has_method(cname, result.class_member, true)) {
1426
result.class_name = cname;
1427
break;
1428
}
1429
cname = ClassDB::get_parent_class(cname);
1430
}
1431
doc_symbol = "method|" + result.class_name + "|" + result.class_member;
1432
} break;
1433
case ScriptLanguage::LOOKUP_RESULT_CLASS_SIGNAL: {
1434
StringName cname = result.class_name;
1435
while (ClassDB::class_exists(cname)) {
1436
if (ClassDB::has_signal(cname, result.class_member, true)) {
1437
result.class_name = cname;
1438
break;
1439
}
1440
cname = ClassDB::get_parent_class(cname);
1441
}
1442
doc_symbol = "signal|" + result.class_name + "|" + result.class_member;
1443
} break;
1444
case ScriptLanguage::LOOKUP_RESULT_CLASS_ENUM: {
1445
StringName cname = result.class_name;
1446
while (ClassDB::class_exists(cname)) {
1447
if (ClassDB::has_enum(cname, result.class_member, true)) {
1448
result.class_name = cname;
1449
break;
1450
}
1451
cname = ClassDB::get_parent_class(cname);
1452
}
1453
doc_symbol = "enum|" + result.class_name + "|" + result.class_member;
1454
} break;
1455
case ScriptLanguage::LOOKUP_RESULT_CLASS_ANNOTATION: {
1456
doc_symbol = "annotation|" + result.class_name + "|" + result.class_member;
1457
} break;
1458
case ScriptLanguage::LOOKUP_RESULT_LOCAL_CONSTANT:
1459
case ScriptLanguage::LOOKUP_RESULT_LOCAL_VARIABLE: {
1460
const String item_type = (result.type == ScriptLanguage::LOOKUP_RESULT_LOCAL_CONSTANT) ? "local_constant" : "local_variable";
1461
Dictionary item_data;
1462
item_data["description"] = result.description;
1463
item_data["is_deprecated"] = result.is_deprecated;
1464
item_data["deprecated_message"] = result.deprecated_message;
1465
item_data["is_experimental"] = result.is_experimental;
1466
item_data["experimental_message"] = result.experimental_message;
1467
item_data["doc_type"] = result.doc_type;
1468
item_data["enumeration"] = result.enumeration;
1469
item_data["is_bitfield"] = result.is_bitfield;
1470
item_data["value"] = result.value;
1471
doc_symbol = item_type + "||" + p_symbol + "|" + JSON::stringify(item_data);
1472
} break;
1473
case ScriptLanguage::LOOKUP_RESULT_SCRIPT_LOCATION:
1474
case ScriptLanguage::LOOKUP_RESULT_CLASS_TBD_GLOBALSCOPE: // Deprecated.
1475
case ScriptLanguage::LOOKUP_RESULT_MAX: {
1476
// Nothing to do.
1477
} break;
1478
}
1479
}
1480
1481
// NOTE: See also `ScriptEditor::_get_debug_tooltip()` for documentation tooltips disabled.
1482
String debug_value = EditorDebuggerNode::get_singleton()->get_var_value(p_symbol);
1483
if (!debug_value.is_empty()) {
1484
constexpr int DISPLAY_LIMIT = 1024;
1485
if (debug_value.size() > DISPLAY_LIMIT) {
1486
debug_value = debug_value.left(DISPLAY_LIMIT) + "... " + TTR("(truncated)");
1487
}
1488
debug_value = TTR("Current value: ") + debug_value.replace("[", "[lb]");
1489
}
1490
1491
if (!doc_symbol.is_empty() || !debug_value.is_empty()) {
1492
EditorHelpBitTooltip::show_tooltip(code_editor->get_text_editor(), doc_symbol, debug_value, true);
1493
}
1494
}
1495
1496
String ScriptTextEditor::_get_absolute_path(const String &rel_path) {
1497
String base_path = script->get_path().get_base_dir();
1498
String path = base_path.path_join(rel_path);
1499
return path.replace("///", "//").simplify_path();
1500
}
1501
1502
void ScriptTextEditor::update_toggle_files_button() {
1503
code_editor->update_toggle_files_button();
1504
}
1505
1506
void ScriptTextEditor::_update_connected_methods() {
1507
CodeEdit *text_edit = code_editor->get_text_editor();
1508
text_edit->set_gutter_width(connection_gutter, text_edit->get_line_height());
1509
for (int i = 0; i < text_edit->get_line_count(); i++) {
1510
text_edit->set_line_gutter_metadata(i, connection_gutter, Dictionary());
1511
text_edit->set_line_gutter_icon(i, connection_gutter, nullptr);
1512
text_edit->set_line_gutter_clickable(i, connection_gutter, false);
1513
}
1514
missing_connections.clear();
1515
1516
if (!script_is_valid) {
1517
return;
1518
}
1519
1520
Node *base = get_tree()->get_edited_scene_root();
1521
if (!base) {
1522
return;
1523
}
1524
1525
// Add connection icons to methods.
1526
Vector<Node *> nodes = _find_all_node_for_script(base, base, script);
1527
HashSet<StringName> methods_found;
1528
for (int i = 0; i < nodes.size(); i++) {
1529
List<Connection> signal_connections;
1530
nodes[i]->get_signals_connected_to_this(&signal_connections);
1531
1532
for (const Connection &connection : signal_connections) {
1533
if (!(connection.flags & CONNECT_PERSIST)) {
1534
continue;
1535
}
1536
1537
// As deleted nodes are still accessible via the undo/redo system, check if they're still on the tree.
1538
Node *source = Object::cast_to<Node>(connection.signal.get_object());
1539
if (source && !source->is_inside_tree()) {
1540
continue;
1541
}
1542
1543
const StringName method = connection.callable.get_method();
1544
if (methods_found.has(method)) {
1545
continue;
1546
}
1547
1548
if (!ClassDB::has_method(script->get_instance_base_type(), method)) {
1549
int line = -1;
1550
1551
for (int j = 0; j < functions.size(); j++) {
1552
String name = functions[j].get_slicec(':', 0);
1553
if (name == method) {
1554
Dictionary line_meta;
1555
line_meta["type"] = "connection";
1556
line_meta["method"] = method;
1557
line = functions[j].get_slicec(':', 1).to_int() - 1;
1558
text_edit->set_line_gutter_metadata(line, connection_gutter, line_meta);
1559
text_edit->set_line_gutter_icon(line, connection_gutter, get_parent_control()->get_editor_theme_icon(SNAME("Slot")));
1560
text_edit->set_line_gutter_clickable(line, connection_gutter, true);
1561
methods_found.insert(method);
1562
break;
1563
}
1564
}
1565
1566
if (line >= 0) {
1567
continue;
1568
}
1569
1570
// There is a chance that the method is inherited from another script.
1571
bool found_inherited_function = false;
1572
Ref<Script> inherited_script = script->get_base_script();
1573
while (inherited_script.is_valid()) {
1574
if (inherited_script->has_method(method)) {
1575
found_inherited_function = true;
1576
break;
1577
}
1578
1579
inherited_script = inherited_script->get_base_script();
1580
}
1581
1582
if (!found_inherited_function) {
1583
missing_connections.push_back(connection);
1584
}
1585
}
1586
}
1587
}
1588
1589
// Add override icons to methods.
1590
methods_found.clear();
1591
for (int i = 0; i < functions.size(); i++) {
1592
String raw_name = functions[i].get_slicec(':', 0);
1593
StringName name = StringName(raw_name);
1594
if (methods_found.has(name)) {
1595
continue;
1596
}
1597
1598
// Account for inner classes by stripping the class names from the method,
1599
// starting from the right since our inner class might be inside of another inner class.
1600
int pos = raw_name.rfind_char('.');
1601
if (pos != -1) {
1602
name = raw_name.substr(pos + 1);
1603
}
1604
1605
String found_base_class;
1606
StringName base_class = script->get_instance_base_type();
1607
Ref<Script> inherited_script = script->get_base_script();
1608
while (inherited_script.is_valid()) {
1609
if (inherited_script->has_method(name)) {
1610
found_base_class = "script:" + inherited_script->get_path();
1611
break;
1612
}
1613
1614
base_class = inherited_script->get_instance_base_type();
1615
inherited_script = inherited_script->get_base_script();
1616
}
1617
1618
if (found_base_class.is_empty()) {
1619
while (base_class) {
1620
List<MethodInfo> methods;
1621
ClassDB::get_method_list(base_class, &methods, true);
1622
for (const MethodInfo &mi : methods) {
1623
if (mi.name == name) {
1624
found_base_class = "builtin:" + base_class;
1625
break;
1626
}
1627
}
1628
1629
ClassDB::ClassInfo *base_class_ptr = ClassDB::classes.getptr(base_class)->inherits_ptr;
1630
if (base_class_ptr == nullptr) {
1631
break;
1632
}
1633
base_class = base_class_ptr->name;
1634
}
1635
}
1636
1637
if (!found_base_class.is_empty()) {
1638
int line = functions[i].get_slicec(':', 1).to_int() - 1;
1639
1640
Dictionary line_meta = text_edit->get_line_gutter_metadata(line, connection_gutter);
1641
if (line_meta.is_empty()) {
1642
// Add override icon to gutter.
1643
line_meta["type"] = "inherits";
1644
line_meta["method"] = name;
1645
line_meta["base_class"] = found_base_class;
1646
text_edit->set_line_gutter_icon(line, connection_gutter, get_parent_control()->get_editor_theme_icon(SNAME("MethodOverride")));
1647
text_edit->set_line_gutter_clickable(line, connection_gutter, true);
1648
} else {
1649
// If method is also connected to signal, then merge icons and keep the click behavior of the slot.
1650
text_edit->set_line_gutter_icon(line, connection_gutter, get_parent_control()->get_editor_theme_icon(SNAME("MethodOverrideAndSlot")));
1651
}
1652
1653
methods_found.insert(StringName(raw_name));
1654
}
1655
}
1656
}
1657
1658
void ScriptTextEditor::_update_gutter_indexes() {
1659
for (int i = 0; i < code_editor->get_text_editor()->get_gutter_count(); i++) {
1660
if (code_editor->get_text_editor()->get_gutter_name(i) == "connection_gutter") {
1661
connection_gutter = i;
1662
continue;
1663
}
1664
1665
if (code_editor->get_text_editor()->get_gutter_name(i) == "line_numbers") {
1666
line_number_gutter = i;
1667
continue;
1668
}
1669
}
1670
}
1671
1672
void ScriptTextEditor::_gutter_clicked(int p_line, int p_gutter) {
1673
if (p_gutter != connection_gutter) {
1674
return;
1675
}
1676
1677
Dictionary meta = code_editor->get_text_editor()->get_line_gutter_metadata(p_line, p_gutter);
1678
String type = meta.get("type", "");
1679
if (type.is_empty()) {
1680
return;
1681
}
1682
1683
// All types currently need a method name.
1684
String method = meta.get("method", "");
1685
if (method.is_empty()) {
1686
return;
1687
}
1688
1689
if (type == "connection") {
1690
Node *base = get_tree()->get_edited_scene_root();
1691
if (!base) {
1692
return;
1693
}
1694
1695
Vector<Node *> nodes = _find_all_node_for_script(base, base, script);
1696
connection_info_dialog->popup_connections(method, nodes);
1697
} else if (type == "inherits") {
1698
String base_class_raw = meta["base_class"];
1699
PackedStringArray base_class_split = base_class_raw.split(":", true, 1);
1700
1701
if (base_class_split[0] == "script") {
1702
// Go to function declaration.
1703
Ref<Script> base_script = ResourceLoader::load(base_class_split[1]);
1704
ERR_FAIL_COND(base_script.is_null());
1705
emit_signal(SNAME("go_to_method"), base_script, method);
1706
} else if (base_class_split[0] == "builtin") {
1707
// Open method documentation.
1708
emit_signal(SNAME("go_to_help"), "class_method:" + base_class_split[1] + ":" + method);
1709
}
1710
}
1711
}
1712
1713
void ScriptTextEditor::_edit_option(int p_op) {
1714
CodeEdit *tx = code_editor->get_text_editor();
1715
tx->apply_ime();
1716
1717
switch (p_op) {
1718
case EDIT_UNDO: {
1719
tx->undo();
1720
callable_mp((Control *)tx, &Control::grab_focus).call_deferred();
1721
} break;
1722
case EDIT_REDO: {
1723
tx->redo();
1724
callable_mp((Control *)tx, &Control::grab_focus).call_deferred();
1725
} break;
1726
case EDIT_CUT: {
1727
tx->cut();
1728
callable_mp((Control *)tx, &Control::grab_focus).call_deferred();
1729
} break;
1730
case EDIT_COPY: {
1731
tx->copy();
1732
callable_mp((Control *)tx, &Control::grab_focus).call_deferred();
1733
} break;
1734
case EDIT_PASTE: {
1735
tx->paste();
1736
callable_mp((Control *)tx, &Control::grab_focus).call_deferred();
1737
} break;
1738
case EDIT_SELECT_ALL: {
1739
tx->select_all();
1740
callable_mp((Control *)tx, &Control::grab_focus).call_deferred();
1741
} break;
1742
case EDIT_MOVE_LINE_UP: {
1743
code_editor->get_text_editor()->move_lines_up();
1744
} break;
1745
case EDIT_MOVE_LINE_DOWN: {
1746
code_editor->get_text_editor()->move_lines_down();
1747
} break;
1748
case EDIT_INDENT: {
1749
Ref<Script> scr = script;
1750
if (scr.is_null()) {
1751
return;
1752
}
1753
tx->indent_lines();
1754
} break;
1755
case EDIT_UNINDENT: {
1756
Ref<Script> scr = script;
1757
if (scr.is_null()) {
1758
return;
1759
}
1760
tx->unindent_lines();
1761
} break;
1762
case EDIT_DELETE_LINE: {
1763
code_editor->get_text_editor()->delete_lines();
1764
} break;
1765
case EDIT_DUPLICATE_SELECTION: {
1766
code_editor->get_text_editor()->duplicate_selection();
1767
} break;
1768
case EDIT_DUPLICATE_LINES: {
1769
code_editor->get_text_editor()->duplicate_lines();
1770
} break;
1771
case EDIT_TOGGLE_FOLD_LINE: {
1772
tx->toggle_foldable_lines_at_carets();
1773
} break;
1774
case EDIT_FOLD_ALL_LINES: {
1775
tx->fold_all_lines();
1776
} break;
1777
case EDIT_UNFOLD_ALL_LINES: {
1778
tx->unfold_all_lines();
1779
} break;
1780
case EDIT_CREATE_CODE_REGION: {
1781
tx->create_code_region();
1782
} break;
1783
case EDIT_TOGGLE_COMMENT: {
1784
_edit_option_toggle_inline_comment();
1785
} break;
1786
case EDIT_COMPLETE: {
1787
tx->request_code_completion(true);
1788
} break;
1789
case EDIT_AUTO_INDENT: {
1790
String text = tx->get_text();
1791
Ref<Script> scr = script;
1792
if (scr.is_null()) {
1793
return;
1794
}
1795
1796
tx->begin_complex_operation();
1797
tx->begin_multicaret_edit();
1798
int begin = tx->get_line_count() - 1, end = 0;
1799
if (tx->has_selection()) {
1800
// Auto indent all lines that have a caret or selection on it.
1801
Vector<Point2i> line_ranges = tx->get_line_ranges_from_carets();
1802
for (Point2i line_range : line_ranges) {
1803
scr->get_language()->auto_indent_code(text, line_range.x, line_range.y);
1804
if (line_range.x < begin) {
1805
begin = line_range.x;
1806
}
1807
if (line_range.y > end) {
1808
end = line_range.y;
1809
}
1810
}
1811
} else {
1812
// Auto indent entire text.
1813
begin = 0;
1814
end = tx->get_line_count() - 1;
1815
scr->get_language()->auto_indent_code(text, begin, end);
1816
}
1817
1818
// Apply auto indented code.
1819
Vector<String> lines = text.split("\n");
1820
for (int i = begin; i <= end; ++i) {
1821
tx->set_line(i, lines[i]);
1822
}
1823
1824
tx->end_multicaret_edit();
1825
tx->end_complex_operation();
1826
} break;
1827
case EDIT_TRIM_TRAILING_WHITESAPCE: {
1828
trim_trailing_whitespace();
1829
} break;
1830
case EDIT_TRIM_FINAL_NEWLINES: {
1831
trim_final_newlines();
1832
} break;
1833
case EDIT_CONVERT_INDENT_TO_SPACES: {
1834
code_editor->set_indent_using_spaces(true);
1835
convert_indent();
1836
} break;
1837
case EDIT_CONVERT_INDENT_TO_TABS: {
1838
code_editor->set_indent_using_spaces(false);
1839
convert_indent();
1840
} break;
1841
case EDIT_PICK_COLOR: {
1842
color_panel->popup();
1843
} break;
1844
case EDIT_TO_UPPERCASE: {
1845
_convert_case(CodeTextEditor::UPPER);
1846
} break;
1847
case EDIT_TO_LOWERCASE: {
1848
_convert_case(CodeTextEditor::LOWER);
1849
} break;
1850
case EDIT_CAPITALIZE: {
1851
_convert_case(CodeTextEditor::CAPITALIZE);
1852
} break;
1853
case EDIT_EVALUATE: {
1854
Expression expression;
1855
tx->begin_complex_operation();
1856
for (int caret_idx = 0; caret_idx < tx->get_caret_count(); caret_idx++) {
1857
Vector<String> lines = tx->get_selected_text(caret_idx).split("\n");
1858
PackedStringArray results;
1859
1860
for (int i = 0; i < lines.size(); i++) {
1861
const String &line = lines[i];
1862
String whitespace = line.substr(0, line.size() - line.strip_edges(true, false).size()); // Extract the whitespace at the beginning.
1863
if (expression.parse(line) == OK) {
1864
Variant result = expression.execute(Array(), Variant(), false, true);
1865
if (expression.get_error_text().is_empty()) {
1866
results.push_back(whitespace + result.get_construct_string());
1867
} else {
1868
results.push_back(line);
1869
}
1870
} else {
1871
results.push_back(line);
1872
}
1873
}
1874
tx->insert_text_at_caret(String("\n").join(results), caret_idx);
1875
}
1876
tx->end_complex_operation();
1877
} break;
1878
case EDIT_TOGGLE_WORD_WRAP: {
1879
TextEdit::LineWrappingMode wrap = code_editor->get_text_editor()->get_line_wrapping_mode();
1880
code_editor->get_text_editor()->set_line_wrapping_mode(wrap == TextEdit::LINE_WRAPPING_BOUNDARY ? TextEdit::LINE_WRAPPING_NONE : TextEdit::LINE_WRAPPING_BOUNDARY);
1881
} break;
1882
case SEARCH_FIND: {
1883
code_editor->get_find_replace_bar()->popup_search();
1884
} break;
1885
case SEARCH_FIND_NEXT: {
1886
code_editor->get_find_replace_bar()->search_next();
1887
} break;
1888
case SEARCH_FIND_PREV: {
1889
code_editor->get_find_replace_bar()->search_prev();
1890
} break;
1891
case SEARCH_REPLACE: {
1892
code_editor->get_find_replace_bar()->popup_replace();
1893
} break;
1894
case SEARCH_IN_FILES: {
1895
String selected_text = tx->get_selected_text();
1896
1897
// Yep, because it doesn't make sense to instance this dialog for every single script open...
1898
// So this will be delegated to the ScriptEditor.
1899
emit_signal(SNAME("search_in_files_requested"), selected_text);
1900
} break;
1901
case REPLACE_IN_FILES: {
1902
String selected_text = tx->get_selected_text();
1903
1904
emit_signal(SNAME("replace_in_files_requested"), selected_text);
1905
} break;
1906
case SEARCH_LOCATE_FUNCTION: {
1907
quick_open->popup_dialog(get_functions());
1908
} break;
1909
case SEARCH_GOTO_LINE: {
1910
goto_line_popup->popup_find_line(code_editor);
1911
} break;
1912
case BOOKMARK_TOGGLE: {
1913
code_editor->toggle_bookmark();
1914
} break;
1915
case BOOKMARK_GOTO_NEXT: {
1916
code_editor->goto_next_bookmark();
1917
} break;
1918
case BOOKMARK_GOTO_PREV: {
1919
code_editor->goto_prev_bookmark();
1920
} break;
1921
case BOOKMARK_REMOVE_ALL: {
1922
code_editor->remove_all_bookmarks();
1923
} break;
1924
case DEBUG_TOGGLE_BREAKPOINT: {
1925
Vector<int> sorted_carets = tx->get_sorted_carets();
1926
int last_line = -1;
1927
for (const int &c : sorted_carets) {
1928
int from = tx->get_selection_from_line(c);
1929
from += from == last_line ? 1 : 0;
1930
int to = tx->get_selection_to_line(c);
1931
if (to < from) {
1932
continue;
1933
}
1934
// Check first if there's any lines with breakpoints in the selection.
1935
bool selection_has_breakpoints = false;
1936
for (int line = from; line <= to; line++) {
1937
if (tx->is_line_breakpointed(line)) {
1938
selection_has_breakpoints = true;
1939
break;
1940
}
1941
}
1942
1943
// Set breakpoint on caret or remove all bookmarks from the selection.
1944
if (!selection_has_breakpoints) {
1945
if (tx->get_caret_line(c) != last_line) {
1946
tx->set_line_as_breakpoint(tx->get_caret_line(c), true);
1947
}
1948
} else {
1949
for (int line = from; line <= to; line++) {
1950
tx->set_line_as_breakpoint(line, false);
1951
}
1952
}
1953
last_line = to;
1954
}
1955
} break;
1956
case DEBUG_REMOVE_ALL_BREAKPOINTS: {
1957
PackedInt32Array bpoints = tx->get_breakpointed_lines();
1958
1959
for (int i = 0; i < bpoints.size(); i++) {
1960
int line = bpoints[i];
1961
bool dobreak = !tx->is_line_breakpointed(line);
1962
tx->set_line_as_breakpoint(line, dobreak);
1963
EditorDebuggerNode::get_singleton()->set_breakpoint(script->get_path(), line + 1, dobreak);
1964
}
1965
} break;
1966
case DEBUG_GOTO_NEXT_BREAKPOINT: {
1967
PackedInt32Array bpoints = tx->get_breakpointed_lines();
1968
if (bpoints.is_empty()) {
1969
return;
1970
}
1971
1972
int current_line = tx->get_caret_line();
1973
int bpoint_idx = 0;
1974
if (current_line < (int)bpoints[bpoints.size() - 1]) {
1975
while (bpoint_idx < bpoints.size() && bpoints[bpoint_idx] <= current_line) {
1976
bpoint_idx++;
1977
}
1978
}
1979
code_editor->goto_line_centered(bpoints[bpoint_idx]);
1980
} break;
1981
case DEBUG_GOTO_PREV_BREAKPOINT: {
1982
PackedInt32Array bpoints = tx->get_breakpointed_lines();
1983
if (bpoints.is_empty()) {
1984
return;
1985
}
1986
1987
int current_line = tx->get_caret_line();
1988
int bpoint_idx = bpoints.size() - 1;
1989
if (current_line > (int)bpoints[0]) {
1990
while (bpoint_idx >= 0 && bpoints[bpoint_idx] >= current_line) {
1991
bpoint_idx--;
1992
}
1993
}
1994
code_editor->goto_line_centered(bpoints[bpoint_idx]);
1995
} break;
1996
case HELP_CONTEXTUAL: {
1997
String text = tx->get_selected_text(0);
1998
if (text.is_empty()) {
1999
text = tx->get_word_under_caret(0);
2000
}
2001
if (!text.is_empty()) {
2002
emit_signal(SNAME("request_help"), text);
2003
}
2004
} break;
2005
case LOOKUP_SYMBOL: {
2006
String text = tx->get_word_under_caret(0);
2007
if (text.is_empty()) {
2008
text = tx->get_selected_text(0);
2009
}
2010
if (!text.is_empty()) {
2011
_lookup_symbol(text, tx->get_caret_line(0), tx->get_caret_column(0));
2012
}
2013
} break;
2014
case EDIT_EMOJI_AND_SYMBOL: {
2015
code_editor->get_text_editor()->show_emoji_and_symbol_picker();
2016
} break;
2017
default: {
2018
if (p_op >= EditorContextMenuPlugin::BASE_ID) {
2019
EditorContextMenuPluginManager::get_singleton()->activate_custom_option(EditorContextMenuPlugin::CONTEXT_SLOT_SCRIPT_EDITOR_CODE, p_op, tx);
2020
}
2021
}
2022
}
2023
}
2024
2025
void ScriptTextEditor::_edit_option_toggle_inline_comment() {
2026
if (script.is_null()) {
2027
return;
2028
}
2029
2030
String delimiter = "#";
2031
2032
for (const String &script_delimiter : script->get_language()->get_comment_delimiters()) {
2033
if (!script_delimiter.contains_char(' ')) {
2034
delimiter = script_delimiter;
2035
break;
2036
}
2037
}
2038
2039
code_editor->toggle_inline_comment(delimiter);
2040
}
2041
2042
void ScriptTextEditor::add_syntax_highlighter(Ref<EditorSyntaxHighlighter> p_highlighter) {
2043
ERR_FAIL_COND(p_highlighter.is_null());
2044
2045
highlighters[p_highlighter->_get_name()] = p_highlighter;
2046
highlighter_menu->add_radio_check_item(p_highlighter->_get_name());
2047
}
2048
2049
void ScriptTextEditor::set_syntax_highlighter(Ref<EditorSyntaxHighlighter> p_highlighter) {
2050
ERR_FAIL_COND(p_highlighter.is_null());
2051
2052
HashMap<String, Ref<EditorSyntaxHighlighter>>::Iterator el = highlighters.begin();
2053
while (el) {
2054
int highlighter_index = highlighter_menu->get_item_idx_from_text(el->key);
2055
highlighter_menu->set_item_checked(highlighter_index, el->value == p_highlighter);
2056
++el;
2057
}
2058
2059
CodeEdit *te = code_editor->get_text_editor();
2060
p_highlighter->_set_edited_resource(script);
2061
te->set_syntax_highlighter(p_highlighter);
2062
}
2063
2064
void ScriptTextEditor::_change_syntax_highlighter(int p_idx) {
2065
set_syntax_highlighter(highlighters[highlighter_menu->get_item_text(p_idx)]);
2066
}
2067
2068
void ScriptTextEditor::_notification(int p_what) {
2069
switch (p_what) {
2070
case NOTIFICATION_TRANSLATION_CHANGED: {
2071
if (is_ready() && is_visible_in_tree()) {
2072
_update_errors();
2073
_update_warnings();
2074
}
2075
} break;
2076
2077
case NOTIFICATION_THEME_CHANGED:
2078
if (!editor_enabled) {
2079
break;
2080
}
2081
if (is_visible_in_tree()) {
2082
_update_warnings();
2083
_update_errors();
2084
_update_background_color();
2085
}
2086
[[fallthrough]];
2087
case NOTIFICATION_ENTER_TREE: {
2088
code_editor->get_text_editor()->set_gutter_width(connection_gutter, code_editor->get_text_editor()->get_line_height());
2089
Ref<Font> code_font = get_theme_font("font", "CodeEdit");
2090
inline_color_options->add_theme_font_override("font", code_font);
2091
inline_color_options->get_popup()->add_theme_font_override("font", code_font);
2092
} break;
2093
}
2094
}
2095
2096
Control *ScriptTextEditor::get_edit_menu() {
2097
return edit_hb;
2098
}
2099
2100
void ScriptTextEditor::clear_edit_menu() {
2101
if (editor_enabled) {
2102
memdelete(edit_hb);
2103
}
2104
}
2105
2106
void ScriptTextEditor::set_find_replace_bar(FindReplaceBar *p_bar) {
2107
code_editor->set_find_replace_bar(p_bar);
2108
}
2109
2110
void ScriptTextEditor::reload(bool p_soft) {
2111
CodeEdit *te = code_editor->get_text_editor();
2112
Ref<Script> scr = script;
2113
if (scr.is_null()) {
2114
return;
2115
}
2116
scr->set_source_code(te->get_text());
2117
bool soft = p_soft || ClassDB::is_parent_class(scr->get_instance_base_type(), "EditorPlugin"); // Always soft-reload editor plugins.
2118
2119
scr->get_language()->reload_tool_script(scr, soft);
2120
}
2121
2122
PackedInt32Array ScriptTextEditor::get_breakpoints() {
2123
return code_editor->get_text_editor()->get_breakpointed_lines();
2124
}
2125
2126
void ScriptTextEditor::set_breakpoint(int p_line, bool p_enabled) {
2127
code_editor->get_text_editor()->set_line_as_breakpoint(p_line, p_enabled);
2128
}
2129
2130
void ScriptTextEditor::clear_breakpoints() {
2131
code_editor->get_text_editor()->clear_breakpointed_lines();
2132
}
2133
2134
void ScriptTextEditor::set_tooltip_request_func(const Callable &p_toolip_callback) {
2135
Variant args[1] = { this };
2136
const Variant *argp[] = { &args[0] };
2137
code_editor->get_text_editor()->set_tooltip_request_func(p_toolip_callback.bindp(argp, 1));
2138
}
2139
2140
void ScriptTextEditor::set_debugger_active(bool p_active) {
2141
}
2142
2143
Control *ScriptTextEditor::get_base_editor() const {
2144
return code_editor->get_text_editor();
2145
}
2146
2147
CodeTextEditor *ScriptTextEditor::get_code_editor() const {
2148
return code_editor;
2149
}
2150
2151
Variant ScriptTextEditor::get_drag_data_fw(const Point2 &p_point, Control *p_from) {
2152
return Variant();
2153
}
2154
2155
bool ScriptTextEditor::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const {
2156
Dictionary d = p_data;
2157
if (d.has("type") &&
2158
(String(d["type"]) == "resource" ||
2159
String(d["type"]) == "files" ||
2160
String(d["type"]) == "nodes" ||
2161
String(d["type"]) == "obj_property" ||
2162
String(d["type"]) == "files_and_dirs")) {
2163
return true;
2164
}
2165
2166
return false;
2167
}
2168
2169
static Node *_find_script_node(Node *p_current_node, const Ref<Script> &script) {
2170
if (p_current_node->get_script() == script) {
2171
return p_current_node;
2172
}
2173
2174
for (int i = 0; i < p_current_node->get_child_count(); i++) {
2175
Node *n = _find_script_node(p_current_node->get_child(i), script);
2176
if (n) {
2177
return n;
2178
}
2179
}
2180
2181
return nullptr;
2182
}
2183
2184
static String _quote_drop_data(const String &str) {
2185
// This function prepares a string for being "dropped" into the script editor.
2186
// The string can be a resource path, node path or property name.
2187
2188
const bool using_single_quotes = EDITOR_GET("text_editor/completion/use_single_quotes");
2189
2190
String escaped = str.c_escape();
2191
2192
// If string is double quoted, there is no need to escape single quotes.
2193
// We can revert the extra escaping added in c_escape().
2194
if (!using_single_quotes) {
2195
escaped = escaped.replace("\\'", "\'");
2196
}
2197
2198
return escaped.quote(using_single_quotes ? "'" : "\"");
2199
}
2200
2201
static String _get_dropped_resource_line(const Ref<Resource> &p_resource, bool p_create_field, bool p_allow_uid) {
2202
String path = p_resource->get_path();
2203
if (p_allow_uid) {
2204
ResourceUID::ID id = ResourceLoader::get_resource_uid(path);
2205
if (id != ResourceUID::INVALID_ID) {
2206
path = ResourceUID::get_singleton()->id_to_text(id);
2207
}
2208
}
2209
const bool is_script = ClassDB::is_parent_class(p_resource->get_class(), "Script");
2210
2211
if (!p_create_field) {
2212
return vformat("preload(%s)", _quote_drop_data(path));
2213
}
2214
2215
String variable_name = p_resource->get_name();
2216
if (variable_name.is_empty()) {
2217
variable_name = p_resource->get_path().get_file().get_basename();
2218
}
2219
2220
if (is_script) {
2221
variable_name = variable_name.to_pascal_case().validate_unicode_identifier();
2222
} else {
2223
variable_name = variable_name.to_snake_case().to_upper().validate_unicode_identifier();
2224
}
2225
return vformat("const %s = preload(%s)", variable_name, _quote_drop_data(path));
2226
}
2227
2228
void ScriptTextEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) {
2229
Dictionary d = p_data;
2230
2231
CodeEdit *te = code_editor->get_text_editor();
2232
Point2i pos = (p_point == Vector2(Math::INF, Math::INF)) ? Point2i(te->get_caret_line(0), te->get_caret_column(0)) : te->get_line_column_at_pos(p_point);
2233
int drop_at_line = pos.y;
2234
int drop_at_column = pos.x;
2235
int selection_index = te->get_selection_at_line_column(drop_at_line, drop_at_column);
2236
2237
bool is_empty_line = false;
2238
if (selection_index >= 0) {
2239
// Dropped on a selection, it will be replaced.
2240
drop_at_line = te->get_selection_from_line(selection_index);
2241
drop_at_column = te->get_selection_from_column(selection_index);
2242
is_empty_line = drop_at_column <= te->get_first_non_whitespace_column(drop_at_line) && te->get_selection_to_column(selection_index) == te->get_line(te->get_selection_to_line(selection_index)).length();
2243
}
2244
2245
const bool drop_modifier_pressed = Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL);
2246
const bool allow_uid = Input::get_singleton()->is_key_pressed(Key::SHIFT) != bool(EDITOR_GET("text_editor/behavior/files/drop_preload_resources_as_uid"));
2247
const String &line = te->get_line(drop_at_line);
2248
2249
if (selection_index < 0) {
2250
is_empty_line = line.is_empty() || te->get_first_non_whitespace_column(drop_at_line) == line.length();
2251
}
2252
2253
String text_to_drop;
2254
bool add_new_line = false;
2255
2256
const String type = d.get("type", "");
2257
if (type == "resource") {
2258
Ref<Resource> resource = d["resource"];
2259
if (resource.is_null()) {
2260
return;
2261
}
2262
2263
const String &path = resource->get_path();
2264
if (path.is_empty() || path.ends_with("::")) {
2265
String warning = TTR("The resource does not have a valid path because it has not been saved.\nPlease save the scene or resource that contains this resource and try again.");
2266
EditorToaster::get_singleton()->popup_str(warning, EditorToaster::SEVERITY_ERROR);
2267
return;
2268
}
2269
2270
if (drop_modifier_pressed) {
2271
if (resource->is_built_in()) {
2272
String warning = TTR("Preloading internal resources is not supported.");
2273
EditorToaster::get_singleton()->popup_str(warning, EditorToaster::SEVERITY_ERROR);
2274
} else {
2275
text_to_drop = _get_dropped_resource_line(resource, is_empty_line, allow_uid);
2276
}
2277
} else {
2278
text_to_drop = _quote_drop_data(path);
2279
}
2280
}
2281
2282
if (type == "files" || type == "files_and_dirs") {
2283
const PackedStringArray files = d["files"];
2284
PackedStringArray parts;
2285
2286
for (const String &path : files) {
2287
if (drop_modifier_pressed && ResourceLoader::exists(path)) {
2288
Ref<Resource> resource = ResourceLoader::load(path);
2289
if (resource.is_null()) {
2290
// Resource exists, but failed to load. We need only path and name, so we can use a dummy Resource instead.
2291
resource.instantiate();
2292
resource->set_path_cache(path);
2293
}
2294
parts.append(_get_dropped_resource_line(resource, is_empty_line, allow_uid));
2295
} else {
2296
parts.append(_quote_drop_data(path));
2297
}
2298
}
2299
String join_string;
2300
if (is_empty_line) {
2301
int indent_level = te->get_indent_level(drop_at_line);
2302
if (te->is_indent_using_spaces()) {
2303
join_string = "\n" + String(" ").repeat(indent_level);
2304
} else {
2305
join_string = "\n" + String("\t").repeat(indent_level / te->get_tab_size());
2306
}
2307
} else {
2308
join_string = ", ";
2309
}
2310
text_to_drop = join_string.join(parts);
2311
if (is_empty_line) {
2312
text_to_drop += join_string;
2313
}
2314
}
2315
2316
if (type == "nodes") {
2317
Node *scene_root = get_tree()->get_edited_scene_root();
2318
if (!scene_root) {
2319
EditorNode::get_singleton()->show_warning(TTR("Can't drop nodes without an open scene."));
2320
return;
2321
}
2322
2323
if (!ClassDB::is_parent_class(script->get_instance_base_type(), "Node")) {
2324
EditorToaster::get_singleton()->popup_str(vformat(TTR("Can't drop nodes because script '%s' does not inherit Node."), get_name()), EditorToaster::SEVERITY_WARNING);
2325
return;
2326
}
2327
2328
Node *sn = _find_script_node(scene_root, script);
2329
if (!sn) {
2330
sn = scene_root;
2331
}
2332
2333
Array nodes = d["nodes"];
2334
2335
if (drop_modifier_pressed) {
2336
const bool use_type = EDITOR_GET("text_editor/completion/add_type_hints");
2337
add_new_line = !is_empty_line && drop_at_column != 0;
2338
2339
for (int i = 0; i < nodes.size(); i++) {
2340
NodePath np = nodes[i];
2341
Node *node = get_node(np);
2342
if (!node) {
2343
continue;
2344
}
2345
2346
bool is_unique = node->is_unique_name_in_owner() && (node->get_owner() == sn || node->get_owner() == sn->get_owner());
2347
String path = is_unique ? String(node->get_name()) : String(sn->get_path_to(node));
2348
for (const String &segment : path.split("/")) {
2349
if (!segment.is_valid_unicode_identifier()) {
2350
path = _quote_drop_data(path);
2351
break;
2352
}
2353
}
2354
2355
String variable_name = String(node->get_name()).to_snake_case().validate_unicode_identifier();
2356
if (use_type) {
2357
StringName class_name = node->get_class_name();
2358
Ref<Script> node_script = node->get_script();
2359
if (node_script.is_valid()) {
2360
StringName global_node_script_name = node_script->get_global_name();
2361
if (global_node_script_name != StringName()) {
2362
class_name = global_node_script_name;
2363
}
2364
}
2365
text_to_drop += vformat("@onready var %s: %s = %c%s", variable_name, class_name, is_unique ? '%' : '$', path);
2366
} else {
2367
text_to_drop += vformat("@onready var %s = %c%s", variable_name, is_unique ? '%' : '$', path);
2368
}
2369
if (i < nodes.size() - 1) {
2370
text_to_drop += "\n";
2371
}
2372
}
2373
2374
if (is_empty_line || drop_at_column == 0) {
2375
text_to_drop += "\n";
2376
}
2377
} else {
2378
for (int i = 0; i < nodes.size(); i++) {
2379
if (i > 0) {
2380
text_to_drop += ", ";
2381
}
2382
2383
NodePath np = nodes[i];
2384
Node *node = get_node(np);
2385
if (!node) {
2386
continue;
2387
}
2388
2389
bool is_unique = node->is_unique_name_in_owner() && (node->get_owner() == sn || node->get_owner() == sn->get_owner());
2390
String path = is_unique ? String(node->get_name()) : String(sn->get_path_to(node));
2391
for (const String &segment : path.split("/")) {
2392
if (!segment.is_valid_ascii_identifier()) {
2393
path = _quote_drop_data(path);
2394
break;
2395
}
2396
}
2397
text_to_drop += (is_unique ? "%" : "$") + path;
2398
}
2399
}
2400
}
2401
2402
if (type == "obj_property") {
2403
bool add_literal = EDITOR_GET("text_editor/completion/add_node_path_literals");
2404
text_to_drop = add_literal ? "^" : "";
2405
// It is unclear whether properties may contain single or double quotes.
2406
// Assume here that double-quotes may not exist. We are escaping single-quotes if necessary.
2407
text_to_drop += _quote_drop_data(String(d["property"]));
2408
}
2409
2410
if (text_to_drop.is_empty()) {
2411
return;
2412
}
2413
2414
// Remove drag caret before any actions so it is not included in undo.
2415
te->remove_drag_caret();
2416
te->begin_complex_operation();
2417
if (selection_index >= 0) {
2418
te->delete_selection(selection_index);
2419
}
2420
te->remove_secondary_carets();
2421
te->deselect();
2422
te->set_caret_line(drop_at_line);
2423
if (add_new_line) {
2424
te->set_caret_column(te->get_line(drop_at_line).length());
2425
text_to_drop = "\n" + text_to_drop;
2426
} else {
2427
te->set_caret_column(drop_at_column);
2428
}
2429
te->insert_text_at_caret(text_to_drop);
2430
te->end_complex_operation();
2431
te->grab_focus();
2432
}
2433
2434
void ScriptTextEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) {
2435
Ref<InputEventMouseButton> mb = ev;
2436
Ref<InputEventKey> k = ev;
2437
Point2 local_pos;
2438
bool create_menu = false;
2439
2440
CodeEdit *tx = code_editor->get_text_editor();
2441
if (mb.is_valid() && mb->get_button_index() == MouseButton::RIGHT && mb->is_pressed()) {
2442
local_pos = mb->get_global_position() - tx->get_global_position();
2443
create_menu = true;
2444
} else if (k.is_valid() && k->is_action("ui_menu", true)) {
2445
tx->adjust_viewport_to_caret(0);
2446
local_pos = tx->get_caret_draw_pos(0);
2447
create_menu = true;
2448
}
2449
2450
if (create_menu) {
2451
tx->apply_ime();
2452
2453
Point2i pos = tx->get_line_column_at_pos(local_pos);
2454
int mouse_line = pos.y;
2455
int mouse_column = pos.x;
2456
2457
tx->set_move_caret_on_right_click_enabled(EDITOR_GET("text_editor/behavior/navigation/move_caret_on_right_click"));
2458
int selection_clicked = -1;
2459
if (tx->is_move_caret_on_right_click_enabled()) {
2460
selection_clicked = tx->get_selection_at_line_column(mouse_line, mouse_column, true);
2461
if (selection_clicked < 0) {
2462
tx->deselect();
2463
tx->remove_secondary_carets();
2464
selection_clicked = 0;
2465
tx->set_caret_line(mouse_line, false, false, -1);
2466
tx->set_caret_column(mouse_column);
2467
}
2468
}
2469
2470
String word_at_pos = tx->get_lookup_word(mouse_line, mouse_column);
2471
if (word_at_pos.is_empty()) {
2472
word_at_pos = tx->get_word_under_caret(selection_clicked);
2473
}
2474
if (word_at_pos.is_empty()) {
2475
word_at_pos = tx->get_selected_text(selection_clicked);
2476
}
2477
2478
bool has_color = (word_at_pos == "Color");
2479
bool foldable = tx->can_fold_line(mouse_line) || tx->is_line_folded(mouse_line);
2480
bool open_docs = false;
2481
bool goto_definition = false;
2482
2483
if (ScriptServer::is_global_class(word_at_pos) || word_at_pos.is_resource_file()) {
2484
open_docs = true;
2485
} else {
2486
Node *base = get_tree()->get_edited_scene_root();
2487
if (base) {
2488
base = _find_node_for_script(base, base, script);
2489
}
2490
ScriptLanguage::LookupResult result;
2491
if (script->get_language()->lookup_code(tx->get_text_for_symbol_lookup(), word_at_pos, script->get_path(), base, result) == OK) {
2492
open_docs = true;
2493
}
2494
}
2495
2496
if (has_color) {
2497
String line = tx->get_line(mouse_line);
2498
color_position.x = mouse_line;
2499
2500
int begin = -1;
2501
int end = -1;
2502
enum EXPRESSION_PATTERNS {
2503
NOT_PARSED,
2504
RGBA_PARAMETER, // Color(float,float,float) or Color(float,float,float,float)
2505
COLOR_NAME, // Color.COLOR_NAME
2506
} expression_pattern = NOT_PARSED;
2507
2508
for (int i = mouse_column; i < line.length(); i++) {
2509
if (line[i] == '(') {
2510
if (expression_pattern == NOT_PARSED) {
2511
begin = i;
2512
expression_pattern = RGBA_PARAMETER;
2513
} else {
2514
// Method call or '(' appearing twice.
2515
expression_pattern = NOT_PARSED;
2516
2517
break;
2518
}
2519
} else if (expression_pattern == RGBA_PARAMETER && line[i] == ')' && end < 0) {
2520
end = i + 1;
2521
2522
break;
2523
} else if (expression_pattern == NOT_PARSED && line[i] == '.') {
2524
begin = i;
2525
expression_pattern = COLOR_NAME;
2526
} else if (expression_pattern == COLOR_NAME && end < 0 && (line[i] == ' ' || line[i] == '\t')) {
2527
// Including '.' and spaces.
2528
continue;
2529
} else if (expression_pattern == COLOR_NAME && !(line[i] == '_' || ('A' <= line[i] && line[i] <= 'Z'))) {
2530
end = i;
2531
2532
break;
2533
}
2534
}
2535
2536
switch (expression_pattern) {
2537
case RGBA_PARAMETER: {
2538
color_args = line.substr(begin, end - begin);
2539
String stripped = color_args.remove_chars(" \t()");
2540
PackedFloat64Array color = stripped.split_floats(",");
2541
if (color.size() > 2) {
2542
float alpha = color.size() > 3 ? color[3] : 1.0f;
2543
color_picker->set_pick_color(Color(color[0], color[1], color[2], alpha));
2544
}
2545
} break;
2546
case COLOR_NAME: {
2547
if (end < 0) {
2548
end = line.length();
2549
}
2550
color_args = line.substr(begin, end - begin);
2551
const String color_name = color_args.remove_chars(" \t.");
2552
const int color_index = Color::find_named_color(color_name);
2553
if (0 <= color_index) {
2554
const Color color_constant = Color::get_named_color(color_index);
2555
color_picker->set_pick_color(color_constant);
2556
} else {
2557
has_color = false;
2558
}
2559
} break;
2560
default:
2561
has_color = false;
2562
break;
2563
}
2564
if (has_color) {
2565
color_panel->set_position(get_screen_position() + local_pos);
2566
color_position.y = begin;
2567
color_position.z = end;
2568
}
2569
}
2570
_make_context_menu(tx->has_selection(), has_color, foldable, open_docs, goto_definition, local_pos);
2571
}
2572
}
2573
2574
void ScriptTextEditor::_color_changed(const Color &p_color) {
2575
String new_args;
2576
const int decimals = 3;
2577
if (p_color.a == 1.0f) {
2578
new_args = String("(" + String::num(p_color.r, decimals) + ", " + String::num(p_color.g, decimals) + ", " + String::num(p_color.b, decimals) + ")");
2579
} else {
2580
new_args = String("(" + String::num(p_color.r, decimals) + ", " + String::num(p_color.g, decimals) + ", " + String::num(p_color.b, decimals) + ", " + String::num(p_color.a, decimals) + ")");
2581
}
2582
2583
String line = code_editor->get_text_editor()->get_line(color_position.x);
2584
String line_with_replaced_args = line.substr(0, color_position.y) + line.substr(color_position.y, color_position.z - color_position.y).replace(color_args, new_args) + line.substr(color_position.z);
2585
2586
color_args = new_args;
2587
code_editor->get_text_editor()->begin_complex_operation();
2588
code_editor->get_text_editor()->set_line(color_position.x, line_with_replaced_args);
2589
code_editor->get_text_editor()->end_complex_operation();
2590
}
2591
2592
void ScriptTextEditor::_prepare_edit_menu() {
2593
const CodeEdit *tx = code_editor->get_text_editor();
2594
PopupMenu *popup = edit_menu->get_popup();
2595
popup->set_item_disabled(popup->get_item_index(EDIT_UNDO), !tx->has_undo());
2596
popup->set_item_disabled(popup->get_item_index(EDIT_REDO), !tx->has_redo());
2597
}
2598
2599
void ScriptTextEditor::_make_context_menu(bool p_selection, bool p_color, bool p_foldable, bool p_open_docs, bool p_goto_definition, Vector2 p_pos) {
2600
context_menu->clear();
2601
if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_EMOJI_AND_SYMBOL_PICKER)) {
2602
context_menu->add_item(TTRC("Emoji & Symbols"), EDIT_EMOJI_AND_SYMBOL);
2603
context_menu->add_separator();
2604
}
2605
context_menu->add_shortcut(ED_GET_SHORTCUT("ui_undo"), EDIT_UNDO);
2606
context_menu->add_shortcut(ED_GET_SHORTCUT("ui_redo"), EDIT_REDO);
2607
2608
context_menu->add_separator();
2609
context_menu->add_shortcut(ED_GET_SHORTCUT("ui_cut"), EDIT_CUT);
2610
context_menu->add_shortcut(ED_GET_SHORTCUT("ui_copy"), EDIT_COPY);
2611
context_menu->add_shortcut(ED_GET_SHORTCUT("ui_paste"), EDIT_PASTE);
2612
2613
context_menu->add_separator();
2614
context_menu->add_shortcut(ED_GET_SHORTCUT("ui_text_select_all"), EDIT_SELECT_ALL);
2615
2616
context_menu->add_separator();
2617
context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/indent"), EDIT_INDENT);
2618
context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/unindent"), EDIT_UNINDENT);
2619
context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_comment"), EDIT_TOGGLE_COMMENT);
2620
context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_bookmark"), BOOKMARK_TOGGLE);
2621
2622
if (p_selection) {
2623
context_menu->add_separator();
2624
context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/convert_to_uppercase"), EDIT_TO_UPPERCASE);
2625
context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/convert_to_lowercase"), EDIT_TO_LOWERCASE);
2626
context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/evaluate_selection"), EDIT_EVALUATE);
2627
context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/create_code_region"), EDIT_CREATE_CODE_REGION);
2628
}
2629
if (p_foldable) {
2630
context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_fold_line"), EDIT_TOGGLE_FOLD_LINE);
2631
}
2632
2633
if (p_color || p_open_docs || p_goto_definition) {
2634
context_menu->add_separator();
2635
if (p_open_docs) {
2636
context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_symbol"), LOOKUP_SYMBOL);
2637
}
2638
if (p_color) {
2639
context_menu->add_item(TTRC("Pick Color"), EDIT_PICK_COLOR);
2640
}
2641
}
2642
2643
const PackedStringArray paths = { String(code_editor->get_text_editor()->get_path()) };
2644
EditorContextMenuPluginManager::get_singleton()->add_options_from_plugins(context_menu, EditorContextMenuPlugin::CONTEXT_SLOT_SCRIPT_EDITOR_CODE, paths);
2645
2646
const CodeEdit *tx = code_editor->get_text_editor();
2647
context_menu->set_item_disabled(context_menu->get_item_index(EDIT_UNDO), !tx->has_undo());
2648
context_menu->set_item_disabled(context_menu->get_item_index(EDIT_REDO), !tx->has_redo());
2649
2650
context_menu->set_position(get_screen_position() + p_pos);
2651
context_menu->reset_size();
2652
context_menu->popup();
2653
}
2654
2655
void ScriptTextEditor::_enable_code_editor() {
2656
ERR_FAIL_COND(code_editor->get_parent());
2657
2658
VSplitContainer *editor_box = memnew(VSplitContainer);
2659
add_child(editor_box);
2660
editor_box->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
2661
editor_box->set_v_size_flags(SIZE_EXPAND_FILL);
2662
2663
editor_box->add_child(code_editor);
2664
code_editor->connect("show_errors_panel", callable_mp(this, &ScriptTextEditor::_show_errors_panel));
2665
code_editor->connect("show_warnings_panel", callable_mp(this, &ScriptTextEditor::_show_warnings_panel));
2666
code_editor->connect("validate_script", callable_mp(this, &ScriptTextEditor::_validate_script));
2667
code_editor->connect("load_theme_settings", callable_mp(this, &ScriptTextEditor::_load_theme_settings));
2668
code_editor->get_text_editor()->connect("symbol_lookup", callable_mp(this, &ScriptTextEditor::_lookup_symbol));
2669
code_editor->get_text_editor()->connect("symbol_hovered", callable_mp(this, &ScriptTextEditor::_show_symbol_tooltip));
2670
code_editor->get_text_editor()->connect("symbol_validate", callable_mp(this, &ScriptTextEditor::_validate_symbol));
2671
code_editor->get_text_editor()->connect("gutter_added", callable_mp(this, &ScriptTextEditor::_update_gutter_indexes));
2672
code_editor->get_text_editor()->connect("gutter_removed", callable_mp(this, &ScriptTextEditor::_update_gutter_indexes));
2673
code_editor->get_text_editor()->connect("gutter_clicked", callable_mp(this, &ScriptTextEditor::_gutter_clicked));
2674
code_editor->get_text_editor()->connect("_fold_line_updated", callable_mp(this, &ScriptTextEditor::_update_background_color));
2675
code_editor->get_text_editor()->connect(SceneStringName(gui_input), callable_mp(this, &ScriptTextEditor::_text_edit_gui_input));
2676
code_editor->show_toggle_files_button();
2677
_update_gutter_indexes();
2678
2679
editor_box->add_child(warnings_panel);
2680
warnings_panel->add_theme_font_override(
2681
"normal_font", EditorNode::get_singleton()->get_editor_theme()->get_font(SNAME("main"), EditorStringName(EditorFonts)));
2682
warnings_panel->add_theme_font_size_override(
2683
"normal_font_size", EditorNode::get_singleton()->get_editor_theme()->get_font_size(SNAME("main_size"), EditorStringName(EditorFonts)));
2684
warnings_panel->connect("meta_clicked", callable_mp(this, &ScriptTextEditor::_warning_clicked));
2685
2686
editor_box->add_child(errors_panel);
2687
errors_panel->add_theme_font_override(
2688
"normal_font", EditorNode::get_singleton()->get_editor_theme()->get_font(SNAME("main"), EditorStringName(EditorFonts)));
2689
errors_panel->add_theme_font_size_override(
2690
"normal_font_size", EditorNode::get_singleton()->get_editor_theme()->get_font_size(SNAME("main_size"), EditorStringName(EditorFonts)));
2691
errors_panel->connect("meta_clicked", callable_mp(this, &ScriptTextEditor::_error_clicked));
2692
2693
add_child(context_menu);
2694
context_menu->connect(SceneStringName(id_pressed), callable_mp(this, &ScriptTextEditor::_edit_option));
2695
2696
add_child(color_panel);
2697
2698
color_picker = memnew(ColorPicker);
2699
color_picker->set_deferred_mode(true);
2700
color_picker->connect("color_changed", callable_mp(this, &ScriptTextEditor::_color_changed));
2701
color_panel->connect("about_to_popup", callable_mp(EditorNode::get_singleton(), &EditorNode::setup_color_picker).bind(color_picker));
2702
2703
color_panel->add_child(color_picker);
2704
2705
quick_open = memnew(ScriptEditorQuickOpen);
2706
quick_open->set_title(TTRC("Go to Function"));
2707
quick_open->connect("goto_line", callable_mp(this, &ScriptTextEditor::_goto_line));
2708
add_child(quick_open);
2709
2710
goto_line_popup = memnew(GotoLinePopup);
2711
add_child(goto_line_popup);
2712
2713
add_child(connection_info_dialog);
2714
2715
edit_hb->add_child(edit_menu);
2716
edit_menu->connect("about_to_popup", callable_mp(this, &ScriptTextEditor::_prepare_edit_menu));
2717
edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("ui_undo"), EDIT_UNDO);
2718
edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("ui_redo"), EDIT_REDO);
2719
edit_menu->get_popup()->add_separator();
2720
edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("ui_cut"), EDIT_CUT);
2721
edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("ui_copy"), EDIT_COPY);
2722
edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("ui_paste"), EDIT_PASTE);
2723
edit_menu->get_popup()->add_separator();
2724
edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("ui_text_select_all"), EDIT_SELECT_ALL);
2725
edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/duplicate_selection"), EDIT_DUPLICATE_SELECTION);
2726
edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/duplicate_lines"), EDIT_DUPLICATE_LINES);
2727
edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/evaluate_selection"), EDIT_EVALUATE);
2728
edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_word_wrap"), EDIT_TOGGLE_WORD_WRAP);
2729
edit_menu->get_popup()->add_separator();
2730
{
2731
PopupMenu *sub_menu = memnew(PopupMenu);
2732
sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/move_up"), EDIT_MOVE_LINE_UP);
2733
sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/move_down"), EDIT_MOVE_LINE_DOWN);
2734
sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/indent"), EDIT_INDENT);
2735
sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/unindent"), EDIT_UNINDENT);
2736
sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/delete_line"), EDIT_DELETE_LINE);
2737
sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_comment"), EDIT_TOGGLE_COMMENT);
2738
sub_menu->connect(SceneStringName(id_pressed), callable_mp(this, &ScriptTextEditor::_edit_option));
2739
edit_menu->get_popup()->add_submenu_node_item(TTRC("Line"), sub_menu);
2740
}
2741
{
2742
PopupMenu *sub_menu = memnew(PopupMenu);
2743
sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_fold_line"), EDIT_TOGGLE_FOLD_LINE);
2744
sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/fold_all_lines"), EDIT_FOLD_ALL_LINES);
2745
sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/unfold_all_lines"), EDIT_UNFOLD_ALL_LINES);
2746
sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/create_code_region"), EDIT_CREATE_CODE_REGION);
2747
sub_menu->connect(SceneStringName(id_pressed), callable_mp(this, &ScriptTextEditor::_edit_option));
2748
edit_menu->get_popup()->add_submenu_node_item(TTRC("Folding"), sub_menu);
2749
}
2750
edit_menu->get_popup()->add_separator();
2751
edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("ui_text_completion_query"), EDIT_COMPLETE);
2752
edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/trim_trailing_whitespace"), EDIT_TRIM_TRAILING_WHITESAPCE);
2753
edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/trim_final_newlines"), EDIT_TRIM_FINAL_NEWLINES);
2754
{
2755
PopupMenu *sub_menu = memnew(PopupMenu);
2756
sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/convert_indent_to_spaces"), EDIT_CONVERT_INDENT_TO_SPACES);
2757
sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/convert_indent_to_tabs"), EDIT_CONVERT_INDENT_TO_TABS);
2758
sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/auto_indent"), EDIT_AUTO_INDENT);
2759
sub_menu->connect(SceneStringName(id_pressed), callable_mp(this, &ScriptTextEditor::_edit_option));
2760
edit_menu->get_popup()->add_submenu_node_item(TTRC("Indentation"), sub_menu);
2761
}
2762
edit_menu->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &ScriptTextEditor::_edit_option));
2763
edit_menu->get_popup()->add_separator();
2764
{
2765
PopupMenu *sub_menu = memnew(PopupMenu);
2766
sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/convert_to_uppercase"), EDIT_TO_UPPERCASE);
2767
sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/convert_to_lowercase"), EDIT_TO_LOWERCASE);
2768
sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/capitalize"), EDIT_CAPITALIZE);
2769
sub_menu->connect(SceneStringName(id_pressed), callable_mp(this, &ScriptTextEditor::_edit_option));
2770
edit_menu->get_popup()->add_submenu_node_item(TTRC("Convert Case"), sub_menu);
2771
}
2772
edit_menu->get_popup()->add_submenu_node_item(TTRC("Syntax Highlighter"), highlighter_menu);
2773
highlighter_menu->connect(SceneStringName(id_pressed), callable_mp(this, &ScriptTextEditor::_change_syntax_highlighter));
2774
2775
edit_hb->add_child(search_menu);
2776
search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/find"), SEARCH_FIND);
2777
search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/find_next"), SEARCH_FIND_NEXT);
2778
search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/find_previous"), SEARCH_FIND_PREV);
2779
search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/replace"), SEARCH_REPLACE);
2780
search_menu->get_popup()->add_separator();
2781
search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("editor/find_in_files"), SEARCH_IN_FILES);
2782
search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/replace_in_files"), REPLACE_IN_FILES);
2783
search_menu->get_popup()->add_separator();
2784
search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/contextual_help"), HELP_CONTEXTUAL);
2785
search_menu->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &ScriptTextEditor::_edit_option));
2786
2787
_load_theme_settings();
2788
2789
edit_hb->add_child(goto_menu);
2790
goto_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_function"), SEARCH_LOCATE_FUNCTION);
2791
goto_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_line"), SEARCH_GOTO_LINE);
2792
goto_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_symbol"), LOOKUP_SYMBOL);
2793
goto_menu->get_popup()->add_separator();
2794
2795
goto_menu->get_popup()->add_submenu_node_item(TTRC("Bookmarks"), bookmarks_menu);
2796
_update_bookmark_list();
2797
bookmarks_menu->connect("about_to_popup", callable_mp(this, &ScriptTextEditor::_update_bookmark_list));
2798
bookmarks_menu->connect("index_pressed", callable_mp(this, &ScriptTextEditor::_bookmark_item_pressed));
2799
2800
goto_menu->get_popup()->add_submenu_node_item(TTRC("Breakpoints"), breakpoints_menu);
2801
_update_breakpoint_list();
2802
breakpoints_menu->connect("about_to_popup", callable_mp(this, &ScriptTextEditor::_update_breakpoint_list));
2803
breakpoints_menu->connect("index_pressed", callable_mp(this, &ScriptTextEditor::_breakpoint_item_pressed));
2804
2805
goto_menu->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &ScriptTextEditor::_edit_option));
2806
}
2807
2808
ScriptTextEditor::ScriptTextEditor() {
2809
code_editor = memnew(CodeTextEditor);
2810
code_editor->set_toggle_list_control(ScriptEditor::get_singleton()->get_left_list_split());
2811
code_editor->add_theme_constant_override("separation", 2);
2812
code_editor->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
2813
code_editor->set_code_complete_func(_code_complete_scripts, this);
2814
code_editor->set_v_size_flags(SIZE_EXPAND_FILL);
2815
2816
code_editor->get_text_editor()->set_draw_breakpoints_gutter(true);
2817
code_editor->get_text_editor()->set_draw_executing_lines_gutter(true);
2818
code_editor->get_text_editor()->connect("breakpoint_toggled", callable_mp(this, &ScriptTextEditor::_breakpoint_toggled));
2819
code_editor->get_text_editor()->connect("caret_changed", callable_mp(this, &ScriptTextEditor::_on_caret_moved));
2820
code_editor->connect("navigation_preview_ended", callable_mp(this, &ScriptTextEditor::_on_caret_moved));
2821
2822
connection_gutter = 1;
2823
code_editor->get_text_editor()->add_gutter(connection_gutter);
2824
code_editor->get_text_editor()->set_gutter_name(connection_gutter, "connection_gutter");
2825
code_editor->get_text_editor()->set_gutter_draw(connection_gutter, false);
2826
code_editor->get_text_editor()->set_gutter_overwritable(connection_gutter, true);
2827
code_editor->get_text_editor()->set_gutter_type(connection_gutter, TextEdit::GUTTER_TYPE_ICON);
2828
2829
warnings_panel = memnew(RichTextLabel);
2830
warnings_panel->set_custom_minimum_size(Size2(0, 100 * EDSCALE));
2831
warnings_panel->set_h_size_flags(SIZE_EXPAND_FILL);
2832
warnings_panel->set_meta_underline(true);
2833
warnings_panel->set_selection_enabled(true);
2834
warnings_panel->set_context_menu_enabled(true);
2835
warnings_panel->set_focus_mode(FOCUS_CLICK);
2836
warnings_panel->hide();
2837
2838
errors_panel = memnew(RichTextLabel);
2839
errors_panel->set_custom_minimum_size(Size2(0, 100 * EDSCALE));
2840
errors_panel->set_h_size_flags(SIZE_EXPAND_FILL);
2841
errors_panel->set_meta_underline(true);
2842
errors_panel->set_selection_enabled(true);
2843
errors_panel->set_context_menu_enabled(true);
2844
errors_panel->set_focus_mode(FOCUS_CLICK);
2845
errors_panel->hide();
2846
2847
update_settings();
2848
2849
code_editor->get_text_editor()->set_symbol_lookup_on_click_enabled(true);
2850
code_editor->get_text_editor()->set_symbol_tooltip_on_hover_enabled(true);
2851
code_editor->get_text_editor()->set_context_menu_enabled(false);
2852
2853
context_menu = memnew(PopupMenu);
2854
2855
color_panel = memnew(PopupPanel);
2856
2857
edit_hb = memnew(HBoxContainer);
2858
2859
edit_menu = memnew(MenuButton);
2860
edit_menu->set_flat(false);
2861
edit_menu->set_theme_type_variation("FlatMenuButton");
2862
edit_menu->set_text(TTRC("Edit"));
2863
edit_menu->set_switch_on_hover(true);
2864
edit_menu->set_shortcut_context(this);
2865
2866
highlighter_menu = memnew(PopupMenu);
2867
2868
Ref<EditorPlainTextSyntaxHighlighter> plain_highlighter;
2869
plain_highlighter.instantiate();
2870
add_syntax_highlighter(plain_highlighter);
2871
2872
Ref<EditorStandardSyntaxHighlighter> highlighter;
2873
highlighter.instantiate();
2874
add_syntax_highlighter(highlighter);
2875
set_syntax_highlighter(highlighter);
2876
2877
search_menu = memnew(MenuButton);
2878
search_menu->set_flat(false);
2879
search_menu->set_theme_type_variation("FlatMenuButton");
2880
search_menu->set_text(TTRC("Search"));
2881
search_menu->set_switch_on_hover(true);
2882
search_menu->set_shortcut_context(this);
2883
2884
goto_menu = memnew(MenuButton);
2885
goto_menu->set_flat(false);
2886
goto_menu->set_theme_type_variation("FlatMenuButton");
2887
goto_menu->set_text(TTRC("Go To"));
2888
goto_menu->set_switch_on_hover(true);
2889
goto_menu->set_shortcut_context(this);
2890
2891
bookmarks_menu = memnew(PopupMenu);
2892
breakpoints_menu = memnew(PopupMenu);
2893
2894
inline_color_popup = memnew(PopupPanel);
2895
add_child(inline_color_popup);
2896
2897
inline_color_picker = memnew(ColorPicker);
2898
inline_color_picker->set_mouse_filter(MOUSE_FILTER_STOP);
2899
inline_color_picker->set_deferred_mode(true);
2900
inline_color_picker->set_hex_visible(false);
2901
inline_color_picker->connect("color_changed", callable_mp(this, &ScriptTextEditor::_picker_color_changed));
2902
inline_color_popup->add_child(inline_color_picker);
2903
2904
inline_color_options = memnew(OptionButton);
2905
inline_color_options->set_h_size_flags(SIZE_FILL);
2906
inline_color_options->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS);
2907
inline_color_options->set_fit_to_longest_item(false);
2908
inline_color_options->connect("item_selected", callable_mp(this, &ScriptTextEditor::_update_color_text).unbind(1));
2909
inline_color_picker->get_slider_container()->add_sibling(inline_color_options);
2910
2911
connection_info_dialog = memnew(ConnectionInfoDialog);
2912
2913
SET_DRAG_FORWARDING_GCD(code_editor->get_text_editor(), ScriptTextEditor);
2914
}
2915
2916
ScriptTextEditor::~ScriptTextEditor() {
2917
highlighters.clear();
2918
2919
if (!editor_enabled) {
2920
memdelete(code_editor);
2921
memdelete(warnings_panel);
2922
memdelete(errors_panel);
2923
memdelete(context_menu);
2924
memdelete(color_panel);
2925
memdelete(edit_hb);
2926
memdelete(edit_menu);
2927
memdelete(highlighter_menu);
2928
memdelete(search_menu);
2929
memdelete(goto_menu);
2930
memdelete(bookmarks_menu);
2931
memdelete(breakpoints_menu);
2932
memdelete(connection_info_dialog);
2933
}
2934
}
2935
2936
static ScriptEditorBase *create_editor(const Ref<Resource> &p_resource) {
2937
if (Object::cast_to<Script>(*p_resource)) {
2938
return memnew(ScriptTextEditor);
2939
}
2940
return nullptr;
2941
}
2942
2943
void ScriptTextEditor::register_editor() {
2944
ED_SHORTCUT("script_text_editor/move_up", TTRC("Move Up"), KeyModifierMask::ALT | Key::UP);
2945
ED_SHORTCUT("script_text_editor/move_down", TTRC("Move Down"), KeyModifierMask::ALT | Key::DOWN);
2946
ED_SHORTCUT("script_text_editor/delete_line", TTRC("Delete Line"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::K);
2947
2948
// Leave these at zero, same can be accomplished with tab/shift-tab, including selection.
2949
// The next/previous in history shortcut in this case makes a lot more sense.
2950
2951
ED_SHORTCUT("script_text_editor/indent", TTRC("Indent"), Key::NONE);
2952
ED_SHORTCUT("script_text_editor/unindent", TTRC("Unindent"), KeyModifierMask::SHIFT | Key::TAB);
2953
ED_SHORTCUT_ARRAY("script_text_editor/toggle_comment", TTRC("Toggle Comment"), { int32_t(KeyModifierMask::CMD_OR_CTRL | Key::K), int32_t(KeyModifierMask::CMD_OR_CTRL | Key::SLASH), int32_t(KeyModifierMask::CMD_OR_CTRL | Key::KP_DIVIDE), int32_t(KeyModifierMask::CMD_OR_CTRL | Key::NUMBERSIGN) });
2954
ED_SHORTCUT("script_text_editor/toggle_fold_line", TTRC("Fold/Unfold Line"), KeyModifierMask::ALT | Key::F);
2955
ED_SHORTCUT_OVERRIDE("script_text_editor/toggle_fold_line", "macos", KeyModifierMask::CTRL | KeyModifierMask::META | Key::F);
2956
ED_SHORTCUT("script_text_editor/fold_all_lines", TTRC("Fold All Lines"), Key::NONE);
2957
ED_SHORTCUT("script_text_editor/create_code_region", TTRC("Create Code Region"), KeyModifierMask::ALT | Key::R);
2958
ED_SHORTCUT("script_text_editor/unfold_all_lines", TTRC("Unfold All Lines"), Key::NONE);
2959
ED_SHORTCUT("script_text_editor/duplicate_selection", TTRC("Duplicate Selection"), KeyModifierMask::SHIFT | KeyModifierMask::CTRL | Key::D);
2960
ED_SHORTCUT_OVERRIDE("script_text_editor/duplicate_selection", "macos", KeyModifierMask::SHIFT | KeyModifierMask::META | Key::C);
2961
ED_SHORTCUT("script_text_editor/duplicate_lines", TTRC("Duplicate Lines"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | Key::DOWN);
2962
ED_SHORTCUT_OVERRIDE("script_text_editor/duplicate_lines", "macos", KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::DOWN);
2963
ED_SHORTCUT("script_text_editor/evaluate_selection", TTRC("Evaluate Selection"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::E);
2964
ED_SHORTCUT("script_text_editor/toggle_word_wrap", TTRC("Toggle Word Wrap"), KeyModifierMask::ALT | Key::Z);
2965
ED_SHORTCUT("script_text_editor/trim_trailing_whitespace", TTRC("Trim Trailing Whitespace"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | Key::T);
2966
ED_SHORTCUT("script_text_editor/trim_final_newlines", TTRC("Trim Final Newlines"), Key::NONE);
2967
ED_SHORTCUT("script_text_editor/convert_indent_to_spaces", TTRC("Convert Indent to Spaces"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::Y);
2968
ED_SHORTCUT("script_text_editor/convert_indent_to_tabs", TTRC("Convert Indent to Tabs"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::I);
2969
ED_SHORTCUT("script_text_editor/auto_indent", TTRC("Auto Indent"), KeyModifierMask::CMD_OR_CTRL | Key::I);
2970
2971
ED_SHORTCUT_AND_COMMAND("script_text_editor/find", TTRC("Find..."), KeyModifierMask::CMD_OR_CTRL | Key::F);
2972
2973
ED_SHORTCUT("script_text_editor/find_next", TTRC("Find Next"), Key::F3);
2974
ED_SHORTCUT_OVERRIDE("script_text_editor/find_next", "macos", KeyModifierMask::META | Key::G);
2975
2976
ED_SHORTCUT("script_text_editor/find_previous", TTRC("Find Previous"), KeyModifierMask::SHIFT | Key::F3);
2977
ED_SHORTCUT_OVERRIDE("script_text_editor/find_previous", "macos", KeyModifierMask::META | KeyModifierMask::SHIFT | Key::G);
2978
2979
ED_SHORTCUT_AND_COMMAND("script_text_editor/replace", TTRC("Replace..."), KeyModifierMask::CTRL | Key::R);
2980
ED_SHORTCUT_OVERRIDE("script_text_editor/replace", "macos", KeyModifierMask::ALT | KeyModifierMask::META | Key::F);
2981
2982
ED_SHORTCUT("script_text_editor/replace_in_files", TTRC("Replace in Files..."), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::R);
2983
2984
ED_SHORTCUT("script_text_editor/contextual_help", TTRC("Contextual Help"), KeyModifierMask::ALT | Key::F1);
2985
ED_SHORTCUT_OVERRIDE("script_text_editor/contextual_help", "macos", KeyModifierMask::ALT | KeyModifierMask::SHIFT | Key::SPACE);
2986
2987
ED_SHORTCUT("script_text_editor/toggle_bookmark", TTRC("Toggle Bookmark"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | Key::B);
2988
2989
ED_SHORTCUT("script_text_editor/goto_next_bookmark", TTRC("Go to Next Bookmark"), KeyModifierMask::CMD_OR_CTRL | Key::B);
2990
ED_SHORTCUT_OVERRIDE("script_text_editor/goto_next_bookmark", "macos", KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | KeyModifierMask::ALT | Key::B);
2991
2992
ED_SHORTCUT("script_text_editor/goto_previous_bookmark", TTRC("Go to Previous Bookmark"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::B);
2993
ED_SHORTCUT("script_text_editor/remove_all_bookmarks", TTRC("Remove All Bookmarks"), Key::NONE);
2994
2995
ED_SHORTCUT("script_text_editor/goto_function", TTRC("Go to Function..."), KeyModifierMask::ALT | KeyModifierMask::CTRL | Key::F);
2996
ED_SHORTCUT_OVERRIDE("script_text_editor/goto_function", "macos", KeyModifierMask::CTRL | KeyModifierMask::META | Key::J);
2997
2998
ED_SHORTCUT("script_text_editor/goto_line", TTRC("Go to Line..."), KeyModifierMask::CMD_OR_CTRL | Key::L);
2999
ED_SHORTCUT("script_text_editor/goto_symbol", TTRC("Lookup Symbol"));
3000
3001
ED_SHORTCUT("script_text_editor/toggle_breakpoint", TTRC("Toggle Breakpoint"), Key::F9);
3002
ED_SHORTCUT_OVERRIDE("script_text_editor/toggle_breakpoint", "macos", KeyModifierMask::META | KeyModifierMask::SHIFT | Key::B);
3003
3004
ED_SHORTCUT("script_text_editor/remove_all_breakpoints", TTRC("Remove All Breakpoints"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::F9);
3005
// Using Control for these shortcuts even on macOS because Command+Comma is taken for opening Editor Settings.
3006
ED_SHORTCUT("script_text_editor/goto_next_breakpoint", TTRC("Go to Next Breakpoint"), KeyModifierMask::CTRL | Key::PERIOD);
3007
ED_SHORTCUT("script_text_editor/goto_previous_breakpoint", TTRC("Go to Previous Breakpoint"), KeyModifierMask::CTRL | Key::COMMA);
3008
3009
ScriptEditor::register_create_script_editor_function(create_editor);
3010
}
3011
3012
void ScriptTextEditor::validate() {
3013
code_editor->validate_script();
3014
}
3015
3016