/*************************************************************************/ /* script_text_editor.cpp */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ /* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ /* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* 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/math/expression.h" #include "core/os/input.h" #include "core/os/keyboard.h" #include "core/project_settings.h" #include "editor/editor_node.h" #include "editor/editor_scale.h" #include "editor/editor_settings.h" #include "editor/script_editor_debugger.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 (List::Element *E = all_connections.front(); E; E = E->next()) { Connection connection = E->get(); if (connection.method != p_method) { continue; } TreeItem *node_item = tree->create_item(root); node_item->set_text(0, Object::cast_to(connection.source)->get_name()); node_item->set_icon(0, EditorNode::get_singleton()->get_object_icon(connection.source, "Node")); node_item->set_selectable(0, false); node_item->set_editable(0, false); node_item->set_text(1, connection.signal); node_item->set_icon(1, get_parent_control()->get_icon("Slot", "EditorIcons")); node_item->set_selectable(1, false); node_item->set_editable(1, false); node_item->set_text(2, Object::cast_to(connection.target)->get_name()); node_item->set_icon(2, EditorNode::get_singleton()->get_object_icon(connection.target, "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_margin(MARGIN_LEFT, ANCHOR_BEGIN, 8 * EDSCALE); vbc->set_anchor_and_margin(MARGIN_TOP, ANCHOR_BEGIN, 8 * EDSCALE); vbc->set_anchor_and_margin(MARGIN_RIGHT, ANCHOR_END, -8 * EDSCALE); vbc->set_anchor_and_margin(MARGIN_BOTTOM, ANCHOR_END, -8 * EDSCALE); add_child(vbc); method = memnew(Label); method->set_align(Label::ALIGN_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(SIZE_EXPAND_FILL); tree->set_allow_rmb_select(true); } //////////////////////////////////////////////////////////////////////////////// Vector ScriptTextEditor::get_functions() { String errortxt; int line = -1, col; TextEdit *te = code_editor->get_text_edit(); String text = te->get_text(); List fnc; if (script->get_language()->validate(text, line, col, errortxt, script->get_path(), &fnc)) { //if valid rewrite functions to latest functions.clear(); for (List::Element *E = fnc.front(); E; E = E->next()) { functions.push_back(E->get()); } } return functions; } void ScriptTextEditor::apply_code() { if (script.is_null()) { return; } script->set_source_code(code_editor->get_text_edit()->get_text()); script->update_exports(); _update_member_keywords(); } RES ScriptTextEditor::get_edited_resource() const { return script; } void ScriptTextEditor::set_edited_resource(const RES &p_res) { ERR_FAIL_COND(script.is_valid()); ERR_FAIL_COND(p_res.is_null()); script = p_res; code_editor->get_text_edit()->set_text(script->get_source_code()); code_editor->get_text_edit()->clear_undo_history(); code_editor->get_text_edit()->tag_saved_version(); emit_signal("name_changed"); code_editor->update_line_and_column(); } void ScriptTextEditor::enable_editor() { if (editor_enabled) { return; } editor_enabled = true; _enable_code_editor(); _set_theme_for_script(); _validate_script(); } void ScriptTextEditor::_update_member_keywords() { member_keywords.clear(); code_editor->get_text_edit()->clear_member_keywords(); Color member_variable_color = EDITOR_GET("text_editor/highlighting/member_variable_color"); StringName instance_base = script->get_instance_base_type(); if (instance_base == StringName()) { return; } List plist; ClassDB::get_property_list(instance_base, &plist); for (List::Element *E = plist.front(); E; E = E->next()) { String name = E->get().name; if (E->get().usage & PROPERTY_USAGE_CATEGORY || E->get().usage & PROPERTY_USAGE_GROUP) { continue; } if (name.find("/") != -1) { continue; } code_editor->get_text_edit()->add_member_keyword(name, member_variable_color); } List clist; ClassDB::get_integer_constant_list(instance_base, &clist); for (List::Element *E = clist.front(); E; E = E->next()) { code_editor->get_text_edit()->add_member_keyword(E->get(), member_variable_color); } } void ScriptTextEditor::_load_theme_settings() { TextEdit *text_edit = code_editor->get_text_edit(); text_edit->clear_colors(); Color background_color = EDITOR_GET("text_editor/highlighting/background_color"); Color completion_background_color = EDITOR_GET("text_editor/highlighting/completion_background_color"); Color completion_selected_color = EDITOR_GET("text_editor/highlighting/completion_selected_color"); Color completion_existing_color = EDITOR_GET("text_editor/highlighting/completion_existing_color"); Color completion_scroll_color = EDITOR_GET("text_editor/highlighting/completion_scroll_color"); Color completion_font_color = EDITOR_GET("text_editor/highlighting/completion_font_color"); Color text_color = EDITOR_GET("text_editor/highlighting/text_color"); Color line_number_color = EDITOR_GET("text_editor/highlighting/line_number_color"); Color safe_line_number_color = EDITOR_GET("text_editor/highlighting/safe_line_number_color"); Color caret_color = EDITOR_GET("text_editor/highlighting/caret_color"); Color caret_background_color = EDITOR_GET("text_editor/highlighting/caret_background_color"); Color text_selected_color = EDITOR_GET("text_editor/highlighting/text_selected_color"); Color selection_color = EDITOR_GET("text_editor/highlighting/selection_color"); Color brace_mismatch_color = EDITOR_GET("text_editor/highlighting/brace_mismatch_color"); Color current_line_color = EDITOR_GET("text_editor/highlighting/current_line_color"); Color line_length_guideline_color = EDITOR_GET("text_editor/highlighting/line_length_guideline_color"); Color word_highlighted_color = EDITOR_GET("text_editor/highlighting/word_highlighted_color"); Color number_color = EDITOR_GET("text_editor/highlighting/number_color"); Color function_color = EDITOR_GET("text_editor/highlighting/function_color"); Color member_variable_color = EDITOR_GET("text_editor/highlighting/member_variable_color"); Color mark_color = EDITOR_GET("text_editor/highlighting/mark_color"); Color bookmark_color = EDITOR_GET("text_editor/highlighting/bookmark_color"); Color breakpoint_color = EDITOR_GET("text_editor/highlighting/breakpoint_color"); Color executing_line_color = EDITOR_GET("text_editor/highlighting/executing_line_color"); Color code_folding_color = EDITOR_GET("text_editor/highlighting/code_folding_color"); Color search_result_color = EDITOR_GET("text_editor/highlighting/search_result_color"); Color search_result_border_color = EDITOR_GET("text_editor/highlighting/search_result_border_color"); Color symbol_color = EDITOR_GET("text_editor/highlighting/symbol_color"); Color keyword_color = EDITOR_GET("text_editor/highlighting/keyword_color"); Color control_flow_keyword_color = EDITOR_GET("text_editor/highlighting/control_flow_keyword_color"); Color basetype_color = EDITOR_GET("text_editor/highlighting/base_type_color"); Color type_color = EDITOR_GET("text_editor/highlighting/engine_type_color"); Color usertype_color = EDITOR_GET("text_editor/highlighting/user_type_color"); Color comment_color = EDITOR_GET("text_editor/highlighting/comment_color"); Color string_color = EDITOR_GET("text_editor/highlighting/string_color"); text_edit->add_color_override("background_color", background_color); text_edit->add_color_override("completion_background_color", completion_background_color); text_edit->add_color_override("completion_selected_color", completion_selected_color); text_edit->add_color_override("completion_existing_color", completion_existing_color); text_edit->add_color_override("completion_scroll_color", completion_scroll_color); text_edit->add_color_override("completion_font_color", completion_font_color); text_edit->add_color_override("font_color", text_color); text_edit->add_color_override("line_number_color", line_number_color); text_edit->add_color_override("safe_line_number_color", safe_line_number_color); text_edit->add_color_override("caret_color", caret_color); text_edit->add_color_override("caret_background_color", caret_background_color); text_edit->add_color_override("font_color_selected", text_selected_color); text_edit->add_color_override("selection_color", selection_color); text_edit->add_color_override("brace_mismatch_color", brace_mismatch_color); text_edit->add_color_override("current_line_color", current_line_color); text_edit->add_color_override("line_length_guideline_color", line_length_guideline_color); text_edit->add_color_override("word_highlighted_color", word_highlighted_color); text_edit->add_color_override("number_color", number_color); text_edit->add_color_override("function_color", function_color); text_edit->add_color_override("member_variable_color", member_variable_color); text_edit->add_color_override("bookmark_color", bookmark_color); text_edit->add_color_override("breakpoint_color", breakpoint_color); text_edit->add_color_override("executing_line_color", executing_line_color); text_edit->add_color_override("mark_color", mark_color); text_edit->add_color_override("code_folding_color", code_folding_color); text_edit->add_color_override("search_result_color", search_result_color); text_edit->add_color_override("search_result_border_color", search_result_border_color); text_edit->add_color_override("symbol_color", symbol_color); text_edit->add_constant_override("line_spacing", EDITOR_DEF("text_editor/theme/line_spacing", 6)); colors_cache.symbol_color = symbol_color; colors_cache.keyword_color = keyword_color; colors_cache.control_flow_keyword_color = control_flow_keyword_color; colors_cache.basetype_color = basetype_color; colors_cache.type_color = type_color; colors_cache.usertype_color = usertype_color; colors_cache.comment_color = comment_color; colors_cache.string_color = string_color; theme_loaded = true; if (!script.is_null()) { _set_theme_for_script(); } } void ScriptTextEditor::_set_theme_for_script() { if (!theme_loaded) { return; } TextEdit *text_edit = code_editor->get_text_edit(); List keywords; script->get_language()->get_reserved_words(&keywords); for (List::Element *E = keywords.front(); E; E = E->next()) { if (script->get_language()->is_control_flow_keyword(E->get())) { // Use a different color for control flow keywords to make them easier to distinguish. text_edit->add_keyword_color(E->get(), colors_cache.control_flow_keyword_color); } else { text_edit->add_keyword_color(E->get(), colors_cache.keyword_color); } } //colorize core types const Color basetype_color = colors_cache.basetype_color; text_edit->add_keyword_color("String", basetype_color); text_edit->add_keyword_color("Vector2", basetype_color); text_edit->add_keyword_color("Rect2", basetype_color); text_edit->add_keyword_color("Transform2D", basetype_color); text_edit->add_keyword_color("Vector3", basetype_color); text_edit->add_keyword_color("AABB", basetype_color); text_edit->add_keyword_color("Basis", basetype_color); text_edit->add_keyword_color("Plane", basetype_color); text_edit->add_keyword_color("Transform", basetype_color); text_edit->add_keyword_color("Quat", basetype_color); text_edit->add_keyword_color("Color", basetype_color); text_edit->add_keyword_color("Object", basetype_color); text_edit->add_keyword_color("NodePath", basetype_color); text_edit->add_keyword_color("RID", basetype_color); text_edit->add_keyword_color("Dictionary", basetype_color); text_edit->add_keyword_color("Array", basetype_color); text_edit->add_keyword_color("PoolByteArray", basetype_color); text_edit->add_keyword_color("PoolIntArray", basetype_color); text_edit->add_keyword_color("PoolRealArray", basetype_color); text_edit->add_keyword_color("PoolStringArray", basetype_color); text_edit->add_keyword_color("PoolVector2Array", basetype_color); text_edit->add_keyword_color("PoolVector3Array", basetype_color); text_edit->add_keyword_color("PoolColorArray", basetype_color); //colorize engine types List types; ClassDB::get_class_list(&types); for (List::Element *E = types.front(); E; E = E->next()) { String n = E->get(); if (n.begins_with("_")) { n = n.substr(1, n.length()); } text_edit->add_keyword_color(n, colors_cache.type_color); } _update_member_keywords(); //colorize user types List global_classes; ScriptServer::get_global_class_list(&global_classes); for (List::Element *E = global_classes.front(); E; E = E->next()) { text_edit->add_keyword_color(E->get(), colors_cache.usertype_color); } //colorize singleton autoloads (as types, just as engine singletons are) List props; ProjectSettings::get_singleton()->get_property_list(&props); for (List::Element *E = props.front(); E; E = E->next()) { String s = E->get().name; if (!s.begins_with("autoload/")) { continue; } String path = ProjectSettings::get_singleton()->get(s); if (path.begins_with("*")) { text_edit->add_keyword_color(s.get_slice("/", 1), colors_cache.usertype_color); } } //colorize comments List comments; script->get_language()->get_comment_delimiters(&comments); for (List::Element *E = comments.front(); E; E = E->next()) { String comment = E->get(); String beg = comment.get_slice(" ", 0); String end = comment.get_slice_count(" ") > 1 ? comment.get_slice(" ", 1) : String(); text_edit->add_color_region(beg, end, colors_cache.comment_color, end == ""); } //colorize strings List strings; script->get_language()->get_string_delimiters(&strings); for (List::Element *E = strings.front(); E; E = E->next()) { String string = E->get(); String beg = string.get_slice(" ", 0); String end = string.get_slice_count(" ") > 1 ? string.get_slice(" ", 1) : String(); text_edit->add_color_region(beg, end, colors_cache.string_color, end == ""); } } 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(); code_editor->get_text_edit()->insert_at("# warning-ignore:" + meta["code"].operator String(), meta["line"].operator int64_t() - 1); _validate_script(); } } void ScriptTextEditor::reload_text() { ERR_FAIL_COND(script.is_null()); TextEdit *te = code_editor->get_text_edit(); int column = te->cursor_get_column(); int row = te->cursor_get_line(); int h = te->get_h_scroll(); int v = te->get_v_scroll(); te->set_text(script->get_source_code()); te->cursor_set_line(row); te->cursor_set_column(column); te->set_h_scroll(h); te->set_v_scroll(v); te->tag_saved_version(); code_editor->update_line_and_column(); } void ScriptTextEditor::add_callback(const String &p_function, PoolStringArray p_args) { String code = code_editor->get_text_edit()->get_text(); int pos = script->get_language()->find_function(p_function, code); if (pos == -1) { //does not exist code_editor->get_text_edit()->deselect(); pos = code_editor->get_text_edit()->get_line_count() + 2; String func = script->get_language()->make_function("", p_function, p_args); //code=code+func; code_editor->get_text_edit()->cursor_set_line(pos + 1); code_editor->get_text_edit()->cursor_set_column(1000000); //none shall be that big code_editor->get_text_edit()->insert_text_at_cursor("\n\n" + func); } code_editor->get_text_edit()->cursor_set_line(pos); code_editor->get_text_edit()->cursor_set_column(1); } bool ScriptTextEditor::show_members_overview() { return true; } void ScriptTextEditor::update_settings() { code_editor->update_editor_settings(); } bool ScriptTextEditor::is_unsaved() { return code_editor->get_text_edit()->get_version() != code_editor->get_text_edit()->get_saved_version(); } 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) { ensure_focus(); } } 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_to_spaces() { code_editor->convert_indent_to_spaces(); } void ScriptTextEditor::convert_indent_to_tabs() { code_editor->convert_indent_to_tabs(); } void ScriptTextEditor::tag_saved_version() { code_editor->get_text_edit()->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_edit()->grab_focus(); } String ScriptTextEditor::get_name() { String name; name = script->get_path().get_file(); if (name.empty()) { // This appears for newly created built-in scripts before saving the scene. name = TTR("[unsaved]"); } else if (script->get_path().find("local://") == -1 || script->get_path().find("::") == -1) { const String &script_name = script->get_name(); if (script_name != "") { // 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_icon() { if (get_parent_control() && get_parent_control()->has_icon(script->get_class(), "EditorIcons")) { return get_parent_control()->get_icon(script->get_class(), "EditorIcons"); } return Ref(); } void ScriptTextEditor::_validate_script() { String errortxt; int line = -1, col; TextEdit *te = code_editor->get_text_edit(); String text = te->get_text(); List fnc; Set safe_lines; List warnings; if (!script->get_language()->validate(text, line, col, errortxt, script->get_path(), &fnc, &warnings, &safe_lines)) { String error_text = "error(" + itos(line) + "," + itos(col) + "): " + errortxt; code_editor->set_error(error_text); code_editor->set_error_pos(line - 1, col - 1); script_is_valid = false; } else { code_editor->set_error(""); line = -1; if (!script->is_tool()) { script->set_source_code(text); script->update_exports(); _update_member_keywords(); } functions.clear(); for (List::Element *E = fnc.front(); E; E = E->next()) { functions.push_back(E->get()); } script_is_valid = true; } _update_connected_methods(); int warning_nb = warnings.size(); warnings_panel->clear(); // 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) { warnings_panel->push_table(1); for (List::Element *E = missing_connections.front(); E; E = E->next()) { Connection connection = E->get(); String base_path = base->get_name(); String source_path = base == connection.source ? base_path : base_path + "/" + base->get_path_to(Object::cast_to(connection.source)); String target_path = base == connection.target ? base_path : base_path + "/" + base->get_path_to(Object::cast_to(connection.target)); warnings_panel->push_cell(); warnings_panel->push_color(warnings_panel->get_color("warning_color", "Editor")); warnings_panel->add_text(vformat(TTR("Missing connected method '%s' for signal '%s' from node '%s' to node '%s'."), connection.method, connection.signal, 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_nb(warning_nb); // Add script warnings. warnings_panel->push_table(3); for (List::Element *E = warnings.front(); E; E = E->next()) { ScriptLanguage::Warning w = E->get(); Dictionary ignore_meta; ignore_meta["line"] = w.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_color("accent_color", "Editor").linear_interpolate(warnings_panel->get_color("mono_color", "Editor"), 0.5)); 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.line - 1); warnings_panel->push_color(warnings_panel->get_color("warning_color", "Editor")); warnings_panel->add_text(TTR("Line") + " " + itos(w.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->pop(); // Cell. } warnings_panel->pop(); // Table. line--; bool highlight_safe = EDITOR_DEF("text_editor/highlighting/highlight_type_safe_lines", true); bool last_is_safe = false; for (int i = 0; i < te->get_line_count(); i++) { te->set_line_as_marked(i, line == i); if (highlight_safe) { if (safe_lines.has(i + 1)) { te->set_line_as_safe(i, true); last_is_safe = true; } else if (last_is_safe && (te->is_line_comment(i) || te->get_line(i).strip_edges().empty())) { te->set_line_as_safe(i, true); } else { te->set_line_as_safe(i, false); last_is_safe = false; } } else { te->set_line_as_safe(i, false); } } emit_signal("name_changed"); emit_signal("edited_script_changed"); } void ScriptTextEditor::_update_bookmark_list() { bookmarks_menu->clear(); bookmarks_menu->set_size(Size2(1, 1)); 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); Array bookmark_list = code_editor->get_text_edit()->get_bookmarks_array(); 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_edit()->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(bookmarks_menu->get_item_count() - 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