ep_vim 0.10.1 → 0.12.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 +6 -6
- package/ep.json +2 -1
- package/index.js +5 -0
- package/package.json +6 -6
- package/static/js/index.js +134 -2
- package/static/js/index.test.js +291 -0
- package/templates/settings.ejs +8 -0
package/README.md
CHANGED
|
@@ -16,21 +16,21 @@ 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
|
|
|
32
|
-
- **Ctrl key mapping** — browser shortcuts conflict with common vim bindings (`Ctrl+d`, `Ctrl+u`, `Ctrl+r`, etc.); These will be implemented under a configurable setting.
|
|
33
|
-
- **Clipboard toggle** — the default register writes to the system clipboard. We will add a setting to turn on the default vim behavior.
|
|
34
34
|
- **No command line, macros, or globals** - these are not planned, but PRs welcome.
|
|
35
35
|
|
|
36
36
|
## Installation
|
package/ep.json
CHANGED
package/index.js
CHANGED
|
@@ -6,3 +6,8 @@ exports.eejsBlock_editbarMenuLeft = (hook, args, cb) => {
|
|
|
6
6
|
args.content += eejs.require('ep_vim/templates/editbarButtons.ejs');
|
|
7
7
|
return cb();
|
|
8
8
|
};
|
|
9
|
+
|
|
10
|
+
exports.eejsBlock_mySettings = (_hook, args, cb) => {
|
|
11
|
+
args.content += eejs.require('ep_vim/templates/settings.ejs');
|
|
12
|
+
return cb();
|
|
13
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ep_vim",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
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
|
@@ -30,6 +30,9 @@ let vimEnabled =
|
|
|
30
30
|
typeof localStorage !== "undefined" &&
|
|
31
31
|
localStorage.getItem("ep_vimEnabled") === "true";
|
|
32
32
|
|
|
33
|
+
let useSystemClipboard = true;
|
|
34
|
+
let useCtrlKeys = true;
|
|
35
|
+
|
|
33
36
|
const state = {
|
|
34
37
|
mode: "normal",
|
|
35
38
|
pendingKey: null,
|
|
@@ -51,6 +54,7 @@ const state = {
|
|
|
51
54
|
searchBuffer: "",
|
|
52
55
|
searchDirection: null,
|
|
53
56
|
lastSearch: null,
|
|
57
|
+
lastVisualSelection: null,
|
|
54
58
|
};
|
|
55
59
|
|
|
56
60
|
// --- Editor operations ---
|
|
@@ -66,7 +70,7 @@ const setRegister = (value) => {
|
|
|
66
70
|
}
|
|
67
71
|
state.register = value;
|
|
68
72
|
const text = Array.isArray(value) ? value.join("\n") + "\n" : value;
|
|
69
|
-
if (navigator.clipboard) {
|
|
73
|
+
if (useSystemClipboard && navigator.clipboard) {
|
|
70
74
|
navigator.clipboard.writeText(text).catch(() => {});
|
|
71
75
|
}
|
|
72
76
|
};
|
|
@@ -764,6 +768,15 @@ commands["visual-line"]["~"] = (ctx) => {
|
|
|
764
768
|
moveBlockCursor(ctx.editorInfo, start[0], start[1]);
|
|
765
769
|
};
|
|
766
770
|
|
|
771
|
+
commands.normal["gv"] = ({ editorInfo, rep }) => {
|
|
772
|
+
if (!state.lastVisualSelection) return;
|
|
773
|
+
const { anchor, cursor, mode } = state.lastVisualSelection;
|
|
774
|
+
state.visualAnchor = anchor;
|
|
775
|
+
state.visualCursor = cursor;
|
|
776
|
+
state.mode = mode;
|
|
777
|
+
updateVisualSelection(editorInfo, rep);
|
|
778
|
+
};
|
|
779
|
+
|
|
767
780
|
// --- Miscellaneous ---
|
|
768
781
|
|
|
769
782
|
commands.normal["u"] = ({ editorInfo }) => {
|
|
@@ -1053,6 +1066,18 @@ commands.normal["zz"] = ({ line }) => {
|
|
|
1053
1066
|
if (lineDiv) lineDiv.scrollIntoView({ block: "center" });
|
|
1054
1067
|
};
|
|
1055
1068
|
|
|
1069
|
+
commands.normal["zt"] = ({ line }) => {
|
|
1070
|
+
if (!state.editorDoc) return;
|
|
1071
|
+
const lineDiv = state.editorDoc.body.querySelectorAll("div")[line];
|
|
1072
|
+
if (lineDiv) lineDiv.scrollIntoView({ block: "start" });
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1075
|
+
commands.normal["zb"] = ({ line }) => {
|
|
1076
|
+
if (!state.editorDoc) return;
|
|
1077
|
+
const lineDiv = state.editorDoc.body.querySelectorAll("div")[line];
|
|
1078
|
+
if (lineDiv) lineDiv.scrollIntoView({ block: "end" });
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1056
1081
|
// --- Dispatch ---
|
|
1057
1082
|
|
|
1058
1083
|
const handleKey = (key, ctx) => {
|
|
@@ -1162,6 +1187,22 @@ exports.postToolbarInit = (_hookName, _args) => {
|
|
|
1162
1187
|
localStorage.setItem("ep_vimEnabled", vimEnabled ? "true" : "false");
|
|
1163
1188
|
btn.classList.toggle("vim-enabled", vimEnabled);
|
|
1164
1189
|
});
|
|
1190
|
+
|
|
1191
|
+
const clipboardCheckbox = document.getElementById(
|
|
1192
|
+
"options-vim-use-system-clipboard",
|
|
1193
|
+
);
|
|
1194
|
+
if (!clipboardCheckbox) return;
|
|
1195
|
+
useSystemClipboard = clipboardCheckbox.checked;
|
|
1196
|
+
clipboardCheckbox.addEventListener("change", () => {
|
|
1197
|
+
useSystemClipboard = clipboardCheckbox.checked;
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
const ctrlKeysCheckbox = document.getElementById("options-vim-use-ctrl-keys");
|
|
1201
|
+
if (!ctrlKeysCheckbox) return;
|
|
1202
|
+
useCtrlKeys = ctrlKeysCheckbox.checked;
|
|
1203
|
+
ctrlKeysCheckbox.addEventListener("change", () => {
|
|
1204
|
+
useCtrlKeys = ctrlKeysCheckbox.checked;
|
|
1205
|
+
});
|
|
1165
1206
|
};
|
|
1166
1207
|
|
|
1167
1208
|
exports.postAceInit = (_hookName, { ace }) => {
|
|
@@ -1181,7 +1222,10 @@ exports.aceKeyEvent = (_hookName, { evt, rep, editorInfo }) => {
|
|
|
1181
1222
|
|
|
1182
1223
|
const isBrowserShortcut =
|
|
1183
1224
|
(evt.ctrlKey || evt.metaKey) &&
|
|
1184
|
-
(evt.key === "x" ||
|
|
1225
|
+
(evt.key === "x" ||
|
|
1226
|
+
evt.key === "c" ||
|
|
1227
|
+
evt.key === "v" ||
|
|
1228
|
+
(evt.key === "r" && !useCtrlKeys));
|
|
1185
1229
|
if (isBrowserShortcut) return false;
|
|
1186
1230
|
|
|
1187
1231
|
state.currentRep = rep;
|
|
@@ -1190,10 +1234,20 @@ exports.aceKeyEvent = (_hookName, { evt, rep, editorInfo }) => {
|
|
|
1190
1234
|
if (evt.key === "Escape") {
|
|
1191
1235
|
state.desiredColumn = null;
|
|
1192
1236
|
if (state.mode === "visual-line") {
|
|
1237
|
+
state.lastVisualSelection = {
|
|
1238
|
+
anchor: state.visualAnchor,
|
|
1239
|
+
cursor: state.visualCursor,
|
|
1240
|
+
mode: "visual-line",
|
|
1241
|
+
};
|
|
1193
1242
|
const line = Math.min(state.visualAnchor[0], state.visualCursor[0]);
|
|
1194
1243
|
state.mode = "normal";
|
|
1195
1244
|
moveBlockCursor(editorInfo, line, 0);
|
|
1196
1245
|
} else if (state.mode === "visual-char") {
|
|
1246
|
+
state.lastVisualSelection = {
|
|
1247
|
+
anchor: state.visualAnchor,
|
|
1248
|
+
cursor: state.visualCursor,
|
|
1249
|
+
mode: "visual-char",
|
|
1250
|
+
};
|
|
1197
1251
|
const [vLine, vChar] = state.visualCursor;
|
|
1198
1252
|
state.mode = "normal";
|
|
1199
1253
|
moveBlockCursor(editorInfo, vLine, vChar);
|
|
@@ -1247,6 +1301,78 @@ exports.aceKeyEvent = (_hookName, { evt, rep, editorInfo }) => {
|
|
|
1247
1301
|
: rep.selStart;
|
|
1248
1302
|
const lineText = rep.lines.atIndex(line).text;
|
|
1249
1303
|
const ctx = { rep, editorInfo, line, char, lineText };
|
|
1304
|
+
|
|
1305
|
+
if (useCtrlKeys && evt.ctrlKey && state.mode === "normal") {
|
|
1306
|
+
if (state.countBuffer !== "") {
|
|
1307
|
+
state.pendingCount = parseInt(state.countBuffer, 10);
|
|
1308
|
+
state.countBuffer = "";
|
|
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;
|
|
1371
|
+
evt.preventDefault();
|
|
1372
|
+
return true;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1250
1376
|
const handled = handleKey(evt.key, ctx);
|
|
1251
1377
|
if (handled) evt.preventDefault();
|
|
1252
1378
|
return handled;
|
|
@@ -1257,3 +1383,9 @@ exports._state = state;
|
|
|
1257
1383
|
exports._handleKey = handleKey;
|
|
1258
1384
|
exports._commands = commands;
|
|
1259
1385
|
exports._parameterized = parameterized;
|
|
1386
|
+
exports._setVimEnabled = (v) => {
|
|
1387
|
+
vimEnabled = v;
|
|
1388
|
+
};
|
|
1389
|
+
exports._setUseCtrlKeys = (v) => {
|
|
1390
|
+
useCtrlKeys = v;
|
|
1391
|
+
};
|
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
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<p>
|
|
2
|
+
<input type="checkbox" id="options-vim-use-system-clipboard" checked></input>
|
|
3
|
+
<label for="options-vim-use-system-clipboard">Use system clipboard (vim)</label>
|
|
4
|
+
</p>
|
|
5
|
+
<p>
|
|
6
|
+
<input type="checkbox" id="options-vim-use-ctrl-keys" checked></input>
|
|
7
|
+
<label for="options-vim-use-ctrl-keys">Use Ctrl keys (vim)</label>
|
|
8
|
+
</p>
|