From c13d9eb6e500ace4e106c50cb016125ef064f4e4 Mon Sep 17 00:00:00 2001
From: Paulb23 <p_batty@hotmail.co.uk>
Date: Sat, 25 Jul 2020 01:15:23 +0100
Subject: [PATCH] Added gutter system to TextEdit

---
 scene/gui/text_edit.cpp | 394 +++++++++++++++++++++++++++++++++++++---
 scene/gui/text_edit.h   | 103 ++++++++++-
 2 files changed, 473 insertions(+), 24 deletions(-)

diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp
index 1d732bec5b3..ab9a468a8ba 100644
--- a/scene/gui/text_edit.cpp
+++ b/scene/gui/text_edit.cpp
@@ -107,6 +107,8 @@ static int _find_first_non_whitespace_column_of_line(const String &line) {
 	return left;
 }
 
+///////////////////////////////////////////////////////////////////////////////
+
 void TextEdit::Text::set_font(const Ref<Font> &p_font) {
 	font = p_font;
 }
@@ -198,6 +200,7 @@ void TextEdit::Text::set(int p_line, const String &p_text) {
 
 void TextEdit::Text::insert(int p_at, const String &p_text) {
 	Line line;
+	line.gutters.resize(gutter_count);
 	line.marked = false;
 	line.safe = false;
 	line.breakpoint = false;
@@ -231,6 +234,32 @@ int TextEdit::Text::get_char_width(char32_t c, char32_t next_c, int px) const {
 	return w;
 }
 
+void TextEdit::Text::add_gutter(int p_at) {
+	for (int i = 0; i < text.size(); i++) {
+		if (p_at < 0 || p_at > gutter_count) {
+			text.write[i].gutters.push_back(Gutter());
+		} else {
+			text.write[i].gutters.insert(p_at, Gutter());
+		}
+	}
+	gutter_count++;
+}
+
+void TextEdit::Text::remove_gutter(int p_gutter) {
+	for (int i = 0; i < text.size(); i++) {
+		text.write[i].gutters.remove(p_gutter);
+	}
+	gutter_count--;
+}
+
+void TextEdit::Text::move_gutters(int p_from_line, int p_to_line) {
+	text.write[p_to_line].gutters = text[p_from_line].gutters;
+	text.write[p_from_line].gutters.clear();
+	text.write[p_from_line].gutters.resize(gutter_count);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
 void TextEdit::_update_scrollbars() {
 	Size2 size = get_size();
 	Size2 hmin = h_scroll->get_combined_minimum_size();
@@ -249,7 +278,7 @@ void TextEdit::_update_scrollbars() {
 	}
 
 	int visible_width = size.width - cache.style_normal->get_minimum_size().width;
-	int total_width = text.get_max_width(true) + vmin.x;
+	int total_width = text.get_max_width(true) + vmin.x + gutters_width + gutter_padding;
 
 	if (line_numbers) {
 		total_width += cache.line_number_w;
@@ -604,7 +633,7 @@ void TextEdit::_notification(int p_what) {
 
 			RID ci = get_canvas_item();
 			RenderingServer::get_singleton()->canvas_item_set_clip(get_canvas_item(), true);
-			int xmargin_beg = cache.style_normal->get_margin(MARGIN_LEFT) + cache.line_number_w + cache.breakpoint_gutter_width + cache.fold_gutter_width + cache.info_gutter_width;
+			int xmargin_beg = cache.style_normal->get_margin(MARGIN_LEFT) + gutters_width + gutter_padding + cache.line_number_w + cache.breakpoint_gutter_width + cache.fold_gutter_width + cache.info_gutter_width;
 
 			int xmargin_end = size.width - cache.style_normal->get_margin(MARGIN_RIGHT) - cache.minimap_width;
 			// Let's do it easy for now.
@@ -1053,6 +1082,54 @@ void TextEdit::_notification(int p_what) {
 					if (line_wrap_index == 0) {
 						// Only do these if we are on the first wrapped part of a line.
 
+						int gutter_offset = cache.style_normal->get_margin(MARGIN_LEFT);
+						for (int g = 0; g < gutters.size(); g++) {
+							const GutterInfo gutter = gutters[g];
+
+							if (!gutter.draw || gutter.width <= 0) {
+								continue;
+							}
+
+							switch (gutter.type) {
+								case GUTTER_TYPE_STRING: {
+									const String &text = get_line_gutter_text(line, g);
+									if (text == "") {
+										break;
+									}
+
+									int yofs = ofs_y + (get_row_height() - cache.font->get_height()) / 2;
+									cache.font->draw(ci, Point2(gutter_offset + ofs_x, yofs + cache.font->get_ascent()), text, get_line_gutter_item_color(line, g));
+								} break;
+								case GUTTER_TPYE_ICON: {
+									const Ref<Texture2D> icon = get_line_gutter_icon(line, g);
+									if (icon.is_null()) {
+										break;
+									}
+
+									Rect2i gutter_rect = Rect2i(Point2i(gutter_offset, ofs_y), Size2i(gutter.width, get_row_height()));
+
+									int horizontal_padding = gutter_rect.size.x / 6;
+									int vertical_padding = gutter_rect.size.y / 6;
+
+									gutter_rect.position += Point2(horizontal_padding, vertical_padding);
+									gutter_rect.size -= Point2(horizontal_padding, vertical_padding) * 2;
+
+									icon->draw_rect(ci, gutter_rect, false, get_line_gutter_item_color(line, g));
+								} break;
+								case GUTTER_TPYE_CUSTOM: {
+									if (gutter.custom_draw_obj.is_valid()) {
+										Object *cdo = ObjectDB::get_instance(gutter.custom_draw_obj);
+										if (cdo) {
+											Rect2i gutter_rect = Rect2i(Point2i(gutter_offset, ofs_y), Size2i(gutter.width, get_row_height()));
+											cdo->call(gutter.custom_draw_callback, line, g, Rect2(gutter_rect));
+										}
+									}
+								} break;
+							}
+
+							gutter_offset += gutter.width;
+						}
+
 						if (text.is_breakpoint(line) && !draw_breakpoint_gutter) {
 #ifdef TOOLS_ENABLED
 							RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(xmargin_beg + ofs_x, ofs_y + get_row_height() - EDSCALE, xmargin_end - xmargin_beg, EDSCALE), cache.breakpoint_color);
@@ -1067,7 +1144,7 @@ void TextEdit::_notification(int p_what) {
 								int vertical_gap = (get_row_height() * 40) / 100;
 								int horizontal_gap = (cache.breakpoint_gutter_width * 30) / 100;
 								int marker_radius = get_row_height() - (vertical_gap * 2);
-								RenderingServer::get_singleton()->canvas_item_add_circle(ci, Point2(cache.style_normal->get_margin(MARGIN_LEFT) + horizontal_gap - 2 + marker_radius / 2, ofs_y + vertical_gap + marker_radius / 2), marker_radius, Color(cache.bookmark_color.r, cache.bookmark_color.g, cache.bookmark_color.b));
+								RenderingServer::get_singleton()->canvas_item_add_circle(ci, Point2(cache.style_normal->get_margin(MARGIN_LEFT) + gutters_width + horizontal_gap - 2 + marker_radius / 2, ofs_y + vertical_gap + marker_radius / 2), marker_radius, Color(cache.bookmark_color.r, cache.bookmark_color.g, cache.bookmark_color.b));
 							}
 						}
 
@@ -1079,7 +1156,7 @@ void TextEdit::_notification(int p_what) {
 								int marker_height = get_row_height() - (vertical_gap * 2);
 								int marker_width = cache.breakpoint_gutter_width - (horizontal_gap * 2);
 								// No transparency on marker.
-								RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(cache.style_normal->get_margin(MARGIN_LEFT) + horizontal_gap - 2, ofs_y + vertical_gap, marker_width, marker_height), Color(cache.breakpoint_color.r, cache.breakpoint_color.g, cache.breakpoint_color.b));
+								RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(cache.style_normal->get_margin(MARGIN_LEFT) + gutters_width + horizontal_gap - 2, ofs_y + vertical_gap, marker_width, marker_height), Color(cache.breakpoint_color.r, cache.breakpoint_color.g, cache.breakpoint_color.b));
 							}
 						}
 
@@ -1087,7 +1164,7 @@ void TextEdit::_notification(int p_what) {
 						if (draw_info_gutter && text.has_info_icon(line)) {
 							int vertical_gap = (get_row_height() * 40) / 100;
 							int horizontal_gap = (cache.info_gutter_width * 30) / 100;
-							int gutter_left = cache.style_normal->get_margin(MARGIN_LEFT) + cache.breakpoint_gutter_width;
+							int gutter_left = cache.style_normal->get_margin(MARGIN_LEFT) + gutters_width + cache.breakpoint_gutter_width;
 
 							Ref<Texture2D> info_icon = text.get_info_icon(line);
 							// Ensure the icon fits the gutter size.
@@ -1116,7 +1193,7 @@ void TextEdit::_notification(int p_what) {
 								int horizontal_gap = (cache.breakpoint_gutter_width * 30) / 100;
 								int marker_height = get_row_height() - (vertical_gap * 2) + icon_extra_size;
 								int marker_width = cache.breakpoint_gutter_width - (horizontal_gap * 2) + icon_extra_size;
-								cache.executing_icon->draw_rect(ci, Rect2(cache.style_normal->get_margin(MARGIN_LEFT) + horizontal_gap - 2 - icon_extra_size / 2, ofs_y + vertical_gap - icon_extra_size / 2, marker_width, marker_height), false, Color(cache.executing_line_color.r, cache.executing_line_color.g, cache.executing_line_color.b));
+								cache.executing_icon->draw_rect(ci, Rect2(cache.style_normal->get_margin(MARGIN_LEFT) + gutters_width + horizontal_gap - 2 - icon_extra_size / 2, ofs_y + vertical_gap - icon_extra_size / 2, marker_width, marker_height), false, Color(cache.executing_line_color.r, cache.executing_line_color.g, cache.executing_line_color.b));
 							} else {
 #ifdef TOOLS_ENABLED
 								RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(xmargin_beg + ofs_x, ofs_y + get_row_height() - EDSCALE, xmargin_end - xmargin_beg, EDSCALE), cache.executing_line_color);
@@ -1129,7 +1206,7 @@ void TextEdit::_notification(int p_what) {
 						// Draw fold markers.
 						if (draw_fold_gutter) {
 							int horizontal_gap = (cache.fold_gutter_width * 30) / 100;
-							int gutter_left = cache.style_normal->get_margin(MARGIN_LEFT) + cache.breakpoint_gutter_width + cache.line_number_w + cache.info_gutter_width;
+							int gutter_left = cache.style_normal->get_margin(MARGIN_LEFT) + gutters_width + cache.breakpoint_gutter_width + cache.line_number_w + cache.info_gutter_width;
 							if (is_folded(line)) {
 								int xofs = horizontal_gap - (cache.can_fold_icon->get_width()) / 2;
 								int yofs = (get_row_height() - cache.folded_icon->get_height()) / 2;
@@ -1149,7 +1226,7 @@ void TextEdit::_notification(int p_what) {
 								fc = line_num_padding + fc;
 							}
 
-							cache.font->draw(ci, Point2(cache.style_normal->get_margin(MARGIN_LEFT) + cache.breakpoint_gutter_width + cache.info_gutter_width + ofs_x, yofs + cache.font->get_ascent()), fc, text.is_safe(line) ? cache.safe_line_number_color : cache.line_number_color);
+							cache.font->draw(ci, Point2(cache.style_normal->get_margin(MARGIN_LEFT) + gutters_width + cache.breakpoint_gutter_width + cache.info_gutter_width + ofs_x, yofs + cache.font->get_ascent()), fc, text.is_safe(line) ? cache.safe_line_number_color : cache.line_number_color);
 						}
 					}
 
@@ -1797,6 +1874,32 @@ void TextEdit::backspace_at_cursor() {
 	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);
+			}
+		}
+	}
+
 	if (is_line_hidden(cursor.line)) {
 		set_line_as_hidden(prev_line, true);
 	}
@@ -1998,7 +2101,7 @@ void TextEdit::_get_mouse_pos(const Point2i &p_mouse, int &r_row, int &r_col) co
 		row = text.size() - 1;
 		col = text[row].size();
 	} else {
-		int colx = p_mouse.x - (cache.style_normal->get_margin(MARGIN_LEFT) + cache.line_number_w + cache.breakpoint_gutter_width + cache.fold_gutter_width + cache.info_gutter_width);
+		int colx = p_mouse.x - (cache.style_normal->get_margin(MARGIN_LEFT) + gutters_width + gutter_padding + cache.line_number_w + cache.breakpoint_gutter_width + cache.fold_gutter_width + cache.info_gutter_width);
 		colx += cursor.x_ofs;
 		col = get_char_pos_for_line(colx, row, wrap_index);
 		if (is_wrap_enabled() && wrap_index < times_line_wraps(row)) {
@@ -2043,7 +2146,7 @@ Vector2i TextEdit::_get_cursor_pixel_pos() {
 
 	// Calculate final pixel position
 	int y = (row - get_v_scroll_offset() + 1 /*Bottom of line*/) * get_row_height();
-	int x = cache.style_normal->get_margin(MARGIN_LEFT) + cache.line_number_w + cache.breakpoint_gutter_width + cache.fold_gutter_width + cache.info_gutter_width - cursor.x_ofs;
+	int x = cache.style_normal->get_margin(MARGIN_LEFT) + gutters_width + gutter_padding + cache.line_number_w + cache.breakpoint_gutter_width + cache.fold_gutter_width + cache.info_gutter_width - cursor.x_ofs;
 	int ix = 0;
 	while (ix < rows2[0].size() && ix < cursor.column) {
 		if (cache.font != nullptr) {
@@ -2178,9 +2281,23 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) {
 				int row, col;
 				_get_mouse_pos(Point2i(mb->get_position().x, mb->get_position().y), row, col);
 
+				int left_margin = cache.style_normal->get_margin(MARGIN_LEFT);
+				for (int i = 0; i < gutters.size(); i++) {
+					if (!gutters[i].draw || gutters[i].width <= 0) {
+						continue;
+					}
+
+					if (mb->get_position().x > left_margin && mb->get_position().x <= (left_margin + gutters[i].width) - 3) {
+						emit_signal("gutter_clicked", row, i);
+						return;
+					}
+
+					left_margin += gutters[i].width;
+				}
+
 				// Toggle breakpoint on gutter click.
 				if (draw_breakpoint_gutter) {
-					int gutter = cache.style_normal->get_margin(MARGIN_LEFT);
+					int gutter = cache.style_normal->get_margin(MARGIN_LEFT) + gutters_width;
 					if (mb->get_position().x > gutter - 6 && mb->get_position().x <= gutter + cache.breakpoint_gutter_width - 3) {
 						set_line_as_breakpoint(row, !is_line_set_as_breakpoint(row));
 						emit_signal("breakpoint_toggled", row);
@@ -2190,8 +2307,8 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) {
 
 				// Emit info clicked.
 				if (draw_info_gutter && text.has_info_icon(row)) {
-					int left_margin = cache.style_normal->get_margin(MARGIN_LEFT);
-					int gutter_left = left_margin + cache.breakpoint_gutter_width;
+					left_margin = cache.style_normal->get_margin(MARGIN_LEFT);
+					int gutter_left = left_margin + gutters_width + cache.breakpoint_gutter_width;
 					if (mb->get_position().x > gutter_left - 6 && mb->get_position().x <= gutter_left + cache.info_gutter_width - 3) {
 						emit_signal("info_clicked", row, text.get_info(row));
 						return;
@@ -2200,8 +2317,8 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) {
 
 				// Toggle fold on gutter click if can.
 				if (draw_fold_gutter) {
-					int left_margin = cache.style_normal->get_margin(MARGIN_LEFT);
-					int gutter_left = left_margin + cache.breakpoint_gutter_width + cache.line_number_w + cache.info_gutter_width;
+					left_margin = cache.style_normal->get_margin(MARGIN_LEFT);
+					int gutter_left = left_margin + gutters_width + cache.breakpoint_gutter_width + cache.line_number_w + cache.info_gutter_width;
 					if (mb->get_position().x > gutter_left - 6 && mb->get_position().x <= gutter_left + cache.fold_gutter_width - 3) {
 						if (is_folded(row)) {
 							unfold_line(row);
@@ -2215,7 +2332,7 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) {
 				// Unfold on folded icon click.
 				if (is_folded(row)) {
 					int line_width = text.get_line_width(row);
-					line_width += cache.style_normal->get_margin(MARGIN_LEFT) + cache.line_number_w + cache.breakpoint_gutter_width + cache.info_gutter_width + cache.fold_gutter_width - cursor.x_ofs;
+					line_width += cache.style_normal->get_margin(MARGIN_LEFT) + gutters_width + gutter_padding + cache.line_number_w + cache.breakpoint_gutter_width + cache.info_gutter_width + cache.fold_gutter_width - cursor.x_ofs;
 					if (mb->get_position().x > line_width - 3 && mb->get_position().x <= line_width + cache.folded_eol_icon->get_width() + 3) {
 						unfold_line(row);
 						return;
@@ -3818,6 +3935,7 @@ void TextEdit::_base_insert_text(int p_line, int p_char, const String &p_text, i
 	}
 
 	if (shift_first_line) {
+		text.move_gutters(p_line, p_line + 1);
 		text.set_breakpoint(p_line + 1, text.is_breakpoint(p_line));
 		text.set_hidden(p_line + 1, text.is_hidden(p_line));
 		if (text.has_info_icon(p_line)) {
@@ -4097,7 +4215,7 @@ int TextEdit::get_total_visible_rows() const {
 }
 
 void TextEdit::_update_wrap_at() {
-	wrap_at = get_size().width - cache.style_normal->get_minimum_size().width - cache.line_number_w - cache.breakpoint_gutter_width - cache.fold_gutter_width - cache.info_gutter_width - cache.minimap_width - wrap_right_offset;
+	wrap_at = get_size().width - cache.style_normal->get_minimum_size().width - gutters_width - gutter_padding - cache.line_number_w - cache.breakpoint_gutter_width - cache.fold_gutter_width - cache.info_gutter_width - cache.minimap_width - wrap_right_offset;
 	update_cursor_wrap_offset();
 	text.clear_wrap_cache();
 
@@ -4132,7 +4250,7 @@ void TextEdit::adjust_viewport_to_cursor() {
 		set_line_as_last_visible(cur_line, cur_wrap);
 	}
 
-	int visible_width = get_size().width - cache.style_normal->get_minimum_size().width - cache.line_number_w - cache.breakpoint_gutter_width - cache.fold_gutter_width - cache.info_gutter_width - cache.minimap_width;
+	int visible_width = get_size().width - cache.style_normal->get_minimum_size().width - gutters_width - gutter_padding - cache.line_number_w - cache.breakpoint_gutter_width - cache.fold_gutter_width - cache.info_gutter_width - cache.minimap_width;
 	if (v_scroll->is_visible_in_tree()) {
 		visible_width -= v_scroll->get_combined_minimum_size().width;
 	}
@@ -4167,7 +4285,7 @@ void TextEdit::center_viewport_to_cursor() {
 	}
 
 	set_line_as_center_visible(cursor.line, get_cursor_wrap_index());
-	int visible_width = get_size().width - cache.style_normal->get_minimum_size().width - cache.line_number_w - cache.breakpoint_gutter_width - cache.fold_gutter_width - cache.info_gutter_width - cache.minimap_width;
+	int visible_width = get_size().width - cache.style_normal->get_minimum_size().width - gutters_width - gutter_padding - cache.line_number_w - cache.breakpoint_gutter_width - cache.fold_gutter_width - cache.info_gutter_width - cache.minimap_width;
 	if (v_scroll->is_visible_in_tree()) {
 		visible_width -= v_scroll->get_combined_minimum_size().width;
 	}
@@ -4614,7 +4732,7 @@ Control::CursorShape TextEdit::get_cursor_shape(const Point2 &p_pos) const {
 		return CURSOR_POINTING_HAND;
 	}
 
-	int gutter = cache.style_normal->get_margin(MARGIN_LEFT) + cache.line_number_w + cache.breakpoint_gutter_width + cache.fold_gutter_width + cache.info_gutter_width;
+	int gutter = cache.style_normal->get_margin(MARGIN_LEFT) + gutters_width + cache.line_number_w + cache.breakpoint_gutter_width + cache.fold_gutter_width + cache.info_gutter_width;
 	if ((completion_active && completion_rect.has_point(p_pos))) {
 		return CURSOR_ARROW;
 	}
@@ -4623,6 +4741,21 @@ Control::CursorShape TextEdit::get_cursor_shape(const Point2 &p_pos) const {
 		_get_mouse_pos(p_pos, row, col);
 		int left_margin = cache.style_normal->get_margin(MARGIN_LEFT);
 
+		for (int i = 0; i < gutters.size(); i++) {
+			if (!gutters[i].draw) {
+				continue;
+			}
+
+			if (p_pos.x > left_margin && p_pos.x <= (left_margin + gutters[i].width) - 3) {
+				if (gutters[i].clickable || is_line_gutter_clickable(row, i)) {
+					return CURSOR_POINTING_HAND;
+				}
+			}
+			left_margin += gutters[i].width;
+		}
+
+		left_margin += gutters_width;
+
 		// Breakpoint icon.
 		if (draw_breakpoint_gutter && p_pos.x > left_margin - 6 && p_pos.x <= left_margin + cache.breakpoint_gutter_width - 3) {
 			return CURSOR_POINTING_HAND;
@@ -4658,7 +4791,7 @@ Control::CursorShape TextEdit::get_cursor_shape(const Point2 &p_pos) const {
 		// EOL fold icon.
 		if (is_folded(row)) {
 			int line_width = text.get_line_width(row);
-			line_width += cache.style_normal->get_margin(MARGIN_LEFT) + cache.line_number_w + cache.breakpoint_gutter_width + cache.fold_gutter_width + cache.info_gutter_width - cursor.x_ofs;
+			line_width += cache.style_normal->get_margin(MARGIN_LEFT) + gutters_width + gutter_padding + cache.line_number_w + cache.breakpoint_gutter_width + cache.fold_gutter_width + cache.info_gutter_width - cursor.x_ofs;
 			if (p_pos.x > line_width - 3 && p_pos.x <= line_width + cache.folded_eol_icon->get_width() + 3) {
 				return CURSOR_POINTING_HAND;
 			}
@@ -4899,6 +5032,7 @@ void TextEdit::_update_caches() {
 	}
 }
 
+/* Syntax Highlighting. */
 Ref<SyntaxHighlighter> TextEdit::get_syntax_highlighter() {
 	return syntax_highlighter;
 }
@@ -4911,6 +5045,187 @@ void TextEdit::set_syntax_highlighter(Ref<SyntaxHighlighter> p_syntax_highlighte
 	update();
 }
 
+/* Gutters. */
+void TextEdit::_update_gutter_width() {
+	gutters_width = 0;
+	for (int i = 0; i < gutters.size(); i++) {
+		if (gutters[i].draw) {
+			gutters_width += gutters[i].width;
+		}
+	}
+	if (gutters_width > 0) {
+		gutter_padding = 2;
+	}
+	update();
+}
+
+void TextEdit::add_gutter(int p_at) {
+	if (p_at < 0 || p_at > gutters.size()) {
+		gutters.push_back(GutterInfo());
+	} else {
+		gutters.insert(p_at, GutterInfo());
+	}
+
+	for (int i = 0; i < text.size() + 1; i++) {
+		text.add_gutter(p_at);
+	}
+	emit_signal("gutter_added");
+	update();
+}
+
+void TextEdit::remove_gutter(int p_gutter) {
+	ERR_FAIL_INDEX(p_gutter, gutters.size());
+
+	gutters.remove(p_gutter);
+
+	for (int i = 0; i < text.size() + 1; i++) {
+		text.remove_gutter(p_gutter);
+	}
+	emit_signal("gutter_removed");
+	update();
+}
+
+int TextEdit::get_gutter_count() const {
+	return gutters.size();
+}
+
+void TextEdit::set_gutter_name(int p_gutter, const String &p_name) {
+	ERR_FAIL_INDEX(p_gutter, gutters.size());
+	gutters.write[p_gutter].name = p_name;
+}
+
+String TextEdit::get_gutter_name(int p_gutter) const {
+	ERR_FAIL_INDEX_V(p_gutter, gutters.size(), "");
+	return gutters[p_gutter].name;
+}
+
+void TextEdit::set_gutter_type(int p_gutter, GutterType p_type) {
+	ERR_FAIL_INDEX(p_gutter, gutters.size());
+	gutters.write[p_gutter].type = p_type;
+	update();
+}
+
+TextEdit::GutterType TextEdit::get_gutter_type(int p_gutter) const {
+	ERR_FAIL_INDEX_V(p_gutter, gutters.size(), GUTTER_TYPE_STRING);
+	return gutters[p_gutter].type;
+}
+
+void TextEdit::set_gutter_width(int p_gutter, int p_width) {
+	ERR_FAIL_INDEX(p_gutter, gutters.size());
+	gutters.write[p_gutter].width = p_width;
+	_update_gutter_width();
+}
+
+int TextEdit::get_gutter_width(int p_gutter) const {
+	ERR_FAIL_INDEX_V(p_gutter, gutters.size(), -1);
+	return gutters[p_gutter].width;
+}
+
+void TextEdit::set_gutter_draw(int p_gutter, bool p_draw) {
+	ERR_FAIL_INDEX(p_gutter, gutters.size());
+	gutters.write[p_gutter].draw = p_draw;
+	_update_gutter_width();
+}
+
+bool TextEdit::is_gutter_drawn(int p_gutter) const {
+	ERR_FAIL_INDEX_V(p_gutter, gutters.size(), false);
+	return gutters[p_gutter].draw;
+}
+
+void TextEdit::set_gutter_clickable(int p_gutter, bool p_clickable) {
+	ERR_FAIL_INDEX(p_gutter, gutters.size());
+	gutters.write[p_gutter].clickable = p_clickable;
+	update();
+}
+
+bool TextEdit::is_gutter_clickable(int p_gutter) const {
+	ERR_FAIL_INDEX_V(p_gutter, gutters.size(), false);
+	return gutters[p_gutter].clickable;
+}
+
+void TextEdit::set_gutter_overwritable(int p_gutter, bool p_overwritable) {
+	ERR_FAIL_INDEX(p_gutter, gutters.size());
+	gutters.write[p_gutter].overwritable = p_overwritable;
+}
+
+bool TextEdit::is_gutter_overwritable(int p_gutter) const {
+	ERR_FAIL_INDEX_V(p_gutter, gutters.size(), false);
+	return gutters[p_gutter].overwritable;
+}
+
+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);
+
+	gutters.write[p_gutter].custom_draw_obj = p_object->get_instance_id();
+	gutters.write[p_gutter].custom_draw_callback = p_callback;
+	update();
+}
+
+// Line gutters.
+void TextEdit::set_line_gutter_metadata(int p_line, int p_gutter, const Variant &p_metadata) {
+	ERR_FAIL_INDEX(p_line, text.size());
+	ERR_FAIL_INDEX(p_gutter, gutters.size());
+	text.set_line_gutter_metadata(p_line, p_gutter, p_metadata);
+}
+
+Variant TextEdit::get_line_gutter_metadata(int p_line, int p_gutter) const {
+	ERR_FAIL_INDEX_V(p_line, text.size(), "");
+	ERR_FAIL_INDEX_V(p_gutter, gutters.size(), "");
+	return text.get_line_gutter_metadata(p_line, p_gutter);
+}
+
+void TextEdit::set_line_gutter_text(int p_line, int p_gutter, const String &p_text) {
+	ERR_FAIL_INDEX(p_line, text.size());
+	ERR_FAIL_INDEX(p_gutter, gutters.size());
+	text.set_line_gutter_text(p_line, p_gutter, p_text);
+	update();
+}
+
+String TextEdit::get_line_gutter_text(int p_line, int p_gutter) const {
+	ERR_FAIL_INDEX_V(p_line, text.size(), "");
+	ERR_FAIL_INDEX_V(p_gutter, gutters.size(), "");
+	return text.get_line_gutter_text(p_line, p_gutter);
+}
+
+void TextEdit::set_line_gutter_icon(int p_line, int p_gutter, Ref<Texture2D> p_icon) {
+	ERR_FAIL_INDEX(p_line, text.size());
+	ERR_FAIL_INDEX(p_gutter, gutters.size());
+	text.set_line_gutter_icon(p_line, p_gutter, p_icon);
+	update();
+}
+
+Ref<Texture2D> TextEdit::get_line_gutter_icon(int p_line, int p_gutter) const {
+	ERR_FAIL_INDEX_V(p_line, text.size(), Ref<Texture2D>());
+	ERR_FAIL_INDEX_V(p_gutter, gutters.size(), Ref<Texture2D>());
+	return text.get_line_gutter_icon(p_line, p_gutter);
+}
+
+void TextEdit::set_line_gutter_item_color(int p_line, int p_gutter, const Color &p_color) {
+	ERR_FAIL_INDEX(p_line, text.size());
+	ERR_FAIL_INDEX(p_gutter, gutters.size());
+	text.set_line_gutter_item_color(p_line, p_gutter, p_color);
+	update();
+}
+
+Color TextEdit::get_line_gutter_item_color(int p_line, int p_gutter) {
+	ERR_FAIL_INDEX_V(p_line, text.size(), Color());
+	ERR_FAIL_INDEX_V(p_gutter, gutters.size(), Color());
+	return text.get_line_gutter_item_color(p_line, p_gutter);
+}
+
+void TextEdit::set_line_gutter_clickable(int p_line, int p_gutter, bool p_clickable) {
+	ERR_FAIL_INDEX(p_line, text.size());
+	ERR_FAIL_INDEX(p_gutter, gutters.size());
+	text.set_line_gutter_clickable(p_line, p_gutter, p_clickable);
+}
+
+bool TextEdit::is_line_gutter_clickable(int p_line, int p_gutter) const {
+	ERR_FAIL_INDEX_V(p_line, text.size(), false);
+	ERR_FAIL_INDEX_V(p_gutter, gutters.size(), false);
+	return text.is_line_gutter_clickable(p_line, p_gutter);
+}
+
 void TextEdit::add_keyword(const String &p_keyword) {
 	keywords.insert(p_keyword);
 }
@@ -6832,6 +7147,40 @@ void TextEdit::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_syntax_highlighter", "syntax_highlighter"), &TextEdit::set_syntax_highlighter);
 	ClassDB::bind_method(D_METHOD("get_syntax_highlighter"), &TextEdit::get_syntax_highlighter);
 
+	/* Gutters. */
+	BIND_ENUM_CONSTANT(GUTTER_TYPE_STRING);
+	BIND_ENUM_CONSTANT(GUTTER_TPYE_ICON);
+	BIND_ENUM_CONSTANT(GUTTER_TPYE_CUSTOM);
+
+	ClassDB::bind_method(D_METHOD("add_gutter", "at"), &TextEdit::add_gutter, DEFVAL(-1));
+	ClassDB::bind_method(D_METHOD("remove_gutter", "gutter"), &TextEdit::remove_gutter);
+	ClassDB::bind_method(D_METHOD("get_gutter_count"), &TextEdit::get_gutter_count);
+	ClassDB::bind_method(D_METHOD("set_gutter_name", "gutter", "name"), &TextEdit::set_gutter_name);
+	ClassDB::bind_method(D_METHOD("get_gutter_name", "gutter"), &TextEdit::get_gutter_name);
+	ClassDB::bind_method(D_METHOD("set_gutter_type", "gutter", "type"), &TextEdit::set_gutter_type);
+	ClassDB::bind_method(D_METHOD("get_gutter_type", "gutter"), &TextEdit::get_gutter_type);
+	ClassDB::bind_method(D_METHOD("set_gutter_width", "gutter", "width"), &TextEdit::set_gutter_width);
+	ClassDB::bind_method(D_METHOD("get_gutter_width", "gutter"), &TextEdit::get_gutter_width);
+	ClassDB::bind_method(D_METHOD("set_gutter_draw", "gutter", "draw"), &TextEdit::set_gutter_draw);
+	ClassDB::bind_method(D_METHOD("is_gutter_drawn", "gutter"), &TextEdit::is_gutter_drawn);
+	ClassDB::bind_method(D_METHOD("set_gutter_clickable", "gutter", "clickable"), &TextEdit::set_gutter_clickable);
+	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("set_gutter_custom_draw", "column", "object", "callback"), &TextEdit::set_gutter_custom_draw);
+
+	// Line gutters.
+	ClassDB::bind_method(D_METHOD("set_line_gutter_metadata", "line", "gutter", "metadata"), &TextEdit::set_line_gutter_metadata);
+	ClassDB::bind_method(D_METHOD("get_line_gutter_metadata", "line", "gutter"), &TextEdit::get_line_gutter_metadata);
+	ClassDB::bind_method(D_METHOD("set_line_gutter_text", "line", "gutter", "text"), &TextEdit::set_line_gutter_text);
+	ClassDB::bind_method(D_METHOD("get_line_gutter_text", "line", "gutter"), &TextEdit::get_line_gutter_text);
+	ClassDB::bind_method(D_METHOD("set_line_gutter_icon", "line", "gutter", "icon"), &TextEdit::set_line_gutter_icon);
+	ClassDB::bind_method(D_METHOD("get_line_gutter_icon", "line", "gutter"), &TextEdit::get_line_gutter_icon);
+	ClassDB::bind_method(D_METHOD("set_line_gutter_item_color", "line", "gutter", "color"), &TextEdit::set_line_gutter_item_color);
+	ClassDB::bind_method(D_METHOD("get_line_gutter_item_color", "line", "gutter"), &TextEdit::get_line_gutter_item_color);
+	ClassDB::bind_method(D_METHOD("set_line_gutter_clickable", "line", "gutter", "clickable"), &TextEdit::set_line_gutter_clickable);
+	ClassDB::bind_method(D_METHOD("is_line_gutter_clickable", "line", "gutter"), &TextEdit::is_line_gutter_clickable);
+
 	ClassDB::bind_method(D_METHOD("set_highlight_current_line", "enabled"), &TextEdit::set_highlight_current_line);
 	ClassDB::bind_method(D_METHOD("is_highlight_current_line_enabled"), &TextEdit::is_highlight_current_line_enabled);
 
@@ -6892,6 +7241,9 @@ void TextEdit::_bind_methods() {
 	ADD_SIGNAL(MethodInfo("text_changed"));
 	ADD_SIGNAL(MethodInfo("line_edited_from", PropertyInfo(Variant::INT, "line")));
 	ADD_SIGNAL(MethodInfo("request_completion"));
+	ADD_SIGNAL(MethodInfo("gutter_clicked", PropertyInfo(Variant::INT, "line"), PropertyInfo(Variant::INT, "gutter")));
+	ADD_SIGNAL(MethodInfo("gutter_added"));
+	ADD_SIGNAL(MethodInfo("gutter_removed"));
 	ADD_SIGNAL(MethodInfo("breakpoint_toggled", PropertyInfo(Variant::INT, "row")));
 	ADD_SIGNAL(MethodInfo("symbol_lookup", PropertyInfo(Variant::STRING, "symbol"), PropertyInfo(Variant::INT, "row"), PropertyInfo(Variant::INT, "column")));
 	ADD_SIGNAL(MethodInfo("info_clicked", PropertyInfo(Variant::INT, "row"), PropertyInfo(Variant::STRING, "info")));
diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h
index 70d7365d71f..970fada209e 100644
--- a/scene/gui/text_edit.h
+++ b/scene/gui/text_edit.h
@@ -41,9 +41,44 @@ class TextEdit : public Control {
 	GDCLASS(TextEdit, Control);
 
 public:
+	enum GutterType {
+		GUTTER_TYPE_STRING,
+		GUTTER_TPYE_ICON,
+		GUTTER_TPYE_CUSTOM
+	};
+
+private:
+	struct GutterInfo {
+		GutterType type = GutterType::GUTTER_TYPE_STRING;
+		String name = "";
+		int width = 24;
+		bool draw = true;
+		bool clickable = false;
+		bool overwritable = false;
+
+		ObjectID custom_draw_obj = ObjectID();
+		StringName custom_draw_callback;
+	};
+	Vector<GutterInfo> gutters;
+	int gutters_width = 0;
+	int gutter_padding = 0;
+
+	void _update_gutter_width();
+
 	class Text {
 	public:
+		struct Gutter {
+			Variant metadata;
+			bool clickable = false;
+
+			Ref<Texture2D> icon = Ref<Texture2D>();
+			String text = "";
+			Color color = Color(1, 1, 1);
+		};
+
 		struct Line {
+			Vector<Gutter> gutters;
+
 			int width_cache : 24;
 			bool marked : 1;
 			bool breakpoint : 1;
@@ -70,7 +105,8 @@ public:
 	private:
 		mutable Vector<Line> text;
 		Ref<Font> font;
-		int indent_size;
+		int indent_size = 4;
+		int gutter_count = 0;
 
 		void _update_line_cache(int p_line) const;
 
@@ -113,10 +149,28 @@ public:
 		void clear_wrap_cache();
 		void clear_info_icons();
 		_FORCE_INLINE_ const String &operator[](int p_line) const { return text[p_line].data; }
-		Text() { indent_size = 4; }
+
+		/* Gutters. */
+		void add_gutter(int p_at);
+		void remove_gutter(int p_gutter);
+		void move_gutters(int p_from_line, int p_to_line);
+
+		void set_line_gutter_metadata(int p_line, int p_gutter, const Variant &p_metadata) { text.write[p_line].gutters.write[p_gutter].metadata = p_metadata; }
+		const Variant &get_line_gutter_metadata(int p_line, int p_gutter) const { return text[p_line].gutters[p_gutter].metadata; }
+
+		void set_line_gutter_text(int p_line, int p_gutter, const String &p_text) { text.write[p_line].gutters.write[p_gutter].text = p_text; }
+		const String &get_line_gutter_text(int p_line, int p_gutter) const { return text[p_line].gutters[p_gutter].text; }
+
+		void set_line_gutter_icon(int p_line, int p_gutter, Ref<Texture2D> p_icon) { text.write[p_line].gutters.write[p_gutter].icon = p_icon; }
+		const Ref<Texture2D> &get_line_gutter_icon(int p_line, int p_gutter) const { return text[p_line].gutters[p_gutter].icon; }
+
+		void set_line_gutter_item_color(int p_line, int p_gutter, const Color &p_color) { text.write[p_line].gutters.write[p_gutter].color = p_color; }
+		const Color &get_line_gutter_item_color(int p_line, int p_gutter) const { return text[p_line].gutters[p_gutter].color; }
+
+		void set_line_gutter_clickable(int p_line, int p_gutter, bool p_clickable) { text.write[p_line].gutters.write[p_gutter].clickable = p_clickable; }
+		bool is_line_gutter_clickable(int p_line, int p_gutter) const { return text[p_line].gutters[p_gutter].clickable; }
 	};
 
-private:
 	struct Cursor {
 		int last_fit_x;
 		int line, column; ///< cursor
@@ -494,9 +548,51 @@ protected:
 	static void _bind_methods();
 
 public:
+	/* Syntax Highlighting. */
 	Ref<SyntaxHighlighter> get_syntax_highlighter();
 	void set_syntax_highlighter(Ref<SyntaxHighlighter> p_syntax_highlighter);
 
+	/* Gutters. */
+	void add_gutter(int p_at = -1);
+	void remove_gutter(int p_gutter);
+	int get_gutter_count() const;
+
+	void set_gutter_name(int p_gutter, const String &p_name);
+	String get_gutter_name(int p_gutter) const;
+
+	void set_gutter_type(int p_gutter, GutterType p_type);
+	GutterType get_gutter_type(int p_gutter) const;
+
+	void set_gutter_width(int p_gutter, int p_width);
+	int get_gutter_width(int p_gutter) const;
+
+	void set_gutter_draw(int p_gutter, bool p_draw);
+	bool is_gutter_drawn(int p_gutter) const;
+
+	void set_gutter_clickable(int p_gutter, bool p_clickable);
+	bool is_gutter_clickable(int p_gutter) const;
+
+	void set_gutter_overwritable(int p_gutter, bool p_overwritable);
+	bool is_gutter_overwritable(int p_gutter) const;
+
+	void set_gutter_custom_draw(int p_gutter, Object *p_object, const StringName &p_callback);
+
+	// Line gutters.
+	void set_line_gutter_metadata(int p_line, int p_gutter, const Variant &p_metadata);
+	Variant get_line_gutter_metadata(int p_line, int p_gutter) const;
+
+	void set_line_gutter_text(int p_line, int p_gutter, const String &p_text);
+	String get_line_gutter_text(int p_line, int p_gutter) const;
+
+	void set_line_gutter_icon(int p_line, int p_gutter, Ref<Texture2D> p_icon);
+	Ref<Texture2D> get_line_gutter_icon(int p_line, int p_gutter) const;
+
+	void set_line_gutter_item_color(int p_line, int p_gutter, const Color &p_color);
+	Color get_line_gutter_item_color(int p_line, int p_gutter);
+
+	void set_line_gutter_clickable(int p_line, int p_gutter, bool p_clickable);
+	bool is_line_gutter_clickable(int p_line, int p_gutter) const;
+
 	enum MenuItems {
 		MENU_CUT,
 		MENU_COPY,
@@ -764,6 +860,7 @@ public:
 	~TextEdit();
 };
 
+VARIANT_ENUM_CAST(TextEdit::GutterType);
 VARIANT_ENUM_CAST(TextEdit::MenuItems);
 VARIANT_ENUM_CAST(TextEdit::SearchFlags);