ep_vim 0.12.1 → 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 +27 -5
- 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
|
@@ -704,9 +704,9 @@ commands.normal["v"] = ({ editorInfo, rep, line, char }) => {
|
|
|
704
704
|
updateVisualSelection(editorInfo, rep);
|
|
705
705
|
};
|
|
706
706
|
|
|
707
|
-
commands.normal["V"] = ({ editorInfo, rep, line }) => {
|
|
708
|
-
state.visualAnchor = [line,
|
|
709
|
-
state.visualCursor = [line,
|
|
707
|
+
commands.normal["V"] = ({ editorInfo, rep, line, char }) => {
|
|
708
|
+
state.visualAnchor = [line, char];
|
|
709
|
+
state.visualCursor = [line, char];
|
|
710
710
|
state.mode = "visual-line";
|
|
711
711
|
updateVisualSelection(editorInfo, rep);
|
|
712
712
|
};
|
|
@@ -786,6 +786,16 @@ commands["visual-line"]["a"] = () => {
|
|
|
786
786
|
state.pendingKey = "a";
|
|
787
787
|
};
|
|
788
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
|
+
|
|
789
799
|
commands.normal["gv"] = ({ editorInfo, rep }) => {
|
|
790
800
|
if (!state.lastVisualSelection) return;
|
|
791
801
|
const { anchor, cursor, mode } = state.lastVisualSelection;
|
|
@@ -869,6 +879,18 @@ commands.normal["x"] = ({ editorInfo, rep, line, char, lineText, count }) => {
|
|
|
869
879
|
}
|
|
870
880
|
};
|
|
871
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
|
+
|
|
872
894
|
commands.normal["p"] = ({ editorInfo, line, char, lineText, count }) => {
|
|
873
895
|
const reg = getActiveRegister();
|
|
874
896
|
if (reg !== null) {
|
|
@@ -1290,9 +1312,9 @@ exports.aceKeyEvent = (_hookName, { evt, rep, editorInfo }) => {
|
|
|
1290
1312
|
cursor: state.visualCursor,
|
|
1291
1313
|
mode: "visual-line",
|
|
1292
1314
|
};
|
|
1293
|
-
const
|
|
1315
|
+
const [vLine, vChar] = state.visualCursor;
|
|
1294
1316
|
state.mode = "normal";
|
|
1295
|
-
moveBlockCursor(editorInfo,
|
|
1317
|
+
moveBlockCursor(editorInfo, vLine, vChar);
|
|
1296
1318
|
} else if (state.mode === "visual-char") {
|
|
1297
1319
|
state.lastVisualSelection = {
|
|
1298
1320
|
anchor: state.visualAnchor,
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,363 @@
|
|
|
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("undo command", () => {
|
|
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("u calls undo", () => {
|
|
95
|
+
const rep = makeRep(["hello"]);
|
|
96
|
+
const { editorInfo: baseEditorInfo } = makeMockEditorInfo();
|
|
97
|
+
|
|
98
|
+
const editorInfo = {
|
|
99
|
+
...baseEditorInfo,
|
|
100
|
+
ace_doUndoRedo: () => {},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const ctx = { rep, editorInfo, line: 0, char: 0, lineText: "hello" };
|
|
104
|
+
commands.normal["u"](ctx);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("repeat command", () => {
|
|
109
|
+
beforeEach(() => {
|
|
110
|
+
state.mode = "normal";
|
|
111
|
+
state.pendingKey = null;
|
|
112
|
+
state.pendingCount = null;
|
|
113
|
+
state.countBuffer = "";
|
|
114
|
+
state.register = null;
|
|
115
|
+
state.marks = {};
|
|
116
|
+
state.lastCharSearch = null;
|
|
117
|
+
state.visualAnchor = null;
|
|
118
|
+
state.visualCursor = null;
|
|
119
|
+
state.editorDoc = null;
|
|
120
|
+
state.currentRep = null;
|
|
121
|
+
state.desiredColumn = null;
|
|
122
|
+
state.lastCommand = null;
|
|
123
|
+
state.searchMode = false;
|
|
124
|
+
state.searchBuffer = "";
|
|
125
|
+
state.searchDirection = null;
|
|
126
|
+
state.lastSearch = null;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it(". repeats last command", () => {
|
|
130
|
+
const rep = makeRep(["hello"]);
|
|
131
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
132
|
+
|
|
133
|
+
state.lastCommand = { key: "h", count: 1, param: null };
|
|
134
|
+
|
|
135
|
+
const ctx = {
|
|
136
|
+
rep,
|
|
137
|
+
editorInfo,
|
|
138
|
+
line: 0,
|
|
139
|
+
char: 3,
|
|
140
|
+
lineText: "hello",
|
|
141
|
+
};
|
|
142
|
+
commands.normal["."](ctx);
|
|
143
|
+
|
|
144
|
+
assert.equal(calls.length, 1);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it(". does nothing with no last command", () => {
|
|
148
|
+
const rep = makeRep(["hello"]);
|
|
149
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
150
|
+
|
|
151
|
+
state.lastCommand = null;
|
|
152
|
+
|
|
153
|
+
const ctx = { rep, editorInfo, line: 0, char: 0, lineText: "hello" };
|
|
154
|
+
commands.normal["."](ctx);
|
|
155
|
+
|
|
156
|
+
assert.equal(calls.length, 0);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it(". repeats parameterized command", () => {
|
|
160
|
+
const rep = makeRep(["hello world"]);
|
|
161
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
162
|
+
|
|
163
|
+
state.lastCommand = { key: "m", count: 1, param: "a" };
|
|
164
|
+
|
|
165
|
+
const ctx = {
|
|
166
|
+
rep,
|
|
167
|
+
editorInfo,
|
|
168
|
+
line: 0,
|
|
169
|
+
char: 2,
|
|
170
|
+
lineText: "hello world",
|
|
171
|
+
};
|
|
172
|
+
commands.normal["."](ctx);
|
|
173
|
+
|
|
174
|
+
assert.deepEqual(state.marks["a"], [0, 2]);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("missing feature: zz center screen", () => {
|
|
179
|
+
beforeEach(resetState);
|
|
180
|
+
|
|
181
|
+
it("zz does nothing when editorDoc is null", () => {
|
|
182
|
+
state.editorDoc = null;
|
|
183
|
+
const rep = makeRep(["hello"]);
|
|
184
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
185
|
+
const ctx = {
|
|
186
|
+
rep,
|
|
187
|
+
editorInfo,
|
|
188
|
+
line: 0,
|
|
189
|
+
char: 0,
|
|
190
|
+
lineText: "hello",
|
|
191
|
+
count: 1,
|
|
192
|
+
hasCount: false,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
assert.doesNotThrow(() => commands.normal["zz"](ctx));
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("zt and zb scroll commands", () => {
|
|
200
|
+
beforeEach(resetState);
|
|
201
|
+
|
|
202
|
+
it("zt does nothing when editorDoc is null", () => {
|
|
203
|
+
state.editorDoc = null;
|
|
204
|
+
const rep = makeRep(["hello"]);
|
|
205
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
206
|
+
const ctx = {
|
|
207
|
+
rep,
|
|
208
|
+
editorInfo,
|
|
209
|
+
line: 0,
|
|
210
|
+
char: 0,
|
|
211
|
+
lineText: "hello",
|
|
212
|
+
count: 1,
|
|
213
|
+
hasCount: false,
|
|
214
|
+
};
|
|
215
|
+
assert.doesNotThrow(() => commands.normal["zt"](ctx));
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("zb does nothing when editorDoc is null", () => {
|
|
219
|
+
state.editorDoc = null;
|
|
220
|
+
const rep = makeRep(["hello"]);
|
|
221
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
222
|
+
const ctx = {
|
|
223
|
+
rep,
|
|
224
|
+
editorInfo,
|
|
225
|
+
line: 0,
|
|
226
|
+
char: 0,
|
|
227
|
+
lineText: "hello",
|
|
228
|
+
count: 1,
|
|
229
|
+
hasCount: false,
|
|
230
|
+
};
|
|
231
|
+
assert.doesNotThrow(() => commands.normal["zb"](ctx));
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("zt calls scrollIntoView with block: start", () => {
|
|
235
|
+
const scrollCalls = [];
|
|
236
|
+
const mockLineDiv = {
|
|
237
|
+
scrollIntoView: (opts) => scrollCalls.push(opts),
|
|
238
|
+
};
|
|
239
|
+
state.editorDoc = {
|
|
240
|
+
body: {
|
|
241
|
+
querySelectorAll: () => [mockLineDiv],
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
const rep = makeRep(["hello"]);
|
|
245
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
246
|
+
const ctx = {
|
|
247
|
+
rep,
|
|
248
|
+
editorInfo,
|
|
249
|
+
line: 0,
|
|
250
|
+
char: 0,
|
|
251
|
+
lineText: "hello",
|
|
252
|
+
count: 1,
|
|
253
|
+
hasCount: false,
|
|
254
|
+
};
|
|
255
|
+
commands.normal["zt"](ctx);
|
|
256
|
+
assert.equal(scrollCalls.length, 1);
|
|
257
|
+
assert.deepEqual(scrollCalls[0], { block: "start" });
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("zb calls scrollIntoView with block: end", () => {
|
|
261
|
+
const scrollCalls = [];
|
|
262
|
+
const mockLineDiv = {
|
|
263
|
+
scrollIntoView: (opts) => scrollCalls.push(opts),
|
|
264
|
+
};
|
|
265
|
+
state.editorDoc = {
|
|
266
|
+
body: {
|
|
267
|
+
querySelectorAll: () => [mockLineDiv],
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
const rep = makeRep(["hello"]);
|
|
271
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
272
|
+
const ctx = {
|
|
273
|
+
rep,
|
|
274
|
+
editorInfo,
|
|
275
|
+
line: 0,
|
|
276
|
+
char: 0,
|
|
277
|
+
lineText: "hello",
|
|
278
|
+
count: 1,
|
|
279
|
+
hasCount: false,
|
|
280
|
+
};
|
|
281
|
+
commands.normal["zb"](ctx);
|
|
282
|
+
assert.equal(scrollCalls.length, 1);
|
|
283
|
+
assert.deepEqual(scrollCalls[0], { block: "end" });
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe("Ctrl+f and Ctrl+b page scroll", () => {
|
|
288
|
+
beforeEach(() => {
|
|
289
|
+
resetState();
|
|
290
|
+
setVimEnabled(true);
|
|
291
|
+
setUseCtrlKeys(true);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const makeCtrlEvt = (key) => ({
|
|
295
|
+
type: "keydown",
|
|
296
|
+
key,
|
|
297
|
+
ctrlKey: true,
|
|
298
|
+
metaKey: false,
|
|
299
|
+
target: { ownerDocument: null },
|
|
300
|
+
preventDefault: () => {},
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("Ctrl+f moves forward one full page", () => {
|
|
304
|
+
const lines = Array.from({ length: 50 }, (_, i) => `line${i}`);
|
|
305
|
+
const rep = makeRep(lines);
|
|
306
|
+
rep.selStart = [0, 0];
|
|
307
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
308
|
+
aceKeyEvent("aceKeyEvent", {
|
|
309
|
+
evt: makeCtrlEvt("f"),
|
|
310
|
+
rep,
|
|
311
|
+
editorInfo,
|
|
312
|
+
});
|
|
313
|
+
const selectCall = calls.find((c) => c.type === "select");
|
|
314
|
+
assert.ok(selectCall, "expected a select call");
|
|
315
|
+
assert.equal(selectCall.start[0], 30);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("Ctrl+b moves backward one full page", () => {
|
|
319
|
+
const lines = Array.from({ length: 50 }, (_, i) => `line${i}`);
|
|
320
|
+
const rep = makeRep(lines);
|
|
321
|
+
rep.selStart = [40, 0];
|
|
322
|
+
state.mode = "normal";
|
|
323
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
324
|
+
aceKeyEvent("aceKeyEvent", {
|
|
325
|
+
evt: makeCtrlEvt("b"),
|
|
326
|
+
rep,
|
|
327
|
+
editorInfo,
|
|
328
|
+
});
|
|
329
|
+
const selectCall = calls.find((c) => c.type === "select");
|
|
330
|
+
assert.ok(selectCall, "expected a select call");
|
|
331
|
+
assert.equal(selectCall.start[0], 10);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("Ctrl+f clamps at end of document", () => {
|
|
335
|
+
const lines = Array.from({ length: 10 }, (_, i) => `line${i}`);
|
|
336
|
+
const rep = makeRep(lines);
|
|
337
|
+
rep.selStart = [5, 0];
|
|
338
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
339
|
+
aceKeyEvent("aceKeyEvent", {
|
|
340
|
+
evt: makeCtrlEvt("f"),
|
|
341
|
+
rep,
|
|
342
|
+
editorInfo,
|
|
343
|
+
});
|
|
344
|
+
const selectCall = calls.find((c) => c.type === "select");
|
|
345
|
+
assert.ok(selectCall, "expected a select call");
|
|
346
|
+
assert.equal(selectCall.start[0], 9);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("Ctrl+b clamps at start of document", () => {
|
|
350
|
+
const lines = Array.from({ length: 10 }, (_, i) => `line${i}`);
|
|
351
|
+
const rep = makeRep(lines);
|
|
352
|
+
rep.selStart = [3, 0];
|
|
353
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
354
|
+
aceKeyEvent("aceKeyEvent", {
|
|
355
|
+
evt: makeCtrlEvt("b"),
|
|
356
|
+
rep,
|
|
357
|
+
editorInfo,
|
|
358
|
+
});
|
|
359
|
+
const selectCall = calls.find((c) => c.type === "select");
|
|
360
|
+
assert.ok(selectCall, "expected a select call");
|
|
361
|
+
assert.equal(selectCall.start[0], 0);
|
|
362
|
+
});
|
|
363
|
+
});
|