editor.text.tests.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <planet/text/editor.hpp>

#include <felspar/test.hpp>


namespace {


    planet::events::text typed(std::string s) { return {.utf8 = std::move(s)}; }
    planet::events::key down(planet::events::scancode const sc) {
        return {.scancode = sc, .action = planet::events::action::down};
    }
    planet::events::key
            down(planet::events::scancode const sc,
                 planet::events::modifiers const m) {
        return {.scancode = sc,
                .action = planet::events::action::down,
                .modifiers = m};
    }
    planet::events::key up(planet::events::scancode const sc) {
        return {.scancode = sc, .action = planet::events::action::up};
    }

The usual keys, which is what everything but the rebinding tests uses

26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
    auto const &keys = planet::text::default_bindings;


    auto const suite = felspar::testsuite("text.editor");


    auto const at_rest = suite.test("at rest", [](auto check) {
        planet::text::editor ed{keys, "Nomad"};

        check(ed.is_editing()) == false;
        check(ed.value()) == "Nomad";
    });


    auto const resting_value = suite.test(
            "resting value",
            [](auto check) {

A resting value handed in from outside takes the place of what was there — the seam a field mirroring a number some other control is changing keeps its text current through.

48
49
50
51
52
53
54
55
                planet::text::editor ed{keys, "Nomad"};

                ed.value("Zeta");

                check(ed.value()) == "Zeta";
                check(ed.is_editing()) == false;
            },
            [](auto check) {

An edit in flight is left untouched: the buffer being typed is never overwritten by what the field is told it now reads, so the mirror only moves the value while the field is at rest.

61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
                planet::text::editor ed{keys, "Nomad"};
                ed.begin();
                ed.handle(typed(" II"));

                ed.value("Zeta");

                check(ed.value()) == "Nomad II";
                check(ed.is_editing()) == true;
            });


    auto const beginning = suite.test("begin", [](auto check) {
        planet::text::editor ed{keys, "Nomad"};

        ed.begin();

        check(ed.is_editing()) == true;

An edit corrects rather than retypes: what is there is what is being edited, with the caret at the end of it ready for more.

 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
        check(ed.value()) == "Nomad";
        check(ed.cursor()) == 5u;
    });


    auto const typing = suite.test(
            "typing",
            [](auto check) {
                planet::text::editor ed{keys, "Nomad"};
                ed.begin();

                check(ed.handle(typed(" Ze")))
                        == planet::text::outcome::changed;
                check(ed.handle(typed("ta"))) == planet::text::outcome::changed;

                check(ed.value()) == "Nomad Zeta";
                check(ed.cursor()) == 10u;
            },
            [](auto check) {

Text arrives at a field whenever it is subscribed, which is all of the time, so an editor at rest has to say that it wanted none of it.

106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
                planet::text::editor ed{keys, "Nomad"};

                check(ed.handle(typed("x"))) == planet::text::outcome::ignored;

                check(ed.value()) == "Nomad";
            });


    auto const control_keys = suite.test(
            "control keys",
            [](auto check) {
                planet::text::editor ed{keys, "Nomad"};
                ed.begin();

                check(ed.handle(down(planet::events::scancode::return_key)))
                        == planet::text::outcome::commit;
                check(ed.handle(down(planet::events::scancode::escape_key)))
                        == planet::text::outcome::cancel;
                check(ed.handle(down(planet::events::scancode::letter_a)))
                        == planet::text::outcome::ignored;

A single line has nowhere for up and down to go either

127
128
129
130
                check(ed.handle(down(planet::events::scancode::up_key)))
                        == planet::text::outcome::ignored;
                check(ed.handle(down(planet::events::scancode::down_key)))
                        == planet::text::outcome::ignored;

Reporting an outcome is not acting on it: the side effects of ending an edit need a baseplate, so they belong to the shell, and the editor is still editing until it is told otherwise.

136
137
138
                check(ed.is_editing()) == true;
            },
            [](auto check) {

Only the key going down ends an edit. A key still held from before the edit began releases into it, and that release must not be what finishes it.

144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
                planet::text::editor ed{keys, "Nomad"};
                ed.begin();

                check(ed.handle(up(planet::events::scancode::return_key)))
                        == planet::text::outcome::ignored;
                check(ed.handle(up(planet::events::scancode::escape_key)))
                        == planet::text::outcome::ignored;
            });


