Browse Source

Move auto brace completion to CodeEdit

Paulb23 4 năm trước cách đây
mục cha
commit
12f0053555

+ 45 - 0
doc/classes/CodeEdit.xml

@@ -30,6 +30,18 @@
 				Override this method to define what happens when the user requests code completion. If [code]force[/code] is true, any checks should be bypassed.
 			</description>
 		</method>
+		<method name="add_auto_brace_completion_pair">
+			<return type="void">
+			</return>
+			<argument index="0" name="start_key" type="String">
+			</argument>
+			<argument index="1" name="end_key" type="String">
+			</argument>
+			<description>
+				Adds a brace pair.
+				Both the start and end keys must be symbols. Only the start key has to be unique.
+			</description>
+		</method>
 		<method name="add_code_completion_option">
 			<return type="void" />
 			<argument index="0" name="type" type="int" enum="CodeEdit.CodeCompletionKind" />
@@ -137,6 +149,15 @@
 				Folds the given line, if possible (see [method can_fold_line]).
 			</description>
 		</method>
+		<method name="get_auto_brace_completion_close_key" qualifiers="const">
+			<return type="String">
+			</return>
+			<argument index="0" name="open_key" type="String">
+			</argument>
+			<description>
+				Gets the matching auto brace close key for [code]open_key[/code].
+			</description>
+		</method>
 		<method name="get_bookmarked_lines" qualifiers="const">
 			<return type="Array" />
 			<description>
@@ -219,6 +240,24 @@
 				Returns the full text with char [code]0xFFFF[/code] at the caret location.
 			</description>
 		</method>
+		<method name="has_auto_brace_completion_close_key" qualifiers="const">
+			<return type="bool">
+			</return>
+			<argument index="0" name="close_key" type="String">
+			</argument>
+			<description>
+				Returns [code]true[/code] if close key [code]close_key[/code] exists.
+			</description>
+		</method>
+		<method name="has_auto_brace_completion_open_key" qualifiers="const">
+			<return type="bool">
+			</return>
+			<argument index="0" name="open_key" type="String">
+			</argument>
+			<description>
+				Returns [code]true[/code] if open key [code]open_key[/code] exists.
+			</description>
+		</method>
 		<method name="has_comment_delimiter" qualifiers="const">
 			<return type="bool" />
 			<argument index="0" name="start_key" type="String" />
@@ -378,6 +417,12 @@
 		</method>
 	</methods>
 	<members>
+		<member name="auto_brace_completion_enabled" type="bool" setter="set_auto_brace_completion_enabled" getter="is_auto_brace_completion_enabled" default="false">
+			Sets whether brace pairs should be autocompleted.
+		</member>
+		<member name="auto_brace_completion_pairs" type="Dictionary" setter="set_auto_brace_completion_pairs" getter="get_auto_brace_completion_pairs" default="{&quot;\&quot;&quot;: &quot;\&quot;&quot;,&quot;&apos;&quot;: &quot;&apos;&quot;,&quot;(&quot;: &quot;)&quot;,&quot;[&quot;: &quot;]&quot;,&quot;{&quot;: &quot;}&quot;}">
+			Sets the brace pairs to be autocompleted.
+		</member>
 		<member name="code_completion_enabled" type="bool" setter="set_code_completion_enabled" getter="is_code_completion_enabled" default="false">
 			Sets whether code completion is allowed.
 		</member>

+ 8 - 0
doc/classes/TextEdit.xml

@@ -16,6 +16,14 @@
 				A virtual method that is called whenever backspace is triggered.
 			</description>
 		</method>
+		<method name="_handle_unicode_input" qualifiers="virtual">
+			<return type="void">
+			</return>
+			<argument index="0" name="unicode" type="int">
+			</argument>
+			<description>
+			</description>
+		</method>
 		<method name="add_gutter">
 			<return type="void" />
 			<argument index="0" name="at" type="int" default="-1" />

+ 2 - 9
editor/code_editor.cpp

@@ -958,7 +958,7 @@ void CodeTextEditor::update_editor_settings() {
 	text_editor->cursor_set_block_mode(EditorSettings::get_singleton()->get("text_editor/cursor/block_caret"));
 	text_editor->cursor_set_blink_enabled(EditorSettings::get_singleton()->get("text_editor/cursor/caret_blink"));
 	text_editor->cursor_set_blink_speed(EditorSettings::get_singleton()->get("text_editor/cursor/caret_blink_speed"));
-	text_editor->set_auto_brace_completion(EditorSettings::get_singleton()->get("text_editor/completion/auto_brace_complete"));
+	text_editor->set_auto_brace_completion_enabled(EditorSettings::get_singleton()->get("text_editor/completion/auto_brace_complete"));
 }
 
 void CodeTextEditor::set_find_replace_bar(FindReplaceBar *p_bar) {
@@ -1609,16 +1609,9 @@ void CodeTextEditor::_apply_settings_change() {
 		} break;
 	}
 
