Composant éditeur : entrée, IME, kill-ring, undo
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 :
- 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. Voirpackages/tui/src/components/editor.ts:534-630. - Édition multiligne :
addNewLinecoupe la ligne au curseur,insertTextAtCursorInternaltraite l'insertion de texte multiligne (collage / complétion). Voirpackages/tui/src/components/editor.ts:1152-1175etpackages/tui/src/components/editor.ts:980-1023. - kill-ring / undo : kill/yank Emacs + undo coalescing style fish ; les kills consécutifs s'accumulent, après un yank le
yank-popfait tourner le ring. Voirpackages/tui/src/components/editor.ts:1817-1896etpackages/tui/src/kill-ring.ts:1-50. - Positionnement du curseur IME :
renderinsère unCURSOR_MARKER(séquence APC zéro-largeur) devant la position du curseur ;TUIl'extrait et y déplace le curseur matériel, pour que la fenêtre de candidats IME reste collée au curseur. Voirpackages/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
packages/tui/src/components/editor.ts:217-288— champs declass Editor:state(lines/cursorLine/cursorCol),killRing,undoStack,pasteBuffer,autocompleteState.packages/tui/src/components/editor.ts:409-470— corps derender(width): layout word-wrap, rendu du curseur, padding/bordure.packages/tui/src/components/editor.ts:534-595— entréehandleInput: détection paste, undo, navigation autocomplete.packages/tui/src/components/editor.ts:1084-1122—handlePaste: décodage des bytes de contrôle CSI-u, filtrage des non printable, auto-espace devant les chemins.packages/tui/src/components/editor.ts:1935-1949—pushUndoSnapshot/undo: snapshot d'état viastructuredClone.packages/tui/src/kill-ring.ts:1-50—KillRingtampon circulaire,pushsupporte prepend/accumulate.packages/tui/src/undo-stack.ts:1-35— pile génériqueUndoStack<S>, clone-on-push.packages/tui/src/editor-component.ts:11-74— interfaceEditorComponent, implémentation remplaçable par une extension.
handleInput commence par détecter les markers de début/fin du bracketed paste, et accumule dans pasteBuffer jusqu'à recevoir \x1b[201~ :
// 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 :
// 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 :
// 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 → onChange → requestRender externe :
Limites et cas d'échec
- tmux
extended-keys-format=csi-uréencode les bytes de contrôle :handlePasteutilise une regex pour restaurer\x1b[<cp>;5uen bytes d'origine, pour éviter qu'un newline ne se retrouve en ESC +[106;5uqui se faufilerait dans l'éditeur. Voirpackages/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é danspasteRegistryet expansé pargetExpandedText. Le rendu ne dessine que le marker, pas de blocage. - Undo atomique à travers un paste :
handlePastefait un seulpushUndoSnapshoten 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. Voirpackages/tui/src/components/editor.ts:538-556. - Sémantique de submit configurable :
shouldSubmitOnBackslashEnterdécide si\nsubmit en fonction du keybindingtui.input.submit(enteroushift+enter). Voirpackages/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.