ep_vim 0.12.3 → 1.0.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
@@ -19,7 +19,8 @@ A vim-mode plugin for [Etherpad](https://etherpad.org/). Adds modal editing with
19
19
  - **Line operations** — `dd`, `cc`, `yy`, `D`, `J` (join), `Y` (yank line)
20
20
  - **Registers** — `"a`–`"z` named registers for yank/delete/put, `"_` blackhole register
21
21
  - **Put** — `p` / `P` with linewise and characterwise register handling
22
- - **Editing** — `i` `a` `A` `I` (insert/append), `x`, `X`, `r`, `s`, `S`, `C`, `o`, `O`, `~` (toggle case)
22
+ - **Editing** — `i` `a` `A` `I` (insert/append), `x`, `X`, `r`, `R` (replace mode), `s`, `S`, `C`, `o`, `O`, `~` (toggle case)
23
+ - **Case operators** — `gu{motion}`/`gU{motion}` lowercase/uppercase over motion or text object (e.g. `guiw`, `gUap`); `u`/`U` in visual mode lowercase/uppercase selection
23
24
  - **Marks** — `m{a-z}` to set, `'{a-z}` / `` `{a-z} `` to jump
24
25
  - **Search** — `/` and `?` forward/backward, `n`/`N` repeat, `*`/`#` search word under cursor
25
26
  - **Scrolling** — `zz`/`zt`/`zb` center/top/bottom, `Ctrl+d`/`Ctrl+u` half-page, `Ctrl+f`/`Ctrl+b` full-page (requires ctrl keys enabled)
@@ -30,13 +31,21 @@ A vim-mode plugin for [Etherpad](https://etherpad.org/). Adds modal editing with
30
31
  - **Toggle** — toolbar button to enable/disable vim mode, persisted in localStorage; settings panel for system clipboard and ctrl key behavior
31
32
 
32
33
  ## Differences from vi
33
- The following are not planned, but PRs are welcome.
34
+ No further features are planned, but PRs are welcome. Notable exclusions in the current implementation are:
34
35
 
35
36
  - **No command line, macros, or globals**
36
37
  - **No visual block mode**
37
38
  - **No indentation operators** — `>>`, `<<`, and `>` / `<` in visual mode
38
39
  - **No increment/decrement** — `Ctrl+a` and `Ctrl+x`
39
40
 
41
+ ## Rate limiting
42
+
43
+ If you encounter rate limit errors, Etherpad's commit rate limiter (default: 10 per second) can be adjusted:
44
+
45
+ ```
46
+ COMMIT_RATE_LIMIT_POINTS=100
47
+ ```
48
+
40
49
  ## Installation
41
50
 
42
51
  From your Etherpad directory run
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ep_vim",
3
- "version": "0.12.3",
3
+ "version": "1.0.0",
4
4
  "description": "Vim-mode plugin for Etherpad with modal editing, motions, and operators",
5
5
  "author": {
6
6
  "name": "Seth Rothschild",
@@ -227,6 +227,17 @@ const applyOperator = (op, start, end, ctx) => {
227
227
  }
228
228
  };
229
229
 
230
+ const applyCaseOp = (op, start, end, ctx) => {
231
+ const { editorInfo, rep } = ctx;
232
+ const before =
233
+ start[0] < end[0] || (start[0] === end[0] && start[1] <= end[1]);
234
+ const [s, e] = before ? [start, end] : [end, start];
235
+ const text = getTextInRange(rep, s, e);
236
+ const transformed = op === "gu" ? text.toLowerCase() : text.toUpperCase();
237
+ replaceRange(editorInfo, s, e, transformed);
238
+ moveBlockCursor(editorInfo, s[0], s[1]);
239
+ };
240
+
230
241
  // --- Command tables ---
231
242
  const commands = {
232
243
  normal: {},
@@ -236,6 +247,7 @@ const commands = {
236
247
 
237
248
  // --- Registration helpers ---
238
249
  const OPERATORS = ["d", "c", "y"];
250
+ const CASE_OPERATORS = ["gu", "gU"];
239
251
 
240
252
  const resolveTextObject = (key, type, line, lineText, char, rep) => {
241
253
  if (key === "p") {
@@ -302,6 +314,17 @@ const registerMotion = (
302
314
  }
303
315
  };
304
316
  }
317
+ for (const op of CASE_OPERATORS) {
318
+ commands.normal[op + key] = (ctx) => {
319
+ state.desiredColumn = null;
320
+ const pos = getEndPos(ctx);
321
+ if (pos) {
322
+ const endChar = inclusive ? pos.char + 1 : pos.char;
323
+ applyCaseOp(op, [ctx.line, ctx.char], [pos.line, endChar], ctx);
324
+ recordCommand(op + key, ctx.count);
325
+ }
326
+ };
327
+ }
305
328
  };
306
329
 
307
330
  const parameterized = {};
@@ -360,6 +383,14 @@ const registerTextObject = (obj, getRange) => {
360
383
  };
361
384
  }
362
385
  }
386
+ for (const op of CASE_OPERATORS) {
387
+ for (const type of ["i", "a"]) {
388
+ commands.normal[`${op}${type}${obj}`] = (ctx) => {
389
+ const range = getRange(ctx, type);
390
+ if (range) applyCaseOp(op, range.start, range.end, ctx);
391
+ };
392
+ }
393
+ }
363
394
  };
364
395
 
