Skip to content

Composant éditeur : entrée, IME, kill-ring, undo

源码版本v0.73.1

Editor est le composant le plus lourd de @mariozechner/pi-tui — 2292 lignes en un seul fichier, qui prend en charge tout le travail ingrat de l'édition multiligne : dispatch des entrées clavier, positionnement de la fenêtre de candidats IME, bracketed paste, kill/yank style Emacs, undo coalescing style fish, autocomplete déclenché par slash et symboles. InteractiveMode l'utilise comme champ d'entrée principal, et les extensions peuvent remplacer l'implémentation via l'interface EditorComponent (mode vim/emacs).

Responsabilités

Editor fait quatre choses :

  1. Dispatch d'entrée : handleInput(data) est l'entrée du flux d'octets brut du terminal ; il traite dans l'ordre jump mode, bracketed paste, undo, navigation autocomplete, caractères normaux. Voir packages/tui/src/components/editor.ts:534-630.
  2. Édition multiligne : addNewLine coupe la ligne au curseur, insertTextAtCursorInternal traite l'insertion de texte multiligne (collage / complétion). Voir packages/tui/src/components/editor.ts:1152-1175 et packages/tui/src/components/editor.ts:980-1023.
  3. kill-ring / undo : kill/yank Emacs + undo coalescing style fish ; les kills consécutifs s'accumulent, après un yank le yank-pop fait tourner le ring. Voir packages/tui/src/components/editor.ts:1817-1896 et packages/tui/src/kill-ring.ts:1-50.
  4. Positionnement du curseur IME : render insère un CURSOR_MARKER (séquence APC zéro-largeur) devant la position du curseur ; TUI l'extrait et y déplace le curseur matériel, pour que la fenêtre de candidats IME reste collée au curseur. Voir packages/tui/src/components/editor.ts:474-501.

Motifs de conception

Pourquoi ne pas utiliser le readline de Node.js ? Parce qu'il n'a pas de support IME, pas de multiligne, pas d'undo, pas de hook d'autocomplete. Editor intègre tout cela dans un même composant, avec trois décisions clés. Premièrement, l'undo coalescing : les caractères consécutifs sont fusionnés en une seule unité undo par défaut (l'espace déclenche une nouvelle unité), pour éviter qu'un snapshot ne soit pushé à chaque caractère et ne produise un bruit d'undo. Deuxièmement, l'accumulation dans le kill-ring : des deleteWordBackwards consécutifs concatènent le texte supprimé dans une même entrée du ring — en suppression arrière c'est prepend, en suppression avant c'est append — de sorte qu'un seul yank récupère le segment entier. Troisièmement, le paste marker : un long collage n'est pas inséré tel quel dans le texte, il est remplacé par un placeholder [paste #N +M lines] ; le contenu réel est conservé dans pasteRegistry et expansé via getExpandedText, pour éviter qu'un collage de plusieurs milliers de lignes ne plombe le rendu.

Fichiers clés

handleInput commence par détecter les markers de début/fin du bracketed paste, et accumule dans pasteBuffer jusqu'à recevoir \x1b[201~ :

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;
    // ...
  }
}

L'undo coalescing fusionne les caractères word consécutifs en une seule unité undo, l'espace forme une unité à part :

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 récupère le sommet du kill-ring et l'insère, yank-pop doit suivre immédiatement un yank ; il supprime d'abord le texte du yank précédent puis fait tourner le 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";
}

Flux de données

raw stdin → Editor.handleInput → changement d'état → onChangerequestRender externe :

Limites et cas d'échec

  • tmux extended-keys-format=csi-u réencode les bytes de contrôle : handlePaste utilise une regex pour restaurer \x1b[<cp>;5u en bytes d'origine, pour éviter qu'un newline ne se retrouve en ESC + [106;5u qui se faufilerait dans l'éditeur. Voir packages/tui/src/components/editor.ts:1091-1101.
  • Les gros collages passent par un marker : au-dessus d'un seuil, le texte est remplacé par [paste #N +M lines], le contenu réel est stocké dans pasteRegistry et expansé par getExpandedText. Le rendu ne dessine que le marker, pas de blocage.
  • Undo atomique à travers un paste : handlePaste fait un seul pushUndoSnapshot en entrée, donc un undo unique revient à l'état d'avant le collage.
  • Le jump mode intercepte la touche suivante : une fois en jump mode, le prochain caractère printable n'insère pas de texte mais déclenche jumpToChar ; un caractère ctrl annule le jump. Voir packages/tui/src/components/editor.ts:538-556.
  • Sémantique de submit configurable : shouldSubmitOnBackslashEnter décide si \n submit en fonction du keybinding tui.input.submit (enter ou shift+enter). Voir packages/tui/src/components/editor.ts:1177-1188.

Synthèse

Editor est un éditeur terminal complet : dispatch d'entrée, IME, kill-ring, undo, autocomplete dans une seule classe. Il s'appuie sur le rendu différentiel de TUI et sur le mécanisme CURSOR_MARKER pour positionner le curseur matériel, voir classe TUI : hub de scheduling du rendu différentiel ; pour le parsing du flux d'octets clavier en key id, voir parsing clavier : protocole kitty ; l'autocomplete déroulant utilise SelectList, voir bibliothèque de composants.