esc
Externally Scriptable Editor
git clone git://mccd.space/esc
| Log | Files | Refs | README |
editor.c (32508B)
1 #include "editor.h"
2 #include "strbuf.h"
3 #include <SDL3/SDL_stdinc.h>
4 #include <ctype.h>
5 #include <stdio.h>
6 #include <stdlib.h>
7 #include <string.h>
8
9 /* Forward declarations for file slot helpers (defined after lifecycle code) */
10 static void slots_on_insert(Editor *ed, int pos, int len);
11 static void slots_on_delete(Editor *ed, int pos, int len);
12
13 /* ---------- undo helpers (called with lock held) ----------------------- */
14
15 static void push_undo_delta(Editor *ed, UndoDelta d) {
16 /* Any new edit clears the redo stack. */
17 for (int i = 0; i < ed->redo_len; i++)
18 free(ed->redo_stack[i].reinsert);
19 ed->redo_len = 0;
20
21 /* Drop oldest entry if full. */
22 if (ed->undo_len == UNDO_MAX) {
23 free(ed->undo_stack[0].reinsert);
24 memmove(ed->undo_stack, ed->undo_stack + 1,
25 (UNDO_MAX - 1) * sizeof(UndoDelta));
26 ed->undo_len--;
27 }
28 ed->undo_stack[ed->undo_len++] = d;
29 }
30
31 /* Raw delete: no delta recorded, no lock acquired. */
32 static void delete_range_raw(Editor *ed, int start, int end) {
33 if (start == end)
34 return;
35 if (start > end) {
36 int tmp = start;
37 start = end;
38 end = tmp;
39 }
40 int len = end - start;
41 size_t new_count = 0;
42 for (size_t i = 0; i < ed->ranges_count; i++) {
43 EditorRange r = ed->ranges[i];
44 if (r.start_byte >= start && r.end_byte <= end)
45 continue;
46 if (r.start_byte >= end) {
47 r.start_byte -= len;
48 r.end_byte -= len;
49 } else {
50 if (r.start_byte > start && r.start_byte < end)
51 r.start_byte = start;
52 if (r.end_byte > start && r.end_byte < end)
53 r.end_byte = start;
54 if (start > r.start_byte && end < r.end_byte)
55 r.end_byte -= len;
56 }
57 if (r.end_byte > r.start_byte)
58 ed->ranges[new_count++] = r;
59 }
60 ed->ranges_count = new_count;
61 slots_on_delete(ed, start, len);
62 strbuf_delete(&ed->text, start, len);
63 ed->cursor_idx = start;
64 ed->selection_anchor = start;
65 }
66
67 /*
68 * Apply a delta: delete [pos, pos+delete_len) then insert reinsert at pos.
69 * Populate *rev with the inverse delta (for pushing onto the opposite stack).
70 * Called with lock held; does not record new undo entries.
71 */
72 static void apply_delta(Editor *ed, const UndoDelta *d, UndoDelta *rev) {
73 /* Capture bytes that will be deleted, for the reverse. */
74 char *captured = NULL;
75 if (d->delete_len > 0) {
76 captured = malloc(d->delete_len + 1);
77 if (captured) {
78 memcpy(captured, ed->text.data + d->pos, d->delete_len);
79 captured[d->delete_len] = '\0';
80 }
81 }
82
83 rev->pos = d->pos;
84 rev->reinsert = captured;
85 rev->reinsert_len = d->delete_len;
86 rev->delete_len = d->reinsert_len;
87 rev->cursor_before = ed->cursor_idx;
88 rev->anchor_before = ed->selection_anchor;
89
90 /* Apply: delete first, then insert. */
91 if (d->delete_len > 0)
92 delete_range_raw(ed, d->pos, d->pos + (int)d->delete_len);
93
94 if (d->reinsert_len > 0) {
95 /* Shift ranges for the insertion. */
96 for (size_t i = 0; i < ed->ranges_count; i++) {
97 if (ed->ranges[i].start_byte >= d->pos) {
98 ed->ranges[i].start_byte += (int)d->reinsert_len;
99 ed->ranges[i].end_byte += (int)d->reinsert_len;
100 } else if (d->pos > ed->ranges[i].start_byte &&
101 d->pos < ed->ranges[i].end_byte) {
102 ed->ranges[i].end_byte += (int)d->reinsert_len;
103 }
104 }
105 strbuf_insert(&ed->text, d->pos, d->reinsert, d->reinsert_len);
106 }
107
108 /* Restore cursor, clamped to buffer size. */
109 int tlen = (int)ed->text.len;
110 ed->cursor_idx = d->cursor_before > tlen ? tlen : d->cursor_before;
111 ed->selection_anchor =
112 d->anchor_before > tlen ? tlen : d->anchor_before;
113 }
114
115 /* ---------- lifecycle -------------------------------------------------- */
116
117 Editor *editor_create(int char_width, float line_height) {
118 Editor *ed = malloc(sizeof(Editor));
119 if (!ed)
120 return NULL;
121
122 strbuf_init(&ed->text, 1024);
123 ed->cursor_idx = 0;
124 ed->selection_anchor = 0;
125
126 ed->mutex = SDL_CreateMutex();
127 if (!ed->mutex) {
128 free(ed);
129 return NULL;
130 }
131
132 // Initialize ranges to zero/NULL
133 ed->ranges = NULL;
134 ed->ranges_count = 0;
135 ed->ranges_capacity = 0;
136
137 ed->files = NULL;
138 ed->files_count = 0;
139 ed->files_cap = 0;
140
141 ed->undo_stack = malloc(UNDO_MAX * sizeof(UndoDelta));
142 ed->undo_len = 0;
143 ed->redo_stack = malloc(UNDO_MAX * sizeof(UndoDelta));
144 ed->redo_len = 0;
145
146 ed->char_width = char_width;
147 ed->line_height = line_height;
148 return ed;
149 }
150
151 void editor_destroy(Editor *ed) {
152 for (int i = 0; i < ed->files_count; i++)
153 free(ed->files[i].path);
154 free(ed->files);
155 strbuf_free(&ed->text);
156 if (ed->mutex)
157 SDL_DestroyMutex(ed->mutex);
158 for (int i = 0; i < ed->undo_len; i++)
159 free(ed->undo_stack[i].reinsert);
160 free(ed->undo_stack);
161 for (int i = 0; i < ed->redo_len; i++)
162 free(ed->redo_stack[i].reinsert);
163 free(ed->redo_stack);
164 free(ed->ranges);
165 free(ed);
166 }
167
168 void editor_lock(Editor *ed) {
169 if (ed && ed->mutex)
170 SDL_LockMutex(ed->mutex);
171 }
172
173 void editor_unlock(Editor *ed) {
174 if (ed && ed->mutex)
175 SDL_UnlockMutex(ed->mutex);
176 }
177
178 void editor_clear_ranges(Editor *ed) {
179 editor_lock(ed);
180 ed->ranges_count = 0;
181 editor_unlock(ed);
182 }
183
184 /* ---------- file slot helpers ------------------------------------------ */
185
186 static void files_grow(Editor *ed) {
187 if (ed->files_count >= ed->files_cap) {
188 int new_cap = ed->files_cap == 0 ? 4 : ed->files_cap * 2;
189 ed->files = realloc(ed->files, new_cap * sizeof(FileSlot));
190 ed->files_cap = new_cap;
191 }
192 }
193
194 /* Shift all slot boundaries after a text insertion at pos. */
195 static void slots_on_insert(Editor *ed, int pos, int len) {
196 for (int i = 0; i < ed->files_count; i++) {
197 FileSlot *s = &ed->files[i];
198 if (s->buf_start <= pos && pos <= s->buf_end) {
199 /* Cursor is inside this slot */
200 s->buf_end += len;
201 if (s->has_range)
202 s->range_end += len;
203 s->dirty = true;
204 } else if (s->buf_start > pos) {
205 s->buf_start += len;
206 s->buf_end += len;
207 }
208 }
209 }
210
211 /* Shift all slot boundaries after a text deletion of [pos, pos+len). */
212 static void slots_on_delete(Editor *ed, int pos, int len) {
213 int del_end = pos + len;
214 for (int i = 0; i < ed->files_count; i++) {
215 FileSlot *s = &ed->files[i];
216 if (s->buf_end <= pos) {
217 /* slot entirely before deletion: no change */
218 } else if (s->buf_start >= del_end) {
219 /* slot entirely after deletion */
220 s->buf_start -= len;
221 s->buf_end -= len;
222 } else {
223 /* overlapping */
224 int overlap_start = s->buf_start > pos ? s->buf_start : pos;
225 int overlap_end = s->buf_end < del_end ? s->buf_end : del_end;
226 int removed = overlap_end - overlap_start;
227 s->buf_end -= removed;
228 if (s->has_range)
229 s->range_end -= removed;
230 if (s->buf_start > pos)
231 s->buf_start = pos;
232 s->dirty = true;
233 }
234 }
235 }
236
237 int editor_file_slot_at(Editor *ed, int byte_pos) {
238 for (int i = 0; i < ed->files_count; i++) {
239 if (byte_pos >= ed->files[i].buf_start &&
240 byte_pos <= ed->files[i].buf_end)
241 return i;
242 }
243 return ed->files_count - 1;
244 }
245
246 bool editor_open_file(Editor *ed, const char *filename) {
247 editor_lock(ed);
248
249 /* Read file into a temporary strbuf */
250 StrBuf tmp;
251 strbuf_init(&tmp, 1024);
252 if (!strbuf_read_file(&tmp, filename)) {
253 strbuf_free(&tmp);
254 editor_unlock(ed);
255 return false;
256 }
257
258 int insert_pos = (int)ed->text.len;
259 /* Append a newline separator between slots (except for the first) */
260 if (ed->files_count > 0 && insert_pos > 0 &&
261 ed->text.data[insert_pos - 1] != '\n') {
262 strbuf_insert(&ed->text, insert_pos, "\n", 1);
263 slots_on_insert(ed, insert_pos, 1);
264 insert_pos++;
265 }
266
267 files_grow(ed);
268 FileSlot *s = &ed->files[ed->files_count];
269 s->path = filename ? strdup(filename) : NULL;
270 s->buf_start = insert_pos;
271 s->dirty = false;
272 s->has_range = false;
273 s->range_start = 0;
274 s->range_end = 0;
275
276 strbuf_insert(&ed->text, insert_pos, tmp.data, tmp.len);
277 s->buf_end = insert_pos + (int)tmp.len;
278 ed->files_count++;
279
280 strbuf_free(&tmp);
281 editor_unlock(ed);
282 return true;
283 }
284
285 bool editor_add_file_slot(Editor *ed, const char *path, const char *data, int len) {
286 editor_lock(ed);
287
288 int insert_pos = (int)ed->text.len;
289 if (ed->files_count > 0 && insert_pos > 0 &&
290 ed->text.data[insert_pos - 1] != '\n') {
291 strbuf_insert(&ed->text, insert_pos, "\n", 1);
292 slots_on_insert(ed, insert_pos, 1);
293 insert_pos++;
294 }
295
296 files_grow(ed);
297 FileSlot *s = &ed->files[ed->files_count];
298 s->path = path ? strdup(path) : NULL;
299 s->buf_start = insert_pos;
300 s->dirty = false;
301 s->has_range = false;
302 s->range_start = 0;
303 s->range_end = 0;
304
305 if (data && len > 0)
306 strbuf_insert(&ed->text, insert_pos, data, len);
307 s->buf_end = insert_pos + (len > 0 ? len : 0);
308 ed->files_count++;
309
310 editor_unlock(ed);
311 return true;
312 }
313
314 void editor_set_file_slot_range(Editor *ed, int idx, int start, int end) {
315 editor_lock(ed);
316 if (idx >= 0 && idx < ed->files_count) {
317 ed->files[idx].has_range = true;
318 ed->files[idx].range_start = start;
319 ed->files[idx].range_end = end;
320 }
321 editor_unlock(ed);
322 }
323
324 bool editor_write_file(Editor *ed, const char *filename) {
325 editor_lock(ed);
326
327 bool any = false;
328 if (filename) {
329 /* Write full buffer to explicit path (legacy / single-file) */
330 bool res = strbuf_write_file(&ed->text, filename);
331 editor_unlock(ed);
332 return res;
333 }
334
335 /* Save all dirty slots */
336 for (int i = 0; i < ed->files_count; i++) {
337 FileSlot *s = &ed->files[i];
338 if (!s->dirty || !s->path)
339 continue;
340 int slot_len = s->buf_end - s->buf_start;
341 /* Write only the slot's bytes */
342 StrBuf tmp;
343 strbuf_init(&tmp, slot_len + 1);
344 strbuf_insert(&tmp, 0, ed->text.data + s->buf_start, slot_len);
345 if (strbuf_write_file(&tmp, s->path)) {
346 s->dirty = false;
347 any = true;
348 }
349 strbuf_free(&tmp);
350 }
351
352 editor_unlock(ed);
353 return any;
354 }
355 static int utf8_char_to_byte_idx(const char *text, int char_idx) {
356
357 int byte_pos = 0;
358 int current_char = 0;
359 while (text[byte_pos] != '\0' && current_char < char_idx) {
360 // In UTF-8, leading bytes are not of the form 10xxxxxx (0x80)
361 if ((text[byte_pos] & 0xC0) != 0x80) {
362 current_char++;
363 }
364 byte_pos++;
365 // Skip over the rest of the multi-byte character
366 while (text[byte_pos] != '\0' &&
367 (text[byte_pos] & 0xC0) == 0x80) {
368 byte_pos++;
369 }
370 }
371 return byte_pos;
372 }
373 void editor_add_formatting_range(Editor *ed, int start_char, int end_char,
374 RangeFormat data) {
375
376 editor_lock(ed);
377
378 int start = utf8_char_to_byte_idx(ed->text.data, start_char);
379 int end = utf8_char_to_byte_idx(ed->text.data, end_char);
380
381 if (ed->ranges_count >= ed->ranges_capacity) {
382 ed->ranges_capacity =
383 ed->ranges_capacity == 0 ? 16 : ed->ranges_capacity * 2;
384 ed->ranges = realloc(ed->ranges,
385 ed->ranges_capacity * sizeof(EditorRange));
386 }
387
388 EditorRange r = {
389 .start_byte = start, .end_byte = end, .type = RANGE_FORMAT};
390 r.data.format = data;
391
392 // Insert in sorted order
393 size_t i = ed->ranges_count;
394 while (i > 0 && ed->ranges[i - 1].start_byte > start) {
395 ed->ranges[i] = ed->ranges[i - 1];
396 i--;
397 }
398 ed->ranges[i] = r;
399 ed->ranges_count++;
400
401 editor_unlock(ed);
402 }
403
404 void editor_add_replace_range(Editor *ed, int start_char, int end_char,
405 int visual_cols) {
406
407 editor_lock(ed);
408 int start = utf8_char_to_byte_idx(ed->text.data, start_char);
409 int end = utf8_char_to_byte_idx(ed->text.data, end_char);
410
411 if (ed->ranges_count >= ed->ranges_capacity) {
412 ed->ranges_capacity =
413 ed->ranges_capacity == 0 ? 16 : ed->ranges_capacity * 2;
414 ed->ranges = realloc(ed->ranges,
415 ed->ranges_capacity * sizeof(EditorRange));
416 }
417 EditorRange r = {start, end, RANGE_REPLACEMENT,
418 .data.replacement = visual_cols};
419 // Insert in sorted order to allow binary search
420 size_t i = ed->ranges_count;
421 while (i > 0 && ed->ranges[i - 1].start_byte > start) {
422 ed->ranges[i] = ed->ranges[i - 1];
423 i--;
424 }
425 ed->ranges[i] = r;
426 ed->ranges_count++;
427
428 editor_unlock(ed);
429 }
430
431 EditorRange *editor_get_range_at(Editor *ed, int byte_idx) {
432 int left = 0;
433 int right = (int)ed->ranges_count - 1;
434
435 while (left <= right) {
436 int mid = left + (right - left) / 2;
437 EditorRange *r = &ed->ranges[mid];
438
439 if (byte_idx >= r->start_byte && byte_idx < r->end_byte) {
440 return r;
441 }
442
443 if (byte_idx < r->start_byte) {
444 right = mid - 1;
445 } else {
446 left = mid + 1;
447 }
448 }
449 return NULL;
450 }
451
452 EditorRange *editor_get_range_ending_at(Editor *ed, int byte_idx) {
453 int left = 0;
454 int right = (int)ed->ranges_count - 1;
455
456 while (left <= right) {
457 int mid = left + (right - left) / 2;
458 EditorRange *r = &ed->ranges[mid];
459
460 if (byte_idx == r->end_byte) {
461 return r;
462 }
463
464 if (byte_idx <= r->start_byte) {
465 right = mid - 1;
466 } else {
467 left = mid + 1;
468 }
469 }
470 return NULL;
471 }
472
473 void editor_goto_pos(Editor *ed, int pos) {
474
475 editor_lock(ed);
476 if (pos > ed->text.len) {
477 ed->cursor_idx = (int)ed->text.len;
478 } else {
479 ed->cursor_idx = pos;
480 }
481 editor_unlock(ed);
482 }
483
484 void editor_select_all(Editor *ed) {
485
486 editor_lock(ed);
487 ed->selection_anchor = 0;
488 editor_goto_pos(ed, ed->text.len);
489
490 editor_unlock(ed);
491 }
492
493 void editor_insert_text(Editor *ed, const char *text, bool replace) {
494
495 editor_lock(ed);
496
497 /* Capture pre-edit cursor state for the undo delta. */
498 int cursor_before = ed->cursor_idx;
499 int anchor_before = ed->selection_anchor;
500
501 /* If replacing a selection, compute the insertion point and save the
502 * displaced bytes so the combined undo delta can restore them. */
503 int ins_pos = ed->cursor_idx;
504 char *saved = NULL;
505 size_t saved_len = 0;
506
507 if (replace && ed->cursor_idx != ed->selection_anchor) {
508 int sel_start = ed->cursor_idx < ed->selection_anchor
509 ? ed->cursor_idx
510 : ed->selection_anchor;
511 int sel_end = ed->cursor_idx > ed->selection_anchor
512 ? ed->cursor_idx
513 : ed->selection_anchor;
514 ins_pos = sel_start;
515 saved_len = (size_t)(sel_end - sel_start);
516 saved = malloc(saved_len + 1);
517 if (saved) {
518 memcpy(saved, ed->text.data + sel_start, saved_len);
519 saved[saved_len] = '\0';
520 }
521 delete_range_raw(ed, sel_start, sel_end);
522 /* delete_range_raw sets cursor_idx = ins_pos. */
523 }
524
525 size_t input_len = strlen(text);
526
527 /* One combined undo delta for the whole replace-and-insert. */
528 UndoDelta d = {
529 .pos = ins_pos,
530 .reinsert = saved,
531 .reinsert_len = saved_len,
532 .delete_len = input_len,
533 .cursor_before = cursor_before,
534 .anchor_before = anchor_before,
535 };
536 push_undo_delta(ed, d);
537
538 // Shift ranges affected by insertion
539 for (size_t i = 0; i < ed->ranges_count; i++) {
540 if (ed->ranges[i].start_byte >= ed->cursor_idx) {
541 ed->ranges[i].start_byte += input_len;
542 ed->ranges[i].end_byte += input_len;
543 } else if (ed->cursor_idx > ed->ranges[i].start_byte &&
544 ed->cursor_idx < ed->ranges[i].end_byte) {
545 ed->ranges[i].end_byte += input_len;
546 }
547 }
548
549 // Shift file slot boundaries
550 slots_on_insert(ed, ed->cursor_idx, (int)input_len);
551
552 editor_unlock(ed);
553
554 strbuf_insert(&ed->text, ed->cursor_idx, text, input_len);
555 ed->cursor_idx += (int)input_len;
556 ed->selection_anchor = ed->cursor_idx;
557 }
558
559 void editor_newline(Editor *ed) { editor_insert_text(ed, "\n", true); }
560
561 void editor_delete_range(Editor *ed, int start, int end) {
562
563 editor_lock(ed);
564 if (start == end) {
565 editor_unlock(ed);
566 return;
567 }
568 if (start > end) {
569 int tmp = start;
570 start = end;
571 end = tmp;
572 }
573
574 /* Capture displaced bytes and cursor state for undo. */
575 int len = end - start;
576 char *saved = malloc(len + 1);
577 if (saved) {
578 memcpy(saved, ed->text.data + start, len);
579 saved[len] = '\0';
580 }
581 UndoDelta d = {
582 .pos = start,
583 .reinsert = saved,
584 .reinsert_len = (size_t)len,
585 .delete_len = 0,
586 .cursor_before = ed->cursor_idx,
587 .anchor_before = ed->selection_anchor,
588 };
589 push_undo_delta(ed, d);
590
591 delete_range_raw(ed, start, end);
592
593 editor_unlock(ed);
594 }
595
596 void editor_delete_back(Editor *ed) {
597
598 editor_lock(ed);
599 if (ed->cursor_idx != ed->selection_anchor) {
600 editor_delete_range(ed, ed->selection_anchor, ed->cursor_idx);
601 } else if (ed->cursor_idx > 0) {
602 /* Hard boundary: cannot backspace across a file slot boundary */
603 for (int i = 1; i < ed->files_count; i++) {
604 if (ed->cursor_idx == ed->files[i].buf_start) {
605 editor_unlock(ed);
606 return;
607 }
608 }
609 int prev = ed->cursor_idx;
610 do {
611 prev--;
612 } while (prev > 0 && (ed->text.data[prev] & 0xC0) == 0x80);
613 editor_delete_range(ed, prev, ed->cursor_idx);
614 }
615
616 editor_unlock(ed);
617 }
618
619 void editor_delete_forward(Editor *ed) {
620
621 editor_lock(ed);
622 if (ed->cursor_idx != ed->selection_anchor) {
623 editor_delete_range(ed, ed->selection_anchor, ed->cursor_idx);
624 } else if (ed->cursor_idx < (int)ed->text.len) {
625 /* Hard boundary: cannot delete-forward across a file slot boundary */
626 for (int i = 0; i < ed->files_count - 1; i++) {
627 if (ed->cursor_idx == ed->files[i].buf_end) {
628 editor_unlock(ed);
629 return;
630 }
631 }
632 const char *ptr = ed->text.data + ed->cursor_idx;
633 const char *next = ptr;
634 SDL_StepUTF8(&next, NULL);
635 editor_delete_range(ed, ed->cursor_idx,
636 (int)(next - ed->text.data));
637 }
638
639 editor_unlock(ed);
640 }
641
642 EditorRange *editor_get_replacement_at(Editor *ed, int byte_idx) {
643
644 for (size_t i = 0; i < ed->ranges_count; i++) {
645 EditorRange *r = &ed->ranges[i];
646 if (r->type == RANGE_REPLACEMENT && byte_idx >= r->start_byte &&
647 byte_idx < r->end_byte) {
648 return r;
649 }
650 }
651 return NULL;
652 }
653
654 RangeFormat editor_get_format_at(Editor *ed, int byte_idx) {
655 RangeFormat fmt = {.bold = false, .italic = false};
656 for (size_t i = 0; i < ed->ranges_count; i++) {
657 EditorRange *r = &ed->ranges[i];
658 if (r->type == RANGE_FORMAT && byte_idx >= r->start_byte &&
659 byte_idx < r->end_byte) {
660 if (r->data.format.bold)
661 fmt.bold = true;
662 if (r->data.format.italic)
663 fmt.italic = true;
664 }
665 }
666 return fmt;
667 }
668
669 void editor_cursor_left(Editor *ed) {
670
671 editor_lock(ed);
672 if (ed->cursor_idx > 0) {
673 // Are we standing at the end of a replacement?
674 // We need to look backwards to see if the previous byte is
675 // inside a replacement.
676 EditorRange *r =
677 editor_get_replacement_at(ed, ed->cursor_idx - 1);
678 if (r && ed->cursor_idx == r->end_byte) {
679 ed->cursor_idx = r->start_byte; // Jump back over it
680 } else {
681 const char *ptr = ed->text.data;
682 const char *cur = ptr + ed->cursor_idx;
683 SDL_StepBackUTF8(ptr, &cur);
684 ed->cursor_idx = (int)(cur - ptr);
685 }
686 }
687
688 editor_unlock(ed);
689 }
690
691 void editor_cursor_right(Editor *ed) {
692
693 editor_lock(ed);
694 if (ed->cursor_idx < (int)ed->text.len) {
695 // Are we standing at the start of a replacement?
696 EditorRange *r = editor_get_replacement_at(ed, ed->cursor_idx);
697 if (r && ed->cursor_idx == r->start_byte) {
698 ed->cursor_idx = r->end_byte; // Jump over it
699 } else {
700 const char *ptr = ed->text.data + ed->cursor_idx;
701 SDL_StepUTF8(&ptr, NULL);
702 ed->cursor_idx = (int)(ptr - ed->text.data);
703 }
704 }
705 editor_unlock(ed);
706 }
707
708 void editor_clear_selection(Editor *ed) {
709
710 editor_lock(ed);
711 ed->selection_anchor = ed->cursor_idx;
712
713 editor_unlock(ed);
714 }
715
716 void editor_set_selection(Editor *ed, int anchor, int cursor) {
717 editor_lock(ed);
718 int tlen = (int)ed->text.len;
719 ed->selection_anchor = anchor > tlen ? tlen : anchor;
720 ed->cursor_idx = cursor > tlen ? tlen : cursor;
721 editor_unlock(ed);
722 }
723
724 /* Return start of line containing pos (byte offset). */
725 static int find_sol(const char *data, int pos) {
726 while (pos > 0 && data[pos - 1] != '\n')
727 pos--;
728 return pos;
729 }
730
731 /* True if the line at byte pos is blank (whitespace-only or just \n). */
732 static bool line_is_blank(const char *data, int len, int pos) {
733 pos = find_sol(data, pos);
734 while (pos < len && data[pos] != '\n') {
735 if (data[pos] != ' ' && data[pos] != '\t' && data[pos] != '\r')
736 return false;
737 pos++;
738 }
739 return true;
740 }
741
742 void editor_expand_selection(Editor *ed, int level) {
743 if (level == 0) {
744 editor_select_word(ed);
745 return;
746 }
747 editor_lock(ed);
748 const char *data = ed->text.data;
749 int len = (int)ed->text.len;
750 int lo = ed->selection_anchor < ed->cursor_idx
751 ? ed->selection_anchor : ed->cursor_idx;
752 int hi = ed->selection_anchor > ed->cursor_idx
753 ? ed->selection_anchor : ed->cursor_idx;
754
755 if (level == 1) {
756 /* Backward to paragraph start */
757 {
758 int p = find_sol(data, lo);
759 while (p > 0) {
760 int prev_start = find_sol(data, p - 1);
761 if (line_is_blank(data, len, prev_start))
762 break;
763 p = prev_start;
764 }
765 lo = p;
766 }
767 /* Forward: to [.?!] + space/\n, or paragraph boundary */
768 bool found = false;
769 for (int p = hi; p < len; p++) {
770 if (data[p] == '\n') {
771 int next = p + 1;
772 if (next >= len || line_is_blank(data, len, next)) {
773 hi = p;
774 found = true;
775 break;
776 }
777 }
778 if ((data[p] == '.' || data[p] == '?' || data[p] == '!') &&
779 (p + 1 >= len || data[p + 1] == ' ' || data[p + 1] == '\n')) {
780 hi = p + 1;
781 found = true;
782 break;
783 }
784 }
785 if (!found)
786 hi = len;
787 } else {
788 /* level >= 2: paragraph — blank-line boundaries */
789
790 /* Backward: walk lines upward until blank line or start */
791 {
792 int p = find_sol(data, lo);
793 while (p > 0) {
794 int prev_start = find_sol(data, p - 1);
795 if (line_is_blank(data, len, prev_start))
796 break;
797 p = prev_start;
798 }
799 lo = p;
800 }
801
802 /* Forward: walk lines downward until blank line or end */
803 {
804 int p = hi;
805 while (p < len) {
806 while (p < len && data[p] != '\n')
807 p++;
808 if (p >= len) {
809 hi = len;
810 break;
811 }
812 int next = p + 1;
813 if (next >= len) {
814 hi = len;
815 break;
816 }
817 if (line_is_blank(data, len, next)) {
818 hi = next; /* blank line's \n */
819 break;
820 }
821 p = next;
822 }
823 if (p >= len)
824 hi = len;
825 }
826
827 /* level >= 3: extend by one more paragraph per extra level */
828 for (int extra = 0; extra < level - 2; extra++) {
829 /* Extend lo backward past blank separator + prev paragraph */
830 if (lo > 0) {
831 int q = find_sol(data, lo - 1);
832 /* skip blank lines above */
833 while (q > 0 && line_is_blank(data, len, q))
834 q = find_sol(data, q - 1);
835 /* walk back through the non-blank paragraph above */
836 while (q > 0) {
837 int prev = find_sol(data, q - 1);
838 if (line_is_blank(data, len, prev))
839 break;
840 q = prev;
841 }
842 lo = q;
843 }
844 /* Extend hi forward past blank separator + next paragraph */
845 if (hi < len) {
846 int q = hi + 1;
847 /* skip blank lines below */
848 while (q < len && line_is_blank(data, len, q)) {
849 while (q < len && data[q] != '\n')
850 q++;
851 if (q < len) q++;
852 }
853 /* walk forward through the non-blank paragraph below */
854 while (q < len) {
855 while (q < len && data[q] != '\n')
856 q++;
857 if (q >= len) {
858 hi = len;
859 break;
860 }
861 int next = q + 1;
862 if (next >= len) {
863 hi = len;
864 break;
865 }
866 if (line_is_blank(data, len, next)) {
867 hi = next;
868 break;
869 }
870 q = next;
871 }
872 if (q >= len)
873 hi = len;
874 }
875 }
876 }
877
878 ed->selection_anchor = lo;
879 ed->cursor_idx = hi;
880 editor_unlock(ed);
881 }
882
883 void editor_cursor_up(Editor *ed) {
884
885 editor_lock(ed);
886 int line_start = ed->cursor_idx;
887 while (line_start > 0 && ed->text.data[line_start - 1] != '\n')
888 line_start--;
889
890 int visual_col = 0;
891 const char *p = ed->text.data + line_start;
892
893 while (p < ed->text.data + ed->cursor_idx) {
894 if (*p == '\t') {
895 visual_col += TAB_SIZE - (visual_col % TAB_SIZE);
896 SDL_StepUTF8(&p, NULL);
897 } else {
898 visual_col++;
899 SDL_StepUTF8(&p, NULL);
900 }
901 }
902
903 if (line_start > 0) {
904 int prev_line_end = line_start - 1;
905 int prev_line_start = prev_line_end;
906 while (prev_line_start > 0 &&
907 ed->text.data[prev_line_start - 1] != '\n')
908 prev_line_start--;
909
910 const char *ptr = ed->text.data + prev_line_start;
911 for (int i = 0; i < visual_col && *ptr != '\n' && *ptr != '\0';
912 i++)
913 SDL_StepUTF8(&ptr, NULL);
914 ed->cursor_idx = (int)(ptr - ed->text.data);
915 }
916
917 editor_unlock(ed);
918 }
919
920 void editor_cursor_down(Editor *ed) {
921
922 editor_lock(ed);
923 int line_start = ed->cursor_idx;
924 while (line_start > 0 && ed->text.data[line_start - 1] != '\n')
925 line_start--;
926
927 int visual_col = 0;
928 const char *p = ed->text.data + line_start;
929 while (p < ed->text.data + ed->cursor_idx) {
930 if (*p == '\t') {
931 visual_col += TAB_SIZE - (visual_col % TAB_SIZE);
932 SDL_StepUTF8(&p, NULL);
933 } else {
934 SDL_StepUTF8(&p, NULL);
935 visual_col++;
936 }
937 }
938
939 const char *next_line = strchr(ed->text.data + ed->cursor_idx, '\n');
940 if (next_line) {
941 int target_idx = (int)(next_line - ed->text.data) + 1;
942 const char *ptr = ed->text.data + target_idx;
943 for (int i = 0; i < visual_col; i++) {
944 if (*ptr == '\n' || *ptr == '\0')
945 break;
946 SDL_StepUTF8(&ptr, NULL);
947 }
948 ed->cursor_idx = (int)(ptr - ed->text.data);
949 }
950
951 editor_unlock(ed);
952 }
953
954 void editor_set_cursor_from_coords(Editor *ed, float mx, float my,
955 float scroll_x, float scroll_y) {
956
957 editor_lock(ed);
958 int target_col =
959 (int)((mx - 20.0f + scroll_x + (ed->char_width / 2.0f)) /
960 ed->char_width);
961 int target_row = (int)((my - 20.0f + scroll_y) / ed->line_height);
962
963 int cur_r = 0, cur_c = 0;
964 const char *ptr = ed->text.data;
965 const char *last_ptr = ed->text.data;
966
967 while (*ptr != '\0') {
968 int current_idx = (int)(ptr - ed->text.data);
969 if (cur_r == target_row && cur_c >= target_col)
970 break;
971
972 last_ptr = ptr;
973
974 EditorRange *r = editor_get_range_at(ed, current_idx);
975 if (r && r->type == RANGE_REPLACEMENT) {
976 cur_c += r->data.replacement.visual_cols;
977 ptr = ed->text.data + r->end_byte; // Skip pointer ahead
978 continue;
979 }
980
981 // --- Standard UTF-8 / Tab logic ---
982 Uint32 cp = SDL_StepUTF8(&ptr, NULL);
983 if (cp == '\t') {
984 cur_c += TAB_SIZE - (cur_c % TAB_SIZE);
985 } else if (cp == '\n') {
986 if (cur_r == target_row) {
987 ptr = last_ptr;
988 break;
989 }
990 cur_r++;
991 cur_c = 0;
992 } else {
993 cur_c++;
994 }
995
996 if (cur_r > target_row) {
997 ptr = last_ptr;
998 break;
999 }
1000 }
1001 ed->cursor_idx = (int)(ptr - ed->text.data);
1002
1003 editor_unlock(ed);
1004 }
1005
1006 static bool is_word_char(char c) {
1007 if (c == '\0')
1008 return false;
1009 // For plumbing, we select everything except standard whitespace
1010 return !isspace((unsigned char)c);
1011 }
1012
1013 void editor_goto_word_start(Editor *ed) {
1014
1015 editor_lock(ed);
1016 if (ed->text.len == 0)
1017 return;
1018
1019 int start = ed->cursor_idx;
1020 if (start > 0 && !is_word_char(ed->text.data[start])) {
1021 if (is_word_char(ed->text.data[start - 1])) {
1022 start--;
1023 }
1024 }
1025 while (start > 0 && is_word_char(ed->text.data[start - 1])) {
1026 start--;
1027 }
1028 ed->cursor_idx = start;
1029
1030 editor_unlock(ed);
1031 }
1032
1033 void editor_select_word(Editor *ed) {
1034
1035 editor_lock(ed);
1036 if (ed->text.len == 0)
1037 return;
1038
1039 int start = ed->cursor_idx;
1040 int end = ed->cursor_idx;
1041
1042 if (start > 0 && !is_word_char(ed->text.data[start])) {
1043 if (is_word_char(ed->text.data[start - 1])) {
1044 start--;
1045 end--;
1046 }
1047 }
1048
1049 // Only proceed if we are actually on a word character
1050 if (!is_word_char(ed->text.data[start]))
1051 return;
1052
1053 // Expand left
1054 while (start > 0 && is_word_char(ed->text.data[start - 1])) {
1055 start--;
1056 }
1057
1058 // Expand right
1059 while (end < (int)ed->text.len && is_word_char(ed->text.data[end])) {
1060 end++;
1061 }
1062
1063 ed->selection_anchor = start;
1064 ed->cursor_idx = end;
1065
1066 editor_unlock(ed);
1067 }
1068
1069 bool editor_has_selection(Editor *ed) {
1070 return ed->selection_anchor != ed->cursor_idx;
1071 }
1072
1073 VisualPos editor_byte_to_visual_pos(const Editor *ed, int byte_idx) {
1074 VisualPos pos = {0, 0};
1075 const char *ptr = ed->text.data;
1076 while (ptr < ed->text.data + byte_idx) {
1077 int current_idx = (int)(ptr - ed->text.data);
1078 EditorRange *r =
1079 editor_get_replacement_at((Editor *)ed, current_idx);
1080 if (r) {
1081 pos.col += r->data.replacement.visual_cols;
1082 ptr = ed->text.data + r->end_byte;
1083 continue;
1084 }
1085 Uint32 cp = SDL_StepUTF8(&ptr, NULL);
1086 if (cp == '\t') {
1087 pos.col += TAB_SIZE - (pos.col % TAB_SIZE);
1088 } else if (cp == '\n') {
1089 pos.row++;
1090 pos.col = 0;
1091 } else {
1092 pos.col++;
1093 }
1094 }
1095 return pos;
1096 }
1097
1098 int editor_visual_pos_to_byte(const Editor *ed, VisualPos target) {
1099 int row = 0, col = 0;
1100 const char *ptr = ed->text.data;
1101 const char *end = ed->text.data + ed->text.len;
1102
1103 while (ptr < end) {
1104 if (row == target.row && col >= target.col)
1105 return (int)(ptr - ed->text.data);
1106
1107 int current_idx = (int)(ptr - ed->text.data);
1108 EditorRange *r =
1109 editor_get_replacement_at((Editor *)ed, current_idx);
1110 if (r) {
1111 col += r->data.replacement.visual_cols;
1112 ptr = ed->text.data + r->end_byte;
1113 continue;
1114 }
1115
1116 const char *prev = ptr;
1117 Uint32 cp = SDL_StepUTF8(&ptr, NULL);
1118 if (cp == '\n') {
1119 if (row == target.row)
1120 return (int)(prev - ed->text.data);
1121 row++;
1122 col = 0;
1123 } else if (cp == '\t') {
1124 col += TAB_SIZE - (col % TAB_SIZE);
1125 } else {
1126 col++;
1127 }
1128 }
1129 return (int)(end - ed->text.data);
1130 }
1131
1132 char *editor_get_selection(Editor *ed) {
1133 if (ed->cursor_idx == ed->selection_anchor)
1134 return NULL;
1135
1136 int start = (ed->cursor_idx < ed->selection_anchor)
1137 ? ed->cursor_idx
1138 : ed->selection_anchor;
1139 int end = (ed->cursor_idx > ed->selection_anchor)
1140 ? ed->cursor_idx
1141 : ed->selection_anchor;
1142 int len = end - start;
1143
1144 char *result = malloc(len + 1);
1145 if (!result)
1146 return NULL;
1147
1148 memcpy(result, &ed->text.data[start], len);
1149 result[len] = '\0';
1150 return result;
1151 }
1152
1153 static int byte_to_utf8_char_idx(const char *text, int byte_limit) {
1154 int char_idx = 0;
1155 for (int i = 0; i < byte_limit && text[i] != '\0'; i++) {
1156 if ((text[i] & 0xC0) != 0x80)
1157 char_idx++;
1158 }
1159 return char_idx;
1160 }
1161
1162 void editor_undo(Editor *ed) {
1163 editor_lock(ed);
1164 if (ed->undo_len == 0) {
1165 editor_unlock(ed);
1166 return;
1167 }
1168 UndoDelta d = ed->undo_stack[--ed->undo_len];
1169 UndoDelta rev = {0};
1170 apply_delta(ed, &d, &rev);
1171 free(d.reinsert);
1172 if (ed->redo_len < UNDO_MAX)
1173 ed->redo_stack[ed->redo_len++] = rev;
1174 else
1175 free(rev.reinsert);
1176 editor_unlock(ed);
1177 }
1178
1179 void editor_redo(Editor *ed) {
1180 editor_lock(ed);
1181 if (ed->redo_len == 0) {
1182 editor_unlock(ed);
1183 return;
1184 }
1185 UndoDelta d = ed->redo_stack[--ed->redo_len];
1186 UndoDelta rev = {0};
1187 apply_delta(ed, &d, &rev);
1188 free(d.reinsert);
1189 if (ed->undo_len < UNDO_MAX)
1190 ed->undo_stack[ed->undo_len++] = rev;
1191 else
1192 free(rev.reinsert);
1193 editor_unlock(ed);
1194 }
1195
1196 void editor_replace_body(Editor *ed, const char *data, size_t len) {
1197 editor_lock(ed);
1198
1199 size_t old_len = ed->text.len;
1200 char *saved = malloc(old_len + 1);
1201 if (saved) {
1202 memcpy(saved, ed->text.data, old_len);
1203 saved[old_len] = '\0';
1204 }
1205 UndoDelta d = {
1206 .pos = 0,
1207 .reinsert = saved,
1208 .reinsert_len = old_len,
1209 .delete_len = len,
1210 .cursor_before = ed->cursor_idx,
1211 .anchor_before = ed->selection_anchor,
1212 };
1213 push_undo_delta(ed, d);
1214
1215 ed->ranges_count = 0;
1216 strbuf_delete(&ed->text, 0, ed->text.len);
1217 strbuf_insert(&ed->text, 0, data ? data : "", len);
1218 ed->cursor_idx = 0;
1219 ed->selection_anchor = 0;
1220
1221 editor_unlock(ed);
1222 editor_parse_ansi_codes(ed);
1223 }
1224
1225 void editor_parse_ansi_codes(Editor *ed) {
1226 editor_lock(ed);
1227 ed->ranges_count = 0;
1228
1229 const char *data = ed->text.data;
1230 size_t len = ed->text.len;
1231
1232 RangeFormat current_fmt = {.bold = false, .italic = false};
1233 int last_segment_start_byte = 0;
1234
1235 for (int i = 0; i < (int)len; i++) {
1236 if (data[i] == '\x1b' && i + 1 < len && data[i + 1] == '[') {
1237
1238 if (i > last_segment_start_byte) {
1239 if (current_fmt.bold || current_fmt.italic) {
1240 // We temporarily unlock because
1241 // editor_add_formatting_range locks
1242 editor_unlock(ed);
1243 editor_add_formatting_range(
1244 ed,
1245 byte_to_utf8_char_idx(
1246 data, last_segment_start_byte),
1247 byte_to_utf8_char_idx(data, i),
1248 current_fmt);
1249 editor_lock(ed);
1250 // Re-evaluate data ptr in case buffer
1251 // moved (unlikely here but good
1252 // practice)
1253 data = ed->text.data;
1254 }
1255 }
1256
1257 int seq_start = i;
1258 int seq_end = i + 2;
1259 while (seq_end < len && data[seq_end] != 'm')
1260 seq_end++;
1261
1262 if (seq_end < len && data[seq_end] == 'm') {
1263 for (int p = seq_start + 2; p < seq_end;) {
1264 int val = atoi(&data[p]);
1265 if (val == 0) {
1266 current_fmt.bold = false;
1267 current_fmt.italic = false;
1268 } else if (val == 1) {
1269 current_fmt.bold = true;
1270 } else if (val == 3) {
1271 current_fmt.italic = true;
1272 } else if (val == 22) {
1273 current_fmt.bold = false;
1274 } else if (val == 23) {
1275 current_fmt.italic = false;
1276 }
1277
1278 while (p < seq_end && (data[p] >= '0' &&
1279 data[p] <= '9'))
1280 p++;
1281 if (p < seq_end && data[p] == ';')
1282 p++;
1283 }
1284
1285 editor_unlock(ed);
1286 editor_add_replace_range(
1287 ed, byte_to_utf8_char_idx(data, seq_start),
1288 byte_to_utf8_char_idx(data, seq_end + 1),
1289 0);
1290 editor_lock(ed);
1291 data = ed->text.data;
1292
1293 i = seq_end;
1294 last_segment_start_byte = i + 1;
1295 }
1296 }
1297 }
1298
1299 if (last_segment_start_byte < len) {
1300 if (current_fmt.bold || current_fmt.italic) {
1301 editor_unlock(ed);
1302 editor_add_formatting_range(
1303 ed,
1304 byte_to_utf8_char_idx(data,
1305 last_segment_start_byte),
1306 byte_to_utf8_char_idx(data, len), current_fmt);
1307 editor_lock(ed);
1308 }
1309 }
1310 editor_unlock(ed);
1311 }