跳转至

41. packages/tui 组件:从 Editor 到可复用显示块

这个模块干什么

components/TUI 所需的 render(width): string[] 协议实现成输入、编辑器、列表、Markdown、图片和布局小件。packages/tui/src/tui.ts:61-88 packages/tui/src/index.ts:11-29

其中 Editor 是多行输入的重心:它保存逻辑行与光标,处理历史、粘贴、撤销、Emacs 风格 kill/yank、可视行移动和异步补全。packages/tui/src/components/editor.ts:208-343

EditorComponent 则是给扩展的兼容合同;扩展可替换编辑实现,但仍须提供文本读取、写入、输入处理和提交/变更回调。packages/tui/src/editor-component.ts:4-34

其余组件不是另一套框架,而是同一行数组协议上的小实现,通常复用 visibleWidth()wrapTextWithAnsi()truncateToWidth() 与全局 keybinding。packages/tui/src/components/text.ts:1-105 packages/tui/src/components/select-list.ts:1-229

核心文件速查

文件 一句话职责 必读优先级
packages/tui/src/editor-component.ts 规定自定义编辑器必须和可选能力,供核心应用与扩展互换编辑实现。packages/tui/src/editor-component.ts:4-74 ⭐⭐⭐
packages/tui/src/components/editor.ts 实现多行编辑、可视折行、历史、粘贴占位、补全、撤销、kill/yank 与 IME 光标。packages/tui/src/components/editor.ts:270-353 ⭐⭐⭐
packages/tui/src/autocomplete.ts 定义 provider 合同,并实现 slash command、普通路径和 @ 附件路径补全。packages/tui/src/autocomplete.ts:219-270 packages/tui/src/autocomplete.ts:272-786 ⭐⭐⭐
packages/tui/src/fuzzy.ts 为命令和设置搜索提供按序字符匹配与排序分数。packages/tui/src/fuzzy.ts:1-137 ⭐⭐
packages/tui/src/undo-stack.ts structuredClone() 保存泛型状态快照。packages/tui/src/undo-stack.ts:1-28 ⭐⭐
packages/tui/src/kill-ring.ts 保存连续删除文本,并支持 yank 与 yank-pop 轮换。packages/tui/src/kill-ring.ts:1-46 ⭐⭐
packages/tui/src/word-navigation.ts Intl.Segmenter 和标点规则计算 word-left/word-right 的目标下标。packages/tui/src/word-navigation.ts:1-117 ⭐⭐
packages/tui/src/components/select-list.ts 渲染可滚动选择列表,是编辑器补全菜单的基础。packages/tui/src/components/select-list.ts:12-58 packages/tui/src/components/select-list.ts:74-137 ⭐⭐⭐
packages/tui/src/components/markdown.ts marked token 渲染 ANSI Markdown、表格、列表、链接和代码块,并缓存结果。packages/tui/src/components/markdown.ts:1-53 packages/tui/src/components/markdown.ts:110-242 ⭐⭐⭐
packages/tui/src/components/input.ts 提供单行输入、横向滚动、粘贴、undo 与 kill/yank。packages/tui/src/components/input.ts:16-47 packages/tui/src/components/input.ts:48-446 ⭐⭐
components/text.tsbox.tstruncated-text.tsspacer.ts 提供文本折行、带背景容器、单行截断和垂直留白。packages/tui/src/components/text.ts:4-105 packages/tui/src/components/box.ts:11-137 packages/tui/src/components/truncated-text.ts:4-64 packages/tui/src/components/spacer.ts:3-27
components/loader.tscancellable-loader.tsimage.tssettings-list.ts 提供转圈/取消、图片协议适配和可搜索设置面板。packages/tui/src/components/loader.ts:14-92 packages/tui/src/components/cancellable-loader.ts:4-40 packages/tui/src/components/image.ts:12-125 packages/tui/src/components/settings-list.ts:7-250 ⭐⭐

精读

1. editor-component.ts:替换编辑器时必须守住的合同

EditorComponent 继承通用 Component,因此替代实现仍要能 render(width)invalidate(),并遵守 TUI 的输入接口。packages/tui/src/editor-component.ts:1-2 packages/tui/src/editor-component.ts:11-24 packages/tui/src/tui.ts:61-88

