@caupulican/pi-tui 0.81.42 → 0.84.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.
- package/dist/autocomplete.d.ts +1 -1
- package/dist/autocomplete.d.ts.map +1 -1
- package/dist/autocomplete.js +15 -23
- package/dist/autocomplete.js.map +1 -1
- package/dist/bracketed-paste.d.ts +33 -0
- package/dist/bracketed-paste.d.ts.map +1 -0
- package/dist/bracketed-paste.js +96 -0
- package/dist/bracketed-paste.js.map +1 -0
- package/dist/components/box.d.ts.map +1 -1
- package/dist/components/box.js +2 -7
- package/dist/components/box.js.map +1 -1
- package/dist/components/cancellable-loader.d.ts.map +1 -1
- package/dist/components/cancellable-loader.js +4 -3
- package/dist/components/cancellable-loader.js.map +1 -1
- package/dist/components/editor.d.ts +20 -8
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +172 -310
- package/dist/components/editor.js.map +1 -1
- package/dist/components/image.d.ts.map +1 -1
- package/dist/components/image.js +12 -35
- package/dist/components/image.js.map +1 -1
- package/dist/components/input.d.ts +8 -8
- package/dist/components/input.d.ts.map +1 -1
- package/dist/components/input.js +29 -75
- package/dist/components/input.js.map +1 -1
- package/dist/components/loader.d.ts.map +1 -1
- package/dist/components/loader.js +7 -9
- package/dist/components/loader.js.map +1 -1
- package/dist/components/markdown.d.ts +3 -11
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +48 -124
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/select-list.d.ts.map +1 -1
- package/dist/components/select-list.js +4 -9
- package/dist/components/select-list.js.map +1 -1
- package/dist/components/settings-list.d.ts.map +1 -1
- package/dist/components/settings-list.js +4 -12
- package/dist/components/settings-list.js.map +1 -1
- package/dist/components/spacer.d.ts.map +1 -1
- package/dist/components/spacer.js +0 -1
- package/dist/components/spacer.js.map +1 -1
- package/dist/components/text-layout.d.ts +49 -0
- package/dist/components/text-layout.d.ts.map +1 -0
- package/dist/components/text-layout.js +103 -0
- package/dist/components/text-layout.js.map +1 -0
- package/dist/components/text.d.ts +2 -11
- package/dist/components/text.d.ts.map +1 -1
- package/dist/components/text.js +16 -83
- package/dist/components/text.js.map +1 -1
- package/dist/components/truncated-text.d.ts.map +1 -1
- package/dist/components/truncated-text.js +0 -3
- package/dist/components/truncated-text.js.map +1 -1
- package/dist/editing-actions.d.ts +17 -0
- package/dist/editing-actions.d.ts.map +1 -0
- package/dist/editing-actions.js +34 -0
- package/dist/editing-actions.js.map +1 -0
- package/dist/editor-component.d.ts +9 -0
- package/dist/editor-component.d.ts.map +1 -1
- package/dist/editor-component.js +4 -1
- package/dist/editor-component.js.map +1 -1
- package/dist/fuzzy.d.ts.map +1 -1
- package/dist/fuzzy.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/keybindings.d.ts +2 -2
- package/dist/keybindings.d.ts.map +1 -1
- package/dist/keybindings.js +4 -6
- package/dist/keybindings.js.map +1 -1
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js.map +1 -1
- package/dist/kill-ring.d.ts.map +1 -1
- package/dist/kill-ring.js +2 -4
- package/dist/kill-ring.js.map +1 -1
- package/dist/native-loader.d.ts +3 -0
- package/dist/native-loader.d.ts.map +1 -0
- package/dist/native-loader.js +25 -0
- package/dist/native-loader.js.map +1 -0
- package/dist/native-modifiers.d.ts.map +1 -1
- package/dist/native-modifiers.js +5 -20
- package/dist/native-modifiers.js.map +1 -1
- package/dist/stdin-buffer.d.ts +8 -2
- package/dist/stdin-buffer.d.ts.map +1 -1
- package/dist/stdin-buffer.js +38 -32
- package/dist/stdin-buffer.js.map +1 -1
- package/dist/terminal-image.d.ts.map +1 -1
- package/dist/terminal-image.js.map +1 -1
- package/dist/terminal.d.ts +1 -0
- package/dist/terminal.d.ts.map +1 -1
- package/dist/terminal.js +47 -72
- package/dist/terminal.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +26 -31
- package/dist/tui.js.map +1 -1
- package/dist/undo-stack.d.ts.map +1 -1
- package/dist/undo-stack.js +2 -4
- package/dist/undo-stack.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +197 -275
- package/dist/utils.js.map +1 -1
- package/dist/word-navigation.d.ts.map +1 -1
- package/dist/word-navigation.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { BracketedPasteBuffer } from "../bracketed-paste.js";
|
|
2
|
+
import { DELETE_CHARACTER_BACKWARD, DELETE_CHARACTER_FORWARD, DELETE_TO_LINE_END, DELETE_TO_LINE_START, DELETE_WORD_BACKWARD, DELETE_WORD_FORWARD, dispatchDeletionInput, } from "../editing-actions.js";
|
|
3
|
+
import { firstNonWhitespaceCharacter } from "../editor-component.js";
|
|
1
4
|
import { getKeybindings } from "../keybindings.js";
|
|
2
5
|
import { decodePrintableKey, matchesKey } from "../keys.js";
|
|
3
6
|
import { KillRing } from "../kill-ring.js";
|
|
@@ -183,68 +186,50 @@ function createScrollBorder(direction, hiddenLineCount, width) {
|
|
|
183
186
|
return sliceByColumn(indicator, 0, indicatorWidth, true) + ellipsis;
|
|
184
187
|
}
|
|
185
188
|
export class Editor {
|
|
186
|
-
state = {
|
|
187
|
-
lines: [""],
|
|
188
|
-
cursorLine: 0,
|
|
189
|
-
cursorCol: 0,
|
|
190
|
-
};
|
|
191
|
-
/** Focusable interface - set by TUI when focus changes */
|
|
192
|
-
focused = false;
|
|
193
|
-
tui;
|
|
194
|
-
theme;
|
|
195
|
-
paddingX = 0;
|
|
196
|
-
// Store last render width for cursor navigation
|
|
197
|
-
lastWidth = 80;
|
|
198
|
-
// Vertical scrolling support
|
|
199
|
-
scrollOffset = 0;
|
|
200
|
-
// Border color (can be changed dynamically)
|
|
201
|
-
borderColor;
|
|
202
|
-
// Autocomplete support
|
|
203
|
-
autocompleteProvider;
|
|
204
|
-
autocompleteList;
|
|
205
|
-
autocompleteState = null;
|
|
206
|
-
autocompletePrefix = "";
|
|
207
|
-
autocompleteMaxVisible = 5;
|
|
208
|
-
autocompleteAbort;
|
|
209
|
-
autocompleteDebounceTimer;
|
|
210
|
-
autocompleteRequestTask = Promise.resolve();
|
|
211
|
-
autocompleteStartToken = 0;
|
|
212
|
-
autocompleteRequestId = 0;
|
|
213
|
-
// Paste tracking for large pastes
|
|
214
|
-
pastes = new Map();
|
|
215
|
-
pasteCounter = 0;
|
|
216
|
-
// Bracketed paste mode buffering
|
|
217
|
-
pasteBuffer = "";
|
|
218
|
-
isInPaste = false;
|
|
219
|
-
// Prompt history for up/down navigation
|
|
220
|
-
history = [];
|
|
221
|
-
historyIndex = -1; // -1 = not browsing, 0 = most recent, 1 = older, etc.
|
|
222
|
-
/**
|
|
223
|
-
* Consulted on Up arrow in an empty editor, before history navigation.
|
|
224
|
-
* Return true after restoring queued message(s) into the editor to consume
|
|
225
|
-
* the key; return false to fall through to history.
|
|
226
|
-
*/
|
|
227
|
-
onRecallQueued;
|
|
228
|
-
// Kill ring for Emacs-style kill/yank operations
|
|
229
|
-
killRing = new KillRing();
|
|
230
|
-
lastAction = null;
|
|
231
|
-
// Character jump mode
|
|
232
|
-
jumpMode = null;
|
|
233
|
-
// Preferred visual column for vertical cursor movement (sticky column)
|
|
234
|
-
preferredVisualCol = null;
|
|
235
|
-
// When the cursor is snapped to the start of an atomic segment, e.g. a
|
|
236
|
-
// paste marker, cursorCol no longer reflects where the cursor would have
|
|
237
|
-
// landed. This field stores the pre-snap cursorCol so that the next
|
|
238
|
-
// vertical move can resolve it to a visual column on whatever VL it belongs
|
|
239
|
-
// to.
|
|
240
|
-
snappedFromCursorCol = null;
|
|
241
|
-
// Undo support
|
|
242
|
-
undoStack = new UndoStack();
|
|
243
|
-
privateContent;
|
|
244
|
-
onSubmit;
|
|
245
|
-
onChange;
|
|
246
|
-
disableSubmit = false;
|
|
247
189
|
constructor(tui, theme, options = {}) {
|
|
190
|
+
this.state = {
|
|
191
|
+
lines: [""],
|
|
192
|
+
cursorLine: 0,
|
|
193
|
+
cursorCol: 0,
|
|
194
|
+
};
|
|
195
|
+
/** Focusable interface - set by TUI when focus changes */
|
|
196
|
+
this.focused = false;
|
|
197
|
+
this.paddingX = 0;
|
|
198
|
+
// Store last render width for cursor navigation
|
|
199
|
+
this.lastWidth = 80;
|
|
200
|
+
// Vertical scrolling support
|
|
201
|
+
this.scrollOffset = 0;
|
|
202
|
+
this.autocompleteState = null;
|
|
203
|
+
this.autocompletePrefix = "";
|
|
204
|
+
this.autocompleteMaxVisible = 5;
|
|
205
|
+
this.autocompleteRequestTask = Promise.resolve();
|
|
206
|
+
this.autocompleteStartToken = 0;
|
|
207
|
+
this.autocompleteRequestId = 0;
|
|
208
|
+
// Paste tracking for large pastes
|
|
209
|
+
this.pastes = new Map();
|
|
210
|
+
this.pasteCounter = 0;
|
|
211
|
+
// Bracketed paste mode buffering
|
|
212
|
+
this.bracketedPaste = new BracketedPasteBuffer();
|
|
213
|
+
// Prompt history for up/down navigation
|
|
214
|
+
this.history = [];
|
|
215
|
+
this.historyIndex = -1; // -1 = not browsing, 0 = most recent, 1 = older, etc.
|
|
216
|
+
// Kill ring for Emacs-style kill/yank operations
|
|
217
|
+
this.killRing = new KillRing();
|
|
218
|
+
this.lastAction = null;
|
|
219
|
+
// Character jump mode
|
|
220
|
+
this.jumpMode = null;
|
|
221
|
+
// Preferred visual column for vertical cursor movement (sticky column)
|
|
222
|
+
this.preferredVisualCol = null;
|
|
223
|
+
// When the cursor is snapped to the start of an atomic segment, e.g. a
|
|
224
|
+
// paste marker, cursorCol no longer reflects where the cursor would have
|
|
225
|
+
// landed. This field stores the pre-snap cursorCol so that the next
|
|
226
|
+
// vertical move can resolve it to a visual column on whatever VL it belongs
|
|
227
|
+
// to.
|
|
228
|
+
this.snappedFromCursorCol = null;
|
|
229
|
+
// Undo support
|
|
230
|
+
this.undoStack = new UndoStack();
|
|
231
|
+
this.supportsChangeSummary = true;
|
|
232
|
+
this.disableSubmit = false;
|
|
248
233
|
this.tui = tui;
|
|
249
234
|
this.theme = theme;
|
|
250
235
|
this.borderColor = theme.borderColor;
|
|
@@ -254,6 +239,19 @@ export class Editor {
|
|
|
254
239
|
const maxVisible = options.autocompleteMaxVisible ?? 5;
|
|
255
240
|
this.autocompleteMaxVisible = Number.isFinite(maxVisible) ? Math.max(3, Math.min(20, Math.floor(maxVisible))) : 5;
|
|
256
241
|
}
|
|
242
|
+
notifyChange() {
|
|
243
|
+
if (this.onChangeSummary) {
|
|
244
|
+
let firstNonWhitespace;
|
|
245
|
+
for (const line of this.state.lines) {
|
|
246
|
+
firstNonWhitespace = firstNonWhitespaceCharacter(line);
|
|
247
|
+
if (firstNonWhitespace !== undefined)
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
this.onChangeSummary({ firstNonWhitespace });
|
|
251
|
+
}
|
|
252
|
+
if (this.onChange)
|
|
253
|
+
this.onChange(this.getText());
|
|
254
|
+
}
|
|
257
255
|
/** Set of currently valid paste IDs, for marker-aware segmentation. */
|
|
258
256
|
validPasteIds() {
|
|
259
257
|
return new Set(this.pastes.keys());
|
|
@@ -360,9 +358,7 @@ export class Editor {
|
|
|
360
358
|
this.setCursorCol(this.state.lines[this.state.cursorLine]?.length || 0);
|
|
361
359
|
// Reset scroll - render() will adjust to show cursor
|
|
362
360
|
this.scrollOffset = 0;
|
|
363
|
-
|
|
364
|
-
this.onChange(this.getText());
|
|
365
|
-
}
|
|
361
|
+
this.notifyChange();
|
|
366
362
|
}
|
|
367
363
|
invalidate() {
|
|
368
364
|
// No cached state to invalidate currently
|
|
@@ -487,30 +483,19 @@ export class Editor {
|
|
|
487
483
|
// Control character - cancel and fall through to normal handling
|
|
488
484
|
this.jumpMode = null;
|
|
489
485
|
}
|
|
490
|
-
|
|
491
|
-
if (
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
if (endIndex !== -1) {
|
|
500
|
-
const pasteContent = this.pasteBuffer.substring(0, endIndex);
|
|
501
|
-
if (pasteContent.length > 0) {
|
|
502
|
-
this.handlePaste(pasteContent);
|
|
503
|
-
}
|
|
504
|
-
this.isInPaste = false;
|
|
505
|
-
const remaining = this.pasteBuffer.substring(endIndex + 6);
|
|
506
|
-
this.pasteBuffer = "";
|
|
507
|
-
if (remaining.length > 0) {
|
|
508
|
-
this.handleInput(remaining);
|
|
509
|
-
}
|
|
510
|
-
return;
|
|
486
|
+
const pasteResult = this.bracketedPaste.consume(data);
|
|
487
|
+
if (pasteResult.kind === "pending")
|
|
488
|
+
return;
|
|
489
|
+
if (pasteResult.kind === "complete") {
|
|
490
|
+
if (pasteResult.content.length > 0) {
|
|
491
|
+
this.handlePaste(pasteResult.content);
|
|
492
|
+
}
|
|
493
|
+
if (pasteResult.remainder.length > 0) {
|
|
494
|
+
this.handleInput(pasteResult.remainder);
|
|
511
495
|
}
|
|
512
496
|
return;
|
|
513
497
|
}
|
|
498
|
+
data = pasteResult.data;
|
|
514
499
|
// Ctrl+C - let parent handle (exit/clear)
|
|
515
500
|
if (kb.matches(data, "tui.input.copy")) {
|
|
516
501
|
return;
|
|
@@ -532,37 +517,21 @@ export class Editor {
|
|
|
532
517
|
return;
|
|
533
518
|
}
|
|
534
519
|
if (kb.matches(data, "tui.input.tab")) {
|
|
535
|
-
|
|
536
|
-
if (selected && this.autocompleteProvider) {
|
|
537
|
-
this.pushUndoSnapshot();
|
|
538
|
-
this.lastAction = null;
|
|
539
|
-
const result = this.autocompleteProvider.applyCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol, selected, this.autocompletePrefix);
|
|
540
|
-
this.state.lines = result.lines;
|
|
541
|
-
this.state.cursorLine = result.cursorLine;
|
|
542
|
-
this.setCursorCol(result.cursorCol);
|
|
520
|
+
if (this.applySelectedAutocomplete()) {
|
|
543
521
|
this.cancelAutocomplete();
|
|
544
|
-
|
|
545
|
-
this.onChange(this.getText());
|
|
522
|
+
this.notifyChange();
|
|
546
523
|
}
|
|
547
524
|
return;
|
|
548
525
|
}
|
|
549
526
|
if (kb.matches(data, "tui.select.confirm")) {
|
|
550
|
-
|
|
551
|
-
if (selected && this.autocompleteProvider) {
|
|
552
|
-
this.pushUndoSnapshot();
|
|
553
|
-
this.lastAction = null;
|
|
554
|
-
const result = this.autocompleteProvider.applyCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol, selected, this.autocompletePrefix);
|
|
555
|
-
this.state.lines = result.lines;
|
|
556
|
-
this.state.cursorLine = result.cursorLine;
|
|
557
|
-
this.setCursorCol(result.cursorCol);
|
|
527
|
+
if (this.applySelectedAutocomplete()) {
|
|
558
528
|
if (this.autocompletePrefix.startsWith("/")) {
|
|
559
529
|
this.cancelAutocomplete();
|
|
560
530
|
// Fall through to submit
|
|
561
531
|
}
|
|
562
532
|
else {
|
|
563
533
|
this.cancelAutocomplete();
|
|
564
|
-
|
|
565
|
-
this.onChange(this.getText());
|
|
534
|
+
this.notifyChange();
|
|
566
535
|
return;
|
|
567
536
|
}
|
|
568
537
|
}
|
|
@@ -573,31 +542,8 @@ export class Editor {
|
|
|
573
542
|
this.handleTabCompletion();
|
|
574
543
|
return;
|
|
575
544
|
}
|
|
576
|
-
|
|
577
|
-
if (kb.matches(data, "tui.editor.deleteToLineEnd")) {
|
|
578
|
-
this.deleteToEndOfLine();
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
if (kb.matches(data, "tui.editor.deleteToLineStart")) {
|
|
582
|
-
this.deleteToStartOfLine();
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
if (kb.matches(data, "tui.editor.deleteWordBackward")) {
|
|
586
|
-
this.deleteWordBackwards();
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
if (kb.matches(data, "tui.editor.deleteWordForward")) {
|
|
590
|
-
this.deleteWordForward();
|
|
591
|
-
return;
|
|
592
|
-
}
|
|
593
|
-
if (kb.matches(data, "tui.editor.deleteCharBackward") || matchesKey(data, "shift+backspace")) {
|
|
594
|
-
this.handleBackspace();
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
if (kb.matches(data, "tui.editor.deleteCharForward") || matchesKey(data, "shift+delete")) {
|
|
598
|
-
this.handleForwardDelete();
|
|
545
|
+
if (dispatchDeletionInput(data, this))
|
|
599
546
|
return;
|
|
600
|
-
}
|
|
601
547
|
// Kill ring actions
|
|
602
548
|
if (kb.matches(data, "tui.editor.yank")) {
|
|
603
549
|
if (!this.privateContent)
|
|
@@ -634,7 +580,7 @@ export class Editor {
|
|
|
634
580
|
(data.length > 1 && data.includes("\x1b") && data.includes("\r")) ||
|
|
635
581
|
(data === "\n" && data.length === 1)) {
|
|
636
582
|
if (this.shouldSubmitOnBackslashEnter(data, kb)) {
|
|
637
|
-
this
|
|
583
|
+
this[DELETE_CHARACTER_BACKWARD]();
|
|
638
584
|
this.submitValue();
|
|
639
585
|
return;
|
|
640
586
|
}
|
|
@@ -649,7 +595,7 @@ export class Editor {
|
|
|
649
595
|
// If char before cursor is \, delete it and insert newline instead of submitting.
|
|
650
596
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
651
597
|
if (this.state.cursorCol > 0 && currentLine[this.state.cursorCol - 1] === "\\") {
|
|
652
|
-
this
|
|
598
|
+
this[DELETE_CHARACTER_BACKWARD]();
|
|
653
599
|
this.addNewLine();
|
|
654
600
|
return;
|
|
655
601
|
}
|
|
@@ -730,6 +676,18 @@ export class Editor {
|
|
|
730
676
|
this.insertCharacter(data);
|
|
731
677
|
}
|
|
732
678
|
}
|
|
679
|
+
applySelectedAutocomplete() {
|
|
680
|
+
const selected = this.autocompleteList?.getSelectedItem();
|
|
681
|
+
if (!selected || !this.autocompleteProvider)
|
|
682
|
+
return false;
|
|
683
|
+
this.pushUndoSnapshot();
|
|
684
|
+
this.lastAction = null;
|
|
685
|
+
const result = this.autocompleteProvider.applyCompletion(this.state.lines, this.state.cursorLine, this.state.cursorCol, selected, this.autocompletePrefix);
|
|
686
|
+
this.state.lines = result.lines;
|
|
687
|
+
this.state.cursorLine = result.cursorLine;
|
|
688
|
+
this.setCursorCol(result.cursorCol);
|
|
689
|
+
return true;
|
|
690
|
+
}
|
|
733
691
|
layoutText(contentWidth) {
|
|
734
692
|
const layoutLines = [];
|
|
735
693
|
if (this.state.lines.length === 0 || (this.state.lines.length === 1 && this.state.lines[0] === "")) {
|
|
@@ -817,12 +775,7 @@ export class Editor {
|
|
|
817
775
|
return this.state.lines.join("\n");
|
|
818
776
|
}
|
|
819
777
|
expandPasteMarkers(text) {
|
|
820
|
-
|
|
821
|
-
for (const [pasteId, pasteContent] of this.pastes) {
|
|
822
|
-
const markerRegex = new RegExp(`\\[paste #${pasteId}( (\\+\\d+ lines|\\d+ chars))?\\]`, "g");
|
|
823
|
-
result = result.replace(markerRegex, () => pasteContent);
|
|
824
|
-
}
|
|
825
|
-
return result;
|
|
778
|
+
return text.replace(PASTE_MARKER_REGEX, (marker, idGroup) => this.pastes.get(Number(idGroup)) ?? marker);
|
|
826
779
|
}
|
|
827
780
|
/**
|
|
828
781
|
* Get text with paste markers expanded to their actual content.
|
|
@@ -856,8 +809,7 @@ export class Editor {
|
|
|
856
809
|
this.state = { lines: [""], cursorLine: 0, cursorCol: 0 };
|
|
857
810
|
this.pastes.clear();
|
|
858
811
|
this.pasteCounter = 0;
|
|
859
|
-
this.
|
|
860
|
-
this.isInPaste = false;
|
|
812
|
+
this.bracketedPaste.clear();
|
|
861
813
|
this.history = [];
|
|
862
814
|
this.historyIndex = -1;
|
|
863
815
|
this.killRing = new KillRing();
|
|
@@ -867,8 +819,7 @@ export class Editor {
|
|
|
867
819
|
this.preferredVisualCol = null;
|
|
868
820
|
this.snappedFromCursorCol = null;
|
|
869
821
|
this.scrollOffset = 0;
|
|
870
|
-
|
|
871
|
-
this.onChange("");
|
|
822
|
+
this.notifyChange();
|
|
872
823
|
}
|
|
873
824
|
/**
|
|
874
825
|
* Insert text at the current cursor position.
|
|
@@ -929,9 +880,7 @@ export class Editor {
|
|
|
929
880
|
this.state.cursorLine += insertedLines.length - 1;
|
|
930
881
|
this.setCursorCol((insertedLines[insertedLines.length - 1] || "").length);
|
|
931
882
|
}
|
|
932
|
-
|
|
933
|
-
this.onChange(this.getText());
|
|
934
|
-
}
|
|
883
|
+
this.notifyChange();
|
|
935
884
|
}
|
|
936
885
|
// All the editor methods from before...
|
|
937
886
|
insertCharacter(char, skipUndoCoalescing) {
|
|
@@ -952,9 +901,7 @@ export class Editor {
|
|
|
952
901
|
const after = line.slice(this.state.cursorCol);
|
|
953
902
|
this.state.lines[this.state.cursorLine] = before + char + after;
|
|
954
903
|
this.setCursorCol(this.state.cursorCol + char.length);
|
|
955
|
-
|
|
956
|
-
this.onChange(this.getText());
|
|
957
|
-
}
|
|
904
|
+
this.notifyChange();
|
|
958
905
|
// Check if we should trigger or update autocomplete
|
|
959
906
|
if (!this.autocompleteState) {
|
|
960
907
|
// Auto-trigger for "/" at the start of a line (slash commands)
|
|
@@ -972,22 +919,28 @@ export class Editor {
|
|
|
972
919
|
}
|
|
973
920
|
// Also auto-trigger when typing letters in a slash command or symbol completion context
|
|
974
921
|
else if (/[a-zA-Z0-9.\-_]/.test(char)) {
|
|
975
|
-
|
|
976
|
-
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
977
|
-
// Check if we're in a slash command (with or without space for arguments)
|
|
978
|
-
if (this.isInSlashCommandContext(textBeforeCursor)) {
|
|
979
|
-
this.tryTriggerAutocomplete();
|
|
980
|
-
}
|
|
981
|
-
// Check if we're in a symbol-based completion context like @ or #
|
|
982
|
-
else if (textBeforeCursor.match(/(?:^|[\s])[@#][^\s]*$/)) {
|
|
983
|
-
this.tryTriggerAutocomplete();
|
|
984
|
-
}
|
|
922
|
+
this.triggerAutocompleteForCurrentContext();
|
|
985
923
|
}
|
|
986
924
|
}
|
|
987
925
|
else {
|
|
988
926
|
this.updateAutocomplete();
|
|
989
927
|
}
|
|
990
928
|
}
|
|
929
|
+
triggerAutocompleteForCurrentContext() {
|
|
930
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
931
|
+
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
932
|
+
if (this.isInSlashCommandContext(textBeforeCursor) || /(?:^|[\s])[@#][^\s]*$/.test(textBeforeCursor)) {
|
|
933
|
+
this.tryTriggerAutocomplete();
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
refreshAutocompleteAfterDeletion() {
|
|
937
|
+
if (this.autocompleteState) {
|
|
938
|
+
this.updateAutocomplete();
|
|
939
|
+
}
|
|
940
|
+
else {
|
|
941
|
+
this.triggerAutocompleteForCurrentContext();
|
|
942
|
+
}
|
|
943
|
+
}
|
|
991
944
|
handlePaste(pastedText) {
|
|
992
945
|
this.cancelAutocomplete();
|
|
993
946
|
this.historyIndex = -1; // Exit history browsing mode
|
|
@@ -1009,10 +962,9 @@ export class Editor {
|
|
|
1009
962
|
// Clean the pasted text: normalize line endings, expand tabs
|
|
1010
963
|
const cleanText = this.normalizeText(decodedText);
|
|
1011
964
|
// Filter out non-printable characters except newlines
|
|
1012
|
-
let filteredText =
|
|
1013
|
-
.
|
|
1014
|
-
.
|
|
1015
|
-
.join("");
|
|
965
|
+
let filteredText = this.privateContent
|
|
966
|
+
? cleanText.replace(/[\x00-\x08\x0b-\x1f]/g, "")
|
|
967
|
+
: cleanText.replace(/[\x00-\x09\x0b-\x1f]/g, "");
|
|
1016
968
|
// If pasting a file path (starts with /, ~, or .) and the character before
|
|
1017
969
|
// the cursor is a word character, prepend a space for better readability
|
|
1018
970
|
if (!this.privateContent && /^[/~.]/.test(filteredText)) {
|
|
@@ -1060,9 +1012,7 @@ export class Editor {
|
|
|
1060
1012
|
// Move cursor to start of new line
|
|
1061
1013
|
this.state.cursorLine++;
|
|
1062
1014
|
this.setCursorCol(0);
|
|
1063
|
-
|
|
1064
|
-
this.onChange(this.getText());
|
|
1065
|
-
}
|
|
1015
|
+
this.notifyChange();
|
|
1066
1016
|
}
|
|
1067
1017
|
shouldSubmitOnBackslashEnter(data, kb) {
|
|
1068
1018
|
if (this.disableSubmit)
|
|
@@ -1084,7 +1034,7 @@ export class Editor {
|
|
|
1084
1034
|
if (this.onSubmit)
|
|
1085
1035
|
this.onSubmit(result);
|
|
1086
1036
|
}
|
|
1087
|
-
|
|
1037
|
+
[DELETE_CHARACTER_BACKWARD]() {
|
|
1088
1038
|
this.historyIndex = -1; // Exit history browsing mode
|
|
1089
1039
|
this.lastAction = null;
|
|
1090
1040
|
if (this.state.cursorCol > 0) {
|
|
@@ -1119,34 +1069,10 @@ export class Editor {
|
|
|
1119
1069
|
}
|
|
1120
1070
|
else if (this.state.cursorLine > 0) {
|
|
1121
1071
|
this.pushUndoSnapshot();
|
|
1122
|
-
|
|
1123
|
-
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1124
|
-
const previousLine = this.state.lines[this.state.cursorLine - 1] || "";
|
|
1125
|
-
this.state.lines[this.state.cursorLine - 1] = previousLine + currentLine;
|
|
1126
|
-
this.state.lines.splice(this.state.cursorLine, 1);
|
|
1127
|
-
this.state.cursorLine--;
|
|
1128
|
-
this.setCursorCol(previousLine.length);
|
|
1129
|
-
}
|
|
1130
|
-
if (this.onChange) {
|
|
1131
|
-
this.onChange(this.getText());
|
|
1132
|
-
}
|
|
1133
|
-
// Update or re-trigger autocomplete after backspace
|
|
1134
|
-
if (this.autocompleteState) {
|
|
1135
|
-
this.updateAutocomplete();
|
|
1136
|
-
}
|
|
1137
|
-
else {
|
|
1138
|
-
// If autocomplete was cancelled (no matches), re-trigger if we're in a completable context
|
|
1139
|
-
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1140
|
-
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
1141
|
-
// Slash command context
|
|
1142
|
-
if (this.isInSlashCommandContext(textBeforeCursor)) {
|
|
1143
|
-
this.tryTriggerAutocomplete();
|
|
1144
|
-
}
|
|
1145
|
-
// Symbol-based completion context like @ or #
|
|
1146
|
-
else if (textBeforeCursor.match(/(?:^|[\s])[@#][^\s]*$/)) {
|
|
1147
|
-
this.tryTriggerAutocomplete();
|
|
1148
|
-
}
|
|
1072
|
+
this.mergeCurrentLineIntoPrevious();
|
|
1149
1073
|
}
|
|
1074
|
+
this.notifyChange();
|
|
1075
|
+
this.refreshAutocompleteAfterDeletion();
|
|
1150
1076
|
}
|
|
1151
1077
|
/**
|
|
1152
1078
|
* Set cursor column and clear preferredVisualCol.
|
|
@@ -1282,7 +1208,33 @@ export class Editor {
|
|
|
1282
1208
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1283
1209
|
this.setCursorCol(currentLine.length);
|
|
1284
1210
|
}
|
|
1285
|
-
|
|
1211
|
+
mergeCurrentLineIntoPrevious() {
|
|
1212
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1213
|
+
const previousLine = this.state.lines[this.state.cursorLine - 1] || "";
|
|
1214
|
+
this.state.lines[this.state.cursorLine - 1] = previousLine + currentLine;
|
|
1215
|
+
this.state.lines.splice(this.state.cursorLine, 1);
|
|
1216
|
+
this.state.cursorLine--;
|
|
1217
|
+
this.setCursorCol(previousLine.length);
|
|
1218
|
+
}
|
|
1219
|
+
mergeNextLineIntoCurrent() {
|
|
1220
|
+
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1221
|
+
const nextLine = this.state.lines[this.state.cursorLine + 1] || "";
|
|
1222
|
+
this.state.lines[this.state.cursorLine] = currentLine + nextLine;
|
|
1223
|
+
this.state.lines.splice(this.state.cursorLine + 1, 1);
|
|
1224
|
+
}
|
|
1225
|
+
isAtCurrentLineEnd(currentLine) {
|
|
1226
|
+
return this.state.cursorCol >= currentLine.length;
|
|
1227
|
+
}
|
|
1228
|
+
hasNextLine() {
|
|
1229
|
+
return this.state.cursorLine < this.state.lines.length - 1;
|
|
1230
|
+
}
|
|
1231
|
+
recordKilledNewline(prepend) {
|
|
1232
|
+
if (this.privateContent)
|
|
1233
|
+
return;
|
|
1234
|
+
this.killRing.push("\n", { prepend, accumulate: this.lastAction === "kill" });
|
|
1235
|
+
this.lastAction = "kill";
|
|
1236
|
+
}
|
|
1237
|
+
[DELETE_TO_LINE_START]() {
|
|
1286
1238
|
this.historyIndex = -1; // Exit history browsing mode
|
|
1287
1239
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1288
1240
|
if (this.state.cursorCol > 0) {
|
|
@@ -1299,22 +1251,12 @@ export class Editor {
|
|
|
1299
1251
|
}
|
|
1300
1252
|
else if (this.state.cursorLine > 0) {
|
|
1301
1253
|
this.pushUndoSnapshot();
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
this.killRing.push("\n", { prepend: true, accumulate: this.lastAction === "kill" });
|
|
1305
|
-
this.lastAction = "kill";
|
|
1306
|
-
}
|
|
1307
|
-
const previousLine = this.state.lines[this.state.cursorLine - 1] || "";
|
|
1308
|
-
this.state.lines[this.state.cursorLine - 1] = previousLine + currentLine;
|
|
1309
|
-
this.state.lines.splice(this.state.cursorLine, 1);
|
|
1310
|
-
this.state.cursorLine--;
|
|
1311
|
-
this.setCursorCol(previousLine.length);
|
|
1312
|
-
}
|
|
1313
|
-
if (this.onChange) {
|
|
1314
|
-
this.onChange(this.getText());
|
|
1254
|
+
this.recordKilledNewline(true);
|
|
1255
|
+
this.mergeCurrentLineIntoPrevious();
|
|
1315
1256
|
}
|
|
1257
|
+
this.notifyChange();
|
|
1316
1258
|
}
|
|
1317
|
-
|
|
1259
|
+
[DELETE_TO_LINE_END]() {
|
|
1318
1260
|
this.historyIndex = -1; // Exit history browsing mode
|
|
1319
1261
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1320
1262
|
if (this.state.cursorCol < currentLine.length) {
|
|
@@ -1330,36 +1272,20 @@ export class Editor {
|
|
|
1330
1272
|
}
|
|
1331
1273
|
else if (this.state.cursorLine < this.state.lines.length - 1) {
|
|
1332
1274
|
this.pushUndoSnapshot();
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
this.killRing.push("\n", { prepend: false, accumulate: this.lastAction === "kill" });
|
|
1336
|
-
this.lastAction = "kill";
|
|
1337
|
-
}
|
|
1338
|
-
const nextLine = this.state.lines[this.state.cursorLine + 1] || "";
|
|
1339
|
-
this.state.lines[this.state.cursorLine] = currentLine + nextLine;
|
|
1340
|
-
this.state.lines.splice(this.state.cursorLine + 1, 1);
|
|
1341
|
-
}
|
|
1342
|
-
if (this.onChange) {
|
|
1343
|
-
this.onChange(this.getText());
|
|
1275
|
+
this.recordKilledNewline(false);
|
|
1276
|
+
this.mergeNextLineIntoCurrent();
|
|
1344
1277
|
}
|
|
1278
|
+
this.notifyChange();
|
|
1345
1279
|
}
|
|
1346
|
-
|
|
1280
|
+
[DELETE_WORD_BACKWARD]() {
|
|
1347
1281
|
this.historyIndex = -1; // Exit history browsing mode
|
|
1348
1282
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1349
1283
|
// If at start of line, behave like backspace at column 0 (merge with previous line)
|
|
1350
1284
|
if (this.state.cursorCol === 0) {
|
|
1351
1285
|
if (this.state.cursorLine > 0) {
|
|
1352
1286
|
this.pushUndoSnapshot();
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
this.killRing.push("\n", { prepend: true, accumulate: this.lastAction === "kill" });
|
|
1356
|
-
this.lastAction = "kill";
|
|
1357
|
-
}
|
|
1358
|
-
const previousLine = this.state.lines[this.state.cursorLine - 1] || "";
|
|
1359
|
-
this.state.lines[this.state.cursorLine - 1] = previousLine + currentLine;
|
|
1360
|
-
this.state.lines.splice(this.state.cursorLine, 1);
|
|
1361
|
-
this.state.cursorLine--;
|
|
1362
|
-
this.setCursorCol(previousLine.length);
|
|
1287
|
+
this.recordKilledNewline(true);
|
|
1288
|
+
this.mergeCurrentLineIntoPrevious();
|
|
1363
1289
|
}
|
|
1364
1290
|
}
|
|
1365
1291
|
else {
|
|
@@ -1379,25 +1305,17 @@ export class Editor {
|
|
|
1379
1305
|
currentLine.slice(0, deleteFrom) + currentLine.slice(this.state.cursorCol);
|
|
1380
1306
|
this.setCursorCol(deleteFrom);
|
|
1381
1307
|
}
|
|
1382
|
-
|
|
1383
|
-
this.onChange(this.getText());
|
|
1384
|
-
}
|
|
1308
|
+
this.notifyChange();
|
|
1385
1309
|
}
|
|
1386
|
-
|
|
1310
|
+
[DELETE_WORD_FORWARD]() {
|
|
1387
1311
|
this.historyIndex = -1; // Exit history browsing mode
|
|
1388
1312
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1389
1313
|
// If at end of line, merge with next line (delete the newline)
|
|
1390
|
-
if (this.
|
|
1391
|
-
if (this.
|
|
1314
|
+
if (this.isAtCurrentLineEnd(currentLine)) {
|
|
1315
|
+
if (this.hasNextLine()) {
|
|
1392
1316
|
this.pushUndoSnapshot();
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
this.killRing.push("\n", { prepend: false, accumulate: this.lastAction === "kill" });
|
|
1396
|
-
this.lastAction = "kill";
|
|
1397
|
-
}
|
|
1398
|
-
const nextLine = this.state.lines[this.state.cursorLine + 1] || "";
|
|
1399
|
-
this.state.lines[this.state.cursorLine] = currentLine + nextLine;
|
|
1400
|
-
this.state.lines.splice(this.state.cursorLine + 1, 1);
|
|
1317
|
+
this.recordKilledNewline(false);
|
|
1318
|
+
this.mergeNextLineIntoCurrent();
|
|
1401
1319
|
}
|
|
1402
1320
|
}
|
|
1403
1321
|
else {
|
|
@@ -1416,11 +1334,9 @@ export class Editor {
|
|
|
1416
1334
|
this.state.lines[this.state.cursorLine] =
|
|
1417
1335
|
currentLine.slice(0, this.state.cursorCol) + currentLine.slice(deleteTo);
|
|
1418
1336
|
}
|
|
1419
|
-
|
|
1420
|
-
this.onChange(this.getText());
|
|
1421
|
-
}
|
|
1337
|
+
this.notifyChange();
|
|
1422
1338
|
}
|
|
1423
|
-
|
|
1339
|
+
[DELETE_CHARACTER_FORWARD]() {
|
|
1424
1340
|
this.historyIndex = -1; // Exit history browsing mode
|
|
1425
1341
|
this.lastAction = null;
|
|
1426
1342
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
@@ -1438,30 +1354,10 @@ export class Editor {
|
|
|
1438
1354
|
}
|
|
1439
1355
|
else if (this.state.cursorLine < this.state.lines.length - 1) {
|
|
1440
1356
|
this.pushUndoSnapshot();
|
|
1441
|
-
|
|
1442
|
-
const nextLine = this.state.lines[this.state.cursorLine + 1] || "";
|
|
1443
|
-
this.state.lines[this.state.cursorLine] = currentLine + nextLine;
|
|
1444
|
-
this.state.lines.splice(this.state.cursorLine + 1, 1);
|
|
1445
|
-
}
|
|
1446
|
-
if (this.onChange) {
|
|
1447
|
-
this.onChange(this.getText());
|
|
1448
|
-
}
|
|
1449
|
-
// Update or re-trigger autocomplete after forward delete
|
|
1450
|
-
if (this.autocompleteState) {
|
|
1451
|
-
this.updateAutocomplete();
|
|
1452
|
-
}
|
|
1453
|
-
else {
|
|
1454
|
-
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1455
|
-
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
1456
|
-
// Slash command context
|
|
1457
|
-
if (this.isInSlashCommandContext(textBeforeCursor)) {
|
|
1458
|
-
this.tryTriggerAutocomplete();
|
|
1459
|
-
}
|
|
1460
|
-
// Symbol-based completion context like @ or #
|
|
1461
|
-
else if (textBeforeCursor.match(/(?:^|[\s])[@#][^\s]*$/)) {
|
|
1462
|
-
this.tryTriggerAutocomplete();
|
|
1463
|
-
}
|
|
1357
|
+
this.mergeNextLineIntoCurrent();
|
|
1464
1358
|
}
|
|
1359
|
+
this.notifyChange();
|
|
1360
|
+
this.refreshAutocompleteAfterDeletion();
|
|
1465
1361
|
}
|
|
1466
1362
|
/**
|
|
1467
1363
|
* Build a mapping from visual lines to logical positions.
|
|
@@ -1634,36 +1530,7 @@ export class Editor {
|
|
|
1634
1530
|
*/
|
|
1635
1531
|
insertYankedText(text) {
|
|
1636
1532
|
this.historyIndex = -1; // Exit history browsing mode
|
|
1637
|
-
|
|
1638
|
-
if (lines.length === 1) {
|
|
1639
|
-
// Single line - insert at cursor
|
|
1640
|
-
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1641
|
-
const before = currentLine.slice(0, this.state.cursorCol);
|
|
1642
|
-
const after = currentLine.slice(this.state.cursorCol);
|
|
1643
|
-
this.state.lines[this.state.cursorLine] = before + text + after;
|
|
1644
|
-
this.setCursorCol(this.state.cursorCol + text.length);
|
|
1645
|
-
}
|
|
1646
|
-
else {
|
|
1647
|
-
// Multi-line insert
|
|
1648
|
-
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1649
|
-
const before = currentLine.slice(0, this.state.cursorCol);
|
|
1650
|
-
const after = currentLine.slice(this.state.cursorCol);
|
|
1651
|
-
// First line merges with text before cursor
|
|
1652
|
-
this.state.lines[this.state.cursorLine] = before + (lines[0] || "");
|
|
1653
|
-
// Insert middle lines
|
|
1654
|
-
for (let i = 1; i < lines.length - 1; i++) {
|
|
1655
|
-
this.state.lines.splice(this.state.cursorLine + i, 0, lines[i] || "");
|
|
1656
|
-
}
|
|
1657
|
-
// Last line merges with text after cursor
|
|
1658
|
-
const lastLineIndex = this.state.cursorLine + lines.length - 1;
|
|
1659
|
-
this.state.lines.splice(lastLineIndex, 0, (lines[lines.length - 1] || "") + after);
|
|
1660
|
-
// Update cursor position
|
|
1661
|
-
this.state.cursorLine = lastLineIndex;
|
|
1662
|
-
this.setCursorCol((lines[lines.length - 1] || "").length);
|
|
1663
|
-
}
|
|
1664
|
-
if (this.onChange) {
|
|
1665
|
-
this.onChange(this.getText());
|
|
1666
|
-
}
|
|
1533
|
+
this.insertTextAtCursorInternal(text);
|
|
1667
1534
|
}
|
|
1668
1535
|
/**
|
|
1669
1536
|
* Delete the previously yanked text (used by yank-pop).
|
|
@@ -1697,9 +1564,7 @@ export class Editor {
|
|
|
1697
1564
|
this.state.cursorLine = startLine;
|
|
1698
1565
|
this.setCursorCol(startCol);
|
|
1699
1566
|
}
|
|
1700
|
-
|
|
1701
|
-
this.onChange(this.getText());
|
|
1702
|
-
}
|
|
1567
|
+
this.notifyChange();
|
|
1703
1568
|
}
|
|
1704
1569
|
pushUndoSnapshot() {
|
|
1705
1570
|
if (this.privateContent)
|
|
@@ -1716,9 +1581,7 @@ export class Editor {
|
|
|
1716
1581
|
this.pasteCounter = snapshot.pasteCounter;
|
|
1717
1582
|
this.lastAction = null;
|
|
1718
1583
|
this.preferredVisualCol = null;
|
|
1719
|
-
|
|
1720
|
-
this.onChange(this.getText());
|
|
1721
|
-
}
|
|
1584
|
+
this.notifyChange();
|
|
1722
1585
|
}
|
|
1723
1586
|
/**
|
|
1724
1587
|
* Jump to the first occurrence of a character in the specified direction.
|
|
@@ -1752,8 +1615,8 @@ export class Editor {
|
|
|
1752
1615
|
this.lastAction = null;
|
|
1753
1616
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1754
1617
|
// If at end of line, move to start of next line
|
|
1755
|
-
if (this.
|
|
1756
|
-
if (this.
|
|
1618
|
+
if (this.isAtCurrentLineEnd(currentLine)) {
|
|
1619
|
+
if (this.hasNextLine()) {
|
|
1757
1620
|
this.state.cursorLine++;
|
|
1758
1621
|
this.setCursorCol(0);
|
|
1759
1622
|
}
|
|
@@ -1900,8 +1763,7 @@ export class Editor {
|
|
|
1900
1763
|
this.state.lines = result.lines;
|
|
1901
1764
|
this.state.cursorLine = result.cursorLine;
|
|
1902
1765
|
this.setCursorCol(result.cursorCol);
|
|
1903
|
-
|
|
1904
|
-
this.onChange(this.getText());
|
|
1766
|
+
this.notifyChange();
|
|
1905
1767
|
this.tui.requestRender();
|
|
1906
1768
|
return;
|
|
1907
1769
|
}
|