    auto const keys_at_rest = suite.test("keys at rest", [](auto check) {
        planet::text::editor ed{keys, "Nomad"};

        check(ed.handle(down(planet::events::scancode::return_key)))
                == planet::text::outcome::ignored;
        check(ed.handle(down(planet::events::scancode::escape_key)))
                == planet::text::outcome::ignored;
        check(ed.handle(down(planet::events::scancode::letter_a)))
                == planet::text::outcome::ignored;
        check(ed.handle(down(planet::events::scancode::backspace_key)))
                == planet::text::outcome::ignored;
        check(ed.handle(down(planet::events::scancode::delete_key)))
                == planet::text::outcome::ignored;

Navigation is ignored at rest as well, caret and all: a field is subscribed to keys whether or not it is being edited, and the arrow keys belong to whatever else is listening for them until an edit takes them.

173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
        check(ed.handle(down(planet::events::scancode::left_key)))
                == planet::text::outcome::ignored;
        check(ed.handle(down(planet::events::scancode::right_key)))
                == planet::text::outcome::ignored;
        check(ed.handle(down(planet::events::scancode::home_key)))
                == planet::text::outcome::ignored;
        check(ed.handle(down(planet::events::scancode::end_key)))
                == planet::text::outcome::ignored;

        check(ed.is_editing()) == false;
        check(ed.value()) == "Nomad";
        check(ed.cursor()) == 0u;
    });


    auto const backspace = suite.test(
            "backspace",
            [](auto check) {
                planet::text::editor ed{keys, "Zeta"};
                ed.begin();

                check(ed.handle(down(planet::events::scancode::backspace_key)))
                        == planet::text::outcome::changed;

                check(ed.value()) == "Zet";

The caret follows the text it took out

199
200
201
                check(ed.cursor()) == 3u;
            },
            [](auto check) {

Nothing to remove is nothing changed

203
204
205
206
207
208
209
210
211
                planet::text::editor ed{keys};
                ed.begin();

                check(ed.handle(down(planet::events::scancode::backspace_key)))
                        == planet::text::outcome::ignored;

                check(ed.value()) == "";
            },
            [](auto check) {

Nor is there anything before a caret at the start of the text

213
214
215
216
217
218
219
220
221
222
223
                planet::text::editor ed{keys, "Zeta"};
                ed.begin();
                ed.handle(down(planet::events::scancode::home_key));

                check(ed.handle(down(planet::events::scancode::backspace_key)))
                        == planet::text::outcome::ignored;

                check(ed.value()) == "Zeta";
                check(ed.cursor()) == 0u;
            },
            [](auto check) {

Correcting a typo and carrying on

225
226
227
228
229
230
231
232
233
234
                planet::text::editor ed{keys, "Zetz"};
                ed.begin();
                ed.handle(down(planet::events::scancode::backspace_key));
                ed.handle(typed("a"));

                ed.commit();

                check(ed.value()) == "Zeta";
            },
            [](auto check) {

A whole character goes, never the tail byte of one: what is left has to still be text.

239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
                planet::text::editor ed{keys, "Zeté"};
                ed.begin();

                check(ed.handle(down(planet::events::scancode::backspace_key)))
                        == planet::text::outcome::changed;

                check(ed.value()) == "Zet";
                check(ed.cursor()) == 3u;
            },
            [](auto check) {
                planet::text::editor ed{keys, "Ze🚀"};
                ed.begin();

                ed.handle(down(planet::events::scancode::backspace_key));

                check(ed.value()) == "Ze";
                check(ed.cursor()) == 2u;
            },
            [](auto check) {

Backspace acts at the caret, not at the end of the text

259
260
261
262
263
264
265
266
267
268
                planet::text::editor ed{keys, "Zeta"};
                ed.begin();
                ed.handle(down(planet::events::scancode::left_key));

                ed.handle(down(planet::events::scancode::backspace_key));

                check(ed.value()) == "Zea";
                check(ed.cursor()) == 2u;
            },
            [](auto check) {

A key coming back up is not a key press, backspace included

270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
                planet::text::editor ed{keys, "Zeta"};
                ed.begin();

                check(ed.handle(up(planet::events::scancode::backspace_key)))
                        == planet::text::outcome::ignored;

                check(ed.value()) == "Zeta";
            });