-	// Auto brace completion.
-	text_editor->set_auto_brace_completion(
-			EDITOR_GET("text_editor/completion/auto_brace_complete"));
-
-	code_complete_timer->set_wait_time(
-			EDITOR_GET("text_editor/completion/code_complete_delay"));
-
-	// Call hint settings.
 	text_editor->set_code_hint_draw_below(EDITOR_GET("text_editor/completion/put_callhint_tooltip_below_current_line"));
 
+	code_complete_timer->set_wait_time(EDITOR_GET("text_editor/completion/code_complete_delay"));
 	idle->set_wait_time(EDITOR_GET("text_editor/completion/idle_parse_delay"));
 }
 

+ 8 - 0
editor/plugins/script_text_editor.cpp

@@ -205,6 +205,10 @@ void ScriptTextEditor::_set_theme_for_script() {
 		String beg = string.get_slice(" ", 0);
 		String end = string.get_slice_count(" ") > 1 ? string.get_slice(" ", 1) : String();
 		text_edit->add_string_delimiter(beg, end, end == "");
+
+		if (!end.is_empty() && !text_edit->has_auto_brace_completion_open_key(beg)) {
+			text_edit->add_auto_brace_completion_pair(beg, end);
+		}
 	}
 
 	List<String> comments;
@@ -214,6 +218,10 @@ void ScriptTextEditor::_set_theme_for_script() {
 		String beg = comment.get_slice(" ", 0);
 		String end = comment.get_slice_count(" ") > 1 ? comment.get_slice(" ", 1) : String();
 		text_edit->add_comment_delimiter(beg, end, end == "");
+
+		if (!end.is_empty() && !text_edit->has_auto_brace_completion_open_key(beg)) {
+			text_edit->add_auto_brace_completion_pair(beg, end);
+		}
 	}
 }
 

+ 4 - 0
editor/plugins/shader_editor_plugin.cpp