必需的简化签名是 getText(): stringsetText(text): voidhandleInput(data): voidonSubmit?: (text) => voidonChange?: (text) => voidpackages/tui/src/editor-component.ts:13-34

这意味着调用方可以只依赖“读取、写入、提交、变化”四种能力,而不关心底层是默认编辑器还是 Vim/Emacs 风格扩展。packages/tui/src/editor-component.ts:4-10 packages/tui/src/editor-component.ts:13-34

addToHistory?(text) 是可选的历史接口,默认 Editor 会实现它。packages/tui/src/editor-component.ts:36-40 packages/tui/src/components/editor.ts:395-409

insertTextAtCursor?(text)getExpandedText?() 是给程序化插入及 paste marker 展开留下的可选能力。packages/tui/src/editor-component.ts:43-53

setAutocompleteProvider?(provider) 让替代编辑器可接入统一的 provider,而不用暴露默认 Editor 的内部状态。packages/tui/src/editor-component.ts:56-60

外观侧只有可选 borderColorsetPaddingX()setAutocompleteMaxVisible(),所以扩展不必假装拥有默认编辑器全部的布局配置。packages/tui/src/editor-component.ts:63-73

容易漏掉的是接口将 handleInput 列为必需成员;自定义 editor 不能只实现渲染,否则获得焦点后无法参与输入管线。packages/tui/src/editor-component.ts:22-24

2. components/editor.ts:多行编辑器的状态、布局和输入

Editor 实现 Component, Focusable,因此既参与每帧渲染,也能在获得焦点时放置 CURSOR_MARKER 给 TUI 定位 IME 光标。packages/tui/src/components/editor.ts:270-279 packages/tui/src/tui.ts:98-120

它的核心状态是 lines: string[]cursorLinecursorCol,初始值是单个空逻辑行和 (0, 0) 光标。packages/tui/src/components/editor.ts:208-220 packages/tui/src/components/editor.ts:270-275

这里的 cursorCol 是逻辑字符串的下标,而渲染和换行时要另用 visibleWidth() 处理宽字符。packages/tui/src/components/editor.ts:893-979 packages/tui/src/utils.ts:213-270

构造函数接收 TUIEditorTheme 与可选 padding/补全最大行数,并把 padding 夹成非负整数、补全可见数夹到 3 到 20。packages/tui/src/components/editor.ts:228-236 packages/tui/src/components/editor.ts:345-353

EditorTheme 只要求边框染色函数和 SelectListTheme,因为补全下拉菜单复用 SelectListpackages/tui/src/components/editor.ts:228-231 packages/tui/src/components/editor.ts:2132-2138

setPaddingX()setAutocompleteMaxVisible() 改值后请求 TUI 重绘,不保存自己的渲染缓存。packages/tui/src/components/editor.ts:365-387 packages/tui/src/components/editor.ts:478-480

setAutocompleteProvider(provider) 会先取消旧请求,再保存 provider,并按 provider 的 trigger characters 更新本地 trigger pattern。packages/tui/src/components/editor.ts:389-393 packages/tui/src/components/editor.ts:2219-2230

文本布局:逻辑行先折成 visual lines

wordWrapLine(line, maxWidth, preSegmented?) 返回带 textstartIndexendIndexTextChunk[],而不是只返回字符串。packages/tui/src/components/editor.ts:93-114

它优先在“空白后接非空白”处换行,并允许任一侧为 CJK grapheme 的边界成为换行点。packages/tui/src/components/editor.ts:130-199

没有合适断点时,超长词按 grapheme 强制切开;一个过宽的 atomic paste marker 仅在视觉层递归拆开,逻辑编辑仍把它看作单元。packages/tui/src/components/editor.ts:142-178

