Browse Source

Move indent management to CodeEdit

Paulb23 4 years ago
parent
commit
1a0cfc219b

+ 40 - 0
doc/classes/CodeEdit.xml

@@ -143,6 +143,20 @@
 				Inserts the selected entry into the text. If [code]replace[/code] is true, any existing text is replaced rather then merged.
 			</description>
 		</method>
+		<method name="do_indent">
+			<return type="void">
+			</return>
+			<description>
+				Perform an indent as if the user activated the "ui_text_indent" action.
+			</description>
+		</method>
+		<method name="do_unindent">
+			<return type="void">
+			</return>
+			<description>
+				Perform an unindent as if the user activated the "ui_text_unindent" action.
+			</description>
+		</method>
 		<method name="fold_all_lines">
 			<return type="void">
 			</return>
@@ -278,6 +292,13 @@
 				Returns [code]true[/code] if string [code]start_key[/code] exists.
 			</description>
 		</method>
+		<method name="indent_lines">
+			<return type="void">
+			</return>
+			<description>
+				Indents selected lines, or in the case of no selection the caret line by one.
+			</description>
+		</method>
 		<method name="is_in_comment" qualifiers="const">
 			<return type="int">
 			</return>
@@ -441,6 +462,13 @@
 				Unfolds all lines that were previously folded.
 			</description>
 		</method>
+		<method name="unindent_lines">
+			<return type="void">
+			</return>
+			<description>
+				Unindents selected lines, or in the case of no selection the caret line by one.
+			</description>
+		</method>
 		<method name="update_code_completion_options">
 			<return type="void">
 			</return>
@@ -475,6 +503,18 @@
 		</member>
 		<member name="draw_line_numbers" type="bool" setter="set_draw_line_numbers" getter="is_draw_line_numbers_enabled" default="false">
 		</member>
+		<member name="indent_automatic" type="bool" setter="set_auto_indent_enabled" getter="is_auto_indent_enabled" default="false">
+			Sets whether automatic indent are enabled, this will add an extra indent if a prefix or brace is found.
+		</member>
+		<member name="indent_automatic_prefixes" type="String[]" setter="set_auto_indent_prefixes" getter="get_auto_indent_prefixes" default="[&quot;(&quot;, &quot;:&quot;, &quot;[&quot;, &quot;{&quot;]">
+			Prefixes to trigger an automatic indent.
+		</member>
+		<member name="indent_size" type="int" setter="set_indent_size" getter="get_indent_size" default="4">
+			Size of tabs, if [code]indent_use_spaces[/code] is enabled the amount of spaces to use.
+		</member>
+		<member name="indent_use_spaces" type="bool" setter="set_indent_using_spaces" getter="is_indent_using_spaces" default="false">
+			Use spaces instead of tabs for indentation.
+		</member>
 		<member name="layout_direction" type="int" setter="set_layout_direction" getter="get_layout_direction" override="true" enum="Control.LayoutDirection" default="2" />
 		<member name="line_folding" type="bool" setter="set_line_folding_enabled" getter="is_line_folding_enabled" default="true">
 			Sets whether line folding is allowed.

+ 58 - 0
doc/classes/TextEdit.xml

@@ -10,6 +10,12 @@
 	<tutorials>
 	</tutorials>
 	<methods>
+		<method name="_backspace" qualifiers="virtual">
+			<return type="void">
+			</return>
+			<description>
+			</description>
+		</method>
 		<method name="add_gutter">
 			<return type="void">
 			</return>
@@ -18,6 +24,12 @@
 			<description>
 			</description>
 		</method>
+		<method name="backspace">
+			<return type="void">
+			</return>
+			<description>
+			</description>
+		</method>
 		<method name="center_viewport_to_cursor">
 			<return type="void">
 			</return>
@@ -96,6 +108,12 @@
 				Cut's the current selection.
 			</description>
 		</method>
+		<method name="delete_selection">
+			<return type="void">
+			</return>
+			<description>
+			</description>
+		</method>
 		<method name="deselect">
 			<return type="void">
 			</return>
@@ -110,6 +128,14 @@
 				Gets the caret pixel draw poistion.
 			</description>
 		</method>
+		<method name="get_first_non_whitespace_column" qualifiers="const">
+			<return type="int">
+			</return>
+			<argument index="0" name="line" type="int">
+			</argument>
+			<description>
+			</description>
+		</method>
 		<method name="get_gutter_count" qualifiers="const">
 			<return type="int">
 			</return>
@@ -140,6 +166,14 @@
 			<description>
 			</description>
 		</method>
+		<method name="get_indent_level" qualifiers="const">
+			<return type="int">
+			</return>
+			<argument index="0" name="line" type="int">
+			</argument>
+			<description>
+			</description>
+		</method>
 		<method name="get_line" qualifiers="const">
 			<return type="String">
 			</return>
@@ -274,6 +308,12 @@
 				Returns the selection end line.
 			</description>
 		</method>
+		<method name="get_tab_size" qualifiers="const">
+			<return type="int">
+			</return>
+			<description>
+			</description>
+		</method>
 		<method name="get_visible_line_count" qualifiers="const">
 			<return type="int">
 			</return>
@@ -354,6 +394,16 @@
 				Triggers a right-click menu action by the specified index. See [enum MenuItems] for a list of available indexes.
 			</description>
 		</method>
+		<method name="merge_gutters">
+			<return type="void">
+			</return>
+			<argument index="0" name="from_line" type="int">
+			</argument>
+			<argument index="1" name="to_line" type="int">
+			</argument>
+			<description>
+			</description>
+		</method>
 		<method name="paste">
 			<return type="void">
 			</return>
@@ -611,6 +661,14 @@
 			<description>
 			</description>
 		</method>
+		<method name="set_tab_size">
+			<return type="void">
+			</return>
+			<argument index="0" name="size" type="int">
+			</argument>
+			<description>
+			</description>
+		</method>
 		<method name="undo">
 			<return type="void">
 			</return>

+ 3 - 3
editor/code_editor.cpp

@@ -930,7 +930,7 @@ void CodeTextEditor::update_editor_settings() {
 	text_editor->set_highlight_current_line(EditorSettings::get_singleton()->get("text_editor/highlighting/highlight_current_line"));
 	text_editor->set_indent_using_spaces(EditorSettings::get_singleton()->get("text_editor/indent/type"));
 	text_editor->set_indent_size(EditorSettings::get_singleton()->get("text_editor/indent/size"));
-	text_editor->set_auto_indent(EditorSettings::get_singleton()->get("text_editor/indent/auto_indent"));
+	text_editor->set_auto_indent_enabled(EditorSettings::get_singleton()->get("text_editor/indent/auto_indent"));
 	text_editor->set_draw_tabs(EditorSettings::get_singleton()->get("text_editor/indent/draw_tabs"));
 	text_editor->set_draw_spaces(EditorSettings::get_singleton()->get("text_editor/indent/draw_spaces"));
 	text_editor->set_smooth_scroll_enabled(EditorSettings::get_singleton()->get("text_editor/navigation/smooth_scrolling"));
@@ -1255,7 +1255,7 @@ void CodeTextEditor::_delete_line(int p_line) {
 		text_editor->cursor_set_line(1);
 		text_editor->cursor_set_column(0);
 	}
-	text_editor->backspace_at_cursor();
+	text_editor->backspace();
 	text_editor->unfold_line(p_line);
 	text_editor->cursor_set_line(p_line);
 }
