Merge pull request #77776 from dalexeev/code-editor-copy-error-button

CodeEditor: Make possible to select and copy error text
This commit is contained in:
Rémi Verschelde 2025-06-12 22:48:08 +02:00
commit f9475a892e
No known key found for this signature in database
GPG Key ID: C3336907360768E1
7 changed files with 150 additions and 81 deletions

View File

@ -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<String> splits = p_error.split("\n");
String trimmed_error = String("\n").join(splits.slice(0, 2));
error->set_text(trimmed_error + "...");
} else {
error->set_text(p_error);
_update_error_content_height();
if (p_error.is_empty()) {
error->set_default_cursor_shape(CURSOR_ARROW);
} else {
error->set_default_cursor_shape(CURSOR_POINTING_HAND);
}
}
void CodeTextEditor::_update_error_content_height() {
float margin_height = 0;
const Ref<StyleBox> 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<Font> 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<Control>(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<Font> 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<StyleBox> 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<Control>(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);

View File

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

View File

@ -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<StyleBox> 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));

View File

@ -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<uint64_t, Variant> &p_values, const String &p_field);

View File

@ -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 {

View File

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

View File

@ -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<ShaderLanguage::FilePosition> include_positions = sl.get_include_positions();
String err_text;
int err_line;
Vector<ShaderLanguage::FilePosition> 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);
}
}