エディタコンポーネント:入力、IME、kill-ring、undo
Editor は @mariozechner/pi-tui で最も重いコンポーネント——単一ファイル 2292 行で、複数行テキスト編集の泥臭い仕事を全部担う:キーボード入力のディスパッチ、IME 候補ウィンドウの位置決め、bracketed paste 処理、Emacs 風 kill/yank、fish 風 undo coalescing、スラッシュコマンドと記号トリガによる 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 ユニットにまとめられ(空白が新ユニットのトリガ)、文字ごとに snapshot を 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入口:ペースト検出、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で状態 snapshot を取る。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 を検出し、ペースト中は \x1b[201~ を受け取るまで pasteBuffer に蓄積する:
// 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 テキストを消してから 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 はペーストをまたいでアトミック:
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 を使う。コンポーネントライブラリ 参照。