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