esc
Externally Scriptable Editor
git clone git://mccd.space/esc
| Log | Files | Refs | README |
test_editor.c (15012B)
1 #include "../editor.h"
2 #include "../strbuf.h"
3 #include "test_harness.h"
4 #include <SDL3/SDL.h>
5 #include <stdlib.h>
6 #include <string.h>
7
8 /* ---- suite_create_destroy ----------------------------------------------- */
9
10 static void suite_create_destroy(void) {
11 RUN_SUITE(suite_create_destroy);
12
13 Editor *ed = editor_create(8, 16.0f);
14 ASSERT_NOT_NULL(ed);
15 ASSERT_EQ_INT(ed->cursor_idx, 0);
16 ASSERT_EQ_INT(ed->selection_anchor, 0);
17 ASSERT_EQ_INT((int)ed->text.len, 0);
18 editor_destroy(ed);
19 }
20
21 /* ---- suite_insert_text -------------------------------------------------- */
22
23 static void suite_insert_text(void) {
24 RUN_SUITE(suite_insert_text);
25
26 Editor *ed = editor_create(8, 16.0f);
27
28 editor_insert_text(ed, "hello", false);
29 ASSERT_EQ_INT((int)ed->text.len, 5);
30 ASSERT_EQ_INT(ed->cursor_idx, 5);
31 ASSERT_EQ_STR(ed->text.data, "hello");
32
33 /* inserting a second string appends (cursor is at end) */
34 editor_insert_text(ed, " world", false);
35 ASSERT_EQ_INT((int)ed->text.len, 11);
36 ASSERT_EQ_STR(ed->text.data, "hello world");
37
38 editor_destroy(ed);
39 }
40
41 /* ---- suite_delete_back -------------------------------------------------- */
42
43 static void suite_delete_back(void) {
44 RUN_SUITE(suite_delete_back);
45
46 Editor *ed = editor_create(8, 16.0f);
47 editor_insert_text(ed, "ab", false);
48
49 /* delete 'b' */
50 editor_delete_back(ed);
51 ASSERT_EQ_INT((int)ed->text.len, 1);
52 ASSERT_EQ_STR(ed->text.data, "a");
53 ASSERT_EQ_INT(ed->cursor_idx, 1);
54
55 /* delete 'a' */
56 editor_delete_back(ed);
57 ASSERT_EQ_INT((int)ed->text.len, 0);
58 ASSERT_EQ_INT(ed->cursor_idx, 0);
59
60 /* no-op at pos 0 */
61 editor_delete_back(ed);
62 ASSERT_EQ_INT((int)ed->text.len, 0);
63 ASSERT_EQ_INT(ed->cursor_idx, 0);
64
65 editor_destroy(ed);
66 }
67
68 /* ---- suite_delete_forward ----------------------------------------------- */
69
70 static void suite_delete_forward(void) {
71 RUN_SUITE(suite_delete_forward);
72
73 Editor *ed = editor_create(8, 16.0f);
74 editor_insert_text(ed, "ab", false);
75 editor_goto_pos(ed, 0);
76 editor_clear_selection(ed);
77
78 /* delete 'a' */
79 editor_delete_forward(ed);
80 ASSERT_EQ_INT((int)ed->text.len, 1);
81 ASSERT_EQ_STR(ed->text.data, "b");
82 ASSERT_EQ_INT(ed->cursor_idx, 0);
83
84 /* delete 'b' */
85 editor_delete_forward(ed);
86 ASSERT_EQ_INT((int)ed->text.len, 0);
87
88 /* no-op at end */
89 editor_delete_forward(ed);
90 ASSERT_EQ_INT((int)ed->text.len, 0);
91
92 editor_destroy(ed);
93 }
94
95 /* ---- suite_delete_multibyte --------------------------------------------- */
96
97 static void suite_delete_multibyte(void) {
98 RUN_SUITE(suite_delete_multibyte);
99
100 /* "aé" = 'a'(1) + \xc3\xa9(2) = 3 bytes total */
101 Editor *ed = editor_create(8, 16.0f);
102 editor_insert_text(ed, "a\xc3\xa9", false); /* cursor at byte 3 */
103 ASSERT_EQ_INT(ed->cursor_idx, 3);
104
105 /* delete_back over 2-byte é: cursor should move back 2 bytes */
106 editor_delete_back(ed);
107 ASSERT_EQ_INT(ed->cursor_idx, 1);
108 ASSERT_EQ_INT((int)ed->text.len, 1);
109 ASSERT_EQ_STR(ed->text.data, "a");
110
111 editor_destroy(ed);
112
113 /* delete_forward over single ASCII byte */
114 ed = editor_create(8, 16.0f);
115 editor_insert_text(ed, "ab", false);
116 editor_goto_pos(ed, 0);
117 editor_clear_selection(ed);
118 editor_delete_forward(ed); /* deletes 'a' (1 byte) */
119 ASSERT_EQ_INT(ed->cursor_idx, 0);
120 ASSERT_EQ_INT((int)ed->text.len, 1);
121 ASSERT_EQ_STR(ed->text.data, "b");
122
123 editor_destroy(ed);
124 }
125
126 /* ---- suite_cursor_movement ---------------------------------------------- */
127
128 static void suite_cursor_movement(void) {
129 RUN_SUITE(suite_cursor_movement);
130
131 Editor *ed = editor_create(8, 16.0f);
132 editor_insert_text(ed, "ab\ncd", false); /* bytes: a=0 b=1 \n=2 c=3 d=4 */
133
134 /* start at end (byte 5), move left one by one */
135 ASSERT_EQ_INT(ed->cursor_idx, 5);
136 editor_cursor_left(ed);
137 ASSERT_EQ_INT(ed->cursor_idx, 4);
138 editor_cursor_left(ed);
139 ASSERT_EQ_INT(ed->cursor_idx, 3);
140 editor_cursor_left(ed);
141 ASSERT_EQ_INT(ed->cursor_idx, 2); /* the '\n' */
142 editor_cursor_left(ed);
143 ASSERT_EQ_INT(ed->cursor_idx, 1);
144 editor_cursor_left(ed);
145 ASSERT_EQ_INT(ed->cursor_idx, 0);
146 editor_cursor_left(ed); /* no-op at start */
147 ASSERT_EQ_INT(ed->cursor_idx, 0);
148
149 /* move right back to end */
150 editor_cursor_right(ed);
151 ASSERT_EQ_INT(ed->cursor_idx, 1);
152 editor_cursor_right(ed);
153 ASSERT_EQ_INT(ed->cursor_idx, 2);
154 editor_cursor_right(ed);
155 ASSERT_EQ_INT(ed->cursor_idx, 3);
156 editor_cursor_right(ed);
157 ASSERT_EQ_INT(ed->cursor_idx, 4);
158 editor_cursor_right(ed);
159 ASSERT_EQ_INT(ed->cursor_idx, 5);
160 editor_cursor_right(ed); /* no-op at end */
161 ASSERT_EQ_INT(ed->cursor_idx, 5);
162
163 /* cursor_up from start of second line (byte 3) -> goes to byte 0 */
164 editor_goto_pos(ed, 3);
165 editor_cursor_up(ed);
166 ASSERT_EQ_INT(ed->cursor_idx, 0);
167
168 /* cursor_down from byte 0 -> goes to byte 3 */
169 editor_goto_pos(ed, 0);
170 editor_cursor_down(ed);
171 ASSERT_EQ_INT(ed->cursor_idx, 3);
172
173 editor_destroy(ed);
174 }
175
176 /* ---- suite_select_all --------------------------------------------------- */
177
178 static void suite_select_all(void) {
179 RUN_SUITE(suite_select_all);
180
181 Editor *ed = editor_create(8, 16.0f);
182 editor_insert_text(ed, "hello", false);
183
184 editor_select_all(ed);
185 ASSERT_EQ_INT(ed->selection_anchor, 0);
186 ASSERT_EQ_INT(ed->cursor_idx, 5);
187 ASSERT(editor_has_selection(ed));
188
189 char *sel = editor_get_selection(ed);
190 ASSERT_NOT_NULL(sel);
191 ASSERT_EQ_STR(sel, "hello");
192 free(sel);
193
194 editor_clear_selection(ed);
195 ASSERT(!editor_has_selection(ed));
196 sel = editor_get_selection(ed);
197 ASSERT_NULL(sel);
198
199 editor_destroy(ed);
200 }
201
202 /* ---- suite_replace_body ------------------------------------------------- */
203
204 static void suite_replace_body(void) {
205 RUN_SUITE(suite_replace_body);
206
207 Editor *ed = editor_create(8, 16.0f);
208 editor_insert_text(ed, "old content", false);
209
210 editor_replace_body(ed, "new", 3);
211 ASSERT_EQ_INT((int)ed->text.len, 3);
212 ASSERT_EQ_STR(ed->text.data, "new");
213 ASSERT_EQ_INT(ed->cursor_idx, 0);
214
215 /* NULL data is safe */
216 editor_replace_body(ed, NULL, 0);
217 ASSERT_EQ_INT((int)ed->text.len, 0);
218 ASSERT_EQ_INT(ed->cursor_idx, 0);
219
220 editor_destroy(ed);
221 }
222
223 /* ---- suite_undo_redo ---------------------------------------------------- */
224
225 static void suite_undo_redo(void) {
226 RUN_SUITE(suite_undo_redo);
227
228 Editor *ed = editor_create(8, 16.0f);
229
230 editor_insert_text(ed, "hello", false);
231 editor_insert_text(ed, " world", false);
232 ASSERT_EQ_STR(ed->text.data, "hello world");
233 ASSERT_EQ_INT(ed->undo_len, 2);
234 ASSERT_EQ_INT(ed->redo_len, 0);
235
236 /* undo removes " world" */
237 editor_undo(ed);
238 ASSERT_EQ_STR(ed->text.data, "hello");
239 ASSERT_EQ_INT(ed->undo_len, 1);
240 ASSERT_EQ_INT(ed->redo_len, 1);
241
242 /* undo removes "hello" */
243 editor_undo(ed);
244 ASSERT_EQ_INT((int)ed->text.len, 0);
245 ASSERT_EQ_INT(ed->undo_len, 0);
246 ASSERT_EQ_INT(ed->redo_len, 2);
247
248 /* undo when empty is a no-op */
249 editor_undo(ed);
250 ASSERT_EQ_INT(ed->undo_len, 0);
251
252 /* redo restores "hello" */
253 editor_redo(ed);
254 ASSERT_EQ_STR(ed->text.data, "hello");
255 ASSERT_EQ_INT(ed->redo_len, 1);
256
257 /* new edit clears redo stack */
258 editor_insert_text(ed, "!", false);
259 ASSERT_EQ_INT(ed->redo_len, 0);
260 ASSERT_EQ_STR(ed->text.data, "hello!");
261
262 editor_destroy(ed);
263 }
264
265 /* ---- suite_insert_replace_selection ------------------------------------- */
266
267 static void suite_insert_replace_selection(void) {
268 RUN_SUITE(suite_insert_replace_selection);
269
270 Editor *ed = editor_create(8, 16.0f);
271 editor_insert_text(ed, "hello", false);
272
273 /* select all then replace */
274 editor_select_all(ed);
275 ASSERT(editor_has_selection(ed));
276
277 editor_insert_text(ed, "world", true);
278 ASSERT_EQ_STR(ed->text.data, "world");
279 ASSERT_EQ_INT((int)ed->text.len, 5);
280 ASSERT(!editor_has_selection(ed));
281
282 editor_destroy(ed);
283 }
284
285 /* ---- suite_formatting_range --------------------------------------------- */
286
287 static void suite_formatting_range(void) {
288 RUN_SUITE(suite_formatting_range);
289
290 Editor *ed = editor_create(8, 16.0f);
291 editor_insert_text(ed, "abcde", false);
292
293 RangeFormat fmt = {.bold = true, .italic = false};
294 editor_add_formatting_range(ed, 1, 3, fmt); /* chars 1..3 = bytes 1..3 */
295
296 /* inside range: bold=true */
297 RangeFormat f = editor_get_format_at(ed, 1);
298 ASSERT(f.bold == true);
299 f = editor_get_format_at(ed, 2);
300 ASSERT(f.bold == true);
301
302 /* outside range: bold=false */
303 f = editor_get_format_at(ed, 0);
304 ASSERT(f.bold == false);
305 f = editor_get_format_at(ed, 3); /* end is exclusive */
306 ASSERT(f.bold == false);
307
308 editor_destroy(ed);
309 }
310
311 /* ---- suite_replace_range ------------------------------------------------ */
312
313 static void suite_replace_range(void) {
314 RUN_SUITE(suite_replace_range);
315
316 Editor *ed = editor_create(8, 16.0f);
317 editor_insert_text(ed, "abcde", false);
318
319 editor_add_replace_range(ed, 1, 3, 2); /* chars 1..3 -> 2 visual cols */
320
321 /* byte 1 is inside the replacement range */
322 EditorRange *r = editor_get_range_at(ed, 1);
323 ASSERT_NOT_NULL(r);
324 ASSERT_EQ_INT(r->type, RANGE_REPLACEMENT);
325 ASSERT_EQ_INT(r->data.replacement.visual_cols, 2);
326
327 /* byte 0 is before the range */
328 ASSERT_NULL(editor_get_range_at(ed, 0));
329
330 /* byte 3 is the exclusive end, not inside */
331 ASSERT_NULL(editor_get_range_at(ed, 3));
332
333 editor_destroy(ed);
334 }
335
336 /* ---- suite_parse_ansi_codes --------------------------------------------- */
337
338 static void suite_parse_ansi_codes(void) {
339 RUN_SUITE(suite_parse_ansi_codes);
340
341 Editor *ed = editor_create(8, 16.0f);
342
343 /*
344 * "\x1b[1m" = ESC [ 1 m = 4 bytes -> replacement range 0..4
345 * "hi" = 2 bytes -> bold format range 4..6
346 * "\x1b[0m" = ESC [ 0 m = 4 bytes -> replacement range 6..10
347 */
348 editor_insert_text(ed, "\x1b[1mhi\x1b[0m", false);
349 ASSERT_EQ_INT((int)ed->text.len, 10);
350
351 editor_parse_ansi_codes(ed);
352
353 /* The ESC sequence at byte 0 should be a replacement range */
354 EditorRange *r = editor_get_range_at(ed, 0);
355 ASSERT_NOT_NULL(r);
356 ASSERT_EQ_INT(r->type, RANGE_REPLACEMENT);
357
358 /* "hi" at bytes 4-5 should have bold formatting */
359 RangeFormat f = editor_get_format_at(ed, 4);
360 ASSERT(f.bold == true);
361 f = editor_get_format_at(ed, 5);
362 ASSERT(f.bold == true);
363
364 /* After the closing sequence, bold should be off */
365 f = editor_get_format_at(ed, 6);
366 ASSERT(f.bold == false);
367
368 editor_destroy(ed);
369 }
370
371 /* ---- suite_byte_to_visual_pos ------------------------------------------ */
372
373 static void suite_byte_to_visual_pos(void) {
374 RUN_SUITE(suite_byte_to_visual_pos);
375
376 /* text "ab\ncd": bytes a=0 b=1 \n=2 c=3 d=4 */
377 Editor *ed = editor_create(8, 16.0f);
378 editor_insert_text(ed, "ab\ncd", false);
379
380 VisualPos p;
381
382 p = editor_byte_to_visual_pos(ed, 0);
383 ASSERT_EQ_INT(p.row, 0); ASSERT_EQ_INT(p.col, 0);
384
385 p = editor_byte_to_visual_pos(ed, 1);
386 ASSERT_EQ_INT(p.row, 0); ASSERT_EQ_INT(p.col, 1);
387
388 p = editor_byte_to_visual_pos(ed, 2);
389 ASSERT_EQ_INT(p.row, 0); ASSERT_EQ_INT(p.col, 2);
390
391 p = editor_byte_to_visual_pos(ed, 3);
392 ASSERT_EQ_INT(p.row, 1); ASSERT_EQ_INT(p.col, 0);
393
394 p = editor_byte_to_visual_pos(ed, 4);
395 ASSERT_EQ_INT(p.row, 1); ASSERT_EQ_INT(p.col, 1);
396
397 editor_destroy(ed);
398 }
399
400 /* ---- suite_visual_pos_to_byte ------------------------------------------ */
401
402 static void suite_visual_pos_to_byte(void) {
403 RUN_SUITE(suite_visual_pos_to_byte);
404
405 /* text "ab\ncd" */
406 Editor *ed = editor_create(8, 16.0f);
407 editor_insert_text(ed, "ab\ncd", false);
408
409 ASSERT_EQ_INT(editor_visual_pos_to_byte(ed, (VisualPos){0, 0}), 0);
410 ASSERT_EQ_INT(editor_visual_pos_to_byte(ed, (VisualPos){0, 1}), 1);
411 ASSERT_EQ_INT(editor_visual_pos_to_byte(ed, (VisualPos){1, 0}), 3);
412 ASSERT_EQ_INT(editor_visual_pos_to_byte(ed, (VisualPos){1, 1}), 4);
413
414 /* col beyond line end clamps to end-of-line ('\n' at byte 2) */
415 ASSERT_EQ_INT(editor_visual_pos_to_byte(ed, (VisualPos){0, 99}), 2);
416
417 editor_destroy(ed);
418 }
419
420 /* ---- suite_tab_expansion ------------------------------------------------ */
421
422 static void suite_tab_expansion(void) {
423 RUN_SUITE(suite_tab_expansion);
424
425 /* "\ta": '\t' at byte 0, 'a' at byte 1.
426 * TAB_SIZE=8: the tab expands to 8 columns, so 'a' is at visual col 8. */
427 Editor *ed = editor_create(8, 16.0f);
428 editor_insert_text(ed, "\ta", false);
429
430 VisualPos p = editor_byte_to_visual_pos(ed, 1);
431 ASSERT_EQ_INT(p.col, TAB_SIZE);
432
433 editor_destroy(ed);
434 }
435
436 /* ---- suite_select_word -------------------------------------------------- */
437
438 static void suite_select_word(void) {
439 RUN_SUITE(suite_select_word);
440
441 /* text "foo bar" */
442 Editor *ed = editor_create(8, 16.0f);
443 editor_insert_text(ed, "foo bar", false);
444
445 /* cursor inside "foo" (byte 1) -> select "foo" */
446 editor_goto_pos(ed, 1);
447 editor_clear_selection(ed);
448 editor_select_word(ed);
449 char *sel = editor_get_selection(ed);
450 ASSERT_NOT_NULL(sel);
451 ASSERT_EQ_STR(sel, "foo");
452 free(sel);
453
454 /* cursor inside "bar" (byte 5) -> select "bar" */
455 editor_goto_pos(ed, 5);
456 editor_clear_selection(ed);
457 editor_select_word(ed);
458 sel = editor_get_selection(ed);
459 ASSERT_NOT_NULL(sel);
460 ASSERT_EQ_STR(sel, "bar");
461 free(sel);
462
463 editor_destroy(ed);
464 }
465
466 /* ---- suite_range_shifts_on_insert --------------------------------------- */
467
468 static void suite_range_shifts_on_insert(void) {
469 RUN_SUITE(suite_range_shifts_on_insert);
470
471 /* Insert "abcd", add bold formatting at chars 2..4 (bytes 2..4). */
472 Editor *ed = editor_create(8, 16.0f);
473 editor_insert_text(ed, "abcd", false);
474
475 RangeFormat fmt = {.bold = true, .italic = false};
476 editor_add_formatting_range(ed, 2, 4, fmt);
477
478 /* Verify bold is at byte 2 before insert */
479 RangeFormat f = editor_get_format_at(ed, 2);
480 ASSERT(f.bold == true);
481 f = editor_get_format_at(ed, 0);
482 ASSERT(f.bold == false);
483
484 /* Insert "XY" at beginning: cursor to 0, then insert */
485 editor_goto_pos(ed, 0);
486 editor_clear_selection(ed);
487 editor_insert_text(ed, "XY", false);
488
489 /* Range should have shifted from [2,4) to [4,6) */
490 f = editor_get_format_at(ed, 0);
491 ASSERT(f.bold == false); /* byte 0 is now 'X', not bold */
492 f = editor_get_format_at(ed, 4);
493 ASSERT(f.bold == true); /* byte 4 is now 'a', bold */
494
495 editor_destroy(ed);
496 }
497
498 /* ---- suite_expand_selection --------------------------------------------- */
499
500 static void suite_expand_selection(void) {
501 RUN_SUITE(suite_expand_selection);
502
503 Editor *ed = editor_create(8, 16.0f);
504 editor_insert_text(ed, "foo bar. baz qux.", false);
505
506 editor_goto_pos(ed, 1);
507 editor_clear_selection(ed);
508
509 /* level 0 → word */
510 editor_expand_selection(ed, 0);
511 char *sel = editor_get_selection(ed);
512 ASSERT_NOT_NULL(sel);
513 ASSERT_EQ_STR(sel, "foo");
514 free(sel);
515
516 /* level 2 → paragraph (single paragraph → whole buffer) */
517 editor_expand_selection(ed, 2);
518 ASSERT_EQ_INT(ed->selection_anchor, 0);
519 ASSERT_EQ_INT(ed->cursor_idx, (int)ed->text.len);
520
521 editor_destroy(ed);
522 }
523
524 /* ---- main --------------------------------------------------------------- */
525
526 int main(void) {
527 SDL_Init(0);
528
529 suite_create_destroy();
530 suite_insert_text();
531 suite_delete_back();
532 suite_delete_forward();
533 suite_delete_multibyte();
534 suite_cursor_movement();
535 suite_select_all();
536 suite_replace_body();
537 suite_undo_redo();
538 suite_insert_replace_selection();
539 suite_formatting_range();
540 suite_replace_range();
541 suite_parse_ansi_codes();
542 suite_byte_to_visual_pos();
543 suite_visual_pos_to_byte();
544 suite_tab_expansion();
545 suite_select_word();
546 suite_range_shifts_on_insert();
547 suite_expand_selection();
548
549 SDL_Quit();
550 REPORT_AND_EXIT();
551 }