esc

Externally Scriptable Editor

git clone git://mccd.space/esc

commit bc7caf758198f2df4a8b542b73c7734a78f2a12d
parent c393ff3dc995bb5cdcba74d5c777a2af17b10a69
Author: Marc Coquand <marc@coquand.email>
Date:   Fri, 20 Feb 2026 12:23:17 +0100

*

Diffstat:
MREADME.md | 22++++++++++++++++++----
Meditor.c | 205+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Meditor.h | 37++++++++++++++++++++++++-------------
Mmain.c | 2+-
4 files changed, 185 insertions(+), 81 deletions(-)
diff --git a/README.md b/README.md
@@ -1,7 +1,6 @@
-# ⎋ esc
-
-Esc (**E**xternally **Sc**riptable ) is an extensible editor written in C.
+# ⎋ Esc
 
+Esc (**E**xternally **Sc**riptable Editor) is an extensible editor written in C.
 ## Dependencies
 
 - SDL3
@@ -16,9 +15,24 @@ cc main.c unix_utils.* editor.* -o esc $(pkg-config --cflags --libs sdl3 sdl3-tt
 
 ## Inspiration
 
-`esc` is heavily inspired by
+Esc is heavily inspired by
 
 * Emacs
 * Kakoune
 * Acme
 
+## Why use this over...
+
+### Emacs
+
+Emacs is also an extensible text editor that uses Lisp as it's main language. They both feature very customizable buffers, however they take slightly different approaches.
+
+Emacs is self-contained, and thus is more portable. Esc tries to be an *integrated* editor, and as a result tries to work better with existing CLI tools and more.
+
+### Kakoune
+
+Kakoune used to be my main editor. It is great.
+
+However, because it is intended to run in a terminal, it has quite a few limitations. It will probably never render images for example.
+
+Esc also is mouse friendlier.
diff --git a/editor.c b/editor.c
@@ -1,16 +1,25 @@
 #include "editor.h"
+#include <ctype.h>
 #include <stdlib.h>
 #include <string.h>
-#include <ctype.h>
 
 Editor *editor_create(int char_width, float line_height) {
 	Editor *ed = malloc(sizeof(Editor));
+	if (!ed)
+		return NULL;
+
 	ed->capacity = 1024;
 	ed->buffer = malloc(ed->capacity);
 	ed->buffer[0] = '\0';
 	ed->length = 0;
 	ed->cursor_idx = 0;
 	ed->selection_anchor = 0;
+
+	// Initialize ranges to zero/NULL
+	ed->ranges = NULL;
+	ed->ranges_count = 0;
+	ed->ranges_capacity = 0;
+
 	ed->char_width = char_width;
 	ed->line_height = line_height;
 	return ed;
@@ -21,31 +30,75 @@ void editor_destroy(Editor *ed) {
 	free(ed);
 }
 
+void editor_clear_ranges(Editor *ed) { ed->ranges_count = 0; }
+
+void editor_add_range(Editor *ed, int start, int end, int visual_cols) {
+	if (ed->ranges_count >= ed->ranges_capacity) {
+		ed->ranges_capacity =
+		    ed->ranges_capacity == 0 ? 16 : ed->ranges_capacity * 2;
+		ed->ranges = realloc(ed->ranges,
+				     ed->ranges_capacity * sizeof(EditorRange));
+	}
+	EditorRange r = {start, end, visual_cols};
+	ed->ranges[ed->ranges_count++] = r;
+}
+
+EditorRange *editor_get_range_at(Editor *ed, int byte_idx) {
+	// XXX Use binary search
+	for (size_t i = 0; i < ed->ranges_count; i++) {
+		if (byte_idx >= ed->ranges[i].start_byte &&
+		    byte_idx < ed->ranges[i].end_byte) {
+			return &ed->ranges[i];
+		}
+	}
+	return NULL;
+}
+
+EditorRange *editor_get_range_ending_at(Editor *ed, int byte_idx) {
+	for (size_t i = 0; i < ed->ranges_count; i++) {
+		if (byte_idx == ed->ranges[i].end_byte) {
+			return &ed->ranges[i];
+		}
+	}
+	return NULL;
+}
+
 void editor_goto_pos(Editor *ed, int pos) {
-    if (pos > ed->length) {
-        ed->cursor_idx = (int)ed->length;
-    } else {
-        ed->cursor_idx = pos;
-    }
+	if (pos > ed->length) {
+		ed->cursor_idx = (int)ed->length;
+	} else {
+		ed->cursor_idx = pos;
+	}
 }
 
 void editor_select_all(Editor *ed) {
-    ed->selection_anchor = 0;
-    editor_goto_pos(ed,ed->length); 
+	ed->selection_anchor = 0;
+	editor_goto_pos(ed, ed->length);
 }
 
 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);
+	size_t required_size = ed->length + input_len + 1;
+
+	if (required_size > ed->capacity) {
+		// Keep doubling until we can fit the new text
+		while (required_size > ed->capacity) {
+			ed->capacity *= 2;
+		}
+		char *new_buf = realloc(ed->buffer, ed->capacity);
+		if (!new_buf)
+			return; // Handle OOM appropriately
+		ed->buffer = new_buf;
 	}
+
 	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;
@@ -145,6 +198,7 @@ void editor_cursor_up(Editor *ed) {
 		ed->cursor_idx = (int)(ptr - ed->buffer);
 	}
 }
