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 +11 -2
- package/package.json +1 -1
- package/static/js/index.js +126 -1
- package/static/js/visual.test.js +2 -2
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
|
-
|
|
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
package/static/js/index.js
CHANGED
|
@@ -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;
|
package/static/js/visual.test.js
CHANGED
|
@@ -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,
|
|
188
|
-
assert.deepEqual(state.visualCursor, [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", () => {
|