ep_vim 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/static/js/index.js +40 -13
- package/static/js/vim-core.js +29 -10
- package/static/js/vim-core.test.js +57 -39
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ A vim-mode plugin for [Etherpad](https://etherpad.org/). Adds modal editing with
|
|
|
6
6
|
|
|
7
7
|
- **Modal editing** — normal, insert, and visual (char + line) modes
|
|
8
8
|
- **Motions** — `h` `j` `k` `l`, `w` `b` `e`, `0` `$` `^`, `gg` `G`, `f`/`F`/`t`/`T` char search
|
|
9
|
-
- **Operators** — `d`, `c`, `y` with motion combinations (`dw`, `ce`, `y$`, etc.) and text objects (`ciw`, `
|
|
9
|
+
- **Operators** — `d`, `c`, `y` with motion combinations (`dw`, `ce`, `y$`, etc.) and text objects (`ciw`, `da"`, `yi(` etc.)
|
|
10
10
|
- **Line operations** — `dd`, `cc`, `yy`, `J` (join), `Y` (yank line)
|
|
11
11
|
- **Put** — `p` / `P` with linewise and characterwise register handling
|
|
12
12
|
- **Editing** — `x`, `r`, `s`, `S`, `C`, `o`, `O`
|
package/package.json
CHANGED
package/static/js/index.js
CHANGED
|
@@ -11,9 +11,9 @@ const {
|
|
|
11
11
|
charSearchPos,
|
|
12
12
|
motionRange,
|
|
13
13
|
charMotionRange,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
textWordRange,
|
|
15
|
+
textQuoteRange,
|
|
16
|
+
textBracketRange,
|
|
17
17
|
getVisualSelection,
|
|
18
18
|
paragraphForward,
|
|
19
19
|
paragraphBackward,
|
|
@@ -40,11 +40,11 @@ let lastCharSearch = null;
|
|
|
40
40
|
const QUOTE_CHARS = new Set(['"', "'"]);
|
|
41
41
|
const BRACKET_CHARS = new Set(["(", ")", "{", "}", "[", "]"]);
|
|
42
42
|
|
|
43
|
-
const
|
|
44
|
-
if (key === "w") return
|
|
45
|
-
if (QUOTE_CHARS.has(key)) return
|
|
46
|
-
if (BRACKET_CHARS.has(key))
|
|
47
|
-
|
|
43
|
+
const textObjectRange = (key, lineText, char, type) => {
|
|
44
|
+
if (key === "w") return textWordRange(lineText, char);
|
|
45
|
+
if (QUOTE_CHARS.has(key)) return textQuoteRange(lineText, char, key, type);
|
|
46
|
+
if (BRACKET_CHARS.has(key))
|
|
47
|
+
return textBracketRange(lineText, char, key, type);
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
// --- Count helpers ---
|
|
@@ -148,6 +148,23 @@ const handleVisualKey = (rep, editorInfo, key) => {
|
|
|
148
148
|
const curChar = visualCursor[1];
|
|
149
149
|
const lineText = getLineText(rep, curLine);
|
|
150
150
|
|
|
151
|
+
if (key === "i" || key === "a") {
|
|
152
|
+
pendingKey = key;
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (pendingKey === "i" || pendingKey === "a") {
|
|
157
|
+
const range = textObjectRange(key, lineText, curChar, pendingKey);
|
|
158
|
+
pendingKey = null;
|
|
159
|
+
if (range) {
|
|
160
|
+
visualAnchor = [curLine, range.start];
|
|
161
|
+
visualCursor = [curLine, range.end];
|
|
162
|
+
setVisualMode("char");
|
|
163
|
+
updateVisualSelection(editorInfo, rep);
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
151
168
|
if (key >= "1" && key <= "9") {
|
|
152
169
|
countBuffer += key;
|
|
153
170
|
return true;
|
|
@@ -559,9 +576,9 @@ const handleNormalKey = (rep, editorInfo, key) => {
|
|
|
559
576
|
return true;
|
|
560
577
|
}
|
|
561
578
|
|
|
562
|
-
if (pendingKey === "di") {
|
|
579
|
+
if (pendingKey === "di" || pendingKey === "da") {
|
|
580
|
+
const range = textObjectRange(key, lineText, char, pendingKey[1]);
|
|
563
581
|
pendingKey = null;
|
|
564
|
-
const range = innerTextObjectRange(key, lineText, char);
|
|
565
582
|
if (range) {
|
|
566
583
|
setRegister(lineText.slice(range.start, range.end));
|
|
567
584
|
replaceRange(editorInfo, [line, range.start], [line, range.end], "");
|
|
@@ -666,9 +683,9 @@ const handleNormalKey = (rep, editorInfo, key) => {
|
|
|
666
683
|
return true;
|
|
667
684
|
}
|
|
668
685
|
|
|
669
|
-
if (pendingKey === "ci") {
|
|
686
|
+
if (pendingKey === "ci" || pendingKey === "ca") {
|
|
687
|
+
const range = textObjectRange(key, lineText, char, pendingKey[1]);
|
|
670
688
|
pendingKey = null;
|
|
671
|
-
const range = innerTextObjectRange(key, lineText, char);
|
|
672
689
|
if (range) {
|
|
673
690
|
setRegister(lineText.slice(range.start, range.end));
|
|
674
691
|
replaceRange(editorInfo, [line, range.start], [line, range.end], "");
|
|
@@ -710,8 +727,8 @@ const handleNormalKey = (rep, editorInfo, key) => {
|
|
|
710
727
|
}
|
|
711
728
|
|
|
712
729
|
if (pendingKey === "yi") {
|
|
730
|
+
const range = textObjectRange(key, lineText, char, pendingKey[1]);
|
|
713
731
|
pendingKey = null;
|
|
714
|
-
const range = innerTextObjectRange(key, lineText, char);
|
|
715
732
|
if (range) {
|
|
716
733
|
setRegister(lineText.slice(range.start, range.end));
|
|
717
734
|
}
|
|
@@ -1171,6 +1188,16 @@ exports.aceKeyEvent = (_hookName, { evt, rep, editorInfo }) => {
|
|
|
1171
1188
|
return handled || true;
|
|
1172
1189
|
}
|
|
1173
1190
|
|
|
1191
|
+
if (
|
|
1192
|
+
!insertMode &&
|
|
1193
|
+
visualMode !== null &&
|
|
1194
|
+
(evt.key === "i" || evt.key === "a")
|
|
1195
|
+
) {
|
|
1196
|
+
const handled = handleVisualKey(rep, editorInfo, evt.key);
|
|
1197
|
+
evt.preventDefault();
|
|
1198
|
+
return handled || true;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1174
1201
|
if (!insertMode && evt.key === "i") {
|
|
1175
1202
|
const [line, char] = rep.selStart;
|
|
1176
1203
|
desiredColumn = null;
|
package/static/js/vim-core.js
CHANGED
|
@@ -208,7 +208,7 @@ const getVisualSelection = (visualMode, visualAnchor, visualCursor, rep) => {
|
|
|
208
208
|
return [visualCursor, visualAnchor];
|
|
209
209
|
};
|
|
210
210
|
|
|
211
|
-
const
|
|
211
|
+
const textWordRange = (lineText, char, type) => {
|
|
212
212
|
if (lineText.length === 0 || char >= lineText.length) return null;
|
|
213
213
|
const ch = lineText[char];
|
|
214
214
|
let start = char;
|
|
@@ -233,7 +233,16 @@ const innerWordRange = (lineText, char) => {
|
|
|
233
233
|
)
|
|
234
234
|
end++;
|
|
235
235
|
}
|
|
236
|
-
|
|
236
|
+
if (type === "i") {
|
|
237
|
+
return { start, end: end + 1 };
|
|
238
|
+
} else {
|
|
239
|
+
if (isWhitespace(ch)) {
|
|
240
|
+
while (start > 0 && isWhitespace(lineText[start - 1])) start--;
|
|
241
|
+
while (end + 1 < lineText.length && isWhitespace(lineText[end + 1]))
|
|
242
|
+
end++;
|
|
243
|
+
}
|
|
244
|
+
return { start, end: end + 1 };
|
|
245
|
+
}
|
|
237
246
|
};
|
|
238
247
|
|
|
239
248
|
const BRACKET_PAIRS = {
|
|
@@ -246,16 +255,20 @@ const BRACKET_PAIRS = {
|
|
|
246
255
|
};
|
|
247
256
|
const OPEN_BRACKETS = new Set(["(", "{", "["]);
|
|
248
257
|
|
|
249
|
-
const
|
|
258
|
+
const textQuoteRange = (lineText, char, quote, type) => {
|
|
250
259
|
const first = lineText.indexOf(quote);
|
|
251
260
|
if (first === -1) return null;
|
|
252
261
|
const second = lineText.indexOf(quote, first + 1);
|
|
253
262
|
if (second === -1) return null;
|
|
254
263
|
if (char < first || char > second) return null;
|
|
255
|
-
|
|
264
|
+
if (type === "i") {
|
|
265
|
+
return { start: first + 1, end: second };
|
|
266
|
+
} else {
|
|
267
|
+
return { start: first, end: second + 1 };
|
|
268
|
+
}
|
|
256
269
|
};
|
|
257
270
|
|
|
258
|
-
const
|
|
271
|
+
const textBracketRange = (lineText, char, bracket, type) => {
|
|
259
272
|
const open = OPEN_BRACKETS.has(bracket) ? bracket : BRACKET_PAIRS[bracket];
|
|
260
273
|
const close = BRACKET_PAIRS[open];
|
|
261
274
|
let depth = 0;
|
|
@@ -275,14 +288,20 @@ const innerBracketRange = (lineText, char, bracket) => {
|
|
|
275
288
|
for (let i = openPos + 1; i < lineText.length; i++) {
|
|
276
289
|
if (lineText[i] === open) depth++;
|
|
277
290
|
if (lineText[i] === close) {
|
|
278
|
-
if (depth === 0)
|
|
291
|
+
if (depth === 0) {
|
|
292
|
+
if (type === "i") {
|
|
293
|
+
return { start: openPos + 1, end: i };
|
|
294
|
+
} else {
|
|
295
|
+
return { start: openPos, end: i + 1 };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
279
298
|
depth--;
|
|
280
299
|
}
|
|
281
300
|
}
|
|
282
301
|
return null;
|
|
283
302
|
};
|
|
284
303
|
|
|
285
|
-
const getTextInRange = (rep, start, end) => {
|
|
304
|
+
const getTextInRange = (rep, start, end, type) => {
|
|
286
305
|
if (start[0] === end[0]) {
|
|
287
306
|
return getLineText(rep, start[0]).slice(start[1], end[1]);
|
|
288
307
|
}
|
|
@@ -310,9 +329,9 @@ module.exports = {
|
|
|
310
329
|
charSearchPos,
|
|
311
330
|
motionRange,
|
|
312
331
|
charMotionRange,
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
332
|
+
textWordRange,
|
|
333
|
+
textQuoteRange,
|
|
334
|
+
textBracketRange,
|
|
316
335
|
getVisualSelection,
|
|
317
336
|
paragraphForward,
|
|
318
337
|
paragraphBackward,
|
|
@@ -17,9 +17,9 @@ const {
|
|
|
17
17
|
charSearchPos,
|
|
18
18
|
motionRange,
|
|
19
19
|
charMotionRange,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
textWordRange,
|
|
21
|
+
textQuoteRange,
|
|
22
|
+
textBracketRange,
|
|
23
23
|
getVisualSelection,
|
|
24
24
|
paragraphForward,
|
|
25
25
|
paragraphBackward,
|
|
@@ -360,185 +360,203 @@ describe("charMotionRange", () => {
|
|
|
360
360
|
|
|
361
361
|
describe("innerWordRange", () => {
|
|
362
362
|
it("selects the whole word when cursor is in the middle", () => {
|
|
363
|
-
assert.deepEqual(
|
|
363
|
+
assert.deepEqual(textWordRange("hello world", 2, "i"), {
|
|
364
|
+
start: 0,
|
|
365
|
+
end: 5,
|
|
366
|
+
});
|
|
364
367
|
});
|
|
365
368
|
|
|
366
369
|
it("selects the whole word when cursor is at the start", () => {
|
|
367
|
-
assert.deepEqual(
|
|
370
|
+
assert.deepEqual(textWordRange("hello world", 0, "i"), {
|
|
371
|
+
start: 0,
|
|
372
|
+
end: 5,
|
|
373
|
+
});
|
|
368
374
|
});
|
|
369
375
|
|
|
370
376
|
it("selects the whole word when cursor is at the end", () => {
|
|
371
|
-
assert.deepEqual(
|
|
377
|
+
assert.deepEqual(textWordRange("hello world", 4, "i"), {
|
|
378
|
+
start: 0,
|
|
379
|
+
end: 5,
|
|
380
|
+
});
|
|
372
381
|
});
|
|
373
382
|
|
|
374
383
|
it("selects a word in the middle of a line", () => {
|
|
375
|
-
assert.deepEqual(
|
|
384
|
+
assert.deepEqual(textWordRange("hello world foo", 6, "i"), {
|
|
376
385
|
start: 6,
|
|
377
386
|
end: 11,
|
|
378
387
|
});
|
|
379
388
|
});
|
|
380
389
|
|
|
381
390
|
it("selects whitespace when cursor is on whitespace", () => {
|
|
382
|
-
assert.deepEqual(
|
|
391
|
+
assert.deepEqual(textWordRange("hello world", 6, "i"), {
|
|
392
|
+
start: 5,
|
|
393
|
+
end: 8,
|
|
394
|
+
});
|
|
383
395
|
});
|
|
384
396
|
|
|
385
397
|
it("selects a run of punctuation", () => {
|
|
386
|
-
assert.deepEqual(
|
|
398
|
+
assert.deepEqual(textWordRange("foo...bar", 3, "i"), { start: 3, end: 6 });
|
|
387
399
|
});
|
|
388
400
|
|
|
389
401
|
it("returns null for empty string", () => {
|
|
390
|
-
assert.equal(
|
|
402
|
+
assert.equal(textWordRange("", 0, "i"), null);
|
|
391
403
|
});
|
|
392
404
|
|
|
393
405
|
it("returns null when char is out of bounds", () => {
|
|
394
|
-
assert.equal(
|
|
406
|
+
assert.equal(textWordRange("hello", 10, "i"), null);
|
|
395
407
|
});
|
|
396
408
|
|
|
397
409
|
it("selects a single-char word", () => {
|
|
398
|
-
assert.deepEqual(
|
|
410
|
+
assert.deepEqual(textWordRange("a b c", 2, "i"), { start: 2, end: 3 });
|
|
399
411
|
});
|
|
400
412
|
|
|
401
413
|
it("selects word with underscores", () => {
|
|
402
|
-
assert.deepEqual(
|
|
414
|
+
assert.deepEqual(textWordRange("foo_bar baz", 3, "i"), {
|
|
415
|
+
start: 0,
|
|
416
|
+
end: 7,
|
|
417
|
+
});
|
|
403
418
|
});
|
|
404
419
|
|
|
405
420
|
it("selects word with digits", () => {
|
|
406
|
-
assert.deepEqual(
|
|
421
|
+
assert.deepEqual(textWordRange("abc123 xyz", 4, "i"), { start: 0, end: 6 });
|
|
407
422
|
});
|
|
408
423
|
|
|
409
424
|
it("selects single whitespace char between words", () => {
|
|
410
|
-
assert.deepEqual(
|
|
425
|
+
assert.deepEqual(textWordRange("hello world", 5, "i"), {
|
|
426
|
+
start: 5,
|
|
427
|
+
end: 6,
|
|
428
|
+
});
|
|
411
429
|
});
|
|
412
430
|
|
|
413
431
|
it("selects last word on line", () => {
|
|
414
|
-
assert.deepEqual(
|
|
432
|
+
assert.deepEqual(textWordRange("foo bar", 4, "i"), { start: 4, end: 7 });
|
|
415
433
|
});
|
|
416
434
|
|
|
417
435
|
it("handles cursor at last char of line", () => {
|
|
418
|
-
assert.deepEqual(
|
|
436
|
+
assert.deepEqual(textWordRange("hello", 4, "i"), { start: 0, end: 5 });
|
|
419
437
|
});
|
|
420
438
|
|
|
421
439
|
it("selects a single-char line", () => {
|
|
422
|
-
assert.deepEqual(
|
|
440
|
+
assert.deepEqual(textWordRange("x", 0, "i"), { start: 0, end: 1 });
|
|
423
441
|
});
|
|
424
442
|
});
|
|
425
443
|
|
|
426
|
-
describe("
|
|
444
|
+
describe("textQuoteRange", () => {
|
|
427
445
|
it("selects content between double quotes", () => {
|
|
428
|
-
assert.deepEqual(
|
|
446
|
+
assert.deepEqual(textQuoteRange('say "hello" ok', 6, '"', "i"), {
|
|
429
447
|
start: 5,
|
|
430
448
|
end: 10,
|
|
431
449
|
});
|
|
432
450
|
});
|
|
433
451
|
|
|
434
452
|
it("works when cursor is on the opening quote", () => {
|
|
435
|
-
assert.deepEqual(
|
|
453
|
+
assert.deepEqual(textQuoteRange('say "hello" ok', 4, '"', "i"), {
|
|
436
454
|
start: 5,
|
|
437
455
|
end: 10,
|
|
438
456
|
});
|
|
439
457
|
});
|
|
440
458
|
|
|
441
459
|
it("works when cursor is on the closing quote", () => {
|
|
442
|
-
assert.deepEqual(
|
|
460
|
+
assert.deepEqual(textQuoteRange('say "hello" ok', 10, '"', "i"), {
|
|
443
461
|
start: 5,
|
|
444
462
|
end: 10,
|
|
445
463
|
});
|
|
446
464
|
});
|
|
447
465
|
|
|
448
466
|
it("returns null when no quotes exist", () => {
|
|
449
|
-
assert.equal(
|
|
467
|
+
assert.equal(textQuoteRange("no quotes here", 3, '"', "i"), null);
|
|
450
468
|
});
|
|
451
469
|
|
|
452
470
|
it("returns null when only one quote exists", () => {
|
|
453
|
-
assert.equal(
|
|
471
|
+
assert.equal(textQuoteRange('say "hello', 6, '"', "i"), null);
|
|
454
472
|
});
|
|
455
473
|
|
|
456
474
|
it("returns null when cursor is outside the quotes", () => {
|
|
457
|
-
assert.equal(
|
|
475
|
+
assert.equal(textQuoteRange('say "hello" ok', 12, '"', "i"), null);
|
|
458
476
|
});
|
|
459
477
|
|
|
460
478
|
it("works with single quotes", () => {
|
|
461
|
-
assert.deepEqual(
|
|
479
|
+
assert.deepEqual(textQuoteRange("say 'fine' ok", 7, "'", "i"), {
|
|
462
480
|
start: 5,
|
|
463
481
|
end: 9,
|
|
464
482
|
});
|
|
465
483
|
});
|
|
466
484
|
|
|
467
485
|
it("selects empty content between adjacent quotes", () => {
|
|
468
|
-
assert.deepEqual(
|
|
486
|
+
assert.deepEqual(textQuoteRange('foo "" bar', 4, '"', "i"), {
|
|
469
487
|
start: 5,
|
|
470
488
|
end: 5,
|
|
471
489
|
});
|
|
472
490
|
});
|
|
473
491
|
});
|
|
474
492
|
|
|
475
|
-
describe("
|
|
493
|
+
describe("textBracketRange", () => {
|
|
476
494
|
it("selects content inside parentheses", () => {
|
|
477
|
-
assert.deepEqual(
|
|
495
|
+
assert.deepEqual(textBracketRange("foo(bar)baz", 5, "(", "i"), {
|
|
478
496
|
start: 4,
|
|
479
497
|
end: 7,
|
|
480
498
|
});
|
|
481
499
|
});
|
|
482
500
|
|
|
483
501
|
it("works with closing bracket as argument", () => {
|
|
484
|
-
assert.deepEqual(
|
|
502
|
+
assert.deepEqual(textBracketRange("foo(bar)baz", 5, ")", "i"), {
|
|
485
503
|
start: 4,
|
|
486
504
|
end: 7,
|
|
487
505
|
});
|
|
488
506
|
});
|
|
489
507
|
|
|
490
508
|
it("selects content inside curly braces", () => {
|
|
491
|
-
assert.deepEqual(
|
|
509
|
+
assert.deepEqual(textBracketRange("if {yes} no", 5, "{", "i"), {
|
|
492
510
|
start: 4,
|
|
493
511
|
end: 7,
|
|
494
512
|
});
|
|
495
513
|
});
|
|
496
514
|
|
|
497
515
|
it("selects content inside square brackets", () => {
|
|
498
|
-
assert.deepEqual(
|
|
516
|
+
assert.deepEqual(textBracketRange("a[bc]d", 2, "[", "i"), {
|
|
499
517
|
start: 2,
|
|
500
518
|
end: 4,
|
|
501
519
|
});
|
|
502
520
|
});
|
|
503
521
|
|
|
504
522
|
it("handles nested brackets", () => {
|
|
505
|
-
assert.deepEqual(
|
|
523
|
+
assert.deepEqual(textBracketRange("(a(b)c)", 3, "(", "i"), {
|
|
506
524
|
start: 3,
|
|
507
525
|
end: 4,
|
|
508
526
|
});
|
|
509
527
|
});
|
|
510
528
|
|
|
511
529
|
it("handles nested brackets from outer position", () => {
|
|
512
|
-
assert.deepEqual(
|
|
530
|
+
assert.deepEqual(textBracketRange("(a(b)c)", 1, "(", "i"), {
|
|
513
531
|
start: 1,
|
|
514
532
|
end: 6,
|
|
515
533
|
});
|
|
516
534
|
});
|
|
517
535
|
|
|
518
536
|
it("returns null when no matching brackets", () => {
|
|
519
|
-
assert.equal(
|
|
537
|
+
assert.equal(textBracketRange("no brackets", 3, "(", "i"), null);
|
|
520
538
|
});
|
|
521
539
|
|
|
522
540
|
it("returns null when cursor is outside brackets", () => {
|
|
523
|
-
assert.equal(
|
|
541
|
+
assert.equal(textBracketRange("x (foo) y", 0, "(", "i"), null);
|
|
524
542
|
});
|
|
525
543
|
|
|
526
544
|
it("selects empty content between adjacent brackets", () => {
|
|
527
|
-
assert.deepEqual(
|
|
545
|
+
assert.deepEqual(textBracketRange("foo()bar", 4, "(", "i"), {
|
|
528
546
|
start: 4,
|
|
529
547
|
end: 4,
|
|
530
548
|
});
|
|
531
549
|
});
|
|
532
550
|
|
|
533
551
|
it("works when cursor is on the opening bracket", () => {
|
|
534
|
-
assert.deepEqual(
|
|
552
|
+
assert.deepEqual(textBracketRange("(hello)", 0, "(", "i"), {
|
|
535
553
|
start: 1,
|
|
536
554
|
end: 6,
|
|
537
555
|
});
|
|
538
556
|
});
|
|
539
557
|
|
|
540
558
|
it("works when cursor is on the closing bracket", () => {
|
|
541
|
-
assert.deepEqual(
|
|
559
|
+
assert.deepEqual(textBracketRange("(hello)", 6, ")", "i"), {
|
|
542
560
|
start: 1,
|
|
543
561
|
end: 6,
|
|
544
562
|
});
|