    auto const navigation = suite.test(
            "navigation",
            [](auto check) {
                planet::text::editor ed{keys, "Nomad"};
                ed.begin();

                check(ed.handle(down(planet::events::scancode::left_key)))
                        == planet::text::outcome::changed;
                check(ed.cursor()) == 4u;
                check(ed.handle(down(planet::events::scancode::right_key)))
                        == planet::text::outcome::changed;
                check(ed.cursor()) == 5u;

Neither arrow runs off the end of the buffer

294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
                check(ed.handle(down(planet::events::scancode::right_key)))
                        == planet::text::outcome::ignored;
                check(ed.cursor()) == 5u;

                check(ed.value()) == "Nomad";
            },
            [](auto check) {
                planet::text::editor ed{keys, "Nomad"};
                ed.begin();

                check(ed.handle(down(planet::events::scancode::home_key)))
                        == planet::text::outcome::changed;
                check(ed.cursor()) == 0u;
                check(ed.handle(down(planet::events::scancode::left_key)))
                        == planet::text::outcome::ignored;
                check(ed.cursor()) == 0u;

                check(ed.handle(down(planet::events::scancode::end_key)))
                        == planet::text::outcome::changed;
                check(ed.cursor()) == 5u;

Already there is nothing changed

315
316
317
318
                check(ed.handle(down(planet::events::scancode::end_key)))
                        == planet::text::outcome::ignored;
            },
            [](auto check) {

The caret steps over whole characters, so it can never land inside one: is three bytes, between two of one.

323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
                planet::text::editor ed{keys, "a→b"};
                ed.begin();

                check(ed.cursor()) == 5u;
                ed.handle(down(planet::events::scancode::left_key));
                check(ed.cursor()) == 4u;
                ed.handle(down(planet::events::scancode::left_key));
                check(ed.cursor()) == 1u;
                ed.handle(down(planet::events::scancode::left_key));
                check(ed.cursor()) == 0u;

                ed.handle(down(planet::events::scancode::right_key));
                check(ed.cursor()) == 1u;
                ed.handle(down(planet::events::scancode::right_key));
                check(ed.cursor()) == 4u;
            });


    auto const insertion = suite.test(
            "insertion",
            [](auto check) {

Typing goes in at the caret and the caret follows it along

345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
                planet::text::editor ed{keys, "Nomad"};
                ed.begin();
                ed.handle(down(planet::events::scancode::home_key));

                check(ed.handle(typed("The ")))
                        == planet::text::outcome::changed;

                check(ed.value()) == "The Nomad";
                check(ed.cursor()) == 4u;

                ed.handle(typed("Old "));

                check(ed.value()) == "The Old Nomad";
                check(ed.cursor()) == 8u;
            },
            [](auto check) {

The caret advances by the bytes inserted, not by characters

362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
                planet::text::editor ed{keys, "Zet"};
                ed.begin();

                ed.handle(typed("é"));

                check(ed.value()) == "Zeté";
                check(ed.cursor()) == 5u;
            });


    auto const forward_delete = suite.test(
            "delete",
            [](auto check) {
                planet::text::editor ed{keys, "Nomad"};
                ed.begin();
                ed.handle(down(planet::events::scancode::home_key));

                check(ed.handle(down(planet::events::scancode::delete_key)))
                        == planet::text::outcome::changed;

                check(ed.value()) == "omad";

The text moves back to the caret, so the caret stays put

384
385
386
                check(ed.cursor()) == 0u;
            },
            [](auto check) {

Nothing follows a caret at the end of the text

388
389
390
391
392
393
394
395
396
                planet::text::editor ed{keys, "Nomad"};
                ed.begin();

                check(ed.handle(down(planet::events::scancode::delete_key)))
                        == planet::text::outcome::ignored;

                check(ed.value()) == "Nomad";
            },
            [](auto check) {

A whole character goes here too

398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
                planet::text::editor ed{keys, "→b"};
                ed.begin();
                ed.handle(down(planet::events::scancode::home_key));

                ed.handle(down(planet::events::scancode::delete_key));

                check(ed.value()) == "b";
                check(ed.cursor()) == 0u;
            });