@@ -1809,7 +1809,7 @@ CodeTextEditor::CodeTextEditor() {
 
 	text_editor->set_draw_line_numbers(true);
 	text_editor->set_brace_matching(true);
-	text_editor->set_auto_indent(true);
+	text_editor->set_auto_indent_enabled(true);
 
 	status_bar = memnew(HBoxContainer);
 	add_child(status_bar);

+ 3 - 3
editor/plugins/script_text_editor.cpp

@@ -524,7 +524,7 @@ void ScriptTextEditor::_validate_script() {
 			if (safe_lines.has(i + 1)) {
 				te->set_line_gutter_item_color(i, line_number_gutter, safe_line_number_color);
 				last_is_safe = true;
-			} else if (last_is_safe && (te->is_line_comment(i) || te->get_line(i).strip_edges().is_empty())) {
+			} else if (last_is_safe && (te->is_in_comment(i) != -1 || te->get_line(i).strip_edges().is_empty())) {
 				te->set_line_gutter_item_color(i, line_number_gutter, safe_line_number_color);
 			} else {
 				te->set_line_gutter_item_color(i, line_number_gutter, default_line_number_color);
@@ -1038,7 +1038,7 @@ void ScriptTextEditor::_edit_option(int p_op) {
 				return;
 			}
 
-			tx->indent_selected_lines_left();
+			tx->unindent_lines();
 		} break;
 		case EDIT_INDENT_RIGHT: {
 			Ref<Script> scr = script;
@@ -1046,7 +1046,7 @@ void ScriptTextEditor::_edit_option(int p_op) {
 				return;
 			}
 
-			tx->indent_selected_lines_right();
+			tx->indent_lines();
 		} break;
 		case EDIT_DELETE_LINE: {
 			code_editor->delete_lines();

+ 2 - 8
editor/plugins/shader_editor_plugin.cpp

@@ -323,19 +323,13 @@ void ShaderEditor::_menu_option(int p_option) {
 			if (shader.is_null()) {
 				return;
 			}
-
-			CodeEdit *tx = shader_editor->get_text_editor();
-			tx->indent_selected_lines_left();
-
+			shader_editor->get_text_editor()->unindent_lines();
 		} break;
 		case EDIT_INDENT_RIGHT: {
 			if (shader.is_null()) {
 				return;
 			}
-
-			CodeEdit *tx = shader_editor->get_text_editor();
-			tx->indent_selected_lines_right();
-
+			shader_editor->get_text_editor()->indent_lines();
 		} break;
 		case EDIT_DELETE_LINE: {
 			shader_editor->delete_lines();

+ 2 - 2
editor/plugins/text_editor.cpp

@@ -320,10 +320,10 @@ void TextEditor::_edit_option(int p_op) {
 			code_editor->move_lines_down();
 		} break;
 		case EDIT_INDENT_LEFT: {
-			tx->indent_selected_lines_left();
+			tx->unindent_lines();
 		} break;
 		case EDIT_INDENT_RIGHT: {
-			tx->indent_selected_lines_right();
+			tx->indent_lines();
 		} break;
 		case EDIT_DELETE_LINE: {
 			code_editor->delete_lines();

+ 497 - 9
scene/gui/code_edit.cpp

@@ -360,7 +360,7 @@ void CodeEdit::_gui_input(const Ref<InputEvent> &p_gui_input) {
 			return;
 		}
 		if (k->is_action("ui_text_backspace", true)) {
-			backspace_at_cursor();
+			backspace();
 			_filter_code_completion_candidates();
 			accept_event();
 			return;
@@ -387,14 +387,34 @@ void CodeEdit::_gui_input(const Ref<InputEvent> &p_gui_input) {
 		set_code_hint("");
 	}
 
-	/* Override input to unfold lines where needed. */
-	if (!is_readonly()) {
-		if (k->is_action("ui_text_newline_above", true) || k->is_action("ui_text_newline_blank", true) || k->is_action("ui_text_newline", true)) {
-			unfold_line(cursor_get_line());
-		}
-		if (cursor_get_line() > 0 && k->is_action("ui_text_backspace", true)) {
-			unfold_line(cursor_get_line() - 1);
-		}
+	/* Indentation */
+	if (k->is_action("ui_text_indent", true)) {
+		do_indent();
+		accept_event();
+		return;
+	}
+
+	if (k->is_action("ui_text_dedent", true)) {
+		do_unindent();
+		accept_event();
+		return;
+	}
+
+	// Override new line actions, for auto indent
+	if (k->is_action("ui_text_newline_above", true)) {
+		_new_line(false, true);
+		accept_event();
+		return;
+	}
+	if (k->is_action("ui_text_newline_blank", true)) {
+		_new_line(false);
+		accept_event();
+		return;
+	}
+	if (k->is_action("ui_text_newline", true)) {
+		_new_line();
+		accept_event();
+		return;
 	}
 
 	/* Remove shift otherwise actions will not match. */
@@ -439,6 +459,443 @@ Control::CursorShape CodeEdit::get_cursor_shape(const Point2 &p_pos) const {
 	return TextEdit::get_cursor_shape(p_pos);
 }
 
+/* Indent management */
+void CodeEdit::set_indent_size(const int p_size) {
+	ERR_FAIL_COND_MSG(p_size <= 0, "Indend size must be greater than 0.");
+	if (indent_size == p_size) {
+		return;
+	}
+
+	indent_size = p_size;
+	if (indent_using_spaces) {
+		indent_text = String(" ").repeat(p_size);
+	} else {
+		indent_text = "\t";
+	}
+	set_tab_size(p_size);
+}
+
+int CodeEdit::get_indent_size() const {
+	return indent_size;
+}
+
+void CodeEdit::set_indent_using_spaces(const bool p_use_spaces) {
+	indent_using_spaces = p_use_spaces;
+	if (indent_using_spaces) {
+		indent_text = String(" ").repeat(indent_size);
+	} else {
+		indent_text = "\t";
+	}
+}
+
+bool CodeEdit::is_indent_using_spaces() const {
+	return indent_using_spaces;
+}
+
+void CodeEdit::set_auto_indent_enabled(bool p_enabled) {
+	auto_indent = p_enabled;
+}
+
+bool CodeEdit::is_auto_indent_enabled() const {
+	return auto_indent;
+}
+
+void CodeEdit::set_auto_indent_prefixes(const TypedArray<String> &p_prefixes) {
+	auto_indent_prefixes.clear();
+	for (int i = 0; i < p_prefixes.size(); i++) {
+		const String prefix = p_prefixes[i];
+		auto_indent_prefixes.insert(prefix[0]);
+	}
+}
+
+TypedArray<String> CodeEdit::get_auto_indent_prefixes() const {
+	TypedArray<String> prefixes;
+	for (const Set<char32_t>::Element *E = auto_indent_prefixes.front(); E; E = E->next()) {
+		prefixes.push_back(String::chr(E->get()));
+	}
+	return prefixes;
+}
+
+void CodeEdit::do_indent() {
+	if (is_readonly()) {
+		return;
+	}
+
+	if (is_selection_active()) {
+		indent_lines();
+		return;
+	}
+
+	if (!indent_using_spaces) {
+		_insert_text_at_cursor("\t");
+		return;
+	}
+
+	int spaces_to_add = _calculate_spaces_till_next_right_indent(cursor_get_column());
+	if (spaces_to_add > 0) {
+		_insert_text_at_cursor(String(" ").repeat(spaces_to_add));
+	}
+}
+
+void CodeEdit::indent_lines() {
+	if (is_readonly()) {
+		return;
+	}
+
+	begin_complex_operation();
+
+	/* This value informs us by how much we changed selection position by indenting right. */
+	/* Default is 1 for tab indentation.                                                   */
+	int selection_offset = 1;
+
+	int start_line = cursor_get_line();
+	int end_line = start_line;
+	if (is_selection_active()) {
+		start_line = get_selection_from_line();
+		end_line = get_selection_to_line();
+
+		/* Ignore the last line if the selection is not past the first column. */
+		if (get_selection_to_column() == 0) {
+			selection_offset = 0;
+			end_line--;
+		}
+	}
+
+	for (int i = start_line; i <= end_line; i++) {
+		const String line_text = get_line(i);
+		if (line_text.size() == 0 && is_selection_active()) {
+			continue;
+		}
+
+		if (!indent_using_spaces) {
+			set_line(i, '\t' + line_text);
+			continue;
+		}
+
+		/* We don't really care where selection is - we just need to know indentation level at the beginning of the line. */
+		/* Since we will add this many spaces, we want to move the whole selection and caret by this much.                */
+		int spaces_to_add = _calculate_spaces_till_next_right_indent(get_first_non_whitespace_column(i));
+		set_line(i, String(" ").repeat(spaces_to_add) + line_text);
+		selection_offset = spaces_to_add;
+	}
+
+	/* Fix selection and caret being off after shifting selection right.*/
+	if (is_selection_active()) {
+		select(start_line, get_selection_from_column() + selection_offset, get_selection_to_line(), get_selection_to_column() + selection_offset);
+	}
+	cursor_set_column(cursor_get_column() + selection_offset, false);
+
+	end_complex_operation();
+}
+
+void CodeEdit::do_unindent() {
+	if (is_readonly()) {
+		return;
+	}
+
+	int cc = cursor_get_column();
+
+	if (is_selection_active() || cc <= 0) {
+		unindent_lines();
+		return;
+	}
+
+	int cl = cursor_get_line();
+	const String &line = get_line(cl);
+
+	if (line[cc - 1] == '\t') {
+		_remove_text(cl, cc - 1, cl, cc);
+		cursor_set_column(MAX(0, cc - 1));
+		return;
+	}
+
+	if (line[cc - 1] != ' ') {
+		return;
+	}
+
+	int spaces_to_remove = _calculate_spaces_till_next_left_indent(cc);
+	if (spaces_to_remove > 0) {
+		for (int i = 1; i <= spaces_to_remove; i++) {
+			if (line[cc - i] != ' ') {
+				spaces_to_remove = i - 1;
+				break;
+			}
+		}
+		_remove_text(cl, cc - spaces_to_remove, cl, cc);
+		cursor_set_column(MAX(0, cc - spaces_to_remove));
+	}
+}
+
+void CodeEdit::unindent_lines() {
+	if (is_readonly()) {
+		return;
+	}
+
+	begin_complex_operation();
+
+	/* Moving caret and selection after unindenting can get tricky because                                                      */
+	/* changing content of line can move caret and selection on its own (if new line ends before previous position of either),  */
+	/* therefore we just remember initial values and at the end of the operation offset them by number of removed characters.   */
+	int removed_characters = 0;
+	int initial_selection_end_column = 0;
+	int initial_cursor_column = cursor_get_column();
+
+	int start_line = cursor_get_line();
+	int end_line = start_line;
+	if (is_selection_active()) {
+		start_line = get_selection_from_line();
+		end_line = get_selection_to_line();
+
+		/* Ignore the last line if the selection is not past the first column. */
+		initial_selection_end_column = get_selection_to_column();
+		if (initial_selection_end_column == 0) {
+			end_line--;
+		}
+	}
+
+	bool first_line_edited = false;
+	bool last_line_edited = false;
+
+	for (int i = start_line; i <= end_line; i++) {
+		String line_text = get_line(i);
+
+		if (line_text.begins_with("\t")) {
+			line_text = line_text.substr(1, line_text.length());
+
+			set_line(i, line_text);
+			removed_characters = 1;
+
+			first_line_edited = (i == start_line) ? true : first_line_edited;
+			last_line_edited = (i == end_line) ? true : last_line_edited;
+			continue;
+		}
+
+		if (line_text.begins_with(" ")) {
+			/* When unindenting we aim to remove spaces before line that has selection no matter what is selected,         */
+			/* Here we remove only enough spaces to align text to nearest full multiple of indentation_size.               */
+			/* In case where selection begins at the start of indentation_size multiple we remove whole indentation level. */
+			int spaces_to_remove = _calculate_spaces_till_next_left_indent(get_first_non_whitespace_column(i));
+			line_text = line_text.substr(spaces_to_remove, line_text.length());
+
+			set_line(i, line_text);
+			removed_characters = spaces_to_remove;
+
+			first_line_edited = (i == start_line) ? true : first_line_edited;
+			last_line_edited = (i == end_line) ? true : last_line_edited;
+		}
+	}
+
+	if (is_selection_active()) {
+		/* Fix selection being off by one on the first line. */
+		if (first_line_edited) {
+			select(get_selection_from_line(), get_selection_from_column() - removed_characters, get_selection_to_line(), initial_selection_end_column);
+		}
+
+		/* Fix selection being off by one on the last line. */
+		if (last_line_edited) {
+			select(get_selection_from_line(), get_selection_from_column(), get_selection_to_line(), initial_selection_end_column - removed_characters);
+		}
+	}
+	cursor_set_column(initial_cursor_column - removed_characters, false);
+
+	end_complex_operation();
+}
+
+int CodeEdit::_calculate_spaces_till_next_left_indent(int p_column) const {
+	int spaces_till_indent = p_column % indent_size;
+	if (spaces_till_indent == 0) {
+		spaces_till_indent = indent_size;
+	}
+	return spaces_till_indent;
+}
+
+int CodeEdit::_calculate_spaces_till_next_right_indent(int p_column) const {
+	return indent_size - p_column % indent_size;
+}
+
+/* TODO: remove once brace completion is refactored. */
+static char32_t _get_right_pair_symbol(char32_t c) {
+	if (c == '"') {
+		return '"';
+	}
+	if (c == '\'') {
+		return '\'';
+	}
+	if (c == '(') {
+		return ')';
+	}
+	if (c == '[') {
+		return ']';
+	}
+	if (c == '{') {
+		return '}';
+	}
+	return 0;
+}
+
+static bool _is_pair_left_symbol(char32_t c) {
+	return c == '"' ||
+		   c == '\'' ||
+		   c == '(' ||
+		   c == '[' ||
+		   c == '{';
+}
+
+void CodeEdit::_new_line(bool p_split_current_line, bool p_above) {
+	if (is_readonly()) {
+		return;
+	}
+
+	const int cc = cursor_get_column();
+	const int cl = cursor_get_line();
+	const String line = get_line(cl);
+
+	String ins = "\n";
+
+	/* Append current indentation. */
+	int space_count = 0;
+	int line_col = 0;
+	for (; line_col < cc; line_col++) {
+		if (line[line_col] == '\t') {
+			ins += indent_text;
+			space_count = 0;
+			continue;
+		}
+
+		if (line[line_col] == ' ') {
+			space_count++;
+
+			if (space_count == indent_size) {
+				ins += indent_text;
+				space_count = 0;
+			}
+			continue;
+		}
+		break;
+	}
+
+	if (is_line_folded(cl)) {
+		unfold_line(cl);
+	}
+
+	/* Indent once again if the previous line needs it, ie ':'.          */
+	/* Then add an addition new line for any closing pairs aka '()'.     */
+	/* Skip this in comments or if we are going above.                   */
+	bool brace_indent = false;
+	if (auto_indent && !p_above && cc > 0 && is_in_comment(cl) == -1) {
+		bool should_indent = false;
+		char32_t indent_char = ' ';
+
+		for (; line_col < cc; line_col++) {
+			char32_t c = line[line_col];
+			if (auto_indent_prefixes.has(c)) {
+				should_indent = true;
+				indent_char = c;
+				continue;
+			}
+
+			/* Make sure this is the last char, trailing whitespace or comments are okay. */
+			if (should_indent && (!_is_whitespace(c) && is_in_comment(cl, cc) == -1)) {
+				should_indent = false;
+			}
+		}
+
+		if (should_indent) {
+			ins += indent_text;
+
+			/* TODO: Change when brace completion is refactored. */
+			char32_t closing_char = _get_right_pair_symbol(indent_char);
+			if (closing_char != 0 && closing_char == line[cc]) {
+				/* No need to move the brace below if we are not taking the text with us. */
+				if (p_split_current_line) {
+					brace_indent = true;
+					ins += "\n" + ins.substr(1, ins.length() - 2);
+				} else {
+					brace_indent = false;
+					ins = "\n" + ins.substr(1, ins.length() - 2);
+				}
+			}
+		}
+	}
+
+	begin_complex_operation();
+
+	bool first_line = false;
+	if (!p_split_current_line) {
+		if (p_above) {
+			if (cl > 0) {
+				cursor_set_line(cl - 1, false);
+				cursor_set_column(get_line(cursor_get_line()).length());
+			} else {
+				cursor_set_column(0);
+				first_line = true;
+			}
+		} else {
+			cursor_set_column(line.length());
+		}
+	}
+
+	insert_text_at_cursor(ins);
+
+	if (first_line) {
+		cursor_set_line(0);
+	} else if (brace_indent) {
+		cursor_set_line(cursor_get_line() - 1, false);
+		cursor_set_column(get_line(cursor_get_line()).length());
+	}
+
+	end_complex_operation();
+}
+
+void CodeEdit::backspace() {
+	if (is_readonly()) {
+		return;
+	}
+
+	int cc = cursor_get_column();
+	int cl = cursor_get_line();
+
+	if (cc == 0 && cl == 0) {
+		return;
+	}
+
+	if (is_selection_active()) {
+		delete_selection();
+		return;
+	}
+
+	if (cl > 0 && is_line_hidden(cl - 1)) {
+		unfold_line(cursor_get_line() - 1);
+	}
+
+	int prev_line = cc ? cl : cl - 1;
+	int prev_column = cc ? (cc - 1) : (get_line(cl - 1).length());
+
+	merge_gutters(cl, prev_line);
+
+	/* TODO: Change when brace completion is refactored. */
+	if (auto_brace_completion_enabled && cc > 0 && _is_pair_left_symbol(get_line(cl)[cc - 1])) {
+		_consume_backspace_for_pair_symbol(prev_line, prev_column);
+		cursor_set_line(prev_line, false, true);
+		cursor_set_column(prev_column);
+		return;
+	}
+
+	/* For space indentation we need to do a simple unindent if there are no chars to the left, acting in the */
+	/* same way as tabs.                                                                                      */
+	if (indent_using_spaces && cc != 0) {
+		if (get_first_non_whitespace_column(cl) > cc) {
+			prev_column = cc - _calculate_spaces_till_next_left_indent(cc);
+			prev_line = cl;
+		}
+	}
+
+	_remove_text(prev_line, prev_column, cl, cc);
+
+	cursor_set_line(prev_line, false, true);
+	cursor_set_column(prev_column);
+}
+
 /* Main Gutter */
 void CodeEdit::_update_draw_main_gutter() {
 	set_gutter_draw(main_gutter, draw_breakpoints || draw_bookmarks || draw_executing_lines);
@@ -1286,6 +1743,25 @@ void CodeEdit::cancel_code_completion() {
 }
 
 void CodeEdit::_bind_methods() {
+	/* Indent management */
+	ClassDB::bind_method(D_METHOD("set_indent_size", "size"), &CodeEdit::set_indent_size);
+	ClassDB::bind_method(D_METHOD("get_indent_size"), &CodeEdit::get_indent_size);
+
+	ClassDB::bind_method(D_METHOD("set_indent_using_spaces", "use_spaces"), &CodeEdit::set_indent_using_spaces);
+	ClassDB::bind_method(D_METHOD("is_indent_using_spaces"), &CodeEdit::is_indent_using_spaces);
+
+	ClassDB::bind_method(D_METHOD("set_auto_indent_enabled", "enable"), &CodeEdit::set_auto_indent_enabled);
+	ClassDB::bind_method(D_METHOD("is_auto_indent_enabled"), &CodeEdit::is_auto_indent_enabled);
+
+	ClassDB::bind_method(D_METHOD("set_auto_indent_prefixes", "prefixes"), &CodeEdit::set_auto_indent_prefixes);
+	ClassDB::bind_method(D_METHOD("get_auto_indent_prefixes"), &CodeEdit::get_auto_indent_prefixes);
+
+	ClassDB::bind_method(D_METHOD("do_indent"), &CodeEdit::do_indent);
+	ClassDB::bind_method(D_METHOD("do_unindent"), &CodeEdit::do_unindent);
+
+	ClassDB::bind_method(D_METHOD("indent_lines"), &CodeEdit::indent_lines);
+	ClassDB::bind_method(D_METHOD("unindent_lines"), &CodeEdit::unindent_lines);
+
 	/* Main Gutter */
 	ClassDB::bind_method(D_METHOD("_main_gutter_draw_callback"), &CodeEdit::_main_gutter_draw_callback);
 
@@ -1436,6 +1912,12 @@ void CodeEdit::_bind_methods() {
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "code_completion_enabled"), "set_code_completion_enabled", "is_code_completion_enabled");
 	ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "code_completion_prefixes"), "set_code_completion_prefixes", "get_code_comletion_prefixes");
 
+	ADD_GROUP("Indentation", "indent_");
+	ADD_PROPERTY(PropertyInfo(Variant::INT, "indent_size"), "set_indent_size", "get_indent_size");
+	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "indent_use_spaces"), "set_indent_using_spaces", "is_indent_using_spaces");
+	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "indent_automatic"), "set_auto_indent_enabled", "is_auto_indent_enabled");
+	ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "indent_automatic_prefixes"), "set_auto_indent_prefixes", "get_auto_indent_prefixes");
+
 	/* Signals */
 	ADD_SIGNAL(MethodInfo("breakpoint_toggled", PropertyInfo(Variant::INT, "line")));
 	ADD_SIGNAL(MethodInfo("request_code_completion"));