365
396
  const getVisibleLineRange = (rep) => {
@@ -770,6 +801,60 @@ commands["visual-line"]["~"] = (ctx) => {
770
801
  moveBlockCursor(ctx.editorInfo, start[0], start[1]);
771
802
  };
772
803
 
804
+ commands["visual-char"]["u"] = (ctx) => {
805
+ const [start, end] = getVisualSelection(
806
+ "char",
807
+ state.visualAnchor,
808
+ state.visualCursor,
809
+ ctx.rep,
810
+ );
811
+ const adjustedEnd = [end[0], end[1] + 1];
812
+ const text = getTextInRange(ctx.rep, start, adjustedEnd);
813
+ replaceRange(ctx.editorInfo, start, adjustedEnd, text.toLowerCase());
814
+ state.mode = "normal";
815
+ moveBlockCursor(ctx.editorInfo, start[0], start[1]);
816
+ };
817
+
818
+ commands["visual-char"]["U"] = (ctx) => {
819
+ const [start, end] = getVisualSelection(
820
+ "char",
821
+ state.visualAnchor,
822
+ state.visualCursor,
823
+ ctx.rep,
824
+ );
825
+ const adjustedEnd = [end[0], end[1] + 1];
826
+ const text = getTextInRange(ctx.rep, start, adjustedEnd);
827
+ replaceRange(ctx.editorInfo, start, adjustedEnd, text.toUpperCase());
828
+ state.mode = "normal";
829
+ moveBlockCursor(ctx.editorInfo, start[0], start[1]);
830
+ };
831
+
832
+ commands["visual-line"]["u"] = (ctx) => {
833
+ const [start, end] = getVisualSelection(
834
+ "line",
835
+ state.visualAnchor,
836
+ state.visualCursor,
837
+ ctx.rep,
838
+ );
839
+ const text = getTextInRange(ctx.rep, start, end);
840
+ replaceRange(ctx.editorInfo, start, end, text.toLowerCase());
841
+ state.mode = "normal";
842
+ moveBlockCursor(ctx.editorInfo, start[0], start[1]);
843
+ };
844
+
845
+ commands["visual-line"]["U"] = (ctx) => {
846
+ const [start, end] = getVisualSelection(
847
+ "line",
848
+ state.visualAnchor,
849
+ state.visualCursor,
850
+ ctx.rep,
851
+ );
852
+ const text = getTextInRange(ctx.rep, start, end);
853
+ replaceRange(ctx.editorInfo, start, end, text.toUpperCase());
854
+ state.mode = "normal";
855
+ moveBlockCursor(ctx.editorInfo, start[0], start[1]);
856
+ };
857
+
773
858
  commands["visual-char"]["i"] = () => {
774
859
  state.pendingKey = "i";
775
860
  };
@@ -850,6 +935,12 @@ commands.normal["O"] = ({ editorInfo, line }) => {
850
935
  state.mode = "insert";
851
936
  };
852
937
 
938
+ commands.normal["R"] = ({ editorInfo, line, char }) => {
939
+ clearEmptyLineCursor();
940
+ moveCursor(editorInfo, line, char);
941
+ state.mode = "replace";
942
+ };
943
+
853
944
  // --- Editing commands ---
854
945
 
855
946
  commands.normal["r"] = () => {
@@ -1324,7 +1415,7 @@ exports.aceKeyEvent = (_hookName, { evt, rep, editorInfo }) => {
1324
1415
  const [vLine, vChar] = state.visualCursor;
1325
1416
  state.mode = "normal";
1326
1417
  moveBlockCursor(editorInfo, vLine, vChar);
1327
- } else if (state.mode === "insert") {
1418
+ } else if (state.mode === "insert" || state.mode === "replace") {
1328
1419
  state.mode = "normal";
1329
1420
  const [curLine, curChar] = rep.selStart;
1330
1421
  moveBlockCursor(editorInfo, curLine, Math.max(0, curChar - 1));
@@ -1342,6 +1433,40 @@ exports.aceKeyEvent = (_hookName, { evt, rep, editorInfo }) => {
1342
1433
 
1343
1434
  if (state.mode === "insert") return false;
1344
1435
 
1436
+ if (state.mode === "replace") {
1437
+ if (evt.key === "Backspace") {
1438
+ const [curLine, curChar] = rep.selStart;
1439
+ if (curChar > 0) {
1440
+ moveCursor(editorInfo, curLine, curChar - 1);
1441
+ }
1442
+ evt.preventDefault();
1443
+ return true;
1444
+ }
1445
+ if (evt.key.length === 1 && !evt.ctrlKey && !evt.metaKey) {
1446
+ const [curLine, curChar] = rep.selStart;
1447
+ const curLineText = rep.lines.atIndex(curLine).text;
1448
+ if (curChar < curLineText.length) {
1449
+ replaceRange(
1450
+ editorInfo,
1451
+ [curLine, curChar],
1452
+ [curLine, curChar + 1],
1453
+ evt.key,
1454
+ );
1455
+ } else {
1456
+ replaceRange(
1457
+ editorInfo,
1458
+ [curLine, curChar],
1459
+ [curLine, curChar],
1460
+ evt.key,
1461
+ );
1462
+ }
1463
+ moveCursor(editorInfo, curLine, curChar + 1);
1464
+ evt.preventDefault();
1465
+ return true;
1466
+ }
1467
+ return false;
1468
+ }
1469
+
1345
1470
  if (state.searchMode) {
1346
1471
  if (evt.key === "Enter") {
1347
1472
  state.searchMode = false;
@@ -184,8 +184,8 @@ describe("visual mode", () => {
184
184
  commands.normal["V"](ctx);
185
185
 
186
186
  assert.equal(state.mode, "visual-line");
187
- assert.deepEqual(state.visualAnchor, [0, 0]);
188
- assert.deepEqual(state.visualCursor, [0, 0]);
187
+ assert.deepEqual(state.visualAnchor, [0, 2]);
188
+ assert.deepEqual(state.visualCursor, [0, 2]);
189
189
  });
190
190
 
191
191
  it("y in visual-line yanks lines", () => {