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 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
@@ -9,7 +9,8 @@
9
9
  "postToolbarInit": "ep_vim/static/js/index"
10
10
  },
11
11
  "hooks": {
12
- "eejsBlock_editbarMenuLeft": "ep_vim/index"
12
+ "eejsBlock_editbarMenuLeft": "ep_vim/index",
13
+ "eejsBlock_mySettings": "ep_vim/index"
13
14
  }
14
15
  }
15
16
  ]
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.10.1",
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
+ }
@@ -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" || evt.key === "c" || evt.key === "v" || evt.key === "r");
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
+ };
@@ -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>