segmentWithMarkers() 会把有效 [paste #N ...] marker 内部的多个 grapheme 合并为单个 segment,失效 ID 的看起来相同的文本不会合并。packages/tui/src/components/editor.ts:21-91

validPasteIds() 从 paste map 的 key 构造集合,segment(text, mode) 再选择 word 或 grapheme segmenter。packages/tui/src/components/editor.ts:355-363

layoutText(contentWidth) 遍历每个逻辑行;能放下的行直接生成一个 LayoutLine,放不下的行按 wordWrapLine() 展开并定位哪一块拥有光标。packages/tui/src/components/editor.ts:893-979

render(width) 会从外部终端行数取 30% 作为编辑区最大可视行数,但最少保留 5 行。packages/tui/src/components/editor.ts:482-519

它根据 cursor 所在 layout line 调整 scrollOffset,再把超出上方或下方的行数画成带 / 的边框提示。packages/tui/src/components/editor.ts:507-531 packages/tui/src/components/editor.ts:581-588

每个可见文本行的 cursor 都使用 inverse video;获得焦点时,反色字符前还会插入零宽 CURSOR_MARKERpackages/tui/src/components/editor.ts:533-579

开启补全时,render()autocompleteList.render(contentWidth) 的行追加在编辑器底边之后,并加回左右 padding。packages/tui/src/components/editor.ts:590-600

普通输入:先是模式机,再是编辑操作

handleInput(data) 一开始先检查 jumpMode;再次按 jump 热键取消,否则将下一个可打印字符交给 jumpToChar()packages/tui/src/components/editor.ts:603-625 packages/tui/src/components/editor.ts:2030-2062

之后它缓冲 bracketed paste,直到检测到结束 marker 后才调用 handlePaste();marker 之后同一 chunk 的剩余输入会递归继续处理。packages/tui/src/components/editor.ts:627-651

tui.input.copy 对应的 Ctrl+C 在 editor 内直接 return,把退出或清屏的决定留给更上层。packages/tui/src/components/editor.ts:653-656

undo、补全模式、Tab、删除、kill/yank、光标移动、换行、提交、历史和 page navigation 都经 getKeybindings().matches() 判断。packages/tui/src/components/editor.ts:658-890 packages/tui/src/keybindings.ts:194-200

当补全菜单存在时,取消键关闭菜单,上下键只交给 SelectList,Tab 或确认键把选中项目交给 provider 的 applyCompletion()packages/tui/src/components/editor.ts:664-723

Tab 在普通模式中若位于未带参数的 slash command 上,就请求 slash 补全;否则强制请求文件补全。packages/tui/src/components/editor.ts:725-729 packages/tui/src/components/editor.ts:2144-2163

换行接受 keybinding 的 tui.input.newLine 以及多种终端序列;输入前一个字符是反斜杠且按键确属 Enter 时,会把反斜杠删掉并提交,作为不支持 Shift+Enter 的终端回退。packages/tui/src/components/editor.ts:785-818 packages/tui/src/components/editor.ts:1249-1258

普通可打印文本通过 decodePrintableKey(data) 优先解析 CSI-u/modifyOtherKeys,解析不到才接收首字符码点至少为 32 的原始数据。packages/tui/src/components/editor.ts:875-890 packages/tui/src/keys.ts:1385-1401

insertCharacter() 对连续 word 字符合并 undo;遇到空白或动作不是 type-word 时先保存 snapshot。packages/tui/src/components/editor.ts:1095-1121

插入后,/ 只在消息开头自动触发命令补全;@# 和 provider 自定义符号只在 token 边界触发;命令或符号上下文中继续输入字母数字、点、连字符、下划线也会更新补全。packages/tui/src/components/editor.ts:1123-1154

大粘贴:界面保持短,提交仍保留全文

handlePaste() 先取消补全并压入 undo snapshot,再把部分终端把 Ctrl 字节重编码成 CSI-u 的内容还原为文字控制字节。packages/tui/src/components/editor.ts:1156-1173

它规范化换行与 tab,过滤除换行外的不可打印字符;如果粘贴路径紧贴单词字符,会先补一个空格。packages/tui/src/components/editor.ts:1175-1192

超过 10 行或 1000 字符的粘贴不直接放进 state.lines,而是保存到 pastes map,并插入 [paste #N +X lines][paste #N Y chars] marker。packages/tui/src/components/editor.ts:1194-1222

getText() 返回当前 marker 文本;getExpandedText() 会按 map 把 marker 展回原内容。packages/tui/src/components/editor.ts:981-1000

提交时 submitValue() 会先展开 marker、trim() 结果、清空编辑器、pastes、历史浏览状态与 undo stack,再触发 onChange("")onSubmit(result)packages/tui/src/components/editor.ts:1260-1274

删除一个 marker 时,handleBackspace() 还会删除相应 paste,并按 ID 重新编号 map 与所有剩余 marker,避免 marker 与存储内容错位。packages/tui/src/components/editor.ts:1276-1359

光标:grapheme、visual line 与 sticky column

左右移动不是 cursorCol ± 1moveCursor() 对当前行做 grapheme segmentation,以整个 emoji 或组合字符为单位跨越。packages/tui/src/components/editor.ts:1790-1851

行间移动先由 buildVisualLineMap(width) 映射 logical line 和折行 chunk,再由 findVisualLineAt() 找到当前位置。packages/tui/src/components/editor.ts:1725-1788

moveToVisualLine() 维护 preferred visual column;目标较短时会保留偏好列,之后遇到够长的行再回到该列。packages/tui/src/components/editor.ts:1371-1508

光标若落在多 grapheme 的 atomic marker 中,会 snap 到 marker 开头并记录原位置;向下跨越 marker continuation 时可以跳到 marker 后的 visual line。packages/tui/src/components/editor.ts:1414-1455

pageScroll() 用同一 visual line 映射,以 max(5, floor(rows * 0.3)) 为页大小移动光标。packages/tui/src/components/editor.ts:1853-1867

moveWordBackwards()moveWordForwards() 跨逻辑行时跳到相邻行端点,同一行时把 marker-aware segmenter 传给 findWordBackward/Forward()packages/tui/src/components/editor.ts:1869-1889 packages/tui/src/components/editor.ts:2064-2083

历史、删除、kill/yank 与 undo

addToHistory(text) 会 trim 空内容、避免紧邻重复,并将最新记录放在 index 0,最多存 100 条。packages/tui/src/components/editor.ts:395-409

第一次进入 history browsing 时,navigateHistory() 保存当前草稿与 undo snapshot;向下离开历史时恢复草稿。packages/tui/src/components/editor.ts:427-462

删除到行首/行尾和删词都会将被删文本放进 KillRing;跨行时把换行符本身也作为 kill 文本。packages/tui/src/components/editor.ts:1521-1586 packages/tui/src/components/editor.ts:1588-1673

yank() 插入 ring 最新项,yankPop() 只允许紧跟 yank/yank-pop,并先删旧插入内容、轮换 ring、再插入新项。packages/tui/src/components/editor.ts:1891-1926 packages/tui/src/components/editor.ts:1970-2010

pushUndoSnapshot() 的 snapshot 不只含 text state,还含 paste map 与 counter;undo() 因此能回滚大粘贴 marker 与它的隐藏正文。packages/tui/src/components/editor.ts:2012-2028

3. autocomplete.tsfuzzy.ts:provider 是异步的,筛选是稳定的

AutocompleteProvider 的简化签名是异步 getSuggestions(lines, line, col, { signal, force? })、同步 applyCompletion(...),以及可选 triggerCharactersshouldTriggerFileCompletion()packages/tui/src/autocomplete.ts:241-270

返回值中的 items 给菜单,prefix 指明当前被替换的部分;applyCompletion() 必须返回新的 linescursorLinecursorColpackages/tui/src/autocomplete.ts:236-266

默认 CombinedAutocompleteProvider 接受 slash command/普通 item 列表、basePath 和可选 fdPathpackages/tui/src/autocomplete.ts:272-282

getSuggestions() 优先识别 @ 前缀,使用异步 fuzzy 文件查找;随后在非 force 状态处理首字符为 / 的命令名或命令参数;最后尝试普通路径。packages/tui/src/autocomplete.ts:284-373

applyCompletion() 对 slash command 会补 / 与尾随空格,对 @ 附件文件补空格但目录不补,且会避免 quoted item 与光标后已有引号重复。packages/tui/src/autocomplete.ts:375-460

普通路径补全使用 readdirSync(),处理 ~、相对/绝对路径、目录 symlink、目录优先排序、保留 ./ 以及带空格路径的引号。packages/tui/src/autocomplete.ts:509-693

@ 的 fuzzy 文件补全只有在提供 fdPath 且 signal 未 abort 时才工作;它调用 fd,跟随文件树、包含隐藏文件、排除 .git,最多取 100 条候选再筛到 20 条。packages/tui/src/autocomplete.ts:123-217 packages/tui/src/autocomplete.ts:719-772

walkDirectoryWithFd() 为 signal 注册 abort handler,abort 时杀掉仍在运行的子进程,并将失败、空 stdout 或非零退出统一为 []packages/tui/src/autocomplete.ts:158-216

fuzzyMatch(query, text) 要求 query 的字符按顺序出现;连续匹配和词边界降低 score,缺口和靠后位置增加 score,完整相等再减 100。packages/tui/src/fuzzy.ts:1-93

它还对“字母+数字”与“数字+字母”两种 query 顺序尝试互换,成功的替代匹配加 5 分。packages/tui/src/fuzzy.ts:75-92

fuzzyFilter() 以空白和 / 拆成多个 token,要求每个 token 都匹配,并按总分由小到大排序。packages/tui/src/fuzzy.ts:95-137

Editor.requestAutocomplete() 每次请求前会取消旧 debounce timer 和 AbortController,再生成新的 start token。packages/tui/src/components/editor.ts:2165-2194 packages/tui/src/components/editor.ts:2322-2341

普通 @/附件上下文会 debounce 20ms,显式 Tab 或 force 请求不延迟。packages/tui/src/components/editor.ts:243-245 packages/tui/src/components/editor.ts:2232-2240

请求按前一个 promise 串行化;开始时记录 request ID、文本、行和列快照。packages/tui/src/components/editor.ts:2196-2217

请求完成后,只有 signal 未 abort、request ID 仍相等、文本与光标仍等于快照才接受结果,避免旧异步结果覆盖新输入。packages/tui/src/components/editor.ts:2242-2308

force + 显式 Tab 且唯一候选时,编辑器直接应用补全;其他候选创建 SelectList 并选中精确匹配或第一个前缀匹配项。packages/tui/src/components/editor.ts:2271-2291 packages/tui/src/components/editor.ts:2102-2138

4. undo-stack.tskill-ring.tsword-navigation.ts:三个小模块约束编辑语义

UndoStack<S>.push(state) 对状态做 structuredClone() 后再入栈,pop() 返回已经脱离当前状态的 snapshot,clear() 清空数组。packages/tui/src/undo-stack.ts:1-28

Editor{ state, pastes, pasteCounter } 放进这个通用栈,所以回滚涵盖文本和大粘贴的外部存储。packages/tui/src/components/editor.ts:215-220 packages/tui/src/components/editor.ts:2012-2028

KillRing.push(text, { prepend, accumulate? }) 忽略空串;连续 kill 时与最后一项拼接,向后删除 append、向前删除 prepend。packages/tui/src/kill-ring.ts:11-28

peek() 不改变 ring,rotate() 在至少两项时把最后一项移到开头。packages/tui/src/kill-ring.ts:30-45

由于 peek() 取数组末尾,rotate() 后“下一项”会成为新的末尾,正好支持 editor 的 yankPop()packages/tui/src/kill-ring.ts:30-45 packages/tui/src/components/editor.ts:1905-1926

findWordBackward(text, cursor, options?) 先跳过尾部空白,再按 atomic segment、word-like segment 或标点 run 决定停点。packages/tui/src/word-navigation.ts:16-70

word-like segment 内部若含 ASCII 标点,会停在最后一个标点后的边界,保留代码式词语的标点导航。packages/tui/src/word-navigation.ts:47-56

findWordForward() 是镜像操作:先跳过前导空白,再处理 atomic、word-like 或标点 run。packages/tui/src/word-navigation.ts:72-117

它接受可注入 segmentisAtomicSegment,使默认 Editor 能把 paste marker 当成一整个 word-navigation 单元。packages/tui/src/word-navigation.ts:5-14 packages/tui/src/components/editor.ts:1869-1889

5. components/ 清单:每个可复用块负责什么

Text

Text 显示多行文本,先把 tab 变成三空格、使用 wrapTextWithAnsi() 折行,再添加 padding、可选背景和缓存。packages/tui/src/components/text.ts:4-105

setText()setCustomBgFn()invalidate() 都会清空缓存,因此它适合内容频繁更新但宽度不变时避免重复布局。packages/tui/src/components/text.ts:13-43 packages/tui/src/components/text.ts:45-105

TruncatedText

TruncatedText 只显示输入的第一逻辑行,减去 padding 后用 truncateToWidth() 截断,并把最终行补到完整 viewport 宽度。packages/tui/src/components/truncated-text.ts:4-64

它适合不能换行的状态栏、标题或行内摘要,而不是正文段落。packages/tui/src/components/truncated-text.ts:33-56

Spacer

Spacer(lines = 1)render() 返回指定数量的空字符串,setLines() 可以在运行时改变垂直间距。packages/tui/src/components/spacer.ts:3-27

它不关心 width,也没有渲染缓存。packages/tui/src/components/spacer.ts:17-27

Box

Box 自己维护 children,向 child 传入扣除左右 padding 的宽度,再把 child 行加上左 padding、上下空白和可选背景。packages/tui/src/components/box.ts:11-27 packages/tui/src/components/box.ts:74-135

它以 width、child 行数组和对 bgFn("test") 的采样共同判断缓存是否命中,因此动态背景函数的输出改变也能失效缓存。packages/tui/src/components/box.ts:4-9 packages/tui/src/components/box.ts:56-65 packages/tui/src/components/box.ts:95-124

Input

Input 是单行版编辑器,保存 value 和单个 cursor,也实现 Focusablepackages/tui/src/components/input.ts:11-37

它支持 bracketed paste、全局键绑定、grapheme 删除、word navigation、kill/yank、undo 以及 Kitty 可打印 CSI-u 解码。packages/tui/src/components/input.ts:48-372

渲染时它以 visibleWidth() 决定横向 scroll window,以 sliceByColumn() 截取,并使用 inverse video 与 CURSOR_MARKER 画光标。packages/tui/src/components/input.ts:378-446

Editor

Editor 是上述多行版本;它额外拥有 logical/visual line 映射、30% 高度 viewport、历史、异步补全和大粘贴 marker registry。packages/tui/src/components/editor.ts:270-343 packages/tui/src/components/editor.ts:482-600

它是交互式 prompt 输入最重要的组件,因为它把终端按键转换成可提交的多行文本和补全 UI。packages/tui/src/components/editor.ts:603-890 packages/tui/src/components/editor.ts:1260-1274

SelectList

SelectList 保存原 items、前缀过滤后的 items、选中下标和最大可见数,并提供 select、cancel、selection-change 回调。packages/tui/src/components/select-list.ts:12-58

它用 startsWith 过滤、以选项中心计算窗口、支持选中项上下循环,并在 Enter/Escape 时调用回调。packages/tui/src/components/select-list.ts:60-137

宽终端会渲染主列与描述两列,窄终端只保留主列;所有主文本和描述都经 truncateToWidth() 限宽。packages/tui/src/components/select-list.ts:139-216

SettingsList

SettingsList 显示 { id, label, currentValue },可选 values 用于循环切换,可选 submenu 工厂用于进入子组件。packages/tui/src/components/settings-list.ts:7-32 packages/tui/src/components/settings-list.ts:199-230

启用搜索时它创建内部 Input,将输入交给该 Input,再以 fuzzyFilter() 过滤 setting label。packages/tui/src/components/settings-list.ts:49-67 packages/tui/src/components/settings-list.ts:168-197 packages/tui/src/components/settings-list.ts:232-235

active submenu 期间 render 和输入都委派给 submenu;done callback 可选地更新值、通知 onChange 并恢复原选中项。packages/tui/src/components/settings-list.ts:81-88 packages/tui/src/components/settings-list.ts:168-174 packages/tui/src/components/settings-list.ts:199-230

Markdown

Markdown 用一个配置了 StrictStrikethroughTokenizerMarked 实例 lexer 文本,并修剪流式输出中不完整的闭合 code fence,避免代码块在最后几个 fence 字符到来时收缩闪烁。packages/tui/src/components/markdown.ts:6-53

它的 render 缓存键是 text 和 width;空文本返回空数组,非空文本依次 token 渲染、ANSI 保留折行、加 padding/背景并缓存。packages/tui/src/components/markdown.ts:110-242

renderToken() 覆盖标题、段落、代码块、列表、表格、引用、分隔线、HTML 和空白 token。packages/tui/src/components/markdown.ts:327-490

代码块可走主题提供的 highlightCode(),否则逐行用 codeBlock();链接在终端能力支持时生成 OSC 8,否则把 URL 以括号文本回退显示。packages/tui/src/components/markdown.ts:378-397 packages/tui/src/components/markdown.ts:537-557

表格先计算自然宽度和最小词宽,再在可用宽度内按比例分配并对单元格折行;过窄时退回原 Markdown 文本。packages/tui/src/components/markdown.ts:671-857

Loader

Loader 继承 Text,默认帧是十个 braille 字符,默认每 80ms 更新一次。packages/tui/src/components/loader.ts:1-26

start() 更新显示并重启 interval,interval 每帧变更后调用 TUI.requestRender()stop() 清理定时器。packages/tui/src/components/loader.ts:43-91

自定义 indicator 若显式传入,其帧会原样渲染;否则 spinner frame 经过颜色函数,消息始终经过消息颜色函数。packages/tui/src/components/loader.ts:59-70 packages/tui/src/components/loader.ts:83-91

CancellableLoader

CancellableLoaderLoader 上增加一个 AbortController、只读 signal/aborted 和可选 onAbortpackages/tui/src/components/cancellable-loader.ts:4-27

它收到 tui.select.cancel 时 abort signal 并调用回调,dispose() 只停止 spinner interval。packages/tui/src/components/cancellable-loader.ts:29-40

Image

Image 接收 base64、MIME、fallback theme 和尺寸限制;没有显式 dimensions 时先解析图片头,失败就使用 800×600 默认值。packages/tui/src/components/image.ts:12-48

渲染时它按当前 cell dimensions 和 width 计算最大尺寸,并缓存生成行。packages/tui/src/components/image.ts:55-68 packages/tui/src/components/image.ts:121-125

Kitty 模式为未分配的图片生成 ID,并返回第一行图形序列加剩余空行;iTerm2 则在最后一行移回去画图以维持 TUI 光标核算。packages/tui/src/components/image.ts:70-111

终端没有图片能力或 renderImage() 返回 null 时,组件用 imageFallback() 文本并经 fallbackColor 渲染。packages/tui/src/components/image.ts:112-119

6. 深入组件一:SelectList 为什么适合补全菜单

SelectList 的条目使用稳定的 value、显示用 label 和可选 description,这正好对应 AutocompleteItem 的三个字段。packages/tui/src/components/select-list.ts:12-16 packages/tui/src/autocomplete.ts:219-223

Editor.createAutocompleteList() 把 provider 返回的 item、autocompleteMaxVisible、编辑器主题内的 selectList theme 交给 SelectListpackages/tui/src/components/editor.ts:2132-2138

组件自己管理 selected index,所以上下移动无需修改编辑器文本或重发 provider 请求。packages/tui/src/components/select-list.ts:40-58 packages/tui/src/components/select-list.ts:112-137

鼠标不在此协议中;键盘的 up/down/confirm/cancel 都经共享 KeybindingsManager,因此用户改键后补全菜单与其他选择器同步。packages/tui/src/components/select-list.ts:112-137 packages/tui/src/keybindings.ts:7-42 packages/tui/src/keybindings.ts:194-200

菜单输出会让选中项尽量居中,超出 maxVisible 时补一个 (current/total) 滚动信息行。packages/tui/src/components/select-list.ts:74-109

补全候选可很长,renderItem() 会先测 prefix 和主列,宽度大于 40 时才给 description 留足至少 10 列的空间。packages/tui/src/components/select-list.ts:139-176

Editor 只在补全激活时将该列表渲染在文本编辑区之后,因此选择 UI 不改变文本的 logical lines。packages/tui/src/components/editor.ts:590-600 packages/tui/src/components/editor.ts:664-723

7. 深入组件二:Markdown 如何在终端保留样式与宽度

Markdown.render(width) 先把 source tabs 统一为三空格,再对 source 做 lexer,并逐 token 调用 renderToken()packages/tui/src/components/markdown.ts:151-187

生成的非图片行会经过 wrapTextWithAnsi(),所以 ANSI 样式与 OSC 8 链接能跨物理行恢复,而图片行不会被文字折行器破坏。packages/tui/src/components/markdown.ts:189-199 packages/tui/src/utils.ts:704-819

最终 padding 时如有背景会调用 applyBackgroundToLine();否则依据 visibleWidth() 补空格到确切 width。packages/tui/src/components/markdown.ts:201-241

inline 渲染在 bold、italic、code、link、strikethrough 后附回当前 style prefix,因此内层 theme reset 不会意外抹掉外层标题或默认样式。packages/tui/src/components/markdown.ts:492-588

引用块不是简单在原文本前加符号;它先以减去 的宽度递归渲染 block token,再折行并补 quote border。packages/tui/src/components/markdown.ts:414-460

这使 Markdown 是一个真正的文本布局组件,而不是把 Markdown 转为字符串后直接 console.logpackages/tui/src/components/markdown.ts:151-242 packages/tui/src/components/markdown.ts:327-490

数据流

下面追踪用户在第一个逻辑行键入 /he,Tab 选择命令后继续输入正文的完整路径。packages/tui/src/components/editor.ts:1123-1154 packages/tui/src/components/editor.ts:2144-2163

stdin 的完整 key sequence
  -> TUI 将输入交给 focused Editor.handleInput(data)
  -> decodePrintableKey() / 原始可打印字符
  -> Editor.insertCharacter()
  -> isAtStartOfMessage() 触发 requestAutocomplete()
  -> provider.getSuggestions(lines, cursorLine, cursorCol, { signal })
  -> CombinedAutocompleteProvider 用 fuzzyFilter() 产生 items + prefix
  -> Editor.createAutocompleteList() -> SelectList
  -> TUI.requestRender() -> Editor.render() 把菜单附到编辑器下方
  -> Tab/Enter -> provider.applyCompletion()
  -> 新 lines/cursor 写回 Editor state,触发 onChange

TUI 只把输入给焦点组件并在 handleInput() 返回后请求重绘,因此 Editor 是这条链中解释按键的边界。packages/tui/src/tui.ts:829-838

Editor.handleInput() 对可打印键调用 decodePrintableKey(),并在无法解码时接收普通可打印数据。packages/tui/src/components/editor.ts:875-890

insertCharacter() 改写当前逻辑行和 cursor,再视 slash 起点或 trigger context 启动/更新补全。packages/tui/src/components/editor.ts:1095-1154

请求传入 AbortSignal,编辑器在每次新请求前撤销旧请求,并用文本/光标快照拒绝过时返回。packages/tui/src/components/editor.ts:2165-2194 packages/tui/src/components/editor.ts:2242-2308

默认 provider 在 / 前缀时把命令转为候选,再让 fuzzyFilter() 过滤 name。packages/tui/src/autocomplete.ts:308-359 packages/tui/src/fuzzy.ts:95-137

编辑器收到候选后创建 SelectList,并在本帧的 render() 将其行追加到编辑器底部。packages/tui/src/components/editor.ts:2310-2320 packages/tui/src/components/editor.ts:590-600

Tab 或确认后,applyCompletion() 返回替换后的 lines 与光标;编辑器保存它们、取消菜单、调用 onChange() 或对 slash completion 继续走提交逻辑。packages/tui/src/components/editor.ts:676-723 packages/tui/src/autocomplete.ts:375-460

自测

  1. 一个扩展编辑器若想取代默认 Editor,哪些成员必须实现,哪些能力可以缺省?packages/tui/src/editor-component.ts:11-74

  2. 为什么大粘贴显示为 [paste #N ...] 仍能在提交时得到原始全文?packages/tui/src/components/editor.ts:1156-1222 packages/tui/src/components/editor.ts:981-1000 packages/tui/src/components/editor.ts:1260-1274

  3. 如何保证一个慢的异步补全请求不会把新文本上的菜单覆盖掉?packages/tui/src/components/editor.ts:2165-2217 packages/tui/src/components/editor.ts:2242-2308

  4. SelectList 的上/下键、确认和取消为何会自动跟随用户的 keybinding 配置?packages/tui/src/components/select-list.ts:112-137 packages/tui/src/keybindings.ts:194-200

  5. Markdown 在折行、链接和背景填充时分别怎样避免 ANSI 样式、OSC 8 与宽字符造成布局错误?packages/tui/src/components/markdown.ts:189-241 packages/tui/src/components/markdown.ts:537-557 packages/tui/src/utils.ts:213-270 packages/tui/src/utils.ts:704-819