/**************************************************************************/ /* 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/math/expression.h" #include "core/os/keyboard.h" #include "editor/debugger/editor_debugger_node.h" #include "editor/editor_command_palette.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/themes/editor_scale.h" #include "scene/gui/rich_text_label.h" #include "scene/gui/split_container.h" void ConnectionInfoDialog::ok_pressed() { } void ConnectionInfoDialog::popup_connections(String p_method, 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(TTR("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_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER); vbc->add_child(method); tree = memnew(Tree); tree->set_columns(3); tree->set_hide_root(true); tree->set_column_titles_visible(true); tree->set_column_title(0, TTR("Source")); tree->set_column_title(1, TTR("Signal")); tree->set_column_title(2, TTR("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_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 safe_line_number_color_updated = updated_safe_line_number_color != safe_line_number_color; bool marked_line_color_updated = updated_marked_line_color != marked_line_color; bool folded_code_region_color_updated = updated_folded_code_region_color != folded_code_region_color; if (safe_line_number_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 (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); } } marked_line_color = updated_marked_line_color; folded_code_region_color = updated_folded_code_region_color; } theme_loaded = true; if (!script.is_null()) { _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(); List strings; script->get_language()->get_string_delimiters(&strings); text_edit->clear_string_delimiters(); for (const String &string : strings) { String beg = string.get_slice(" ", 0); String end = string.get_slice_count(" ") > 1 ? string.get_slice(" ", 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(); List comments; script->get_language()->get_comment_delimiters(&comments); for (const String &comment : comments) { String beg = comment.get_slice(" ", 0); String end = comment.get_slice_count(" ") > 1 ? comment.get_slice(" ", 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); } } List doc_comments; script->get_language()->get_doc_comment_delimiters(&doc_comments); for (const String &doc_comment : doc_comments) { String beg = doc_comment.get_slice(" ", 0); String end = doc_comment.get_slice_count(" ") > 1 ? doc_comment.get_slice(" ", 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(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(")"); const String text_to_insert = ", " + code.quote(quote_style); prev_line = prev_line.insert(closing_bracket_idx, text_to_insert); text_editor->set_line(line - 1, prev_line); } 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(Variant p_line) { if (p_line.get_type() == Variant::INT) { code_editor->get_text_editor()->remove_secondary_carets(); code_editor->get_text_editor()->set_caret_line(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()) { code_editor->get_text_editor()->remove_secondary_carets(); code_editor->get_text_editor()->set_caret_line(line); } else { Ref scr = ResourceLoader::load(path); if (!scr.is_valid()) { EditorNode::get_singleton()->show_warning(TTR("Could not load file at:") + "\n\n" + path, TTR("Error!")); } else { ScriptEditor::get_singleton()->edit(scr, line, 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(); _validate_script(); } void ScriptTextEditor::add_callback(const String &p_function, PackedStringArray p_args) { String code = code_editor->get_text_editor()->get_text(); int pos = script->get_language()->find_function(p_function, code); code_editor->get_text_editor()->remove_secondary_carets(); if (pos == -1) { //does not exist code_editor->get_text_editor()->deselect(); pos = code_editor->get_text_editor()->get_line_count() + 2; String func = script->get_language()->make_function("", p_function, p_args); //code=code+func; code_editor->get_text_editor()->set_caret_line(pos + 1); code_editor->get_text_editor()->set_caret_column(1000000); //none shall be that big code_editor->get_text_editor()->insert_text_at_caret("\n\n" + func); } code_editor->get_text_editor()->set_caret_line(pos); code_editor->get_text_editor()->set_caret_column(1); } bool ScriptTextEditor::show_members_overview() { return true; } void ScriptTextEditor::update_settings() { code_editor->get_text_editor()->set_gutter_draw(connection_gutter, EDITOR_GET("text_editor/appearance/gutters/show_info_gutter")); 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(); } 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::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(); } void ScriptTextEditor::goto_line(int p_line, bool p_with_error) { code_editor->goto_line(p_line); } 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) { code_editor->goto_line_centered(p_line); } 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)) { for (List::Element *E = errors.front(); E; 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(); } } if (errors.size() > 0) { // TRANSLATORS: Script error pointing to a line and column number. String error_text = vformat(TTR("Error at (%d, %d):"), errors[0].line, errors[0].column) + " " + errors[0].message; code_editor->set_error(error_text); code_editor->set_error_pos(errors[0].line - 1, errors[0].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(); 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").booleanize()) { 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 + "/" + base->get_path_to(Object::cast_to(connection.signal.get_object())); String target_path = base == connection.callable.get_object() ? base_path : base_path + "/" + 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(TTR("Line") + " " + itos(w.start_line)); warnings_panel->add_text(" (" + 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(TTR("Line") + " " + itos(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(TTR("Line") + " " + itos(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. } CodeEdit *te = code_editor->get_text_editor(); bool highlight_safe = EDITOR_GET("text_editor/appearance/gutters/highlight_type_safe_lines"); bool last_is_safe = false; for (int i = 0; i < te->get_line_count(); i++) { if (errors.is_empty()) { 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)); } else { for (const ScriptLanguage::ScriptError &E : errors) { bool error_line = i == E.line - 1; te->set_line_background_color(i, error_line ? marked_line_color : Color(0, 0, 0, 0)); if (error_line) { break; } } } 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.size() == 0) { 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((int)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