+
 void editor_cursor_down(Editor *ed) {
 	int line_start = ed->cursor_idx;
 	while (line_start > 0 && ed->buffer[line_start - 1] != '\n')
@@ -174,6 +228,7 @@ void editor_cursor_down(Editor *ed) {
 		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 =
@@ -186,9 +241,21 @@ void editor_set_cursor_from_coords(Editor *ed, float mx, float my,
 	const char *last_ptr = ed->buffer;
 
 	while (*ptr != '\0') {
-		if (cur_r == target_row && cur_c == target_col)
+		int current_idx = (int)(ptr - ed->buffer);
+		if (cur_r == target_row && cur_c >= target_col)
 			break;
+
 		last_ptr = ptr;
+
+		// --- NEW: Check for ranges first ---
+		EditorRange *r = editor_get_range_at(ed, current_idx);
+		if (r) {
+			cur_c += r->visual_cols;
+			ptr = ed->buffer + r->end_byte; // Skip pointer ahead
+			continue;
+		}
+
+		// --- Standard UTF-8 / Tab logic ---
 		Uint32 cp = SDL_StepUTF8(&ptr, NULL);
 		if (cp == '\t') {
 			cur_c += TAB_SIZE - (cur_c % TAB_SIZE);
@@ -199,8 +266,10 @@ void editor_set_cursor_from_coords(Editor *ed, float mx, float my,
 			}
 			cur_r++;
 			cur_c = 0;
-		} else
+		} else {
 			cur_c++;
+		}
+
 		if (cur_r > target_row) {
 			ptr = last_ptr;
 			break;
@@ -210,71 +279,81 @@ void editor_set_cursor_from_coords(Editor *ed, float mx, float my,
 }
 
 static bool is_word_char(char c) {
-    if (c == '\0') return false;
-    // For plumbing, we select everything except standard whitespace
-    return !isspace((unsigned char)c);
+	if (c == '\0')
+		return false;
+	// For plumbing, we select everything except standard whitespace
+	return !isspace((unsigned char)c);
 }
 
 void editor_goto_word_start(Editor *ed) {
-    if (ed->length == 0) return;
-
-    int start = ed->cursor_idx;
-    if (start > 0 && !is_word_char(ed->buffer[start])) {
-        if (is_word_char(ed->buffer[start - 1])) {
-            start--;
-        }
-    }
-    while (start > 0 && is_word_char(ed->buffer[start - 1])) {
-        start--;
-    }
-    ed->cursor_idx = start;
+	if (ed->length == 0)
+		return;
+
+	int start = ed->cursor_idx;
+	if (start > 0 && !is_word_char(ed->buffer[start])) {
+		if (is_word_char(ed->buffer[start - 1])) {
+			start--;
+		}
+	}
+	while (start > 0 && is_word_char(ed->buffer[start - 1])) {
+		start--;
+	}
+	ed->cursor_idx = start;
 }
 
 void editor_select_word(Editor *ed) {
-    if (ed->length == 0) return;
+	if (ed->length == 0)
+		return;
 
-    int start = ed->cursor_idx;
-    int end = ed->cursor_idx;
+	int start = ed->cursor_idx;
+	int end = ed->cursor_idx;
 
-    if (start > 0 && !is_word_char(ed->buffer[start])) {
-        if (is_word_char(ed->buffer[start - 1])) {
-            start--;
-            end--;
-        }
-    }
+	if (start > 0 && !is_word_char(ed->buffer[start])) {
+		if (is_word_char(ed->buffer[start - 1])) {
+			start--;
+			end--;
+		}
+	}
 
-    // Only proceed if we are actually on a word character
-    if (!is_word_char(ed->buffer[start])) return;
+	// Only proceed if we are actually on a word character
+	if (!is_word_char(ed->buffer[start]))
+		return;
 
-    // Expand left
-    while (start > 0 && is_word_char(ed->buffer[start - 1])) {
-        start--;
-    }
+	// Expand left
+	while (start > 0 && is_word_char(ed->buffer[start - 1])) {
+		start--;
+	}
 
-    // Expand right
-    while (end < (int)ed->length && is_word_char(ed->buffer[end])) {
-        end++;
-    }
+	// Expand right
+	while (end < (int)ed->length && is_word_char(ed->buffer[end])) {
+		end++;
+	}
 
-    ed->selection_anchor = start;
-    ed->cursor_idx = end;
+	ed->selection_anchor = start;
+	ed->cursor_idx = end;
 }
 
 bool editor_has_selection(Editor *ed) {
-    return ed->selection_anchor != ed->cursor_idx;
+	return ed->selection_anchor != ed->cursor_idx;
 }
 
-char* editor_get_selection(Editor *ed) {
-    if (ed->cursor_idx == ed->selection_anchor) return NULL;
-
-    int start = (ed->cursor_idx < ed->selection_anchor) ? ed->cursor_idx : ed->selection_anchor;
-    int end = (ed->cursor_idx > ed->selection_anchor) ? ed->cursor_idx : ed->selection_anchor;
-    int len = end - start;
-
-    char *result = malloc(len + 1);
-    if (!result) return NULL;
-    
-    memcpy(result, &ed->buffer[start], len);
-    result[len] = '\0';
-    return result;
+char *editor_get_selection(Editor *ed) {
+	if (ed->cursor_idx == ed->selection_anchor)
+		return NULL;
+
+	int start = (ed->cursor_idx < ed->selection_anchor)
+			? ed->cursor_idx
+			: ed->selection_anchor;
+	int end = (ed->cursor_idx > ed->selection_anchor)
+		      ? ed->cursor_idx
+		      : ed->selection_anchor;
+	int len = end - start;
+
+	char *result = malloc(len + 1);
+	if (!result)
+		return NULL;
+
+	memcpy(result, &ed->buffer[start], len);
+	result[len] = '\0';
+	return result;
 }
diff --git a/editor.h b/editor.h
@@ -6,23 +6,33 @@
 #include <stdbool.h>
 
 typedef struct {
-    char *buffer;
-    size_t capacity;
-    size_t length;
-    int cursor_idx;
-    int selection_anchor; // Where the selection started
-    
-    // Metrics for coordinate calculations
-    int char_width;
-    float line_height;
+	int start_byte;
+	int end_byte;
+	int visual_cols; // 0 for invisible, N for replacements
+} EditorRange;
+
+typedef struct {
+	char *buffer;
+	size_t capacity;
+	size_t length;
+	int cursor_idx;
+	int selection_anchor; // Where the selection started
+
+	EditorRange *ranges;
+	size_t ranges_count;
+	size_t ranges_capacity;
+
+	// Metrics for coordinate calculations
+	int char_width;
+	float line_height;
 } Editor;
 
 // Lifecycle
-Editor* editor_create(int char_width, float line_height);
+Editor *editor_create(int char_width, float line_height);
 void editor_destroy(Editor *ed);
 
 // Core Actions
-void editor_insert_text(Editor *ed, const char *text,  bool replace);
+void editor_insert_text(Editor *ed, const char *text, bool replace);
 void editor_newline(Editor *ed);
 void editor_delete_back(Editor *ed);
 void editor_delete_forward(Editor *ed);
@@ -36,13 +46,14 @@ void editor_cursor_up(Editor *ed);
 void editor_cursor_down(Editor *ed);
 
 // Selection & Coordination
-void editor_set_cursor_from_coords(Editor *ed, float mx, float my, float scroll_x, float scroll_y);
+void editor_set_cursor_from_coords(Editor *ed, float mx, float my,
+				   float scroll_x, float scroll_y);
 void editor_set_selection(Editor *ed, int anchor, int cursor);
 void editor_clear_selection(Editor *ed);
 void editor_select_all(Editor *ed);
 void editor_goto_pos(Editor *ed, int pos);
 
-char* editor_get_selection(Editor *ed);
+char *editor_get_selection(Editor *ed);
 void editor_select_word(Editor *ed);
 bool editor_has_selection(Editor *ed);
 
diff --git a/main.c b/main.c
@@ -28,7 +28,7 @@ int main(int argc, char *argv[]) {
 	SDL_SetRenderLogicalPresentation(renderer, 800, 600,
 					 SDL_LOGICAL_PRESENTATION_DISABLED);
 
-	TTF_Font *font = TTF_OpenFont("Go-Mono.ttf", 24.0f);
+	TTF_Font *font = TTF_OpenFont("IosevkaTermSS13-Extended.ttf", 24.0f);
 	int char_width;
 	TTF_GetGlyphMetrics(font, 'A', NULL, NULL, NULL, NULL, &char_width);
 	float line_height = (float)TTF_GetFontHeight(font)*1.5;