Add code region folding to CodeEdit

This commit is contained in:
Jean-Michel Bernard 2023-03-12 17:48:37 +01:00
parent 221884e6bc
commit 67dce301aa
16 changed files with 506 additions and 27 deletions

View file

@ -137,6 +137,15 @@
Values of [code]-1[/code] convert the entire text.
</description>
</method>
<method name="create_code_region">
<return type="void" />
<description>
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])
</description>
</method>
<method name="do_indent">
<return type="void" />
<description>
@ -200,6 +209,18 @@
Gets the index of the current selected completion option.
</description>
</method>
<method name="get_code_region_end_tag" qualifiers="const">
<return type="String" />
<description>
Returns the code region end tag (without comment delimiter).
</description>
</method>
<method name="get_code_region_start_tag" qualifiers="const">
<return type="String" />
<description>
Returns the code region start tag (without comment delimiter).
</description>
</method>
<method name="get_delimiter_end_key" qualifiers="const">
<return type="String" />
<param index="0" name="delimiter_index" type="int" />
@ -326,6 +347,20 @@
Returns whether the line at the specified index is breakpointed or not.
</description>
</method>
<method name="is_line_code_region_end" qualifiers="const">
<return type="bool" />
<param index="0" name="line" type="int" />
<description>
Returns whether the line at the specified index is a code region end.
</description>
</method>
<method name="is_line_code_region_start" qualifiers="const">
<return type="bool" />
<param index="0" name="line" type="int" />
<description>
Returns whether the line at the specified index is a code region start.
</description>
</method>
<method name="is_line_executing" qualifiers="const">
<return type="bool" />
<param index="0" name="line" type="int" />
@ -382,6 +417,14 @@
Sets if the code hint should draw below the text.
</description>
</method>
<method name="set_code_region_tags">
<return type="void" />
<param index="0" name="start" type="String" default="&quot;region&quot;" />
<param index="1" name="end" type="String" default="&quot;endregion&quot;" />
<description>
Sets the code region start and end tags (without comment delimiter).
</description>
</method>
<method name="set_line_as_bookmarked">
<return type="void" />
<param index="0" name="line" type="int" />
@ -629,6 +672,9 @@
<theme_item name="executing_line_color" data_type="color" type="Color" default="Color(0.98, 0.89, 0.27, 1)">
[Color] of the executing icon for executing lines.
</theme_item>
<theme_item name="folded_code_region_color" data_type="color" type="Color" default="Color(0.68, 0.46, 0.77, 0.2)">
[Color] of background line highlight for folded code region.
</theme_item>
<theme_item name="font_color" data_type="color" type="Color" default="Color(0.875, 0.875, 0.875, 1)">
Sets the font [Color].
</theme_item>
@ -693,12 +739,18 @@
<theme_item name="can_fold" data_type="icon" type="Texture2D">
Sets a custom [Texture2D] to draw in the line folding gutter when a line can be folded.
</theme_item>
<theme_item name="can_fold_code_region" data_type="icon" type="Texture2D">
Sets a custom [Texture2D] to draw in the line folding gutter when a code region can be folded.
</theme_item>
<theme_item name="executing_line" data_type="icon" type="Texture2D">
Icon to draw in the executing gutter for executing lines.
</theme_item>
<theme_item name="folded" data_type="icon" type="Texture2D">
Sets a custom [Texture2D] to draw in the line folding gutter when a line is folded and can be unfolded.
</theme_item>
<theme_item name="folded_code_region" data_type="icon" type="Texture2D">
Sets a custom [Texture2D] to draw in the line folding gutter when a code region is folded and can be unfolded.
</theme_item>
<theme_item name="folded_eol_icon" data_type="icon" type="Texture2D">
Sets a custom [Texture2D] to draw at the end of a folded line.
</theme_item>

View file

@ -959,6 +959,9 @@
<member name="text_editor/theme/highlighting/executing_line_color" type="Color" setter="" getter="">
The script editor's color for the debugger's executing line icon (displayed in the gutter).
</member>
<member name="text_editor/theme/highlighting/folded_code_region_color" type="Color" setter="" getter="">
The script editor's background line highlighting color for folded code region.
</member>
<member name="text_editor/theme/highlighting/function_color" type="Color" setter="" getter="">
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]).

View file

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

View file

@ -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<Theme> create_editor_theme(const Ref<Theme> 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<Theme> create_editor_theme(const Ref<Theme> 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<Theme> create_editor_theme(const Ref<Theme> 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<Theme> create_editor_theme(const Ref<Theme> 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"));

View file

@ -0,0 +1 @@
<svg height="12" width="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path d="M2 1a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H6V2a1 1 0 0 0-1-1zm1 5a1 1 0 0 1 1.414-1.414L6 6.172l1.586-1.586A1 1 0 0 1 9 6L6.707 8.293a1 1 0 0 1-1.414 0Z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 289 B

View file

@ -0,0 +1 @@
<svg height="12" width="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path d="M2 1a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H6V2a1 1 0 0 0-1-1zm3.5 8a1 1 0 0 1-1.414-1.414L5.672 6 4.086 4.414A1 1 0 0 1 5.5 3l2.293 2.293a1 1 0 0 1 0 1.414Z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 293 B

View file

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

View file

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

View file

@ -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,11 +1635,30 @@ void CodeEdit::fold_line(int p_line) {
const int line_count = get_line_count() - 1;
int end_line = line_count;
// 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;
}
}
}
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. */
// 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)) {
@ -1630,6 +1682,7 @@ void CodeEdit::fold_line(int p_line) {
}
}
}
}
for (int i = p_line + 1; i <= end_line; i++) {
_set_line_as_hidden(i, true);
@ -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<int> 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<int> caret_edit_order = get_caret_index_edit_order();
Vector<int> 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<String> CodeEdit::_get_delimiters(DelimiterType p_type) const {

View file

@ -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<Texture2D> can_fold_icon;
Ref<Texture2D> folded_icon;
Ref<Texture2D> can_fold_code_region_icon;
Ref<Texture2D> folded_code_region_icon;
Ref<Texture2D> folded_eol_icon;
Color breakpoint_color = Color(1, 1, 1);
@ -397,6 +405,14 @@ public:
bool is_line_folded(int p_line) const;
TypedArray<int> 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);

View file

@ -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 */

View file

@ -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<Texture2D> folded_eol_icon;
/* Search */

View file

@ -478,6 +478,8 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &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<Font>());
@ -501,6 +503,7 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &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));

View file

@ -0,0 +1 @@
<svg height="12" width="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path d="M2 1a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H6V2a1 1 0 0 0-1-1zm1 5a1 1 0 0 1 1.414-1.414L6 6.172l1.586-1.586A1 1 0 0 1 9 6L6.707 8.293a1 1 0 0 1-1.414 0Z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 289 B

View file

@ -0,0 +1 @@
<svg height="12" width="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path d="M2 1a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H6V2a1 1 0 0 0-1-1zm3.5 8a1 1 0 0 1-1.414-1.414L5.672 6 4.086 4.414A1 1 0 0 1 5.5 3l2.293 2.293a1 1 0 0 1 0 1.414Z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 293 B

View file

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