    auto const cursor_from_outside = suite.test(
            "set_cursor",
            [](auto check) {
                planet::text::editor ed{keys, "Nomad"};
                ed.begin();

                ed.set_cursor(2);
                check(ed.cursor()) == 2u;
                ed.set_cursor(0);
                check(ed.cursor()) == 0u;

Past the end is the end, not the boundary before it — a click past the last character puts the caret after it.

423
424
425
426
                ed.set_cursor(99);
                check(ed.cursor()) == 5u;
            },
            [](auto check) {

A presentation measures its way to an offset and can land inside a character. What it gets is the boundary at or before where it asked, never somewhere illegal.

432
433
434
435
436
437
438
439
440
441
442
                planet::text::editor ed{keys, "a→b"};
                ed.begin();

                ed.set_cursor(2);
                check(ed.cursor()) == 1u;
                ed.set_cursor(3);
                check(ed.cursor()) == 1u;
                ed.set_cursor(4);
                check(ed.cursor()) == 4u;
            },
            [](auto check) {

So what is typed after one is still valid text

444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
                planet::text::editor ed{keys, "a→b"};
                ed.begin();
                ed.set_cursor(3);

                ed.handle(typed("x"));

                check(ed.value()) == "ax→b";
            });


    auto const ending = suite.test(
            "ending an edit",
            [](auto check) {
                planet::text::editor ed{keys, "Nomad"};
                ed.begin();
                ed.handle(typed(" II"));

                ed.commit();

                check(ed.value()) == "Nomad II";
                check(ed.is_editing()) == false;
            },
            [](auto check) {
                planet::text::editor ed{keys, "Nomad"};
                ed.begin();
                ed.handle(typed(" II"));

                ed.cancel();

                check(ed.value()) == "Nomad";
                check(ed.is_editing()) == false;

The caret comes back inside the value that was put back: it was past the end of it a moment ago, and every offset the editor holds has to be one into the text it holds now.

480
481
482
483
484
485
486
                check(ed.cursor()) == 5u;
            });


    auto const filtering = suite.test(
            "what the value is allowed to be",
            [](auto check) {

A cap on how long the value may grow. The filter is handed the value as it would be, so a rule about the whole of it is written as a rule about the whole of it.

492
493
494
495
496
497
498
499
                planet::text::editor ed{
                        keys,
                        [](std::string_view const v) { return v.size() <= 7; },
                        "Nomad"};
                ed.begin();

                check(ed.handle(typed(" I"))) == planet::text::outcome::changed;
                check(ed.value()) == "Nomad I";

The insertion that would take it past the cap does not happen at all: the buffer and the caret are exactly as they were, and the editor reports that nothing changed so no redraw is charged for a keystroke that did nothing.

506
507
508
509
510
                check(ed.handle(typed("I"))) == planet::text::outcome::ignored;
                check(ed.value()) == "Nomad I";
                check(ed.cursor()) == 7u;
            },
            [](auto check) {

A refused insertion at the caret leaves what is on either side of it alone, not just what is on the end.

515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
                planet::text::editor ed{
                        keys,
                        [](std::string_view const v) {
                            return v.find('x') == std::string_view::npos;
                        },
                        "Nomad"};
                ed.begin();
                ed.handle(down(planet::events::scancode::home_key));

                check(ed.handle(typed("x"))) == planet::text::outcome::ignored;

                check(ed.value()) == "Nomad";
                check(ed.cursor()) == 0u;
            },
            [](auto check) {

The whole of what was typed goes back, however many bytes it took: what a refused insertion leaves behind has to still be the text that was there before it.

535
536
537
538
539
540
541
542
543
544
545
546
                planet::text::editor ed{
                        keys,
                        [](std::string_view const v) { return v.size() < 4; },
                        "a"};
                ed.begin();

                check(ed.handle(typed("→"))) == planet::text::outcome::ignored;

                check(ed.value()) == "a";
                check(ed.cursor()) == 1u;
            },
            [](auto check) {

A removal is consulted but never obeyed, so a value can always be cut down to nothing — a cap that stopped it being cut back would be no use, and a rule that it may not be blank has to leave a way to clear it and start again.

553
554
555
556
557
558
559
560
561
                planet::text::editor ed{
                        keys,
                        [](std::string_view const v) { return not v.empty(); },
                        "Nomad"};
                ed.begin();
                while (not ed.value().empty()) {
                    ed.handle(down(planet::events::scancode::backspace_key));
                }
                check(ed.value()) == "";

What it will not do is let the edit end on one

564
565
566
567
568
569
570
                check(ed.commit()) == false;

                check(ed.value()) == "Nomad";
                check(ed.is_editing()) == false;
                check(ed.cursor()) == 5u;
            },
            [](auto check) {

A value the filter is happy with commits as it always did

572
573
574
575
576
577
578
579
580
581
582
583
584
                planet::text::editor ed{
                        keys,
                        [](std::string_view const v) { return not v.empty(); },
                        "Nomad"};
                ed.begin();
                ed.handle(typed(" II"));

                check(ed.commit()) == true;

                check(ed.value()) == "Nomad II";
                check(ed.is_editing()) == false;
            },
            [](auto check) {

A removal is a change like any other, so a call site that reacts to the value as it is edited is told of it: the filter is consulted after a deletion — backwards and forwards — with the value the buffer has become, though the deletion itself is never blocked.

592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
                std::string consulted;
                planet::text::editor ed{
                        keys,
                        [&consulted](std::string_view const v) {
                            consulted += v;
                            consulted += ';';
                            return true;
                        },
                        "Zeta"};
                ed.begin();

                ed.handle(down(planet::events::scancode::backspace_key));
                ed.handle(down(planet::events::scancode::home_key));
                ed.handle(down(planet::events::scancode::delete_key));

                check(consulted) == "Zet;et;";
            });


