編輯器元件:輸入、IME、kill-ring、undo
Editor 是 @mariozechner/pi-tui 裡最重的元件——單檔 2292 行,承擔多行文字編輯的全部髒活:鍵盤輸入分發、IME 候選視窗定位、bracketed paste 處理、Emacs 風格 kill/yank、fish 風格 undo coalescing、slash 命令與符號觸發 autocomplete。InteractiveMode 用它當主輸入框,擴充也能透過 EditorComponent 介面替換實作(vim/emacs 模式)。
職責
Editor 做四件事:
- 輸入分發:
handleInput(data)是 raw 終端機位元組流入口,按順序處理 jump mode、bracketed paste、undo、autocomplete 導航、普通字元。見packages/tui/src/components/editor.ts:534-630。 - 多行編輯:
addNewLine在游標處切行,insertTextAtCursorInternal處理貼上/補全插入的多行文字。見packages/tui/src/components/editor.ts:1152-1175與packages/tui/src/components/editor.ts:980-1023。 - kill-ring / undo:Emacs kill/yank + fish 風格 undo coalescing,kill 連續刪除可累積,yank 後
yank-pop迴圈 ring。見packages/tui/src/components/editor.ts:1817-1896與packages/tui/src/kill-ring.ts:1-50。 - IME 游標定位:
render在游標位置前插CURSOR_MARKER(零寬 APC 序列),TUI提取後把硬體游標移到該位置,IME 候選視窗就能貼住游標。見packages/tui/src/components/editor.ts:474-501。
設計動機
為什麼不用 Node.js 的 readline?因為它沒有 IME 支援、沒有多行、沒有 undo、沒有 autocomplete 鉤子。Editor 把這些做進同一元件,關鍵決策有三個。一是 undo coalescing:連續輸入字元預設併成一個 undo 單元(空格觸發新單元),避免每個字元都 push 一次快照導致 undo 雜訊。二是 kill-ring 累積:連續 deleteWordBackwards 把刪除文字拼到同一個 ring 項裡,反向刪除時 prepend,正向 append,這樣 yank 一次能恢復整段。三是 paste marker:大段貼上不直接塞進文字,而是替換成 [paste #N +M lines] 占位,展開時再呼叫 getExpandedText,避免幾千行貼上把渲染拖死。
關鍵檔案
packages/tui/src/components/editor.ts:217-288—class Editor欄位:state(lines/cursorLine/cursorCol)、killRing、undoStack、pasteBuffer、autocompleteState。packages/tui/src/components/editor.ts:409-470—render(width)主體:word-wrap 佈局、游標渲染、padding/邊框。packages/tui/src/components/editor.ts:534-595—handleInput入口:paste 偵測、undo、autocomplete 導航。packages/tui/src/components/editor.ts:1084-1122—handlePaste:CSI-u 控制位元組解碼、過濾非 printable、路徑前自動補空格。packages/tui/src/components/editor.ts:1935-1949—pushUndoSnapshot/undo:structuredClone狀態快照。packages/tui/src/kill-ring.ts:1-50—KillRing環形緩衝,push支援 prepend/accumulate。packages/tui/src/undo-stack.ts:1-35—UndoStack<S>通用堆疊,clone-on-push。packages/tui/src/editor-component.ts:11-74—EditorComponent介面,擴充可替換實作。
handleInput 進來先偵測 bracketed paste 起止 marker,貼上期間累積到 pasteBuffer 直到收到 \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;
// ...
}
}undo coalescing 把連續 word 字元合併成一個 undo 單元,空格單獨成單元:
// 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 從 kill-ring 取堆疊頂端插入,yank-pop 必須緊跟 yank,先刪上次 yank 的文字再 rotate 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";
}資料流
raw stdin → Editor.handleInput → 狀態變更 → onChange → 外部 requestRender:
邊界與失敗
- tmux
extended-keys-format=csi-u把控制位元組重編碼:handlePaste用正規表示式把\x1b[<cp>;5u還原成原始位元組,避免 newline 被當成 ESC +[106;5u漏進編輯器。見packages/tui/src/components/editor.ts:1091-1101。 - 大貼上走 marker:超閾值文字替換成
[paste #N +M lines],真正內容存在pasteRegistry,getExpandedText展開。渲染只繪 marker,不會卡。 - undo 跨 paste 原子:
handlePaste入口pushUndoSnapshot一次,貼上後單次 undo 回到貼上前狀態。 - jump mode 攔截下一鍵:進入 jump 後下一個 printable 字元不插入文字,而是觸發
jumpToChar,ctrl 字元取消 jump。見packages/tui/src/components/editor.ts:538-556。 - submit 語義可設定:
shouldSubmitOnBackslashEnter根據 keybindings 裡tui.input.submit是enter還是shift+enter決定\n是否提交。見packages/tui/src/components/editor.ts:1177-1188。
小結
Editor 是個五臟俱全的終端機編輯器:輸入分發、IME、kill-ring、undo、autocomplete 都在一個類別裡。它依賴 TUI 的差分渲染和 CURSOR_MARKER 機制定位硬體游標,見 TUI 類別:差分渲染的排程中樞;鍵盤位元組流怎麼解析成 key id 見 鍵盤解析:kitty 協定;autocomplete 下拉用 SelectList,見 元件庫。