Componente editor: entrada, IME, kill-ring, undo
Editor es el componente más pesado de @mariozechner/pi-tui: 2292 líneas en un solo archivo, haciendo todo el trabajo sucio de edición multilínea: dispatch de teclado, posicionamiento de la ventana de candidatos IME, bracketed paste, kill/yank estilo Emacs, undo coalescing estilo fish, y autocompletado por slash y disparadores de símbolos. InteractiveMode lo usa como caja de entrada principal, y las extensiones pueden reemplazarlo vía la interfaz EditorComponent (modos vim/emacs).
Responsabilidades
Editor hace cuatro cosas:
- Dispatch de entrada:
handleInput(data)es la entrada de byte stream raw de la terminal; procesa en orden jump mode, bracketed paste, undo, navegación de autocompletado, caracteres normales. Verpackages/tui/src/components/editor.ts:534-630. - Edición multilínea:
addNewLinecorta la línea en la posición del cursor,insertTextAtCursorInternalgestiona inserción multilínea de pegados/completados. Verpackages/tui/src/components/editor.ts:1152-1175ypackages/tui/src/components/editor.ts:980-1023. - kill-ring / undo: kill/yank estilo Emacs + undo coalescing estilo fish; los kill consecuentes pueden acumularse, y tras yank se hace
yank-poprotando el ring. Verpackages/tui/src/components/editor.ts:1817-1896ypackages/tui/src/kill-ring.ts:1-50. - Posicionamiento del cursor IME:
renderinsertaCURSOR_MARKER(una secuencia APC de ancho cero) antes de la posición del cursor;TUIlo extrae y mueve el cursor hardware a esa posición, dejando la ventana de candidatos IME pegada al cursor. Verpackages/tui/src/components/editor.ts:474-501.
Motivación de diseño
¿Por qué no usar el readline de Node.js? Porque no soporta IME, ni multilínea, ni undo, ni hooks de autocompletado. Editor lo mete todo en un mismo componente, con tres decisiones clave. Una, undo coalescing: la entrada consecutiva de caracteres se une por defecto en una sola unidad de undo (el espacio dispara una nueva), evitando que cada carácter empuje un snapshot y llene el undo de ruido. Dos, acumulación en kill-ring: deleteWordBackwards consecutivo concatena el texto borrado en el mismo ítem del ring, prependiendo al borrar hacia atrás y appendiceando al borrar hacia adelante; así un solo yank puede restaurar el bloque entero. Tres, paste marker: las pegadas grandes no se insertan en el texto, se reemplazan por un placeholder [paste #N +M lines] y al expandir se llama getExpandedText, evitando que miles de líneas pegadas ahoguen el render.
Archivos clave
packages/tui/src/components/editor.ts:217-288— Campos declass Editor:state(lines/cursorLine/cursorCol),killRing,undoStack,pasteBuffer,autocompleteState.packages/tui/src/components/editor.ts:409-470—render(width)principal: layout word-wrap, render de cursor, padding/border.packages/tui/src/components/editor.ts:534-595— Entrada dehandleInput: detección de paste, undo, navegación de autocompletado.packages/tui/src/components/editor.ts:1084-1122—handlePaste: decodificación de bytes de control CSI-u, filtrado de no imprimibles, autocompletado de espacio antes de ruta.packages/tui/src/components/editor.ts:1935-1949—pushUndoSnapshot/undo: snapshot víastructuredClone.packages/tui/src/kill-ring.ts:1-50—KillRingbuffer circular,pushsoporta prepend/accumulate.packages/tui/src/undo-stack.ts:1-35—UndoStack<S>pila genérica, clone-on-push.packages/tui/src/editor-component.ts:11-74— InterfazEditorComponent, implementación reemplazable por extensiones.
handleInput al entrar primero detecta los marcadores de bracketed paste y acumula en pasteBuffer hasta recibir \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;
// ...
}
}El undo coalescing funde los caracteres word consecutivos en una unidad de undo, el espacio es su propia unidad:
// 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 saca la cima del kill-ring y la inserta; yank-pop debe seguir a yank, primero borra el texto del yank anterior y luego rota el 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";
}Flujo de datos
raw stdin → Editor.handleInput → mutación de estado → onChange → requestRender externo:
Límites y fallos
- tmux
extended-keys-format=csi-urecodifica bytes de control:handlePasteusa regex para reconstruir\x1b[<cp>;5ual byte original, evitando que un newline se cuele como ESC +[106;5uen el editor. Verpackages/tui/src/components/editor.ts:1091-1101. - Paste grande vía marker: el texto que supera el umbral se reemplaza por
[paste #N +M lines], el contenido real vive enpasteRegistry;getExpandedTextlo expande. El render sólo dibuja el marker, sin lag. - Undo atómico entre paste:
handlePastehace un únicopushUndoSnapshotal entrar; tras pegar, un solo undo vuelve al estado previo. - Jump mode intercepta la siguiente tecla: al entrar en jump, el siguiente carácter imprimible no inserta texto sino que dispara
jumpToChar; los caracteres ctrl cancelan el jump. Verpackages/tui/src/components/editor.ts:538-556. - Semántica de submit configurable:
shouldSubmitOnBackslashEnterdecide si\nhace submit segúntui.input.submiten keybindings (enteroshift+enter). Verpackages/tui/src/components/editor.ts:1177-1188.
Resumen
Editor es un editor de terminal completo: dispatch de entrada, IME, kill-ring, undo y autocompletado, todo en una clase. Depende del render diferencial de TUI y del mecanismo CURSOR_MARKER para posicionar el cursor hardware; ver clase TUI: centro de scheduling de renderizado diferencial. Cómo el byte stream de teclado se parsea a key id en parseo de teclado: protocolo kitty; el dropdown de autocompletado usa SelectList, ver biblioteca de componentes.