esc

Externally Scriptable Editor

git clone git://mccd.space/esc

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 }