Skip to content

Editor-Komponente: Eingabe, IME, kill-ring, undo

源码版本v0.73.1

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:

  1. 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. Siehe packages/tui/src/components/editor.ts:534-630.
  2. Mehrzeilige Bearbeitung: addNewLine spaltet am Cursor eine Zeile, insertTextAtCursorInternal behandelt mehrzeiligen Text aus Paste oder autocomplete. Siehe packages/tui/src/components/editor.ts:1152-1175 und packages/tui/src/components/editor.ts:980-1023.
  3. kill-ring / undo: Emacs kill/yank plus fish-artiges undo coalescing; aufeinanderfolgende kills werden im selben Ring-Eintrag gesammelt, nach yank rotiert yank-pop den Ring. Siehe packages/tui/src/components/editor.ts:1817-1896 und packages/tui/src/kill-ring.ts:1-50.
  4. IME-Cursor-Positionierung: render setzt am Cursor einen CURSOR_MARKER (eine nullbreite APC-Sequenz); TUI extrahiert ihn und verschiebt den Hardware-Cursor dorthin, sodass das IME-Kandidatenfenster am Cursor klebt. Siehe packages/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

handleInput prüft zuerst die bracketed-paste-Start/End-Marker und sammelt während der Paste in pasteBuffer, bis \x1b[201~ ankommt:

typescript
// 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:

typescript
// 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:

typescript
// 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-u codiert Kontrollbytes neu: handlePaste wandelt \x1b[<cp>;5u per Regex in das Originalbyte zurück, damit ein newline nicht als ESC + [106;5u in den Editor rutscht. Siehe packages/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 in pasteRegistry, getExpandedText expandiert. Das Rendering zeichnet nur den Marker und hakt nicht.
  • undo über Paste ist atomar: handlePaste ruft am Einstieg einmal pushUndoSnapshot auf; 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 jumpToChar aus; ctrl-Zeichen bricht jump ab. Siehe packages/tui/src/components/editor.ts:538-556.
  • submit-Semantik konfigurierbar: shouldSubmitOnBackslashEnter entscheidet anhand der keybindings, ob \n submitted, je nachdem ob tui.input.submit auf enter oder shift+enter steht. Siehe packages/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.