    auto const rebinding = suite.test(
            "rebinding",
            [](auto check) {

A game that has loaded the player's own choices hands them over, and from then on those are the keys the edit answers to. A default stops meaning anything the moment something else is bound in its place.

620
621
622
623
624
625
626
627
628
629
630
                planet::text::configuration const bound{
                        .commit = {planet::events::scancode::tab_key, {}}};
                planet::text::editor ed{bound, "Nomad"};
                ed.begin();

                check(ed.handle(down(planet::events::scancode::tab_key)))
                        == planet::text::outcome::commit;
                check(ed.handle(down(planet::events::scancode::return_key)))
                        == planet::text::outcome::ignored;
            },
            [](auto check) {

A binding that asks for a modifier needs it held

632
633
634
635
636
637
638
639
640
641
642
                planet::text::configuration const bound{
                        .caret_to_start = {
                                planet::events::scancode::letter_a,
                                {.ctrl = true}}};
                planet::text::editor ed{bound, "Nomad"};
                ed.begin();

                check(ed.handle(down(
                        planet::events::scancode::letter_a, {.ctrl = true})))
                        == planet::text::outcome::changed;
                check(ed.cursor()) == 0u;

Unmodified the same key is a letter to type rather than a control, and a key event carrying a letter is not what puts one in the buffer — the character arrives as text.

648
649
650
                check(ed.handle(down(planet::events::scancode::letter_a)))
                        == planet::text::outcome::ignored;
                check(ed.cursor()) == 0u;

Home has nothing bound to it any more

653
654
655
656
657
658
                ed.handle(down(planet::events::scancode::end_key));
                check(ed.handle(down(planet::events::scancode::home_key)))
                        == planet::text::outcome::ignored;
                check(ed.cursor()) == 5u;
            },
            [](auto check) {

One key bound to two things is answered rather than refused: the bindings are tried in the order text::configuration declares them, so the first of the two is what happens.

665
666
667
668
669
670
671
672
673
                planet::text::configuration bound;
                bound.cancel = bound.commit;
                planet::text::editor ed{bound, "Nomad"};
                ed.begin();

                check(ed.handle(down(planet::events::scancode::return_key)))
                        == planet::text::outcome::commit;
            },
            [](auto check) {

The configuration is held by reference, so a key the player rebinds is at once the key every editor reading that configuration answers to — an edit already running included. Nothing has to be reached into and told.

680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
                planet::text::configuration bound;
                planet::text::editor ed{bound, "Nomad"};
                ed.begin();

                bound.commit = {planet::events::scancode::tab_key, {}};

                check(ed.handle(down(planet::events::scancode::tab_key)))
                        == planet::text::outcome::commit;
                check(ed.handle(down(planet::events::scancode::return_key)))
                        == planet::text::outcome::ignored;
            });


    auto const exact_modifiers =
            suite.test("modifiers are exact", [](auto check) {

The defaults are the bare keys, so a modifier held with one leaves the edit alone: shift-Left belongs to whatever the game means by it rather than moving the caret.

700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
                planet::text::editor ed{keys, "Nomad"};
                ed.begin();

                check(ed.handle(down(
                        planet::events::scancode::left_key, {.shift = true})))
                        == planet::text::outcome::ignored;
                check(ed.cursor()) == 5u;

                check(ed.handle(
                        down(planet::events::scancode::backspace_key,
                             {.ctrl = true})))
                        == planet::text::outcome::ignored;
                check(ed.value()) == "Nomad";
            });


}