@@ -158,6 +158,10 @@ void ShaderTextEditor::_load_theme_settings() {
 	text_editor->add_comment_delimiter("/*", "*/", false);
 	text_editor->add_comment_delimiter("//", "", true);
 
+	if (!text_editor->has_auto_brace_completion_open_key("/*")) {
+		text_editor->add_auto_brace_completion_pair("/*", "*/");
+	}
+
 	if (warnings_panel) {
 		// Warnings panel
 		warnings_panel->add_theme_font_override("normal_font", EditorNode::get_singleton()->get_gui_base()->get_theme_font(SNAME("main"), SNAME("EditorFonts")));

+ 4 - 0
editor/plugins/visual_shader_editor_plugin.cpp

@@ -921,6 +921,10 @@ void VisualShaderGraphPlugin::add_node(VisualShader::Type p_type, int p_id) {
 		expression_box->add_comment_delimiter("/*", "*/", false);
 		expression_box->add_comment_delimiter("//", "", true);
 
+		if (!expression_box->has_auto_brace_completion_open_key("/*")) {
+			expression_box->add_auto_brace_completion_pair("/*", "*/");
+		}
+
 		expression_box->set_text(expression);
 		expression_box->set_context_menu_enabled(false);
 		expression_box->set_draw_line_numbers(true);

+ 243 - 53
scene/gui/code_edit.cpp

@@ -437,6 +437,7 @@ void CodeEdit::_gui_input(const Ref<InputEvent> &p_gui_input) {
 	}
 }
 
+/* General overrides */
 Control::CursorShape CodeEdit::get_cursor_shape(const Point2 &p_pos) const {
 	if ((code_completion_active && code_completion_rect.has_point(p_pos)) || (is_readonly() && (!is_selecting_enabled() || get_line_count() == 0))) {
 		return CURSOR_ARROW;
@@ -459,6 +460,58 @@ Control::CursorShape CodeEdit::get_cursor_shape(const Point2 &p_pos) const {
 	return TextEdit::get_cursor_shape(p_pos);
 }
 
+void CodeEdit::handle_unicode_input(uint32_t p_unicode) {
+	bool had_selection = is_selection_active();
+	if (had_selection) {
+		begin_complex_operation();
+		delete_selection();
+	}
+
+	// Remove the old character if in insert mode and no selection.
+	if (is_insert_mode() && !had_selection) {
+		begin_complex_operation();
+
+		// Make sure we don't try and remove empty space.
+		if (cursor_get_column() < get_line(cursor_get_line()).length()) {
+			_remove_text(cursor_get_line(), cursor_get_column(), cursor_get_line(), cursor_get_column() + 1);
+		}
+	}
+
+	const char32_t chr[2] = { (char32_t)p_unicode, 0 };
+
+	if (auto_brace_completion_enabled) {
+		int cl = cursor_get_line();
+		int cc = cursor_get_column();
+		int caret_move_offset = 1;
+
+		int post_brace_pair = cc < get_line(cl).length() ? _get_auto_brace_pair_close_at_pos(cl, cc) : -1;
+
+		if (has_string_delimiter(chr) && cc > 0 && _is_char(get_line(cl)[cc - 1]) && post_brace_pair == -1) {
+			insert_text_at_cursor(chr);
+		} else if (cc < get_line(cl).length() && _is_char(get_line(cl)[cc])) {
+			insert_text_at_cursor(chr);
+		} else if (post_brace_pair != -1 && auto_brace_completion_pairs[post_brace_pair].close_key[0] == chr[0]) {
+			caret_move_offset = auto_brace_completion_pairs[post_brace_pair].close_key.length();
+		} else if (is_in_comment(cl, cc) != -1 || (is_in_string(cl, cc) != -1 && has_string_delimiter(chr))) {
+			insert_text_at_cursor(chr);
+		} else {
+			insert_text_at_cursor(chr);
+
+			int pre_brace_pair = _get_auto_brace_pair_open_at_pos(cl, cc + 1);
+			if (pre_brace_pair != -1) {
+				insert_text_at_cursor(auto_brace_completion_pairs[pre_brace_pair].close_key);
+			}
+		}
+		cursor_set_column(cc + caret_move_offset);
+	} else {
+		insert_text_at_cursor(chr);
+	}
+
+	if ((is_insert_mode() && !had_selection) || (had_selection)) {
+		end_complex_operation();
+	}
+}
+
 /* 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.");
@@ -527,13 +580,13 @@ void CodeEdit::do_indent() {
 	}
 
 	if (!indent_using_spaces) {
-		_insert_text_at_cursor("\t");
+		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));
+		insert_text_at_cursor(String(" ").repeat(spaces_to_add));
 	}
 }
 
@@ -713,34 +766,6 @@ 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;
@@ -803,9 +828,8 @@ void CodeEdit::_new_line(bool p_split_current_line, bool p_above) {
 		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]) {
+			String closing_pair = get_auto_brace_completion_close_key(String::chr(indent_char));
+			if (!closing_pair.is_empty() && line.find(closing_pair, cc) == 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;
@@ -873,12 +897,20 @@ void CodeEdit::backspace() {
 
 	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;
+	if (auto_brace_completion_enabled && cc > 0) {
+		int idx = _get_auto_brace_pair_open_at_pos(cl, cc);
+		if (idx != -1) {
+			prev_column = cc - auto_brace_completion_pairs[idx].open_key.length();
+
+			if (_get_auto_brace_pair_close_at_pos(cl, cc) == idx) {
+				_remove_text(prev_line, prev_column, cl, cc + auto_brace_completion_pairs[idx].close_key.length());
+			} else {
+				_remove_text(prev_line, prev_column, cl, cc);
+			}
+			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 */
@@ -896,6 +928,84 @@ void CodeEdit::backspace() {
 	cursor_set_column(prev_column);
 }
 
+/* Auto brace completion */
+void CodeEdit::set_auto_brace_completion_enabled(bool p_enabled) {
+	auto_brace_completion_enabled = p_enabled;
+}
+
+bool CodeEdit::is_auto_brace_completion_enabled() const {
+	return auto_brace_completion_enabled;
+}
+
+void CodeEdit::add_auto_brace_completion_pair(const String &p_open_key, const String &p_close_key) {
+	ERR_FAIL_COND_MSG(p_open_key.is_empty(), "auto brace completion open key cannot be empty");
+	ERR_FAIL_COND_MSG(p_close_key.is_empty(), "auto brace completion close key cannot be empty");
+
+	for (int i = 0; i < p_open_key.length(); i++) {
+		ERR_FAIL_COND_MSG(!is_symbol(p_open_key[i]), "auto brace completion open key must be a symbol");
+	}
+	for (int i = 0; i < p_close_key.length(); i++) {
+		ERR_FAIL_COND_MSG(!is_symbol(p_close_key[i]), "auto brace completion close key must be a symbol");
+	}
+
+	int at = 0;
+	for (int i = 0; i < auto_brace_completion_pairs.size(); i++) {
+		ERR_FAIL_COND_MSG(auto_brace_completion_pairs[i].open_key == p_open_key, "auto brace completion open key '" + p_open_key + "' already exists.");
+		if (p_open_key.length() < auto_brace_completion_pairs[i].open_key.length()) {
+			at++;
+		}
+	}
+
+	BracePair brace_pair;
+	brace_pair.open_key = p_open_key;
+	brace_pair.close_key = p_close_key;
+	auto_brace_completion_pairs.insert(at, brace_pair);
+}
+
+void CodeEdit::set_auto_brace_completion_pairs(const Dictionary &p_auto_brace_completion_pairs) {
+	auto_brace_completion_pairs.clear();
+
+	Array keys = p_auto_brace_completion_pairs.keys();
+	for (int i = 0; i < keys.size(); i++) {
+		add_auto_brace_completion_pair(keys[i], p_auto_brace_completion_pairs[keys[i]]);
+	}
+}
+
+Dictionary CodeEdit::get_auto_brace_completion_pairs() const {
+	Dictionary brace_pairs;
+	for (int i = 0; i < auto_brace_completion_pairs.size(); i++) {
+		brace_pairs[auto_brace_completion_pairs[i].open_key] = auto_brace_completion_pairs[i].close_key;
+	}
+	return brace_pairs;
+}
+
+bool CodeEdit::has_auto_brace_completion_open_key(const String &p_open_key) const {
+	for (int i = 0; i < auto_brace_completion_pairs.size(); i++) {
+		if (auto_brace_completion_pairs[i].open_key == p_open_key) {
+			return true;
+		}
+	}
+	return false;
+}
+
+bool CodeEdit::has_auto_brace_completion_close_key(const String &p_close_key) const {
+	for (int i = 0; i < auto_brace_completion_pairs.size(); i++) {
+		if (auto_brace_completion_pairs[i].close_key == p_close_key) {
+			return true;
+		}
+	}
+	return false;
+}
+
+String CodeEdit::get_auto_brace_completion_close_key(const String &p_open_key) const {
+	for (int i = 0; i < auto_brace_completion_pairs.size(); i++) {
+		if (auto_brace_completion_pairs[i].open_key == p_open_key) {
+			return auto_brace_completion_pairs[i].close_key;
+		}
+	}
+	return String();
+}
+
 /* Main Gutter */
 void CodeEdit::_update_draw_main_gutter() {
 	set_gutter_draw(main_gutter, draw_breakpoints || draw_bookmarks || draw_executing_lines);
@@ -1700,35 +1810,40 @@ void CodeEdit::confirm_code_completion(bool p_replace) {
 		insert_text_at_cursor(insert_text.substr(matching_chars));
 	}
 
-	/* TODO: merge with autobrace completion, when in CodeEdit. */
 	/* Handle merging of symbols eg strings, brackets. */
 	const String line = get_line(caret_line);
 	char32_t next_char = line[cursor_get_column()];
 	char32_t last_completion_char = insert_text[insert_text.length() - 1];
 	char32_t last_completion_char_display = display_text[display_text.length() - 1];
 
-	if ((last_completion_char == '"' || last_completion_char == '\'') && (last_completion_char == next_char || last_completion_char_display == next_char)) {
+	int pre_brace_pair = cursor_get_column() > 0 ? _get_auto_brace_pair_open_at_pos(caret_line, cursor_get_column()) : -1;
+	int post_brace_pair = cursor_get_column() < get_line(caret_line).length() ? _get_auto_brace_pair_close_at_pos(caret_line, cursor_get_column()) : -1;
+
+	if (post_brace_pair != -1 && (last_completion_char == next_char || last_completion_char_display == next_char)) {
 		_remove_text(caret_line, cursor_get_column(), caret_line, cursor_get_column() + 1);
 	}
 
-	if (last_completion_char == '(') {
-		if (next_char == last_completion_char) {
-			_remove_text(caret_line, cursor_get_column() - 1, caret_line, cursor_get_column());
-		} else if (auto_brace_completion_enabled) {
-			insert_text_at_cursor(")");
-			cursor_set_column(cursor_get_column() - 1);
-		}
-	} else if (last_completion_char == ')' && next_char == '(') {
-		_remove_text(caret_line, cursor_get_column() - 2, caret_line, cursor_get_column());
-		if (line[cursor_get_column() + 1] != ')') {
-			cursor_set_column(cursor_get_column() - 1);
+	if (pre_brace_pair != -1 && pre_brace_pair != post_brace_pair && (last_completion_char == next_char || last_completion_char_display == next_char)) {
+		_remove_text(caret_line, cursor_get_column(), caret_line, cursor_get_column() + 1);
+	} else if (auto_brace_completion_enabled && pre_brace_pair != -1 && post_brace_pair == -1) {
+		insert_text_at_cursor(auto_brace_completion_pairs[pre_brace_pair].close_key);
+		cursor_set_column(cursor_get_column() - auto_brace_completion_pairs[pre_brace_pair].close_key.length());
+	}
+
+	if (pre_brace_pair == -1 && post_brace_pair == -1 && cursor_get_column() > 0 && cursor_get_column() < get_line(caret_line).length()) {
+		pre_brace_pair = _get_auto_brace_pair_open_at_pos(caret_line, cursor_get_column() + 1);
+		if (pre_brace_pair == _get_auto_brace_pair_close_at_pos(caret_line, cursor_get_column() - 1)) {
+			_remove_text(caret_line, cursor_get_column() - 2, caret_line, cursor_get_column());
+			if (_get_auto_brace_pair_close_at_pos(caret_line, cursor_get_column() - 1) != pre_brace_pair) {
+				cursor_set_column(cursor_get_column() - 1);
+			}
 		}
 	}
 
 	end_complex_operation();
 
 	cancel_code_completion();
-	if (last_completion_char == '(') {
+	if (code_completion_prefixes.has(String::chr(last_completion_char))) {
 		request_code_completion();
 	}
 }
@@ -1762,6 +1877,19 @@ void CodeEdit::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("indent_lines"), &CodeEdit::indent_lines);
 	ClassDB::bind_method(D_METHOD("unindent_lines"), &CodeEdit::unindent_lines);
 
+	/* Auto brace completion */
+	ClassDB::bind_method(D_METHOD("set_auto_brace_completion_enabled", "enable"), &CodeEdit::set_auto_brace_completion_enabled);
+	ClassDB::bind_method(D_METHOD("is_auto_brace_completion_enabled"), &CodeEdit::is_auto_brace_completion_enabled);
+
+	ClassDB::bind_method(D_METHOD("add_auto_brace_completion_pair", "start_key", "end_key"), &CodeEdit::add_auto_brace_completion_pair);
+	ClassDB::bind_method(D_METHOD("set_auto_brace_completion_pairs", "pairs"), &CodeEdit::set_auto_brace_completion_pairs);
+	ClassDB::bind_method(D_METHOD("get_auto_brace_completion_pairs"), &CodeEdit::get_auto_brace_completion_pairs);
+
+	ClassDB::bind_method(D_METHOD("has_auto_brace_completion_open_key", "open_key"), &CodeEdit::has_auto_brace_completion_open_key);
+	ClassDB::bind_method(D_METHOD("has_auto_brace_completion_close_key", "close_key"), &CodeEdit::has_auto_brace_completion_close_key);
+
+	ClassDB::bind_method(D_METHOD("get_auto_brace_completion_close_key", "open_key"), &CodeEdit::get_auto_brace_completion_close_key);
+
 	/* Main Gutter */
 	ClassDB::bind_method(D_METHOD("_main_gutter_draw_callback"), &CodeEdit::_main_gutter_draw_callback);
 
@@ -1918,11 +2046,66 @@ void CodeEdit::_bind_methods() {
 	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");
 
+	ADD_GROUP("Auto brace completion", "auto_brace_completion_");
+	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "auto_brace_completion_enabled"), "set_auto_brace_completion_enabled", "is_auto_brace_completion_enabled");
+	ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "auto_brace_completion_pairs"), "set_auto_brace_completion_pairs", "get_auto_brace_completion_pairs");
+
 	/* Signals */
 	ADD_SIGNAL(MethodInfo("breakpoint_toggled", PropertyInfo(Variant::INT, "line")));
 	ADD_SIGNAL(MethodInfo("request_code_completion"));
 }
 
+/* Auto brace completion */
+int CodeEdit::_get_auto_brace_pair_open_at_pos(int p_line, int p_col) {
+	const String &line = get_line(p_line);
+
+	/* Should be fast enough, expecting low amount of pairs... */
+	for (int i = 0; i < auto_brace_completion_pairs.size(); i++) {
+		const String &open_key = auto_brace_completion_pairs[i].open_key;
+		if (p_col - open_key.length() < 0) {
+			continue;
+		}
+
+		bool is_match = true;
+		for (int j = 0; j < open_key.length(); j++) {
+			if (line[(p_col - 1) - j] != open_key[(open_key.length() - 1) - j]) {
+				is_match = false;
+				break;
+			}
+		}
+
+		if (is_match) {
+			return i;
+		}
+	}
+	return -1;
+}
+
+int CodeEdit::_get_auto_brace_pair_close_at_pos(int p_line, int p_col) {
+	const String &line = get_line(p_line);
+
+	/* Should be fast enough, expecting low amount of pairs... */
+	for (int i = 0; i < auto_brace_completion_pairs.size(); i++) {
+		if (p_col + auto_brace_completion_pairs[i].close_key.length() > line.length()) {
+			continue;
+		}
+
+		bool is_match = true;
+		for (int j = 0; j < auto_brace_completion_pairs[i].close_key.length(); j++) {
+			if (line[p_col + j] != auto_brace_completion_pairs[i].close_key[j]) {
+				is_match = false;
+				break;
+			}
+		}
+
+		if (is_match) {
+			return i;
+		}
+	}
+	return -1;
+}
+
+/* Gutters */
 void CodeEdit::_gutter_clicked(int p_line, int p_gutter) {
 	if (p_gutter == main_gutter) {
 		if (draw_breakpoints) {
@@ -2547,6 +2730,13 @@ CodeEdit::CodeEdit() {
 	auto_indent_prefixes.insert('[');
 	auto_indent_prefixes.insert('(');
 
+	/* Auto brace completion */
+	add_auto_brace_completion_pair("(", ")");
+	add_auto_brace_completion_pair("{", "}");
+	add_auto_brace_completion_pair("[", "]");
+	add_auto_brace_completion_pair("\"", "\"");
+	add_auto_brace_completion_pair("\'", "\'");
+
 	/* Text Direction */
 	set_layout_direction(LAYOUT_DIRECTION_LTR);
 	set_text_direction(TEXT_DIRECTION_LTR);

+ 28 - 0
scene/gui/code_edit.h

@@ -66,6 +66,19 @@ private:
 
 	void _new_line(bool p_split_current_line = true, bool p_above = false);
 
+	/* Auto brace completion */
+	bool auto_brace_completion_enabled = false;
+
+	/* BracePair open_key must be uniquie and ordered by length. */
+	struct BracePair {
+		String open_key = "";
+		String close_key = "";
+	};
+	Vector<BracePair> auto_brace_completion_pairs;
+
+	int _get_auto_brace_pair_open_at_pos(int p_line, int p_col);
+	int _get_auto_brace_pair_close_at_pos(int p_line, int p_col);
+
 	/* Main Gutter */
 	enum MainGutterType {
 		MAIN_GUTTER_BREAKPOINT = 0x01,
@@ -217,7 +230,9 @@ protected:
 	static void _bind_methods();
 
 public:
+	/* General overrides */
 	virtual CursorShape get_cursor_shape(const Point2 &p_pos = Point2i()) const override;
+	virtual void handle_unicode_input(uint32_t p_unicode) override;
 
 	/* Indent management */
 	void set_indent_size(const int p_size);
@@ -240,6 +255,19 @@ public:
 
 	virtual void backspace() override;
 
+	/* Auto brace completion */
+	void set_auto_brace_completion_enabled(bool p_enabled);
+	bool is_auto_brace_completion_enabled() const;
+
+	void add_auto_brace_completion_pair(const String &p_open_key, const String &p_close_key);
+	void set_auto_brace_completion_pairs(const Dictionary &p_auto_brace_completion_pairs);
+	Dictionary get_auto_brace_completion_pairs() const;
+
+	bool has_auto_brace_completion_open_key(const String &p_open_key) const;
+	bool has_auto_brace_completion_close_key(const String &p_close_key) const;
+
+	String get_auto_brace_completion_close_key(const String &p_open_key) const;
+
 	/* Main Gutter */
 	void set_draw_breakpoints_gutter(bool p_draw);
 	bool is_drawing_breakpoints_gutter() const;

+ 29 - 207
scene/gui/text_edit.cpp

@@ -63,45 +63,6 @@ static bool _is_char(char32_t c) {
 	return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
 }
 
-static bool _is_pair_right_symbol(char32_t c) {
-	return c == '"' ||
-		   c == '\'' ||
-		   c == ')' ||
-		   c == ']' ||
-		   c == '}';
-}
-
-static bool _is_pair_left_symbol(char32_t c) {
-	return c == '"' ||
-		   c == '\'' ||
-		   c == '(' ||
-		   c == '[' ||
-		   c == '{';
-}
-
-static bool _is_pair_symbol(char32_t c) {
-	return _is_pair_left_symbol(c) || _is_pair_right_symbol(c);
-}
-
-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;
-}
-
 ///////////////////////////////////////////////////////////////////////////////
 
 void TextEdit::Text::set_font(const Ref<Font> &p_font) {
@@ -1567,130 +1528,6 @@ void TextEdit::_notification(int p_what) {
 	}
 }
 
-void TextEdit::_consume_pair_symbol(char32_t ch) {
-	int cursor_position_to_move = cursor_get_column() + 1;
-
-	char32_t ch_single[2] = { ch, 0 };
-	char32_t ch_single_pair[2] = { _get_right_pair_symbol(ch), 0 };
-	char32_t ch_pair[3] = { ch, _get_right_pair_symbol(ch), 0 };
-
-	if (is_selection_active()) {
-		int new_column, new_line;
-
-		begin_complex_operation();
-		_insert_text(get_selection_from_line(), get_selection_from_column(),
-				ch_single,
-				&new_line, &new_column);
-
-		int to_col_offset = 0;
-		if (get_selection_from_line() == get_selection_to_line()) {
-			to_col_offset = 1;
-		}
-
-		_insert_text(get_selection_to_line(),
-				get_selection_to_column() + to_col_offset,
-				ch_single_pair,
-				&new_line, &new_column);
-		end_complex_operation();
-
-		cursor_set_line(get_selection_to_line());
-		cursor_set_column(get_selection_to_column() + to_col_offset);
-
-		deselect();
-		update();
-		return;
-	}
-
-	if ((ch == '\'' || ch == '"') &&
-			cursor_get_column() > 0 && _is_text_char(text[cursor.line][cursor_get_column() - 1]) && !_is_pair_right_symbol(text[cursor.line][cursor_get_column()])) {
-		insert_text_at_cursor(ch_single);
-		cursor_set_column(cursor_position_to_move);
-		return;
-	}
-
-	if (cursor_get_column() < text[cursor.line].length()) {
-		if (_is_text_char(text[cursor.line][cursor_get_column()])) {
-			insert_text_at_cursor(ch_single);
-			cursor_set_column(cursor_position_to_move);
-			return;
-		}
-		if (_is_pair_right_symbol(ch) &&
-				text[cursor.line][cursor_get_column()] == ch) {
-			cursor_set_column(cursor_position_to_move);
-			return;
-		}
-	}
-
-	String line = text[cursor.line];
-
-	bool in_single_quote = false;
-	bool in_double_quote = false;
-	bool found_comment = false;
-
-	int c = 0;
-	while (c < line.length()) {
-		if (line[c] == '\\') {
-			c++; // Skip quoted anything.
-
-			if (cursor.column == c) {
-				break;
-			}
-		} else if (!in_single_quote && !in_double_quote && line[c] == '#') {
-			found_comment = true;
-			break;
-		} else {
-			if (line[c] == '\'' && !in_double_quote) {
-				in_single_quote = !in_single_quote;
-			} else if (line[c] == '"' && !in_single_quote) {
-				in_double_quote = !in_double_quote;
-			}
-		}
-
-		c++;
-
-		if (cursor.column == c) {
-			break;
-		}
-	}
-
-	// Do not need to duplicate quotes while in comments
-	if (found_comment) {
-		insert_text_at_cursor(ch_single);
-		cursor_set_column(cursor_position_to_move);
-
-		return;
-	}
-
-	// Disallow inserting duplicated quotes while already in string
-	if ((in_single_quote || in_double_quote) && (ch == '"' || ch == '\'')) {
-		insert_text_at_cursor(ch_single);
-		cursor_set_column(cursor_position_to_move);
-
-		return;
-	}
-
-	insert_text_at_cursor(ch_pair);
-	cursor_set_column(cursor_position_to_move);
-}
-
-void TextEdit::_consume_backspace_for_pair_symbol(int prev_line, int prev_column) {
-	bool remove_right_symbol = false;
-
-	if (cursor.column < text[cursor.line].length() && cursor.column > 0) {
-		char32_t left_char = text[cursor.line][cursor.column - 1];
-		char32_t right_char = text[cursor.line][cursor.column];
-
-		if (right_char == _get_right_pair_symbol(left_char)) {
-			remove_right_symbol = true;
-		}
-	}
-	if (remove_right_symbol) {
-		_remove_text(prev_line, prev_column, cursor.line, cursor.column + 1);
-	} else {
-		_remove_text(prev_line, prev_column, cursor.line, cursor.column);
-	}
-}
-
 void TextEdit::backspace() {
 	ScriptInstance *si = get_script_instance();
 	if (si && si->has_method("_backspace")) {
@@ -1719,14 +1556,7 @@ void TextEdit::backspace() {
 	if (is_line_hidden(cursor.line)) {
 		set_line_as_hidden(prev_line, true);
 	}
-
-	if (auto_brace_completion_enabled &&
-			cursor.column > 0 &&
-			_is_pair_left_symbol(text[cursor.line][cursor.column - 1])) {
-		_consume_backspace_for_pair_symbol(prev_line, prev_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);
@@ -2101,6 +1931,7 @@ void TextEdit::delete_selection() {
 	}
 
 	selection.active = false;
+	selection.selecting_mode = SelectionMode::SELECTION_MODE_NONE;
 	_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);
@@ -2137,13 +1968,21 @@ 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) {
+void TextEdit::handle_unicode_input(uint32_t p_unicode) {
+	ScriptInstance *si = get_script_instance();
+	if (si && si->has_method("_handle_unicode_input")) {
+		si->call("_handle_unicode_input", p_unicode);
+		return;
+	}
+
+	bool had_selection = selection.active;
+	if (had_selection) {
+		begin_complex_operation();
 		delete_selection();
 	}
 
 	// Remove the old character if in insert mode and no selection.
-	if (insert_mode && !p_had_selection) {
+	if (insert_mode && !had_selection) {
 		begin_complex_operation();
 
 		// Make sure we don't try and remove empty space.
@@ -2152,15 +1991,10 @@ void TextEdit::_handle_unicode_character(uint32_t unicode, bool p_had_selection)
 		}
 	}
 
-	const char32_t chr[2] = { (char32_t)unicode, 0 };
+	const char32_t chr[2] = { (char32_t)p_unicode, 0 };
+	insert_text_at_cursor(chr);
 
-	if (auto_brace_completion_enabled && _is_pair_symbol(chr[0])) {
-		_consume_pair_symbol(chr[0]);
-	} else {
-		_insert_text_at_cursor(chr);
-	}
-
-	if ((insert_mode && !p_had_selection) || (selection.active != p_had_selection)) {
+	if ((insert_mode && !had_selection) || (had_selection)) {
 		end_complex_operation();
 	}
 }
@@ -2598,9 +2432,6 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) {
 		// * No Modifiers are pressed (except shift)
 		bool allow_unicode_handling = !(k->is_command_pressed() || k->is_ctrl_pressed() || k->is_alt_pressed() || k->is_meta_pressed());
 
-		// Save here for insert mode, just in case it is cleared in the following section.
-		bool had_selection = selection.active;
-
 		selection.selecting_text = false;
 
 		// Check and handle all built in shortcuts.
@@ -2806,9 +2637,9 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) {
 			return;
 		}
 
+		// Handle Unicode (if no modifiers active).
 		if (allow_unicode_handling && !readonly && k->get_unicode() >= 32) {
-			// Handle Unicode (if no modifiers active).
-			_handle_unicode_character(k->get_unicode(), had_selection);
+			handle_unicode_input(k->get_unicode());
 			accept_event();
 			return;
 		}
@@ -3151,16 +2982,6 @@ void TextEdit::_remove_text(int p_from_line, int p_from_column, int p_to_line, i
 	current_op = op;
 }
 
-void TextEdit::_insert_text_at_cursor(const String &p_text) {
-	int new_column, new_line;
-	_insert_text(cursor.line, cursor.column, p_text, &new_line, &new_column);
-	_update_scrollbars();
-	cursor_set_line(new_line, false);
-	cursor_set_column(new_column);
-
-	update();
-}
-
 int TextEdit::get_char_count() {
 	int totalsize = 0;
 
@@ -3704,15 +3525,15 @@ int TextEdit::get_column_x_offset_for_line(int p_char, int p_line) const {
 
 void TextEdit::insert_text_at_cursor(const String &p_text) {
 	if (selection.active) {
-		cursor_set_line(selection.from_line, false);
-		cursor_set_column(selection.from_column);
-
-		_remove_text(selection.from_line, selection.from_column, selection.to_line, selection.to_column);
-		selection.active = false;
-		selection.selecting_mode = SelectionMode::SELECTION_MODE_NONE;
+		delete_selection();
 	}
 
-	_insert_text_at_cursor(p_text);
+	int new_column, new_line;
+	_insert_text(cursor.line, cursor.column, p_text, &new_line, &new_column);
+	_update_scrollbars();
+
+	cursor_set_line(new_line, false);
+	cursor_set_column(new_column);
 	update();
 }
 
@@ -3753,7 +3574,7 @@ void TextEdit::set_text(String p_text) {
 	setting_text = true;
 	if (!undo_enabled) {
 		_clear();
-		_insert_text_at_cursor(p_text);
+		insert_text_at_cursor(p_text);
 	}
 
 	if (undo_enabled) {
@@ -3762,7 +3583,7 @@ void TextEdit::set_text(String p_text) {
 
 		begin_complex_operation();
 		_remove_text(0, 0, MAX(0, get_line_count() - 1), MAX(get_line(MAX(get_line_count() - 1, 0)).size() - 1, 0));
-		_insert_text_at_cursor(p_text);
+		insert_text_at_cursor(p_text);
 		end_complex_operation();
 		selection.active = false;
 	}
@@ -4364,7 +4185,7 @@ void TextEdit::paste() {
 		clipboard += ins;
 	}
 
-	_insert_text_at_cursor(clipboard);
+	insert_text_at_cursor(clipboard);
 	end_complex_operation();
 
 	update();
@@ -5719,6 +5540,7 @@ void TextEdit::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("delete_selection"), &TextEdit::delete_selection);
 	ClassDB::bind_method(D_METHOD("backspace"), &TextEdit::backspace);
 	BIND_VMETHOD(MethodInfo("_backspace"));
+	BIND_VMETHOD(MethodInfo("_handle_unicode_input", PropertyInfo(Variant::INT, "unicode")))
 
 	ClassDB::bind_method(D_METHOD("cut"), &TextEdit::cut);
 	ClassDB::bind_method(D_METHOD("copy"), &TextEdit::copy);

+ 1 - 10
scene/gui/text_edit.h

@@ -431,11 +431,8 @@ private:
 	void _delete(bool p_word = false, bool p_all_to_right = false);
 	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);
 
 protected:
-	bool auto_brace_completion_enabled = false;
-
 	struct Cache {
 		Ref<Texture2D> tab_icon;
 		Ref<Texture2D> space_icon;
@@ -470,13 +467,9 @@ protected:
 
 	void _insert_text(int p_line, int p_char, const String &p_text, int *r_end_line = nullptr, int *r_end_char = nullptr);
 	void _remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column);
-	void _insert_text_at_cursor(const String &p_text);
 	virtual void _gui_input(const Ref<InputEvent> &p_gui_input);
 	void _notification(int p_what);
 
-	void _consume_pair_symbol(char32_t ch);
-	void _consume_backspace_for_pair_symbol(int prev_line, int prev_column);
-
 	static void _bind_methods();
 
 	bool _set(const StringName &p_name, const Variant &p_value);
@@ -635,9 +628,6 @@ public:
 		scroll_past_end_of_file_enabled = p_enabled;
 		update();
 	}
-	inline void set_auto_brace_completion(bool p_enabled) {
-		auto_brace_completion_enabled = p_enabled;
-	}
 	inline void set_brace_matching(bool p_enabled) {
 		brace_matching_enabled = p_enabled;
 		update();
@@ -686,6 +676,7 @@ public:
 
 	void delete_selection();
 
+	virtual void handle_unicode_input(uint32_t p_unicode);
 	virtual void backspace();
 	void cut();
 	void copy();