esc

Externally Scriptable Editor

git clone git://mccd.space/esc

commit 1eb764779f5ba844ea4f62079651c5c43b4a92aa
parent fd4536628f126a57db6a451a12feb265047a1371
Author: Marc Coquand <marc@coquand.email>
Date:   Thu, 19 Feb 2026 15:48:05 +0100

Support highlighting properly

Diffstat:
Meditor.c | 281++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Meditor.h | 1+
Mmain.c | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
3 files changed, 230 insertions(+), 135 deletions(-)
diff --git a/editor.c b/editor.c
@@ -2,149 +2,194 @@
 #include <stdlib.h>
 #include <string.h>
 
-Editor* editor_create(int char_width, float line_height) {
-    Editor *ed = malloc(sizeof(Editor));
-    ed->capacity = 1024;
-    ed->buffer = malloc(ed->capacity);
-    ed->buffer[0] = '\0';
-    ed->length = 0;
-    ed->cursor_idx = 0;
-    ed->selection_anchor = 0;
-    ed->char_width = char_width;
-    ed->line_height = line_height;
-    return ed;
+Editor *editor_create(int char_width, float line_height) {
+	Editor *ed = malloc(sizeof(Editor));
+	ed->capacity = 1024;
+	ed->buffer = malloc(ed->capacity);
+	ed->buffer[0] = '\0';
+	ed->length = 0;
+	ed->cursor_idx = 0;
+	ed->selection_anchor = 0;
+	ed->char_width = char_width;
+	ed->line_height = line_height;
+	return ed;
 }
 
 void editor_destroy(Editor *ed) {
-    free(ed->buffer);
-    free(ed);
+	free(ed->buffer);
+	free(ed);
 }
 
-void editor_insert_text(Editor *ed, const char *text,bool replace) {
-    if (replace && ed->cursor_idx != ed->selection_anchor) {
-        editor_delete_range(ed, ed->cursor_idx, ed->selection_anchor) ;
-    }
-    size_t input_len = strlen(text);
-    if (ed->length + input_len + 1 > ed->capacity) {
-        ed->capacity *= 2;
-        ed->buffer = realloc(ed->buffer, ed->capacity);
-    }
-    memmove(&ed->buffer[ed->cursor_idx + input_len], &ed->buffer[ed->cursor_idx], ed->length - ed->cursor_idx + 1);
-    memcpy(&ed->buffer[ed->cursor_idx], text, input_len);
-    ed->length += input_len;
-    ed->cursor_idx += (int)input_len;
-    ed->selection_anchor = ed->cursor_idx;
+void editor_insert_text(Editor *ed, const char *text, bool replace) {
+	if (replace && ed->cursor_idx != ed->selection_anchor) {
+		editor_delete_range(ed, ed->cursor_idx, ed->selection_anchor);
+	}
+	size_t input_len = strlen(text);
+	if (ed->length + input_len + 1 > ed->capacity) {
+		ed->capacity *= 2;
+		ed->buffer = realloc(ed->buffer, ed->capacity);
+	}
+	memmove(&ed->buffer[ed->cursor_idx + input_len],
+		&ed->buffer[ed->cursor_idx], ed->length - ed->cursor_idx + 1);
+	memcpy(&ed->buffer[ed->cursor_idx], text, input_len);
+	ed->length += input_len;
+	ed->cursor_idx += (int)input_len;
+	ed->selection_anchor = ed->cursor_idx;
 }
 
-void editor_newline(Editor *ed) {
-    editor_insert_text(ed, "\n", true);
-}
+void editor_newline(Editor *ed) { editor_insert_text(ed, "\n", true); }
 
 void editor_delete_range(Editor *ed, int start, int end) {
-    if (start == end) return;
-    if (start > end) { int tmp = start; start = end; end = tmp; }
-    
-    memmove(&ed->buffer[start], &ed->buffer[end], ed->length - end + 1);
-    ed->length -= (end - start);
-    ed->cursor_idx = start;
-    ed->selection_anchor = start;
+	if (start == end)
+		return;
+	if (start > end) {
+		int tmp = start;
+		start = end;
+		end = tmp;
+	}
+
+	memmove(&ed->buffer[start], &ed->buffer[end], ed->length - end + 1);
+	ed->length -= (end - start);
+	ed->cursor_idx = start;
+	ed->selection_anchor = start;
 }
 
 void editor_delete_back(Editor *ed) {
-    if (ed->cursor_idx != ed->selection_anchor) {
-        editor_delete_range(ed, ed->selection_anchor, ed->cursor_idx);
-    } else if (ed->cursor_idx > 0) {
-        int prev = ed->cursor_idx;
-        do { prev--; } while (prev > 0 && (ed->buffer[prev] & 0xC0) == 0x80);
-        editor_delete_range(ed, prev, ed->cursor_idx);
-    }
+	if (ed->cursor_idx != ed->selection_anchor) {
+		editor_delete_range(ed, ed->selection_anchor, ed->cursor_idx);
+	} else if (ed->cursor_idx > 0) {
+		int prev = ed->cursor_idx;
+		do {
+			prev--;
+		} while (prev > 0 && (ed->buffer[prev] & 0xC0) == 0x80);
+		editor_delete_range(ed, prev, ed->cursor_idx);
+	}
 }
 
 void editor_delete_forward(Editor *ed) {
-    if (ed->cursor_idx != ed->selection_anchor) {
-        editor_delete_range(ed, ed->selection_anchor, ed->cursor_idx);
-    } else if (ed->cursor_idx < (int)ed->length) {
-        const char *ptr = ed->buffer + ed->cursor_idx;
-        const char *next = ptr;
-        SDL_StepUTF8(&next, NULL);
-        editor_delete_range(ed, ed->cursor_idx, (int)(next - ed->buffer));
-    }
+	if (ed->cursor_idx != ed->selection_anchor) {
+		editor_delete_range(ed, ed->selection_anchor, ed->cursor_idx);
+	} else if (ed->cursor_idx < (int)ed->length) {
+		const char *ptr = ed->buffer + ed->cursor_idx;
+		const char *next = ptr;
+		SDL_StepUTF8(&next, NULL);
+		editor_delete_range(ed, ed->cursor_idx,
+				    (int)(next - ed->buffer));
+	}
 }
 
 void editor_cursor_left(Editor *ed) {
-    if (ed->cursor_idx > 0) {
-        ed->cursor_idx--;
-        while (ed->cursor_idx > 0 && (ed->buffer[ed->cursor_idx] & 0xC0) == 0x80) ed->cursor_idx--;
-    }
-    ed->selection_anchor = ed->cursor_idx;
+	if (ed->cursor_idx > 0) {
+		ed->cursor_idx--;
+		while (ed->cursor_idx > 0 &&
+		       (ed->buffer[ed->cursor_idx] & 0xC0) == 0x80)
+			ed->cursor_idx--;
+	}
+	ed->selection_anchor = ed->cursor_idx;
 }
 
 void editor_cursor_right(Editor *ed) {
-    if (ed->cursor_idx < (int)ed->length) {
-        const char *next = ed->buffer + ed->cursor_idx;
-        SDL_StepUTF8(&next, NULL);
-        ed->cursor_idx = (int)(next - ed->buffer);
-    }
-    ed->selection_anchor = ed->cursor_idx;
+	if (ed->cursor_idx < (int)ed->length) {
+		const char *next = ed->buffer + ed->cursor_idx;
+		SDL_StepUTF8(&next, NULL);
+		ed->cursor_idx = (int)(next - ed->buffer);
+	}
+	ed->selection_anchor = ed->cursor_idx;
 }
 
 void editor_cursor_up(Editor *ed) {
-    int line_start = ed->cursor_idx;
-    while (line_start > 0 && ed->buffer[line_start - 1] != '\n') line_start--;
-    
-    int visual_col = 0;
-    const char *p = ed->buffer + line_start;
-    while (p < ed->buffer + ed->cursor_idx) { SDL_StepUTF8(&p, NULL); visual_col++; }
-
-    if (line_start > 0) {
-        int prev_line_end = line_start - 1;
-        int prev_line_start = prev_line_end;
-        while (prev_line_start > 0 && ed->buffer[prev_line_start - 1] != '\n') prev_line_start--;
-        
-        const char *ptr = ed->buffer + prev_line_start;
-        for (int i = 0; i < visual_col && *ptr != '\n' && *ptr != '\0'; i++) SDL_StepUTF8(&ptr, NULL);
-        ed->cursor_idx = (int)(ptr - ed->buffer);
-    }
-    ed->selection_anchor = ed->cursor_idx;
+	int line_start = ed->cursor_idx;
+	while (line_start > 0 && ed->buffer[line_start - 1] != '\n')
+		line_start--;
+
+	int visual_col = 0;
+	const char *p = ed->buffer + line_start;
+	while (p < ed->buffer + ed->cursor_idx) {
+		if (*p == '\t') {
+			visual_col += TAB_SIZE - (visual_col % TAB_SIZE);
+			p++;
+		} else {
+			SDL_StepUTF8(&p, NULL);
+			visual_col++;
+		}
+	}
+
+	if (line_start > 0) {
+		int prev_line_end = line_start - 1;
+		int prev_line_start = prev_line_end;
+		while (prev_line_start > 0 &&
+		       ed->buffer[prev_line_start - 1] != '\n')
+			prev_line_start--;
+
+		const char *ptr = ed->buffer + prev_line_start;
+		for (int i = 0; i < visual_col && *ptr != '\n' && *ptr != '\0';
+		     i++)
+			SDL_StepUTF8(&ptr, NULL);
+		ed->cursor_idx = (int)(ptr - ed->buffer);
+	}
+	ed->selection_anchor = ed->cursor_idx;
 }
 void editor_cursor_down(Editor *ed) {
-    int line_start = ed->cursor_idx;
-    while (line_start > 0 && ed->buffer[line_start - 1] != '\n') line_start--;
-    
-    int visual_col = 0;
-    const char *p = ed->buffer + line_start;
-    while (p < ed->buffer + ed->cursor_idx) { SDL_StepUTF8(&p, NULL); visual_col++; }
-
-    const char *next_line = strchr(ed->buffer + ed->cursor_idx, '\n');
-    if (next_line) {
-        int target_idx = (int)(next_line - ed->buffer) + 1;
-        const char *ptr = ed->buffer + target_idx; 
-        for(int i=0; i < visual_col; i++) {
-            if (*ptr == '\n' || *ptr == '\0') break;
-            SDL_StepUTF8(&ptr, NULL);
-        }
-        ed->cursor_idx = (int)(ptr - ed->buffer);
-    }
-
-    ed->selection_anchor = ed->cursor_idx;
+	int line_start = ed->cursor_idx;
+	while (line_start > 0 && ed->buffer[line_start - 1] != '\n')
+		line_start--;
+
+	int visual_col = 0;
+	const char *p = ed->buffer + line_start;
+	while (p < ed->buffer + ed->cursor_idx) {
+		if (*p == '\t') {
+			visual_col += TAB_SIZE - (visual_col % TAB_SIZE);
+		} else {
+			SDL_StepUTF8(&p, NULL);
+			visual_col++;
+		}
+	}
+
+	const char *next_line = strchr(ed->buffer + ed->cursor_idx, '\n');
+	if (next_line) {
+		int target_idx = (int)(next_line - ed->buffer) + 1;
+		const char *ptr = ed->buffer + target_idx;
+		for (int i = 0; i < visual_col; i++) {
+			if (*ptr == '\n' || *ptr == '\0')
+				break;
+			SDL_StepUTF8(&ptr, NULL);
+		}
+		ed->cursor_idx = (int)(ptr - ed->buffer);
+	}
+
+	ed->selection_anchor = ed->cursor_idx;
 }
-void editor_set_cursor_from_coords(Editor *ed, float mx, float my, float scroll_x, float scroll_y) {
-    int target_col = (int)((mx - 20.0f + scroll_x + (ed->char_width / 2.0f)) / ed->char_width);
-    int target_row = (int)((my - 20.0f + scroll_y) / ed->line_height);
-
-    int cur_r = 0, cur_c = 0;
-    const char *ptr = ed->buffer;
-    const char *last_ptr = ed->buffer;
-
-    while (*ptr != '\0') {
-        if (cur_r == target_row && cur_c == target_col) break;
-        last_ptr = ptr;
-        Uint32 cp = SDL_StepUTF8(&ptr, NULL);
-        if (cp == '\n') {
-            if (cur_r == target_row) { ptr = last_ptr; break; }
-            cur_r++; cur_c = 0;
-        } else cur_c++;
-        if (cur_r > target_row) { ptr = last_ptr; break; }
-    }
-    ed->cursor_idx = (int)(ptr - ed->buffer);
+void editor_set_cursor_from_coords(Editor *ed, float mx, float my,
+				   float scroll_x, float scroll_y) {
+	int target_col =
+	    (int)((mx - 20.0f + scroll_x + (ed->char_width / 2.0f)) /
+		  ed->char_width);
+	int target_row = (int)((my - 20.0f + scroll_y) / ed->line_height);
+
+	int cur_r = 0, cur_c = 0;
+	const char *ptr = ed->buffer;
+	const char *last_ptr = ed->buffer;
+
+	while (*ptr != '\0') {
+		if (cur_r == target_row && cur_c == target_col)
+			break;
+		last_ptr = ptr;
+		Uint32 cp = SDL_StepUTF8(&ptr, NULL);
+		if (cp == '\t') {
+			cur_c += TAB_SIZE - (cur_c % TAB_SIZE);
+		} else if (cp == '\n') {
+			if (cur_r == target_row) {
+				ptr = last_ptr;
+				break;
+			}
+			cur_r++;
+			cur_c = 0;
+		} else
+			cur_c++;
+		if (cur_r > target_row) {
+			ptr = last_ptr;
+			break;
+		}
+	}
+	ed->cursor_idx = (int)(ptr - ed->buffer);
 }
diff --git a/editor.h b/editor.h
@@ -1,5 +1,6 @@
 #ifndef EDITOR_H
 #define EDITOR_H
+#define TAB_SIZE 8
 
 #include <SDL3/SDL.h>
 #include <stdbool.h>
diff --git a/main.c b/main.c
@@ -29,7 +29,9 @@ int get_idx_from_coords(const char *buffer, float mx, float my, float scroll_x,
 		last_ptr = ptr;
 		Uint32 codepoint = SDL_StepUTF8(&ptr, NULL);
 
-		if (codepoint == '\n') {
+		if (codepoint == '\t') {
+			current_col += TAB_SIZE - (current_col % TAB_SIZE);
+		} else if (codepoint == '\n') {
 			if (current_row == target_row) {
 				ptr = last_ptr;
 				break;
@@ -75,7 +77,7 @@ int main(int argc, char *argv[]) {
 
 	SDL_StartTextInput(window);
 	bool is_selecting = false;
-	Editor* ed = editor_create(char_width, line_height);
+	Editor *ed = editor_create(char_width, line_height);
 	ed->cursor_idx = 0;
 
 	while (running) {
@@ -105,6 +107,10 @@ int main(int argc, char *argv[]) {
 					case SDLK_DELETE:
 						editor_delete_forward(ed);
 						break;
+					case SDLK_TAB:
+						editor_insert_text(ed, "\t",
+								   true);
+						break;
 					case SDLK_RETURN:
 						editor_newline(ed);
 						break;
@@ -132,7 +138,7 @@ int main(int argc, char *argv[]) {
 						    ed, mx, my, scroll_x,
 						    scroll_y);
 						ed->selection_anchor =
-						    ed->cursor_idx; 
+						    ed->cursor_idx;
 						is_dragging = true;
 					}
 					break;
@@ -159,7 +165,8 @@ int main(int argc, char *argv[]) {
 					break;
 
 				case SDL_EVENT_TEXT_INPUT:
-					editor_insert_text(ed, event.text.text, true);
+					editor_insert_text(ed, event.text.text,
+							   true);
 					break;
 				}
 			} while (SDL_PollEvent(&event));
@@ -188,6 +195,14 @@ int main(int argc, char *argv[]) {
 			while (p < ed->buffer + ed->length) {
 				int start_of_char_idx = (int)(p - ed->buffer);
 
+				Uint32 cp = SDL_StepUTF8(&p, NULL);
+
+				int cols_for_char = 1;
+				if (cp == '\t') {
+					cols_for_char =
+					    TAB_SIZE - (cur_col % TAB_SIZE);
+				}
+
 				if (start_of_char_idx >= sel_min &&
 				    start_of_char_idx < sel_max) {
 					SDL_FRect sel_rect = {
@@ -195,19 +210,21 @@ int main(int argc, char *argv[]) {
 						scroll_x,
 					    20.0f + (cur_row * line_height) -
 						scroll_y,
-					    (float)char_width, line_height};
+					    (float)(cols_for_char *
+						    char_width), // <-- dynamic
+								 // width!
+					    line_height};
 					SDL_RenderFillRect(renderer, &sel_rect);
 				}
 
-				if (*p == '\n') {
+				if (cp == '\n') {
 					cur_row++;
 					cur_col = 0;
-					p++;
 				} else {
-					SDL_StepUTF8(&p, NULL);
-					cur_col++;
+					cur_col += cols_for_char;
 				}
-				if ((int)(p - ed->buffer) > sel_max)
+
+				if ((int)(p - ed->buffer) >= sel_max)
 					break;
 			}
 		}
@@ -226,9 +243,42 @@ int main(int argc, char *argv[]) {
 
 				if (len > 0 && current_y + line_height > 0 &&
 				    current_y < render_h) {
+
+					// Expand tabs into spaces for correct
+					// rendering alignment
+					char *temp_line =
+					    malloc(len * TAB_SIZE + 1);
+					int t_idx = 0;
+					int vis_col = 0;
+
+					for (size_t i = 0; i < len; i++) {
+						if (line_start[i] == '\t') {
+							int spaces = TAB_SIZE -
+								     (vis_col %
+								      TAB_SIZE);
+							for (int s = 0;
+							     s < spaces; s++) {
+								temp_line
+								    [t_idx++] =
+									' ';
+							}
+							vis_col += spaces;
+						} else {
+							if ((line_start[i] &
+							     0xC0) != 0x80)
+								vis_col++; // Track
+									   // start
+									   // bytes
+							temp_line[t_idx++] =
+							    line_start[i];
+						}
+					}
+					temp_line[t_idx] = '\0';
 					SDL_Surface *surf =
 					    TTF_RenderText_Blended(
-						font, line_start, len, black);
+						font, temp_line, t_idx, black);
+					free(temp_line);
+
 					if (surf) {
 						SDL_Texture *tex =
 						    SDL_CreateTextureFromSurface(
@@ -244,21 +294,20 @@ int main(int argc, char *argv[]) {
 					}
 				}
 				current_y += line_height;
-				line_start = line_end
-						 ? line_end + 1
-						 : NULL; // Move to next line
+				line_start = line_end ? line_end + 1 : NULL;
 			}
 		}
 
 		int cur_row = 0, cur_col = 0;
 		const char *ptr = ed->buffer;
 		while (ptr < ed->buffer + ed->cursor_idx) {
-			if (*ptr == '\n') {
+			Uint32 cp = SDL_StepUTF8(&ptr, NULL);
+			if (cp == '\t') {
+				cur_col += TAB_SIZE - (cur_col % TAB_SIZE);
+			} else if (cp == '\n') {
 				cur_row++;
 				cur_col = 0;
-				ptr++;
 			} else {
-				SDL_StepUTF8(&ptr, NULL);
 				cur_col++;
 			}
 		}