Editor-Komponente: Eingabe, IME, kill-ring, undo
Editor ist die schwerste Komponente in @mariozechner/pi-tui — eine einzige Datei mit 2292 Zeilen, die die ganze Drecksarbeit für mehrzeilige Textbearbeitung übernimmt: Verteilung von Tastatureingaben, Positionierung des IME-Kandidatenfensters, bracketed paste, Emacs-artiges kill/yank, fish-artiges undo coalescing, Slash-Kommandos und Symbol-triggertes autocomplete. InteractiveMode nutzt sie als Haupteingabefeld; Erweiterungen können über das EditorComponent-Interface eine andere Implementierung einschieben (vim-/emacs-Modus).
Zuständigkeiten
Editor macht vier Dinge:
- Eingabe-Verteilung:
handleInput(data)ist der Einstiegspunkt für den rohen Terminal-Byte-Stream und verarbeitet nacheinander jump mode, bracketed paste, undo, autocomplete-Navigation und normale Zeichen. Siehepackages/tui/src/components/editor.ts:534-630. - Mehrzeilige Bearbeitung:
addNewLinespaltet am Cursor eine Zeile,insertTextAtCursorInternalbehandelt mehrzeiligen Text aus Paste oder autocomplete. Siehepackages/tui/src/components/editor.ts:1152-1175undpackages/tui/src/components/editor.ts:980-1023. - kill-ring / undo: Emacs kill/yank plus fish-artiges undo coalescing; aufeinanderfolgende kills werden im selben Ring-Eintrag gesammelt, nach yank rotiert
yank-popden Ring. Siehepackages/tui/src/components/editor.ts:1817-1896undpackages/tui/src/kill-ring.ts:1-50. - IME-Cursor-Positionierung:
rendersetzt am Cursor einenCURSOR_MARKER(eine nullbreite APC-Sequenz);TUIextrahiert ihn und verschiebt den Hardware-Cursor dorthin, sodass das IME-Kandidatenfenster am Cursor klebt. Siehepackages/tui/src/components/editor.ts:474-501.
Designmotivation
Warum nicht Node.js' readline? Weil es kein IME-Support, keine Mehrzeiligkeit, kein undo und keine autocomplete-Hooks hat. Editor baut all das in eine Komponente; drei Entscheidungen sind entscheidend. Erstens undo coalescing: Aufeinanderfolgende Zeicheneingaben werden standardmäßig zu einer undo-Einheit zusammengefasst (ein Leerzeichen startet eine neue Einheit), damit nicht jedes Zeichen einen Snapshot erzeugt und undo laut wird. Zweitens kill-ring-Akkumulation: Aufeinanderfolgende deleteWordBackwards hängen den gelöschten Text an denselben Ring-Eintrag an, bei Rückwärtslöschen wird prepend, bei Vorwärts append; so gibt yank mit einem Aufruf den ganzen Block zurück. Drittens paste marker: Große Pastes werden nicht direkt in den Text gesteckt, sondern durch [paste #N +M lines] ersetzt; die echten Inhalte liegen in pasteRegistry und werden über getExpandedText expandiert, damit ein paar tausend eingefügte Zeilen das Rendering nicht blockieren.
Wichtige Dateien
packages/tui/src/components/editor.ts:217-288—class EditorFelder:state(lines/cursorLine/cursorCol),killRing,undoStack,pasteBuffer,autocompleteState.packages/tui/src/components/editor.ts:409-470—render(width)-Körper: word-wrap-Layout, Cursor-Rendering, padding/Border.packages/tui/src/components/editor.ts:534-595—handleInput-Einstieg: paste-Erkennung, undo, autocomplete-Navigation.packages/tui/src/components/editor.ts:1084-1122—handlePaste: CSI-u-Kontrollbytes dekodieren, non-printable herausfiltern, vor Pfaden automatisch ein Leerzeichen einfügen.packages/tui/src/components/editor.ts:1935-1949—pushUndoSnapshot/undo:structuredClone-Snapshot des Zustands.packages/tui/src/kill-ring.ts:1-50—KillRingRing-Buffer,pushmit prepend/accumulate.packages/tui/src/undo-stack.ts:1-35—UndoStack<S>generischer Stack mit clone-on-push.packages/tui/src/editor-component.ts:11-74—EditorComponent-Interface, Erweiterungen können die Implementierung ersetzen.
handleInput prüft zuerst die bracketed-paste-Start/End-Marker und sammelt während der Paste in pasteBuffer, bis \x1b[201~ ankommt:
// packages/tui/src/components/editor.ts:559-582
if (data.includes("\x1b[200~")) {
this.isInPaste = true;
this.pasteBuffer = "";
data = data.replace("\x1b[200~", "");
}
if (this.isInPaste) {
this.pasteBuffer += data;
const endIndex = this.pasteBuffer.indexOf("\x1b[201~");
if (endIndex !== -1) {
const pasteContent = this.pasteBuffer.substring(0, endIndex);
if (pasteContent.length > 0) this.handlePaste(pasteContent);
this.isInPaste = false;
// ...
}
}undo coalescing fasst aufeinanderfolgende Word-Zeichen zu einer undo-Einheit zusammen; Leerzeichen werden eigene Einheiten:
// packages/tui/src/components/editor.ts:1027-1037
// - Consecutive word chars coalesce into one undo unit
// - Space captures state before itself (so undo removes space+following word together)
// - Each space is separately undoable
if (!skipUndoCoalescing) {
if (isWhitespaceChar(char) || this.lastAction !== "type-word") {
this.pushUndoSnapshot();
}
this.lastAction = "type-word";
}yank nimmt das Top-Element aus dem kill-ring und fügt es ein; yank-pop muss direkt auf yank folgen, löscht zuerst den zuletzt yankierten Text und rotiert dann den Ring:
// packages/tui/src/components/editor.ts:1832-1848
private yankPop(): void {
if (this.lastAction !== "yank" || this.killRing.length <= 1) return;
this.pushUndoSnapshot();
this.deleteYankedText();
this.killRing.rotate();
const text = this.killRing.peek()!;
this.insertYankedText(text);
this.lastAction = "yank";
}Datenfluss
raw stdin → Editor.handleInput → Zustandsänderung → onChange → externe requestRender:
Randbedingungen und Fehler
- tmux
extended-keys-format=csi-ucodiert Kontrollbytes neu:handlePastewandelt\x1b[<cp>;5uper Regex in das Originalbyte zurück, damit ein newline nicht als ESC +[106;5uin den Editor rutscht. Siehepackages/tui/src/components/editor.ts:1091-1101. - Große Paste läuft über Marker: Text über dem Schwellwert wird durch
[paste #N +M lines]ersetzt; die echte Inhalte liegen inpasteRegistry,getExpandedTextexpandiert. Das Rendering zeichnet nur den Marker und hakt nicht. - undo über Paste ist atomar:
handlePasteruft am Einstieg einmalpushUndoSnapshotauf; nach der Paste reicht ein undo, um in den Zustand vor der Paste zurückzukehren. - jump mode fängt den nächsten Tastendruck ab: Ist jump aktiv, wird das nächste printable Zeichen nicht eingefügt, sondern löst
jumpToCharaus; ctrl-Zeichen bricht jump ab. Siehepackages/tui/src/components/editor.ts:538-556. - submit-Semantik konfigurierbar:
shouldSubmitOnBackslashEnterentscheidet anhand der keybindings, ob\nsubmitted, je nachdem obtui.input.submitaufenterodershift+entersteht. Siehepackages/tui/src/components/editor.ts:1177-1188.
Zusammenfassung
Editor ist ein vollständiger Terminal-Editor in einer Klasse: Eingabe-Verteilung, IME, kill-ring, undo, autocomplete. Er baut auf dem differentiellen Rendering von TUI und dem CURSOR_MARKER-Mechanismus zur Positionierung des Hardware-Cursors auf, siehe TUI-Klasse: Scheduler-Zentrale für differentielles Rendering; wie der Byte-Stream zu key ids geparst wird, steht in Tastatur-Parsing: kitty-Protokoll; das autocomplete-Dropdown nutzt SelectList, siehe Komponentenbibliothek.