@@ -2062,6 +2544,12 @@ void CodeEdit::_lines_edited_from(int p_from_line, int p_to_line) {
 }
 
 CodeEdit::CodeEdit() {
+	/* Indent management */
+	auto_indent_prefixes.insert(':');
+	auto_indent_prefixes.insert('{');
+	auto_indent_prefixes.insert('[');
+	auto_indent_prefixes.insert('(');
+
 	/* Text Direction */
 	set_layout_direction(LAYOUT_DIRECTION_LTR);
 	set_text_direction(TEXT_DIRECTION_LTR);

+ 34 - 0
scene/gui/code_edit.h

@@ -53,6 +53,19 @@ public:
 	};
 
 private:
+	/* Indent management */
+	int indent_size = 4;
+	String indent_text = "\t";
+
+	bool auto_indent = false;
+	Set<char32_t> auto_indent_prefixes;
+
+	bool indent_using_spaces = false;
+	int _calculate_spaces_till_next_left_indent(int p_column) const;
+	int _calculate_spaces_till_next_right_indent(int p_column) const;
+
+	void _new_line(bool p_split_current_line = true, bool p_above = false);
+
 	/* Main Gutter */
 	enum MainGutterType {
 		MAIN_GUTTER_BREAKPOINT = 0x01,
@@ -206,6 +219,27 @@ protected:
 public:
 	virtual CursorShape get_cursor_shape(const Point2 &p_pos = Point2i()) const override;
 
+	/* Indent management */
+	void set_indent_size(const int p_size);
+	int get_indent_size() const;
+
+	void set_indent_using_spaces(const bool p_use_spaces);
+	bool is_indent_using_spaces() const;
+
+	void set_auto_indent_enabled(bool p_enabled);
+	bool is_auto_indent_enabled() const;
+
+	void set_auto_indent_prefixes(const TypedArray<String> &p_prefixes);
+	TypedArray<String> get_auto_indent_prefixes() const;
+
+	void do_indent();
+	void do_unindent();
+
+	void indent_lines();
+	void unindent_lines();
+
+	virtual void backspace() override;
+
 	/* Main Gutter */
 	void set_draw_breakpoints_gutter(bool p_draw);
 	bool is_drawing_breakpoints_gutter() const;

+ 111 - 430
scene/gui/text_edit.cpp

@@ -102,14 +102,6 @@ static char32_t _get_right_pair_symbol(char32_t c) {
 	return 0;
 }
 
-static int _find_first_non_whitespace_column_of_line(const String &line) {
-	int left = 0;
-	while (left < line.length() && _is_whitespace(line[left])) {
-		left++;
-	}
-	return left;
-}
-
 ///////////////////////////////////////////////////////////////////////////////
 
 void TextEdit::Text::set_font(const Ref<Font> &p_font) {
@@ -120,8 +112,12 @@ void TextEdit::Text::set_font_size(int p_font_size) {
 	font_size = p_font_size;
 }
 
-void TextEdit::Text::set_indent_size(int p_indent_size) {
-	indent_size = p_indent_size;
+void TextEdit::Text::set_tab_size(int p_tab_size) {
+	tab_size = p_tab_size;
+}
+
+int TextEdit::Text::get_tab_size() const {
+	return tab_size;
 }
 
 void TextEdit::Text::set_font_features(const Dictionary &p_features) {
@@ -204,9 +200,9 @@ void TextEdit::Text::invalidate_cache(int p_line, int p_column, const String &p_
 	}
 
 	// Apply tab align.
-	if (indent_size > 0) {
+	if (tab_size > 0) {
 		Vector<float> tabs;
-		tabs.push_back(font->get_char_size(' ', 0, font_size).width * indent_size);
+		tabs.push_back(font->get_char_size(' ', 0, font_size).width * tab_size);
 		text.write[p_line].data_buf->tab_align(tabs);
 	}
 }
@@ -214,9 +210,9 @@ void TextEdit::Text::invalidate_cache(int p_line, int p_column, const String &p_
 void TextEdit::Text::invalidate_all_lines() {
 	for (int i = 0; i < text.size(); i++) {
 		text.write[i].data_buf->set_width(width);
-		if (indent_size > 0) {
+		if (tab_size > 0) {
 			Vector<float> tabs;
-			tabs.push_back(font->get_char_size(' ', 0, font_size).width * indent_size);
+			tabs.push_back(font->get_char_size(' ', 0, font_size).width * tab_size);
 			text.write[i].data_buf->tab_align(tabs);
 		}
 	}
@@ -822,7 +818,7 @@ void TextEdit::_notification(int p_what) {
 			if (draw_minimap) {
 				int minimap_visible_lines = _get_minimap_visible_rows();
 				int minimap_line_height = (minimap_char_size.y + minimap_line_spacing);
-				int minimap_tab_size = minimap_char_size.x * indent_size;
+				int minimap_tab_size = minimap_char_size.x * text.get_tab_size();
 
 				// calculate viewport size and y offset
 				int viewport_height = (draw_amount - 1) * minimap_line_height;
@@ -1695,7 +1691,13 @@ void TextEdit::_consume_backspace_for_pair_symbol(int prev_line, int prev_column
 	}
 }
 
-void TextEdit::backspace_at_cursor() {
+void TextEdit::backspace() {
+	ScriptInstance *si = get_script_instance();
+	if (si && si->has_method("_backspace")) {
+		si->call("_backspace");
+		return;
+	}
+
 	if (readonly) {
 		return;
 	}
@@ -1704,34 +1706,15 @@ void TextEdit::backspace_at_cursor() {
 		return;
 	}
 
+	if (is_selection_active()) {
+		delete_selection();
+		return;
+	}
+
 	int prev_line = cursor.column ? cursor.line : cursor.line - 1;
 	int prev_column = cursor.column ? (cursor.column - 1) : (text[cursor.line - 1].length());
 
-	if (cursor.line != prev_line) {
-		for (int i = 0; i < gutters.size(); i++) {
-			if (!gutters[i].overwritable) {
-				continue;
-			}
-
-			if (text.get_line_gutter_text(cursor.line, i) != "") {
-				text.set_line_gutter_text(prev_line, i, text.get_line_gutter_text(cursor.line, i));
-				text.set_line_gutter_item_color(prev_line, i, text.get_line_gutter_item_color(cursor.line, i));
-			}
-
-			if (text.get_line_gutter_icon(cursor.line, i).is_valid()) {
-				text.set_line_gutter_icon(prev_line, i, text.get_line_gutter_icon(cursor.line, i));
-				text.set_line_gutter_item_color(prev_line, i, text.get_line_gutter_item_color(cursor.line, i));
-			}
-
-			if (text.get_line_gutter_metadata(cursor.line, i) != "") {
-				text.set_line_gutter_metadata(prev_line, i, text.get_line_gutter_metadata(cursor.line, i));
-			}
-
-			if (text.is_line_gutter_clickable(cursor.line, i)) {
-				text.set_line_gutter_clickable(prev_line, i, true);
-			}
-		}
-	}
+	merge_gutters(cursor.line, prev_line);
 
 	if (is_line_hidden(cursor.line)) {
 		set_line_as_hidden(prev_line, true);
@@ -1742,168 +1725,13 @@ void TextEdit::backspace_at_cursor() {
 			_is_pair_left_symbol(text[cursor.line][cursor.column - 1])) {
 		_consume_backspace_for_pair_symbol(prev_line, prev_column);
 	} else {
-		// Handle space indentation.
-		if (cursor.column != 0 && indent_using_spaces) {
-			// Check if there are no other chars before cursor, just indentation.
-			bool unindent = true;
-			int i = 0;
-			while (i < cursor.column && i < text[cursor.line].length()) {
-				if (!_is_whitespace(text[cursor.line][i])) {
-					unindent = false;
-					break;
-				}
-				i++;
-			}
-
-			// Then we can remove all spaces as a single character.
-			if (unindent) {
-				// We want to remove spaces up to closest indent, or whole indent if cursor is pointing at it.
-				int spaces_to_delete = _calculate_spaces_till_next_left_indent(cursor.column);
-				prev_column = cursor.column - spaces_to_delete;
-				_remove_text(cursor.line, prev_column, cursor.line, cursor.column);
-			} else {
-				_remove_text(prev_line, prev_column, cursor.line, cursor.column);
-			}
-		} else {
-			_remove_text(prev_line, prev_column, cursor.line, cursor.column);
-		}
+		_remove_text(prev_line, prev_column, cursor.line, cursor.column);
 	}
 
 	cursor_set_line(prev_line, false, true);
 	cursor_set_column(prev_column);
 }
 
-void TextEdit::indent_selected_lines_right() {
-	int start_line;
-	int end_line;
-
-	// This value informs us by how much we changed selection position by indenting right.
-	// Default is 1 for tab indentation.
-	int selection_offset = 1;
-	begin_complex_operation();
-
-	if (is_selection_active()) {
-		start_line = get_selection_from_line();
-		end_line = get_selection_to_line();
-	} else {
-		start_line = cursor.line;
-		end_line = start_line;
-	}
-
-	// Ignore if the cursor is not past the first column.
-	if (is_selection_active() && get_selection_to_column() == 0) {
-		selection_offset = 0;
-		end_line--;
-	}
-
-	for (int i = start_line; i <= end_line; i++) {
-		String line_text = get_line(i);
-		if (line_text.size() == 0 && is_selection_active()) {
-			continue;
-		}
-		if (indent_using_spaces) {
-			// We don't really care where selection is - we just need to know indentation level at the beginning of the line.
-			int left = _find_first_non_whitespace_column_of_line(line_text);
-			int spaces_to_add = _calculate_spaces_till_next_right_indent(left);
-			// Since we will add these many spaces, we want to move the whole selection and cursor by this much.
-			selection_offset = spaces_to_add;
-			for (int j = 0; j < spaces_to_add; j++) {
-				line_text = ' ' + line_text;
-			}
-		} else {
-			line_text = '\t' + line_text;
-		}
-		set_line(i, line_text);
-	}
-
-	// Fix selection and cursor being off after shifting selection right.
-	if (is_selection_active()) {
-		select(selection.from_line, selection.from_column + selection_offset, selection.to_line, selection.to_column + selection_offset);
-	}
-	cursor_set_column(cursor.column + selection_offset, false);
-	end_complex_operation();
-	update();
-}
-
-void TextEdit::indent_selected_lines_left() {
-	int start_line;
-	int end_line;
-
-	// Moving cursor and selection after unindenting can get tricky because
-	// changing content of line can move cursor and selection on its own (if new line ends before previous position of either),
-	// therefore we just remember initial values and at the end of the operation offset them by number of removed characters.
-	int removed_characters = 0;
-	int initial_selection_end_column = selection.to_column;
-	int initial_cursor_column = cursor.column;
-
-	begin_complex_operation();
-
-	if (is_selection_active()) {
-		start_line = get_selection_from_line();
-		end_line = get_selection_to_line();
-	} else {
-		start_line = cursor.line;
-		end_line = start_line;
-	}
-
-	// Ignore if the cursor is not past the first column.
-	if (is_selection_active() && get_selection_to_column() == 0) {
-		end_line--;
-	}
-	String first_line_text = get_line(start_line);
-	String last_line_text = get_line(end_line);
-
-	for (int i = start_line; i <= end_line; i++) {
-		String line_text = get_line(i);
-
-		if (line_text.begins_with("\t")) {
-			line_text = line_text.substr(1, line_text.length());
-			set_line(i, line_text);
-			removed_characters = 1;
-		} else if (line_text.begins_with(" ")) {
-			// When unindenting we aim to remove spaces before line that has selection no matter what is selected,
-			// so we start of by finding first non whitespace character of line
-			int left = _find_first_non_whitespace_column_of_line(line_text);
-
-			// Here we remove only enough spaces to align text to nearest full multiple of indentation_size.
-			// In case where selection begins at the start of indentation_size multiple we remove whole indentation level.
-			int spaces_to_remove = _calculate_spaces_till_next_left_indent(left);
-
-			line_text = line_text.substr(spaces_to_remove, line_text.length());
-			set_line(i, line_text);
-			removed_characters = spaces_to_remove;
-		}
-	}
-
-	if (is_selection_active()) {
-		// Fix selection being off by one on the first line.
-		if (first_line_text != get_line(start_line)) {
-			select(selection.from_line, selection.from_column - removed_characters,
-					selection.to_line, initial_selection_end_column);
-		}
-		// Fix selection being off by one on the last line.
-		if (last_line_text != get_line(end_line)) {
-			select(selection.from_line, selection.from_column,
-					selection.to_line, initial_selection_end_column - removed_characters);
-		}
-	}
-	cursor_set_column(initial_cursor_column - removed_characters, false);
-	end_complex_operation();
-	update();
-}
-
-int TextEdit::_calculate_spaces_till_next_left_indent(int column) {
-	int spaces_till_indent = column % indent_size;
-	if (spaces_till_indent == 0) {
-		spaces_till_indent = indent_size;
-	}
-	return spaces_till_indent;
-}
-
-int TextEdit::_calculate_spaces_till_next_right_indent(int column) {
-	return indent_size - column % indent_size;
-}
-
 void TextEdit::_swap_current_input_direction() {
 	if (input_direction == TEXT_DIRECTION_LTR) {
 		input_direction = TEXT_DIRECTION_RTL;
@@ -1919,90 +1747,8 @@ void TextEdit::_new_line(bool p_split_current_line, bool p_above) {
 		return;
 	}
 
-	String ins = "\n";
-
-	// Keep indentation.
-	int space_count = 0;
-	for (int i = 0; i < cursor.column; i++) {
-		if (text[cursor.line][i] == '\t') {
-			if (indent_using_spaces) {
-				ins += space_indent;
-			} else {
-				ins += "\t";
-			}
-			space_count = 0;
-		} else if (text[cursor.line][i] == ' ') {
-			space_count++;
-
-			if (space_count == indent_size) {
-				if (indent_using_spaces) {
-					ins += space_indent;
-				} else {
-					ins += "\t";
-				}
-				space_count = 0;
-			}
-		} else {
-			break;
-		}
-	}
-
-	bool brace_indent = false;
-
-	// No need to indent if we are going upwards.
-	if (auto_indent && !p_above) {
-		// Indent once again if previous line will end with ':','{','[','(' and the line is not a comment
-		// (i.e. colon/brace precedes current cursor position).
-		if (cursor.column > 0) {
-			bool indent_char_found = false;
-			bool should_indent = false;
-			char indent_char = ':';
-			char c = text[cursor.line][cursor.column];
-
-			for (int i = 0; i < cursor.column; i++) {
-				c = text[cursor.line][i];
-				switch (c) {
-					case ':':
-					case '{':
-					case '[':
-					case '(':
-						indent_char_found = true;
-						should_indent = true;
-						indent_char = c;
-						continue;
-				}
-
-				if (indent_char_found && is_line_comment(cursor.line)) {
-					should_indent = true;
-					break;
-				} else if (indent_char_found && !_is_whitespace(c)) {
-					should_indent = false;
-					indent_char_found = false;
-				}
-			}
-
-			if (!is_line_comment(cursor.line) && should_indent) {
-				if (indent_using_spaces) {
-					ins += space_indent;
-				} else {
-					ins += "\t";
-				}
-
-				// No need to move the brace below if we are not taking the text with us.
-				char32_t closing_char = _get_right_pair_symbol(indent_char);
-				if ((closing_char != 0) && (closing_char == text[cursor.line][cursor.column])) {
-					if (p_split_current_line) {
-						brace_indent = true;
-						ins += "\n" + ins.substr(1, ins.length() - 2);
-					} else {
-						brace_indent = false;
-						ins = "\n" + ins.substr(1, ins.length() - 2);
-					}
-				}
-			}
-		}
-	}
 	begin_complex_operation();
+
 	bool first_line = false;
 	if (!p_split_current_line) {
 		if (p_above) {
@@ -2018,83 +1764,13 @@ void TextEdit::_new_line(bool p_split_current_line, bool p_above) {
 		}
 	}
 
-	insert_text_at_cursor(ins);
+	insert_text_at_cursor("\n");
 
 	if (first_line) {
 		cursor_set_line(0);
-	} else if (brace_indent) {
-		cursor_set_line(cursor.line - 1, false);
-		cursor_set_column(text[cursor.line].length());
-	}
-	end_complex_operation();
-}
-
-void TextEdit::_indent_right() {
-	if (readonly) {
-		return;
-	}
-
-	if (is_selection_active()) {
-		indent_selected_lines_right();
-	} else {
-		// Simple indent.
-		if (indent_using_spaces) {
-			// Insert only as much spaces as needed till next indentation level.
-			int spaces_to_add = _calculate_spaces_till_next_right_indent(cursor.column);
-			String indent_to_insert = String();
-			for (int i = 0; i < spaces_to_add; i++) {
-				indent_to_insert = ' ' + indent_to_insert;
-			}
-			_insert_text_at_cursor(indent_to_insert);
-		} else {
-			_insert_text_at_cursor("\t");
-		}
-	}
-}
-
-void TextEdit::_indent_left() {
-	if (readonly) {
-		return;
 	}
 
-	if (is_selection_active()) {
-		indent_selected_lines_left();
-	} else {
-		// Simple unindent.
-		int cc = cursor.column;
-		const String &line = text[cursor.line];
-
-		int left = _find_first_non_whitespace_column_of_line(line);
-		cc = MIN(cc, left);
-
-		while (cc < indent_size && cc < left && line[cc] == ' ') {
-			cc++;
-		}
-
-		if (cc > 0 && cc <= text[cursor.line].length()) {
-			if (text[cursor.line][cc - 1] == '\t') {
-				// Tabs unindentation.
-				_remove_text(cursor.line, cc - 1, cursor.line, cc);
-				if (cursor.column >= left) {
-					cursor_set_column(MAX(0, cursor.column - 1));
-				}
-				update();
-			} else {
-				// Spaces unindentation.
-				int spaces_to_remove = _calculate_spaces_till_next_left_indent(cc);
-				if (spaces_to_remove > 0) {
-					_remove_text(cursor.line, cc - spaces_to_remove, cursor.line, cc);
-					if (cursor.column > left - spaces_to_remove) { // Inside text?
-						cursor_set_column(MAX(0, cursor.column - spaces_to_remove));
-					}
-					update();
-				}
-			}
-		} else if (cc == 0 && line.length() > 0 && line[0] == '\t') {
-			_remove_text(cursor.line, 0, cursor.line, 1);
-			update();
-		}
-	}
+	end_complex_operation();
 }
 
 void TextEdit::_move_cursor_left(bool p_select, bool p_move_by_word) {
@@ -2331,20 +2007,24 @@ void TextEdit::_move_cursor_page_down(bool p_select) {
 	}
 }
 
-void TextEdit::_backspace(bool p_word, bool p_all_to_left) {
+void TextEdit::_do_backspace(bool p_word, bool p_all_to_left) {
 	if (readonly) {
 		return;
 	}
 
-	if (is_selection_active()) {
-		_delete_selection();
+	if (is_selection_active() || (!p_all_to_left && !p_word)) {
+		backspace();
 		return;
 	}
+
 	if (p_all_to_left) {
 		int cursor_current_column = cursor.column;
 		cursor.column = 0;
 		_remove_text(cursor.line, 0, cursor.line, cursor_current_column);
-	} else if (p_word) {
+		return;
+	}
+
+	if (p_word) {
 		int line = cursor.line;
 		int column = cursor.column;
 
@@ -2360,8 +2040,7 @@ void TextEdit::_backspace(bool p_word, bool p_all_to_left) {
 
 		cursor_set_line(line, false);
 		cursor_set_column(column);
-	} else {
-		backspace_at_cursor();
+		return;
 	}
 }
 
@@ -2371,7 +2050,7 @@ void TextEdit::_delete(bool p_word, bool p_all_to_right) {
 	}
 
 	if (is_selection_active()) {
-		_delete_selection();
+		delete_selection();
 		return;
 	}
 	int curline_len = text[cursor.line].length();
@@ -2416,15 +2095,16 @@ void TextEdit::_delete(bool p_word, bool p_all_to_right) {
 	update();
 }
 
-void TextEdit::_delete_selection() {
-	if (is_selection_active()) {
-		selection.active = false;
-		update();
-		_remove_text(selection.from_line, selection.from_column, selection.to_line, selection.to_column);
-		cursor_set_line(selection.from_line, false, false);
-		cursor_set_column(selection.from_column);
-		update();
+void TextEdit::delete_selection() {
+	if (!is_selection_active()) {
+		return;
 	}
+
+	selection.active = false;
+	_remove_text(selection.from_line, selection.from_column, selection.to_line, selection.to_column);
+	cursor_set_line(selection.from_line, false, false);
+	cursor_set_column(selection.from_column);
+	update();
 }
 
 void TextEdit::_move_cursor_document_start(bool p_select) {
@@ -2459,7 +2139,7 @@ void TextEdit::_move_cursor_document_end(bool p_select) {
 
 void TextEdit::_handle_unicode_character(uint32_t unicode, bool p_had_selection) {
 	if (p_had_selection) {
-		_delete_selection();
+		delete_selection();
 	}
 
 	// Remove the old character if in insert mode and no selection.
@@ -2942,31 +2622,19 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) {
 			return;
 		}
 
-		// INDENTATION.
-		if (k->is_action("ui_text_dedent", true)) {
-			_indent_left();
-			accept_event();
-			return;
-		}
-		if (k->is_action("ui_text_indent", true)) {
-			_indent_right();
-			accept_event();
-			return;
-		}
-
 		// BACKSPACE AND DELETE.
 		if (k->is_action("ui_text_backspace_all_to_left", true)) {
-			_backspace(false, true);
+			_do_backspace(false, true);
 			accept_event();
 			return;
 		}
 		if (k->is_action("ui_text_backspace_word", true)) {
-			_backspace(true);
+			_do_backspace(true);
 			accept_event();
 			return;
 		}
 		if (k->is_action("ui_text_backspace", true)) {
-			_backspace();
+			_do_backspace();
 			accept_event();
 			return;
 		}
@@ -4540,6 +4208,39 @@ bool TextEdit::is_gutter_overwritable(int p_gutter) const {
 	return gutters[p_gutter].overwritable;
 }
 
+void TextEdit::merge_gutters(int p_from_line, int p_to_line) {
+	ERR_FAIL_INDEX(p_from_line, text.size());
+	ERR_FAIL_INDEX(p_to_line, text.size());
+	if (p_from_line == p_to_line) {
+		return;
+	}
+
+	for (int i = 0; i < gutters.size(); i++) {
+		if (!gutters[i].overwritable) {
+			continue;
+		}
+
+		if (text.get_line_gutter_text(p_from_line, i) != "") {
+			text.set_line_gutter_text(p_to_line, i, text.get_line_gutter_text(p_from_line, i));
+			text.set_line_gutter_item_color(p_to_line, i, text.get_line_gutter_item_color(p_from_line, i));
+		}
+
+		if (text.get_line_gutter_icon(p_from_line, i).is_valid()) {
+			text.set_line_gutter_icon(p_to_line, i, text.get_line_gutter_icon(p_from_line, i));
+			text.set_line_gutter_item_color(p_to_line, i, text.get_line_gutter_item_color(p_from_line, i));
+		}
+
+		if (text.get_line_gutter_metadata(p_from_line, i) != "") {
+			text.set_line_gutter_metadata(p_to_line, i, text.get_line_gutter_metadata(p_from_line, i));
+		}
+
+		if (text.is_line_gutter_clickable(p_from_line, i)) {
+			text.set_line_gutter_clickable(p_to_line, i, true);
+		}
+	}
+	update();
+}
+
 void TextEdit::set_gutter_custom_draw(int p_gutter, Object *p_object, const StringName &p_callback) {
 	ERR_FAIL_INDEX(p_gutter, gutters.size());
 	ERR_FAIL_NULL(p_object);
@@ -4625,10 +4326,6 @@ Color TextEdit::get_line_background_color(int p_line) {
 	return text.get_line_background_color(p_line);
 }
 
-void TextEdit::set_auto_indent(bool p_auto_indent) {
-	auto_indent = p_auto_indent;
-}
-
 void TextEdit::cut() {
 	if (readonly) {
 		return;
@@ -4644,7 +4341,7 @@ void TextEdit::cut() {
 			_remove_text(cursor.line, 0, cursor.line + 1, 0);
 		} else {
 			_remove_text(cursor.line, 0, cursor.line, text[cursor.line].length());
-			backspace_at_cursor();
+			backspace();
 			cursor_set_line(cursor.line + 1);
 		}
 
@@ -5191,7 +4888,6 @@ int TextEdit::get_last_unhidden_line() const {
 int TextEdit::get_indent_level(int p_line) const {
 	ERR_FAIL_INDEX_V(p_line, text.size(), 0);
 
-	// Counts number of tabs and spaces before line starts.
 	int tab_count = 0;
 	int whitespace_count = 0;
 	int line_length = text[p_line].size();
@@ -5204,28 +4900,17 @@ int TextEdit::get_indent_level(int p_line) const {
 			break;
 		}
 	}
-	return tab_count * indent_size + whitespace_count;
+	return tab_count * text.get_tab_size() + whitespace_count;
 }
 
-bool TextEdit::is_line_comment(int p_line) const {
-	// Checks to see if this line is the start of a comment.
-	ERR_FAIL_INDEX_V(p_line, text.size(), false);
+int TextEdit::get_first_non_whitespace_column(int p_line) const {
+	ERR_FAIL_INDEX_V(p_line, text.size(), 0);
 
-	int line_length = text[p_line].size();
-	for (int i = 0; i < line_length - 1; i++) {
-		if (_is_whitespace(text[p_line][i])) {
-			continue;
-		}
-		if (_is_symbol(text[p_line][i])) {
-			if (text[p_line][i] == '\\') {
-				i++; // Skip quoted anything.
-				continue;
-			}
-			return text[p_line][i] == '#' || (i + 1 < line_length && text[p_line][i] == '/' && text[p_line][i + 1] == '/');
-		}
-		break;
+	int col = 0;
+	while (col < text[p_line].length() && _is_whitespace(text[p_line][col])) {
+		col++;
 	}
-	return false;
+	return col;
 }
 
 int TextEdit::get_line_count() const {
@@ -5401,32 +5086,18 @@ void TextEdit::_push_current_op() {
 	}
 }
 
-void TextEdit::set_indent_using_spaces(const bool p_use_spaces) {
-	indent_using_spaces = p_use_spaces;
-}
-
-bool TextEdit::is_indent_using_spaces() const {
-	return indent_using_spaces;
-}
-
-void TextEdit::set_indent_size(const int p_size) {
-	ERR_FAIL_COND_MSG(p_size <= 0, "Indend size must be greater than 0.");
-	if (indent_size != p_size) {
-		indent_size = p_size;
-		text.set_indent_size(p_size);
-		text.invalidate_all_lines();
-	}
-
-	space_indent = "";
-	for (int i = 0; i < p_size; i++) {
-		space_indent += " ";
+void TextEdit::set_tab_size(const int p_size) {
+	ERR_FAIL_COND_MSG(p_size <= 0, "Tab size must be greater than 0.");
+	if (p_size == text.get_tab_size()) {
+		return;
 	}
-
+	text.set_tab_size(p_size);
+	text.invalidate_all_lines();
 	update();
 }
 
-int TextEdit::get_indent_size() {
-	return indent_size;
+int TextEdit::get_tab_size() const {
+	return text.get_tab_size();
 }
 
 void TextEdit::set_draw_tabs(bool p_draw) {
@@ -6019,6 +5690,11 @@ void TextEdit::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_language", "language"), &TextEdit::set_language);
 	ClassDB::bind_method(D_METHOD("get_language"), &TextEdit::get_language);
 
+	ClassDB::bind_method(D_METHOD("get_first_non_whitespace_column", "line"), &TextEdit::get_first_non_whitespace_column);
+	ClassDB::bind_method(D_METHOD("get_indent_level", "line"), &TextEdit::get_indent_level);
+	ClassDB::bind_method(D_METHOD("set_tab_size", "size"), &TextEdit::set_tab_size);
+	ClassDB::bind_method(D_METHOD("get_tab_size"), &TextEdit::get_tab_size);
+
 	ClassDB::bind_method(D_METHOD("set_text", "text"), &TextEdit::set_text);
 	ClassDB::bind_method(D_METHOD("insert_text_at_cursor", "text"), &TextEdit::insert_text_at_cursor);
 
@@ -6073,6 +5749,10 @@ void TextEdit::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_selecting_enabled", "enable"), &TextEdit::set_selecting_enabled);
 	ClassDB::bind_method(D_METHOD("is_selecting_enabled"), &TextEdit::is_selecting_enabled);
 
+	ClassDB::bind_method(D_METHOD("delete_selection"), &TextEdit::delete_selection);
+	ClassDB::bind_method(D_METHOD("backspace"), &TextEdit::backspace);
+	BIND_VMETHOD(MethodInfo("_backspace"));
+
 	ClassDB::bind_method(D_METHOD("cut"), &TextEdit::cut);
 	ClassDB::bind_method(D_METHOD("copy"), &TextEdit::copy);
 	ClassDB::bind_method(D_METHOD("paste"), &TextEdit::paste);
@@ -6128,6 +5808,7 @@ void TextEdit::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("is_gutter_clickable", "gutter"), &TextEdit::is_gutter_clickable);
 	ClassDB::bind_method(D_METHOD("set_gutter_overwritable", "gutter", "overwritable"), &TextEdit::set_gutter_overwritable);
 	ClassDB::bind_method(D_METHOD("is_gutter_overwritable", "gutter"), &TextEdit::is_gutter_overwritable);
+	ClassDB::bind_method(D_METHOD("merge_gutters", "from_line", "to_line"), &TextEdit::merge_gutters);
 	ClassDB::bind_method(D_METHOD("set_gutter_custom_draw", "column", "object", "callback"), &TextEdit::set_gutter_custom_draw);
 
 	// Line gutters.
@@ -6254,7 +5935,7 @@ TextEdit::TextEdit() {
 	_update_caches();
 	set_default_cursor_shape(CURSOR_IBEAM);
 
-	text.set_indent_size(indent_size);
+	text.set_tab_size(text.get_tab_size());
 	text.clear();
 
 	h_scroll = memnew(HScrollBar);

+ 12 - 22
scene/gui/text_edit.h

@@ -112,11 +112,12 @@ private:
 
 		int width = -1;
 
-		int indent_size = 4;
+		int tab_size = 4;
 		int gutter_count = 0;
 
 	public:
-		void set_indent_size(int p_indent_size);
+		void set_tab_size(int p_tab_size);
+		int get_tab_size() const;
 		void set_font(const Ref<Font> &p_font);
 		void set_font_size(int p_font_size);
 		void set_font_features(const Dictionary &p_features);
@@ -259,9 +260,6 @@ private:
 
 	int max_chars = 0;
 	bool readonly = true; // Initialise to opposite first, so we get past the early-out in set_readonly.
-	bool indent_using_spaces = false;
-	int indent_size = 4;
-	String space_indent = "    ";
 
 	Timer *caret_blink_timer;
 	bool caret_blink_enabled = false;
@@ -296,7 +294,6 @@ private:
 	bool scroll_past_end_of_file_enabled = false;
 	bool brace_matching_enabled = false;
 	bool highlight_current_line = false;
-	bool auto_indent = false;
 
 	String cut_copy_line;
 	bool insert_mode = false;
@@ -420,14 +417,9 @@ private:
 
 	void _clear();
 
-	int _calculate_spaces_till_next_left_indent(int column);
-	int _calculate_spaces_till_next_right_indent(int column);
-
 	// Methods used in shortcuts
 	void _swap_current_input_direction();
 	void _new_line(bool p_split_current = true, bool p_above = false);
-	void _indent_right();
-	void _indent_left();
 	void _move_cursor_left(bool p_select, bool p_move_by_word = false);
 	void _move_cursor_right(bool p_select, bool p_move_by_word = false);
 	void _move_cursor_up(bool p_select);
@@ -436,9 +428,8 @@ private:
 	void _move_cursor_to_line_end(bool p_select);
 	void _move_cursor_page_up(bool p_select);
 	void _move_cursor_page_down(bool p_select);
-	void _backspace(bool p_word = false, bool p_all_to_left = false);
+	void _do_backspace(bool p_word = false, bool p_all_to_left = false);
 	void _delete(bool p_word = false, bool p_all_to_right = false);
-	void _delete_selection();
 	void _move_cursor_document_start(bool p_select);
 	void _move_cursor_document_end(bool p_select);
 	void _handle_unicode_character(uint32_t unicode, bool p_had_selection);
@@ -522,6 +513,8 @@ public:
 	void set_gutter_overwritable(int p_gutter, bool p_overwritable);
 	bool is_gutter_overwritable(int p_gutter) const;
 
+	void merge_gutters(int p_from_line, int p_to_line);
+
 	void set_gutter_custom_draw(int p_gutter, Object *p_object, const StringName &p_callback);
 
 	// Line gutters.
@@ -635,12 +628,9 @@ public:
 	bool has_ime_text() const;
 	void set_line(int line, String new_text);
 	int get_row_height() const;
-	void backspace_at_cursor();
 
-	void indent_selected_lines_left();
-	void indent_selected_lines_right();
 	int get_indent_level(int p_line) const;
-	bool is_line_comment(int p_line) const;
+	int get_first_non_whitespace_column(int p_line) const;
 
 	inline void set_scroll_pass_end_of_file(bool p_enabled) {
 		scroll_past_end_of_file_enabled = p_enabled;
@@ -653,7 +643,6 @@ public:
 		brace_matching_enabled = p_enabled;
 		update();
 	}
-	void set_auto_indent(bool p_auto_indent);
 
 	void center_viewport_to_cursor();
 
@@ -699,6 +688,9 @@ public:
 
 	void clear();
 
+	void delete_selection();
+
+	virtual void backspace();
 	void cut();
 	void copy();
 	void paste();
@@ -730,10 +722,8 @@ public:
 	void redo();
 	void clear_undo_history();
 
-	void set_indent_using_spaces(const bool p_use_spaces);
-	bool is_indent_using_spaces() const;
-	void set_indent_size(const int p_size);
-	int get_indent_size();
+	void set_tab_size(const int p_size);
+	int get_tab_size() const;
 	void set_draw_tabs(bool p_draw);
 	bool is_drawing_tabs() const;
 	void set_draw_spaces(bool p_draw);