ep_vim 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/static/js/index.js +368 -736
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ep_vim",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Vim-mode plugin for Etherpad with modal editing, motions, and operators",
5
5
  "author": {
6
6
  "name": "Seth Rothschild",
@@ -28,6 +28,7 @@ let visualMode = null;
28
28
  let visualAnchor = null;
29
29
  let visualCursor = null;
30
30
  let pendingKey = null;
31
+ let pendingOperator = null;
31
32
  let pendingCount = null;
32
33
  let countBuffer = "";
33
34
  let register = null;
@@ -41,7 +42,7 @@ const QUOTE_CHARS = new Set(['"', "'"]);
41
42
  const BRACKET_CHARS = new Set(["(", ")", "{", "}", "[", "]"]);
42
43
 
43
44
  const textObjectRange = (key, lineText, char, type) => {
44
- if (key === "w") return textWordRange(lineText, char);
45
+ if (key === "w") return textWordRange(lineText, char, type);
45
46
  if (QUOTE_CHARS.has(key)) return textQuoteRange(lineText, char, key, type);
46
47
  if (BRACKET_CHARS.has(key))
47
48
  return textBracketRange(lineText, char, key, type);
@@ -53,7 +54,7 @@ const consumeCount = () => {
53
54
  if (countBuffer !== "") {
54
55
  pendingCount = parseInt(countBuffer, 10);
55
56
  countBuffer = "";
56
- } else if (pendingKey === null) {
57
+ } else if (pendingKey === null && pendingOperator === null) {
57
58
  pendingCount = null;
58
59
  }
59
60
  };
@@ -129,8 +130,6 @@ const setVisualMode = (value) => {
129
130
  }
130
131
  };
131
132
 
132
- // --- Visual mode helpers ---
133
-
134
133
  const updateVisualSelection = (editorInfo, rep) => {
135
134
  const [start, end] = getVisualSelection(
136
135
  visualMode,
@@ -141,42 +140,9 @@ const updateVisualSelection = (editorInfo, rep) => {
141
140
  selectRange(editorInfo, start, end);
142
141
  };
143
142
 
144
- // --- Visual mode key handler ---
145
-
146
- const handleVisualKey = (rep, editorInfo, key) => {
147
- const curLine = visualCursor[0];
148
- const curChar = visualCursor[1];
149
- const lineText = getLineText(rep, curLine);
150
-
151
- if (key === "i" || key === "a") {
152
- pendingKey = key;
153
- return true;
154
- }
155
-
156
- if (pendingKey === "i" || pendingKey === "a") {
157
- const range = textObjectRange(key, lineText, curChar, pendingKey);
158
- pendingKey = null;
159
- if (range) {
160
- visualAnchor = [curLine, range.start];
161
- visualCursor = [curLine, range.end];
162
- setVisualMode("char");
163
- updateVisualSelection(editorInfo, rep);
164
- }
165
- return true;
166
- }
167
-
168
- if (key >= "1" && key <= "9") {
169
- countBuffer += key;
170
- return true;
171
- }
172
- if (key === "0" && countBuffer !== "") {
173
- countBuffer += key;
174
- return true;
175
- }
176
-
177
- consumeCount();
178
- const count = getCount();
143
+ // --- Motion resolution (shared between normal and visual) ---
179
144
 
145
+ const resolveMotion = (key, line, char, lineText, rep, count) => {
180
146
  if (
181
147
  pendingKey === "f" ||
182
148
  pendingKey === "F" ||
@@ -186,13 +152,12 @@ const handleVisualKey = (rep, editorInfo, key) => {
186
152
  const direction = pendingKey;
187
153
  pendingKey = null;
188
154
  lastCharSearch = { direction, target: key };
189
- const pos = charSearchPos(direction, lineText, curChar, key, count);
155
+ const pos = charSearchPos(direction, lineText, char, key, count);
190
156
  if (pos !== -1) {
191
157
  desiredColumn = null;
192
- visualCursor = [curLine, pos];
193
- updateVisualSelection(editorInfo, rep);
158
+ return { line, char: pos };
194
159
  }
195
- return true;
160
+ return { line, char };
196
161
  }
197
162
 
198
163
  if (pendingKey === "'" || pendingKey === "`") {
@@ -203,97 +168,100 @@ const handleVisualKey = (rep, editorInfo, key) => {
203
168
  desiredColumn = null;
204
169
  if (jumpType === "'") {
205
170
  const targetLineText = getLineText(rep, markLine);
206
- visualCursor = [markLine, firstNonBlank(targetLineText)];
207
- } else {
208
- visualCursor = [markLine, markChar];
171
+ return { line: markLine, char: firstNonBlank(targetLineText) };
209
172
  }
210
- updateVisualSelection(editorInfo, rep);
173
+ return { line: markLine, char: markChar };
174
+ }
175
+ return { line, char };
176
+ }
177
+
178
+ if (pendingKey === "g") {
179
+ pendingKey = null;
180
+ if (key === "g") {
181
+ desiredColumn = null;
182
+ if (pendingCount !== null) {
183
+ return { line: clampLine(pendingCount - 1, rep), char: 0 };
184
+ }
185
+ return { line: 0, char: 0 };
211
186
  }
212
- return true;
213
187
  }
214
188
 
215
189
  if (key === "h") {
216
190
  desiredColumn = null;
217
- visualCursor = [curLine, Math.max(0, curChar - count)];
218
- updateVisualSelection(editorInfo, rep);
219
- return true;
191
+ return { line, char: Math.max(0, char - count) };
220
192
  }
221
193
 
222
194
  if (key === "l") {
223
195
  desiredColumn = null;
224
- visualCursor = [curLine, clampChar(curChar + count, lineText)];
225
- updateVisualSelection(editorInfo, rep);
226
- return true;
196
+ return { line, char: clampChar(char + count, lineText) };
227
197
  }
228
198
 
229
199
  if (key === "j") {
230
- if (desiredColumn === null) {
231
- desiredColumn = curChar;
232
- }
233
- const newLine = clampLine(curLine + count, rep);
200
+ if (desiredColumn === null) desiredColumn = char;
201
+ const newLine = clampLine(line + count, rep);
234
202
  const newLineText = getLineText(rep, newLine);
235
- visualCursor = [newLine, clampChar(desiredColumn, newLineText)];
236
- updateVisualSelection(editorInfo, rep);
237
- return true;
203
+ return { line: newLine, char: clampChar(desiredColumn, newLineText) };
238
204
  }
239
205
 
240
206
  if (key === "k") {
241
- if (desiredColumn === null) {
242
- desiredColumn = curChar;
243
- }
244
- const newLine = clampLine(curLine - count, rep);
207
+ if (desiredColumn === null) desiredColumn = char;
208
+ const newLine = clampLine(line - count, rep);
245
209
  const newLineText = getLineText(rep, newLine);
246
- visualCursor = [newLine, clampChar(desiredColumn, newLineText)];
247
- updateVisualSelection(editorInfo, rep);
248
- return true;
210
+ return { line: newLine, char: clampChar(desiredColumn, newLineText) };
211
+ }
212
+
213
+ if (key === "w") {
214
+ desiredColumn = null;
215
+ let pos = char;
216
+ for (let i = 0; i < count; i++) pos = wordForward(lineText, pos);
217
+ return { line, char: clampChar(pos, lineText) };
218
+ }
219
+
220
+ if (key === "b") {
221
+ desiredColumn = null;
222
+ let pos = char;
223
+ for (let i = 0; i < count; i++) pos = wordBackward(lineText, pos);
224
+ return { line, char: pos };
225
+ }
226
+
227
+ if (key === "e") {
228
+ desiredColumn = null;
229
+ let pos = char;
230
+ for (let i = 0; i < count; i++) pos = wordEnd(lineText, pos);
231
+ return { line, char: clampChar(pos, lineText) };
249
232
  }
250
233
 
251
234
  if (key === "0") {
252
235
  desiredColumn = null;
253
- visualCursor = [curLine, 0];
254
- updateVisualSelection(editorInfo, rep);
255
- return true;
236
+ return { line, char: 0 };
256
237
  }
257
238
 
258
239
  if (key === "$") {
259
240
  desiredColumn = null;
260
- visualCursor = [curLine, clampChar(lineText.length - 1, lineText)];
261
- updateVisualSelection(editorInfo, rep);
262
- return true;
241
+ return { line, char: clampChar(lineText.length - 1, lineText) };
263
242
  }
264
243
 
265
244
  if (key === "^") {
266
245
  desiredColumn = null;
267
- visualCursor = [curLine, firstNonBlank(lineText)];
268
- updateVisualSelection(editorInfo, rep);
269
- return true;
246
+ return { line, char: firstNonBlank(lineText) };
270
247
  }
271
248
 
272
- if (key === "w") {
249
+ if (key === "}") {
273
250
  desiredColumn = null;
274
- let pos = curChar;
275
- for (let i = 0; i < count; i++) pos = wordForward(lineText, pos);
276
- visualCursor = [curLine, clampChar(pos, lineText)];
277
- updateVisualSelection(editorInfo, rep);
278
- return true;
251
+ return { line: paragraphForward(rep, line, count), char: 0 };
279
252
  }
280
253
 
281
- if (key === "b") {
254
+ if (key === "{") {
282
255
  desiredColumn = null;
283
- let pos = curChar;
284
- for (let i = 0; i < count; i++) pos = wordBackward(lineText, pos);
285
- visualCursor = [curLine, pos];
286
- updateVisualSelection(editorInfo, rep);
287
- return true;
256
+ return { line: paragraphBackward(rep, line, count), char: 0 };
288
257
  }
289
258
 
290
- if (key === "e") {
259
+ if (key === "G") {
291
260
  desiredColumn = null;
292
- let pos = curChar;
293
- for (let i = 0; i < count; i++) pos = wordEnd(lineText, pos);
294
- visualCursor = [curLine, clampChar(pos, lineText)];
295
- updateVisualSelection(editorInfo, rep);
296
- return true;
261
+ if (pendingCount !== null) {
262
+ return { line: clampLine(pendingCount - 1, rep), char: 0 };
263
+ }
264
+ return { line: rep.lines.length() - 1, char: 0 };
297
265
  }
298
266
 
299
267
  if (key === ";") {
@@ -301,17 +269,16 @@ const handleVisualKey = (rep, editorInfo, key) => {
301
269
  const pos = charSearchPos(
302
270
  lastCharSearch.direction,
303
271
  lineText,
304
- curChar,
272
+ char,
305
273
  lastCharSearch.target,
306
274
  count,
307
275
  );
308
276
  if (pos !== -1) {
309
277
  desiredColumn = null;
310
- visualCursor = [curLine, pos];
311
- updateVisualSelection(editorInfo, rep);
278
+ return { line, char: pos };
312
279
  }
313
280
  }
314
- return true;
281
+ return { line, char };
315
282
  }
316
283
 
317
284
  if (key === ",") {
@@ -321,199 +288,139 @@ const handleVisualKey = (rep, editorInfo, key) => {
321
288
  const pos = charSearchPos(
322
289
  reverseDir,
323
290
  lineText,
324
- curChar,
291
+ char,
325
292
  lastCharSearch.target,
326
293
  count,
327
294
  );
328
295
  if (pos !== -1) {
329
296
  desiredColumn = null;
330
- visualCursor = [curLine, pos];
331
- updateVisualSelection(editorInfo, rep);
297
+ return { line, char: pos };
332
298
  }
333
299
  }
334
- return true;
335
- }
336
-
337
- if (key === "}") {
338
- desiredColumn = null;
339
- const target = paragraphForward(rep, curLine, count);
340
- visualCursor = [target, 0];
341
- updateVisualSelection(editorInfo, rep);
342
- return true;
300
+ return { line, char };
343
301
  }
344
302
 
345
- if (key === "{") {
346
- desiredColumn = null;
347
- const target = paragraphBackward(rep, curLine, count);
348
- visualCursor = [target, 0];
349
- updateVisualSelection(editorInfo, rep);
350
- return true;
303
+ if (key === "f" || key === "F" || key === "t" || key === "T") {
304
+ pendingKey = key;
305
+ return "pending";
351
306
  }
352
307
 
353
- if (key === "G") {
354
- pendingKey = null;
355
- desiredColumn = null;
356
- if (pendingCount !== null) {
357
- visualCursor = [clampLine(pendingCount - 1, rep), curChar];
358
- } else {
359
- visualCursor = [rep.lines.length() - 1, curChar];
360
- }
361
- updateVisualSelection(editorInfo, rep);
362
- return true;
308
+ if (key === "'" || key === "`") {
309
+ pendingKey = key;
310
+ return "pending";
363
311
  }
364
312
 
365
313
  if (key === "g") {
366
- if (pendingKey === "g") {
367
- pendingKey = null;
368
- desiredColumn = null;
369
- visualCursor = [0, curChar];
370
- updateVisualSelection(editorInfo, rep);
371
- } else {
372
- pendingKey = "g";
373
- }
374
- return true;
314
+ pendingKey = "g";
315
+ return "pending";
375
316
  }
376
317
 
377
- if (key === "f" || key === "F" || key === "t" || key === "T") {
378
- pendingKey = key;
379
- return true;
318
+ return null;
319
+ };
320
+
321
+ // --- Apply motion (mode-aware cursor placement) ---
322
+
323
+ const applyMotion = (editorInfo, rep, newLine, newChar) => {
324
+ if (visualMode !== null) {
325
+ visualCursor = [newLine, newChar];
326
+ updateVisualSelection(editorInfo, rep);
327
+ } else {
328
+ moveBlockCursor(editorInfo, newLine, newChar);
380
329
  }
381
330
 
382
- if (key === "'" || key === "`") {
383
- pendingKey = key;
384
- return true;
331
+ if (editorDoc) {
332
+ const lineDiv = editorDoc.body.querySelectorAll("div")[newLine];
333
+ if (lineDiv) lineDiv.scrollIntoView({ block: "nearest" });
385
334
  }
335
+ };
386
336
 
387
- if (key === "~") {
388
- const [start, end] = getVisualSelection(
389
- visualMode,
390
- visualAnchor,
391
- visualCursor,
392
- rep,
337
+ // --- Line deletion helper ---
338
+
339
+ const deleteLines = (editorInfo, rep, topLine, bottomLine) => {
340
+ const totalLines = rep.lines.length();
341
+ if (bottomLine === totalLines - 1 && topLine > 0) {
342
+ const prevLineLen = getLineText(rep, topLine - 1).length;
343
+ replaceRange(
344
+ editorInfo,
345
+ [topLine - 1, prevLineLen],
346
+ [bottomLine, getLineText(rep, bottomLine).length],
347
+ "",
393
348
  );
394
- const text = getTextInRange(rep, start, end);
395
- let toggled = "";
396
- for (let i = 0; i < text.length; i++) {
397
- const ch = text[i];
398
- toggled += ch === ch.toLowerCase() ? ch.toUpperCase() : ch.toLowerCase();
399
- }
400
- replaceRange(editorInfo, start, end, toggled);
401
- setVisualMode(null);
402
- moveBlockCursor(editorInfo, start[0], start[1]);
403
- return true;
349
+ return topLine - 1;
350
+ }
351
+ if (bottomLine < totalLines - 1) {
352
+ replaceRange(editorInfo, [topLine, 0], [bottomLine + 1, 0], "");
353
+ return topLine;
404
354
  }
355
+ replaceRange(
356
+ editorInfo,
357
+ [0, 0],
358
+ [bottomLine, getLineText(rep, bottomLine).length],
359
+ "",
360
+ );
361
+ return 0;
362
+ };
405
363
 
406
- if (key === "y") {
407
- const [start] = getVisualSelection(
408
- visualMode,
409
- visualAnchor,
410
- visualCursor,
411
- rep,
412
- );
364
+ // --- Operator application ---
413
365
 
414
- if (visualMode === "char") {
415
- const [, end] = getVisualSelection(
416
- visualMode,
417
- visualAnchor,
418
- visualCursor,
419
- rep,
420
- );
421
- setRegister(getTextInRange(rep, start, end));
422
- setVisualMode(null);
423
- moveBlockCursor(editorInfo, start[0], start[1]);
424
- return true;
425
- }
366
+ const applyCharOperator = (operator, start, end, editorInfo, rep) => {
367
+ if (start[0] === end[0]) {
368
+ const lineText = getLineText(rep, start[0]);
369
+ setRegister(lineText.slice(start[1], end[1]));
370
+ } else {
371
+ setRegister(getTextInRange(rep, start, end));
372
+ }
373
+ if (operator === "y") {
374
+ moveBlockCursor(editorInfo, start[0], start[1]);
375
+ return;
376
+ }
377
+ replaceRange(editorInfo, start, end, "");
378
+ if (operator === "c") {
379
+ moveCursor(editorInfo, start[0], start[1]);
380
+ setInsertMode(true);
381
+ } else {
382
+ const newLineText = getLineText(rep, start[0]);
383
+ moveBlockCursor(editorInfo, start[0], clampChar(start[1], newLineText));
384
+ }
385
+ };
426
386
 
427
- const topLine = start[0];
428
- const bottomLine = Math.max(visualAnchor[0], visualCursor[0]);
429
- const lines = [];
430
- for (let i = topLine; i <= bottomLine; i++) {
431
- lines.push(getLineText(rep, i));
432
- }
433
- setRegister(lines);
434
- setVisualMode(null);
387
+ const applyLineOperator = (
388
+ operator,
389
+ topLine,
390
+ bottomLine,
391
+ editorInfo,
392
+ rep,
393
+ char,
394
+ ) => {
395
+ const lines = [];
396
+ for (let i = topLine; i <= bottomLine; i++) {
397
+ lines.push(getLineText(rep, i));
398
+ }
399
+ setRegister(lines);
400
+ if (operator === "y") {
435
401
  moveBlockCursor(editorInfo, topLine, 0);
436
- return true;
402
+ return;
437
403
  }
438
-
439
- if (key === "d" || key === "c") {
440
- const enterInsert = key === "c";
441
- const [start, end] = getVisualSelection(
442
- visualMode,
443
- visualAnchor,
444
- visualCursor,
445
- rep,
446
- );
447
-
448
- if (visualMode === "char") {
449
- setRegister(getTextInRange(rep, start, end));
450
- replaceRange(editorInfo, start, end, "");
451
- if (enterInsert) {
452
- moveCursor(editorInfo, start[0], start[1]);
453
- setVisualMode(null);
454
- setInsertMode(true);
455
- } else {
456
- setVisualMode(null);
457
- moveBlockCursor(editorInfo, start[0], start[1]);
458
- }
459
- return true;
460
- }
461
-
462
- const topLine = start[0];
463
- const bottomLine = Math.max(visualAnchor[0], visualCursor[0]);
464
- const totalLines = rep.lines.length();
465
- const lines = [];
404
+ if (operator === "c") {
466
405
  for (let i = topLine; i <= bottomLine; i++) {
467
- lines.push(getLineText(rep, i));
468
- }
469
- setRegister(lines);
470
-
471
- if (enterInsert) {
472
- for (let i = topLine; i <= bottomLine; i++) {
473
- const text = getLineText(rep, i);
474
- replaceRange(editorInfo, [topLine, 0], [topLine, text.length], "");
475
- }
476
- moveCursor(editorInfo, topLine, 0);
477
- setVisualMode(null);
478
- setInsertMode(true);
479
- return true;
480
- }
481
-
482
- if (bottomLine === totalLines - 1 && topLine > 0) {
483
- const prevLineLen = getLineText(rep, topLine - 1).length;
484
- replaceRange(
485
- editorInfo,
486
- [topLine - 1, prevLineLen],
487
- [bottomLine, getLineText(rep, bottomLine).length],
488
- "",
489
- );
490
- moveBlockCursor(editorInfo, topLine - 1, 0);
491
- } else if (bottomLine < totalLines - 1) {
492
- replaceRange(editorInfo, [topLine, 0], [bottomLine + 1, 0], "");
493
- moveBlockCursor(editorInfo, topLine, 0);
494
- } else {
495
- replaceRange(
496
- editorInfo,
497
- [0, 0],
498
- [bottomLine, getLineText(rep, bottomLine).length],
499
- "",
500
- );
501
- moveBlockCursor(editorInfo, 0, 0);
406
+ const text = getLineText(rep, i);
407
+ replaceRange(editorInfo, [topLine, 0], [topLine, text.length], "");
502
408
  }
503
-
504
- setVisualMode(null);
505
- return true;
409
+ moveCursor(editorInfo, topLine, 0);
410
+ setInsertMode(true);
411
+ return;
506
412
  }
507
-
508
- pendingKey = null;
509
- return false;
413
+ const cursorLine = deleteLines(editorInfo, rep, topLine, bottomLine);
414
+ const newLineText = getLineText(rep, cursorLine);
415
+ moveBlockCursor(editorInfo, cursorLine, clampChar(char, newLineText));
510
416
  };
511
417
 
512
- // --- Normal mode key handler ---
418
+ // --- Unified key handler ---
513
419
 
514
- const handleNormalKey = (rep, editorInfo, key) => {
515
- const [line, char] = rep.selStart;
516
- const lineCount = rep.lines.length();
420
+ const handleKey = (rep, editorInfo, key) => {
421
+ const inVisual = visualMode !== null;
422
+ const line = inVisual ? visualCursor[0] : rep.selStart[0];
423
+ const char = inVisual ? visualCursor[1] : rep.selStart[1];
517
424
  const lineText = getLineText(rep, line);
518
425
 
519
426
  if (key >= "1" && key <= "9") {
@@ -528,6 +435,8 @@ const handleNormalKey = (rep, editorInfo, key) => {
528
435
  consumeCount();
529
436
  const count = getCount();
530
437
 
438
+ // --- Normal-only pending states: r + char, m + letter ---
439
+
531
440
  if (pendingKey === "r") {
532
441
  pendingKey = null;
533
442
  if (lineText.length > 0) {
@@ -537,306 +446,190 @@ const handleNormalKey = (rep, editorInfo, key) => {
537
446
  return true;
538
447
  }
539
448
 
540
- if (
541
- pendingKey === "f" ||
542
- pendingKey === "F" ||
543
- pendingKey === "t" ||
544
- pendingKey === "T"
545
- ) {
546
- const direction = pendingKey;
547
- pendingKey = null;
548
- lastCharSearch = { direction, target: key };
549
- const pos = charSearchPos(direction, lineText, char, key, count);
550
- if (pos !== -1) {
551
- desiredColumn = null;
552
- moveBlockCursor(editorInfo, line, pos);
553
- }
554
- return true;
555
- }
556
-
557
- if (
558
- pendingKey === "df" ||
559
- pendingKey === "dF" ||
560
- pendingKey === "dt" ||
561
- pendingKey === "dT"
562
- ) {
563
- const motion = pendingKey[1];
449
+ if (pendingKey === "m") {
564
450
  pendingKey = null;
565
- const searchDir = motion === "f" || motion === "t" ? motion : motion;
566
- const pos = charSearchPos(searchDir, lineText, char, key, count);
567
- if (pos !== -1) {
568
- const range = charMotionRange(motion, char, pos);
569
- if (range) {
570
- setRegister(lineText.slice(range.start, range.end));
571
- replaceRange(editorInfo, [line, range.start], [line, range.end], "");
572
- const newLineText = getLineText(rep, line);
573
- moveBlockCursor(editorInfo, line, clampChar(range.start, newLineText));
574
- }
451
+ if (key >= "a" && key <= "z") {
452
+ marks[key] = [line, char];
575
453
  }
576
454
  return true;
577
455
  }
578
456
 
579
- if (pendingKey === "di" || pendingKey === "da") {
580
- const range = textObjectRange(key, lineText, char, pendingKey[1]);
581
- pendingKey = null;
582
- if (range) {
583
- setRegister(lineText.slice(range.start, range.end));
584
- replaceRange(editorInfo, [line, range.start], [line, range.end], "");
585
- const newLineText = getLineText(rep, line);
586
- moveBlockCursor(editorInfo, line, clampChar(range.start, newLineText));
587
- }
588
- return true;
589
- }
457
+ // --- Operator-pending: resolve target ---
590
458
 
591
- if (pendingKey === "d") {
592
- pendingKey = null;
459
+ if (pendingOperator !== null) {
460
+ const op = pendingOperator;
593
461
 
594
- if (key === "d") {
595
- const deleteCount = Math.min(count, lineCount - line);
596
- const lastDeleteLine = line + deleteCount - 1;
597
- const deletedLines = [];
598
- for (let i = line; i <= lastDeleteLine; i++) {
599
- deletedLines.push(getLineText(rep, i));
600
- }
601
- setRegister(deletedLines);
602
- if (lastDeleteLine === lineCount - 1 && line > 0) {
603
- const prevLineText = getLineText(rep, line - 1);
604
- replaceRange(
605
- editorInfo,
606
- [line - 1, prevLineText.length],
607
- [lastDeleteLine, getLineText(rep, lastDeleteLine).length],
608
- "",
609
- );
610
- moveBlockCursor(editorInfo, line - 1, clampChar(char, prevLineText));
611
- } else if (lineCount > deleteCount) {
612
- replaceRange(editorInfo, [line, 0], [lastDeleteLine + 1, 0], "");
613
- const newLineText = getLineText(rep, line);
614
- moveBlockCursor(editorInfo, line, clampChar(char, newLineText));
615
- } else {
616
- replaceRange(
617
- editorInfo,
618
- [0, 0],
619
- [lastDeleteLine, getLineText(rep, lastDeleteLine).length],
620
- "",
621
- );
622
- moveBlockCursor(editorInfo, 0, 0);
623
- }
624
- return true;
625
- }
626
-
627
- if (key === "i") {
628
- pendingKey = "di";
629
- return true;
630
- }
631
-
632
- if (key === "f" || key === "F" || key === "t" || key === "T") {
633
- pendingKey = "d" + key;
462
+ if (key === op) {
463
+ pendingOperator = null;
464
+ const lineCount = rep.lines.length();
465
+ const opCount = Math.min(count, lineCount - line);
466
+ const lastLine = line + opCount - 1;
467
+ applyLineOperator(op, line, lastLine, editorInfo, rep, char);
634
468
  return true;
635
469
  }
636
470
 
637
- const range = motionRange(key, char, lineText, count);
638
- if (range && range.end > range.start) {
639
- setRegister(lineText.slice(range.start, range.end));
640
- replaceRange(editorInfo, [line, range.start], [line, range.end], "");
641
- const newLineText = getLineText(rep, line);
642
- moveBlockCursor(editorInfo, line, clampChar(range.start, newLineText));
643
- }
644
- return true;
645
- }
646
-
647
- if (
648
- pendingKey === "yf" ||
649
- pendingKey === "yF" ||
650
- pendingKey === "yt" ||
651
- pendingKey === "yT"
652
- ) {
653
- const motion = pendingKey[1];
654
- pendingKey = null;
655
- const pos = charSearchPos(motion, lineText, char, key, count);
656
- if (pos !== -1) {
657
- const range = charMotionRange(motion, char, pos);
658
- if (range) {
659
- setRegister(lineText.slice(range.start, range.end));
660
- }
661
- }
662
- return true;
663
- }
664
-
665
- if (
666
- pendingKey === "cf" ||
667
- pendingKey === "cF" ||
668
- pendingKey === "ct" ||
669
- pendingKey === "cT"
670
- ) {
671
- const motion = pendingKey[1];
672
- pendingKey = null;
673
- const pos = charSearchPos(motion, lineText, char, key, count);
674
- if (pos !== -1) {
675
- const range = charMotionRange(motion, char, pos);
471
+ if (pendingKey === "i" || pendingKey === "a") {
472
+ const type = pendingKey;
473
+ pendingKey = null;
474
+ pendingOperator = null;
475
+ const range = textObjectRange(key, lineText, char, type);
676
476
  if (range) {
677
- setRegister(lineText.slice(range.start, range.end));
678
- replaceRange(editorInfo, [line, range.start], [line, range.end], "");
679
- moveCursor(editorInfo, line, range.start);
680
- setInsertMode(true);
477
+ applyCharOperator(
478
+ op,
479
+ [line, range.start],
480
+ [line, range.end],
481
+ editorInfo,
482
+ rep,
483
+ );
681
484
  }
682
- }
683
- return true;
684
- }
685
-
686
- if (pendingKey === "ci" || pendingKey === "ca") {
687
- const range = textObjectRange(key, lineText, char, pendingKey[1]);
688
- pendingKey = null;
689
- if (range) {
690
- setRegister(lineText.slice(range.start, range.end));
691
- replaceRange(editorInfo, [line, range.start], [line, range.end], "");
692
- moveCursor(editorInfo, line, range.start);
693
- setInsertMode(true);
694
- }
695
- return true;
696
- }
697
-
698
- if (pendingKey === "c") {
699
- pendingKey = null;
700
-
701
- if (key === "c") {
702
- setRegister(lineText);
703
- replaceRange(editorInfo, [line, 0], [line, lineText.length], "");
704
- moveCursor(editorInfo, line, 0);
705
- setInsertMode(true);
706
- return true;
707
- }
708
-
709
- if (key === "i") {
710
- pendingKey = "ci";
711
- return true;
712
- }
713
-
714
- if (key === "f" || key === "F" || key === "t" || key === "T") {
715
- pendingKey = "c" + key;
716
485
  return true;
717
486
  }
718
487
 
719
- const range = motionRange(key, char, lineText, count);
720
- if (range && range.end > range.start) {
721
- setRegister(lineText.slice(range.start, range.end));
722
- replaceRange(editorInfo, [line, range.start], [line, range.end], "");
723
- moveCursor(editorInfo, line, range.start);
724
- setInsertMode(true);
725
- }
726
- return true;
727
- }
728
-
729
- if (pendingKey === "yi") {
730
- const range = textObjectRange(key, lineText, char, pendingKey[1]);
731
- pendingKey = null;
732
- if (range) {
733
- setRegister(lineText.slice(range.start, range.end));
734
- }
735
- return true;
736
- }
737
-
738
- if (pendingKey === "y") {
739
- pendingKey = null;
740
-
741
- if (key === "y") {
742
- const yankCount = Math.min(count, lineCount - line);
743
- const lastYankLine = line + yankCount - 1;
744
- const yankedLines = [];
745
- for (let i = line; i <= lastYankLine; i++) {
746
- yankedLines.push(getLineText(rep, i));
488
+ if (
489
+ pendingKey === "f" ||
490
+ pendingKey === "F" ||
491
+ pendingKey === "t" ||
492
+ pendingKey === "T"
493
+ ) {
494
+ const direction = pendingKey;
495
+ pendingKey = null;
496
+ pendingOperator = null;
497
+ lastCharSearch = { direction, target: key };
498
+ const pos = charSearchPos(direction, lineText, char, key, count);
499
+ if (pos !== -1) {
500
+ const range = charMotionRange(direction, char, pos);
501
+ if (range) {
502
+ applyCharOperator(
503
+ op,
504
+ [line, range.start],
505
+ [line, range.end],
506
+ editorInfo,
507
+ rep,
508
+ );
509
+ }
747
510
  }
748
- setRegister(yankedLines);
749
511
  return true;
750
512
  }
751
513
 
752
- if (key === "i") {
753
- pendingKey = "yi";
514
+ if (key === "i" || key === "a") {
515
+ pendingKey = key;
754
516
  return true;
755
517
  }
756
518
 
757
519
  if (key === "f" || key === "F" || key === "t" || key === "T") {
758
- pendingKey = "y" + key;
520
+ pendingKey = key;
759
521
  return true;
760
522
  }
761
523
 
762
- const range = motionRange(key, char, lineText, count);
763
- if (range && range.end > range.start) {
764
- setRegister(lineText.slice(range.start, range.end));
765
- }
766
- return true;
767
- }
768
-
769
- if (pendingKey === "m") {
770
- pendingKey = null;
771
- if (key >= "a" && key <= "z") {
772
- marks[key] = [line, char];
773
- }
774
- return true;
775
- }
776
-
777
- if (pendingKey === "'" || pendingKey === "`") {
778
- const jumpType = pendingKey;
779
- pendingKey = null;
780
- if (key >= "a" && key <= "z" && marks[key]) {
781
- const [markLine, markChar] = marks[key];
782
- desiredColumn = null;
783
- if (jumpType === "'") {
784
- const targetLineText = getLineText(rep, markLine);
785
- moveBlockCursor(editorInfo, markLine, firstNonBlank(targetLineText));
786
- } else {
787
- moveBlockCursor(editorInfo, markLine, markChar);
788
- }
524
+ pendingOperator = null;
525
+ const range = motionRange(key, char, lineText, count);
526
+ if (range && range.end > range.start) {
527
+ applyCharOperator(
528
+ op,
529
+ [line, range.start],
530
+ [line, range.end],
531
+ editorInfo,
532
+ rep,
533
+ );
789
534
  }
790
535
  return true;
791
536
  }
792
537
 
793
- if (key === "h") {
794
- desiredColumn = null;
795
- moveBlockCursor(editorInfo, line, Math.max(0, char - count));
538
+ // --- Text object in visual mode (i/a + object key) ---
539
+
540
+ if (inVisual && (pendingKey === "i" || pendingKey === "a")) {
541
+ const type = pendingKey;
542
+ pendingKey = null;
543
+ const range = textObjectRange(key, lineText, char, type);
544
+ if (range) {
545
+ visualAnchor = [line, range.start];
546
+ visualCursor = [line, range.end];
547
+ setVisualMode("char");
548
+ updateVisualSelection(editorInfo, rep);
549
+ }
796
550
  return true;
797
551
  }
798
552
 
799
- if (key === "l") {
800
- desiredColumn = null;
801
- moveBlockCursor(editorInfo, line, clampChar(char + count, lineText));
553
+ // --- Motions (shared between normal and visual) ---
554
+
555
+ const motion = resolveMotion(key, line, char, lineText, rep, count);
556
+ if (motion === "pending") return true;
557
+ if (motion) {
558
+ applyMotion(editorInfo, rep, motion.line, motion.char);
802
559
  return true;
803
560
  }
804
561
 
805
- if (key === "k") {
806
- if (desiredColumn === null) {
807
- desiredColumn = char;
562
+ // --- Operators (d/c/y) ---
563
+
564
+ if (key === "d" || key === "c" || key === "y") {
565
+ if (inVisual) {
566
+ if (visualMode === "char") {
567
+ const [start, end] = getVisualSelection(
568
+ visualMode,
569
+ visualAnchor,
570
+ visualCursor,
571
+ rep,
572
+ );
573
+ setVisualMode(null);
574
+ applyCharOperator(key, start, end, editorInfo, rep);
575
+ } else {
576
+ const topLine = Math.min(visualAnchor[0], visualCursor[0]);
577
+ const bottomLine = Math.max(visualAnchor[0], visualCursor[0]);
578
+ setVisualMode(null);
579
+ applyLineOperator(key, topLine, bottomLine, editorInfo, rep, 0);
580
+ }
581
+ return true;
808
582
  }
809
- const newLine = clampLine(line - count, rep);
810
- const newLineText = getLineText(rep, newLine);
811
- moveBlockCursor(editorInfo, newLine, clampChar(desiredColumn, newLineText));
583
+ pendingOperator = key;
812
584
  return true;
813
585
  }
814
586
 
815
- if (key === "j") {
816
- if (desiredColumn === null) {
817
- desiredColumn = char;
587
+ // --- Visual-mode specific ---
588
+
589
+ if (inVisual) {
590
+ if (key === "i" || key === "a") {
591
+ pendingKey = key;
592
+ return true;
818
593
  }
819
- const newLine = clampLine(line + count, rep);
820
- const newLineText = getLineText(rep, newLine);
821
- moveBlockCursor(editorInfo, newLine, clampChar(desiredColumn, newLineText));
822
- return true;
594
+
595
+ if (key === "~") {
596
+ const [start, end] = getVisualSelection(
597
+ visualMode,
598
+ visualAnchor,
599
+ visualCursor,
600
+ rep,
601
+ );
602
+ const text = getTextInRange(rep, start, end);
603
+ let toggled = "";
604
+ for (let i = 0; i < text.length; i++) {
605
+ const ch = text[i];
606
+ toggled +=
607
+ ch === ch.toLowerCase() ? ch.toUpperCase() : ch.toLowerCase();
608
+ }
609
+ replaceRange(editorInfo, start, end, toggled);
610
+ setVisualMode(null);
611
+ moveBlockCursor(editorInfo, start[0], start[1]);
612
+ return true;
613
+ }
614
+
615
+ pendingKey = null;
616
+ return false;
823
617
  }
824
618
 
825
- if (key === "0") {
826
- desiredColumn = null;
827
- moveBlockCursor(editorInfo, line, 0);
619
+ // --- Normal-mode only commands ---
620
+
621
+ if (key === "Y") {
622
+ setRegister([lineText]);
828
623
  return true;
829
624
  }
830
625
 
831
- if (key === "$") {
832
- desiredColumn = null;
833
- moveBlockCursor(editorInfo, line, clampChar(lineText.length - 1, lineText));
626
+ if (key === "r") {
627
+ if (lineText.length > 0) pendingKey = "r";
834
628
  return true;
835
629
  }
836
630
 
837
- if (key === "^") {
838
- desiredColumn = null;
839
- moveBlockCursor(editorInfo, line, firstNonBlank(lineText));
631
+ if (key === "m") {
632
+ pendingKey = "m";
840
633
  return true;
841
634
  }
842
635
 
@@ -850,22 +643,6 @@ const handleNormalKey = (rep, editorInfo, key) => {
850
643
  return true;
851
644
  }
852
645
 
853
- if (key === "w") {
854
- desiredColumn = null;
855
- let pos = char;
856
- for (let i = 0; i < count; i++) pos = wordForward(lineText, pos);
857
- moveBlockCursor(editorInfo, line, clampChar(pos, lineText));
858
- return true;
859
- }
860
-
861
- if (key === "b") {
862
- desiredColumn = null;
863
- let pos = char;
864
- for (let i = 0; i < count; i++) pos = wordBackward(lineText, pos);
865
- moveBlockCursor(editorInfo, line, pos);
866
- return true;
867
- }
868
-
869
646
  if (key === "o") {
870
647
  replaceRange(
871
648
  editorInfo,
@@ -937,70 +714,8 @@ const handleNormalKey = (rep, editorInfo, key) => {
937
714
  return true;
938
715
  }
939
716
 
940
- if (key === "G") {
941
- desiredColumn = null;
942
- if (pendingCount !== null) {
943
- moveBlockCursor(editorInfo, clampLine(pendingCount - 1, rep), 0);
944
- } else {
945
- moveBlockCursor(editorInfo, lineCount - 1, 0);
946
- }
947
- return true;
948
- }
949
-
950
- if (key === "g") {
951
- if (pendingKey === "g") {
952
- pendingKey = null;
953
- desiredColumn = null;
954
- moveBlockCursor(editorInfo, 0, 0);
955
- } else {
956
- pendingKey = "g";
957
- }
958
- return true;
959
- }
960
-
961
- if (key === "r") {
962
- if (lineText.length > 0) {
963
- pendingKey = "r";
964
- }
965
- return true;
966
- }
967
-
968
- if (key === "f" || key === "F" || key === "t" || key === "T") {
969
- pendingKey = key;
970
- return true;
971
- }
972
-
973
- if (key === "m") {
974
- pendingKey = "m";
975
- return true;
976
- }
977
-
978
- if (key === "'" || key === "`") {
979
- pendingKey = key;
980
- return true;
981
- }
982
-
983
- if (key === "d") {
984
- pendingKey = "d";
985
- return true;
986
- }
987
-
988
- if (key === "c") {
989
- pendingKey = "c";
990
- return true;
991
- }
992
-
993
- if (key === "y") {
994
- pendingKey = "y";
995
- return true;
996
- }
997
-
998
- if (key === "Y") {
999
- setRegister([lineText]);
1000
- return true;
1001
- }
1002
-
1003
717
  if (key === "J") {
718
+ const lineCount = rep.lines.length();
1004
719
  const joins = Math.min(count, lineCount - 1 - line);
1005
720
  let cursorChar = lineText.length;
1006
721
  for (let i = 0; i < joins; i++) {
@@ -1079,65 +794,7 @@ const handleNormalKey = (rep, editorInfo, key) => {
1079
794
  return true;
1080
795
  }
1081
796
 
1082
- if (key === ";") {
1083
- if (lastCharSearch) {
1084
- const pos = charSearchPos(
1085
- lastCharSearch.direction,
1086
- lineText,
1087
- char,
1088
- lastCharSearch.target,
1089
- count,
1090
- );
1091
- if (pos !== -1) {
1092
- desiredColumn = null;
1093
- moveBlockCursor(editorInfo, line, pos);
1094
- }
1095
- }
1096
- return true;
1097
- }
1098
-
1099
- if (key === ",") {
1100
- if (lastCharSearch) {
1101
- const opposite = { f: "F", F: "f", t: "T", T: "t" };
1102
- const reverseDir = opposite[lastCharSearch.direction];
1103
- const pos = charSearchPos(
1104
- reverseDir,
1105
- lineText,
1106
- char,
1107
- lastCharSearch.target,
1108
- count,
1109
- );
1110
- if (pos !== -1) {
1111
- desiredColumn = null;
1112
- moveBlockCursor(editorInfo, line, pos);
1113
- }
1114
- }
1115
- return true;
1116
- }
1117
-
1118
- if (key === "}") {
1119
- desiredColumn = null;
1120
- const target = paragraphForward(rep, line, count);
1121
- moveBlockCursor(editorInfo, target, 0);
1122
- return true;
1123
- }
1124
-
1125
- if (key === "{") {
1126
- desiredColumn = null;
1127
- const target = paragraphBackward(rep, line, count);
1128
- moveBlockCursor(editorInfo, target, 0);
1129
- return true;
1130
- }
1131
-
1132
797
  pendingKey = null;
1133
-
1134
- if (key === "e") {
1135
- let pos = char;
1136
- for (let i = 0; i < count; i++) pos = wordEnd(lineText, pos);
1137
- moveBlockCursor(editorInfo, line, clampChar(pos, lineText));
1138
- return true;
1139
- }
1140
-
1141
798
  return false;
1142
799
  };
1143
800
 
@@ -1176,71 +833,6 @@ exports.aceKeyEvent = (_hookName, { evt, rep, editorInfo }) => {
1176
833
  setInsertMode(insertMode);
1177
834
  }
1178
835
 
1179
- if (visualMode !== null && pendingKey !== null) {
1180
- const handled = handleVisualKey(rep, editorInfo, evt.key);
1181
- evt.preventDefault();
1182
- return handled || true;
1183
- }
1184
-
1185
- if (!insertMode && visualMode === null && pendingKey !== null) {
1186
- const handled = handleNormalKey(rep, editorInfo, evt.key);
1187
- evt.preventDefault();
1188
- return handled || true;
1189
- }
1190
-
1191
- if (
1192
- !insertMode &&
1193
- visualMode !== null &&
1194
- (evt.key === "i" || evt.key === "a")
1195
- ) {
1196
- const handled = handleVisualKey(rep, editorInfo, evt.key);
1197
- evt.preventDefault();
1198
- return handled || true;
1199
- }
1200
-
1201
- if (!insertMode && evt.key === "i") {
1202
- const [line, char] = rep.selStart;
1203
- desiredColumn = null;
1204
- moveCursor(editorInfo, line, char);
1205
- setVisualMode(null);
1206
- setInsertMode(true);
1207
- evt.preventDefault();
1208
- return true;
1209
- }
1210
-
1211
- if (!insertMode && evt.key === "a") {
1212
- const [line, char] = rep.selStart;
1213
- const lineText = getLineText(rep, line);
1214
- desiredColumn = null;
1215
- moveCursor(editorInfo, line, Math.min(char + 1, lineText.length));
1216
- setVisualMode(null);
1217
- setInsertMode(true);
1218
- evt.preventDefault();
1219
- return true;
1220
- }
1221
-
1222
- if (!insertMode && evt.key === "A") {
1223
- const [line] = rep.selStart;
1224
- const lineText = getLineText(rep, line);
1225
- desiredColumn = null;
1226
- moveCursor(editorInfo, line, lineText.length);
1227
- setVisualMode(null);
1228
- setInsertMode(true);
1229
- evt.preventDefault();
1230
- return true;
1231
- }
1232
-
1233
- if (!insertMode && evt.key === "I") {
1234
- const [line] = rep.selStart;
1235
- const lineText = getLineText(rep, line);
1236
- desiredColumn = null;
1237
- moveCursor(editorInfo, line, firstNonBlank(lineText));
1238
- setVisualMode(null);
1239
- setInsertMode(true);
1240
- evt.preventDefault();
1241
- return true;
1242
- }
1243
-
1244
836
  if (evt.key === "Escape") {
1245
837
  if (insertMode) {
1246
838
  setInsertMode(false);
@@ -1254,43 +846,83 @@ exports.aceKeyEvent = (_hookName, { evt, rep, editorInfo }) => {
1254
846
  }
1255
847
  countBuffer = "";
1256
848
  pendingKey = null;
849
+ pendingOperator = null;
1257
850
  pendingCount = null;
1258
851
  desiredColumn = null;
1259
852
  evt.preventDefault();
1260
853
  return true;
1261
854
  }
1262
855
 
1263
- if (!insertMode && visualMode === null && evt.key === "V") {
1264
- const [line] = rep.selStart;
1265
- visualAnchor = [line, 0];
1266
- visualCursor = [line, 0];
1267
- setVisualMode("line");
1268
- updateVisualSelection(editorInfo, rep);
1269
- evt.preventDefault();
1270
- return true;
1271
- }
1272
-
1273
- if (!insertMode && visualMode === null && evt.key === "v") {
1274
- const [line, char] = rep.selStart;
1275
- visualAnchor = [line, char];
1276
- visualCursor = [line, char];
1277
- setVisualMode("char");
1278
- updateVisualSelection(editorInfo, rep);
1279
- evt.preventDefault();
1280
- return true;
1281
- }
856
+ if (insertMode) return false;
1282
857
 
1283
- if (visualMode !== null) {
1284
- const handled = handleVisualKey(rep, editorInfo, evt.key);
858
+ if (pendingKey !== null || pendingOperator !== null) {
859
+ const handled = handleKey(rep, editorInfo, evt.key);
1285
860
  evt.preventDefault();
1286
861
  return handled || true;
1287
862
  }
1288
863
 
1289
- if (insertMode) {
1290
- return false;
864
+ if (visualMode === null) {
865
+ if (evt.key === "i") {
866
+ const [line, char] = rep.selStart;
867
+ desiredColumn = null;
868
+ moveCursor(editorInfo, line, char);
869
+ setInsertMode(true);
870
+ evt.preventDefault();
871
+ return true;
872
+ }
873
+
874
+ if (evt.key === "a") {
875
+ const [line, char] = rep.selStart;
876
+ const lineText = getLineText(rep, line);
877
+ desiredColumn = null;
878
+ moveCursor(editorInfo, line, Math.min(char + 1, lineText.length));
879
+ setInsertMode(true);
880
+ evt.preventDefault();
881
+ return true;
882
+ }
883
+
884
+ if (evt.key === "A") {
885
+ const [line] = rep.selStart;
886
+ const lineText = getLineText(rep, line);
887
+ desiredColumn = null;
888
+ moveCursor(editorInfo, line, lineText.length);
889
+ setInsertMode(true);
890
+ evt.preventDefault();
891
+ return true;
892
+ }
893
+
894
+ if (evt.key === "I") {
895
+ const [line] = rep.selStart;
896
+ const lineText = getLineText(rep, line);
897
+ desiredColumn = null;
898
+ moveCursor(editorInfo, line, firstNonBlank(lineText));
899
+ setInsertMode(true);
900
+ evt.preventDefault();
901
+ return true;
902
+ }
903
+
904
+ if (evt.key === "V") {
905
+ const [line] = rep.selStart;
906
+ visualAnchor = [line, 0];
907
+ visualCursor = [line, 0];
908
+ setVisualMode("line");
909
+ updateVisualSelection(editorInfo, rep);
910
+ evt.preventDefault();
911
+ return true;
912
+ }
913
+
914
+ if (evt.key === "v") {
915
+ const [line, char] = rep.selStart;
916
+ visualAnchor = [line, char];
917
+ visualCursor = [line, char];
918
+ setVisualMode("char");
919
+ updateVisualSelection(editorInfo, rep);
920
+ evt.preventDefault();
921
+ return true;
922
+ }
1291
923
  }
1292
924
 
1293
- const handled = handleNormalKey(rep, editorInfo, evt.key);
925
+ const handled = handleKey(rep, editorInfo, evt.key);
1294
926
  evt.preventDefault();
1295
927
  return handled || true;
1296
928
  };