esc
Externally Scriptable Editor
git clone git://mccd.space/esc
| Log | Files | Refs | README |
commit bc7caf758198f2df4a8b542b73c7734a78f2a12d parent c393ff3dc995bb5cdcba74d5c777a2af17b10a69 Author: Marc Coquand <marc@coquand.email> Date: Fri, 20 Feb 2026 12:23:17 +0100 * Diffstat:
| M | README.md | | | 22 | ++++++++++++++++++---- |
| M | editor.c | | | 205 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------ |
| M | editor.h | | | 37 | ++++++++++++++++++++++++------------- |
| M | main.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;