ep_vim 0.1.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.
@@ -0,0 +1,1173 @@
1
+ 'use strict';
2
+
3
+ // --- State variables ---
4
+
5
+ let vimEnabled = localStorage.getItem('ep_vimEnabled') === 'true';
6
+ let insertMode = false;
7
+ let visualMode = null;
8
+ let visualAnchor = null;
9
+ let visualCursor = null;
10
+ let pendingKey = null;
11
+ let pendingCount = null;
12
+ let countBuffer = '';
13
+ let register = null;
14
+ let marks = {};
15
+ let editorDoc = null;
16
+ let currentRep = null;
17
+
18
+ // --- Utility helpers ---
19
+
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;
44
+ };
45
+
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
+ };
56
+
57
+ const consumeCount = () => {
58
+ if (countBuffer !== '') {
59
+ pendingCount = parseInt(countBuffer, 10);
60
+ countBuffer = '';
61
+ } else if (pendingKey === null) {
62
+ pendingCount = null;
63
+ }
64
+ };
65
+
66
+ const getCount = () => pendingCount || 1;
67
+
68
+ const setRegister = (value) => {
69
+ register = value;
70
+ const text = Array.isArray(value) ? value.join('\n') + '\n' : value;
71
+ navigator.clipboard.writeText(text).catch(() => {});
72
+ };
73
+
74
+ // --- Etherpad API wrappers ---
75
+
76
+ const moveCursor = (editorInfo, line, char) => {
77
+ const pos = [line, char];
78
+ editorInfo.ace_inCallStackIfNecessary('vim-move', () => {
79
+ editorInfo.ace_performSelectionChange(pos, pos, false);
80
+ editorInfo.ace_updateBrowserSelectionFromRep();
81
+ });
82
+ };
83
+
84
+ const clearEmptyLineCursor = () => {
85
+ if (!editorDoc) return;
86
+ const old = editorDoc.querySelector('.vim-empty-line-cursor');
87
+ if (old) old.classList.remove('vim-empty-line-cursor');
88
+ };
89
+
90
+ const moveBlockCursor = (editorInfo, line, char) => {
91
+ clearEmptyLineCursor();
92
+ const lineText = currentRep ? getLineText(currentRep, line) : '';
93
+ if (lineText.length === 0 && editorDoc) {
94
+ const lineDiv = editorDoc.body.querySelectorAll('div')[line];
95
+ if (lineDiv) lineDiv.classList.add('vim-empty-line-cursor');
96
+ selectRange(editorInfo, [line, 0], [line, 0]);
97
+ } else {
98
+ selectRange(editorInfo, [line, char], [line, char + 1]);
99
+ }
100
+ };
101
+
102
+ const selectRange = (editorInfo, start, end) => {
103
+ editorInfo.ace_inCallStackIfNecessary('vim-select', () => {
104
+ editorInfo.ace_performSelectionChange(start, end, false);
105
+ editorInfo.ace_updateBrowserSelectionFromRep();
106
+ });
107
+ };
108
+
109
+ const replaceRange = (editorInfo, start, end, text) => {
110
+ editorInfo.ace_inCallStackIfNecessary('vim-edit', () => {
111
+ editorInfo.ace_performDocumentReplaceRange(start, end, text);
112
+ });
113
+ };
114
+
115
+ const undo = (editorInfo) => {
116
+ editorInfo.ace_doUndoRedo('undo');
117
+ };
118
+
119
+ // --- Mode management ---
120
+
121
+ const setInsertMode = (value) => {
122
+ insertMode = value;
123
+ if (value) clearEmptyLineCursor();
124
+ if (editorDoc) {
125
+ editorDoc.body.classList.toggle('vim-insert-mode', value);
126
+ }
127
+ };
128
+
129
+ const setVisualMode = (value) => {
130
+ visualMode = value;
131
+ if (editorDoc) {
132
+ editorDoc.body.classList.toggle('vim-visual-line-mode', value === 'line');
133
+ editorDoc.body.classList.toggle('vim-visual-char-mode', value === 'char');
134
+ }
135
+ };
136
+
137
+ // --- Visual mode helpers ---
138
+
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
+ const updateVisualSelection = (editorInfo, rep) => {
171
+ const [start, end] = getVisualSelection(rep);
172
+ selectRange(editorInfo, start, end);
173
+ };
174
+
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
+ // --- Visual mode key handler ---
211
+
212
+ const handleVisualKey = (rep, editorInfo, key) => {
213
+ const curLine = visualCursor[0];
214
+ const curChar = visualCursor[1];
215
+ const lineText = getLineText(rep, curLine);
216
+
217
+ if (key >= '1' && key <= '9') {
218
+ countBuffer += key;
219
+ return true;
220
+ }
221
+ if (key === '0' && countBuffer !== '') {
222
+ countBuffer += key;
223
+ return true;
224
+ }
225
+
226
+ consumeCount();
227
+ const count = getCount();
228
+
229
+ if (pendingKey === 'f' || pendingKey === 'F' || pendingKey === 't' || pendingKey === 'T') {
230
+ const direction = pendingKey;
231
+ 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
+ }
244
+ if (pos !== -1) {
245
+ visualCursor = [curLine, pos];
246
+ updateVisualSelection(editorInfo, rep);
247
+ }
248
+ return true;
249
+ }
250
+
251
+ if (pendingKey === "'" || pendingKey === '`') {
252
+ const jumpType = pendingKey;
253
+ pendingKey = null;
254
+ if (key >= 'a' && key <= 'z' && marks[key]) {
255
+ const [markLine, markChar] = marks[key];
256
+ if (jumpType === "'") {
257
+ const targetLineText = getLineText(rep, markLine);
258
+ visualCursor = [markLine, firstNonBlank(targetLineText)];
259
+ } else {
260
+ visualCursor = [markLine, markChar];
261
+ }
262
+ updateVisualSelection(editorInfo, rep);
263
+ }
264
+ return true;
265
+ }
266
+
267
+ if (key === 'h') {
268
+ visualCursor = [curLine, Math.max(0, curChar - count)];
269
+ updateVisualSelection(editorInfo, rep);
270
+ return true;
271
+ }
272
+
273
+ if (key === 'l') {
274
+ visualCursor = [curLine, clampChar(curChar + count, lineText)];
275
+ updateVisualSelection(editorInfo, rep);
276
+ return true;
277
+ }
278
+
279
+ if (key === 'j') {
280
+ visualCursor = [clampLine(curLine + count, rep), curChar];
281
+ updateVisualSelection(editorInfo, rep);
282
+ return true;
283
+ }
284
+
285
+ if (key === 'k') {
286
+ visualCursor = [clampLine(curLine - count, rep), curChar];
287
+ updateVisualSelection(editorInfo, rep);
288
+ return true;
289
+ }
290
+
291
+ if (key === '0') {
292
+ visualCursor = [curLine, 0];
293
+ updateVisualSelection(editorInfo, rep);
294
+ return true;
295
+ }
296
+
297
+ if (key === '$') {
298
+ visualCursor = [curLine, clampChar(lineText.length - 1, lineText)];
299
+ updateVisualSelection(editorInfo, rep);
300
+ return true;
301
+ }
302
+
303
+ if (key === '^') {
304
+ visualCursor = [curLine, firstNonBlank(lineText)];
305
+ updateVisualSelection(editorInfo, rep);
306
+ return true;
307
+ }
308
+
309
+ if (key === 'w') {
310
+ let pos = curChar;
311
+ for (let i = 0; i < count; i++) pos = wordForward(lineText, pos);
312
+ visualCursor = [curLine, clampChar(pos, lineText)];
313
+ updateVisualSelection(editorInfo, rep);
314
+ return true;
315
+ }
316
+
317
+ if (key === 'b') {
318
+ let pos = curChar;
319
+ for (let i = 0; i < count; i++) pos = wordBackward(lineText, pos);
320
+ visualCursor = [curLine, pos];
321
+ updateVisualSelection(editorInfo, rep);
322
+ return true;
323
+ }
324
+
325
+ if (key === 'e') {
326
+ let pos = curChar;
327
+ for (let i = 0; i < count; i++) pos = wordEnd(lineText, pos);
328
+ visualCursor = [curLine, clampChar(pos, lineText)];
329
+ updateVisualSelection(editorInfo, rep);
330
+ return true;
331
+ }
332
+
333
+ if (key === 'G') {
334
+ pendingKey = null;
335
+ if (pendingCount !== null) {
336
+ visualCursor = [clampLine(pendingCount - 1, rep), curChar];
337
+ } else {
338
+ visualCursor = [rep.lines.length() - 1, curChar];
339
+ }
340
+ updateVisualSelection(editorInfo, rep);
341
+ return true;
342
+ }
343
+
344
+ if (key === 'g') {
345
+ if (pendingKey === 'g') {
346
+ pendingKey = null;
347
+ visualCursor = [0, curChar];
348
+ updateVisualSelection(editorInfo, rep);
349
+ } else {
350
+ pendingKey = 'g';
351
+ }
352
+ return true;
353
+ }
354
+
355
+ if (key === 'f' || key === 'F' || key === 't' || key === 'T') {
356
+ pendingKey = key;
357
+ return true;
358
+ }
359
+
360
+ if (key === "'" || key === '`') {
361
+ pendingKey = key;
362
+ return true;
363
+ }
364
+
365
+ if (key === 'y') {
366
+ const [start] = getVisualSelection(rep);
367
+
368
+ if (visualMode === 'char') {
369
+ const [, end] = getVisualSelection(rep);
370
+ setRegister(getTextInRange(rep, start, end));
371
+ setVisualMode(null);
372
+ moveBlockCursor(editorInfo, start[0], start[1]);
373
+ return true;
374
+ }
375
+
376
+ const topLine = start[0];
377
+ const bottomLine = Math.max(visualAnchor[0], visualCursor[0]);
378
+ const lines = [];
379
+ for (let i = topLine; i <= bottomLine; i++) {
380
+ lines.push(getLineText(rep, i));
381
+ }
382
+ setRegister(lines);
383
+ setVisualMode(null);
384
+ moveBlockCursor(editorInfo, topLine, 0);
385
+ return true;
386
+ }
387
+
388
+ if (key === 'd' || key === 'c') {
389
+ const enterInsert = key === 'c';
390
+ const [start, end] = getVisualSelection(rep);
391
+
392
+ if (visualMode === 'char') {
393
+ setRegister(getTextInRange(rep, start, end));
394
+ replaceRange(editorInfo, start, end, '');
395
+ if (enterInsert) {
396
+ moveCursor(editorInfo, start[0], start[1]);
397
+ setVisualMode(null);
398
+ setInsertMode(true);
399
+ } else {
400
+ setVisualMode(null);
401
+ moveBlockCursor(editorInfo, start[0], start[1]);
402
+ }
403
+ return true;
404
+ }
405
+
406
+ const topLine = start[0];
407
+ const bottomLine = Math.max(visualAnchor[0], visualCursor[0]);
408
+ const totalLines = rep.lines.length();
409
+ const lines = [];
410
+ for (let i = topLine; i <= bottomLine; i++) {
411
+ lines.push(getLineText(rep, i));
412
+ }
413
+ setRegister(lines);
414
+
415
+ if (enterInsert) {
416
+ for (let i = topLine; i <= bottomLine; i++) {
417
+ const text = getLineText(rep, i);
418
+ replaceRange(editorInfo, [topLine, 0], [topLine, text.length], '');
419
+ }
420
+ moveCursor(editorInfo, topLine, 0);
421
+ setVisualMode(null);
422
+ setInsertMode(true);
423
+ return true;
424
+ }
425
+
426
+ if (bottomLine === totalLines - 1 && topLine > 0) {
427
+ const prevLineLen = getLineText(rep, topLine - 1).length;
428
+ replaceRange(editorInfo, [topLine - 1, prevLineLen], [bottomLine, getLineText(rep, bottomLine).length], '');
429
+ moveBlockCursor(editorInfo, topLine - 1, 0);
430
+ } else if (bottomLine < totalLines - 1) {
431
+ replaceRange(editorInfo, [topLine, 0], [bottomLine + 1, 0], '');
432
+ moveBlockCursor(editorInfo, topLine, 0);
433
+ } else {
434
+ replaceRange(editorInfo, [0, 0], [bottomLine, getLineText(rep, bottomLine).length], '');
435
+ moveBlockCursor(editorInfo, 0, 0);
436
+ }
437
+
438
+ setVisualMode(null);
439
+ return true;
440
+ }
441
+
442
+ pendingKey = null;
443
+ return false;
444
+ };
445
+
446
+ // --- Normal mode key handler ---
447
+
448
+ const handleNormalKey = (rep, editorInfo, key) => {
449
+ const [line, char] = rep.selStart;
450
+ const lineCount = rep.lines.length();
451
+ const lineText = getLineText(rep, line);
452
+
453
+ if (key >= '1' && key <= '9') {
454
+ countBuffer += key;
455
+ return true;
456
+ }
457
+ if (key === '0' && countBuffer !== '') {
458
+ countBuffer += key;
459
+ return true;
460
+ }
461
+
462
+ consumeCount();
463
+ const count = getCount();
464
+
465
+ if (pendingKey === 'r') {
466
+ pendingKey = null;
467
+ if (lineText.length > 0) {
468
+ replaceRange(editorInfo, [line, char], [line, char + 1], key);
469
+ moveBlockCursor(editorInfo, line, char);
470
+ }
471
+ return true;
472
+ }
473
+
474
+ if (pendingKey === 'f' || pendingKey === 'F' || pendingKey === 't' || pendingKey === 'T') {
475
+ const direction = pendingKey;
476
+ 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;
488
+ }
489
+ if (pos !== -1) moveBlockCursor(editorInfo, line, pos);
490
+ return true;
491
+ }
492
+
493
+ if (pendingKey === 'df' || pendingKey === 'dF' || pendingKey === 'dt' || pendingKey === 'dT') {
494
+ const motion = pendingKey[1];
495
+ 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
+ }
502
+ 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], '');
521
+ const newLineText = getLineText(rep, line);
522
+ moveBlockCursor(editorInfo, line, clampChar(delStart, newLineText));
523
+ }
524
+ }
525
+ return true;
526
+ }
527
+
528
+ if (pendingKey === 'd') {
529
+ pendingKey = null;
530
+
531
+ if (key === 'd') {
532
+ const deleteCount = Math.min(count, lineCount - line);
533
+ const lastDeleteLine = line + deleteCount - 1;
534
+ const deletedLines = [];
535
+ for (let i = line; i <= lastDeleteLine; i++) {
536
+ deletedLines.push(getLineText(rep, i));
537
+ }
538
+ setRegister(deletedLines);
539
+ if (lastDeleteLine === lineCount - 1 && line > 0) {
540
+ const prevLineText = getLineText(rep, line - 1);
541
+ replaceRange(editorInfo, [line - 1, prevLineText.length], [lastDeleteLine, getLineText(rep, lastDeleteLine).length], '');
542
+ moveBlockCursor(editorInfo, line - 1, clampChar(char, prevLineText));
543
+ } else if (lineCount > deleteCount) {
544
+ replaceRange(editorInfo, [line, 0], [lastDeleteLine + 1, 0], '');
545
+ const newLineText = getLineText(rep, line);
546
+ moveBlockCursor(editorInfo, line, clampChar(char, newLineText));
547
+ } else {
548
+ replaceRange(editorInfo, [0, 0], [lastDeleteLine, getLineText(rep, lastDeleteLine).length], '');
549
+ moveBlockCursor(editorInfo, 0, 0);
550
+ }
551
+ return true;
552
+ }
553
+
554
+ if (key === 'f' || key === 'F' || key === 't' || key === 'T') {
555
+ pendingKey = 'd' + key;
556
+ return true;
557
+ }
558
+
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);
593
+ }
594
+
595
+ if (delEnd > delStart && delStart !== -1) {
596
+ setRegister(lineText.slice(delStart, delEnd));
597
+ replaceRange(editorInfo, [line, delStart], [line, delEnd], '');
598
+ const newLineText = getLineText(rep, line);
599
+ moveBlockCursor(editorInfo, line, clampChar(delStart, newLineText));
600
+ }
601
+ return true;
602
+ }
603
+
604
+ if (pendingKey === 'yf' || pendingKey === 'yF' || pendingKey === 'yt' || pendingKey === 'yT') {
605
+ const motion = pendingKey[1];
606
+ 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
+ }
613
+ 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));
631
+ }
632
+ }
633
+ return true;
634
+ }
635
+
636
+ if (pendingKey === 'cf' || pendingKey === 'cF' || pendingKey === 'ct' || pendingKey === 'cT') {
637
+ const motion = pendingKey[1];
638
+ 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
+ }
645
+ 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);
665
+ setInsertMode(true);
666
+ }
667
+ }
668
+ return true;
669
+ }
670
+
671
+ if (pendingKey === 'c') {
672
+ pendingKey = null;
673
+
674
+ if (key === 'c') {
675
+ setRegister(lineText);
676
+ replaceRange(editorInfo, [line, 0], [line, lineText.length], '');
677
+ moveCursor(editorInfo, line, 0);
678
+ setInsertMode(true);
679
+ return true;
680
+ }
681
+
682
+ if (key === 'f' || key === 'F' || key === 't' || key === 'T') {
683
+ pendingKey = 'c' + key;
684
+ return true;
685
+ }
686
+
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);
721
+ }
722
+
723
+ if (delEnd > delStart && delStart !== -1) {
724
+ setRegister(lineText.slice(delStart, delEnd));
725
+ replaceRange(editorInfo, [line, delStart], [line, delEnd], '');
726
+ moveCursor(editorInfo, line, delStart);
727
+ setInsertMode(true);
728
+ }
729
+ return true;
730
+ }
731
+
732
+ if (pendingKey === 'y') {
733
+ pendingKey = null;
734
+
735
+ if (key === 'y') {
736
+ const yankCount = Math.min(count, lineCount - line);
737
+ const lastYankLine = line + yankCount - 1;
738
+ const yankedLines = [];
739
+ for (let i = line; i <= lastYankLine; i++) {
740
+ yankedLines.push(getLineText(rep, i));
741
+ }
742
+ setRegister(yankedLines);
743
+ return true;
744
+ }
745
+
746
+ if (key === 'f' || key === 'F' || key === 't' || key === 'T') {
747
+ pendingKey = 'y' + key;
748
+ return true;
749
+ }
750
+
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);
785
+ }
786
+
787
+ if (yankEnd > yankStart && yankStart !== -1) {
788
+ setRegister(lineText.slice(yankStart, yankEnd));
789
+ }
790
+ return true;
791
+ }
792
+
793
+ if (pendingKey === 'm') {
794
+ pendingKey = null;
795
+ if (key >= 'a' && key <= 'z') {
796
+ marks[key] = [line, char];
797
+ }
798
+ return true;
799
+ }
800
+
801
+ if (pendingKey === "'" || pendingKey === '`') {
802
+ const jumpType = pendingKey;
803
+ pendingKey = null;
804
+ if (key >= 'a' && key <= 'z' && marks[key]) {
805
+ const [markLine, markChar] = marks[key];
806
+ if (jumpType === "'") {
807
+ const targetLineText = getLineText(rep, markLine);
808
+ moveBlockCursor(editorInfo, markLine, firstNonBlank(targetLineText));
809
+ } else {
810
+ moveBlockCursor(editorInfo, markLine, markChar);
811
+ }
812
+ }
813
+ return true;
814
+ }
815
+
816
+ if (key === 'h') {
817
+ moveBlockCursor(editorInfo, line, Math.max(0, char - count));
818
+ return true;
819
+ }
820
+
821
+ if (key === 'l') {
822
+ moveBlockCursor(editorInfo, line, clampChar(char + count, lineText));
823
+ return true;
824
+ }
825
+
826
+ if (key === 'k') {
827
+ const newLine = clampLine(line - count, rep);
828
+ const newLineText = getLineText(rep, newLine);
829
+ moveBlockCursor(editorInfo, newLine, clampChar(char, newLineText));
830
+ return true;
831
+ }
832
+
833
+ if (key === 'j') {
834
+ const newLine = clampLine(line + count, rep);
835
+ const newLineText = getLineText(rep, newLine);
836
+ moveBlockCursor(editorInfo, newLine, clampChar(char, newLineText));
837
+ return true;
838
+ }
839
+
840
+ if (key === '0') {
841
+ moveBlockCursor(editorInfo, line, 0);
842
+ return true;
843
+ }
844
+
845
+ if (key === '$') {
846
+ moveBlockCursor(editorInfo, line, clampChar(lineText.length - 1, lineText));
847
+ return true;
848
+ }
849
+
850
+ if (key === '^') {
851
+ moveBlockCursor(editorInfo, line, firstNonBlank(lineText));
852
+ return true;
853
+ }
854
+
855
+ if (key === 'x') {
856
+ if (lineText.length > 0) {
857
+ const deleteCount = Math.min(count, lineText.length - char);
858
+ replaceRange(editorInfo, [line, char], [line, char + deleteCount], '');
859
+ const newLineText = getLineText(rep, line);
860
+ moveBlockCursor(editorInfo, line, clampChar(char, newLineText));
861
+ }
862
+ return true;
863
+ }
864
+
865
+ if (key === 'w') {
866
+ let pos = char;
867
+ for (let i = 0; i < count; i++) pos = wordForward(lineText, pos);
868
+ moveBlockCursor(editorInfo, line, clampChar(pos, lineText));
869
+ return true;
870
+ }
871
+
872
+ if (key === 'b') {
873
+ let pos = char;
874
+ for (let i = 0; i < count; i++) pos = wordBackward(lineText, pos);
875
+ moveBlockCursor(editorInfo, line, pos);
876
+ return true;
877
+ }
878
+
879
+ if (key === 'o') {
880
+ replaceRange(editorInfo, [line, lineText.length], [line, lineText.length], '\n');
881
+ moveCursor(editorInfo, line + 1, 0);
882
+ setInsertMode(true);
883
+ return true;
884
+ }
885
+
886
+ if (key === 'O') {
887
+ replaceRange(editorInfo, [line, 0], [line, 0], '\n');
888
+ moveCursor(editorInfo, line, 0);
889
+ setInsertMode(true);
890
+ return true;
891
+ }
892
+
893
+ if (key === 'u') {
894
+ undo(editorInfo);
895
+ return true;
896
+ }
897
+
898
+ if (key === 'p') {
899
+ if (register !== null) {
900
+ if (typeof register === 'string') {
901
+ const insertPos = Math.min(char + 1, lineText.length);
902
+ const repeated = register.repeat(count);
903
+ replaceRange(editorInfo, [line, insertPos], [line, insertPos], repeated);
904
+ moveBlockCursor(editorInfo, line, insertPos);
905
+ } else {
906
+ const block = register.join('\n');
907
+ const parts = [];
908
+ 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);
911
+ moveBlockCursor(editorInfo, line + 1, 0);
912
+ }
913
+ }
914
+ return true;
915
+ }
916
+
917
+ if (key === 'P') {
918
+ if (register !== null) {
919
+ if (typeof register === 'string') {
920
+ const repeated = register.repeat(count);
921
+ replaceRange(editorInfo, [line, char], [line, char], repeated);
922
+ moveBlockCursor(editorInfo, line, char);
923
+ } else {
924
+ const block = register.join('\n');
925
+ const parts = [];
926
+ for (let i = 0; i < count; i++) parts.push(block);
927
+ const insertText = parts.join('\n') + '\n';
928
+ replaceRange(editorInfo, [line, 0], [line, 0], insertText);
929
+ moveBlockCursor(editorInfo, line, 0);
930
+ }
931
+ }
932
+ return true;
933
+ }
934
+
935
+ if (key === 'G') {
936
+ if (pendingCount !== null) {
937
+ moveBlockCursor(editorInfo, clampLine(pendingCount - 1, rep), 0);
938
+ } else {
939
+ moveBlockCursor(editorInfo, lineCount - 1, 0);
940
+ }
941
+ return true;
942
+ }
943
+
944
+ if (key === 'g') {
945
+ if (pendingKey === 'g') {
946
+ pendingKey = null;
947
+ moveBlockCursor(editorInfo, 0, 0);
948
+ } else {
949
+ pendingKey = 'g';
950
+ }
951
+ return true;
952
+ }
953
+
954
+ if (key === 'r') {
955
+ if (lineText.length > 0) {
956
+ pendingKey = 'r';
957
+ }
958
+ return true;
959
+ }
960
+
961
+ if (key === 'f' || key === 'F' || key === 't' || key === 'T') {
962
+ pendingKey = key;
963
+ return true;
964
+ }
965
+
966
+ if (key === 'm') {
967
+ pendingKey = 'm';
968
+ return true;
969
+ }
970
+
971
+ if (key === "'" || key === '`') {
972
+ pendingKey = key;
973
+ return true;
974
+ }
975
+
976
+ if (key === 'd') {
977
+ pendingKey = 'd';
978
+ return true;
979
+ }
980
+
981
+ if (key === 'c') {
982
+ pendingKey = 'c';
983
+ return true;
984
+ }
985
+
986
+ if (key === 'y') {
987
+ pendingKey = 'y';
988
+ return true;
989
+ }
990
+
991
+ if (key === 'Y') {
992
+ setRegister([lineText]);
993
+ return true;
994
+ }
995
+
996
+ if (key === 'J') {
997
+ const joins = Math.min(count, lineCount - 1 - line);
998
+ let cursorChar = lineText.length;
999
+ for (let i = 0; i < joins; i++) {
1000
+ const curLineText = getLineText(rep, line);
1001
+ const nextLineText = getLineText(rep, line + 1);
1002
+ const trimmedNext = nextLineText.replace(/^\s+/, '');
1003
+ const separator = curLineText.length === 0 ? '' : ' ';
1004
+ if (i === 0) cursorChar = curLineText.length;
1005
+ replaceRange(editorInfo, [line, curLineText.length], [line + 1, nextLineText.length], separator + trimmedNext);
1006
+ }
1007
+ moveBlockCursor(editorInfo, line, cursorChar);
1008
+ return true;
1009
+ }
1010
+
1011
+ if (key === 'C') {
1012
+ setRegister(lineText.slice(char));
1013
+ replaceRange(editorInfo, [line, char], [line, lineText.length], '');
1014
+ moveCursor(editorInfo, line, char);
1015
+ setInsertMode(true);
1016
+ return true;
1017
+ }
1018
+
1019
+ if (key === 's') {
1020
+ setRegister(lineText.slice(char, char + 1));
1021
+ replaceRange(editorInfo, [line, char], [line, Math.min(char + count, lineText.length)], '');
1022
+ moveCursor(editorInfo, line, char);
1023
+ setInsertMode(true);
1024
+ return true;
1025
+ }
1026
+
1027
+ if (key === 'S') {
1028
+ setRegister(lineText);
1029
+ replaceRange(editorInfo, [line, 0], [line, lineText.length], '');
1030
+ moveCursor(editorInfo, line, 0);
1031
+ setInsertMode(true);
1032
+ return true;
1033
+ }
1034
+
1035
+ pendingKey = null;
1036
+
1037
+ if (key === 'e') {
1038
+ let pos = char;
1039
+ for (let i = 0; i < count; i++) pos = wordEnd(lineText, pos);
1040
+ moveBlockCursor(editorInfo, line, clampChar(pos, lineText));
1041
+ return true;
1042
+ }
1043
+
1044
+ return false;
1045
+ };
1046
+
1047
+ // --- Exports ---
1048
+
1049
+ exports.aceEditorCSS = () => ['ep_vim/static/css/vim.css'];
1050
+
1051
+ exports.postToolbarInit = (_hookName, _args) => {
1052
+ const btn = document.getElementById('vim-toggle-btn');
1053
+ if (!btn) return;
1054
+ btn.classList.toggle('vim-enabled', vimEnabled);
1055
+ btn.addEventListener('click', () => {
1056
+ vimEnabled = !vimEnabled;
1057
+ localStorage.setItem('ep_vimEnabled', vimEnabled ? 'true' : 'false');
1058
+ btn.classList.toggle('vim-enabled', vimEnabled);
1059
+ });
1060
+ };
1061
+
1062
+ exports.aceKeyEvent = (_hookName, {evt, rep, editorInfo}) => {
1063
+ if (!vimEnabled) return false;
1064
+ if (evt.type !== 'keydown') return false;
1065
+ currentRep = rep;
1066
+ if (!editorDoc) {
1067
+ editorDoc = evt.target.ownerDocument;
1068
+ setInsertMode(insertMode);
1069
+ }
1070
+
1071
+ if (visualMode !== null && pendingKey !== null) {
1072
+ const handled = handleVisualKey(rep, editorInfo, evt.key);
1073
+ evt.preventDefault();
1074
+ return handled || true;
1075
+ }
1076
+
1077
+ if (!insertMode && visualMode === null && pendingKey !== null) {
1078
+ const handled = handleNormalKey(rep, editorInfo, evt.key);
1079
+ evt.preventDefault();
1080
+ return handled || true;
1081
+ }
1082
+
1083
+ if (!insertMode && evt.key === 'i') {
1084
+ const [line, char] = rep.selStart;
1085
+ moveCursor(editorInfo, line, char);
1086
+ setVisualMode(null);
1087
+ setInsertMode(true);
1088
+ evt.preventDefault();
1089
+ return true;
1090
+ }
1091
+
1092
+ if (!insertMode && evt.key === 'a') {
1093
+ const [line, char] = rep.selStart;
1094
+ const lineText = getLineText(rep, line);
1095
+ moveCursor(editorInfo, line, Math.min(char + 1, lineText.length));
1096
+ setVisualMode(null);
1097
+ setInsertMode(true);
1098
+ evt.preventDefault();
1099
+ return true;
1100
+ }
1101
+
1102
+ if (!insertMode && evt.key === 'A') {
1103
+ const [line] = rep.selStart;
1104
+ const lineText = getLineText(rep, line);
1105
+ moveCursor(editorInfo, line, lineText.length);
1106
+ setVisualMode(null);
1107
+ setInsertMode(true);
1108
+ evt.preventDefault();
1109
+ return true;
1110
+ }
1111
+
1112
+ if (!insertMode && evt.key === 'I') {
1113
+ const [line] = rep.selStart;
1114
+ const lineText = getLineText(rep, line);
1115
+ moveCursor(editorInfo, line, firstNonBlank(lineText));
1116
+ setVisualMode(null);
1117
+ setInsertMode(true);
1118
+ evt.preventDefault();
1119
+ return true;
1120
+ }
1121
+
1122
+ if (evt.key === 'Escape') {
1123
+ if (insertMode) {
1124
+ setInsertMode(false);
1125
+ const [line, char] = rep.selStart;
1126
+ moveBlockCursor(editorInfo, line, Math.max(0, char - 1));
1127
+ }
1128
+ if (visualMode !== null) {
1129
+ const [vLine, vChar] = visualCursor;
1130
+ setVisualMode(null);
1131
+ moveBlockCursor(editorInfo, vLine, vChar);
1132
+ }
1133
+ countBuffer = '';
1134
+ pendingKey = null;
1135
+ pendingCount = null;
1136
+ evt.preventDefault();
1137
+ return true;
1138
+ }
1139
+
1140
+ if (!insertMode && visualMode === null && evt.key === 'V') {
1141
+ const [line] = rep.selStart;
1142
+ visualAnchor = [line, 0];
1143
+ visualCursor = [line, 0];
1144
+ setVisualMode('line');
1145
+ updateVisualSelection(editorInfo, rep);
1146
+ evt.preventDefault();
1147
+ return true;
1148
+ }
1149
+
1150
+ if (!insertMode && visualMode === null && evt.key === 'v') {
1151
+ const [line, char] = rep.selStart;
1152
+ visualAnchor = [line, char];
1153
+ visualCursor = [line, char];
1154
+ setVisualMode('char');
1155
+ updateVisualSelection(editorInfo, rep);
1156
+ evt.preventDefault();
1157
+ return true;
1158
+ }
1159
+
1160
+ if (visualMode !== null) {
1161
+ const handled = handleVisualKey(rep, editorInfo, evt.key);
1162
+ evt.preventDefault();
1163
+ return handled || true;
1164
+ }
1165
+
1166
+ if (insertMode) {
1167
+ return false;
1168
+ }
1169
+
1170
+ const handled = handleNormalKey(rep, editorInfo, evt.key);
1171
+ evt.preventDefault();
1172
+ return handled || true;
1173
+ };