From db9b8ff0037cf44312bccbbc293bb7ec0e6d8e17 Mon Sep 17 00:00:00 2001 From: Danil Alexeev Date: Wed, 7 May 2025 00:16:03 +0300 Subject: [PATCH] CodeEditor: Make possible to select and copy error text --- editor/code_editor.cpp | 106 ++++++++++++--------- editor/code_editor.h | 17 ++-- editor/debugger/script_editor_debugger.cpp | 47 +++++++-- editor/debugger/script_editor_debugger.h | 3 +- editor/plugins/script_text_editor.cpp | 8 +- editor/plugins/text_editor.cpp | 2 +- editor/plugins/text_shader_editor.cpp | 48 +++++++--- 7 files changed, 150 insertions(+), 81 deletions(-) diff --git a/editor/code_editor.cpp b/editor/code_editor.cpp index 9347af6f97c..f0d80d654a9 100644 --- a/editor/code_editor.cpp +++ b/editor/code_editor.cpp @@ -39,9 +39,13 @@ #include "editor/plugins/script_editor_plugin.h" #include "editor/themes/editor_scale.h" #include "editor/themes/editor_theme_manager.h" +#include "scene/gui/check_box.h" +#include "scene/gui/label.h" #include "scene/gui/line_edit.h" #include "scene/gui/menu_button.h" +#include "scene/gui/rich_text_label.h" #include "scene/gui/separator.h" +#include "scene/main/timer.h" #include "scene/resources/font.h" void GotoLinePopup::popup_find_line(CodeTextEditor *p_text_editor) { @@ -716,10 +720,6 @@ bool FindReplaceBar::is_selection_only() const { return selection_only->is_pressed(); } -void FindReplaceBar::set_error(const String &p_label) { - emit_signal(SNAME("error"), p_label); -} - void FindReplaceBar::set_text_edit(CodeTextEditor *p_text_editor) { if (p_text_editor == base_text_editor) { return; @@ -749,8 +749,6 @@ void FindReplaceBar::set_text_edit(CodeTextEditor *p_text_editor) { void FindReplaceBar::_bind_methods() { ClassDB::bind_method("_search_current", &FindReplaceBar::search_current); - - ADD_SIGNAL(MethodInfo("error")); } FindReplaceBar::FindReplaceBar() { @@ -1187,7 +1185,6 @@ void CodeTextEditor::set_find_replace_bar(FindReplaceBar *p_bar) { find_replace_bar = p_bar; find_replace_bar->set_text_edit(this); - find_replace_bar->connect("error", callable_mp(error, &Label::set_text)); } void CodeTextEditor::remove_find_replace_bar() { @@ -1195,7 +1192,6 @@ void CodeTextEditor::remove_find_replace_bar() { return; } - find_replace_bar->disconnect("error", callable_mp(error, &Label::set_text)); find_replace_bar = nullptr; } @@ -1516,20 +1512,35 @@ Variant CodeTextEditor::get_navigation_state() { } void CodeTextEditor::set_error(const String &p_error) { - // Trim the error message if it is more than 2 lines long. - if (p_error.count("\n") >= 2) { - Vector splits = p_error.split("\n"); - String trimmed_error = String("\n").join(splits.slice(0, 2)); - error->set_text(trimmed_error + "..."); + error->set_text(p_error); + + _update_error_content_height(); + + if (p_error.is_empty()) { + error->set_default_cursor_shape(CURSOR_ARROW); } else { - error->set_text(p_error); + error->set_default_cursor_shape(CURSOR_POINTING_HAND); + } +} + +void CodeTextEditor::_update_error_content_height() { + float margin_height = 0; + const Ref style = error->get_theme_stylebox(CoreStringName(normal)); + if (style.is_valid()) { + margin_height += style->get_content_margin(SIDE_TOP) + style->get_content_margin(SIDE_BOTTOM); } - if (!p_error.is_empty()) { - error->set_default_cursor_shape(CURSOR_POINTING_HAND); - } else { - error->set_default_cursor_shape(CURSOR_ARROW); + const float content_height = margin_height + error->get_content_height(); + + float content_max_height = margin_height; + for (int i = 0; i < 3; i++) { + if (i >= error->get_line_count()) { + break; + } + content_max_height += error->get_line_height(i); } + + error->set_custom_minimum_size(Size2(0, CLAMP(content_height, 0, content_max_height))); } void CodeTextEditor::set_error_pos(int p_line, int p_column) { @@ -1559,28 +1570,36 @@ void CodeTextEditor::goto_error() { void CodeTextEditor::_update_text_editor_theme() { emit_signal(SNAME("load_theme_settings")); - error_button->set_button_icon(get_editor_theme_icon(SNAME("StatusError"))); - warning_button->set_button_icon(get_editor_theme_icon(SNAME("NodeWarning"))); - - Ref status_bar_font = get_theme_font(SNAME("status_source"), EditorStringName(EditorFonts)); - int status_bar_font_size = get_theme_font_size(SNAME("status_source_size"), EditorStringName(EditorFonts)); - - int count = status_bar->get_child_count(); - for (int i = 0; i < count; i++) { - Control *n = Object::cast_to(status_bar->get_child(i)); - if (n) { - n->add_theme_font_override(SceneStringName(font), status_bar_font); - n->add_theme_font_size_override(SceneStringName(font_size), status_bar_font_size); - } - } - + const Ref status_bar_font = get_theme_font(SNAME("status_source"), EditorStringName(EditorFonts)); + const int status_bar_font_size = get_theme_font_size(SNAME("status_source_size"), EditorStringName(EditorFonts)); const Color &error_color = get_theme_color(SNAME("error_color"), EditorStringName(Editor)); const Color &warning_color = get_theme_color(SNAME("warning_color"), EditorStringName(Editor)); + const Ref label_stylebox = get_theme_stylebox(SNAME("normal"), SNAME("Label")); // Empty stylebox. - error->add_theme_color_override(SceneStringName(font_color), error_color); + error->begin_bulk_theme_override(); + error->add_theme_font_override(SNAME("normal_font"), status_bar_font); + error->add_theme_font_size_override(SNAME("normal_font_size"), status_bar_font_size); + error->add_theme_color_override(SNAME("default_color"), error_color); + error->add_theme_style_override(SNAME("normal"), label_stylebox); + error->end_bulk_theme_override(); + + error_button->set_button_icon(get_editor_theme_icon(SNAME("StatusError"))); error_button->add_theme_color_override(SceneStringName(font_color), error_color); + + warning_button->set_button_icon(get_editor_theme_icon(SNAME("NodeWarning"))); warning_button->add_theme_color_override(SceneStringName(font_color), warning_color); + const int child_count = status_bar->get_child_count(); + for (int i = 0; i < child_count; i++) { + Control *child = Object::cast_to(status_bar->get_child(i)); + if (child) { + child->begin_bulk_theme_override(); + child->add_theme_font_override(SceneStringName(font), status_bar_font); + child->add_theme_font_size_override(SceneStringName(font_size), status_bar_font_size); + child->end_bulk_theme_override(); + } + } + _update_font_ligatures(); } @@ -1909,19 +1928,16 @@ CodeTextEditor::CodeTextEditor() { toggle_files_button->hide(); // Error - ScrollContainer *scroll = memnew(ScrollContainer); - scroll->set_h_size_flags(SIZE_EXPAND_FILL); - scroll->set_v_size_flags(SIZE_EXPAND_FILL); - scroll->set_vertical_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED); - status_bar->add_child(scroll); - - error = memnew(Label); - error->set_focus_mode(FOCUS_ACCESSIBILITY); + error = memnew(RichTextLabel); + error->set_use_bbcode(true); + error->set_selection_enabled(true); + error->set_context_menu_enabled(true); error->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); - error->set_v_size_flags(SIZE_EXPAND | SIZE_SHRINK_CENTER); - error->set_mouse_filter(MOUSE_FILTER_STOP); - scroll->add_child(error); + error->set_h_size_flags(SIZE_EXPAND_FILL); + error->set_v_size_flags(SIZE_SHRINK_CENTER); error->connect(SceneStringName(gui_input), callable_mp(this, &CodeTextEditor::_error_pressed)); + error->connect(SceneStringName(resized), callable_mp(this, &CodeTextEditor::_update_error_content_height)); + status_bar->add_child(error); // Errors error_button = memnew(Button); diff --git a/editor/code_editor.h b/editor/code_editor.h index ad78647dcab..7b6d1316261 100644 --- a/editor/code_editor.h +++ b/editor/code_editor.h @@ -31,17 +31,17 @@ #pragma once #include "scene/gui/box_container.h" -#include "scene/gui/button.h" -#include "scene/gui/check_box.h" #include "scene/gui/code_edit.h" #include "scene/gui/dialogs.h" -#include "scene/gui/label.h" -#include "scene/gui/popup.h" -#include "scene/main/timer.h" -class MenuButton; +class Button; +class CheckBox; class CodeTextEditor; +class Label; class LineEdit; +class MenuButton; +class RichTextLabel; +class Timer; class GotoLinePopup : public PopupPanel { GDCLASS(GotoLinePopup, PopupPanel); @@ -138,7 +138,6 @@ public: bool is_case_sensitive() const; bool is_whole_words() const; bool is_selection_only() const; - void set_error(const String &p_label); void set_text_edit(CodeTextEditor *p_text_editor); @@ -183,7 +182,7 @@ class CodeTextEditor : public VBoxContainer { float zoom_factor = 1.0f; - Label *error = nullptr; + RichTextLabel *error = nullptr; int error_line; int error_column; @@ -211,6 +210,8 @@ class CodeTextEditor : public VBoxContainer { void _zoom_out(); void _zoom_to(float p_zoom_factor); + void _update_error_content_height(); + void _error_button_pressed(); void _warning_button_pressed(); void _set_show_errors_panel(bool p_show); diff --git a/editor/debugger/script_editor_debugger.cpp b/editor/debugger/script_editor_debugger.cpp index 6d35c1adf2c..1a86c248979 100644 --- a/editor/debugger/script_editor_debugger.cpp +++ b/editor/debugger/script_editor_debugger.cpp @@ -643,6 +643,7 @@ void ScriptEditorDebugger::_msg_error(uint64_t p_thread_id, const Array &p_data) // item with the original error condition. error_title += oe.error_descr.is_empty() ? oe.error : oe.error_descr; error->set_text(1, error_title); + error->set_autowrap_mode(1, TextServer::AUTOWRAP_WORD_SMART); tooltip += " " + error_title + "\n"; // Find the language of the error's source file. @@ -980,16 +981,20 @@ void ScriptEditorDebugger::_init_parse_message_handlers() { void ScriptEditorDebugger::_set_reason_text(const String &p_reason, MessageType p_type) { switch (p_type) { case MESSAGE_ERROR: - reason->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("error_color"), EditorStringName(Editor))); + reason->add_theme_color_override(SNAME("default_color"), get_theme_color(SNAME("error_color"), EditorStringName(Editor))); break; case MESSAGE_WARNING: - reason->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("warning_color"), EditorStringName(Editor))); + reason->add_theme_color_override(SNAME("default_color"), get_theme_color(SNAME("warning_color"), EditorStringName(Editor))); break; default: - reason->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("success_color"), EditorStringName(Editor))); + reason->add_theme_color_override(SNAME("default_color"), get_theme_color(SNAME("success_color"), EditorStringName(Editor))); + break; } + reason->set_text(p_reason); + _update_reason_content_height(); + const PackedInt32Array boundaries = TS->string_get_word_breaks(p_reason, "", 80); PackedStringArray lines; for (int i = 0; i < boundaries.size(); i += 2) { @@ -1001,6 +1006,26 @@ void ScriptEditorDebugger::_set_reason_text(const String &p_reason, MessageType reason->set_tooltip_text(String("\n").join(lines)); } +void ScriptEditorDebugger::_update_reason_content_height() { + float margin_height = 0; + const Ref style = reason->get_theme_stylebox(CoreStringName(normal)); + if (style.is_valid()) { + margin_height += style->get_content_margin(SIDE_TOP) + style->get_content_margin(SIDE_BOTTOM); + } + + const float content_height = margin_height + reason->get_content_height(); + + float content_max_height = margin_height; + for (int i = 0; i < 3; i++) { + if (i >= reason->get_line_count()) { + break; + } + content_max_height += reason->get_line_height(i); + } + + reason->set_custom_minimum_size(Size2(0, CLAMP(content_height, 0, content_max_height))); +} + void ScriptEditorDebugger::_notification(int p_what) { switch (p_what) { case NOTIFICATION_ENTER_TREE: { @@ -1031,7 +1056,8 @@ void ScriptEditorDebugger::_notification(int p_what) { vmem_export->set_button_icon(get_editor_theme_icon(SNAME("Save"))); search->set_right_icon(get_editor_theme_icon(SNAME("Search"))); - reason->add_theme_color_override(SceneStringName(font_color), get_theme_color(SNAME("error_color"), EditorStringName(Editor))); + reason->add_theme_color_override(SNAME("default_color"), get_theme_color(SNAME("error_color"), EditorStringName(Editor))); + reason->add_theme_style_override(SNAME("normal"), get_theme_stylebox(SNAME("normal"), SNAME("Label"))); // Empty stylebox. TreeItem *error_root = error_tree->get_root(); if (error_root) { @@ -1245,6 +1271,7 @@ void ScriptEditorDebugger::stop() { peer.unref(); reason->set_text(""); reason->set_tooltip_text(""); + reason->set_custom_minimum_size(Size2(0, 0)); } node_path_cache.clear(); @@ -1966,14 +1993,14 @@ ScriptEditorDebugger::ScriptEditorDebugger() { HBoxContainer *hbc = memnew(HBoxContainer); vbc->add_child(hbc); - reason = memnew(Label); + reason = memnew(RichTextLabel); reason->set_focus_mode(FOCUS_ACCESSIBILITY); - reason->set_text(""); - hbc->add_child(reason); + reason->set_selection_enabled(true); + reason->set_context_menu_enabled(true); reason->set_h_size_flags(SIZE_EXPAND_FILL); - reason->set_autowrap_mode(TextServer::AUTOWRAP_WORD_SMART); - reason->set_max_lines_visible(3); - reason->set_mouse_filter(Control::MOUSE_FILTER_PASS); + reason->set_v_size_flags(SIZE_SHRINK_CENTER); + reason->connect(SceneStringName(resized), callable_mp(this, &ScriptEditorDebugger::_update_reason_content_height)); + hbc->add_child(reason); hbc->add_child(memnew(VSeparator)); diff --git a/editor/debugger/script_editor_debugger.h b/editor/debugger/script_editor_debugger.h index a2a9f2721e6..beef460c7fc 100644 --- a/editor/debugger/script_editor_debugger.h +++ b/editor/debugger/script_editor_debugger.h @@ -118,7 +118,7 @@ private: TabContainer *tabs = nullptr; - Label *reason = nullptr; + RichTextLabel *reason = nullptr; Button *skip_breakpoints = nullptr; Button *ignore_error_breaks = nullptr; @@ -231,6 +231,7 @@ private: void _parse_message(const String &p_msg, uint64_t p_thread_id, const Array &p_data); void _set_reason_text(const String &p_reason, MessageType p_type); + void _update_reason_content_height(); void _update_buttons_state(); void _remote_object_selected(ObjectID p_object); void _remote_objects_edited(const String &p_prop, const TypedDictionary &p_values, const String &p_field); diff --git a/editor/plugins/script_text_editor.cpp b/editor/plugins/script_text_editor.cpp index beb36b6bb73..ce46c04f268 100644 --- a/editor/plugins/script_text_editor.cpp +++ b/editor/plugins/script_text_editor.cpp @@ -819,10 +819,12 @@ void ScriptTextEditor::_validate_script() { } if (errors.size() > 0) { - // TRANSLATORS: Script error pointing to a line and column number. - String error_text = vformat(TTR("Error at (%d, %d):"), errors.front()->get().line, errors.front()->get().column) + " " + errors.front()->get().message; + const int line = errors.front()->get().line; + const int column = errors.front()->get().column; + const String message = errors.front()->get().message.replace("[", "[lb]"); + const String error_text = vformat(TTR("Error at ([hint=Line %d, column %d]%d, %d[/hint]):"), line, column, line, column) + " " + message; code_editor->set_error(error_text); - code_editor->set_error_pos(errors.front()->get().line - 1, errors.front()->get().column - 1); + code_editor->set_error_pos(line - 1, column - 1); } script_is_valid = false; } else { diff --git a/editor/plugins/text_editor.cpp b/editor/plugins/text_editor.cpp index 61ceba89459..b8cee7b74de 100644 --- a/editor/plugins/text_editor.cpp +++ b/editor/plugins/text_editor.cpp @@ -200,7 +200,7 @@ void TextEditor::_validate_script() { code_editor->set_error(""); if (json_file->parse(te->get_text(), true) != OK) { - code_editor->set_error(json_file->get_error_message()); + code_editor->set_error(json_file->get_error_message().replace("[", "[lb]")); code_editor->set_error_pos(json_file->get_error_line(), 0); te->set_line_background_color(code_editor->get_error_pos().x, EDITOR_GET("text_editor/theme/highlighting/mark_color")); } diff --git a/editor/plugins/text_shader_editor.cpp b/editor/plugins/text_shader_editor.cpp index f16a2960876..99b50d8dd00 100644 --- a/editor/plugins/text_shader_editor.cpp +++ b/editor/plugins/text_shader_editor.cpp @@ -515,28 +515,35 @@ void ShaderTextEditor::_validate_script() { set_error_count(0); if (last_compile_result != OK) { - //preprocessor error + // Preprocessor error. ERR_FAIL_COND(err_positions.is_empty()); - String err_text = error_pp; - int err_line = err_positions.front()->get().line; + String err_text; + const int err_line = err_positions.front()->get().line; if (err_positions.size() == 1) { - // Error in main file - err_text = "error(" + itos(err_line) + "): " + err_text; + // Error in the main file. + const String message = error_pp.replace("[", "[lb]"); + + err_text = vformat(TTR("Error at line %d:"), err_line) + " " + message; } else { - err_text = "error(" + itos(err_line) + ") in include " + err_positions.back()->get().file.get_file() + ":" + itos(err_positions.back()->get().line) + ": " + err_text; + // Error in an included file. + const String inc_file = err_positions.back()->get().file.get_file(); + const int inc_line = err_positions.back()->get().line; + const String message = error_pp.replace("[", "[lb]"); + + err_text = vformat(TTR("Error at line %d in include %s:%d:"), err_line, inc_file, inc_line) + " " + message; set_error_count(err_positions.size() - 1); } set_error(err_text); set_error_pos(err_line - 1, 0); + for (int i = 0; i < get_text_editor()->get_line_count(); i++) { get_text_editor()->set_line_background_color(i, Color(0, 0, 0, 0)); } get_text_editor()->set_line_background_color(err_line - 1, marked_line_color); set_warning_count(0); - } else { ShaderLanguage sl; @@ -579,21 +586,33 @@ void ShaderTextEditor::_validate_script() { last_compile_result = sl.compile(code, comp_info); if (last_compile_result != OK) { + Vector include_positions = sl.get_include_positions(); + String err_text; int err_line; - Vector include_positions = sl.get_include_positions(); if (include_positions.size() > 1) { - //error is in an include + // Error in an included file. err_line = include_positions[0].line; - err_text = "error(" + itos(err_line) + ") in include " + include_positions[include_positions.size() - 1].file + ":" + itos(include_positions[include_positions.size() - 1].line) + ": " + sl.get_error_text(); + + const String inc_file = include_positions[include_positions.size() - 1].file; + const int inc_line = include_positions[include_positions.size() - 1].line; + const String message = sl.get_error_text().replace("[", "[lb]"); + + err_text = vformat(TTR("Error at line %d in include %s:%d:"), err_line, inc_file, inc_line) + " " + message; set_error_count(include_positions.size() - 1); } else { + // Error in the main file. err_line = sl.get_error_line(); - err_text = "error(" + itos(err_line) + "): " + sl.get_error_text(); + + const String message = sl.get_error_text().replace("[", "[lb]"); + + err_text = vformat(TTR("Error at line %d:"), err_line) + " " + message; set_error_count(0); } + set_error(err_text); set_error_pos(err_line - 1, 0); + get_text_editor()->set_line_background_color(err_line - 1, marked_line_color); } else { set_error(""); @@ -624,9 +643,12 @@ void ShaderTextEditor::_update_warning_panel() { for (const ShaderWarning &w : warnings) { if (warning_count == 0) { if (saved_treat_warning_as_errors) { - String error_text = "error(" + itos(w.get_line()) + "): " + w.get_message() + " " + TTR("Warnings should be fixed to prevent errors."); - set_error_pos(w.get_line() - 1, 0); + const String message = (w.get_message() + " " + TTR("Warnings should be fixed to prevent errors.")).replace("[", "[lb]"); + const String error_text = vformat(TTR("Error at line %d:"), w.get_line()) + " " + message; + set_error(error_text); + set_error_pos(w.get_line() - 1, 0); + get_text_editor()->set_line_background_color(w.get_line() - 1, marked_line_color); } }