/**************************************************************************/ /* script_text_editor.cpp */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /**************************************************************************/ /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ /* "Software"), to deal in the Software without restriction, including */ /* without limitation the rights to use, copy, modify, merge, publish, */ /* distribute, sublicense, and/or sell copies of the Software, and to */ /* permit persons to whom the Software is furnished to do so, subject to */ /* the following conditions: */ /* */ /* The above copyright notice and this permission notice shall be */ /* included in all copies or substantial portions of the Software. */ /* */ /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ #include "script_text_editor.h" #include "core/config/project_settings.h" #include "core/io/json.h" #include "core/math/expression.h" #include "core/os/keyboard.h" #include "editor/debugger/editor_debugger_node.h" #include "editor/editor_command_palette.h" #include "editor/editor_help.h" #include "editor/editor_node.h" #include "editor/editor_settings.h" #include "editor/editor_string_names.h" #include "editor/gui/editor_toaster.h" #include "editor/plugins/editor_context_menu_plugin.h" #include "editor/themes/editor_scale.h" #include "scene/gui/grid_container.h" #include "scene/gui/menu_button.h" #include "scene/gui/rich_text_label.h" #include "scene/gui/slider.h" #include "scene/gui/split_container.h" void ConnectionInfoDialog::ok_pressed() { } void ConnectionInfoDialog::popup_connections(const String &p_method, const Vector &p_nodes) { method->set_text(p_method); tree->clear(); TreeItem *root = tree->create_item(); for (int i = 0; i < p_nodes.size(); i++) { List all_connections; p_nodes[i]->get_signals_connected_to_this(&all_connections); for (const Connection &connection : all_connections) { if (connection.callable.get_method() != p_method) { continue; } TreeItem *node_item = tree->create_item(root); node_item->set_text(0, Object::cast_to(connection.signal.get_object())->get_name()); node_item->set_icon(0, EditorNode::get_singleton()->get_object_icon(connection.signal.get_object(), "Node")); node_item->set_selectable(0, false); node_item->set_editable(0, false); node_item->set_text(1, connection.signal.get_name()); Control *p = Object::cast_to(get_parent()); node_item->set_icon(1, p->get_editor_theme_icon(SNAME("Slot"))); node_item->set_selectable(1, false); node_item->set_editable(1, false); node_item->set_text(2, Object::cast_to(connection.callable.get_object())->get_name()); node_item->set_icon(2, EditorNode::get_singleton()->get_object_icon(connection.callable.get_object(), "Node")); node_item->set_selectable(2, false); node_item->set_editable(2, false); } } popup_centered(Size2(600, 300) * EDSCALE); } ConnectionInfoDialog::ConnectionInfoDialog() { set_title(TTRC("Connections to method:")); VBoxContainer *vbc = memnew(VBoxContainer); vbc->set_anchor_and_offset(SIDE_LEFT, Control::ANCHOR_BEGIN, 8 * EDSCALE); vbc->set_anchor_and_offset(SIDE_TOP, Control::ANCHOR_BEGIN, 8 * EDSCALE); vbc->set_anchor_and_offset(SIDE_RIGHT, Control::ANCHOR_END, -8 * EDSCALE); vbc->set_anchor_and_offset(SIDE_BOTTOM, Control::ANCHOR_END, -8 * EDSCALE); add_child(vbc); method = memnew(Label); method->set_focus_mode(Control::FOCUS_ACCESSIBILITY); method->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); method->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER); vbc->add_child(method); tree = memnew(Tree); tree->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); tree->set_columns(3); tree->set_hide_root(true); tree->set_column_titles_visible(true); tree->set_column_title(0, TTRC("Source")); tree->set_column_title(1, TTRC("Signal")); tree->set_column_title(2, TTRC("Target")); vbc->add_child(tree); tree->set_v_size_flags(Control::SIZE_EXPAND_FILL); tree->set_allow_rmb_select(true); } //////////////////////////////////////////////////////////////////////////////// Vector ScriptTextEditor::get_functions() { CodeEdit *te = code_editor->get_text_editor(); String text = te->get_text(); List fnc; if (script->get_language()->validate(text, script->get_path(), &fnc)) { //if valid rewrite functions to latest functions.clear(); for (const String &E : fnc) { functions.push_back(E); } } return functions; } void ScriptTextEditor::apply_code() { if (script.is_null()) { return; } script->set_source_code(code_editor->get_text_editor()->get_text()); script->update_exports(); code_editor->get_text_editor()->get_syntax_highlighter()->update_cache(); } Ref ScriptTextEditor::get_edited_resource() const { return script; } void ScriptTextEditor::set_edited_resource(const Ref &p_res) { ERR_FAIL_COND(script.is_valid()); ERR_FAIL_COND(p_res.is_null()); script = p_res; code_editor->get_text_editor()->set_text(script->get_source_code()); code_editor->get_text_editor()->clear_undo_history(); code_editor->get_text_editor()->tag_saved_version(); emit_signal(SNAME("name_changed")); code_editor->update_line_and_column(); } void ScriptTextEditor::enable_editor(Control *p_shortcut_context) { if (editor_enabled) { return; } editor_enabled = true; _enable_code_editor(); _validate_script(); if (p_shortcut_context) { for (int i = 0; i < edit_hb->get_child_count(); ++i) { Control *c = cast_to(edit_hb->get_child(i)); if (c) { c->set_shortcut_context(p_shortcut_context); } } } } void ScriptTextEditor::_load_theme_settings() { CodeEdit *text_edit = code_editor->get_text_editor(); Color updated_warning_line_color = EDITOR_GET("text_editor/theme/highlighting/warning_color"); Color updated_marked_line_color = EDITOR_GET("text_editor/theme/highlighting/mark_color"); Color updated_safe_line_number_color = EDITOR_GET("text_editor/theme/highlighting/safe_line_number_color"); Color updated_folded_code_region_color = EDITOR_GET("text_editor/theme/highlighting/folded_code_region_color"); bool warning_line_color_updated = updated_warning_line_color != warning_line_color; bool marked_line_color_updated = updated_marked_line_color != marked_line_color; bool safe_line_number_color_updated = updated_safe_line_number_color != safe_line_number_color; bool folded_code_region_color_updated = updated_folded_code_region_color != folded_code_region_color; if (safe_line_number_color_updated || warning_line_color_updated || marked_line_color_updated || folded_code_region_color_updated) { safe_line_number_color = updated_safe_line_number_color; for (int i = 0; i < text_edit->get_line_count(); i++) { if (warning_line_color_updated && text_edit->get_line_background_color(i) == warning_line_color) { text_edit->set_line_background_color(i, updated_warning_line_color); } if (marked_line_color_updated && text_edit->get_line_background_color(i) == marked_line_color) { text_edit->set_line_background_color(i, updated_marked_line_color); } if (safe_line_number_color_updated && text_edit->get_line_gutter_item_color(i, line_number_gutter) != default_line_number_color) { text_edit->set_line_gutter_item_color(i, line_number_gutter, safe_line_number_color); } if (folded_code_region_color_updated && text_edit->get_line_background_color(i) == folded_code_region_color) { text_edit->set_line_background_color(i, updated_folded_code_region_color); } } warning_line_color = updated_warning_line_color; marked_line_color = updated_marked_line_color; folded_code_region_color = updated_folded_code_region_color; } theme_loaded = true; if (script.is_valid()) { _set_theme_for_script(); } } void ScriptTextEditor::_set_theme_for_script() { if (!theme_loaded) { return; } CodeEdit *text_edit = code_editor->get_text_editor(); text_edit->get_syntax_highlighter()->update_cache(); Vector strings = script->get_language()->get_string_delimiters(); text_edit->clear_string_delimiters(); for (const String &string : strings) { String beg = string.get_slicec(' ', 0); String end = string.get_slice_count(" ") > 1 ? string.get_slicec(' ', 1) : String(); if (!text_edit->has_string_delimiter(beg)) { text_edit->add_string_delimiter(beg, end, end.is_empty()); } if (!end.is_empty() && !text_edit->has_auto_brace_completion_open_key(beg)) { text_edit->add_auto_brace_completion_pair(beg, end); } } text_edit->clear_comment_delimiters(); for (const String &comment : script->get_language()->get_comment_delimiters()) { String beg = comment.get_slicec(' ', 0); String end = comment.get_slice_count(" ") > 1 ? comment.get_slicec(' ', 1) : String(); text_edit->add_comment_delimiter(beg, end, end.is_empty()); if (!end.is_empty() && !text_edit->has_auto_brace_completion_open_key(beg)) { text_edit->add_auto_brace_completion_pair(beg, end); } } for (const String &doc_comment : script->get_language()->get_doc_comment_delimiters()) { String beg = doc_comment.get_slicec(' ', 0); String end = doc_comment.get_slice_count(" ") > 1 ? doc_comment.get_slicec(' ', 1) : String(); text_edit->add_comment_delimiter(beg, end, end.is_empty()); if (!end.is_empty() && !text_edit->has_auto_brace_completion_open_key(beg)) { text_edit->add_auto_brace_completion_pair(beg, end); } } } void ScriptTextEditor::_show_errors_panel(bool p_show) { errors_panel->set_visible(p_show); } void ScriptTextEditor::_show_warnings_panel(bool p_show) { warnings_panel->set_visible(p_show); } void ScriptTextEditor::_warning_clicked(const Variant &p_line) { if (p_line.get_type() == Variant::INT) { goto_line_centered(p_line.operator int64_t()); } else if (p_line.get_type() == Variant::DICTIONARY) { Dictionary meta = p_line.operator Dictionary(); const int line = meta["line"].operator int64_t() - 1; const String code = meta["code"].operator String(); const String quote_style = EDITOR_GET("text_editor/completion/use_single_quotes") ? "'" : "\""; CodeEdit *text_editor = code_editor->get_text_editor(); String prev_line = line > 0 ? text_editor->get_line(line - 1) : ""; if (prev_line.contains("@warning_ignore")) { const int closing_bracket_idx = prev_line.find_char(')'); const String text_to_insert = ", " + code.quote(quote_style); text_editor->insert_text(text_to_insert, line - 1, closing_bracket_idx); } else { const int indent = text_editor->get_indent_level(line) / text_editor->get_indent_size(); String annotation_indent; if (!text_editor->is_indent_using_spaces()) { annotation_indent = String("\t").repeat(indent); } else { annotation_indent = String(" ").repeat(text_editor->get_indent_size() * indent); } text_editor->insert_line_at(line, annotation_indent + "@warning_ignore(" + code.quote(quote_style) + ")"); } _validate_script(); } } void ScriptTextEditor::_error_clicked(const Variant &p_line) { if (p_line.get_type() == Variant::INT) { goto_line_centered(p_line.operator int64_t()); } else if (p_line.get_type() == Variant::DICTIONARY) { Dictionary meta = p_line.operator Dictionary(); const String path = meta["path"].operator String(); const int line = meta["line"].operator int64_t(); const int column = meta["column"].operator int64_t(); if (path.is_empty()) { goto_line_centered(line, column); } else { Ref scr = ResourceLoader::load(path); if (scr.is_null()) { EditorNode::get_singleton()->show_warning(TTR("Could not load file at:") + "\n\n" + path, TTR("Error!")); } else { int corrected_column = column; const String line_text = code_editor->get_text_editor()->get_line(line); const int indent_size = code_editor->get_text_editor()->get_indent_size(); if (indent_size > 1) { const int tab_count = line_text.length() - line_text.lstrip("\t").length(); corrected_column -= tab_count * (indent_size - 1); } ScriptEditor::get_singleton()->edit(scr, line, corrected_column); } } } } void ScriptTextEditor::reload_text() { ERR_FAIL_COND(script.is_null()); CodeEdit *te = code_editor->get_text_editor(); int column = te->get_caret_column(); int row = te->get_caret_line(); int h = te->get_h_scroll(); int v = te->get_v_scroll(); te->set_text(script->get_source_code()); te->set_caret_line(row); te->set_caret_column(column); te->set_h_scroll(h); te->set_v_scroll(v); te->tag_saved_version(); code_editor->update_line_and_column(); if (editor_enabled) { _validate_script(); } } void ScriptTextEditor::add_callback(const String &p_function, const PackedStringArray &p_args) { ScriptLanguage *language = script->get_language(); if (!language->can_make_function()) { return; } code_editor->get_text_editor()->begin_complex_operation(); code_editor->get_text_editor()->remove_secondary_carets(); code_editor->get_text_editor()->deselect(); String code = code_editor->get_text_editor()->get_text(); int pos = language->find_function(p_function, code); if (pos == -1) { // Function does not exist, create it at the end of the file. int last_line = code_editor->get_text_editor()->get_line_count() - 1; String func = language->make_function("", p_function, p_args); code_editor->get_text_editor()->insert_text("\n\n" + func, last_line, code_editor->get_text_editor()->get_line(last_line).length()); pos = last_line + 3; } // Put caret on the line after the function, after the indent. int indent_column = 1; if (EDITOR_GET("text_editor/behavior/indent/type")) { indent_column = EDITOR_GET("text_editor/behavior/indent/size"); } code_editor->get_text_editor()->set_caret_line(pos, true, true, -1); code_editor->get_text_editor()->set_caret_column(indent_column); code_editor->get_text_editor()->end_complex_operation(); } bool ScriptTextEditor::show_members_overview() { return true; } bool ScriptTextEditor::_is_valid_color_info(const Dictionary &p_info) { if (p_info.get_valid("color").get_type() != Variant::COLOR) { return false; } if (!p_info.get_valid("color_end").is_num() || !p_info.get_valid("color_mode").is_num()) { return false; } return true; } Array ScriptTextEditor::_inline_object_parse(const String &p_text, int p_line) { Array result; int i_end_previous = 0; int i_start = p_text.find("Color"); while (i_start != -1) { // Ignore words that just have "Color" in them. if (i_start == 0 || !("_" + p_text.substr(i_start - 1, 1)).is_valid_ascii_identifier()) { int i_par_start = p_text.find_char('(', i_start + 5); if (i_par_start != -1) { int i_par_end = p_text.find_char(')', i_start + 5); if (i_par_end != -1) { Dictionary color_info; color_info["line"] = p_line; color_info["column"] = i_start; color_info["width_ratio"] = 1.0; color_info["color_end"] = i_par_end; String fn_name = p_text.substr(i_start + 5, i_par_start - i_start - 5); String s_params = p_text.substr(i_par_start + 1, i_par_end - i_par_start - 1); bool has_added_color = false; if (fn_name.is_empty()) { String stripped = s_params.strip_edges(true, true); // String constructor. if (stripped.length() > 0 && (stripped[0] == '\"')) { String color_string = stripped.substr(1, stripped.length() - 2); color_info["color"] = Color(color_string); color_info["color_mode"] = MODE_STRING; has_added_color = true; } // Hex constructor. else if (stripped.length() == 10 && stripped.substr(0, 2) == "0x") { color_info["color"] = Color(stripped.substr(2, stripped.length() - 2)); color_info["color_mode"] = MODE_HEX; has_added_color = true; } // Empty Color() constructor. else if (stripped.is_empty()) { color_info["color"] = Color(); color_info["color_mode"] = MODE_RGB; has_added_color = true; } } // Float & int parameters. if (!has_added_color && s_params.size() > 0) { PackedFloat64Array params = s_params.split_floats(",", false); if (params.size() == 3) { params.resize(4); params.set(3, 1.0); } if (params.size() == 4) { has_added_color = true; if (fn_name == ".from_ok_hsl") { color_info["color"] = Color::from_ok_hsl(params[0], params[1], params[2], params[3]); color_info["color_mode"] = MODE_OKHSL; } else if (fn_name == ".from_hsv") { color_info["color"] = Color::from_hsv(params[0], params[1], params[2], params[3]); color_info["color_mode"] = MODE_HSV; } else if (fn_name == ".from_rgba8") { color_info["color"] = Color::from_rgba8(int(params[0]), int(params[1]), int(params[2]), int(params[3])); color_info["color_mode"] = MODE_RGB8; } else if (fn_name.is_empty()) { color_info["color"] = Color(params[0], params[1], params[2], params[3]); color_info["color_mode"] = MODE_RGB; } else { has_added_color = false; } } } if (has_added_color) { result.push_back(color_info); i_end_previous = i_par_end + 1; } } } } i_end_previous = MAX(i_end_previous, i_start); i_start = p_text.find("Color", i_start + 1); } return result; } void ScriptTextEditor::_inline_object_draw(const Dictionary &p_info, const Rect2 &p_rect) { if (_is_valid_color_info(p_info)) { Rect2 col_rect = p_rect.grow(-4); if (color_alpha_texture.is_null()) { color_alpha_texture = inline_color_picker->get_theme_icon("sample_bg", "ColorPicker"); } code_editor->get_text_editor()->draw_texture_rect(color_alpha_texture, col_rect, false); code_editor->get_text_editor()->draw_rect(col_rect, Color(p_info["color"])); code_editor->get_text_editor()->draw_rect(col_rect, Color(1, 1, 1), false, 1); } } void ScriptTextEditor::_inline_object_handle_click(const Dictionary &p_info, const Rect2 &p_rect) { if (_is_valid_color_info(p_info)) { inline_color_picker->set_pick_color(p_info["color"]); inline_color_line = p_info["line"]; inline_color_start = p_info["column"]; inline_color_end = p_info["color_end"]; // Reset tooltip hover timer. code_editor->get_text_editor()->set_symbol_tooltip_on_hover_enabled(false); code_editor->get_text_editor()->set_symbol_tooltip_on_hover_enabled(true); _update_color_constructor_options(); inline_color_options->select(p_info["color_mode"]); EditorNode::get_singleton()->setup_color_picker(inline_color_picker); // Move popup above the line if it's too low. float_t view_h = get_viewport_rect().size.y; float_t pop_h = inline_color_popup->get_contents_minimum_size().y; float_t pop_y = p_rect.get_end().y; float_t pop_x = p_rect.position.x; if (pop_y + pop_h > view_h) { pop_y = p_rect.position.y - pop_h; } // Move popup to the right if it's too high. if (pop_y < 0) { pop_x = p_rect.get_end().x; } inline_color_popup->popup(Rect2(pop_x, pop_y, 0, 0)); } } String ScriptTextEditor::_picker_color_stringify(const Color &p_color, COLOR_MODE p_mode) { String result; String fname; Vector str_params; switch (p_mode) { case ScriptTextEditor::MODE_STRING: { str_params.push_back("\"" + p_color.to_html() + "\""); } break; case ScriptTextEditor::MODE_HEX: { str_params.push_back("0x" + p_color.to_html()); } break; case ScriptTextEditor::MODE_RGB: { str_params = { String::num(p_color.r, 3), String::num(p_color.g, 3), String::num(p_color.b, 3), String::num(p_color.a, 3) }; } break; case ScriptTextEditor::MODE_HSV: { str_params = { String::num(p_color.get_h(), 3), String::num(p_color.get_s(), 3), String::num(p_color.get_v(), 3), String::num(p_color.a, 3) }; fname = ".from_hsv"; } break; case ScriptTextEditor::MODE_OKHSL: { str_params = { String::num(p_color.get_ok_hsl_h(), 3), String::num(p_color.get_ok_hsl_s(), 3), String::num(p_color.get_ok_hsl_l(), 3), String::num(p_color.a, 3) }; fname = ".from_ok_hsl"; } break; case ScriptTextEditor::MODE_RGB8: { str_params = { itos(p_color.get_r8()), itos(p_color.get_g8()), itos(p_color.get_b8()), itos(p_color.get_a8()) }; fname = ".from_rgba8"; } break; default: { } break; } result = "Color" + fname + "(" + String(", ").join(str_params) + ")"; return result; } void ScriptTextEditor::_picker_color_changed(const Color &p_color) { _update_color_constructor_options(); _update_color_text(); } void ScriptTextEditor::_update_color_constructor_options() { int item_count = inline_color_options->get_item_count(); // Update or add each constructor as an option. for (int i = 0; i < MODE_MAX; i++) { String option_text = _picker_color_stringify(inline_color_picker->get_pick_color(), (COLOR_MODE)i); if (i >= item_count) { inline_color_options->add_item(option_text); } else { inline_color_options->set_item_text(i, option_text); } } } void ScriptTextEditor::_update_background_color() { // Clear background lines. CodeEdit *te = code_editor->get_text_editor(); for (int i = 0; i < te->get_line_count(); i++) { bool is_folded_code_region = te->is_line_code_region_start(i) && te->is_line_folded(i); te->set_line_background_color(i, is_folded_code_region ? folded_code_region_color : Color(0, 0, 0, 0)); } // Set the warning background. if (warning_line_color.a != 0.0) { for (const ScriptLanguage::Warning &warning : warnings) { int warning_start_line = CLAMP(warning.start_line - 1, 0, te->get_line_count() - 1); int warning_end_line = CLAMP(warning.end_line - 1, 0, te->get_line_count() - 1); int folded_line_header = te->get_folded_line_header(warning_start_line); // If the warning highlight is too long, only highlight the start line. const int warning_max_lines = 20; te->set_line_background_color(folded_line_header, warning_line_color); if (warning_end_line - warning_start_line < warning_max_lines) { for (int i = warning_start_line + 1; i <= warning_end_line; i++) { te->set_line_background_color(i, warning_line_color); } } } } // Set the error background. if (marked_line_color.a != 0.0) { for (const ScriptLanguage::ScriptError &error : errors) { int error_line = CLAMP(error.line - 1, 0, te->get_line_count() - 1); int folded_line_header = te->get_folded_line_header(error_line); te->set_line_background_color(folded_line_header, marked_line_color); } } } void ScriptTextEditor::_update_color_text() { if (inline_color_line < 0) { return; } String result = inline_color_options->get_item_text(inline_color_options->get_selected_id()); code_editor->get_text_editor()->begin_complex_operation(); code_editor->get_text_editor()->remove_text(inline_color_line, inline_color_start, inline_color_line, inline_color_end + 1); inline_color_end = inline_color_start + result.size() - 2; code_editor->get_text_editor()->insert_text(result, inline_color_line, inline_color_start); code_editor->get_text_editor()->end_complex_operation(); } void ScriptTextEditor::update_settings() { code_editor->get_text_editor()->set_gutter_draw(connection_gutter, EDITOR_GET("text_editor/appearance/gutters/show_info_gutter")); if (EDITOR_GET("text_editor/appearance/enable_inline_color_picker")) { code_editor->get_text_editor()->set_inline_object_handlers( callable_mp(this, &ScriptTextEditor::_inline_object_parse), callable_mp(this, &ScriptTextEditor::_inline_object_draw), callable_mp(this, &ScriptTextEditor::_inline_object_handle_click)); } else { code_editor->get_text_editor()->set_inline_object_handlers(Callable(), Callable(), Callable()); } code_editor->update_editor_settings(); } bool ScriptTextEditor::is_unsaved() { const bool unsaved = code_editor->get_text_editor()->get_version() != code_editor->get_text_editor()->get_saved_version() || script->get_path().is_empty(); // In memory. return unsaved; } Variant ScriptTextEditor::get_edit_state() { return code_editor->get_edit_state(); } void ScriptTextEditor::set_edit_state(const Variant &p_state) { code_editor->set_edit_state(p_state); Dictionary state = p_state; if (state.has("syntax_highlighter")) { int idx = highlighter_menu->get_item_idx_from_text(state["syntax_highlighter"]); if (idx >= 0) { _change_syntax_highlighter(idx); } } if (editor_enabled) { #ifndef ANDROID_ENABLED ensure_focus(); #endif } } Variant ScriptTextEditor::get_navigation_state() { return code_editor->get_navigation_state(); } Variant ScriptTextEditor::get_previous_state() { return code_editor->get_previous_state(); } void ScriptTextEditor::store_previous_state() { return code_editor->store_previous_state(); } void ScriptTextEditor::_convert_case(CodeTextEditor::CaseStyle p_case) { code_editor->convert_case(p_case); } void ScriptTextEditor::trim_trailing_whitespace() { code_editor->trim_trailing_whitespace(); } void ScriptTextEditor::trim_final_newlines() { code_editor->trim_final_newlines(); } void ScriptTextEditor::insert_final_newline() { code_editor->insert_final_newline(); } void ScriptTextEditor::convert_indent() { code_editor->get_text_editor()->convert_indent(); } void ScriptTextEditor::tag_saved_version() { code_editor->get_text_editor()->tag_saved_version(); edited_file_data.last_modified_time = FileAccess::get_modified_time(edited_file_data.path); } void ScriptTextEditor::goto_line(int p_line, int p_column) { code_editor->goto_line(p_line, p_column); } void ScriptTextEditor::goto_line_selection(int p_line, int p_begin, int p_end) { code_editor->goto_line_selection(p_line, p_begin, p_end); } void ScriptTextEditor::goto_line_centered(int p_line, int p_column) { code_editor->goto_line_centered(p_line, p_column); } void ScriptTextEditor::set_executing_line(int p_line) { code_editor->set_executing_line(p_line); } void ScriptTextEditor::clear_executing_line() { code_editor->clear_executing_line(); } void ScriptTextEditor::ensure_focus() { code_editor->get_text_editor()->grab_focus(); } String ScriptTextEditor::get_name() { String name; name = script->get_path().get_file(); if (name.is_empty()) { // This appears for newly created built-in scripts before saving the scene. name = TTR("[unsaved]"); } else if (script->is_built_in()) { const String &script_name = script->get_name(); if (!script_name.is_empty()) { // If the built-in script has a custom resource name defined, // display the built-in script name as follows: `ResourceName (scene_file.tscn)` name = vformat("%s (%s)", script_name, name.get_slice("::", 0)); } } if (is_unsaved()) { name += "(*)"; } return name; } Ref ScriptTextEditor::get_theme_icon() { if (get_parent_control()) { String icon_name = script->get_class(); if (script->is_built_in()) { icon_name += "Internal"; } if (get_parent_control()->has_theme_icon(icon_name, EditorStringName(EditorIcons))) { return get_parent_control()->get_editor_theme_icon(icon_name); } else if (get_parent_control()->has_theme_icon(script->get_class(), EditorStringName(EditorIcons))) { return get_parent_control()->get_editor_theme_icon(script->get_class()); } } return Ref(); } void ScriptTextEditor::_validate_script() { CodeEdit *te = code_editor->get_text_editor(); String text = te->get_text(); List fnc; warnings.clear(); errors.clear(); depended_errors.clear(); safe_lines.clear(); if (!script->get_language()->validate(text, script->get_path(), &fnc, &errors, &warnings, &safe_lines)) { List::Element *E = errors.front(); while (E) { List::Element *next_E = E->next(); if ((E->get().path.is_empty() && !script->get_path().is_empty()) || E->get().path != script->get_path()) { depended_errors[E->get().path].push_back(E->get()); E->erase(); } E = next_E; } if (errors.size() > 0) { const int line = errors.front()->get().line; const int column = errors.front()->get().column; const String message = errors.front()->get().message.replace("[", "[lb]"); const String error_text = vformat(TTR("Error at ([hint=Line %d, column %d]%d, %d[/hint]):"), line, column, line, column) + " " + message; code_editor->set_error(error_text); code_editor->set_error_pos(line - 1, column - 1); } script_is_valid = false; } else { code_editor->set_error(""); if (!script->is_tool()) { script->set_source_code(text); script->update_exports(); te->get_syntax_highlighter()->update_cache(); } functions.clear(); for (const String &E : fnc) { functions.push_back(E); } script_is_valid = true; } _update_connected_methods(); _update_warnings(); _update_errors(); _update_background_color(); emit_signal(SNAME("name_changed")); emit_signal(SNAME("edited_script_changed")); } void ScriptTextEditor::_update_warnings() { int warning_nb = warnings.size(); warnings_panel->clear(); bool has_connections_table = false; // Add missing connections. if (GLOBAL_GET("debug/gdscript/warnings/enable")) { Node *base = get_tree()->get_edited_scene_root(); if (base && missing_connections.size() > 0) { has_connections_table = true; warnings_panel->push_table(1); for (const Connection &connection : missing_connections) { String base_path = base->get_name(); String source_path = base == connection.signal.get_object() ? base_path : base_path + "/" + String(base->get_path_to(Object::cast_to(connection.signal.get_object()))); String target_path = base == connection.callable.get_object() ? base_path : base_path + "/" + String(base->get_path_to(Object::cast_to(connection.callable.get_object()))); warnings_panel->push_cell(); warnings_panel->push_color(warnings_panel->get_theme_color(SNAME("warning_color"), EditorStringName(Editor))); warnings_panel->add_text(vformat(TTR("Missing connected method '%s' for signal '%s' from node '%s' to node '%s'."), connection.callable.get_method(), connection.signal.get_name(), source_path, target_path)); warnings_panel->pop(); // Color. warnings_panel->pop(); // Cell. } warnings_panel->pop(); // Table. warning_nb += missing_connections.size(); } } code_editor->set_warning_count(warning_nb); if (has_connections_table) { warnings_panel->add_newline(); } // Add script warnings. warnings_panel->push_table(3); for (const ScriptLanguage::Warning &w : warnings) { Dictionary ignore_meta; ignore_meta["line"] = w.start_line; ignore_meta["code"] = w.string_code.to_lower(); warnings_panel->push_cell(); warnings_panel->push_meta(ignore_meta); warnings_panel->push_color( warnings_panel->get_theme_color(SNAME("accent_color"), EditorStringName(Editor)).lerp(warnings_panel->get_theme_color(SNAME("mono_color"), EditorStringName(Editor)), 0.5f)); warnings_panel->add_text(TTR("[Ignore]")); warnings_panel->pop(); // Color. warnings_panel->pop(); // Meta ignore. warnings_panel->pop(); // Cell. warnings_panel->push_cell(); warnings_panel->push_meta(w.start_line - 1); warnings_panel->push_color(warnings_panel->get_theme_color(SNAME("warning_color"), EditorStringName(Editor))); warnings_panel->add_text(vformat(TTR("Line %d (%s):"), w.start_line, w.string_code)); warnings_panel->pop(); // Color. warnings_panel->pop(); // Meta goto. warnings_panel->pop(); // Cell. warnings_panel->push_cell(); warnings_panel->add_text(w.message); warnings_panel->add_newline(); warnings_panel->pop(); // Cell. } warnings_panel->pop(); // Table. } void ScriptTextEditor::_update_errors() { code_editor->set_error_count(errors.size()); errors_panel->clear(); errors_panel->push_table(2); for (const ScriptLanguage::ScriptError &err : errors) { Dictionary click_meta; click_meta["line"] = err.line; click_meta["column"] = err.column; errors_panel->push_cell(); errors_panel->push_meta(err.line - 1); errors_panel->push_color(warnings_panel->get_theme_color(SNAME("error_color"), EditorStringName(Editor))); errors_panel->add_text(vformat(TTR("Line %d:"), err.line)); errors_panel->pop(); // Color. errors_panel->pop(); // Meta goto. errors_panel->pop(); // Cell. errors_panel->push_cell(); errors_panel->add_text(err.message); errors_panel->add_newline(); errors_panel->pop(); // Cell. } errors_panel->pop(); // Table for (const KeyValue> &KV : depended_errors) { Dictionary click_meta; click_meta["path"] = KV.key; click_meta["line"] = 1; errors_panel->add_newline(); errors_panel->add_newline(); errors_panel->push_meta(click_meta); errors_panel->add_text(vformat(R"(%s:)", KV.key)); errors_panel->pop(); // Meta goto. errors_panel->add_newline(); errors_panel->push_indent(1); errors_panel->push_table(2); String filename = KV.key.get_file(); for (const ScriptLanguage::ScriptError &err : KV.value) { click_meta["line"] = err.line; click_meta["column"] = err.column; errors_panel->push_cell(); errors_panel->push_meta(click_meta); errors_panel->push_color(errors_panel->get_theme_color(SNAME("error_color"), EditorStringName(Editor))); errors_panel->add_text(vformat(TTR("Line %d:"), err.line)); errors_panel->pop(); // Color. errors_panel->pop(); // Meta goto. errors_panel->pop(); // Cell. errors_panel->push_cell(); errors_panel->add_text(err.message); errors_panel->pop(); // Cell. } errors_panel->pop(); // Table errors_panel->pop(); // Indent. } bool highlight_safe = EDITOR_GET("text_editor/appearance/gutters/highlight_type_safe_lines"); bool last_is_safe = false; CodeEdit *te = code_editor->get_text_editor(); for (int i = 0; i < te->get_line_count(); i++) { if (highlight_safe) { if (safe_lines.has(i + 1)) { te->set_line_gutter_item_color(i, line_number_gutter, safe_line_number_color); last_is_safe = true; } else if (last_is_safe && (te->is_in_comment(i) != -1 || te->get_line(i).strip_edges().is_empty())) { te->set_line_gutter_item_color(i, line_number_gutter, safe_line_number_color); } else { te->set_line_gutter_item_color(i, line_number_gutter, default_line_number_color); last_is_safe = false; } } else { te->set_line_gutter_item_color(i, 1, default_line_number_color); } } } void ScriptTextEditor::_update_bookmark_list() { bookmarks_menu->clear(); bookmarks_menu->reset_size(); bookmarks_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_bookmark"), BOOKMARK_TOGGLE); bookmarks_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/remove_all_bookmarks"), BOOKMARK_REMOVE_ALL); bookmarks_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_next_bookmark"), BOOKMARK_GOTO_NEXT); bookmarks_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_previous_bookmark"), BOOKMARK_GOTO_PREV); PackedInt32Array bookmark_list = code_editor->get_text_editor()->get_bookmarked_lines(); if (bookmark_list.is_empty()) { return; } bookmarks_menu->add_separator(); for (int i = 0; i < bookmark_list.size(); i++) { // Strip edges to remove spaces or tabs. // Also replace any tabs by spaces, since we can't print tabs in the menu. String line = code_editor->get_text_editor()->get_line(bookmark_list[i]).replace("\t", " ").strip_edges(); // Limit the size of the line if too big. if (line.length() > 50) { line = line.substr(0, 50); } bookmarks_menu->add_item(String::num_int64(bookmark_list[i] + 1) + " - `" + line + "`"); bookmarks_menu->set_item_metadata(-1, bookmark_list[i]); } } void ScriptTextEditor::_bookmark_item_pressed(int p_idx) { if (p_idx < 4) { // Any item before the separator. _edit_option(bookmarks_menu->get_item_id(p_idx)); } else { code_editor->goto_line_centered(bookmarks_menu->get_item_metadata(p_idx)); } } static Vector _find_all_node_for_script(Node *p_base, Node *p_current, const Ref