ep_vim 0.1.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,676 @@
1
+ "use strict";
2
+
3
+ const { describe, it } = require("node:test");
4
+ const assert = require("node:assert/strict");
5
+ const {
6
+ isWordChar,
7
+ isWhitespace,
8
+ clampLine,
9
+ clampChar,
10
+ getLineText,
11
+ firstNonBlank,
12
+ findCharForward,
13
+ findCharBackward,
14
+ wordForward,
15
+ wordBackward,
16
+ wordEnd,
17
+ charSearchPos,
18
+ motionRange,
19
+ charMotionRange,
20
+ textWordRange,
21
+ textQuoteRange,
22
+ textBracketRange,
23
+ getVisualSelection,
24
+ paragraphForward,
25
+ paragraphBackward,
26
+ getTextInRange,
27
+ } = require("./vim-core");
28
+
29
+ const makeRep = (lines) => ({
30
+ lines: {
31
+ length: () => lines.length,
32
+ atIndex: (n) => ({ text: lines[n] }),
33
+ },
34
+ });
35
+
36
+ describe("isWordChar", () => {
37
+ it("returns true for letters", () => {
38
+ assert.equal(isWordChar("a"), true);
39
+ assert.equal(isWordChar("Z"), true);
40
+ });
41
+
42
+ it("returns true for digits", () => {
43
+ assert.equal(isWordChar("0"), true);
44
+ assert.equal(isWordChar("9"), true);
45
+ });
46
+
47
+ it("returns true for underscore", () => {
48
+ assert.equal(isWordChar("_"), true);
49
+ });
50
+
51
+ it("returns false for punctuation and space", () => {
52
+ assert.equal(isWordChar("."), false);
53
+ assert.equal(isWordChar(" "), false);
54
+ assert.equal(isWordChar("-"), false);
55
+ });
56
+ });
57
+
58
+ describe("isWhitespace", () => {
59
+ it("returns true for space and tab", () => {
60
+ assert.equal(isWhitespace(" "), true);
61
+ assert.equal(isWhitespace("\t"), true);
62
+ });
63
+
64
+ it("returns false for letters and punctuation", () => {
65
+ assert.equal(isWhitespace("a"), false);
66
+ assert.equal(isWhitespace("."), false);
67
+ });
68
+ });
69
+
70
+ describe("clampLine", () => {
71
+ const rep = makeRep(["a", "b", "c"]);
72
+
73
+ it("clamps below zero to zero", () => {
74
+ assert.equal(clampLine(-1, rep), 0);
75
+ assert.equal(clampLine(-100, rep), 0);
76
+ });
77
+
78
+ it("clamps above max to last line", () => {
79
+ assert.equal(clampLine(5, rep), 2);
80
+ assert.equal(clampLine(3, rep), 2);
81
+ });
82
+
83
+ it("returns value when in range", () => {
84
+ assert.equal(clampLine(0, rep), 0);
85
+ assert.equal(clampLine(1, rep), 1);
86
+ assert.equal(clampLine(2, rep), 2);
87
+ });
88
+ });
89
+
90
+ describe("clampChar", () => {
91
+ it("clamps below zero to zero", () => {
92
+ assert.equal(clampChar(-1, "hello"), 0);
93
+ });
94
+
95
+ it("clamps above max to last char index", () => {
96
+ assert.equal(clampChar(10, "hello"), 4);
97
+ });
98
+
99
+ it("returns value when in range", () => {
100
+ assert.equal(clampChar(2, "hello"), 2);
101
+ });
102
+
103
+ it("clamps to zero for empty string", () => {
104
+ assert.equal(clampChar(0, ""), 0);
105
+ });
106
+ });
107
+
108
+ describe("getLineText", () => {
109
+ it("returns the text of the given line", () => {
110
+ const rep = makeRep(["first", "second", "third"]);
111
+ assert.equal(getLineText(rep, 0), "first");
112
+ assert.equal(getLineText(rep, 1), "second");
113
+ assert.equal(getLineText(rep, 2), "third");
114
+ });
115
+ });
116
+
117
+ describe("firstNonBlank", () => {
118
+ it("returns 0 when no leading whitespace", () => {
119
+ assert.equal(firstNonBlank("hello"), 0);
120
+ });
121
+
122
+ it("skips leading spaces", () => {
123
+ assert.equal(firstNonBlank(" hello"), 3);
124
+ });
125
+
126
+ it("skips leading tabs", () => {
127
+ assert.equal(firstNonBlank("\t\thello"), 2);
128
+ });
129
+
130
+ it("returns length for all-whitespace line", () => {
131
+ assert.equal(firstNonBlank(" "), 3);
132
+ });
133
+
134
+ it("returns 0 for empty string", () => {
135
+ assert.equal(firstNonBlank(""), 0);
136
+ });
137
+ });
138
+
139
+ describe("findCharForward", () => {
140
+ it("finds the first occurrence after startChar", () => {
141
+ assert.equal(findCharForward("hello world", 0, "o", 1), 4);
142
+ });
143
+
144
+ it("finds the nth occurrence", () => {
145
+ assert.equal(findCharForward("abacada", 0, "a", 1), 2);
146
+ assert.equal(findCharForward("abacada", 0, "a", 2), 4);
147
+ assert.equal(findCharForward("abacada", 0, "a", 3), 6);
148
+ });
149
+
150
+ it("returns -1 when not found", () => {
151
+ assert.equal(findCharForward("hello", 0, "z", 1), -1);
152
+ });
153
+
154
+ it("returns -1 when count exceeds occurrences", () => {
155
+ assert.equal(findCharForward("hello", 0, "l", 3), -1);
156
+ });
157
+
158
+ it("searches only after startChar", () => {
159
+ assert.equal(findCharForward("aba", 1, "a", 1), 2);
160
+ });
161
+ });
162
+
163
+ describe("findCharBackward", () => {
164
+ it("finds the first occurrence before startChar", () => {
165
+ assert.equal(findCharBackward("hello world", 7, "o", 1), 4);
166
+ });
167
+
168
+ it("finds the nth occurrence backward", () => {
169
+ assert.equal(findCharBackward("abacada", 6, "a", 1), 4);
170
+ assert.equal(findCharBackward("abacada", 6, "a", 2), 2);
171
+ });
172
+
173
+ it("returns -1 when not found", () => {
174
+ assert.equal(findCharBackward("hello", 4, "z", 1), -1);
175
+ });
176
+
177
+ it("searches only before startChar", () => {
178
+ assert.equal(findCharBackward("aba", 1, "a", 1), 0);
179
+ });
180
+ });
181
+
182
+ describe("wordForward", () => {
183
+ it("moves past a word to start of next word", () => {
184
+ assert.equal(wordForward("hello world", 0), 6);
185
+ });
186
+
187
+ it("moves past punctuation to start of next token", () => {
188
+ assert.equal(wordForward("foo.bar", 0), 3);
189
+ });
190
+
191
+ it("skips trailing whitespace", () => {
192
+ assert.equal(wordForward("hello world", 0), 8);
193
+ });
194
+
195
+ it("moves to end of line when no next word", () => {
196
+ assert.equal(wordForward("hello", 0), 5);
197
+ });
198
+
199
+ it("moves from whitespace to next word", () => {
200
+ assert.equal(wordForward(" hello", 0), 2);
201
+ });
202
+ });
203
+
204
+ describe("wordBackward", () => {
205
+ it("moves back to start of previous word", () => {
206
+ assert.equal(wordBackward("hello world", 6), 0);
207
+ });
208
+
209
+ it("moves back past whitespace", () => {
210
+ assert.equal(wordBackward("hello world", 8), 0);
211
+ });
212
+
213
+ it("stops at start of line", () => {
214
+ assert.equal(wordBackward("hello", 0), 0);
215
+ });
216
+
217
+ it("moves to start of current word from middle", () => {
218
+ assert.equal(wordBackward("hello world", 8), 6);
219
+ });
220
+ });
221
+
222
+ describe("wordEnd", () => {
223
+ it("moves to end of current/next word", () => {
224
+ assert.equal(wordEnd("hello world", 0), 4);
225
+ });
226
+
227
+ it("skips whitespace then finds end of next word", () => {
228
+ assert.equal(wordEnd("hello world", 4), 10);
229
+ });
230
+
231
+ it("moves to end of punctuation run", () => {
232
+ assert.equal(wordEnd("foo...bar", 0), 2);
233
+ });
234
+ });
235
+
236
+ describe("charSearchPos", () => {
237
+ const line = "hello world";
238
+
239
+ it("f finds char forward", () => {
240
+ assert.equal(charSearchPos("f", line, 0, "o", 1), 4);
241
+ });
242
+
243
+ it("F finds char backward", () => {
244
+ assert.equal(charSearchPos("F", line, 7, "o", 1), 4);
245
+ });
246
+
247
+ it("t lands one before the found char", () => {
248
+ assert.equal(charSearchPos("t", line, 0, "o", 1), 3);
249
+ });
250
+
251
+ it("T lands one after the found char", () => {
252
+ assert.equal(charSearchPos("T", line, 7, "o", 1), 5);
253
+ });
254
+
255
+ it("returns -1 when char not found", () => {
256
+ assert.equal(charSearchPos("f", line, 0, "z", 1), -1);
257
+ });
258
+
259
+ it("t returns -1 when char not found", () => {
260
+ assert.equal(charSearchPos("t", line, 0, "z", 1), -1);
261
+ });
262
+
263
+ it("respects count for f", () => {
264
+ assert.equal(charSearchPos("f", "ababa", 0, "b", 2), 3);
265
+ });
266
+ });
267
+
268
+ describe("motionRange", () => {
269
+ it("w computes word-forward range", () => {
270
+ const range = motionRange("w", 0, "hello world", 1);
271
+ assert.deepEqual(range, { start: 0, end: 6 });
272
+ });
273
+
274
+ it("e computes word-end range (inclusive)", () => {
275
+ const range = motionRange("e", 0, "hello world", 1);
276
+ assert.deepEqual(range, { start: 0, end: 5 });
277
+ });
278
+
279
+ it("b computes word-backward range", () => {
280
+ const range = motionRange("b", 6, "hello world", 1);
281
+ assert.deepEqual(range, { start: 0, end: 6 });
282
+ });
283
+
284
+ it("$ computes to end of line", () => {
285
+ const range = motionRange("$", 3, "hello world", 1);
286
+ assert.deepEqual(range, { start: 3, end: 11 });
287
+ });
288
+
289
+ it("0 computes to start of line", () => {
290
+ const range = motionRange("0", 5, "hello world", 1);
291
+ assert.deepEqual(range, { start: 0, end: 5 });
292
+ });
293
+
294
+ it("^ computes to first non-blank", () => {
295
+ const range = motionRange("^", 6, " hello", 1);
296
+ assert.deepEqual(range, { start: 2, end: 6 });
297
+ });
298
+
299
+ it("^ when cursor is before first non-blank", () => {
300
+ const range = motionRange("^", 0, " hello", 1);
301
+ assert.deepEqual(range, { start: 0, end: 2 });
302
+ });
303
+
304
+ it("h computes backward range", () => {
305
+ const range = motionRange("h", 5, "hello world", 2);
306
+ assert.deepEqual(range, { start: 3, end: 5 });
307
+ });
308
+
309
+ it("h clamps at start of line", () => {
310
+ const range = motionRange("h", 1, "hello", 5);
311
+ assert.deepEqual(range, { start: 0, end: 1 });
312
+ });
313
+
314
+ it("l computes forward range", () => {
315
+ const range = motionRange("l", 3, "hello world", 2);
316
+ assert.deepEqual(range, { start: 3, end: 5 });
317
+ });
318
+
319
+ it("l clamps at end of line", () => {
320
+ const range = motionRange("l", 9, "hello world", 5);
321
+ assert.deepEqual(range, { start: 9, end: 11 });
322
+ });
323
+
324
+ it("returns null for unknown motion key", () => {
325
+ assert.equal(motionRange("z", 0, "hello", 1), null);
326
+ });
327
+
328
+ it("w with count moves multiple words", () => {
329
+ const range = motionRange("w", 0, "one two three", 2);
330
+ assert.deepEqual(range, { start: 0, end: 8 });
331
+ });
332
+ });
333
+
334
+ describe("charMotionRange", () => {
335
+ it("f includes the found char", () => {
336
+ const range = charMotionRange("f", 2, 5);
337
+ assert.deepEqual(range, { start: 2, end: 6 });
338
+ });
339
+
340
+ it("t deletes up to adjusted pos (pos from charSearchPos already adjusted)", () => {
341
+ const range = charMotionRange("t", 2, 5);
342
+ assert.deepEqual(range, { start: 2, end: 6 });
343
+ });
344
+
345
+ it("F includes cursor char going backward", () => {
346
+ const range = charMotionRange("F", 5, 2);
347
+ assert.deepEqual(range, { start: 2, end: 6 });
348
+ });
349
+
350
+ it("T deletes from adjusted pos to cursor (pos from charSearchPos already adjusted)", () => {
351
+ const range = charMotionRange("T", 5, 2);
352
+ assert.deepEqual(range, { start: 2, end: 5 });
353
+ });
354
+
355
+ it("returns null for f when pos equals char", () => {
356
+ const range = charMotionRange("f", 2, 1);
357
+ assert.equal(range, null);
358
+ });
359
+ });
360
+
361
+ describe("innerWordRange", () => {
362
+ it("selects the whole word when cursor is in the middle", () => {
363
+ assert.deepEqual(textWordRange("hello world", 2, "i"), {
364
+ start: 0,
365
+ end: 5,
366
+ });
367
+ });
368
+
369
+ it("selects the whole word when cursor is at the start", () => {
370
+ assert.deepEqual(textWordRange("hello world", 0, "i"), {
371
+ start: 0,
372
+ end: 5,
373
+ });
374
+ });
375
+
376
+ it("selects the whole word when cursor is at the end", () => {
377
+ assert.deepEqual(textWordRange("hello world", 4, "i"), {
378
+ start: 0,
379
+ end: 5,
380
+ });
381
+ });
382
+
383
+ it("selects a word in the middle of a line", () => {
384
+ assert.deepEqual(textWordRange("hello world foo", 6, "i"), {
385
+ start: 6,
386
+ end: 11,
387
+ });
388
+ });
389
+
390
+ it("selects whitespace when cursor is on whitespace", () => {
391
+ assert.deepEqual(textWordRange("hello world", 6, "i"), {
392
+ start: 5,
393
+ end: 8,
394
+ });
395
+ });
396
+
397
+ it("selects a run of punctuation", () => {
398
+ assert.deepEqual(textWordRange("foo...bar", 3, "i"), { start: 3, end: 6 });
399
+ });
400
+
401
+ it("returns null for empty string", () => {
402
+ assert.equal(textWordRange("", 0, "i"), null);
403
+ });
404
+
405
+ it("returns null when char is out of bounds", () => {
406
+ assert.equal(textWordRange("hello", 10, "i"), null);
407
+ });
408
+
409
+ it("selects a single-char word", () => {
410
+ assert.deepEqual(textWordRange("a b c", 2, "i"), { start: 2, end: 3 });
411
+ });
412
+
413
+ it("selects word with underscores", () => {
414
+ assert.deepEqual(textWordRange("foo_bar baz", 3, "i"), {
415
+ start: 0,
416
+ end: 7,
417
+ });
418
+ });
419
+
420
+ it("selects word with digits", () => {
421
+ assert.deepEqual(textWordRange("abc123 xyz", 4, "i"), { start: 0, end: 6 });
422
+ });
423
+
424
+ it("selects single whitespace char between words", () => {
425
+ assert.deepEqual(textWordRange("hello world", 5, "i"), {
426
+ start: 5,
427
+ end: 6,
428
+ });
429
+ });
430
+
431
+ it("selects last word on line", () => {
432
+ assert.deepEqual(textWordRange("foo bar", 4, "i"), { start: 4, end: 7 });
433
+ });
434
+
435
+ it("handles cursor at last char of line", () => {
436
+ assert.deepEqual(textWordRange("hello", 4, "i"), { start: 0, end: 5 });
437
+ });
438
+
439
+ it("selects a single-char line", () => {
440
+ assert.deepEqual(textWordRange("x", 0, "i"), { start: 0, end: 1 });
441
+ });
442
+ });
443
+
444
+ describe("textQuoteRange", () => {
445
+ it("selects content between double quotes", () => {
446
+ assert.deepEqual(textQuoteRange('say "hello" ok', 6, '"', "i"), {
447
+ start: 5,
448
+ end: 10,
449
+ });
450
+ });
451
+
452
+ it("works when cursor is on the opening quote", () => {
453
+ assert.deepEqual(textQuoteRange('say "hello" ok', 4, '"', "i"), {
454
+ start: 5,
455
+ end: 10,
456
+ });
457
+ });
458
+
459
+ it("works when cursor is on the closing quote", () => {
460
+ assert.deepEqual(textQuoteRange('say "hello" ok', 10, '"', "i"), {
461
+ start: 5,
462
+ end: 10,
463
+ });
464
+ });
465
+
466
+ it("returns null when no quotes exist", () => {
467
+ assert.equal(textQuoteRange("no quotes here", 3, '"', "i"), null);
468
+ });
469
+
470
+ it("returns null when only one quote exists", () => {
471
+ assert.equal(textQuoteRange('say "hello', 6, '"', "i"), null);
472
+ });
473
+
474
+ it("returns null when cursor is outside the quotes", () => {
475
+ assert.equal(textQuoteRange('say "hello" ok', 12, '"', "i"), null);
476
+ });
477
+
478
+ it("works with single quotes", () => {
479
+ assert.deepEqual(textQuoteRange("say 'fine' ok", 7, "'", "i"), {
480
+ start: 5,
481
+ end: 9,
482
+ });
483
+ });
484
+
485
+ it("selects empty content between adjacent quotes", () => {
486
+ assert.deepEqual(textQuoteRange('foo "" bar', 4, '"', "i"), {
487
+ start: 5,
488
+ end: 5,
489
+ });
490
+ });
491
+ });
492
+
493
+ describe("textBracketRange", () => {
494
+ it("selects content inside parentheses", () => {
495
+ assert.deepEqual(textBracketRange("foo(bar)baz", 5, "(", "i"), {
496
+ start: 4,
497
+ end: 7,
498
+ });
499
+ });
500
+
501
+ it("works with closing bracket as argument", () => {
502
+ assert.deepEqual(textBracketRange("foo(bar)baz", 5, ")", "i"), {
503
+ start: 4,
504
+ end: 7,
505
+ });
506
+ });
507
+
508
+ it("selects content inside curly braces", () => {
509
+ assert.deepEqual(textBracketRange("if {yes} no", 5, "{", "i"), {
510
+ start: 4,
511
+ end: 7,
512
+ });
513
+ });
514
+
515
+ it("selects content inside square brackets", () => {
516
+ assert.deepEqual(textBracketRange("a[bc]d", 2, "[", "i"), {
517
+ start: 2,
518
+ end: 4,
519
+ });
520
+ });
521
+
522
+ it("handles nested brackets", () => {
523
+ assert.deepEqual(textBracketRange("(a(b)c)", 3, "(", "i"), {
524
+ start: 3,
525
+ end: 4,
526
+ });
527
+ });
528
+
529
+ it("handles nested brackets from outer position", () => {
530
+ assert.deepEqual(textBracketRange("(a(b)c)", 1, "(", "i"), {
531
+ start: 1,
532
+ end: 6,
533
+ });
534
+ });
535
+
536
+ it("returns null when no matching brackets", () => {
537
+ assert.equal(textBracketRange("no brackets", 3, "(", "i"), null);
538
+ });
539
+
540
+ it("returns null when cursor is outside brackets", () => {
541
+ assert.equal(textBracketRange("x (foo) y", 0, "(", "i"), null);
542
+ });
543
+
544
+ it("selects empty content between adjacent brackets", () => {
545
+ assert.deepEqual(textBracketRange("foo()bar", 4, "(", "i"), {
546
+ start: 4,
547
+ end: 4,
548
+ });
549
+ });
550
+
551
+ it("works when cursor is on the opening bracket", () => {
552
+ assert.deepEqual(textBracketRange("(hello)", 0, "(", "i"), {
553
+ start: 1,
554
+ end: 6,
555
+ });
556
+ });
557
+
558
+ it("works when cursor is on the closing bracket", () => {
559
+ assert.deepEqual(textBracketRange("(hello)", 6, ")", "i"), {
560
+ start: 1,
561
+ end: 6,
562
+ });
563
+ });
564
+ });
565
+
566
+ describe("getVisualSelection", () => {
567
+ const rep = makeRep(["hello", "world", "foo"]);
568
+
569
+ it("char mode with anchor before cursor returns [anchor, cursor]", () => {
570
+ const result = getVisualSelection("char", [0, 1], [0, 4], rep);
571
+ assert.deepEqual(result, [
572
+ [0, 1],
573
+ [0, 4],
574
+ ]);
575
+ });
576
+
577
+ it("char mode with anchor after cursor returns [cursor, anchor]", () => {
578
+ const result = getVisualSelection("char", [0, 4], [0, 1], rep);
579
+ assert.deepEqual(result, [
580
+ [0, 1],
581
+ [0, 4],
582
+ ]);
583
+ });
584
+
585
+ it("char mode across lines orders by line", () => {
586
+ const result = getVisualSelection("char", [1, 2], [0, 3], rep);
587
+ assert.deepEqual(result, [
588
+ [0, 3],
589
+ [1, 2],
590
+ ]);
591
+ });
592
+
593
+ it("line mode selects full lines", () => {
594
+ const result = getVisualSelection("line", [0, 0], [1, 0], rep);
595
+ assert.deepEqual(result, [
596
+ [0, 0],
597
+ [2, 0],
598
+ ]);
599
+ });
600
+
601
+ it("line mode on last line uses end of line", () => {
602
+ const result = getVisualSelection("line", [2, 0], [2, 0], rep);
603
+ assert.deepEqual(result, [
604
+ [2, 0],
605
+ [2, 3],
606
+ ]);
607
+ });
608
+
609
+ it("line mode with reversed anchor/cursor", () => {
610
+ const result = getVisualSelection("line", [1, 0], [0, 0], rep);
611
+ assert.deepEqual(result, [
612
+ [0, 0],
613
+ [2, 0],
614
+ ]);
615
+ });
616
+ });
617
+
618
+ describe("getTextInRange", () => {
619
+ const rep = makeRep(["hello", "world", "foo bar"]);
620
+
621
+ it("extracts text within a single line", () => {
622
+ assert.equal(getTextInRange(rep, [0, 1], [0, 4]), "ell");
623
+ });
624
+
625
+ it("extracts text across two lines", () => {
626
+ assert.equal(getTextInRange(rep, [0, 3], [1, 3]), "lo\nwor");
627
+ });
628
+
629
+ it("extracts text across three lines", () => {
630
+ assert.equal(getTextInRange(rep, [0, 3], [2, 3]), "lo\nworld\nfoo");
631
+ });
632
+
633
+ it("extracts full line when range covers it", () => {
634
+ assert.equal(getTextInRange(rep, [1, 0], [1, 5]), "world");
635
+ });
636
+ });
637
+
638
+ describe("paragraphForward", () => {
639
+ const rep = makeRep(["hello", "world", "", "foo", "bar", "", "baz"]);
640
+
641
+ it("jumps to next blank line", () => {
642
+ assert.equal(paragraphForward(rep, 0, 1), 2);
643
+ });
644
+
645
+ it("jumps multiple paragraphs with count", () => {
646
+ assert.equal(paragraphForward(rep, 0, 2), 5);
647
+ });
648
+
649
+ it("stops at last line when not enough blank lines", () => {
650
+ assert.equal(paragraphForward(rep, 0, 5), 6);
651
+ });
652
+
653
+ it("jumps from blank line to next blank line", () => {
654
+ assert.equal(paragraphForward(rep, 2, 1), 5);
655
+ });
656
+ });
657
+
658
+ describe("paragraphBackward", () => {
659
+ const rep = makeRep(["hello", "world", "", "foo", "bar", "", "baz"]);
660
+
661
+ it("jumps to previous blank line", () => {
662
+ assert.equal(paragraphBackward(rep, 6, 1), 5);
663
+ });
664
+
665
+ it("jumps multiple paragraphs with count", () => {
666
+ assert.equal(paragraphBackward(rep, 6, 2), 2);
667
+ });
668
+
669
+ it("stops at first line when not enough blank lines", () => {
670
+ assert.equal(paragraphBackward(rep, 6, 5), 0);
671
+ });
672
+
673
+ it("jumps from blank line to previous blank line", () => {
674
+ assert.equal(paragraphBackward(rep, 5, 1), 2);
675
+ });
676
+ });