@vemjs/core 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.
package/dist/editor.js ADDED
@@ -0,0 +1,818 @@
1
+ import { VimBuffer, UndoManager } from './buffer';
2
+ import { getWordForward, getWordBackward, getWordEndForward, getTextObjectRange } from './motions';
3
+ import { parseKeys } from './parser';
4
+ export class VemEditorState {
5
+ mode = 'NORMAL';
6
+ cursor = { line: 0, character: 0 };
7
+ desiredCol = 0;
8
+ buffer;
9
+ undoManager;
10
+ register = null;
11
+ pendingKeys = [];
12
+ visualSelection = null;
13
+ isInsertMutated = false;
14
+ changeCallbacks = [];
15
+ commandText = '';
16
+ saveCallbacks = [];
17
+ quitCallbacks = [];
18
+ splitCallbacks = [];
19
+ customKeybindings = new Map();
20
+ didOpenBufferCallbacks = [];
21
+ changeBufferCallbacks = [];
22
+ changeModeCallbacks = [];
23
+ pluginCommandCallbacks = [];
24
+ diagnostics = [];
25
+ publishDiagnosticsCallbacks = [];
26
+ constructor(initialText) {
27
+ this.buffer = new VimBuffer(initialText);
28
+ this.undoManager = new UndoManager();
29
+ this.buffer.onChange(() => {
30
+ this.triggerChangeBuffer();
31
+ });
32
+ setTimeout(() => {
33
+ this.triggerDidOpenBuffer();
34
+ }, 0);
35
+ }
36
+ // --- Callbacks & Events ---
37
+ onChange(callback) {
38
+ this.changeCallbacks.push(callback);
39
+ }
40
+ triggerChange() {
41
+ for (const cb of this.changeCallbacks) {
42
+ cb();
43
+ }
44
+ }
45
+ onSave(callback) {
46
+ this.saveCallbacks.push(callback);
47
+ }
48
+ onQuit(callback) {
49
+ this.quitCallbacks.push(callback);
50
+ }
51
+ onSplit(callback) {
52
+ this.splitCallbacks.push(callback);
53
+ }
54
+ triggerSave() {
55
+ for (const cb of this.saveCallbacks) {
56
+ cb();
57
+ }
58
+ }
59
+ triggerQuit() {
60
+ for (const cb of this.quitCallbacks) {
61
+ cb();
62
+ }
63
+ }
64
+ triggerSplit(direction) {
65
+ for (const cb of this.splitCallbacks) {
66
+ cb(direction);
67
+ }
68
+ }
69
+ registerKeybinding(mode, keys, commandName) {
70
+ if (!this.customKeybindings.has(mode)) {
71
+ this.customKeybindings.set(mode, new Map());
72
+ }
73
+ this.customKeybindings.get(mode).set(keys, commandName);
74
+ }
75
+ onDidOpenBuffer(callback) {
76
+ this.didOpenBufferCallbacks.push(callback);
77
+ }
78
+ onDidChangeBuffer(callback) {
79
+ this.changeBufferCallbacks.push(callback);
80
+ }
81
+ onDidChangeMode(callback) {
82
+ this.changeModeCallbacks.push(callback);
83
+ }
84
+ onExecutePluginCommand(callback) {
85
+ this.pluginCommandCallbacks.push(callback);
86
+ }
87
+ onPublishDiagnostics(callback) {
88
+ this.publishDiagnosticsCallbacks.push(callback);
89
+ }
90
+ setDiagnostics(diagnostics) {
91
+ this.diagnostics = diagnostics;
92
+ for (const cb of this.publishDiagnosticsCallbacks) {
93
+ cb(this.diagnostics);
94
+ }
95
+ // Trigger a render-change so renderers can repaint highlight overlays
96
+ for (const cb of this.changeCallbacks) {
97
+ cb();
98
+ }
99
+ }
100
+ getDiagnostics() {
101
+ return this.diagnostics;
102
+ }
103
+ triggerDidOpenBuffer() {
104
+ for (const cb of this.didOpenBufferCallbacks) {
105
+ cb();
106
+ }
107
+ }
108
+ triggerChangeBuffer() {
109
+ for (const cb of this.changeBufferCallbacks) {
110
+ cb();
111
+ }
112
+ }
113
+ triggerChangeMode(mode) {
114
+ for (const cb of this.changeModeCallbacks) {
115
+ cb(mode);
116
+ }
117
+ }
118
+ executePluginCommand(commandName) {
119
+ for (const cb of this.pluginCommandCallbacks) {
120
+ cb(commandName);
121
+ }
122
+ }
123
+ // --- Getters & Setters ---
124
+ getMode() {
125
+ return this.mode;
126
+ }
127
+ getText() {
128
+ return this.buffer.getText();
129
+ }
130
+ setMode(mode) {
131
+ if (this.mode === mode)
132
+ return;
133
+ // Handle exiting insert mode
134
+ if (this.mode === 'INSERT') {
135
+ this.isInsertMutated = false;
136
+ }
137
+ this.mode = mode;
138
+ // Initialize visual selection if entering visual mode
139
+ if (mode === 'VISUAL') {
140
+ this.visualSelection = {
141
+ type: 'char',
142
+ anchor: { ...this.cursor },
143
+ active: { ...this.cursor },
144
+ };
145
+ }
146
+ else {
147
+ this.visualSelection = null;
148
+ }
149
+ this.triggerChangeMode(mode);
150
+ this.triggerChange();
151
+ }
152
+ getCursor() {
153
+ return { ...this.cursor };
154
+ }
155
+ getBuffer() {
156
+ return this.buffer;
157
+ }
158
+ getPendingKeys() {
159
+ return [...this.pendingKeys];
160
+ }
161
+ getVisualSelection() {
162
+ return this.visualSelection;
163
+ }
164
+ getRegister() {
165
+ return this.register;
166
+ }
167
+ getCommandText() {
168
+ return this.commandText;
169
+ }
170
+ // --- Key Input Entry Point ---
171
+ handleKey(key) {
172
+ if (this.mode === 'COMMAND') {
173
+ if (key === 'Escape') {
174
+ this.setMode('NORMAL');
175
+ this.commandText = '';
176
+ this.triggerChange();
177
+ return;
178
+ }
179
+ if (key === 'Enter') {
180
+ this.executeCommandLineText(this.commandText);
181
+ this.setMode('NORMAL');
182
+ this.commandText = '';
183
+ this.triggerChange();
184
+ return;
185
+ }
186
+ if (key === 'Backspace') {
187
+ this.commandText = this.commandText.substring(0, this.commandText.length - 1);
188
+ this.triggerChange();
189
+ return;
190
+ }
191
+ if (key.length === 1) {
192
+ this.commandText += key;
193
+ this.triggerChange();
194
+ return;
195
+ }
196
+ return;
197
+ }
198
+ // Check custom keybindings
199
+ const modeBindings = this.customKeybindings.get(this.mode);
200
+ if (modeBindings) {
201
+ const currentSequence = [...this.pendingKeys, key].join('');
202
+ let hasExactMatch = false;
203
+ let hasPartialMatch = false;
204
+ let matchedCommand = '';
205
+ for (const [keys, cmd] of modeBindings.entries()) {
206
+ if (keys === currentSequence) {
207
+ hasExactMatch = true;
208
+ matchedCommand = cmd;
209
+ }
210
+ else if (keys.startsWith(currentSequence)) {
211
+ hasPartialMatch = true;
212
+ }
213
+ }
214
+ if (hasExactMatch && !hasPartialMatch) {
215
+ this.pendingKeys = [];
216
+ this.executePluginCommand(matchedCommand);
217
+ this.triggerChange();
218
+ return;
219
+ }
220
+ if (hasPartialMatch) {
221
+ this.pendingKeys.push(key);
222
+ this.triggerChange();
223
+ return;
224
+ }
225
+ if (this.pendingKeys.length > 0) {
226
+ const keysToReplay = [...this.pendingKeys, key];
227
+ this.pendingKeys = [];
228
+ for (const k of keysToReplay) {
229
+ this.handleKeyStandard(k);
230
+ }
231
+ return;
232
+ }
233
+ }
234
+ this.handleKeyStandard(key);
235
+ }
236
+ handleKeyStandard(key) {
237
+ if (this.mode === 'INSERT') {
238
+ if (key === 'Escape') {
239
+ this.setMode('NORMAL');
240
+ // Move cursor back one character in Normal mode
241
+ this.cursor.character = Math.max(0, this.cursor.character - 1);
242
+ this.desiredCol = this.cursor.character;
243
+ this.triggerChange();
244
+ return;
245
+ }
246
+ if (key === 'Backspace') {
247
+ this.handleBackspaceInInsert();
248
+ this.triggerChange();
249
+ return;
250
+ }
251
+ if (key === 'Enter') {
252
+ this.handleEnterInInsert();
253
+ this.triggerChange();
254
+ return;
255
+ }
256
+ if (key.length === 1) {
257
+ this.handleCharInputInInsert(key);
258
+ this.triggerChange();
259
+ return;
260
+ }
261
+ return;
262
+ }
263
+ // Normal or Visual Mode
264
+ if (key === 'Escape') {
265
+ this.pendingKeys = [];
266
+ if (this.mode === 'VISUAL') {
267
+ this.setMode('NORMAL');
268
+ }
269
+ this.triggerChange();
270
+ return;
271
+ }
272
+ this.pendingKeys.push(key);
273
+ const parsed = parseKeys(this.pendingKeys, this.mode);
274
+ if (!parsed.isValid) {
275
+ this.pendingKeys = [];
276
+ return;
277
+ }
278
+ if (parsed.isComplete) {
279
+ this.pendingKeys = [];
280
+ this.executeCommand(parsed);
281
+ this.triggerChange();
282
+ }
283
+ }
284
+ // --- Insertion Helpers ---
285
+ handleCharInputInInsert(char) {
286
+ if (!this.isInsertMutated) {
287
+ this.saveStateForUndo();
288
+ this.isInsertMutated = true;
289
+ }
290
+ const line = this.buffer.getLine(this.cursor.line);
291
+ const before = line.substring(0, this.cursor.character);
292
+ const after = line.substring(this.cursor.character);
293
+ this.buffer.setLine(this.cursor.line, before + char + after);
294
+ this.cursor.character++;
295
+ this.desiredCol = this.cursor.character;
296
+ }
297
+ handleBackspaceInInsert() {
298
+ if (!this.isInsertMutated) {
299
+ this.saveStateForUndo();
300
+ this.isInsertMutated = true;
301
+ }
302
+ if (this.cursor.character > 0) {
303
+ const line = this.buffer.getLine(this.cursor.line);
304
+ const before = line.substring(0, this.cursor.character - 1);
305
+ const after = line.substring(this.cursor.character);
306
+ this.buffer.setLine(this.cursor.line, before + after);
307
+ this.cursor.character--;
308
+ this.desiredCol = this.cursor.character;
309
+ }
310
+ else if (this.cursor.line > 0) {
311
+ const prevLine = this.buffer.getLine(this.cursor.line - 1);
312
+ const currLine = this.buffer.getLine(this.cursor.line);
313
+ this.buffer.setLine(this.cursor.line - 1, prevLine + currLine);
314
+ this.buffer.deleteLines(this.cursor.line, this.cursor.line);
315
+ this.cursor.line--;
316
+ this.cursor.character = prevLine.length;
317
+ this.desiredCol = this.cursor.character;
318
+ }
319
+ }
320
+ handleEnterInInsert() {
321
+ if (!this.isInsertMutated) {
322
+ this.saveStateForUndo();
323
+ this.isInsertMutated = true;
324
+ }
325
+ const line = this.buffer.getLine(this.cursor.line);
326
+ const before = line.substring(0, this.cursor.character);
327
+ const after = line.substring(this.cursor.character);
328
+ this.buffer.setLine(this.cursor.line, before);
329
+ this.buffer.insertLine(this.cursor.line + 1, after);
330
+ this.cursor.line++;
331
+ this.cursor.character = 0;
332
+ this.desiredCol = 0;
333
+ }
334
+ // --- Command Execution ---
335
+ executeCommand(cmd) {
336
+ if (this.mode === 'VISUAL') {
337
+ this.executeVisualCommand(cmd);
338
+ return;
339
+ }
340
+ // NORMAL mode commands
341
+ if (cmd.operator) {
342
+ this.executeOperatorCommand(cmd);
343
+ return;
344
+ }
345
+ if (cmd.motion) {
346
+ this.moveCursorByMotion(cmd.motion, cmd.count);
347
+ return;
348
+ }
349
+ if (cmd.command) {
350
+ switch (cmd.command) {
351
+ case 'i':
352
+ this.setMode('INSERT');
353
+ break;
354
+ case 'I':
355
+ this.moveToFirstNonWhitespace();
356
+ this.setMode('INSERT');
357
+ break;
358
+ case 'a':
359
+ this.moveCursorRightForInsert();
360
+ this.setMode('INSERT');
361
+ break;
362
+ case 'A':
363
+ this.moveToEndOfLine();
364
+ this.setMode('INSERT');
365
+ break;
366
+ case 'o':
367
+ this.saveStateForUndo();
368
+ this.buffer.insertLine(this.cursor.line + 1, '');
369
+ this.cursor.line++;
370
+ this.cursor.character = 0;
371
+ this.desiredCol = 0;
372
+ this.setMode('INSERT');
373
+ break;
374
+ case 'O':
375
+ this.saveStateForUndo();
376
+ this.buffer.insertLine(this.cursor.line, '');
377
+ this.cursor.character = 0;
378
+ this.desiredCol = 0;
379
+ this.setMode('INSERT');
380
+ break;
381
+ case 'x':
382
+ this.saveStateForUndo();
383
+ this.deleteCharUnderCursor(cmd.count);
384
+ break;
385
+ case 'u':
386
+ this.undo();
387
+ break;
388
+ case '<C-r>':
389
+ this.redo();
390
+ break;
391
+ case 'v':
392
+ this.setMode('VISUAL');
393
+ if (this.visualSelection)
394
+ this.visualSelection.type = 'char';
395
+ break;
396
+ case 'V':
397
+ this.setMode('VISUAL');
398
+ if (this.visualSelection)
399
+ this.visualSelection.type = 'line';
400
+ break;
401
+ case '<C-v>':
402
+ this.setMode('VISUAL');
403
+ if (this.visualSelection)
404
+ this.visualSelection.type = 'block';
405
+ break;
406
+ case 'p':
407
+ this.saveStateForUndo();
408
+ this.paste(false);
409
+ break;
410
+ case 'P':
411
+ this.saveStateForUndo();
412
+ this.paste(true);
413
+ break;
414
+ case ':':
415
+ this.setMode('COMMAND');
416
+ this.commandText = '';
417
+ break;
418
+ }
419
+ }
420
+ }
421
+ executeCommandLineText(cmd) {
422
+ if (cmd === 'w') {
423
+ this.triggerSave();
424
+ }
425
+ else if (cmd === 'q') {
426
+ this.triggerQuit();
427
+ }
428
+ else if (cmd === 'vsp') {
429
+ this.triggerSplit('vertical');
430
+ }
431
+ else if (cmd === 'sp') {
432
+ this.triggerSplit('horizontal');
433
+ }
434
+ }
435
+ executeVisualCommand(cmd) {
436
+ if (cmd.motion) {
437
+ this.moveCursorByMotion(cmd.motion, cmd.count);
438
+ if (this.visualSelection) {
439
+ this.visualSelection.active = { ...this.cursor };
440
+ }
441
+ return;
442
+ }
443
+ const op = cmd.command || cmd.operator;
444
+ if (op === 'd' || op === 'x' || op === 'c' || op === 'y') {
445
+ this.saveStateForUndo();
446
+ this.operateOnVisualSelection(op);
447
+ if (op === 'c') {
448
+ this.setMode('INSERT');
449
+ }
450
+ else {
451
+ this.setMode('NORMAL');
452
+ }
453
+ }
454
+ }
455
+ // --- Motions Execution ---
456
+ moveCursorByMotion(motion, count) {
457
+ for (let i = 0; i < count; i++) {
458
+ switch (motion) {
459
+ case 'h':
460
+ this.cursor.character = Math.max(0, this.cursor.character - 1);
461
+ this.desiredCol = this.cursor.character;
462
+ break;
463
+ case 'l': {
464
+ const lineLen = this.buffer.getLine(this.cursor.line).length;
465
+ const maxChar = this.mode === 'INSERT' ? lineLen : Math.max(0, lineLen - 1);
466
+ this.cursor.character = Math.min(maxChar, this.cursor.character + 1);
467
+ this.desiredCol = this.cursor.character;
468
+ break;
469
+ }
470
+ case 'j':
471
+ if (this.cursor.line < this.buffer.getLineCount() - 1) {
472
+ this.cursor.line++;
473
+ const lineLen = this.buffer.getLine(this.cursor.line).length;
474
+ const maxChar = this.mode === 'INSERT' ? lineLen : Math.max(0, lineLen - 1);
475
+ this.cursor.character = Math.min(maxChar, this.desiredCol);
476
+ }
477
+ break;
478
+ case 'k':
479
+ if (this.cursor.line > 0) {
480
+ this.cursor.line--;
481
+ const lineLen = this.buffer.getLine(this.cursor.line).length;
482
+ const maxChar = this.mode === 'INSERT' ? lineLen : Math.max(0, lineLen - 1);
483
+ this.cursor.character = Math.min(maxChar, this.desiredCol);
484
+ }
485
+ break;
486
+ case 'w':
487
+ this.cursor = getWordForward(this.buffer, this.cursor);
488
+ this.desiredCol = this.cursor.character;
489
+ break;
490
+ case 'b':
491
+ this.cursor = getWordBackward(this.buffer, this.cursor);
492
+ this.desiredCol = this.cursor.character;
493
+ break;
494
+ case 'e':
495
+ this.cursor = getWordEndForward(this.buffer, this.cursor);
496
+ this.desiredCol = this.cursor.character;
497
+ break;
498
+ case '0':
499
+ this.cursor.character = 0;
500
+ this.desiredCol = 0;
501
+ break;
502
+ case '$': {
503
+ const lineLen = this.buffer.getLine(this.cursor.line).length;
504
+ this.cursor.character = Math.max(0, lineLen - 1);
505
+ this.desiredCol = Infinity;
506
+ break;
507
+ }
508
+ case 'gg':
509
+ this.cursor.line = 0;
510
+ this.cursor.character = 0;
511
+ this.desiredCol = 0;
512
+ break;
513
+ case 'G':
514
+ this.cursor.line = this.buffer.getLineCount() - 1;
515
+ this.cursor.character = 0;
516
+ this.desiredCol = 0;
517
+ break;
518
+ }
519
+ }
520
+ }
521
+ // --- Operator Command Execution ---
522
+ executeOperatorCommand(cmd) {
523
+ const op = cmd.operator;
524
+ const count = cmd.count;
525
+ // Double operator e.g. dd, cc, yy
526
+ if (cmd.command === op + op) {
527
+ this.saveStateForUndo();
528
+ const startLine = this.cursor.line;
529
+ const endLine = Math.min(this.buffer.getLineCount() - 1, this.cursor.line + count - 1);
530
+ // Extract text for yank
531
+ const yankedLines = [];
532
+ for (let l = startLine; l <= endLine; l++) {
533
+ yankedLines.push(this.buffer.getLine(l));
534
+ }
535
+ this.register = {
536
+ text: yankedLines.join('\n') + '\n',
537
+ type: 'line',
538
+ };
539
+ if (op === 'd' || op === 'c') {
540
+ this.buffer.deleteLines(startLine, endLine);
541
+ this.cursor.line = Math.min(this.cursor.line, this.buffer.getLineCount() - 1);
542
+ this.cursor.character = 0;
543
+ this.desiredCol = 0;
544
+ if (op === 'c') {
545
+ this.buffer.insertLine(this.cursor.line, '');
546
+ this.setMode('INSERT');
547
+ }
548
+ }
549
+ return;
550
+ }
551
+ // Motions or Text Objects
552
+ let range = null;
553
+ let isLineWise = false;
554
+ if (cmd.textObject) {
555
+ range = getTextObjectRange(this.buffer, this.cursor, cmd.textObject);
556
+ }
557
+ else if (cmd.motion) {
558
+ const startPos = { ...this.cursor };
559
+ this.moveCursorByMotion(cmd.motion, count);
560
+ const endPos = { ...this.cursor };
561
+ this.cursor = { ...startPos }; // Restore cursor before operation
562
+ range = { start: startPos, end: endPos };
563
+ // Line-wise motion check
564
+ if (cmd.motion === 'j' || cmd.motion === 'k' || cmd.motion === 'gg' || cmd.motion === 'G') {
565
+ isLineWise = true;
566
+ }
567
+ }
568
+ if (!range)
569
+ return;
570
+ this.saveStateForUndo();
571
+ // Ensure start is before end
572
+ let s = { ...range.start };
573
+ let e = { ...range.end };
574
+ if (s.line > e.line || (s.line === e.line && s.character > e.character)) {
575
+ const temp = s;
576
+ s = e;
577
+ e = temp;
578
+ }
579
+ if (isLineWise) {
580
+ const yankedLines = [];
581
+ for (let l = s.line; l <= e.line; l++) {
582
+ yankedLines.push(this.buffer.getLine(l));
583
+ }
584
+ this.register = {
585
+ text: yankedLines.join('\n') + '\n',
586
+ type: 'line',
587
+ };
588
+ if (op === 'd' || op === 'c') {
589
+ this.buffer.deleteLines(s.line, e.line);
590
+ this.cursor.line = Math.min(s.line, this.buffer.getLineCount() - 1);
591
+ this.cursor.character = 0;
592
+ this.desiredCol = 0;
593
+ if (op === 'c') {
594
+ this.buffer.insertLine(this.cursor.line, '');
595
+ this.setMode('INSERT');
596
+ }
597
+ }
598
+ }
599
+ else {
600
+ // Character-wise operation
601
+ // Inclusive vs Exclusive: motions like $ or text objects are inclusive. Others are exclusive.
602
+ // E.g. 'dw' deletes from start to start of next word (exclusive).
603
+ // 'd$' deletes to end of line (inclusive).
604
+ let isInclusive = cmd.textObject !== undefined || cmd.motion === '$';
605
+ const startLineText = this.buffer.getLine(s.line);
606
+ const endLineText = this.buffer.getLine(e.line);
607
+ let yankText = '';
608
+ if (s.line === e.line) {
609
+ yankText = startLineText.substring(s.character, e.character + (isInclusive ? 1 : 0));
610
+ }
611
+ else {
612
+ yankText = startLineText.substring(s.character) + '\n';
613
+ for (let l = s.line + 1; l < e.line; l++) {
614
+ yankText += this.buffer.getLine(l) + '\n';
615
+ }
616
+ yankText += endLineText.substring(0, e.character + (isInclusive ? 1 : 0));
617
+ }
618
+ this.register = {
619
+ text: yankText,
620
+ type: 'char',
621
+ };
622
+ if (op === 'd' || op === 'c') {
623
+ const deleteEnd = { ...e };
624
+ if (isInclusive) {
625
+ deleteEnd.character++;
626
+ }
627
+ this.buffer.deleteRange(s, deleteEnd);
628
+ this.cursor = { ...s };
629
+ this.desiredCol = this.cursor.character;
630
+ if (op === 'c') {
631
+ this.setMode('INSERT');
632
+ }
633
+ }
634
+ }
635
+ }
636
+ // --- Visual Mode Operations ---
637
+ operateOnVisualSelection(op) {
638
+ if (!this.visualSelection)
639
+ return;
640
+ const { type, anchor, active } = this.visualSelection;
641
+ let s = { ...anchor };
642
+ let e = { ...active };
643
+ // Standard ordering
644
+ if (s.line > e.line || (s.line === e.line && s.character > e.character)) {
645
+ const temp = s;
646
+ s = e;
647
+ e = temp;
648
+ }
649
+ if (type === 'line') {
650
+ const startLine = Math.min(s.line, e.line);
651
+ const endLine = Math.max(s.line, e.line);
652
+ const yankedLines = [];
653
+ for (let l = startLine; l <= endLine; l++) {
654
+ yankedLines.push(this.buffer.getLine(l));
655
+ }
656
+ this.register = {
657
+ text: yankedLines.join('\n') + '\n',
658
+ type: 'line',
659
+ };
660
+ if (op === 'd' || op === 'c') {
661
+ this.buffer.deleteLines(startLine, endLine);
662
+ this.cursor.line = Math.min(startLine, this.buffer.getLineCount() - 1);
663
+ this.cursor.character = 0;
664
+ this.desiredCol = 0;
665
+ if (op === 'c') {
666
+ this.buffer.insertLine(this.cursor.line, '');
667
+ }
668
+ }
669
+ }
670
+ else if (type === 'char') {
671
+ const startLineText = this.buffer.getLine(s.line);
672
+ const endLineText = this.buffer.getLine(e.line);
673
+ let yankText = '';
674
+ if (s.line === e.line) {
675
+ yankText = startLineText.substring(s.character, e.character + 1);
676
+ }
677
+ else {
678
+ yankText = startLineText.substring(s.character) + '\n';
679
+ for (let l = s.line + 1; l < e.line; l++) {
680
+ yankText += this.buffer.getLine(l) + '\n';
681
+ }
682
+ yankText += endLineText.substring(0, e.character + 1);
683
+ }
684
+ this.register = {
685
+ text: yankText,
686
+ type: 'char',
687
+ };
688
+ if (op === 'd' || op === 'c') {
689
+ const deleteEnd = { ...e };
690
+ deleteEnd.character++; // Inclusive delete
691
+ this.buffer.deleteRange(s, deleteEnd);
692
+ this.cursor = { ...s };
693
+ this.desiredCol = this.cursor.character;
694
+ }
695
+ }
696
+ else if (type === 'block') {
697
+ // Block-wise selection yanks/deletes character rectangles
698
+ const minCol = Math.min(anchor.character, active.character);
699
+ const maxCol = Math.max(anchor.character, active.character);
700
+ const minLine = Math.min(anchor.line, active.line);
701
+ const maxLine = Math.max(anchor.line, active.line);
702
+ const yankedBlocks = [];
703
+ for (let l = minLine; l <= maxLine; l++) {
704
+ const lineText = this.buffer.getLine(l);
705
+ const chunk = lineText.substring(minCol, maxCol + 1);
706
+ yankedBlocks.push(chunk);
707
+ }
708
+ this.register = {
709
+ text: yankedBlocks.join('\n'),
710
+ type: 'block',
711
+ };
712
+ if (op === 'd' || op === 'c') {
713
+ for (let l = minLine; l <= maxLine; l++) {
714
+ const lineText = this.buffer.getLine(l);
715
+ const before = lineText.substring(0, minCol);
716
+ const after = lineText.substring(maxCol + 1);
717
+ this.buffer.setLine(l, before + after);
718
+ }
719
+ this.cursor = { line: minLine, character: minCol };
720
+ this.desiredCol = minCol;
721
+ }
722
+ }
723
+ }
724
+ // --- Editing Actions ---
725
+ moveToFirstNonWhitespace() {
726
+ const line = this.buffer.getLine(this.cursor.line);
727
+ const match = /^[ \t]*/.exec(line);
728
+ const index = match ? match[0].length : 0;
729
+ this.cursor.character = Math.min(index, Math.max(0, line.length - 1));
730
+ this.desiredCol = this.cursor.character;
731
+ }
732
+ moveCursorRightForInsert() {
733
+ const lineLen = this.buffer.getLine(this.cursor.line).length;
734
+ this.cursor.character = Math.min(lineLen, this.cursor.character + 1);
735
+ this.desiredCol = this.cursor.character;
736
+ }
737
+ moveToEndOfLine() {
738
+ const lineLen = this.buffer.getLine(this.cursor.line).length;
739
+ this.cursor.character = lineLen;
740
+ this.desiredCol = Infinity;
741
+ }
742
+ deleteCharUnderCursor(count) {
743
+ const line = this.buffer.getLine(this.cursor.line);
744
+ if (line.length === 0)
745
+ return;
746
+ const deleteCount = Math.min(count, line.length - this.cursor.character);
747
+ const deletedText = line.substring(this.cursor.character, this.cursor.character + deleteCount);
748
+ this.register = {
749
+ text: deletedText,
750
+ type: 'char',
751
+ };
752
+ const before = line.substring(0, this.cursor.character);
753
+ const after = line.substring(this.cursor.character + deleteCount);
754
+ this.buffer.setLine(this.cursor.line, before + after);
755
+ // Adjust cursor if it's past the end of the line in Normal mode
756
+ const newLineLen = this.buffer.getLine(this.cursor.line).length;
757
+ this.cursor.character = Math.min(this.cursor.character, Math.max(0, newLineLen - 1));
758
+ this.desiredCol = this.cursor.character;
759
+ }
760
+ paste(before) {
761
+ if (!this.register)
762
+ return;
763
+ const { text, type } = this.register;
764
+ if (type === 'line') {
765
+ const targetLine = before ? this.cursor.line : this.cursor.line + 1;
766
+ const pasteLines = text.split('\n');
767
+ // If trailing newline, split leaves empty string at end, remove it
768
+ if (pasteLines[pasteLines.length - 1] === '') {
769
+ pasteLines.pop();
770
+ }
771
+ for (let i = 0; i < pasteLines.length; i++) {
772
+ this.buffer.insertLine(targetLine + i, pasteLines[i]);
773
+ }
774
+ this.cursor.line = targetLine;
775
+ this.cursor.character = 0;
776
+ this.desiredCol = 0;
777
+ }
778
+ else {
779
+ // Character-wise or block-wise paste (block-wise simple fallback to char paste)
780
+ const line = this.buffer.getLine(this.cursor.line);
781
+ const insertIdx = before
782
+ ? this.cursor.character
783
+ : Math.min(line.length, this.cursor.character + 1);
784
+ const endPos = this.buffer.insertText({ line: this.cursor.line, character: insertIdx }, text);
785
+ this.cursor = endPos;
786
+ // In Normal mode, leave cursor on last pasted character
787
+ this.cursor.character = Math.max(0, this.cursor.character - 1);
788
+ this.desiredCol = this.cursor.character;
789
+ }
790
+ }
791
+ // --- Undo & Redo ---
792
+ saveStateForUndo() {
793
+ this.undoManager.push(this.buffer.getLines());
794
+ }
795
+ undo() {
796
+ const prevState = this.undoManager.undo(this.buffer.getLines());
797
+ if (prevState) {
798
+ this.buffer.setLines(prevState);
799
+ // Clamp cursor
800
+ this.cursor.line = Math.min(this.cursor.line, this.buffer.getLineCount() - 1);
801
+ const lineLen = this.buffer.getLine(this.cursor.line).length;
802
+ this.cursor.character = Math.min(this.cursor.character, Math.max(0, lineLen - 1));
803
+ this.desiredCol = this.cursor.character;
804
+ }
805
+ }
806
+ redo() {
807
+ const nextState = this.undoManager.redo(this.buffer.getLines());
808
+ if (nextState) {
809
+ this.buffer.setLines(nextState);
810
+ // Clamp cursor
811
+ this.cursor.line = Math.min(this.cursor.line, this.buffer.getLineCount() - 1);
812
+ const lineLen = this.buffer.getLine(this.cursor.line).length;
813
+ this.cursor.character = Math.min(this.cursor.character, Math.max(0, lineLen - 1));
814
+ this.desiredCol = this.cursor.character;
815
+ }
816
+ }
817
+ }
818
+ //# sourceMappingURL=editor.js.map