Path: blob/master/editor/script/script_text_editor.cpp
20831 views
/**************************************************************************/1/* script_text_editor.cpp */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930#include "script_text_editor.h"3132#include "core/config/project_settings.h"33#include "core/input/input.h"34#include "core/io/dir_access.h"35#include "core/io/json.h"36#include "core/math/expression.h"37#include "core/os/keyboard.h"38#include "editor/debugger/editor_debugger_node.h"39#include "editor/doc/editor_help.h"40#include "editor/docks/filesystem_dock.h"41#include "editor/editor_interface.h"42#include "editor/editor_node.h"43#include "editor/editor_string_names.h"44#include "editor/gui/editor_toaster.h"45#include "editor/inspector/editor_context_menu_plugin.h"46#include "editor/inspector/editor_inspector.h"47#include "editor/inspector/multi_node_edit.h"48#include "editor/script/syntax_highlighters.h"49#include "editor/settings/editor_command_palette.h"50#include "editor/settings/editor_settings.h"51#include "editor/themes/editor_scale.h"52#include "scene/gui/grid_container.h"53#include "scene/gui/menu_button.h"54#include "scene/gui/rich_text_label.h"55#include "scene/gui/split_container.h"5657void ConnectionInfoDialog::ok_pressed() {58}5960void ConnectionInfoDialog::popup_connections(const String &p_method, const Vector<Node *> &p_nodes) {61method->set_text(p_method);6263tree->clear();64TreeItem *root = tree->create_item();6566for (int i = 0; i < p_nodes.size(); i++) {67List<Connection> all_connections;68p_nodes[i]->get_signals_connected_to_this(&all_connections);6970for (const Connection &connection : all_connections) {71if (connection.callable.get_method() != p_method) {72continue;73}7475TreeItem *node_item = tree->create_item(root);7677node_item->set_text(0, Object::cast_to<Node>(connection.signal.get_object())->get_name());78node_item->set_icon(0, EditorNode::get_singleton()->get_object_icon(connection.signal.get_object()));79node_item->set_selectable(0, false);80node_item->set_editable(0, false);8182node_item->set_text(1, connection.signal.get_name());83Control *p = Object::cast_to<Control>(get_parent());84node_item->set_icon(1, p->get_editor_theme_icon(SNAME("Slot")));85node_item->set_selectable(1, false);86node_item->set_editable(1, false);8788node_item->set_text(2, Object::cast_to<Node>(connection.callable.get_object())->get_name());89node_item->set_icon(2, EditorNode::get_singleton()->get_object_icon(connection.callable.get_object()));90node_item->set_selectable(2, false);91node_item->set_editable(2, false);92}93}9495popup_centered(Size2(600, 300) * EDSCALE);96}9798ConnectionInfoDialog::ConnectionInfoDialog() {99set_title(TTRC("Connections to method:"));100101VBoxContainer *vbc = memnew(VBoxContainer);102vbc->set_anchor_and_offset(SIDE_LEFT, Control::ANCHOR_BEGIN, 8 * EDSCALE);103vbc->set_anchor_and_offset(SIDE_TOP, Control::ANCHOR_BEGIN, 8 * EDSCALE);104vbc->set_anchor_and_offset(SIDE_RIGHT, Control::ANCHOR_END, -8 * EDSCALE);105vbc->set_anchor_and_offset(SIDE_BOTTOM, Control::ANCHOR_END, -8 * EDSCALE);106add_child(vbc);107108method = memnew(Label);109method->set_focus_mode(Control::FOCUS_ACCESSIBILITY);110method->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);111method->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);112vbc->add_child(method);113114tree = memnew(Tree);115tree->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);116tree->set_theme_type_variation("TreeTable");117tree->set_hide_folding(true);118tree->set_columns(3);119tree->set_hide_root(true);120tree->set_column_titles_visible(true);121tree->set_column_title(0, TTRC("Source"));122tree->set_column_title(1, TTRC("Signal"));123tree->set_column_title(2, TTRC("Target"));124vbc->add_child(tree);125tree->set_v_size_flags(Control::SIZE_EXPAND_FILL);126tree->set_allow_rmb_select(true);127}128129////////////////////////////////////////////////////////////////////////////////130131void ScriptTextEditor::EditMenusSTE::_update_breakpoint_list() {132TextEditorBase *script_text_editor = _get_active_editor();133ERR_FAIL_NULL(script_text_editor);134breakpoints_menu->clear();135breakpoints_menu->reset_size();136137breakpoints_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_breakpoint"), DEBUG_TOGGLE_BREAKPOINT);138breakpoints_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/remove_all_breakpoints"), DEBUG_REMOVE_ALL_BREAKPOINTS);139breakpoints_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_next_breakpoint"), DEBUG_GOTO_NEXT_BREAKPOINT);140breakpoints_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_previous_breakpoint"), DEBUG_GOTO_PREV_BREAKPOINT);141142PackedInt32Array breakpoint_list = script_text_editor->get_code_editor()->get_text_editor()->get_breakpointed_lines();143if (breakpoint_list.is_empty()) {144return;145}146147breakpoints_menu->add_separator();148149for (int i = 0; i < breakpoint_list.size(); i++) {150// Strip edges to remove spaces or tabs.151// Also replace any tabs by spaces, since we can't print tabs in the menu.152String line = script_text_editor->get_code_editor()->get_text_editor()->get_line(breakpoint_list[i]).replace("\t", " ").strip_edges();153154// Limit the size of the line if too big.155if (line.length() > 50) {156line = line.substr(0, 50);157}158159breakpoints_menu->add_item(String::num_int64(breakpoint_list[i] + 1) + " - `" + line + "`");160breakpoints_menu->set_item_metadata(-1, breakpoint_list[i]);161}162}163164void ScriptTextEditor::EditMenusSTE::_breakpoint_item_pressed(int p_idx) {165TextEditorBase *script_text_editor = _get_active_editor();166ERR_FAIL_NULL(script_text_editor);167if (p_idx < 4) { // Any item before the separator.168_edit_option(breakpoints_menu->get_item_id(p_idx));169} else {170script_text_editor->get_code_editor()->goto_line_centered(breakpoints_menu->get_item_metadata(p_idx));171}172}173174ScriptTextEditor::EditMenusSTE::EditMenusSTE() {175edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/evaluate_selection"), EDIT_EVALUATE);176_popup_move_item(EDIT_DUPLICATE_LINES, edit_menu->get_popup());177goto_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_function"), SEARCH_LOCATE_FUNCTION);178_popup_move_item(SEARCH_GOTO_LINE, goto_menu->get_popup(), false);179goto_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_symbol"), LOOKUP_SYMBOL);180_popup_move_item(SEARCH_GOTO_LINE, goto_menu->get_popup());181182edit_menu_fold->add_shortcut(ED_GET_SHORTCUT("script_text_editor/create_code_region"), EDIT_CREATE_CODE_REGION);183edit_menu_convert_indent->add_shortcut(ED_GET_SHORTCUT("script_text_editor/auto_indent"), EDIT_AUTO_INDENT);184185search_menu->get_popup()->add_separator();186search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/contextual_help"), HELP_CONTEXTUAL);187188breakpoints_menu = memnew(PopupMenu);189goto_menu->get_popup()->add_submenu_node_item(TTRC("Breakpoints"), breakpoints_menu);190breakpoints_menu->connect("about_to_popup", callable_mp(this, &EditMenusSTE::_update_breakpoint_list));191breakpoints_menu->connect("index_pressed", callable_mp(this, &EditMenusSTE::_breakpoint_item_pressed));192}193194////////////////////////////////////////////////////////////////////////////////195196Vector<String> ScriptTextEditor::get_functions() {197CodeEdit *te = code_editor->get_text_editor();198String text = te->get_text();199List<String> fnc;200201Ref<Script> script = edited_res;202if (script->is_valid() && script->get_language()->validate(text, script->get_path(), &fnc)) {203//if valid rewrite functions to latest204functions.clear();205for (const String &E : fnc) {206functions.push_back(E);207}208}209210return functions;211}212213void ScriptTextEditor::apply_code() {214Ref<Script> script = edited_res;215if (script->is_valid()) {216script->set_source_code(code_editor->get_text_editor()->get_text());217script->update_exports();218if (!pending_dragged_exports.is_empty()) {219_assign_dragged_export_variables();220}221}222223code_editor->get_text_editor()->get_syntax_highlighter()->update_cache();224}225226void ScriptTextEditor::set_edited_resource(const Ref<Resource> &p_res) {227ERR_FAIL_COND(p_res.is_null());228ERR_FAIL_COND(edited_res.is_valid());229230edited_res = p_res;231232Ref<Script> script = edited_res;233ERR_FAIL_COND(script.is_null());234code_editor->get_text_editor()->set_text(script->get_source_code());235236code_editor->get_text_editor()->clear_undo_history();237code_editor->get_text_editor()->tag_saved_version();238239emit_signal(SNAME("name_changed"));240code_editor->update_line_and_column();241}242243void ScriptTextEditor::enable_editor() {244if (editor_enabled) {245return;246}247248_enable_code_editor();249250if (pending_state != Variant()) {251code_editor->set_edit_state(pending_state);252pending_state = Variant();253}254255TextEditorBase::enable_editor();256}257258void ScriptTextEditor::_load_theme_settings() {259if (!editor_enabled) {260return;261}262CodeEdit *text_edit = code_editor->get_text_editor();263264Color updated_warning_line_color = EDITOR_GET("text_editor/theme/highlighting/warning_color");265Color updated_marked_line_color = EDITOR_GET("text_editor/theme/highlighting/mark_color");266Color updated_safe_line_number_color = EDITOR_GET("text_editor/theme/highlighting/safe_line_number_color");267Color updated_folded_code_region_color = EDITOR_GET("text_editor/theme/highlighting/folded_code_region_color");268269bool warning_line_color_updated = updated_warning_line_color != warning_line_color;270bool marked_line_color_updated = updated_marked_line_color != marked_line_color;271bool safe_line_number_color_updated = updated_safe_line_number_color != safe_line_number_color;272bool folded_code_region_color_updated = updated_folded_code_region_color != folded_code_region_color;273if (safe_line_number_color_updated || warning_line_color_updated || marked_line_color_updated || folded_code_region_color_updated) {274safe_line_number_color = updated_safe_line_number_color;275for (int i = 0; i < text_edit->get_line_count(); i++) {276if (warning_line_color_updated && text_edit->get_line_background_color(i) == warning_line_color) {277text_edit->set_line_background_color(i, updated_warning_line_color);278}279280if (marked_line_color_updated && text_edit->get_line_background_color(i) == marked_line_color) {281text_edit->set_line_background_color(i, updated_marked_line_color);282}283284if (safe_line_number_color_updated && text_edit->get_line_gutter_item_color(i, line_number_gutter) != default_line_number_color) {285text_edit->set_line_gutter_item_color(i, line_number_gutter, safe_line_number_color);286}287288if (folded_code_region_color_updated && text_edit->get_line_background_color(i) == folded_code_region_color) {289text_edit->set_line_background_color(i, updated_folded_code_region_color);290}291}292warning_line_color = updated_warning_line_color;293marked_line_color = updated_marked_line_color;294folded_code_region_color = updated_folded_code_region_color;295}296297theme_loaded = true;298Ref<Script> script = edited_res;299if (script.is_valid()) {300_set_theme_for_script();301}302}303304void ScriptTextEditor::_set_theme_for_script() {305if (!theme_loaded) {306return;307}308309CodeEdit *text_edit = code_editor->get_text_editor();310text_edit->get_syntax_highlighter()->update_cache();311312Ref<Script> script = edited_res;313Vector<String> strings = script->get_language()->get_string_delimiters();314text_edit->clear_string_delimiters();315for (const String &string : strings) {316String beg = string.get_slicec(' ', 0);317String end = string.get_slice_count(" ") > 1 ? string.get_slicec(' ', 1) : String();318if (!text_edit->has_string_delimiter(beg)) {319text_edit->add_string_delimiter(beg, end, end.is_empty());320}321322if (!end.is_empty() && !text_edit->has_auto_brace_completion_open_key(beg)) {323text_edit->add_auto_brace_completion_pair(beg, end);324}325}326327text_edit->clear_comment_delimiters();328329for (const String &comment : script->get_language()->get_comment_delimiters()) {330String beg = comment.get_slicec(' ', 0);331String end = comment.get_slice_count(" ") > 1 ? comment.get_slicec(' ', 1) : String();332text_edit->add_comment_delimiter(beg, end, end.is_empty());333334if (!end.is_empty() && !text_edit->has_auto_brace_completion_open_key(beg)) {335text_edit->add_auto_brace_completion_pair(beg, end);336}337}338339for (const String &doc_comment : script->get_language()->get_doc_comment_delimiters()) {340String beg = doc_comment.get_slicec(' ', 0);341String end = doc_comment.get_slice_count(" ") > 1 ? doc_comment.get_slicec(' ', 1) : String();342text_edit->add_comment_delimiter(beg, end, end.is_empty());343344if (!end.is_empty() && !text_edit->has_auto_brace_completion_open_key(beg)) {345text_edit->add_auto_brace_completion_pair(beg, end);346}347}348}349350void ScriptTextEditor::_show_errors_panel(bool p_show) {351errors_panel->set_visible(p_show);352}353354void ScriptTextEditor::_show_warnings_panel(bool p_show) {355warnings_panel->set_visible(p_show);356}357358bool ScriptTextEditor::_warning_clicked(const Variant &p_line) {359if (CodeEditorBase::_warning_clicked(p_line)) {360return true;361} else if (p_line.get_type() == Variant::DICTIONARY) {362Dictionary meta = p_line.operator Dictionary();363const int line = meta["line"].operator int64_t() - 1;364const String code = meta["code"].operator String();365const String quote_style = EDITOR_GET("text_editor/completion/use_single_quotes") ? "'" : "\"";366367CodeEdit *text_editor = code_editor->get_text_editor();368String prev_line = line > 0 ? text_editor->get_line(line - 1) : "";369if (prev_line.contains("@warning_ignore")) {370const int closing_bracket_idx = prev_line.find_char(')');371const String text_to_insert = ", " + code.quote(quote_style);372text_editor->insert_text(text_to_insert, line - 1, closing_bracket_idx);373} else {374const int indent = text_editor->get_indent_level(line) / text_editor->get_indent_size();375String annotation_indent;376if (!text_editor->is_indent_using_spaces()) {377annotation_indent = String("\t").repeat(indent);378} else {379annotation_indent = String(" ").repeat(text_editor->get_indent_size() * indent);380}381text_editor->insert_line_at(line, annotation_indent + "@warning_ignore(" + code.quote(quote_style) + ")");382}383384_validate_script();385return true;386}387return false;388}389390void ScriptTextEditor::_error_clicked(const Variant &p_line) {391if (p_line.get_type() == Variant::INT) {392goto_line_centered(p_line.operator int64_t());393} else if (p_line.get_type() == Variant::DICTIONARY) {394Dictionary meta = p_line.operator Dictionary();395const String path = meta["path"].operator String();396const int line = meta["line"].operator int64_t();397const int column = meta["column"].operator int64_t();398if (path.is_empty()) {399goto_line_centered(line, column);400} else {401Ref<Resource> scr = ResourceLoader::load(path);402if (scr.is_null()) {403EditorNode::get_singleton()->show_warning(TTR("Could not load file at:") + "\n\n" + path, TTR("Error!"));404} else {405int corrected_column = column;406407const String line_text = code_editor->get_text_editor()->get_line(line);408const int indent_size = code_editor->get_text_editor()->get_indent_size();409if (indent_size > 1) {410const int tab_count = line_text.length() - line_text.lstrip("\t").length();411corrected_column -= tab_count * (indent_size - 1);412}413414ScriptEditor::get_singleton()->edit(scr, line, corrected_column);415}416}417}418}419420void ScriptTextEditor::add_callback(const String &p_function, const PackedStringArray &p_args) {421Ref<Script> script = edited_res;422ScriptLanguage *language = script->get_language();423if (!language->can_make_function()) {424return;425}426code_editor->get_text_editor()->begin_complex_operation();427code_editor->get_text_editor()->remove_secondary_carets();428code_editor->get_text_editor()->deselect();429String code = code_editor->get_text_editor()->get_text();430int pos = language->find_function(p_function, code);431if (pos == -1) {432// Function does not exist, create it at the end of the file.433int last_line = code_editor->get_text_editor()->get_line_count() - 1;434String func = language->make_function("", p_function, p_args);435code_editor->get_text_editor()->insert_text("\n\n" + func, last_line, code_editor->get_text_editor()->get_line(last_line).length());436pos = last_line + 3;437}438// Put caret on the line after the function, after the indent.439int indent_column = 1;440if (EDITOR_GET("text_editor/behavior/indent/type")) {441indent_column = EDITOR_GET("text_editor/behavior/indent/size");442}443code_editor->get_text_editor()->set_caret_line(pos, true, true, -1);444code_editor->get_text_editor()->set_caret_column(indent_column);445code_editor->get_text_editor()->end_complex_operation();446}447448bool ScriptTextEditor::_is_valid_color_info(const Dictionary &p_info) {449if (p_info.get_valid("color").get_type() != Variant::COLOR) {450return false;451}452if (!p_info.get_valid("color_end").is_num() || !p_info.get_valid("color_mode").is_num()) {453return false;454}455return true;456}457458Array ScriptTextEditor::_inline_object_parse(const String &p_text) {459Array result;460int i_end_previous = 0;461int i_start = p_text.find("Color");462463while (i_start != -1) {464// Ignore words that just have "Color" in them.465if (i_start != 0 && ('_' + p_text.substr(i_start - 1, 1)).is_valid_ascii_identifier()) {466i_end_previous = MAX(i_end_previous, i_start);467i_start = p_text.find("Color", i_start + 1);468continue;469}470471const int i_par_start = p_text.find_char('(', i_start + 5);472const int i_par_end = p_text.find_char(')', i_start + 5);473if (i_par_start == -1 || i_par_end == -1) {474i_end_previous = MAX(i_end_previous, i_start);475i_start = p_text.find("Color", i_start + 1);476continue;477}478479Dictionary color_info;480color_info["column"] = i_start;481color_info["width_ratio"] = 1.0;482color_info["color_end"] = i_par_end;483484const String fn_name = p_text.substr(i_start + 5, i_par_start - i_start - 5);485const String s_params = p_text.substr(i_par_start + 1, i_par_end - i_par_start - 1);486bool has_added_color = false;487488if (fn_name.is_empty()) {489String stripped = s_params.strip_edges(true, true);490if (stripped.length() > 1 && (stripped[0] == '"' || stripped[0] == '\'')) {491// String constructor.492const char32_t string_delimiter = stripped[0];493if (stripped[stripped.length() - 1] == string_delimiter) {494const String color_string = stripped.substr(1, stripped.length() - 2);495if (!color_string.contains_char(string_delimiter)) {496color_info["color"] = Color::from_string(color_string, Color());497color_info["color_mode"] = MODE_STRING;498has_added_color = true;499}500}501} else if (stripped.length() == 10 && stripped.begins_with("0x")) {502// Hex constructor.503const String color_string = stripped.substr(2);504if (color_string.is_valid_hex_number(false)) {505color_info["color"] = Color::from_string(color_string, Color());506color_info["color_mode"] = MODE_HEX;507has_added_color = true;508}509} else if (stripped.is_empty()) {510// Empty Color() constructor.511color_info["color"] = Color();512color_info["color_mode"] = MODE_RGB;513has_added_color = true;514}515}516// Float & int parameters.517if (!has_added_color && s_params.size() > 0) {518const PackedStringArray s_params_split = s_params.split(",", false, 4);519PackedFloat64Array params;520bool valid_floats = true;521for (const String &s_param : s_params_split) {522// Only allow float literals, expressions won't be evaluated and could get replaced.523if (!s_param.strip_edges().is_valid_float()) {524valid_floats = false;525break;526}527params.push_back(s_param.to_float());528}529if (valid_floats && params.size() == 3) {530if (fn_name == ".from_rgba8") {531params.push_back(255);532} else {533params.push_back(1.0);534}535}536if (valid_floats && params.size() == 4) {537has_added_color = true;538if (fn_name == ".from_ok_hsl") {539color_info["color"] = Color::from_ok_hsl(params[0], params[1], params[2], params[3]);540color_info["color_mode"] = MODE_OKHSL;541} else if (fn_name == ".from_hsv") {542color_info["color"] = Color::from_hsv(params[0], params[1], params[2], params[3]);543color_info["color_mode"] = MODE_HSV;544} else if (fn_name == ".from_rgba8") {545color_info["color"] = Color::from_rgba8(int(params[0]), int(params[1]), int(params[2]), int(params[3]));546color_info["color_mode"] = MODE_RGB8;547} else if (fn_name.is_empty()) {548color_info["color"] = Color(params[0], params[1], params[2], params[3]);549color_info["color_mode"] = MODE_RGB;550} else {551has_added_color = false;552}553}554}555556if (has_added_color) {557result.push_back(color_info);558i_end_previous = i_par_end + 1;559}560i_end_previous = MAX(i_end_previous, i_start);561i_start = p_text.find("Color", i_start + 1);562}563return result;564}565566void ScriptTextEditor::_inline_object_draw(const Dictionary &p_info, const Rect2 &p_rect) {567if (_is_valid_color_info(p_info)) {568Rect2 col_rect = p_rect.grow(-4);569if (color_alpha_texture.is_null()) {570color_alpha_texture = inline_color_picker->get_theme_icon("sample_bg", "ColorPicker");571}572RID text_ci = code_editor->get_text_editor()->get_text_canvas_item();573RS::get_singleton()->canvas_item_add_rect(text_ci, p_rect.grow(-3), Color(1, 1, 1));574color_alpha_texture->draw_rect(text_ci, col_rect);575RS::get_singleton()->canvas_item_add_rect(text_ci, col_rect, Color(p_info["color"]));576}577}578579void ScriptTextEditor::_inline_object_handle_click(const Dictionary &p_info, const Rect2 &p_rect) {580if (_is_valid_color_info(p_info)) {581inline_color_picker->set_pick_color(p_info["color"]);582inline_color_line = p_info["line"];583inline_color_start = p_info["column"];584inline_color_end = p_info["color_end"];585586// Reset tooltip hover timer.587code_editor->get_text_editor()->set_symbol_tooltip_on_hover_enabled(false);588code_editor->get_text_editor()->set_symbol_tooltip_on_hover_enabled(true);589590_update_color_constructor_options();591inline_color_options->select(p_info["color_mode"]);592EditorNode::get_singleton()->setup_color_picker(inline_color_picker);593594// Move popup above the line if it's too low.595float_t view_h = get_viewport_rect().size.y;596float_t pop_h = inline_color_popup->get_contents_minimum_size().y;597float_t pop_y = p_rect.get_end().y;598float_t pop_x = p_rect.position.x;599if (pop_y + pop_h > view_h) {600pop_y = p_rect.position.y - pop_h;601}602// Move popup to the right if it's too high.603if (pop_y < 0) {604pop_x = p_rect.get_end().x;605}606607inline_color_popup->popup(Rect2(pop_x, pop_y, 0, 0));608}609}610611String ScriptTextEditor::_picker_color_stringify(const Color &p_color, COLOR_MODE p_mode) {612String result;613String fname;614Vector<String> str_params;615switch (p_mode) {616case ScriptTextEditor::MODE_STRING: {617str_params.push_back("\"" + p_color.to_html() + "\"");618} break;619case ScriptTextEditor::MODE_HEX: {620str_params.push_back("0x" + p_color.to_html());621} break;622case ScriptTextEditor::MODE_RGB: {623str_params = {624String::num(p_color.r, 3),625String::num(p_color.g, 3),626String::num(p_color.b, 3),627String::num(p_color.a, 3)628};629} break;630case ScriptTextEditor::MODE_HSV: {631str_params = {632String::num(p_color.get_h(), 3),633String::num(p_color.get_s(), 3),634String::num(p_color.get_v(), 3),635String::num(p_color.a, 3)636};637fname = ".from_hsv";638} break;639case ScriptTextEditor::MODE_OKHSL: {640str_params = {641String::num(p_color.get_ok_hsl_h(), 3),642String::num(p_color.get_ok_hsl_s(), 3),643String::num(p_color.get_ok_hsl_l(), 3),644String::num(p_color.a, 3)645};646fname = ".from_ok_hsl";647} break;648case ScriptTextEditor::MODE_RGB8: {649str_params = {650itos(p_color.get_r8()),651itos(p_color.get_g8()),652itos(p_color.get_b8()),653itos(p_color.get_a8())654};655fname = ".from_rgba8";656} break;657default: {658} break;659}660result = "Color" + fname + "(" + String(", ").join(str_params) + ")";661return result;662}663664void ScriptTextEditor::_picker_color_changed(const Color &p_color) {665_update_color_constructor_options();666_update_color_text();667}668669void ScriptTextEditor::_update_color_constructor_options() {670int item_count = inline_color_options->get_item_count();671// Update or add each constructor as an option.672for (int i = 0; i < MODE_MAX; i++) {673String option_text = _picker_color_stringify(inline_color_picker->get_pick_color(), (COLOR_MODE)i);674if (i >= item_count) {675inline_color_options->add_item(option_text);676} else {677inline_color_options->set_item_text(i, option_text);678}679}680}681682void ScriptTextEditor::_update_background_color() {683// Clear background lines.684CodeEdit *te = code_editor->get_text_editor();685for (int i = 0; i < te->get_line_count(); i++) {686bool is_folded_code_region = te->is_line_code_region_start(i) && te->is_line_folded(i);687te->set_line_background_color(i, is_folded_code_region ? folded_code_region_color : Color(0, 0, 0, 0));688}689690// Set the warning background.691if (warning_line_color.a != 0.0) {692for (const ScriptLanguage::Warning &warning : warnings) {693int warning_start_line = CLAMP(warning.start_line - 1, 0, te->get_line_count() - 1);694int warning_end_line = CLAMP(warning.end_line - 1, 0, te->get_line_count() - 1);695int folded_line_header = te->get_folded_line_header(warning_start_line);696697// If the warning highlight is too long, only highlight the start line.698const int warning_max_lines = 20;699700te->set_line_background_color(folded_line_header, warning_line_color);701if (warning_end_line - warning_start_line < warning_max_lines) {702for (int i = warning_start_line + 1; i <= warning_end_line; i++) {703te->set_line_background_color(i, warning_line_color);704}705}706}707}708709// Set the error background.710if (marked_line_color.a != 0.0) {711for (const ScriptLanguage::ScriptError &error : errors) {712int error_line = CLAMP(error.line - 1, 0, te->get_line_count() - 1);713int folded_line_header = te->get_folded_line_header(error_line);714715te->set_line_background_color(folded_line_header, marked_line_color);716}717}718}719720void ScriptTextEditor::_update_color_text() {721if (inline_color_line < 0) {722return;723}724String result = inline_color_options->get_item_text(inline_color_options->get_selected_id());725code_editor->get_text_editor()->begin_complex_operation();726code_editor->get_text_editor()->remove_text(inline_color_line, inline_color_start, inline_color_line, inline_color_end + 1);727inline_color_end = inline_color_start + result.size() - 2;728code_editor->get_text_editor()->insert_text(result, inline_color_line, inline_color_start);729code_editor->get_text_editor()->end_complex_operation();730}731732void ScriptTextEditor::update_settings() {733code_editor->get_text_editor()->set_gutter_draw(connection_gutter, EDITOR_GET("text_editor/appearance/gutters/show_info_gutter"));734if (EDITOR_GET("text_editor/appearance/enable_inline_color_picker")) {735code_editor->get_text_editor()->set_inline_object_handlers(736callable_mp(this, &ScriptTextEditor::_inline_object_parse),737callable_mp(this, &ScriptTextEditor::_inline_object_draw),738callable_mp(this, &ScriptTextEditor::_inline_object_handle_click));739} else {740code_editor->get_text_editor()->set_inline_object_handlers(Callable(), Callable(), Callable());741}742TextEditorBase::update_settings();743}744745Variant ScriptTextEditor::get_edit_state() {746if (pending_state != Variant()) {747return pending_state;748}749return TextEditorBase::get_edit_state();750}751752void ScriptTextEditor::set_edit_state(const Variant &p_state) {753if (editor_enabled) {754code_editor->set_edit_state(p_state);755} else {756// The editor is not fully initialized, so the state can't be loaded properly.757pending_state = p_state;758}759760Dictionary state = p_state;761if (state.has("syntax_highlighter")) {762for (const Ref<EditorSyntaxHighlighter> &highlighter : highlighters) {763if (highlighter->_get_name() == String(state["syntax_highlighter"])) {764set_syntax_highlighter(highlighter);765break;766}767}768}769770if (editor_enabled) {771#ifndef ANDROID_ENABLED772ensure_focus();773#endif774}775}776777Variant ScriptTextEditor::get_previous_state() {778return code_editor->get_previous_state();779}780781void ScriptTextEditor::store_previous_state() {782return code_editor->store_previous_state();783}784785Ref<Texture2D> ScriptTextEditor::get_theme_icon() {786Ref<Script> script = edited_res;787if (get_parent_control()) {788String icon_name = script->get_class();789if (script->is_built_in()) {790icon_name += "Internal";791}792793if (get_parent_control()->has_theme_icon(icon_name, EditorStringName(EditorIcons))) {794return get_parent_control()->get_editor_theme_icon(icon_name);795} else if (get_parent_control()->has_theme_icon(script->get_class(), EditorStringName(EditorIcons))) {796return get_parent_control()->get_editor_theme_icon(script->get_class());797}798}799800Ref<Texture2D> extension_language_icon = EditorNode::get_editor_data().extension_class_get_icon(script->get_class());801Ref<Texture2D> extension_language_alt_icon;802if (script->is_built_in()) {803extension_language_alt_icon = EditorNode::get_editor_data().extension_class_get_icon(script->get_class() + "Internal");804}805806if (extension_language_alt_icon.is_valid()) {807return extension_language_alt_icon;808} else if (extension_language_icon.is_valid()) {809return extension_language_icon;810}811812return Ref<Texture2D>();813}814815void ScriptTextEditor::_validate_script() {816CodeEdit *te = code_editor->get_text_editor();817818String text = te->get_text();819List<String> fnc;820821warnings.clear();822errors.clear();823depended_errors.clear();824safe_lines.clear();825826Ref<Script> script = edited_res;827if (!script->get_language()->validate(text, script->get_path(), &fnc, &errors, &warnings, &safe_lines)) {828List<ScriptLanguage::ScriptError>::Element *E = errors.front();829while (E) {830List<ScriptLanguage::ScriptError>::Element *next_E = E->next();831if ((E->get().path.is_empty() && !script->get_path().is_empty()) || E->get().path != script->get_path()) {832depended_errors[E->get().path].push_back(E->get());833E->erase();834}835E = next_E;836}837838if (errors.size() > 0) {839const int line = errors.front()->get().line;840const int column = errors.front()->get().column;841const String message = errors.front()->get().message.replace("[", "[lb]");842const String error_text = vformat(TTR("Error at ([hint=Line %d, column %d]%d, %d[/hint]):"), line, column, line, column) + " " + message;843code_editor->set_error(error_text);844code_editor->set_error_pos(line - 1, column - 1);845}846script_is_valid = false;847} else {848code_editor->set_error("");849if (!script->is_tool()) {850script->set_source_code(text);851script->update_exports();852te->get_syntax_highlighter()->update_cache();853}854855functions.clear();856for (const String &E : fnc) {857functions.push_back(E);858}859script_is_valid = true;860}861_update_connected_methods();862_update_warnings();863_update_errors();864_update_background_color();865866if (!pending_dragged_exports.is_empty()) {867_assign_dragged_export_variables();868}869870TextEditorBase::_validate_script();871}872873void ScriptTextEditor::_update_warnings() {874int warning_nb = warnings.size();875warnings_panel->clear();876877bool has_connections_table = false;878// Add missing connections.879if (GLOBAL_GET("debug/gdscript/warnings/enable")) {880Node *base = get_tree()->get_edited_scene_root();881if (base && missing_connections.size() > 0) {882has_connections_table = true;883warnings_panel->push_table(1);884for (const Connection &connection : missing_connections) {885String base_path = base->get_name();886String source_path = base == connection.signal.get_object() ? base_path : base_path + "/" + String(base->get_path_to(Object::cast_to<Node>(connection.signal.get_object())));887String target_path = base == connection.callable.get_object() ? base_path : base_path + "/" + String(base->get_path_to(Object::cast_to<Node>(connection.callable.get_object())));888889warnings_panel->push_cell();890warnings_panel->push_color(warnings_panel->get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));891warnings_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));892warnings_panel->pop(); // Color.893warnings_panel->pop(); // Cell.894}895warnings_panel->pop(); // Table.896897warning_nb += missing_connections.size();898}899}900901code_editor->set_warning_count(warning_nb);902903if (has_connections_table) {904warnings_panel->add_newline();905}906907// Add script warnings.908warnings_panel->push_table(3);909for (const ScriptLanguage::Warning &w : warnings) {910Dictionary ignore_meta;911ignore_meta["line"] = w.start_line;912ignore_meta["code"] = w.string_code.to_lower();913warnings_panel->push_cell();914warnings_panel->push_meta(ignore_meta);915warnings_panel->push_color(916warnings_panel->get_theme_color(SNAME("accent_color"), EditorStringName(Editor)).lerp(warnings_panel->get_theme_color(SNAME("mono_color"), EditorStringName(Editor)), 0.5f));917warnings_panel->add_text(TTR("[Ignore]"));918warnings_panel->pop(); // Color.919warnings_panel->pop(); // Meta ignore.920warnings_panel->pop(); // Cell.921922warnings_panel->push_cell();923warnings_panel->push_meta(w.start_line - 1);924warnings_panel->push_color(warnings_panel->get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));925warnings_panel->add_text(vformat(TTR("Line %d (%s):"), w.start_line, w.string_code));926warnings_panel->pop(); // Color.927warnings_panel->pop(); // Meta goto.928warnings_panel->pop(); // Cell.929930warnings_panel->push_cell();931warnings_panel->add_text(w.message);932warnings_panel->add_newline();933warnings_panel->pop(); // Cell.934}935warnings_panel->pop(); // Table.936}937938void ScriptTextEditor::_update_errors() {939code_editor->set_error_count(errors.size());940941errors_panel->clear();942errors_panel->push_table(2);943for (const ScriptLanguage::ScriptError &err : errors) {944Dictionary click_meta;945click_meta["line"] = err.line;946click_meta["column"] = err.column;947948errors_panel->push_cell();949errors_panel->push_meta(err.line - 1);950errors_panel->push_color(warnings_panel->get_theme_color(SNAME("error_color"), EditorStringName(Editor)));951errors_panel->add_text(vformat(TTR("Line %d:"), err.line));952errors_panel->pop(); // Color.953errors_panel->pop(); // Meta goto.954errors_panel->pop(); // Cell.955956errors_panel->push_cell();957errors_panel->add_text(err.message);958errors_panel->add_newline();959errors_panel->pop(); // Cell.960}961errors_panel->pop(); // Table962963for (const KeyValue<String, List<ScriptLanguage::ScriptError>> &KV : depended_errors) {964Dictionary click_meta;965click_meta["path"] = KV.key;966click_meta["line"] = 1;967968errors_panel->add_newline();969errors_panel->add_newline();970errors_panel->push_meta(click_meta);971errors_panel->add_text(vformat(R"(%s:)", KV.key));972errors_panel->pop(); // Meta goto.973errors_panel->add_newline();974975errors_panel->push_indent(1);976errors_panel->push_table(2);977String filename = KV.key.get_file();978for (const ScriptLanguage::ScriptError &err : KV.value) {979click_meta["line"] = err.line;980click_meta["column"] = err.column;981982errors_panel->push_cell();983errors_panel->push_meta(click_meta);984errors_panel->push_color(errors_panel->get_theme_color(SNAME("error_color"), EditorStringName(Editor)));985errors_panel->add_text(vformat(TTR("Line %d:"), err.line));986errors_panel->pop(); // Color.987errors_panel->pop(); // Meta goto.988errors_panel->pop(); // Cell.989990errors_panel->push_cell();991errors_panel->add_text(err.message);992errors_panel->pop(); // Cell.993}994errors_panel->pop(); // Table995errors_panel->pop(); // Indent.996}997998bool highlight_safe = EDITOR_GET("text_editor/appearance/gutters/highlight_type_safe_lines");999bool last_is_safe = false;1000CodeEdit *te = code_editor->get_text_editor();10011002for (int i = 0; i < te->get_line_count(); i++) {1003if (highlight_safe) {1004if (safe_lines.has(i + 1)) {1005te->set_line_gutter_item_color(i, line_number_gutter, safe_line_number_color);1006last_is_safe = true;1007} else if (last_is_safe && (te->is_in_comment(i) != -1 || te->get_line(i).strip_edges().is_empty())) {1008te->set_line_gutter_item_color(i, line_number_gutter, safe_line_number_color);1009} else {1010te->set_line_gutter_item_color(i, line_number_gutter, default_line_number_color);1011last_is_safe = false;1012}1013} else {1014te->set_line_gutter_item_color(i, 1, default_line_number_color);1015}1016}1017}10181019static Vector<Node *> _find_all_node_for_script(Node *p_base, Node *p_current, const Ref<Script> &p_script) {1020Vector<Node *> nodes;10211022if (p_current->get_owner() != p_base && p_base != p_current) {1023return nodes;1024}10251026Ref<Script> c = p_current->get_script();1027if (c == p_script) {1028nodes.push_back(p_current);1029}10301031for (int i = 0; i < p_current->get_child_count(); i++) {1032Vector<Node *> found = _find_all_node_for_script(p_base, p_current->get_child(i), p_script);1033nodes.append_array(found);1034}10351036return nodes;1037}10381039static Node *_find_node_for_script(Node *p_base, Node *p_current, const Ref<Script> &p_script) {1040if (p_current->get_owner() != p_base && p_base != p_current) {1041return nullptr;1042}1043Ref<Script> c = p_current->get_script();1044if (c == p_script) {1045return p_current;1046}1047for (int i = 0; i < p_current->get_child_count(); i++) {1048Node *found = _find_node_for_script(p_base, p_current->get_child(i), p_script);1049if (found) {1050return found;1051}1052}10531054return nullptr;1055}10561057static void _find_changed_scripts_for_external_editor(Node *p_base, Node *p_current, HashSet<Ref<Script>> &r_scripts) {1058if (p_current->get_owner() != p_base && p_base != p_current) {1059return;1060}1061Ref<Script> c = p_current->get_script();10621063if (c.is_valid()) {1064r_scripts.insert(c);1065}10661067for (int i = 0; i < p_current->get_child_count(); i++) {1068_find_changed_scripts_for_external_editor(p_base, p_current->get_child(i), r_scripts);1069}1070}10711072void ScriptEditor::_update_modified_scripts_for_external_editor(Ref<Script> p_for_script) {1073bool use_external_editor = bool(EDITOR_GET("text_editor/external/use_external_editor"));10741075ERR_FAIL_NULL(get_tree());10761077HashSet<Ref<Script>> scripts;10781079Node *base = get_tree()->get_edited_scene_root();1080if (base) {1081_find_changed_scripts_for_external_editor(base, base, scripts);1082}10831084for (const Ref<Script> &E : scripts) {1085Ref<Script> scr = E;10861087if (!use_external_editor && !scr->get_language()->overrides_external_editor()) {1088continue; // We're not using an external editor for this script.1089}10901091if (p_for_script.is_valid() && p_for_script != scr) {1092continue;1093}10941095if (scr->is_built_in()) {1096continue; //internal script, who cares, though weird1097}10981099uint64_t last_date = scr->get_last_modified_time();1100uint64_t date = FileAccess::get_modified_time(scr->get_path());11011102if (last_date != date) {1103Ref<Script> rel_scr = ResourceLoader::load(scr->get_path(), scr->get_class(), ResourceFormatLoader::CACHE_MODE_IGNORE);1104ERR_CONTINUE(rel_scr.is_null());1105scr->set_source_code(rel_scr->get_source_code());1106scr->set_last_modified_time(rel_scr->get_last_modified_time());1107scr->update_exports();11081109trigger_live_script_reload(scr->get_path());1110}1111}1112}11131114void ScriptTextEditor::_code_complete_scripts(void *p_ud, const String &p_code, List<ScriptLanguage::CodeCompletionOption> *r_options, bool &r_force) {1115ScriptTextEditor *ste = (ScriptTextEditor *)p_ud;1116ste->_code_complete_script(p_code, r_options, r_force);1117}11181119void ScriptTextEditor::_code_complete_script(const String &p_code, List<ScriptLanguage::CodeCompletionOption> *r_options, bool &r_force) {1120if (color_panel->is_visible()) {1121return;1122}11231124Ref<Script> script = edited_res;1125Node *base = get_tree()->get_edited_scene_root();1126if (base) {1127base = _find_node_for_script(base, base, script);1128}1129String hint;1130Error err = script->get_language()->complete_code(p_code, script->get_path(), base, r_options, r_force, hint);11311132if (err == OK) {1133code_editor->get_text_editor()->set_code_hint(hint);1134}1135}11361137void ScriptTextEditor::_breakpoint_toggled(int p_row) {1138const CodeEdit *ce = code_editor->get_text_editor();1139bool enabled = p_row < ce->get_line_count() && ce->is_line_breakpointed(p_row);1140EditorDebuggerNode::get_singleton()->set_breakpoint(edited_res->get_path(), p_row + 1, enabled);1141}11421143void ScriptTextEditor::_on_caret_moved() {1144if (code_editor->is_previewing_navigation_change()) {1145return;1146}1147int current_line = code_editor->get_text_editor()->get_caret_line();1148if (Math::abs(current_line - previous_line) >= 10) {1149Dictionary nav_state = get_navigation_state();1150nav_state["row"] = previous_line;1151nav_state["scroll_position"] = -1;1152emit_signal(SNAME("request_save_previous_state"), nav_state);1153store_previous_state();1154}1155previous_line = current_line;1156}11571158void ScriptTextEditor::_lookup_symbol(const String &p_symbol, int p_row, int p_column) {1159Ref<Script> script = edited_res;1160Node *base = get_tree()->get_edited_scene_root();1161if (base) {1162base = _find_node_for_script(base, base, script);1163}11641165ScriptLanguage::LookupResult result;1166String code_text = code_editor->get_text_editor()->get_text_with_cursor_char(p_row, p_column);1167Error lc_error = script->get_language()->lookup_code(code_text, p_symbol, script->get_path(), base, result);1168if (ScriptServer::is_global_class(p_symbol)) {1169EditorNode::get_singleton()->load_resource(ScriptServer::get_global_class_path(p_symbol));1170} else if (p_symbol.is_resource_file() || p_symbol.begins_with("uid://")) {1171if (DirAccess::dir_exists_absolute(p_symbol)) {1172FileSystemDock::get_singleton()->navigate_to_path(p_symbol);1173} else {1174EditorNode::get_singleton()->load_scene_or_resource(p_symbol);1175}1176} else if (lc_error == OK) {1177_goto_line(p_row);11781179if (!result.class_name.is_empty() && EditorHelp::get_doc_data()->class_list.has(result.class_name) && !EditorHelp::get_doc_data()->class_list[result.class_name].is_script_doc) {1180switch (result.type) {1181case ScriptLanguage::LOOKUP_RESULT_CLASS: {1182emit_signal(SNAME("go_to_help"), "class_name:" + result.class_name);1183} break;1184case ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT: {1185StringName cname = result.class_name;1186while (ClassDB::class_exists(cname)) {1187if (ClassDB::has_integer_constant(cname, result.class_member, true)) {1188result.class_name = cname;1189break;1190}1191cname = ClassDB::get_parent_class(cname);1192}1193emit_signal(SNAME("go_to_help"), "class_constant:" + result.class_name + ":" + result.class_member);1194} break;1195case ScriptLanguage::LOOKUP_RESULT_CLASS_PROPERTY: {1196StringName cname = result.class_name;1197while (ClassDB::class_exists(cname)) {1198if (ClassDB::has_property(cname, result.class_member, true)) {1199result.class_name = cname;1200break;1201}1202cname = ClassDB::get_parent_class(cname);1203}1204emit_signal(SNAME("go_to_help"), "class_property:" + result.class_name + ":" + result.class_member);1205} break;1206case ScriptLanguage::LOOKUP_RESULT_CLASS_METHOD: {1207StringName cname = result.class_name;1208while (ClassDB::class_exists(cname)) {1209if (ClassDB::has_method(cname, result.class_member, true)) {1210result.class_name = cname;1211break;1212}1213cname = ClassDB::get_parent_class(cname);1214}1215emit_signal(SNAME("go_to_help"), "class_method:" + result.class_name + ":" + result.class_member);1216} break;1217case ScriptLanguage::LOOKUP_RESULT_CLASS_SIGNAL: {1218StringName cname = result.class_name;1219while (ClassDB::class_exists(cname)) {1220if (ClassDB::has_signal(cname, result.class_member, true)) {1221result.class_name = cname;1222break;1223}1224cname = ClassDB::get_parent_class(cname);1225}1226emit_signal(SNAME("go_to_help"), "class_signal:" + result.class_name + ":" + result.class_member);1227} break;1228case ScriptLanguage::LOOKUP_RESULT_CLASS_ENUM: {1229StringName cname = result.class_name;1230while (ClassDB::class_exists(cname)) {1231if (ClassDB::has_enum(cname, result.class_member, true)) {1232result.class_name = cname;1233break;1234}1235cname = ClassDB::get_parent_class(cname);1236}1237emit_signal(SNAME("go_to_help"), "class_enum:" + result.class_name + ":" + result.class_member);1238} break;1239case ScriptLanguage::LOOKUP_RESULT_CLASS_ANNOTATION: {1240emit_signal(SNAME("go_to_help"), "class_annotation:" + result.class_name + ":" + result.class_member);1241} break;1242case ScriptLanguage::LOOKUP_RESULT_CLASS_TBD_GLOBALSCOPE: { // Deprecated.1243emit_signal(SNAME("go_to_help"), "class_global:" + result.class_name + ":" + result.class_member);1244} break;1245case ScriptLanguage::LOOKUP_RESULT_SCRIPT_LOCATION:1246case ScriptLanguage::LOOKUP_RESULT_LOCAL_CONSTANT:1247case ScriptLanguage::LOOKUP_RESULT_LOCAL_VARIABLE:1248case ScriptLanguage::LOOKUP_RESULT_MAX: {1249// Nothing to do.1250} break;1251}1252} else if (result.location >= 0) {1253if (result.script.is_valid()) {1254emit_signal(SNAME("request_open_script_at_line"), result.script, result.location - 1);1255} else {1256emit_signal(SNAME("request_save_history"));1257goto_line_centered(result.location - 1);1258}1259}1260} else if (ProjectSettings::get_singleton()->has_autoload(p_symbol)) {1261// Check for Autoload scenes.1262const ProjectSettings::AutoloadInfo &info = ProjectSettings::get_singleton()->get_autoload(p_symbol);1263if (info.is_singleton) {1264EditorNode::get_singleton()->load_scene(info.path);1265}1266} else if (p_symbol.is_relative_path()) {1267// Every symbol other than absolute path is relative path so keep this condition at last.1268String path = _get_absolute_path(p_symbol);1269if (FileAccess::exists(path)) {1270EditorNode::get_singleton()->load_scene_or_resource(path);1271}1272}1273}12741275void ScriptTextEditor::_validate_symbol(const String &p_symbol) {1276CodeEdit *text_edit = code_editor->get_text_editor();12771278Ref<Script> script = edited_res;1279Node *base = get_tree()->get_edited_scene_root();1280if (base) {1281base = _find_node_for_script(base, base, script);1282}12831284ScriptLanguage::LookupResult result;1285String lc_text = code_editor->get_text_editor()->get_text_for_symbol_lookup();1286Error lc_error = script->get_language()->lookup_code(lc_text, p_symbol, script->get_path(), base, result);1287bool is_singleton = ProjectSettings::get_singleton()->has_autoload(p_symbol) && ProjectSettings::get_singleton()->get_autoload(p_symbol).is_singleton;1288if (lc_error == OK || is_singleton || ScriptServer::is_global_class(p_symbol) || p_symbol.is_resource_file() || p_symbol.begins_with("uid://")) {1289text_edit->set_symbol_lookup_word_as_valid(true);1290} else if (p_symbol.is_relative_path()) {1291String path = _get_absolute_path(p_symbol);1292if (FileAccess::exists(path)) {1293text_edit->set_symbol_lookup_word_as_valid(true);1294} else {1295text_edit->set_symbol_lookup_word_as_valid(false);1296}1297} else {1298text_edit->set_symbol_lookup_word_as_valid(false);1299}1300}13011302void ScriptTextEditor::_show_symbol_tooltip(const String &p_symbol, int p_row, int p_column) {1303if (!EDITOR_GET("text_editor/behavior/documentation/enable_tooltips").booleanize()) {1304return;1305}13061307if (p_symbol.begins_with("res://") || p_symbol.begins_with("uid://")) {1308Control *tmp = EditorHelpBitTooltip::make_tooltip(code_editor->get_text_editor(), "resource||" + p_symbol);1309memdelete(tmp);1310return;1311}13121313Ref<Script> script = edited_res;1314Node *base = get_tree()->get_edited_scene_root();1315if (base) {1316base = _find_node_for_script(base, base, script);1317}13181319ScriptLanguage::LookupResult result;1320String doc_symbol;1321const String code_text = code_editor->get_text_editor()->get_text_with_cursor_char(p_row, p_column);1322const Error lc_error = script->get_language()->lookup_code(code_text, p_symbol, script->get_path(), base, result);1323if (lc_error == OK) {1324switch (result.type) {1325case ScriptLanguage::LOOKUP_RESULT_CLASS: {1326doc_symbol = "class|" + result.class_name + "|";1327} break;1328case ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT: {1329StringName cname = result.class_name;1330while (ClassDB::class_exists(cname)) {1331if (ClassDB::has_integer_constant(cname, result.class_member, true)) {1332result.class_name = cname;1333break;1334}1335cname = ClassDB::get_parent_class(cname);1336}1337doc_symbol = "constant|" + result.class_name + "|" + result.class_member;1338} break;1339case ScriptLanguage::LOOKUP_RESULT_CLASS_PROPERTY: {1340StringName cname = result.class_name;1341while (ClassDB::class_exists(cname)) {1342if (ClassDB::has_property(cname, result.class_member, true)) {1343result.class_name = cname;1344break;1345}1346cname = ClassDB::get_parent_class(cname);1347}1348doc_symbol = "property|" + result.class_name + "|" + result.class_member;1349} break;1350case ScriptLanguage::LOOKUP_RESULT_CLASS_METHOD: {1351StringName cname = result.class_name;1352while (ClassDB::class_exists(cname)) {1353if (ClassDB::has_method(cname, result.class_member, true)) {1354result.class_name = cname;1355break;1356}1357cname = ClassDB::get_parent_class(cname);1358}1359doc_symbol = "method|" + result.class_name + "|" + result.class_member;1360} break;1361case ScriptLanguage::LOOKUP_RESULT_CLASS_SIGNAL: {1362StringName cname = result.class_name;1363while (ClassDB::class_exists(cname)) {1364if (ClassDB::has_signal(cname, result.class_member, true)) {1365result.class_name = cname;1366break;1367}1368cname = ClassDB::get_parent_class(cname);1369}1370doc_symbol = "signal|" + result.class_name + "|" + result.class_member;1371} break;1372case ScriptLanguage::LOOKUP_RESULT_CLASS_ENUM: {1373StringName cname = result.class_name;1374while (ClassDB::class_exists(cname)) {1375if (ClassDB::has_enum(cname, result.class_member, true)) {1376result.class_name = cname;1377break;1378}1379cname = ClassDB::get_parent_class(cname);1380}1381doc_symbol = "enum|" + result.class_name + "|" + result.class_member;1382} break;1383case ScriptLanguage::LOOKUP_RESULT_CLASS_ANNOTATION: {1384doc_symbol = "annotation|" + result.class_name + "|" + result.class_member;1385} break;1386case ScriptLanguage::LOOKUP_RESULT_LOCAL_CONSTANT:1387case ScriptLanguage::LOOKUP_RESULT_LOCAL_VARIABLE: {1388const String item_type = (result.type == ScriptLanguage::LOOKUP_RESULT_LOCAL_CONSTANT) ? "local_constant" : "local_variable";1389Dictionary item_data;1390item_data["description"] = result.description;1391item_data["is_deprecated"] = result.is_deprecated;1392item_data["deprecated_message"] = result.deprecated_message;1393item_data["is_experimental"] = result.is_experimental;1394item_data["experimental_message"] = result.experimental_message;1395item_data["doc_type"] = result.doc_type;1396item_data["enumeration"] = result.enumeration;1397item_data["is_bitfield"] = result.is_bitfield;1398item_data["value"] = result.value;1399doc_symbol = item_type + "||" + p_symbol + "|" + JSON::stringify(item_data);1400} break;1401case ScriptLanguage::LOOKUP_RESULT_SCRIPT_LOCATION:1402case ScriptLanguage::LOOKUP_RESULT_CLASS_TBD_GLOBALSCOPE: // Deprecated.1403case ScriptLanguage::LOOKUP_RESULT_MAX: {1404// Nothing to do.1405} break;1406}1407}14081409// NOTE: See also `ScriptEditor::_get_debug_tooltip()` for documentation tooltips disabled.1410String debug_value = EditorDebuggerNode::get_singleton()->get_var_value(p_symbol);1411if (!debug_value.is_empty()) {1412constexpr int DISPLAY_LIMIT = 1024;1413if (debug_value.size() > DISPLAY_LIMIT) {1414debug_value = debug_value.left(DISPLAY_LIMIT) + "... " + TTR("(truncated)");1415}1416debug_value = TTR("Current value: ") + debug_value.replace("[", "[lb]");1417}14181419if (!doc_symbol.is_empty() || !debug_value.is_empty()) {1420Control *tmp = EditorHelpBitTooltip::make_tooltip(code_editor->get_text_editor(), doc_symbol, debug_value, true);1421memdelete(tmp);1422}1423}14241425String ScriptTextEditor::_get_absolute_path(const String &rel_path) {1426String base_path = edited_res->get_path().get_base_dir();1427String path = base_path.path_join(rel_path);1428return path.replace("///", "//").simplify_path();1429}14301431void ScriptTextEditor::_update_connected_methods() {1432CodeEdit *text_edit = code_editor->get_text_editor();1433text_edit->set_gutter_width(connection_gutter, text_edit->get_line_height());1434for (int i = 0; i < text_edit->get_line_count(); i++) {1435text_edit->set_line_gutter_metadata(i, connection_gutter, Dictionary());1436text_edit->set_line_gutter_icon(i, connection_gutter, nullptr);1437text_edit->set_line_gutter_clickable(i, connection_gutter, false);1438}1439missing_connections.clear();14401441if (!script_is_valid) {1442return;1443}14441445Node *base = get_tree()->get_edited_scene_root();1446if (!base) {1447return;1448}14491450// Add connection icons to methods.1451Ref<Script> script = edited_res;1452Vector<Node *> nodes = _find_all_node_for_script(base, base, script);1453HashSet<StringName> methods_found;1454for (int i = 0; i < nodes.size(); i++) {1455List<Connection> signal_connections;1456nodes[i]->get_signals_connected_to_this(&signal_connections);14571458for (const Connection &connection : signal_connections) {1459if (!(connection.flags & CONNECT_PERSIST)) {1460continue;1461}14621463// As deleted nodes are still accessible via the undo/redo system, check if they're still on the tree.1464Node *source = Object::cast_to<Node>(connection.signal.get_object());1465if (source && !source->is_inside_tree()) {1466continue;1467}14681469const StringName method = connection.callable.get_method();1470if (methods_found.has(method)) {1471continue;1472}14731474if (!ClassDB::has_method(script->get_instance_base_type(), method)) {1475int line = -1;14761477for (int j = 0; j < functions.size(); j++) {1478String name = functions[j].get_slicec(':', 0);1479if (name == method) {1480Dictionary line_meta;1481line_meta["type"] = "connection";1482line_meta["method"] = method;1483line = functions[j].get_slicec(':', 1).to_int() - 1;1484text_edit->set_line_gutter_metadata(line, connection_gutter, line_meta);1485text_edit->set_line_gutter_icon(line, connection_gutter, get_parent_control()->get_editor_theme_icon(SNAME("Slot")));1486text_edit->set_line_gutter_clickable(line, connection_gutter, true);1487methods_found.insert(method);1488break;1489}1490}14911492if (line >= 0) {1493continue;1494}14951496// There is a chance that the method is inherited from another script.1497bool found_inherited_function = false;1498Ref<Script> inherited_script = script->get_base_script();1499while (inherited_script.is_valid()) {1500if (inherited_script->has_method(method)) {1501found_inherited_function = true;1502break;1503}15041505inherited_script = inherited_script->get_base_script();1506}15071508if (!found_inherited_function) {1509missing_connections.push_back(connection);1510}1511}1512}1513}15141515// Add override icons to methods.1516methods_found.clear();1517for (int i = 0; i < functions.size(); i++) {1518String raw_name = functions[i].get_slicec(':', 0);1519StringName name = StringName(raw_name);1520if (methods_found.has(name)) {1521continue;1522}15231524// Account for inner classes by stripping the class names from the method,1525// starting from the right since our inner class might be inside of another inner class.1526int pos = raw_name.rfind_char('.');1527if (pos != -1) {1528name = raw_name.substr(pos + 1);1529}15301531String found_base_class;1532StringName base_class = script->get_instance_base_type();1533Ref<Script> inherited_script = script->get_base_script();1534while (inherited_script.is_valid()) {1535if (inherited_script->has_method(name)) {1536found_base_class = "script:" + inherited_script->get_path();1537break;1538}15391540base_class = inherited_script->get_instance_base_type();1541inherited_script = inherited_script->get_base_script();1542}15431544if (found_base_class.is_empty()) {1545while (base_class) {1546List<MethodInfo> methods;1547ClassDB::get_method_list(base_class, &methods, true);1548for (const MethodInfo &mi : methods) {1549if (mi.name == name) {1550found_base_class = "builtin:" + base_class;1551break;1552}1553}15541555ClassDB::ClassInfo *base_class_ptr = ClassDB::classes.getptr(base_class)->inherits_ptr;1556if (base_class_ptr == nullptr) {1557break;1558}1559base_class = base_class_ptr->name;1560}1561}15621563if (!found_base_class.is_empty()) {1564int line = functions[i].get_slicec(':', 1).to_int() - 1;15651566Dictionary line_meta = text_edit->get_line_gutter_metadata(line, connection_gutter);1567if (line_meta.is_empty()) {1568// Add override icon to gutter.1569line_meta["type"] = "inherits";1570line_meta["method"] = name;1571line_meta["base_class"] = found_base_class;1572text_edit->set_line_gutter_icon(line, connection_gutter, get_parent_control()->get_editor_theme_icon(SNAME("MethodOverride")));1573text_edit->set_line_gutter_clickable(line, connection_gutter, true);1574} else {1575// If method is also connected to signal, then merge icons and keep the click behavior of the slot.1576text_edit->set_line_gutter_icon(line, connection_gutter, get_parent_control()->get_editor_theme_icon(SNAME("MethodOverrideAndSlot")));1577}15781579methods_found.insert(StringName(raw_name));1580}1581}1582}15831584void ScriptTextEditor::_update_gutter_indexes() {1585for (int i = 0; i < code_editor->get_text_editor()->get_gutter_count(); i++) {1586if (code_editor->get_text_editor()->get_gutter_name(i) == "connection_gutter") {1587connection_gutter = i;1588continue;1589}15901591if (code_editor->get_text_editor()->get_gutter_name(i) == "line_numbers") {1592line_number_gutter = i;1593continue;1594}1595}1596}15971598void ScriptTextEditor::_gutter_clicked(int p_line, int p_gutter) {1599if (p_gutter != connection_gutter) {1600return;1601}16021603Dictionary meta = code_editor->get_text_editor()->get_line_gutter_metadata(p_line, p_gutter);1604String type = meta.get("type", "");1605if (type.is_empty()) {1606return;1607}16081609// All types currently need a method name.1610String method = meta.get("method", "");1611if (method.is_empty()) {1612return;1613}16141615if (type == "connection") {1616Node *base = get_tree()->get_edited_scene_root();1617if (!base) {1618return;1619}16201621Ref<Script> script = edited_res;1622Vector<Node *> nodes = _find_all_node_for_script(base, base, script);1623connection_info_dialog->popup_connections(method, nodes);1624} else if (type == "inherits") {1625String base_class_raw = meta["base_class"];1626PackedStringArray base_class_split = base_class_raw.split(":", true, 1);16271628if (base_class_split[0] == "script") {1629// Go to function declaration.1630Ref<Script> base_script = ResourceLoader::load(base_class_split[1]);1631ERR_FAIL_COND(base_script.is_null());1632emit_signal(SNAME("go_to_method"), base_script, method);1633} else if (base_class_split[0] == "builtin") {1634// Open method documentation.1635emit_signal(SNAME("go_to_help"), "class_method:" + base_class_split[1] + ":" + method);1636}1637}1638}16391640bool ScriptTextEditor::_edit_option(int p_op) {1641CodeEdit *tx = code_editor->get_text_editor();1642tx->apply_ime();16431644switch (p_op) {1645case EDIT_CREATE_CODE_REGION: {1646tx->create_code_region();1647} break;1648case EDIT_TOGGLE_COMMENT: {1649_edit_option_toggle_inline_comment();1650} break;1651case EDIT_COMPLETE: {1652tx->request_code_completion(true);1653} break;1654case EDIT_AUTO_INDENT: {1655String text = tx->get_text();1656Ref<Script> scr = edited_res;1657if (scr.is_null()) {1658return true;1659}16601661tx->begin_complex_operation();1662tx->begin_multicaret_edit();1663int begin = tx->get_line_count() - 1, end = 0;1664if (tx->has_selection()) {1665// Auto indent all lines that have a caret or selection on it.1666Vector<Point2i> line_ranges = tx->get_line_ranges_from_carets();1667for (Point2i line_range : line_ranges) {1668scr->get_language()->auto_indent_code(text, line_range.x, line_range.y);1669if (line_range.x < begin) {1670begin = line_range.x;1671}1672if (line_range.y > end) {1673end = line_range.y;1674}1675}1676} else {1677// Auto indent entire text.1678begin = 0;1679end = tx->get_line_count() - 1;1680scr->get_language()->auto_indent_code(text, begin, end);1681}16821683// Apply auto indented code.1684Vector<String> lines = text.split("\n");1685for (int i = begin; i <= end; ++i) {1686tx->set_line(i, lines[i]);1687}16881689tx->end_multicaret_edit();1690tx->end_complex_operation();1691} break;1692case EDIT_PICK_COLOR: {1693color_panel->popup();1694} break;1695case EDIT_EVALUATE: {1696Expression expression;1697tx->begin_complex_operation();1698for (int caret_idx = 0; caret_idx < tx->get_caret_count(); caret_idx++) {1699Vector<String> lines = tx->get_selected_text(caret_idx).split("\n");1700PackedStringArray results;17011702for (int i = 0; i < lines.size(); i++) {1703const String &line = lines[i];1704String whitespace = line.substr(0, line.size() - line.strip_edges(true, false).size()); // Extract the whitespace at the beginning.1705if (expression.parse(line) == OK) {1706Variant result = expression.execute(Array(), Variant(), false, true);1707if (expression.get_error_text().is_empty()) {1708results.push_back(whitespace + result.get_construct_string());1709} else {1710results.push_back(line);1711}1712} else {1713results.push_back(line);1714}1715}1716tx->insert_text_at_caret(String("\n").join(results), caret_idx);1717}1718tx->end_complex_operation();1719} break;1720case SEARCH_LOCATE_FUNCTION: {1721quick_open->popup_dialog(get_functions());1722} break;1723case DEBUG_TOGGLE_BREAKPOINT: {1724Vector<int> sorted_carets = tx->get_sorted_carets();1725int last_line = -1;1726for (const int &c : sorted_carets) {1727int from = tx->get_selection_from_line(c);1728from += from == last_line ? 1 : 0;1729int to = tx->get_selection_to_line(c);1730if (to < from) {1731continue;1732}1733// Check first if there's any lines with breakpoints in the selection.1734bool selection_has_breakpoints = false;1735for (int line = from; line <= to; line++) {1736if (tx->is_line_breakpointed(line)) {1737selection_has_breakpoints = true;1738break;1739}1740}17411742// Set breakpoint on caret or remove all bookmarks from the selection.1743if (!selection_has_breakpoints) {1744if (tx->get_caret_line(c) != last_line) {1745tx->set_line_as_breakpoint(tx->get_caret_line(c), true);1746}1747} else {1748for (int line = from; line <= to; line++) {1749tx->set_line_as_breakpoint(line, false);1750}1751}1752last_line = to;1753}1754} break;1755case DEBUG_REMOVE_ALL_BREAKPOINTS: {1756PackedInt32Array bpoints = tx->get_breakpointed_lines();17571758for (int i = 0; i < bpoints.size(); i++) {1759int line = bpoints[i];1760bool dobreak = !tx->is_line_breakpointed(line);1761tx->set_line_as_breakpoint(line, dobreak);1762EditorDebuggerNode::get_singleton()->set_breakpoint(edited_res->get_path(), line + 1, dobreak);1763}1764} break;1765case DEBUG_GOTO_NEXT_BREAKPOINT: {1766PackedInt32Array bpoints = tx->get_breakpointed_lines();1767if (bpoints.is_empty()) {1768return true;1769}17701771int current_line = tx->get_caret_line();1772int bpoint_idx = 0;1773if (current_line < (int)bpoints[bpoints.size() - 1]) {1774while (bpoint_idx < bpoints.size() && bpoints[bpoint_idx] <= current_line) {1775bpoint_idx++;1776}1777}1778code_editor->goto_line_centered(bpoints[bpoint_idx]);1779} break;1780case DEBUG_GOTO_PREV_BREAKPOINT: {1781PackedInt32Array bpoints = tx->get_breakpointed_lines();1782if (bpoints.is_empty()) {1783return true;1784}17851786int current_line = tx->get_caret_line();1787int bpoint_idx = bpoints.size() - 1;1788if (current_line > (int)bpoints[0]) {1789while (bpoint_idx >= 0 && bpoints[bpoint_idx] >= current_line) {1790bpoint_idx--;1791}1792}1793code_editor->goto_line_centered(bpoints[bpoint_idx]);1794} break;1795case HELP_CONTEXTUAL: {1796String text = tx->get_selected_text(0);1797if (text.is_empty()) {1798text = tx->get_word_under_caret(0);1799}1800if (!text.is_empty()) {1801emit_signal(SNAME("request_help"), text);1802}1803} break;1804case LOOKUP_SYMBOL: {1805String text = tx->get_word_under_caret(0);1806if (text.is_empty()) {1807text = tx->get_selected_text(0);1808}1809if (!text.is_empty()) {1810_lookup_symbol(text, tx->get_caret_line(0), tx->get_caret_column(0));1811}1812} break;1813default: {1814if (TextEditorBase::_edit_option(p_op)) {1815return true;1816}1817if (p_op >= EditorContextMenuPlugin::BASE_ID) {1818EditorContextMenuPluginManager::get_singleton()->activate_custom_option(EditorContextMenuPlugin::CONTEXT_SLOT_SCRIPT_EDITOR_CODE, p_op, tx);1819}1820}1821}1822return true;1823}18241825void ScriptTextEditor::_edit_option_toggle_inline_comment() {1826Ref<Script> script = edited_res;1827if (script.is_null()) {1828return;1829}18301831String delimiter = "#";18321833for (const String &script_delimiter : script->get_language()->get_comment_delimiters()) {1834if (!script_delimiter.contains_char(' ')) {1835delimiter = script_delimiter;1836break;1837}1838}18391840code_editor->toggle_inline_comment(delimiter);1841}18421843void ScriptTextEditor::_notification(int p_what) {1844switch (p_what) {1845case NOTIFICATION_TRANSLATION_CHANGED: {1846if (is_ready() && is_visible_in_tree()) {1847_update_errors();1848_update_warnings();1849}1850} break;18511852case NOTIFICATION_THEME_CHANGED:1853if (!editor_enabled) {1854break;1855}1856if (is_visible_in_tree()) {1857_update_warnings();1858_update_errors();1859_update_background_color();1860}1861[[fallthrough]];1862case NOTIFICATION_ENTER_TREE: {1863code_editor->get_text_editor()->set_gutter_width(connection_gutter, code_editor->get_text_editor()->get_line_height());1864Ref<Font> code_font = get_theme_font("font", "CodeEdit");1865inline_color_options->add_theme_font_override("font", code_font);1866inline_color_options->get_popup()->add_theme_font_override("font", code_font);1867} break;1868}1869}18701871Control *ScriptTextEditor::get_edit_menu() {1872if (!edit_menus) {1873edit_menus = memnew(EditMenusSTE);1874}1875return edit_menus;1876}18771878PackedInt32Array ScriptTextEditor::get_breakpoints() {1879return code_editor->get_text_editor()->get_breakpointed_lines();1880}18811882void ScriptTextEditor::set_breakpoint(int p_line, bool p_enabled) {1883code_editor->get_text_editor()->set_line_as_breakpoint(p_line, p_enabled);1884}18851886void ScriptTextEditor::clear_breakpoints() {1887code_editor->get_text_editor()->clear_breakpointed_lines();1888}18891890Variant ScriptTextEditor::get_drag_data_fw(const Point2 &p_point, Control *p_from) {1891return Variant();1892}18931894bool ScriptTextEditor::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const {1895Dictionary d = p_data;1896if (d.has("type") &&1897(String(d["type"]) == "resource" ||1898String(d["type"]) == "files" ||1899String(d["type"]) == "nodes" ||1900String(d["type"]) == "obj_property" ||1901String(d["type"]) == "files_and_dirs")) {1902return true;1903}19041905return false;1906}19071908static Node *_find_script_node(Node *p_current_node, const Ref<Script> &script) {1909if (p_current_node->get_script() == script) {1910return p_current_node;1911}19121913for (int i = 0; i < p_current_node->get_child_count(); i++) {1914Node *n = _find_script_node(p_current_node->get_child(i), script);1915if (n) {1916return n;1917}1918}19191920return nullptr;1921}19221923static String _quote_drop_data(const String &str) {1924// This function prepares a string for being "dropped" into the script editor.1925// The string can be a resource path, node path or property name.19261927const bool using_single_quotes = EDITOR_GET("text_editor/completion/use_single_quotes");19281929String escaped = str.c_escape();19301931// If string is double quoted, there is no need to escape single quotes.1932// We can revert the extra escaping added in c_escape().1933if (!using_single_quotes) {1934escaped = escaped.replace("\\'", "\'");1935}19361937return escaped.quote(using_single_quotes ? "'" : "\"");1938}19391940static String _get_dropped_resource_as_member(const Ref<Resource> &p_resource, bool p_create_field, bool p_allow_uid) {1941String path = p_resource->get_path();1942if (p_allow_uid) {1943ResourceUID::ID id = ResourceLoader::get_resource_uid(path);1944if (id != ResourceUID::INVALID_ID) {1945path = ResourceUID::get_singleton()->id_to_text(id);1946}1947}1948const bool is_script = ClassDB::is_parent_class(p_resource->get_class(), "Script");19491950if (!p_create_field) {1951return vformat("preload(%s)", _quote_drop_data(path));1952}19531954String variable_name = p_resource->get_name();1955if (variable_name.is_empty()) {1956variable_name = p_resource->get_path().get_file().get_basename();1957}19581959if (is_script) {1960variable_name = variable_name.to_pascal_case().validate_unicode_identifier();1961} else {1962variable_name = variable_name.to_snake_case().to_upper().validate_unicode_identifier();1963}1964return vformat("const %s = preload(%s)", variable_name, _quote_drop_data(path));1965}19661967String ScriptTextEditor::_get_dropped_resource_as_exported_member(const Ref<Resource> &p_resource, const Vector<ObjectID> &p_script_instance_obj_ids) {1968String variable_name = p_resource->get_name();1969if (variable_name.is_empty()) {1970variable_name = p_resource->get_path().get_file().get_basename();1971}19721973variable_name = variable_name.to_snake_case().validate_unicode_identifier();19741975StringName class_name = p_resource->get_class();1976Ref<Script> resource_script = p_resource->get_script();19771978if (resource_script.is_valid()) {1979StringName global_resource_script_name = resource_script->get_global_name();1980if (!global_resource_script_name.is_empty()) {1981class_name = global_resource_script_name;1982}1983}19841985for (ObjectID obj_id : p_script_instance_obj_ids) {1986pending_dragged_exports.push_back(DraggedExport{ obj_id, variable_name, p_resource, class_name });1987}19881989return vformat("@export var %s: %s", variable_name, class_name);1990}19911992void ScriptTextEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) {1993Dictionary d = p_data;19941995CodeEdit *te = code_editor->get_text_editor();1996Point2i pos = (p_point == Vector2(Math::INF, Math::INF)) ? Point2i(te->get_caret_line(0), te->get_caret_column(0)) : te->get_line_column_at_pos(p_point);1997int drop_at_line = pos.y;1998int drop_at_column = pos.x;1999int selection_index = te->get_selection_at_line_column(drop_at_line, drop_at_column);20002001bool is_empty_line = false;2002if (selection_index >= 0) {2003// Dropped on a selection, it will be replaced.2004drop_at_line = te->get_selection_from_line(selection_index);2005drop_at_column = te->get_selection_from_column(selection_index);2006is_empty_line = drop_at_column <= te->get_first_non_whitespace_column(drop_at_line) && te->get_selection_to_column(selection_index) == te->get_line(te->get_selection_to_line(selection_index)).length();2007}20082009Node *scene_root = get_tree()->get_edited_scene_root();20102011const bool member_drop_modifier_pressed = Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL);2012const bool export_drop_modifier_pressed = Input::get_singleton()->is_key_pressed(Key::ALT);20132014const bool allow_uid = Input::get_singleton()->is_key_pressed(Key::SHIFT) != bool(EDITOR_GET("text_editor/behavior/files/drop_preload_resources_as_uid"));2015const String &line = te->get_line(drop_at_line);20162017if (selection_index < 0) {2018is_empty_line = line.is_empty() || te->get_first_non_whitespace_column(drop_at_line) == line.length();2019}20202021String text_to_drop;2022bool add_new_line = false;20232024const String type = d.get("type", "");2025if (type == "resource") {2026Ref<Resource> resource = d["resource"];2027if (resource.is_null()) {2028return;2029}20302031const String &path = resource->get_path();2032if (path.is_empty() || path.ends_with("::")) {2033String warning = TTR("The resource does not have a valid path because it has not been saved.\nPlease save the scene or resource that contains this resource and try again.");2034EditorToaster::get_singleton()->popup_str(warning, EditorToaster::SEVERITY_ERROR);2035return;2036}20372038if (member_drop_modifier_pressed) {2039if (resource->is_built_in()) {2040String warning = TTR("Preloading internal resources is not supported.");2041EditorToaster::get_singleton()->popup_str(warning, EditorToaster::SEVERITY_ERROR);2042} else {2043text_to_drop = _get_dropped_resource_as_member(resource, is_empty_line, allow_uid);2044}2045} else if (export_drop_modifier_pressed) {2046Vector<ObjectID> obj_ids = _get_objects_for_export_assignment();2047text_to_drop = _get_dropped_resource_as_exported_member(resource, obj_ids);20482049} else {2050text_to_drop = _quote_drop_data(path);2051}20522053if (is_empty_line) {2054text_to_drop += "\n";2055}2056}20572058if (type == "files" || type == "files_and_dirs") {2059const PackedStringArray files = d["files"];2060PackedStringArray parts;20612062for (const String &path : files) {2063if ((member_drop_modifier_pressed || export_drop_modifier_pressed) && ResourceLoader::exists(path)) {2064Ref<Resource> resource = ResourceLoader::load(path);2065if (resource.is_null()) {2066// Resource exists, but failed to load. We need only path and name, so we can use a dummy Resource instead.2067resource.instantiate();2068resource->set_path_cache(path);2069}20702071if (member_drop_modifier_pressed) {2072parts.append(_get_dropped_resource_as_member(resource, is_empty_line, allow_uid));2073} else if (export_drop_modifier_pressed) {2074Vector<ObjectID> obj_ids = _get_objects_for_export_assignment();2075parts.append(_get_dropped_resource_as_exported_member(resource, obj_ids));2076}2077} else {2078parts.append(_quote_drop_data(path));2079}2080}2081String join_string;2082if (is_empty_line) {2083int indent_level = te->get_indent_level(drop_at_line);2084if (te->is_indent_using_spaces()) {2085join_string = "\n" + String(" ").repeat(indent_level);2086} else {2087join_string = "\n" + String("\t").repeat(indent_level / te->get_tab_size());2088}2089} else {2090join_string = ", ";2091}2092text_to_drop = join_string.join(parts);2093if (is_empty_line) {2094text_to_drop += join_string;2095}2096}20972098if (type == "nodes") {2099if (!scene_root) {2100EditorNode::get_singleton()->show_warning(TTR("Can't drop nodes without an open scene."));2101return;2102}21032104Ref<Script> script = edited_res;2105if (!ClassDB::is_parent_class(script->get_instance_base_type(), "Node")) {2106EditorToaster::get_singleton()->popup_str(vformat(TTR("Can't drop nodes because script '%s' does not inherit Node."), get_name()), EditorToaster::SEVERITY_WARNING);2107return;2108}21092110Node *sn = _find_script_node(scene_root, script);2111if (!sn) {2112sn = scene_root;2113}21142115Array nodes = d["nodes"];21162117if (member_drop_modifier_pressed) {2118const bool use_type = EDITOR_GET("text_editor/completion/add_type_hints");2119add_new_line = !is_empty_line && drop_at_column != 0;21202121for (int i = 0; i < nodes.size(); i++) {2122NodePath np = nodes[i];2123Node *node = get_node(np);2124if (!node) {2125continue;2126}21272128bool is_unique = node->is_unique_name_in_owner() && (node->get_owner() == sn || node->get_owner() == sn->get_owner());2129String path = is_unique ? String(node->get_name()) : String(sn->get_path_to(node));2130for (const String &segment : path.split("/")) {2131if (!segment.is_valid_unicode_identifier()) {2132path = _quote_drop_data(path);2133break;2134}2135}21362137String variable_name = String(node->get_name()).to_snake_case().validate_unicode_identifier();2138if (use_type) {2139StringName custom_class_name;2140Ref<Script> node_script = node->get_script();2141while (node_script.is_valid() && custom_class_name.is_empty()) {2142custom_class_name = node_script->get_global_name();2143node_script = node_script->get_base_script();2144}2145const StringName class_name = custom_class_name.is_empty() ? node->get_class_name() : custom_class_name;2146text_to_drop += vformat("@onready var %s: %s = %c%s", variable_name, class_name, is_unique ? '%' : '$', path);2147} else {2148text_to_drop += vformat("@onready var %s = %c%s", variable_name, is_unique ? '%' : '$', path);2149}2150if (i < nodes.size() - 1) {2151text_to_drop += "\n";2152}2153}21542155if (is_empty_line || drop_at_column == 0) {2156text_to_drop += "\n";2157}2158} else if (export_drop_modifier_pressed) {2159Vector<ObjectID> obj_ids = _get_objects_for_export_assignment();21602161for (int i = 0; i < nodes.size(); i++) {2162NodePath np = nodes[i];2163Node *node = get_node(np);2164if (!node) {2165continue;2166}21672168String variable_name = String(node->get_name()).to_snake_case().validate_unicode_identifier();2169StringName class_name = node->get_class_name();2170Ref<Script> node_script = node->get_script();2171if (node_script.is_valid()) {2172StringName global_node_script_name = node_script->get_global_name();2173if (!global_node_script_name.is_empty()) {2174class_name = global_node_script_name;2175}2176}21772178text_to_drop += vformat("@export var %s: %s\n", variable_name, class_name);2179for (ObjectID obj_id : obj_ids) {2180pending_dragged_exports.push_back(DraggedExport{ obj_id, variable_name, node, class_name });2181}2182}2183} else {2184for (int i = 0; i < nodes.size(); i++) {2185if (i > 0) {2186text_to_drop += ", ";2187}21882189NodePath np = nodes[i];2190Node *node = get_node(np);2191if (!node) {2192continue;2193}21942195bool is_unique = node->is_unique_name_in_owner() && (node->get_owner() == sn || node->get_owner() == sn->get_owner());2196String path = is_unique ? String(node->get_name()) : String(sn->get_path_to(node));2197for (const String &segment : path.split("/")) {2198if (!segment.is_valid_ascii_identifier()) {2199path = _quote_drop_data(path);2200break;2201}2202}2203text_to_drop += (is_unique ? "%" : "$") + path;2204}2205}2206}22072208if (type == "obj_property") {2209bool add_literal = EDITOR_GET("text_editor/completion/add_node_path_literals");2210text_to_drop = add_literal ? "^" : "";2211// It is unclear whether properties may contain single or double quotes.2212// Assume here that double-quotes may not exist. We are escaping single-quotes if necessary.2213text_to_drop += _quote_drop_data(String(d["property"]));2214}22152216if (text_to_drop.is_empty()) {2217return;2218}22192220// Remove drag caret before any actions so it is not included in undo.2221te->remove_drag_caret();2222te->begin_complex_operation();2223if (selection_index >= 0) {2224te->delete_selection(selection_index);2225}2226te->remove_secondary_carets();2227te->deselect();2228te->set_caret_line(drop_at_line);2229if (add_new_line) {2230te->set_caret_column(te->get_line(drop_at_line).length());2231text_to_drop = "\n" + text_to_drop;2232} else {2233te->set_caret_column(drop_at_column);2234}2235te->insert_text_at_caret(text_to_drop);2236te->end_complex_operation();2237te->grab_focus();2238}22392240Vector<ObjectID> ScriptTextEditor::_get_objects_for_export_assignment() const {2241Vector<ObjectID> objects;2242Node *scene_root = get_tree()->get_edited_scene_root();2243Ref<Script> script = edited_res;2244bool assign_export_variables = scene_root && ClassDB::is_parent_class(script->get_instance_base_type(), "Node");22452246if (!assign_export_variables) {2247return objects;2248}22492250EditorInspector *inspector = EditorInterface::get_singleton()->get_inspector();2251if (inspector) {2252Object *edited_object = inspector->get_edited_object();2253Node *node_edit = Object::cast_to<Node>(edited_object);2254MultiNodeEdit *multi_node_edit = Object::cast_to<MultiNodeEdit>(edited_object);22552256if (node_edit != nullptr) {2257if (node_edit->get_script() == script) {2258objects.push_back(node_edit->get_instance_id());2259}2260} else if (multi_node_edit != nullptr) {2261Node *es = EditorNode::get_singleton()->get_edited_scene();2262for (int i = 0; i < multi_node_edit->get_node_count(); i++) {2263NodePath np = multi_node_edit->get_node(i);2264Node *node = es->get_node(np);2265if (node->get_script() == script) {2266objects.push_back(node->get_instance_id());2267}2268}2269}2270}22712272// In case there is no current editor selection/editor selection does not contain this script,2273// it often still makes sense to try to assign the export variable,2274// so we default to the first node with the script we find in the scene.2275if (objects.is_empty()) {2276Node *sn = _find_script_node(scene_root, script);2277if (sn) {2278objects.push_back(sn->get_instance_id());2279}2280}22812282return objects;2283}22842285void ScriptTextEditor::_assign_dragged_export_variables() {2286ERR_FAIL_COND(pending_dragged_exports.is_empty());22872288bool export_variable_set = false;22892290for (int i = pending_dragged_exports.size() - 1; i >= 0; i--) {2291const DraggedExport &dragged_export = pending_dragged_exports[i];2292Object *obj = ObjectDB::get_instance(dragged_export.obj_id);2293if (!obj) {2294WARN_PRINT("Object not found, can't assign export variable.");2295pending_dragged_exports.remove_at(i);2296continue;2297}22982299ScriptInstance *si = obj->get_script_instance();2300if (!si) {2301WARN_PRINT("Script on " + obj->to_string() + " does not exist anymore, can't assign export variable.");2302pending_dragged_exports.remove_at(i);2303continue;2304}23052306bool script_has_errors = false;2307String scr_path = si->get_script()->get_path();23082309for (const ScriptLanguage::ScriptError &error : errors) {2310if (error.path == scr_path) {2311script_has_errors = true;2312break;2313}2314}23152316if (!script_has_errors) {2317bool success = false;2318List<PropertyInfo> properties;2319si->get_property_list(&properties);2320for (const PropertyInfo &pi : properties) {2321if (pi.name == dragged_export.variable_name && pi.hint_string == dragged_export.class_name) {2322success = si->set(dragged_export.variable_name, dragged_export.value);2323break;2324}2325}23262327if (success) {2328export_variable_set = true;2329}2330pending_dragged_exports.remove_at(i);2331}2332}23332334if (export_variable_set) {2335EditorInterface::get_singleton()->mark_scene_as_unsaved();2336}2337}23382339void ScriptTextEditor::_text_edit_gui_input(const Ref<InputEvent> &p_ev) {2340Ref<InputEventMouseButton> mb = p_ev;2341Ref<InputEventKey> k = p_ev;2342Point2 local_pos;2343bool create_menu = false;23442345CodeEdit *tx = code_editor->get_text_editor();2346if (mb.is_valid() && mb->get_button_index() == MouseButton::RIGHT && mb->is_pressed()) {2347local_pos = mb->get_global_position() - tx->get_global_position();2348create_menu = true;2349} else if (k.is_valid() && k->is_action("ui_menu", true)) {2350tx->adjust_viewport_to_caret(0);2351local_pos = tx->get_caret_draw_pos(0);2352create_menu = true;2353}23542355if (create_menu) {2356tx->apply_ime();23572358Point2i pos = tx->get_line_column_at_pos(local_pos);2359int mouse_line = pos.y;2360int mouse_column = pos.x;23612362tx->set_move_caret_on_right_click_enabled(EDITOR_GET("text_editor/behavior/navigation/move_caret_on_right_click"));2363int selection_clicked = -1;2364if (tx->is_move_caret_on_right_click_enabled()) {2365selection_clicked = tx->get_selection_at_line_column(mouse_line, mouse_column, true);2366if (selection_clicked < 0) {2367tx->deselect();2368tx->remove_secondary_carets();2369selection_clicked = 0;2370tx->set_caret_line(mouse_line, false, false, -1);2371tx->set_caret_column(mouse_column);2372}2373}23742375String word_at_pos = tx->get_lookup_word(mouse_line, mouse_column);2376if (word_at_pos.is_empty()) {2377word_at_pos = tx->get_word_under_caret(selection_clicked);2378}2379if (word_at_pos.is_empty()) {2380word_at_pos = tx->get_selected_text(selection_clicked);2381}23822383bool has_color = (word_at_pos == "Color");2384bool foldable = tx->can_fold_line(mouse_line) || tx->is_line_folded(mouse_line);2385bool open_docs = false;2386bool goto_definition = false;23872388if (ScriptServer::is_global_class(word_at_pos) || word_at_pos.is_resource_file()) {2389open_docs = true;2390} else {2391Ref<Script> script = edited_res;2392Node *base = get_tree()->get_edited_scene_root();2393if (base) {2394base = _find_node_for_script(base, base, script);2395}2396ScriptLanguage::LookupResult result;2397if (script->get_language()->lookup_code(tx->get_text_for_symbol_lookup(), word_at_pos, script->get_path(), base, result) == OK) {2398open_docs = true;2399}2400}24012402if (has_color) {2403String line = tx->get_line(mouse_line);2404color_position.x = mouse_line;24052406int begin = -1;2407int end = -1;2408enum EXPRESSION_PATTERNS {2409NOT_PARSED,2410RGBA_PARAMETER, // Color(float,float,float) or Color(float,float,float,float)2411COLOR_NAME, // Color.COLOR_NAME2412} expression_pattern = NOT_PARSED;24132414for (int i = mouse_column; i < line.length(); i++) {2415if (line[i] == '(') {2416if (expression_pattern == NOT_PARSED) {2417begin = i;2418expression_pattern = RGBA_PARAMETER;2419} else {2420// Method call or '(' appearing twice.2421expression_pattern = NOT_PARSED;24222423break;2424}2425} else if (expression_pattern == RGBA_PARAMETER && line[i] == ')' && end < 0) {2426end = i + 1;24272428break;2429} else if (expression_pattern == NOT_PARSED && line[i] == '.') {2430begin = i;2431expression_pattern = COLOR_NAME;2432} else if (expression_pattern == COLOR_NAME && end < 0 && (line[i] == ' ' || line[i] == '\t')) {2433// Including '.' and spaces.2434continue;2435} else if (expression_pattern == COLOR_NAME && !(line[i] == '_' || ('A' <= line[i] && line[i] <= 'Z'))) {2436end = i;24372438break;2439}2440}24412442switch (expression_pattern) {2443case RGBA_PARAMETER: {2444color_args = line.substr(begin, end - begin);2445String stripped = color_args.remove_chars(" \t()");2446PackedFloat64Array color = stripped.split_floats(",");2447if (color.size() > 2) {2448float alpha = color.size() > 3 ? color[3] : 1.0f;2449color_picker->set_pick_color(Color(color[0], color[1], color[2], alpha));2450}2451} break;2452case COLOR_NAME: {2453if (end < 0) {2454end = line.length();2455}2456color_args = line.substr(begin, end - begin);2457const String color_name = color_args.remove_chars(" \t.");2458const int color_index = Color::find_named_color(color_name);2459if (0 <= color_index) {2460const Color color_constant = Color::get_named_color(color_index);2461color_picker->set_pick_color(color_constant);2462} else {2463has_color = false;2464}2465} break;2466default:2467has_color = false;2468break;2469}2470if (has_color) {2471color_panel->set_position(get_screen_position() + local_pos);2472color_position.y = begin;2473color_position.z = end;2474}2475}2476_make_context_menu(tx->has_selection(), has_color, foldable, open_docs, goto_definition, local_pos);2477}2478}24792480void ScriptTextEditor::_color_changed(const Color &p_color) {2481String new_args;2482const int decimals = 3;2483if (p_color.a == 1.0f) {2484new_args = String("(" + String::num(p_color.r, decimals) + ", " + String::num(p_color.g, decimals) + ", " + String::num(p_color.b, decimals) + ")");2485} else {2486new_args = String("(" + String::num(p_color.r, decimals) + ", " + String::num(p_color.g, decimals) + ", " + String::num(p_color.b, decimals) + ", " + String::num(p_color.a, decimals) + ")");2487}24882489String line = code_editor->get_text_editor()->get_line(color_position.x);2490String line_with_replaced_args = line.substr(0, color_position.y) + line.substr(color_position.y, color_position.z - color_position.y).replace(color_args, new_args) + line.substr(color_position.z);24912492color_args = new_args;2493code_editor->get_text_editor()->begin_complex_operation();2494code_editor->get_text_editor()->set_line(color_position.x, line_with_replaced_args);2495code_editor->get_text_editor()->end_complex_operation();2496}24972498void ScriptTextEditor::_make_context_menu(bool p_selection, bool p_color, bool p_foldable, bool p_open_docs, bool p_goto_definition, const Vector2 &p_position) {2499TextEditorBase::_make_context_menu(p_selection, p_foldable, p_position, false);2500context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_comment"), EDIT_TOGGLE_COMMENT);2501_popup_move_item(EDIT_UNINDENT, context_menu);25022503if (p_selection) {2504context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/evaluate_selection"), EDIT_EVALUATE);2505_popup_move_item(EDIT_TO_LOWERCASE, context_menu);2506context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/create_code_region"), EDIT_CREATE_CODE_REGION);2507_popup_move_item(EDIT_EVALUATE, context_menu);2508}25092510if (p_color || p_open_docs || p_goto_definition) {2511context_menu->add_separator();2512if (p_open_docs) {2513context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_symbol"), LOOKUP_SYMBOL);2514}2515if (p_color) {2516context_menu->add_item(TTRC("Pick Color"), EDIT_PICK_COLOR);2517}2518}25192520const PackedStringArray paths = { String(code_editor->get_text_editor()->get_path()) };2521EditorContextMenuPluginManager::get_singleton()->add_options_from_plugins(context_menu, EditorContextMenuPlugin::CONTEXT_SLOT_SCRIPT_EDITOR_CODE, paths);25222523_show_context_menu(p_position);2524}25252526void ScriptTextEditor::register_editor() {2527ED_SHORTCUT("script_text_editor/move_up", TTRC("Move Up"), KeyModifierMask::ALT | Key::UP);2528ED_SHORTCUT("script_text_editor/move_down", TTRC("Move Down"), KeyModifierMask::ALT | Key::DOWN);2529ED_SHORTCUT("script_text_editor/delete_line", TTRC("Delete Line"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::K);25302531// Leave these at zero, same can be accomplished with tab/shift-tab, including selection.2532// The next/previous in history shortcut in this case makes a lot more sense.25332534ED_SHORTCUT("script_text_editor/indent", TTRC("Indent"), Key::NONE);2535ED_SHORTCUT("script_text_editor/unindent", TTRC("Unindent"), KeyModifierMask::SHIFT | Key::TAB);2536ED_SHORTCUT_ARRAY("script_text_editor/toggle_comment", TTRC("Toggle Comment"), { int32_t(KeyModifierMask::CMD_OR_CTRL | Key::K), int32_t(KeyModifierMask::CMD_OR_CTRL | Key::SLASH), int32_t(KeyModifierMask::CMD_OR_CTRL | Key::KP_DIVIDE), int32_t(KeyModifierMask::CMD_OR_CTRL | Key::NUMBERSIGN) });2537ED_SHORTCUT("script_text_editor/toggle_fold_line", TTRC("Fold/Unfold Line"), KeyModifierMask::ALT | Key::F);2538ED_SHORTCUT_OVERRIDE("script_text_editor/toggle_fold_line", "macos", KeyModifierMask::CTRL | KeyModifierMask::META | Key::F);2539ED_SHORTCUT("script_text_editor/fold_all_lines", TTRC("Fold All Lines"), Key::NONE);2540ED_SHORTCUT("script_text_editor/create_code_region", TTRC("Create Code Region"), KeyModifierMask::ALT | Key::R);2541ED_SHORTCUT("script_text_editor/unfold_all_lines", TTRC("Unfold All Lines"), Key::NONE);2542ED_SHORTCUT("script_text_editor/duplicate_selection", TTRC("Duplicate Selection"), KeyModifierMask::SHIFT | KeyModifierMask::CTRL | Key::D);2543ED_SHORTCUT_OVERRIDE("script_text_editor/duplicate_selection", "macos", KeyModifierMask::SHIFT | KeyModifierMask::META | Key::C);2544ED_SHORTCUT("script_text_editor/duplicate_lines", TTRC("Duplicate Lines"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | Key::DOWN);2545ED_SHORTCUT_OVERRIDE("script_text_editor/duplicate_lines", "macos", KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::DOWN);2546ED_SHORTCUT("script_text_editor/evaluate_selection", TTRC("Evaluate Selection"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::E);2547ED_SHORTCUT("script_text_editor/toggle_word_wrap", TTRC("Toggle Word Wrap"), KeyModifierMask::ALT | Key::Z);2548ED_SHORTCUT("script_text_editor/trim_trailing_whitespace", TTRC("Trim Trailing Whitespace"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | Key::T);2549ED_SHORTCUT("script_text_editor/trim_final_newlines", TTRC("Trim Final Newlines"), Key::NONE);2550ED_SHORTCUT("script_text_editor/convert_indent_to_spaces", TTRC("Convert Indent to Spaces"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::Y);2551ED_SHORTCUT("script_text_editor/convert_indent_to_tabs", TTRC("Convert Indent to Tabs"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::I);2552ED_SHORTCUT("script_text_editor/auto_indent", TTRC("Auto Indent"), KeyModifierMask::CMD_OR_CTRL | Key::I);25532554ED_SHORTCUT_AND_COMMAND("script_text_editor/find", TTRC("Find..."), KeyModifierMask::CMD_OR_CTRL | Key::F);25552556ED_SHORTCUT("script_text_editor/find_next", TTRC("Find Next"), Key::F3);2557ED_SHORTCUT_OVERRIDE("script_text_editor/find_next", "macos", KeyModifierMask::META | Key::G);25582559ED_SHORTCUT("script_text_editor/find_previous", TTRC("Find Previous"), KeyModifierMask::SHIFT | Key::F3);2560ED_SHORTCUT_OVERRIDE("script_text_editor/find_previous", "macos", KeyModifierMask::META | KeyModifierMask::SHIFT | Key::G);25612562ED_SHORTCUT_AND_COMMAND("script_text_editor/replace", TTRC("Replace..."), KeyModifierMask::CTRL | Key::R);2563ED_SHORTCUT_OVERRIDE("script_text_editor/replace", "macos", KeyModifierMask::ALT | KeyModifierMask::META | Key::F);25642565ED_SHORTCUT("script_text_editor/replace_in_files", TTRC("Replace in Files..."), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::R);25662567ED_SHORTCUT("script_text_editor/contextual_help", TTRC("Contextual Help"), KeyModifierMask::ALT | Key::F1);2568ED_SHORTCUT_OVERRIDE("script_text_editor/contextual_help", "macos", KeyModifierMask::ALT | KeyModifierMask::SHIFT | Key::SPACE);25692570ED_SHORTCUT("script_text_editor/toggle_bookmark", TTRC("Toggle Bookmark"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | Key::B);25712572ED_SHORTCUT("script_text_editor/goto_next_bookmark", TTRC("Go to Next Bookmark"), KeyModifierMask::CMD_OR_CTRL | Key::B);2573ED_SHORTCUT_OVERRIDE("script_text_editor/goto_next_bookmark", "macos", KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | KeyModifierMask::ALT | Key::B);25742575ED_SHORTCUT("script_text_editor/goto_previous_bookmark", TTRC("Go to Previous Bookmark"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::B);2576ED_SHORTCUT("script_text_editor/remove_all_bookmarks", TTRC("Remove All Bookmarks"), Key::NONE);25772578ED_SHORTCUT("script_text_editor/goto_function", TTRC("Go to Function..."), KeyModifierMask::ALT | KeyModifierMask::CTRL | Key::F);2579ED_SHORTCUT_OVERRIDE("script_text_editor/goto_function", "macos", KeyModifierMask::CTRL | KeyModifierMask::META | Key::J);25802581ED_SHORTCUT("script_text_editor/goto_line", TTRC("Go to Line..."), KeyModifierMask::CMD_OR_CTRL | Key::G);2582ED_SHORTCUT_OVERRIDE("script_text_editor/goto_line", "macos", KeyModifierMask::CMD_OR_CTRL | Key::L);2583ED_SHORTCUT("script_text_editor/goto_symbol", TTRC("Lookup Symbol"));25842585ED_SHORTCUT("script_text_editor/toggle_breakpoint", TTRC("Toggle Breakpoint"), Key::F9);2586ED_SHORTCUT_OVERRIDE("script_text_editor/toggle_breakpoint", "macos", KeyModifierMask::META | KeyModifierMask::SHIFT | Key::B);25872588ED_SHORTCUT("script_text_editor/remove_all_breakpoints", TTRC("Remove All Breakpoints"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::F9);2589// Using Control for these shortcuts even on macOS because Command+Comma is taken for opening Editor Settings.2590ED_SHORTCUT("script_text_editor/goto_next_breakpoint", TTRC("Go to Next Breakpoint"), KeyModifierMask::CTRL | Key::PERIOD);2591ED_SHORTCUT("script_text_editor/goto_previous_breakpoint", TTRC("Go to Previous Breakpoint"), KeyModifierMask::CTRL | Key::COMMA);25922593ScriptEditor::register_create_script_editor_function(create_editor);2594}25952596void ScriptTextEditor::_enable_code_editor() {2597code_editor->connect("show_errors_panel", callable_mp(this, &ScriptTextEditor::_show_errors_panel));2598code_editor->connect("show_warnings_panel", callable_mp(this, &ScriptTextEditor::_show_warnings_panel));2599code_editor->get_text_editor()->connect("symbol_lookup", callable_mp(this, &ScriptTextEditor::_lookup_symbol));2600code_editor->get_text_editor()->connect("symbol_hovered", callable_mp(this, &ScriptTextEditor::_show_symbol_tooltip));2601code_editor->get_text_editor()->connect("symbol_validate", callable_mp(this, &ScriptTextEditor::_validate_symbol));2602code_editor->get_text_editor()->connect("gutter_added", callable_mp(this, &ScriptTextEditor::_update_gutter_indexes));2603code_editor->get_text_editor()->connect("gutter_removed", callable_mp(this, &ScriptTextEditor::_update_gutter_indexes));2604code_editor->get_text_editor()->connect("gutter_clicked", callable_mp(this, &ScriptTextEditor::_gutter_clicked));2605code_editor->get_text_editor()->connect("_fold_line_updated", callable_mp(this, &ScriptTextEditor::_update_background_color));2606_update_gutter_indexes();26072608editor_box->add_child(errors_panel);2609errors_panel->connect("meta_clicked", callable_mp(this, &ScriptTextEditor::_error_clicked));26102611add_child(color_panel);26122613color_picker = memnew(ColorPicker);2614color_picker->set_deferred_mode(true);2615color_picker->connect("color_changed", callable_mp(this, &ScriptTextEditor::_color_changed));2616color_panel->connect("about_to_popup", callable_mp(EditorNode::get_singleton(), &EditorNode::setup_color_picker).bind(color_picker));26172618color_panel->add_child(color_picker);26192620quick_open = memnew(ScriptEditorQuickOpen);2621quick_open->set_title(TTRC("Go to Function"));2622quick_open->connect("goto_line", callable_mp(this, &ScriptTextEditor::_goto_line));2623add_child(quick_open);26242625add_child(connection_info_dialog);2626}26272628ScriptTextEditor::ScriptTextEditor() {2629code_editor->set_code_complete_func(_code_complete_scripts, this);2630code_editor->get_text_editor()->set_draw_breakpoints_gutter(true);2631code_editor->get_text_editor()->set_draw_executing_lines_gutter(true);2632code_editor->get_text_editor()->connect("breakpoint_toggled", callable_mp(this, &ScriptTextEditor::_breakpoint_toggled));2633code_editor->get_text_editor()->connect("caret_changed", callable_mp(this, &ScriptTextEditor::_on_caret_moved));2634code_editor->connect("navigation_preview_ended", callable_mp(this, &ScriptTextEditor::_on_caret_moved));26352636connection_gutter = 1;2637code_editor->get_text_editor()->add_gutter(connection_gutter);2638code_editor->get_text_editor()->set_gutter_name(connection_gutter, "connection_gutter");2639code_editor->get_text_editor()->set_gutter_draw(connection_gutter, false);2640code_editor->get_text_editor()->set_gutter_overwritable(connection_gutter, true);2641code_editor->get_text_editor()->set_gutter_type(connection_gutter, TextEdit::GUTTER_TYPE_ICON);26422643errors_panel = memnew(RichTextLabel);2644errors_panel->set_custom_minimum_size(Size2(0, 100 * EDSCALE));2645errors_panel->set_h_size_flags(SIZE_EXPAND_FILL);2646errors_panel->set_meta_underline(true);2647errors_panel->set_selection_enabled(true);2648errors_panel->set_context_menu_enabled(true);2649errors_panel->set_focus_mode(FOCUS_CLICK);2650errors_panel->hide();26512652code_editor->get_text_editor()->set_symbol_lookup_on_click_enabled(true);2653code_editor->get_text_editor()->set_symbol_tooltip_on_hover_enabled(true);26542655color_panel = memnew(PopupPanel);26562657inline_color_popup = memnew(PopupPanel);2658add_child(inline_color_popup);26592660inline_color_picker = memnew(ColorPicker);2661inline_color_picker->set_mouse_filter(MOUSE_FILTER_STOP);2662inline_color_picker->set_deferred_mode(true);2663inline_color_picker->set_hex_visible(false);2664inline_color_picker->connect("color_changed", callable_mp(this, &ScriptTextEditor::_picker_color_changed));2665inline_color_popup->add_child(inline_color_picker);26662667inline_color_options = memnew(OptionButton);2668inline_color_options->set_h_size_flags(SIZE_FILL);2669inline_color_options->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS);2670inline_color_options->set_fit_to_longest_item(false);2671inline_color_options->connect("item_selected", callable_mp(this, &ScriptTextEditor::_update_color_text).unbind(1));2672inline_color_picker->get_slider_container()->add_sibling(inline_color_options);26732674connection_info_dialog = memnew(ConnectionInfoDialog);26752676update_settings();26772678SET_DRAG_FORWARDING_GCD(code_editor->get_text_editor(), ScriptTextEditor);2679}26802681ScriptTextEditor::~ScriptTextEditor() {2682if (!editor_enabled) {2683memdelete(errors_panel);2684memdelete(color_panel);2685memdelete(connection_info_dialog);2686}2687}26882689ScriptEditorBase *ScriptTextEditor::create_editor(const Ref<Resource> &p_resource) {2690if (Object::cast_to<Script>(*p_resource)) {2691return memnew(ScriptTextEditor);2692}2693return nullptr;2694}269526962697