ep_vim 0.1.0 → 0.5.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.
@@ -1,63 +1,58 @@
1
- 'use strict';
1
+ "use strict";
2
+
3
+ const {
4
+ clampLine,
5
+ clampChar,
6
+ getLineText,
7
+ firstNonBlank,
8
+ wordForward,
9
+ wordBackward,
10
+ wordEnd,
11
+ charSearchPos,
12
+ motionRange,
13
+ charMotionRange,
14
+ innerWordRange,
15
+ innerQuoteRange,
16
+ innerBracketRange,
17
+ getVisualSelection,
18
+ paragraphForward,
19
+ paragraphBackward,
20
+ getTextInRange,
21
+ } = require("./vim-core");
2
22
 
3
23
  // --- State variables ---
4
24
 
5
- let vimEnabled = localStorage.getItem('ep_vimEnabled') === 'true';
25
+ let vimEnabled = localStorage.getItem("ep_vimEnabled") === "true";
6
26
  let insertMode = false;
7
27
  let visualMode = null;
8
28
  let visualAnchor = null;
9
29
  let visualCursor = null;
10
30
  let pendingKey = null;
11
31
  let pendingCount = null;
12
- let countBuffer = '';
32
+ let countBuffer = "";
13
33
  let register = null;
14
34
  let marks = {};
15
35
  let editorDoc = null;
16
36
  let currentRep = null;
37
+ let desiredColumn = null;
38
+ let lastCharSearch = null;
17
39
 
18
- // --- Utility helpers ---
40
+ const QUOTE_CHARS = new Set(['"', "'"]);
41
+ const BRACKET_CHARS = new Set(["(", ")", "{", "}", "[", "]"]);
19
42
 
