ep_vim 0.9.3 → 0.11.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 +9 -4
- package/ep.json +2 -1
- package/index.js +5 -0
- package/package.json +6 -6
- package/static/js/index.js +157 -17
- package/static/js/index.test.js +651 -0
- package/templates/settings.ejs +8 -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,16 @@ 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
|
+
- **No command line, macros, or globals** - these are not planned, but PRs welcome.
|
|
33
|
+
|
|
29
34
|
## Installation
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
From your Etherpad directory run
|
|
32
37
|
|
|
33
38
|
```
|
|
34
|
-
pnpm install ep_vim
|
|
39
|
+
pnpm run plugins install ep_vim
|
|
35
40
|
```
|
|
36
41
|
|
|
37
42
|
|
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.11.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
|
-
},
|
|
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
|
@@ -30,12 +30,18 @@ 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,
|
|
36
39
|
pendingCount: null,
|
|
37
40
|
countBuffer: "",
|
|
38
41
|
register: null,
|
|
42
|
+
namedRegisters: {},
|
|
43
|
+
pendingRegister: null,
|
|
44
|
+
awaitingRegister: false,
|
|
39
45
|
marks: {},
|
|
40
46
|
lastCharSearch: null,
|
|
41
47
|
visualAnchor: null,
|
|
@@ -53,13 +59,34 @@ const state = {
|
|
|
53
59
|
// --- Editor operations ---
|
|
54
60
|
|
|
55
61
|
const setRegister = (value) => {
|
|
62
|
+
if (state.pendingRegister === "_") {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (state.pendingRegister && /^[a-zA-Z]$/.test(state.pendingRegister)) {
|
|
66
|
+
const name = state.pendingRegister.toLowerCase();
|
|
67
|
+
state.namedRegisters[name] = value;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
56
70
|
state.register = value;
|
|
57
71
|
const text = Array.isArray(value) ? value.join("\n") + "\n" : value;
|
|
58
|
-
if (navigator.clipboard) {
|
|
72
|
+
if (useSystemClipboard && navigator.clipboard) {
|
|
59
73
|
navigator.clipboard.writeText(text).catch(() => {});
|
|
60
74
|
}
|
|
61
75
|
};
|
|
62
76
|
|
|
77
|
+
const getActiveRegister = () => {
|
|
78
|
+
if (state.pendingRegister === "_") {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
if (state.pendingRegister && /^[a-zA-Z]$/.test(state.pendingRegister)) {
|
|
82
|
+
const name = state.pendingRegister.toLowerCase();
|
|
83
|
+
return state.namedRegisters[name] !== undefined
|
|
84
|
+
? state.namedRegisters[name]
|
|
85
|
+
: null;
|
|
86
|
+
}
|
|
87
|
+
return state.register;
|
|
88
|
+
};
|
|
89
|
+
|
|
63
90
|
const moveCursor = (editorInfo, line, char) => {
|
|
64
91
|
const pos = [line, char];
|
|
65
92
|
editorInfo.ace_inCallStackIfNecessary("vim-move", () => {
|
|
@@ -833,6 +860,7 @@ parameterized["r"] = (key, { editorInfo, line, char, lineText, count }) => {
|
|
|
833
860
|
|
|
834
861
|
commands.normal["Y"] = ({ rep, line }) => {
|
|
835
862
|
setRegister([getLineText(rep, line)]);
|
|
863
|
+
recordCommand("Y", 1);
|
|
836
864
|
};
|
|
837
865
|
|
|
838
866
|
commands.normal["x"] = ({ editorInfo, rep, line, char, lineText, count }) => {
|
|
@@ -847,14 +875,15 @@ commands.normal["x"] = ({ editorInfo, rep, line, char, lineText, count }) => {
|
|
|
847
875
|
};
|
|
848
876
|
|
|
849
877
|
commands.normal["p"] = ({ editorInfo, line, char, lineText, count }) => {
|
|
850
|
-
|
|
851
|
-
|
|
878
|
+
const reg = getActiveRegister();
|
|
879
|
+
if (reg !== null) {
|
|
880
|
+
if (typeof reg === "string") {
|
|
852
881
|
const insertPos = Math.min(char + 1, lineText.length);
|
|
853
|
-
const repeated =
|
|
882
|
+
const repeated = reg.repeat(count);
|
|
854
883
|
replaceRange(editorInfo, [line, insertPos], [line, insertPos], repeated);
|
|
855
|
-
moveBlockCursor(editorInfo, line, insertPos);
|
|
884
|
+
moveBlockCursor(editorInfo, line, insertPos + repeated.length - 1);
|
|
856
885
|
} else {
|
|
857
|
-
const block =
|
|
886
|
+
const block = reg.join("\n");
|
|
858
887
|
const parts = [];
|
|
859
888
|
for (let i = 0; i < count; i++) parts.push(block);
|
|
860
889
|
const insertText = "\n" + parts.join("\n");
|
|
@@ -864,25 +893,26 @@ commands.normal["p"] = ({ editorInfo, line, char, lineText, count }) => {
|
|
|
864
893
|
[line, lineText.length],
|
|
865
894
|
insertText,
|
|
866
895
|
);
|
|
867
|
-
moveBlockCursor(editorInfo, line + 1, 0);
|
|
896
|
+
moveBlockCursor(editorInfo, line + 1, firstNonBlank(reg[0]));
|
|
868
897
|
}
|
|
869
898
|
recordCommand("p", count);
|
|
870
899
|
}
|
|
871
900
|
};
|
|
872
901
|
|
|
873
902
|
commands.normal["P"] = ({ editorInfo, line, char, count }) => {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
903
|
+
const reg = getActiveRegister();
|
|
904
|
+
if (reg !== null) {
|
|
905
|
+
if (typeof reg === "string") {
|
|
906
|
+
const repeated = reg.repeat(count);
|
|
877
907
|
replaceRange(editorInfo, [line, char], [line, char], repeated);
|
|
878
|
-
moveBlockCursor(editorInfo, line, char);
|
|
908
|
+
moveBlockCursor(editorInfo, line, char + repeated.length - 1);
|
|
879
909
|
} else {
|
|
880
|
-
const block =
|
|
910
|
+
const block = reg.join("\n");
|
|
881
911
|
const parts = [];
|
|
882
912
|
for (let i = 0; i < count; i++) parts.push(block);
|
|
883
913
|
const insertText = parts.join("\n") + "\n";
|
|
884
914
|
replaceRange(editorInfo, [line, 0], [line, 0], insertText);
|
|
885
|
-
moveBlockCursor(editorInfo, line, 0);
|
|
915
|
+
moveBlockCursor(editorInfo, line, firstNonBlank(reg[0]));
|
|
886
916
|
}
|
|
887
917
|
recordCommand("P", count);
|
|
888
918
|
}
|
|
@@ -944,7 +974,7 @@ commands.normal["C"] = ({ editorInfo, line, char, lineText }) => {
|
|
|
944
974
|
|
|
945
975
|
commands.normal["s"] = ({ editorInfo, rep, line, char, lineText, count }) => {
|
|
946
976
|
clearEmptyLineCursor();
|
|
947
|
-
setRegister(lineText.slice(char, char +
|
|
977
|
+
setRegister(lineText.slice(char, Math.min(char + count, lineText.length)));
|
|
948
978
|
replaceRange(
|
|
949
979
|
editorInfo,
|
|
950
980
|
[line, char],
|
|
@@ -958,7 +988,7 @@ commands.normal["s"] = ({ editorInfo, rep, line, char, lineText, count }) => {
|
|
|
958
988
|
|
|
959
989
|
commands.normal["S"] = ({ editorInfo, line, lineText }) => {
|
|
960
990
|
clearEmptyLineCursor();
|
|
961
|
-
setRegister(lineText);
|
|
991
|
+
setRegister([lineText]);
|
|
962
992
|
replaceRange(editorInfo, [line, 0], [line, lineText.length], "");
|
|
963
993
|
moveCursor(editorInfo, line, 0);
|
|
964
994
|
state.mode = "insert";
|
|
@@ -995,9 +1025,50 @@ commands.normal["N"] = (ctx) => {
|
|
|
995
1025
|
if (pos) moveBlockCursor(ctx.editorInfo, pos[0], pos[1]);
|
|
996
1026
|
};
|
|
997
1027
|
|
|
1028
|
+
const getWordAt = (text, char) => {
|
|
1029
|
+
if (char >= text.length || !/\w/.test(text[char])) return null;
|
|
1030
|
+
let start = char;
|
|
1031
|
+
while (start > 0 && /\w/.test(text[start - 1])) start--;
|
|
1032
|
+
let end = char;
|
|
1033
|
+
while (end < text.length && /\w/.test(text[end])) end++;
|
|
1034
|
+
return text.slice(start, end);
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
commands.normal["*"] = (ctx) => {
|
|
1038
|
+
const word = getWordAt(ctx.lineText, ctx.char);
|
|
1039
|
+
if (!word) return;
|
|
1040
|
+
state.lastSearch = { pattern: word, direction: "/" };
|
|
1041
|
+
const pos = searchForward(ctx.rep, ctx.line, ctx.char + 1, word, ctx.count);
|
|
1042
|
+
if (pos) moveBlockCursor(ctx.editorInfo, pos[0], pos[1]);
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
commands.normal["#"] = (ctx) => {
|
|
1046
|
+
const word = getWordAt(ctx.lineText, ctx.char);
|
|
1047
|
+
if (!word) return;
|
|
1048
|
+
state.lastSearch = { pattern: word, direction: "?" };
|
|
1049
|
+
const pos = searchBackward(ctx.rep, ctx.line, ctx.char, word, ctx.count);
|
|
1050
|
+
if (pos) moveBlockCursor(ctx.editorInfo, pos[0], pos[1]);
|
|
1051
|
+
};
|
|
1052
|
+
|
|
1053
|
+
commands.normal["zz"] = ({ line }) => {
|
|
1054
|
+
if (!state.editorDoc) return;
|
|
1055
|
+
const lineDiv = state.editorDoc.body.querySelectorAll("div")[line];
|
|
1056
|
+
if (lineDiv) lineDiv.scrollIntoView({ block: "center" });
|
|
1057
|
+
};
|
|
1058
|
+
|
|
998
1059
|
// --- Dispatch ---
|
|
999
1060
|
|
|
1000
1061
|
const handleKey = (key, ctx) => {
|
|
1062
|
+
if (state.awaitingRegister) {
|
|
1063
|
+
state.pendingRegister = key;
|
|
1064
|
+
state.awaitingRegister = false;
|
|
1065
|
+
return true;
|
|
1066
|
+
}
|
|
1067
|
+
if (key === '"' && state.mode === "normal") {
|
|
1068
|
+
state.awaitingRegister = true;
|
|
1069
|
+
return true;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1001
1072
|
if (key >= "1" && key <= "9") {
|
|
1002
1073
|
state.countBuffer += key;
|
|
1003
1074
|
return true;
|
|
@@ -1019,6 +1090,7 @@ const handleKey = (key, ctx) => {
|
|
|
1019
1090
|
state.pendingKey = null;
|
|
1020
1091
|
handler(key, ctx);
|
|
1021
1092
|
state.pendingCount = null;
|
|
1093
|
+
state.pendingRegister = null;
|
|
1022
1094
|
return true;
|
|
1023
1095
|
}
|
|
1024
1096
|
|
|
@@ -1028,7 +1100,10 @@ const handleKey = (key, ctx) => {
|
|
|
1028
1100
|
if (map[seq]) {
|
|
1029
1101
|
state.pendingKey = null;
|
|
1030
1102
|
map[seq](ctx);
|
|
1031
|
-
if (state.pendingKey === null)
|
|
1103
|
+
if (state.pendingKey === null) {
|
|
1104
|
+
state.pendingCount = null;
|
|
1105
|
+
state.pendingRegister = null;
|
|
1106
|
+
}
|
|
1032
1107
|
return true;
|
|
1033
1108
|
}
|
|
1034
1109
|
|
|
@@ -1073,6 +1148,7 @@ const handleKey = (key, ctx) => {
|
|
|
1073
1148
|
|
|
1074
1149
|
state.pendingKey = null;
|
|
1075
1150
|
state.pendingCount = null;
|
|
1151
|
+
state.pendingRegister = null;
|
|
1076
1152
|
return true;
|
|
1077
1153
|
};
|
|
1078
1154
|
|
|
@@ -1089,6 +1165,22 @@ exports.postToolbarInit = (_hookName, _args) => {
|
|
|
1089
1165
|
localStorage.setItem("ep_vimEnabled", vimEnabled ? "true" : "false");
|
|
1090
1166
|
btn.classList.toggle("vim-enabled", vimEnabled);
|
|
1091
1167
|
});
|
|
1168
|
+
|
|
1169
|
+
const clipboardCheckbox = document.getElementById(
|
|
1170
|
+
"options-vim-use-system-clipboard",
|
|
1171
|
+
);
|
|
1172
|
+
if (!clipboardCheckbox) return;
|
|
1173
|
+
useSystemClipboard = clipboardCheckbox.checked;
|
|
1174
|
+
clipboardCheckbox.addEventListener("change", () => {
|
|
1175
|
+
useSystemClipboard = clipboardCheckbox.checked;
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
const ctrlKeysCheckbox = document.getElementById("options-vim-use-ctrl-keys");
|
|
1179
|
+
if (!ctrlKeysCheckbox) return;
|
|
1180
|
+
useCtrlKeys = ctrlKeysCheckbox.checked;
|
|
1181
|
+
ctrlKeysCheckbox.addEventListener("change", () => {
|
|
1182
|
+
useCtrlKeys = ctrlKeysCheckbox.checked;
|
|
1183
|
+
});
|
|
1092
1184
|
};
|
|
1093
1185
|
|
|
1094
1186
|
exports.postAceInit = (_hookName, { ace }) => {
|
|
@@ -1108,7 +1200,10 @@ exports.aceKeyEvent = (_hookName, { evt, rep, editorInfo }) => {
|
|
|
1108
1200
|
|
|
1109
1201
|
const isBrowserShortcut =
|
|
1110
1202
|
(evt.ctrlKey || evt.metaKey) &&
|
|
1111
|
-
(evt.key === "x" ||
|
|
1203
|
+
(evt.key === "x" ||
|
|
1204
|
+
evt.key === "c" ||
|
|
1205
|
+
evt.key === "v" ||
|
|
1206
|
+
(evt.key === "r" && !useCtrlKeys));
|
|
1112
1207
|
if (isBrowserShortcut) return false;
|
|
1113
1208
|
|
|
1114
1209
|
state.currentRep = rep;
|
|
@@ -1174,6 +1269,51 @@ exports.aceKeyEvent = (_hookName, { evt, rep, editorInfo }) => {
|
|
|
1174
1269
|
: rep.selStart;
|
|
1175
1270
|
const lineText = rep.lines.atIndex(line).text;
|
|
1176
1271
|
const ctx = { rep, editorInfo, line, char, lineText };
|
|
1272
|
+
|
|
1273
|
+
if (useCtrlKeys && evt.ctrlKey && state.mode === "normal") {
|
|
1274
|
+
if (state.countBuffer !== "") {
|
|
1275
|
+
state.pendingCount = parseInt(state.countBuffer, 10);
|
|
1276
|
+
state.countBuffer = "";
|
|
1277
|
+
}
|
|
1278
|
+
const count = state.pendingCount !== null ? state.pendingCount : 1;
|
|
1279
|
+
|
|
1280
|
+
if (evt.key === "r") {
|
|
1281
|
+
editorInfo.ace_doUndoRedo("redo");
|
|
1282
|
+
state.pendingCount = null;
|
|
1283
|
+
state.pendingRegister = null;
|
|
1284
|
+
evt.preventDefault();
|
|
1285
|
+
return true;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
const halfPage = 15;
|
|
1289
|
+
if (evt.key === "d") {
|
|
1290
|
+
const target = Math.min(line + halfPage * count, rep.lines.length() - 1);
|
|
1291
|
+
const targetLen = rep.lines.atIndex(target).text.length;
|
|
1292
|
+
moveBlockCursor(
|
|
1293
|
+
editorInfo,
|
|
1294
|
+
target,
|
|
1295
|
+
Math.min(char, Math.max(0, targetLen - 1)),
|
|
1296
|
+
);
|
|
1297
|
+
state.pendingCount = null;
|
|
1298
|
+
state.pendingRegister = null;
|
|
1299
|
+
evt.preventDefault();
|
|
1300
|
+
return true;
|
|
1301
|
+
}
|
|
1302
|
+
if (evt.key === "u") {
|
|
1303
|
+
const target = Math.max(line - halfPage * count, 0);
|
|
1304
|
+
const targetLen = rep.lines.atIndex(target).text.length;
|
|
1305
|
+
moveBlockCursor(
|
|
1306
|
+
editorInfo,
|
|
1307
|
+
target,
|
|
1308
|
+
Math.min(char, Math.max(0, targetLen - 1)),
|
|
1309
|
+
);
|
|
1310
|
+
state.pendingCount = null;
|
|
1311
|
+
state.pendingRegister = null;
|
|
1312
|
+
evt.preventDefault();
|
|
1313
|
+
return true;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1177
1317
|
const handled = handleKey(evt.key, ctx);
|
|
1178
1318
|
if (handled) evt.preventDefault();
|
|
1179
1319
|
return handled;
|
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
|
+
});
|
|
@@ -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>
|