ep_vim 0.12.1 → 0.12.3
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.
- package/README.md +6 -2
- package/package.json +2 -2
- package/static/js/index.js +27 -5
- package/static/js/insert.test.js +198 -0
- package/static/js/misc.test.js +363 -0
- package/static/js/motions.test.js +1377 -0
- package/static/js/operators.test.js +1118 -0
- package/static/js/paste_registers.test.js +778 -0
- package/static/js/search.test.js +234 -0
- package/static/js/visual.test.js +382 -0
- package/static/js/index.test.js +0 -4023
|
@@ -0,0 +1,1377 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { describe, it, beforeEach } = require("node:test");
|
|
4
|
+
const assert = require("node:assert/strict");
|
|
5
|
+
|
|
6
|
+
// Mock navigator for clipboard operations
|
|
7
|
+
global.navigator = {
|
|
8
|
+
clipboard: {
|
|
9
|
+
writeText: () => Promise.resolve(),
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
_state: state,
|
|
15
|
+
_handleKey: handleKey,
|
|
16
|
+
_commands: commands,
|
|
17
|
+
_parameterized: parameterized,
|
|
18
|
+
_setVimEnabled: setVimEnabled,
|
|
19
|
+
_setUseCtrlKeys: setUseCtrlKeys,
|
|
20
|
+
aceKeyEvent,
|
|
21
|
+
} = require("./index.js");
|
|
22
|
+
|
|
23
|
+
const makeRep = (lines) => ({
|
|
24
|
+
lines: {
|
|
25
|
+
length: () => lines.length,
|
|
26
|
+
atIndex: (n) => ({ text: lines[n] }),
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const makeMockEditorInfo = () => {
|
|
31
|
+
const calls = [];
|
|
32
|
+
return {
|
|
33
|
+
editorInfo: {
|
|
34
|
+
ace_inCallStackIfNecessary: (_name, fn) => fn(),
|
|
35
|
+
ace_performSelectionChange: (start, end, _flag) => {
|
|
36
|
+
calls.push({ type: "select", start, end });
|
|
37
|
+
},
|
|
38
|
+
ace_updateBrowserSelectionFromRep: () => {},
|
|
39
|
+
ace_performDocumentReplaceRange: (start, end, newText) => {
|
|
40
|
+
calls.push({ type: "replace", start, end, newText });
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
calls,
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
const resetState = () => {
|
|
50
|
+
state.mode = "normal";
|
|
51
|
+
state.pendingKey = null;
|
|
52
|
+
state.pendingCount = null;
|
|
53
|
+
state.countBuffer = "";
|
|
54
|
+
state.register = null;
|
|
55
|
+
state.namedRegisters = {};
|
|
56
|
+
state.pendingRegister = null;
|
|
57
|
+
state.awaitingRegister = false;
|
|
58
|
+
state.marks = {};
|
|
59
|
+
state.lastCharSearch = null;
|
|
60
|
+
state.visualAnchor = null;
|
|
61
|
+
state.visualCursor = null;
|
|
62
|
+
state.editorDoc = null;
|
|
63
|
+
state.currentRep = null;
|
|
64
|
+
state.desiredColumn = null;
|
|
65
|
+
state.lastCommand = null;
|
|
66
|
+
state.searchMode = false;
|
|
67
|
+
state.searchBuffer = "";
|
|
68
|
+
state.searchDirection = null;
|
|
69
|
+
state.lastSearch = null;
|
|
70
|
+
state.lastVisualSelection = null;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
describe("char search repeat (semicolon)", () => {
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
// Reset state before each test
|
|
76
|
+
state.mode = "normal";
|
|
77
|
+
state.pendingKey = null;
|
|
78
|
+
state.pendingCount = null;
|
|
79
|
+
state.countBuffer = "";
|
|
80
|
+
state.register = null;
|
|
81
|
+
state.marks = {};
|
|
82
|
+
state.lastCharSearch = null;
|
|
83
|
+
state.visualAnchor = null;
|
|
84
|
+
state.visualCursor = null;
|
|
85
|
+
state.editorDoc = null;
|
|
86
|
+
state.currentRep = null;
|
|
87
|
+
state.desiredColumn = null;
|
|
88
|
+
state.lastCommand = null;
|
|
89
|
+
state.searchMode = false;
|
|
90
|
+
state.searchBuffer = "";
|
|
91
|
+
state.searchDirection = null;
|
|
92
|
+
state.lastSearch = null;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("repeats forward char search with ; in normal mode", () => {
|
|
96
|
+
const rep = makeRep(["hello world"]);
|
|
97
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
98
|
+
|
|
99
|
+
// Set up a prior 'f' search for 'o' at position 0
|
|
100
|
+
state.lastCharSearch = { direction: "f", target: "o" };
|
|
101
|
+
|
|
102
|
+
// Call ; to repeat the search
|
|
103
|
+
const ctx = {
|
|
104
|
+
rep,
|
|
105
|
+
editorInfo,
|
|
106
|
+
line: 0,
|
|
107
|
+
char: 0,
|
|
108
|
+
lineText: "hello world",
|
|
109
|
+
count: 1,
|
|
110
|
+
};
|
|
111
|
+
commands.normal[";"](ctx);
|
|
112
|
+
|
|
113
|
+
// Should have moved to first 'o' at position 4
|
|
114
|
+
assert.equal(
|
|
115
|
+
calls.length,
|
|
116
|
+
1,
|
|
117
|
+
`Expected 1 call, got ${calls.length}. Calls: ${JSON.stringify(calls)}`,
|
|
118
|
+
);
|
|
119
|
+
assert.deepEqual(calls[0].start, [0, 4]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("does nothing when no prior char search", () => {
|
|
123
|
+
const rep = makeRep(["hello world"]);
|
|
124
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
125
|
+
|
|
126
|
+
state.lastCharSearch = null;
|
|
127
|
+
|
|
128
|
+
const ctx = { rep, editorInfo, line: 0, char: 0, lineText: "hello world" };
|
|
129
|
+
commands.normal[";"](ctx);
|
|
130
|
+
|
|
131
|
+
// Should not move cursor
|
|
132
|
+
assert.equal(calls.length, 0);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("repeats t search (till char) with ;", () => {
|
|
136
|
+
const rep = makeRep(["hello world"]);
|
|
137
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
138
|
+
|
|
139
|
+
state.lastCharSearch = { direction: "t", target: "o" };
|
|
140
|
+
|
|
141
|
+
const ctx = {
|
|
142
|
+
rep,
|
|
143
|
+
editorInfo,
|
|
144
|
+
line: 0,
|
|
145
|
+
char: 0,
|
|
146
|
+
lineText: "hello world",
|
|
147
|
+
count: 1,
|
|
148
|
+
};
|
|
149
|
+
commands.normal[";"](ctx);
|
|
150
|
+
|
|
151
|
+
// 't' finds 'o' at position 4, but lands one before (position 3)
|
|
152
|
+
assert.equal(calls.length, 1);
|
|
153
|
+
assert.deepEqual(calls[0].start, [0, 3]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("repeats with count", () => {
|
|
157
|
+
const rep = makeRep(["abacada"]);
|
|
158
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
159
|
+
|
|
160
|
+
state.lastCharSearch = { direction: "f", target: "a" };
|
|
161
|
+
|
|
162
|
+
const ctx = {
|
|
163
|
+
rep,
|
|
164
|
+
editorInfo,
|
|
165
|
+
line: 0,
|
|
166
|
+
char: 0,
|
|
167
|
+
lineText: "abacada",
|
|
168
|
+
count: 2,
|
|
169
|
+
};
|
|
170
|
+
commands.normal[";"](ctx);
|
|
171
|
+
|
|
172
|
+
// With count 2, should find the 2nd 'a' after position 0, which is at position 4
|
|
173
|
+
assert.equal(calls.length, 1);
|
|
174
|
+
assert.deepEqual(calls[0].start, [0, 4]);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("char search reverse (comma)", () => {
|
|
179
|
+
beforeEach(() => {
|
|
180
|
+
state.mode = "normal";
|
|
181
|
+
state.pendingKey = null;
|
|
182
|
+
state.pendingCount = null;
|
|
183
|
+
state.countBuffer = "";
|
|
184
|
+
state.register = null;
|
|
185
|
+
state.marks = {};
|
|
186
|
+
state.lastCharSearch = null;
|
|
187
|
+
state.visualAnchor = null;
|
|
188
|
+
state.visualCursor = null;
|
|
189
|
+
state.editorDoc = null;
|
|
190
|
+
state.currentRep = null;
|
|
191
|
+
state.desiredColumn = null;
|
|
192
|
+
state.lastCommand = null;
|
|
193
|
+
state.searchMode = false;
|
|
194
|
+
state.searchBuffer = "";
|
|
195
|
+
state.searchDirection = null;
|
|
196
|
+
state.lastSearch = null;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("reverses f search to F with ,", () => {
|
|
200
|
+
const rep = makeRep(["hello world"]);
|
|
201
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
202
|
+
|
|
203
|
+
state.lastCharSearch = { direction: "f", target: "o" };
|
|
204
|
+
|
|
205
|
+
// Start at position 7 and search backward
|
|
206
|
+
const ctx = {
|
|
207
|
+
rep,
|
|
208
|
+
editorInfo,
|
|
209
|
+
line: 0,
|
|
210
|
+
char: 7,
|
|
211
|
+
lineText: "hello world",
|
|
212
|
+
count: 1,
|
|
213
|
+
};
|
|
214
|
+
commands.normal[","](ctx);
|
|
215
|
+
|
|
216
|
+
// 'F' search from position 7 finds 'o' at position 4 (going backward)
|
|
217
|
+
assert.equal(calls.length, 1);
|
|
218
|
+
assert.deepEqual(calls[0].start, [0, 4]);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("reverses t search to T with ,", () => {
|
|
222
|
+
const rep = makeRep(["hello world"]);
|
|
223
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
224
|
+
|
|
225
|
+
state.lastCharSearch = { direction: "t", target: "o" };
|
|
226
|
+
|
|
227
|
+
const ctx = {
|
|
228
|
+
rep,
|
|
229
|
+
editorInfo,
|
|
230
|
+
line: 0,
|
|
231
|
+
char: 7,
|
|
232
|
+
lineText: "hello world",
|
|
233
|
+
count: 1,
|
|
234
|
+
};
|
|
235
|
+
commands.normal[","](ctx);
|
|
236
|
+
|
|
237
|
+
// 'T' finds 'o' at position 4, then lands one after (position 5)
|
|
238
|
+
assert.equal(calls.length, 1);
|
|
239
|
+
assert.deepEqual(calls[0].start, [0, 5]);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("does nothing when no prior char search", () => {
|
|
243
|
+
const rep = makeRep(["hello world"]);
|
|
244
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
245
|
+
|
|
246
|
+
state.lastCharSearch = null;
|
|
247
|
+
|
|
248
|
+
const ctx = {
|
|
249
|
+
rep,
|
|
250
|
+
editorInfo,
|
|
251
|
+
line: 0,
|
|
252
|
+
char: 7,
|
|
253
|
+
lineText: "hello world",
|
|
254
|
+
count: 1,
|
|
255
|
+
};
|
|
256
|
+
commands.normal[","](ctx);
|
|
257
|
+
|
|
258
|
+
assert.equal(calls.length, 0);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe("basic motions", () => {
|
|
263
|
+
beforeEach(() => {
|
|
264
|
+
state.mode = "normal";
|
|
265
|
+
state.pendingKey = null;
|
|
266
|
+
state.pendingCount = null;
|
|
267
|
+
state.countBuffer = "";
|
|
268
|
+
state.register = null;
|
|
269
|
+
state.marks = {};
|
|
270
|
+
state.lastCharSearch = null;
|
|
271
|
+
state.visualAnchor = null;
|
|
272
|
+
state.visualCursor = null;
|
|
273
|
+
state.editorDoc = null;
|
|
274
|
+
state.currentRep = null;
|
|
275
|
+
state.desiredColumn = null;
|
|
276
|
+
state.lastCommand = null;
|
|
277
|
+
state.searchMode = false;
|
|
278
|
+
state.searchBuffer = "";
|
|
279
|
+
state.searchDirection = null;
|
|
280
|
+
state.lastSearch = null;
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("h moves cursor left", () => {
|
|
284
|
+
const rep = makeRep(["hello"]);
|
|
285
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
286
|
+
|
|
287
|
+
const ctx = {
|
|
288
|
+
rep,
|
|
289
|
+
editorInfo,
|
|
290
|
+
line: 0,
|
|
291
|
+
char: 3,
|
|
292
|
+
lineText: "hello",
|
|
293
|
+
count: 1,
|
|
294
|
+
};
|
|
295
|
+
commands.normal["h"](ctx);
|
|
296
|
+
|
|
297
|
+
assert.equal(calls.length, 1);
|
|
298
|
+
assert.deepEqual(calls[0].start, [0, 2]);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("h with count moves left multiple times", () => {
|
|
302
|
+
const rep = makeRep(["hello"]);
|
|
303
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
304
|
+
|
|
305
|
+
const ctx = {
|
|
306
|
+
rep,
|
|
307
|
+
editorInfo,
|
|
308
|
+
line: 0,
|
|
309
|
+
char: 4,
|
|
310
|
+
lineText: "hello",
|
|
311
|
+
count: 3,
|
|
312
|
+
};
|
|
313
|
+
commands.normal["h"](ctx);
|
|
314
|
+
|
|
315
|
+
assert.equal(calls.length, 1);
|
|
316
|
+
assert.deepEqual(calls[0].start, [0, 1]);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("l moves cursor right", () => {
|
|
320
|
+
const rep = makeRep(["hello"]);
|
|
321
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
322
|
+
|
|
323
|
+
const ctx = {
|
|
324
|
+
rep,
|
|
325
|
+
editorInfo,
|
|
326
|
+
line: 0,
|
|
327
|
+
char: 2,
|
|
328
|
+
lineText: "hello",
|
|
329
|
+
count: 1,
|
|
330
|
+
};
|
|
331
|
+
commands.normal["l"](ctx);
|
|
332
|
+
|
|
333
|
+
assert.equal(calls.length, 1);
|
|
334
|
+
assert.deepEqual(calls[0].start, [0, 3]);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("l with count moves right multiple times", () => {
|
|
338
|
+
const rep = makeRep(["hello"]);
|
|
339
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
340
|
+
|
|
341
|
+
const ctx = {
|
|
342
|
+
rep,
|
|
343
|
+
editorInfo,
|
|
344
|
+
line: 0,
|
|
345
|
+
char: 0,
|
|
346
|
+
lineText: "hello",
|
|
347
|
+
count: 2,
|
|
348
|
+
};
|
|
349
|
+
commands.normal["l"](ctx);
|
|
350
|
+
|
|
351
|
+
assert.equal(calls.length, 1);
|
|
352
|
+
assert.deepEqual(calls[0].start, [0, 2]);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("0 moves to line start", () => {
|
|
356
|
+
const rep = makeRep(["hello"]);
|
|
357
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
358
|
+
|
|
359
|
+
const ctx = {
|
|
360
|
+
rep,
|
|
361
|
+
editorInfo,
|
|
362
|
+
line: 0,
|
|
363
|
+
char: 3,
|
|
364
|
+
lineText: "hello",
|
|
365
|
+
count: 1,
|
|
366
|
+
};
|
|
367
|
+
commands.normal["0"](ctx);
|
|
368
|
+
|
|
369
|
+
assert.equal(calls.length, 1);
|
|
370
|
+
assert.deepEqual(calls[0].start, [0, 0]);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("$ moves to line end", () => {
|
|
374
|
+
const rep = makeRep(["hello"]);
|
|
375
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
376
|
+
|
|
377
|
+
const ctx = {
|
|
378
|
+
rep,
|
|
379
|
+
editorInfo,
|
|
380
|
+
line: 0,
|
|
381
|
+
char: 0,
|
|
382
|
+
lineText: "hello",
|
|
383
|
+
count: 1,
|
|
384
|
+
};
|
|
385
|
+
commands.normal["$"](ctx);
|
|
386
|
+
|
|
387
|
+
assert.equal(calls.length, 1);
|
|
388
|
+
assert.deepEqual(calls[0].start, [0, 4]);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("^ moves to first non-blank", () => {
|
|
392
|
+
const rep = makeRep([" hello"]);
|
|
393
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
394
|
+
|
|
395
|
+
const ctx = {
|
|
396
|
+
rep,
|
|
397
|
+
editorInfo,
|
|
398
|
+
line: 0,
|
|
399
|
+
char: 0,
|
|
400
|
+
lineText: " hello",
|
|
401
|
+
count: 1,
|
|
402
|
+
};
|
|
403
|
+
commands.normal["^"](ctx);
|
|
404
|
+
|
|
405
|
+
assert.equal(calls.length, 1);
|
|
406
|
+
assert.deepEqual(calls[0].start, [0, 2]);
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
describe("marks", () => {
|
|
411
|
+
beforeEach(() => {
|
|
412
|
+
state.mode = "normal";
|
|
413
|
+
state.pendingKey = null;
|
|
414
|
+
state.pendingCount = null;
|
|
415
|
+
state.countBuffer = "";
|
|
416
|
+
state.register = null;
|
|
417
|
+
state.marks = {};
|
|
418
|
+
state.lastCharSearch = null;
|
|
419
|
+
state.visualAnchor = null;
|
|
420
|
+
state.visualCursor = null;
|
|
421
|
+
state.editorDoc = null;
|
|
422
|
+
state.currentRep = null;
|
|
423
|
+
state.desiredColumn = null;
|
|
424
|
+
state.lastCommand = null;
|
|
425
|
+
state.searchMode = false;
|
|
426
|
+
state.searchBuffer = "";
|
|
427
|
+
state.searchDirection = null;
|
|
428
|
+
state.lastSearch = null;
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("m sets a mark", () => {
|
|
432
|
+
const ctx = { rep: makeRep([]), line: 5, char: 10 };
|
|
433
|
+
|
|
434
|
+
parameterized["m"]("a", ctx);
|
|
435
|
+
|
|
436
|
+
assert.deepEqual(state.marks["a"], [5, 10]);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it("' jumps to mark (line start)", () => {
|
|
440
|
+
const rep = makeRep(["line0", "line1", "line2"]);
|
|
441
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
442
|
+
|
|
443
|
+
state.marks["a"] = [1, 3];
|
|
444
|
+
|
|
445
|
+
const ctx = {
|
|
446
|
+
rep,
|
|
447
|
+
editorInfo,
|
|
448
|
+
line: 0,
|
|
449
|
+
char: 0,
|
|
450
|
+
lineText: "line0",
|
|
451
|
+
count: 1,
|
|
452
|
+
};
|
|
453
|
+
parameterized["'"]("a", ctx);
|
|
454
|
+
|
|
455
|
+
assert.equal(calls.length, 1);
|
|
456
|
+
assert.deepEqual(calls[0].start, [1, 0]);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it("` jumps to mark (exact position)", () => {
|
|
460
|
+
const rep = makeRep(["line0", "line1", "line2"]);
|
|
461
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
462
|
+
|
|
463
|
+
state.marks["b"] = [1, 3];
|
|
464
|
+
|
|
465
|
+
const ctx = {
|
|
466
|
+
rep,
|
|
467
|
+
editorInfo,
|
|
468
|
+
line: 0,
|
|
469
|
+
char: 0,
|
|
470
|
+
lineText: "line0",
|
|
471
|
+
count: 1,
|
|
472
|
+
};
|
|
473
|
+
parameterized["`"]("b", ctx);
|
|
474
|
+
|
|
475
|
+
assert.equal(calls.length, 1);
|
|
476
|
+
assert.deepEqual(calls[0].start, [1, 3]);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("' does nothing with nonexistent mark", () => {
|
|
480
|
+
const rep = makeRep(["line0"]);
|
|
481
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
482
|
+
|
|
483
|
+
state.marks = {};
|
|
484
|
+
|
|
485
|
+
const ctx = {
|
|
486
|
+
rep,
|
|
487
|
+
editorInfo,
|
|
488
|
+
line: 0,
|
|
489
|
+
char: 0,
|
|
490
|
+
lineText: "line0",
|
|
491
|
+
count: 1,
|
|
492
|
+
};
|
|
493
|
+
parameterized["'"]("z", ctx);
|
|
494
|
+
|
|
495
|
+
assert.equal(calls.length, 0);
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
describe("line navigation", () => {
|
|
500
|
+
beforeEach(() => {
|
|
501
|
+
state.mode = "normal";
|
|
502
|
+
state.pendingKey = null;
|
|
503
|
+
state.pendingCount = null;
|
|
504
|
+
state.countBuffer = "";
|
|
505
|
+
state.register = null;
|
|
506
|
+
state.marks = {};
|
|
507
|
+
state.lastCharSearch = null;
|
|
508
|
+
state.visualAnchor = null;
|
|
509
|
+
state.visualCursor = null;
|
|
510
|
+
state.editorDoc = null;
|
|
511
|
+
state.currentRep = null;
|
|
512
|
+
state.desiredColumn = null;
|
|
513
|
+
state.lastCommand = null;
|
|
514
|
+
state.searchMode = false;
|
|
515
|
+
state.searchBuffer = "";
|
|
516
|
+
state.searchDirection = null;
|
|
517
|
+
state.lastSearch = null;
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it("j moves down one line", () => {
|
|
521
|
+
const rep = makeRep(["line0", "line1", "line2"]);
|
|
522
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
523
|
+
|
|
524
|
+
const ctx = {
|
|
525
|
+
rep,
|
|
526
|
+
editorInfo,
|
|
527
|
+
line: 0,
|
|
528
|
+
char: 2,
|
|
529
|
+
lineText: "line0",
|
|
530
|
+
count: 1,
|
|
531
|
+
};
|
|
532
|
+
commands.normal["j"](ctx);
|
|
533
|
+
|
|
534
|
+
assert.equal(calls.length, 1);
|
|
535
|
+
assert.deepEqual(calls[0].start, [1, 2]);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it("j with count moves down multiple lines", () => {
|
|
539
|
+
const rep = makeRep(["line0", "line1", "line2", "line3"]);
|
|
540
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
541
|
+
|
|
542
|
+
const ctx = {
|
|
543
|
+
rep,
|
|
544
|
+
editorInfo,
|
|
545
|
+
line: 0,
|
|
546
|
+
char: 1,
|
|
547
|
+
lineText: "line0",
|
|
548
|
+
count: 2,
|
|
549
|
+
};
|
|
550
|
+
commands.normal["j"](ctx);
|
|
551
|
+
|
|
552
|
+
assert.equal(calls.length, 1);
|
|
553
|
+
assert.deepEqual(calls[0].start, [2, 1]);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it("k moves up one line", () => {
|
|
557
|
+
const rep = makeRep(["line0", "line1", "line2"]);
|
|
558
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
559
|
+
|
|
560
|
+
const ctx = {
|
|
561
|
+
rep,
|
|
562
|
+
editorInfo,
|
|
563
|
+
line: 2,
|
|
564
|
+
char: 2,
|
|
565
|
+
lineText: "line2",
|
|
566
|
+
count: 1,
|
|
567
|
+
};
|
|
568
|
+
commands.normal["k"](ctx);
|
|
569
|
+
|
|
570
|
+
assert.equal(calls.length, 1);
|
|
571
|
+
assert.deepEqual(calls[0].start, [1, 2]);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it("gg goes to first line", () => {
|
|
575
|
+
const rep = makeRep(["line0", "line1", "line2"]);
|
|
576
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
577
|
+
|
|
578
|
+
const ctx = {
|
|
579
|
+
rep,
|
|
580
|
+
editorInfo,
|
|
581
|
+
line: 2,
|
|
582
|
+
char: 3,
|
|
583
|
+
lineText: "line2",
|
|
584
|
+
count: 1,
|
|
585
|
+
hasCount: false,
|
|
586
|
+
};
|
|
587
|
+
commands.normal["gg"](ctx);
|
|
588
|
+
|
|
589
|
+
assert.equal(calls.length, 1);
|
|
590
|
+
assert.deepEqual(calls[0].start, [0, 0]);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it("G goes to last line", () => {
|
|
594
|
+
const rep = makeRep(["line0", "line1", "line2"]);
|
|
595
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
596
|
+
|
|
597
|
+
const ctx = {
|
|
598
|
+
rep,
|
|
599
|
+
editorInfo,
|
|
600
|
+
line: 0,
|
|
601
|
+
char: 2,
|
|
602
|
+
lineText: "line0",
|
|
603
|
+
count: 1,
|
|
604
|
+
hasCount: false,
|
|
605
|
+
};
|
|
606
|
+
commands.normal["G"](ctx);
|
|
607
|
+
|
|
608
|
+
assert.equal(calls.length, 1);
|
|
609
|
+
assert.deepEqual(calls[0].start, [2, 0]);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it("G with count goes to specific line", () => {
|
|
613
|
+
const rep = makeRep(["line0", "line1", "line2", "line3"]);
|
|
614
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
615
|
+
|
|
616
|
+
const ctx = {
|
|
617
|
+
rep,
|
|
618
|
+
editorInfo,
|
|
619
|
+
line: 0,
|
|
620
|
+
char: 0,
|
|
621
|
+
lineText: "line0",
|
|
622
|
+
count: 3,
|
|
623
|
+
hasCount: true,
|
|
624
|
+
};
|
|
625
|
+
commands.normal["G"](ctx);
|
|
626
|
+
|
|
627
|
+
assert.equal(calls.length, 1);
|
|
628
|
+
assert.deepEqual(calls[0].start, [2, 0]);
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
describe("word motions", () => {
|
|
633
|
+
beforeEach(() => {
|
|
634
|
+
state.mode = "normal";
|
|
635
|
+
state.pendingKey = null;
|
|
636
|
+
state.pendingCount = null;
|
|
637
|
+
state.countBuffer = "";
|
|
638
|
+
state.register = null;
|
|
639
|
+
state.marks = {};
|
|
640
|
+
state.lastCharSearch = null;
|
|
641
|
+
state.visualAnchor = null;
|
|
642
|
+
state.visualCursor = null;
|
|
643
|
+
state.editorDoc = null;
|
|
644
|
+
state.currentRep = null;
|
|
645
|
+
state.desiredColumn = null;
|
|
646
|
+
state.lastCommand = null;
|
|
647
|
+
state.searchMode = false;
|
|
648
|
+
state.searchBuffer = "";
|
|
649
|
+
state.searchDirection = null;
|
|
650
|
+
state.lastSearch = null;
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it("w moves to next word", () => {
|
|
654
|
+
const rep = makeRep(["hello world foo"]);
|
|
655
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
656
|
+
|
|
657
|
+
const ctx = {
|
|
658
|
+
rep,
|
|
659
|
+
editorInfo,
|
|
660
|
+
line: 0,
|
|
661
|
+
char: 0,
|
|
662
|
+
lineText: "hello world foo",
|
|
663
|
+
count: 1,
|
|
664
|
+
};
|
|
665
|
+
commands.normal["w"](ctx);
|
|
666
|
+
|
|
667
|
+
assert.equal(calls.length, 1);
|
|
668
|
+
assert.deepEqual(calls[0].start, [0, 6]);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it("w with count moves multiple words", () => {
|
|
672
|
+
const rep = makeRep(["hello world foo"]);
|
|
673
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
674
|
+
|
|
675
|
+
const ctx = {
|
|
676
|
+
rep,
|
|
677
|
+
editorInfo,
|
|
678
|
+
line: 0,
|
|
679
|
+
char: 0,
|
|
680
|
+
lineText: "hello world foo",
|
|
681
|
+
count: 2,
|
|
682
|
+
};
|
|
683
|
+
commands.normal["w"](ctx);
|
|
684
|
+
|
|
685
|
+
assert.equal(calls.length, 1);
|
|
686
|
+
assert.deepEqual(calls[0].start, [0, 12]);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it("b moves to previous word", () => {
|
|
690
|
+
const rep = makeRep(["hello world foo"]);
|
|
691
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
692
|
+
|
|
693
|
+
const ctx = {
|
|
694
|
+
rep,
|
|
695
|
+
editorInfo,
|
|
696
|
+
line: 0,
|
|
697
|
+
char: 12,
|
|
698
|
+
lineText: "hello world foo",
|
|
699
|
+
count: 1,
|
|
700
|
+
};
|
|
701
|
+
commands.normal["b"](ctx);
|
|
702
|
+
|
|
703
|
+
assert.equal(calls.length, 1);
|
|
704
|
+
assert.deepEqual(calls[0].start, [0, 6]);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it("e moves to end of word", () => {
|
|
708
|
+
const rep = makeRep(["hello world foo"]);
|
|
709
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
710
|
+
|
|
711
|
+
const ctx = {
|
|
712
|
+
rep,
|
|
713
|
+
editorInfo,
|
|
714
|
+
line: 0,
|
|
715
|
+
char: 0,
|
|
716
|
+
lineText: "hello world foo",
|
|
717
|
+
count: 1,
|
|
718
|
+
};
|
|
719
|
+
commands.normal["e"](ctx);
|
|
720
|
+
|
|
721
|
+
assert.equal(calls.length, 1);
|
|
722
|
+
assert.deepEqual(calls[0].start, [0, 4]);
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
describe("char motions (f/F/t/T)", () => {
|
|
727
|
+
beforeEach(() => {
|
|
728
|
+
state.mode = "normal";
|
|
729
|
+
state.pendingKey = null;
|
|
730
|
+
state.pendingCount = null;
|
|
731
|
+
state.countBuffer = "";
|
|
732
|
+
state.register = null;
|
|
733
|
+
state.marks = {};
|
|
734
|
+
state.lastCharSearch = null;
|
|
735
|
+
state.visualAnchor = null;
|
|
736
|
+
state.visualCursor = null;
|
|
737
|
+
state.editorDoc = null;
|
|
738
|
+
state.currentRep = null;
|
|
739
|
+
state.desiredColumn = null;
|
|
740
|
+
state.lastCommand = null;
|
|
741
|
+
state.searchMode = false;
|
|
742
|
+
state.searchBuffer = "";
|
|
743
|
+
state.searchDirection = null;
|
|
744
|
+
state.lastSearch = null;
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it("f enters pending mode for char search", () => {
|
|
748
|
+
const ctx = { rep: makeRep([]), line: 0, char: 0, lineText: "" };
|
|
749
|
+
commands.normal["f"](ctx);
|
|
750
|
+
|
|
751
|
+
assert.equal(state.pendingKey, "f");
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
it("t enters pending mode for till search", () => {
|
|
755
|
+
const ctx = { rep: makeRep([]), line: 0, char: 0, lineText: "" };
|
|
756
|
+
commands.normal["t"](ctx);
|
|
757
|
+
|
|
758
|
+
assert.equal(state.pendingKey, "t");
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it("F enters pending mode for backward char search", () => {
|
|
762
|
+
const ctx = { rep: makeRep([]), line: 0, char: 0, lineText: "" };
|
|
763
|
+
commands.normal["F"](ctx);
|
|
764
|
+
|
|
765
|
+
assert.equal(state.pendingKey, "F");
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
it("T enters pending mode for backward till search", () => {
|
|
769
|
+
const ctx = { rep: makeRep([]), line: 0, char: 0, lineText: "" };
|
|
770
|
+
commands.normal["T"](ctx);
|
|
771
|
+
|
|
772
|
+
assert.equal(state.pendingKey, "T");
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
describe("paragraph motions", () => {
|
|
777
|
+
beforeEach(() => {
|
|
778
|
+
state.mode = "normal";
|
|
779
|
+
state.pendingKey = null;
|
|
780
|
+
state.pendingCount = null;
|
|
781
|
+
state.countBuffer = "";
|
|
782
|
+
state.register = null;
|
|
783
|
+
state.marks = {};
|
|
784
|
+
state.lastCharSearch = null;
|
|
785
|
+
state.visualAnchor = null;
|
|
786
|
+
state.visualCursor = null;
|
|
787
|
+
state.editorDoc = null;
|
|
788
|
+
state.currentRep = null;
|
|
789
|
+
state.desiredColumn = null;
|
|
790
|
+
state.lastCommand = null;
|
|
791
|
+
state.searchMode = false;
|
|
792
|
+
state.searchBuffer = "";
|
|
793
|
+
state.searchDirection = null;
|
|
794
|
+
state.lastSearch = null;
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it("{ moves to previous empty line", () => {
|
|
798
|
+
const rep = makeRep(["text", "text", "", "text"]);
|
|
799
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
800
|
+
|
|
801
|
+
const ctx = {
|
|
802
|
+
rep,
|
|
803
|
+
editorInfo,
|
|
804
|
+
line: 3,
|
|
805
|
+
char: 0,
|
|
806
|
+
lineText: "text",
|
|
807
|
+
count: 1,
|
|
808
|
+
};
|
|
809
|
+
commands.normal["{"](ctx);
|
|
810
|
+
|
|
811
|
+
assert.equal(calls.length, 1);
|
|
812
|
+
assert.deepEqual(calls[0].start, [2, 0]);
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
it("} moves to next empty line", () => {
|
|
816
|
+
const rep = makeRep(["text", "", "text", "text"]);
|
|
817
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
818
|
+
|
|
819
|
+
const ctx = {
|
|
820
|
+
rep,
|
|
821
|
+
editorInfo,
|
|
822
|
+
line: 0,
|
|
823
|
+
char: 0,
|
|
824
|
+
lineText: "text",
|
|
825
|
+
count: 1,
|
|
826
|
+
};
|
|
827
|
+
commands.normal["}"](ctx);
|
|
828
|
+
|
|
829
|
+
assert.equal(calls.length, 1);
|
|
830
|
+
assert.deepEqual(calls[0].start, [1, 0]);
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
describe("line reference motions", () => {
|
|
835
|
+
beforeEach(() => {
|
|
836
|
+
state.mode = "normal";
|
|
837
|
+
state.pendingKey = null;
|
|
838
|
+
state.pendingCount = null;
|
|
839
|
+
state.countBuffer = "";
|
|
840
|
+
state.register = null;
|
|
841
|
+
state.marks = {};
|
|
842
|
+
state.lastCharSearch = null;
|
|
843
|
+
state.visualAnchor = null;
|
|
844
|
+
state.visualCursor = null;
|
|
845
|
+
state.editorDoc = null;
|
|
846
|
+
state.currentRep = null;
|
|
847
|
+
state.desiredColumn = null;
|
|
848
|
+
state.lastCommand = null;
|
|
849
|
+
state.searchMode = false;
|
|
850
|
+
state.searchBuffer = "";
|
|
851
|
+
state.searchDirection = null;
|
|
852
|
+
state.lastSearch = null;
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
it("H moves to top of visible area", () => {
|
|
856
|
+
const rep = makeRep(["a", "b", "c", "d", "e"]);
|
|
857
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
858
|
+
|
|
859
|
+
const ctx = {
|
|
860
|
+
rep,
|
|
861
|
+
editorInfo,
|
|
862
|
+
line: 2,
|
|
863
|
+
char: 0,
|
|
864
|
+
lineText: "c",
|
|
865
|
+
count: 1,
|
|
866
|
+
};
|
|
867
|
+
commands.normal["H"](ctx);
|
|
868
|
+
|
|
869
|
+
// Should move to first non-blank of top line
|
|
870
|
+
assert.equal(calls.length, 1);
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
it("L moves to bottom of visible area", () => {
|
|
874
|
+
const rep = makeRep(["a", "b", "c", "d", "e"]);
|
|
875
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
876
|
+
|
|
877
|
+
const ctx = {
|
|
878
|
+
rep,
|
|
879
|
+
editorInfo,
|
|
880
|
+
line: 0,
|
|
881
|
+
char: 0,
|
|
882
|
+
lineText: "a",
|
|
883
|
+
count: 1,
|
|
884
|
+
};
|
|
885
|
+
commands.normal["L"](ctx);
|
|
886
|
+
|
|
887
|
+
assert.equal(calls.length, 1);
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
it("M moves to middle of visible area", () => {
|
|
891
|
+
const rep = makeRep(["a", "b", "c", "d", "e"]);
|
|
892
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
893
|
+
|
|
894
|
+
const ctx = {
|
|
895
|
+
rep,
|
|
896
|
+
editorInfo,
|
|
897
|
+
line: 0,
|
|
898
|
+
char: 0,
|
|
899
|
+
lineText: "a",
|
|
900
|
+
count: 1,
|
|
901
|
+
};
|
|
902
|
+
commands.normal["M"](ctx);
|
|
903
|
+
|
|
904
|
+
assert.equal(calls.length, 1);
|
|
905
|
+
});
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
describe("edge cases: motions on empty lines", () => {
|
|
909
|
+
beforeEach(resetState);
|
|
910
|
+
|
|
911
|
+
it("w on empty line stays on same position", () => {
|
|
912
|
+
const rep = makeRep([""]);
|
|
913
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
914
|
+
|
|
915
|
+
const ctx = {
|
|
916
|
+
rep,
|
|
917
|
+
editorInfo,
|
|
918
|
+
line: 0,
|
|
919
|
+
char: 0,
|
|
920
|
+
lineText: "",
|
|
921
|
+
count: 1,
|
|
922
|
+
};
|
|
923
|
+
commands.normal["w"](ctx);
|
|
924
|
+
|
|
925
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
926
|
+
assert.ok(selects.length > 0, "should produce a cursor move");
|
|
927
|
+
assert.deepEqual(selects[0].start, [0, 0]);
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
it("$ on empty line does not produce negative char", () => {
|
|
931
|
+
const rep = makeRep([""]);
|
|
932
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
933
|
+
|
|
934
|
+
const ctx = {
|
|
935
|
+
rep,
|
|
936
|
+
editorInfo,
|
|
937
|
+
line: 0,
|
|
938
|
+
char: 0,
|
|
939
|
+
lineText: "",
|
|
940
|
+
count: 1,
|
|
941
|
+
};
|
|
942
|
+
commands.normal["$"](ctx);
|
|
943
|
+
|
|
944
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
945
|
+
assert.ok(selects.length > 0);
|
|
946
|
+
const charPos = selects[0].start[1];
|
|
947
|
+
assert.ok(charPos >= 0, `$ on empty line gave char ${charPos}`);
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
it("x on empty line does nothing", () => {
|
|
951
|
+
const rep = makeRep([""]);
|
|
952
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
953
|
+
|
|
954
|
+
const ctx = {
|
|
955
|
+
rep,
|
|
956
|
+
editorInfo,
|
|
957
|
+
line: 0,
|
|
958
|
+
char: 0,
|
|
959
|
+
lineText: "",
|
|
960
|
+
count: 1,
|
|
961
|
+
};
|
|
962
|
+
commands.normal["x"](ctx);
|
|
963
|
+
|
|
964
|
+
const replaces = calls.filter((c) => c.type === "replace");
|
|
965
|
+
assert.equal(replaces.length, 0, "x on empty line should not replace");
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
it("~ on empty line does nothing", () => {
|
|
969
|
+
const rep = makeRep([""]);
|
|
970
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
971
|
+
|
|
972
|
+
const ctx = {
|
|
973
|
+
rep,
|
|
974
|
+
editorInfo,
|
|
975
|
+
line: 0,
|
|
976
|
+
char: 0,
|
|
977
|
+
lineText: "",
|
|
978
|
+
count: 1,
|
|
979
|
+
};
|
|
980
|
+
commands.normal["~"](ctx);
|
|
981
|
+
|
|
982
|
+
const replaces = calls.filter((c) => c.type === "replace");
|
|
983
|
+
assert.equal(replaces.length, 0, "~ on empty line should not replace");
|
|
984
|
+
});
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
describe("edge cases: boundary motions", () => {
|
|
988
|
+
beforeEach(resetState);
|
|
989
|
+
|
|
990
|
+
it("h at column 0 stays at column 0", () => {
|
|
991
|
+
const rep = makeRep(["hello"]);
|
|
992
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
993
|
+
|
|
994
|
+
const ctx = {
|
|
995
|
+
rep,
|
|
996
|
+
editorInfo,
|
|
997
|
+
line: 0,
|
|
998
|
+
char: 0,
|
|
999
|
+
lineText: "hello",
|
|
1000
|
+
count: 1,
|
|
1001
|
+
};
|
|
1002
|
+
commands.normal["h"](ctx);
|
|
1003
|
+
|
|
1004
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
1005
|
+
assert.deepEqual(selects[0].start, [0, 0]);
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
it("l at last character stays at last character", () => {
|
|
1009
|
+
const rep = makeRep(["hello"]);
|
|
1010
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1011
|
+
|
|
1012
|
+
const ctx = {
|
|
1013
|
+
rep,
|
|
1014
|
+
editorInfo,
|
|
1015
|
+
line: 0,
|
|
1016
|
+
char: 4,
|
|
1017
|
+
lineText: "hello",
|
|
1018
|
+
count: 1,
|
|
1019
|
+
};
|
|
1020
|
+
commands.normal["l"](ctx);
|
|
1021
|
+
|
|
1022
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
1023
|
+
assert.deepEqual(
|
|
1024
|
+
selects[0].start,
|
|
1025
|
+
[0, 4],
|
|
1026
|
+
"l at last char should not go past it",
|
|
1027
|
+
);
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
it("j at last line stays at last line", () => {
|
|
1031
|
+
const rep = makeRep(["line1", "line2"]);
|
|
1032
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1033
|
+
|
|
1034
|
+
const ctx = {
|
|
1035
|
+
rep,
|
|
1036
|
+
editorInfo,
|
|
1037
|
+
line: 1,
|
|
1038
|
+
char: 0,
|
|
1039
|
+
lineText: "line2",
|
|
1040
|
+
count: 1,
|
|
1041
|
+
};
|
|
1042
|
+
commands.normal["j"](ctx);
|
|
1043
|
+
|
|
1044
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
1045
|
+
assert.equal(selects[0].start[0], 1, "j at last line should stay");
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
it("k at first line stays at first line", () => {
|
|
1049
|
+
const rep = makeRep(["line1", "line2"]);
|
|
1050
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1051
|
+
|
|
1052
|
+
const ctx = {
|
|
1053
|
+
rep,
|
|
1054
|
+
editorInfo,
|
|
1055
|
+
line: 0,
|
|
1056
|
+
char: 0,
|
|
1057
|
+
lineText: "line1",
|
|
1058
|
+
count: 1,
|
|
1059
|
+
};
|
|
1060
|
+
commands.normal["k"](ctx);
|
|
1061
|
+
|
|
1062
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
1063
|
+
assert.equal(selects[0].start[0], 0, "k at first line should stay");
|
|
1064
|
+
});
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
describe("edge cases: t/f adjacent and edge positions", () => {
|
|
1068
|
+
beforeEach(resetState);
|
|
1069
|
+
|
|
1070
|
+
it("t to adjacent char should not move (lands on self)", () => {
|
|
1071
|
+
const rep = makeRep(["ab"]);
|
|
1072
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1073
|
+
|
|
1074
|
+
const ctx = {
|
|
1075
|
+
rep,
|
|
1076
|
+
editorInfo,
|
|
1077
|
+
line: 0,
|
|
1078
|
+
char: 0,
|
|
1079
|
+
lineText: "ab",
|
|
1080
|
+
count: 1,
|
|
1081
|
+
};
|
|
1082
|
+
parameterized["t"]("b", ctx);
|
|
1083
|
+
|
|
1084
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
1085
|
+
if (selects.length > 0) {
|
|
1086
|
+
assert.deepEqual(
|
|
1087
|
+
selects[0].start,
|
|
1088
|
+
[0, 0],
|
|
1089
|
+
"t to adjacent char should not move forward (would land on current pos)",
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
it("f at end of line should not find char", () => {
|
|
1095
|
+
const rep = makeRep(["abc"]);
|
|
1096
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1097
|
+
|
|
1098
|
+
const ctx = {
|
|
1099
|
+
rep,
|
|
1100
|
+
editorInfo,
|
|
1101
|
+
line: 0,
|
|
1102
|
+
char: 2,
|
|
1103
|
+
lineText: "abc",
|
|
1104
|
+
count: 1,
|
|
1105
|
+
};
|
|
1106
|
+
parameterized["f"]("x", ctx);
|
|
1107
|
+
|
|
1108
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
1109
|
+
assert.equal(selects.length, 0, "f for missing char should not move");
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
it("F at start of line should not find char", () => {
|
|
1113
|
+
const rep = makeRep(["abc"]);
|
|
1114
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1115
|
+
|
|
1116
|
+
const ctx = {
|
|
1117
|
+
rep,
|
|
1118
|
+
editorInfo,
|
|
1119
|
+
line: 0,
|
|
1120
|
+
char: 0,
|
|
1121
|
+
lineText: "abc",
|
|
1122
|
+
count: 1,
|
|
1123
|
+
};
|
|
1124
|
+
parameterized["F"]("x", ctx);
|
|
1125
|
+
|
|
1126
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
1127
|
+
assert.equal(selects.length, 0, "F for missing char should not move");
|
|
1128
|
+
});
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
describe("edge cases: w/b word motions", () => {
|
|
1132
|
+
beforeEach(resetState);
|
|
1133
|
+
|
|
1134
|
+
it("w at last word on line clamps to end", () => {
|
|
1135
|
+
const rep = makeRep(["hello world"]);
|
|
1136
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1137
|
+
|
|
1138
|
+
const ctx = {
|
|
1139
|
+
rep,
|
|
1140
|
+
editorInfo,
|
|
1141
|
+
line: 0,
|
|
1142
|
+
char: 6,
|
|
1143
|
+
lineText: "hello world",
|
|
1144
|
+
count: 1,
|
|
1145
|
+
};
|
|
1146
|
+
commands.normal["w"](ctx);
|
|
1147
|
+
|
|
1148
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
1149
|
+
assert.ok(selects.length > 0);
|
|
1150
|
+
const endChar = selects[0].start[1];
|
|
1151
|
+
assert.ok(
|
|
1152
|
+
endChar <= 10,
|
|
1153
|
+
`w at last word should clamp to line end, got ${endChar}`,
|
|
1154
|
+
);
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
it("b at first word stays at position 0", () => {
|
|
1158
|
+
const rep = makeRep(["hello world"]);
|
|
1159
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1160
|
+
|
|
1161
|
+
const ctx = {
|
|
1162
|
+
rep,
|
|
1163
|
+
editorInfo,
|
|
1164
|
+
line: 0,
|
|
1165
|
+
char: 0,
|
|
1166
|
+
lineText: "hello world",
|
|
1167
|
+
count: 1,
|
|
1168
|
+
};
|
|
1169
|
+
commands.normal["b"](ctx);
|
|
1170
|
+
|
|
1171
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
1172
|
+
assert.ok(selects.length > 0);
|
|
1173
|
+
assert.deepEqual(selects[0].start, [0, 0]);
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
it("w on line with only spaces", () => {
|
|
1177
|
+
const rep = makeRep([" "]);
|
|
1178
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1179
|
+
|
|
1180
|
+
const ctx = {
|
|
1181
|
+
rep,
|
|
1182
|
+
editorInfo,
|
|
1183
|
+
line: 0,
|
|
1184
|
+
char: 0,
|
|
1185
|
+
lineText: " ",
|
|
1186
|
+
count: 1,
|
|
1187
|
+
};
|
|
1188
|
+
commands.normal["w"](ctx);
|
|
1189
|
+
|
|
1190
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
1191
|
+
assert.ok(selects.length > 0);
|
|
1192
|
+
assert.ok(
|
|
1193
|
+
selects[0].start[1] >= 0,
|
|
1194
|
+
"w on whitespace-only line should not crash",
|
|
1195
|
+
);
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
it("e on single character word", () => {
|
|
1199
|
+
const rep = makeRep(["a b c"]);
|
|
1200
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1201
|
+
|
|
1202
|
+
const ctx = {
|
|
1203
|
+
rep,
|
|
1204
|
+
editorInfo,
|
|
1205
|
+
line: 0,
|
|
1206
|
+
char: 0,
|
|
1207
|
+
lineText: "a b c",
|
|
1208
|
+
count: 1,
|
|
1209
|
+
};
|
|
1210
|
+
commands.normal["e"](ctx);
|
|
1211
|
+
|
|
1212
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
1213
|
+
assert.ok(selects.length > 0);
|
|
1214
|
+
assert.equal(
|
|
1215
|
+
selects[0].start[1],
|
|
1216
|
+
2,
|
|
1217
|
+
"e from 'a' should jump to 'b' (next word end)",
|
|
1218
|
+
);
|
|
1219
|
+
});
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
describe("edge cases: j/k desiredColumn stickiness", () => {
|
|
1223
|
+
beforeEach(resetState);
|
|
1224
|
+
|
|
1225
|
+
it("j through short line preserves desired column", () => {
|
|
1226
|
+
const rep = makeRep(["long line here", "ab", "long line here"]);
|
|
1227
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1228
|
+
|
|
1229
|
+
const ctx1 = {
|
|
1230
|
+
rep,
|
|
1231
|
+
editorInfo,
|
|
1232
|
+
line: 0,
|
|
1233
|
+
char: 10,
|
|
1234
|
+
lineText: "long line here",
|
|
1235
|
+
count: 1,
|
|
1236
|
+
};
|
|
1237
|
+
commands.normal["j"](ctx1);
|
|
1238
|
+
|
|
1239
|
+
const select1 = calls.filter((c) => c.type === "select");
|
|
1240
|
+
assert.equal(select1[0].start[0], 1, "should be on line 1");
|
|
1241
|
+
assert.equal(
|
|
1242
|
+
select1[0].start[1],
|
|
1243
|
+
1,
|
|
1244
|
+
"should clamp to last char of short line",
|
|
1245
|
+
);
|
|
1246
|
+
|
|
1247
|
+
const ctx2 = {
|
|
1248
|
+
rep,
|
|
1249
|
+
editorInfo,
|
|
1250
|
+
line: 1,
|
|
1251
|
+
char: 1,
|
|
1252
|
+
lineText: "ab",
|
|
1253
|
+
count: 1,
|
|
1254
|
+
};
|
|
1255
|
+
commands.normal["j"](ctx2);
|
|
1256
|
+
|
|
1257
|
+
const select2 = calls.filter((c) => c.type === "select");
|
|
1258
|
+
const lastSelect = select2[select2.length - 1];
|
|
1259
|
+
assert.equal(lastSelect.start[0], 2);
|
|
1260
|
+
assert.equal(
|
|
1261
|
+
lastSelect.start[1],
|
|
1262
|
+
10,
|
|
1263
|
+
"j should restore desired column on longer line",
|
|
1264
|
+
);
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
it("h resets desired column", () => {
|
|
1268
|
+
const rep = makeRep(["long line here", "ab", "long line here"]);
|
|
1269
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1270
|
+
|
|
1271
|
+
const ctx1 = {
|
|
1272
|
+
rep,
|
|
1273
|
+
editorInfo,
|
|
1274
|
+
line: 0,
|
|
1275
|
+
char: 10,
|
|
1276
|
+
lineText: "long line here",
|
|
1277
|
+
count: 1,
|
|
1278
|
+
};
|
|
1279
|
+
commands.normal["j"](ctx1);
|
|
1280
|
+
|
|
1281
|
+
commands.normal["h"]({
|
|
1282
|
+
rep,
|
|
1283
|
+
editorInfo,
|
|
1284
|
+
line: 1,
|
|
1285
|
+
char: 1,
|
|
1286
|
+
lineText: "ab",
|
|
1287
|
+
count: 1,
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
commands.normal["j"]({
|
|
1291
|
+
rep,
|
|
1292
|
+
editorInfo,
|
|
1293
|
+
line: 1,
|
|
1294
|
+
char: 0,
|
|
1295
|
+
lineText: "ab",
|
|
1296
|
+
count: 1,
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
1300
|
+
const lastSelect = selects[selects.length - 1];
|
|
1301
|
+
assert.equal(lastSelect.start[0], 2);
|
|
1302
|
+
assert.equal(
|
|
1303
|
+
lastSelect.start[1],
|
|
1304
|
+
0,
|
|
1305
|
+
"h should reset desiredColumn so subsequent j uses actual column",
|
|
1306
|
+
);
|
|
1307
|
+
});
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
describe("edge cases: gg and G with count", () => {
|
|
1311
|
+
beforeEach(resetState);
|
|
1312
|
+
|
|
1313
|
+
it("5gg goes to line 5 (0-indexed line 4)", () => {
|
|
1314
|
+
const rep = makeRep(Array.from({ length: 10 }, (_, i) => `line${i}`));
|
|
1315
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1316
|
+
|
|
1317
|
+
const ctx = {
|
|
1318
|
+
rep,
|
|
1319
|
+
editorInfo,
|
|
1320
|
+
line: 0,
|
|
1321
|
+
char: 0,
|
|
1322
|
+
lineText: "line0",
|
|
1323
|
+
count: 5,
|
|
1324
|
+
hasCount: true,
|
|
1325
|
+
};
|
|
1326
|
+
commands.normal["gg"](ctx);
|
|
1327
|
+
|
|
1328
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
1329
|
+
assert.equal(
|
|
1330
|
+
selects[0].start[0],
|
|
1331
|
+
4,
|
|
1332
|
+
"5gg should go to line index 4 (line 5)",
|
|
1333
|
+
);
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
it("gg with count beyond document clamps to last line", () => {
|
|
1337
|
+
const rep = makeRep(["line0", "line1", "line2"]);
|
|
1338
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1339
|
+
|
|
1340
|
+
const ctx = {
|
|
1341
|
+
rep,
|
|
1342
|
+
editorInfo,
|
|
1343
|
+
line: 0,
|
|
1344
|
+
char: 0,
|
|
1345
|
+
lineText: "line0",
|
|
1346
|
+
count: 100,
|
|
1347
|
+
hasCount: true,
|
|
1348
|
+
};
|
|
1349
|
+
commands.normal["gg"](ctx);
|
|
1350
|
+
|
|
1351
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
1352
|
+
assert.equal(
|
|
1353
|
+
selects[0].start[0],
|
|
1354
|
+
2,
|
|
1355
|
+
"gg with huge count should clamp to last line",
|
|
1356
|
+
);
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
it("G without count goes to last line", () => {
|
|
1360
|
+
const rep = makeRep(["line0", "line1", "line2"]);
|
|
1361
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1362
|
+
|
|
1363
|
+
const ctx = {
|
|
1364
|
+
rep,
|
|
1365
|
+
editorInfo,
|
|
1366
|
+
line: 0,
|
|
1367
|
+
char: 0,
|
|
1368
|
+
lineText: "line0",
|
|
1369
|
+
count: 1,
|
|
1370
|
+
hasCount: false,
|
|
1371
|
+
};
|
|
1372
|
+
commands.normal["G"](ctx);
|
|
1373
|
+
|
|
1374
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
1375
|
+
assert.equal(selects[0].start[0], 2, "G should go to last line");
|
|
1376
|
+
});
|
|
1377
|
+
});
|