esc

Externally Scriptable Editor

git clone git://mccd.space/esc

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 }