Browse Source

Add comment and string tracking to CodeEdit

Paulb23 5 years ago
parent
commit
680dc9e81a

+ 20 - 0
editor/plugins/script_text_editor.cpp

@@ -203,6 +203,26 @@ void ScriptTextEditor::_set_theme_for_script() {
 	CodeEdit *text_edit = code_editor->get_text_editor();
 	text_edit->get_syntax_highlighter()->update_cache();
 
+	List<String> strings;
+	script->get_language()->get_string_delimiters(&strings);
+	text_edit->clear_string_delimiters();
+	for (List<String>::Element *E = strings.front(); E; E = E->next()) {
+		String string = E->get();
+		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 == "");
+	}
+
+	List<String> comments;
+	script->get_language()->get_comment_delimiters(&comments);
+	text_edit->clear_comment_delimiters();
+	for (List<String>::Element *E = comments.front(); E; E = E->next()) {
+		String comment = E->get();
+		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 == "");
+	}
+
 	/* add keywords for auto completion */
 	// singleton autoloads (as types, just as engine singletons are)
 	Map<StringName, ProjectSettings::AutoloadInfo> autoloads = ProjectSettings::get_singleton()->get_autoload_list();

+ 4 - 0
editor/plugins/shader_editor_plugin.cpp

@@ -154,6 +154,10 @@ void ShaderTextEditor::_load_theme_settings() {
 	syntax_highlighter->add_color_region("/*", "*/", comment_color, false);
 	syntax_highlighter->add_color_region("//", "", comment_color, true);
 
+	text_editor->clear_comment_delimiters();
+	text_editor->add_comment_delimiter("/*", "*/", false);
+	text_editor->add_comment_delimiter("//", "", true);
+
 	if (warnings_panel) {
 		// Warnings panel
 		warnings_panel->add_theme_font_override("normal_font", EditorNode::get_singleton()->get_gui_base()->get_theme_font("main", "EditorFonts"));

+ 8 - 0
editor/plugins/visual_shader_editor_plugin.cpp

@@ -848,6 +848,10 @@ void VisualShaderGraphPlugin::add_node(VisualShader::Type p_type, int p_id) {
 		expression_syntax_highlighter->add_color_region("/*", "*/", comment_color, false);
 		expression_syntax_highlighter->add_color_region("//", "", comment_color, true);
 
+		expression_box->clear_comment_delimiters();
+		expression_box->add_comment_delimiter("/*", "*/", false);
+		expression_box->add_comment_delimiter("//", "", true);
+
 		expression_box->set_text(expression);
 		expression_box->set_context_menu_enabled(false);
 		expression_box->set_draw_line_numbers(true);
@@ -2984,6 +2988,10 @@ void VisualShaderEditor::_notification(int p_what) {
 			syntax_highlighter->add_color_region("/*", "*/", comment_color, false);
 			syntax_highlighter->add_color_region("//", "", comment_color, true);
 
+			preview_text->clear_comment_delimiters();
+			preview_text->add_comment_delimiter("/*", "*/", false);
+			preview_text->add_comment_delimiter("//", "", true);
+
 			error_text->add_theme_font_override("font", get_theme_font("status_source", "EditorFonts"));
 			error_text->add_theme_font_size_override("font_size", get_theme_font_size("status_source_size", "EditorFonts"));
 			error_text->add_theme_color_override("font_color", get_theme_color("error_color", "Editor"));

+ 534 - 19
scene/gui/code_edit.cpp

@@ -30,6 +30,12 @@
 
 #include "code_edit.h"
 
+#include "core/string/ustring.h"
+
+static bool _is_whitespace(char32_t c) {
+	return c == '\t' || c == ' ';
+}
+
 void CodeEdit::_notification(int p_what) {
 	switch (p_what) {
 		case NOTIFICATION_THEME_CHANGED:
@@ -275,6 +281,175 @@ void CodeEdit::_fold_gutter_draw_callback(int p_line, int p_gutter, Rect2 p_regi
 	folded_icon->draw_rect(get_canvas_item(), p_region, false, folding_color);
 }
 
+/* Delimiters */
+// Strings
+void CodeEdit::add_string_delimiter(const String &p_start_key, const String &p_end_key, bool p_line_only) {
+	_add_delimiter(p_start_key, p_end_key, p_line_only, TYPE_STRING);
+}
+
+void CodeEdit::remove_string_delimiter(const String &p_start_key) {
+	_remove_delimiter(p_start_key, TYPE_STRING);
+}
+
+bool CodeEdit::has_string_delimiter(const String &p_start_key) const {
+	return _has_delimiter(p_start_key, TYPE_STRING);
+}
+
+void CodeEdit::set_string_delimiters(const TypedArray<String> &p_string_delimiters) {
+	_set_delimiters(p_string_delimiters, TYPE_STRING);
+}
+
+void CodeEdit::clear_string_delimiters() {
+	_clear_delimiters(TYPE_STRING);
+}
+
+TypedArray<String> CodeEdit::get_string_delimiters() const {
+	return _get_delimiters(TYPE_STRING);
+}
+
+int CodeEdit::is_in_string(int p_line, int p_column) const {
+	return _is_in_delimiter(p_line, p_column, TYPE_STRING);
+}
+
+// Comments
+void CodeEdit::add_comment_delimiter(const String &p_start_key, const String &p_end_key, bool p_line_only) {
+	_add_delimiter(p_start_key, p_end_key, p_line_only, TYPE_COMMENT);
+}
+
+void CodeEdit::remove_comment_delimiter(const String &p_start_key) {
+	_remove_delimiter(p_start_key, TYPE_COMMENT);
+}
+
+bool CodeEdit::has_comment_delimiter(const String &p_start_key) const {
+	return _has_delimiter(p_start_key, TYPE_COMMENT);
+}
+
+void CodeEdit::set_comment_delimiters(const TypedArray<String> &p_comment_delimiters) {
+	_set_delimiters(p_comment_delimiters, TYPE_COMMENT);
+}
+
+void CodeEdit::clear_comment_delimiters() {
+	_clear_delimiters(TYPE_COMMENT);
+}
+
+TypedArray<String> CodeEdit::get_comment_delimiters() const {
+	return _get_delimiters(TYPE_COMMENT);
+}
+
+int CodeEdit::is_in_comment(int p_line, int p_column) const {
+	return _is_in_delimiter(p_line, p_column, TYPE_COMMENT);
+}
+
+String CodeEdit::get_delimiter_start_key(int p_delimiter_idx) const {
+	ERR_FAIL_INDEX_V(p_delimiter_idx, delimiters.size(), "");
+	return delimiters[p_delimiter_idx].start_key;
+}
+
+String CodeEdit::get_delimiter_end_key(int p_delimiter_idx) const {
+	ERR_FAIL_INDEX_V(p_delimiter_idx, delimiters.size(), "");
+	return delimiters[p_delimiter_idx].end_key;
+}
+
+Point2 CodeEdit::get_delimiter_start_position(int p_line, int p_column) const {
+	if (delimiters.size() == 0) {
+		return Point2(-1, -1);
+	}
+	ERR_FAIL_INDEX_V(p_line, get_line_count(), Point2(-1, -1));
+	ERR_FAIL_COND_V(p_column - 1 > get_line(p_line).size(), Point2(-1, -1));
+
+	Point2 start_position;
+	start_position.y = -1;
+	start_position.x = -1;
+
+	bool in_region = ((p_line <= 0 || delimiter_cache[p_line - 1].size() < 1) ? -1 : delimiter_cache[p_line - 1].back()->value()) != -1;
+
+	/* Check the keys for this line. */
+	for (Map<int, int>::Element *E = delimiter_cache[p_line].front(); E; E = E->next()) {
+		if (E->key() > p_column) {
+			break;
+		}
+		in_region = E->value() != -1;
+		start_position.x = in_region ? E->key() : -1;
+	}
+
+	/* Region was found on this line and is not a multiline continuation. */
+	if (start_position.x != -1 && start_position.x != get_line(p_line).length() + 1) {
+		start_position.y = p_line;
+		return start_position;
+	}
+
+	/* Not in a region */
+	if (!in_region) {
+		return start_position;
+	}
+
+	/* Region starts on a previous line */
+	for (int i = p_line - 1; i >= 0; i--) {
+		if (delimiter_cache[i].size() < 1) {
+			continue;
+		}
+		start_position.y = i;
+		start_position.x = delimiter_cache[i].back()->key();
+
+		/* Make sure it's not a multiline continuation. */
+		if (start_position.x != get_line(i).length() + 1) {
+			break;
+		}
+	}
+	return start_position;
+}
+
+Point2 CodeEdit::get_delimiter_end_position(int p_line, int p_column) const {
+	if (delimiters.size() == 0) {
+		return Point2(-1, -1);
+	}
+	ERR_FAIL_INDEX_V(p_line, get_line_count(), Point2(-1, -1));
+	ERR_FAIL_COND_V(p_column - 1 > get_line(p_line).size(), Point2(-1, -1));
+
+	Point2 end_position;
+	end_position.y = -1;
+	end_position.x = -1;
+
+	int region = (p_line <= 0 || delimiter_cache[p_line - 1].size() < 1) ? -1 : delimiter_cache[p_line - 1].back()->value();
+
+	/* Check the keys for this line. */
+	for (Map<int, int>::Element *E = delimiter_cache[p_line].front(); E; E = E->next()) {
+		end_position.x = (E->value() == -1) ? E->key() : -1;
+		if (E->key() > p_column) {
+			break;
+		}
+		region = E->value();
+	}
+
+	/* Region was found on this line and is not a multiline continuation. */
+	if (region != -1 && end_position.x != -1 && (delimiters[region].line_only || end_position.x != get_line(p_line).length() + 1)) {
+		end_position.y = p_line;
+		return end_position;
+	}
+
+	/* Not in a region */
+	if (region == -1) {
+		end_position.x = -1;
+		return end_position;
+	}
+
+	/* Region ends on a later line */
+	for (int i = p_line + 1; i < get_line_count(); i++) {
+		if (delimiter_cache[i].size() < 1 || delimiter_cache[i].front()->value() != -1) {
+			continue;
+		}
+		end_position.x = delimiter_cache[i].front()->key();
+
+		/* Make sure it's not a multiline continuation. */
+		if (get_line(i).length() > 0 && end_position.x != get_line(i).length() + 1) {
+			end_position.y = i;
+			break;
+		}
+		end_position.x = -1;
+	}
+	return end_position;
+}
+
 void CodeEdit::_bind_methods() {
 	/* Main Gutter */
 	ClassDB::bind_method(D_METHOD("_main_gutter_draw_callback"), &CodeEdit::_main_gutter_draw_callback);
@@ -320,6 +495,36 @@ void CodeEdit::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_draw_fold_gutter", "enable"), &CodeEdit::set_draw_fold_gutter);
 	ClassDB::bind_method(D_METHOD("is_drawing_fold_gutter"), &CodeEdit::is_drawing_fold_gutter);
 
+	/* Delimiters */
+	// Strings
+	ClassDB::bind_method(D_METHOD("add_string_delimiter", "start_key", "end_key", "line_only"), &CodeEdit::add_string_delimiter, DEFVAL(false));
+	ClassDB::bind_method(D_METHOD("remove_string_delimiter", "start_key"), &CodeEdit::remove_string_delimiter);
+	ClassDB::bind_method(D_METHOD("has_string_delimiter", "start_key"), &CodeEdit::has_string_delimiter);
+
+	ClassDB::bind_method(D_METHOD("set_string_delimiters", "string_delimiters"), &CodeEdit::set_string_delimiters);
+	ClassDB::bind_method(D_METHOD("clear_string_delimiters"), &CodeEdit::clear_string_delimiters);
+	ClassDB::bind_method(D_METHOD("get_string_delimiters"), &CodeEdit::get_string_delimiters);
+
+	ClassDB::bind_method(D_METHOD("is_in_string", "line", "column"), &CodeEdit::is_in_string, DEFVAL(-1));
+
+	// Comments
+	ClassDB::bind_method(D_METHOD("add_comment_delimiter", "start_key", "end_key", "line_only"), &CodeEdit::add_comment_delimiter, DEFVAL(false));
+	ClassDB::bind_method(D_METHOD("remove_comment_delimiter", "start_key"), &CodeEdit::remove_comment_delimiter);
+	ClassDB::bind_method(D_METHOD("has_comment_delimiter", "start_key"), &CodeEdit::has_comment_delimiter);
+
+	ClassDB::bind_method(D_METHOD("set_comment_delimiters", "comment_delimiters"), &CodeEdit::set_comment_delimiters);
+	ClassDB::bind_method(D_METHOD("clear_comment_delimiters"), &CodeEdit::clear_comment_delimiters);
+	ClassDB::bind_method(D_METHOD("get_comment_delimiters"), &CodeEdit::get_comment_delimiters);
+
+	ClassDB::bind_method(D_METHOD("is_in_comment", "line", "column"), &CodeEdit::is_in_comment, DEFVAL(-1));
+
+	// Util
+	ClassDB::bind_method(D_METHOD("get_delimiter_start_key", "delimiter_index"), &CodeEdit::get_delimiter_start_key);
+	ClassDB::bind_method(D_METHOD("get_delimiter_end_key", "delimiter_index"), &CodeEdit::get_delimiter_end_key);
+
+	ClassDB::bind_method(D_METHOD("get_delimiter_start_postion", "line", "column"), &CodeEdit::get_delimiter_start_position);
+	ClassDB::bind_method(D_METHOD("get_delimiter_end_postion", "line", "column"), &CodeEdit::get_delimiter_end_position);
+
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_breakpoints_gutter"), "set_draw_breakpoints_gutter", "is_drawing_breakpoints_gutter");
 
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_bookmarks"), "set_draw_bookmarks_gutter", "is_drawing_bookmarks_gutter");
@@ -331,6 +536,10 @@ void CodeEdit::_bind_methods() {
 
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_fold_gutter"), "set_draw_fold_gutter", "is_drawing_fold_gutter");
 
+	ADD_GROUP("Delimiters", "delimiter_");
+	ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "delimiter_strings"), "set_string_delimiters", "get_string_delimiters");
+	ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "delimiter_comments"), "set_comment_delimiters", "get_comment_delimiters");
+
 	ADD_SIGNAL(MethodInfo("breakpoint_toggled", PropertyInfo(Variant::INT, "line")));
 }
 
@@ -360,7 +569,332 @@ void CodeEdit::_gutter_clicked(int p_line, int p_gutter) {
 	}
 }
 
+void CodeEdit::_update_gutter_indexes() {
+	for (int i = 0; i < get_gutter_count(); i++) {
+		if (get_gutter_name(i) == "main_gutter") {
+			main_gutter = i;
+			continue;
+		}
+
+		if (get_gutter_name(i) == "line_numbers") {
+			line_number_gutter = i;
+			continue;
+		}
+
+		if (get_gutter_name(i) == "fold_gutter") {
+			fold_gutter = i;
+			continue;
+		}
+	}
+}
+
+/* Delimiters */
+void CodeEdit::_update_delimiter_cache(int p_from_line, int p_to_line) {
+	if (delimiters.size() == 0) {
+		return;
+	}
+
+	int line_count = get_line_count();
+	if (p_to_line == -1) {
+		p_to_line = line_count;
+	}
+
+	int start_line = MIN(p_from_line, p_to_line);
+	int end_line = MAX(p_from_line, p_to_line);
+
+	/* Make sure delimiter_cache has all the lines. */
+	if (start_line != end_line) {
+		if (p_to_line < p_from_line) {
+			for (int i = end_line; i > start_line; i--) {
+				delimiter_cache.remove(i);
+			}
+		} else {
+			for (int i = start_line; i < end_line; i++) {
+				delimiter_cache.insert(i, Map<int, int>());
+			}
+		}
+	}
+
+	int in_region = -1;
+	for (int i = start_line; i < MIN(end_line + 1, line_count); i++) {
+		int current_end_region = (i <= 0 || delimiter_cache[i].size() < 1) ? -1 : delimiter_cache[i].back()->value();
+		in_region = (i <= 0 || delimiter_cache[i - 1].size() < 1) ? -1 : delimiter_cache[i - 1].back()->value();
+
+		const String &str = get_line(i);
+		const int line_length = str.length();
+		delimiter_cache.write[i].clear();
+
+		if (str.length() == 0) {
+			if (in_region != -1) {
+				delimiter_cache.write[i][0] = in_region;
+			}
+			if (i == end_line && current_end_region != in_region) {
+				end_line = MIN(end_line++, line_count);
+			}
+			continue;
+		}
+
+		int end_region = -1;
+		for (int j = 0; j < line_length; j++) {
+			int from = j;
+			for (; from < line_length; from++) {
+				if (str[from] == '\\') {
+					from++;
+					continue;
+				}
+				break;
+			}
+
+			/* check if we are in entering a region */
+			bool same_line = false;
+			if (in_region == -1) {
+				for (int d = 0; d < delimiters.size(); d++) {
+					/* check there is enough room */
+					int chars_left = line_length - from;
+					int start_key_length = delimiters[d].start_key.length();
+					int end_key_length = delimiters[d].end_key.length();
+					if (chars_left < start_key_length) {
+						continue;
+					}
+
+					/* search the line */
+					bool match = true;
+					const char32_t *start_key = delimiters[d].start_key.get_data();
+					for (int k = 0; k < start_key_length; k++) {
+						if (start_key[k] != str[from + k]) {
+							match = false;
+							break;
+						}
+					}
+					if (!match) {
+						continue;
+					}
+					same_line = true;
+					in_region = d;
+					delimiter_cache.write[i][from + 1] = d;
+					from += start_key_length;
+
+					/* check if it's the whole line */
+					if (end_key_length == 0 || delimiters[d].line_only || from + end_key_length > line_length) {
+						j = line_length;
+						if (delimiters[d].line_only) {
+							delimiter_cache.write[i][line_length + 1] = -1;
+						} else {
+							end_region = in_region;
+						}
+					}
+					break;
+				}
+
+				if (j == line_length || in_region == -1) {
+					continue;
+				}
+			}
+
+			/* if we are in one find the end key */
+			/* search the line */
+			int region_end_index = -1;
+			int end_key_length = delimiters[in_region].end_key.length();
+			const char32_t *end_key = delimiters[in_region].end_key.get_data();
+			for (; from < line_length; from++) {
+				if (line_length - from < end_key_length) {
+					break;
+				}
+
+				if (!is_symbol(str[from])) {
+					continue;
+				}
+
+				if (str[from] == '\\') {
+					from++;
+					continue;
+				}
+
+				region_end_index = from;
+				for (int k = 0; k < end_key_length; k++) {
+					if (end_key[k] != str[from + k]) {
+						region_end_index = -1;
+						break;
+					}
+				}
+
+				if (region_end_index != -1) {
+					break;
+				}
+			}
+
+			j = from + (end_key_length - 1);
+			end_region = (region_end_index == -1) ? in_region : -1;
+			if (!same_line || region_end_index != -1) {
+				delimiter_cache.write[i][j + 1] = end_region;
+			}
+			in_region = -1;
+		}
+
+		if (i == end_line && current_end_region != end_region) {
+			end_line = MIN(end_line++, line_count);
+		}
+	}
+}
+
+int CodeEdit::_is_in_delimiter(int p_line, int p_column, DelimiterType p_type) const {
+	if (delimiters.size() == 0) {
+		return -1;
+	}
+	ERR_FAIL_INDEX_V(p_line, get_line_count(), 0);
+
+	int region = (p_line <= 0 || delimiter_cache[p_line - 1].size() < 1) ? -1 : delimiter_cache[p_line - 1].back()->value();
+	bool in_region = region != -1 && delimiters[region].type == p_type;
+	for (Map<int, int>::Element *E = delimiter_cache[p_line].front(); E; E = E->next()) {
+		/* If column is specified, loop untill the key is larger then the column. */
+		if (p_column != -1) {
+			if (E->key() > p_column) {
+				break;
+			}
+			in_region = E->value() != -1 && delimiters[E->value()].type == p_type;
+			region = in_region ? E->value() : -1;
+			continue;
+		}
+
+		/* If no column, calulate if the entire line is a region       */
+		/* excluding whitespace.                                       */
+		const String line = get_line(p_line);
+		if (!in_region) {
+			if (E->value() == -1 || delimiters[E->value()].type != p_type) {
+				break;
+			}
+
+			region = E->value();
+			in_region = true;
+			for (int i = E->key() - 2; i >= 0; i--) {
+				if (!_is_whitespace(line[i])) {
+					return -1;
+				}
+			}
+		}
+
+		if (delimiters[region].line_only) {
+			return region;
+		}
+
+		int end_col = E->key();
+		if (E->value() != -1) {
+			if (!E->next()) {
+				return region;
+			}
+			end_col = E->next()->key();
+		}
+
+		for (int i = end_col; i < line.length(); i++) {
+			if (!_is_whitespace(line[i])) {
+				return -1;
+			}
+		}
+		return region;
+	}
+	return in_region ? region : -1;
+}
+
+void CodeEdit::_add_delimiter(const String &p_start_key, const String &p_end_key, bool p_line_only, DelimiterType p_type) {
+	if (p_start_key.length() > 0) {
+		for (int i = 0; i < p_start_key.length(); i++) {
+			ERR_FAIL_COND_MSG(!is_symbol(p_start_key[i]), "delimiter must start with a symbol");
+		}
+	}
+
+	if (p_end_key.length() > 0) {
+		for (int i = 0; i < p_end_key.length(); i++) {
+			ERR_FAIL_COND_MSG(!is_symbol(p_end_key[i]), "delimiter must end with a symbol");
+		}
+	}
+
+	int at = 0;
+	for (int i = 0; i < delimiters.size(); i++) {
+		ERR_FAIL_COND_MSG(delimiters[i].start_key == p_start_key, "delimiter with start key '" + p_start_key + "' already exists.");
+		if (p_start_key.length() < delimiters[i].start_key.length()) {
+			at++;
+		}
+	}
+
+	Delimiter delimiter;
+	delimiter.type = p_type;
+	delimiter.start_key = p_start_key;
+	delimiter.end_key = p_end_key;
+	delimiter.line_only = p_line_only || p_end_key == "";
+	delimiters.insert(at, delimiter);
+	if (!setting_delimiters) {
+		delimiter_cache.clear();
+		_update_delimiter_cache();
+	}
+}
+
+void CodeEdit::_remove_delimiter(const String &p_start_key, DelimiterType p_type) {
+	for (int i = 0; i < delimiters.size(); i++) {
+		if (delimiters[i].start_key != p_start_key) {
+			continue;
+		}
+
+		if (delimiters[i].type != p_type) {
+			break;
+		}
+
+		delimiters.remove(i);
+		if (!setting_delimiters) {
+			delimiter_cache.clear();
+			_update_delimiter_cache();
+		}
+		break;
+	}
+}
+
+bool CodeEdit::_has_delimiter(const String &p_start_key, DelimiterType p_type) const {
+	for (int i = 0; i < delimiters.size(); i++) {
+		if (delimiters[i].start_key == p_start_key) {
+			return delimiters[i].type == p_type;
+		}
+	}
+	return false;
+}
+
+void CodeEdit::_set_delimiters(const TypedArray<String> &p_delimiters, DelimiterType p_type) {
+	setting_delimiters = true;
+	_clear_delimiters(p_type);
+
+	for (int i = 0; i < p_delimiters.size(); i++) {
+		String key = p_delimiters[i].is_null() ? "" : p_delimiters[i];
+
+		const String start_key = key.get_slice(" ", 0);
+		const String end_key = key.get_slice_count(" ") > 1 ? key.get_slice(" ", 1) : String();
+
+		_add_delimiter(start_key, end_key, end_key == "", p_type);
+	}
+	setting_delimiters = false;
+	_update_delimiter_cache();
+}
+
+void CodeEdit::_clear_delimiters(DelimiterType p_type) {
+	for (int i = delimiters.size() - 1; i >= 0; i--) {
+		if (delimiters[i].type == p_type) {
+			delimiters.remove(i);
+		}
+	}
+	delimiter_cache.clear();
+}
+
+TypedArray<String> CodeEdit::_get_delimiters(DelimiterType p_type) const {
+	TypedArray<String> r_delimiters;
+	for (int i = 0; i < delimiters.size(); i++) {
+		if (delimiters[i].type != p_type) {
+			continue;
+		}
+		r_delimiters.push_back(delimiters[i].start_key + (delimiters[i].end_key.empty() ? "" : " " + delimiters[i].end_key));
+	}
+	return r_delimiters;
+}
+
 void CodeEdit::_lines_edited_from(int p_from_line, int p_to_line) {
+	_update_delimiter_cache(p_from_line, p_to_line);
+
 	if (p_from_line == p_to_line) {
 		return;
 	}
@@ -392,25 +926,6 @@ void CodeEdit::_lines_edited_from(int p_from_line, int p_to_line) {
 	}
 }
 
-void CodeEdit::_update_gutter_indexes() {
-	for (int i = 0; i < get_gutter_count(); i++) {
-		if (get_gutter_name(i) == "main_gutter") {
-			main_gutter = i;
-			continue;
-		}
-
-		if (get_gutter_name(i) == "line_numbers") {
-			line_number_gutter = i;
-			continue;
-		}
-
-		if (get_gutter_name(i) == "fold_gutter") {
-			fold_gutter = i;
-			continue;
-		}
-	}
-}
-
 CodeEdit::CodeEdit() {
 	/* Text Direction */
 	set_layout_direction(LAYOUT_DIRECTION_LTR);

+ 91 - 2
scene/gui/code_edit.h

@@ -80,10 +80,72 @@ private:
 	void _fold_gutter_draw_callback(int p_line, int p_gutter, Rect2 p_region);
 
 	void _gutter_clicked(int p_line, int p_gutter);
-	void _lines_edited_from(int p_from_line, int p_to_line);
-
 	void _update_gutter_indexes();
 
+	/* Delimiters */
+	enum DelimiterType {
+		TYPE_STRING,
+		TYPE_COMMENT,
+	};
+
+	struct Delimiter {
+		DelimiterType type;
+		String start_key = "";
+		String end_key = "";
+		bool line_only = true;
+	};
+	bool setting_delimiters = false;
+	Vector<Delimiter> delimiters;
+	/*
+	 *  Vector entry per line, contains a Map of column numbers to delimiter index, -1 marks the end of a region.
+	 *  e.g the following text will be stored as so:
+	 *
+	 *  0: nothing here
+	 *  1:
+	 *  2: # test
+	 *  3: "test" text "multiline
+	 *  4:
+	 *  5: test
+	 *  6: string"
+	 *
+	 *  Vector [
+	 *      0 = []
+	 *      1 = []
+	 *      2 = [
+	 *          1 = 1
+	 *          6 = -1
+	 *      ]
+	 *      3 = [
+	 *	        1 = 0
+	 *          6 = -1
+	 *          13 = 0
+	 *      ]
+	 *      4 = [
+	 *          0 = 0
+	 *      ]
+	 *      5 = [
+	 *          5 = 0
+	 *      ]
+	 *      6 = [
+	 *          7 = -1
+	 *      ]
+	 *  ]
+	 */
+	Vector<Map<int, int>> delimiter_cache;
+
+	void _update_delimiter_cache(int p_from_line = 0, int p_to_line = -1);
+	int _is_in_delimiter(int p_line, int p_column, DelimiterType p_type) const;
+
+	void _add_delimiter(const String &p_start_key, const String &p_end_key, bool p_line_only, DelimiterType p_type);
+	void _remove_delimiter(const String &p_start_key, DelimiterType p_type);
+	bool _has_delimiter(const String &p_start_key, DelimiterType p_type) const;
+
+	void _set_delimiters(const TypedArray<String> &p_delimiters, DelimiterType p_type);
+	void _clear_delimiters(DelimiterType p_type);
+	TypedArray<String> _get_delimiters(DelimiterType p_type) const;
+
+	void _lines_edited_from(int p_from_line, int p_to_line);
+
 protected:
 	void _notification(int p_what);
 
@@ -128,6 +190,33 @@ public:
 	void set_draw_fold_gutter(bool p_draw);
 	bool is_drawing_fold_gutter() 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);
+	bool has_string_delimiter(const String &p_start_key) const;
+
+	void set_string_delimiters(const TypedArray<String> &p_string_delimiters);
+	void clear_string_delimiters();
+	TypedArray<String> get_string_delimiters() const;
+
+	int is_in_string(int p_line, int p_column = -1) const;
+
+	void add_comment_delimiter(const String &p_start_key, const String &p_end_key, bool p_line_only = false);
+	void remove_comment_delimiter(const String &p_start_key);
+	bool has_comment_delimiter(const String &p_start_key) const;
+
+	void set_comment_delimiters(const TypedArray<String> &p_comment_delimiters);
+	void clear_comment_delimiters();
+	TypedArray<String> get_comment_delimiters() const;
+
+	int is_in_comment(int p_line, int p_column = -1) const;
+
+	String get_delimiter_start_key(int p_delimiter_idx) const;
+	String get_delimiter_end_key(int p_delimiter_idx) const;
+
+	Point2 get_delimiter_start_position(int p_line, int p_column) const;
+	Point2 get_delimiter_end_position(int p_line, int p_column) const;
+
 	CodeEdit();
 	~CodeEdit();
 };