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/math/expression.h"
35#include "core/os/keyboard.h"
36#include "editor/debugger/editor_debugger_node.h"
37#include "editor/editor_command_palette.h"
38#include "editor/editor_node.h"
39#include "editor/editor_scale.h"
40#include "editor/editor_settings.h"
41#include "editor/editor_string_names.h"
42#include "editor/gui/editor_toaster.h"
43#include "scene/gui/rich_text_label.h"
44#include "scene/gui/split_container.h"
45
46void ConnectionInfoDialog::ok_pressed() {
47}
48
49void ConnectionInfoDialog::popup_connections(String p_method, Vector<Node *> p_nodes) {
50 method->set_text(p_method);
51
52 tree->clear();
53 TreeItem *root = tree->create_item();
54
55 for (int i = 0; i < p_nodes.size(); i++) {
56 List<Connection> all_connections;
57 p_nodes[i]->get_signals_connected_to_this(&all_connections);
58
59 for (const Connection &connection : all_connections) {
60 if (connection.callable.get_method() != p_method) {
61 continue;
62 }
63
64 TreeItem *node_item = tree->create_item(root);
65
66 node_item->set_text(0, Object::cast_to<Node>(connection.signal.get_object())->get_name());
67 node_item->set_icon(0, EditorNode::get_singleton()->get_object_icon(connection.signal.get_object(), "Node"));
68 node_item->set_selectable(0, false);
69 node_item->set_editable(0, false);
70
71 node_item->set_text(1, connection.signal.get_name());
72 Control *p = Object::cast_to<Control>(get_parent());
73 node_item->set_icon(1, p->get_editor_theme_icon(SNAME("Slot")));
74 node_item->set_selectable(1, false);
75 node_item->set_editable(1, false);
76
77 node_item->set_text(2, Object::cast_to<Node>(connection.callable.get_object())->get_name());
78 node_item->set_icon(2, EditorNode::get_singleton()->get_object_icon(connection.callable.get_object(), "Node"));
79 node_item->set_selectable(2, false);
80 node_item->set_editable(2, false);
81 }
82 }
83
84 popup_centered(Size2(600, 300) * EDSCALE);
85}
86
87ConnectionInfoDialog::ConnectionInfoDialog() {
88 set_title(TTR("Connections to method:"));
89
90 VBoxContainer *vbc = memnew(VBoxContainer);
91 vbc->set_anchor_and_offset(SIDE_LEFT, Control::ANCHOR_BEGIN, 8 * EDSCALE);
92 vbc->set_anchor_and_offset(SIDE_TOP, Control::ANCHOR_BEGIN, 8 * EDSCALE);
93 vbc->set_anchor_and_offset(SIDE_RIGHT, Control::ANCHOR_END, -8 * EDSCALE);
94 vbc->set_anchor_and_offset(SIDE_BOTTOM, Control::ANCHOR_END, -8 * EDSCALE);
95 add_child(vbc);
96
97 method = memnew(Label);
98 method->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
99 vbc->add_child(method);
100
101 tree = memnew(Tree);
102 tree->set_columns(3);
103 tree->set_hide_root(true);
104 tree->set_column_titles_visible(true);
105 tree->set_column_title(0, TTR("Source"));
106 tree->set_column_title(1, TTR("Signal"));
107 tree->set_column_title(2, TTR("Target"));
108 vbc->add_child(tree);
109 tree->set_v_size_flags(Control::SIZE_EXPAND_FILL);
110 tree->set_allow_rmb_select(true);
111}
112
113////////////////////////////////////////////////////////////////////////////////
114
115Vector<String> ScriptTextEditor::get_functions() {
116 CodeEdit *te = code_editor->get_text_editor();
117 String text = te->get_text();
118 List<String> fnc;
119
120 if (script->get_language()->validate(text, script->get_path(), &fnc)) {
121 //if valid rewrite functions to latest
122 functions.clear();
123 for (const String &E : fnc) {
124 functions.push_back(E);
125 }
126 }
127
128 return functions;
129}
130
131void ScriptTextEditor::apply_code() {
132 if (script.is_null()) {
133 return;
134 }
135 script->set_source_code(code_editor->get_text_editor()->get_text());
136 script->update_exports();
137 code_editor->get_text_editor()->get_syntax_highlighter()->update_cache();
138}
139
140Ref<Resource> ScriptTextEditor::get_edited_resource() const {
141 return script;
142}
143
144void ScriptTextEditor::set_edited_resource(const Ref<Resource> &p_res) {
145 ERR_FAIL_COND(script.is_valid());
146 ERR_FAIL_COND(p_res.is_null());
147
148 script = p_res;
149
150 code_editor->get_text_editor()->set_text(script->get_source_code());
151 code_editor->get_text_editor()->clear_undo_history();
152 code_editor->get_text_editor()->tag_saved_version();
153
154 emit_signal(SNAME("name_changed"));
155 code_editor->update_line_and_column();
156}
157
158void ScriptTextEditor::enable_editor(Control *p_shortcut_context) {
159 if (editor_enabled) {
160 return;
161 }
162
163 editor_enabled = true;
164
165 _enable_code_editor();
166
167 _validate_script();
168
169 if (p_shortcut_context) {
170 for (int i = 0; i < edit_hb->get_child_count(); ++i) {
171 Control *c = cast_to<Control>(edit_hb->get_child(i));
172 if (c) {
173 c->set_shortcut_context(p_shortcut_context);
174 }
175 }
176 }
177}
178
179void ScriptTextEditor::_load_theme_settings() {
180 CodeEdit *text_edit = code_editor->get_text_editor();
181
182 Color updated_marked_line_color = EDITOR_GET("text_editor/theme/highlighting/mark_color");
183 Color updated_safe_line_number_color = EDITOR_GET("text_editor/theme/highlighting/safe_line_number_color");
184 Color updated_folded_code_region_color = EDITOR_GET("text_editor/theme/highlighting/folded_code_region_color");
185
186 bool safe_line_number_color_updated = updated_safe_line_number_color != safe_line_number_color;
187 bool marked_line_color_updated = updated_marked_line_color != marked_line_color;
188 bool folded_code_region_color_updated = updated_folded_code_region_color != folded_code_region_color;
189 if (safe_line_number_color_updated || marked_line_color_updated || folded_code_region_color_updated) {
190 safe_line_number_color = updated_safe_line_number_color;
191 for (int i = 0; i < text_edit->get_line_count(); i++) {
192 if (marked_line_color_updated && text_edit->get_line_background_color(i) == marked_line_color) {
193 text_edit->set_line_background_color(i, updated_marked_line_color);
194 }
195
196 if (safe_line_number_color_updated && text_edit->get_line_gutter_item_color(i, line_number_gutter) != default_line_number_color) {
197 text_edit->set_line_gutter_item_color(i, line_number_gutter, safe_line_number_color);
198 }
199
200 if (folded_code_region_color_updated && text_edit->get_line_background_color(i) == folded_code_region_color) {
201 text_edit->set_line_background_color(i, updated_folded_code_region_color);
202 }
203 }
204 marked_line_color = updated_marked_line_color;
205 folded_code_region_color = updated_folded_code_region_color;
206 }
207
208 theme_loaded = true;
209 if (!script.is_null()) {
210 _set_theme_for_script();
211 }
212}
213
214void ScriptTextEditor::_set_theme_for_script() {
215 if (!theme_loaded) {
216 return;
217 }
218
219 CodeEdit *text_edit = code_editor->get_text_editor();
220 text_edit->get_syntax_highlighter()->update_cache();
221
222 List<String> strings;
223 script->get_language()->get_string_delimiters(&strings);
224 text_edit->clear_string_delimiters();
225 for (const String &string : strings) {
226 String beg = string.get_slice(" ", 0);
227 String end = string.get_slice_count(" ") > 1 ? string.get_slice(" ", 1) : String();
228 if (!text_edit->has_string_delimiter(beg)) {
229 text_edit->add_string_delimiter(beg, end, end.is_empty());
230 }
231
232 if (!end.is_empty() && !text_edit->has_auto_brace_completion_open_key(beg)) {
233 text_edit->add_auto_brace_completion_pair(beg, end);
234 }
235 }
236
237 List<String> comments;
238 script->get_language()->get_comment_delimiters(&comments);
239 text_edit->clear_comment_delimiters();
240 for (const String &comment : comments) {
241 String beg = comment.get_slice(" ", 0);
242 String end = comment.get_slice_count(" ") > 1 ? comment.get_slice(" ", 1) : String();
243 text_edit->add_comment_delimiter(beg, end, end.is_empty());
244
245 if (!end.is_empty() && !text_edit->has_auto_brace_completion_open_key(beg)) {
246 text_edit->add_auto_brace_completion_pair(beg, end);
247 }
248 }
249}
250
251void ScriptTextEditor::_show_errors_panel(bool p_show) {
252 errors_panel->set_visible(p_show);
253}
254
255void ScriptTextEditor::_show_warnings_panel(bool p_show) {
256 warnings_panel->set_visible(p_show);
257}
258
259void ScriptTextEditor::_warning_clicked(Variant p_line) {
260 if (p_line.get_type() == Variant::INT) {
261 goto_line_centered(p_line.operator int64_t());
262 } else if (p_line.get_type() == Variant::DICTIONARY) {
263 Dictionary meta = p_line.operator Dictionary();
264 const int line = meta["line"].operator int64_t() - 1;
265 const String code = meta["code"].operator String();
266 const String quote_style = EDITOR_GET("text_editor/completion/use_single_quotes") ? "'" : "\"";
267
268 CodeEdit *text_editor = code_editor->get_text_editor();
269 String prev_line = line > 0 ? text_editor->get_line(line - 1) : "";
270 if (prev_line.contains("@warning_ignore")) {
271 const int closing_bracket_idx = prev_line.find(")");
272 const String text_to_insert = ", " + code.quote(quote_style);
273 prev_line = prev_line.insert(closing_bracket_idx, text_to_insert);
274 text_editor->set_line(line - 1, prev_line);
275 } else {
276 const int indent = text_editor->get_indent_level(line) / text_editor->get_indent_size();
277 String annotation_indent;
278 if (!text_editor->is_indent_using_spaces()) {
279 annotation_indent = String("\t").repeat(indent);
280 } else {
281 annotation_indent = String(" ").repeat(text_editor->get_indent_size() * indent);
282 }
283 text_editor->insert_line_at(line, annotation_indent + "@warning_ignore(" + code.quote(quote_style) + ")");
284 }
285
286 _validate_script();
287 }
288}
289
290void ScriptTextEditor::_error_clicked(Variant p_line) {
291 if (p_line.get_type() == Variant::INT) {
292 code_editor->get_text_editor()->remove_secondary_carets();
293 code_editor->get_text_editor()->set_caret_line(p_line.operator int64_t());
294 } else if (p_line.get_type() == Variant::DICTIONARY) {
295 Dictionary meta = p_line.operator Dictionary();
296 const String path = meta["path"].operator String();
297 const int line = meta["line"].operator int64_t();
298 const int column = meta["column"].operator int64_t();
299 if (path.is_empty()) {
300 code_editor->get_text_editor()->remove_secondary_carets();
301 code_editor->get_text_editor()->set_caret_line(line);
302 } else {
303 Ref<Resource> scr = ResourceLoader::load(path);
304 if (!scr.is_valid()) {
305 EditorNode::get_singleton()->show_warning(TTR("Could not load file at:") + "\n\n" + path, TTR("Error!"));
306 } else {
307 ScriptEditor::get_singleton()->edit(scr, line, column);
308 }
309 }
310 }
311}
312
313void ScriptTextEditor::reload_text() {
314 ERR_FAIL_COND(script.is_null());
315
316 CodeEdit *te = code_editor->get_text_editor();
317 int column = te->get_caret_column();
318 int row = te->get_caret_line();
319 int h = te->get_h_scroll();
320 int v = te->get_v_scroll();
321
322 te->set_text(script->get_source_code());
323 te->set_caret_line(row);
324 te->set_caret_column(column);
325 te->set_h_scroll(h);
326 te->set_v_scroll(v);
327
328 te->tag_saved_version();
329
330 code_editor->update_line_and_column();
331 _validate_script();
332}
333
334void ScriptTextEditor::add_callback(const String &p_function, PackedStringArray p_args) {
335 String code = code_editor->get_text_editor()->get_text();
336 int pos = script->get_language()->find_function(p_function, code);
337 code_editor->get_text_editor()->remove_secondary_carets();
338 if (pos == -1) {
339 //does not exist
340 code_editor->get_text_editor()->deselect();
341 pos = code_editor->get_text_editor()->get_line_count() + 2;
342 String func = script->get_language()->make_function("", p_function, p_args);
343 //code=code+func;
344 code_editor->get_text_editor()->set_caret_line(pos + 1);
345 code_editor->get_text_editor()->set_caret_column(1000000); //none shall be that big
346 code_editor->get_text_editor()->insert_text_at_caret("\n\n" + func);
347 }
348 code_editor->get_text_editor()->set_caret_line(pos);
349 code_editor->get_text_editor()->set_caret_column(1);
350}
351
352bool ScriptTextEditor::show_members_overview() {
353 return true;
354}
355
356void ScriptTextEditor::update_settings() {
357 code_editor->get_text_editor()->set_gutter_draw(connection_gutter, EDITOR_GET("text_editor/appearance/gutters/show_info_gutter"));
358 code_editor->update_editor_settings();
359}
360
361bool ScriptTextEditor::is_unsaved() {
362 const bool unsaved =
363 code_editor->get_text_editor()->get_version() != code_editor->get_text_editor()->get_saved_version() ||
364 script->get_path().is_empty(); // In memory.
365 return unsaved;
366}
367
368Variant ScriptTextEditor::get_edit_state() {
369 return code_editor->get_edit_state();
370}
371
372void ScriptTextEditor::set_edit_state(const Variant &p_state) {
373 code_editor->set_edit_state(p_state);
374
375 Dictionary state = p_state;
376 if (state.has("syntax_highlighter")) {
377 int idx = highlighter_menu->get_item_idx_from_text(state["syntax_highlighter"]);
378 if (idx >= 0) {
379 _change_syntax_highlighter(idx);
380 }
381 }
382
383 if (editor_enabled) {
384#ifndef ANDROID_ENABLED
385 ensure_focus();
386#endif
387 }
388}
389
390Variant ScriptTextEditor::get_navigation_state() {
391 return code_editor->get_navigation_state();
392}
393
394void ScriptTextEditor::_convert_case(CodeTextEditor::CaseStyle p_case) {
395 code_editor->convert_case(p_case);
396}
397
398void ScriptTextEditor::trim_trailing_whitespace() {
399 code_editor->trim_trailing_whitespace();
400}
401
402void ScriptTextEditor::insert_final_newline() {
403 code_editor->insert_final_newline();
404}
405
406void ScriptTextEditor::convert_indent() {
407 code_editor->get_text_editor()->convert_indent();
408}
409
410void ScriptTextEditor::tag_saved_version() {
411 code_editor->get_text_editor()->tag_saved_version();
412}
413
414void ScriptTextEditor::goto_line(int p_line, bool p_with_error) {
415 code_editor->goto_line(p_line);
416}
417
418void ScriptTextEditor::goto_line_selection(int p_line, int p_begin, int p_end) {
419 code_editor->goto_line_selection(p_line, p_begin, p_end);
420}
421
422void ScriptTextEditor::goto_line_centered(int p_line) {
423 code_editor->goto_line_centered(p_line);
424}
425
426void ScriptTextEditor::set_executing_line(int p_line) {
427 code_editor->set_executing_line(p_line);
428}
429
430void ScriptTextEditor::clear_executing_line() {
431 code_editor->clear_executing_line();
432}
433
434void ScriptTextEditor::ensure_focus() {
435 code_editor->get_text_editor()->grab_focus();
436}
437
438String ScriptTextEditor::get_name() {
439 String name;
440
441 name = script->get_path().get_file();
442 if (name.is_empty()) {
443 // This appears for newly created built-in scripts before saving the scene.
444 name = TTR("[unsaved]");
445 } else if (script->is_built_in()) {
446 const String &script_name = script->get_name();
447 if (!script_name.is_empty()) {
448 // If the built-in script has a custom resource name defined,
449 // display the built-in script name as follows: `ResourceName (scene_file.tscn)`
450 name = vformat("%s (%s)", script_name, name.get_slice("::", 0));
451 }
452 }
453
454 if (is_unsaved()) {
455 name += "(*)";
456 }
457
458 return name;
459}
460
461Ref<Texture2D> ScriptTextEditor::get_theme_icon() {
462 if (get_parent_control()) {
463 String icon_name = script->get_class();
464 if (script->is_built_in()) {
465 icon_name += "Internal";
466 }
467
468 if (get_parent_control()->has_theme_icon(icon_name, EditorStringName(EditorIcons))) {
469 return get_parent_control()->get_editor_theme_icon(icon_name);
470 } else if (get_parent_control()->has_theme_icon(script->get_class(), EditorStringName(EditorIcons))) {
471 return get_parent_control()->get_editor_theme_icon(script->get_class());
472 }
473 }
474
475 return Ref<Texture2D>();
476}
477
478void ScriptTextEditor::_validate_script() {
479 CodeEdit *te = code_editor->get_text_editor();
480
481 String text = te->get_text();
482 List<String> fnc;
483
484 warnings.clear();
485 errors.clear();
486 depended_errors.clear();
487 safe_lines.clear();
488
489 if (!script->get_language()->validate(text, script->get_path(), &fnc, &errors, &warnings, &safe_lines)) {
490 for (List<ScriptLanguage::ScriptError>::Element *E = errors.front(); E; E = E->next()) {
491 if ((E->get().path.is_empty() && !script->get_path().is_empty()) || E->get().path != script->get_path()) {
492 depended_errors[E->get().path].push_back(E->get());
493 E->erase();
494 }
495 }
496
497 if (errors.size() > 0) {
498 // TRANSLATORS: Script error pointing to a line and column number.
499 String error_text = vformat(TTR("Error at (%d, %d):"), errors[0].line, errors[0].column) + " " + errors[0].message;
500 code_editor->set_error(error_text);
501 code_editor->set_error_pos(errors[0].line - 1, errors[0].column - 1);
502 }
503 script_is_valid = false;
504 } else {
505 code_editor->set_error("");
506 if (!script->is_tool()) {
507 script->set_source_code(text);
508 script->update_exports();
509 te->get_syntax_highlighter()->update_cache();
510 }
511
512 functions.clear();
513 for (const String &E : fnc) {
514 functions.push_back(E);
515 }
516 script_is_valid = true;
517 }
518 _update_connected_methods();
519 _update_warnings();
520 _update_errors();
521
522 emit_signal(SNAME("name_changed"));
523 emit_signal(SNAME("edited_script_changed"));
524}
525
526void ScriptTextEditor::_update_warnings() {
527 int warning_nb = warnings.size();
528 warnings_panel->clear();
529
530 bool has_connections_table = false;
531 // Add missing connections.
532 if (GLOBAL_GET("debug/gdscript/warnings/enable").booleanize()) {
533 Node *base = get_tree()->get_edited_scene_root();
534 if (base && missing_connections.size() > 0) {
535 has_connections_table = true;
536 warnings_panel->push_table(1);
537 for (const Connection &connection : missing_connections) {
538 String base_path = base->get_name();
539 String source_path = base == connection.signal.get_object() ? base_path : base_path + "/" + base->get_path_to(Object::cast_to<Node>(connection.signal.get_object()));
540 String target_path = base == connection.callable.get_object() ? base_path : base_path + "/" + base->get_path_to(Object::cast_to<Node>(connection.callable.get_object()));
541
542 warnings_panel->push_cell();
543 warnings_panel->push_color(warnings_panel->get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));
544 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));
545 warnings_panel->pop(); // Color.
546 warnings_panel->pop(); // Cell.
547 }
548 warnings_panel->pop(); // Table.
549
550 warning_nb += missing_connections.size();
551 }
552 }
553
554 code_editor->set_warning_count(warning_nb);
555
556 if (has_connections_table) {
557 warnings_panel->add_newline();
558 }
559
560 // Add script warnings.
561 warnings_panel->push_table(3);
562 for (const ScriptLanguage::Warning &w : warnings) {
563 Dictionary ignore_meta;
564 ignore_meta["line"] = w.start_line;
565 ignore_meta["code"] = w.string_code.to_lower();
566 warnings_panel->push_cell();
567 warnings_panel->push_meta(ignore_meta);
568 warnings_panel->push_color(
569 warnings_panel->get_theme_color(SNAME("accent_color"), EditorStringName(Editor)).lerp(warnings_panel->get_theme_color(SNAME("mono_color"), EditorStringName(Editor)), 0.5f));
570 warnings_panel->add_text(TTR("[Ignore]"));
571 warnings_panel->pop(); // Color.
572 warnings_panel->pop(); // Meta ignore.
573 warnings_panel->pop(); // Cell.
574
575 warnings_panel->push_cell();
576 warnings_panel->push_meta(w.start_line - 1);
577 warnings_panel->push_color(warnings_panel->get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));
578 warnings_panel->add_text(TTR("Line") + " " + itos(w.start_line));
579 warnings_panel->add_text(" (" + w.string_code + "):");
580 warnings_panel->pop(); // Color.
581 warnings_panel->pop(); // Meta goto.
582 warnings_panel->pop(); // Cell.
583
584 warnings_panel->push_cell();
585 warnings_panel->add_text(w.message);
586 warnings_panel->add_newline();
587 warnings_panel->pop(); // Cell.
588 }
589 warnings_panel->pop(); // Table.
590}
591
592void ScriptTextEditor::_update_errors() {
593 code_editor->set_error_count(errors.size());
594
595 errors_panel->clear();
596 errors_panel->push_table(2);
597 for (const ScriptLanguage::ScriptError &err : errors) {
598 Dictionary click_meta;
599 click_meta["line"] = err.line;
600 click_meta["column"] = err.column;
601
602 errors_panel->push_cell();
603 errors_panel->push_meta(err.line - 1);
604 errors_panel->push_color(warnings_panel->get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
605 errors_panel->add_text(TTR("Line") + " " + itos(err.line) + ":");
606 errors_panel->pop(); // Color.
607 errors_panel->pop(); // Meta goto.
608 errors_panel->pop(); // Cell.
609
610 errors_panel->push_cell();
611 errors_panel->add_text(err.message);
612 errors_panel->add_newline();
613 errors_panel->pop(); // Cell.
614 }
615 errors_panel->pop(); // Table
616
617 for (const KeyValue<String, List<ScriptLanguage::ScriptError>> &KV : depended_errors) {
618 Dictionary click_meta;
619 click_meta["path"] = KV.key;
620 click_meta["line"] = 1;
621
622 errors_panel->add_newline();
623 errors_panel->add_newline();
624 errors_panel->push_meta(click_meta);
625 errors_panel->add_text(vformat(R"(%s:)", KV.key));
626 errors_panel->pop(); // Meta goto.
627 errors_panel->add_newline();
628
629 errors_panel->push_indent(1);
630 errors_panel->push_table(2);
631 String filename = KV.key.get_file();
632 for (const ScriptLanguage::ScriptError &err : KV.value) {
633 click_meta["line"] = err.line;
634 click_meta["column"] = err.column;
635
636 errors_panel->push_cell();
637 errors_panel->push_meta(click_meta);
638 errors_panel->push_color(errors_panel->get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
639 errors_panel->add_text(TTR("Line") + " " + itos(err.line) + ":");
640 errors_panel->pop(); // Color.
641 errors_panel->pop(); // Meta goto.
642 errors_panel->pop(); // Cell.
643
644 errors_panel->push_cell();
645 errors_panel->add_text(err.message);
646 errors_panel->pop(); // Cell.
647 }
648 errors_panel->pop(); // Table
649 errors_panel->pop(); // Indent.
650 }
651
652 CodeEdit *te = code_editor->get_text_editor();
653 bool highlight_safe = EDITOR_GET("text_editor/appearance/gutters/highlight_type_safe_lines");
654 bool last_is_safe = false;
655 for (int i = 0; i < te->get_line_count(); i++) {
656 if (errors.is_empty()) {
657 bool is_folded_code_region = te->is_line_code_region_start(i) && te->is_line_folded(i);
658 te->set_line_background_color(i, is_folded_code_region ? folded_code_region_color : Color(0, 0, 0, 0));
659 } else {
660 for (const ScriptLanguage::ScriptError &E : errors) {
661 bool error_line = i == E.line - 1;
662 te->set_line_background_color(i, error_line ? marked_line_color : Color(0, 0, 0, 0));
663 if (error_line) {
664 break;
665 }
666 }
667 }
668
669 if (highlight_safe) {
670 if (safe_lines.has(i + 1)) {
671 te->set_line_gutter_item_color(i, line_number_gutter, safe_line_number_color);
672 last_is_safe = true;
673 } else if (last_is_safe && (te->is_in_comment(i) != -1 || te->get_line(i).strip_edges().is_empty())) {
674 te->set_line_gutter_item_color(i, line_number_gutter, safe_line_number_color);
675 } else {
676 te->set_line_gutter_item_color(i, line_number_gutter, default_line_number_color);
677 last_is_safe = false;
678 }
679 } else {
680 te->set_line_gutter_item_color(i, 1, default_line_number_color);
681 }
682 }
683}
684
685void ScriptTextEditor::_update_bookmark_list() {
686 bookmarks_menu->clear();
687 bookmarks_menu->reset_size();
688
689 bookmarks_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_bookmark"), BOOKMARK_TOGGLE);
690 bookmarks_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/remove_all_bookmarks"), BOOKMARK_REMOVE_ALL);
691 bookmarks_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_next_bookmark"), BOOKMARK_GOTO_NEXT);
692 bookmarks_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_previous_bookmark"), BOOKMARK_GOTO_PREV);
693
694 PackedInt32Array bookmark_list = code_editor->get_text_editor()->get_bookmarked_lines();
695 if (bookmark_list.size() == 0) {
696 return;
697 }
698
699 bookmarks_menu->add_separator();
700
701 for (int i = 0; i < bookmark_list.size(); i++) {
702 // Strip edges to remove spaces or tabs.
703 // Also replace any tabs by spaces, since we can't print tabs in the menu.
704 String line = code_editor->get_text_editor()->get_line(bookmark_list[i]).replace("\t", " ").strip_edges();
705
706 // Limit the size of the line if too big.
707 if (line.length() > 50) {
708 line = line.substr(0, 50);
709 }
710
711 bookmarks_menu->add_item(String::num((int)bookmark_list[i] + 1) + " - `" + line + "`");
712 bookmarks_menu->set_item_metadata(-1, bookmark_list[i]);
713 }
714}
715
716void ScriptTextEditor::_bookmark_item_pressed(int p_idx) {
717 if (p_idx < 4) { // Any item before the separator.
718 _edit_option(bookmarks_menu->get_item_id(p_idx));
719 } else {
720 code_editor->goto_line_centered(bookmarks_menu->get_item_metadata(p_idx));
721 }
722}
723
724static Vector<Node *> _find_all_node_for_script(Node *p_base, Node *p_current, const Ref<Script> &p_script) {
725 Vector<Node *> nodes;
726
727 if (p_current->get_owner() != p_base && p_base != p_current) {
728 return nodes;
729 }
730
731 Ref<Script> c = p_current->get_script();
732 if (c == p_script) {
733 nodes.push_back(p_current);
734 }
735
736 for (int i = 0; i < p_current->get_child_count(); i++) {
737 Vector<Node *> found = _find_all_node_for_script(p_base, p_current->get_child(i), p_script);
738 nodes.append_array(found);
739 }
740
741 return nodes;
742}
743
744static Node *_find_node_for_script(Node *p_base, Node *p_current, const Ref<Script> &p_script) {
745 if (p_current->get_owner() != p_base && p_base != p_current) {
746 return nullptr;
747 }
748 Ref<Script> c = p_current->get_script();
749 if (c == p_script) {
750 return p_current;
751 }
752 for (int i = 0; i < p_current->get_child_count(); i++) {
753 Node *found = _find_node_for_script(p_base, p_current->get_child(i), p_script);
754 if (found) {
755 return found;
756 }
757 }
758
759 return nullptr;
760}
761
762static void _find_changed_scripts_for_external_editor(Node *p_base, Node *p_current, HashSet<Ref<Script>> &r_scripts) {
763 if (p_current->get_owner() != p_base && p_base != p_current) {
764 return;
765 }
766 Ref<Script> c = p_current->get_script();
767
768 if (c.is_valid()) {
769 r_scripts.insert(c);
770 }
771
772 for (int i = 0; i < p_current->get_child_count(); i++) {
773 _find_changed_scripts_for_external_editor(p_base, p_current->get_child(i), r_scripts);
774 }
775}
776
777void ScriptEditor::_update_modified_scripts_for_external_editor(Ref<Script> p_for_script) {
778 if (!bool(EDITOR_GET("text_editor/external/use_external_editor"))) {
779 return;
780 }
781
782 ERR_FAIL_COND(!get_tree());
783
784 HashSet<Ref<Script>> scripts;
785
786 Node *base = get_tree()->get_edited_scene_root();
787 if (base) {
788 _find_changed_scripts_for_external_editor(base, base, scripts);
789 }
790
791 for (const Ref<Script> &E : scripts) {
792 Ref<Script> scr = E;
793
794 if (p_for_script.is_valid() && p_for_script != scr) {
795 continue;
796 }
797
798 if (scr->is_built_in()) {
799 continue; //internal script, who cares, though weird
800 }
801
802 uint64_t last_date = scr->get_last_modified_time();
803 uint64_t date = FileAccess::get_modified_time(scr->get_path());
804
805 if (last_date != date) {
806 Ref<Script> rel_scr = ResourceLoader::load(scr->get_path(), scr->get_class(), ResourceFormatLoader::CACHE_MODE_IGNORE);
807 ERR_CONTINUE(!rel_scr.is_valid());
808 scr->set_source_code(rel_scr->get_source_code());
809 scr->set_last_modified_time(rel_scr->get_last_modified_time());
810 scr->update_exports();
811
812 _trigger_live_script_reload();
813 }
814 }
815}
816
817void ScriptTextEditor::_code_complete_scripts(void *p_ud, const String &p_code, List<ScriptLanguage::CodeCompletionOption> *r_options, bool &r_force) {
818 ScriptTextEditor *ste = (ScriptTextEditor *)p_ud;
819 ste->_code_complete_script(p_code, r_options, r_force);
820}
821
822void ScriptTextEditor::_code_complete_script(const String &p_code, List<ScriptLanguage::CodeCompletionOption> *r_options, bool &r_force) {
823 if (color_panel->is_visible()) {
824 return;
825 }
826 Node *base = get_tree()->get_edited_scene_root();
827 if (base) {
828 base = _find_node_for_script(base, base, script);
829 }
830 String hint;
831 Error err = script->get_language()->complete_code(p_code, script->get_path(), base, r_options, r_force, hint);
832
833 if (err == OK) {
834 code_editor->get_text_editor()->set_code_hint(hint);
835 }
836}
837
838void ScriptTextEditor::_update_breakpoint_list() {
839 breakpoints_menu->clear();
840 breakpoints_menu->reset_size();
841
842 breakpoints_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_breakpoint"), DEBUG_TOGGLE_BREAKPOINT);
843 breakpoints_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/remove_all_breakpoints"), DEBUG_REMOVE_ALL_BREAKPOINTS);
844 breakpoints_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_next_breakpoint"), DEBUG_GOTO_NEXT_BREAKPOINT);
845 breakpoints_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_previous_breakpoint"), DEBUG_GOTO_PREV_BREAKPOINT);
846
847 PackedInt32Array breakpoint_list = code_editor->get_text_editor()->get_breakpointed_lines();
848 if (breakpoint_list.size() == 0) {
849 return;
850 }
851
852 breakpoints_menu->add_separator();
853
854 for (int i = 0; i < breakpoint_list.size(); i++) {
855 // Strip edges to remove spaces or tabs.
856 // Also replace any tabs by spaces, since we can't print tabs in the menu.
857 String line = code_editor->get_text_editor()->get_line(breakpoint_list[i]).replace("\t", " ").strip_edges();
858
859 // Limit the size of the line if too big.
860 if (line.length() > 50) {
861 line = line.substr(0, 50);
862 }
863
864 breakpoints_menu->add_item(String::num((int)breakpoint_list[i] + 1) + " - `" + line + "`");
865 breakpoints_menu->set_item_metadata(-1, breakpoint_list[i]);
866 }
867}
868
869void ScriptTextEditor::_breakpoint_item_pressed(int p_idx) {
870 if (p_idx < 4) { // Any item before the separator.
871 _edit_option(breakpoints_menu->get_item_id(p_idx));
872 } else {
873 code_editor->goto_line(breakpoints_menu->get_item_metadata(p_idx));
874 code_editor->get_text_editor()->call_deferred(SNAME("center_viewport_to_caret")); //Need to be deferred, because goto uses call_deferred().
875 }
876}
877
878void ScriptTextEditor::_breakpoint_toggled(int p_row) {
879 EditorDebuggerNode::get_singleton()->set_breakpoint(script->get_path(), p_row + 1, code_editor->get_text_editor()->is_line_breakpointed(p_row));
880}
881
882void ScriptTextEditor::_lookup_symbol(const String &p_symbol, int p_row, int p_column) {
883 Node *base = get_tree()->get_edited_scene_root();
884 if (base) {
885 base = _find_node_for_script(base, base, script);
886 }
887
888 ScriptLanguage::LookupResult result;
889 String code_text = code_editor->get_text_editor()->get_text_with_cursor_char(p_row, p_column);
890 Error lc_error = script->get_language()->lookup_code(code_text, p_symbol, script->get_path(), base, result);
891 if (ScriptServer::is_global_class(p_symbol)) {
892 EditorNode::get_singleton()->load_resource(ScriptServer::get_global_class_path(p_symbol));
893 } else if (p_symbol.is_resource_file()) {
894 List<String> scene_extensions;
895 ResourceLoader::get_recognized_extensions_for_type("PackedScene", &scene_extensions);
896
897 if (scene_extensions.find(p_symbol.get_extension())) {
898 EditorNode::get_singleton()->load_scene(p_symbol);
899 } else {
900 EditorNode::get_singleton()->load_resource(p_symbol);
901 }
902
903 } else if (lc_error == OK) {
904 _goto_line(p_row);
905
906 switch (result.type) {
907 case ScriptLanguage::LOOKUP_RESULT_SCRIPT_LOCATION: {
908 if (result.script.is_valid()) {
909 emit_signal(SNAME("request_open_script_at_line"), result.script, result.location - 1);
910 } else {
911 emit_signal(SNAME("request_save_history"));
912 goto_line_centered(result.location - 1);
913 }
914 } break;
915 case ScriptLanguage::LOOKUP_RESULT_CLASS: {
916 emit_signal(SNAME("go_to_help"), "class_name:" + result.class_name);
917 } break;
918 case ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT: {
919 StringName cname = result.class_name;
920 bool success;
921 while (true) {
922 ClassDB::get_integer_constant(cname, result.class_member, &success);
923 if (success) {
924 result.class_name = cname;
925 cname = ClassDB::get_parent_class(cname);
926 } else {
927 break;
928 }
929 }
930
931 emit_signal(SNAME("go_to_help"), "class_constant:" + result.class_name + ":" + result.class_member);
932
933 } break;
934 case ScriptLanguage::LOOKUP_RESULT_CLASS_PROPERTY: {
935 emit_signal(SNAME("go_to_help"), "class_property:" + result.class_name + ":" + result.class_member);
936
937 } break;
938 case ScriptLanguage::LOOKUP_RESULT_CLASS_METHOD: {
939 StringName cname = result.class_name;
940
941 while (true) {
942 if (ClassDB::has_method(cname, result.class_member)) {
943 result.class_name = cname;
944 cname = ClassDB::get_parent_class(cname);
945 } else {
946 break;
947 }
948 }
949
950 emit_signal(SNAME("go_to_help"), "class_method:" + result.class_name + ":" + result.class_member);
951
952 } break;
953 case ScriptLanguage::LOOKUP_RESULT_CLASS_SIGNAL: {
954 StringName cname = result.class_name;
955
956 while (true) {
957 if (ClassDB::has_signal(cname, result.class_member)) {
958 result.class_name = cname;
959 cname = ClassDB::get_parent_class(cname);
960 } else {
961 break;
962 }
963 }
964
965 emit_signal(SNAME("go_to_help"), "class_signal:" + result.class_name + ":" + result.class_member);
966
967 } break;
968 case ScriptLanguage::LOOKUP_RESULT_CLASS_ENUM: {
969 StringName cname = result.class_name;
970 StringName success;
971 while (true) {
972 success = ClassDB::get_integer_constant_enum(cname, result.class_member, true);
973 if (success != StringName()) {
974 result.class_name = cname;
975 cname = ClassDB::get_parent_class(cname);
976 } else {
977 break;
978 }
979 }
980
981 emit_signal(SNAME("go_to_help"), "class_enum:" + result.class_name + ":" + result.class_member);
982
983 } break;
984 case ScriptLanguage::LOOKUP_RESULT_CLASS_ANNOTATION: {
985 emit_signal(SNAME("go_to_help"), "class_annotation:" + result.class_name + ":" + result.class_member);
986 } break;
987 case ScriptLanguage::LOOKUP_RESULT_CLASS_TBD_GLOBALSCOPE: {
988 emit_signal(SNAME("go_to_help"), "class_global:" + result.class_name + ":" + result.class_member);
989 } break;
990 default: {
991 }
992 }
993 } else if (ProjectSettings::get_singleton()->has_autoload(p_symbol)) {
994 // Check for Autoload scenes.
995 const ProjectSettings::AutoloadInfo &info = ProjectSettings::get_singleton()->get_autoload(p_symbol);
996 if (info.is_singleton) {
997 EditorNode::get_singleton()->load_scene(info.path);
998 }
999 } else if (p_symbol.is_relative_path()) {
1000 // Every symbol other than absolute path is relative path so keep this condition at last.
1001 String path = _get_absolute_path(p_symbol);
1002 if (FileAccess::exists(path)) {
1003 List<String> scene_extensions;
1004 ResourceLoader::get_recognized_extensions_for_type("PackedScene", &scene_extensions);
1005
1006 if (scene_extensions.find(path.get_extension())) {
1007 EditorNode::get_singleton()->load_scene(path);
1008 } else {
1009 EditorNode::get_singleton()->load_resource(path);
1010 }
1011 }
1012 }
1013}
1014
1015void ScriptTextEditor::_validate_symbol(const String &p_symbol) {
1016 CodeEdit *text_edit = code_editor->get_text_editor();
1017
1018 Node *base = get_tree()->get_edited_scene_root();
1019 if (base) {
1020 base = _find_node_for_script(base, base, script);
1021 }
1022
1023 ScriptLanguage::LookupResult result;
1024 String lc_text = code_editor->get_text_editor()->get_text_for_symbol_lookup();
1025 Error lc_error = script->get_language()->lookup_code(lc_text, p_symbol, script->get_path(), base, result);
1026 bool is_singleton = ProjectSettings::get_singleton()->has_autoload(p_symbol) && ProjectSettings::get_singleton()->get_autoload(p_symbol).is_singleton;
1027 if (ScriptServer::is_global_class(p_symbol) || p_symbol.is_resource_file() || lc_error == OK || is_singleton) {
1028 text_edit->set_symbol_lookup_word_as_valid(true);
1029 } else if (p_symbol.is_relative_path()) {
1030 String path = _get_absolute_path(p_symbol);
1031 if (FileAccess::exists(path)) {
1032 text_edit->set_symbol_lookup_word_as_valid(true);
1033 } else {
1034 text_edit->set_symbol_lookup_word_as_valid(false);
1035 }
1036 } else {
1037 text_edit->set_symbol_lookup_word_as_valid(false);
1038 }
1039}
1040
1041String ScriptTextEditor::_get_absolute_path(const String &rel_path) {
1042 String base_path = script->get_path().get_base_dir();
1043 String path = base_path.path_join(rel_path);
1044 return path.replace("///", "//").simplify_path();
1045}
1046
1047void ScriptTextEditor::update_toggle_scripts_button() {
1048 code_editor->update_toggle_scripts_button();
1049}
1050
1051void ScriptTextEditor::_update_connected_methods() {
1052 CodeEdit *text_edit = code_editor->get_text_editor();
1053 text_edit->set_gutter_width(connection_gutter, text_edit->get_line_height());
1054 for (int i = 0; i < text_edit->get_line_count(); i++) {
1055 text_edit->set_line_gutter_metadata(i, connection_gutter, Dictionary());
1056 text_edit->set_line_gutter_icon(i, connection_gutter, nullptr);
1057 text_edit->set_line_gutter_clickable(i, connection_gutter, false);
1058 }
1059 missing_connections.clear();
1060
1061 if (!script_is_valid) {
1062 return;
1063 }
1064
1065 Node *base = get_tree()->get_edited_scene_root();
1066 if (!base) {
1067 return;
1068 }
1069
1070 // Add connection icons to methods.
1071 Vector<Node *> nodes = _find_all_node_for_script(base, base, script);
1072 HashSet<StringName> methods_found;
1073 for (int i = 0; i < nodes.size(); i++) {
1074 List<Connection> signal_connections;
1075 nodes[i]->get_signals_connected_to_this(&signal_connections);
1076
1077 for (const Connection &connection : signal_connections) {
1078 if (!(connection.flags & CONNECT_PERSIST)) {
1079 continue;
1080 }
1081
1082 // As deleted nodes are still accessible via the undo/redo system, check if they're still on the tree.
1083 Node *source = Object::cast_to<Node>(connection.signal.get_object());
1084 if (source && !source->is_inside_tree()) {
1085 continue;
1086 }
1087
1088 const StringName method = connection.callable.get_method();
1089 if (methods_found.has(method)) {
1090 continue;
1091 }
1092
1093 if (!ClassDB::has_method(script->get_instance_base_type(), method)) {
1094 int line = -1;
1095
1096 for (int j = 0; j < functions.size(); j++) {
1097 String name = functions[j].get_slice(":", 0);
1098 if (name == method) {
1099 Dictionary line_meta;
1100 line_meta["type"] = "connection";
1101 line_meta["method"] = method;
1102 line = functions[j].get_slice(":", 1).to_int() - 1;
1103 text_edit->set_line_gutter_metadata(line, connection_gutter, line_meta);
1104 text_edit->set_line_gutter_icon(line, connection_gutter, get_parent_control()->get_editor_theme_icon(SNAME("Slot")));
1105 text_edit->set_line_gutter_clickable(line, connection_gutter, true);
1106 methods_found.insert(method);
1107 break;
1108 }
1109 }
1110
1111 if (line >= 0) {
1112 continue;
1113 }
1114
1115 // There is a chance that the method is inherited from another script.
1116 bool found_inherited_function = false;
1117 Ref<Script> inherited_script = script->get_base_script();
1118 while (!inherited_script.is_null()) {
1119 if (inherited_script->has_method(method)) {
1120 found_inherited_function = true;
1121 break;
1122 }
1123
1124 inherited_script = inherited_script->get_base_script();
1125 }
1126
1127 if (!found_inherited_function) {
1128 missing_connections.push_back(connection);
1129 }
1130 }
1131 }
1132 }
1133
1134 // Add override icons to methods.
1135 methods_found.clear();
1136 for (int i = 0; i < functions.size(); i++) {
1137 StringName name = StringName(functions[i].get_slice(":", 0));
1138 if (methods_found.has(name)) {
1139 continue;
1140 }
1141
1142 String found_base_class;
1143 StringName base_class = script->get_instance_base_type();
1144 Ref<Script> inherited_script = script->get_base_script();
1145 while (!inherited_script.is_null()) {
1146 if (inherited_script->has_method(name)) {
1147 found_base_class = "script:" + inherited_script->get_path();
1148 break;
1149 }
1150
1151 base_class = inherited_script->get_instance_base_type();
1152 inherited_script = inherited_script->get_base_script();
1153 }
1154
1155 if (found_base_class.is_empty()) {
1156 while (base_class) {
1157 List<MethodInfo> methods;
1158 ClassDB::get_method_list(base_class, &methods, true);
1159 for (int j = 0; j < methods.size(); j++) {
1160 if (methods[j].name == name) {
1161 found_base_class = "builtin:" + base_class;
1162 break;
1163 }
1164 }
1165
1166 ClassDB::ClassInfo *base_class_ptr = ClassDB::classes.getptr(base_class)->inherits_ptr;
1167 if (base_class_ptr == nullptr) {
1168 break;
1169 }
1170 base_class = base_class_ptr->name;
1171 }
1172 }
1173
1174 if (!found_base_class.is_empty()) {
1175 int line = functions[i].get_slice(":", 1).to_int() - 1;
1176
1177 Dictionary line_meta = text_edit->get_line_gutter_metadata(line, connection_gutter);
1178 if (line_meta.is_empty()) {
1179 // Add override icon to gutter.
1180 line_meta["type"] = "inherits";
1181 line_meta["method"] = name;
1182 line_meta["base_class"] = found_base_class;
1183 text_edit->set_line_gutter_icon(line, connection_gutter, get_parent_control()->get_editor_theme_icon(SNAME("MethodOverride")));
1184 text_edit->set_line_gutter_clickable(line, connection_gutter, true);
1185 } else {
1186 // If method is also connected to signal, then merge icons and keep the click behavior of the slot.
1187 text_edit->set_line_gutter_icon(line, connection_gutter, get_parent_control()->get_editor_theme_icon(SNAME("MethodOverrideAndSlot")));
1188 }
1189
1190 methods_found.insert(name);
1191 }
1192 }
1193}
1194
1195void ScriptTextEditor::_update_gutter_indexes() {
1196 for (int i = 0; i < code_editor->get_text_editor()->get_gutter_count(); i++) {
1197 if (code_editor->get_text_editor()->get_gutter_name(i) == "connection_gutter") {
1198 connection_gutter = i;
1199 continue;
1200 }
1201
1202 if (code_editor->get_text_editor()->get_gutter_name(i) == "line_numbers") {
1203 line_number_gutter = i;
1204 continue;
1205 }
1206 }
1207}
1208
1209void ScriptTextEditor::_gutter_clicked(int p_line, int p_gutter) {
1210 if (p_gutter != connection_gutter) {
1211 return;
1212 }
1213
1214 Dictionary meta = code_editor->get_text_editor()->get_line_gutter_metadata(p_line, p_gutter);
1215 String type = meta.get("type", "");
1216 if (type.is_empty()) {
1217 return;
1218 }
1219
1220 // All types currently need a method name.
1221 String method = meta.get("method", "");
1222 if (method.is_empty()) {
1223 return;
1224 }
1225
1226 if (type == "connection") {
1227 Node *base = get_tree()->get_edited_scene_root();
1228 if (!base) {
1229 return;
1230 }
1231
1232 Vector<Node *> nodes = _find_all_node_for_script(base, base, script);
1233 connection_info_dialog->popup_connections(method, nodes);
1234 } else if (type == "inherits") {
1235 String base_class_raw = meta["base_class"];
1236 PackedStringArray base_class_split = base_class_raw.split(":", true, 1);
1237
1238 if (base_class_split[0] == "script") {
1239 // Go to function declaration.
1240 Ref<Script> base_script = ResourceLoader::load(base_class_split[1]);
1241 ERR_FAIL_COND(!base_script.is_valid());
1242 emit_signal(SNAME("go_to_method"), base_script, method);
1243 } else if (base_class_split[0] == "builtin") {
1244 // Open method documentation.
1245 emit_signal(SNAME("go_to_help"), "class_method:" + base_class_split[1] + ":" + method);
1246 }
1247 }
1248}
1249
1250void ScriptTextEditor::_edit_option(int p_op) {
1251 CodeEdit *tx = code_editor->get_text_editor();
1252
1253 switch (p_op) {
1254 case EDIT_UNDO: {
1255 tx->undo();
1256 tx->call_deferred(SNAME("grab_focus"));
1257 } break;
1258 case EDIT_REDO: {
1259 tx->redo();
1260 tx->call_deferred(SNAME("grab_focus"));
1261 } break;
1262 case EDIT_CUT: {
1263 tx->cut();
1264 tx->call_deferred(SNAME("grab_focus"));
1265 } break;
1266 case EDIT_COPY: {
1267 tx->copy();
1268 tx->call_deferred(SNAME("grab_focus"));
1269 } break;
1270 case EDIT_PASTE: {
1271 tx->paste();
1272 tx->call_deferred(SNAME("grab_focus"));
1273 } break;
1274 case EDIT_SELECT_ALL: {
1275 tx->select_all();
1276 tx->call_deferred(SNAME("grab_focus"));
1277 } break;
1278 case EDIT_MOVE_LINE_UP: {
1279 code_editor->move_lines_up();
1280 } break;
1281 case EDIT_MOVE_LINE_DOWN: {
1282 code_editor->move_lines_down();
1283 } break;
1284 case EDIT_INDENT: {
1285 Ref<Script> scr = script;
1286 if (scr.is_null()) {
1287 return;
1288 }
1289 tx->indent_lines();
1290 } break;
1291 case EDIT_UNINDENT: {
1292 Ref<Script> scr = script;
1293 if (scr.is_null()) {
1294 return;
1295 }
1296 tx->unindent_lines();
1297 } break;
1298 case EDIT_DELETE_LINE: {
1299 code_editor->delete_lines();
1300 } break;
1301 case EDIT_DUPLICATE_SELECTION: {
1302 code_editor->duplicate_selection();
1303 } break;
1304 case EDIT_TOGGLE_FOLD_LINE: {
1305 int previous_line = -1;
1306 for (int caret_idx : tx->get_caret_index_edit_order()) {
1307 int line_idx = tx->get_caret_line(caret_idx);
1308 if (line_idx != previous_line) {
1309 tx->toggle_foldable_line(line_idx);
1310 previous_line = line_idx;
1311 }
1312 }
1313 tx->queue_redraw();
1314 } break;
1315 case EDIT_FOLD_ALL_LINES: {
1316 tx->fold_all_lines();
1317 tx->queue_redraw();
1318 } break;
1319 case EDIT_UNFOLD_ALL_LINES: {
1320 tx->unfold_all_lines();
1321 tx->queue_redraw();
1322 } break;
1323 case EDIT_CREATE_CODE_REGION: {
1324 tx->create_code_region();
1325 } break;
1326 case EDIT_TOGGLE_COMMENT: {
1327 _edit_option_toggle_inline_comment();
1328 } break;
1329 case EDIT_COMPLETE: {
1330 tx->request_code_completion(true);
1331 } break;
1332 case EDIT_AUTO_INDENT: {
1333 String text = tx->get_text();
1334 Ref<Script> scr = script;
1335 if (scr.is_null()) {
1336 return;
1337 }
1338
1339 tx->begin_complex_operation();
1340 int begin, end;
1341 if (tx->has_selection()) {
1342 begin = tx->get_selection_from_line();
1343 end = tx->get_selection_to_line();
1344 // ignore if the cursor is not past the first column
1345 if (tx->get_selection_to_column() == 0) {
1346 end--;
1347 }
1348 } else {
1349 begin = 0;
1350 end = tx->get_line_count() - 1;
1351 }
1352 scr->get_language()->auto_indent_code(text, begin, end);
1353 Vector<String> lines = text.split("\n");
1354 for (int i = begin; i <= end; ++i) {
1355 tx->set_line(i, lines[i]);
1356 }
1357
1358 tx->end_complex_operation();
1359 } break;
1360 case EDIT_TRIM_TRAILING_WHITESAPCE: {
1361 trim_trailing_whitespace();
1362 } break;
1363 case EDIT_CONVERT_INDENT_TO_SPACES: {
1364 tx->set_indent_using_spaces(true);
1365 convert_indent();
1366 } break;
1367 case EDIT_CONVERT_INDENT_TO_TABS: {
1368 tx->set_indent_using_spaces(false);
1369 convert_indent();
1370 } break;
1371 case EDIT_PICK_COLOR: {
1372 color_panel->popup();
1373 } break;
1374 case EDIT_TO_UPPERCASE: {
1375 _convert_case(CodeTextEditor::UPPER);
1376 } break;
1377 case EDIT_TO_LOWERCASE: {
1378 _convert_case(CodeTextEditor::LOWER);
1379 } break;
1380 case EDIT_CAPITALIZE: {
1381 _convert_case(CodeTextEditor::CAPITALIZE);
1382 } break;
1383 case EDIT_EVALUATE: {
1384 Expression expression;
1385 tx->begin_complex_operation();
1386 for (int caret_idx = 0; caret_idx < tx->get_caret_count(); caret_idx++) {
1387 Vector<String> lines = tx->get_selected_text(caret_idx).split("\n");
1388 PackedStringArray results;
1389
1390 for (int i = 0; i < lines.size(); i++) {
1391 String line = lines[i];
1392 String whitespace = line.substr(0, line.size() - line.strip_edges(true, false).size()); // Extract the whitespace at the beginning.
1393 if (expression.parse(line) == OK) {
1394 Variant result = expression.execute(Array(), Variant(), false, true);
1395 if (expression.get_error_text().is_empty()) {
1396 results.push_back(whitespace + result.get_construct_string());
1397 } else {
1398 results.push_back(line);
1399 }
1400 } else {
1401 results.push_back(line);
1402 }
1403 }
1404 tx->insert_text_at_caret(String("\n").join(results), caret_idx);
1405 }
1406 tx->end_complex_operation();
1407 } break;
1408 case EDIT_TOGGLE_WORD_WRAP: {
1409 TextEdit::LineWrappingMode wrap = code_editor->get_text_editor()->get_line_wrapping_mode();
1410 code_editor->get_text_editor()->set_line_wrapping_mode(wrap == TextEdit::LINE_WRAPPING_BOUNDARY ? TextEdit::LINE_WRAPPING_NONE : TextEdit::LINE_WRAPPING_BOUNDARY);
1411 } break;
1412 case SEARCH_FIND: {
1413 code_editor->get_find_replace_bar()->popup_search();
1414 } break;
1415 case SEARCH_FIND_NEXT: {
1416 code_editor->get_find_replace_bar()->search_next();
1417 } break;
1418 case SEARCH_FIND_PREV: {
1419 code_editor->get_find_replace_bar()->search_prev();
1420 } break;
1421 case SEARCH_REPLACE: {
1422 code_editor->get_find_replace_bar()->popup_replace();
1423 } break;
1424 case SEARCH_IN_FILES: {
1425 String selected_text = tx->get_selected_text();
1426
1427 // Yep, because it doesn't make sense to instance this dialog for every single script open...
1428 // So this will be delegated to the ScriptEditor.
1429 emit_signal(SNAME("search_in_files_requested"), selected_text);
1430 } break;
1431 case REPLACE_IN_FILES: {
1432 String selected_text = tx->get_selected_text();
1433
1434 emit_signal(SNAME("replace_in_files_requested"), selected_text);
1435 } break;
1436 case SEARCH_LOCATE_FUNCTION: {
1437 quick_open->popup_dialog(get_functions());
1438 quick_open->set_title(TTR("Go to Function"));
1439 } break;
1440 case SEARCH_GOTO_LINE: {
1441 goto_line_dialog->popup_find_line(tx);
1442 } break;
1443 case BOOKMARK_TOGGLE: {
1444 code_editor->toggle_bookmark();
1445 } break;
1446 case BOOKMARK_GOTO_NEXT: {
1447 code_editor->goto_next_bookmark();
1448 } break;
1449 case BOOKMARK_GOTO_PREV: {
1450 code_editor->goto_prev_bookmark();
1451 } break;
1452 case BOOKMARK_REMOVE_ALL: {
1453 code_editor->remove_all_bookmarks();
1454 } break;
1455 case DEBUG_TOGGLE_BREAKPOINT: {
1456 Vector<int> caret_edit_order = tx->get_caret_index_edit_order();
1457 caret_edit_order.reverse();
1458 int last_line = -1;
1459 for (const int &c : caret_edit_order) {
1460 int from = tx->has_selection(c) ? tx->get_selection_from_line(c) : tx->get_caret_line(c);
1461 from += from == last_line ? 1 : 0;
1462 int to = tx->has_selection(c) ? tx->get_selection_to_line(c) : tx->get_caret_line(c);
1463 if (to < from) {
1464 continue;
1465 }
1466 // Check first if there's any lines with breakpoints in the selection.
1467 bool selection_has_breakpoints = false;
1468 for (int line = from; line <= to; line++) {
1469 if (tx->is_line_breakpointed(line)) {
1470 selection_has_breakpoints = true;
1471 break;
1472 }
1473 }
1474
1475 // Set breakpoint on caret or remove all bookmarks from the selection.
1476 if (!selection_has_breakpoints) {
1477 if (tx->get_caret_line(c) != last_line) {
1478 tx->set_line_as_breakpoint(tx->get_caret_line(c), true);
1479 }
1480 } else {
1481 for (int line = from; line <= to; line++) {
1482 tx->set_line_as_breakpoint(line, false);
1483 }
1484 }
1485 last_line = to;
1486 }
1487 } break;
1488 case DEBUG_REMOVE_ALL_BREAKPOINTS: {
1489 PackedInt32Array bpoints = tx->get_breakpointed_lines();
1490
1491 for (int i = 0; i < bpoints.size(); i++) {
1492 int line = bpoints[i];
1493 bool dobreak = !tx->is_line_breakpointed(line);
1494 tx->set_line_as_breakpoint(line, dobreak);
1495 EditorDebuggerNode::get_singleton()->set_breakpoint(script->get_path(), line + 1, dobreak);
1496 }
1497 } break;
1498 case DEBUG_GOTO_NEXT_BREAKPOINT: {
1499 PackedInt32Array bpoints = tx->get_breakpointed_lines();
1500 if (bpoints.size() <= 0) {
1501 return;
1502 }
1503
1504 int current_line = tx->get_caret_line();
1505 int bpoint_idx = 0;
1506 if (current_line < (int)bpoints[bpoints.size() - 1]) {
1507 while (bpoint_idx < bpoints.size() && bpoints[bpoint_idx] <= current_line) {
1508 bpoint_idx++;
1509 }
1510 }
1511 code_editor->goto_line_centered(bpoints[bpoint_idx]);
1512 } break;
1513 case DEBUG_GOTO_PREV_BREAKPOINT: {
1514 PackedInt32Array bpoints = tx->get_breakpointed_lines();
1515 if (bpoints.size() <= 0) {
1516 return;
1517 }
1518
1519 int current_line = tx->get_caret_line();
1520 int bpoint_idx = bpoints.size() - 1;
1521 if (current_line > (int)bpoints[0]) {
1522 while (bpoint_idx >= 0 && bpoints[bpoint_idx] >= current_line) {
1523 bpoint_idx--;
1524 }
1525 }
1526 code_editor->goto_line_centered(bpoints[bpoint_idx]);
1527 } break;
1528 case HELP_CONTEXTUAL: {
1529 String text = tx->get_selected_text(0);
1530 if (text.is_empty()) {
1531 text = tx->get_word_under_caret(0);
1532 }
1533 if (!text.is_empty()) {
1534 emit_signal(SNAME("request_help"), text);
1535 }
1536 } break;
1537 case LOOKUP_SYMBOL: {
1538 String text = tx->get_word_under_caret(0);
1539 if (text.is_empty()) {
1540 text = tx->get_selected_text(0);
1541 }
1542 if (!text.is_empty()) {
1543 _lookup_symbol(text, tx->get_caret_line(0), tx->get_caret_column(0));
1544 }
1545 } break;
1546 }
1547}
1548
1549void ScriptTextEditor::_edit_option_toggle_inline_comment() {
1550 if (script.is_null()) {
1551 return;
1552 }
1553
1554 String delimiter = "#";
1555 List<String> comment_delimiters;
1556 script->get_language()->get_comment_delimiters(&comment_delimiters);
1557
1558 for (const String &script_delimiter : comment_delimiters) {
1559 if (!script_delimiter.contains(" ")) {
1560 delimiter = script_delimiter;
1561 break;
1562 }
1563 }
1564
1565 code_editor->toggle_inline_comment(delimiter);
1566}
1567
1568void ScriptTextEditor::add_syntax_highlighter(Ref<EditorSyntaxHighlighter> p_highlighter) {
1569 ERR_FAIL_COND(p_highlighter.is_null());
1570
1571 highlighters[p_highlighter->_get_name()] = p_highlighter;
1572 highlighter_menu->add_radio_check_item(p_highlighter->_get_name());
1573}
1574
1575void ScriptTextEditor::set_syntax_highlighter(Ref<EditorSyntaxHighlighter> p_highlighter) {
1576 ERR_FAIL_COND(p_highlighter.is_null());
1577
1578 HashMap<String, Ref<EditorSyntaxHighlighter>>::Iterator el = highlighters.begin();
1579 while (el) {
1580 int highlighter_index = highlighter_menu->get_item_idx_from_text(el->key);
1581 highlighter_menu->set_item_checked(highlighter_index, el->value == p_highlighter);
1582 ++el;
1583 }
1584
1585 CodeEdit *te = code_editor->get_text_editor();
1586 p_highlighter->_set_edited_resource(script);
1587 te->set_syntax_highlighter(p_highlighter);
1588}
1589
1590void ScriptTextEditor::_change_syntax_highlighter(int p_idx) {
1591 set_syntax_highlighter(highlighters[highlighter_menu->get_item_text(p_idx)]);
1592}
1593
1594void ScriptTextEditor::_notification(int p_what) {
1595 switch (p_what) {
1596 case NOTIFICATION_THEME_CHANGED:
1597 if (!editor_enabled) {
1598 break;
1599 }
1600 if (is_visible_in_tree()) {
1601 _update_warnings();
1602 _update_errors();
1603 }
1604 [[fallthrough]];
1605 case NOTIFICATION_ENTER_TREE: {
1606 code_editor->get_text_editor()->set_gutter_width(connection_gutter, code_editor->get_text_editor()->get_line_height());
1607 } break;
1608 }
1609}
1610
1611Control *ScriptTextEditor::get_edit_menu() {
1612 return edit_hb;
1613}
1614
1615void ScriptTextEditor::clear_edit_menu() {
1616 if (editor_enabled) {
1617 memdelete(edit_hb);
1618 }
1619}
1620
1621void ScriptTextEditor::set_find_replace_bar(FindReplaceBar *p_bar) {
1622 code_editor->set_find_replace_bar(p_bar);
1623}
1624
1625void ScriptTextEditor::reload(bool p_soft) {
1626 CodeEdit *te = code_editor->get_text_editor();
1627 Ref<Script> scr = script;
1628 if (scr.is_null()) {
1629 return;
1630 }
1631 scr->set_source_code(te->get_text());
1632 bool soft = p_soft || scr->get_instance_base_type() == "EditorPlugin"; // Always soft-reload editor plugins.
1633
1634 scr->get_language()->reload_tool_script(scr, soft);
1635}
1636
1637PackedInt32Array ScriptTextEditor::get_breakpoints() {
1638 return code_editor->get_text_editor()->get_breakpointed_lines();
1639}
1640
1641void ScriptTextEditor::set_breakpoint(int p_line, bool p_enabled) {
1642 code_editor->get_text_editor()->set_line_as_breakpoint(p_line, p_enabled);
1643}
1644
1645void ScriptTextEditor::clear_breakpoints() {
1646 code_editor->get_text_editor()->clear_breakpointed_lines();
1647}
1648
1649void ScriptTextEditor::set_tooltip_request_func(const Callable &p_toolip_callback) {
1650 Variant args[1] = { this };
1651 const Variant *argp[] = { &args[0] };
1652 code_editor->get_text_editor()->set_tooltip_request_func(p_toolip_callback.bindp(argp, 1));
1653}
1654
1655void ScriptTextEditor::set_debugger_active(bool p_active) {
1656}
1657
1658Control *ScriptTextEditor::get_base_editor() const {
1659 return code_editor->get_text_editor();
1660}
1661
1662Variant ScriptTextEditor::get_drag_data_fw(const Point2 &p_point, Control *p_from) {
1663 return Variant();
1664}
1665
1666bool ScriptTextEditor::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const {
1667 Dictionary d = p_data;
1668 if (d.has("type") &&
1669 (String(d["type"]) == "resource" ||
1670 String(d["type"]) == "files" ||
1671 String(d["type"]) == "nodes" ||
1672 String(d["type"]) == "obj_property" ||
1673 String(d["type"]) == "files_and_dirs")) {
1674 return true;
1675 }
1676
1677 return false;
1678}
1679
1680static Node *_find_script_node(Node *p_edited_scene, Node *p_current_node, const Ref<Script> &script) {
1681 // Check scripts only for the nodes belonging to the edited scene.
1682 if (p_current_node == p_edited_scene || p_current_node->get_owner() == p_edited_scene) {
1683 Ref<Script> scr = p_current_node->get_script();
1684 if (scr.is_valid() && scr == script) {
1685 return p_current_node;
1686 }
1687 }
1688
1689 // Traverse all children, even the ones not owned by the edited scene as they
1690 // can still have child nodes added within the edited scene and thus owned by
1691 // it (e.g. nodes added to subscene's root or to its editable children).
1692 for (int i = 0; i < p_current_node->get_child_count(); i++) {
1693 Node *n = _find_script_node(p_edited_scene, p_current_node->get_child(i), script);
1694 if (n) {
1695 return n;
1696 }
1697 }
1698
1699 return nullptr;
1700}
1701
1702static String _quote_drop_data(const String &str) {
1703 // This function prepares a string for being "dropped" into the script editor.
1704 // The string can be a resource path, node path or property name.
1705
1706 const bool using_single_quotes = EDITOR_GET("text_editor/completion/use_single_quotes");
1707
1708 String escaped = str.c_escape();
1709
1710 // If string is double quoted, there is no need to escape single quotes.
1711 // We can revert the extra escaping added in c_escape().
1712 if (!using_single_quotes) {
1713 escaped = escaped.replace("\\'", "\'");
1714 }
1715
1716 return escaped.quote(using_single_quotes ? "'" : "\"");
1717}
1718
1719void ScriptTextEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) {
1720 Dictionary d = p_data;
1721
1722 CodeEdit *te = code_editor->get_text_editor();
1723 Point2i pos = te->get_line_column_at_pos(p_point);
1724 int row = pos.y;
1725 int col = pos.x;
1726
1727 if (d.has("type") && String(d["type"]) == "resource") {
1728 te->remove_secondary_carets();
1729 Ref<Resource> res = d["resource"];
1730 if (!res.is_valid()) {
1731 return;
1732 }
1733
1734 if (res->get_path().is_resource_file()) {
1735 EditorNode::get_singleton()->show_warning(TTR("Only resources from filesystem can be dropped."));
1736 return;
1737 }
1738
1739 te->set_caret_line(row);
1740 te->set_caret_column(col);
1741 te->insert_text_at_caret(res->get_path());
1742 te->grab_focus();
1743 }
1744
1745 if (d.has("type") && (String(d["type"]) == "files" || String(d["type"]) == "files_and_dirs")) {
1746 te->remove_secondary_carets();
1747 Array files = d["files"];
1748
1749 String text_to_drop;
1750 bool preload = Input::get_singleton()->is_key_pressed(Key::CTRL);
1751 for (int i = 0; i < files.size(); i++) {
1752 if (i > 0) {
1753 text_to_drop += ", ";
1754 }
1755
1756 if (preload) {
1757 text_to_drop += "preload(" + _quote_drop_data(String(files[i])) + ")";
1758 } else {
1759 text_to_drop += _quote_drop_data(String(files[i]));
1760 }
1761 }
1762
1763 te->set_caret_line(row);
1764 te->set_caret_column(col);
1765 te->insert_text_at_caret(text_to_drop);
1766 te->grab_focus();
1767 }
1768
1769 if (d.has("type") && String(d["type"]) == "nodes") {
1770 te->remove_secondary_carets();
1771 Node *scene_root = get_tree()->get_edited_scene_root();
1772 if (!scene_root) {
1773 EditorNode::get_singleton()->show_warning(TTR("Can't drop nodes without an open scene."));
1774 return;
1775 }
1776
1777 if (!ClassDB::is_parent_class(script->get_instance_base_type(), "Node")) {
1778 EditorToaster::get_singleton()->popup_str(vformat(TTR("Can't drop nodes because script '%s' does not inherit Node."), get_name()), EditorToaster::SEVERITY_WARNING);
1779 return;
1780 }
1781
1782 Node *sn = _find_script_node(scene_root, scene_root, script);
1783 if (!sn) {
1784 sn = scene_root;
1785 }
1786
1787 Array nodes = d["nodes"];
1788 String text_to_drop;
1789
1790 if (Input::get_singleton()->is_key_pressed(Key::CTRL)) {
1791 bool use_type = EDITOR_GET("text_editor/completion/add_type_hints");
1792 for (int i = 0; i < nodes.size(); i++) {
1793 NodePath np = nodes[i];
1794 Node *node = get_node(np);
1795 if (!node) {
1796 continue;
1797 }
1798
1799 bool is_unique = false;
1800 String path;
1801 if (node->is_unique_name_in_owner()) {
1802 path = node->get_name();
1803 is_unique = true;
1804 } else {
1805 path = sn->get_path_to(node);
1806 }
1807 for (const String &segment : path.split("/")) {
1808 if (!segment.is_valid_identifier()) {
1809 path = _quote_drop_data(path);
1810 break;
1811 }
1812 }
1813
1814 String variable_name = String(node->get_name()).to_snake_case().validate_identifier();
1815 if (use_type) {
1816 StringName class_name = node->get_class_name();
1817 Ref<Script> node_script = node->get_script();
1818 if (node_script.is_valid()) {
1819 StringName global_node_script_name = node_script->get_global_name();
1820 if (global_node_script_name != StringName()) {
1821 class_name = global_node_script_name;
1822 }
1823 }
1824 text_to_drop += vformat("@onready var %s: %s = %c%s\n", variable_name, class_name, is_unique ? '%' : '$', path);
1825 } else {
1826 text_to_drop += vformat("@onready var %s = %c%s\n", variable_name, is_unique ? '%' : '$', path);
1827 }
1828 }
1829 } else {
1830 for (int i = 0; i < nodes.size(); i++) {
1831 if (i > 0) {
1832 text_to_drop += ", ";
1833 }
1834
1835 NodePath np = nodes[i];
1836 Node *node = get_node(np);
1837 if (!node) {
1838 continue;
1839 }
1840
1841 bool is_unique = false;
1842 String path;
1843 if (node->is_unique_name_in_owner()) {
1844 path = node->get_name();
1845 is_unique = true;
1846 } else {
1847 path = sn->get_path_to(node);
1848 }
1849
1850 for (const String &segment : path.split("/")) {
1851 if (!segment.is_valid_identifier()) {
1852 path = _quote_drop_data(path);
1853 break;
1854 }
1855 }
1856 text_to_drop += (is_unique ? "%" : "$") + path;
1857 }
1858 }
1859
1860 te->set_caret_line(row);
1861 te->set_caret_column(col);
1862 te->insert_text_at_caret(text_to_drop);
1863 te->grab_focus();
1864 }
1865
1866 if (d.has("type") && String(d["type"]) == "obj_property") {
1867 te->remove_secondary_carets();
1868 // It is unclear whether properties may contain single or double quotes.
1869 // Assume here that double-quotes may not exist. We are escaping single-quotes if necessary.
1870 const String text_to_drop = _quote_drop_data(String(d["property"]));
1871
1872 te->set_caret_line(row);
1873 te->set_caret_column(col);
1874 te->insert_text_at_caret(text_to_drop);
1875 te->grab_focus();
1876 }
1877}
1878
1879void ScriptTextEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) {
1880 Ref<InputEventMouseButton> mb = ev;
1881 Ref<InputEventKey> k = ev;
1882 Point2 local_pos;
1883 bool create_menu = false;
1884
1885 CodeEdit *tx = code_editor->get_text_editor();
1886 if (mb.is_valid() && mb->get_button_index() == MouseButton::RIGHT && mb->is_pressed()) {
1887 local_pos = mb->get_global_position() - tx->get_global_position();
1888 create_menu = true;
1889 } else if (k.is_valid() && k->is_action("ui_menu", true)) {
1890 tx->adjust_viewport_to_caret(0);
1891 local_pos = tx->get_caret_draw_pos(0);
1892 create_menu = true;
1893 }
1894
1895 if (create_menu) {
1896 Point2i pos = tx->get_line_column_at_pos(local_pos);
1897 int row = pos.y;
1898 int col = pos.x;
1899
1900 tx->set_move_caret_on_right_click_enabled(EDITOR_GET("text_editor/behavior/navigation/move_caret_on_right_click"));
1901 int caret_clicked = -1;
1902 if (tx->is_move_caret_on_right_click_enabled()) {
1903 if (tx->has_selection()) {
1904 for (int i = 0; i < tx->get_caret_count(); i++) {
1905 int from_line = tx->get_selection_from_line(i);
1906 int to_line = tx->get_selection_to_line(i);
1907 int from_column = tx->get_selection_from_column(i);
1908 int to_column = tx->get_selection_to_column(i);
1909
1910 if (row >= from_line && row <= to_line && (row != from_line || col >= from_column) && (row != to_line || col <= to_column)) {
1911 // Right click in one of the selected text
1912 caret_clicked = i;
1913 break;
1914 }
1915 }
1916 }
1917 if (caret_clicked < 0) {
1918 tx->deselect();
1919 tx->remove_secondary_carets();
1920 caret_clicked = 0;
1921 tx->set_caret_line(row, false, false);
1922 tx->set_caret_column(col);
1923 }
1924 }
1925
1926 String word_at_pos = tx->get_word_at_pos(local_pos);
1927 if (word_at_pos.is_empty()) {
1928 word_at_pos = tx->get_word_under_caret(caret_clicked);
1929 }
1930 if (word_at_pos.is_empty()) {
1931 word_at_pos = tx->get_selected_text(caret_clicked);
1932 }
1933
1934 bool has_color = (word_at_pos == "Color");
1935 bool foldable = tx->can_fold_line(row) || tx->is_line_folded(row);
1936 bool open_docs = false;
1937 bool goto_definition = false;
1938
1939 if (ScriptServer::is_global_class(word_at_pos) || word_at_pos.is_resource_file()) {
1940 open_docs = true;
1941 } else {
1942 Node *base = get_tree()->get_edited_scene_root();
1943 if (base) {
1944 base = _find_node_for_script(base, base, script);
1945 }
1946 ScriptLanguage::LookupResult result;
1947 if (script->get_language()->lookup_code(tx->get_text_for_symbol_lookup(), word_at_pos, script->get_path(), base, result) == OK) {
1948 open_docs = true;
1949 }
1950 }
1951
1952 if (has_color) {
1953 String line = tx->get_line(row);
1954 color_position.x = row;
1955 color_position.y = col;
1956
1957 int begin = -1;
1958 int end = -1;
1959 enum EXPRESSION_PATTERNS {
1960 NOT_PARSED,
1961 RGBA_PARAMETER, // Color(float,float,float) or Color(float,float,float,float)
1962 COLOR_NAME, // Color.COLOR_NAME
1963 } expression_pattern = NOT_PARSED;
1964
1965 for (int i = col; i < line.length(); i++) {
1966 if (line[i] == '(') {
1967 if (expression_pattern == NOT_PARSED) {
1968 begin = i;
1969 expression_pattern = RGBA_PARAMETER;
1970 } else {
1971 // Method call or '(' appearing twice.
1972 expression_pattern = NOT_PARSED;
1973
1974 break;
1975 }
1976 } else if (expression_pattern == RGBA_PARAMETER && line[i] == ')' && end < 0) {
1977 end = i + 1;
1978
1979 break;
1980 } else if (expression_pattern == NOT_PARSED && line[i] == '.') {
1981 begin = i;
1982 expression_pattern = COLOR_NAME;
1983 } else if (expression_pattern == COLOR_NAME && end < 0 && (line[i] == ' ' || line[i] == '\t')) {
1984 // Including '.' and spaces.
1985 continue;
1986 } else if (expression_pattern == COLOR_NAME && !(line[i] == '_' || ('A' <= line[i] && line[i] <= 'Z'))) {
1987 end = i;
1988
1989 break;
1990 }
1991 }
1992
1993 switch (expression_pattern) {
1994 case RGBA_PARAMETER: {
1995 color_args = line.substr(begin, end - begin);
1996 String stripped = color_args.replace(" ", "").replace("\t", "").replace("(", "").replace(")", "");
1997 PackedFloat64Array color = stripped.split_floats(",");
1998 if (color.size() > 2) {
1999 float alpha = color.size() > 3 ? color[3] : 1.0f;
2000 color_picker->set_pick_color(Color(color[0], color[1], color[2], alpha));
2001 }
2002 } break;
2003 case COLOR_NAME: {
2004 if (end < 0) {
2005 end = line.length();
2006 }
2007 color_args = line.substr(begin, end - begin);
2008 const String color_name = color_args.replace(" ", "").replace("\t", "").replace(".", "");
2009 const int color_index = Color::find_named_color(color_name);
2010 if (0 <= color_index) {
2011 const Color color_constant = Color::get_named_color(color_index);
2012 color_picker->set_pick_color(color_constant);
2013 } else {
2014 has_color = false;
2015 }
2016 } break;
2017 default:
2018 has_color = false;
2019 break;
2020 }
2021 if (has_color) {
2022 color_panel->set_position(get_screen_position() + local_pos);
2023 }
2024 }
2025 _make_context_menu(tx->has_selection(), has_color, foldable, open_docs, goto_definition, local_pos);
2026 }
2027}
2028
2029void ScriptTextEditor::_color_changed(const Color &p_color) {
2030 String new_args;
2031 if (p_color.a == 1.0f) {
2032 new_args = String("(" + rtos(p_color.r) + ", " + rtos(p_color.g) + ", " + rtos(p_color.b) + ")");
2033 } else {
2034 new_args = String("(" + rtos(p_color.r) + ", " + rtos(p_color.g) + ", " + rtos(p_color.b) + ", " + rtos(p_color.a) + ")");
2035 }
2036
2037 String line = code_editor->get_text_editor()->get_line(color_position.x);
2038 String line_with_replaced_args = line.replace(color_args, new_args);
2039
2040 color_args = new_args;
2041 code_editor->get_text_editor()->begin_complex_operation();
2042 code_editor->get_text_editor()->set_line(color_position.x, line_with_replaced_args);
2043 code_editor->get_text_editor()->end_complex_operation();
2044 code_editor->get_text_editor()->queue_redraw();
2045}
2046
2047void ScriptTextEditor::_prepare_edit_menu() {
2048 const CodeEdit *tx = code_editor->get_text_editor();
2049 PopupMenu *popup = edit_menu->get_popup();
2050 popup->set_item_disabled(popup->get_item_index(EDIT_UNDO), !tx->has_undo());
2051 popup->set_item_disabled(popup->get_item_index(EDIT_REDO), !tx->has_redo());
2052}
2053
2054void ScriptTextEditor::_make_context_menu(bool p_selection, bool p_color, bool p_foldable, bool p_open_docs, bool p_goto_definition, Vector2 p_pos) {
2055 context_menu->clear();
2056 context_menu->add_shortcut(ED_GET_SHORTCUT("ui_undo"), EDIT_UNDO);
2057 context_menu->add_shortcut(ED_GET_SHORTCUT("ui_redo"), EDIT_REDO);
2058
2059 context_menu->add_separator();
2060 context_menu->add_shortcut(ED_GET_SHORTCUT("ui_cut"), EDIT_CUT);
2061 context_menu->add_shortcut(ED_GET_SHORTCUT("ui_copy"), EDIT_COPY);
2062 context_menu->add_shortcut(ED_GET_SHORTCUT("ui_paste"), EDIT_PASTE);
2063
2064 context_menu->add_separator();
2065 context_menu->add_shortcut(ED_GET_SHORTCUT("ui_text_select_all"), EDIT_SELECT_ALL);
2066
2067 context_menu->add_separator();
2068 context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/indent"), EDIT_INDENT);
2069 context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/unindent"), EDIT_UNINDENT);
2070 context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_comment"), EDIT_TOGGLE_COMMENT);
2071 context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_bookmark"), BOOKMARK_TOGGLE);
2072
2073 if (p_selection) {
2074 context_menu->add_separator();
2075 context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/convert_to_uppercase"), EDIT_TO_UPPERCASE);
2076 context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/convert_to_lowercase"), EDIT_TO_LOWERCASE);
2077 context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/evaluate_selection"), EDIT_EVALUATE);
2078 context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/create_code_region"), EDIT_CREATE_CODE_REGION);
2079 }
2080 if (p_foldable) {
2081 context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_fold_line"), EDIT_TOGGLE_FOLD_LINE);
2082 }
2083
2084 if (p_color || p_open_docs || p_goto_definition) {
2085 context_menu->add_separator();
2086 if (p_open_docs) {
2087 context_menu->add_item(TTR("Lookup Symbol"), LOOKUP_SYMBOL);
2088 }
2089 if (p_color) {
2090 context_menu->add_item(TTR("Pick Color"), EDIT_PICK_COLOR);
2091 }
2092 }
2093
2094 const CodeEdit *tx = code_editor->get_text_editor();
2095 context_menu->set_item_disabled(context_menu->get_item_index(EDIT_UNDO), !tx->has_undo());
2096 context_menu->set_item_disabled(context_menu->get_item_index(EDIT_REDO), !tx->has_redo());
2097
2098 context_menu->set_position(get_screen_position() + p_pos);
2099 context_menu->reset_size();
2100 context_menu->popup();
2101}
2102
2103void ScriptTextEditor::_enable_code_editor() {
2104 ERR_FAIL_COND(code_editor->get_parent());
2105
2106 VSplitContainer *editor_box = memnew(VSplitContainer);
2107 add_child(editor_box);
2108 editor_box->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
2109 editor_box->set_v_size_flags(SIZE_EXPAND_FILL);
2110
2111 editor_box->add_child(code_editor);
2112 code_editor->connect("show_errors_panel", callable_mp(this, &ScriptTextEditor::_show_errors_panel));
2113 code_editor->connect("show_warnings_panel", callable_mp(this, &ScriptTextEditor::_show_warnings_panel));
2114 code_editor->connect("validate_script", callable_mp(this, &ScriptTextEditor::_validate_script));
2115 code_editor->connect("load_theme_settings", callable_mp(this, &ScriptTextEditor::_load_theme_settings));
2116 code_editor->get_text_editor()->connect("symbol_lookup", callable_mp(this, &ScriptTextEditor::_lookup_symbol));
2117 code_editor->get_text_editor()->connect("symbol_validate", callable_mp(this, &ScriptTextEditor::_validate_symbol));
2118 code_editor->get_text_editor()->connect("gutter_added", callable_mp(this, &ScriptTextEditor::_update_gutter_indexes));
2119 code_editor->get_text_editor()->connect("gutter_removed", callable_mp(this, &ScriptTextEditor::_update_gutter_indexes));
2120 code_editor->get_text_editor()->connect("gutter_clicked", callable_mp(this, &ScriptTextEditor::_gutter_clicked));
2121 code_editor->get_text_editor()->connect("gui_input", callable_mp(this, &ScriptTextEditor::_text_edit_gui_input));
2122 code_editor->show_toggle_scripts_button();
2123 _update_gutter_indexes();
2124
2125 editor_box->add_child(warnings_panel);
2126 warnings_panel->add_theme_font_override(
2127 "normal_font", EditorNode::get_singleton()->get_editor_theme()->get_font(SNAME("main"), EditorStringName(EditorFonts)));
2128 warnings_panel->add_theme_font_size_override(
2129 "normal_font_size", EditorNode::get_singleton()->get_editor_theme()->get_font_size(SNAME("main_size"), EditorStringName(EditorFonts)));
2130 warnings_panel->connect("meta_clicked", callable_mp(this, &ScriptTextEditor::_warning_clicked));
2131
2132 editor_box->add_child(errors_panel);
2133 errors_panel->add_theme_font_override(
2134 "normal_font", EditorNode::get_singleton()->get_editor_theme()->get_font(SNAME("main"), EditorStringName(EditorFonts)));
2135 errors_panel->add_theme_font_size_override(
2136 "normal_font_size", EditorNode::get_singleton()->get_editor_theme()->get_font_size(SNAME("main_size"), EditorStringName(EditorFonts)));
2137 errors_panel->connect("meta_clicked", callable_mp(this, &ScriptTextEditor::_error_clicked));
2138
2139 add_child(context_menu);
2140 context_menu->connect("id_pressed", callable_mp(this, &ScriptTextEditor::_edit_option));
2141
2142 add_child(color_panel);
2143
2144 color_picker = memnew(ColorPicker);
2145 color_picker->set_deferred_mode(true);
2146 color_picker->connect("color_changed", callable_mp(this, &ScriptTextEditor::_color_changed));
2147 color_panel->connect("about_to_popup", callable_mp(EditorNode::get_singleton(), &EditorNode::setup_color_picker).bind(color_picker));
2148
2149 color_panel->add_child(color_picker);
2150
2151 quick_open = memnew(ScriptEditorQuickOpen);
2152 quick_open->connect("goto_line", callable_mp(this, &ScriptTextEditor::_goto_line));
2153 add_child(quick_open);
2154
2155 goto_line_dialog = memnew(GotoLineDialog);
2156 add_child(goto_line_dialog);
2157
2158 add_child(connection_info_dialog);
2159
2160 edit_hb->add_child(edit_menu);
2161 edit_menu->connect("about_to_popup", callable_mp(this, &ScriptTextEditor::_prepare_edit_menu));
2162 edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("ui_undo"), EDIT_UNDO);
2163 edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("ui_redo"), EDIT_REDO);
2164 edit_menu->get_popup()->add_separator();
2165 edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("ui_cut"), EDIT_CUT);
2166 edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("ui_copy"), EDIT_COPY);
2167 edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("ui_paste"), EDIT_PASTE);
2168 edit_menu->get_popup()->add_separator();
2169 edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("ui_text_select_all"), EDIT_SELECT_ALL);
2170 edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/duplicate_selection"), EDIT_DUPLICATE_SELECTION);
2171 edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/evaluate_selection"), EDIT_EVALUATE);
2172 edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_word_wrap"), EDIT_TOGGLE_WORD_WRAP);
2173 edit_menu->get_popup()->add_separator();
2174 {
2175 PopupMenu *sub_menu = memnew(PopupMenu);
2176 sub_menu->set_name("line_menu");
2177 sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/move_up"), EDIT_MOVE_LINE_UP);
2178 sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/move_down"), EDIT_MOVE_LINE_DOWN);
2179 sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/indent"), EDIT_INDENT);
2180 sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/unindent"), EDIT_UNINDENT);
2181 sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/delete_line"), EDIT_DELETE_LINE);
2182 sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_comment"), EDIT_TOGGLE_COMMENT);
2183 sub_menu->connect("id_pressed", callable_mp(this, &ScriptTextEditor::_edit_option));
2184 edit_menu->get_popup()->add_child(sub_menu);
2185 edit_menu->get_popup()->add_submenu_item(TTR("Line"), "line_menu");
2186 }
2187 {
2188 PopupMenu *sub_menu = memnew(PopupMenu);
2189 sub_menu->set_name("folding_menu");
2190 sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_fold_line"), EDIT_TOGGLE_FOLD_LINE);
2191 sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/fold_all_lines"), EDIT_FOLD_ALL_LINES);
2192 sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/unfold_all_lines"), EDIT_UNFOLD_ALL_LINES);
2193 sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/create_code_region"), EDIT_CREATE_CODE_REGION);
2194 sub_menu->connect("id_pressed", callable_mp(this, &ScriptTextEditor::_edit_option));
2195 edit_menu->get_popup()->add_child(sub_menu);
2196 edit_menu->get_popup()->add_submenu_item(TTR("Folding"), "folding_menu");
2197 }
2198 edit_menu->get_popup()->add_separator();
2199 edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("ui_text_completion_query"), EDIT_COMPLETE);
2200 edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/trim_trailing_whitespace"), EDIT_TRIM_TRAILING_WHITESAPCE);
2201 {
2202 PopupMenu *sub_menu = memnew(PopupMenu);
2203 sub_menu->set_name("indent_menu");
2204 sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/convert_indent_to_spaces"), EDIT_CONVERT_INDENT_TO_SPACES);
2205 sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/convert_indent_to_tabs"), EDIT_CONVERT_INDENT_TO_TABS);
2206 sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/auto_indent"), EDIT_AUTO_INDENT);
2207 sub_menu->connect("id_pressed", callable_mp(this, &ScriptTextEditor::_edit_option));
2208 edit_menu->get_popup()->add_child(sub_menu);
2209 edit_menu->get_popup()->add_submenu_item(TTR("Indentation"), "indent_menu");
2210 }
2211 edit_menu->get_popup()->connect("id_pressed", callable_mp(this, &ScriptTextEditor::_edit_option));
2212 edit_menu->get_popup()->add_separator();
2213 {
2214 PopupMenu *sub_menu = memnew(PopupMenu);
2215 sub_menu->set_name("convert_case");
2216 sub_menu->add_shortcut(ED_SHORTCUT("script_text_editor/convert_to_uppercase", TTR("Uppercase"), KeyModifierMask::SHIFT | Key::F4), EDIT_TO_UPPERCASE);
2217 sub_menu->add_shortcut(ED_SHORTCUT("script_text_editor/convert_to_lowercase", TTR("Lowercase"), KeyModifierMask::SHIFT | Key::F5), EDIT_TO_LOWERCASE);
2218 sub_menu->add_shortcut(ED_SHORTCUT("script_text_editor/capitalize", TTR("Capitalize"), KeyModifierMask::SHIFT | Key::F6), EDIT_CAPITALIZE);
2219 sub_menu->connect("id_pressed", callable_mp(this, &ScriptTextEditor::_edit_option));
2220 edit_menu->get_popup()->add_child(sub_menu);
2221 edit_menu->get_popup()->add_submenu_item(TTR("Convert Case"), "convert_case");
2222 }
2223 edit_menu->get_popup()->add_child(highlighter_menu);
2224 edit_menu->get_popup()->add_submenu_item(TTR("Syntax Highlighter"), "highlighter_menu");
2225 highlighter_menu->connect("id_pressed", callable_mp(this, &ScriptTextEditor::_change_syntax_highlighter));
2226
2227 edit_hb->add_child(search_menu);
2228 search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/find"), SEARCH_FIND);
2229 search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/find_next"), SEARCH_FIND_NEXT);
2230 search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/find_previous"), SEARCH_FIND_PREV);
2231 search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/replace"), SEARCH_REPLACE);
2232 search_menu->get_popup()->add_separator();
2233 search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/find_in_files"), SEARCH_IN_FILES);
2234 search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/replace_in_files"), REPLACE_IN_FILES);
2235 search_menu->get_popup()->add_separator();
2236 search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/contextual_help"), HELP_CONTEXTUAL);
2237 search_menu->get_popup()->connect("id_pressed", callable_mp(this, &ScriptTextEditor::_edit_option));
2238
2239 _load_theme_settings();
2240
2241 edit_hb->add_child(goto_menu);
2242 goto_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_function"), SEARCH_LOCATE_FUNCTION);
2243 goto_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_line"), SEARCH_GOTO_LINE);
2244 goto_menu->get_popup()->add_separator();
2245
2246 goto_menu->get_popup()->add_child(bookmarks_menu);
2247 goto_menu->get_popup()->add_submenu_item(TTR("Bookmarks"), "Bookmarks");
2248 _update_bookmark_list();
2249 bookmarks_menu->connect("about_to_popup", callable_mp(this, &ScriptTextEditor::_update_bookmark_list));
2250 bookmarks_menu->connect("index_pressed", callable_mp(this, &ScriptTextEditor::_bookmark_item_pressed));
2251
2252 goto_menu->get_popup()->add_child(breakpoints_menu);
2253 goto_menu->get_popup()->add_submenu_item(TTR("Breakpoints"), "Breakpoints");
2254 _update_breakpoint_list();
2255 breakpoints_menu->connect("about_to_popup", callable_mp(this, &ScriptTextEditor::_update_breakpoint_list));
2256 breakpoints_menu->connect("index_pressed", callable_mp(this, &ScriptTextEditor::_breakpoint_item_pressed));
2257
2258 goto_menu->get_popup()->connect("id_pressed", callable_mp(this, &ScriptTextEditor::_edit_option));
2259}
2260
2261ScriptTextEditor::ScriptTextEditor() {
2262 code_editor = memnew(CodeTextEditor);
2263 code_editor->add_theme_constant_override("separation", 2);
2264 code_editor->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
2265 code_editor->set_code_complete_func(_code_complete_scripts, this);
2266 code_editor->set_v_size_flags(SIZE_EXPAND_FILL);
2267
2268 code_editor->get_text_editor()->set_draw_breakpoints_gutter(true);
2269 code_editor->get_text_editor()->set_draw_executing_lines_gutter(true);
2270 code_editor->get_text_editor()->connect("breakpoint_toggled", callable_mp(this, &ScriptTextEditor::_breakpoint_toggled));
2271
2272 connection_gutter = 1;
2273 code_editor->get_text_editor()->add_gutter(connection_gutter);
2274 code_editor->get_text_editor()->set_gutter_name(connection_gutter, "connection_gutter");
2275 code_editor->get_text_editor()->set_gutter_draw(connection_gutter, false);
2276 code_editor->get_text_editor()->set_gutter_overwritable(connection_gutter, true);
2277 code_editor->get_text_editor()->set_gutter_type(connection_gutter, TextEdit::GUTTER_TYPE_ICON);
2278
2279 warnings_panel = memnew(RichTextLabel);
2280 warnings_panel->set_custom_minimum_size(Size2(0, 100 * EDSCALE));
2281 warnings_panel->set_h_size_flags(SIZE_EXPAND_FILL);
2282 warnings_panel->set_meta_underline(true);
2283 warnings_panel->set_selection_enabled(true);
2284 warnings_panel->set_context_menu_enabled(true);
2285 warnings_panel->set_focus_mode(FOCUS_CLICK);
2286 warnings_panel->hide();
2287
2288 errors_panel = memnew(RichTextLabel);
2289 errors_panel->set_custom_minimum_size(Size2(0, 100 * EDSCALE));
2290 errors_panel->set_h_size_flags(SIZE_EXPAND_FILL);
2291 errors_panel->set_meta_underline(true);
2292 errors_panel->set_selection_enabled(true);
2293 errors_panel->set_context_menu_enabled(true);
2294 errors_panel->set_focus_mode(FOCUS_CLICK);
2295 errors_panel->hide();
2296
2297 update_settings();
2298
2299 code_editor->get_text_editor()->set_code_hint_draw_below(EDITOR_GET("text_editor/completion/put_callhint_tooltip_below_current_line"));
2300
2301 code_editor->get_text_editor()->set_symbol_lookup_on_click_enabled(true);
2302 code_editor->get_text_editor()->set_context_menu_enabled(false);
2303
2304 context_menu = memnew(PopupMenu);
2305
2306 color_panel = memnew(PopupPanel);
2307
2308 edit_hb = memnew(HBoxContainer);
2309
2310 edit_menu = memnew(MenuButton);
2311 edit_menu->set_text(TTR("Edit"));
2312 edit_menu->set_switch_on_hover(true);
2313 edit_menu->set_shortcut_context(this);
2314
2315 highlighter_menu = memnew(PopupMenu);
2316 highlighter_menu->set_name("highlighter_menu");
2317
2318 Ref<EditorPlainTextSyntaxHighlighter> plain_highlighter;
2319 plain_highlighter.instantiate();
2320 add_syntax_highlighter(plain_highlighter);
2321
2322 Ref<EditorStandardSyntaxHighlighter> highlighter;
2323 highlighter.instantiate();
2324 add_syntax_highlighter(highlighter);
2325 set_syntax_highlighter(highlighter);
2326
2327 search_menu = memnew(MenuButton);
2328 search_menu->set_text(TTR("Search"));
2329 search_menu->set_switch_on_hover(true);
2330 search_menu->set_shortcut_context(this);
2331
2332 goto_menu = memnew(MenuButton);
2333 goto_menu->set_text(TTR("Go To"));
2334 goto_menu->set_switch_on_hover(true);
2335 goto_menu->set_shortcut_context(this);
2336
2337 bookmarks_menu = memnew(PopupMenu);
2338 bookmarks_menu->set_name("Bookmarks");
2339
2340 breakpoints_menu = memnew(PopupMenu);
2341 breakpoints_menu->set_name("Breakpoints");
2342
2343 connection_info_dialog = memnew(ConnectionInfoDialog);
2344
2345 SET_DRAG_FORWARDING_GCD(code_editor->get_text_editor(), ScriptTextEditor);
2346}
2347
2348ScriptTextEditor::~ScriptTextEditor() {
2349 highlighters.clear();
2350
2351 if (!editor_enabled) {
2352 memdelete(code_editor);
2353 memdelete(warnings_panel);
2354 memdelete(errors_panel);
2355 memdelete(context_menu);
2356 memdelete(color_panel);
2357 memdelete(edit_hb);
2358 memdelete(edit_menu);
2359 memdelete(highlighter_menu);
2360 memdelete(search_menu);
2361 memdelete(goto_menu);
2362 memdelete(bookmarks_menu);
2363 memdelete(breakpoints_menu);
2364 memdelete(connection_info_dialog);
2365 }
2366}
2367
2368static ScriptEditorBase *create_editor(const Ref<Resource> &p_resource) {
2369 if (Object::cast_to<Script>(*p_resource)) {
2370 return memnew(ScriptTextEditor);
2371 }
2372 return nullptr;
2373}
2374
2375void ScriptTextEditor::register_editor() {
2376 ED_SHORTCUT("script_text_editor/move_up", TTR("Move Up"), KeyModifierMask::ALT | Key::UP);
2377 ED_SHORTCUT("script_text_editor/move_down", TTR("Move Down"), KeyModifierMask::ALT | Key::DOWN);
2378 ED_SHORTCUT("script_text_editor/delete_line", TTR("Delete Line"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::K);
2379
2380 // Leave these at zero, same can be accomplished with tab/shift-tab, including selection.
2381 // The next/previous in history shortcut in this case makes a lot more sense.
2382
2383 ED_SHORTCUT("script_text_editor/indent", TTR("Indent"), Key::NONE);
2384 ED_SHORTCUT("script_text_editor/unindent", TTR("Unindent"), KeyModifierMask::SHIFT | Key::TAB);
2385 ED_SHORTCUT_ARRAY("script_text_editor/toggle_comment", TTR("Toggle Comment"), { int32_t(KeyModifierMask::CMD_OR_CTRL | Key::K), int32_t(KeyModifierMask::CMD_OR_CTRL | Key::SLASH) });
2386 ED_SHORTCUT("script_text_editor/toggle_fold_line", TTR("Fold/Unfold Line"), KeyModifierMask::ALT | Key::F);
2387 ED_SHORTCUT_OVERRIDE("script_text_editor/toggle_fold_line", "macos", KeyModifierMask::CTRL | KeyModifierMask::META | Key::F);
2388 ED_SHORTCUT("script_text_editor/fold_all_lines", TTR("Fold All Lines"), Key::NONE);
2389 ED_SHORTCUT("script_text_editor/create_code_region", TTR("Create Code Region"), KeyModifierMask::ALT | Key::R);
2390 ED_SHORTCUT("script_text_editor/unfold_all_lines", TTR("Unfold All Lines"), Key::NONE);
2391 ED_SHORTCUT("script_text_editor/duplicate_selection", TTR("Duplicate Selection"), KeyModifierMask::SHIFT | KeyModifierMask::CTRL | Key::D);
2392 ED_SHORTCUT_OVERRIDE("script_text_editor/duplicate_selection", "macos", KeyModifierMask::SHIFT | KeyModifierMask::META | Key::C);
2393 ED_SHORTCUT("script_text_editor/evaluate_selection", TTR("Evaluate Selection"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::E);
2394 ED_SHORTCUT("script_text_editor/toggle_word_wrap", TTR("Toggle Word Wrap"), KeyModifierMask::ALT | Key::Z);
2395 ED_SHORTCUT("script_text_editor/trim_trailing_whitespace", TTR("Trim Trailing Whitespace"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | Key::T);
2396 ED_SHORTCUT("script_text_editor/convert_indent_to_spaces", TTR("Convert Indent to Spaces"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::Y);
2397 ED_SHORTCUT("script_text_editor/convert_indent_to_tabs", TTR("Convert Indent to Tabs"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::I);
2398 ED_SHORTCUT("script_text_editor/auto_indent", TTR("Auto Indent"), KeyModifierMask::CMD_OR_CTRL | Key::I);
2399
2400 ED_SHORTCUT_AND_COMMAND("script_text_editor/find", TTR("Find..."), KeyModifierMask::CMD_OR_CTRL | Key::F);
2401
2402 ED_SHORTCUT("script_text_editor/find_next", TTR("Find Next"), Key::F3);
2403 ED_SHORTCUT_OVERRIDE("script_text_editor/find_next", "macos", KeyModifierMask::META | Key::G);
2404
2405 ED_SHORTCUT("script_text_editor/find_previous", TTR("Find Previous"), KeyModifierMask::SHIFT | Key::F3);
2406 ED_SHORTCUT_OVERRIDE("script_text_editor/find_previous", "macos", KeyModifierMask::META | KeyModifierMask::SHIFT | Key::G);
2407
2408 ED_SHORTCUT_AND_COMMAND("script_text_editor/replace", TTR("Replace..."), KeyModifierMask::CTRL | Key::R);
2409 ED_SHORTCUT_OVERRIDE("script_text_editor/replace", "macos", KeyModifierMask::ALT | KeyModifierMask::META | Key::F);
2410
2411 ED_SHORTCUT("script_text_editor/find_in_files", TTR("Find in Files..."), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::F);
2412 ED_SHORTCUT("script_text_editor/replace_in_files", TTR("Replace in Files..."), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::R);
2413
2414 ED_SHORTCUT("script_text_editor/contextual_help", TTR("Contextual Help"), KeyModifierMask::ALT | Key::F1);
2415 ED_SHORTCUT_OVERRIDE("script_text_editor/contextual_help", "macos", KeyModifierMask::ALT | KeyModifierMask::SHIFT | Key::SPACE);
2416
2417 ED_SHORTCUT("script_text_editor/toggle_bookmark", TTR("Toggle Bookmark"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | Key::B);
2418 ED_SHORTCUT("script_text_editor/goto_next_bookmark", TTR("Go to Next Bookmark"), KeyModifierMask::CMD_OR_CTRL | Key::B);
2419 ED_SHORTCUT("script_text_editor/goto_previous_bookmark", TTR("Go to Previous Bookmark"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::B);
2420 ED_SHORTCUT("script_text_editor/remove_all_bookmarks", TTR("Remove All Bookmarks"), Key::NONE);
2421
2422 ED_SHORTCUT("script_text_editor/goto_function", TTR("Go to Function..."), KeyModifierMask::ALT | KeyModifierMask::CTRL | Key::F);
2423 ED_SHORTCUT_OVERRIDE("script_text_editor/goto_function", "macos", KeyModifierMask::CTRL | KeyModifierMask::META | Key::J);
2424
2425 ED_SHORTCUT("script_text_editor/goto_line", TTR("Go to Line..."), KeyModifierMask::CMD_OR_CTRL | Key::L);
2426
2427 ED_SHORTCUT("script_text_editor/toggle_breakpoint", TTR("Toggle Breakpoint"), Key::F9);
2428 ED_SHORTCUT_OVERRIDE("script_text_editor/toggle_breakpoint", "macos", KeyModifierMask::META | KeyModifierMask::SHIFT | Key::B);
2429
2430 ED_SHORTCUT("script_text_editor/remove_all_breakpoints", TTR("Remove All Breakpoints"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::F9);
2431 ED_SHORTCUT("script_text_editor/goto_next_breakpoint", TTR("Go to Next Breakpoint"), KeyModifierMask::CMD_OR_CTRL | Key::PERIOD);
2432 ED_SHORTCUT("script_text_editor/goto_previous_breakpoint", TTR("Go to Previous Breakpoint"), KeyModifierMask::CMD_OR_CTRL | Key::COMMA);
2433
2434 ScriptEditor::register_create_script_editor_function(create_editor);
2435}
2436
2437void ScriptTextEditor::validate() {
2438 this->code_editor->validate_script();
2439}
2440