From 67dce301aa79448b29dc418c9122ca08c6f96413 Mon Sep 17 00:00:00 2001 From: Jean-Michel Bernard Date: Sun, 12 Mar 2023 17:48:37 +0100 Subject: [PATCH] Add code region folding to CodeEdit --- doc/classes/CodeEdit.xml | 52 +++++ doc/classes/EditorSettings.xml | 3 + editor/editor_settings.cpp | 1 + editor/editor_themes.cpp | 7 + editor/icons/CodeRegionFoldDownArrow.svg | 1 + editor/icons/CodeRegionFoldedRightArrow.svg | 1 + editor/plugins/script_text_editor.cpp | 18 +- editor/plugins/script_text_editor.h | 2 + scene/gui/code_edit.cpp | 236 +++++++++++++++++--- scene/gui/code_edit.h | 16 ++ scene/gui/text_edit.cpp | 2 + scene/gui/text_edit.h | 1 + scene/theme/default_theme.cpp | 3 + scene/theme/icons/folder_down_arrow.svg | 1 + scene/theme/icons/folder_right_arrow.svg | 1 + tests/scene/test_code_edit.h | 188 ++++++++++++++++ 16 files changed, 506 insertions(+), 27 deletions(-) create mode 100644 editor/icons/CodeRegionFoldDownArrow.svg create mode 100644 editor/icons/CodeRegionFoldedRightArrow.svg create mode 100644 scene/theme/icons/folder_down_arrow.svg create mode 100644 scene/theme/icons/folder_right_arrow.svg diff --git a/doc/classes/CodeEdit.xml b/doc/classes/CodeEdit.xml index 0e829127f27..d9146cf604b 100644 --- a/doc/classes/CodeEdit.xml +++ b/doc/classes/CodeEdit.xml @@ -137,6 +137,15 @@ Values of [code]-1[/code] convert the entire text. + + + + Creates a new code region with the selection. At least one single line comment delimiter have to be defined (see [method add_comment_delimiter]). + A code region is a part of code that is highlighted when folded and can help organize your script. + Code region start and end tags can be customized (see [method set_code_region_tags]). + Code regions are delimited using start and end tags (respectively [code]region[/code] and [code]endregion[/code] by default) preceded by one line comment delimiter. (eg. [code]#region[/code] and [code]#endregion[/code]) + + @@ -200,6 +209,18 @@ Gets the index of the current selected completion option. + + + + Returns the code region end tag (without comment delimiter). + + + + + + Returns the code region start tag (without comment delimiter). + + @@ -326,6 +347,20 @@ Returns whether the line at the specified index is breakpointed or not. + + + + + Returns whether the line at the specified index is a code region end. + + + + + + + Returns whether the line at the specified index is a code region start. + + @@ -382,6 +417,14 @@ Sets if the code hint should draw below the text. + + + + + + Sets the code region start and end tags (without comment delimiter). + + @@ -629,6 +672,9 @@ [Color] of the executing icon for executing lines. + + [Color] of background line highlight for folded code region. + Sets the font [Color]. @@ -693,12 +739,18 @@ Sets a custom [Texture2D] to draw in the line folding gutter when a line can be folded. + + Sets a custom [Texture2D] to draw in the line folding gutter when a code region can be folded. + Icon to draw in the executing gutter for executing lines. Sets a custom [Texture2D] to draw in the line folding gutter when a line is folded and can be unfolded. + + Sets a custom [Texture2D] to draw in the line folding gutter when a code region is folded and can be unfolded. + Sets a custom [Texture2D] to draw at the end of a folded line. diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index aa3bf3c5354..5a0cb9fc5ee 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -959,6 +959,9 @@ The script editor's color for the debugger's executing line icon (displayed in the gutter). + + The script editor's background line highlighting color for folded code region. + The script editor's function call color. [b]Note:[/b] When using the GDScript syntax highlighter, this is replaced by the function definition color configured in the syntax theme for function definitions (e.g. [code]func _ready():[/code]). diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp index e042d8570fe..f12972ce871 100644 --- a/editor/editor_settings.cpp +++ b/editor/editor_settings.cpp @@ -848,6 +848,7 @@ void EditorSettings::_load_godot2_text_editor_theme() { _initial_set("text_editor/theme/highlighting/breakpoint_color", Color(0.9, 0.29, 0.3)); _initial_set("text_editor/theme/highlighting/executing_line_color", Color(0.98, 0.89, 0.27)); _initial_set("text_editor/theme/highlighting/code_folding_color", Color(0.8, 0.8, 0.8, 0.8)); + _initial_set("text_editor/theme/highlighting/folded_code_region_color", Color(0.68, 0.46, 0.77, 0.2)); _initial_set("text_editor/theme/highlighting/search_result_color", Color(0.05, 0.25, 0.05, 1)); _initial_set("text_editor/theme/highlighting/search_result_border_color", Color(0.41, 0.61, 0.91, 0.38)); } diff --git a/editor/editor_themes.cpp b/editor/editor_themes.cpp index 311e532e637..54fd8d63af4 100644 --- a/editor/editor_themes.cpp +++ b/editor/editor_themes.cpp @@ -208,6 +208,8 @@ void EditorColorMap::create() { add_conversion_exception("GuiSpace"); add_conversion_exception("CodeFoldedRightArrow"); add_conversion_exception("CodeFoldDownArrow"); + add_conversion_exception("CodeRegionFoldedRightArrow"); + add_conversion_exception("CodeRegionFoldDownArrow"); add_conversion_exception("TextEditorPlay"); add_conversion_exception("Breakpoint"); } @@ -2088,6 +2090,7 @@ Ref create_editor_theme(const Ref p_theme) { const Color breakpoint_color = dark_theme ? error_color : Color(1, 0.27, 0.2, 1); const Color executing_line_color = Color(0.98, 0.89, 0.27); const Color code_folding_color = alpha3; + const Color folded_code_region_color = Color(0.68, 0.46, 0.77, 0.2); const Color search_result_color = alpha1; const Color search_result_border_color = dark_theme ? Color(0.41, 0.61, 0.91, 0.38) : Color(0, 0.4, 1, 0.38); @@ -2128,6 +2131,7 @@ Ref create_editor_theme(const Ref p_theme) { setting->set_initial_value("text_editor/theme/highlighting/breakpoint_color", breakpoint_color, true); setting->set_initial_value("text_editor/theme/highlighting/executing_line_color", executing_line_color, true); setting->set_initial_value("text_editor/theme/highlighting/code_folding_color", code_folding_color, true); + setting->set_initial_value("text_editor/theme/highlighting/folded_code_region_color", folded_code_region_color, true); setting->set_initial_value("text_editor/theme/highlighting/search_result_color", search_result_color, true); setting->set_initial_value("text_editor/theme/highlighting/search_result_border_color", search_result_border_color, true); } else if (text_editor_color_theme == "Godot 2") { @@ -2147,6 +2151,8 @@ Ref create_editor_theme(const Ref p_theme) { theme->set_icon("space", "CodeEdit", theme->get_icon(SNAME("GuiSpace"), EditorStringName(EditorIcons))); theme->set_icon("folded", "CodeEdit", theme->get_icon(SNAME("CodeFoldedRightArrow"), EditorStringName(EditorIcons))); theme->set_icon("can_fold", "CodeEdit", theme->get_icon(SNAME("CodeFoldDownArrow"), EditorStringName(EditorIcons))); + theme->set_icon("folded_code_region", "CodeEdit", theme->get_icon(SNAME("CodeRegionFoldedRightArrow"), EditorStringName(EditorIcons))); + theme->set_icon("can_fold_code_region", "CodeEdit", theme->get_icon(SNAME("CodeRegionFoldDownArrow"), EditorStringName(EditorIcons))); theme->set_icon("executing_line", "CodeEdit", theme->get_icon(SNAME("TextEditorPlay"), EditorStringName(EditorIcons))); theme->set_icon("breakpoint", "CodeEdit", theme->get_icon(SNAME("Breakpoint"), EditorStringName(EditorIcons))); @@ -2172,6 +2178,7 @@ Ref create_editor_theme(const Ref p_theme) { theme->set_color("breakpoint_color", "CodeEdit", EDITOR_GET("text_editor/theme/highlighting/breakpoint_color")); theme->set_color("executing_line_color", "CodeEdit", EDITOR_GET("text_editor/theme/highlighting/executing_line_color")); theme->set_color("code_folding_color", "CodeEdit", EDITOR_GET("text_editor/theme/highlighting/code_folding_color")); + theme->set_color("folded_code_region_color", "CodeEdit", EDITOR_GET("text_editor/theme/highlighting/folded_code_region_color")); theme->set_color("search_result_color", "CodeEdit", EDITOR_GET("text_editor/theme/highlighting/search_result_color")); theme->set_color("search_result_border_color", "CodeEdit", EDITOR_GET("text_editor/theme/highlighting/search_result_border_color")); diff --git a/editor/icons/CodeRegionFoldDownArrow.svg b/editor/icons/CodeRegionFoldDownArrow.svg new file mode 100644 index 00000000000..3bc4f3f73bd --- /dev/null +++ b/editor/icons/CodeRegionFoldDownArrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/editor/icons/CodeRegionFoldedRightArrow.svg b/editor/icons/CodeRegionFoldedRightArrow.svg new file mode 100644 index 00000000000..a9b81d54f30 --- /dev/null +++ b/editor/icons/CodeRegionFoldedRightArrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/editor/plugins/script_text_editor.cpp b/editor/plugins/script_text_editor.cpp index 511e4dfd155..8132fd8e9bb 100644 --- a/editor/plugins/script_text_editor.cpp +++ b/editor/plugins/script_text_editor.cpp @@ -181,10 +181,12 @@ void ScriptTextEditor::_load_theme_settings() { Color updated_marked_line_color = EDITOR_GET("text_editor/theme/highlighting/mark_color"); Color updated_safe_line_number_color = EDITOR_GET("text_editor/theme/highlighting/safe_line_number_color"); + Color updated_folded_code_region_color = EDITOR_GET("text_editor/theme/highlighting/folded_code_region_color"); bool safe_line_number_color_updated = updated_safe_line_number_color != safe_line_number_color; bool marked_line_color_updated = updated_marked_line_color != marked_line_color; - if (safe_line_number_color_updated || marked_line_color_updated) { + bool folded_code_region_color_updated = updated_folded_code_region_color != folded_code_region_color; + if (safe_line_number_color_updated || marked_line_color_updated || folded_code_region_color_updated) { safe_line_number_color = updated_safe_line_number_color; for (int i = 0; i < text_edit->get_line_count(); i++) { if (marked_line_color_updated && text_edit->get_line_background_color(i) == marked_line_color) { @@ -194,8 +196,13 @@ void ScriptTextEditor::_load_theme_settings() { if (safe_line_number_color_updated && text_edit->get_line_gutter_item_color(i, line_number_gutter) != default_line_number_color) { text_edit->set_line_gutter_item_color(i, line_number_gutter, safe_line_number_color); } + + if (folded_code_region_color_updated && text_edit->get_line_background_color(i) == folded_code_region_color) { + text_edit->set_line_background_color(i, updated_folded_code_region_color); + } } marked_line_color = updated_marked_line_color; + folded_code_region_color = updated_folded_code_region_color; } theme_loaded = true; @@ -647,7 +654,8 @@ void ScriptTextEditor::_update_errors() { bool last_is_safe = false; for (int i = 0; i < te->get_line_count(); i++) { if (errors.is_empty()) { - te->set_line_background_color(i, Color(0, 0, 0, 0)); + bool is_folded_code_region = te->is_line_code_region_start(i) && te->is_line_folded(i); + te->set_line_background_color(i, is_folded_code_region ? folded_code_region_color : Color(0, 0, 0, 0)); } else { for (const ScriptLanguage::ScriptError &E : errors) { bool error_line = i == E.line - 1; @@ -1312,6 +1320,9 @@ void ScriptTextEditor::_edit_option(int p_op) { tx->unfold_all_lines(); tx->queue_redraw(); } break; + case EDIT_CREATE_CODE_REGION: { + tx->create_code_region(); + } break; case EDIT_TOGGLE_COMMENT: { _edit_option_toggle_inline_comment(); } break; @@ -2064,6 +2075,7 @@ void ScriptTextEditor::_make_context_menu(bool p_selection, bool p_color, bool p context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/convert_to_uppercase"), EDIT_TO_UPPERCASE); context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/convert_to_lowercase"), EDIT_TO_LOWERCASE); context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/evaluate_selection"), EDIT_EVALUATE); + context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/create_code_region"), EDIT_CREATE_CODE_REGION); } if (p_foldable) { context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_fold_line"), EDIT_TOGGLE_FOLD_LINE); @@ -2178,6 +2190,7 @@ void ScriptTextEditor::_enable_code_editor() { sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_fold_line"), EDIT_TOGGLE_FOLD_LINE); sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/fold_all_lines"), EDIT_FOLD_ALL_LINES); sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/unfold_all_lines"), EDIT_UNFOLD_ALL_LINES); + sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/create_code_region"), EDIT_CREATE_CODE_REGION); sub_menu->connect("id_pressed", callable_mp(this, &ScriptTextEditor::_edit_option)); edit_menu->get_popup()->add_child(sub_menu); edit_menu->get_popup()->add_submenu_item(TTR("Folding"), "folding_menu"); @@ -2373,6 +2386,7 @@ void ScriptTextEditor::register_editor() { ED_SHORTCUT("script_text_editor/toggle_fold_line", TTR("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", TTR("Fold All Lines"), Key::NONE); + ED_SHORTCUT("script_text_editor/create_code_region", TTR("Create Code Region"), KeyModifierMask::ALT | Key::R); ED_SHORTCUT("script_text_editor/unfold_all_lines", TTR("Unfold All Lines"), Key::NONE); ED_SHORTCUT("script_text_editor/duplicate_selection", TTR("Duplicate Selection"), KeyModifierMask::SHIFT | KeyModifierMask::CTRL | Key::D); ED_SHORTCUT_OVERRIDE("script_text_editor/duplicate_selection", "macos", KeyModifierMask::SHIFT | KeyModifierMask::META | Key::C); diff --git a/editor/plugins/script_text_editor.h b/editor/plugins/script_text_editor.h index d275013b916..0efe7d54e3b 100644 --- a/editor/plugins/script_text_editor.h +++ b/editor/plugins/script_text_editor.h @@ -98,6 +98,7 @@ class ScriptTextEditor : public ScriptEditorBase { Color safe_line_number_color = Color(1, 1, 1); Color marked_line_color = Color(1, 1, 1); + Color folded_code_region_color = Color(1, 1, 1); PopupPanel *color_panel = nullptr; ColorPicker *color_picker = nullptr; @@ -133,6 +134,7 @@ class ScriptTextEditor : public ScriptEditorBase { EDIT_TOGGLE_WORD_WRAP, EDIT_TOGGLE_FOLD_LINE, EDIT_FOLD_ALL_LINES, + EDIT_CREATE_CODE_REGION, EDIT_UNFOLD_ALL_LINES, SEARCH_FIND, SEARCH_FIND_NEXT, diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp index 6a5fdc33605..f74d7fb906a 100644 --- a/scene/gui/code_edit.cpp +++ b/scene/gui/code_edit.cpp @@ -1523,7 +1523,19 @@ void CodeEdit::_fold_gutter_draw_callback(int p_line, int p_gutter, Rect2 p_regi p_region.position += Point2(horizontal_padding, vertical_padding); p_region.size -= Point2(horizontal_padding, vertical_padding) * 2; - if (can_fold_line(p_line)) { + bool can_fold = can_fold_line(p_line); + + if (is_line_code_region_start(p_line)) { + Color region_icon_color = theme_cache.folded_code_region_color; + region_icon_color.a = MAX(region_icon_color.a, 0.4f); + if (can_fold) { + theme_cache.can_fold_code_region_icon->draw_rect(get_canvas_item(), p_region, false, region_icon_color); + } else { + theme_cache.folded_code_region_icon->draw_rect(get_canvas_item(), p_region, false, region_icon_color); + } + return; + } + if (can_fold) { theme_cache.can_fold_icon->draw_rect(get_canvas_item(), p_region, false, theme_cache.code_folding_color); return; } @@ -1554,6 +1566,27 @@ bool CodeEdit::can_fold_line(int p_line) const { return false; } + // Check for code region. + if (is_line_code_region_end(p_line)) { + return false; + } + if (is_line_code_region_start(p_line)) { + int region_level = 0; + // Check if there is a valid end region tag. + for (int next_line = p_line + 1; next_line < get_line_count(); next_line++) { + if (is_line_code_region_end(next_line)) { + region_level -= 1; + if (region_level == -1) { + return true; + } + } + if (is_line_code_region_start(next_line)) { + region_level += 1; + } + } + return false; + } + /* Check for full multiline line or block strings / comments. */ int in_comment = is_in_comment(p_line); int in_string = (in_comment == -1) ? is_in_string(p_line) : -1; @@ -1562,13 +1595,13 @@ bool CodeEdit::can_fold_line(int p_line) const { return false; } - int delimter_end_line = get_delimiter_end_position(p_line, get_line(p_line).size() - 1).y; + int delimiter_end_line = get_delimiter_end_position(p_line, get_line(p_line).size() - 1).y; /* No end line, therefore we have a multiline region over the rest of the file. */ - if (delimter_end_line == -1) { + if (delimiter_end_line == -1) { return true; } /* End line is the same therefore we have a block. */ - if (delimter_end_line == p_line) { + if (delimiter_end_line == p_line) { /* Check we are the start of the block. */ if (p_line - 1 >= 0) { if ((in_string != -1 && is_in_string(p_line - 1) != -1) || (in_comment != -1 && is_in_comment(p_line - 1) != -1)) { @@ -1578,7 +1611,7 @@ bool CodeEdit::can_fold_line(int p_line) const { /* Check it continues for at least one line. */ return ((in_string != -1 && is_in_string(p_line + 1) != -1) || (in_comment != -1 && is_in_comment(p_line + 1) != -1)); } - return ((in_string != -1 && is_in_string(delimter_end_line) != -1) || (in_comment != -1 && is_in_comment(delimter_end_line) != -1)); + return ((in_string != -1 && is_in_string(delimiter_end_line) != -1) || (in_comment != -1 && is_in_comment(delimiter_end_line) != -1)); } /* Otherwise check indent levels. */ @@ -1602,31 +1635,51 @@ void CodeEdit::fold_line(int p_line) { const int line_count = get_line_count() - 1; int end_line = line_count; - int in_comment = is_in_comment(p_line); - int in_string = (in_comment == -1) ? is_in_string(p_line) : -1; - if (in_string != -1 || in_comment != -1) { - end_line = get_delimiter_end_position(p_line, get_line(p_line).size() - 1).y; - /* End line is the same therefore we have a block of single line delimiters. */ - if (end_line == p_line) { - for (int i = p_line + 1; i <= line_count; i++) { - if ((in_string != -1 && is_in_string(i) == -1) || (in_comment != -1 && is_in_comment(i) == -1)) { + // Fold code region. + if (is_line_code_region_start(p_line)) { + int region_level = 0; + for (int endregion_line = p_line + 1; endregion_line < get_line_count(); endregion_line++) { + if (is_line_code_region_start(endregion_line)) { + region_level += 1; + } + if (is_line_code_region_end(endregion_line)) { + region_level -= 1; + if (region_level == -1) { + end_line = endregion_line; break; } - end_line = i; } } - } else { - int start_indent = get_indent_level(p_line); - for (int i = p_line + 1; i <= line_count; i++) { - if (get_line(i).strip_edges().size() == 0) { - continue; + set_line_background_color(p_line, theme_cache.folded_code_region_color); + } + + int in_comment = is_in_comment(p_line); + int in_string = (in_comment == -1) ? is_in_string(p_line) : -1; + if (!is_line_code_region_start(p_line)) { + if (in_string != -1 || in_comment != -1) { + end_line = get_delimiter_end_position(p_line, get_line(p_line).size() - 1).y; + // End line is the same therefore we have a block of single line delimiters. + if (end_line == p_line) { + for (int i = p_line + 1; i <= line_count; i++) { + if ((in_string != -1 && is_in_string(i) == -1) || (in_comment != -1 && is_in_comment(i) == -1)) { + break; + } + end_line = i; + } } - if (get_indent_level(i) > start_indent) { - end_line = i; - continue; - } - if (is_in_string(i) == -1 && is_in_comment(i) == -1) { - break; + } else { + int start_indent = get_indent_level(p_line); + for (int i = p_line + 1; i <= line_count; i++) { + if (get_line(i).strip_edges().size() == 0) { + continue; + } + if (get_indent_level(i) > start_indent) { + end_line = i; + continue; + } + if (is_in_string(i) == -1 && is_in_comment(i) == -1) { + break; + } } } } @@ -1677,6 +1730,9 @@ void CodeEdit::unfold_line(int p_line) { break; } _set_line_as_hidden(i, false); + if (is_line_code_region_start(i - 1)) { + set_line_background_color(i - 1, Color(0.0, 0.0, 0.0, 0.0)); + } } queue_redraw(); } @@ -1716,6 +1772,95 @@ TypedArray CodeEdit::get_folded_lines() const { return folded_lines; } +/* Code region */ +void CodeEdit::create_code_region() { + // Abort if there is no selected text. + if (!has_selection()) { + return; + } + // Check that region tag find a comment delimiter and is valid. + if (code_region_start_string.is_empty()) { + WARN_PRINT_ONCE("Cannot create code region without any one line comment delimiters"); + return; + } + begin_complex_operation(); + // Merge selections if selection starts on the same line the previous one ends. + Vector caret_edit_order = get_caret_index_edit_order(); + Vector carets_to_remove; + for (int i = 1; i < caret_edit_order.size(); i++) { + int current_caret = caret_edit_order[i - 1]; + int next_caret = caret_edit_order[i]; + if (get_selection_from_line(current_caret) == get_selection_to_line(next_caret)) { + select(get_selection_from_line(next_caret), get_selection_from_column(next_caret), get_selection_to_line(current_caret), get_selection_to_column(current_caret), next_caret); + carets_to_remove.append(current_caret); + } + } + // Sort and remove backwards to preserve indices. + carets_to_remove.sort(); + for (int i = carets_to_remove.size() - 1; i >= 0; i--) { + remove_caret(carets_to_remove[i]); + } + + // Adding start and end region tags. + int first_region_start = -1; + for (int caret_idx : get_caret_index_edit_order()) { + if (!has_selection(caret_idx)) { + continue; + } + int from_line = get_selection_from_line(caret_idx); + if (first_region_start == -1 || from_line < first_region_start) { + first_region_start = from_line; + } + int to_line = get_selection_to_line(caret_idx); + set_line(to_line, get_line(to_line) + "\n" + code_region_end_string); + insert_line_at(from_line, code_region_start_string + " " + RTR("New Code Region")); + fold_line(from_line); + } + + // Select name of the first region to allow quick edit. + remove_secondary_carets(); + set_caret_line(first_region_start); + int tag_length = code_region_start_string.length() + RTR("New Code Region").length() + 1; + set_caret_column(tag_length); + select(first_region_start, code_region_start_string.length() + 1, first_region_start, tag_length); + + end_complex_operation(); + queue_redraw(); +} + +String CodeEdit::get_code_region_start_tag() const { + return code_region_start_tag; +} + +String CodeEdit::get_code_region_end_tag() const { + return code_region_end_tag; +} + +void CodeEdit::set_code_region_tags(const String &p_start, const String &p_end) { + ERR_FAIL_COND_MSG(p_start == p_end, "Starting and ending region tags cannot be identical."); + ERR_FAIL_COND_MSG(p_start.is_empty(), "Starting region tag cannot be empty."); + ERR_FAIL_COND_MSG(p_end.is_empty(), "Ending region tag cannot be empty."); + code_region_start_tag = p_start; + code_region_end_tag = p_end; + _update_code_region_tags(); +} + +bool CodeEdit::is_line_code_region_start(int p_line) const { + ERR_FAIL_INDEX_V(p_line, get_line_count(), false); + if (code_region_start_string.is_empty()) { + return false; + } + return get_line(p_line).strip_edges().begins_with(code_region_start_string); +} + +bool CodeEdit::is_line_code_region_end(int p_line) const { + ERR_FAIL_INDEX_V(p_line, get_line_count(), false); + if (code_region_start_string.is_empty()) { + return false; + } + return get_line(p_line).strip_edges().begins_with(code_region_end_string); +} + /* Delimiters */ // Strings void CodeEdit::add_string_delimiter(const String &p_start_key, const String &p_end_key, bool p_line_only) { @@ -2344,6 +2489,14 @@ void CodeEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("is_line_folded", "line"), &CodeEdit::is_line_folded); ClassDB::bind_method(D_METHOD("get_folded_lines"), &CodeEdit::get_folded_lines); + /* Code region */ + ClassDB::bind_method(D_METHOD("create_code_region"), &CodeEdit::create_code_region); + ClassDB::bind_method(D_METHOD("get_code_region_start_tag"), &CodeEdit::get_code_region_start_tag); + ClassDB::bind_method(D_METHOD("get_code_region_end_tag"), &CodeEdit::get_code_region_end_tag); + ClassDB::bind_method(D_METHOD("set_code_region_tags", "start", "end"), &CodeEdit::set_code_region_tags, DEFVAL("region"), DEFVAL("endregion")); + ClassDB::bind_method(D_METHOD("is_line_code_region_start", "line"), &CodeEdit::is_line_code_region_start); + ClassDB::bind_method(D_METHOD("is_line_code_region_end", "line"), &CodeEdit::is_line_code_region_end); + /* Delimiters */ // Strings ClassDB::bind_method(D_METHOD("add_string_delimiter", "start_key", "end_key", "line_only"), &CodeEdit::add_string_delimiter, DEFVAL(false)); @@ -2483,8 +2636,11 @@ void CodeEdit::_bind_methods() { /* Theme items */ /* Gutters */ BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, CodeEdit, code_folding_color); + BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, CodeEdit, folded_code_region_color); BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, CodeEdit, can_fold_icon, "can_fold"); BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, CodeEdit, folded_icon, "folded"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, CodeEdit, can_fold_code_region_icon, "can_fold_code_region"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, CodeEdit, folded_code_region_icon, "folded_code_region"); BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, CodeEdit, folded_eol_icon); BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, CodeEdit, breakpoint_color); @@ -2628,6 +2784,27 @@ void CodeEdit::_update_gutter_indexes() { } } +/* Code Region */ +void CodeEdit::_update_code_region_tags() { + code_region_start_string = ""; + code_region_end_string = ""; + + if (code_region_start_tag.is_empty() || code_region_end_tag.is_empty()) { + return; + } + + for (int i = 0; i < delimiters.size(); i++) { + if (delimiters[i].type != DelimiterType::TYPE_COMMENT) { + continue; + } + if (delimiters[i].end_key.is_empty() && delimiters[i].line_only == true) { + code_region_start_string = delimiters[i].start_key + code_region_start_tag; + code_region_end_string = delimiters[i].start_key + code_region_end_tag; + return; + } + } +} + /* Delimiters */ void CodeEdit::_update_delimiter_cache(int p_from_line, int p_to_line) { if (delimiters.size() == 0) { @@ -2871,6 +3048,9 @@ void CodeEdit::_add_delimiter(const String &p_start_key, const String &p_end_key delimiter_cache.clear(); _update_delimiter_cache(); } + if (p_type == DelimiterType::TYPE_COMMENT) { + _update_code_region_tags(); + } } void CodeEdit::_remove_delimiter(const String &p_start_key, DelimiterType p_type) { @@ -2888,6 +3068,9 @@ void CodeEdit::_remove_delimiter(const String &p_start_key, DelimiterType p_type delimiter_cache.clear(); _update_delimiter_cache(); } + if (p_type == DelimiterType::TYPE_COMMENT) { + _update_code_region_tags(); + } break; } } @@ -2931,6 +3114,9 @@ void CodeEdit::_clear_delimiters(DelimiterType p_type) { if (!setting_delimiters) { _update_delimiter_cache(); } + if (p_type == DelimiterType::TYPE_COMMENT) { + _update_code_region_tags(); + } } TypedArray CodeEdit::_get_delimiters(DelimiterType p_type) const { diff --git a/scene/gui/code_edit.h b/scene/gui/code_edit.h index d00bd22cd52..53ff65f3760 100644 --- a/scene/gui/code_edit.h +++ b/scene/gui/code_edit.h @@ -125,6 +125,11 @@ private: /* Line Folding */ bool line_folding_enabled = false; + String code_region_start_string; + String code_region_end_string; + String code_region_start_tag = "region"; + String code_region_end_tag = "endregion"; + void _update_code_region_tags(); /* Delimiters */ enum DelimiterType { @@ -232,8 +237,11 @@ private: struct ThemeCache { /* Gutters */ Color code_folding_color = Color(1, 1, 1); + Color folded_code_region_color = Color(1, 1, 1); Ref can_fold_icon; Ref folded_icon; + Ref can_fold_code_region_icon; + Ref folded_code_region_icon; Ref folded_eol_icon; Color breakpoint_color = Color(1, 1, 1); @@ -397,6 +405,14 @@ public: bool is_line_folded(int p_line) const; TypedArray get_folded_lines() const; + /* Code region */ + void create_code_region(); + String get_code_region_start_tag() const; + String get_code_region_end_tag() const; + void set_code_region_tags(const String &p_start = "region", const String &p_end = "endregion"); + bool is_line_code_region_start(int p_line) const; + bool is_line_code_region_end(int p_line) const; + /* Delimiters */ void add_string_delimiter(const String &p_start_key, const String &p_end_key, bool p_line_only = false); void remove_string_delimiter(const String &p_start_key); diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 292809b40db..cc519ad38ba 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -3000,6 +3000,7 @@ void TextEdit::_update_theme_item_cache() { Control::_update_theme_item_cache(); theme_cache.base_scale = get_theme_default_base_scale(); + theme_cache.folded_code_region_color = get_theme_color(SNAME("folded_code_region_color"), SNAME("CodeEdit")); use_selected_font_color = theme_cache.font_selected_color != Color(0, 0, 0, 0); if (text.get_line_height() + theme_cache.line_spacing < 1) { @@ -6476,6 +6477,7 @@ void TextEdit::_bind_methods() { /* Internal API for CodeEdit */ BIND_THEME_ITEM_EXT(Theme::DATA_TYPE_COLOR, TextEdit, brace_mismatch_color, "brace_mismatch_color", "CodeEdit"); BIND_THEME_ITEM_EXT(Theme::DATA_TYPE_COLOR, TextEdit, code_folding_color, "code_folding_color", "CodeEdit"); + BIND_THEME_ITEM_EXT(Theme::DATA_TYPE_COLOR, TextEdit, folded_code_region_color, "folded_code_region_color", "CodeEdit"); BIND_THEME_ITEM_EXT(Theme::DATA_TYPE_ICON, TextEdit, folded_eol_icon, "folded_eol_icon", "CodeEdit"); /* Search */ diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index b52fde93612..d874db34bf4 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -546,6 +546,7 @@ private: /* Internal API for CodeEdit */ Color brace_mismatch_color; Color code_folding_color = Color(1, 1, 1); + Color folded_code_region_color = Color(1, 1, 1); Ref folded_eol_icon; /* Search */ diff --git a/scene/theme/default_theme.cpp b/scene/theme/default_theme.cpp index 61e018c6567..7efbc74bf38 100644 --- a/scene/theme/default_theme.cpp +++ b/scene/theme/default_theme.cpp @@ -478,6 +478,8 @@ void fill_default_theme(Ref &theme, const Ref &default_font, const theme->set_icon("executing_line", "CodeEdit", icons["arrow_right"]); theme->set_icon("can_fold", "CodeEdit", icons["arrow_down"]); theme->set_icon("folded", "CodeEdit", icons["arrow_right"]); + theme->set_icon("can_fold_code_region", "CodeEdit", icons["folder_down_arrow"]); + theme->set_icon("folded_code_region", "CodeEdit", icons["folder_right_arrow"]); theme->set_icon("folded_eol_icon", "CodeEdit", icons["text_edit_ellipsis"]); theme->set_font("font", "CodeEdit", Ref()); @@ -501,6 +503,7 @@ void fill_default_theme(Ref &theme, const Ref &default_font, const theme->set_color("executing_line_color", "CodeEdit", Color(0.98, 0.89, 0.27)); theme->set_color("current_line_color", "CodeEdit", Color(0.25, 0.25, 0.26, 0.8)); theme->set_color("code_folding_color", "CodeEdit", Color(0.8, 0.8, 0.8, 0.8)); + theme->set_color("folded_code_region_color", "CodeEdit", Color(0.68, 0.46, 0.77, 0.2)); theme->set_color("caret_color", "CodeEdit", control_font_color); theme->set_color("caret_background_color", "CodeEdit", Color(0, 0, 0)); theme->set_color("brace_mismatch_color", "CodeEdit", Color(1, 0.2, 0.2)); diff --git a/scene/theme/icons/folder_down_arrow.svg b/scene/theme/icons/folder_down_arrow.svg new file mode 100644 index 00000000000..3bc4f3f73bd --- /dev/null +++ b/scene/theme/icons/folder_down_arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scene/theme/icons/folder_right_arrow.svg b/scene/theme/icons/folder_right_arrow.svg new file mode 100644 index 00000000000..a9b81d54f30 --- /dev/null +++ b/scene/theme/icons/folder_right_arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/scene/test_code_edit.h b/tests/scene/test_code_edit.h index 72421c9cc90..8576b38ce26 100644 --- a/tests/scene/test_code_edit.h +++ b/tests/scene/test_code_edit.h @@ -2839,6 +2839,194 @@ TEST_CASE("[SceneTree][CodeEdit] folding") { memdelete(code_edit); } +TEST_CASE("[SceneTree][CodeEdit] region folding") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + code_edit->grab_focus(); + + SUBCASE("[CodeEdit] region folding") { + code_edit->set_line_folding_enabled(true); + + // Region tag detection. + code_edit->set_text("#region region_name\nline2\n#endregion"); + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("#", ""); + CHECK(code_edit->is_line_code_region_start(0)); + CHECK_FALSE(code_edit->is_line_code_region_start(1)); + CHECK_FALSE(code_edit->is_line_code_region_start(2)); + CHECK_FALSE(code_edit->is_line_code_region_end(0)); + CHECK_FALSE(code_edit->is_line_code_region_end(1)); + CHECK(code_edit->is_line_code_region_end(2)); + + // Region tag customization. + code_edit->set_text("#region region_name\nline2\n#endregion\n#open region_name\nline2\n#close"); + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("#", ""); + CHECK(code_edit->is_line_code_region_start(0)); + CHECK(code_edit->is_line_code_region_end(2)); + CHECK_FALSE(code_edit->is_line_code_region_start(3)); + CHECK_FALSE(code_edit->is_line_code_region_end(5)); + code_edit->set_code_region_tags("open", "close"); + CHECK_FALSE(code_edit->is_line_code_region_start(0)); + CHECK_FALSE(code_edit->is_line_code_region_end(2)); + CHECK(code_edit->is_line_code_region_start(3)); + CHECK(code_edit->is_line_code_region_end(5)); + code_edit->set_code_region_tags("region", "endregion"); + + // Setting identical start and end region tags should fail. + CHECK(code_edit->get_code_region_start_tag() == "region"); + CHECK(code_edit->get_code_region_end_tag() == "endregion"); + ERR_PRINT_OFF; + code_edit->set_code_region_tags("same_tag", "same_tag"); + ERR_PRINT_ON; + CHECK(code_edit->get_code_region_start_tag() == "region"); + CHECK(code_edit->get_code_region_end_tag() == "endregion"); + + // Region creation with selection adds start / close region lines. + code_edit->set_text("line1\nline2\nline3"); + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("#", ""); + code_edit->select(1, 0, 1, 4); + code_edit->create_code_region(); + CHECK(code_edit->is_line_code_region_start(1)); + CHECK(code_edit->get_line(2).contains("line2")); + CHECK(code_edit->is_line_code_region_end(3)); + + // Region creation without any selection has no effect. + code_edit->set_text("line1\nline2\nline3"); + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("#", ""); + code_edit->create_code_region(); + CHECK(code_edit->get_text() == "line1\nline2\nline3"); + + // Region creation with multiple selections. + code_edit->set_text("line1\nline2\nline3"); + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("#", ""); + code_edit->select(0, 0, 0, 4, 0); + code_edit->add_caret(2, 5); + code_edit->select(2, 0, 2, 5, 1); + code_edit->create_code_region(); + CHECK(code_edit->get_text() == "#region New Code Region\nline1\n#endregion\nline2\n#region New Code Region\nline3\n#endregion"); + + // Two selections on the same line create only one region. + code_edit->set_text("test line1\ntest line2\ntest line3"); + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("#", ""); + code_edit->select(0, 0, 1, 2, 0); + code_edit->add_caret(1, 4); + code_edit->select(1, 4, 2, 5, 1); + code_edit->create_code_region(); + CHECK(code_edit->get_text() == "#region New Code Region\ntest line1\ntest line2\ntest line3\n#endregion"); + + // Region tag with // comment delimiter. + code_edit->set_text("//region region_name\nline2\n//endregion"); + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("//", ""); + CHECK(code_edit->is_line_code_region_start(0)); + CHECK(code_edit->is_line_code_region_end(2)); + + // Creating region with no valid one line comment delimiter has no effect. + code_edit->set_text("line1\nline2\nline3"); + code_edit->clear_comment_delimiters(); + code_edit->create_code_region(); + CHECK(code_edit->get_text() == "line1\nline2\nline3"); + code_edit->add_comment_delimiter("/*", "*/"); + code_edit->create_code_region(); + CHECK(code_edit->get_text() == "line1\nline2\nline3"); + + // Choose one line comment delimiter. + code_edit->set_text("//region region_name\nline2\n//endregion"); + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("/*", "*/"); + code_edit->add_comment_delimiter("//", ""); + CHECK(code_edit->is_line_code_region_start(0)); + CHECK(code_edit->is_line_code_region_end(2)); + + // Update code region delimiter when removing comment delimiter. + code_edit->set_text("//region region_name\nline2\n//endregion\n#region region_name\nline2\n#endregion"); + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("//", ""); + code_edit->add_comment_delimiter("#", ""); + CHECK(code_edit->is_line_code_region_start(0)); + CHECK(code_edit->is_line_code_region_end(2)); + CHECK_FALSE(code_edit->is_line_code_region_start(3)); + CHECK_FALSE(code_edit->is_line_code_region_end(5)); + code_edit->remove_comment_delimiter("//"); + CHECK_FALSE(code_edit->is_line_code_region_start(0)); + CHECK_FALSE(code_edit->is_line_code_region_end(2)); + CHECK(code_edit->is_line_code_region_start(3)); + CHECK(code_edit->is_line_code_region_end(5)); + + // Update code region delimiter when clearing comment delimiters. + code_edit->set_text("//region region_name\nline2\n//endregion"); + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("//", ""); + CHECK(code_edit->is_line_code_region_start(0)); + CHECK(code_edit->is_line_code_region_end(2)); + code_edit->clear_comment_delimiters(); + CHECK_FALSE(code_edit->is_line_code_region_start(0)); + CHECK_FALSE(code_edit->is_line_code_region_end(2)); + + // Fold region. + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("#", ""); + code_edit->set_text("#region region_name\nline2\nline3\n#endregion\nvisible line"); + CHECK(code_edit->can_fold_line(0)); + for (int i = 1; i < 5; i++) { + CHECK_FALSE(code_edit->can_fold_line(i)); + } + for (int i = 0; i < 5; i++) { + CHECK_FALSE(code_edit->is_line_folded(i)); + } + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4); + + // Region with no end can't be folded. + ERR_PRINT_OFF; + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("#", ""); + code_edit->set_text("#region region_name\nline2\nline3\n#bad_end_tag\nvisible line"); + CHECK_FALSE(code_edit->can_fold_line(0)); + ERR_PRINT_ON; + + // Bad nested region can't be folded. + ERR_PRINT_OFF; + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("#", ""); + code_edit->set_text("#region without end\n#region region2\nline3\n#endregion\n#no_end"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK(code_edit->can_fold_line(1)); + ERR_PRINT_ON; + + // Nested region folding. + ERR_PRINT_OFF; + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("#", ""); + code_edit->set_text("#region region1\n#region region2\nline3\n#endregion\n#endregion"); + CHECK(code_edit->can_fold_line(0)); + CHECK(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK(code_edit->get_next_visible_line_offset_from(2, 1) == 3); + code_edit->fold_line(0); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4); + ERR_PRINT_ON; + + // Unfolding a line inside a region unfold whole region. + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("#", ""); + code_edit->set_text("#region region\ninside\nline3\n#endregion\nvisible"); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4); + code_edit->unfold_line(1); + CHECK_FALSE(code_edit->is_line_folded(0)); + } + + memdelete(code_edit); +} + TEST_CASE("[SceneTree][CodeEdit] completion") { CodeEdit *code_edit = memnew(CodeEdit); SceneTree::get_singleton()->get_root()->add_child(code_edit);