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 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`, `diw`, `yiw`)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ep_vim",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Vim-mode plugin for Etherpad with modal editing, motions, and operators",
5
5
  "author": {
6
6
  "name": "Seth Rothschild",
@@ -11,9 +11,9 @@ const {
11
11
  charSearchPos,
12
12
  motionRange,
13
13
  charMotionRange,
14
- innerWordRange,
15
- innerQuoteRange,
16
- innerBracketRange,
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 innerTextObjectRange = (key, lineText, char) => {
44
- if (key === "w") return innerWordRange(lineText, char);
45
- if (QUOTE_CHARS.has(key)) return innerQuoteRange(lineText, char, key);
46
- if (BRACKET_CHARS.has(key)) return innerBracketRange(lineText, char, key);
47
- return null;
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;
@@ -208,7 +208,7 @@ const getVisualSelection = (visualMode, visualAnchor, visualCursor, rep) => {
208
208
  return [visualCursor, visualAnchor];
209
209
  };
210
210
 
211
- const innerWordRange = (lineText, char) => {
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
- return { start, end: end + 1 };
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 innerQuoteRange = (lineText, char, quote) => {
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
- return { start: first + 1, end: second };
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 innerBracketRange = (lineText, char, bracket) => {
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) return { start: openPos + 1, end: i };
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
- innerWordRange,
314
- innerQuoteRange,
315
- innerBracketRange,
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
- innerWordRange,
21
- innerQuoteRange,
22
- innerBracketRange,
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(innerWordRange("hello world", 2), { start: 0, end: 5 });
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(innerWordRange("hello world", 0), { start: 0, end: 5 });
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(innerWordRange("hello world", 4), { start: 0, end: 5 });
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(innerWordRange("hello world foo", 6), {
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(innerWordRange("hello world", 6), { start: 5, end: 8 });
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(innerWordRange("foo...bar", 3), { start: 3, end: 6 });
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(innerWordRange("", 0), null);
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(innerWordRange("hello", 10), null);
406
+ assert.equal(textWordRange("hello", 10, "i"), null);
395
407
  });
396
408
 
397
409
  it("selects a single-char word", () => {
398
- assert.deepEqual(innerWordRange("a b c", 2), { start: 2, end: 3 });
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(innerWordRange("foo_bar baz", 3), { start: 0, end: 7 });
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(innerWordRange("abc123 xyz", 4), { start: 0, end: 6 });
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(innerWordRange("hello world", 5), { start: 5, end: 6 });
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(innerWordRange("foo bar", 4), { start: 4, end: 7 });
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(innerWordRange("hello", 4), { start: 0, end: 5 });
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(innerWordRange("x", 0), { start: 0, end: 1 });
440
+ assert.deepEqual(textWordRange("x", 0, "i"), { start: 0, end: 1 });
423
441
  });
424
442
  });
425
443
 
426
- describe("innerQuoteRange", () => {
444
+ describe("textQuoteRange", () => {
427
445
  it("selects content between double quotes", () => {
428
- assert.deepEqual(innerQuoteRange('say "hello" ok', 6, '"'), {
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(innerQuoteRange('say "hello" ok', 4, '"'), {
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(innerQuoteRange('say "hello" ok', 10, '"'), {
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(innerQuoteRange("no quotes here", 3, '"'), null);
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(innerQuoteRange('say "hello', 6, '"'), null);
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(innerQuoteRange('say "hello" ok', 12, '"'), null);
475
+ assert.equal(textQuoteRange('say "hello" ok', 12, '"', "i"), null);
458
476
  });
459
477
 
460
478
  it("works with single quotes", () => {
461
- assert.deepEqual(innerQuoteRange("say 'fine' ok", 7, "'"), {
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(innerQuoteRange('foo "" bar', 4, '"'), {
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("innerBracketRange", () => {
493
+ describe("textBracketRange", () => {
476
494
  it("selects content inside parentheses", () => {
477
- assert.deepEqual(innerBracketRange("foo(bar)baz", 5, "("), {
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(innerBracketRange("foo(bar)baz", 5, ")"), {
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(innerBracketRange("if {yes} no", 5, "{"), {
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(innerBracketRange("a[bc]d", 2, "["), {
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(innerBracketRange("(a(b)c)", 3, "("), {
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(innerBracketRange("(a(b)c)", 1, "("), {
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(innerBracketRange("no brackets", 3, "("), null);
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(innerBracketRange("x (foo) y", 0, "("), null);
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(innerBracketRange("foo()bar", 4, "("), {
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(innerBracketRange("(hello)", 0, "("), {
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(innerBracketRange("(hello)", 6, ")"), {
559
+ assert.deepEqual(textBracketRange("(hello)", 6, ")", "i"), {
542
560
  start: 1,
543
561
  end: 6,
544
562
  });