/**************************************************************************/
/*  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/input/input.h"
#include "core/io/dir_access.h"
#include "core/io/json.h"
#include "core/io/resource_loader.h"
#include "core/math/expression.h"
#include "core/object/callable_mp.h"
#include "core/object/class_db.h"
#include "core/os/keyboard.h"
#include "editor/debugger/editor_debugger_node.h"
#include "editor/doc/editor_help.h"
#include "editor/docks/filesystem_dock.h"
#include "editor/editor_interface.h"
#include "editor/editor_node.h"
#include "editor/editor_string_names.h"
#include "editor/gui/editor_toaster.h"
#include "editor/inspector/editor_context_menu_plugin.h"
#include "editor/inspector/editor_inspector.h"
#include "editor/inspector/multi_node_edit.h"
#include "editor/script/syntax_highlighters.h"
#include "editor/settings/editor_command_palette.h"
#include "editor/settings/editor_settings.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/split_container.h"
#include "scene/main/scene_tree.h"
#include "scene/resources/style_box_flat.h"
#include "servers/rendering/rendering_server.h"

void ConnectionInfoDialog::ok_pressed() {
}

void ConnectionInfoDialog::popup_connections(const String &p_method, const Vector<Node *> &p_nodes) {
	method->set_text(p_method);

	tree->clear();
	TreeItem *root = tree->create_item();

	for (int i = 0; i < p_nodes.size(); i++) {
		List<Connection> 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<Node>(connection.signal.get_object())->get_name());
			node_item->set_icon(0, EditorNode::get_singleton()->get_object_icon(connection.signal.get_object()));
			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<Control>(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<Node>(connection.callable.get_object())->get_name());
			node_item->set_icon(2, EditorNode::get_singleton()->get_object_icon(connection.callable.get_object()));
			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_theme_type_variation("TreeTable");
	tree->set_hide_folding(true);
	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);
}

////////////////////////////////////////////////////////////////////////////////

void ScriptTextEditor::EditMenusSTE::_update_breakpoint_list() {
	breakpoints_menu->clear();
	breakpoints_menu->reset_size();

	breakpoints_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_breakpoint"), DEBUG_TOGGLE_BREAKPOINT);
	breakpoints_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/remove_all_breakpoints"), DEBUG_REMOVE_ALL_BREAKPOINTS);
	breakpoints_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_next_breakpoint"), DEBUG_GOTO_NEXT_BREAKPOINT);
	breakpoints_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_previous_breakpoint"), DEBUG_GOTO_PREV_BREAKPOINT);

	TextEditorBase *script_text_editor = _get_active_editor();
	if (script_text_editor == nullptr) {
		return;
	}

	PackedInt32Array breakpoint_list = script_text_editor->get_code_editor()->get_text_editor()->get_breakpointed_lines();
	if (breakpoint_list.is_empty()) {
		return;
	}

	breakpoints_menu->add_separator();

	for (int i = 0; i < breakpoint_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 = script_text_editor->get_code_editor()->get_text_editor()->get_line(breakpoint_list[i]).replace("\t", "  ").strip_edges();

		// Limit the size of the line if too big.
		if (line.length() > 50) {
			line = line.substr(0, 50);
		}

		breakpoints_menu->add_item(String::num_int64(breakpoint_list[i] + 1) + " - `" + line + "`");
		breakpoints_menu->set_item_metadata(-1, breakpoint_list[i]);
	}
}

void ScriptTextEditor::EditMenusSTE::_breakpoint_item_pressed(int p_idx) {
	TextEditorBase *script_text_editor = _get_active_editor();
	ERR_FAIL_NULL(script_text_editor);
	if (p_idx < 4) { // Any item before the separator.
		_edit_option(breakpoints_menu->get_item_id(p_idx));
	} else {
		script_text_editor->get_code_editor()->goto_line_centered(breakpoints_menu->get_item_metadata(p_idx));
	}
}

ScriptTextEditor::EditMenusSTE::EditMenusSTE() {
	edit_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/evaluate_selection"), EDIT_EVALUATE);
	_popup_move_item(EDIT_DUPLICATE_LINES, edit_menu->get_popup());
	goto_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_function"), SEARCH_LOCATE_FUNCTION);
	_popup_move_item(SEARCH_GOTO_LINE, goto_menu->get_popup(), false);
	goto_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_symbol"), LOOKUP_SYMBOL);
	_popup_move_item(SEARCH_GOTO_LINE, goto_menu->get_popup());

	edit_menu_fold->add_shortcut(ED_GET_SHORTCUT("script_text_editor/create_code_region"), EDIT_CREATE_CODE_REGION);
	edit_menu_convert_indent->add_shortcut(ED_GET_SHORTCUT("script_text_editor/auto_indent"), EDIT_AUTO_INDENT);

	search_menu->get_popup()->add_separator();
	search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/show_tooltip"), SHOW_TOOLTIP_AT_CARET);
	search_menu->get_popup()->add_shortcut(ED_GET_SHORTCUT("script_text_editor/contextual_help"), HELP_CONTEXTUAL);

	breakpoints_menu = memnew(PopupMenu);
	goto_menu->get_popup()->add_submenu_node_item(TTRC("Breakpoints"), breakpoints_menu);
	breakpoints_menu->connect("about_to_popup", callable_mp(this, &EditMenusSTE::_update_breakpoint_list));
	breakpoints_menu->connect("index_pressed", callable_mp(this, &EditMenusSTE::_breakpoint_item_pressed));

	// Update immediately for shortcuts.
	_update_breakpoint_list();
}

////////////////////////////////////////////////////////////////////////////////

Vector<String> ScriptTextEditor::get_functions() {
	CodeEdit *te = code_editor->get_text_editor();
	String text = te->get_text();
	List<String> fnc;

	Ref<Script> script = edited_res;
	if (script.is_valid() && 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() {
	Ref<Script> script = edited_res;
	if (script.is_valid()) {
		script->set_source_code(code_editor->get_text_editor()->get_text());
		script->update_exports();
		if (!pending_dragged_exports.is_empty()) {
			_assign_dragged_export_variables();
		}
	}

	code_editor->get_text_editor()->get_syntax_highlighter()->update_cache();
}

void ScriptTextEditor::set_edited_resource(const Ref<Resource> &p_res) {
	ERR_FAIL_COND(p_res.is_null());
	ERR_FAIL_COND(edited_res.is_valid());

	edited_res = p_res;

	Ref<Script> script = edited_res;
	ERR_FAIL_COND(script.is_null());
	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() {
	if (editor_enabled) {
		return;
	}

	_enable_code_editor();

	if (pending_state != Variant()) {
		code_editor->set_edit_state(pending_state);
		pending_state = Variant();
	}

	TextEditorBase::enable_editor();
}

void ScriptTextEditor::_load_theme_settings() {
	if (!editor_enabled) {
		return;
	}
	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;
	}

	Ref<StyleBoxFlat> drag_info_sb = drag_info_label->get_theme_stylebox(CoreStringName(normal));
	drag_info_sb->set_bg_color(EDITOR_GET("text_editor/theme/highlighting/completion_background_color"));
	Color info_color = EDITOR_GET("text_editor/theme/highlighting/text_color");
	info_color.a = 0.5f;
	drag_info_sb->set_border_color(info_color);
	info_color.a = 0.7f;
	drag_info_label->add_theme_color_override(SceneStringName(font_color), info_color);

	theme_loaded = true;
	Ref<Script> script = edited_res;
	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();

	Ref<Script> script = edited_res;
	Vector<String> 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);
}

bool ScriptTextEditor::_warning_clicked(const Variant &p_line) {
	if (CodeEditorBase::_warning_clicked(p_line)) {
		return true;
	} else if (p_line.get_type() == Variant::DICTIONARY) {
		Dictionary meta = p_line.operator Dictionary();
		const int line = meta["line"].operator int64_t();
		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();
		return true;
	}
	return false;
}

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<Resource> 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 {
				ScriptEditor::get_singleton()->edit(scr, line, column);
			}
		}
	}
}

void ScriptTextEditor::_on_mouse_exited() {
	drag_info_label->hide();
}

void ScriptTextEditor::add_callback(const String &p_function, const PackedStringArray &p_args) {
	Ref<Script> script = edited_res;
	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::_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) {
	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()) {
			i_end_previous = MAX(i_end_previous, i_start);
			i_start = p_text.find("Color", i_start + 1);
			continue;
		}

		const int i_par_start = p_text.find_char('(', i_start + 5);
		const int i_par_end = p_text.find_char(')', i_start + 5);
		if (i_par_start == -1 || i_par_end == -1) {
			i_end_previous = MAX(i_end_previous, i_start);
			i_start = p_text.find("Color", i_start + 1);
			continue;
		}

		Dictionary color_info;
		color_info["column"] = i_start;
		color_info["width_ratio"] = 1.0;
		color_info["color_end"] = i_par_end;

		const String fn_name = p_text.substr(i_start + 5, i_par_start - i_start - 5);
		const 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);
			if (stripped.length() > 1 && (stripped[0] == '"' || stripped[0] == '\'')) {
				// String constructor.
				const char32_t string_delimiter = stripped[0];
				if (stripped[stripped.length() - 1] == string_delimiter) {
					const String color_string = stripped.substr(1, stripped.length() - 2);
					if (!color_string.contains_char(string_delimiter)) {
						color_info["color"] = Color::from_string(color_string, Color());
						color_info["color_mode"] = MODE_STRING;
						has_added_color = true;
					}
				}
			} else if (stripped.length() == 10 && stripped.begins_with("0x")) {
				// Hex constructor.
				const String color_string = stripped.substr(2);
				if (color_string.is_valid_hex_number(false)) {
					color_info["color"] = Color::from_string(color_string, Color());
					color_info["color_mode"] = MODE_HEX;
					has_added_color = true;
				}
			} else if (stripped.is_empty()) {
				// Empty Color() constructor.
				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) {
			const PackedStringArray s_params_split = s_params.split(",", false, 4);
			PackedFloat64Array params;
			bool valid_floats = true;
			for (const String &s_param : s_params_split) {
				// Only allow float literals, expressions won't be evaluated and could get replaced.
				if (!s_param.strip_edges().is_valid_float()) {
					valid_floats = false;
					break;
				}
				params.push_back(s_param.to_float());
			}
			if (valid_floats && params.size() == 3) {
				if (fn_name == ".from_rgba8") {
					params.push_back(255);
				} else {
					params.push_back(1.0);
				}
			}
			if (valid_floats && 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");
		}
		RID text_ci = code_editor->get_text_editor()->get_text_canvas_item();
		RS::get_singleton()->canvas_item_add_rect(text_ci, p_rect.grow(-3), Color(1, 1, 1));
		color_alpha_texture->draw_rect(text_ci, col_rect);
		RS::get_singleton()->canvas_item_add_rect(text_ci, col_rect, Color(p_info["color"]));
	}
}

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<String> 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());
	}
	TextEditorBase::update_settings();
}

Variant ScriptTextEditor::get_edit_state() {
	if (pending_state != Variant()) {
		return pending_state;
	}
	return TextEditorBase::get_edit_state();
}

void ScriptTextEditor::set_edit_state(const Variant &p_state) {
	if (editor_enabled) {
		code_editor->set_edit_state(p_state);
	} else {
		// The editor is not fully initialized, so the state can't be loaded properly.
		pending_state = p_state;
	}

	Dictionary state = p_state;
	if (state.has("syntax_highlighter")) {
		for (const Ref<EditorSyntaxHighlighter> &highlighter : highlighters) {
			if (highlighter->_get_name() == String(state["syntax_highlighter"])) {
				set_syntax_highlighter(highlighter);
				break;
			}
		}
	}

	if (editor_enabled) {
#ifndef ANDROID_ENABLED
		ensure_focus();
#endif
	}
}

Variant ScriptTextEditor::get_previous_state() {
	return code_editor->get_previous_state();
}

void ScriptTextEditor::store_previous_state() {
	return code_editor->store_previous_state();
}

Ref<Texture2D> ScriptTextEditor::get_theme_icon() {
	Ref<Script> script = edited_res;
	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());
		}
	}

	Ref<Texture2D> extension_language_icon = EditorNode::get_editor_data().extension_class_get_icon(script->get_class());
	Ref<Texture2D> extension_language_alt_icon;
	if (script->is_built_in()) {
		extension_language_alt_icon = EditorNode::get_editor_data().extension_class_get_icon(script->get_class() + "Internal");
	}

	if (extension_language_alt_icon.is_valid()) {
		return extension_language_alt_icon;
	} else if (extension_language_icon.is_valid()) {
		return extension_language_icon;
	}

	return Ref<Texture2D>();
}

struct ScriptErrorLineComparator {
	bool operator()(const ScriptLanguage::ScriptError &p_a, const ScriptLanguage::ScriptError &p_b) const {
		if (p_a.line != p_b.line) {
			return p_a.line < p_b.line;
		}
		return p_a.column < p_b.column;
	}
};

void ScriptTextEditor::_validate_script() {
	CodeEdit *te = code_editor->get_text_editor();

	String text = te->get_text();
	List<String> fnc;

	warnings.clear();
	errors.clear();
	depended_errors.clear();
	safe_lines.clear();

	Ref<Script> script = edited_res;
	if (!script->get_language()->validate(text, script->get_path(), &fnc, &errors, &warnings, &safe_lines)) {
		errors.sort_custom<ScriptErrorLineComparator>();

		List<ScriptLanguage::ScriptError>::Element *E = errors.front();
		while (E) {
			List<ScriptLanguage::ScriptError>::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) {
			code_editor->set_error_pos(errors.front()->get().line - 1, errors.front()->get().column - 1);
			const String message = errors.front()->get().message.replace("[", "[lb]");
			const Point2i display_pos = code_editor->get_pos_for_display(code_editor->get_error_pos());
			const String error_text = vformat(TTR("Error at ([hint=Line %d, column %d]%d, %d[/hint]):"), display_pos.x, display_pos.y, display_pos.x, display_pos.y) + " " + message;
			code_editor->set_error(error_text);
		}
		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();

	if (!pending_dragged_exports.is_empty()) {
		_assign_dragged_export_variables();
	}

	TextEditorBase::_validate_script();
}

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<Node>(connection.signal.get_object())));
				String target_path = base == connection.callable.get_object() ? base_path : base_path + "/" + String(base->get_path_to(Object::cast_to<Node>(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 - 1;
		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) {
		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<String, List<ScriptLanguage::ScriptError>> &KV : depended_errors) {
		Dictionary click_meta_script;
		click_meta_script["path"] = KV.key;
		click_meta_script["line"] = 0;

		errors_panel->add_newline();
		errors_panel->add_newline();
		errors_panel->push_meta(click_meta_script);
		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) {
			Dictionary click_meta;
			click_meta["path"] = KV.key;
			click_meta["line"] = err.line - 1;
			click_meta["column"] = err.column - 1;

			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);
		}
	}
}

static Vector<Node *> _find_all_node_for_script(Node *p_base, Node *p_current, const Ref<Script> &p_script) {
	Vector<Node *> nodes;

	if (p_current->get_owner() != p_base && p_base != p_current) {
		return nodes;
	}

	Ref<Script> c = p_current->get_script();
	if (c == p_script) {
		nodes.push_back(p_current);
	}

	for (int i = 0; i < p_current->get_child_count(); i++) {
		Vector<Node *> found = _find_all_node_for_script(p_base, p_current->get_child(i), p_script);
		nodes.append_array(found);
	}

	return nodes;
}

static Node *_find_node_for_script(Node *p_base, Node *p_current, const Ref<Script> &p_script) {
	if (p_current->get_owner() != p_base && p_base != p_current) {
		return nullptr;
	}
	Ref<Script> c = p_current->get_script();
	if (c == p_script) {
		return p_current;
	}
	for (int i = 0; i < p_current->get_child_count(); i++) {
		Node *found = _find_node_for_script(p_base, p_current->get_child(i), p_script);
		if (found) {
			return found;
		}
	}

	return nullptr;
}

static void _find_changed_scripts_for_external_editor(Node *p_base, Node *p_current, HashSet<Ref<Script>> &r_scripts) {
	if (p_current->get_owner() != p_base && p_base != p_current) {
		return;
	}
	Ref<Script> c = p_current->get_script();

	if (c.is_valid()) {
		r_scripts.insert(c);
	}

	for (int i = 0; i < p_current->get_child_count(); i++) {
		_find_changed_scripts_for_external_editor(p_base, p_current->get_child(i), r_scripts);
	}
}

void ScriptEditor::_update_modified_scripts_for_external_editor(Ref<Script> p_for_script) {
	bool use_external_editor = bool(EDITOR_GET("text_editor/external/use_external_editor"));

	ERR_FAIL_NULL(get_tree());

	HashSet<Ref<Script>> scripts;

	Node *base = get_tree()->get_edited_scene_root();
	if (base) {
		_find_changed_scripts_for_external_editor(base, base, scripts);
	}

	for (const Ref<Script> &E : scripts) {
		Ref<Script> scr = E;

		if (!use_external_editor && !scr->get_language()->overrides_external_editor()) {
			continue; // We're not using an external editor for this script.
		}

		if (p_for_script.is_valid() && p_for_script != scr) {
			continue;
		}

		if (scr->is_built_in()) {
			continue; //internal script, who cares, though weird
		}

		uint64_t last_date = scr->get_last_modified_time();
		uint64_t date = FileAccess::get_modified_time(scr->get_path());

		if (last_date != date) {
			Ref<Script> rel_scr = ResourceLoader::load(scr->get_path(), scr->get_class(), ResourceFormatLoader::CACHE_MODE_IGNORE);
			ERR_CONTINUE(rel_scr.is_null());
			scr->set_source_code(rel_scr->get_source_code());
			scr->set_last_modified_time(rel_scr->get_last_modified_time());
			scr->update_exports();

			trigger_live_script_reload(scr->get_path());
		}
	}
}

void ScriptTextEditor::_code_complete_scripts(void *p_ud, const String &p_code, List<ScriptLanguage::CodeCompletionOption> *r_options, bool &r_force) {
	ScriptTextEditor *ste = (ScriptTextEditor *)p_ud;
	ste->_code_complete_script(p_code, r_options, r_force);
}

void ScriptTextEditor::_code_complete_script(const String &p_code, List<ScriptLanguage::CodeCompletionOption> *r_options, bool &r_force) {
	if (color_panel->is_visible()) {
		return;
	}

	Ref<Script> script = edited_res;
	Node *base = get_tree()->get_edited_scene_root();
	if (base) {
		base = _find_node_for_script(base, base, script);
	}
	String hint;
	Error err = script->get_language()->complete_code(p_code, script->get_path(), base, r_options, r_force, hint);

	if (err == OK) {
		code_editor->get_text_editor()->set_code_hint(hint);
	}
}

void ScriptTextEditor::_breakpoint_toggled(int p_row) {
	const CodeEdit *ce = code_editor->get_text_editor();
	bool enabled = p_row < ce->get_line_count() && ce->is_line_breakpointed(p_row);
	EditorDebuggerNode::get_singleton()->set_breakpoint(edited_res->get_path(), p_row + 1, enabled);
}

void ScriptTextEditor::_on_caret_moved() {
	if (code_editor->is_previewing_navigation_change()) {
		return;
	}
	if (is_layout_pending_in_tree()) {
		call_on_all_layout_pending_finished(callable_mp(this, &ScriptTextEditor::_on_caret_moved));
		return;
	}
	// When previous_line < 0, it means the user has just switched to this editor from a different one
	// (which already saved a state in the history). In this case, we should not save this editor's previous state.
	int current_line = code_editor->get_text_editor()->get_caret_line();
	if (previous_line >= 0 && Math::abs(current_line - previous_line) >= 10) {
		Dictionary nav_state = get_navigation_state();
		nav_state["row"] = previous_line;
		nav_state["scroll_position"] = -1;
		nav_state["ensure_caret_visible"] = true;
		emit_signal(SNAME("request_save_previous_state"), nav_state);
		store_previous_state();
	}
	previous_line = current_line;
}

void ScriptTextEditor::_lookup_symbol(const String &p_symbol, int p_row, int p_column) {
	Ref<Script> script = edited_res;
	Node *base = get_tree()->get_edited_scene_root();
	if (base) {
		base = _find_node_for_script(base, base, script);
	}

	ScriptLanguage::LookupResult result;
	String code_text = code_editor->get_text_editor()->get_text_with_cursor_char(p_row, p_column);
	Error lc_error = script->get_language()->lookup_code(code_text, p_symbol, script->get_path(), base, result);
	if (ScriptServer::is_global_class(p_symbol)) {
		EditorNode::get_singleton()->load_resource(ScriptServer::get_global_class_path(p_symbol));
	} else if (p_symbol.is_resource_file() || p_symbol.begins_with("uid://")) {
		if (DirAccess::dir_exists_absolute(p_symbol)) {
			FileSystemDock::get_singleton()->navigate_to_path(p_symbol);
		} else {
			EditorNode::get_singleton()->load_scene_or_resource(p_symbol);
		}
	} else if (lc_error == OK) {
		_goto_line(p_row);

		if (!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) {
			switch (result.type) {
				case ScriptLanguage::LOOKUP_RESULT_CLASS: {
					emit_signal(SNAME("go_to_help"), "class_name:" + result.class_name);
				} break;
				case ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT: {
					StringName cname = result.class_name;
					while (ClassDB::class_exists(cname)) {
						if (ClassDB::has_integer_constant(cname, result.class_member, true)) {
							result.class_name = cname;
							break;
						}
						cname = ClassDB::get_parent_class(cname);
					}
					emit_signal(SNAME("go_to_help"), "class_constant:" + result.class_name + ":" + result.class_member);
				} break;
				case ScriptLanguage::LOOKUP_RESULT_CLASS_PROPERTY: {
					StringName cname = result.class_name;
					while (ClassDB::class_exists(cname)) {
						if (ClassDB::has_property(cname, result.class_member, true)) {
							result.class_name = cname;
							break;
						}
						cname = ClassDB::get_parent_class(cname);
					}
					emit_signal(SNAME("go_to_help"), "class_property:" + result.class_name + ":" + result.class_member);
				} break;
				case ScriptLanguage::LOOKUP_RESULT_CLASS_METHOD: {
					StringName cname = result.class_name;
					while (ClassDB::class_exists(cname)) {
						if (ClassDB::has_method(cname, result.class_member, true)) {
							result.class_name = cname;
							break;
						}
						cname = ClassDB::get_parent_class(cname);
					}
					emit_signal(SNAME("go_to_help"), "class_method:" + result.class_name + ":" + result.class_member);
				} break;
				case ScriptLanguage::LOOKUP_RESULT_CLASS_SIGNAL: {
					StringName cname = result.class_name;
					while (ClassDB::class_exists(cname)) {
						if (ClassDB::has_signal(cname, result.class_member, true)) {
							result.class_name = cname;
							break;
						}
						cname = ClassDB::get_parent_class(cname);
					}
					emit_signal(SNAME("go_to_help"), "class_signal:" + result.class_name + ":" + result.class_member);
				} break;
				case ScriptLanguage::LOOKUP_RESULT_CLASS_ENUM: {
					StringName cname = result.class_name;
					while (ClassDB::class_exists(cname)) {
						if (ClassDB::has_enum(cname, result.class_member, true)) {
							result.class_name = cname;
							break;
						}
						cname = ClassDB::get_parent_class(cname);
					}
					emit_signal(SNAME("go_to_help"), "class_enum:" + result.class_name + ":" + result.class_member);
				} break;
				case ScriptLanguage::LOOKUP_RESULT_CLASS_ANNOTATION: {
					emit_signal(SNAME("go_to_help"), "class_annotation:" + result.class_name + ":" + result.class_member);
				} break;
				case ScriptLanguage::LOOKUP_RESULT_CLASS_TBD_GLOBALSCOPE: { // Deprecated.
					emit_signal(SNAME("go_to_help"), "class_global:" + result.class_name + ":" + result.class_member);
				} break;
				case ScriptLanguage::LOOKUP_RESULT_SCRIPT_LOCATION:
				case ScriptLanguage::LOOKUP_RESULT_LOCAL_CONSTANT:
				case ScriptLanguage::LOOKUP_RESULT_LOCAL_VARIABLE:
				case ScriptLanguage::LOOKUP_RESULT_MAX: {
					// Nothing to do.
				} break;
			}
		} else if (result.location >= 0) {
			if (result.script.is_valid()) {
				emit_signal(SNAME("request_open_script_at_line"), result.script, result.location - 1);
			} else {
				emit_signal(SNAME("request_save_history"));
				goto_line_centered(result.location - 1);
			}
		}
	} else if (ProjectSettings::get_singleton()->has_autoload(p_symbol)) {
		// Check for Autoload scenes.
		const ProjectSettings::AutoloadInfo &info = ProjectSettings::get_singleton()->get_autoload(p_symbol);
		if (info.is_singleton) {
			EditorNode::get_singleton()->open_scene(info.path);
		}
	} else if (p_symbol.is_relative_path()) {
		// Every symbol other than absolute path is relative path so keep this condition at last.
		String path = _get_absolute_path(p_symbol);
		if (FileAccess::exists(path)) {
			EditorNode::get_singleton()->load_scene_or_resource(path);
		}
	}
}

void ScriptTextEditor::_validate_symbol(const String &p_symbol) {
	CodeEdit *text_edit = code_editor->get_text_editor();

	Ref<Script> script = edited_res;
	Node *base = get_tree()->get_edited_scene_root();
	if (base) {
		base = _find_node_for_script(base, base, script);
	}

	ScriptLanguage::LookupResult result;
	String lc_text = code_editor->get_text_editor()->get_text_for_symbol_lookup();
	Error lc_error = script->get_language()->lookup_code(lc_text, p_symbol, script->get_path(), base, result);
	bool is_singleton = ProjectSettings::get_singleton()->has_autoload(p_symbol) && ProjectSettings::get_singleton()->get_autoload(p_symbol).is_singleton;
	if (lc_error == OK || is_singleton || ScriptServer::is_global_class(p_symbol) || p_symbol.is_resource_file() || p_symbol.begins_with("uid://")) {
		text_edit->set_symbol_lookup_word_as_valid(true);
	} else if (p_symbol.is_relative_path()) {
		String path = _get_absolute_path(p_symbol);
		if (FileAccess::exists(path)) {
			text_edit->set_symbol_lookup_word_as_valid(true);
		} else {
			text_edit->set_symbol_lookup_word_as_valid(false);
		}
	} else {
		text_edit->set_symbol_lookup_word_as_valid(false);
	}
}

void ScriptTextEditor::_show_symbol_tooltip(const String &p_symbol, int p_row, int p_column, bool p_shortcut) {
	if (!EDITOR_GET("text_editor/behavior/documentation/enable_tooltips").booleanize()) {
		return;
	}

	if (p_symbol.begins_with("res://") || p_symbol.begins_with("uid://")) {
		Control *tmp = EditorHelpBitTooltip::make_tooltip(code_editor->get_text_editor(), "resource||" + p_symbol);
		memdelete(tmp);
		return;
	}

	Ref<Script> script = edited_res;
	Node *base = get_tree()->get_edited_scene_root();
	if (base) {
		base = _find_node_for_script(base, base, script);
	}

	ScriptLanguage::LookupResult result;
	String doc_symbol;
	const String code_text = code_editor->get_text_editor()->get_text_with_cursor_char(p_row, p_column);
	const Error lc_error = script->get_language()->lookup_code(code_text, p_symbol, script->get_path(), base, result);
	if (lc_error == OK) {
		switch (result.type) {
			case ScriptLanguage::LOOKUP_RESULT_CLASS: {
				doc_symbol = "class|" + result.class_name + "|";
			} break;
			case ScriptLanguage::LOOKUP_RESULT_CLASS_CONSTANT: {
				StringName cname = result.class_name;
				while (ClassDB::class_exists(cname)) {
					if (ClassDB::has_integer_constant(cname, result.class_member, true)) {
						result.class_name = cname;
						break;
					}
					cname = ClassDB::get_parent_class(cname);
				}
				doc_symbol = "constant|" + result.class_name + "|" + result.class_member;
			} break;
			case ScriptLanguage::LOOKUP_RESULT_CLASS_PROPERTY: {
				StringName cname = result.class_name;
				while (ClassDB::class_exists(cname)) {
					if (ClassDB::has_property(cname, result.class_member, true)) {
						result.class_name = cname;
						break;
					}
					cname = ClassDB::get_parent_class(cname);
				}
				doc_symbol = "property|" + result.class_name + "|" + result.class_member;
			} break;
			case ScriptLanguage::LOOKUP_RESULT_CLASS_METHOD: {
				StringName cname = result.class_name;
				while (ClassDB::class_exists(cname)) {
					if (ClassDB::has_method(cname, result.class_member, true)) {
						result.class_name = cname;
						break;
					}
					cname = ClassDB::get_parent_class(cname);
				}
				doc_symbol = "method|" + result.class_name + "|" + result.class_member;
			} break;
			case ScriptLanguage::LOOKUP_RESULT_CLASS_SIGNAL: {
				StringName cname = result.class_name;
				while (ClassDB::class_exists(cname)) {
					if (ClassDB::has_signal(cname, result.class_member, true)) {
						result.class_name = cname;
						break;
					}
					cname = ClassDB::get_parent_class(cname);
				}
				doc_symbol = "signal|" + result.class_name + "|" + result.class_member;
			} break;
			case ScriptLanguage::LOOKUP_RESULT_CLASS_ENUM: {
				StringName cname = result.class_name;
				while (ClassDB::class_exists(cname)) {
					if (ClassDB::has_enum(cname, result.class_member, true)) {
						result.class_name = cname;
						break;
					}
					cname = ClassDB::get_parent_class(cname);
				}
				doc_symbol = "enum|" + result.class_name + "|" + result.class_member;
			} break;
			case ScriptLanguage::LOOKUP_RESULT_CLASS_ANNOTATION: {
				doc_symbol = "annotation|" + result.class_name + "|" + result.class_member;
			} break;
			case ScriptLanguage::LOOKUP_RESULT_LOCAL_CONSTANT:
			case ScriptLanguage::LOOKUP_RESULT_LOCAL_VARIABLE: {
				const String item_type = (result.type == ScriptLanguage::LOOKUP_RESULT_LOCAL_CONSTANT) ? "local_constant" : "local_variable";
				Dictionary item_data;
				item_data["description"] = result.description;
				item_data["is_deprecated"] = result.is_deprecated;
				item_data["deprecated_message"] = result.deprecated_message;
				item_data["is_experimental"] = result.is_experimental;
				item_data["experimental_message"] = result.experimental_message;
				item_data["doc_type"] = result.doc_type;
				item_data["enumeration"] = result.enumeration;
				item_data["is_bitfield"] = result.is_bitfield;
				item_data["value"] = result.value;
				doc_symbol = item_type + "||" + p_symbol + "|" + JSON::stringify(item_data);
			} break;
			case ScriptLanguage::LOOKUP_RESULT_SCRIPT_LOCATION:
			case ScriptLanguage::LOOKUP_RESULT_CLASS_TBD_GLOBALSCOPE: // Deprecated.
			case ScriptLanguage::LOOKUP_RESULT_MAX: {
				// Nothing to do.
			} break;
		}
	}

	// NOTE: See also `ScriptEditor::_get_debug_tooltip()` for documentation tooltips disabled.
	String debug_value = EditorDebuggerNode::get_singleton()->get_var_value(p_symbol);
	if (!debug_value.is_empty()) {
		constexpr int DISPLAY_LIMIT = 1024;
		if (debug_value.size() > DISPLAY_LIMIT) {
			debug_value = debug_value.left(DISPLAY_LIMIT) + "... " + TTR("(truncated)");
		}
		debug_value = TTR("Current value: ") + debug_value.replace("[", "[lb]");
	}

	if (!doc_symbol.is_empty() || !debug_value.is_empty()) {
		Control *tmp = EditorHelpBitTooltip::make_tooltip(code_editor->get_text_editor(), doc_symbol, debug_value, true, p_shortcut);
		memdelete(tmp);
	}
}

String ScriptTextEditor::_get_absolute_path(const String &rel_path) {
	String base_path = edited_res->get_path().get_base_dir();
	String path = base_path.path_join(rel_path);
	return path.replace("///", "//").simplify_path();
}

void ScriptTextEditor::_update_connected_methods() {
	CodeEdit *text_edit = code_editor->get_text_editor();
	text_edit->set_gutter_width(connection_gutter, text_edit->get_line_height());
	for (int i = 0; i < text_edit->get_line_count(); i++) {
		text_edit->set_line_gutter_metadata(i, connection_gutter, Dictionary());
		text_edit->set_line_gutter_icon(i, connection_gutter, nullptr);
		text_edit->set_line_gutter_clickable(i, connection_gutter, false);
	}
	missing_connections.clear();

	if (!script_is_valid) {
		return;
	}

	Node *base = get_tree()->get_edited_scene_root();
	if (!base) {
		return;
	}

	// Add connection icons to methods.
	Ref<Script> script = edited_res;
	Vector<Node *> nodes = _find_all_node_for_script(base, base, script);
	HashSet<StringName> methods_found;
	for (int i = 0; i < nodes.size(); i++) {
		List<Connection> signal_connections;
		nodes[i]->get_signals_connected_to_this(&signal_connections);

		for (const Connection &connection : signal_connections) {
			if (!(connection.flags & CONNECT_PERSIST)) {
				continue;
			}

			// As deleted nodes are still accessible via the undo/redo system, check if they're still on the tree.
			Node *source = Object::cast_to<Node>(connection.signal.get_object());
			if (source && !source->is_inside_tree()) {
				continue;
			}

			const StringName method = connection.callable.get_method();
			if (methods_found.has(method)) {
				continue;
			}

			if (!ClassDB::has_method(script->get_instance_base_type(), method)) {
				int line = -1;

				for (int j = 0; j < functions.size(); j++) {
					String name = functions[j].get_slicec(':', 0);
					if (name == method) {
						Dictionary line_meta;
						line_meta["type"] = "connection";
						line_meta["method"] = method;
						line = functions[j].get_slicec(':', 1).to_int() - 1;
						text_edit->set_line_gutter_metadata(line, connection_gutter, line_meta);
						text_edit->set_line_gutter_icon(line, connection_gutter, get_parent_control()->get_editor_theme_icon(SNAME("Slot")));
						text_edit->set_line_gutter_clickable(line, connection_gutter, true);
						methods_found.insert(method);
						break;
					}
				}

				if (line >= 0) {
					continue;
				}

				// There is a chance that the method is inherited from another script.
				bool found_inherited_function = false;
				Ref<Script> inherited_script = script->get_base_script();
				while (inherited_script.is_valid()) {
					if (inherited_script->has_method(method)) {
						found_inherited_function = true;
						break;
					}

					inherited_script = inherited_script->get_base_script();
				}

				if (!found_inherited_function) {
					missing_connections.push_back(connection);
				}
			}
		}
	}

	// Add override icons to methods.
	methods_found.clear();
	for (int i = 0; i < functions.size(); i++) {
		String raw_name = functions[i].get_slicec(':', 0);
		StringName name = StringName(raw_name);
		if (methods_found.has(name)) {
			continue;
		}

		// Account for inner classes by stripping the class names from the method,
		// starting from the right since our inner class might be inside of another inner class.
		int pos = raw_name.rfind_char('.');
		if (pos != -1) {
			name = raw_name.substr(pos + 1);
		}

		String found_base_class;
		StringName base_class = script->get_instance_base_type();
		Ref<Script> inherited_script = script->get_base_script();
		while (inherited_script.is_valid()) {
			if (inherited_script->has_method(name)) {
				found_base_class = "script:" + inherited_script->get_path();
				break;
			}

			base_class = inherited_script->get_instance_base_type();
			inherited_script = inherited_script->get_base_script();
		}

		if (found_base_class.is_empty()) {
			while (base_class) {
				List<MethodInfo> methods;
				ClassDB::get_method_list(base_class, &methods, true);
				for (const MethodInfo &mi : methods) {
					if (mi.name == name) {
						found_base_class = "builtin:" + base_class;
						break;
					}
				}

				ClassDB::ClassInfo *base_class_ptr = ClassDB::classes.getptr(base_class)->inherits_ptr;
				if (base_class_ptr == nullptr) {
					break;
				}
				base_class = base_class_ptr->gdtype->get_name();
			}
		}

		if (!found_base_class.is_empty()) {
			int line = functions[i].get_slicec(':', 1).to_int() - 1;

			Dictionary line_meta = text_edit->get_line_gutter_metadata(line, connection_gutter);
			if (line_meta.is_empty()) {
				// Add override icon to gutter.
				line_meta["type"] = "inherits";
				line_meta["method"] = name;
				line_meta["base_class"] = found_base_class;
				text_edit->set_line_gutter_icon(line, connection_gutter, get_parent_control()->get_editor_theme_icon(SNAME("MethodOverride")));
				text_edit->set_line_gutter_clickable(line, connection_gutter, true);
			} else {
				// If method is also connected to signal, then merge icons and keep the click behavior of the slot.
				text_edit->set_line_gutter_icon(line, connection_gutter, get_parent_control()->get_editor_theme_icon(SNAME("MethodOverrideAndSlot")));
			}

			methods_found.insert(StringName(raw_name));
		}
	}
}

void ScriptTextEditor::_update_gutter_indexes() {
	for (int i = 0; i < code_editor->get_text_editor()->get_gutter_count(); i++) {
		if (code_editor->get_text_editor()->get_gutter_name(i) == "connection_gutter") {
			connection_gutter = i;
			continue;
		}

		if (code_editor->get_text_editor()->get_gutter_name(i) == "line_numbers") {
			line_number_gutter = i;
			continue;
		}
	}
}

void ScriptTextEditor::_gutter_clicked(int p_line, int p_gutter) {
	if (p_gutter != connection_gutter) {
		return;
	}

	Dictionary meta = code_editor->get_text_editor()->get_line_gutter_metadata(p_line, p_gutter);
	String type = meta.get("type", "");
	if (type.is_empty()) {
		return;
	}

	// All types currently need a method name.
	String method = meta.get("method", "");
	if (method.is_empty()) {
		return;
	}

	if (type == "connection") {
		Node *base = get_tree()->get_edited_scene_root();
		if (!base) {
			return;
		}

		Ref<Script> script = edited_res;
		Vector<Node *> nodes = _find_all_node_for_script(base, base, script);
		connection_info_dialog->popup_connections(method, nodes);
	} else if (type == "inherits") {
		String base_class_raw = meta["base_class"];
		PackedStringArray base_class_split = base_class_raw.split(":", true, 1);

		if (base_class_split[0] == "script") {
			// Go to function declaration.
			Ref<Script> base_script = ResourceLoader::load(base_class_split[1]);
			ERR_FAIL_COND(base_script.is_null());
			emit_signal(SNAME("go_to_method"), base_script, method);
		} else if (base_class_split[0] == "builtin") {
			// Open method documentation.
			emit_signal(SNAME("go_to_help"), "class_method:" + base_class_split[1] + ":" + method);
		}
	}
}

bool ScriptTextEditor::_edit_option(int p_op) {
	CodeEdit *tx = code_editor->get_text_editor();
	tx->apply_ime();

	switch (p_op) {
		case EDIT_CREATE_CODE_REGION: {
			tx->create_code_region();
		} break;
		case EDIT_TOGGLE_COMMENT: {
			_edit_option_toggle_inline_comment();
		} break;
		case EDIT_COMPLETE: {
			tx->request_code_completion(true);
		} break;
		case EDIT_AUTO_INDENT: {
			String text = tx->get_text();
			Ref<Script> scr = edited_res;
			if (scr.is_null()) {
				return true;
			}

			tx->begin_complex_operation();
			tx->begin_multicaret_edit();
			int begin = tx->get_line_count() - 1, end = 0;
			if (tx->has_selection()) {
				// Auto indent all lines that have a caret or selection on it.
				Vector<Point2i> line_ranges = tx->get_line_ranges_from_carets();
				for (Point2i line_range : line_ranges) {
					scr->get_language()->auto_indent_code(text, line_range.x, line_range.y);
					if (line_range.x < begin) {
						begin = line_range.x;
					}
					if (line_range.y > end) {
						end = line_range.y;
					}
				}
			} else {
				// Auto indent entire text.
				begin = 0;
				end = tx->get_line_count() - 1;
				scr->get_language()->auto_indent_code(text, begin, end);
			}

			// Apply auto indented code.
			Vector<String> lines = text.split("\n");
			for (int i = begin; i <= end; ++i) {
				tx->set_line(i, lines[i]);
			}

			tx->end_multicaret_edit();
			tx->end_complex_operation();
		} break;
		case EDIT_PICK_COLOR: {
			color_panel->popup();
		} break;
		case EDIT_EVALUATE: {
			Expression expression;
			tx->begin_complex_operation();
			for (int caret_idx = 0; caret_idx < tx->get_caret_count(); caret_idx++) {
				Vector<String> lines = tx->get_selected_text(caret_idx).split("\n");
				PackedStringArray results;

				for (int i = 0; i < lines.size(); i++) {
					const String &line = lines[i];
					String whitespace = line.substr(0, line.size() - line.strip_edges(true, false).size()); // Extract the whitespace at the beginning.
					if (expression.parse(line) == OK) {
						Variant result = expression.execute(Array(), Variant(), false, true);
						if (expression.get_error_text().is_empty()) {
							results.push_back(whitespace + result.get_construct_string());
						} else {
							results.push_back(line);
						}
					} else {
						results.push_back(line);
					}
				}
				tx->insert_text_at_caret(String("\n").join(results), caret_idx);
			}
			tx->end_complex_operation();
		} break;
		case SEARCH_LOCATE_FUNCTION: {
			quick_open->popup_dialog(get_functions());
		} break;
		case DEBUG_TOGGLE_BREAKPOINT: {
			Vector<int> sorted_carets = tx->get_sorted_carets();
			int last_line = -1;
			for (const int &c : sorted_carets) {
				int from = tx->get_selection_from_line(c);
				from += from == last_line ? 1 : 0;
				int to = tx->get_selection_to_line(c);
				if (to < from) {
					continue;
				}
				// Check first if there's any lines with breakpoints in the selection.
				bool selection_has_breakpoints = false;
				for (int line = from; line <= to; line++) {
					if (tx->is_line_breakpointed(line)) {
						selection_has_breakpoints = true;
						break;
					}
				}

				// Set breakpoint on caret or remove all bookmarks from the selection.
				if (!selection_has_breakpoints) {
					if (tx->get_caret_line(c) != last_line) {
						tx->set_line_as_breakpoint(tx->get_caret_line(c), true);
					}
				} else {
					for (int line = from; line <= to; line++) {
						tx->set_line_as_breakpoint(line, false);
					}
				}
				last_line = to;
			}
		} break;
		case DEBUG_REMOVE_ALL_BREAKPOINTS: {
			PackedInt32Array bpoints = tx->get_breakpointed_lines();

			for (int i = 0; i < bpoints.size(); i++) {
				int line = bpoints[i];
				bool dobreak = !tx->is_line_breakpointed(line);
				tx->set_line_as_breakpoint(line, dobreak);
				EditorDebuggerNode::get_singleton()->set_breakpoint(edited_res->get_path(), line + 1, dobreak);
			}
		} break;
		case DEBUG_GOTO_NEXT_BREAKPOINT: {
			PackedInt32Array bpoints = tx->get_breakpointed_lines();
			if (bpoints.is_empty()) {
				return true;
			}

			int current_line = tx->get_caret_line();
			int bpoint_idx = 0;
			if (current_line < (int)bpoints[bpoints.size() - 1]) {
				while (bpoint_idx < bpoints.size() && bpoints[bpoint_idx] <= current_line) {
					bpoint_idx++;
				}
			}
			code_editor->goto_line_centered(bpoints[bpoint_idx]);
		} break;
		case DEBUG_GOTO_PREV_BREAKPOINT: {
			PackedInt32Array bpoints = tx->get_breakpointed_lines();
			if (bpoints.is_empty()) {
				return true;
			}

			int current_line = tx->get_caret_line();
			int bpoint_idx = bpoints.size() - 1;
			if (current_line > (int)bpoints[0]) {
				while (bpoint_idx >= 0 && bpoints[bpoint_idx] >= current_line) {
					bpoint_idx--;
				}
			}
			code_editor->goto_line_centered(bpoints[bpoint_idx]);
		} break;
		case SHOW_TOOLTIP_AT_CARET: {
			_show_symbol_tooltip(tx->get_word_under_caret(), tx->get_caret_line(), tx->get_caret_column(), true);
		} break;
		case HELP_CONTEXTUAL: {
			String text = tx->get_selected_text(0);
			if (text.is_empty()) {
				text = tx->get_word_under_caret(0);
			}
			if (!text.is_empty()) {
				emit_signal(SNAME("request_help"), text);
			}
		} break;
		case LOOKUP_SYMBOL: {
			String text = tx->get_word_under_caret(0);
			if (text.is_empty()) {
				text = tx->get_selected_text(0);
			}
			if (!text.is_empty()) {
				_lookup_symbol(text, tx->get_caret_line(0), tx->get_caret_column(0));
			}
		} break;
		default: {
			if (TextEditorBase::_edit_option(p_op)) {
				return true;
			}
			if (p_op >= EditorContextMenuPlugin::BASE_ID) {
				EditorContextMenuPluginManager::get_singleton()->activate_custom_option(EditorContextMenuPlugin::CONTEXT_SLOT_SCRIPT_EDITOR_CODE, p_op, tx);
			}
		}
	}
	return true;
}

void ScriptTextEditor::_edit_option_toggle_inline_comment() {
	Ref<Script> script = edited_res;
	if (script.is_null()) {
		return;
	}

	String delimiter = "#";

	for (const String &script_delimiter : script->get_language()->get_comment_delimiters()) {
		if (!script_delimiter.contains_char(' ')) {
			delimiter = script_delimiter;
			break;
		}
	}

	code_editor->toggle_inline_comment(delimiter);
}

void ScriptTextEditor::_notification(int p_what) {
	switch (p_what) {
		case NOTIFICATION_TRANSLATION_CHANGED: {
			if (is_ready() && is_visible_in_tree()) {
				_update_errors();
				_update_warnings();
			}
		} break;

		case NOTIFICATION_THEME_CHANGED:
			if (!editor_enabled) {
				break;
			}
			if (is_visible_in_tree()) {
				_update_warnings();
				_update_errors();
				_update_background_color();
			}
			[[fallthrough]];
		case NOTIFICATION_ENTER_TREE: {
			code_editor->get_text_editor()->set_gutter_width(connection_gutter, code_editor->get_text_editor()->get_line_height());
			Ref<Font> code_font = get_theme_font("font", "CodeEdit");
			inline_color_options->add_theme_font_override("font", code_font);
			inline_color_options->get_popup()->add_theme_font_override("font", code_font);
		} break;
		case NOTIFICATION_DRAG_END: {
			drag_info_label->hide();
		} break;
		case NOTIFICATION_VISIBILITY_CHANGED: {
			if (!is_visible()) {
				previous_line = -1;
			}
		} break;
	}
}

Control *ScriptTextEditor::get_edit_menu() {
	if (!edit_menus) {
		edit_menus = memnew(EditMenusSTE);
	}
	return edit_menus;
}

PackedInt32Array ScriptTextEditor::get_breakpoints() {
	return code_editor->get_text_editor()->get_breakpointed_lines();
}

void ScriptTextEditor::set_breakpoint(int p_line, bool p_enabled) {
	code_editor->get_text_editor()->set_line_as_breakpoint(p_line, p_enabled);
}

void ScriptTextEditor::clear_breakpoints() {
	code_editor->get_text_editor()->clear_breakpointed_lines();
}

Variant ScriptTextEditor::get_drag_data_fw(const Point2 &p_point, Control *p_from) {
	return Variant();
}

bool ScriptTextEditor::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const {
	Dictionary d = p_data;
	if (d.has("type") &&
			(String(d["type"]) == "resource" ||
					String(d["type"]) == "files" ||
					String(d["type"]) == "nodes" ||
					String(d["type"]) == "obj_property" ||
					String(d["type"]) == "files_and_dirs")) {
		_set_drop_info_text(d);
		return true;
	}

	return false;
}

void ScriptTextEditor::_set_drop_info_text(const Dictionary &p_info) const {
	if (drag_info_label->is_visible() || !EDITOR_GET("text_editor/appearance/drag_and_drop_info/show_drag_and_drop_info")) {
		return;
	}

	String text;
	String c = keycode_get_string((Key)KeyModifierMask::CMD_OR_CTRL);
	bool drop_as_uid = bool(EDITOR_GET("text_editor/behavior/files/drop_preload_resources_as_uid"));
	String default_drop_option = drop_as_uid ? TTR("UID path") : TTR("file path");
	String alternate_drop_option = drop_as_uid ? TTR("file path") : TTR("UID path");

	const String type = p_info.get("type", "");

	if (type == "files" || type == "files_and_dirs" || type == "resource") {
		Array files = p_info.get("files", Array());
		text = TTRN("Drop file path.", "Drop file paths.", files.size()) +
				"\n" + vformat(TTRN("Hold %s: Add const preload by %s.", "Hold %s: Add const preloads by %s.", files.size()), c, default_drop_option) +
				"\n" + vformat(TTRN("Hold %s+Shift: Add const preload by %s.", "Hold %s+Shift: Add const preloads by %s.", files.size()), c, alternate_drop_option) +
				"\n" + TTRN("Hold Alt: Add @export var pointing to the resource.", "Hold Alt: Add @export vars pointing to the resources.", files.size());
	} else if (type == "nodes") {
		Array nodes = p_info["nodes"];
		text = TTRN("Drop node path.", "Drop node paths.", nodes.size()) +
				"\n" + vformat(TTRN("Hold %s: Add @onready var pointing to the node path.", "Hold %s: Add @onready vars pointing to the node paths.", nodes.size()), c) +
				"\n" + TTRN("Hold Alt: Add @export var pointing to the node.", "Hold Alt: Add @export vars pointing to the nodes.", nodes.size());
	} else if (type == "obj_property") {
		text = TTR("Drop property path.");
	}

	if (!text.is_empty()) {
		drag_info_label->show();
		drag_info_label->set_text(text);
		drag_info_label->set_anchors_and_offsets_preset(Control::PRESET_BOTTOM_RIGHT, Control::PRESET_MODE_MINSIZE, 20 * EDSCALE);
	}
}

static Node *_find_script_node(Node *p_current_node, const Ref<Script> &script) {
	if (p_current_node->get_script() == script) {
		return p_current_node;
	}

	for (int i = 0; i < p_current_node->get_child_count(); i++) {
		Node *n = _find_script_node(p_current_node->get_child(i), script);
		if (n) {
			return n;
		}
	}

	return nullptr;
}

static String _quote_drop_data(const String &str) {
	// This function prepares a string for being "dropped" into the script editor.
	// The string can be a resource path, node path or property name.

	const bool using_single_quotes = EDITOR_GET("text_editor/completion/use_single_quotes");

	String escaped = str.c_escape();

	// If string is double quoted, there is no need to escape single quotes.
	// We can revert the extra escaping added in c_escape().
	if (!using_single_quotes) {
		escaped = escaped.replace("\\'", "\'");
	}

	return escaped.quote(using_single_quotes ? "'" : "\"");
}

static String _get_dropped_resource_as_member(const Ref<Resource> &p_resource, bool p_create_field, bool p_allow_uid) {
	String path = p_resource->get_path();
	if (p_allow_uid) {
		ResourceUID::ID id = ResourceLoader::get_resource_uid(path);
		if (id != ResourceUID::INVALID_ID) {
			path = ResourceUID::get_singleton()->id_to_text(id);
		}
	}
	const bool is_script = ClassDB::is_parent_class(p_resource->get_class(), "Script");

	if (!p_create_field) {
		return vformat("preload(%s)", _quote_drop_data(path));
	}

	String variable_name = p_resource->get_name();
	if (variable_name.is_empty()) {
		variable_name = p_resource->get_path().get_file().get_basename();
	}

	if (is_script) {
		variable_name = variable_name.to_pascal_case().validate_unicode_identifier();
	} else {
		variable_name = variable_name.to_snake_case().to_upper().validate_unicode_identifier();
	}
	return vformat("const %s = preload(%s)", variable_name, _quote_drop_data(path));
}

String ScriptTextEditor::_get_dropped_resource_as_exported_member(const Ref<Resource> &p_resource, const Vector<ObjectID> &p_script_instance_obj_ids) {
	String variable_name = p_resource->get_name();
	if (variable_name.is_empty()) {
		variable_name = p_resource->get_path().get_file().get_basename();
	}

	variable_name = variable_name.to_snake_case().validate_unicode_identifier();

	StringName class_name = p_resource->get_class();
	Ref<Script> resource_script = p_resource->get_script();

	if (resource_script.is_valid()) {
		StringName global_resource_script_name = resource_script->get_global_name();
		if (!global_resource_script_name.is_empty()) {
			class_name = global_resource_script_name;
		}
	}

	for (ObjectID obj_id : p_script_instance_obj_ids) {
		pending_dragged_exports.push_back(DraggedExport{ obj_id, variable_name, p_resource, class_name });
	}

	return vformat("@export var %s: %s", variable_name, class_name);
}

void ScriptTextEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) {
	Dictionary d = p_data;

	CodeEdit *te = code_editor->get_text_editor();
	Point2i 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);
	int drop_at_line = pos.y;
	int drop_at_column = pos.x;
	int selection_index = te->get_selection_at_line_column(drop_at_line, drop_at_column);

	bool is_empty_line = false;
	if (selection_index >= 0) {
		// Dropped on a selection, it will be replaced.
		drop_at_line = te->get_selection_from_line(selection_index);
		drop_at_column = te->get_selection_from_column(selection_index);
		is_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();
	}

	Node *scene_root = get_tree()->get_edited_scene_root();

	const bool member_drop_modifier_pressed = Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL);
	const bool export_drop_modifier_pressed = Input::get_singleton()->is_key_pressed(Key::ALT);

	const bool allow_uid = Input::get_singleton()->is_key_pressed(Key::SHIFT) != bool(EDITOR_GET("text_editor/behavior/files/drop_preload_resources_as_uid"));
	const String &line = te->get_line(drop_at_line);

	if (selection_index < 0) {
		is_empty_line = line.is_empty() || te->get_first_non_whitespace_column(drop_at_line) == line.length();
	}

	String text_to_drop;
	bool add_new_line = false;

	const String type = d.get("type", "");
	if (type == "resource") {
		Ref<Resource> resource = d["resource"];
		if (resource.is_null()) {
			return;
		}

		const String &path = resource->get_path();
		if (path.is_empty() || path.ends_with("::")) {
			String 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.");
			EditorToaster::get_singleton()->popup_str(warning, EditorToaster::SEVERITY_ERROR);
			return;
		}

		if (member_drop_modifier_pressed) {
			if (resource->is_built_in()) {
				String warning = TTR("Preloading internal resources is not supported.");
				EditorToaster::get_singleton()->popup_str(warning, EditorToaster::SEVERITY_ERROR);
			} else {
				text_to_drop = _get_dropped_resource_as_member(resource, is_empty_line, allow_uid);
			}
		} else if (export_drop_modifier_pressed) {
			Vector<ObjectID> obj_ids = _get_objects_for_export_assignment();
			text_to_drop = _get_dropped_resource_as_exported_member(resource, obj_ids);

		} else {
			text_to_drop = _quote_drop_data(path);
		}

		if (is_empty_line) {
			text_to_drop += "\n";
		}
	}

	if (type == "files" || type == "files_and_dirs") {
		const PackedStringArray files = d["files"];
		PackedStringArray parts;

		for (const String &path : files) {
			if ((member_drop_modifier_pressed || export_drop_modifier_pressed) && ResourceLoader::exists(path)) {
				Ref<Resource> resource = ResourceLoader::load(path);
				if (resource.is_null()) {
					// Resource exists, but failed to load. We need only path and name, so we can use a dummy Resource instead.
					resource.instantiate();
					resource->set_path_cache(path);
				}

				if (member_drop_modifier_pressed) {
					parts.append(_get_dropped_resource_as_member(resource, is_empty_line, allow_uid));
				} else if (export_drop_modifier_pressed) {
					Vector<ObjectID> obj_ids = _get_objects_for_export_assignment();
					parts.append(_get_dropped_resource_as_exported_member(resource, obj_ids));
				}
			} else {
				parts.append(_quote_drop_data(path));
			}
		}
		String join_string;
		if (is_empty_line) {
			int indent_level = te->get_indent_level(drop_at_line);
			if (te->is_indent_using_spaces()) {
				join_string = "\n" + String(" ").repeat(indent_level);
			} else {
				join_string = "\n" + String("\t").repeat(indent_level / te->get_tab_size());
			}
		} else {
			join_string = ", ";
		}
		text_to_drop = join_string.join(parts);
		if (is_empty_line) {
			text_to_drop += join_string;
		}
	}

	if (type == "nodes") {
		if (!scene_root) {
			EditorNode::get_singleton()->show_warning(TTR("Can't drop nodes without an open scene."));
			return;
		}

		Ref<Script> script = edited_res;
		if (!ClassDB::is_parent_class(script->get_instance_base_type(), "Node")) {
			EditorToaster::get_singleton()->popup_str(vformat(TTR("Can't drop nodes because script '%s' does not inherit Node."), get_name()), EditorToaster::SEVERITY_WARNING);
			return;
		}

		Node *sn = _find_script_node(scene_root, script);
		if (!sn) {
			sn = scene_root;
		}

		Array nodes = d["nodes"];

		if (member_drop_modifier_pressed) {
			const bool use_type = EDITOR_GET("text_editor/completion/add_type_hints");
			add_new_line = !is_empty_line && drop_at_column != 0;

			for (int i = 0; i < nodes.size(); i++) {
				NodePath np = nodes[i];
				Node *node = get_node(np);
				if (!node) {
					continue;
				}

				bool is_unique = node->is_unique_name_in_owner() && (node->get_owner() == sn || node->get_owner() == sn->get_owner());
				String path = is_unique ? String(node->get_name()) : String(sn->get_path_to(node));
				for (const String &segment : path.split("/")) {
					if (!segment.is_valid_unicode_identifier()) {
						path = _quote_drop_data(path);
						break;
					}
				}

				String variable_name = String(node->get_name()).to_snake_case().validate_unicode_identifier();
				if (use_type) {
					StringName custom_class_name;
					Ref<Script> node_script = node->get_script();
					while (node_script.is_valid() && custom_class_name.is_empty()) {
						custom_class_name = node_script->get_global_name();
						node_script = node_script->get_base_script();
					}
					const StringName class_name = custom_class_name.is_empty() ? node->get_class_name() : custom_class_name;
					text_to_drop += vformat("@onready var %s: %s = %c%s", variable_name, class_name, is_unique ? '%' : '$', path);
				} else {
					text_to_drop += vformat("@onready var %s = %c%s", variable_name, is_unique ? '%' : '$', path);
				}
				if (i < nodes.size() - 1) {
					text_to_drop += "\n";
				}
			}

			if (is_empty_line || drop_at_column == 0) {
				text_to_drop += "\n";
			}
		} else if (export_drop_modifier_pressed) {
			Vector<ObjectID> obj_ids = _get_objects_for_export_assignment();

			for (int i = 0; i < nodes.size(); i++) {
				NodePath np = nodes[i];
				Node *node = get_node(np);
				if (!node) {
					continue;
				}

				String variable_name = String(node->get_name()).to_snake_case().validate_unicode_identifier();
				StringName custom_class_name;
				Ref<Script> node_script = node->get_script();
				while (node_script.is_valid() && custom_class_name.is_empty()) {
					custom_class_name = node_script->get_global_name();
					node_script = node_script->get_base_script();
				}
				const StringName class_name = custom_class_name.is_empty() ? node->get_class_name() : custom_class_name;
				text_to_drop += vformat("@export var %s: %s\n", variable_name, class_name);

				for (ObjectID obj_id : obj_ids) {
					pending_dragged_exports.push_back(DraggedExport{ obj_id, variable_name, node, class_name });
				}
			}
		} else {
			for (int i = 0; i < nodes.size(); i++) {
				if (i > 0) {
					text_to_drop += ", ";
				}

				NodePath np = nodes[i];
				Node *node = get_node(np);
				if (!node) {
					continue;
				}

				bool is_unique = node->is_unique_name_in_owner() && (node->get_owner() == sn || node->get_owner() == sn->get_owner());
				String path = is_unique ? String(node->get_name()) : String(sn->get_path_to(node));
				for (const String &segment : path.split("/")) {
					if (!segment.is_valid_ascii_identifier()) {
						path = _quote_drop_data(path);
						break;
					}
				}
				text_to_drop += (is_unique ? "%" : "$") + path;
			}
		}
	}

	if (type == "obj_property") {
		bool add_literal = EDITOR_GET("text_editor/completion/add_node_path_literals");
		text_to_drop = add_literal ? "^" : "";
		// It is unclear whether properties may contain single or double quotes.
		// Assume here that double-quotes may not exist. We are escaping single-quotes if necessary.
		text_to_drop += _quote_drop_data(String(d["property"]));
	}

	if (text_to_drop.is_empty()) {
		return;
	}

	// Remove drag caret before any actions so it is not included in undo.
	te->remove_drag_caret();
	te->begin_complex_operation();
	if (selection_index >= 0) {
		te->delete_selection(selection_index);
	}
	te->remove_secondary_carets();
	te->deselect();
	te->set_caret_line(drop_at_line);
	if (add_new_line) {
		te->set_caret_column(te->get_line(drop_at_line).length());
		text_to_drop = "\n" + text_to_drop;
	} else {
		te->set_caret_column(drop_at_column);
	}
	te->insert_text_at_caret(text_to_drop);
	te->end_complex_operation();
	te->grab_focus();

	drag_info_label->hide();
}

Vector<ObjectID> ScriptTextEditor::_get_objects_for_export_assignment() const {
	Vector<ObjectID> objects;
	Node *scene_root = get_tree()->get_edited_scene_root();
	Ref<Script> script = edited_res;
	bool assign_export_variables = scene_root && ClassDB::is_parent_class(script->get_instance_base_type(), "Node");

	if (!assign_export_variables) {
		return objects;
	}

	EditorInspector *inspector = EditorInterface::get_singleton()->get_inspector();
	if (inspector) {
		Object *edited_object = inspector->get_edited_object();
		Node *node_edit = Object::cast_to<Node>(edited_object);
		MultiNodeEdit *multi_node_edit = Object::cast_to<MultiNodeEdit>(edited_object);

		if (node_edit != nullptr) {
			if (node_edit->get_script() == script) {
				objects.push_back(node_edit->get_instance_id());
			}
		} else if (multi_node_edit != nullptr) {
			Node *es = EditorNode::get_singleton()->get_edited_scene();
			for (int i = 0; i < multi_node_edit->get_node_count(); i++) {
				NodePath np = multi_node_edit->get_node(i);
				Node *node = es->get_node(np);
				if (node->get_script() == script) {
					objects.push_back(node->get_instance_id());
				}
			}
		}
	}

	// In case there is no current editor selection/editor selection does not contain this script,
	// it often still makes sense to try to assign the export variable,
	// so we default to the first node with the script we find in the scene.
	if (objects.is_empty()) {
		Node *sn = _find_script_node(scene_root, script);
		if (sn) {
			objects.push_back(sn->get_instance_id());
		}
	}

	return objects;
}

void ScriptTextEditor::_assign_dragged_export_variables() {
	ERR_FAIL_COND(pending_dragged_exports.is_empty());

	bool export_variable_set = false;

	for (int i = pending_dragged_exports.size() - 1; i >= 0; i--) {
		const DraggedExport &dragged_export = pending_dragged_exports[i];
		Object *obj = ObjectDB::get_instance(dragged_export.obj_id);
		if (!obj) {
			WARN_PRINT("Object not found, can't assign export variable.");
			pending_dragged_exports.remove_at(i);
			continue;
		}

		ScriptInstance *si = obj->get_script_instance();
		if (!si) {
			WARN_PRINT("Script on " + obj->to_string() + " does not exist anymore, can't assign export variable.");
			pending_dragged_exports.remove_at(i);
			continue;
		}

		bool script_has_errors = false;
		String scr_path = si->get_script()->get_path();

		for (const ScriptLanguage::ScriptError &error : errors) {
			if (error.path == scr_path) {
				script_has_errors = true;
				break;
			}
		}

		if (!script_has_errors) {
			bool success = false;
			List<PropertyInfo> properties;
			si->get_property_list(&properties);
			for (const PropertyInfo &pi : properties) {
				if (pi.name == dragged_export.variable_name && pi.hint_string == dragged_export.class_name) {
					success = si->set(dragged_export.variable_name, dragged_export.value);
					break;
				}
			}

			if (success) {
				export_variable_set = true;
			}
			pending_dragged_exports.remove_at(i);
		}
	}

	if (export_variable_set) {
		EditorInterface::get_singleton()->mark_scene_as_unsaved();
	}
}

void ScriptTextEditor::_text_edit_gui_input(const Ref<InputEvent> &p_ev) {
	Ref<InputEventMouseButton> mb = p_ev;
	Ref<InputEventKey> k = p_ev;
	Point2 local_pos;
	bool create_menu = false;

	CodeEdit *tx = code_editor->get_text_editor();
	if (mb.is_valid() && mb->get_button_index() == MouseButton::RIGHT && mb->is_pressed()) {
		local_pos = mb->get_global_position() - tx->get_global_position();
		create_menu = true;
	} else if (k.is_valid() && k->is_action("ui_menu", true)) {
		tx->adjust_viewport_to_caret(0);
		local_pos = tx->get_caret_draw_pos(0);
		create_menu = true;
	}

	if (create_menu) {
		tx->apply_ime();

		Point2i pos = tx->get_line_column_at_pos(local_pos);
		int mouse_line = pos.y;
		int mouse_column = pos.x;

		tx->set_move_caret_on_right_click_enabled(EDITOR_GET("text_editor/behavior/navigation/move_caret_on_right_click"));
		int selection_clicked = -1;
		if (tx->is_move_caret_on_right_click_enabled()) {
			selection_clicked = tx->get_selection_at_line_column(mouse_line, mouse_column, true);
			if (selection_clicked < 0) {
				tx->deselect();
				tx->remove_secondary_carets();
				selection_clicked = 0;
				tx->set_caret_line(mouse_line, false, false, -1);
				tx->set_caret_column(mouse_column);
			}
		}

		String word_at_pos = tx->get_lookup_word(mouse_line, mouse_column);
		if (word_at_pos.is_empty()) {
			word_at_pos = tx->get_word_under_caret(selection_clicked);
		}
		if (word_at_pos.is_empty()) {
			word_at_pos = tx->get_selected_text(selection_clicked);
		}

		bool has_color = (word_at_pos == "Color");
		bool foldable = tx->can_fold_line(mouse_line) || tx->is_line_folded(mouse_line);
		bool open_docs = false;
		bool goto_definition = false;

		if (ScriptServer::is_global_class(word_at_pos) || word_at_pos.is_resource_file()) {
			open_docs = true;
		} else {
			Ref<Script> script = edited_res;
			Node *base = get_tree()->get_edited_scene_root();
			if (base) {
				base = _find_node_for_script(base, base, script);
			}
			ScriptLanguage::LookupResult result;
			if (script->get_language()->lookup_code(tx->get_text_for_symbol_lookup(), word_at_pos, script->get_path(), base, result) == OK) {
				open_docs = true;
			}
		}

		if (has_color) {
			String line = tx->get_line(mouse_line);
			color_position.x = mouse_line;

			int begin = -1;
			int end = -1;
			enum EXPRESSION_PATTERNS {
				NOT_PARSED,
				RGBA_PARAMETER, // Color(float,float,float) or Color(float,float,float,float)
				COLOR_NAME, // Color.COLOR_NAME
			} expression_pattern = NOT_PARSED;

			for (int i = mouse_column; i < line.length(); i++) {
				if (line[i] == '(') {
					if (expression_pattern == NOT_PARSED) {
						begin = i;
						expression_pattern = RGBA_PARAMETER;
					} else {
						// Method call or '(' appearing twice.
						expression_pattern = NOT_PARSED;

						break;
					}
				} else if (expression_pattern == RGBA_PARAMETER && line[i] == ')' && end < 0) {
					end = i + 1;

					break;
				} else if (expression_pattern == NOT_PARSED && line[i] == '.') {
					begin = i;
					expression_pattern = COLOR_NAME;
				} else if (expression_pattern == COLOR_NAME && end < 0 && (line[i] == ' ' || line[i] == '\t')) {
					// Including '.' and spaces.
					continue;
				} else if (expression_pattern == COLOR_NAME && !(line[i] == '_' || ('A' <= line[i] && line[i] <= 'Z'))) {
					end = i;

					break;
				}
			}

			switch (expression_pattern) {
				case RGBA_PARAMETER: {
					color_args = line.substr(begin, end - begin);
					String stripped = color_args.remove_chars(" \t()");
					PackedFloat64Array color = stripped.split_floats(",");
					if (color.size() > 2) {
						float alpha = color.size() > 3 ? color[3] : 1.0f;
						color_picker->set_pick_color(Color(color[0], color[1], color[2], alpha));
					}
				} break;
				case COLOR_NAME: {
					if (end < 0) {
						end = line.length();
					}
					color_args = line.substr(begin, end - begin);
					const String color_name = color_args.remove_chars(" \t.");
					const int color_index = Color::find_named_color(color_name);
					if (0 <= color_index) {
						const Color color_constant = Color::get_named_color(color_index);
						color_picker->set_pick_color(color_constant);
					} else {
						has_color = false;
					}
				} break;
				default:
					has_color = false;
					break;
			}
			if (has_color) {
				color_panel->set_position(get_screen_position() + local_pos);
				color_position.y = begin;
				color_position.z = end;
			}
		}
		_make_context_menu(tx->has_selection(), has_color, foldable, open_docs, goto_definition, local_pos);
	}
}

void ScriptTextEditor::_color_changed(const Color &p_color) {
	String new_args;
	const int decimals = 3;
	if (p_color.a == 1.0f) {
		new_args = String("(" + String::num(p_color.r, decimals) + ", " + String::num(p_color.g, decimals) + ", " + String::num(p_color.b, decimals) + ")");
	} else {
		new_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) + ")");
	}

	String line = code_editor->get_text_editor()->get_line(color_position.x);
	String 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);

	color_args = new_args;
	code_editor->get_text_editor()->begin_complex_operation();
	code_editor->get_text_editor()->set_line(color_position.x, line_with_replaced_args);
	code_editor->get_text_editor()->end_complex_operation();
}

void 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) {
	TextEditorBase::_make_context_menu(p_selection, p_foldable, p_position, false);
	context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_comment"), EDIT_TOGGLE_COMMENT);
	_popup_move_item(EDIT_UNINDENT, context_menu);

	if (p_selection) {
		context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/evaluate_selection"), EDIT_EVALUATE);
		_popup_move_item(EDIT_TO_LOWERCASE, context_menu);
		context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/create_code_region"), EDIT_CREATE_CODE_REGION);
		_popup_move_item(EDIT_EVALUATE, context_menu);
	}

	if (p_color || p_open_docs || p_goto_definition) {
		context_menu->add_separator();
		if (p_open_docs) {
			context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/goto_symbol"), LOOKUP_SYMBOL);
		}
		if (p_color) {
			context_menu->add_item(TTRC("Pick Color"), EDIT_PICK_COLOR);
		}
	}

	const PackedStringArray paths = { String(code_editor->get_text_editor()->get_path()) };
	EditorContextMenuPluginManager::get_singleton()->add_options_from_plugins(context_menu, EditorContextMenuPlugin::CONTEXT_SLOT_SCRIPT_EDITOR_CODE, paths);

	_show_context_menu(p_position);
}

void ScriptTextEditor::register_editor() {
	ED_SHORTCUT("script_text_editor/move_up", TTRC("Move Up"), KeyModifierMask::ALT | Key::UP);
	ED_SHORTCUT("script_text_editor/move_down", TTRC("Move Down"), KeyModifierMask::ALT | Key::DOWN);
	ED_SHORTCUT("script_text_editor/delete_line", TTRC("Delete Line"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::K);
	ED_SHORTCUT("script_text_editor/join_lines", TTRC("Join Lines"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::J);

	// Leave these at zero, same can be accomplished with tab/shift-tab, including selection.
	// The next/previous in history shortcut in this case makes a lot more sense.

	ED_SHORTCUT("script_text_editor/indent", TTRC("Indent"), Key::NONE);
	ED_SHORTCUT("script_text_editor/unindent", TTRC("Unindent"), KeyModifierMask::SHIFT | Key::TAB);
	ED_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) });
	ED_SHORTCUT("script_text_editor/toggle_fold_line", TTRC("Fold/Unfold Line"), KeyModifierMask::ALT | Key::F);
	ED_SHORTCUT_OVERRIDE("script_text_editor/toggle_fold_line", "macos", KeyModifierMask::CTRL | KeyModifierMask::META | Key::F);
	ED_SHORTCUT("script_text_editor/fold_all_lines", TTRC("Fold All Lines"), Key::NONE);
	ED_SHORTCUT("script_text_editor/create_code_region", TTRC("Create Code Region"), KeyModifierMask::ALT | Key::R);
	ED_SHORTCUT("script_text_editor/unfold_all_lines", TTRC("Unfold All Lines"), Key::NONE);
	ED_SHORTCUT("script_text_editor/duplicate_selection", TTRC("Duplicate Selection"), KeyModifierMask::SHIFT | KeyModifierMask::CTRL | Key::D);
	ED_SHORTCUT_OVERRIDE("script_text_editor/duplicate_selection", "macos", KeyModifierMask::SHIFT | KeyModifierMask::META | Key::C);
	ED_SHORTCUT("script_text_editor/duplicate_lines", TTRC("Duplicate Lines"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | Key::DOWN);
	ED_SHORTCUT_OVERRIDE("script_text_editor/duplicate_lines", "macos", KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::DOWN);
	ED_SHORTCUT("script_text_editor/evaluate_selection", TTRC("Evaluate Selection"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::E);
	ED_SHORTCUT("script_text_editor/toggle_word_wrap", TTRC("Toggle Word Wrap"), KeyModifierMask::ALT | Key::Z);
	ED_SHORTCUT("script_text_editor/trim_trailing_whitespace", TTRC("Trim Trailing Whitespace"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | Key::T);
	ED_SHORTCUT("script_text_editor/trim_final_newlines", TTRC("Trim Final Newlines"), Key::NONE);
	ED_SHORTCUT("script_text_editor/convert_indent_to_spaces", TTRC("Convert Indent to Spaces"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::Y);
	ED_SHORTCUT("script_text_editor/convert_indent_to_tabs", TTRC("Convert Indent to Tabs"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::I);
	ED_SHORTCUT("script_text_editor/auto_indent", TTRC("Auto Indent"), KeyModifierMask::CMD_OR_CTRL | Key::I);

	ED_SHORTCUT_AND_COMMAND("script_text_editor/find", TTRC("Find..."), KeyModifierMask::CMD_OR_CTRL | Key::F);

	ED_SHORTCUT("script_text_editor/find_next", TTRC("Find Next"), Key::F3);
	ED_SHORTCUT_OVERRIDE("script_text_editor/find_next", "macos", KeyModifierMask::META | Key::G);

	ED_SHORTCUT("script_text_editor/find_previous", TTRC("Find Previous"), KeyModifierMask::SHIFT | Key::F3);
	ED_SHORTCUT_OVERRIDE("script_text_editor/find_previous", "macos", KeyModifierMask::META | KeyModifierMask::SHIFT | Key::G);

	ED_SHORTCUT_AND_COMMAND("script_text_editor/replace", TTRC("Replace..."), KeyModifierMask::CTRL | Key::R);
	ED_SHORTCUT_OVERRIDE("script_text_editor/replace", "macos", KeyModifierMask::ALT | KeyModifierMask::META | Key::F);

	ED_SHORTCUT("script_text_editor/replace_in_files", TTRC("Replace in Files..."), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::R);

	ED_SHORTCUT("script_text_editor/show_tooltip", TTRC("Show Tooltip"), KeyModifierMask::ALT | Key::SLASH, true);
	ED_SHORTCUT("script_text_editor/contextual_help", TTRC("Contextual Help"), KeyModifierMask::ALT | Key::F1);
	ED_SHORTCUT_OVERRIDE("script_text_editor/contextual_help", "macos", KeyModifierMask::ALT | KeyModifierMask::SHIFT | Key::SPACE);

	ED_SHORTCUT("script_text_editor/toggle_bookmark", TTRC("Toggle Bookmark"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | Key::B);

	ED_SHORTCUT("script_text_editor/goto_next_bookmark", TTRC("Go to Next Bookmark"), KeyModifierMask::CMD_OR_CTRL | Key::B);
	ED_SHORTCUT_OVERRIDE("script_text_editor/goto_next_bookmark", "macos", KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | KeyModifierMask::ALT | Key::B);

	ED_SHORTCUT("script_text_editor/goto_previous_bookmark", TTRC("Go to Previous Bookmark"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::B);
	ED_SHORTCUT("script_text_editor/remove_all_bookmarks", TTRC("Remove All Bookmarks"), Key::NONE);

	ED_SHORTCUT("script_text_editor/goto_function", TTRC("Go to Function..."), KeyModifierMask::ALT | KeyModifierMask::CTRL | Key::F);
	ED_SHORTCUT_OVERRIDE("script_text_editor/goto_function", "macos", KeyModifierMask::CTRL | KeyModifierMask::META | Key::J);

	ED_SHORTCUT("script_text_editor/goto_line", TTRC("Go to Line..."), KeyModifierMask::CMD_OR_CTRL | Key::G);
	ED_SHORTCUT_OVERRIDE("script_text_editor/goto_line", "macos", KeyModifierMask::CMD_OR_CTRL | Key::L);
	ED_SHORTCUT("script_text_editor/goto_symbol", TTRC("Lookup Symbol"));

	ED_SHORTCUT("script_text_editor/toggle_breakpoint", TTRC("Toggle Breakpoint"), Key::F9);
	ED_SHORTCUT_OVERRIDE("script_text_editor/toggle_breakpoint", "macos", KeyModifierMask::META | KeyModifierMask::SHIFT | Key::B);

	ED_SHORTCUT("script_text_editor/remove_all_breakpoints", TTRC("Remove All Breakpoints"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::F9);
	// Using Control for these shortcuts even on macOS because Command+Comma is taken for opening Editor Settings.
	ED_SHORTCUT("script_text_editor/goto_next_breakpoint", TTRC("Go to Next Breakpoint"), KeyModifierMask::CTRL | Key::PERIOD);
	ED_SHORTCUT("script_text_editor/goto_previous_breakpoint", TTRC("Go to Previous Breakpoint"), KeyModifierMask::CTRL | Key::COMMA);

	ScriptEditor::register_create_script_editor_function(create_editor);
}

void ScriptTextEditor::_enable_code_editor() {
	code_editor->connect("show_errors_panel", callable_mp(this, &ScriptTextEditor::_show_errors_panel));
	code_editor->connect("show_warnings_panel", callable_mp(this, &ScriptTextEditor::_show_warnings_panel));
	code_editor->get_text_editor()->connect("symbol_lookup", callable_mp(this, &ScriptTextEditor::_lookup_symbol));
	code_editor->get_text_editor()->connect("symbol_hovered", callable_mp(this, &ScriptTextEditor::_show_symbol_tooltip).bind(false));
	code_editor->get_text_editor()->connect("symbol_validate", callable_mp(this, &ScriptTextEditor::_validate_symbol));
	code_editor->get_text_editor()->connect("gutter_added", callable_mp(this, &ScriptTextEditor::_update_gutter_indexes));
	code_editor->get_text_editor()->connect("gutter_removed", callable_mp(this, &ScriptTextEditor::_update_gutter_indexes));
	code_editor->get_text_editor()->connect("gutter_clicked", callable_mp(this, &ScriptTextEditor::_gutter_clicked));
	code_editor->get_text_editor()->connect("_fold_line_updated", callable_mp(this, &ScriptTextEditor::_update_background_color));
	_update_gutter_indexes();

	editor_box->add_child(errors_panel);
	errors_panel->connect("meta_clicked", callable_mp(this, &ScriptTextEditor::_error_clicked));

	add_child(color_panel);

	color_picker = memnew(ColorPicker);
	color_picker->set_deferred_mode(true);
	color_picker->connect("color_changed", callable_mp(this, &ScriptTextEditor::_color_changed));
	color_panel->connect("about_to_popup", callable_mp(EditorNode::get_singleton(), &EditorNode::setup_color_picker).bind(color_picker));

	color_panel->add_child(color_picker);

	quick_open = memnew(ScriptEditorQuickOpen);
	quick_open->set_title(TTRC("Go to Function"));
	quick_open->connect("goto_line", callable_mp(this, &ScriptTextEditor::_goto_line));
	add_child(quick_open);

	add_child(connection_info_dialog);
}

ScriptTextEditor::ScriptTextEditor() {
	code_editor->set_code_complete_func(_code_complete_scripts, this);
	code_editor->get_text_editor()->set_draw_breakpoints_gutter(true);
	code_editor->get_text_editor()->set_draw_executing_lines_gutter(true);
	code_editor->get_text_editor()->connect("breakpoint_toggled", callable_mp(this, &ScriptTextEditor::_breakpoint_toggled));
	code_editor->get_text_editor()->connect("caret_changed", callable_mp(this, &ScriptTextEditor::_on_caret_moved));
	code_editor->connect("navigation_preview_ended", callable_mp(this, &ScriptTextEditor::_on_caret_moved));

	connection_gutter = 1;
	code_editor->get_text_editor()->add_gutter(connection_gutter);
	code_editor->get_text_editor()->set_gutter_name(connection_gutter, "connection_gutter");
	code_editor->get_text_editor()->set_gutter_draw(connection_gutter, false);
	code_editor->get_text_editor()->set_gutter_overwritable(connection_gutter, true);
	code_editor->get_text_editor()->set_gutter_type(connection_gutter, TextEdit::GUTTER_TYPE_ICON);

	errors_panel = memnew(RichTextLabel);
	errors_panel->set_custom_minimum_size(Size2(0, 100 * EDSCALE));
	errors_panel->set_h_size_flags(SIZE_EXPAND_FILL);
	errors_panel->set_meta_underline(true);
	errors_panel->set_selection_enabled(true);
	errors_panel->set_context_menu_enabled(true);
	errors_panel->set_focus_mode(FOCUS_CLICK);
	errors_panel->hide();

	drag_info_label = memnew(Label);
	drag_info_label->set_anchors_preset(Control::PRESET_BOTTOM_RIGHT);
	drag_info_label->set_focus_mode(FOCUS_ACCESSIBILITY);
	drag_info_label->add_theme_constant_override("line_spacing", 0);

	Ref<StyleBoxFlat> drag_info_sb;
	drag_info_sb.instantiate();
	drag_info_sb->set_border_width_all(2 * EDSCALE);
	drag_info_sb->set_corner_radius_all(5 * EDSCALE);
	drag_info_sb->set_corner_detail(5);
	drag_info_sb->set_expand_margin_all(5);
	drag_info_label->add_theme_style_override(CoreStringName(normal), drag_info_sb);

	code_editor->get_text_editor()->connect(SceneStringName(mouse_exited), callable_mp(this, &ScriptTextEditor::_on_mouse_exited));
	code_editor->get_text_editor()->add_child(drag_info_label);
	drag_info_label->hide();

	code_editor->get_text_editor()->set_symbol_lookup_on_click_enabled(true);
	code_editor->get_text_editor()->set_symbol_tooltip_on_hover_enabled(true);

	color_panel = memnew(PopupPanel);

	inline_color_popup = memnew(PopupPanel);
	add_child(inline_color_popup);

	inline_color_picker = memnew(ColorPicker);
	inline_color_picker->set_mouse_filter(MOUSE_FILTER_STOP);
	inline_color_picker->set_deferred_mode(true);
	inline_color_picker->set_hex_visible(false);
	inline_color_picker->connect("color_changed", callable_mp(this, &ScriptTextEditor::_picker_color_changed));
	inline_color_popup->add_child(inline_color_picker);

	inline_color_options = memnew(OptionButton);
	inline_color_options->set_h_size_flags(SIZE_FILL);
	inline_color_options->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS);
	inline_color_options->set_fit_to_longest_item(false);
	inline_color_options->connect("item_selected", callable_mp(this, &ScriptTextEditor::_update_color_text).unbind(1));
	inline_color_picker->get_slider_container()->add_sibling(inline_color_options);

	connection_info_dialog = memnew(ConnectionInfoDialog);

	update_settings();

	SET_DRAG_FORWARDING_GCD(code_editor->get_text_editor(), ScriptTextEditor);
}

ScriptTextEditor::~ScriptTextEditor() {
	if (!editor_enabled) {
		memdelete(errors_panel);
		memdelete(color_panel);
		memdelete(connection_info_dialog);
	}
}

ScriptEditorBase *ScriptTextEditor::create_editor(const Ref<Resource> &p_resource) {
	if (Object::cast_to<Script>(*p_resource)) {
		return memnew(ScriptTextEditor);
	}
	return nullptr;
}