20
- const isWordChar = (ch) => /\w/.test(ch);
21
- const isWhitespace = (ch) => /\s/.test(ch);
22
-
23
- const clampLine = (line, rep) => Math.max(0, Math.min(line, rep.lines.length() - 1));
24
-
25
- const clampChar = (char, lineText) => Math.max(0, Math.min(char, lineText.length - 1));
26
-
27
- const getLineText = (rep, line) => rep.lines.atIndex(line).text;
28
-
29
- const firstNonBlank = (lineText) => {
30
- let i = 0;
31
- while (i < lineText.length && isWhitespace(lineText[i])) i++;
32
- return i;
33
- };
34
-
35
- const findCharForward = (lineText, startChar, targetChar, count) => {
36
- let found = 0;
37
- for (let i = startChar + 1; i < lineText.length; i++) {
38
- if (lineText[i] === targetChar) {
39
- found++;
40
- if (found === count) return i;
41
- }
42
- }
43
- return -1;
43
+ const innerTextObjectRange = (key, lineText, char) => {
44
+ if (key === "w") return innerWordRange(lineText, char);
45
+ if (QUOTE_CHARS.has(key)) return innerQuoteRange(lineText, char, key);
46
+ if (BRACKET_CHARS.has(key)) return innerBracketRange(lineText, char, key);
47
+ return null;
44
48
  };
45
49
 
46
- const findCharBackward = (lineText, startChar, targetChar, count) => {
47
- let found = 0;
48
- for (let i = startChar - 1; i >= 0; i--) {
49
- if (lineText[i] === targetChar) {
50
- found++;
51
- if (found === count) return i;
52
- }
53
- }
54
- return -1;
55
- };
50
+ // --- Count helpers ---
56
51
 
57
52
  const consumeCount = () => {
58
- if (countBuffer !== '') {
53
+ if (countBuffer !== "") {
59
54
  pendingCount = parseInt(countBuffer, 10);
60
- countBuffer = '';
55
+ countBuffer = "";
61
56
  } else if (pendingKey === null) {
62
57
  pendingCount = null;
63
58
  }
@@ -65,17 +60,17 @@ const consumeCount = () => {
65
60
 
66
61
  const getCount = () => pendingCount || 1;
67
62
 
63
+ // --- Side-effectful helpers ---
64
+
68
65
  const setRegister = (value) => {
69
66
  register = value;
70
- const text = Array.isArray(value) ? value.join('\n') + '\n' : value;
67
+ const text = Array.isArray(value) ? value.join("\n") + "\n" : value;
71
68
  navigator.clipboard.writeText(text).catch(() => {});
72
69
  };
73
70
 
74
- // --- Etherpad API wrappers ---
75
-
76
71
  const moveCursor = (editorInfo, line, char) => {
77
72
  const pos = [line, char];
78
- editorInfo.ace_inCallStackIfNecessary('vim-move', () => {
73
+ editorInfo.ace_inCallStackIfNecessary("vim-move", () => {
79
74
  editorInfo.ace_performSelectionChange(pos, pos, false);
80
75
  editorInfo.ace_updateBrowserSelectionFromRep();
81
76
  });
@@ -83,16 +78,16 @@ const moveCursor = (editorInfo, line, char) => {
83
78
 
84
79
  const clearEmptyLineCursor = () => {
85
80
  if (!editorDoc) return;
86
- const old = editorDoc.querySelector('.vim-empty-line-cursor');
87
- if (old) old.classList.remove('vim-empty-line-cursor');
81
+ const old = editorDoc.querySelector(".vim-empty-line-cursor");
82
+ if (old) old.classList.remove("vim-empty-line-cursor");
88
83
  };
89
84
 
90
85
  const moveBlockCursor = (editorInfo, line, char) => {
91
86
  clearEmptyLineCursor();
92
- const lineText = currentRep ? getLineText(currentRep, line) : '';
87
+ const lineText = currentRep ? getLineText(currentRep, line) : "";
93
88
  if (lineText.length === 0 && editorDoc) {
94
- const lineDiv = editorDoc.body.querySelectorAll('div')[line];
95
- if (lineDiv) lineDiv.classList.add('vim-empty-line-cursor');
89
+ const lineDiv = editorDoc.body.querySelectorAll("div")[line];
90
+ if (lineDiv) lineDiv.classList.add("vim-empty-line-cursor");
96
91
  selectRange(editorInfo, [line, 0], [line, 0]);
97
92
  } else {
98
93
  selectRange(editorInfo, [line, char], [line, char + 1]);
@@ -100,20 +95,20 @@ const moveBlockCursor = (editorInfo, line, char) => {
100
95
  };
101
96
 
102
97
  const selectRange = (editorInfo, start, end) => {
103
- editorInfo.ace_inCallStackIfNecessary('vim-select', () => {
98
+ editorInfo.ace_inCallStackIfNecessary("vim-select", () => {
104
99
  editorInfo.ace_performSelectionChange(start, end, false);
105
100
  editorInfo.ace_updateBrowserSelectionFromRep();
106
101
  });
107
102
  };
108
103
 
109
104
  const replaceRange = (editorInfo, start, end, text) => {
110
- editorInfo.ace_inCallStackIfNecessary('vim-edit', () => {
105
+ editorInfo.ace_inCallStackIfNecessary("vim-edit", () => {
111
106
  editorInfo.ace_performDocumentReplaceRange(start, end, text);
112
107
  });
113
108
  };
114
109
 
115
110
  const undo = (editorInfo) => {
116
- editorInfo.ace_doUndoRedo('undo');
111
+ editorInfo.ace_doUndoRedo("undo");
117
112
  };
118
113
 
119
114
  // --- Mode management ---
@@ -122,91 +117,30 @@ const setInsertMode = (value) => {
122
117
  insertMode = value;
123
118
  if (value) clearEmptyLineCursor();
124
119
  if (editorDoc) {
125
- editorDoc.body.classList.toggle('vim-insert-mode', value);
120
+ editorDoc.body.classList.toggle("vim-insert-mode", value);
126
121
  }
127
122
  };
128
123
 
129
124
  const setVisualMode = (value) => {
130
125
  visualMode = value;
131
126
  if (editorDoc) {
132
- editorDoc.body.classList.toggle('vim-visual-line-mode', value === 'line');
133
- editorDoc.body.classList.toggle('vim-visual-char-mode', value === 'char');
127
+ editorDoc.body.classList.toggle("vim-visual-line-mode", value === "line");
128
+ editorDoc.body.classList.toggle("vim-visual-char-mode", value === "char");
134
129
  }
135
130
  };
136
131
 
137
132
  // --- Visual mode helpers ---
138
133
 
139
- const getVisualSelection = (rep) => {
140
- if (visualMode === 'line') {
141
- const topLine = Math.min(visualAnchor[0], visualCursor[0]);
142
- const bottomLine = Math.max(visualAnchor[0], visualCursor[0]);
143
- const lineCount = rep.lines.length();
144
- const start = [topLine, 0];
145
- const end = bottomLine + 1 < lineCount
146
- ? [bottomLine + 1, 0]
147
- : [bottomLine, getLineText(rep, bottomLine).length];
148
- return [start, end];
149
- }
150
- if (visualAnchor[0] < visualCursor[0] ||
151
- (visualAnchor[0] === visualCursor[0] && visualAnchor[1] <= visualCursor[1])) {
152
- return [visualAnchor, visualCursor];
153
- }
154
- return [visualCursor, visualAnchor];
155
- };
156
-
157
- const getTextInRange = (rep, start, end) => {
158
- if (start[0] === end[0]) {
159
- return getLineText(rep, start[0]).slice(start[1], end[1]);
160
- }
161
- const parts = [];
162
- parts.push(getLineText(rep, start[0]).slice(start[1]));
163
- for (let i = start[0] + 1; i < end[0]; i++) {
164
- parts.push(getLineText(rep, i));
165
- }
166
- parts.push(getLineText(rep, end[0]).slice(0, end[1]));
167
- return parts.join('\n');
168
- };
169
-
170
134
  const updateVisualSelection = (editorInfo, rep) => {
171
- const [start, end] = getVisualSelection(rep);
135
+ const [start, end] = getVisualSelection(
136
+ visualMode,
137
+ visualAnchor,
138
+ visualCursor,
139
+ rep,
140
+ );
172
141
  selectRange(editorInfo, start, end);
173
142
  };
174
143
 
175
- // --- Word motion helpers ---
176
-
177
- const wordForward = (lineText, startChar) => {
178
- let pos = startChar;
179
- if (pos < lineText.length && isWordChar(lineText[pos])) {
180
- while (pos < lineText.length && isWordChar(lineText[pos])) pos++;
181
- } else if (pos < lineText.length && !isWhitespace(lineText[pos])) {
182
- while (pos < lineText.length && !isWordChar(lineText[pos]) && !isWhitespace(lineText[pos])) pos++;
183
- }
184
- while (pos < lineText.length && isWhitespace(lineText[pos])) pos++;
185
- return pos;
186
- };
187
-
188
- const wordBackward = (lineText, startChar) => {
189
- let pos = startChar - 1;
190
- while (pos >= 0 && isWhitespace(lineText[pos])) pos--;
191
- if (pos >= 0 && isWordChar(lineText[pos])) {
192
- while (pos > 0 && isWordChar(lineText[pos - 1])) pos--;
193
- } else {
194
- while (pos > 0 && !isWordChar(lineText[pos - 1]) && !isWhitespace(lineText[pos - 1])) pos--;
195
- }
196
- return Math.max(0, pos);
197
- };
198
-
199
- const wordEnd = (lineText, startChar) => {
200
- let pos = startChar + 1;
201
- while (pos < lineText.length && isWhitespace(lineText[pos])) pos++;
202
- if (pos < lineText.length && isWordChar(lineText[pos])) {
203
- while (pos + 1 < lineText.length && isWordChar(lineText[pos + 1])) pos++;
204
- } else {
205
- while (pos + 1 < lineText.length && !isWordChar(lineText[pos + 1]) && !isWhitespace(lineText[pos + 1])) pos++;
206
- }
207
- return pos;
208
- };
209
-
210
144
  // --- Visual mode key handler ---
211
145
 
212
146
  const handleVisualKey = (rep, editorInfo, key) => {
@@ -214,11 +148,11 @@ const handleVisualKey = (rep, editorInfo, key) => {
214
148
  const curChar = visualCursor[1];
215
149
  const lineText = getLineText(rep, curLine);
216
150
 
217
- if (key >= '1' && key <= '9') {
151
+ if (key >= "1" && key <= "9") {
218
152
  countBuffer += key;
219
153
  return true;
220
154
  }
221
- if (key === '0' && countBuffer !== '') {
155
+ if (key === "0" && countBuffer !== "") {
222
156
  countBuffer += key;
223
157
  return true;
224
158
  }
@@ -226,33 +160,30 @@ const handleVisualKey = (rep, editorInfo, key) => {
226
160
  consumeCount();
227
161
  const count = getCount();
228
162
 
229
- if (pendingKey === 'f' || pendingKey === 'F' || pendingKey === 't' || pendingKey === 'T') {
163
+ if (
164
+ pendingKey === "f" ||
165
+ pendingKey === "F" ||
166
+ pendingKey === "t" ||
167
+ pendingKey === "T"
168
+ ) {
230
169
  const direction = pendingKey;
231
170
  pendingKey = null;
232
- let pos = -1;
233
- if (direction === 'f') {
234
- pos = findCharForward(lineText, curChar, key, count);
235
- } else if (direction === 'F') {
236
- pos = findCharBackward(lineText, curChar, key, count);
237
- } else if (direction === 't') {
238
- pos = findCharForward(lineText, curChar, key, count);
239
- if (pos !== -1) pos = pos - 1;
240
- } else if (direction === 'T') {
241
- pos = findCharBackward(lineText, curChar, key, count);
242
- if (pos !== -1) pos = pos + 1;
243
- }
171
+ lastCharSearch = { direction, target: key };
172
+ const pos = charSearchPos(direction, lineText, curChar, key, count);
244
173
  if (pos !== -1) {
174
+ desiredColumn = null;
245
175
  visualCursor = [curLine, pos];
246
176
  updateVisualSelection(editorInfo, rep);
247
177
  }
248
178
  return true;
249
179
  }
250
180
 
251
- if (pendingKey === "'" || pendingKey === '`') {
181
+ if (pendingKey === "'" || pendingKey === "`") {
252
182
  const jumpType = pendingKey;
253
183
  pendingKey = null;
254
- if (key >= 'a' && key <= 'z' && marks[key]) {
184
+ if (key >= "a" && key <= "z" && marks[key]) {
255
185
  const [markLine, markChar] = marks[key];
186
+ desiredColumn = null;
256
187
  if (jumpType === "'") {
257
188
  const targetLineText = getLineText(rep, markLine);
258
189
  visualCursor = [markLine, firstNonBlank(targetLineText)];
@@ -264,49 +195,65 @@ const handleVisualKey = (rep, editorInfo, key) => {
264
195
  return true;
265
196
  }
266
197
 
267
- if (key === 'h') {
198
+ if (key === "h") {
199
+ desiredColumn = null;
268
200
  visualCursor = [curLine, Math.max(0, curChar - count)];
269
201
  updateVisualSelection(editorInfo, rep);
270
202
  return true;
271
203
  }
272
204
 
273
- if (key === 'l') {
205
+ if (key === "l") {
206
+ desiredColumn = null;
274
207
  visualCursor = [curLine, clampChar(curChar + count, lineText)];
275
208
  updateVisualSelection(editorInfo, rep);
276
209
  return true;
277
210
  }
278
211
 
279
- if (key === 'j') {
280
- visualCursor = [clampLine(curLine + count, rep), curChar];
212
+ if (key === "j") {
213
+ if (desiredColumn === null) {
214
+ desiredColumn = curChar;
215
+ }
216
+ const newLine = clampLine(curLine + count, rep);
217
+ const newLineText = getLineText(rep, newLine);
218
+ visualCursor = [newLine, clampChar(desiredColumn, newLineText)];
281
219
  updateVisualSelection(editorInfo, rep);
282
220
  return true;
283
221
  }
284
222
 
285
- if (key === 'k') {
286
- visualCursor = [clampLine(curLine - count, rep), curChar];
223
+ if (key === "k") {
224
+ if (desiredColumn === null) {
225
+ desiredColumn = curChar;
226
+ }
227
+ const newLine = clampLine(curLine - count, rep);
228
+ const newLineText = getLineText(rep, newLine);
229
+ visualCursor = [newLine, clampChar(desiredColumn, newLineText)];
287
230
  updateVisualSelection(editorInfo, rep);
288
231
  return true;
289
232
  }
290
233
 
291
- if (key === '0') {
234
+ if (key === "0") {
235
+ desiredColumn = null;
292
236
  visualCursor = [curLine, 0];
293
237
  updateVisualSelection(editorInfo, rep);
294
238
  return true;
295
239
  }
296
240
 
297
- if (key === '$') {
241
+ if (key === "$") {
242
+ desiredColumn = null;
298
243
  visualCursor = [curLine, clampChar(lineText.length - 1, lineText)];
299
244
  updateVisualSelection(editorInfo, rep);
300
245
  return true;
301
246
  }
302
247
 
303
- if (key === '^') {
248
+ if (key === "^") {
249
+ desiredColumn = null;
304
250
  visualCursor = [curLine, firstNonBlank(lineText)];
305
251
  updateVisualSelection(editorInfo, rep);
306
252
  return true;
307
253
  }
308
254
 
309
- if (key === 'w') {
255
+ if (key === "w") {
256
+ desiredColumn = null;
310
257
  let pos = curChar;
311
258
  for (let i = 0; i < count; i++) pos = wordForward(lineText, pos);
312
259
  visualCursor = [curLine, clampChar(pos, lineText)];
@@ -314,7 +261,8 @@ const handleVisualKey = (rep, editorInfo, key) => {
314
261
  return true;
315
262
  }
316
263
 
317
- if (key === 'b') {
264
+ if (key === "b") {
265
+ desiredColumn = null;
318
266
  let pos = curChar;
319
267
  for (let i = 0; i < count; i++) pos = wordBackward(lineText, pos);
320
268
  visualCursor = [curLine, pos];
@@ -322,7 +270,8 @@ const handleVisualKey = (rep, editorInfo, key) => {
322
270
  return true;
323
271
  }
324
272
 
325
- if (key === 'e') {
273
+ if (key === "e") {
274
+ desiredColumn = null;
326
275
  let pos = curChar;
327
276
  for (let i = 0; i < count; i++) pos = wordEnd(lineText, pos);
328
277
  visualCursor = [curLine, clampChar(pos, lineText)];
@@ -330,8 +279,63 @@ const handleVisualKey = (rep, editorInfo, key) => {
330
279
  return true;
331
280
  }
332
281
 
333
- if (key === 'G') {
282
+ if (key === ";") {
283
+ if (lastCharSearch) {
284
+ const pos = charSearchPos(
285
+ lastCharSearch.direction,
286
+ lineText,
287
+ curChar,
288
+ lastCharSearch.target,
289
+ count,
290
+ );
291
+ if (pos !== -1) {
292
+ desiredColumn = null;
293
+ visualCursor = [curLine, pos];
294
+ updateVisualSelection(editorInfo, rep);
295
+ }
296
+ }
297
+ return true;
298
+ }
299
+
300
+ if (key === ",") {
301
+ if (lastCharSearch) {
302
+ const opposite = { f: "F", F: "f", t: "T", T: "t" };
303
+ const reverseDir = opposite[lastCharSearch.direction];
304
+ const pos = charSearchPos(
305
+ reverseDir,
306
+ lineText,
307
+ curChar,
308
+ lastCharSearch.target,
309
+ count,
310
+ );
311
+ if (pos !== -1) {
312
+ desiredColumn = null;
313
+ visualCursor = [curLine, pos];
314
+ updateVisualSelection(editorInfo, rep);
315
+ }
316
+ }
317
+ return true;
318
+ }
319
+
320
+ if (key === "}") {
321
+ desiredColumn = null;
322
+ const target = paragraphForward(rep, curLine, count);
323
+ visualCursor = [target, 0];
324
+ updateVisualSelection(editorInfo, rep);
325
+ return true;
326
+ }
327
+
328
+ if (key === "{") {
329
+ desiredColumn = null;
330
+ const target = paragraphBackward(rep, curLine, count);
331
+ visualCursor = [target, 0];
332
+ updateVisualSelection(editorInfo, rep);
333
+ return true;
334
+ }
335
+
336
+ if (key === "G") {
334
337
  pendingKey = null;
338
+ desiredColumn = null;
335
339
  if (pendingCount !== null) {
336
340
  visualCursor = [clampLine(pendingCount - 1, rep), curChar];
337
341
  } else {
@@ -341,32 +345,62 @@ const handleVisualKey = (rep, editorInfo, key) => {
341
345
  return true;
342
346
  }
343
347
 
344
- if (key === 'g') {
345
- if (pendingKey === 'g') {
348
+ if (key === "g") {
349
+ if (pendingKey === "g") {
346
350
  pendingKey = null;
351
+ desiredColumn = null;
347
352
  visualCursor = [0, curChar];
348
353
  updateVisualSelection(editorInfo, rep);
349
354
  } else {
350
- pendingKey = 'g';
355
+ pendingKey = "g";
351
356
  }
352
357
  return true;
353
358
  }
354
359
 
355
- if (key === 'f' || key === 'F' || key === 't' || key === 'T') {
360
+ if (key === "f" || key === "F" || key === "t" || key === "T") {
356
361
  pendingKey = key;
357
362
  return true;
358
363
  }
359
364
 
360
- if (key === "'" || key === '`') {
365
+ if (key === "'" || key === "`") {
361
366
  pendingKey = key;
362
367
  return true;
363
368
  }
364
369
 
365
- if (key === 'y') {
366
- const [start] = getVisualSelection(rep);
370
+ if (key === "~") {
371
+ const [start, end] = getVisualSelection(
372
+ visualMode,
373
+ visualAnchor,
374
+ visualCursor,
375
+ rep,
376
+ );
377
+ const text = getTextInRange(rep, start, end);
378
+ let toggled = "";
379
+ for (let i = 0; i < text.length; i++) {
380
+ const ch = text[i];
381
+ toggled += ch === ch.toLowerCase() ? ch.toUpperCase() : ch.toLowerCase();
382
+ }
383
+ replaceRange(editorInfo, start, end, toggled);
384
+ setVisualMode(null);
385
+ moveBlockCursor(editorInfo, start[0], start[1]);
386
+ return true;
387
+ }
388
+
389
+ if (key === "y") {
390
+ const [start] = getVisualSelection(
391
+ visualMode,
392
+ visualAnchor,
393
+ visualCursor,
394
+ rep,
395
+ );
367
396
 
368
- if (visualMode === 'char') {
369
- const [, end] = getVisualSelection(rep);
397
+ if (visualMode === "char") {
398
+ const [, end] = getVisualSelection(
399
+ visualMode,
400
+ visualAnchor,
401
+ visualCursor,
402
+ rep,
403
+ );
370
404
  setRegister(getTextInRange(rep, start, end));
371
405
  setVisualMode(null);
372
406
  moveBlockCursor(editorInfo, start[0], start[1]);
@@ -385,13 +419,18 @@ const handleVisualKey = (rep, editorInfo, key) => {
385
419
  return true;
386
420
  }
387
421
 
388
- if (key === 'd' || key === 'c') {
389
- const enterInsert = key === 'c';
390
- const [start, end] = getVisualSelection(rep);
422
+ if (key === "d" || key === "c") {
423
+ const enterInsert = key === "c";
424
+ const [start, end] = getVisualSelection(
425
+ visualMode,
426
+ visualAnchor,
427
+ visualCursor,
428
+ rep,
429
+ );
391
430
 
392
- if (visualMode === 'char') {
431
+ if (visualMode === "char") {
393
432
  setRegister(getTextInRange(rep, start, end));
394
- replaceRange(editorInfo, start, end, '');
433
+ replaceRange(editorInfo, start, end, "");
395
434
  if (enterInsert) {
396
435
  moveCursor(editorInfo, start[0], start[1]);
397
436
  setVisualMode(null);
@@ -415,7 +454,7 @@ const handleVisualKey = (rep, editorInfo, key) => {
415
454
  if (enterInsert) {
416
455
  for (let i = topLine; i <= bottomLine; i++) {
417
456
  const text = getLineText(rep, i);
418
- replaceRange(editorInfo, [topLine, 0], [topLine, text.length], '');
457
+ replaceRange(editorInfo, [topLine, 0], [topLine, text.length], "");
419
458
  }
420
459
  moveCursor(editorInfo, topLine, 0);
421
460
  setVisualMode(null);
@@ -425,13 +464,23 @@ const handleVisualKey = (rep, editorInfo, key) => {
425
464
 
426
465
  if (bottomLine === totalLines - 1 && topLine > 0) {
427
466
  const prevLineLen = getLineText(rep, topLine - 1).length;
428
- replaceRange(editorInfo, [topLine - 1, prevLineLen], [bottomLine, getLineText(rep, bottomLine).length], '');
467
+ replaceRange(
468
+ editorInfo,
469
+ [topLine - 1, prevLineLen],
470
+ [bottomLine, getLineText(rep, bottomLine).length],
471
+ "",
472
+ );
429
473
  moveBlockCursor(editorInfo, topLine - 1, 0);
430
474
  } else if (bottomLine < totalLines - 1) {
431
- replaceRange(editorInfo, [topLine, 0], [bottomLine + 1, 0], '');
475
+ replaceRange(editorInfo, [topLine, 0], [bottomLine + 1, 0], "");
432
476
  moveBlockCursor(editorInfo, topLine, 0);
433
477
  } else {
434
- replaceRange(editorInfo, [0, 0], [bottomLine, getLineText(rep, bottomLine).length], '');
478
+ replaceRange(
479
+ editorInfo,
480
+ [0, 0],
481
+ [bottomLine, getLineText(rep, bottomLine).length],
482
+ "",
483
+ );
435
484
  moveBlockCursor(editorInfo, 0, 0);
436
485
  }
437
486
 
@@ -450,11 +499,11 @@ const handleNormalKey = (rep, editorInfo, key) => {
450
499
  const lineCount = rep.lines.length();
451
500
  const lineText = getLineText(rep, line);
452
501
 
453
- if (key >= '1' && key <= '9') {
502
+ if (key >= "1" && key <= "9") {
454
503
  countBuffer += key;
455
504
  return true;
456
505
  }
457
- if (key === '0' && countBuffer !== '') {
506
+ if (key === "0" && countBuffer !== "") {
458
507
  countBuffer += key;
459
508
  return true;
460
509
  }
@@ -462,7 +511,7 @@ const handleNormalKey = (rep, editorInfo, key) => {
462
511
  consumeCount();
463
512
  const count = getCount();
464
513
 
465
- if (pendingKey === 'r') {
514
+ if (pendingKey === "r") {
466
515
  pendingKey = null;
467
516
  if (lineText.length > 0) {
468
517
  replaceRange(editorInfo, [line, char], [line, char + 1], key);
@@ -471,64 +520,61 @@ const handleNormalKey = (rep, editorInfo, key) => {
471
520
  return true;
472
521
  }
473
522
 
474
- if (pendingKey === 'f' || pendingKey === 'F' || pendingKey === 't' || pendingKey === 'T') {
523
+ if (
524
+ pendingKey === "f" ||
525
+ pendingKey === "F" ||
526
+ pendingKey === "t" ||
527
+ pendingKey === "T"
528
+ ) {
475
529
  const direction = pendingKey;
476
530
  pendingKey = null;
477
- let pos = -1;
478
- if (direction === 'f') {
479
- pos = findCharForward(lineText, char, key, count);
480
- } else if (direction === 'F') {
481
- pos = findCharBackward(lineText, char, key, count);
482
- } else if (direction === 't') {
483
- pos = findCharForward(lineText, char, key, count);
484
- if (pos !== -1) pos = pos - 1;
485
- } else if (direction === 'T') {
486
- pos = findCharBackward(lineText, char, key, count);
487
- if (pos !== -1) pos = pos + 1;
531
+ lastCharSearch = { direction, target: key };
532
+ const pos = charSearchPos(direction, lineText, char, key, count);
533
+ if (pos !== -1) {
534
+ desiredColumn = null;
535
+ moveBlockCursor(editorInfo, line, pos);
488
536
  }
489
- if (pos !== -1) moveBlockCursor(editorInfo, line, pos);
490
537
  return true;
491
538
  }
492
539
 
493
- if (pendingKey === 'df' || pendingKey === 'dF' || pendingKey === 'dt' || pendingKey === 'dT') {
540
+ if (
541
+ pendingKey === "df" ||
542
+ pendingKey === "dF" ||
543
+ pendingKey === "dt" ||
544
+ pendingKey === "dT"
545
+ ) {
494
546
  const motion = pendingKey[1];
495
547
  pendingKey = null;
496
- let pos = -1;
497
- if (motion === 'f' || motion === 't') {
498
- pos = findCharForward(lineText, char, key, count);
499
- } else {
500
- pos = findCharBackward(lineText, char, key, count);
501
- }
548
+ const searchDir = motion === "f" || motion === "t" ? motion : motion;
549
+ const pos = charSearchPos(searchDir, lineText, char, key, count);
502
550
  if (pos !== -1) {
503
- let delStart = char;
504
- let delEnd = char;
505
- if (motion === 'f') {
506
- delStart = char;
507
- delEnd = pos + 1;
508
- } else if (motion === 't') {
509
- delStart = char;
510
- delEnd = pos;
511
- } else if (motion === 'F') {
512
- delStart = pos;
513
- delEnd = char + 1;
514
- } else if (motion === 'T') {
515
- delStart = pos + 1;
516
- delEnd = char + 1;
517
- }
518
- if (delEnd > delStart) {
519
- setRegister(lineText.slice(delStart, delEnd));
520
- replaceRange(editorInfo, [line, delStart], [line, delEnd], '');
551
+ const range = charMotionRange(motion, char, pos);
552
+ if (range) {
553
+ setRegister(lineText.slice(range.start, range.end));
554
+ replaceRange(editorInfo, [line, range.start], [line, range.end], "");
521
555
  const newLineText = getLineText(rep, line);
522
- moveBlockCursor(editorInfo, line, clampChar(delStart, newLineText));
556
+ moveBlockCursor(editorInfo, line, clampChar(range.start, newLineText));
523
557
  }
524
558
  }
525
559
  return true;
526
560
  }
527
561
 
528
- if (pendingKey === 'd') {
562
+ if (pendingKey === "di") {
563
+ pendingKey = null;
564
+ const range = innerTextObjectRange(key, lineText, char);
565
+ if (range) {
566
+ setRegister(lineText.slice(range.start, range.end));
567
+ replaceRange(editorInfo, [line, range.start], [line, range.end], "");
568
+ const newLineText = getLineText(rep, line);
569
+ moveBlockCursor(editorInfo, line, clampChar(range.start, newLineText));
570
+ }
571
+ return true;
572
+ }
573
+
574
+ if (pendingKey === "d") {
529
575
  pendingKey = null;
530
576
 
531
- if (key === 'd') {
577
+ if (key === "d") {
532
578
  const deleteCount = Math.min(count, lineCount - line);
533
579
  const lastDeleteLine = line + deleteCount - 1;
534
580
  const deletedLines = [];
@@ -538,201 +584,144 @@ const handleNormalKey = (rep, editorInfo, key) => {
538
584
  setRegister(deletedLines);
539
585
  if (lastDeleteLine === lineCount - 1 && line > 0) {
540
586
  const prevLineText = getLineText(rep, line - 1);
541
- replaceRange(editorInfo, [line - 1, prevLineText.length], [lastDeleteLine, getLineText(rep, lastDeleteLine).length], '');
587
+ replaceRange(
588
+ editorInfo,
589
+ [line - 1, prevLineText.length],
590
+ [lastDeleteLine, getLineText(rep, lastDeleteLine).length],
591
+ "",
592
+ );
542
593
  moveBlockCursor(editorInfo, line - 1, clampChar(char, prevLineText));
543
594
  } else if (lineCount > deleteCount) {
544
- replaceRange(editorInfo, [line, 0], [lastDeleteLine + 1, 0], '');
595
+ replaceRange(editorInfo, [line, 0], [lastDeleteLine + 1, 0], "");
545
596
  const newLineText = getLineText(rep, line);
546
597
  moveBlockCursor(editorInfo, line, clampChar(char, newLineText));
547
598
  } else {
548
- replaceRange(editorInfo, [0, 0], [lastDeleteLine, getLineText(rep, lastDeleteLine).length], '');
599
+ replaceRange(
600
+ editorInfo,
601
+ [0, 0],
602
+ [lastDeleteLine, getLineText(rep, lastDeleteLine).length],
603
+ "",
604
+ );
549
605
  moveBlockCursor(editorInfo, 0, 0);
550
606
  }
551
607
  return true;
552
608
  }
553
609
 
554
- if (key === 'f' || key === 'F' || key === 't' || key === 'T') {
555
- pendingKey = 'd' + key;
610
+ if (key === "i") {
611
+ pendingKey = "di";
556
612
  return true;
557
613
  }
558
614
 
559
- let delStart = -1;
560
- let delEnd = -1;
561
-
562
- if (key === 'w') {
563
- let pos = char;
564
- for (let i = 0; i < count; i++) pos = wordForward(lineText, pos);
565
- delStart = char;
566
- delEnd = Math.min(pos, lineText.length);
567
- } else if (key === 'e') {
568
- let pos = char;
569
- for (let i = 0; i < count; i++) pos = wordEnd(lineText, pos);
570
- delStart = char;
571
- delEnd = Math.min(pos + 1, lineText.length);
572
- } else if (key === 'b') {
573
- let pos = char;
574
- for (let i = 0; i < count; i++) pos = wordBackward(lineText, pos);
575
- delStart = pos;
576
- delEnd = char;
577
- } else if (key === '$') {
578
- delStart = char;
579
- delEnd = lineText.length;
580
- } else if (key === '0') {
581
- delStart = 0;
582
- delEnd = char;
583
- } else if (key === '^') {
584
- const fnb = firstNonBlank(lineText);
585
- delStart = Math.min(char, fnb);
586
- delEnd = Math.max(char, fnb);
587
- } else if (key === 'h') {
588
- delStart = Math.max(0, char - count);
589
- delEnd = char;
590
- } else if (key === 'l') {
591
- delStart = char;
592
- delEnd = Math.min(char + count, lineText.length);
615
+ if (key === "f" || key === "F" || key === "t" || key === "T") {
616
+ pendingKey = "d" + key;
617
+ return true;
593
618
  }
594
619
 
595
- if (delEnd > delStart && delStart !== -1) {
596
- setRegister(lineText.slice(delStart, delEnd));
597
- replaceRange(editorInfo, [line, delStart], [line, delEnd], '');
620
+ const range = motionRange(key, char, lineText, count);
621
+ if (range && range.end > range.start) {
622
+ setRegister(lineText.slice(range.start, range.end));
623
+ replaceRange(editorInfo, [line, range.start], [line, range.end], "");
598
624
  const newLineText = getLineText(rep, line);
599
- moveBlockCursor(editorInfo, line, clampChar(delStart, newLineText));
625
+ moveBlockCursor(editorInfo, line, clampChar(range.start, newLineText));
600
626
  }
601
627
  return true;
602
628
  }
603
629
 
604
- if (pendingKey === 'yf' || pendingKey === 'yF' || pendingKey === 'yt' || pendingKey === 'yT') {
630
+ if (
631
+ pendingKey === "yf" ||
632
+ pendingKey === "yF" ||
633
+ pendingKey === "yt" ||
634
+ pendingKey === "yT"
635
+ ) {
605
636
  const motion = pendingKey[1];
606
637
  pendingKey = null;
607
- let pos = -1;
608
- if (motion === 'f' || motion === 't') {
609
- pos = findCharForward(lineText, char, key, count);
610
- } else {
611
- pos = findCharBackward(lineText, char, key, count);
612
- }
638
+ const pos = charSearchPos(motion, lineText, char, key, count);
613
639
  if (pos !== -1) {
614
- let yankStart = char;
615
- let yankEnd = char;
616
- if (motion === 'f') {
617
- yankStart = char;
618
- yankEnd = pos + 1;
619
- } else if (motion === 't') {
620
- yankStart = char;
621
- yankEnd = pos;
622
- } else if (motion === 'F') {
623
- yankStart = pos;
624
- yankEnd = char + 1;
625
- } else if (motion === 'T') {
626
- yankStart = pos + 1;
627
- yankEnd = char + 1;
628
- }
629
- if (yankEnd > yankStart) {
630
- setRegister(lineText.slice(yankStart, yankEnd));
640
+ const range = charMotionRange(motion, char, pos);
641
+ if (range) {
642
+ setRegister(lineText.slice(range.start, range.end));
631
643
  }
632
644
  }
633
645
  return true;
634
646
  }
635
647
 
636
- if (pendingKey === 'cf' || pendingKey === 'cF' || pendingKey === 'ct' || pendingKey === 'cT') {
648
+ if (
649
+ pendingKey === "cf" ||
650
+ pendingKey === "cF" ||
651
+ pendingKey === "ct" ||
652
+ pendingKey === "cT"
653
+ ) {
637
654
  const motion = pendingKey[1];
638
655
  pendingKey = null;
639
- let pos = -1;
640
- if (motion === 'f' || motion === 't') {
641
- pos = findCharForward(lineText, char, key, count);
642
- } else {
643
- pos = findCharBackward(lineText, char, key, count);
644
- }
656
+ const pos = charSearchPos(motion, lineText, char, key, count);
645
657
  if (pos !== -1) {
646
- let delStart = char;
647
- let delEnd = char;
648
- if (motion === 'f') {
649
- delStart = char;
650
- delEnd = pos + 1;
651
- } else if (motion === 't') {
652
- delStart = char;
653
- delEnd = pos;
654
- } else if (motion === 'F') {
655
- delStart = pos;
656
- delEnd = char + 1;
657
- } else if (motion === 'T') {
658
- delStart = pos + 1;
659
- delEnd = char + 1;
660
- }
661
- if (delEnd > delStart) {
662
- setRegister(lineText.slice(delStart, delEnd));
663
- replaceRange(editorInfo, [line, delStart], [line, delEnd], '');
664
- moveCursor(editorInfo, line, delStart);
658
+ const range = charMotionRange(motion, char, pos);
659
+ if (range) {
660
+ setRegister(lineText.slice(range.start, range.end));
661
+ replaceRange(editorInfo, [line, range.start], [line, range.end], "");
662
+ moveCursor(editorInfo, line, range.start);
665
663
  setInsertMode(true);
666
664
  }
667
665
  }
668
666
  return true;
669
667
  }
670
668
 
671
- if (pendingKey === 'c') {
669
+ if (pendingKey === "ci") {
672
670
  pendingKey = null;
671
+ const range = innerTextObjectRange(key, lineText, char);
672
+ if (range) {
673
+ setRegister(lineText.slice(range.start, range.end));
674
+ replaceRange(editorInfo, [line, range.start], [line, range.end], "");
675
+ moveCursor(editorInfo, line, range.start);
676
+ setInsertMode(true);
677
+ }
678
+ return true;
679
+ }
673
680
 
674
- if (key === 'c') {
681
+ if (pendingKey === "c") {
682
+ pendingKey = null;
683
+
684
+ if (key === "c") {
675
685
  setRegister(lineText);
676
- replaceRange(editorInfo, [line, 0], [line, lineText.length], '');
686
+ replaceRange(editorInfo, [line, 0], [line, lineText.length], "");
677
687
  moveCursor(editorInfo, line, 0);
678
688
  setInsertMode(true);
679
689
  return true;
680
690
  }
681
691
 
682
- if (key === 'f' || key === 'F' || key === 't' || key === 'T') {
683
- pendingKey = 'c' + key;
692
+ if (key === "i") {
693
+ pendingKey = "ci";
684
694
  return true;
685
695
  }
686
696
 
687
- let delStart = -1;
688
- let delEnd = -1;
689
-
690
- if (key === 'w') {
691
- let pos = char;
692
- for (let i = 0; i < count; i++) pos = wordForward(lineText, pos);
693
- delStart = char;
694
- delEnd = Math.min(pos, lineText.length);
695
- } else if (key === 'e') {
696
- let pos = char;
697
- for (let i = 0; i < count; i++) pos = wordEnd(lineText, pos);
698
- delStart = char;
699
- delEnd = Math.min(pos + 1, lineText.length);
700
- } else if (key === 'b') {
701
- let pos = char;
702
- for (let i = 0; i < count; i++) pos = wordBackward(lineText, pos);
703
- delStart = pos;
704
- delEnd = char;
705
- } else if (key === '$') {
706
- delStart = char;
707
- delEnd = lineText.length;
708
- } else if (key === '0') {
709
- delStart = 0;
710
- delEnd = char;
711
- } else if (key === '^') {
712
- const fnb = firstNonBlank(lineText);
713
- delStart = Math.min(char, fnb);
714
- delEnd = Math.max(char, fnb);
715
- } else if (key === 'h') {
716
- delStart = Math.max(0, char - count);
717
- delEnd = char;
718
- } else if (key === 'l') {
719
- delStart = char;
720
- delEnd = Math.min(char + count, lineText.length);
697
+ if (key === "f" || key === "F" || key === "t" || key === "T") {
698
+ pendingKey = "c" + key;
699
+ return true;
721
700
  }
722
701
 
723
- if (delEnd > delStart && delStart !== -1) {
724
- setRegister(lineText.slice(delStart, delEnd));
725
- replaceRange(editorInfo, [line, delStart], [line, delEnd], '');
726
- moveCursor(editorInfo, line, delStart);
702
+ const range = motionRange(key, char, lineText, count);
703
+ if (range && range.end > range.start) {
704
+ setRegister(lineText.slice(range.start, range.end));
705
+ replaceRange(editorInfo, [line, range.start], [line, range.end], "");
706
+ moveCursor(editorInfo, line, range.start);
727
707
  setInsertMode(true);
728
708
  }
729
709
  return true;
730
710
  }
731
711
 
732
- if (pendingKey === 'y') {
712
+ if (pendingKey === "yi") {
713
+ pendingKey = null;
714
+ const range = innerTextObjectRange(key, lineText, char);
715
+ if (range) {
716
+ setRegister(lineText.slice(range.start, range.end));
717
+ }
718
+ return true;
719
+ }
720
+
721
+ if (pendingKey === "y") {
733
722
  pendingKey = null;
734
723
 
735
- if (key === 'y') {
724
+ if (key === "y") {
736
725
  const yankCount = Math.min(count, lineCount - line);
737
726
  const lastYankLine = line + yankCount - 1;
738
727
  const yankedLines = [];
@@ -743,66 +732,37 @@ const handleNormalKey = (rep, editorInfo, key) => {
743
732
  return true;
744
733
  }
745
734
 
746
- if (key === 'f' || key === 'F' || key === 't' || key === 'T') {
747
- pendingKey = 'y' + key;
735
+ if (key === "i") {
736
+ pendingKey = "yi";
748
737
  return true;
749
738
  }
750
739
 
751
- let yankStart = -1;
752
- let yankEnd = -1;
753
-
754
- if (key === 'w') {
755
- let pos = char;
756
- for (let i = 0; i < count; i++) pos = wordForward(lineText, pos);
757
- yankStart = char;
758
- yankEnd = Math.min(pos, lineText.length);
759
- } else if (key === 'e') {
760
- let pos = char;
761
- for (let i = 0; i < count; i++) pos = wordEnd(lineText, pos);
762
- yankStart = char;
763
- yankEnd = Math.min(pos + 1, lineText.length);
764
- } else if (key === 'b') {
765
- let pos = char;
766
- for (let i = 0; i < count; i++) pos = wordBackward(lineText, pos);
767
- yankStart = pos;
768
- yankEnd = char;
769
- } else if (key === '$') {
770
- yankStart = char;
771
- yankEnd = lineText.length;
772
- } else if (key === '0') {
773
- yankStart = 0;
774
- yankEnd = char;
775
- } else if (key === '^') {
776
- const fnb = firstNonBlank(lineText);
777
- yankStart = Math.min(char, fnb);
778
- yankEnd = Math.max(char, fnb);
779
- } else if (key === 'h') {
780
- yankStart = Math.max(0, char - count);
781
- yankEnd = char;
782
- } else if (key === 'l') {
783
- yankStart = char;
784
- yankEnd = Math.min(char + count, lineText.length);
740
+ if (key === "f" || key === "F" || key === "t" || key === "T") {
741
+ pendingKey = "y" + key;
742
+ return true;
785
743
  }
786
744
 
787
- if (yankEnd > yankStart && yankStart !== -1) {
788
- setRegister(lineText.slice(yankStart, yankEnd));
745
+ const range = motionRange(key, char, lineText, count);
746
+ if (range && range.end > range.start) {
747
+ setRegister(lineText.slice(range.start, range.end));
789
748
  }
790
749
  return true;
791
750
  }
792
751
 
793
- if (pendingKey === 'm') {
752
+ if (pendingKey === "m") {
794
753
  pendingKey = null;
795
- if (key >= 'a' && key <= 'z') {
754
+ if (key >= "a" && key <= "z") {
796
755
  marks[key] = [line, char];
797
756
  }
798
757
  return true;
799
758
  }
800
759
 
801
- if (pendingKey === "'" || pendingKey === '`') {
760
+ if (pendingKey === "'" || pendingKey === "`") {
802
761
  const jumpType = pendingKey;
803
762
  pendingKey = null;
804
- if (key >= 'a' && key <= 'z' && marks[key]) {
763
+ if (key >= "a" && key <= "z" && marks[key]) {
805
764
  const [markLine, markChar] = marks[key];
765
+ desiredColumn = null;
806
766
  if (jumpType === "'") {
807
767
  const targetLineText = getLineText(rep, markLine);
808
768
  moveBlockCursor(editorInfo, markLine, firstNonBlank(targetLineText));
@@ -813,118 +773,146 @@ const handleNormalKey = (rep, editorInfo, key) => {
813
773
  return true;
814
774
  }
815
775
 
816
- if (key === 'h') {
776
+ if (key === "h") {
777
+ desiredColumn = null;
817
778
  moveBlockCursor(editorInfo, line, Math.max(0, char - count));
818
779
  return true;
819
780
  }
820
781
 
821
- if (key === 'l') {
782
+ if (key === "l") {
783
+ desiredColumn = null;
822
784
  moveBlockCursor(editorInfo, line, clampChar(char + count, lineText));
823
785
  return true;
824
786
  }
825
787
 
826
- if (key === 'k') {
788
+ if (key === "k") {
789
+ if (desiredColumn === null) {
790
+ desiredColumn = char;
791
+ }
827
792
  const newLine = clampLine(line - count, rep);
828
793
  const newLineText = getLineText(rep, newLine);
829
- moveBlockCursor(editorInfo, newLine, clampChar(char, newLineText));
794
+ moveBlockCursor(editorInfo, newLine, clampChar(desiredColumn, newLineText));
830
795
  return true;
831
796
  }
832
797
 
833
- if (key === 'j') {
798
+ if (key === "j") {
799
+ if (desiredColumn === null) {
800
+ desiredColumn = char;
801
+ }
834
802
  const newLine = clampLine(line + count, rep);
835
803
  const newLineText = getLineText(rep, newLine);
836
- moveBlockCursor(editorInfo, newLine, clampChar(char, newLineText));
804
+ moveBlockCursor(editorInfo, newLine, clampChar(desiredColumn, newLineText));
837
805
  return true;
838
806
  }
839
807
 
840
- if (key === '0') {
808
+ if (key === "0") {
809
+ desiredColumn = null;
841
810
  moveBlockCursor(editorInfo, line, 0);
842
811
  return true;
843
812
  }
844
813
 
845
- if (key === '$') {
814
+ if (key === "$") {
815
+ desiredColumn = null;
846
816
  moveBlockCursor(editorInfo, line, clampChar(lineText.length - 1, lineText));
847
817
  return true;
848
818
  }
849
819
 
850
- if (key === '^') {
820
+ if (key === "^") {
821
+ desiredColumn = null;
851
822
  moveBlockCursor(editorInfo, line, firstNonBlank(lineText));
852
823
  return true;
853
824
  }
854
825
 
855
- if (key === 'x') {
826
+ if (key === "x") {
856
827
  if (lineText.length > 0) {
857
828
  const deleteCount = Math.min(count, lineText.length - char);
858
- replaceRange(editorInfo, [line, char], [line, char + deleteCount], '');
829
+ replaceRange(editorInfo, [line, char], [line, char + deleteCount], "");
859
830
  const newLineText = getLineText(rep, line);
860
831
  moveBlockCursor(editorInfo, line, clampChar(char, newLineText));
861
832
  }
862
833
  return true;
863
834
  }
864
835
 
865
- if (key === 'w') {
836
+ if (key === "w") {
837
+ desiredColumn = null;
866
838
  let pos = char;
867
839
  for (let i = 0; i < count; i++) pos = wordForward(lineText, pos);
868
840
  moveBlockCursor(editorInfo, line, clampChar(pos, lineText));
869
841
  return true;
870
842
  }
871
843
 
872
- if (key === 'b') {
844
+ if (key === "b") {
845
+ desiredColumn = null;
873
846
  let pos = char;
874
847
  for (let i = 0; i < count; i++) pos = wordBackward(lineText, pos);
875
848
  moveBlockCursor(editorInfo, line, pos);
876
849
  return true;
877
850
  }
878
851
 
879
- if (key === 'o') {
880
- replaceRange(editorInfo, [line, lineText.length], [line, lineText.length], '\n');
852
+ if (key === "o") {
853
+ replaceRange(
854
+ editorInfo,
855
+ [line, lineText.length],
856
+ [line, lineText.length],
857
+ "\n",
858
+ );
881
859
  moveCursor(editorInfo, line + 1, 0);
882
860
  setInsertMode(true);
883
861
  return true;
884
862
  }
885
863
 
886
- if (key === 'O') {
887
- replaceRange(editorInfo, [line, 0], [line, 0], '\n');
864
+ if (key === "O") {
865
+ replaceRange(editorInfo, [line, 0], [line, 0], "\n");
888
866
  moveCursor(editorInfo, line, 0);
889
867
  setInsertMode(true);
890
868
  return true;
891
869
  }
892
870
 
893
- if (key === 'u') {
871
+ if (key === "u") {
894
872
  undo(editorInfo);
895
873
  return true;
896
874
  }
897
875
 
898
- if (key === 'p') {
876
+ if (key === "p") {
899
877
  if (register !== null) {
900
- if (typeof register === 'string') {
878
+ if (typeof register === "string") {
901
879
  const insertPos = Math.min(char + 1, lineText.length);
902
880
  const repeated = register.repeat(count);
903
- replaceRange(editorInfo, [line, insertPos], [line, insertPos], repeated);
881
+ replaceRange(
882
+ editorInfo,
883
+ [line, insertPos],
884
+ [line, insertPos],
885
+ repeated,
886
+ );
904
887
  moveBlockCursor(editorInfo, line, insertPos);
905
888
  } else {
906
- const block = register.join('\n');
889
+ const block = register.join("\n");
907
890
  const parts = [];
908
891
  for (let i = 0; i < count; i++) parts.push(block);
909
- const insertText = '\n' + parts.join('\n');
910
- replaceRange(editorInfo, [line, lineText.length], [line, lineText.length], insertText);
892
+ const insertText = "\n" + parts.join("\n");
893
+ replaceRange(
894
+ editorInfo,
895
+ [line, lineText.length],
896
+ [line, lineText.length],
897
+ insertText,
898
+ );
911
899
  moveBlockCursor(editorInfo, line + 1, 0);
912
900
  }
913
901
  }
914
902
  return true;
915
903
  }
916
904
 
917
- if (key === 'P') {
905
+ if (key === "P") {
918
906
  if (register !== null) {
919
- if (typeof register === 'string') {
907
+ if (typeof register === "string") {
920
908
  const repeated = register.repeat(count);
921
909
  replaceRange(editorInfo, [line, char], [line, char], repeated);
922
910
  moveBlockCursor(editorInfo, line, char);
923
911
  } else {
924
- const block = register.join('\n');
912
+ const block = register.join("\n");
925
913
  const parts = [];
926
914
  for (let i = 0; i < count; i++) parts.push(block);
927
- const insertText = parts.join('\n') + '\n';
915
+ const insertText = parts.join("\n") + "\n";
928
916
  replaceRange(editorInfo, [line, 0], [line, 0], insertText);
929
917
  moveBlockCursor(editorInfo, line, 0);
930
918
  }
@@ -932,7 +920,8 @@ const handleNormalKey = (rep, editorInfo, key) => {
932
920
  return true;
933
921
  }
934
922
 
935
- if (key === 'G') {
923
+ if (key === "G") {
924
+ desiredColumn = null;
936
925
  if (pendingCount !== null) {
937
926
  moveBlockCursor(editorInfo, clampLine(pendingCount - 1, rep), 0);
938
927
  } else {
@@ -941,100 +930,191 @@ const handleNormalKey = (rep, editorInfo, key) => {
941
930
  return true;
942
931
  }
943
932
 
944
- if (key === 'g') {
945
- if (pendingKey === 'g') {
933
+ if (key === "g") {
934
+ if (pendingKey === "g") {
946
935
  pendingKey = null;
936
+ desiredColumn = null;
947
937
  moveBlockCursor(editorInfo, 0, 0);
948
938
  } else {
949
- pendingKey = 'g';
939
+ pendingKey = "g";
950
940
  }
951
941
  return true;
952
942
  }
953
943
 
954
- if (key === 'r') {
944
+ if (key === "r") {
955
945
  if (lineText.length > 0) {
956
- pendingKey = 'r';
946
+ pendingKey = "r";
957
947
  }
958
948
  return true;
959
949
  }
960
950
 
961
- if (key === 'f' || key === 'F' || key === 't' || key === 'T') {
951
+ if (key === "f" || key === "F" || key === "t" || key === "T") {
962
952
  pendingKey = key;
963
953
  return true;
964
954
  }
965
955
 
966
- if (key === 'm') {
967
- pendingKey = 'm';
956
+ if (key === "m") {
957
+ pendingKey = "m";
968
958
  return true;
969
959
  }
970
960
 
971
- if (key === "'" || key === '`') {
961
+ if (key === "'" || key === "`") {
972
962
  pendingKey = key;
973
963
  return true;
974
964
  }
975
965
 
976
- if (key === 'd') {
977
- pendingKey = 'd';
966
+ if (key === "d") {
967
+ pendingKey = "d";
978
968
  return true;
979
969
  }
980
970
 
981
- if (key === 'c') {
982
- pendingKey = 'c';
971
+ if (key === "c") {
972
+ pendingKey = "c";
983
973
  return true;
984
974
  }
985
975
 
986
- if (key === 'y') {
987
- pendingKey = 'y';
976
+ if (key === "y") {
977
+ pendingKey = "y";
988
978
  return true;
989
979
  }
990
980
 
991
- if (key === 'Y') {
981
+ if (key === "Y") {
992
982
  setRegister([lineText]);
993
983
  return true;
994
984
  }
995
985
 
996
- if (key === 'J') {
986
+ if (key === "J") {
997
987
  const joins = Math.min(count, lineCount - 1 - line);
998
988
  let cursorChar = lineText.length;
999
989
  for (let i = 0; i < joins; i++) {
1000
990
  const curLineText = getLineText(rep, line);
1001
991
  const nextLineText = getLineText(rep, line + 1);
1002
- const trimmedNext = nextLineText.replace(/^\s+/, '');
1003
- const separator = curLineText.length === 0 ? '' : ' ';
992
+ const trimmedNext = nextLineText.replace(/^\s+/, "");
993
+ const separator = curLineText.length === 0 ? "" : " ";
1004
994
  if (i === 0) cursorChar = curLineText.length;
1005
- replaceRange(editorInfo, [line, curLineText.length], [line + 1, nextLineText.length], separator + trimmedNext);
995
+ replaceRange(
996
+ editorInfo,
997
+ [line, curLineText.length],
998
+ [line + 1, nextLineText.length],
999
+ separator + trimmedNext,
1000
+ );
1006
1001
  }
1007
1002
  moveBlockCursor(editorInfo, line, cursorChar);
1008
1003
  return true;
1009
1004
  }
1010
1005
 
1011
- if (key === 'C') {
1006
+ if (key === "~") {
1007
+ if (lineText.length > 0) {
1008
+ const toggleCount = Math.min(count, lineText.length - char);
1009
+ const slice = lineText.slice(char, char + toggleCount);
1010
+ let toggled = "";
1011
+ for (let i = 0; i < slice.length; i++) {
1012
+ const ch = slice[i];
1013
+ toggled +=
1014
+ ch === ch.toLowerCase() ? ch.toUpperCase() : ch.toLowerCase();
1015
+ }
1016
+ replaceRange(
1017
+ editorInfo,
1018
+ [line, char],
1019
+ [line, char + toggleCount],
1020
+ toggled,
1021
+ );
1022
+ const newChar = Math.min(char + toggleCount, lineText.length - 1);
1023
+ moveBlockCursor(editorInfo, line, newChar);
1024
+ }
1025
+ return true;
1026
+ }
1027
+
1028
+ if (key === "D") {
1012
1029
  setRegister(lineText.slice(char));
1013
- replaceRange(editorInfo, [line, char], [line, lineText.length], '');
1030
+ replaceRange(editorInfo, [line, char], [line, lineText.length], "");
1031
+ const newLineText = getLineText(rep, line);
1032
+ moveBlockCursor(editorInfo, line, clampChar(char, newLineText));
1033
+ return true;
1034
+ }
1035
+
1036
+ if (key === "C") {
1037
+ setRegister(lineText.slice(char));
1038
+ replaceRange(editorInfo, [line, char], [line, lineText.length], "");
1014
1039
  moveCursor(editorInfo, line, char);
1015
1040
  setInsertMode(true);
1016
1041
  return true;
1017
1042
  }
1018
1043
 
1019
- if (key === 's') {
1044
+ if (key === "s") {
1020
1045
  setRegister(lineText.slice(char, char + 1));
1021
- replaceRange(editorInfo, [line, char], [line, Math.min(char + count, lineText.length)], '');
1046
+ replaceRange(
1047
+ editorInfo,
1048
+ [line, char],
1049
+ [line, Math.min(char + count, lineText.length)],
1050
+ "",
1051
+ );
1022
1052
  moveCursor(editorInfo, line, char);
1023
1053
  setInsertMode(true);
1024
1054
  return true;
1025
1055
  }
1026
1056
 
1027
- if (key === 'S') {
1057
+ if (key === "S") {
1028
1058
  setRegister(lineText);
1029
- replaceRange(editorInfo, [line, 0], [line, lineText.length], '');
1059
+ replaceRange(editorInfo, [line, 0], [line, lineText.length], "");
1030
1060
  moveCursor(editorInfo, line, 0);
1031
1061
  setInsertMode(true);
1032
1062
  return true;
1033
1063
  }
1034
1064
 
1065
+ if (key === ";") {
1066
+ if (lastCharSearch) {
1067
+ const pos = charSearchPos(
1068
+ lastCharSearch.direction,
1069
+ lineText,
1070
+ char,
1071
+ lastCharSearch.target,
1072
+ count,
1073
+ );
1074
+ if (pos !== -1) {
1075
+ desiredColumn = null;
1076
+ moveBlockCursor(editorInfo, line, pos);
1077
+ }
1078
+ }
1079
+ return true;
1080
+ }
1081
+
1082
+ if (key === ",") {
1083
+ if (lastCharSearch) {
1084
+ const opposite = { f: "F", F: "f", t: "T", T: "t" };
1085
+ const reverseDir = opposite[lastCharSearch.direction];
1086
+ const pos = charSearchPos(
1087
+ reverseDir,
1088
+ lineText,
1089
+ char,
1090
+ lastCharSearch.target,
1091
+ count,
1092
+ );
1093
+ if (pos !== -1) {
1094
+ desiredColumn = null;
1095
+ moveBlockCursor(editorInfo, line, pos);
1096
+ }
1097
+ }
1098
+ return true;
1099
+ }
1100
+
1101
+ if (key === "}") {
1102
+ desiredColumn = null;
1103
+ const target = paragraphForward(rep, line, count);
1104
+ moveBlockCursor(editorInfo, target, 0);
1105
+ return true;
1106
+ }
1107
+
1108
+ if (key === "{") {
1109
+ desiredColumn = null;
1110
+ const target = paragraphBackward(rep, line, count);
1111
+ moveBlockCursor(editorInfo, target, 0);
1112
+ return true;
1113
+ }
1114
+
1035
1115
  pendingKey = null;
1036
1116
 
1037
- if (key === 'e') {
1117
+ if (key === "e") {
1038
1118
  let pos = char;
1039
1119
  for (let i = 0; i < count; i++) pos = wordEnd(lineText, pos);
1040
1120
  moveBlockCursor(editorInfo, line, clampChar(pos, lineText));
@@ -1046,22 +1126,33 @@ const handleNormalKey = (rep, editorInfo, key) => {
1046
1126
 
1047
1127
  // --- Exports ---
1048
1128
 
1049
- exports.aceEditorCSS = () => ['ep_vim/static/css/vim.css'];
1129
+ exports.aceEditorCSS = () => ["ep_vim/static/css/vim.css"];
1050
1130
 
1051
1131
  exports.postToolbarInit = (_hookName, _args) => {
1052
- const btn = document.getElementById('vim-toggle-btn');
1132
+ const btn = document.getElementById("vim-toggle-btn");
1053
1133
  if (!btn) return;
1054
- btn.classList.toggle('vim-enabled', vimEnabled);
1055
- btn.addEventListener('click', () => {
1134
+ btn.classList.toggle("vim-enabled", vimEnabled);
1135
+ btn.addEventListener("click", () => {
1056
1136
  vimEnabled = !vimEnabled;
1057
- localStorage.setItem('ep_vimEnabled', vimEnabled ? 'true' : 'false');
1058
- btn.classList.toggle('vim-enabled', vimEnabled);
1137
+ localStorage.setItem("ep_vimEnabled", vimEnabled ? "true" : "false");
1138
+ btn.classList.toggle("vim-enabled", vimEnabled);
1139
+ });
1140
+ };
1141
+
1142
+ exports.postAceInit = (_hookName, { ace }) => {
1143
+ if (!vimEnabled) return;
1144
+ ace.callWithAce((aceTop) => {
1145
+ const rep = aceTop.ace_getRep();
1146
+ if (rep && rep.selStart) {
1147
+ currentRep = rep;
1148
+ selectRange(aceTop, rep.selStart, [rep.selStart[0], rep.selStart[1] + 1]);
1149
+ }
1059
1150
  });
1060
1151
  };
1061
1152
 
1062
- exports.aceKeyEvent = (_hookName, {evt, rep, editorInfo}) => {
1153
+ exports.aceKeyEvent = (_hookName, { evt, rep, editorInfo }) => {
1063
1154
  if (!vimEnabled) return false;
1064
- if (evt.type !== 'keydown') return false;
1155
+ if (evt.type !== "keydown") return false;
1065
1156
  currentRep = rep;
1066
1157
  if (!editorDoc) {
1067
1158
  editorDoc = evt.target.ownerDocument;
@@ -1080,8 +1171,9 @@ exports.aceKeyEvent = (_hookName, {evt, rep, editorInfo}) => {
1080
1171
  return handled || true;
1081
1172
  }
1082
1173
 
1083
- if (!insertMode && evt.key === 'i') {
1174
+ if (!insertMode && evt.key === "i") {
1084
1175
  const [line, char] = rep.selStart;
1176
+ desiredColumn = null;
1085
1177
  moveCursor(editorInfo, line, char);
1086
1178
  setVisualMode(null);
1087
1179
  setInsertMode(true);
@@ -1089,9 +1181,10 @@ exports.aceKeyEvent = (_hookName, {evt, rep, editorInfo}) => {
1089
1181
  return true;
1090
1182
  }
1091
1183
 
1092
- if (!insertMode && evt.key === 'a') {
1184
+ if (!insertMode && evt.key === "a") {
1093
1185
  const [line, char] = rep.selStart;
1094
1186
  const lineText = getLineText(rep, line);
1187
+ desiredColumn = null;
1095
1188
  moveCursor(editorInfo, line, Math.min(char + 1, lineText.length));
1096
1189
  setVisualMode(null);
1097
1190
  setInsertMode(true);
@@ -1099,9 +1192,10 @@ exports.aceKeyEvent = (_hookName, {evt, rep, editorInfo}) => {
1099
1192
  return true;
1100
1193
  }
1101
1194
 
1102
- if (!insertMode && evt.key === 'A') {
1195
+ if (!insertMode && evt.key === "A") {
1103
1196
  const [line] = rep.selStart;
1104
1197
  const lineText = getLineText(rep, line);
1198
+ desiredColumn = null;
1105
1199
  moveCursor(editorInfo, line, lineText.length);
1106
1200
  setVisualMode(null);
1107
1201
  setInsertMode(true);
@@ -1109,9 +1203,10 @@ exports.aceKeyEvent = (_hookName, {evt, rep, editorInfo}) => {
1109
1203
  return true;
1110
1204
  }
1111
1205
 
1112
- if (!insertMode && evt.key === 'I') {
1206
+ if (!insertMode && evt.key === "I") {
1113
1207
  const [line] = rep.selStart;
1114
1208
  const lineText = getLineText(rep, line);
1209
+ desiredColumn = null;
1115
1210
  moveCursor(editorInfo, line, firstNonBlank(lineText));
1116
1211
  setVisualMode(null);
1117
1212
  setInsertMode(true);
@@ -1119,7 +1214,7 @@ exports.aceKeyEvent = (_hookName, {evt, rep, editorInfo}) => {
1119
1214
  return true;
1120
1215
  }
1121
1216
 
1122
- if (evt.key === 'Escape') {
1217
+ if (evt.key === "Escape") {
1123
1218
  if (insertMode) {
1124
1219
  setInsertMode(false);
1125
1220
  const [line, char] = rep.selStart;
@@ -1130,28 +1225,29 @@ exports.aceKeyEvent = (_hookName, {evt, rep, editorInfo}) => {
1130
1225
  setVisualMode(null);
1131
1226
  moveBlockCursor(editorInfo, vLine, vChar);
1132
1227
  }
1133
- countBuffer = '';
1228
+ countBuffer = "";
1134
1229
  pendingKey = null;
1135
1230
  pendingCount = null;
1231
+ desiredColumn = null;
1136
1232
  evt.preventDefault();
1137
1233
  return true;
1138
1234
  }
1139
1235
 
1140
- if (!insertMode && visualMode === null && evt.key === 'V') {
1236
+ if (!insertMode && visualMode === null && evt.key === "V") {
1141
1237
  const [line] = rep.selStart;
1142
1238
  visualAnchor = [line, 0];
1143
1239
  visualCursor = [line, 0];
1144
- setVisualMode('line');
1240
+ setVisualMode("line");
1145
1241
  updateVisualSelection(editorInfo, rep);
1146
1242
  evt.preventDefault();
1147
1243
  return true;
1148
1244
  }
1149
1245
 
1150
- if (!insertMode && visualMode === null && evt.key === 'v') {
1246
+ if (!insertMode && visualMode === null && evt.key === "v") {
1151
1247
  const [line, char] = rep.selStart;
1152
1248
  visualAnchor = [line, char];
1153
1249
  visualCursor = [line, char];
1154
- setVisualMode('char');
1250
+ setVisualMode("char");
1155
1251
  updateVisualSelection(editorInfo, rep);
1156
1252
  evt.preventDefault();
1157
1253
  return true;