ep_vim 0.11.0 → 0.12.1
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 -4
- package/package.json +6 -6
- package/static/js/index.js +122 -68
- package/static/js/index.test.js +291 -0
package/README.md
CHANGED
|
@@ -16,16 +16,18 @@ A vim-mode plugin for [Etherpad](https://etherpad.org/). Adds modal editing with
|
|
|
16
16
|
- **Bracket matching** — `%` jump to matching bracket
|
|
17
17
|
- **Text objects** — `iw`/`aw` (word), `i"`/`a"` and `i'`/`a'` (quotes), `i{`/`a{` etc. (brackets), `ip`/`ap` (paragraph), `is`/`as` (sentence)
|
|
18
18
|
- **Operators** — `d`, `c`, `y` with motion and text object combinations (`dw`, `ce`, `y$`, `ciw`, `da"`, `yi(`, etc.)
|
|
19
|
-
- **Line operations** — `dd`, `cc`, `yy`, `J` (join), `Y` (yank line)
|
|
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
22
|
- **Editing** — `i` `a` `A` `I` (insert/append), `x`, `r`, `s`, `S`, `C`, `o`, `O`, `~` (toggle case)
|
|
23
23
|
- **Marks** — `m{a-z}` to set, `'{a-z}` / `` `{a-z} `` to jump
|
|
24
|
-
- **Search** — `/` and `?` forward/backward, `n`/`N` repeat
|
|
24
|
+
- **Search** — `/` and `?` forward/backward, `n`/`N` repeat, `*`/`#` search word under cursor
|
|
25
|
+
- **Scrolling** — `zz`/`zt`/`zb` center/top/bottom, `Ctrl+d`/`Ctrl+u` half-page, `Ctrl+f`/`Ctrl+b` full-page (requires ctrl keys enabled)
|
|
26
|
+
- **Visual** — `v` char, `V` line, `gv` reselect last selection; `~` toggle case in visual
|
|
25
27
|
- **Repeat** — `.` repeat last command
|
|
26
28
|
- **Counts** — numeric prefixes work with motions and operators
|
|
27
|
-
- **Undo** — `u`
|
|
28
|
-
- **Toggle** — toolbar button to enable/disable vim mode, persisted in localStorage
|
|
29
|
+
- **Undo/redo** — `u` undo, `Ctrl+r` redo (requires ctrl keys enabled)
|
|
30
|
+
- **Toggle** — toolbar button to enable/disable vim mode, persisted in localStorage; settings panel for system clipboard and ctrl key behavior
|
|
29
31
|
|
|
30
32
|
## Differences from vi
|
|
31
33
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ep_vim",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "Vim-mode plugin for Etherpad with modal editing, motions, and operators",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Seth Rothschild",
|
|
@@ -24,14 +24,14 @@
|
|
|
24
24
|
"peerDependencies": {
|
|
25
25
|
"ep_etherpad-lite": ">=1.8.6"
|
|
26
26
|
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "npm run format && node --test static/js/vim-core.test.js static/js/index.test.js",
|
|
29
|
+
"format": "prettier -w static/js/*"
|
|
30
|
+
},
|
|
27
31
|
"engines": {
|
|
28
32
|
"node": ">=20.0.0"
|
|
29
33
|
},
|
|
30
34
|
"dependencies": {
|
|
31
35
|
"prettier": "^3.8.1"
|
|
32
|
-
},
|
|
33
|
-
"scripts": {
|
|
34
|
-
"test": "npm run format && node --test static/js/vim-core.test.js static/js/index.test.js",
|
|
35
|
-
"format": "prettier -w static/js/*"
|
|
36
36
|
}
|
|
37
|
-
}
|
|
37
|
+
}
|
package/static/js/index.js
CHANGED
|
@@ -54,6 +54,7 @@ const state = {
|
|
|
54
54
|
searchBuffer: "",
|
|
55
55
|
searchDirection: null,
|
|
56
56
|
lastSearch: null,
|
|
57
|
+
lastVisualSelection: null,
|
|
57
58
|
};
|
|
58
59
|
|
|
59
60
|
// --- Editor operations ---
|
|
@@ -539,6 +540,8 @@ registerMotion("L", (ctx) => {
|
|
|
539
540
|
};
|
|
540
541
|
});
|
|
541
542
|
|
|
543
|
+
// --- Char search motions ---
|
|
544
|
+
|
|
542
545
|
registerParamMotion("f", (key, ctx) => {
|
|
543
546
|
const pos = charSearchPos("f", ctx.lineText, ctx.char, key, ctx.count);
|
|
544
547
|
return pos !== -1 ? pos : null;
|
|
@@ -767,24 +770,32 @@ commands["visual-line"]["~"] = (ctx) => {
|
|
|
767
770
|
moveBlockCursor(ctx.editorInfo, start[0], start[1]);
|
|
768
771
|
};
|
|
769
772
|
|
|
770
|
-
|
|
773
|
+
commands["visual-char"]["i"] = () => {
|
|
774
|
+
state.pendingKey = "i";
|
|
775
|
+
};
|
|
771
776
|
|
|
772
|
-
commands
|
|
773
|
-
|
|
777
|
+
commands["visual-char"]["a"] = () => {
|
|
778
|
+
state.pendingKey = "a";
|
|
774
779
|
};
|
|
775
780
|
|
|
776
|
-
commands
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
const newCtx = { ...ctx, count };
|
|
783
|
-
commands[state.mode][key](newCtx);
|
|
784
|
-
}
|
|
781
|
+
commands["visual-line"]["i"] = () => {
|
|
782
|
+
state.pendingKey = "i";
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
commands["visual-line"]["a"] = () => {
|
|
786
|
+
state.pendingKey = "a";
|
|
785
787
|
};
|
|
786
788
|
|
|
787
|
-
|
|
789
|
+
commands.normal["gv"] = ({ editorInfo, rep }) => {
|
|
790
|
+
if (!state.lastVisualSelection) return;
|
|
791
|
+
const { anchor, cursor, mode } = state.lastVisualSelection;
|
|
792
|
+
state.visualAnchor = anchor;
|
|
793
|
+
state.visualCursor = cursor;
|
|
794
|
+
state.mode = mode;
|
|
795
|
+
updateVisualSelection(editorInfo, rep);
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
// --- Insert mode entry ---
|
|
788
799
|
|
|
789
800
|
commands.normal["i"] = ({ editorInfo, line, char }) => {
|
|
790
801
|
clearEmptyLineCursor();
|
|
@@ -810,22 +821,6 @@ commands.normal["I"] = ({ editorInfo, line, lineText }) => {
|
|
|
810
821
|
state.mode = "insert";
|
|
811
822
|
};
|
|
812
823
|
|
|
813
|
-
commands["visual-char"]["i"] = () => {
|
|
814
|
-
state.pendingKey = "i";
|
|
815
|
-
};
|
|
816
|
-
|
|
817
|
-
commands["visual-char"]["a"] = () => {
|
|
818
|
-
state.pendingKey = "a";
|
|
819
|
-
};
|
|
820
|
-
|
|
821
|
-
commands["visual-line"]["i"] = () => {
|
|
822
|
-
state.pendingKey = "i";
|
|
823
|
-
};
|
|
824
|
-
|
|
825
|
-
commands["visual-line"]["a"] = () => {
|
|
826
|
-
state.pendingKey = "a";
|
|
827
|
-
};
|
|
828
|
-
|
|
829
824
|
commands.normal["o"] = ({ editorInfo, line, lineText }) => {
|
|
830
825
|
clearEmptyLineCursor();
|
|
831
826
|
replaceRange(
|
|
@@ -845,7 +840,7 @@ commands.normal["O"] = ({ editorInfo, line }) => {
|
|
|
845
840
|
state.mode = "insert";
|
|
846
841
|
};
|
|
847
842
|
|
|
848
|
-
// ---
|
|
843
|
+
// --- Editing commands ---
|
|
849
844
|
|
|
850
845
|
commands.normal["r"] = () => {
|
|
851
846
|
state.pendingKey = "r";
|
|
@@ -995,6 +990,27 @@ commands.normal["S"] = ({ editorInfo, line, lineText }) => {
|
|
|
995
990
|
recordCommand("S", 1);
|
|
996
991
|
};
|
|
997
992
|
|
|
993
|
+
// --- Undo, redo, repeat ---
|
|
994
|
+
|
|
995
|
+
commands.normal["u"] = ({ editorInfo }) => {
|
|
996
|
+
editorInfo.ace_doUndoRedo("undo");
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
commands.normal["<C-r>"] = ({ editorInfo }) => {
|
|
1000
|
+
editorInfo.ace_doUndoRedo("redo");
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
commands.normal["."] = (ctx) => {
|
|
1004
|
+
if (!state.lastCommand) return;
|
|
1005
|
+
const { key, count, param } = state.lastCommand;
|
|
1006
|
+
if (param !== null && parameterized[key]) {
|
|
1007
|
+
parameterized[key](param, ctx);
|
|
1008
|
+
} else if (commands[state.mode] && commands[state.mode][key]) {
|
|
1009
|
+
const newCtx = { ...ctx, count };
|
|
1010
|
+
commands[state.mode][key](newCtx);
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
|
|
998
1014
|
// --- Search ---
|
|
999
1015
|
|
|
1000
1016
|
commands.normal["/"] = () => {
|
|
@@ -1050,12 +1066,69 @@ commands.normal["#"] = (ctx) => {
|
|
|
1050
1066
|
if (pos) moveBlockCursor(ctx.editorInfo, pos[0], pos[1]);
|
|
1051
1067
|
};
|
|
1052
1068
|
|
|
1069
|
+
// --- Scroll ---
|
|
1070
|
+
|
|
1053
1071
|
commands.normal["zz"] = ({ line }) => {
|
|
1054
1072
|
if (!state.editorDoc) return;
|
|
1055
1073
|
const lineDiv = state.editorDoc.body.querySelectorAll("div")[line];
|
|
1056
1074
|
if (lineDiv) lineDiv.scrollIntoView({ block: "center" });
|
|
1057
1075
|
};
|
|
1058
1076
|
|
|
1077
|
+
commands.normal["zt"] = ({ line }) => {
|
|
1078
|
+
if (!state.editorDoc) return;
|
|
1079
|
+
const lineDiv = state.editorDoc.body.querySelectorAll("div")[line];
|
|
1080
|
+
if (lineDiv) lineDiv.scrollIntoView({ block: "start" });
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
commands.normal["zb"] = ({ line }) => {
|
|
1084
|
+
if (!state.editorDoc) return;
|
|
1085
|
+
const lineDiv = state.editorDoc.body.querySelectorAll("div")[line];
|
|
1086
|
+
if (lineDiv) lineDiv.scrollIntoView({ block: "end" });
|
|
1087
|
+
};
|
|
1088
|
+
|
|
1089
|
+
const halfPage = 15;
|
|
1090
|
+
const fullPage = halfPage * 2;
|
|
1091
|
+
|
|
1092
|
+
commands.normal["<C-d>"] = ({ editorInfo, rep, line, char, count }) => {
|
|
1093
|
+
const target = Math.min(line + halfPage * count, rep.lines.length() - 1);
|
|
1094
|
+
const targetLen = rep.lines.atIndex(target).text.length;
|
|
1095
|
+
moveBlockCursor(
|
|
1096
|
+
editorInfo,
|
|
1097
|
+
target,
|
|
1098
|
+
Math.min(char, Math.max(0, targetLen - 1)),
|
|
1099
|
+
);
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
commands.normal["<C-u>"] = ({ editorInfo, rep, line, char, count }) => {
|
|
1103
|
+
const target = Math.max(line - halfPage * count, 0);
|
|
1104
|
+
const targetLen = rep.lines.atIndex(target).text.length;
|
|
1105
|
+
moveBlockCursor(
|
|
1106
|
+
editorInfo,
|
|
1107
|
+
target,
|
|
1108
|
+
Math.min(char, Math.max(0, targetLen - 1)),
|
|
1109
|
+
);
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
commands.normal["<C-f>"] = ({ editorInfo, rep, line, char, count }) => {
|
|
1113
|
+
const target = Math.min(line + fullPage * count, rep.lines.length() - 1);
|
|
1114
|
+
const targetLen = rep.lines.atIndex(target).text.length;
|
|
1115
|
+
moveBlockCursor(
|
|
1116
|
+
editorInfo,
|
|
1117
|
+
target,
|
|
1118
|
+
Math.min(char, Math.max(0, targetLen - 1)),
|
|
1119
|
+
);
|
|
1120
|
+
};
|
|
1121
|
+
|
|
1122
|
+
commands.normal["<C-b>"] = ({ editorInfo, rep, line, char, count }) => {
|
|
1123
|
+
const target = Math.max(line - fullPage * count, 0);
|
|
1124
|
+
const targetLen = rep.lines.atIndex(target).text.length;
|
|
1125
|
+
moveBlockCursor(
|
|
1126
|
+
editorInfo,
|
|
1127
|
+
target,
|
|
1128
|
+
Math.min(char, Math.max(0, targetLen - 1)),
|
|
1129
|
+
);
|
|
1130
|
+
};
|
|
1131
|
+
|
|
1059
1132
|
// --- Dispatch ---
|
|
1060
1133
|
|
|
1061
1134
|
const handleKey = (key, ctx) => {
|
|
@@ -1212,10 +1285,20 @@ exports.aceKeyEvent = (_hookName, { evt, rep, editorInfo }) => {
|
|
|
1212
1285
|
if (evt.key === "Escape") {
|
|
1213
1286
|
state.desiredColumn = null;
|
|
1214
1287
|
if (state.mode === "visual-line") {
|
|
1288
|
+
state.lastVisualSelection = {
|
|
1289
|
+
anchor: state.visualAnchor,
|
|
1290
|
+
cursor: state.visualCursor,
|
|
1291
|
+
mode: "visual-line",
|
|
1292
|
+
};
|
|
1215
1293
|
const line = Math.min(state.visualAnchor[0], state.visualCursor[0]);
|
|
1216
1294
|
state.mode = "normal";
|
|
1217
1295
|
moveBlockCursor(editorInfo, line, 0);
|
|
1218
1296
|
} else if (state.mode === "visual-char") {
|
|
1297
|
+
state.lastVisualSelection = {
|
|
1298
|
+
anchor: state.visualAnchor,
|
|
1299
|
+
cursor: state.visualCursor,
|
|
1300
|
+
mode: "visual-char",
|
|
1301
|
+
};
|
|
1219
1302
|
const [vLine, vChar] = state.visualCursor;
|
|
1220
1303
|
state.mode = "normal";
|
|
1221
1304
|
moveBlockCursor(editorInfo, vLine, vChar);
|
|
@@ -1271,44 +1354,9 @@ exports.aceKeyEvent = (_hookName, { evt, rep, editorInfo }) => {
|
|
|
1271
1354
|
const ctx = { rep, editorInfo, line, char, lineText };
|
|
1272
1355
|
|
|
1273
1356
|
if (useCtrlKeys && evt.ctrlKey && state.mode === "normal") {
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
}
|
|
1278
|
-
const count = state.pendingCount !== null ? state.pendingCount : 1;
|
|
1279
|
-
|
|
1280
|
-
if (evt.key === "r") {
|
|
1281
|
-
editorInfo.ace_doUndoRedo("redo");
|
|
1282
|
-
state.pendingCount = null;
|
|
1283
|
-
state.pendingRegister = null;
|
|
1284
|
-
evt.preventDefault();
|
|
1285
|
-
return true;
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
const halfPage = 15;
|
|
1289
|
-
if (evt.key === "d") {
|
|
1290
|
-
const target = Math.min(line + halfPage * count, rep.lines.length() - 1);
|
|
1291
|
-
const targetLen = rep.lines.atIndex(target).text.length;
|
|
1292
|
-
moveBlockCursor(
|
|
1293
|
-
editorInfo,
|
|
1294
|
-
target,
|
|
1295
|
-
Math.min(char, Math.max(0, targetLen - 1)),
|
|
1296
|
-
);
|
|
1297
|
-
state.pendingCount = null;
|
|
1298
|
-
state.pendingRegister = null;
|
|
1299
|
-
evt.preventDefault();
|
|
1300
|
-
return true;
|
|
1301
|
-
}
|
|
1302
|
-
if (evt.key === "u") {
|
|
1303
|
-
const target = Math.max(line - halfPage * count, 0);
|
|
1304
|
-
const targetLen = rep.lines.atIndex(target).text.length;
|
|
1305
|
-
moveBlockCursor(
|
|
1306
|
-
editorInfo,
|
|
1307
|
-
target,
|
|
1308
|
-
Math.min(char, Math.max(0, targetLen - 1)),
|
|
1309
|
-
);
|
|
1310
|
-
state.pendingCount = null;
|
|
1311
|
-
state.pendingRegister = null;
|
|
1357
|
+
const ctrlKey = "<C-" + evt.key + ">";
|
|
1358
|
+
if (commands.normal[ctrlKey]) {
|
|
1359
|
+
handleKey(ctrlKey, ctx);
|
|
1312
1360
|
evt.preventDefault();
|
|
1313
1361
|
return true;
|
|
1314
1362
|
}
|
|
@@ -1324,3 +1372,9 @@ exports._state = state;
|
|
|
1324
1372
|
exports._handleKey = handleKey;
|
|
1325
1373
|
exports._commands = commands;
|
|
1326
1374
|
exports._parameterized = parameterized;
|
|
1375
|
+
exports._setVimEnabled = (v) => {
|
|
1376
|
+
vimEnabled = v;
|
|
1377
|
+
};
|
|
1378
|
+
exports._setUseCtrlKeys = (v) => {
|
|
1379
|
+
useCtrlKeys = v;
|
|
1380
|
+
};
|
package/static/js/index.test.js
CHANGED
|
@@ -15,6 +15,9 @@ const {
|
|
|
15
15
|
_handleKey: handleKey,
|
|
16
16
|
_commands: commands,
|
|
17
17
|
_parameterized: parameterized,
|
|
18
|
+
_setVimEnabled: setVimEnabled,
|
|
19
|
+
_setUseCtrlKeys: setUseCtrlKeys,
|
|
20
|
+
aceKeyEvent,
|
|
18
21
|
} = require("./index.js");
|
|
19
22
|
|
|
20
23
|
const makeRep = (lines) => ({
|
|
@@ -2022,6 +2025,7 @@ const resetState = () => {
|
|
|
2022
2025
|
state.searchBuffer = "";
|
|
2023
2026
|
state.searchDirection = null;
|
|
2024
2027
|
state.lastSearch = null;
|
|
2028
|
+
state.lastVisualSelection = null;
|
|
2025
2029
|
};
|
|
2026
2030
|
|
|
2027
2031
|
describe("edge cases: cc with count", () => {
|
|
@@ -3730,3 +3734,290 @@ describe("missing feature: zz center screen", () => {
|
|
|
3730
3734
|
assert.doesNotThrow(() => commands.normal["zz"](ctx));
|
|
3731
3735
|
});
|
|
3732
3736
|
});
|
|
3737
|
+
|
|
3738
|
+
describe("zt and zb scroll commands", () => {
|
|
3739
|
+
beforeEach(resetState);
|
|
3740
|
+
|
|
3741
|
+
it("zt does nothing when editorDoc is null", () => {
|
|
3742
|
+
state.editorDoc = null;
|
|
3743
|
+
const rep = makeRep(["hello"]);
|
|
3744
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
3745
|
+
const ctx = {
|
|
3746
|
+
rep,
|
|
3747
|
+
editorInfo,
|
|
3748
|
+
line: 0,
|
|
3749
|
+
char: 0,
|
|
3750
|
+
lineText: "hello",
|
|
3751
|
+
count: 1,
|
|
3752
|
+
hasCount: false,
|
|
3753
|
+
};
|
|
3754
|
+
assert.doesNotThrow(() => commands.normal["zt"](ctx));
|
|
3755
|
+
});
|
|
3756
|
+
|
|
3757
|
+
it("zb does nothing when editorDoc is null", () => {
|
|
3758
|
+
state.editorDoc = null;
|
|
3759
|
+
const rep = makeRep(["hello"]);
|
|
3760
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
3761
|
+
const ctx = {
|
|
3762
|
+
rep,
|
|
3763
|
+
editorInfo,
|
|
3764
|
+
line: 0,
|
|
3765
|
+
char: 0,
|
|
3766
|
+
lineText: "hello",
|
|
3767
|
+
count: 1,
|
|
3768
|
+
hasCount: false,
|
|
3769
|
+
};
|
|
3770
|
+
assert.doesNotThrow(() => commands.normal["zb"](ctx));
|
|
3771
|
+
});
|
|
3772
|
+
|
|
3773
|
+
it("zt calls scrollIntoView with block: start", () => {
|
|
3774
|
+
const scrollCalls = [];
|
|
3775
|
+
const mockLineDiv = {
|
|
3776
|
+
scrollIntoView: (opts) => scrollCalls.push(opts),
|
|
3777
|
+
};
|
|
3778
|
+
state.editorDoc = {
|
|
3779
|
+
body: {
|
|
3780
|
+
querySelectorAll: () => [mockLineDiv],
|
|
3781
|
+
},
|
|
3782
|
+
};
|
|
3783
|
+
const rep = makeRep(["hello"]);
|
|
3784
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
3785
|
+
const ctx = {
|
|
3786
|
+
rep,
|
|
3787
|
+
editorInfo,
|
|
3788
|
+
line: 0,
|
|
3789
|
+
char: 0,
|
|
3790
|
+
lineText: "hello",
|
|
3791
|
+
count: 1,
|
|
3792
|
+
hasCount: false,
|
|
3793
|
+
};
|
|
3794
|
+
commands.normal["zt"](ctx);
|
|
3795
|
+
assert.equal(scrollCalls.length, 1);
|
|
3796
|
+
assert.deepEqual(scrollCalls[0], { block: "start" });
|
|
3797
|
+
});
|
|
3798
|
+
|
|
3799
|
+
it("zb calls scrollIntoView with block: end", () => {
|
|
3800
|
+
const scrollCalls = [];
|
|
3801
|
+
const mockLineDiv = {
|
|
3802
|
+
scrollIntoView: (opts) => scrollCalls.push(opts),
|
|
3803
|
+
};
|
|
3804
|
+
state.editorDoc = {
|
|
3805
|
+
body: {
|
|
3806
|
+
querySelectorAll: () => [mockLineDiv],
|
|
3807
|
+
},
|
|
3808
|
+
};
|
|
3809
|
+
const rep = makeRep(["hello"]);
|
|
3810
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
3811
|
+
const ctx = {
|
|
3812
|
+
rep,
|
|
3813
|
+
editorInfo,
|
|
3814
|
+
line: 0,
|
|
3815
|
+
char: 0,
|
|
3816
|
+
lineText: "hello",
|
|
3817
|
+
count: 1,
|
|
3818
|
+
hasCount: false,
|
|
3819
|
+
};
|
|
3820
|
+
commands.normal["zb"](ctx);
|
|
3821
|
+
assert.equal(scrollCalls.length, 1);
|
|
3822
|
+
assert.deepEqual(scrollCalls[0], { block: "end" });
|
|
3823
|
+
});
|
|
3824
|
+
});
|
|
3825
|
+
|
|
3826
|
+
describe("gv reselect last visual", () => {
|
|
3827
|
+
beforeEach(resetState);
|
|
3828
|
+
|
|
3829
|
+
it("does nothing when lastVisualSelection is null", () => {
|
|
3830
|
+
state.lastVisualSelection = null;
|
|
3831
|
+
const rep = makeRep(["hello world"]);
|
|
3832
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
3833
|
+
const ctx = {
|
|
3834
|
+
rep,
|
|
3835
|
+
editorInfo,
|
|
3836
|
+
line: 0,
|
|
3837
|
+
char: 0,
|
|
3838
|
+
lineText: "hello world",
|
|
3839
|
+
count: 1,
|
|
3840
|
+
hasCount: false,
|
|
3841
|
+
};
|
|
3842
|
+
commands.normal["gv"](ctx);
|
|
3843
|
+
assert.equal(calls.length, 0);
|
|
3844
|
+
assert.equal(state.mode, "normal");
|
|
3845
|
+
});
|
|
3846
|
+
|
|
3847
|
+
it("restores visual-char selection", () => {
|
|
3848
|
+
state.lastVisualSelection = {
|
|
3849
|
+
anchor: [0, 2],
|
|
3850
|
+
cursor: [0, 5],
|
|
3851
|
+
mode: "visual-char",
|
|
3852
|
+
};
|
|
3853
|
+
const rep = makeRep(["hello world"]);
|
|
3854
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
3855
|
+
const ctx = {
|
|
3856
|
+
rep,
|
|
3857
|
+
editorInfo,
|
|
3858
|
+
line: 0,
|
|
3859
|
+
char: 0,
|
|
3860
|
+
lineText: "hello world",
|
|
3861
|
+
count: 1,
|
|
3862
|
+
hasCount: false,
|
|
3863
|
+
};
|
|
3864
|
+
commands.normal["gv"](ctx);
|
|
3865
|
+
assert.equal(state.mode, "visual-char");
|
|
3866
|
+
assert.deepEqual(state.visualAnchor, [0, 2]);
|
|
3867
|
+
assert.deepEqual(state.visualCursor, [0, 5]);
|
|
3868
|
+
assert.ok(calls.length > 0, "expected a selection call");
|
|
3869
|
+
});
|
|
3870
|
+
|
|
3871
|
+
it("restores visual-line selection", () => {
|
|
3872
|
+
state.lastVisualSelection = {
|
|
3873
|
+
anchor: [1, 0],
|
|
3874
|
+
cursor: [2, 0],
|
|
3875
|
+
mode: "visual-line",
|
|
3876
|
+
};
|
|
3877
|
+
const rep = makeRep(["aaa", "bbb", "ccc"]);
|
|
3878
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
3879
|
+
const ctx = {
|
|
3880
|
+
rep,
|
|
3881
|
+
editorInfo,
|
|
3882
|
+
line: 0,
|
|
3883
|
+
char: 0,
|
|
3884
|
+
lineText: "aaa",
|
|
3885
|
+
count: 1,
|
|
3886
|
+
hasCount: false,
|
|
3887
|
+
};
|
|
3888
|
+
commands.normal["gv"](ctx);
|
|
3889
|
+
assert.equal(state.mode, "visual-line");
|
|
3890
|
+
assert.deepEqual(state.visualAnchor, [1, 0]);
|
|
3891
|
+
assert.deepEqual(state.visualCursor, [2, 0]);
|
|
3892
|
+
});
|
|
3893
|
+
|
|
3894
|
+
it("escape from visual-char saves lastVisualSelection", () => {
|
|
3895
|
+
state.mode = "visual-char";
|
|
3896
|
+
state.visualAnchor = [0, 1];
|
|
3897
|
+
state.visualCursor = [0, 4];
|
|
3898
|
+
state.editorDoc = null;
|
|
3899
|
+
const rep = makeRep(["hello world"]);
|
|
3900
|
+
rep.selStart = [0, 4];
|
|
3901
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
3902
|
+
const mockEvt = {
|
|
3903
|
+
type: "keydown",
|
|
3904
|
+
key: "Escape",
|
|
3905
|
+
ctrlKey: false,
|
|
3906
|
+
metaKey: false,
|
|
3907
|
+
target: { ownerDocument: null },
|
|
3908
|
+
preventDefault: () => {},
|
|
3909
|
+
};
|
|
3910
|
+
setVimEnabled(true);
|
|
3911
|
+
aceKeyEvent("aceKeyEvent", { evt: mockEvt, rep, editorInfo });
|
|
3912
|
+
setVimEnabled(false);
|
|
3913
|
+
assert.deepEqual(state.lastVisualSelection, {
|
|
3914
|
+
anchor: [0, 1],
|
|
3915
|
+
cursor: [0, 4],
|
|
3916
|
+
mode: "visual-char",
|
|
3917
|
+
});
|
|
3918
|
+
});
|
|
3919
|
+
|
|
3920
|
+
it("escape from visual-line saves lastVisualSelection", () => {
|
|
3921
|
+
state.mode = "visual-line";
|
|
3922
|
+
state.visualAnchor = [0, 0];
|
|
3923
|
+
state.visualCursor = [2, 0];
|
|
3924
|
+
state.editorDoc = null;
|
|
3925
|
+
const rep = makeRep(["aaa", "bbb", "ccc"]);
|
|
3926
|
+
rep.selStart = [2, 0];
|
|
3927
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
3928
|
+
const mockEvt = {
|
|
3929
|
+
type: "keydown",
|
|
3930
|
+
key: "Escape",
|
|
3931
|
+
ctrlKey: false,
|
|
3932
|
+
metaKey: false,
|
|
3933
|
+
target: { ownerDocument: null },
|
|
3934
|
+
preventDefault: () => {},
|
|
3935
|
+
};
|
|
3936
|
+
setVimEnabled(true);
|
|
3937
|
+
aceKeyEvent("aceKeyEvent", { evt: mockEvt, rep, editorInfo });
|
|
3938
|
+
setVimEnabled(false);
|
|
3939
|
+
assert.deepEqual(state.lastVisualSelection, {
|
|
3940
|
+
anchor: [0, 0],
|
|
3941
|
+
cursor: [2, 0],
|
|
3942
|
+
mode: "visual-line",
|
|
3943
|
+
});
|
|
3944
|
+
});
|
|
3945
|
+
});
|
|
3946
|
+
|
|
3947
|
+
describe("Ctrl+f and Ctrl+b page scroll", () => {
|
|
3948
|
+
beforeEach(() => {
|
|
3949
|
+
resetState();
|
|
3950
|
+
setVimEnabled(true);
|
|
3951
|
+
setUseCtrlKeys(true);
|
|
3952
|
+
});
|
|
3953
|
+
|
|
3954
|
+
const makeCtrlEvt = (key) => ({
|
|
3955
|
+
type: "keydown",
|
|
3956
|
+
key,
|
|
3957
|
+
ctrlKey: true,
|
|
3958
|
+
metaKey: false,
|
|
3959
|
+
target: { ownerDocument: null },
|
|
3960
|
+
preventDefault: () => {},
|
|
3961
|
+
});
|
|
3962
|
+
|
|
3963
|
+
it("Ctrl+f moves forward one full page", () => {
|
|
3964
|
+
const lines = Array.from({ length: 50 }, (_, i) => `line${i}`);
|
|
3965
|
+
const rep = makeRep(lines);
|
|
3966
|
+
rep.selStart = [0, 0];
|
|
3967
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
3968
|
+
aceKeyEvent("aceKeyEvent", {
|
|
3969
|
+
evt: makeCtrlEvt("f"),
|
|
3970
|
+
rep,
|
|
3971
|
+
editorInfo,
|
|
3972
|
+
});
|
|
3973
|
+
const selectCall = calls.find((c) => c.type === "select");
|
|
3974
|
+
assert.ok(selectCall, "expected a select call");
|
|
3975
|
+
assert.equal(selectCall.start[0], 30);
|
|
3976
|
+
});
|
|
3977
|
+
|
|
3978
|
+
it("Ctrl+b moves backward one full page", () => {
|
|
3979
|
+
const lines = Array.from({ length: 50 }, (_, i) => `line${i}`);
|
|
3980
|
+
const rep = makeRep(lines);
|
|
3981
|
+
rep.selStart = [40, 0];
|
|
3982
|
+
state.mode = "normal";
|
|
3983
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
3984
|
+
aceKeyEvent("aceKeyEvent", {
|
|
3985
|
+
evt: makeCtrlEvt("b"),
|
|
3986
|
+
rep,
|
|
3987
|
+
editorInfo,
|
|
3988
|
+
});
|
|
3989
|
+
const selectCall = calls.find((c) => c.type === "select");
|
|
3990
|
+
assert.ok(selectCall, "expected a select call");
|
|
3991
|
+
assert.equal(selectCall.start[0], 10);
|
|
3992
|
+
});
|
|
3993
|
+
|
|
3994
|
+
it("Ctrl+f clamps at end of document", () => {
|
|
3995
|
+
const lines = Array.from({ length: 10 }, (_, i) => `line${i}`);
|
|
3996
|
+
const rep = makeRep(lines);
|
|
3997
|
+
rep.selStart = [5, 0];
|
|
3998
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
3999
|
+
aceKeyEvent("aceKeyEvent", {
|
|
4000
|
+
evt: makeCtrlEvt("f"),
|
|
4001
|
+
rep,
|
|
4002
|
+
editorInfo,
|
|
4003
|
+
});
|
|
4004
|
+
const selectCall = calls.find((c) => c.type === "select");
|
|
4005
|
+
assert.ok(selectCall, "expected a select call");
|
|
4006
|
+
assert.equal(selectCall.start[0], 9);
|
|
4007
|
+
});
|
|
4008
|
+
|
|
4009
|
+
it("Ctrl+b clamps at start of document", () => {
|
|
4010
|
+
const lines = Array.from({ length: 10 }, (_, i) => `line${i}`);
|
|
4011
|
+
const rep = makeRep(lines);
|
|
4012
|
+
rep.selStart = [3, 0];
|
|
4013
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
4014
|
+
aceKeyEvent("aceKeyEvent", {
|
|
4015
|
+
evt: makeCtrlEvt("b"),
|
|
4016
|
+
rep,
|
|
4017
|
+
editorInfo,
|
|
4018
|
+
});
|
|
4019
|
+
const selectCall = calls.find((c) => c.type === "select");
|
|
4020
|
+
assert.ok(selectCall, "expected a select call");
|
|
4021
|
+
assert.equal(selectCall.start[0], 0);
|
|
4022
|
+
});
|
|
4023
|
+
});
|