ep_vim 0.12.0 → 0.12.3
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 +6 -2
- package/package.json +2 -2
- package/static/js/index.js +116 -105
- package/static/js/insert.test.js +198 -0
- package/static/js/misc.test.js +363 -0
- package/static/js/motions.test.js +1377 -0
- package/static/js/operators.test.js +1118 -0
- package/static/js/paste_registers.test.js +778 -0
- package/static/js/search.test.js +234 -0
- package/static/js/visual.test.js +382 -0
- package/static/js/index.test.js +0 -4023
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ 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`, `r`, `s`, `S`, `C`, `o`, `O`, `~` (toggle case)
|
|
22
|
+
- **Editing** — `i` `a` `A` `I` (insert/append), `x`, `X`, `r`, `s`, `S`, `C`, `o`, `O`, `~` (toggle case)
|
|
23
23
|
- **Marks** — `m{a-z}` to set, `'{a-z}` / `` `{a-z} `` to jump
|
|
24
24
|
- **Search** — `/` and `?` forward/backward, `n`/`N` repeat, `*`/`#` search word under cursor
|
|
25
25
|
- **Scrolling** — `zz`/`zt`/`zb` center/top/bottom, `Ctrl+d`/`Ctrl+u` half-page, `Ctrl+f`/`Ctrl+b` full-page (requires ctrl keys enabled)
|
|
@@ -30,8 +30,12 @@ A vim-mode plugin for [Etherpad](https://etherpad.org/). Adds modal editing with
|
|
|
30
30
|
- **Toggle** — toolbar button to enable/disable vim mode, persisted in localStorage; settings panel for system clipboard and ctrl key behavior
|
|
31
31
|
|
|
32
32
|
## Differences from vi
|
|
33
|
+
The following are not planned, but PRs are welcome.
|
|
33
34
|
|
|
34
|
-
- **No command line, macros, or globals**
|
|
35
|
+
- **No command line, macros, or globals**
|
|
36
|
+
- **No visual block mode**
|
|
37
|
+
- **No indentation operators** — `>>`, `<<`, and `>` / `<` in visual mode
|
|
38
|
+
- **No increment/decrement** — `Ctrl+a` and `Ctrl+x`
|
|
35
39
|
|
|
36
40
|
## Installation
|
|
37
41
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ep_vim",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.3",
|
|
4
4
|
"description": "Vim-mode plugin for Etherpad with modal editing, motions, and operators",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Seth Rothschild",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"ep_etherpad-lite": ">=1.8.6"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
|
-
"test": "npm run format && node --test static/js/vim-core.test.js static/js/
|
|
28
|
+
"test": "npm run format && node --test static/js/vim-core.test.js static/js/motions.test.js static/js/operators.test.js static/js/insert.test.js static/js/visual.test.js static/js/paste_registers.test.js static/js/search.test.js static/js/misc.test.js",
|
|
29
29
|
"format": "prettier -w static/js/*"
|
|
30
30
|
},
|
|
31
31
|
"engines": {
|
package/static/js/index.js
CHANGED
|
@@ -540,6 +540,8 @@ registerMotion("L", (ctx) => {
|
|
|
540
540
|
};
|
|
541
541
|
});
|
|
542
542
|
|
|
543
|
+
// --- Char search motions ---
|
|
544
|
+
|
|
543
545
|
registerParamMotion("f", (key, ctx) => {
|
|
544
546
|
const pos = charSearchPos("f", ctx.lineText, ctx.char, key, ctx.count);
|
|
545
547
|
return pos !== -1 ? pos : null;
|
|
@@ -702,9 +704,9 @@ commands.normal["v"] = ({ editorInfo, rep, line, char }) => {
|
|
|
702
704
|
updateVisualSelection(editorInfo, rep);
|
|
703
705
|
};
|
|
704
706
|
|
|
705
|
-
commands.normal["V"] = ({ editorInfo, rep, line }) => {
|
|
706
|
-
state.visualAnchor = [line,
|
|
707
|
-
state.visualCursor = [line,
|
|
707
|
+
commands.normal["V"] = ({ editorInfo, rep, line, char }) => {
|
|
708
|
+
state.visualAnchor = [line, char];
|
|
709
|
+
state.visualCursor = [line, char];
|
|
708
710
|
state.mode = "visual-line";
|
|
709
711
|
updateVisualSelection(editorInfo, rep);
|
|
710
712
|
};
|
|
@@ -768,6 +770,32 @@ commands["visual-line"]["~"] = (ctx) => {
|
|
|
768
770
|
moveBlockCursor(ctx.editorInfo, start[0], start[1]);
|
|
769
771
|
};
|
|
770
772
|
|
|
773
|
+
commands["visual-char"]["i"] = () => {
|
|
774
|
+
state.pendingKey = "i";
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
commands["visual-char"]["a"] = () => {
|
|
778
|
+
state.pendingKey = "a";
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
commands["visual-line"]["i"] = () => {
|
|
782
|
+
state.pendingKey = "i";
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
commands["visual-line"]["a"] = () => {
|
|
786
|
+
state.pendingKey = "a";
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
const swapVisualEnds = ({ editorInfo, rep }) => {
|
|
790
|
+
const tmp = state.visualAnchor;
|
|
791
|
+
state.visualAnchor = state.visualCursor;
|
|
792
|
+
state.visualCursor = tmp;
|
|
793
|
+
updateVisualSelection(editorInfo, rep);
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
commands["visual-char"]["o"] = swapVisualEnds;
|
|
797
|
+
commands["visual-line"]["o"] = swapVisualEnds;
|
|
798
|
+
|
|
771
799
|
commands.normal["gv"] = ({ editorInfo, rep }) => {
|
|
772
800
|
if (!state.lastVisualSelection) return;
|
|
773
801
|
const { anchor, cursor, mode } = state.lastVisualSelection;
|
|
@@ -777,24 +805,7 @@ commands.normal["gv"] = ({ editorInfo, rep }) => {
|
|
|
777
805
|
updateVisualSelection(editorInfo, rep);
|
|
778
806
|
};
|
|
779
807
|
|
|
780
|
-
// ---
|
|
781
|
-
|
|
782
|
-
commands.normal["u"] = ({ editorInfo }) => {
|
|
783
|
-
editorInfo.ace_doUndoRedo("undo");
|
|
784
|
-
};
|
|
785
|
-
|
|
786
|
-
commands.normal["."] = (ctx) => {
|
|
787
|
-
if (!state.lastCommand) return;
|
|
788
|
-
const { key, count, param } = state.lastCommand;
|
|
789
|
-
if (param !== null && parameterized[key]) {
|
|
790
|
-
parameterized[key](param, ctx);
|
|
791
|
-
} else if (commands[state.mode] && commands[state.mode][key]) {
|
|
792
|
-
const newCtx = { ...ctx, count };
|
|
793
|
-
commands[state.mode][key](newCtx);
|
|
794
|
-
}
|
|
795
|
-
};
|
|
796
|
-
|
|
797
|
-
// --- Mode transitions ---
|
|
808
|
+
// --- Insert mode entry ---
|
|
798
809
|
|
|
799
810
|
commands.normal["i"] = ({ editorInfo, line, char }) => {
|
|
800
811
|
clearEmptyLineCursor();
|
|
@@ -820,22 +831,6 @@ commands.normal["I"] = ({ editorInfo, line, lineText }) => {
|
|
|
820
831
|
state.mode = "insert";
|
|
821
832
|
};
|
|
822
833
|
|
|
823
|
-
commands["visual-char"]["i"] = () => {
|
|
824
|
-
state.pendingKey = "i";
|
|
825
|
-
};
|
|
826
|
-
|
|
827
|
-
commands["visual-char"]["a"] = () => {
|
|
828
|
-
state.pendingKey = "a";
|
|
829
|
-
};
|
|
830
|
-
|
|
831
|
-
commands["visual-line"]["i"] = () => {
|
|
832
|
-
state.pendingKey = "i";
|
|
833
|
-
};
|
|
834
|
-
|
|
835
|
-
commands["visual-line"]["a"] = () => {
|
|
836
|
-
state.pendingKey = "a";
|
|
837
|
-
};
|
|
838
|
-
|
|
839
834
|
commands.normal["o"] = ({ editorInfo, line, lineText }) => {
|
|
840
835
|
clearEmptyLineCursor();
|
|
841
836
|
replaceRange(
|
|
@@ -855,7 +850,7 @@ commands.normal["O"] = ({ editorInfo, line }) => {
|
|
|
855
850
|
state.mode = "insert";
|
|
856
851
|
};
|
|
857
852
|
|
|
858
|
-
// ---
|
|
853
|
+
// --- Editing commands ---
|
|
859
854
|
|
|
860
855
|
commands.normal["r"] = () => {
|
|
861
856
|
state.pendingKey = "r";
|
|
@@ -884,6 +879,18 @@ commands.normal["x"] = ({ editorInfo, rep, line, char, lineText, count }) => {
|
|
|
884
879
|
}
|
|
885
880
|
};
|
|
886
881
|
|
|
882
|
+
commands.normal["X"] = ({ editorInfo, rep, line, char, lineText, count }) => {
|
|
883
|
+
if (char > 0) {
|
|
884
|
+
const deleteCount = Math.min(count, char);
|
|
885
|
+
const startChar = char - deleteCount;
|
|
886
|
+
setRegister(lineText.slice(startChar, char));
|
|
887
|
+
replaceRange(editorInfo, [line, startChar], [line, char], "");
|
|
888
|
+
const newLineText = getLineText(rep, line);
|
|
889
|
+
moveBlockCursor(editorInfo, line, clampChar(startChar, newLineText));
|
|
890
|
+
recordCommand("X", count);
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
|
|
887
894
|
commands.normal["p"] = ({ editorInfo, line, char, lineText, count }) => {
|
|
888
895
|
const reg = getActiveRegister();
|
|
889
896
|
if (reg !== null) {
|
|
@@ -1005,6 +1012,27 @@ commands.normal["S"] = ({ editorInfo, line, lineText }) => {
|
|
|
1005
1012
|
recordCommand("S", 1);
|
|
1006
1013
|
};
|
|
1007
1014
|
|
|
1015
|
+
// --- Undo, redo, repeat ---
|
|
1016
|
+
|
|
1017
|
+
commands.normal["u"] = ({ editorInfo }) => {
|
|
1018
|
+
editorInfo.ace_doUndoRedo("undo");
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
commands.normal["<C-r>"] = ({ editorInfo }) => {
|
|
1022
|
+
editorInfo.ace_doUndoRedo("redo");
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
commands.normal["."] = (ctx) => {
|
|
1026
|
+
if (!state.lastCommand) return;
|
|
1027
|
+
const { key, count, param } = state.lastCommand;
|
|
1028
|
+
if (param !== null && parameterized[key]) {
|
|
1029
|
+
parameterized[key](param, ctx);
|
|
1030
|
+
} else if (commands[state.mode] && commands[state.mode][key]) {
|
|
1031
|
+
const newCtx = { ...ctx, count };
|
|
1032
|
+
commands[state.mode][key](newCtx);
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1008
1036
|
// --- Search ---
|
|
1009
1037
|
|
|
1010
1038
|
commands.normal["/"] = () => {
|
|
@@ -1060,6 +1088,8 @@ commands.normal["#"] = (ctx) => {
|
|
|
1060
1088
|
if (pos) moveBlockCursor(ctx.editorInfo, pos[0], pos[1]);
|
|
1061
1089
|
};
|
|
1062
1090
|
|
|
1091
|
+
// --- Scroll ---
|
|
1092
|
+
|
|
1063
1093
|
commands.normal["zz"] = ({ line }) => {
|
|
1064
1094
|
if (!state.editorDoc) return;
|
|
1065
1095
|
const lineDiv = state.editorDoc.body.querySelectorAll("div")[line];
|
|
@@ -1078,6 +1108,49 @@ commands.normal["zb"] = ({ line }) => {
|
|
|
1078
1108
|
if (lineDiv) lineDiv.scrollIntoView({ block: "end" });
|
|
1079
1109
|
};
|
|
1080
1110
|
|
|
1111
|
+
const halfPage = 15;
|
|
1112
|
+
const fullPage = halfPage * 2;
|
|
1113
|
+
|
|
1114
|
+
commands.normal["<C-d>"] = ({ editorInfo, rep, line, char, count }) => {
|
|
1115
|
+
const target = Math.min(line + halfPage * count, rep.lines.length() - 1);
|
|
1116
|
+
const targetLen = rep.lines.atIndex(target).text.length;
|
|
1117
|
+
moveBlockCursor(
|
|
1118
|
+
editorInfo,
|
|
1119
|
+
target,
|
|
1120
|
+
Math.min(char, Math.max(0, targetLen - 1)),
|
|
1121
|
+
);
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
commands.normal["<C-u>"] = ({ editorInfo, rep, line, char, count }) => {
|
|
1125
|
+
const target = Math.max(line - halfPage * count, 0);
|
|
1126
|
+
const targetLen = rep.lines.atIndex(target).text.length;
|
|
1127
|
+
moveBlockCursor(
|
|
1128
|
+
editorInfo,
|
|
1129
|
+
target,
|
|
1130
|
+
Math.min(char, Math.max(0, targetLen - 1)),
|
|
1131
|
+
);
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
commands.normal["<C-f>"] = ({ editorInfo, rep, line, char, count }) => {
|
|
1135
|
+
const target = Math.min(line + fullPage * count, rep.lines.length() - 1);
|
|
1136
|
+
const targetLen = rep.lines.atIndex(target).text.length;
|
|
1137
|
+
moveBlockCursor(
|
|
1138
|
+
editorInfo,
|
|
1139
|
+
target,
|
|
1140
|
+
Math.min(char, Math.max(0, targetLen - 1)),
|
|
1141
|
+
);
|
|
1142
|
+
};
|
|
1143
|
+
|
|
1144
|
+
commands.normal["<C-b>"] = ({ editorInfo, rep, line, char, count }) => {
|
|
1145
|
+
const target = Math.max(line - fullPage * count, 0);
|
|
1146
|
+
const targetLen = rep.lines.atIndex(target).text.length;
|
|
1147
|
+
moveBlockCursor(
|
|
1148
|
+
editorInfo,
|
|
1149
|
+
target,
|
|
1150
|
+
Math.min(char, Math.max(0, targetLen - 1)),
|
|
1151
|
+
);
|
|
1152
|
+
};
|
|
1153
|
+
|
|
1081
1154
|
// --- Dispatch ---
|
|
1082
1155
|
|
|
1083
1156
|
const handleKey = (key, ctx) => {
|
|
@@ -1239,9 +1312,9 @@ exports.aceKeyEvent = (_hookName, { evt, rep, editorInfo }) => {
|
|
|
1239
1312
|
cursor: state.visualCursor,
|
|
1240
1313
|
mode: "visual-line",
|
|
1241
1314
|
};
|
|
1242
|
-
const
|
|
1315
|
+
const [vLine, vChar] = state.visualCursor;
|
|
1243
1316
|
state.mode = "normal";
|
|
1244
|
-
moveBlockCursor(editorInfo,
|
|
1317
|
+
moveBlockCursor(editorInfo, vLine, vChar);
|
|
1245
1318
|
} else if (state.mode === "visual-char") {
|
|
1246
1319
|
state.lastVisualSelection = {
|
|
1247
1320
|
anchor: state.visualAnchor,
|
|
@@ -1303,71 +1376,9 @@ exports.aceKeyEvent = (_hookName, { evt, rep, editorInfo }) => {
|
|
|
1303
1376
|
const ctx = { rep, editorInfo, line, char, lineText };
|
|
1304
1377
|
|
|
1305
1378
|
if (useCtrlKeys && evt.ctrlKey && state.mode === "normal") {
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
}
|
|
1310
|
-
const count = state.pendingCount !== null ? state.pendingCount : 1;
|
|
1311
|
-
|
|
1312
|
-
if (evt.key === "r") {
|
|
1313
|
-
editorInfo.ace_doUndoRedo("redo");
|
|
1314
|
-
state.pendingCount = null;
|
|
1315
|
-
state.pendingRegister = null;
|
|
1316
|
-
evt.preventDefault();
|
|
1317
|
-
return true;
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
const halfPage = 15;
|
|
1321
|
-
if (evt.key === "d") {
|
|
1322
|
-
const target = Math.min(line + halfPage * count, rep.lines.length() - 1);
|
|
1323
|
-
const targetLen = rep.lines.atIndex(target).text.length;
|
|
1324
|
-
moveBlockCursor(
|
|
1325
|
-
editorInfo,
|
|
1326
|
-
target,
|
|
1327
|
-
Math.min(char, Math.max(0, targetLen - 1)),
|
|
1328
|
-
);
|
|
1329
|
-
state.pendingCount = null;
|
|
1330
|
-
state.pendingRegister = null;
|
|
1331
|
-
evt.preventDefault();
|
|
1332
|
-
return true;
|
|
1333
|
-
}
|
|
1334
|
-
if (evt.key === "u") {
|
|
1335
|
-
const target = Math.max(line - halfPage * count, 0);
|
|
1336
|
-
const targetLen = rep.lines.atIndex(target).text.length;
|
|
1337
|
-
moveBlockCursor(
|
|
1338
|
-
editorInfo,
|
|
1339
|
-
target,
|
|
1340
|
-
Math.min(char, Math.max(0, targetLen - 1)),
|
|
1341
|
-
);
|
|
1342
|
-
state.pendingCount = null;
|
|
1343
|
-
state.pendingRegister = null;
|
|
1344
|
-
evt.preventDefault();
|
|
1345
|
-
return true;
|
|
1346
|
-
}
|
|
1347
|
-
const fullPage = halfPage * 2;
|
|
1348
|
-
if (evt.key === "f") {
|
|
1349
|
-
const target = Math.min(line + fullPage * count, rep.lines.length() - 1);
|
|
1350
|
-
const targetLen = rep.lines.atIndex(target).text.length;
|
|
1351
|
-
moveBlockCursor(
|
|
1352
|
-
editorInfo,
|
|
1353
|
-
target,
|
|
1354
|
-
Math.min(char, Math.max(0, targetLen - 1)),
|
|
1355
|
-
);
|
|
1356
|
-
state.pendingCount = null;
|
|
1357
|
-
state.pendingRegister = null;
|
|
1358
|
-
evt.preventDefault();
|
|
1359
|
-
return true;
|
|
1360
|
-
}
|
|
1361
|
-
if (evt.key === "b") {
|
|
1362
|
-
const target = Math.max(line - fullPage * count, 0);
|
|
1363
|
-
const targetLen = rep.lines.atIndex(target).text.length;
|
|
1364
|
-
moveBlockCursor(
|
|
1365
|
-
editorInfo,
|
|
1366
|
-
target,
|
|
1367
|
-
Math.min(char, Math.max(0, targetLen - 1)),
|
|
1368
|
-
);
|
|
1369
|
-
state.pendingCount = null;
|
|
1370
|
-
state.pendingRegister = null;
|
|
1379
|
+
const ctrlKey = "<C-" + evt.key + ">";
|
|
1380
|
+
if (commands.normal[ctrlKey]) {
|
|
1381
|
+
handleKey(ctrlKey, ctx);
|
|
1371
1382
|
evt.preventDefault();
|
|
1372
1383
|
return true;
|
|
1373
1384
|
}
|
|
@@ -0,0 +1,198 @@
|
|
|
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
|
+
_setVimEnabled: setVimEnabled,
|
|
19
|
+
_setUseCtrlKeys: setUseCtrlKeys,
|
|
20
|
+
aceKeyEvent,
|
|
21
|
+
} = require("./index.js");
|
|
22
|
+
|
|
23
|
+
const makeRep = (lines) => ({
|
|
24
|
+
lines: {
|
|
25
|
+
length: () => lines.length,
|
|
26
|
+
atIndex: (n) => ({ text: lines[n] }),
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const makeMockEditorInfo = () => {
|
|
31
|
+
const calls = [];
|
|
32
|
+
return {
|
|
33
|
+
editorInfo: {
|
|
34
|
+
ace_inCallStackIfNecessary: (_name, fn) => fn(),
|
|
35
|
+
ace_performSelectionChange: (start, end, _flag) => {
|
|
36
|
+
calls.push({ type: "select", start, end });
|
|
37
|
+
},
|
|
38
|
+
ace_updateBrowserSelectionFromRep: () => {},
|
|
39
|
+
ace_performDocumentReplaceRange: (start, end, newText) => {
|
|
40
|
+
calls.push({ type: "replace", start, end, newText });
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
calls,
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
const resetState = () => {
|
|
50
|
+
state.mode = "normal";
|
|
51
|
+
state.pendingKey = null;
|
|
52
|
+
state.pendingCount = null;
|
|
53
|
+
state.countBuffer = "";
|
|
54
|
+
state.register = null;
|
|
55
|
+
state.namedRegisters = {};
|
|
56
|
+
state.pendingRegister = null;
|
|
57
|
+
state.awaitingRegister = false;
|
|
58
|
+
state.marks = {};
|
|
59
|
+
state.lastCharSearch = null;
|
|
60
|
+
state.visualAnchor = null;
|
|
61
|
+
state.visualCursor = null;
|
|
62
|
+
state.editorDoc = null;
|
|
63
|
+
state.currentRep = null;
|
|
64
|
+
state.desiredColumn = null;
|
|
65
|
+
state.lastCommand = null;
|
|
66
|
+
state.searchMode = false;
|
|
67
|
+
state.searchBuffer = "";
|
|
68
|
+
state.searchDirection = null;
|
|
69
|
+
state.lastSearch = null;
|
|
70
|
+
state.lastVisualSelection = null;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
describe("insert mode commands", () => {
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
state.mode = "normal";
|
|
76
|
+
state.pendingKey = null;
|
|
77
|
+
state.pendingCount = null;
|
|
78
|
+
state.countBuffer = "";
|
|
79
|
+
state.register = null;
|
|
80
|
+
state.marks = {};
|
|
81
|
+
state.lastCharSearch = null;
|
|
82
|
+
state.visualAnchor = null;
|
|
83
|
+
state.visualCursor = null;
|
|
84
|
+
state.editorDoc = null;
|
|
85
|
+
state.currentRep = null;
|
|
86
|
+
state.desiredColumn = null;
|
|
87
|
+
state.lastCommand = null;
|
|
88
|
+
state.searchMode = false;
|
|
89
|
+
state.searchBuffer = "";
|
|
90
|
+
state.searchDirection = null;
|
|
91
|
+
state.lastSearch = null;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("i enters insert mode at cursor", () => {
|
|
95
|
+
const rep = makeRep(["hello"]);
|
|
96
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
97
|
+
|
|
98
|
+
const ctx = { rep, editorInfo, line: 0, char: 2, lineText: "hello" };
|
|
99
|
+
commands.normal["i"](ctx);
|
|
100
|
+
|
|
101
|
+
assert.equal(state.mode, "insert");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("a enters insert mode after cursor", () => {
|
|
105
|
+
const rep = makeRep(["hello"]);
|
|
106
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
107
|
+
|
|
108
|
+
const ctx = { rep, editorInfo, line: 0, char: 2, lineText: "hello" };
|
|
109
|
+
commands.normal["a"](ctx);
|
|
110
|
+
|
|
111
|
+
assert.equal(state.mode, "insert");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("A enters insert mode at end of line", () => {
|
|
115
|
+
const rep = makeRep(["hello"]);
|
|
116
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
117
|
+
|
|
118
|
+
const ctx = { rep, editorInfo, line: 0, char: 2, lineText: "hello" };
|
|
119
|
+
commands.normal["A"](ctx);
|
|
120
|
+
|
|
121
|
+
assert.equal(state.mode, "insert");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("I enters insert mode at first non-blank", () => {
|
|
125
|
+
const rep = makeRep([" hello"]);
|
|
126
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
127
|
+
|
|
128
|
+
const ctx = { rep, editorInfo, line: 0, char: 2, lineText: " hello" };
|
|
129
|
+
commands.normal["I"](ctx);
|
|
130
|
+
|
|
131
|
+
assert.equal(state.mode, "insert");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("o opens line below", () => {
|
|
135
|
+
const rep = makeRep(["hello"]);
|
|
136
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
137
|
+
|
|
138
|
+
const ctx = { rep, editorInfo, line: 0, char: 0, lineText: "hello" };
|
|
139
|
+
commands.normal["o"](ctx);
|
|
140
|
+
|
|
141
|
+
assert.equal(state.mode, "insert");
|
|
142
|
+
assert.equal(calls.length, 2);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("O opens line above", () => {
|
|
146
|
+
const rep = makeRep(["hello"]);
|
|
147
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
148
|
+
|
|
149
|
+
const ctx = { rep, editorInfo, line: 0, char: 0, lineText: "hello" };
|
|
150
|
+
commands.normal["O"](ctx);
|
|
151
|
+
|
|
152
|
+
assert.equal(state.mode, "insert");
|
|
153
|
+
assert.equal(calls.length, 2);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("s replaces character and enters insert", () => {
|
|
157
|
+
const rep = makeRep(["hello"]);
|
|
158
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
159
|
+
|
|
160
|
+
const ctx = {
|
|
161
|
+
rep,
|
|
162
|
+
editorInfo,
|
|
163
|
+
line: 0,
|
|
164
|
+
char: 1,
|
|
165
|
+
lineText: "hello",
|
|
166
|
+
count: 1,
|
|
167
|
+
};
|
|
168
|
+
commands.normal["s"](ctx);
|
|
169
|
+
|
|
170
|
+
assert.equal(state.mode, "insert");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("S replaces entire line and enters insert", () => {
|
|
174
|
+
const rep = makeRep(["hello"]);
|
|
175
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
176
|
+
|
|
177
|
+
const ctx = { rep, editorInfo, line: 0, char: 2, lineText: "hello" };
|
|
178
|
+
commands.normal["S"](ctx);
|
|
179
|
+
|
|
180
|
+
assert.equal(state.mode, "insert");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("C changes to end of line and enters insert", () => {
|
|
184
|
+
const rep = makeRep(["hello world"]);
|
|
185
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
186
|
+
|
|
187
|
+
const ctx = {
|
|
188
|
+
rep,
|
|
189
|
+
editorInfo,
|
|
190
|
+
line: 0,
|
|
191
|
+
char: 6,
|
|
192
|
+
lineText: "hello world",
|
|
193
|
+
};
|
|
194
|
+
commands.normal["C"](ctx);
|
|
195
|
+
|
|
196
|
+
assert.equal(state.mode, "insert");
|
|
197
|
+
});
|
|
198
|
+
});
|