Browse Source

Merge pull request #74843 from jmb462/code_region

Add code region folding to CodeEdit
Rémi Verschelde 2 years ago
parent
commit
2c2ca3d958

+ 52 - 0
doc/classes/CodeEdit.xml

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

+ 3 - 0
doc/classes/EditorSettings.xml

@@ -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]).

+ 1 - 0
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));
 }

+ 7 - 0
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<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"));
 

+ 1 - 0
editor/icons/CodeRegionFoldDownArrow.svg

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

+ 1 - 0
editor/icons/CodeRegionFoldedRightArrow.svg

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

+ 16 - 2
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);

+ 2 - 0
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,

+ 211 - 25
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;
-			}
-			if (get_indent_level(i) > start_indent) {
-				end_line = i;
-				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 (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<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 {

+ 16 - 0
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<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);

+ 2 - 0
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 */

+ 1 - 0
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<Texture2D> folded_eol_icon;
 
 		/* Search */

+ 3 - 0
scene/theme/default_theme.cpp

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

+ 1 - 0
scene/theme/icons/folder_down_arrow.svg

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

+ 1 - 0
scene/theme/icons/folder_right_arrow.svg

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

+ 188 - 0
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);