ep_vim 0.7.1 → 0.9.2
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 +10 -3
- package/package.json +6 -6
- package/static/js/index.js +959 -825
- package/static/js/index.test.js +3081 -0
- package/static/js/vim-core.js +79 -0
- package/static/js/vim-core.test.js +118 -0
|
@@ -0,0 +1,3081 @@
|
|
|
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
|
+
} = require("./index.js");
|
|
19
|
+
|
|
20
|
+
const makeRep = (lines) => ({
|
|
21
|
+
lines: {
|
|
22
|
+
length: () => lines.length,
|
|
23
|
+
atIndex: (n) => ({ text: lines[n] }),
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const makeMockEditorInfo = () => {
|
|
28
|
+
const calls = [];
|
|
29
|
+
return {
|
|
30
|
+
editorInfo: {
|
|
31
|
+
ace_inCallStackIfNecessary: (_name, fn) => fn(),
|
|
32
|
+
ace_performSelectionChange: (start, end, _flag) => {
|
|
33
|
+
calls.push({ type: "select", start, end });
|
|
34
|
+
},
|
|
35
|
+
ace_updateBrowserSelectionFromRep: () => {},
|
|
36
|
+
ace_performDocumentReplaceRange: (start, end, newText) => {
|
|
37
|
+
calls.push({ type: "replace", start, end, newText });
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
calls,
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
describe("char search repeat (semicolon)", () => {
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
// Reset state before each test
|
|
47
|
+
state.mode = "normal";
|
|
48
|
+
state.pendingKey = null;
|
|
49
|
+
state.pendingCount = null;
|
|
50
|
+
state.countBuffer = "";
|
|
51
|
+
state.register = null;
|
|
52
|
+
state.marks = {};
|
|
53
|
+
state.lastCharSearch = null;
|
|
54
|
+
state.visualAnchor = null;
|
|
55
|
+
state.visualCursor = null;
|
|
56
|
+
state.editorDoc = null;
|
|
57
|
+
state.currentRep = null;
|
|
58
|
+
state.desiredColumn = null;
|
|
59
|
+
state.lastCommand = null;
|
|
60
|
+
state.searchMode = false;
|
|
61
|
+
state.searchBuffer = "";
|
|
62
|
+
state.searchDirection = null;
|
|
63
|
+
state.lastSearch = null;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("repeats forward char search with ; in normal mode", () => {
|
|
67
|
+
const rep = makeRep(["hello world"]);
|
|
68
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
69
|
+
|
|
70
|
+
// Set up a prior 'f' search for 'o' at position 0
|
|
71
|
+
state.lastCharSearch = { direction: "f", target: "o" };
|
|
72
|
+
|
|
73
|
+
// Call ; to repeat the search
|
|
74
|
+
const ctx = {
|
|
75
|
+
rep,
|
|
76
|
+
editorInfo,
|
|
77
|
+
line: 0,
|
|
78
|
+
char: 0,
|
|
79
|
+
lineText: "hello world",
|
|
80
|
+
count: 1,
|
|
81
|
+
};
|
|
82
|
+
commands.normal[";"](ctx);
|
|
83
|
+
|
|
84
|
+
// Should have moved to first 'o' at position 4
|
|
85
|
+
assert.equal(
|
|
86
|
+
calls.length,
|
|
87
|
+
1,
|
|
88
|
+
`Expected 1 call, got ${calls.length}. Calls: ${JSON.stringify(calls)}`,
|
|
89
|
+
);
|
|
90
|
+
assert.deepEqual(calls[0].start, [0, 4]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("does nothing when no prior char search", () => {
|
|
94
|
+
const rep = makeRep(["hello world"]);
|
|
95
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
96
|
+
|
|
97
|
+
state.lastCharSearch = null;
|
|
98
|
+
|
|
99
|
+
const ctx = { rep, editorInfo, line: 0, char: 0, lineText: "hello world" };
|
|
100
|
+
commands.normal[";"](ctx);
|
|
101
|
+
|
|
102
|
+
// Should not move cursor
|
|
103
|
+
assert.equal(calls.length, 0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("repeats t search (till char) with ;", () => {
|
|
107
|
+
const rep = makeRep(["hello world"]);
|
|
108
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
109
|
+
|
|
110
|
+
state.lastCharSearch = { direction: "t", target: "o" };
|
|
111
|
+
|
|
112
|
+
const ctx = {
|
|
113
|
+
rep,
|
|
114
|
+
editorInfo,
|
|
115
|
+
line: 0,
|
|
116
|
+
char: 0,
|
|
117
|
+
lineText: "hello world",
|
|
118
|
+
count: 1,
|
|
119
|
+
};
|
|
120
|
+
commands.normal[";"](ctx);
|
|
121
|
+
|
|
122
|
+
// 't' finds 'o' at position 4, but lands one before (position 3)
|
|
123
|
+
assert.equal(calls.length, 1);
|
|
124
|
+
assert.deepEqual(calls[0].start, [0, 3]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("repeats with count", () => {
|
|
128
|
+
const rep = makeRep(["abacada"]);
|
|
129
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
130
|
+
|
|
131
|
+
state.lastCharSearch = { direction: "f", target: "a" };
|
|
132
|
+
|
|
133
|
+
const ctx = {
|
|
134
|
+
rep,
|
|
135
|
+
editorInfo,
|
|
136
|
+
line: 0,
|
|
137
|
+
char: 0,
|
|
138
|
+
lineText: "abacada",
|
|
139
|
+
count: 2,
|
|
140
|
+
};
|
|
141
|
+
commands.normal[";"](ctx);
|
|
142
|
+
|
|
143
|
+
// With count 2, should find the 2nd 'a' after position 0, which is at position 4
|
|
144
|
+
assert.equal(calls.length, 1);
|
|
145
|
+
assert.deepEqual(calls[0].start, [0, 4]);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("char search reverse (comma)", () => {
|
|
150
|
+
beforeEach(() => {
|
|
151
|
+
state.mode = "normal";
|
|
152
|
+
state.pendingKey = null;
|
|
153
|
+
state.pendingCount = null;
|
|
154
|
+
state.countBuffer = "";
|
|
155
|
+
state.register = null;
|
|
156
|
+
state.marks = {};
|
|
157
|
+
state.lastCharSearch = null;
|
|
158
|
+
state.visualAnchor = null;
|
|
159
|
+
state.visualCursor = null;
|
|
160
|
+
state.editorDoc = null;
|
|
161
|
+
state.currentRep = null;
|
|
162
|
+
state.desiredColumn = null;
|
|
163
|
+
state.lastCommand = null;
|
|
164
|
+
state.searchMode = false;
|
|
165
|
+
state.searchBuffer = "";
|
|
166
|
+
state.searchDirection = null;
|
|
167
|
+
state.lastSearch = null;
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("reverses f search to F with ,", () => {
|
|
171
|
+
const rep = makeRep(["hello world"]);
|
|
172
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
173
|
+
|
|
174
|
+
state.lastCharSearch = { direction: "f", target: "o" };
|
|
175
|
+
|
|
176
|
+
// Start at position 7 and search backward
|
|
177
|
+
const ctx = {
|
|
178
|
+
rep,
|
|
179
|
+
editorInfo,
|
|
180
|
+
line: 0,
|
|
181
|
+
char: 7,
|
|
182
|
+
lineText: "hello world",
|
|
183
|
+
count: 1,
|
|
184
|
+
};
|
|
185
|
+
commands.normal[","](ctx);
|
|
186
|
+
|
|
187
|
+
// 'F' search from position 7 finds 'o' at position 4 (going backward)
|
|
188
|
+
assert.equal(calls.length, 1);
|
|
189
|
+
assert.deepEqual(calls[0].start, [0, 4]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("reverses t search to T with ,", () => {
|
|
193
|
+
const rep = makeRep(["hello world"]);
|
|
194
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
195
|
+
|
|
196
|
+
state.lastCharSearch = { direction: "t", target: "o" };
|
|
197
|
+
|
|
198
|
+
const ctx = {
|
|
199
|
+
rep,
|
|
200
|
+
editorInfo,
|
|
201
|
+
line: 0,
|
|
202
|
+
char: 7,
|
|
203
|
+
lineText: "hello world",
|
|
204
|
+
count: 1,
|
|
205
|
+
};
|
|
206
|
+
commands.normal[","](ctx);
|
|
207
|
+
|
|
208
|
+
// 'T' finds 'o' at position 4, then lands one after (position 5)
|
|
209
|
+
assert.equal(calls.length, 1);
|
|
210
|
+
assert.deepEqual(calls[0].start, [0, 5]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("does nothing when no prior char search", () => {
|
|
214
|
+
const rep = makeRep(["hello world"]);
|
|
215
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
216
|
+
|
|
217
|
+
state.lastCharSearch = null;
|
|
218
|
+
|
|
219
|
+
const ctx = {
|
|
220
|
+
rep,
|
|
221
|
+
editorInfo,
|
|
222
|
+
line: 0,
|
|
223
|
+
char: 7,
|
|
224
|
+
lineText: "hello world",
|
|
225
|
+
count: 1,
|
|
226
|
+
};
|
|
227
|
+
commands.normal[","](ctx);
|
|
228
|
+
|
|
229
|
+
assert.equal(calls.length, 0);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("basic motions", () => {
|
|
234
|
+
beforeEach(() => {
|
|
235
|
+
state.mode = "normal";
|
|
236
|
+
state.pendingKey = null;
|
|
237
|
+
state.pendingCount = null;
|
|
238
|
+
state.countBuffer = "";
|
|
239
|
+
state.register = null;
|
|
240
|
+
state.marks = {};
|
|
241
|
+
state.lastCharSearch = null;
|
|
242
|
+
state.visualAnchor = null;
|
|
243
|
+
state.visualCursor = null;
|
|
244
|
+
state.editorDoc = null;
|
|
245
|
+
state.currentRep = null;
|
|
246
|
+
state.desiredColumn = null;
|
|
247
|
+
state.lastCommand = null;
|
|
248
|
+
state.searchMode = false;
|
|
249
|
+
state.searchBuffer = "";
|
|
250
|
+
state.searchDirection = null;
|
|
251
|
+
state.lastSearch = null;
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("h moves cursor left", () => {
|
|
255
|
+
const rep = makeRep(["hello"]);
|
|
256
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
257
|
+
|
|
258
|
+
const ctx = {
|
|
259
|
+
rep,
|
|
260
|
+
editorInfo,
|
|
261
|
+
line: 0,
|
|
262
|
+
char: 3,
|
|
263
|
+
lineText: "hello",
|
|
264
|
+
count: 1,
|
|
265
|
+
};
|
|
266
|
+
commands.normal["h"](ctx);
|
|
267
|
+
|
|
268
|
+
assert.equal(calls.length, 1);
|
|
269
|
+
assert.deepEqual(calls[0].start, [0, 2]);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("h with count moves left multiple times", () => {
|
|
273
|
+
const rep = makeRep(["hello"]);
|
|
274
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
275
|
+
|
|
276
|
+
const ctx = {
|
|
277
|
+
rep,
|
|
278
|
+
editorInfo,
|
|
279
|
+
line: 0,
|
|
280
|
+
char: 4,
|
|
281
|
+
lineText: "hello",
|
|
282
|
+
count: 3,
|
|
283
|
+
};
|
|
284
|
+
commands.normal["h"](ctx);
|
|
285
|
+
|
|
286
|
+
assert.equal(calls.length, 1);
|
|
287
|
+
assert.deepEqual(calls[0].start, [0, 1]);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("l moves cursor right", () => {
|
|
291
|
+
const rep = makeRep(["hello"]);
|
|
292
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
293
|
+
|
|
294
|
+
const ctx = {
|
|
295
|
+
rep,
|
|
296
|
+
editorInfo,
|
|
297
|
+
line: 0,
|
|
298
|
+
char: 2,
|
|
299
|
+
lineText: "hello",
|
|
300
|
+
count: 1,
|
|
301
|
+
};
|
|
302
|
+
commands.normal["l"](ctx);
|
|
303
|
+
|
|
304
|
+
assert.equal(calls.length, 1);
|
|
305
|
+
assert.deepEqual(calls[0].start, [0, 3]);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("l with count moves right multiple times", () => {
|
|
309
|
+
const rep = makeRep(["hello"]);
|
|
310
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
311
|
+
|
|
312
|
+
const ctx = {
|
|
313
|
+
rep,
|
|
314
|
+
editorInfo,
|
|
315
|
+
line: 0,
|
|
316
|
+
char: 0,
|
|
317
|
+
lineText: "hello",
|
|
318
|
+
count: 2,
|
|
319
|
+
};
|
|
320
|
+
commands.normal["l"](ctx);
|
|
321
|
+
|
|
322
|
+
assert.equal(calls.length, 1);
|
|
323
|
+
assert.deepEqual(calls[0].start, [0, 2]);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("0 moves to line start", () => {
|
|
327
|
+
const rep = makeRep(["hello"]);
|
|
328
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
329
|
+
|
|
330
|
+
const ctx = {
|
|
331
|
+
rep,
|
|
332
|
+
editorInfo,
|
|
333
|
+
line: 0,
|
|
334
|
+
char: 3,
|
|
335
|
+
lineText: "hello",
|
|
336
|
+
count: 1,
|
|
337
|
+
};
|
|
338
|
+
commands.normal["0"](ctx);
|
|
339
|
+
|
|
340
|
+
assert.equal(calls.length, 1);
|
|
341
|
+
assert.deepEqual(calls[0].start, [0, 0]);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("$ moves to line end", () => {
|
|
345
|
+
const rep = makeRep(["hello"]);
|
|
346
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
347
|
+
|
|
348
|
+
const ctx = {
|
|
349
|
+
rep,
|
|
350
|
+
editorInfo,
|
|
351
|
+
line: 0,
|
|
352
|
+
char: 0,
|
|
353
|
+
lineText: "hello",
|
|
354
|
+
count: 1,
|
|
355
|
+
};
|
|
356
|
+
commands.normal["$"](ctx);
|
|
357
|
+
|
|
358
|
+
assert.equal(calls.length, 1);
|
|
359
|
+
assert.deepEqual(calls[0].start, [0, 4]);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("^ moves to first non-blank", () => {
|
|
363
|
+
const rep = makeRep([" hello"]);
|
|
364
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
365
|
+
|
|
366
|
+
const ctx = {
|
|
367
|
+
rep,
|
|
368
|
+
editorInfo,
|
|
369
|
+
line: 0,
|
|
370
|
+
char: 0,
|
|
371
|
+
lineText: " hello",
|
|
372
|
+
count: 1,
|
|
373
|
+
};
|
|
374
|
+
commands.normal["^"](ctx);
|
|
375
|
+
|
|
376
|
+
assert.equal(calls.length, 1);
|
|
377
|
+
assert.deepEqual(calls[0].start, [0, 2]);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
describe("marks", () => {
|
|
382
|
+
beforeEach(() => {
|
|
383
|
+
state.mode = "normal";
|
|
384
|
+
state.pendingKey = null;
|
|
385
|
+
state.pendingCount = null;
|
|
386
|
+
state.countBuffer = "";
|
|
387
|
+
state.register = null;
|
|
388
|
+
state.marks = {};
|
|
389
|
+
state.lastCharSearch = null;
|
|
390
|
+
state.visualAnchor = null;
|
|
391
|
+
state.visualCursor = null;
|
|
392
|
+
state.editorDoc = null;
|
|
393
|
+
state.currentRep = null;
|
|
394
|
+
state.desiredColumn = null;
|
|
395
|
+
state.lastCommand = null;
|
|
396
|
+
state.searchMode = false;
|
|
397
|
+
state.searchBuffer = "";
|
|
398
|
+
state.searchDirection = null;
|
|
399
|
+
state.lastSearch = null;
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("m sets a mark", () => {
|
|
403
|
+
const ctx = { rep: makeRep([]), line: 5, char: 10 };
|
|
404
|
+
|
|
405
|
+
parameterized["m"]("a", ctx);
|
|
406
|
+
|
|
407
|
+
assert.deepEqual(state.marks["a"], [5, 10]);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("' jumps to mark (line start)", () => {
|
|
411
|
+
const rep = makeRep(["line0", "line1", "line2"]);
|
|
412
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
413
|
+
|
|
414
|
+
state.marks["a"] = [1, 3];
|
|
415
|
+
|
|
416
|
+
const ctx = {
|
|
417
|
+
rep,
|
|
418
|
+
editorInfo,
|
|
419
|
+
line: 0,
|
|
420
|
+
char: 0,
|
|
421
|
+
lineText: "line0",
|
|
422
|
+
count: 1,
|
|
423
|
+
};
|
|
424
|
+
parameterized["'"]("a", ctx);
|
|
425
|
+
|
|
426
|
+
assert.equal(calls.length, 1);
|
|
427
|
+
assert.deepEqual(calls[0].start, [1, 0]);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("` jumps to mark (exact position)", () => {
|
|
431
|
+
const rep = makeRep(["line0", "line1", "line2"]);
|
|
432
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
433
|
+
|
|
434
|
+
state.marks["b"] = [1, 3];
|
|
435
|
+
|
|
436
|
+
const ctx = {
|
|
437
|
+
rep,
|
|
438
|
+
editorInfo,
|
|
439
|
+
line: 0,
|
|
440
|
+
char: 0,
|
|
441
|
+
lineText: "line0",
|
|
442
|
+
count: 1,
|
|
443
|
+
};
|
|
444
|
+
parameterized["`"]("b", ctx);
|
|
445
|
+
|
|
446
|
+
assert.equal(calls.length, 1);
|
|
447
|
+
assert.deepEqual(calls[0].start, [1, 3]);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("' does nothing with nonexistent mark", () => {
|
|
451
|
+
const rep = makeRep(["line0"]);
|
|
452
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
453
|
+
|
|
454
|
+
state.marks = {};
|
|
455
|
+
|
|
456
|
+
const ctx = {
|
|
457
|
+
rep,
|
|
458
|
+
editorInfo,
|
|
459
|
+
line: 0,
|
|
460
|
+
char: 0,
|
|
461
|
+
lineText: "line0",
|
|
462
|
+
count: 1,
|
|
463
|
+
};
|
|
464
|
+
parameterized["'"]("z", ctx);
|
|
465
|
+
|
|
466
|
+
assert.equal(calls.length, 0);
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
describe("line navigation", () => {
|
|
471
|
+
beforeEach(() => {
|
|
472
|
+
state.mode = "normal";
|
|
473
|
+
state.pendingKey = null;
|
|
474
|
+
state.pendingCount = null;
|
|
475
|
+
state.countBuffer = "";
|
|
476
|
+
state.register = null;
|
|
477
|
+
state.marks = {};
|
|
478
|
+
state.lastCharSearch = null;
|
|
479
|
+
state.visualAnchor = null;
|
|
480
|
+
state.visualCursor = null;
|
|
481
|
+
state.editorDoc = null;
|
|
482
|
+
state.currentRep = null;
|
|
483
|
+
state.desiredColumn = null;
|
|
484
|
+
state.lastCommand = null;
|
|
485
|
+
state.searchMode = false;
|
|
486
|
+
state.searchBuffer = "";
|
|
487
|
+
state.searchDirection = null;
|
|
488
|
+
state.lastSearch = null;
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("j moves down one line", () => {
|
|
492
|
+
const rep = makeRep(["line0", "line1", "line2"]);
|
|
493
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
494
|
+
|
|
495
|
+
const ctx = {
|
|
496
|
+
rep,
|
|
497
|
+
editorInfo,
|
|
498
|
+
line: 0,
|
|
499
|
+
char: 2,
|
|
500
|
+
lineText: "line0",
|
|
501
|
+
count: 1,
|
|
502
|
+
};
|
|
503
|
+
commands.normal["j"](ctx);
|
|
504
|
+
|
|
505
|
+
assert.equal(calls.length, 1);
|
|
506
|
+
assert.deepEqual(calls[0].start, [1, 2]);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("j with count moves down multiple lines", () => {
|
|
510
|
+
const rep = makeRep(["line0", "line1", "line2", "line3"]);
|
|
511
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
512
|
+
|
|
513
|
+
const ctx = {
|
|
514
|
+
rep,
|
|
515
|
+
editorInfo,
|
|
516
|
+
line: 0,
|
|
517
|
+
char: 1,
|
|
518
|
+
lineText: "line0",
|
|
519
|
+
count: 2,
|
|
520
|
+
};
|
|
521
|
+
commands.normal["j"](ctx);
|
|
522
|
+
|
|
523
|
+
assert.equal(calls.length, 1);
|
|
524
|
+
assert.deepEqual(calls[0].start, [2, 1]);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it("k moves up one line", () => {
|
|
528
|
+
const rep = makeRep(["line0", "line1", "line2"]);
|
|
529
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
530
|
+
|
|
531
|
+
const ctx = {
|
|
532
|
+
rep,
|
|
533
|
+
editorInfo,
|
|
534
|
+
line: 2,
|
|
535
|
+
char: 2,
|
|
536
|
+
lineText: "line2",
|
|
537
|
+
count: 1,
|
|
538
|
+
};
|
|
539
|
+
commands.normal["k"](ctx);
|
|
540
|
+
|
|
541
|
+
assert.equal(calls.length, 1);
|
|
542
|
+
assert.deepEqual(calls[0].start, [1, 2]);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it("gg goes to first line", () => {
|
|
546
|
+
const rep = makeRep(["line0", "line1", "line2"]);
|
|
547
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
548
|
+
|
|
549
|
+
const ctx = {
|
|
550
|
+
rep,
|
|
551
|
+
editorInfo,
|
|
552
|
+
line: 2,
|
|
553
|
+
char: 3,
|
|
554
|
+
lineText: "line2",
|
|
555
|
+
count: 1,
|
|
556
|
+
hasCount: false,
|
|
557
|
+
};
|
|
558
|
+
commands.normal["gg"](ctx);
|
|
559
|
+
|
|
560
|
+
assert.equal(calls.length, 1);
|
|
561
|
+
assert.deepEqual(calls[0].start, [0, 0]);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it("G goes to last line", () => {
|
|
565
|
+
const rep = makeRep(["line0", "line1", "line2"]);
|
|
566
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
567
|
+
|
|
568
|
+
const ctx = {
|
|
569
|
+
rep,
|
|
570
|
+
editorInfo,
|
|
571
|
+
line: 0,
|
|
572
|
+
char: 2,
|
|
573
|
+
lineText: "line0",
|
|
574
|
+
count: 1,
|
|
575
|
+
hasCount: false,
|
|
576
|
+
};
|
|
577
|
+
commands.normal["G"](ctx);
|
|
578
|
+
|
|
579
|
+
assert.equal(calls.length, 1);
|
|
580
|
+
assert.deepEqual(calls[0].start, [2, 0]);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it("G with count goes to specific line", () => {
|
|
584
|
+
const rep = makeRep(["line0", "line1", "line2", "line3"]);
|
|
585
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
586
|
+
|
|
587
|
+
const ctx = {
|
|
588
|
+
rep,
|
|
589
|
+
editorInfo,
|
|
590
|
+
line: 0,
|
|
591
|
+
char: 0,
|
|
592
|
+
lineText: "line0",
|
|
593
|
+
count: 3,
|
|
594
|
+
hasCount: true,
|
|
595
|
+
};
|
|
596
|
+
commands.normal["G"](ctx);
|
|
597
|
+
|
|
598
|
+
assert.equal(calls.length, 1);
|
|
599
|
+
assert.deepEqual(calls[0].start, [2, 0]);
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
describe("word motions", () => {
|
|
604
|
+
beforeEach(() => {
|
|
605
|
+
state.mode = "normal";
|
|
606
|
+
state.pendingKey = null;
|
|
607
|
+
state.pendingCount = null;
|
|
608
|
+
state.countBuffer = "";
|
|
609
|
+
state.register = null;
|
|
610
|
+
state.marks = {};
|
|
611
|
+
state.lastCharSearch = null;
|
|
612
|
+
state.visualAnchor = null;
|
|
613
|
+
state.visualCursor = null;
|
|
614
|
+
state.editorDoc = null;
|
|
615
|
+
state.currentRep = null;
|
|
616
|
+
state.desiredColumn = null;
|
|
617
|
+
state.lastCommand = null;
|
|
618
|
+
state.searchMode = false;
|
|
619
|
+
state.searchBuffer = "";
|
|
620
|
+
state.searchDirection = null;
|
|
621
|
+
state.lastSearch = null;
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it("w moves to next word", () => {
|
|
625
|
+
const rep = makeRep(["hello world foo"]);
|
|
626
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
627
|
+
|
|
628
|
+
const ctx = {
|
|
629
|
+
rep,
|
|
630
|
+
editorInfo,
|
|
631
|
+
line: 0,
|
|
632
|
+
char: 0,
|
|
633
|
+
lineText: "hello world foo",
|
|
634
|
+
count: 1,
|
|
635
|
+
};
|
|
636
|
+
commands.normal["w"](ctx);
|
|
637
|
+
|
|
638
|
+
assert.equal(calls.length, 1);
|
|
639
|
+
assert.deepEqual(calls[0].start, [0, 6]);
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it("w with count moves multiple words", () => {
|
|
643
|
+
const rep = makeRep(["hello world foo"]);
|
|
644
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
645
|
+
|
|
646
|
+
const ctx = {
|
|
647
|
+
rep,
|
|
648
|
+
editorInfo,
|
|
649
|
+
line: 0,
|
|
650
|
+
char: 0,
|
|
651
|
+
lineText: "hello world foo",
|
|
652
|
+
count: 2,
|
|
653
|
+
};
|
|
654
|
+
commands.normal["w"](ctx);
|
|
655
|
+
|
|
656
|
+
assert.equal(calls.length, 1);
|
|
657
|
+
assert.deepEqual(calls[0].start, [0, 12]);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it("b moves to previous word", () => {
|
|
661
|
+
const rep = makeRep(["hello world foo"]);
|
|
662
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
663
|
+
|
|
664
|
+
const ctx = {
|
|
665
|
+
rep,
|
|
666
|
+
editorInfo,
|
|
667
|
+
line: 0,
|
|
668
|
+
char: 12,
|
|
669
|
+
lineText: "hello world foo",
|
|
670
|
+
count: 1,
|
|
671
|
+
};
|
|
672
|
+
commands.normal["b"](ctx);
|
|
673
|
+
|
|
674
|
+
assert.equal(calls.length, 1);
|
|
675
|
+
assert.deepEqual(calls[0].start, [0, 6]);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it("e moves to end of word", () => {
|
|
679
|
+
const rep = makeRep(["hello world foo"]);
|
|
680
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
681
|
+
|
|
682
|
+
const ctx = {
|
|
683
|
+
rep,
|
|
684
|
+
editorInfo,
|
|
685
|
+
line: 0,
|
|
686
|
+
char: 0,
|
|
687
|
+
lineText: "hello world foo",
|
|
688
|
+
count: 1,
|
|
689
|
+
};
|
|
690
|
+
commands.normal["e"](ctx);
|
|
691
|
+
|
|
692
|
+
assert.equal(calls.length, 1);
|
|
693
|
+
assert.deepEqual(calls[0].start, [0, 4]);
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
describe("char motions (f/F/t/T)", () => {
|
|
698
|
+
beforeEach(() => {
|
|
699
|
+
state.mode = "normal";
|
|
700
|
+
state.pendingKey = null;
|
|
701
|
+
state.pendingCount = null;
|
|
702
|
+
state.countBuffer = "";
|
|
703
|
+
state.register = null;
|
|
704
|
+
state.marks = {};
|
|
705
|
+
state.lastCharSearch = null;
|
|
706
|
+
state.visualAnchor = null;
|
|
707
|
+
state.visualCursor = null;
|
|
708
|
+
state.editorDoc = null;
|
|
709
|
+
state.currentRep = null;
|
|
710
|
+
state.desiredColumn = null;
|
|
711
|
+
state.lastCommand = null;
|
|
712
|
+
state.searchMode = false;
|
|
713
|
+
state.searchBuffer = "";
|
|
714
|
+
state.searchDirection = null;
|
|
715
|
+
state.lastSearch = null;
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it("f enters pending mode for char search", () => {
|
|
719
|
+
const ctx = { rep: makeRep([]), line: 0, char: 0, lineText: "" };
|
|
720
|
+
commands.normal["f"](ctx);
|
|
721
|
+
|
|
722
|
+
assert.equal(state.pendingKey, "f");
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it("t enters pending mode for till search", () => {
|
|
726
|
+
const ctx = { rep: makeRep([]), line: 0, char: 0, lineText: "" };
|
|
727
|
+
commands.normal["t"](ctx);
|
|
728
|
+
|
|
729
|
+
assert.equal(state.pendingKey, "t");
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
it("F enters pending mode for backward char search", () => {
|
|
733
|
+
const ctx = { rep: makeRep([]), line: 0, char: 0, lineText: "" };
|
|
734
|
+
commands.normal["F"](ctx);
|
|
735
|
+
|
|
736
|
+
assert.equal(state.pendingKey, "F");
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it("T enters pending mode for backward till search", () => {
|
|
740
|
+
const ctx = { rep: makeRep([]), line: 0, char: 0, lineText: "" };
|
|
741
|
+
commands.normal["T"](ctx);
|
|
742
|
+
|
|
743
|
+
assert.equal(state.pendingKey, "T");
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
describe("paragraph motions", () => {
|
|
748
|
+
beforeEach(() => {
|
|
749
|
+
state.mode = "normal";
|
|
750
|
+
state.pendingKey = null;
|
|
751
|
+
state.pendingCount = null;
|
|
752
|
+
state.countBuffer = "";
|
|
753
|
+
state.register = null;
|
|
754
|
+
state.marks = {};
|
|
755
|
+
state.lastCharSearch = null;
|
|
756
|
+
state.visualAnchor = null;
|
|
757
|
+
state.visualCursor = null;
|
|
758
|
+
state.editorDoc = null;
|
|
759
|
+
state.currentRep = null;
|
|
760
|
+
state.desiredColumn = null;
|
|
761
|
+
state.lastCommand = null;
|
|
762
|
+
state.searchMode = false;
|
|
763
|
+
state.searchBuffer = "";
|
|
764
|
+
state.searchDirection = null;
|
|
765
|
+
state.lastSearch = null;
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
it("{ moves to previous empty line", () => {
|
|
769
|
+
const rep = makeRep(["text", "text", "", "text"]);
|
|
770
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
771
|
+
|
|
772
|
+
const ctx = {
|
|
773
|
+
rep,
|
|
774
|
+
editorInfo,
|
|
775
|
+
line: 3,
|
|
776
|
+
char: 0,
|
|
777
|
+
lineText: "text",
|
|
778
|
+
count: 1,
|
|
779
|
+
};
|
|
780
|
+
commands.normal["{"](ctx);
|
|
781
|
+
|
|
782
|
+
assert.equal(calls.length, 1);
|
|
783
|
+
assert.deepEqual(calls[0].start, [2, 0]);
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
it("} moves to next empty line", () => {
|
|
787
|
+
const rep = makeRep(["text", "", "text", "text"]);
|
|
788
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
789
|
+
|
|
790
|
+
const ctx = {
|
|
791
|
+
rep,
|
|
792
|
+
editorInfo,
|
|
793
|
+
line: 0,
|
|
794
|
+
char: 0,
|
|
795
|
+
lineText: "text",
|
|
796
|
+
count: 1,
|
|
797
|
+
};
|
|
798
|
+
commands.normal["}"](ctx);
|
|
799
|
+
|
|
800
|
+
assert.equal(calls.length, 1);
|
|
801
|
+
assert.deepEqual(calls[0].start, [1, 0]);
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
describe("line reference motions", () => {
|
|
806
|
+
beforeEach(() => {
|
|
807
|
+
state.mode = "normal";
|
|
808
|
+
state.pendingKey = null;
|
|
809
|
+
state.pendingCount = null;
|
|
810
|
+
state.countBuffer = "";
|
|
811
|
+
state.register = null;
|
|
812
|
+
state.marks = {};
|
|
813
|
+
state.lastCharSearch = null;
|
|
814
|
+
state.visualAnchor = null;
|
|
815
|
+
state.visualCursor = null;
|
|
816
|
+
state.editorDoc = null;
|
|
817
|
+
state.currentRep = null;
|
|
818
|
+
state.desiredColumn = null;
|
|
819
|
+
state.lastCommand = null;
|
|
820
|
+
state.searchMode = false;
|
|
821
|
+
state.searchBuffer = "";
|
|
822
|
+
state.searchDirection = null;
|
|
823
|
+
state.lastSearch = null;
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
it("H moves to top of visible area", () => {
|
|
827
|
+
const rep = makeRep(["a", "b", "c", "d", "e"]);
|
|
828
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
829
|
+
|
|
830
|
+
const ctx = {
|
|
831
|
+
rep,
|
|
832
|
+
editorInfo,
|
|
833
|
+
line: 2,
|
|
834
|
+
char: 0,
|
|
835
|
+
lineText: "c",
|
|
836
|
+
count: 1,
|
|
837
|
+
};
|
|
838
|
+
commands.normal["H"](ctx);
|
|
839
|
+
|
|
840
|
+
// Should move to first non-blank of top line
|
|
841
|
+
assert.equal(calls.length, 1);
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
it("L moves to bottom of visible area", () => {
|
|
845
|
+
const rep = makeRep(["a", "b", "c", "d", "e"]);
|
|
846
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
847
|
+
|
|
848
|
+
const ctx = {
|
|
849
|
+
rep,
|
|
850
|
+
editorInfo,
|
|
851
|
+
line: 0,
|
|
852
|
+
char: 0,
|
|
853
|
+
lineText: "a",
|
|
854
|
+
count: 1,
|
|
855
|
+
};
|
|
856
|
+
commands.normal["L"](ctx);
|
|
857
|
+
|
|
858
|
+
assert.equal(calls.length, 1);
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
it("M moves to middle of visible area", () => {
|
|
862
|
+
const rep = makeRep(["a", "b", "c", "d", "e"]);
|
|
863
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
864
|
+
|
|
865
|
+
const ctx = {
|
|
866
|
+
rep,
|
|
867
|
+
editorInfo,
|
|
868
|
+
line: 0,
|
|
869
|
+
char: 0,
|
|
870
|
+
lineText: "a",
|
|
871
|
+
count: 1,
|
|
872
|
+
};
|
|
873
|
+
commands.normal["M"](ctx);
|
|
874
|
+
|
|
875
|
+
assert.equal(calls.length, 1);
|
|
876
|
+
});
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
describe("delete operations", () => {
|
|
880
|
+
beforeEach(() => {
|
|
881
|
+
state.mode = "normal";
|
|
882
|
+
state.pendingKey = null;
|
|
883
|
+
state.pendingCount = null;
|
|
884
|
+
state.countBuffer = "";
|
|
885
|
+
state.register = null;
|
|
886
|
+
state.marks = {};
|
|
887
|
+
state.lastCharSearch = null;
|
|
888
|
+
state.visualAnchor = null;
|
|
889
|
+
state.visualCursor = null;
|
|
890
|
+
state.editorDoc = null;
|
|
891
|
+
state.currentRep = null;
|
|
892
|
+
state.desiredColumn = null;
|
|
893
|
+
state.lastCommand = null;
|
|
894
|
+
state.searchMode = false;
|
|
895
|
+
state.searchBuffer = "";
|
|
896
|
+
state.searchDirection = null;
|
|
897
|
+
state.lastSearch = null;
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
it("x deletes character at cursor", () => {
|
|
901
|
+
const rep = makeRep(["hello"]);
|
|
902
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
903
|
+
|
|
904
|
+
const ctx = {
|
|
905
|
+
rep,
|
|
906
|
+
editorInfo,
|
|
907
|
+
line: 0,
|
|
908
|
+
char: 1,
|
|
909
|
+
lineText: "hello",
|
|
910
|
+
count: 1,
|
|
911
|
+
};
|
|
912
|
+
commands.normal["x"](ctx);
|
|
913
|
+
|
|
914
|
+
assert.equal(state.register, "e");
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
it("x with count deletes multiple characters", () => {
|
|
918
|
+
const rep = makeRep(["hello"]);
|
|
919
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
920
|
+
|
|
921
|
+
const ctx = {
|
|
922
|
+
rep,
|
|
923
|
+
editorInfo,
|
|
924
|
+
line: 0,
|
|
925
|
+
char: 1,
|
|
926
|
+
lineText: "hello",
|
|
927
|
+
count: 3,
|
|
928
|
+
};
|
|
929
|
+
commands.normal["x"](ctx);
|
|
930
|
+
|
|
931
|
+
assert.equal(state.register, "ell");
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
it("dd deletes entire line", () => {
|
|
935
|
+
const rep = makeRep(["line1", "line2", "line3"]);
|
|
936
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
937
|
+
|
|
938
|
+
const ctx = {
|
|
939
|
+
rep,
|
|
940
|
+
editorInfo,
|
|
941
|
+
line: 1,
|
|
942
|
+
char: 0,
|
|
943
|
+
lineText: "line2",
|
|
944
|
+
count: 1,
|
|
945
|
+
};
|
|
946
|
+
commands.normal["dd"](ctx);
|
|
947
|
+
|
|
948
|
+
assert.deepEqual(state.register, ["line2"]);
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
it("yy yanks entire line to register", () => {
|
|
952
|
+
const rep = makeRep(["line1", "line2", "line3"]);
|
|
953
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
954
|
+
|
|
955
|
+
const ctx = {
|
|
956
|
+
rep,
|
|
957
|
+
editorInfo,
|
|
958
|
+
line: 1,
|
|
959
|
+
char: 0,
|
|
960
|
+
lineText: "line2",
|
|
961
|
+
count: 1,
|
|
962
|
+
};
|
|
963
|
+
commands.normal["yy"](ctx);
|
|
964
|
+
|
|
965
|
+
assert.equal(state.register.length, 1);
|
|
966
|
+
assert.equal(state.register[0], "line2");
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
it("cc changes entire line (deletes and enters insert)", () => {
|
|
970
|
+
const rep = makeRep(["line1", "line2", "line3"]);
|
|
971
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
972
|
+
|
|
973
|
+
const ctx = {
|
|
974
|
+
rep,
|
|
975
|
+
editorInfo,
|
|
976
|
+
line: 1,
|
|
977
|
+
char: 0,
|
|
978
|
+
lineText: "line2",
|
|
979
|
+
count: 1,
|
|
980
|
+
};
|
|
981
|
+
commands.normal["cc"](ctx);
|
|
982
|
+
|
|
983
|
+
assert.equal(state.mode, "insert");
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
it("D deletes from cursor to end of line", () => {
|
|
987
|
+
const rep = makeRep(["hello world"]);
|
|
988
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
989
|
+
|
|
990
|
+
const ctx = {
|
|
991
|
+
rep,
|
|
992
|
+
editorInfo,
|
|
993
|
+
line: 0,
|
|
994
|
+
char: 6,
|
|
995
|
+
lineText: "hello world",
|
|
996
|
+
};
|
|
997
|
+
commands.normal["D"](ctx);
|
|
998
|
+
|
|
999
|
+
assert.equal(state.register, "world");
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
it("J joins lines", () => {
|
|
1003
|
+
const rep = makeRep(["hello", "world"]);
|
|
1004
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1005
|
+
|
|
1006
|
+
const ctx = {
|
|
1007
|
+
rep,
|
|
1008
|
+
editorInfo,
|
|
1009
|
+
line: 0,
|
|
1010
|
+
char: 0,
|
|
1011
|
+
lineText: "hello",
|
|
1012
|
+
count: 1,
|
|
1013
|
+
};
|
|
1014
|
+
commands.normal["J"](ctx);
|
|
1015
|
+
|
|
1016
|
+
assert.equal(state.mode, "normal");
|
|
1017
|
+
});
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
describe("insert mode commands", () => {
|
|
1021
|
+
beforeEach(() => {
|
|
1022
|
+
state.mode = "normal";
|
|
1023
|
+
state.pendingKey = null;
|
|
1024
|
+
state.pendingCount = null;
|
|
1025
|
+
state.countBuffer = "";
|
|
1026
|
+
state.register = null;
|
|
1027
|
+
state.marks = {};
|
|
1028
|
+
state.lastCharSearch = null;
|
|
1029
|
+
state.visualAnchor = null;
|
|
1030
|
+
state.visualCursor = null;
|
|
1031
|
+
state.editorDoc = null;
|
|
1032
|
+
state.currentRep = null;
|
|
1033
|
+
state.desiredColumn = null;
|
|
1034
|
+
state.lastCommand = null;
|
|
1035
|
+
state.searchMode = false;
|
|
1036
|
+
state.searchBuffer = "";
|
|
1037
|
+
state.searchDirection = null;
|
|
1038
|
+
state.lastSearch = null;
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
it("i enters insert mode at cursor", () => {
|
|
1042
|
+
const rep = makeRep(["hello"]);
|
|
1043
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1044
|
+
|
|
1045
|
+
const ctx = { rep, editorInfo, line: 0, char: 2, lineText: "hello" };
|
|
1046
|
+
commands.normal["i"](ctx);
|
|
1047
|
+
|
|
1048
|
+
assert.equal(state.mode, "insert");
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
it("a enters insert mode after cursor", () => {
|
|
1052
|
+
const rep = makeRep(["hello"]);
|
|
1053
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1054
|
+
|
|
1055
|
+
const ctx = { rep, editorInfo, line: 0, char: 2, lineText: "hello" };
|
|
1056
|
+
commands.normal["a"](ctx);
|
|
1057
|
+
|
|
1058
|
+
assert.equal(state.mode, "insert");
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
it("A enters insert mode at end of line", () => {
|
|
1062
|
+
const rep = makeRep(["hello"]);
|
|
1063
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1064
|
+
|
|
1065
|
+
const ctx = { rep, editorInfo, line: 0, char: 2, lineText: "hello" };
|
|
1066
|
+
commands.normal["A"](ctx);
|
|
1067
|
+
|
|
1068
|
+
assert.equal(state.mode, "insert");
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
it("I enters insert mode at first non-blank", () => {
|
|
1072
|
+
const rep = makeRep([" hello"]);
|
|
1073
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1074
|
+
|
|
1075
|
+
const ctx = { rep, editorInfo, line: 0, char: 2, lineText: " hello" };
|
|
1076
|
+
commands.normal["I"](ctx);
|
|
1077
|
+
|
|
1078
|
+
assert.equal(state.mode, "insert");
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
it("o opens line below", () => {
|
|
1082
|
+
const rep = makeRep(["hello"]);
|
|
1083
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1084
|
+
|
|
1085
|
+
const ctx = { rep, editorInfo, line: 0, char: 0, lineText: "hello" };
|
|
1086
|
+
commands.normal["o"](ctx);
|
|
1087
|
+
|
|
1088
|
+
assert.equal(state.mode, "insert");
|
|
1089
|
+
assert.equal(calls.length, 2);
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
it("O opens line above", () => {
|
|
1093
|
+
const rep = makeRep(["hello"]);
|
|
1094
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1095
|
+
|
|
1096
|
+
const ctx = { rep, editorInfo, line: 0, char: 0, lineText: "hello" };
|
|
1097
|
+
commands.normal["O"](ctx);
|
|
1098
|
+
|
|
1099
|
+
assert.equal(state.mode, "insert");
|
|
1100
|
+
assert.equal(calls.length, 2);
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
it("s replaces character and enters insert", () => {
|
|
1104
|
+
const rep = makeRep(["hello"]);
|
|
1105
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1106
|
+
|
|
1107
|
+
const ctx = {
|
|
1108
|
+
rep,
|
|
1109
|
+
editorInfo,
|
|
1110
|
+
line: 0,
|
|
1111
|
+
char: 1,
|
|
1112
|
+
lineText: "hello",
|
|
1113
|
+
count: 1,
|
|
1114
|
+
};
|
|
1115
|
+
commands.normal["s"](ctx);
|
|
1116
|
+
|
|
1117
|
+
assert.equal(state.mode, "insert");
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
it("S replaces entire line and enters insert", () => {
|
|
1121
|
+
const rep = makeRep(["hello"]);
|
|
1122
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1123
|
+
|
|
1124
|
+
const ctx = { rep, editorInfo, line: 0, char: 2, lineText: "hello" };
|
|
1125
|
+
commands.normal["S"](ctx);
|
|
1126
|
+
|
|
1127
|
+
assert.equal(state.mode, "insert");
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
it("C changes to end of line and enters insert", () => {
|
|
1131
|
+
const rep = makeRep(["hello world"]);
|
|
1132
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1133
|
+
|
|
1134
|
+
const ctx = {
|
|
1135
|
+
rep,
|
|
1136
|
+
editorInfo,
|
|
1137
|
+
line: 0,
|
|
1138
|
+
char: 6,
|
|
1139
|
+
lineText: "hello world",
|
|
1140
|
+
};
|
|
1141
|
+
commands.normal["C"](ctx);
|
|
1142
|
+
|
|
1143
|
+
assert.equal(state.mode, "insert");
|
|
1144
|
+
});
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
describe("replace command", () => {
|
|
1148
|
+
beforeEach(() => {
|
|
1149
|
+
state.mode = "normal";
|
|
1150
|
+
state.pendingKey = null;
|
|
1151
|
+
state.pendingCount = null;
|
|
1152
|
+
state.countBuffer = "";
|
|
1153
|
+
state.register = null;
|
|
1154
|
+
state.marks = {};
|
|
1155
|
+
state.lastCharSearch = null;
|
|
1156
|
+
state.visualAnchor = null;
|
|
1157
|
+
state.visualCursor = null;
|
|
1158
|
+
state.editorDoc = null;
|
|
1159
|
+
state.currentRep = null;
|
|
1160
|
+
state.desiredColumn = null;
|
|
1161
|
+
state.lastCommand = null;
|
|
1162
|
+
state.searchMode = false;
|
|
1163
|
+
state.searchBuffer = "";
|
|
1164
|
+
state.searchDirection = null;
|
|
1165
|
+
state.lastSearch = null;
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
it("r enters pending mode for replace", () => {
|
|
1169
|
+
const ctx = { rep: makeRep([]), line: 0, char: 0, lineText: "" };
|
|
1170
|
+
commands.normal["r"](ctx);
|
|
1171
|
+
|
|
1172
|
+
assert.equal(state.pendingKey, "r");
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
it("r replaces single character", () => {
|
|
1176
|
+
const rep = makeRep(["hello"]);
|
|
1177
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1178
|
+
|
|
1179
|
+
const ctx = {
|
|
1180
|
+
rep,
|
|
1181
|
+
editorInfo,
|
|
1182
|
+
line: 0,
|
|
1183
|
+
char: 1,
|
|
1184
|
+
lineText: "hello",
|
|
1185
|
+
count: 1,
|
|
1186
|
+
};
|
|
1187
|
+
parameterized["r"]("x", ctx);
|
|
1188
|
+
|
|
1189
|
+
assert.equal(calls.length, 2);
|
|
1190
|
+
assert.deepEqual(calls[0], {
|
|
1191
|
+
type: "replace",
|
|
1192
|
+
start: [0, 1],
|
|
1193
|
+
end: [0, 2],
|
|
1194
|
+
newText: "x",
|
|
1195
|
+
});
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
it("r with count replaces multiple characters", () => {
|
|
1199
|
+
const rep = makeRep(["hello"]);
|
|
1200
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1201
|
+
|
|
1202
|
+
const ctx = {
|
|
1203
|
+
rep,
|
|
1204
|
+
editorInfo,
|
|
1205
|
+
line: 0,
|
|
1206
|
+
char: 1,
|
|
1207
|
+
lineText: "hello",
|
|
1208
|
+
count: 3,
|
|
1209
|
+
};
|
|
1210
|
+
parameterized["r"]("x", ctx);
|
|
1211
|
+
|
|
1212
|
+
assert.equal(calls.length, 2);
|
|
1213
|
+
});
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
describe("paste commands", () => {
|
|
1217
|
+
beforeEach(() => {
|
|
1218
|
+
state.mode = "normal";
|
|
1219
|
+
state.pendingKey = null;
|
|
1220
|
+
state.pendingCount = null;
|
|
1221
|
+
state.countBuffer = "";
|
|
1222
|
+
state.register = null;
|
|
1223
|
+
state.marks = {};
|
|
1224
|
+
state.lastCharSearch = null;
|
|
1225
|
+
state.visualAnchor = null;
|
|
1226
|
+
state.visualCursor = null;
|
|
1227
|
+
state.editorDoc = null;
|
|
1228
|
+
state.currentRep = null;
|
|
1229
|
+
state.desiredColumn = null;
|
|
1230
|
+
state.lastCommand = null;
|
|
1231
|
+
state.searchMode = false;
|
|
1232
|
+
state.searchBuffer = "";
|
|
1233
|
+
state.searchDirection = null;
|
|
1234
|
+
state.lastSearch = null;
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
it("p pastes string register after cursor", () => {
|
|
1238
|
+
const rep = makeRep(["hello"]);
|
|
1239
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1240
|
+
|
|
1241
|
+
state.register = "x";
|
|
1242
|
+
const ctx = {
|
|
1243
|
+
rep,
|
|
1244
|
+
editorInfo,
|
|
1245
|
+
line: 0,
|
|
1246
|
+
char: 0,
|
|
1247
|
+
lineText: "hello",
|
|
1248
|
+
count: 1,
|
|
1249
|
+
};
|
|
1250
|
+
commands.normal["p"](ctx);
|
|
1251
|
+
|
|
1252
|
+
assert.equal(state.mode, "normal");
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
it("p pastes with count repeats content", () => {
|
|
1256
|
+
const rep = makeRep(["hello"]);
|
|
1257
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1258
|
+
|
|
1259
|
+
state.register = "a";
|
|
1260
|
+
const ctx = {
|
|
1261
|
+
rep,
|
|
1262
|
+
editorInfo,
|
|
1263
|
+
line: 0,
|
|
1264
|
+
char: 0,
|
|
1265
|
+
lineText: "hello",
|
|
1266
|
+
count: 2,
|
|
1267
|
+
};
|
|
1268
|
+
commands.normal["p"](ctx);
|
|
1269
|
+
|
|
1270
|
+
assert.equal(state.mode, "normal");
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
it("p pastes line register on new line", () => {
|
|
1274
|
+
const rep = makeRep(["hello", "world"]);
|
|
1275
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1276
|
+
|
|
1277
|
+
state.register = ["inserted"];
|
|
1278
|
+
const ctx = {
|
|
1279
|
+
rep,
|
|
1280
|
+
editorInfo,
|
|
1281
|
+
line: 0,
|
|
1282
|
+
char: 0,
|
|
1283
|
+
lineText: "hello",
|
|
1284
|
+
count: 1,
|
|
1285
|
+
};
|
|
1286
|
+
commands.normal["p"](ctx);
|
|
1287
|
+
|
|
1288
|
+
assert.equal(state.mode, "normal");
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
it("P pastes string register before cursor", () => {
|
|
1292
|
+
const rep = makeRep(["hello"]);
|
|
1293
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1294
|
+
|
|
1295
|
+
state.register = "x";
|
|
1296
|
+
const ctx = {
|
|
1297
|
+
rep,
|
|
1298
|
+
editorInfo,
|
|
1299
|
+
line: 0,
|
|
1300
|
+
char: 2,
|
|
1301
|
+
lineText: "hello",
|
|
1302
|
+
count: 1,
|
|
1303
|
+
};
|
|
1304
|
+
commands.normal["P"](ctx);
|
|
1305
|
+
|
|
1306
|
+
assert.equal(state.mode, "normal");
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
it("P pastes line register on new line above", () => {
|
|
1310
|
+
const rep = makeRep(["hello", "world"]);
|
|
1311
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1312
|
+
|
|
1313
|
+
state.register = ["inserted"];
|
|
1314
|
+
const ctx = {
|
|
1315
|
+
rep,
|
|
1316
|
+
editorInfo,
|
|
1317
|
+
line: 1,
|
|
1318
|
+
char: 0,
|
|
1319
|
+
lineText: "world",
|
|
1320
|
+
count: 1,
|
|
1321
|
+
};
|
|
1322
|
+
commands.normal["P"](ctx);
|
|
1323
|
+
|
|
1324
|
+
assert.equal(state.mode, "normal");
|
|
1325
|
+
});
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
describe("case toggle", () => {
|
|
1329
|
+
beforeEach(() => {
|
|
1330
|
+
state.mode = "normal";
|
|
1331
|
+
state.pendingKey = null;
|
|
1332
|
+
state.pendingCount = null;
|
|
1333
|
+
state.countBuffer = "";
|
|
1334
|
+
state.register = null;
|
|
1335
|
+
state.marks = {};
|
|
1336
|
+
state.lastCharSearch = null;
|
|
1337
|
+
state.visualAnchor = null;
|
|
1338
|
+
state.visualCursor = null;
|
|
1339
|
+
state.editorDoc = null;
|
|
1340
|
+
state.currentRep = null;
|
|
1341
|
+
state.desiredColumn = null;
|
|
1342
|
+
state.lastCommand = null;
|
|
1343
|
+
state.searchMode = false;
|
|
1344
|
+
state.searchBuffer = "";
|
|
1345
|
+
state.searchDirection = null;
|
|
1346
|
+
state.lastSearch = null;
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
it("~ toggles case of character", () => {
|
|
1350
|
+
const rep = makeRep(["hello"]);
|
|
1351
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1352
|
+
|
|
1353
|
+
const ctx = {
|
|
1354
|
+
rep,
|
|
1355
|
+
editorInfo,
|
|
1356
|
+
line: 0,
|
|
1357
|
+
char: 0,
|
|
1358
|
+
lineText: "hello",
|
|
1359
|
+
count: 1,
|
|
1360
|
+
};
|
|
1361
|
+
commands.normal["~"](ctx);
|
|
1362
|
+
|
|
1363
|
+
assert.equal(state.mode, "normal");
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
it("~ with count toggles multiple characters", () => {
|
|
1367
|
+
const rep = makeRep(["hello"]);
|
|
1368
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1369
|
+
|
|
1370
|
+
const ctx = {
|
|
1371
|
+
rep,
|
|
1372
|
+
editorInfo,
|
|
1373
|
+
line: 0,
|
|
1374
|
+
char: 0,
|
|
1375
|
+
lineText: "hello",
|
|
1376
|
+
count: 3,
|
|
1377
|
+
};
|
|
1378
|
+
commands.normal["~"](ctx);
|
|
1379
|
+
|
|
1380
|
+
assert.equal(state.mode, "normal");
|
|
1381
|
+
});
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
describe("undo command", () => {
|
|
1385
|
+
beforeEach(() => {
|
|
1386
|
+
state.mode = "normal";
|
|
1387
|
+
state.pendingKey = null;
|
|
1388
|
+
state.pendingCount = null;
|
|
1389
|
+
state.countBuffer = "";
|
|
1390
|
+
state.register = null;
|
|
1391
|
+
state.marks = {};
|
|
1392
|
+
state.lastCharSearch = null;
|
|
1393
|
+
state.visualAnchor = null;
|
|
1394
|
+
state.visualCursor = null;
|
|
1395
|
+
state.editorDoc = null;
|
|
1396
|
+
state.currentRep = null;
|
|
1397
|
+
state.desiredColumn = null;
|
|
1398
|
+
state.lastCommand = null;
|
|
1399
|
+
state.searchMode = false;
|
|
1400
|
+
state.searchBuffer = "";
|
|
1401
|
+
state.searchDirection = null;
|
|
1402
|
+
state.lastSearch = null;
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
it("u calls undo", () => {
|
|
1406
|
+
const rep = makeRep(["hello"]);
|
|
1407
|
+
const { editorInfo: baseEditorInfo } = makeMockEditorInfo();
|
|
1408
|
+
|
|
1409
|
+
const editorInfo = {
|
|
1410
|
+
...baseEditorInfo,
|
|
1411
|
+
ace_doUndoRedo: () => {},
|
|
1412
|
+
};
|
|
1413
|
+
|
|
1414
|
+
const ctx = { rep, editorInfo, line: 0, char: 0, lineText: "hello" };
|
|
1415
|
+
commands.normal["u"](ctx);
|
|
1416
|
+
});
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
describe("repeat command", () => {
|
|
1420
|
+
beforeEach(() => {
|
|
1421
|
+
state.mode = "normal";
|
|
1422
|
+
state.pendingKey = null;
|
|
1423
|
+
state.pendingCount = null;
|
|
1424
|
+
state.countBuffer = "";
|
|
1425
|
+
state.register = null;
|
|
1426
|
+
state.marks = {};
|
|
1427
|
+
state.lastCharSearch = null;
|
|
1428
|
+
state.visualAnchor = null;
|
|
1429
|
+
state.visualCursor = null;
|
|
1430
|
+
state.editorDoc = null;
|
|
1431
|
+
state.currentRep = null;
|
|
1432
|
+
state.desiredColumn = null;
|
|
1433
|
+
state.lastCommand = null;
|
|
1434
|
+
state.searchMode = false;
|
|
1435
|
+
state.searchBuffer = "";
|
|
1436
|
+
state.searchDirection = null;
|
|
1437
|
+
state.lastSearch = null;
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
it(". repeats last command", () => {
|
|
1441
|
+
const rep = makeRep(["hello"]);
|
|
1442
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1443
|
+
|
|
1444
|
+
state.lastCommand = { key: "h", count: 1, param: null };
|
|
1445
|
+
|
|
1446
|
+
const ctx = {
|
|
1447
|
+
rep,
|
|
1448
|
+
editorInfo,
|
|
1449
|
+
line: 0,
|
|
1450
|
+
char: 3,
|
|
1451
|
+
lineText: "hello",
|
|
1452
|
+
};
|
|
1453
|
+
commands.normal["."](ctx);
|
|
1454
|
+
|
|
1455
|
+
assert.equal(calls.length, 1);
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
it(". does nothing with no last command", () => {
|
|
1459
|
+
const rep = makeRep(["hello"]);
|
|
1460
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1461
|
+
|
|
1462
|
+
state.lastCommand = null;
|
|
1463
|
+
|
|
1464
|
+
const ctx = { rep, editorInfo, line: 0, char: 0, lineText: "hello" };
|
|
1465
|
+
commands.normal["."](ctx);
|
|
1466
|
+
|
|
1467
|
+
assert.equal(calls.length, 0);
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
it(". repeats parameterized command", () => {
|
|
1471
|
+
const rep = makeRep(["hello world"]);
|
|
1472
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1473
|
+
|
|
1474
|
+
state.lastCommand = { key: "m", count: 1, param: "a" };
|
|
1475
|
+
|
|
1476
|
+
const ctx = {
|
|
1477
|
+
rep,
|
|
1478
|
+
editorInfo,
|
|
1479
|
+
line: 0,
|
|
1480
|
+
char: 2,
|
|
1481
|
+
lineText: "hello world",
|
|
1482
|
+
};
|
|
1483
|
+
commands.normal["."](ctx);
|
|
1484
|
+
|
|
1485
|
+
assert.deepEqual(state.marks["a"], [0, 2]);
|
|
1486
|
+
});
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
describe("visual mode", () => {
|
|
1490
|
+
beforeEach(() => {
|
|
1491
|
+
state.mode = "normal";
|
|
1492
|
+
state.pendingKey = null;
|
|
1493
|
+
state.pendingCount = null;
|
|
1494
|
+
state.countBuffer = "";
|
|
1495
|
+
state.register = null;
|
|
1496
|
+
state.marks = {};
|
|
1497
|
+
state.lastCharSearch = null;
|
|
1498
|
+
state.visualAnchor = null;
|
|
1499
|
+
state.visualCursor = null;
|
|
1500
|
+
state.editorDoc = null;
|
|
1501
|
+
state.currentRep = null;
|
|
1502
|
+
state.desiredColumn = null;
|
|
1503
|
+
state.lastCommand = null;
|
|
1504
|
+
state.searchMode = false;
|
|
1505
|
+
state.searchBuffer = "";
|
|
1506
|
+
state.searchDirection = null;
|
|
1507
|
+
state.lastSearch = null;
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
it("v enters visual-char mode and shows character highlighted", () => {
|
|
1511
|
+
const rep = makeRep(["abcdef"]);
|
|
1512
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1513
|
+
|
|
1514
|
+
const ctx = { rep, editorInfo, line: 0, char: 1, lineText: "abcdef" };
|
|
1515
|
+
commands.normal["v"](ctx);
|
|
1516
|
+
|
|
1517
|
+
assert.equal(state.mode, "visual-char");
|
|
1518
|
+
assert.deepEqual(state.visualAnchor, [0, 1]);
|
|
1519
|
+
assert.deepEqual(state.visualCursor, [0, 1]);
|
|
1520
|
+
// Should call selectRange with [0,1] and [0,2] to show char at position 1 highlighted
|
|
1521
|
+
assert.equal(calls.length, 1);
|
|
1522
|
+
assert.deepEqual(calls[0].start, [0, 1]);
|
|
1523
|
+
assert.deepEqual(calls[0].end, [0, 2]);
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
it("l in visual-char mode extends selection correctly", () => {
|
|
1527
|
+
const rep = makeRep(["abcdef"]);
|
|
1528
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1529
|
+
|
|
1530
|
+
state.mode = "visual-char";
|
|
1531
|
+
state.visualAnchor = [0, 1];
|
|
1532
|
+
state.visualCursor = [0, 1];
|
|
1533
|
+
|
|
1534
|
+
const ctx = {
|
|
1535
|
+
rep,
|
|
1536
|
+
editorInfo,
|
|
1537
|
+
line: 0,
|
|
1538
|
+
char: 1,
|
|
1539
|
+
lineText: "abcdef",
|
|
1540
|
+
count: 1,
|
|
1541
|
+
};
|
|
1542
|
+
commands["visual-char"]["l"](ctx);
|
|
1543
|
+
|
|
1544
|
+
// After moving right once, cursor should be at position 2
|
|
1545
|
+
assert.deepEqual(state.visualCursor, [0, 2]);
|
|
1546
|
+
// Should select from 1 to 3 (positions 1 and 2)
|
|
1547
|
+
assert.equal(calls.length, 1);
|
|
1548
|
+
assert.deepEqual(calls[0].start, [0, 1]);
|
|
1549
|
+
assert.deepEqual(calls[0].end, [0, 3]);
|
|
1550
|
+
});
|
|
1551
|
+
|
|
1552
|
+
it("ll in visual-char mode extends selection to include cursor position", () => {
|
|
1553
|
+
const rep = makeRep(["abcdef"]);
|
|
1554
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1555
|
+
|
|
1556
|
+
state.mode = "visual-char";
|
|
1557
|
+
state.visualAnchor = [0, 1];
|
|
1558
|
+
state.visualCursor = [0, 1];
|
|
1559
|
+
|
|
1560
|
+
// Press l twice to move from pos 1 to pos 3
|
|
1561
|
+
const ctx1 = {
|
|
1562
|
+
rep,
|
|
1563
|
+
editorInfo,
|
|
1564
|
+
line: 0,
|
|
1565
|
+
char: 1,
|
|
1566
|
+
lineText: "abcdef",
|
|
1567
|
+
count: 2,
|
|
1568
|
+
};
|
|
1569
|
+
commands["visual-char"]["l"](ctx1);
|
|
1570
|
+
|
|
1571
|
+
// After moving right twice, cursor should be at position 3
|
|
1572
|
+
assert.deepEqual(state.visualCursor, [0, 3]);
|
|
1573
|
+
// Should select positions 1, 2, 3 -> range [1, 4)
|
|
1574
|
+
assert.equal(calls.length, 1);
|
|
1575
|
+
assert.deepEqual(calls[0].start, [0, 1]);
|
|
1576
|
+
assert.deepEqual(calls[0].end, [0, 4]);
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1579
|
+
it("d in visual-char deletes all selected characters including cursor", () => {
|
|
1580
|
+
const rep = makeRep(["abcdef"]);
|
|
1581
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1582
|
+
|
|
1583
|
+
state.mode = "visual-char";
|
|
1584
|
+
state.visualAnchor = [0, 1];
|
|
1585
|
+
state.visualCursor = [0, 3];
|
|
1586
|
+
|
|
1587
|
+
const ctx = { rep, editorInfo, line: 0, char: 3, lineText: "abcdef" };
|
|
1588
|
+
commands["visual-char"]["d"](ctx);
|
|
1589
|
+
|
|
1590
|
+
// Should delete positions 1, 2, 3 -> "bcd"
|
|
1591
|
+
assert.equal(state.mode, "normal");
|
|
1592
|
+
assert.equal(state.register, "bcd");
|
|
1593
|
+
});
|
|
1594
|
+
|
|
1595
|
+
it("V enters visual-line mode", () => {
|
|
1596
|
+
const rep = makeRep(["hello"]);
|
|
1597
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1598
|
+
|
|
1599
|
+
const ctx = { rep, editorInfo, line: 0, char: 2, lineText: "hello" };
|
|
1600
|
+
commands.normal["V"](ctx);
|
|
1601
|
+
|
|
1602
|
+
assert.equal(state.mode, "visual-line");
|
|
1603
|
+
assert.deepEqual(state.visualAnchor, [0, 0]);
|
|
1604
|
+
assert.deepEqual(state.visualCursor, [0, 0]);
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
it("y in visual-line yanks lines", () => {
|
|
1608
|
+
const rep = makeRep(["line1", "line2", "line3"]);
|
|
1609
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1610
|
+
|
|
1611
|
+
state.mode = "visual-line";
|
|
1612
|
+
state.visualAnchor = [0, 0];
|
|
1613
|
+
state.visualCursor = [1, 0];
|
|
1614
|
+
|
|
1615
|
+
const ctx = {
|
|
1616
|
+
rep,
|
|
1617
|
+
editorInfo,
|
|
1618
|
+
line: 1,
|
|
1619
|
+
char: 0,
|
|
1620
|
+
lineText: "line2",
|
|
1621
|
+
};
|
|
1622
|
+
commands["visual-line"]["y"](ctx);
|
|
1623
|
+
|
|
1624
|
+
assert.equal(state.mode, "normal");
|
|
1625
|
+
assert.equal(state.register.length, 2);
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
it("~ toggles case in visual-char", () => {
|
|
1629
|
+
const rep = makeRep(["hello world"]);
|
|
1630
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1631
|
+
|
|
1632
|
+
state.mode = "visual-char";
|
|
1633
|
+
state.visualAnchor = [0, 0];
|
|
1634
|
+
state.visualCursor = [0, 5];
|
|
1635
|
+
|
|
1636
|
+
const ctx = { rep, editorInfo, line: 0, char: 5, lineText: "hello world" };
|
|
1637
|
+
commands["visual-char"]["~"](ctx);
|
|
1638
|
+
|
|
1639
|
+
assert.equal(state.mode, "normal");
|
|
1640
|
+
});
|
|
1641
|
+
});
|
|
1642
|
+
|
|
1643
|
+
describe("search commands", () => {
|
|
1644
|
+
beforeEach(() => {
|
|
1645
|
+
state.mode = "normal";
|
|
1646
|
+
state.pendingKey = null;
|
|
1647
|
+
state.pendingCount = null;
|
|
1648
|
+
state.countBuffer = "";
|
|
1649
|
+
state.register = null;
|
|
1650
|
+
state.marks = {};
|
|
1651
|
+
state.lastCharSearch = null;
|
|
1652
|
+
state.visualAnchor = null;
|
|
1653
|
+
state.visualCursor = null;
|
|
1654
|
+
state.editorDoc = null;
|
|
1655
|
+
state.currentRep = null;
|
|
1656
|
+
state.desiredColumn = null;
|
|
1657
|
+
state.lastCommand = null;
|
|
1658
|
+
state.searchMode = false;
|
|
1659
|
+
state.searchBuffer = "";
|
|
1660
|
+
state.searchDirection = null;
|
|
1661
|
+
state.lastSearch = null;
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
it("/ enters search mode forward", () => {
|
|
1665
|
+
commands.normal["/"](state);
|
|
1666
|
+
|
|
1667
|
+
assert.equal(state.searchMode, true);
|
|
1668
|
+
assert.equal(state.searchDirection, "/");
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
it("? enters search mode backward", () => {
|
|
1672
|
+
commands.normal["?"](state);
|
|
1673
|
+
|
|
1674
|
+
assert.equal(state.searchMode, true);
|
|
1675
|
+
assert.equal(state.searchDirection, "?");
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
it("n searches next match", () => {
|
|
1679
|
+
const rep = makeRep(["hello world hello"]);
|
|
1680
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1681
|
+
|
|
1682
|
+
state.lastSearch = { pattern: "hello", direction: "/" };
|
|
1683
|
+
|
|
1684
|
+
const ctx = {
|
|
1685
|
+
rep,
|
|
1686
|
+
editorInfo,
|
|
1687
|
+
line: 0,
|
|
1688
|
+
char: 0,
|
|
1689
|
+
lineText: "hello world hello",
|
|
1690
|
+
};
|
|
1691
|
+
commands.normal["n"](ctx);
|
|
1692
|
+
|
|
1693
|
+
assert.equal(calls.length, 1);
|
|
1694
|
+
});
|
|
1695
|
+
|
|
1696
|
+
it("N searches previous match", () => {
|
|
1697
|
+
const rep = makeRep(["hello world hello"]);
|
|
1698
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1699
|
+
|
|
1700
|
+
state.lastSearch = { pattern: "hello", direction: "/" };
|
|
1701
|
+
|
|
1702
|
+
const ctx = {
|
|
1703
|
+
rep,
|
|
1704
|
+
editorInfo,
|
|
1705
|
+
line: 0,
|
|
1706
|
+
char: 12,
|
|
1707
|
+
lineText: "hello world hello",
|
|
1708
|
+
};
|
|
1709
|
+
commands.normal["N"](ctx);
|
|
1710
|
+
|
|
1711
|
+
assert.equal(calls.length, 1);
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
it("n does nothing with no last search", () => {
|
|
1715
|
+
const rep = makeRep(["hello world"]);
|
|
1716
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1717
|
+
|
|
1718
|
+
state.lastSearch = null;
|
|
1719
|
+
|
|
1720
|
+
const ctx = { rep, editorInfo, line: 0, char: 0, lineText: "hello world" };
|
|
1721
|
+
commands.normal["n"](ctx);
|
|
1722
|
+
|
|
1723
|
+
assert.equal(calls.length, 0);
|
|
1724
|
+
});
|
|
1725
|
+
});
|
|
1726
|
+
|
|
1727
|
+
describe("text objects", () => {
|
|
1728
|
+
beforeEach(() => {
|
|
1729
|
+
state.mode = "normal";
|
|
1730
|
+
state.pendingKey = null;
|
|
1731
|
+
state.pendingCount = null;
|
|
1732
|
+
state.countBuffer = "";
|
|
1733
|
+
state.register = null;
|
|
1734
|
+
state.marks = {};
|
|
1735
|
+
state.lastCharSearch = null;
|
|
1736
|
+
state.visualAnchor = null;
|
|
1737
|
+
state.visualCursor = null;
|
|
1738
|
+
state.editorDoc = null;
|
|
1739
|
+
state.currentRep = null;
|
|
1740
|
+
state.desiredColumn = null;
|
|
1741
|
+
state.lastCommand = null;
|
|
1742
|
+
state.searchMode = false;
|
|
1743
|
+
state.searchBuffer = "";
|
|
1744
|
+
state.searchDirection = null;
|
|
1745
|
+
state.lastSearch = null;
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1748
|
+
it("diw deletes inner word", () => {
|
|
1749
|
+
const rep = makeRep(["hello world"]);
|
|
1750
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1751
|
+
|
|
1752
|
+
const ctx = {
|
|
1753
|
+
rep,
|
|
1754
|
+
editorInfo,
|
|
1755
|
+
line: 0,
|
|
1756
|
+
char: 0,
|
|
1757
|
+
lineText: "hello world",
|
|
1758
|
+
};
|
|
1759
|
+
commands.normal["diw"](ctx);
|
|
1760
|
+
});
|
|
1761
|
+
|
|
1762
|
+
it("daw deletes a word", () => {
|
|
1763
|
+
const rep = makeRep(["hello world"]);
|
|
1764
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1765
|
+
|
|
1766
|
+
const ctx = {
|
|
1767
|
+
rep,
|
|
1768
|
+
editorInfo,
|
|
1769
|
+
line: 0,
|
|
1770
|
+
char: 0,
|
|
1771
|
+
lineText: "hello world",
|
|
1772
|
+
};
|
|
1773
|
+
commands.normal["daw"](ctx);
|
|
1774
|
+
});
|
|
1775
|
+
|
|
1776
|
+
it("yiw yanks inner word", () => {
|
|
1777
|
+
const rep = makeRep(["hello world"]);
|
|
1778
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1779
|
+
|
|
1780
|
+
const ctx = {
|
|
1781
|
+
rep,
|
|
1782
|
+
editorInfo,
|
|
1783
|
+
line: 0,
|
|
1784
|
+
char: 0,
|
|
1785
|
+
lineText: "hello world",
|
|
1786
|
+
};
|
|
1787
|
+
commands.normal["yiw"](ctx);
|
|
1788
|
+
|
|
1789
|
+
assert.equal(typeof state.register, "string");
|
|
1790
|
+
});
|
|
1791
|
+
|
|
1792
|
+
it("ci( changes inner parentheses", () => {
|
|
1793
|
+
const rep = makeRep(["func(arg)"]);
|
|
1794
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1795
|
+
|
|
1796
|
+
const ctx = {
|
|
1797
|
+
rep,
|
|
1798
|
+
editorInfo,
|
|
1799
|
+
line: 0,
|
|
1800
|
+
char: 5,
|
|
1801
|
+
lineText: "func(arg)",
|
|
1802
|
+
};
|
|
1803
|
+
commands.normal["ci("](ctx);
|
|
1804
|
+
|
|
1805
|
+
assert.equal(state.mode, "insert");
|
|
1806
|
+
});
|
|
1807
|
+
|
|
1808
|
+
it("ca[ changes around brackets", () => {
|
|
1809
|
+
const rep = makeRep(["array[1]"]);
|
|
1810
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1811
|
+
|
|
1812
|
+
const ctx = {
|
|
1813
|
+
rep,
|
|
1814
|
+
editorInfo,
|
|
1815
|
+
line: 0,
|
|
1816
|
+
char: 5,
|
|
1817
|
+
lineText: "array[1]",
|
|
1818
|
+
};
|
|
1819
|
+
commands.normal["ca["](ctx);
|
|
1820
|
+
|
|
1821
|
+
assert.equal(state.mode, "insert");
|
|
1822
|
+
});
|
|
1823
|
+
|
|
1824
|
+
it('di" deletes inner quotes', () => {
|
|
1825
|
+
const rep = makeRep(['text "hello world" here']);
|
|
1826
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1827
|
+
|
|
1828
|
+
const ctx = {
|
|
1829
|
+
rep,
|
|
1830
|
+
editorInfo,
|
|
1831
|
+
line: 0,
|
|
1832
|
+
char: 7,
|
|
1833
|
+
lineText: 'text "hello world" here',
|
|
1834
|
+
};
|
|
1835
|
+
commands.normal['di"'](ctx);
|
|
1836
|
+
});
|
|
1837
|
+
});
|
|
1838
|
+
|
|
1839
|
+
describe("miscellaneous commands", () => {
|
|
1840
|
+
beforeEach(() => {
|
|
1841
|
+
state.mode = "normal";
|
|
1842
|
+
state.pendingKey = null;
|
|
1843
|
+
state.pendingCount = null;
|
|
1844
|
+
state.countBuffer = "";
|
|
1845
|
+
state.register = null;
|
|
1846
|
+
state.marks = {};
|
|
1847
|
+
state.lastCharSearch = null;
|
|
1848
|
+
state.visualAnchor = null;
|
|
1849
|
+
state.visualCursor = null;
|
|
1850
|
+
state.editorDoc = null;
|
|
1851
|
+
state.currentRep = null;
|
|
1852
|
+
state.desiredColumn = null;
|
|
1853
|
+
state.lastCommand = null;
|
|
1854
|
+
state.searchMode = false;
|
|
1855
|
+
state.searchBuffer = "";
|
|
1856
|
+
state.searchDirection = null;
|
|
1857
|
+
state.lastSearch = null;
|
|
1858
|
+
});
|
|
1859
|
+
|
|
1860
|
+
it("Y yanks line", () => {
|
|
1861
|
+
const rep = makeRep(["hello world"]);
|
|
1862
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1863
|
+
|
|
1864
|
+
const ctx = {
|
|
1865
|
+
rep,
|
|
1866
|
+
editorInfo,
|
|
1867
|
+
line: 0,
|
|
1868
|
+
char: 0,
|
|
1869
|
+
lineText: "hello world",
|
|
1870
|
+
};
|
|
1871
|
+
commands.normal["Y"](ctx);
|
|
1872
|
+
|
|
1873
|
+
assert.deepEqual(state.register, ["hello world"]);
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1876
|
+
it("operator with motion works (dh)", () => {
|
|
1877
|
+
const rep = makeRep(["hello"]);
|
|
1878
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1879
|
+
|
|
1880
|
+
const ctx = {
|
|
1881
|
+
rep,
|
|
1882
|
+
editorInfo,
|
|
1883
|
+
line: 0,
|
|
1884
|
+
char: 2,
|
|
1885
|
+
lineText: "hello",
|
|
1886
|
+
};
|
|
1887
|
+
commands.normal["dh"](ctx);
|
|
1888
|
+
|
|
1889
|
+
assert.equal(typeof state.register, "string");
|
|
1890
|
+
assert(state.register.length > 0);
|
|
1891
|
+
});
|
|
1892
|
+
|
|
1893
|
+
it("yy with count yanks multiple lines", () => {
|
|
1894
|
+
const rep = makeRep(["line1", "line2", "line3"]);
|
|
1895
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1896
|
+
|
|
1897
|
+
const ctx = {
|
|
1898
|
+
rep,
|
|
1899
|
+
editorInfo,
|
|
1900
|
+
line: 0,
|
|
1901
|
+
char: 0,
|
|
1902
|
+
lineText: "line1",
|
|
1903
|
+
count: 2,
|
|
1904
|
+
};
|
|
1905
|
+
commands.normal["yy"](ctx);
|
|
1906
|
+
|
|
1907
|
+
assert.equal(state.register.length, 2);
|
|
1908
|
+
assert.deepEqual(state.register, ["line1", "line2"]);
|
|
1909
|
+
});
|
|
1910
|
+
|
|
1911
|
+
it("dd with count deletes multiple lines", () => {
|
|
1912
|
+
const rep = makeRep(["line1", "line2", "line3", "line4"]);
|
|
1913
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1914
|
+
|
|
1915
|
+
const ctx = {
|
|
1916
|
+
rep,
|
|
1917
|
+
editorInfo,
|
|
1918
|
+
line: 0,
|
|
1919
|
+
char: 0,
|
|
1920
|
+
lineText: "line1",
|
|
1921
|
+
count: 2,
|
|
1922
|
+
};
|
|
1923
|
+
commands.normal["dd"](ctx);
|
|
1924
|
+
|
|
1925
|
+
assert.equal(state.register.length, 2);
|
|
1926
|
+
assert.deepEqual(state.register, ["line1", "line2"]);
|
|
1927
|
+
});
|
|
1928
|
+
|
|
1929
|
+
it("w motion moves to next word", () => {
|
|
1930
|
+
const rep = makeRep(["hello world"]);
|
|
1931
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1932
|
+
|
|
1933
|
+
const ctx = {
|
|
1934
|
+
rep,
|
|
1935
|
+
editorInfo,
|
|
1936
|
+
line: 0,
|
|
1937
|
+
char: 0,
|
|
1938
|
+
lineText: "hello world",
|
|
1939
|
+
count: 1,
|
|
1940
|
+
};
|
|
1941
|
+
commands.normal["w"](ctx);
|
|
1942
|
+
|
|
1943
|
+
// w should move to position 6 (start of "world")
|
|
1944
|
+
assert(calls.length > 0, "w should generate a motion call");
|
|
1945
|
+
const moveCall = calls[calls.length - 1];
|
|
1946
|
+
assert.equal(moveCall.start[1], 6, "w should move to position 6");
|
|
1947
|
+
});
|
|
1948
|
+
|
|
1949
|
+
it("dw deletes from cursor to start of next word", () => {
|
|
1950
|
+
const rep = makeRep(["hello world more"]);
|
|
1951
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
1952
|
+
|
|
1953
|
+
const ctx = {
|
|
1954
|
+
rep,
|
|
1955
|
+
editorInfo,
|
|
1956
|
+
line: 0,
|
|
1957
|
+
char: 0,
|
|
1958
|
+
lineText: "hello world more",
|
|
1959
|
+
};
|
|
1960
|
+
commands.normal["dw"](ctx);
|
|
1961
|
+
|
|
1962
|
+
// dw should generate replace call(s) for deletion
|
|
1963
|
+
const replaceCalls = calls.filter((c) => c.type === "replace");
|
|
1964
|
+
assert(replaceCalls.length > 0, "dw should perform deletion");
|
|
1965
|
+
});
|
|
1966
|
+
|
|
1967
|
+
it("ye yanks to end of word", () => {
|
|
1968
|
+
const rep = makeRep(["hello world"]);
|
|
1969
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1970
|
+
|
|
1971
|
+
const ctx = {
|
|
1972
|
+
rep,
|
|
1973
|
+
editorInfo,
|
|
1974
|
+
line: 0,
|
|
1975
|
+
char: 0,
|
|
1976
|
+
lineText: "hello world",
|
|
1977
|
+
};
|
|
1978
|
+
commands.normal["ye"](ctx);
|
|
1979
|
+
|
|
1980
|
+
assert.equal(typeof state.register, "string");
|
|
1981
|
+
});
|
|
1982
|
+
|
|
1983
|
+
it("cl changes one character", () => {
|
|
1984
|
+
const rep = makeRep(["hello"]);
|
|
1985
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
1986
|
+
|
|
1987
|
+
const ctx = {
|
|
1988
|
+
rep,
|
|
1989
|
+
editorInfo,
|
|
1990
|
+
line: 0,
|
|
1991
|
+
char: 1,
|
|
1992
|
+
lineText: "hello",
|
|
1993
|
+
};
|
|
1994
|
+
commands.normal["cl"](ctx);
|
|
1995
|
+
|
|
1996
|
+
assert.equal(state.mode, "insert");
|
|
1997
|
+
});
|
|
1998
|
+
});
|
|
1999
|
+
|
|
2000
|
+
// ---------------------------------------------------------------------------
|
|
2001
|
+
// Edge-case tests for vim motions and commands
|
|
2002
|
+
// ---------------------------------------------------------------------------
|
|
2003
|
+
|
|
2004
|
+
const resetState = () => {
|
|
2005
|
+
state.mode = "normal";
|
|
2006
|
+
state.pendingKey = null;
|
|
2007
|
+
state.pendingCount = null;
|
|
2008
|
+
state.countBuffer = "";
|
|
2009
|
+
state.register = null;
|
|
2010
|
+
state.marks = {};
|
|
2011
|
+
state.lastCharSearch = null;
|
|
2012
|
+
state.visualAnchor = null;
|
|
2013
|
+
state.visualCursor = null;
|
|
2014
|
+
state.editorDoc = null;
|
|
2015
|
+
state.currentRep = null;
|
|
2016
|
+
state.desiredColumn = null;
|
|
2017
|
+
state.lastCommand = null;
|
|
2018
|
+
state.searchMode = false;
|
|
2019
|
+
state.searchBuffer = "";
|
|
2020
|
+
state.searchDirection = null;
|
|
2021
|
+
state.lastSearch = null;
|
|
2022
|
+
};
|
|
2023
|
+
|
|
2024
|
+
describe("edge cases: cc with count", () => {
|
|
2025
|
+
beforeEach(resetState);
|
|
2026
|
+
|
|
2027
|
+
it("2cc should delete extra lines and clear remaining line", () => {
|
|
2028
|
+
const rep = makeRep(["aaa", "bbb", "ccc"]);
|
|
2029
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2030
|
+
|
|
2031
|
+
const ctx = {
|
|
2032
|
+
rep,
|
|
2033
|
+
editorInfo,
|
|
2034
|
+
line: 0,
|
|
2035
|
+
char: 0,
|
|
2036
|
+
lineText: "aaa",
|
|
2037
|
+
count: 2,
|
|
2038
|
+
};
|
|
2039
|
+
commands.normal["cc"](ctx);
|
|
2040
|
+
|
|
2041
|
+
assert.equal(state.mode, "insert");
|
|
2042
|
+
|
|
2043
|
+
const replaces = calls.filter((c) => c.type === "replace");
|
|
2044
|
+
const deletesLine = replaces.some(
|
|
2045
|
+
(r) =>
|
|
2046
|
+
r.start[0] !== r.end[0] ||
|
|
2047
|
+
(r.start[0] === r.end[0] && r.end[1] === 0 && r.start[1] === 0),
|
|
2048
|
+
);
|
|
2049
|
+
assert.ok(
|
|
2050
|
+
deletesLine || replaces.length === 1,
|
|
2051
|
+
"2cc should delete extra lines, not just clear text on each line separately",
|
|
2052
|
+
);
|
|
2053
|
+
});
|
|
2054
|
+
});
|
|
2055
|
+
|
|
2056
|
+
describe("edge cases: motions on empty lines", () => {
|
|
2057
|
+
beforeEach(resetState);
|
|
2058
|
+
|
|
2059
|
+
it("w on empty line stays on same position", () => {
|
|
2060
|
+
const rep = makeRep([""]);
|
|
2061
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2062
|
+
|
|
2063
|
+
const ctx = {
|
|
2064
|
+
rep,
|
|
2065
|
+
editorInfo,
|
|
2066
|
+
line: 0,
|
|
2067
|
+
char: 0,
|
|
2068
|
+
lineText: "",
|
|
2069
|
+
count: 1,
|
|
2070
|
+
};
|
|
2071
|
+
commands.normal["w"](ctx);
|
|
2072
|
+
|
|
2073
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
2074
|
+
assert.ok(selects.length > 0, "should produce a cursor move");
|
|
2075
|
+
assert.deepEqual(selects[0].start, [0, 0]);
|
|
2076
|
+
});
|
|
2077
|
+
|
|
2078
|
+
it("$ on empty line does not produce negative char", () => {
|
|
2079
|
+
const rep = makeRep([""]);
|
|
2080
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2081
|
+
|
|
2082
|
+
const ctx = {
|
|
2083
|
+
rep,
|
|
2084
|
+
editorInfo,
|
|
2085
|
+
line: 0,
|
|
2086
|
+
char: 0,
|
|
2087
|
+
lineText: "",
|
|
2088
|
+
count: 1,
|
|
2089
|
+
};
|
|
2090
|
+
commands.normal["$"](ctx);
|
|
2091
|
+
|
|
2092
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
2093
|
+
assert.ok(selects.length > 0);
|
|
2094
|
+
const charPos = selects[0].start[1];
|
|
2095
|
+
assert.ok(charPos >= 0, `$ on empty line gave char ${charPos}`);
|
|
2096
|
+
});
|
|
2097
|
+
|
|
2098
|
+
it("x on empty line does nothing", () => {
|
|
2099
|
+
const rep = makeRep([""]);
|
|
2100
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2101
|
+
|
|
2102
|
+
const ctx = {
|
|
2103
|
+
rep,
|
|
2104
|
+
editorInfo,
|
|
2105
|
+
line: 0,
|
|
2106
|
+
char: 0,
|
|
2107
|
+
lineText: "",
|
|
2108
|
+
count: 1,
|
|
2109
|
+
};
|
|
2110
|
+
commands.normal["x"](ctx);
|
|
2111
|
+
|
|
2112
|
+
const replaces = calls.filter((c) => c.type === "replace");
|
|
2113
|
+
assert.equal(replaces.length, 0, "x on empty line should not replace");
|
|
2114
|
+
});
|
|
2115
|
+
|
|
2116
|
+
it("~ on empty line does nothing", () => {
|
|
2117
|
+
const rep = makeRep([""]);
|
|
2118
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2119
|
+
|
|
2120
|
+
const ctx = {
|
|
2121
|
+
rep,
|
|
2122
|
+
editorInfo,
|
|
2123
|
+
line: 0,
|
|
2124
|
+
char: 0,
|
|
2125
|
+
lineText: "",
|
|
2126
|
+
count: 1,
|
|
2127
|
+
};
|
|
2128
|
+
commands.normal["~"](ctx);
|
|
2129
|
+
|
|
2130
|
+
const replaces = calls.filter((c) => c.type === "replace");
|
|
2131
|
+
assert.equal(replaces.length, 0, "~ on empty line should not replace");
|
|
2132
|
+
});
|
|
2133
|
+
});
|
|
2134
|
+
|
|
2135
|
+
describe("edge cases: boundary motions", () => {
|
|
2136
|
+
beforeEach(resetState);
|
|
2137
|
+
|
|
2138
|
+
it("h at column 0 stays at column 0", () => {
|
|
2139
|
+
const rep = makeRep(["hello"]);
|
|
2140
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2141
|
+
|
|
2142
|
+
const ctx = {
|
|
2143
|
+
rep,
|
|
2144
|
+
editorInfo,
|
|
2145
|
+
line: 0,
|
|
2146
|
+
char: 0,
|
|
2147
|
+
lineText: "hello",
|
|
2148
|
+
count: 1,
|
|
2149
|
+
};
|
|
2150
|
+
commands.normal["h"](ctx);
|
|
2151
|
+
|
|
2152
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
2153
|
+
assert.deepEqual(selects[0].start, [0, 0]);
|
|
2154
|
+
});
|
|
2155
|
+
|
|
2156
|
+
it("l at last character stays at last character", () => {
|
|
2157
|
+
const rep = makeRep(["hello"]);
|
|
2158
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2159
|
+
|
|
2160
|
+
const ctx = {
|
|
2161
|
+
rep,
|
|
2162
|
+
editorInfo,
|
|
2163
|
+
line: 0,
|
|
2164
|
+
char: 4,
|
|
2165
|
+
lineText: "hello",
|
|
2166
|
+
count: 1,
|
|
2167
|
+
};
|
|
2168
|
+
commands.normal["l"](ctx);
|
|
2169
|
+
|
|
2170
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
2171
|
+
assert.deepEqual(
|
|
2172
|
+
selects[0].start,
|
|
2173
|
+
[0, 4],
|
|
2174
|
+
"l at last char should not go past it",
|
|
2175
|
+
);
|
|
2176
|
+
});
|
|
2177
|
+
|
|
2178
|
+
it("j at last line stays at last line", () => {
|
|
2179
|
+
const rep = makeRep(["line1", "line2"]);
|
|
2180
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2181
|
+
|
|
2182
|
+
const ctx = {
|
|
2183
|
+
rep,
|
|
2184
|
+
editorInfo,
|
|
2185
|
+
line: 1,
|
|
2186
|
+
char: 0,
|
|
2187
|
+
lineText: "line2",
|
|
2188
|
+
count: 1,
|
|
2189
|
+
};
|
|
2190
|
+
commands.normal["j"](ctx);
|
|
2191
|
+
|
|
2192
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
2193
|
+
assert.equal(selects[0].start[0], 1, "j at last line should stay");
|
|
2194
|
+
});
|
|
2195
|
+
|
|
2196
|
+
it("k at first line stays at first line", () => {
|
|
2197
|
+
const rep = makeRep(["line1", "line2"]);
|
|
2198
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2199
|
+
|
|
2200
|
+
const ctx = {
|
|
2201
|
+
rep,
|
|
2202
|
+
editorInfo,
|
|
2203
|
+
line: 0,
|
|
2204
|
+
char: 0,
|
|
2205
|
+
lineText: "line1",
|
|
2206
|
+
count: 1,
|
|
2207
|
+
};
|
|
2208
|
+
commands.normal["k"](ctx);
|
|
2209
|
+
|
|
2210
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
2211
|
+
assert.equal(selects[0].start[0], 0, "k at first line should stay");
|
|
2212
|
+
});
|
|
2213
|
+
});
|
|
2214
|
+
|
|
2215
|
+
describe("edge cases: t/f adjacent and edge positions", () => {
|
|
2216
|
+
beforeEach(resetState);
|
|
2217
|
+
|
|
2218
|
+
it("t to adjacent char should not move (lands on self)", () => {
|
|
2219
|
+
const rep = makeRep(["ab"]);
|
|
2220
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2221
|
+
|
|
2222
|
+
const ctx = {
|
|
2223
|
+
rep,
|
|
2224
|
+
editorInfo,
|
|
2225
|
+
line: 0,
|
|
2226
|
+
char: 0,
|
|
2227
|
+
lineText: "ab",
|
|
2228
|
+
count: 1,
|
|
2229
|
+
};
|
|
2230
|
+
parameterized["t"]("b", ctx);
|
|
2231
|
+
|
|
2232
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
2233
|
+
if (selects.length > 0) {
|
|
2234
|
+
assert.deepEqual(
|
|
2235
|
+
selects[0].start,
|
|
2236
|
+
[0, 0],
|
|
2237
|
+
"t to adjacent char should not move forward (would land on current pos)",
|
|
2238
|
+
);
|
|
2239
|
+
}
|
|
2240
|
+
});
|
|
2241
|
+
|
|
2242
|
+
it("f at end of line should not find char", () => {
|
|
2243
|
+
const rep = makeRep(["abc"]);
|
|
2244
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2245
|
+
|
|
2246
|
+
const ctx = {
|
|
2247
|
+
rep,
|
|
2248
|
+
editorInfo,
|
|
2249
|
+
line: 0,
|
|
2250
|
+
char: 2,
|
|
2251
|
+
lineText: "abc",
|
|
2252
|
+
count: 1,
|
|
2253
|
+
};
|
|
2254
|
+
parameterized["f"]("x", ctx);
|
|
2255
|
+
|
|
2256
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
2257
|
+
assert.equal(selects.length, 0, "f for missing char should not move");
|
|
2258
|
+
});
|
|
2259
|
+
|
|
2260
|
+
it("F at start of line should not find char", () => {
|
|
2261
|
+
const rep = makeRep(["abc"]);
|
|
2262
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2263
|
+
|
|
2264
|
+
const ctx = {
|
|
2265
|
+
rep,
|
|
2266
|
+
editorInfo,
|
|
2267
|
+
line: 0,
|
|
2268
|
+
char: 0,
|
|
2269
|
+
lineText: "abc",
|
|
2270
|
+
count: 1,
|
|
2271
|
+
};
|
|
2272
|
+
parameterized["F"]("x", ctx);
|
|
2273
|
+
|
|
2274
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
2275
|
+
assert.equal(selects.length, 0, "F for missing char should not move");
|
|
2276
|
+
});
|
|
2277
|
+
});
|
|
2278
|
+
|
|
2279
|
+
describe("edge cases: dd edge cases", () => {
|
|
2280
|
+
beforeEach(resetState);
|
|
2281
|
+
|
|
2282
|
+
it("dd on single-line document leaves empty line", () => {
|
|
2283
|
+
const rep = makeRep(["hello"]);
|
|
2284
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2285
|
+
|
|
2286
|
+
const ctx = {
|
|
2287
|
+
rep,
|
|
2288
|
+
editorInfo,
|
|
2289
|
+
line: 0,
|
|
2290
|
+
char: 0,
|
|
2291
|
+
lineText: "hello",
|
|
2292
|
+
count: 1,
|
|
2293
|
+
};
|
|
2294
|
+
commands.normal["dd"](ctx);
|
|
2295
|
+
|
|
2296
|
+
const replaces = calls.filter((c) => c.type === "replace");
|
|
2297
|
+
assert.ok(replaces.length > 0);
|
|
2298
|
+
assert.equal(
|
|
2299
|
+
replaces[0].newText,
|
|
2300
|
+
"",
|
|
2301
|
+
"dd on single line should clear content",
|
|
2302
|
+
);
|
|
2303
|
+
assert.deepEqual(replaces[0].start, [0, 0]);
|
|
2304
|
+
assert.deepEqual(replaces[0].end, [0, 5]);
|
|
2305
|
+
});
|
|
2306
|
+
|
|
2307
|
+
it("3dd with only 2 lines remaining deletes to end", () => {
|
|
2308
|
+
const rep = makeRep(["aaa", "bbb"]);
|
|
2309
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2310
|
+
|
|
2311
|
+
const ctx = {
|
|
2312
|
+
rep,
|
|
2313
|
+
editorInfo,
|
|
2314
|
+
line: 0,
|
|
2315
|
+
char: 0,
|
|
2316
|
+
lineText: "aaa",
|
|
2317
|
+
count: 3,
|
|
2318
|
+
};
|
|
2319
|
+
commands.normal["dd"](ctx);
|
|
2320
|
+
|
|
2321
|
+
assert.ok(Array.isArray(state.register), "dd should yank lines as array");
|
|
2322
|
+
assert.equal(
|
|
2323
|
+
state.register.length,
|
|
2324
|
+
2,
|
|
2325
|
+
"3dd on 2-line doc should yank both lines",
|
|
2326
|
+
);
|
|
2327
|
+
});
|
|
2328
|
+
});
|
|
2329
|
+
|
|
2330
|
+
describe("edge cases: J (join) edge cases", () => {
|
|
2331
|
+
beforeEach(resetState);
|
|
2332
|
+
|
|
2333
|
+
it("J on last line does nothing", () => {
|
|
2334
|
+
const rep = makeRep(["only line"]);
|
|
2335
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2336
|
+
|
|
2337
|
+
const ctx = {
|
|
2338
|
+
rep,
|
|
2339
|
+
editorInfo,
|
|
2340
|
+
line: 0,
|
|
2341
|
+
char: 0,
|
|
2342
|
+
lineText: "only line",
|
|
2343
|
+
count: 1,
|
|
2344
|
+
};
|
|
2345
|
+
commands.normal["J"](ctx);
|
|
2346
|
+
|
|
2347
|
+
const replaces = calls.filter((c) => c.type === "replace");
|
|
2348
|
+
assert.equal(replaces.length, 0, "J on last line should not join");
|
|
2349
|
+
});
|
|
2350
|
+
|
|
2351
|
+
it("J trims leading whitespace from joined line", () => {
|
|
2352
|
+
const rep = makeRep(["hello", " world"]);
|
|
2353
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2354
|
+
|
|
2355
|
+
const ctx = {
|
|
2356
|
+
rep,
|
|
2357
|
+
editorInfo,
|
|
2358
|
+
line: 0,
|
|
2359
|
+
char: 0,
|
|
2360
|
+
lineText: "hello",
|
|
2361
|
+
count: 1,
|
|
2362
|
+
};
|
|
2363
|
+
commands.normal["J"](ctx);
|
|
2364
|
+
|
|
2365
|
+
const replaces = calls.filter((c) => c.type === "replace");
|
|
2366
|
+
assert.ok(replaces.length > 0);
|
|
2367
|
+
assert.equal(
|
|
2368
|
+
replaces[0].newText,
|
|
2369
|
+
" world",
|
|
2370
|
+
"J should trim leading whitespace and add single space",
|
|
2371
|
+
);
|
|
2372
|
+
});
|
|
2373
|
+
});
|
|
2374
|
+
|
|
2375
|
+
describe("edge cases: count handling", () => {
|
|
2376
|
+
beforeEach(resetState);
|
|
2377
|
+
|
|
2378
|
+
it("count 0 after digits is part of count (e.g., 10j)", () => {
|
|
2379
|
+
const rep = makeRep(Array.from({ length: 20 }, (_, i) => `line${i}`));
|
|
2380
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2381
|
+
|
|
2382
|
+
const baseCtx = {
|
|
2383
|
+
rep,
|
|
2384
|
+
editorInfo,
|
|
2385
|
+
line: 0,
|
|
2386
|
+
char: 0,
|
|
2387
|
+
lineText: "line0",
|
|
2388
|
+
};
|
|
2389
|
+
|
|
2390
|
+
handleKey("1", baseCtx);
|
|
2391
|
+
handleKey("0", baseCtx);
|
|
2392
|
+
handleKey("j", baseCtx);
|
|
2393
|
+
|
|
2394
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
2395
|
+
assert.ok(selects.length > 0);
|
|
2396
|
+
assert.equal(
|
|
2397
|
+
selects[selects.length - 1].start[0],
|
|
2398
|
+
10,
|
|
2399
|
+
"10j should go to line 10",
|
|
2400
|
+
);
|
|
2401
|
+
});
|
|
2402
|
+
|
|
2403
|
+
it("0 without prior digits is motion to column 0", () => {
|
|
2404
|
+
const rep = makeRep(["hello world"]);
|
|
2405
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2406
|
+
|
|
2407
|
+
const ctx = {
|
|
2408
|
+
rep,
|
|
2409
|
+
editorInfo,
|
|
2410
|
+
line: 0,
|
|
2411
|
+
char: 5,
|
|
2412
|
+
lineText: "hello world",
|
|
2413
|
+
};
|
|
2414
|
+
|
|
2415
|
+
handleKey("0", ctx);
|
|
2416
|
+
|
|
2417
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
2418
|
+
assert.ok(selects.length > 0);
|
|
2419
|
+
assert.deepEqual(selects[0].start, [0, 0]);
|
|
2420
|
+
});
|
|
2421
|
+
});
|
|
2422
|
+
|
|
2423
|
+
describe("edge cases: dw at end of line", () => {
|
|
2424
|
+
beforeEach(resetState);
|
|
2425
|
+
|
|
2426
|
+
it("dw at last word deletes to end of line", () => {
|
|
2427
|
+
const rep = makeRep(["hello world"]);
|
|
2428
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2429
|
+
|
|
2430
|
+
const ctx = {
|
|
2431
|
+
rep,
|
|
2432
|
+
editorInfo,
|
|
2433
|
+
line: 0,
|
|
2434
|
+
char: 6,
|
|
2435
|
+
lineText: "hello world",
|
|
2436
|
+
count: 1,
|
|
2437
|
+
};
|
|
2438
|
+
commands.normal["dw"](ctx);
|
|
2439
|
+
|
|
2440
|
+
const replaces = calls.filter((c) => c.type === "replace");
|
|
2441
|
+
assert.ok(replaces.length > 0);
|
|
2442
|
+
const deleted = ctx.lineText.slice(
|
|
2443
|
+
replaces[0].start[1],
|
|
2444
|
+
replaces[0].end[1],
|
|
2445
|
+
);
|
|
2446
|
+
assert.equal(
|
|
2447
|
+
deleted,
|
|
2448
|
+
"world",
|
|
2449
|
+
"dw at last word should delete to end of line",
|
|
2450
|
+
);
|
|
2451
|
+
});
|
|
2452
|
+
});
|
|
2453
|
+
|
|
2454
|
+
describe("edge cases: d$ and D", () => {
|
|
2455
|
+
beforeEach(resetState);
|
|
2456
|
+
|
|
2457
|
+
it("d$ at last char deletes that character", () => {
|
|
2458
|
+
const rep = makeRep(["abc"]);
|
|
2459
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2460
|
+
|
|
2461
|
+
const ctx = {
|
|
2462
|
+
rep,
|
|
2463
|
+
editorInfo,
|
|
2464
|
+
line: 0,
|
|
2465
|
+
char: 2,
|
|
2466
|
+
lineText: "abc",
|
|
2467
|
+
count: 1,
|
|
2468
|
+
};
|
|
2469
|
+
commands.normal["d$"](ctx);
|
|
2470
|
+
|
|
2471
|
+
const replaces = calls.filter((c) => c.type === "replace");
|
|
2472
|
+
assert.ok(replaces.length > 0);
|
|
2473
|
+
assert.deepEqual(replaces[0].start, [0, 2]);
|
|
2474
|
+
assert.deepEqual(
|
|
2475
|
+
replaces[0].end,
|
|
2476
|
+
[0, 3],
|
|
2477
|
+
"d$ at last char should delete it (inclusive)",
|
|
2478
|
+
);
|
|
2479
|
+
});
|
|
2480
|
+
|
|
2481
|
+
it("D at column 0 deletes entire line content", () => {
|
|
2482
|
+
const rep = makeRep(["hello"]);
|
|
2483
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2484
|
+
|
|
2485
|
+
const ctx = {
|
|
2486
|
+
rep,
|
|
2487
|
+
editorInfo,
|
|
2488
|
+
line: 0,
|
|
2489
|
+
char: 0,
|
|
2490
|
+
lineText: "hello",
|
|
2491
|
+
count: 1,
|
|
2492
|
+
};
|
|
2493
|
+
commands.normal["D"](ctx);
|
|
2494
|
+
|
|
2495
|
+
assert.equal(state.register, "hello");
|
|
2496
|
+
const replaces = calls.filter((c) => c.type === "replace");
|
|
2497
|
+
assert.deepEqual(replaces[0].start, [0, 0]);
|
|
2498
|
+
assert.deepEqual(replaces[0].end, [0, 5]);
|
|
2499
|
+
});
|
|
2500
|
+
});
|
|
2501
|
+
|
|
2502
|
+
describe("edge cases: p (paste) edge cases", () => {
|
|
2503
|
+
beforeEach(resetState);
|
|
2504
|
+
|
|
2505
|
+
it("p on empty line pastes at position 0 (not 1)", () => {
|
|
2506
|
+
const rep = makeRep([""]);
|
|
2507
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2508
|
+
|
|
2509
|
+
state.register = "text";
|
|
2510
|
+
|
|
2511
|
+
const ctx = {
|
|
2512
|
+
rep,
|
|
2513
|
+
editorInfo,
|
|
2514
|
+
line: 0,
|
|
2515
|
+
char: 0,
|
|
2516
|
+
lineText: "",
|
|
2517
|
+
count: 1,
|
|
2518
|
+
};
|
|
2519
|
+
commands.normal["p"](ctx);
|
|
2520
|
+
|
|
2521
|
+
const replaces = calls.filter((c) => c.type === "replace");
|
|
2522
|
+
assert.ok(replaces.length > 0);
|
|
2523
|
+
assert.equal(
|
|
2524
|
+
replaces[0].start[1],
|
|
2525
|
+
0,
|
|
2526
|
+
"p on empty line should paste at 0, not 1",
|
|
2527
|
+
);
|
|
2528
|
+
});
|
|
2529
|
+
|
|
2530
|
+
it("P with nothing in register does nothing", () => {
|
|
2531
|
+
const rep = makeRep(["hello"]);
|
|
2532
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2533
|
+
|
|
2534
|
+
state.register = null;
|
|
2535
|
+
|
|
2536
|
+
const ctx = {
|
|
2537
|
+
rep,
|
|
2538
|
+
editorInfo,
|
|
2539
|
+
line: 0,
|
|
2540
|
+
char: 0,
|
|
2541
|
+
lineText: "hello",
|
|
2542
|
+
count: 1,
|
|
2543
|
+
};
|
|
2544
|
+
commands.normal["P"](ctx);
|
|
2545
|
+
|
|
2546
|
+
const replaces = calls.filter((c) => c.type === "replace");
|
|
2547
|
+
assert.equal(replaces.length, 0);
|
|
2548
|
+
});
|
|
2549
|
+
});
|
|
2550
|
+
|
|
2551
|
+
describe("edge cases: w/b word motions", () => {
|
|
2552
|
+
beforeEach(resetState);
|
|
2553
|
+
|
|
2554
|
+
it("w at last word on line clamps to end", () => {
|
|
2555
|
+
const rep = makeRep(["hello world"]);
|
|
2556
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2557
|
+
|
|
2558
|
+
const ctx = {
|
|
2559
|
+
rep,
|
|
2560
|
+
editorInfo,
|
|
2561
|
+
line: 0,
|
|
2562
|
+
char: 6,
|
|
2563
|
+
lineText: "hello world",
|
|
2564
|
+
count: 1,
|
|
2565
|
+
};
|
|
2566
|
+
commands.normal["w"](ctx);
|
|
2567
|
+
|
|
2568
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
2569
|
+
assert.ok(selects.length > 0);
|
|
2570
|
+
const endChar = selects[0].start[1];
|
|
2571
|
+
assert.ok(
|
|
2572
|
+
endChar <= 10,
|
|
2573
|
+
`w at last word should clamp to line end, got ${endChar}`,
|
|
2574
|
+
);
|
|
2575
|
+
});
|
|
2576
|
+
|
|
2577
|
+
it("b at first word stays at position 0", () => {
|
|
2578
|
+
const rep = makeRep(["hello world"]);
|
|
2579
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2580
|
+
|
|
2581
|
+
const ctx = {
|
|
2582
|
+
rep,
|
|
2583
|
+
editorInfo,
|
|
2584
|
+
line: 0,
|
|
2585
|
+
char: 0,
|
|
2586
|
+
lineText: "hello world",
|
|
2587
|
+
count: 1,
|
|
2588
|
+
};
|
|
2589
|
+
commands.normal["b"](ctx);
|
|
2590
|
+
|
|
2591
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
2592
|
+
assert.ok(selects.length > 0);
|
|
2593
|
+
assert.deepEqual(selects[0].start, [0, 0]);
|
|
2594
|
+
});
|
|
2595
|
+
|
|
2596
|
+
it("w on line with only spaces", () => {
|
|
2597
|
+
const rep = makeRep([" "]);
|
|
2598
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2599
|
+
|
|
2600
|
+
const ctx = {
|
|
2601
|
+
rep,
|
|
2602
|
+
editorInfo,
|
|
2603
|
+
line: 0,
|
|
2604
|
+
char: 0,
|
|
2605
|
+
lineText: " ",
|
|
2606
|
+
count: 1,
|
|
2607
|
+
};
|
|
2608
|
+
commands.normal["w"](ctx);
|
|
2609
|
+
|
|
2610
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
2611
|
+
assert.ok(selects.length > 0);
|
|
2612
|
+
assert.ok(
|
|
2613
|
+
selects[0].start[1] >= 0,
|
|
2614
|
+
"w on whitespace-only line should not crash",
|
|
2615
|
+
);
|
|
2616
|
+
});
|
|
2617
|
+
|
|
2618
|
+
it("e on single character word", () => {
|
|
2619
|
+
const rep = makeRep(["a b c"]);
|
|
2620
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2621
|
+
|
|
2622
|
+
const ctx = {
|
|
2623
|
+
rep,
|
|
2624
|
+
editorInfo,
|
|
2625
|
+
line: 0,
|
|
2626
|
+
char: 0,
|
|
2627
|
+
lineText: "a b c",
|
|
2628
|
+
count: 1,
|
|
2629
|
+
};
|
|
2630
|
+
commands.normal["e"](ctx);
|
|
2631
|
+
|
|
2632
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
2633
|
+
assert.ok(selects.length > 0);
|
|
2634
|
+
assert.equal(
|
|
2635
|
+
selects[0].start[1],
|
|
2636
|
+
2,
|
|
2637
|
+
"e from 'a' should jump to 'b' (next word end)",
|
|
2638
|
+
);
|
|
2639
|
+
});
|
|
2640
|
+
});
|
|
2641
|
+
|
|
2642
|
+
describe("edge cases: j/k desiredColumn stickiness", () => {
|
|
2643
|
+
beforeEach(resetState);
|
|
2644
|
+
|
|
2645
|
+
it("j through short line preserves desired column", () => {
|
|
2646
|
+
const rep = makeRep(["long line here", "ab", "long line here"]);
|
|
2647
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2648
|
+
|
|
2649
|
+
const ctx1 = {
|
|
2650
|
+
rep,
|
|
2651
|
+
editorInfo,
|
|
2652
|
+
line: 0,
|
|
2653
|
+
char: 10,
|
|
2654
|
+
lineText: "long line here",
|
|
2655
|
+
count: 1,
|
|
2656
|
+
};
|
|
2657
|
+
commands.normal["j"](ctx1);
|
|
2658
|
+
|
|
2659
|
+
const select1 = calls.filter((c) => c.type === "select");
|
|
2660
|
+
assert.equal(select1[0].start[0], 1, "should be on line 1");
|
|
2661
|
+
assert.equal(
|
|
2662
|
+
select1[0].start[1],
|
|
2663
|
+
1,
|
|
2664
|
+
"should clamp to last char of short line",
|
|
2665
|
+
);
|
|
2666
|
+
|
|
2667
|
+
const ctx2 = {
|
|
2668
|
+
rep,
|
|
2669
|
+
editorInfo,
|
|
2670
|
+
line: 1,
|
|
2671
|
+
char: 1,
|
|
2672
|
+
lineText: "ab",
|
|
2673
|
+
count: 1,
|
|
2674
|
+
};
|
|
2675
|
+
commands.normal["j"](ctx2);
|
|
2676
|
+
|
|
2677
|
+
const select2 = calls.filter((c) => c.type === "select");
|
|
2678
|
+
const lastSelect = select2[select2.length - 1];
|
|
2679
|
+
assert.equal(lastSelect.start[0], 2);
|
|
2680
|
+
assert.equal(
|
|
2681
|
+
lastSelect.start[1],
|
|
2682
|
+
10,
|
|
2683
|
+
"j should restore desired column on longer line",
|
|
2684
|
+
);
|
|
2685
|
+
});
|
|
2686
|
+
|
|
2687
|
+
it("h resets desired column", () => {
|
|
2688
|
+
const rep = makeRep(["long line here", "ab", "long line here"]);
|
|
2689
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2690
|
+
|
|
2691
|
+
const ctx1 = {
|
|
2692
|
+
rep,
|
|
2693
|
+
editorInfo,
|
|
2694
|
+
line: 0,
|
|
2695
|
+
char: 10,
|
|
2696
|
+
lineText: "long line here",
|
|
2697
|
+
count: 1,
|
|
2698
|
+
};
|
|
2699
|
+
commands.normal["j"](ctx1);
|
|
2700
|
+
|
|
2701
|
+
commands.normal["h"]({
|
|
2702
|
+
rep,
|
|
2703
|
+
editorInfo,
|
|
2704
|
+
line: 1,
|
|
2705
|
+
char: 1,
|
|
2706
|
+
lineText: "ab",
|
|
2707
|
+
count: 1,
|
|
2708
|
+
});
|
|
2709
|
+
|
|
2710
|
+
commands.normal["j"]({
|
|
2711
|
+
rep,
|
|
2712
|
+
editorInfo,
|
|
2713
|
+
line: 1,
|
|
2714
|
+
char: 0,
|
|
2715
|
+
lineText: "ab",
|
|
2716
|
+
count: 1,
|
|
2717
|
+
});
|
|
2718
|
+
|
|
2719
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
2720
|
+
const lastSelect = selects[selects.length - 1];
|
|
2721
|
+
assert.equal(lastSelect.start[0], 2);
|
|
2722
|
+
assert.equal(
|
|
2723
|
+
lastSelect.start[1],
|
|
2724
|
+
0,
|
|
2725
|
+
"h should reset desiredColumn so subsequent j uses actual column",
|
|
2726
|
+
);
|
|
2727
|
+
});
|
|
2728
|
+
});
|
|
2729
|
+
|
|
2730
|
+
describe("edge cases: visual mode text objects", () => {
|
|
2731
|
+
beforeEach(resetState);
|
|
2732
|
+
|
|
2733
|
+
it("viw in visual mode selects inner word", () => {
|
|
2734
|
+
const rep = makeRep(["hello world"]);
|
|
2735
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
2736
|
+
|
|
2737
|
+
state.mode = "visual-char";
|
|
2738
|
+
state.visualAnchor = [0, 3];
|
|
2739
|
+
state.visualCursor = [0, 3];
|
|
2740
|
+
|
|
2741
|
+
const ctx = {
|
|
2742
|
+
rep,
|
|
2743
|
+
editorInfo,
|
|
2744
|
+
line: 0,
|
|
2745
|
+
char: 3,
|
|
2746
|
+
lineText: "hello world",
|
|
2747
|
+
count: 1,
|
|
2748
|
+
};
|
|
2749
|
+
|
|
2750
|
+
handleKey("i", ctx);
|
|
2751
|
+
handleKey("w", ctx);
|
|
2752
|
+
|
|
2753
|
+
assert.deepEqual(
|
|
2754
|
+
state.visualAnchor,
|
|
2755
|
+
[0, 0],
|
|
2756
|
+
"viw anchor should be word start",
|
|
2757
|
+
);
|
|
2758
|
+
assert.deepEqual(
|
|
2759
|
+
state.visualCursor,
|
|
2760
|
+
[0, 5],
|
|
2761
|
+
"viw cursor should be word end",
|
|
2762
|
+
);
|
|
2763
|
+
});
|
|
2764
|
+
});
|
|
2765
|
+
|
|
2766
|
+
describe("edge cases: de vs dw", () => {
|
|
2767
|
+
beforeEach(resetState);
|
|
2768
|
+
|
|
2769
|
+
it("de deletes to end of current word (inclusive)", () => {
|
|
2770
|
+
const rep = makeRep(["hello world"]);
|
|
2771
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2772
|
+
|
|
2773
|
+
const ctx = {
|
|
2774
|
+
rep,
|
|
2775
|
+
editorInfo,
|
|
2776
|
+
line: 0,
|
|
2777
|
+
char: 0,
|
|
2778
|
+
lineText: "hello world",
|
|
2779
|
+
count: 1,
|
|
2780
|
+
};
|
|
2781
|
+
commands.normal["de"](ctx);
|
|
2782
|
+
|
|
2783
|
+
const replaces = calls.filter((c) => c.type === "replace");
|
|
2784
|
+
assert.ok(replaces.length > 0);
|
|
2785
|
+
assert.deepEqual(replaces[0].start, [0, 0]);
|
|
2786
|
+
assert.deepEqual(
|
|
2787
|
+
replaces[0].end,
|
|
2788
|
+
[0, 5],
|
|
2789
|
+
"de should delete 'hello' (inclusive of last char)",
|
|
2790
|
+
);
|
|
2791
|
+
});
|
|
2792
|
+
|
|
2793
|
+
it("dw at start of word deletes word and trailing space", () => {
|
|
2794
|
+
const rep = makeRep(["hello world"]);
|
|
2795
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2796
|
+
|
|
2797
|
+
const ctx = {
|
|
2798
|
+
rep,
|
|
2799
|
+
editorInfo,
|
|
2800
|
+
line: 0,
|
|
2801
|
+
char: 0,
|
|
2802
|
+
lineText: "hello world",
|
|
2803
|
+
count: 1,
|
|
2804
|
+
};
|
|
2805
|
+
commands.normal["dw"](ctx);
|
|
2806
|
+
|
|
2807
|
+
const replaces = calls.filter((c) => c.type === "replace");
|
|
2808
|
+
assert.ok(replaces.length > 0);
|
|
2809
|
+
assert.deepEqual(replaces[0].start, [0, 0]);
|
|
2810
|
+
assert.deepEqual(
|
|
2811
|
+
replaces[0].end,
|
|
2812
|
+
[0, 6],
|
|
2813
|
+
"dw should delete 'hello ' (word + trailing space)",
|
|
2814
|
+
);
|
|
2815
|
+
});
|
|
2816
|
+
});
|
|
2817
|
+
|
|
2818
|
+
describe("edge cases: gg and G with count", () => {
|
|
2819
|
+
beforeEach(resetState);
|
|
2820
|
+
|
|
2821
|
+
it("5gg goes to line 5 (0-indexed line 4)", () => {
|
|
2822
|
+
const rep = makeRep(Array.from({ length: 10 }, (_, i) => `line${i}`));
|
|
2823
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2824
|
+
|
|
2825
|
+
const ctx = {
|
|
2826
|
+
rep,
|
|
2827
|
+
editorInfo,
|
|
2828
|
+
line: 0,
|
|
2829
|
+
char: 0,
|
|
2830
|
+
lineText: "line0",
|
|
2831
|
+
count: 5,
|
|
2832
|
+
hasCount: true,
|
|
2833
|
+
};
|
|
2834
|
+
commands.normal["gg"](ctx);
|
|
2835
|
+
|
|
2836
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
2837
|
+
assert.equal(
|
|
2838
|
+
selects[0].start[0],
|
|
2839
|
+
4,
|
|
2840
|
+
"5gg should go to line index 4 (line 5)",
|
|
2841
|
+
);
|
|
2842
|
+
});
|
|
2843
|
+
|
|
2844
|
+
it("gg with count beyond document clamps to last line", () => {
|
|
2845
|
+
const rep = makeRep(["line0", "line1", "line2"]);
|
|
2846
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2847
|
+
|
|
2848
|
+
const ctx = {
|
|
2849
|
+
rep,
|
|
2850
|
+
editorInfo,
|
|
2851
|
+
line: 0,
|
|
2852
|
+
char: 0,
|
|
2853
|
+
lineText: "line0",
|
|
2854
|
+
count: 100,
|
|
2855
|
+
hasCount: true,
|
|
2856
|
+
};
|
|
2857
|
+
commands.normal["gg"](ctx);
|
|
2858
|
+
|
|
2859
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
2860
|
+
assert.equal(
|
|
2861
|
+
selects[0].start[0],
|
|
2862
|
+
2,
|
|
2863
|
+
"gg with huge count should clamp to last line",
|
|
2864
|
+
);
|
|
2865
|
+
});
|
|
2866
|
+
|
|
2867
|
+
it("G without count goes to last line", () => {
|
|
2868
|
+
const rep = makeRep(["line0", "line1", "line2"]);
|
|
2869
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2870
|
+
|
|
2871
|
+
const ctx = {
|
|
2872
|
+
rep,
|
|
2873
|
+
editorInfo,
|
|
2874
|
+
line: 0,
|
|
2875
|
+
char: 0,
|
|
2876
|
+
lineText: "line0",
|
|
2877
|
+
count: 1,
|
|
2878
|
+
hasCount: false,
|
|
2879
|
+
};
|
|
2880
|
+
commands.normal["G"](ctx);
|
|
2881
|
+
|
|
2882
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
2883
|
+
assert.equal(selects[0].start[0], 2, "G should go to last line");
|
|
2884
|
+
});
|
|
2885
|
+
});
|
|
2886
|
+
|
|
2887
|
+
describe("edge cases: s with count", () => {
|
|
2888
|
+
beforeEach(resetState);
|
|
2889
|
+
|
|
2890
|
+
it("3s at position 1 deletes 3 chars and enters insert", () => {
|
|
2891
|
+
const rep = makeRep(["abcdef"]);
|
|
2892
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2893
|
+
|
|
2894
|
+
const ctx = {
|
|
2895
|
+
rep,
|
|
2896
|
+
editorInfo,
|
|
2897
|
+
line: 0,
|
|
2898
|
+
char: 1,
|
|
2899
|
+
lineText: "abcdef",
|
|
2900
|
+
count: 3,
|
|
2901
|
+
};
|
|
2902
|
+
commands.normal["s"](ctx);
|
|
2903
|
+
|
|
2904
|
+
assert.equal(state.mode, "insert");
|
|
2905
|
+
const replaces = calls.filter((c) => c.type === "replace");
|
|
2906
|
+
assert.ok(replaces.length > 0);
|
|
2907
|
+
assert.deepEqual(replaces[0].start, [0, 1]);
|
|
2908
|
+
assert.deepEqual(replaces[0].end, [0, 4], "3s should delete 3 chars");
|
|
2909
|
+
});
|
|
2910
|
+
|
|
2911
|
+
it("s with count exceeding line length clamps", () => {
|
|
2912
|
+
const rep = makeRep(["ab"]);
|
|
2913
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2914
|
+
|
|
2915
|
+
const ctx = {
|
|
2916
|
+
rep,
|
|
2917
|
+
editorInfo,
|
|
2918
|
+
line: 0,
|
|
2919
|
+
char: 0,
|
|
2920
|
+
lineText: "ab",
|
|
2921
|
+
count: 10,
|
|
2922
|
+
};
|
|
2923
|
+
commands.normal["s"](ctx);
|
|
2924
|
+
|
|
2925
|
+
assert.equal(state.mode, "insert");
|
|
2926
|
+
const replaces = calls.filter((c) => c.type === "replace");
|
|
2927
|
+
assert.deepEqual(
|
|
2928
|
+
replaces[0].end,
|
|
2929
|
+
[0, 2],
|
|
2930
|
+
"s with large count should clamp to line length",
|
|
2931
|
+
);
|
|
2932
|
+
});
|
|
2933
|
+
});
|
|
2934
|
+
|
|
2935
|
+
describe("edge cases: x at end of line", () => {
|
|
2936
|
+
beforeEach(resetState);
|
|
2937
|
+
|
|
2938
|
+
it("x at last character should delete it", () => {
|
|
2939
|
+
const rep = makeRep(["abc"]);
|
|
2940
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2941
|
+
|
|
2942
|
+
const ctx = {
|
|
2943
|
+
rep,
|
|
2944
|
+
editorInfo,
|
|
2945
|
+
line: 0,
|
|
2946
|
+
char: 2,
|
|
2947
|
+
lineText: "abc",
|
|
2948
|
+
count: 1,
|
|
2949
|
+
};
|
|
2950
|
+
commands.normal["x"](ctx);
|
|
2951
|
+
|
|
2952
|
+
const replaces = calls.filter((c) => c.type === "replace");
|
|
2953
|
+
assert.ok(replaces.length > 0);
|
|
2954
|
+
assert.equal(state.register, "c");
|
|
2955
|
+
});
|
|
2956
|
+
|
|
2957
|
+
it("3x with only 2 chars remaining deletes what's available", () => {
|
|
2958
|
+
const rep = makeRep(["abcd"]);
|
|
2959
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2960
|
+
|
|
2961
|
+
const ctx = {
|
|
2962
|
+
rep,
|
|
2963
|
+
editorInfo,
|
|
2964
|
+
line: 0,
|
|
2965
|
+
char: 2,
|
|
2966
|
+
lineText: "abcd",
|
|
2967
|
+
count: 3,
|
|
2968
|
+
};
|
|
2969
|
+
commands.normal["x"](ctx);
|
|
2970
|
+
|
|
2971
|
+
assert.equal(state.register, "cd", "3x with 2 remaining should delete 2");
|
|
2972
|
+
});
|
|
2973
|
+
});
|
|
2974
|
+
|
|
2975
|
+
describe("edge cases: df and dt (operator + char motion)", () => {
|
|
2976
|
+
beforeEach(resetState);
|
|
2977
|
+
|
|
2978
|
+
it("df deletes up to and including target char", () => {
|
|
2979
|
+
const rep = makeRep(["hello world"]);
|
|
2980
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
2981
|
+
|
|
2982
|
+
const ctx = {
|
|
2983
|
+
rep,
|
|
2984
|
+
editorInfo,
|
|
2985
|
+
line: 0,
|
|
2986
|
+
char: 0,
|
|
2987
|
+
lineText: "hello world",
|
|
2988
|
+
count: 1,
|
|
2989
|
+
};
|
|
2990
|
+
parameterized["df"]("o", ctx);
|
|
2991
|
+
|
|
2992
|
+
const replaces = calls.filter((c) => c.type === "replace");
|
|
2993
|
+
assert.ok(replaces.length > 0);
|
|
2994
|
+
assert.deepEqual(replaces[0].start, [0, 0]);
|
|
2995
|
+
assert.deepEqual(
|
|
2996
|
+
replaces[0].end,
|
|
2997
|
+
[0, 5],
|
|
2998
|
+
"df o should delete 'hello' (inclusive of 'o' at position 4)",
|
|
2999
|
+
);
|
|
3000
|
+
});
|
|
3001
|
+
|
|
3002
|
+
it("dt deletes up to but not including target char", () => {
|
|
3003
|
+
const rep = makeRep(["hello world"]);
|
|
3004
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
3005
|
+
|
|
3006
|
+
const ctx = {
|
|
3007
|
+
rep,
|
|
3008
|
+
editorInfo,
|
|
3009
|
+
line: 0,
|
|
3010
|
+
char: 0,
|
|
3011
|
+
lineText: "hello world",
|
|
3012
|
+
count: 1,
|
|
3013
|
+
};
|
|
3014
|
+
parameterized["dt"]("o", ctx);
|
|
3015
|
+
|
|
3016
|
+
const replaces = calls.filter((c) => c.type === "replace");
|
|
3017
|
+
assert.ok(replaces.length > 0);
|
|
3018
|
+
assert.deepEqual(replaces[0].start, [0, 0]);
|
|
3019
|
+
assert.deepEqual(
|
|
3020
|
+
replaces[0].end,
|
|
3021
|
+
[0, 4],
|
|
3022
|
+
"dt o should delete 'hell' (up to but not including 'o')",
|
|
3023
|
+
);
|
|
3024
|
+
});
|
|
3025
|
+
|
|
3026
|
+
it("dF deletes backward including target", () => {
|
|
3027
|
+
const rep = makeRep(["hello world"]);
|
|
3028
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
3029
|
+
|
|
3030
|
+
const ctx = {
|
|
3031
|
+
rep,
|
|
3032
|
+
editorInfo,
|
|
3033
|
+
line: 0,
|
|
3034
|
+
char: 7,
|
|
3035
|
+
lineText: "hello world",
|
|
3036
|
+
count: 1,
|
|
3037
|
+
};
|
|
3038
|
+
parameterized["dF"]("o", ctx);
|
|
3039
|
+
|
|
3040
|
+
const replaces = calls.filter((c) => c.type === "replace");
|
|
3041
|
+
assert.ok(replaces.length > 0);
|
|
3042
|
+
assert.deepEqual(
|
|
3043
|
+
replaces[0].start,
|
|
3044
|
+
[0, 4],
|
|
3045
|
+
"dF o from pos 7 should start at 'o' (pos 4)",
|
|
3046
|
+
);
|
|
3047
|
+
assert.deepEqual(
|
|
3048
|
+
replaces[0].end,
|
|
3049
|
+
[0, 8],
|
|
3050
|
+
"dF o from pos 7 should delete up to and including cursor pos",
|
|
3051
|
+
);
|
|
3052
|
+
});
|
|
3053
|
+
|
|
3054
|
+
it("dT deletes backward not including target", () => {
|
|
3055
|
+
const rep = makeRep(["hello world"]);
|
|
3056
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
3057
|
+
|
|
3058
|
+
const ctx = {
|
|
3059
|
+
rep,
|
|
3060
|
+
editorInfo,
|
|
3061
|
+
line: 0,
|
|
3062
|
+
char: 7,
|
|
3063
|
+
lineText: "hello world",
|
|
3064
|
+
count: 1,
|
|
3065
|
+
};
|
|
3066
|
+
parameterized["dT"]("o", ctx);
|
|
3067
|
+
|
|
3068
|
+
const replaces = calls.filter((c) => c.type === "replace");
|
|
3069
|
+
assert.ok(replaces.length > 0);
|
|
3070
|
+
assert.deepEqual(
|
|
3071
|
+
replaces[0].start,
|
|
3072
|
+
[0, 5],
|
|
3073
|
+
"dT o from pos 7 should start after 'o' (pos 5)",
|
|
3074
|
+
);
|
|
3075
|
+
assert.deepEqual(
|
|
3076
|
+
replaces[0].end,
|
|
3077
|
+
[0, 7],
|
|
3078
|
+
"dT o from pos 7 should end before cursor",
|
|
3079
|
+
);
|
|
3080
|
+
});
|
|
3081
|
+
});
|