ep_vim 0.9.3 → 0.10.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 +11 -4
- package/package.json +6 -6
- package/static/js/index.js +88 -15
- package/static/js/index.test.js +651 -0
package/README.md
CHANGED
|
@@ -10,15 +10,16 @@ A vim-mode plugin for [Etherpad](https://etherpad.org/). Adds modal editing with
|
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
|
-
- **Modal editing** — normal, insert, and visual (char + line) modes
|
|
13
|
+
- **Modal editing** — normal, insert, and visual (char + line) modes; visual selections support all operators (`d`, `c`, `y`)
|
|
14
14
|
- **Motions** — `h` `j` `k` `l`, `w` `b` `e`, `0` `$` `^`, `gg` `G`, `f`/`F`/`t`/`T` char search, `{` `}` paragraph forward/backward, `H` `M` `L` viewport (top/middle/bottom)
|
|
15
15
|
- **Char search** — `f`/`F`/`t`/`T` find, `;` repeat last search, `,` reverse direction
|
|
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
19
|
- **Line operations** — `dd`, `cc`, `yy`, `J` (join), `Y` (yank line)
|
|
20
|
+
- **Registers** — `"a`–`"z` named registers for yank/delete/put, `"_` blackhole register
|
|
20
21
|
- **Put** — `p` / `P` with linewise and characterwise register handling
|
|
21
|
-
- **Editing** — `x`, `r`, `s`, `S`, `C`, `o`, `O`, `~` (toggle case)
|
|
22
|
+
- **Editing** — `i` `a` `A` `I` (insert/append), `x`, `r`, `s`, `S`, `C`, `o`, `O`, `~` (toggle case)
|
|
22
23
|
- **Marks** — `m{a-z}` to set, `'{a-z}` / `` `{a-z} `` to jump
|
|
23
24
|
- **Search** — `/` and `?` forward/backward, `n`/`N` repeat
|
|
24
25
|
- **Repeat** — `.` repeat last command
|
|
@@ -26,12 +27,18 @@ A vim-mode plugin for [Etherpad](https://etherpad.org/). Adds modal editing with
|
|
|
26
27
|
- **Undo** — `u`
|
|
27
28
|
- **Toggle** — toolbar button to enable/disable vim mode, persisted in localStorage
|
|
28
29
|
|
|
30
|
+
## Differences from vi
|
|
31
|
+
|
|
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
|
+
- **No command line, macros, or globals** - these are not planned, but PRs welcome.
|
|
35
|
+
|
|
29
36
|
## Installation
|
|
30
37
|
|
|
31
|
-
|
|
38
|
+
From your Etherpad directory run
|
|
32
39
|
|
|
33
40
|
```
|
|
34
|
-
pnpm install ep_vim
|
|
41
|
+
pnpm run plugins install ep_vim
|
|
35
42
|
```
|
|
36
43
|
|
|
37
44
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ep_vim",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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
|
-
},
|
|
31
27
|
"engines": {
|
|
32
28
|
"node": ">=20.0.0"
|
|
33
29
|
},
|
|
34
30
|
"dependencies": {
|
|
35
31
|
"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
|
@@ -36,6 +36,9 @@ const state = {
|
|
|
36
36
|
pendingCount: null,
|
|
37
37
|
countBuffer: "",
|
|
38
38
|
register: null,
|
|
39
|
+
namedRegisters: {},
|
|
40
|
+
pendingRegister: null,
|
|
41
|
+
awaitingRegister: false,
|
|
39
42
|
marks: {},
|
|
40
43
|
lastCharSearch: null,
|
|
41
44
|
visualAnchor: null,
|
|
@@ -53,6 +56,14 @@ const state = {
|
|
|
53
56
|
// --- Editor operations ---
|
|
54
57
|
|
|
55
58
|
const setRegister = (value) => {
|
|
59
|
+
if (state.pendingRegister === "_") {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (state.pendingRegister && /^[a-zA-Z]$/.test(state.pendingRegister)) {
|
|
63
|
+
const name = state.pendingRegister.toLowerCase();
|
|
64
|
+
state.namedRegisters[name] = value;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
56
67
|
state.register = value;
|
|
57
68
|
const text = Array.isArray(value) ? value.join("\n") + "\n" : value;
|
|
58
69
|
if (navigator.clipboard) {
|
|
@@ -60,6 +71,19 @@ const setRegister = (value) => {
|
|
|
60
71
|
}
|
|
61
72
|
};
|
|
62
73
|
|
|
74
|
+
const getActiveRegister = () => {
|
|
75
|
+
if (state.pendingRegister === "_") {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
if (state.pendingRegister && /^[a-zA-Z]$/.test(state.pendingRegister)) {
|
|
79
|
+
const name = state.pendingRegister.toLowerCase();
|
|
80
|
+
return state.namedRegisters[name] !== undefined
|
|
81
|
+
? state.namedRegisters[name]
|
|
82
|
+
: null;
|
|
83
|
+
}
|
|
84
|
+
return state.register;
|
|
85
|
+
};
|
|
86
|
+
|
|
63
87
|
const moveCursor = (editorInfo, line, char) => {
|
|
64
88
|
const pos = [line, char];
|
|
65
89
|
editorInfo.ace_inCallStackIfNecessary("vim-move", () => {
|
|
@@ -833,6 +857,7 @@ parameterized["r"] = (key, { editorInfo, line, char, lineText, count }) => {
|
|
|
833
857
|
|
|
834
858
|
commands.normal["Y"] = ({ rep, line }) => {
|
|
835
859
|
setRegister([getLineText(rep, line)]);
|
|
860
|
+
recordCommand("Y", 1);
|
|
836
861
|
};
|
|
837
862
|
|
|
838
863
|
commands.normal["x"] = ({ editorInfo, rep, line, char, lineText, count }) => {
|
|
@@ -847,14 +872,15 @@ commands.normal["x"] = ({ editorInfo, rep, line, char, lineText, count }) => {
|
|
|
847
872
|
};
|
|
848
873
|
|
|
849
874
|
commands.normal["p"] = ({ editorInfo, line, char, lineText, count }) => {
|
|
850
|
-
|
|
851
|
-
|
|
875
|
+
const reg = getActiveRegister();
|
|
876
|
+
if (reg !== null) {
|
|
877
|
+
if (typeof reg === "string") {
|
|
852
878
|
const insertPos = Math.min(char + 1, lineText.length);
|
|
853
|
-
const repeated =
|
|
879
|
+
const repeated = reg.repeat(count);
|
|
854
880
|
replaceRange(editorInfo, [line, insertPos], [line, insertPos], repeated);
|
|
855
|
-
moveBlockCursor(editorInfo, line, insertPos);
|
|
881
|
+
moveBlockCursor(editorInfo, line, insertPos + repeated.length - 1);
|
|
856
882
|
} else {
|
|
857
|
-
const block =
|
|
883
|
+
const block = reg.join("\n");
|
|
858
884
|
const parts = [];
|
|
859
885
|
for (let i = 0; i < count; i++) parts.push(block);
|
|
860
886
|
const insertText = "\n" + parts.join("\n");
|
|
@@ -864,25 +890,26 @@ commands.normal["p"] = ({ editorInfo, line, char, lineText, count }) => {
|
|
|
864
890
|
[line, lineText.length],
|
|
865
891
|
insertText,
|
|
866
892
|
);
|
|
867
|
-
moveBlockCursor(editorInfo, line + 1, 0);
|
|
893
|
+
moveBlockCursor(editorInfo, line + 1, firstNonBlank(reg[0]));
|
|
868
894
|
}
|
|
869
895
|
recordCommand("p", count);
|
|
870
896
|
}
|
|
871
897
|
};
|
|
872
898
|
|
|
873
899
|
commands.normal["P"] = ({ editorInfo, line, char, count }) => {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
900
|
+
const reg = getActiveRegister();
|
|
901
|
+
if (reg !== null) {
|
|
902
|
+
if (typeof reg === "string") {
|
|
903
|
+
const repeated = reg.repeat(count);
|
|
877
904
|
replaceRange(editorInfo, [line, char], [line, char], repeated);
|
|
878
|
-
moveBlockCursor(editorInfo, line, char);
|
|
905
|
+
moveBlockCursor(editorInfo, line, char + repeated.length - 1);
|
|
879
906
|
} else {
|
|
880
|
-
const block =
|
|
907
|
+
const block = reg.join("\n");
|
|
881
908
|
const parts = [];
|
|
882
909
|
for (let i = 0; i < count; i++) parts.push(block);
|
|
883
910
|
const insertText = parts.join("\n") + "\n";
|
|
884
911
|
replaceRange(editorInfo, [line, 0], [line, 0], insertText);
|
|
885
|
-
moveBlockCursor(editorInfo, line, 0);
|
|
912
|
+
moveBlockCursor(editorInfo, line, firstNonBlank(reg[0]));
|
|
886
913
|
}
|
|
887
914
|
recordCommand("P", count);
|
|
888
915
|
}
|
|
@@ -944,7 +971,7 @@ commands.normal["C"] = ({ editorInfo, line, char, lineText }) => {
|
|
|
944
971
|
|
|
945
972
|
commands.normal["s"] = ({ editorInfo, rep, line, char, lineText, count }) => {
|
|
946
973
|
clearEmptyLineCursor();
|
|
947
|
-
setRegister(lineText.slice(char, char +
|
|
974
|
+
setRegister(lineText.slice(char, Math.min(char + count, lineText.length)));
|
|
948
975
|
replaceRange(
|
|
949
976
|
editorInfo,
|
|
950
977
|
[line, char],
|
|
@@ -958,7 +985,7 @@ commands.normal["s"] = ({ editorInfo, rep, line, char, lineText, count }) => {
|
|
|
958
985
|
|
|
959
986
|
commands.normal["S"] = ({ editorInfo, line, lineText }) => {
|
|
960
987
|
clearEmptyLineCursor();
|
|
961
|
-
setRegister(lineText);
|
|
988
|
+
setRegister([lineText]);
|
|
962
989
|
replaceRange(editorInfo, [line, 0], [line, lineText.length], "");
|
|
963
990
|
moveCursor(editorInfo, line, 0);
|
|
964
991
|
state.mode = "insert";
|
|
@@ -995,9 +1022,50 @@ commands.normal["N"] = (ctx) => {
|
|
|
995
1022
|
if (pos) moveBlockCursor(ctx.editorInfo, pos[0], pos[1]);
|
|
996
1023
|
};
|
|
997
1024
|
|
|
1025
|
+
const getWordAt = (text, char) => {
|
|
1026
|
+
if (char >= text.length || !/\w/.test(text[char])) return null;
|
|
1027
|
+
let start = char;
|
|
1028
|
+
while (start > 0 && /\w/.test(text[start - 1])) start--;
|
|
1029
|
+
let end = char;
|
|
1030
|
+
while (end < text.length && /\w/.test(text[end])) end++;
|
|
1031
|
+
return text.slice(start, end);
|
|
1032
|
+
};
|
|
1033
|
+
|
|
1034
|
+
commands.normal["*"] = (ctx) => {
|
|
1035
|
+
const word = getWordAt(ctx.lineText, ctx.char);
|
|
1036
|
+
if (!word) return;
|
|
1037
|
+
state.lastSearch = { pattern: word, direction: "/" };
|
|
1038
|
+
const pos = searchForward(ctx.rep, ctx.line, ctx.char + 1, word, ctx.count);
|
|
1039
|
+
if (pos) moveBlockCursor(ctx.editorInfo, pos[0], pos[1]);
|
|
1040
|
+
};
|
|
1041
|
+
|
|
1042
|
+
commands.normal["#"] = (ctx) => {
|
|
1043
|
+
const word = getWordAt(ctx.lineText, ctx.char);
|
|
1044
|
+
if (!word) return;
|
|
1045
|
+
state.lastSearch = { pattern: word, direction: "?" };
|
|
1046
|
+
const pos = searchBackward(ctx.rep, ctx.line, ctx.char, word, ctx.count);
|
|
1047
|
+
if (pos) moveBlockCursor(ctx.editorInfo, pos[0], pos[1]);
|
|
1048
|
+
};
|
|
1049
|
+
|
|
1050
|
+
commands.normal["zz"] = ({ line }) => {
|
|
1051
|
+
if (!state.editorDoc) return;
|
|
1052
|
+
const lineDiv = state.editorDoc.body.querySelectorAll("div")[line];
|
|
1053
|
+
if (lineDiv) lineDiv.scrollIntoView({ block: "center" });
|
|
1054
|
+
};
|
|
1055
|
+
|
|
998
1056
|
// --- Dispatch ---
|
|
999
1057
|
|
|
1000
1058
|
const handleKey = (key, ctx) => {
|
|
1059
|
+
if (state.awaitingRegister) {
|
|
1060
|
+
state.pendingRegister = key;
|
|
1061
|
+
state.awaitingRegister = false;
|
|
1062
|
+
return true;
|
|
1063
|
+
}
|
|
1064
|
+
if (key === '"' && state.mode === "normal") {
|
|
1065
|
+
state.awaitingRegister = true;
|
|
1066
|
+
return true;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1001
1069
|
if (key >= "1" && key <= "9") {
|
|
1002
1070
|
state.countBuffer += key;
|
|
1003
1071
|
return true;
|
|
@@ -1019,6 +1087,7 @@ const handleKey = (key, ctx) => {
|
|
|
1019
1087
|
state.pendingKey = null;
|
|
1020
1088
|
handler(key, ctx);
|
|
1021
1089
|
state.pendingCount = null;
|
|
1090
|
+
state.pendingRegister = null;
|
|
1022
1091
|
return true;
|
|
1023
1092
|
}
|
|
1024
1093
|
|
|
@@ -1028,7 +1097,10 @@ const handleKey = (key, ctx) => {
|
|
|
1028
1097
|
if (map[seq]) {
|
|
1029
1098
|
state.pendingKey = null;
|
|
1030
1099
|
map[seq](ctx);
|
|
1031
|
-
if (state.pendingKey === null)
|
|
1100
|
+
if (state.pendingKey === null) {
|
|
1101
|
+
state.pendingCount = null;
|
|
1102
|
+
state.pendingRegister = null;
|
|
1103
|
+
}
|
|
1032
1104
|
return true;
|
|
1033
1105
|
}
|
|
1034
1106
|
|
|
@@ -1073,6 +1145,7 @@ const handleKey = (key, ctx) => {
|
|
|
1073
1145
|
|
|
1074
1146
|
state.pendingKey = null;
|
|
1075
1147
|
state.pendingCount = null;
|
|
1148
|
+
state.pendingRegister = null;
|
|
1076
1149
|
return true;
|
|
1077
1150
|
};
|
|
1078
1151
|
|
package/static/js/index.test.js
CHANGED
|
@@ -2007,6 +2007,9 @@ const resetState = () => {
|
|
|
2007
2007
|
state.pendingCount = null;
|
|
2008
2008
|
state.countBuffer = "";
|
|
2009
2009
|
state.register = null;
|
|
2010
|
+
state.namedRegisters = {};
|
|
2011
|
+
state.pendingRegister = null;
|
|
2012
|
+
state.awaitingRegister = false;
|
|
2010
2013
|
state.marks = {};
|
|
2011
2014
|
state.lastCharSearch = null;
|
|
2012
2015
|
state.visualAnchor = null;
|
|
@@ -3079,3 +3082,651 @@ describe("edge cases: df and dt (operator + char motion)", () => {
|
|
|
3079
3082
|
);
|
|
3080
3083
|
});
|
|
3081
3084
|
});
|
|
3085
|
+
|
|
3086
|
+
// --- Register bugs ---
|
|
3087
|
+
|
|
3088
|
+
describe("register bug: s ignores count", () => {
|
|
3089
|
+
beforeEach(resetState);
|
|
3090
|
+
|
|
3091
|
+
it("s with count=3 saves 3 chars to register", () => {
|
|
3092
|
+
const rep = makeRep(["abcdef"]);
|
|
3093
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
3094
|
+
|
|
3095
|
+
const ctx = {
|
|
3096
|
+
rep,
|
|
3097
|
+
editorInfo,
|
|
3098
|
+
line: 0,
|
|
3099
|
+
char: 1,
|
|
3100
|
+
lineText: "abcdef",
|
|
3101
|
+
count: 3,
|
|
3102
|
+
};
|
|
3103
|
+
commands.normal["s"](ctx);
|
|
3104
|
+
|
|
3105
|
+
assert.equal(state.register, "bcd", "3s should save 3 chars, not 1");
|
|
3106
|
+
});
|
|
3107
|
+
|
|
3108
|
+
it("s with count=1 saves 1 char to register", () => {
|
|
3109
|
+
const rep = makeRep(["abcdef"]);
|
|
3110
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
3111
|
+
|
|
3112
|
+
const ctx = {
|
|
3113
|
+
rep,
|
|
3114
|
+
editorInfo,
|
|
3115
|
+
line: 0,
|
|
3116
|
+
char: 2,
|
|
3117
|
+
lineText: "abcdef",
|
|
3118
|
+
count: 1,
|
|
3119
|
+
};
|
|
3120
|
+
commands.normal["s"](ctx);
|
|
3121
|
+
|
|
3122
|
+
assert.equal(state.register, "c");
|
|
3123
|
+
});
|
|
3124
|
+
|
|
3125
|
+
it("s with count exceeding line saves up to end of line", () => {
|
|
3126
|
+
const rep = makeRep(["abc"]);
|
|
3127
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
3128
|
+
|
|
3129
|
+
const ctx = {
|
|
3130
|
+
rep,
|
|
3131
|
+
editorInfo,
|
|
3132
|
+
line: 0,
|
|
3133
|
+
char: 1,
|
|
3134
|
+
lineText: "abc",
|
|
3135
|
+
count: 10,
|
|
3136
|
+
};
|
|
3137
|
+
commands.normal["s"](ctx);
|
|
3138
|
+
|
|
3139
|
+
assert.equal(
|
|
3140
|
+
state.register,
|
|
3141
|
+
"bc",
|
|
3142
|
+
"s with large count should save remaining chars",
|
|
3143
|
+
);
|
|
3144
|
+
});
|
|
3145
|
+
});
|
|
3146
|
+
|
|
3147
|
+
describe("register bug: S uses char-wise register instead of line-wise", () => {
|
|
3148
|
+
beforeEach(resetState);
|
|
3149
|
+
|
|
3150
|
+
it("S saves register as array (line-wise)", () => {
|
|
3151
|
+
const rep = makeRep(["hello world"]);
|
|
3152
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
3153
|
+
|
|
3154
|
+
const ctx = {
|
|
3155
|
+
rep,
|
|
3156
|
+
editorInfo,
|
|
3157
|
+
line: 0,
|
|
3158
|
+
char: 0,
|
|
3159
|
+
lineText: "hello world",
|
|
3160
|
+
count: 1,
|
|
3161
|
+
};
|
|
3162
|
+
commands.normal["S"](ctx);
|
|
3163
|
+
|
|
3164
|
+
assert.ok(
|
|
3165
|
+
Array.isArray(state.register),
|
|
3166
|
+
"S should store register as array (line-wise), not string",
|
|
3167
|
+
);
|
|
3168
|
+
assert.deepEqual(state.register, ["hello world"]);
|
|
3169
|
+
});
|
|
3170
|
+
|
|
3171
|
+
it("S register is line-wise so p pastes on new line", () => {
|
|
3172
|
+
const rep = makeRep(["hello"]);
|
|
3173
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
3174
|
+
|
|
3175
|
+
const ctx = {
|
|
3176
|
+
rep,
|
|
3177
|
+
editorInfo,
|
|
3178
|
+
line: 0,
|
|
3179
|
+
char: 0,
|
|
3180
|
+
lineText: "hello",
|
|
3181
|
+
count: 1,
|
|
3182
|
+
};
|
|
3183
|
+
commands.normal["S"](ctx);
|
|
3184
|
+
|
|
3185
|
+
assert.ok(
|
|
3186
|
+
Array.isArray(state.register),
|
|
3187
|
+
"register should be line-wise after S",
|
|
3188
|
+
);
|
|
3189
|
+
|
|
3190
|
+
const replaces = calls.filter((c) => c.type === "replace");
|
|
3191
|
+
assert.ok(replaces.length > 0);
|
|
3192
|
+
assert.equal(replaces[0].newText, "", "S should clear the line");
|
|
3193
|
+
});
|
|
3194
|
+
});
|
|
3195
|
+
|
|
3196
|
+
describe("register bug: p cursor at start of paste instead of end", () => {
|
|
3197
|
+
beforeEach(resetState);
|
|
3198
|
+
|
|
3199
|
+
it("p char-wise: cursor lands at last char of pasted text", () => {
|
|
3200
|
+
const rep = makeRep(["hello"]);
|
|
3201
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
3202
|
+
|
|
3203
|
+
state.register = "abc";
|
|
3204
|
+
const ctx = {
|
|
3205
|
+
rep,
|
|
3206
|
+
editorInfo,
|
|
3207
|
+
line: 0,
|
|
3208
|
+
char: 0,
|
|
3209
|
+
lineText: "hello",
|
|
3210
|
+
count: 1,
|
|
3211
|
+
};
|
|
3212
|
+
commands.normal["p"](ctx);
|
|
3213
|
+
|
|
3214
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
3215
|
+
const lastSelect = selects[selects.length - 1];
|
|
3216
|
+
assert.deepEqual(
|
|
3217
|
+
lastSelect.start,
|
|
3218
|
+
[0, 3],
|
|
3219
|
+
"cursor after p should be at last char of pasted text (insertPos + length - 1 = 1+3-1 = 3)",
|
|
3220
|
+
);
|
|
3221
|
+
});
|
|
3222
|
+
|
|
3223
|
+
it("p char-wise with count: cursor lands at last char of all pasted text", () => {
|
|
3224
|
+
const rep = makeRep(["hello"]);
|
|
3225
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
3226
|
+
|
|
3227
|
+
state.register = "ab";
|
|
3228
|
+
const ctx = {
|
|
3229
|
+
rep,
|
|
3230
|
+
editorInfo,
|
|
3231
|
+
line: 0,
|
|
3232
|
+
char: 0,
|
|
3233
|
+
lineText: "hello",
|
|
3234
|
+
count: 3,
|
|
3235
|
+
};
|
|
3236
|
+
commands.normal["p"](ctx);
|
|
3237
|
+
|
|
3238
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
3239
|
+
const lastSelect = selects[selects.length - 1];
|
|
3240
|
+
assert.deepEqual(
|
|
3241
|
+
lastSelect.start,
|
|
3242
|
+
[0, 6],
|
|
3243
|
+
"cursor after 3p 'ab': insertPos=1, repeated='ababab' length=6, cursor=1+6-1=6",
|
|
3244
|
+
);
|
|
3245
|
+
});
|
|
3246
|
+
});
|
|
3247
|
+
|
|
3248
|
+
describe("register bug: P cursor at start of paste instead of end", () => {
|
|
3249
|
+
beforeEach(resetState);
|
|
3250
|
+
|
|
3251
|
+
it("P char-wise: cursor lands at last char of pasted text", () => {
|
|
3252
|
+
const rep = makeRep(["hello"]);
|
|
3253
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
3254
|
+
|
|
3255
|
+
state.register = "abc";
|
|
3256
|
+
const ctx = {
|
|
3257
|
+
rep,
|
|
3258
|
+
editorInfo,
|
|
3259
|
+
line: 0,
|
|
3260
|
+
char: 2,
|
|
3261
|
+
lineText: "hello",
|
|
3262
|
+
count: 1,
|
|
3263
|
+
};
|
|
3264
|
+
commands.normal["P"](ctx);
|
|
3265
|
+
|
|
3266
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
3267
|
+
const lastSelect = selects[selects.length - 1];
|
|
3268
|
+
assert.deepEqual(
|
|
3269
|
+
lastSelect.start,
|
|
3270
|
+
[0, 4],
|
|
3271
|
+
"cursor after P should be at last char of pasted text (char + length - 1 = 2+3-1 = 4)",
|
|
3272
|
+
);
|
|
3273
|
+
});
|
|
3274
|
+
|
|
3275
|
+
it("P char-wise with count: cursor lands at last char of all pasted text", () => {
|
|
3276
|
+
const rep = makeRep(["hello"]);
|
|
3277
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
3278
|
+
|
|
3279
|
+
state.register = "ab";
|
|
3280
|
+
const ctx = {
|
|
3281
|
+
rep,
|
|
3282
|
+
editorInfo,
|
|
3283
|
+
line: 0,
|
|
3284
|
+
char: 1,
|
|
3285
|
+
lineText: "hello",
|
|
3286
|
+
count: 2,
|
|
3287
|
+
};
|
|
3288
|
+
commands.normal["P"](ctx);
|
|
3289
|
+
|
|
3290
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
3291
|
+
const lastSelect = selects[selects.length - 1];
|
|
3292
|
+
assert.deepEqual(
|
|
3293
|
+
lastSelect.start,
|
|
3294
|
+
[0, 4],
|
|
3295
|
+
"cursor after 2P 'ab' should be at char + 4 - 1 = 1+4-1 = 4",
|
|
3296
|
+
);
|
|
3297
|
+
});
|
|
3298
|
+
});
|
|
3299
|
+
|
|
3300
|
+
describe("register bug: line-wise p cursor at col 0 instead of first non-blank", () => {
|
|
3301
|
+
beforeEach(resetState);
|
|
3302
|
+
|
|
3303
|
+
it("p line-wise: cursor lands on first non-blank of pasted line", () => {
|
|
3304
|
+
const rep = makeRep(["hello", "world"]);
|
|
3305
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
3306
|
+
|
|
3307
|
+
state.register = [" indented"];
|
|
3308
|
+
const ctx = {
|
|
3309
|
+
rep,
|
|
3310
|
+
editorInfo,
|
|
3311
|
+
line: 0,
|
|
3312
|
+
char: 0,
|
|
3313
|
+
lineText: "hello",
|
|
3314
|
+
count: 1,
|
|
3315
|
+
};
|
|
3316
|
+
commands.normal["p"](ctx);
|
|
3317
|
+
|
|
3318
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
3319
|
+
const lastSelect = selects[selects.length - 1];
|
|
3320
|
+
assert.deepEqual(
|
|
3321
|
+
lastSelect.start,
|
|
3322
|
+
[1, 2],
|
|
3323
|
+
"p line-wise cursor should be at first non-blank (col 2) of pasted line",
|
|
3324
|
+
);
|
|
3325
|
+
});
|
|
3326
|
+
|
|
3327
|
+
it("p line-wise with unindented content: cursor at col 0", () => {
|
|
3328
|
+
const rep = makeRep(["hello", "world"]);
|
|
3329
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
3330
|
+
|
|
3331
|
+
state.register = ["noindent"];
|
|
3332
|
+
const ctx = {
|
|
3333
|
+
rep,
|
|
3334
|
+
editorInfo,
|
|
3335
|
+
line: 0,
|
|
3336
|
+
char: 0,
|
|
3337
|
+
lineText: "hello",
|
|
3338
|
+
count: 1,
|
|
3339
|
+
};
|
|
3340
|
+
commands.normal["p"](ctx);
|
|
3341
|
+
|
|
3342
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
3343
|
+
const lastSelect = selects[selects.length - 1];
|
|
3344
|
+
assert.deepEqual(lastSelect.start, [1, 0]);
|
|
3345
|
+
});
|
|
3346
|
+
});
|
|
3347
|
+
|
|
3348
|
+
describe("register bug: line-wise P cursor at col 0 instead of first non-blank", () => {
|
|
3349
|
+
beforeEach(resetState);
|
|
3350
|
+
|
|
3351
|
+
it("P line-wise: cursor lands on first non-blank of pasted line", () => {
|
|
3352
|
+
const rep = makeRep(["hello", "world"]);
|
|
3353
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
3354
|
+
|
|
3355
|
+
state.register = [" indented"];
|
|
3356
|
+
const ctx = {
|
|
3357
|
+
rep,
|
|
3358
|
+
editorInfo,
|
|
3359
|
+
line: 1,
|
|
3360
|
+
char: 0,
|
|
3361
|
+
lineText: "world",
|
|
3362
|
+
count: 1,
|
|
3363
|
+
};
|
|
3364
|
+
commands.normal["P"](ctx);
|
|
3365
|
+
|
|
3366
|
+
const selects = calls.filter((c) => c.type === "select");
|
|
3367
|
+
const lastSelect = selects[selects.length - 1];
|
|
3368
|
+
assert.deepEqual(
|
|
3369
|
+
lastSelect.start,
|
|
3370
|
+
[1, 2],
|
|
3371
|
+
"P line-wise cursor should be at first non-blank (col 2) of pasted line",
|
|
3372
|
+
);
|
|
3373
|
+
});
|
|
3374
|
+
});
|
|
3375
|
+
|
|
3376
|
+
describe("register bug: Y missing recordCommand", () => {
|
|
3377
|
+
beforeEach(resetState);
|
|
3378
|
+
|
|
3379
|
+
it("Y sets lastCommand so dot can repeat it", () => {
|
|
3380
|
+
const rep = makeRep(["hello"]);
|
|
3381
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
3382
|
+
|
|
3383
|
+
const ctx = {
|
|
3384
|
+
rep,
|
|
3385
|
+
editorInfo,
|
|
3386
|
+
line: 0,
|
|
3387
|
+
char: 0,
|
|
3388
|
+
lineText: "hello",
|
|
3389
|
+
count: 1,
|
|
3390
|
+
};
|
|
3391
|
+
commands.normal["Y"](ctx);
|
|
3392
|
+
|
|
3393
|
+
assert.notEqual(
|
|
3394
|
+
state.lastCommand,
|
|
3395
|
+
null,
|
|
3396
|
+
"Y should record command for dot-repeat",
|
|
3397
|
+
);
|
|
3398
|
+
assert.equal(state.lastCommand.key, "Y");
|
|
3399
|
+
});
|
|
3400
|
+
|
|
3401
|
+
it("Y does not corrupt lastCommand of a prior operation", () => {
|
|
3402
|
+
const rep = makeRep(["hello"]);
|
|
3403
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
3404
|
+
|
|
3405
|
+
state.lastCommand = { key: "dd", count: 1, param: null };
|
|
3406
|
+
const ctx = {
|
|
3407
|
+
rep,
|
|
3408
|
+
editorInfo,
|
|
3409
|
+
line: 0,
|
|
3410
|
+
char: 0,
|
|
3411
|
+
lineText: "hello",
|
|
3412
|
+
count: 1,
|
|
3413
|
+
};
|
|
3414
|
+
commands.normal["Y"](ctx);
|
|
3415
|
+
|
|
3416
|
+
assert.equal(
|
|
3417
|
+
state.lastCommand.key,
|
|
3418
|
+
"Y",
|
|
3419
|
+
"Y should overwrite lastCommand with its own entry",
|
|
3420
|
+
);
|
|
3421
|
+
});
|
|
3422
|
+
});
|
|
3423
|
+
|
|
3424
|
+
describe("missing feature: named registers", () => {
|
|
3425
|
+
beforeEach(resetState);
|
|
3426
|
+
|
|
3427
|
+
it('"ayy yanks into named register a', () => {
|
|
3428
|
+
// In vim, "a followed by yy copies the line into register 'a'.
|
|
3429
|
+
// This requires parsing the " prefix in handleKey and routing
|
|
3430
|
+
// setRegister calls to the named register slot.
|
|
3431
|
+
const rep = makeRep(["hello"]);
|
|
3432
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
3433
|
+
const ctx = {
|
|
3434
|
+
rep,
|
|
3435
|
+
editorInfo,
|
|
3436
|
+
line: 0,
|
|
3437
|
+
char: 0,
|
|
3438
|
+
lineText: "hello",
|
|
3439
|
+
count: 1,
|
|
3440
|
+
hasCount: false,
|
|
3441
|
+
};
|
|
3442
|
+
|
|
3443
|
+
// Simulate: " -> a -> yy
|
|
3444
|
+
handleKey('"', ctx);
|
|
3445
|
+
handleKey("a", ctx);
|
|
3446
|
+
handleKey("y", ctx);
|
|
3447
|
+
handleKey("y", ctx);
|
|
3448
|
+
|
|
3449
|
+
assert.ok(
|
|
3450
|
+
state.namedRegisters && state.namedRegisters["a"],
|
|
3451
|
+
"register a should be set",
|
|
3452
|
+
);
|
|
3453
|
+
assert.deepEqual(state.namedRegisters["a"], ["hello"]);
|
|
3454
|
+
});
|
|
3455
|
+
|
|
3456
|
+
it('"ap pastes from named register a', () => {
|
|
3457
|
+
state.namedRegisters = { a: ["yanked line"] };
|
|
3458
|
+
const rep = makeRep(["hello"]);
|
|
3459
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
3460
|
+
const ctx = {
|
|
3461
|
+
rep,
|
|
3462
|
+
editorInfo,
|
|
3463
|
+
line: 0,
|
|
3464
|
+
char: 0,
|
|
3465
|
+
lineText: "hello",
|
|
3466
|
+
count: 1,
|
|
3467
|
+
hasCount: false,
|
|
3468
|
+
};
|
|
3469
|
+
|
|
3470
|
+
// Simulate: " -> a -> p
|
|
3471
|
+
handleKey('"', ctx);
|
|
3472
|
+
handleKey("a", ctx);
|
|
3473
|
+
handleKey("p", ctx);
|
|
3474
|
+
|
|
3475
|
+
const replaces = calls.filter((c) => c.type === "replace");
|
|
3476
|
+
assert.ok(replaces.length > 0, "should paste from named register");
|
|
3477
|
+
});
|
|
3478
|
+
|
|
3479
|
+
it('"add deletes line into named register a', () => {
|
|
3480
|
+
const rep = makeRep(["hello", "world"]);
|
|
3481
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
3482
|
+
const ctx = {
|
|
3483
|
+
rep,
|
|
3484
|
+
editorInfo,
|
|
3485
|
+
line: 0,
|
|
3486
|
+
char: 0,
|
|
3487
|
+
lineText: "hello",
|
|
3488
|
+
count: 1,
|
|
3489
|
+
hasCount: false,
|
|
3490
|
+
};
|
|
3491
|
+
|
|
3492
|
+
handleKey('"', ctx);
|
|
3493
|
+
handleKey("a", ctx);
|
|
3494
|
+
handleKey("d", ctx);
|
|
3495
|
+
handleKey("d", ctx);
|
|
3496
|
+
|
|
3497
|
+
assert.ok(
|
|
3498
|
+
state.namedRegisters && state.namedRegisters["a"],
|
|
3499
|
+
"register a should be set after delete",
|
|
3500
|
+
);
|
|
3501
|
+
assert.deepEqual(state.namedRegisters["a"], ["hello"]);
|
|
3502
|
+
assert.equal(state.register, null, "anonymous register should not be set");
|
|
3503
|
+
});
|
|
3504
|
+
|
|
3505
|
+
it('"ayw yanks word into named register a', () => {
|
|
3506
|
+
const rep = makeRep(["hello world"]);
|
|
3507
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
3508
|
+
const ctx = {
|
|
3509
|
+
rep,
|
|
3510
|
+
editorInfo,
|
|
3511
|
+
line: 0,
|
|
3512
|
+
char: 0,
|
|
3513
|
+
lineText: "hello world",
|
|
3514
|
+
count: 1,
|
|
3515
|
+
hasCount: false,
|
|
3516
|
+
};
|
|
3517
|
+
|
|
3518
|
+
handleKey('"', ctx);
|
|
3519
|
+
handleKey("a", ctx);
|
|
3520
|
+
handleKey("y", ctx);
|
|
3521
|
+
handleKey("w", ctx);
|
|
3522
|
+
|
|
3523
|
+
assert.deepEqual(state.namedRegisters["a"], "hello ");
|
|
3524
|
+
});
|
|
3525
|
+
|
|
3526
|
+
it("named registers are independent", () => {
|
|
3527
|
+
const rep = makeRep(["first", "second"]);
|
|
3528
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
3529
|
+
|
|
3530
|
+
const ctx0 = {
|
|
3531
|
+
rep,
|
|
3532
|
+
editorInfo,
|
|
3533
|
+
line: 0,
|
|
3534
|
+
char: 0,
|
|
3535
|
+
lineText: "first",
|
|
3536
|
+
count: 1,
|
|
3537
|
+
hasCount: false,
|
|
3538
|
+
};
|
|
3539
|
+
handleKey('"', ctx0);
|
|
3540
|
+
handleKey("a", ctx0);
|
|
3541
|
+
handleKey("y", ctx0);
|
|
3542
|
+
handleKey("y", ctx0);
|
|
3543
|
+
|
|
3544
|
+
const ctx1 = {
|
|
3545
|
+
rep,
|
|
3546
|
+
editorInfo,
|
|
3547
|
+
line: 1,
|
|
3548
|
+
char: 0,
|
|
3549
|
+
lineText: "second",
|
|
3550
|
+
count: 1,
|
|
3551
|
+
hasCount: false,
|
|
3552
|
+
};
|
|
3553
|
+
handleKey('"', ctx1);
|
|
3554
|
+
handleKey("b", ctx1);
|
|
3555
|
+
handleKey("y", ctx1);
|
|
3556
|
+
handleKey("y", ctx1);
|
|
3557
|
+
|
|
3558
|
+
assert.deepEqual(state.namedRegisters["a"], ["first"]);
|
|
3559
|
+
assert.deepEqual(state.namedRegisters["b"], ["second"]);
|
|
3560
|
+
});
|
|
3561
|
+
|
|
3562
|
+
it('"_yy yank to blackhole does not affect anonymous register', () => {
|
|
3563
|
+
state.register = "preserved";
|
|
3564
|
+
const rep = makeRep(["hello"]);
|
|
3565
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
3566
|
+
const ctx = {
|
|
3567
|
+
rep,
|
|
3568
|
+
editorInfo,
|
|
3569
|
+
line: 0,
|
|
3570
|
+
char: 0,
|
|
3571
|
+
lineText: "hello",
|
|
3572
|
+
count: 1,
|
|
3573
|
+
hasCount: false,
|
|
3574
|
+
};
|
|
3575
|
+
|
|
3576
|
+
handleKey('"', ctx);
|
|
3577
|
+
handleKey("_", ctx);
|
|
3578
|
+
handleKey("y", ctx);
|
|
3579
|
+
handleKey("y", ctx);
|
|
3580
|
+
|
|
3581
|
+
assert.equal(state.register, "preserved", "anonymous register unchanged");
|
|
3582
|
+
});
|
|
3583
|
+
|
|
3584
|
+
it('"_p pastes nothing from blackhole register', () => {
|
|
3585
|
+
state.register = "fallback";
|
|
3586
|
+
const rep = makeRep(["hello"]);
|
|
3587
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
3588
|
+
const ctx = {
|
|
3589
|
+
rep,
|
|
3590
|
+
editorInfo,
|
|
3591
|
+
line: 0,
|
|
3592
|
+
char: 0,
|
|
3593
|
+
lineText: "hello",
|
|
3594
|
+
count: 1,
|
|
3595
|
+
hasCount: false,
|
|
3596
|
+
};
|
|
3597
|
+
|
|
3598
|
+
handleKey('"', ctx);
|
|
3599
|
+
handleKey("_", ctx);
|
|
3600
|
+
handleKey("p", ctx);
|
|
3601
|
+
|
|
3602
|
+
const replaces = calls.filter((c) => c.type === "replace");
|
|
3603
|
+
assert.equal(replaces.length, 0, "blackhole p should paste nothing");
|
|
3604
|
+
});
|
|
3605
|
+
|
|
3606
|
+
it('"_dd deletes to blackhole register without overwriting anonymous', () => {
|
|
3607
|
+
state.register = "preserved";
|
|
3608
|
+
const rep = makeRep(["hello", "world"]);
|
|
3609
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
3610
|
+
const ctx = {
|
|
3611
|
+
rep,
|
|
3612
|
+
editorInfo,
|
|
3613
|
+
line: 0,
|
|
3614
|
+
char: 0,
|
|
3615
|
+
lineText: "hello",
|
|
3616
|
+
count: 1,
|
|
3617
|
+
hasCount: false,
|
|
3618
|
+
};
|
|
3619
|
+
|
|
3620
|
+
// Simulate: " -> _ -> dd
|
|
3621
|
+
handleKey('"', ctx);
|
|
3622
|
+
handleKey("_", ctx);
|
|
3623
|
+
handleKey("d", ctx);
|
|
3624
|
+
handleKey("d", ctx);
|
|
3625
|
+
|
|
3626
|
+
assert.equal(
|
|
3627
|
+
state.register,
|
|
3628
|
+
"preserved",
|
|
3629
|
+
"blackhole register should not overwrite anonymous register",
|
|
3630
|
+
);
|
|
3631
|
+
});
|
|
3632
|
+
});
|
|
3633
|
+
|
|
3634
|
+
describe("missing feature: * and # word search", () => {
|
|
3635
|
+
beforeEach(resetState);
|
|
3636
|
+
|
|
3637
|
+
it("* searches forward for word under cursor", () => {
|
|
3638
|
+
const rep = makeRep(["hello world hello"]);
|
|
3639
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
3640
|
+
const ctx = {
|
|
3641
|
+
rep,
|
|
3642
|
+
editorInfo,
|
|
3643
|
+
line: 0,
|
|
3644
|
+
char: 0,
|
|
3645
|
+
lineText: "hello world hello",
|
|
3646
|
+
count: 1,
|
|
3647
|
+
hasCount: false,
|
|
3648
|
+
};
|
|
3649
|
+
|
|
3650
|
+
commands.normal["*"](ctx);
|
|
3651
|
+
|
|
3652
|
+
assert.equal(calls.length, 1, "should move cursor to next match");
|
|
3653
|
+
assert.deepEqual(state.lastSearch, { pattern: "hello", direction: "/" });
|
|
3654
|
+
});
|
|
3655
|
+
|
|
3656
|
+
it("* on non-word char does nothing", () => {
|
|
3657
|
+
const rep = makeRep(["hello world"]);
|
|
3658
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
3659
|
+
const ctx = {
|
|
3660
|
+
rep,
|
|
3661
|
+
editorInfo,
|
|
3662
|
+
line: 0,
|
|
3663
|
+
char: 5,
|
|
3664
|
+
lineText: "hello world",
|
|
3665
|
+
count: 1,
|
|
3666
|
+
hasCount: false,
|
|
3667
|
+
};
|
|
3668
|
+
|
|
3669
|
+
commands.normal["*"](ctx);
|
|
3670
|
+
|
|
3671
|
+
assert.equal(calls.length, 0, "should not move cursor");
|
|
3672
|
+
assert.equal(state.lastSearch, null);
|
|
3673
|
+
});
|
|
3674
|
+
|
|
3675
|
+
it("# searches backward for word under cursor", () => {
|
|
3676
|
+
const rep = makeRep(["hello world hello"]);
|
|
3677
|
+
const { editorInfo, calls } = makeMockEditorInfo();
|
|
3678
|
+
const ctx = {
|
|
3679
|
+
rep,
|
|
3680
|
+
editorInfo,
|
|
3681
|
+
line: 0,
|
|
3682
|
+
char: 12,
|
|
3683
|
+
lineText: "hello world hello",
|
|
3684
|
+
count: 1,
|
|
3685
|
+
hasCount: false,
|
|
3686
|
+
};
|
|
3687
|
+
|
|
3688
|
+
commands.normal["#"](ctx);
|
|
3689
|
+
|
|
3690
|
+
assert.equal(calls.length, 1, "should move cursor to previous match");
|
|
3691
|
+
assert.deepEqual(state.lastSearch, { pattern: "hello", direction: "?" });
|
|
3692
|
+
});
|
|
3693
|
+
|
|
3694
|
+
it("* sets lastSearch so n repeats the search", () => {
|
|
3695
|
+
const rep = makeRep(["foo bar foo bar"]);
|
|
3696
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
3697
|
+
const ctx = {
|
|
3698
|
+
rep,
|
|
3699
|
+
editorInfo,
|
|
3700
|
+
line: 0,
|
|
3701
|
+
char: 0,
|
|
3702
|
+
lineText: "foo bar foo bar",
|
|
3703
|
+
count: 1,
|
|
3704
|
+
hasCount: false,
|
|
3705
|
+
};
|
|
3706
|
+
|
|
3707
|
+
commands.normal["*"](ctx);
|
|
3708
|
+
|
|
3709
|
+
assert.deepEqual(state.lastSearch, { pattern: "foo", direction: "/" });
|
|
3710
|
+
});
|
|
3711
|
+
});
|
|
3712
|
+
|
|
3713
|
+
describe("missing feature: zz center screen", () => {
|
|
3714
|
+
beforeEach(resetState);
|
|
3715
|
+
|
|
3716
|
+
it("zz does nothing when editorDoc is null", () => {
|
|
3717
|
+
state.editorDoc = null;
|
|
3718
|
+
const rep = makeRep(["hello"]);
|
|
3719
|
+
const { editorInfo } = makeMockEditorInfo();
|
|
3720
|
+
const ctx = {
|
|
3721
|
+
rep,
|
|
3722
|
+
editorInfo,
|
|
3723
|
+
line: 0,
|
|
3724
|
+
char: 0,
|
|
3725
|
+
lineText: "hello",
|
|
3726
|
+
count: 1,
|
|
3727
|
+
hasCount: false,
|
|
3728
|
+
};
|
|
3729
|
+
|
|
3730
|
+
assert.doesNotThrow(() => commands.normal["zz"](ctx));
|
|
3731
|
+
});
|
|
3732
|
+
});
|