TUI 文本布局、换行和文本渲染原语
这一阶段是终端界面的幕后排版工,不管业务,只保证文字画得稳。render 和 line_utils 管边距、空行、加前缀等基础小活;width 算正文还剩多宽。ansi-escape 处理颜色加粗,truncation 负责截短长行。Markdown 合并器保留原文位置,链接模块让可点击链接不扰乱排版。wrapping 和 live_wrap 按真实宽度换行,尽量不断网址;scrollable_diff 让长文本和 diff 能安全滚动。
共享渲染几何
这些小型原语定义通用矩形和行组合辅助工具,供其他文本布局代码构建使用。
tui/src/render/mod.rs源码 ↗
终端界面绘制时,经常要先拿到一块矩形区域,再在里面留出一点空白边,比如弹窗里面要有内边距,菜单边上要空一格。这个文件做的就是这件小但很重要的事。它定义了 Insets,也就是“四条边各留多少空白”:左、上、右、下。它还给 ratatui 的 Rect(一个表示终端屏幕上矩形区域的位置和大小的类型)加了 inset 方法,用来把原来的区域向内缩一圈。这里特别用了“饱和加减”,意思是数值不够减时不会变成奇怪的大数,而是安全地停在 0。可以把它理解成裁纸:原纸太小,最多裁到没有,不会裁出负数尺寸。文件还把 highlight、line_utils、renderable 这些渲染相关子模块挂出来,作为渲染模块的入口之一。
Insets::tlbr16–23 ↗
fn tlbr(top: u16, left: u16, bottom: u16, right: u16) -> Self
作用:按“上、左、下、右”的顺序创建一组边距。适合四条边留白不一样的场景,比如弹窗顶部和底部留的空间不同。
数据流:输入是四个数字:top、left、bottom、right,分别表示上、左、下、右要空出多少格。函数把它们放进一个 Insets 结构里,不做额外计算。输出就是这组明确指定的边距,之后可拿去缩小某个矩形绘制区域。
调用关系:它是很多具体渲染代码的“边距说明书”。像 layout_areas_with_textarea_right_reserve、render_ref、render_popup、as_renderable、from 等地方在需要精确指定四边留白时会调用它;创建好的 Insets 通常会继续交给 Rect::inset 之类的区域计算来真正改出可绘制范围。
调用图:被 15 处调用(layout_areas_with_textarea_right_reserve, render_ref, render_ref, render_popup, render_ref, as_renderable, render_ref, from, render_lines, render_markdown_content (+5 more))。
Insets::vh25–32 ↗
fn vh(v: u16, h: u16) -> Self
作用:用“垂直边距”和“水平边距”快速创建一组边距。适合上下相同、左右相同的常见留白,比如四周统一留一圈空间。
数据流:输入是两个数字:v 表示上下各留多少格,h 表示左右各留多少格。函数把 top 和 bottom 都设成 v,把 left 和 right 都设成 h。输出是一组对称的 Insets,方便后续用于缩小绘制区域。
调用关系:它服务于更常见、更省事的布局写法。多个 render 函数、menu_surface_inset、render_ref 等在只需要统一留白时会调用它;这样调用方不用每次写四个数字,也不容易把左右上下顺序写错。
调用图:被 7 处调用(render, render, render, render, menu_surface_inset, render, render_ref)。
Rect::inset40–49 ↗
fn inset(&self, insets: Insets) -> Rect
作用:把一个矩形区域按给定边距向内缩小,得到真正可用的内部区域。它常用于给边框、弹窗、菜单或文本内容留出空白。
数据流:输入是一块原始 Rect 和一组 Insets。函数先算左右边距总和、上下边距总和;再把矩形的起点向右、向下移动;同时把宽度和高度减掉对应的边距。输出是缩小后的新 Rect,原来的 Rect 不会被改动。计算时使用安全的饱和加减,所以区域太小时也不会出现负宽度、负高度这类危险结果。
调用关系:它是边距真正生效的地方。Insets::tlbr 或 Insets::vh 只是先描述“要留多少空”,而 Rect::inset 把这个描述应用到具体屏幕区域上。各类渲染代码拿到外层区域后,会通过这个方法得到内层区域,再把后续绘制交给文本、菜单、弹窗等组件。
tui/src/render/line_utils.rs源码 ↗
这个文件服务的是终端用户界面,也就是在命令行窗口里显示文字的界面。项目用 ratatui 这个库来表示一行文字,Line 是“一整行”,Span 是“一小段带样式的文字”。这里最重要的问题是:有些 Line 只是借用了别处的文字,不能长期保存;而界面渲染常常需要把它变成自己拥有的内容,像把便签上的内容重新抄到自己的本子里。line_to_static 和 push_owned_lines 就做这件事。另一个常见需求是给一组行加上统一前缀,比如第一行前面放图标,后续行前面放缩进,prefix_lines 负责这个。测试里还需要判断一行是不是只由空格组成,is_blank_line_spaces_only 就提供这个判断。整体看,它不是负责画整个界面,而是给很多显示模块提供可靠的小零件。
line_to_static5–18 ↗
fn line_to_static(line: &Line<'_>) -> Line<'static>
作用:把一行可能借用别人文字的终端文本,复制成完全属于自己的版本。这样这行文字就可以安全地保存、传递,不会因为原来的文字失效而出问题。
数据流:输入是一条 ratatui 的 Line,它里面有整行样式、对齐方式和若干段 Span。函数逐段复制文字内容,把每段文字转成自己拥有的字符串,同时保留原来的样式;最后输出一条新的 Line<'static>,也就是不再依赖外部借用数据的文字行。
调用关系:它是这个文件里“复制成自有内容”的核心小步骤。push_owned_lines 在批量处理多行时会逐行调用它,所以外面的显示代码通常不直接关心复制细节,只把多行交给 push_owned_lines。
调用图:被 1 处调用(push_owned_lines)。
push_owned_lines21–25 ↗
fn push_owned_lines(src: &[Line<'a>], out: &mut Vec<Line<'static>>)
作用:把一批借来的文字行,逐条复制成自有版本,然后追加到一个输出列表里。它适合在拼装界面内容时,把临时生成或借用来的文字安全放进最终显示结果。
数据流:输入是一组源 Line 和一个可修改的输出 Vec。函数遍历源列表,对每一行调用 line_to_static 做完整复制,再把复制后的行 push 进输出列表;它不返回新列表,而是直接改变传入的输出列表,让里面多出这些安全可保存的行。
调用关系:它被很多渲染路径使用,比如命令显示、探索状态显示、转录文本、Markdown 追加、字段渲染和自适应换行等场景。它在这些流程中像一个“搬运工”:上游生成一些行,它负责把这些行变成可长期持有的版本,再交给后续界面拼装。
调用图:调用 1 个内部函数(line_to_static);被 9 处调用(command_display_lines, exploring_display_lines, transcript_lines, user_shell_output_is_limited_by_screen_lines, append_markdown, append_markdown_agent, render_stacked_field, adaptive_wrap_lines, word_wrap_lines)。
is_blank_line_spaces_only30–37 ↗
fn is_blank_line_spaces_only(line: &Line<'_>) -> bool
作用:判断一行是不是空白行,而且这里的空白只认空字符串和普通空格。它主要用于测试或特定判断,避免把制表符、换行符这类字符误当成普通空格。
数据流:输入是一条 Line。函数先看它有没有文字段;没有任何段就算空白。否则它检查每个 Span 的内容:要么为空,要么每个字符都是普通空格。所有段都满足才输出 true;只要有一个非空格字符,就输出 false。
调用关系:它被 commit_complete_lines 使用,用来判断提交完成相关的显示内容里哪些行算真正的空白。因为这个函数带有测试配置标记,它只在测试构建中启用,作用更像是帮测试精确核对渲染结果。
调用图:被 1 处调用(commit_complete_lines)。
prefix_lines41–60 ↗
fn prefix_lines(
lines: Vec<Line<'static>>,
initial_prefix: Span<'static>,
subsequent_prefix: Span<'static>,
) -> Vec<Line<'static>>
作用:给一组文字行的开头加前缀,并且第一行和后面的行可以用不同前缀。常见用途是第一行放项目符号或图标,后续行放对齐用的空白。
数据流:输入是一组已经拥有内容的 Line,以及两个前缀 Span:一个给第一行,一个给后续行。函数按行号遍历,每行新建一个 Span 列表,先放合适的前缀,再接上原来的文字段,并保留原行样式;最后输出一组新的 Line。
调用关系:它被页脚渲染、变更块渲染、命令显示、探索显示、协作事件显示等地方使用。它处在内容生成之后、最终绘制之前,负责把已经准备好的多行文字整理成更好读的版式,就像给多行清单统一加缩进和标记。
调用图:被 7 处调用(render_footer_from_props, render_footer_line, render_changes_block, command_display_lines, exploring_display_lines, user_shell_output_is_limited_by_screen_lines, collab_event)。
宽度与行整形辅助工具
这些工具在更高层的换行逻辑运行前,处理宽度保护、ANSI 到文本转换、行截断和 markdown 文本合并。
tui/src/width.rs源码 ↗
终端里显示文字时,很多行前面会先放固定的东西,比如项目符号、边栏、标签或缩进。问题是,终端窗口可能很窄,这些固定列数一扣,留给正文的空间可能变成 0,甚至根本不够扣。这个文件把这件事统一成一个小规则:先扣掉预留列,如果剩下的宽度大于 0,就返回这个宽度;如果没有可用空间,就返回 None。None 在这里的意思是“别再按正文换行渲染了,只显示前缀之类的兜底内容”。这像是在桌面上先给标签纸留位置,剩下真的还能写字才写;如果没地方了,就不要假装还能写。文件里还提供了 u16 版本,方便直接接终端尺寸使用,并用测试确认窄窗口时不会误返回 0。
usable_content_width22–26 ↗
fn usable_content_width(total_width: usize, reserved_cols: usize) -> Option<usize>
作用:这个函数计算扣掉固定前缀后,正文还剩多少可用宽度。它最重要的规矩是:只要剩下的宽度不是正数,就直接说“没有可用宽度”,而不是返回 0。
数据流:进去的是总宽度 total_width 和要预留的列数 reserved_cols → 它先安全地做减法,避免预留列比总宽度还大时出错,再检查剩下的数是否大于 0 → 出来的是 Some(剩余宽度),或者 None;它不修改外部数据。
调用关系:它是这个文件里的核心小工具。usable_content_width_u16 会把终端常见的 u16 宽度转换后交给它计算,其他渲染代码通过这个规则决定是正常排正文,还是走“只显示前缀”的兜底显示。
调用图:被 1 处调用(usable_content_width_u16)。
usable_content_width_u1632–34 ↗
fn usable_content_width_u16(total_width: u16, reserved_cols: u16) -> Option<usize>
作用:这个函数是给终端尺寸用的便捷版本。很多终端库给出的宽度是 u16,也就是较小范围的无符号整数;它负责转换类型,然后沿用同一套宽度判断规则。
数据流:进去的是 u16 类型的总宽度和预留列数 → 它把两个数转换成 usize,也就是 Rust 里常用来表示大小和长度的整数类型 → 然后调用 usable_content_width,最后返回同样含义的 Some(可用宽度) 或 None;它不改动其他状态。
调用关系:它站在终端渲染代码和核心计算函数之间,像一个转换接头。display_hyperlink_lines 和 lines 这类显示文本行的地方会用它,因为它们拿到的宽度通常来自终端尺寸;真正的判断则交给 usable_content_width。
调用图:调用 1 个内部函数(usable_content_width);被 2 处调用(display_hyperlink_lines, lines);外部调用 1 个(from)。
tests::usable_content_width_returns_none_when_reserved_exhausts_width42–59 ↗
fn usable_content_width_returns_none_when_reserved_exhausts_width()
作用:这个测试确认核心函数在“宽度被吃光”时会返回 None,而不是返回 0 或乱算。这样可以保护很窄终端下的显示行为稳定。
数据流:进去的是几组手写的总宽度和预留列数组合 → 测试调用 usable_content_width,并用 assert_eq! 比较实际结果和预期结果 → 如果结果不符合预期,测试失败;它只验证行为,不改变程序运行状态。
调用关系:它是 usable_content_width 的安全网。开发者改动宽度计算时,这个测试会提醒他们不要破坏“必须剩下正数才算可用”的约定。
调用图:外部调用 1 个(assert_eq!)。
tests::usable_content_width_u16_matches_usize_variant62–71 ↗
fn usable_content_width_u16_matches_usize_variant()
作用:这个测试确认 u16 便捷版本和核心版本遵守同样的规则。也就是说,换了输入数字类型,判断结果不能变。
数据流:进去的是几组 u16 宽度数据 → 测试调用 usable_content_width_u16,并用 assert_eq! 检查它在宽度耗尽和只剩 1 列时的返回值 → 输出是测试通过或失败;它不修改外部数据。
调用关系:它保护 usable_content_width_u16 这个“转换接头”不出错。因为渲染代码会通过这个函数间接使用核心规则,所以这个测试保证转换以后仍然走 usable_content_width 的同一套逻辑。
调用图:外部调用 1 个(assert_eq!)。
ansi-escape/src/lib.rs源码 ↗
很多命令行输出不只是普通文字,里面会夹着 ANSI 转义码,也就是一些看不见的控制符,用来表示“这里变红”“这里加粗”之类的样式。如果直接把这些内容塞进文本界面,可能会显示乱码,或者样式丢失。这个文件就是一个小转换器:它先在单行场景里把制表符 tab 换成空格,避免行号栏和正文挤在一起;然后调用 ansi_to_tui 这个库,把字符串解析成 ratatui 的 Text 或 Line。Text 可以理解成带样式的多行文字,Line 是其中一行。这里还有一个重要约定:ansi_escape_line 只适合本来就应该是一行的内容,如果解析出来多行,它会记一条警告,只取第一行,避免界面被意外撑乱。
expand_tabs11–21 ↗
fn expand_tabs(s: &str) -> std::borrow::Cow<'_, str>
作用:这个函数把字符串里的 tab 制表符换成 4 个空格,主要是为了让终端界面里的对齐更稳定。没有 tab 时,它尽量不复制字符串,省一点开销。
数据流:进去的是一段字符串引用。它先检查里面有没有 tab:如果有,就新建一份字符串,把每个 tab 替换成 4 个空格后返回;如果没有,就直接借用原来的字符串返回,不做多余加工。出来的是一段“可能是原文、也可能是新文本”的结果。
调用关系:它是 ansi_escape_line 的前置清理步骤。ansi_escape_line 在把单行内容交给 ANSI 解析器之前,会先用它把 tab 处理掉,避免后面的显示区域出现奇怪的空隙或重叠。
调用图:被 1 处调用(ansi_escape_line);外部调用 2 个(Borrowed, Owned)。
ansi_escape_line26–38 ↗
fn ansi_escape_line(s: &str) -> Line<'static>
作用:这个函数用于处理“应该只有一行”的带样式字符串,并把它变成 ratatui 可以显示的一行富文本。如果意外出现多行,它会提醒开发者,并只拿第一行来显示。
数据流:进去的是一段字符串。它先调用 expand_tabs,把 tab 换成空格;再调用 ansi_escape,把 ANSI 颜色和样式解析成 Text;最后查看解析结果有几行:没有内容就返回空行,刚好一行就返回这一行,多于一行就记录警告并返回第一行。
调用关系:它是外部代码在需要“单行显示”时会用的入口,比如列表项、标题、状态行这类地方。它把清理 tab 的活交给 expand_tabs,把真正解析 ANSI 的活交给 ansi_escape;如果发现输入不符合“单行”的约定,就通过 warn! 记一条日志。
调用图:调用 2 个内部函数(ansi_escape, expand_tabs);外部调用 1 个(warn!)。
ansi_escape40–58 ↗
fn ansi_escape(s: &str) -> Text<'static>
作用:这个函数把可能带有 ANSI 转义码的字符串,转换成 ratatui 的 Text,也就是界面能直接渲染的带样式多行文字。
数据流:进去的是一段字符串。它调用 ansi_to_tui 提供的转换能力,把里面的颜色、加粗等控制信息解析出来,变成 Text。正常时输出这份 Text;如果解析库报告 NomError 或 Utf8Error,它会写错误日志并直接 panic,也就是让程序停止,因为这些错误在这里被认为是不该发生的严重问题。
调用关系:它是这个文件里真正做 ANSI 解析的核心函数。ansi_escape_line 会调用它来得到完整的富文本结果,然后再挑出其中一行。它自己不再把工作交给项目内的其他函数,而是依赖外部库完成解析,并在异常时用 error! 记录问题、用 panic! 中止执行。
调用图:被 1 处调用(ansi_escape_line);外部调用 2 个(panic!, error!)。
tui/src/line_truncation.rs源码 ↗
终端里的文字宽度不只是“有几个字”这么简单。英文通常占 1 格,中文、日文等字符常常占 2 格,还有些符号可能不占格。如果直接按字节或字符数量裁剪,界面就可能对不齐,甚至把一个多字节字符切坏。这个文件用 unicode_width 这个库来计算“屏幕上真正占几格”。它处理的是 ratatui 的 Line,也就是终端 UI 里一整行带样式的文字;Line 里面又分成多个 Span,可以理解成一段段颜色、样式不同的小文字。line_width 先算整行宽度;truncate_line_to_width 会保留原来的样式和对齐方式,把行裁到指定宽度;truncate_line_with_ellipsis_if_overflow 则在文字超出时先裁短,再补一个省略号“…”提示读者后面还有内容。它很适合列表、页脚、详情行这些空间有限的 UI 区域。
line_width6–10 ↗
fn line_width(line: &Line<'_>) -> usize
作用:计算一整行带样式文字在终端屏幕上实际会占多少格。有人需要判断文字会不会超出可显示区域时,就会先用它量一下。
数据流:输入是一条 Line,里面有多个 Span,也就是多段可能带不同样式的文字。它逐段读取文字内容,用 Unicode 宽度规则算出每段占几格,再把这些宽度加起来。输出是一个数字,表示这整行在终端里占用的显示宽度;它不会改动原来的文字。
调用关系:它是截断前的“尺子”。truncate_line_with_ellipsis_if_overflow 会先调用它看看整行是否已经放得下;如果放得下,就不用做任何裁剪,直接保留原行。
调用图:被 1 处调用(truncate_line_with_ellipsis_if_overflow);外部调用 1 个(iter)。
truncate_line_to_width12–67 ↗
fn truncate_line_to_width(line: Line<'static>, max_width: usize) -> Line<'static>
作用:把一条带样式的终端文字行裁剪到指定宽度以内,同时尽量保留原来的样式、对齐方式和已有分段。它用来防止太长的文字把界面撑出边界。
数据流:输入是一条拥有所有权的 Line 和一个最大宽度 max_width。它先处理最大宽度为 0 的特殊情况,直接返回空行;然后从左到右检查每个 Span。如果整段放得下,就原样放进结果;如果某段只放得下一部分,就按字符逐个计算屏幕宽度,找到刚好不超出的切断点,再用原来的样式生成一段新文字。输出是一条新的 Line,样式和对齐设置保留,但文字内容被裁到指定宽度内。
调用关系:它是真正动刀裁剪文字的函数。truncate_line_with_ellipsis_if_overflow 在发现文字超宽后,会把最大宽度先留出 1 格给省略号,然后调用它裁出前半段内容。
调用图:被 1 处调用(truncate_line_with_ellipsis_if_overflow);外部调用 6 个(from, styled, width, width, new, with_capacity)。
truncate_line_with_ellipsis_if_overflow75–100 ↗
fn truncate_line_with_ellipsis_if_overflow(
line: Line<'static>,
max_width: usize,
) -> Line<'static>
作用:在一行文字太长时,把它截短并在末尾加上省略号“…”;如果本来就放得下,就完全不改。它让终端界面既不溢出,又能告诉用户“后面还有内容”。
数据流:输入是一条 Line 和最大可显示宽度。它先处理宽度为 0 的情况,返回空行;接着调用 line_width 量出整行真实宽度。如果没超出,就原样输出。若超出,它调用 truncate_line_to_width,把内容裁到比最大宽度少 1 格的位置,再取最后一段文字的样式给省略号使用,最后把“…”追加到末尾。输出是一条适合显示的新 Line;原内容可能被缩短,并多了省略号提示。
调用关系:它是外部界面代码最常用的入口。render_with_mask_and_textarea_right_reserve、detail_wrapped_lines、render_footer、build_line、render、render_rows_single_line_with_col_width_mode、render_items 等渲染流程会在准备显示短行、列表行或页脚时调用它。它自己把测量工作交给 line_width,把实际裁剪工作交给 truncate_line_to_width。
调用图:调用 2 个内部函数(line_width, truncate_line_to_width);被 8 处调用(render_with_mask_and_textarea_right_reserve, detail_wrapped_lines, render_footer, build_line, render, render_rows_single_line_with_col_width_mode, render_items, render);外部调用 3 个(from, styled, new)。
tui/src/markdown_text_merge.rs源码 ↗
Markdown 解析器有时会把看起来连续的一句话拆成几个“文字事件”。比如因为某些 Markdown 分隔符或扩展规则,肉眼看到是一段字,程序却收到好几小块。如果后面的界面渲染要识别跨片段的内容,就会出错。这个文件提供了一个小包装器 DecodedTextMerge,像在流水线中间加了一个“拼接工”。它从原始事件流里一个个取事件;如果遇到普通文字,就继续偷看后面是不是还是文字,是的话就把内容接起来,并把来源范围的结尾更新到最后一块。不是文字的事件,比如链接、段落开始结束等,则原样放过去。重要的是,它拼的是解析器已经解码好的文字,而不是回头从 Markdown 原文里重新切,这样可以避免转义字符、实体字符等内容被错误还原。
DecodedTextMerge::new18–22 ↗
fn new(iter: I) -> Self
作用:创建一个“会合并相邻文字”的迭代器包装器。调用者把原来的 Markdown 事件流交给它,之后再从它这里取事件,就能自动得到合并后的文字事件。
数据流:进去的是一个普通迭代器,也就是一串 Markdown 解析事件和它们在原文中的位置范围。函数把它变成可以“偷看下一个元素”的 Peekable(一种能预先看下一项但不取走的迭代器),再放进 DecodedTextMerge 里。出来的是一个新的包装器,原始数据还没被消费,只是准备好了后续合并所需的查看能力。
调用关系:render_markdown_lines_with_width_and_cwd 在准备渲染 Markdown 行时会调用它,把解析器产出的事件流套上一层。它自己只做初始化,真正的合并工作交给 DecodedTextMerge::next 在后续取数据时完成。
调用图:被 1 处调用(render_markdown_lines_with_width_and_cwd);外部调用 1 个(peekable)。
DecodedTextMerge::next31–49 ↗
fn next(&mut self) -> Option<Self::Item>
作用:从事件流里取出下一个事件;如果当前和后面连续几个都是文字,就把它们拼成一个文字事件返回。这样下游看到的是更接近人眼阅读的一整段文字。
数据流:进去的是包装器内部剩下的事件流。它先取出第一项;如果不是文字事件,就直接原样返回。如果是文字事件,它会偷看后面是否还是文字:不是的话也直接返回;是的话就不断取出后续文字,把文字内容追加到一起,并把来源范围的结束位置延长到最后一个文字片段。出来的是一个事件和范围:可能是原样事件,也可能是合并后的 Event::Text,同时内部迭代器会前进到这些已处理事件之后。
调用关系:它是这个包装器被当作迭代器使用时的核心动作。渲染流程每次需要下一个 Markdown 事件时,就会间接触发它;它会调用底层迭代器的 next 来取数据,用 peek 能力判断后面是否还能继续合并,最后把整理好的事件交回给渲染代码。
换行与保留超链接的布局
这条核心布局路径在文本换行时保留语义超链接边界,并保留下游渲染可重建的范围。
tui/src/terminal_hyperlinks.rs源码 ↗
终端里的超链接靠 OSC 8(一种终端控制码,意思是“从这里开始/结束一个链接”)实现。麻烦在于,这些控制码本身不显示,却会混进字符串里;如果太早加入,界面测宽、换行、对齐都会算错。这个文件的做法像给书页贴透明便签:正文还是普通文字,另有一张表记录第几列到第几列是哪个网址。等文字真正写进终端缓冲区或历史记录时,才把 OSC 8 包到对应字符外面。它还能自动识别 http/https 网址,处理括号和标点,换行后重新把链接范围对准新行,并且只接受安全的网址,避免把奇怪控制字符写进终端。
HyperlinkLine::new39–44 ↗
fn new(line: Line<'static>) -> Self
作用:创建一行“可能带链接的文字”。刚创建时只有可见文字,还没有任何链接标注。
数据流:输入一行普通的终端文字 → 把它放进 HyperlinkLine 里,并准备一个空的链接列表 → 输出一个可继续添加链接信息的行对象。
调用关系:很多地方在生成聊天记录、显示行、历史行或空行时都会先用它打底;后续函数再往里面加文字、链接或样式。
调用图:被 23 处调用(display_lines_for_history_insert, render_transcript_lines_for_reflow, display_hyperlink_lines, ensure_line, hard_break, pop_link, push_blank_line, push_line, push_text_spans, push_text_spans_to_table_cell (+13 more));外部调用 1 个(new)。
HyperlinkLine::width46–48 ↗
fn width(&self) -> usize
作用:计算这一行在终端里实际占多少列。这里算的是用户看得见的宽度,不包括隐藏的链接控制码。
数据流:读取这一行的可见文字 → 按终端字符宽度规则计算列数 → 返回一个数字,表示显示宽度。
调用关系:写历史行和追加片段时会用它来知道新内容应该从哪一列开始,避免链接范围贴错位置。
调用图:被 2 处调用(write_history_line, push_span);外部调用 1 个(width)。
HyperlinkLine::push_span50–62 ↗
fn push_span(&mut self, span: Span<'static>, destination: Option<&str>)
作用:往这一行后面追加一段带样式的文字;如果同时给了网址,就把这段文字标成可点击链接。
数据流:输入一个文字片段和可选网址 → 先算追加前后的列范围,再把文字放进行里;如果网址是合法网页地址,就记录这段列范围对应的网址 → 行对象被原地更新。
调用关系:它依赖 HyperlinkLine::width 找追加位置,也会调用网址检查逻辑,保证只有 http/https 这类网页链接会被记录。
HyperlinkLine::style64–67 ↗
fn style(mut self, style: ratatui::style::Style) -> Self
作用:给整行文字套上统一样式,比如颜色或加粗。链接标注不变。
数据流:输入一个已有的 HyperlinkLine 和一个样式 → 把样式应用到里面的可见文字行 → 返回套好样式的新 HyperlinkLine。
调用关系:预先换好行的内容进入显示流程时会用它,让文字外观变化,但链接位置继续保留。
调用图:被 1 处调用(push_prewrapped_line);外部调用 1 个(style)。
HyperlinkLine::from83–85 ↗
fn from(text: String) -> Self
作用:把普通字符串方便地转成 HyperlinkLine。它是一个快捷入口,让调用者不用手动写 new。
数据流:输入一个字符串 → 先变成终端可显示的一行文字 → 再包装成没有链接的 HyperlinkLine → 输出包装后的行。
调用关系:转录记录、活动单元格内容和显示行生成时会用这个转换,作为进入“可带链接行”体系的入口。
调用图:被 3 处调用(active_cell_transcript_hyperlink_lines, display_hyperlink_lines, transcript_hyperlink_lines);外部调用 2 个(from, new)。
visible_lines88–90 ↗
fn visible_lines(lines: Vec<HyperlinkLine>) -> Vec<Line<'static>>
作用:把带链接的行还原成普通可见文字行。适合只关心排版或高度、不需要点击链接的时候用。
数据流:输入一组 HyperlinkLine → 丢掉每行的链接表,只取里面的可见文字 → 输出普通 Line 列表。
调用关系:渲染高度计算、模式显示、Markdown 渲染等地方会用它,因为那些步骤只需要知道屏幕上显示哪些字。
调用图:被 9 处调用(render, desired_transcript_height, display_lines_for_mode, render_markdown_text_with_width_and_cwd, render, desired_height, render, controller_live_view_matches_render_during_interleaved_table_streaming, hyperlink_lines_to_plain_strings)。
plain_hyperlink_lines92–94 ↗
fn plain_hyperlink_lines(lines: Vec<Line<'static>>) -> Vec<HyperlinkLine>
作用:把一组普通文字行包装成“不带链接的可链接行”。这是让老的普通文本进入新链接系统的适配器。
数据流:输入普通 Line 列表 → 每一行都包成 HyperlinkLine,链接列表为空 → 输出 HyperlinkLine 列表。
调用关系:显示、历史插入、源码渲染和换行重映射都会用它,作为统一数据格式的桥梁。
调用图:被 8 处调用(display_hyperlink_lines, display_hyperlink_lines_for_mode, transcript_hyperlink_lines, insert_history_lines_with_mode_and_wrap_policy, display_hyperlink_lines, render_source, remap_wrapped_line, insert_history_lines_with_wrap_policy)。
prefix_hyperlink_lines96–121 ↗
fn prefix_hyperlink_lines(
lines: Vec<HyperlinkLine>,
initial_prefix: Span<'static>,
subsequent_prefix: Span<'static>,
) -> Vec<HyperlinkLine>
作用:给多行文字加前缀,比如第一行一个前缀、后续行另一个前缀,同时把链接位置整体往右挪。
数据流:输入多行带链接文字、首行前缀和续行前缀 → 给每行开头插入对应前缀 → 按前缀宽度平移原有链接列范围 → 输出加好前缀且链接仍对齐的新行列表。
调用关系:显示行渲染时会用它;它解决的是“加了项目符号或缩进后,原链接不能还停在旧列上”的问题。
调用图:被 1 处调用(render_display_lines)。
adaptive_wrap_hyperlink_lines123–145 ↗
fn adaptive_wrap_hyperlink_lines(
lines: &[HyperlinkLine],
options: RtOptions<'static>,
) -> Vec<HyperlinkLine>
作用:按终端宽度把带链接的行智能换行,并让链接跟着新行正确移动。
数据流:输入带链接行和换行选项 → 每行先按普通文字规则换行 → 再用 remap_wrapped_line 把原来的链接范围映射到换出来的新行 → 输出换行后的 HyperlinkLine 列表。
调用关系:显示内容时会调用它;它把真正的文字换行工作交给 adaptive_wrap_line,再负责保住链接不丢。
调用图:调用 2 个内部函数(remap_wrapped_line, adaptive_wrap_line);被 1 处调用(display_hyperlink_lines);外部调用 3 个(new, iter, clone)。
annotate_web_urls147–149 ↗
fn annotate_web_urls(lines: Vec<Line<'static>>) -> Vec<HyperlinkLine>
作用:扫描一批普通文字行,把里面看起来像网页的网址标成可点击链接。
数据流:输入普通 Line 列表 → 逐行交给 annotate_web_urls_in_line 分析 → 输出带有链接标注的 HyperlinkLine 列表。
调用关系:多个显示流程会在准备展示文本时使用它,让裸露的 http/https 地址自动变成终端可点击链接。
调用图:被 3 处调用(display_hyperlink_lines, display_hyperlink_lines, display_hyperlink_lines)。
annotate_web_urls_in_line151–160 ↗
fn annotate_web_urls_in_line(line: Line<'static>) -> HyperlinkLine
作用:扫描单独一行文字,找出里面的网页链接并记录它们占用的列范围。
数据流:输入一行带样式的文字 → 把所有片段拼成纯文本用于查找网址 → 用 web_links_in_text 找链接范围 → 输出原文字不变、但附带链接表的 HyperlinkLine。
调用关系:它被自动标注流程和一些文本片段推入流程调用,是“普通文本变可点击文本”的核心小步骤。
调用图:调用 2 个内部函数(new, web_links_in_text);被 3 处调用(writes_semantic_web_link_without_changing_visible_text, push_text_spans, push_text_spans_to_table_cell)。
remap_wrapped_line167–209 ↗
fn remap_wrapped_line(
source: &HyperlinkLine,
wrapped: Vec<Line<'static>>,
) -> Vec<HyperlinkLine>
作用:一行文字被换成多行后,重新计算每个新行里哪些列还是原来的链接。
数据流:输入原始带链接行和换行后的普通行 → 按显示顺序把新行文字和原始文字对上,忽略行首边界处被换行处理掉的空白 → 对每个字符查它在原行是否属于链接 → 输出带有新链接范围的换行结果。
调用关系:自动换行、表格单元格换行、历史插入和缓冲区标记都会用它;它调用 line_text、longest_suffix_matching_prefix 和 push_link_range 完成对齐与合并。
调用图:调用 4 个内部函数(line_text, longest_suffix_matching_prefix, plain_hyperlink_lines, push_link_range);被 7 处调用(insert_history_hyperlink_lines_with_mode_and_wrap_policy, flush_current_line, wrap_cell, wrap_cell, adaptive_wrap_hyperlink_lines, mark_buffer_hyperlinks, wrapping_maps_repeated_link_labels_by_source_position)。
line_text211–216 ↗
fn line_text(line: &Line<'_>) -> String
作用:把一行里多个样式片段拼成一段纯文字。它只取文字内容,不管颜色样式。
数据流:输入一个 Line → 依次读取每个 Span 的文本 → 拼接成 String 输出。
调用关系:remap_wrapped_line 用它取得原行和换行后行的可见文字,才能比较两边字符位置。
调用图:被 1 处调用(remap_wrapped_line)。
longest_suffix_matching_prefix218–224 ↗
fn longest_suffix_matching_prefix(rendered: &str, source: &str) -> Option<usize>
作用:在渲染出来的一行里,找到哪一段后缀能和原始剩余文字的开头对上。
数据流:输入渲染行文字和源文字剩余部分 → 从渲染行的不同起点尝试匹配 → 返回第一个能对上的字节位置;找不到就返回空。
调用关系:remap_wrapped_line 用它处理换行、缩进或边界空白带来的偏差,让链接位置尽量重新贴回正确文字。
调用图:被 1 处调用(remap_wrapped_line);外部调用 1 个(once)。
push_link_range226–241 ↗
fn push_link_range(line: &mut HyperlinkLine, range: Range<usize>, destination: &str)
作用:往一行里添加一个链接范围,并且如果它和上一个链接正好连着、目标也一样,就合并成一个范围。
数据流:输入一行、列范围和网址 → 空范围直接忽略;能和最后一个链接合并就延长它,否则新增一条链接记录 → 修改这行的链接列表。
调用关系:remap_wrapped_line 在逐字符重建链接时会频繁调用它,用合并避免链接表变成很多碎片。
调用图:被 1 处调用(remap_wrapped_line);外部调用 1 个(is_empty)。
web_links_in_text243–271 ↗
fn web_links_in_text(text: &str) -> Vec<TerminalHyperlink>
作用:从一段纯文本里找出合法网页链接,并算出它们在终端列上的位置。
数据流:输入纯文本 → 按空白分词,去掉网址前后的普通标点,检查是否是 http/https 且有主机名的网址 → 输出 TerminalHyperlink 列表,里面有列范围和目标地址。
调用关系:annotate_web_urls_in_line 调用它完成自动识别网址;它会借助 trailing_url_end 和 web_destination 排除不该算进链接的字符和不安全地址。
调用图:调用 2 个内部函数(trailing_url_end, web_destination);被 1 处调用(annotate_web_urls_in_line);外部调用 1 个(new)。
is_leading_punctuation273–278 ↗
fn is_leading_punctuation(ch: char) -> bool
作用:判断一个字符是不是网址前面常见的包围符号或标点,比如括号、逗号、引号。
数据流:输入一个字符 → 和一组预设标点比较 → 返回是或否。
调用关系:web_links_in_text 用它剥掉网址前面的标点,避免把“(https://...”里的左括号也算成链接。
调用图:外部调用 1 个(matches!)。
trailing_url_end280–296 ↗
fn trailing_url_end(candidate: &str) -> usize
作用:找出网址候选文本真正应该结束的位置,去掉末尾不属于网址的标点。
数据流:输入一个候选字符串 → 从末尾往前看,删掉逗号、句号、引号等;遇到右括号这类符号时,会检查它是不是多出来的 → 返回有效结尾的字节位置。
调用关系:web_links_in_text 用它清理网址尾巴;它会调用 has_unmatched_closing_delimiter 来避免误删网址内部成对的括号。
调用图:调用 1 个内部函数(has_unmatched_closing_delimiter);被 1 处调用(web_links_in_text);外部调用 1 个(matches!)。
has_unmatched_closing_delimiter298–308 ↗
fn has_unmatched_closing_delimiter(candidate: &str, closing: char) -> bool
作用:判断某种右括号是不是比左括号多,也就是这个右括号很可能只是句子标点,不是网址内容。
数据流:输入候选文本和一个右括号字符 → 找到对应左括号 → 统计左右括号数量 → 如果右括号更多就返回 true。
调用关系:trailing_url_end 调用它来决定末尾的 )、]、}、> 要不要从网址里剔除。
调用图:被 1 处调用(trailing_url_end)。
web_destination310–320 ↗
fn web_destination(destination: &str) -> Option<String>
作用:检查一个链接目标能不能安全地作为终端网页链接使用。这里只接受 http 和 https,并且必须有域名。
数据流:输入网址字符串 → 移除控制字符,尝试按 URL 解析 → 确认协议是 http/https 且有主机名 → 成功则返回清理后的网址,否则返回空。
调用关系:自动识别、OSC 8 包装和缓冲区标记都会调用它;它是防线,避免 mailto、文件路径或带控制字符的内容进入终端链接。
调用图:被 4 处调用(pop_link, mark_matching_cells, osc8_hyperlink, web_links_in_text);外部调用 2 个(parse, matches!)。
osc8_hyperlink322–327 ↗
fn osc8_hyperlink(destination: &str, text: &str) -> String
作用:把一段可见文字包成终端可点击链接。终端支持时,用户点击这段文字会打开对应网页。
数据流:输入目标网址和显示文字 → 先用 web_destination 校验网址 → 合法就生成带 OSC 8 开始/结束控制码的字符串;不合法就原样返回显示文字。
调用关系:mark_buffer_hyperlinks 和 mark_matching_cells 在真正写终端缓冲区时调用它,把已经确定的字符变成可点击字符。
调用图:调用 1 个内部函数(web_destination);被 2 处调用(mark_buffer_hyperlinks, mark_matching_cells);外部调用 1 个(format!)。
strip_osc8330–360 ↗
fn strip_osc8(text: &str) -> String
作用:测试用:从字符串里去掉 OSC 8 超链接控制码,只留下用户能看见的文字。
数据流:输入可能带 OSC 8 控制码的字符串 → 逐字节扫描,遇到链接开始/结束控制片段就跳过 → 输出干净的可见文本。
调用关系:只在测试里使用,用来确认加链接不会改变屏幕上实际显示的文字。
调用图:外部调用 1 个(with_capacity)。
decorate_spans362–409 ↗
fn decorate_spans(line: &HyperlinkLine) -> Vec<Span<'static>>
作用:给一行的文字片段插入 OSC 8 控制码,通常用于写入历史记录或滚动回放文本。
数据流:输入带链接标注的 HyperlinkLine → 按字符和列位置走一遍,进入链接范围时插入“开始链接”,离开时插入“结束链接” → 输出新的 Span 列表,样式尽量保持原样。
调用关系:写历史行时会调用它;它把低层拼接工作交给 push_styled_content 和 append_to_last_span。
调用图:调用 2 个内部函数(append_to_last_span, push_styled_content);被 1 处调用(write_history_line);外部调用 2 个(new, format!)。
push_styled_content411–419 ↗
fn push_styled_content(out: &mut Vec<Span<'static>>, content: &str, style: ratatui::style::Style)
作用:把一小段文字追加到输出片段里,并尽量和前一个同样样式的片段合并。
数据流:输入输出列表、文字和样式 → 如果最后一个片段样式相同,就把文字接到后面;否则新建一个带样式片段 → 更新输出列表。
调用关系:decorate_spans 用它避免生成太多零碎 Span,让加了链接控制码后的文本仍然保持合理结构。
调用图:被 1 处调用(decorate_spans);外部调用 1 个(styled)。
append_to_last_span421–425 ↗
fn append_to_last_span(out: &mut [Span<'static>], content: &str)
作用:把一段文字直接接到最后一个输出片段后面。这里常用来补上“结束链接”的控制码。
数据流:输入 Span 列表和要追加的内容 → 如果列表不空,就改动最后一个 Span 的文本 → 没有返回新对象,只原地修改。
调用关系:decorate_spans 在链接结束时调用它,把关闭 OSC 8 的控制码贴到最近的片段末尾。
调用图:被 1 处调用(decorate_spans);外部调用 1 个(last_mut)。
mark_buffer_hyperlinks427–484 ↗
fn mark_buffer_hyperlinks(
buf: &mut Buffer,
area: Rect,
lines: &[HyperlinkLine],
scroll_rows: usize,
)
作用:在已经渲染好的终端缓冲区里,把对应字符改成可点击链接,同时尊重自动换行和滚动位置。
数据流:输入终端缓冲区、显示区域、带链接行和滚动掉的行数 → 先用同样宽度模拟段落换行 → 把原链接范围映射到换行后的行 → 找到屏幕上的对应单元格,把单元格文字包上 OSC 8 链接 → 原缓冲区被更新。
调用关系:主渲染流程会调用它;它依赖 remap_wrapped_line 保证换行后链接仍贴着正确字符,再用 osc8_hyperlink 写入终端控制码。
调用图:调用 2 个内部函数(osc8_hyperlink, remap_wrapped_line);被 4 处调用(render, render, render, buffer_hyperlinks_follow_word_wrapping);外部调用 6 个(empty, new, new, from, try_from, from)。
mark_url_hyperlink486–490 ↗
fn mark_url_hyperlink(buf: &mut Buffer, area: Rect, destination: &str)
作用:把某个区域里看起来像“网址样式”的文字标成同一个链接。这里的网址样式指青色并带下划线。
数据流:输入缓冲区、区域和目标网址 → 设定匹配条件:青色且下划线 → 交给 mark_matching_cells 遍历并包装匹配单元格 → 缓冲区中符合条件的字符变成可点击。
调用关系:渲染流程中遇到已经用颜色和下划线表现的网址时会调用它;实际扫描和安全校验交给 mark_matching_cells。
调用图:调用 1 个内部函数(mark_matching_cells);被 3 处调用(render, render, mark_url_hyperlink)。
mark_underlined_hyperlink492–496 ↗
fn mark_underlined_hyperlink(buf: &mut Buffer, area: Rect, destination: &str)
作用:把某个区域里所有带下划线的非空文字标成同一个链接。
数据流:输入缓冲区、区域和目标网址 → 设定匹配条件:带下划线 → 调用 mark_matching_cells 给符合条件的单元格包 OSC 8 → 修改缓冲区。
调用关系:引用或特定渲染区域需要整块下划线文字可点击时会用它;它复用通用的 mark_matching_cells。
调用图:调用 1 个内部函数(mark_matching_cells);被 2 处调用(mark_underlined_hyperlink, render_ref)。
mark_matching_cells498–514 ↗
fn mark_matching_cells(
buf: &mut Buffer,
area: Rect,
destination: &str,
matches: impl Fn(&ratatui::buffer::Cell) -> bool,
)
作用:遍历一个屏幕区域,把满足条件的单元格文字包成同一个安全网页链接。
数据流:输入缓冲区、区域、网址和一个匹配规则 → 先检查网址是否合法;再逐个看区域内单元格,跳过空白或被跳过的格子;符合规则就用 osc8_hyperlink 包住符号 → 缓冲区被原地改写。
调用关系:mark_url_hyperlink 和 mark_underlined_hyperlink 都把具体筛选规则交给它;它负责统一的安全检查、遍历和写入。
调用图:调用 2 个内部函数(osc8_hyperlink, web_destination);被 2 处调用(mark_underlined_hyperlink, mark_url_hyperlink);外部调用 1 个(positions)。
tests::only_web_destinations_receive_osc8522–533 ↗
fn only_web_destinations_receive_osc8()
作用:测试只有真正的网页链接会被加 OSC 8,非网页链接不会被偷偷变成可点击。
数据流:输入几种网址样例 → 调用 osc8_hyperlink 和 strip_osc8 检查结果 → 断言网页链接含控制码、mailto 不含控制码、控制字符会被清理、可见文字不变。
调用关系:它保护 web_destination 和 osc8_hyperlink 的安全边界,防止以后改代码时放宽了不该放宽的链接类型。
调用图:外部调用 2 个(assert!, assert_eq!)。
tests::discovers_punctuated_web_url_columns536–544 ↗
fn discovers_punctuated_web_url_columns()
作用:测试带括号和句号的句子里,网址范围能被正确识别。
数据流:输入“See (网址).”这样的文本 → 调用 web_links_in_text → 检查得到的列范围不包含括号和句号,只覆盖网址本身。
调用关系:它验证 web_links_in_text、is_leading_punctuation 和 trailing_url_end 配合正常。
调用图:外部调用 1 个(assert_eq!)。
tests::preserves_balanced_parentheses_in_bare_web_urls547–556 ↗
fn preserves_balanced_parentheses_in_bare_web_urls()
作用:测试网址内部成对的括号不会被误删,尤其是维基百科这类链接常见这种情况。
数据流:输入一个路径里带括号的网址,并把它放在句子括号中 → 调用 web_links_in_text → 断言链接范围保留网址内部括号,只去掉句子外层标点。
调用关系:它主要保护 trailing_url_end 和 has_unmatched_closing_delimiter 的判断逻辑。
调用图:外部调用 1 个(assert_eq!)。
tests::decorates_a_contiguous_web_link_with_one_osc8_pair559–577 ↗
fn decorates_a_contiguous_web_link_with_one_osc8_pair()
作用:测试一整段连续链接只会被包一对 OSC 8 控制码,而不是每个字符都包一次。
数据流:构造一行完整网址并标注整段为链接 → 调用 decorate_spans → 检查输出等于一次性包装后的文本;再检查无链接行会原样输出。
调用关系:它保护 decorate_spans、push_styled_content 和 append_to_last_span 的合并行为。
调用图:外部调用 3 个(from, assert_eq!, vec!)。
tests::wrapping_maps_repeated_link_labels_by_source_position580–596 ↗
fn wrapping_maps_repeated_link_labels_by_source_position()
作用:测试同样文字重复出现时,链接会按原始位置匹配,而不是随便贴到第一个相同词上。
数据流:构造“here here”,只把第二个 here 标成链接 → 调用 remap_wrapped_line → 断言输出仍然只标第二个 here。
调用关系:它保护 remap_wrapped_line 的位置映射逻辑,避免重复文本场景下链接错位。
调用图:调用 2 个内部函数(new, remap_wrapped_line);外部调用 3 个(from, assert_eq!, vec!)。
tests::buffer_hyperlinks_follow_word_wrapping599–626 ↗
fn buffer_hyperlinks_follow_word_wrapping()
作用:测试文字在窄区域里自动换行后,缓冲区里的链接仍能覆盖完整网址。
数据流:构造一行包含长网址的文本和较窄显示区域 → 先用 Paragraph 渲染换行,再调用 mark_buffer_hyperlinks → 收集带 OSC 8 的单元格并去掉控制码 → 断言拼回来的正好是完整网址。
调用关系:它把渲染、remap_wrapped_line、mark_buffer_hyperlinks 和 osc8_hyperlink 串起来测,保证真实显示场景不丢链接。
调用图:调用 2 个内部函数(new, mark_buffer_hyperlinks);外部调用 7 个(empty, from, new, new, from, assert_eq!, format!)。
tui/src/wrapping.rs源码 ↗
终端窗口宽度有限,一长段文字必须折成多行。普通换行库会把 /、- 当成可断开的地方,这对网址很糟糕,比如 https://.../a-b 被拆开后,很多终端就不能把它当成一个链接。这个文件像一个“聪明排版员”:先把带样式的 Line 摊平成纯文字,算出每一行该截哪一段,再把原来的颜色、加粗等样式贴回去。它有两条路:确定是普通话的文字走标准换行;可能含网址的文字先做粗略识别,发现网址就尽量整块保留。若一行里既有网址又有普通话,它会只保护网址,普通词仍正常换行,太长的非网址才拆开。文件里还包含很多测试,保证缩进、emoji 宽度、样式、网址识别和边界情况都不会出错。
wrap_ranges42–80 ↗
fn wrap_ranges(text: &str, width_or_options: O) -> Vec<Range<usize>>
作用:把一段纯文字按宽度换行,但返回的不是文字本身,而是每行在原文里的字节范围。它还保留行尾空格,并多给一个“哨兵”位置,方便文本框光标定位。
数据流:输入原文和宽度或换行选项 → 调用换行库得到每个折行片段 → 尝试把片段对应回原文位置,补上尾随空格和额外位置 → 输出一组原文范围,并推进内部游标。
调用关系:它是给需要知道“屏幕上的行对应原文哪里”的代码用的。过程中先找借用片段的真实位置,找不到时交给 map_owned_wrapped_line_to_range 兜底。测试会专门检查带缩进时它能不能还原原文。
调用图:调用 2 个内部函数(borrowed_slice_range, map_owned_wrapped_line_to_range);被 3 处调用(wrapped_lines, wrap_ranges_indent_prefix_coincides_with_source_char, wrap_ranges_recovers_with_non_space_indents);外部调用 3 个(into, new, wrap)。
wrap_ranges_trim85–119 ↗
fn wrap_ranges_trim(text: &str, width_or_options: O) -> Vec<Range<usize>>
作用:和 wrap_ranges 类似,也是算每个折行对应原文哪里,但不保留行尾空格,也不加额外哨兵。它适合普通显示排版。
数据流:输入原文和换行设置 → 调用换行库 → 把每个结果片段映射回原文范围 → 输出干净的范围列表,并更新游标到已处理位置。
调用关系:word_wrap_line 用它决定每一行要从原始带样式文本里切哪一段。它也复用 borrowed_slice_range 和 map_owned_wrapped_line_to_range 来处理换行库返回的不同形式。
调用图:调用 2 个内部函数(borrowed_slice_range, map_owned_wrapped_line_to_range);被 2 处调用(wrap_ranges_trim_handles_owned_lines_with_penalty_char, word_wrap_line);外部调用 3 个(into, new, wrap)。
borrowed_slice_range121–132 ↗
fn borrowed_slice_range(text: &str, slice: &str) -> Option<Range<usize>>
作用:判断一个字符串切片是不是原文的一部分,并算出它在原文里的起止位置。可以理解成看这张小纸条是不是从原稿上剪下来的。
数据流:输入原文和一个切片 → 比较两者内存地址是否包含关系 → 如果切片确实来自原文,就输出字节范围;否则输出空。
调用关系:wrap_ranges 和 wrap_ranges_trim 会先用它走最快、最准的路径。若发现切片不是直接借自原文,就改走 map_owned_wrapped_line_to_range。
调用图:被 2 处调用(wrap_ranges, wrap_ranges_trim)。
map_owned_wrapped_line_to_range141–203 ↗
fn map_owned_wrapped_line_to_range(
text: &str,
cursor: usize,
wrapped: &str,
synthetic_prefix: &str,
) -> Range<usize>
作用:当换行库返回的是新造出来的一行,而不是原文切片时,这个函数负责把它尽量对应回原文范围。常见原因是换行库加了缩进或连字符。
数据流:输入原文、当前游标、换行后的文字和可能的人工缩进 → 去掉人工缩进,跳过原文中不该出现在行首的空格 → 一个字符一个字符对照原文,忽略末尾人工加的 - → 输出能确认对应的原文范围,必要时记录警告。
调用关系:它是 wrap_ranges 和 wrap_ranges_trim 的兜底映射器。多个测试直接调用它,覆盖缩进碰巧和原文开头相同、外部字符串、部分不匹配等容易出错的情况。
调用图:被 6 处调用(borrowed_slice_range_rejects_slices_outside_source_text, map_owned_wrapped_line_to_range_indent_coincides_with_source, map_owned_wrapped_line_to_range_recovers_on_non_prefix_mismatch, map_owned_wrapped_line_to_range_repro_overconsumes_repeated_prefix_patterns, wrap_ranges, wrap_ranges_trim);外部调用 2 个(warn!, unreachable!)。
line_contains_url_like208–215 ↗
fn line_contains_url_like(line: &Line<'_>) -> bool
作用:判断一整行带样式文字里是否看起来含有网址。它会跨过不同样式片段,把内容合起来看。
数据流:输入一个 ratatui 的 Line → 收集所有 span 的文字成一个 String → 交给 text_contains_url_like 判断 → 输出是或否。
调用关系:adaptive_wrap_line 用它决定是否要进入网址保护模式。历史链接插入相关代码也会用它判断当前行是否需要特殊处理。
调用图:调用 1 个内部函数(text_contains_url_like);被 2 处调用(insert_history_hyperlink_lines_with_mode_and_wrap_policy, adaptive_wrap_line)。
line_has_mixed_url_and_non_url_tokens222–229 ↗
fn line_has_mixed_url_and_non_url_tokens(line: &Line<'_>) -> bool
作用:判断一行是不是既有网址,又有真正的普通文字。它会忽略 -、1.、竖线这类装饰符号。
数据流:输入带样式的一行 → 合并所有文字片段 → 交给 text_has_mixed_url_and_non_url_tokens 分析每个空白分隔的词 → 输出是否属于“网址加正文”的混合行。
调用关系:adaptive_wrap_line 用它选择更细的混合换行算法。历史链接渲染也用它避免把纯网址列表误判成普通段落。
调用图:调用 1 个内部函数(text_has_mixed_url_and_non_url_tokens);被 2 处调用(insert_history_hyperlink_lines_with_mode_and_wrap_policy, adaptive_wrap_line)。
text_contains_url_like242–244 ↗
fn text_contains_url_like(text: &str) -> bool
作用:在一段纯文字里查找有没有像网址的词。这里的“像”是启发式判断,不是百分百解析互联网地址。
数据流:输入文字 → 按 ASCII 空白切成一个个 token → 只要有一个 token 被 is_url_like_token 判定为网址样子 → 输出 true,否则 false。
调用关系:line_contains_url_like 把带样式行转成纯文字后调用它。它是网址保护换行的第一道开关。
调用图:被 1 处调用(line_contains_url_like)。
text_has_mixed_url_and_non_url_tokens248–265 ↗
fn text_has_mixed_url_and_non_url_tokens(text: &str) -> bool
作用:检查一段文字里是否同时出现网址样子的词和有意义的非网址词。这样能区分“只有一个链接”和“正文里夹了链接”。
数据流:输入文字 → 逐个查看空白分隔的 token → 分别记录是否见过网址、是否见过普通有效词 → 两者都见到就立刻返回 true,否则最后返回 false。
调用关系:line_has_mixed_url_and_non_url_tokens 是它的外层包装。它把网址判断交给 is_url_like_token,把普通词判断交给 is_substantive_non_url_token。
调用图:调用 2 个内部函数(is_substantive_non_url_token, is_url_like_token);被 1 处调用(line_has_mixed_url_and_non_url_tokens)。
is_url_like_token271–274 ↗
fn is_url_like_token(raw_token: &str) -> bool
作用:判断单个词是不是像网址。它会先去掉外层括号、逗号、句号等标点,再看是完整网址还是裸域名网址。
数据流:输入一个 token → 用 trim_url_token 去掉包围标点 → 检查是否为空 → 分别尝试 is_absolute_url_like 和 is_bare_url_like → 输出真假。
调用关系:它被混合行检测和 mixed_url_wrap_ranges 使用,是整个“保护网址不要拆开”的核心小判官。
调用图:调用 3 个内部函数(is_absolute_url_like, is_bare_url_like, trim_url_token);被 2 处调用(mixed_url_wrap_ranges, text_has_mixed_url_and_non_url_tokens)。
is_substantive_non_url_token276–283 ↗
fn is_substantive_non_url_token(raw_token: &str) -> bool
作用:判断一个词是不是有实际内容的非网址词。它会把列表符号、边框线这类装饰排除掉。
数据流:输入原始 token → 去掉网址外围标点 → 若为空或是装饰符号就返回 false → 否则只要含字母或数字就认为是有效普通词。
调用关系:text_has_mixed_url_and_non_url_tokens 用它判断一行里除了网址外是否还有正文。装饰符号判断交给 is_decorative_marker_token。
调用图:调用 2 个内部函数(is_decorative_marker_token, trim_url_token);被 1 处调用(text_has_mixed_url_and_non_url_tokens)。
is_decorative_marker_token285–305 ↗
fn is_decorative_marker_token(raw_token: &str, token: &str) -> bool
作用:识别那些看起来像排版标记、但不算正文的词,比如项目符号、引用竖线、树形边框符号。
数据流:输入原始 token 和修剪后的 token → 先匹配一组固定装饰字符 → 再检查是否是 1. 或 2) 这类有序列表编号 → 输出是否为装饰标记。
调用关系:is_substantive_non_url_token 会调用它,避免把 - https://... 里的 - 当成普通文字,从而误判为混合行。
调用图:调用 1 个内部函数(is_ordered_list_marker);被 1 处调用(is_substantive_non_url_token);外部调用 1 个(matches!)。
is_ordered_list_marker307–310 ↗
fn is_ordered_list_marker(raw_token: &str, token: &str) -> bool
作用:判断一个词是不是有序列表编号,比如 1. 或 23)。这种编号只负责排版,不算真正正文。
数据流:输入原始 token 和去标点后的 token → 确认去标点部分全是数字 → 再确认原始 token 以点号或右括号结尾 → 输出真假。
调用关系:它只服务于 is_decorative_marker_token,是识别列表前缀的小助手。
调用图:被 1 处调用(is_decorative_marker_token)。
trim_url_token312–332 ↗
fn trim_url_token(token: &str) -> &str
作用:去掉网址 token 外面常见的包围标点,比如括号、逗号、句号、引号。这样 (https://a.com) 也能被认出。
数据流:输入一个 token → 从两端剥掉指定标点字符 → 输出修剪后的字符串切片,不复制正文。
调用关系:is_url_like_token 和 is_substantive_non_url_token 都会先用它清理 token,再做判断。
调用图:被 2 处调用(is_substantive_non_url_token, is_url_like_token)。
is_absolute_url_like337–354 ↗
fn is_absolute_url_like(token: &str) -> bool
作用:判断一个 token 是否像带协议的完整网址,比如 https://... 或 myapp://...。协议就是 :// 前面的那段名字。
数据流:输入 token → 若没有 :// 直接否定 → 尝试用 URL 解析库解析 → 常见网络协议要求必须有主机名,自定义协议解析不过时再检查协议名字格式 → 输出真假。
调用关系:is_url_like_token 会先尝试它,再尝试裸网址判断。它把自定义协议的兜底检查交给 has_valid_scheme_prefix。
调用图:调用 1 个内部函数(has_valid_scheme_prefix);被 1 处调用(is_url_like_token);外部调用 2 个(matches!, parse)。
has_valid_scheme_prefix356–370 ↗
fn has_valid_scheme_prefix(token: &str) -> bool
作用:在标准 URL 解析失败时,粗略检查 xxx:// 前面的协议名是否合法。这样自定义应用链接不会被错过。
数据流:输入 token → 按 :// 分成协议和后半段 → 确保两边都不空,协议以英文字母开头,后续只含允许字符 → 输出真假。
调用关系:is_absolute_url_like 在 URL 库不接受某些自定义链接时调用它兜底。
调用图:被 1 处调用(is_absolute_url_like)。
is_bare_url_like380–402 ↗
fn is_bare_url_like(token: &str) -> bool
作用:判断没有 http:// 的词是否也像网址,比如 www.example.com、localhost:3000/api、127.0.0.1/health。
数据流:输入 token → 分出主机端口和路径/查询等后缀 → 没有后缀时要求以 www. 开头 → 再拆主机和端口,验证端口 → 最后检查主机是不是 localhost、IPv4 或合法域名 → 输出真假。
调用关系:is_url_like_token 用它补充识别裸域名网址。它把各个小检查分给 split_host_port_and_trailer、split_host_and_port、is_valid_port、is_ipv4、is_domain_name。
调用图:调用 5 个内部函数(is_domain_name, is_ipv4, is_valid_port, split_host_and_port, split_host_port_and_trailer);被 1 处调用(is_url_like_token)。
split_host_port_and_trailer404–410 ↗
fn split_host_port_and_trailer(token: &str) -> (&str, bool)
作用:把裸网址切成“主机加端口”和“后面有没有路径、查询、片段”。后缀就是 /、?、# 之后的部分。
数据流:输入 token → 查找第一个 /、? 或 # → 找到就返回前半段和 true,没找到就返回整个 token 和 false。
调用关系:is_bare_url_like 先用它确认裸域名是不是足够像网址,而不是普通的 hello.world。
调用图:被 1 处调用(is_bare_url_like)。
split_host_and_port412–427 ↗
fn split_host_and_port(host_port: &str) -> (&str, Option<&str>)
作用:把 example.com:8080 这种主机端口拆开。它故意不支持方括号 IPv6,避免误判。
数据流:输入主机端口字符串 → 如果以 [ 开头就不拆 → 否则从最后一个冒号处尝试拆分,只有端口全是数字才接受 → 输出主机和可选端口。
调用关系:is_bare_url_like 用它把端口单独拿出来交给 is_valid_port 验证。
调用图:被 1 处调用(is_bare_url_like)。
is_valid_port429–435 ↗
fn is_valid_port(port: &str) -> bool
作用:检查端口号是否合法。端口可以理解成同一台机器上不同服务的门牌号。
数据流:输入端口字符串 → 确保不空、不超过 5 位、全是数字 → 再解析成 u16 范围内的数字 → 输出真假。
调用关系:is_bare_url_like 用它排除 localhost:99999/path 或 example.com:abc/path 这类假网址。
调用图:被 1 处调用(is_bare_url_like)。
is_ipv4437–446 ↗
fn is_ipv4(host: &str) -> bool
作用:判断主机名是不是 IPv4 地址,也就是 192.168.1.1 这种四段数字地址。
数据流:输入主机字符串 → 按点号切成四段 → 每段都必须非空且能解析成 0 到 255 的数字 → 输出真假。
调用关系:is_bare_url_like 用它允许 127.0.0.1:8080/health 这类本机或内网链接。
调用图:被 1 处调用(is_bare_url_like)。
is_domain_name448–463 ↗
fn is_domain_name(host: &str) -> bool
作用:判断主机名是不是正常域名,比如 example.com。它会要求有点号,并检查顶级域名和各段格式。
数据流:输入主机字符串 → 转小写 → 没有点号就否定 → 取最后一段做顶级域名检查 → 其余每段检查域名标签格式 → 输出真假。
调用关系:is_bare_url_like 用它确认裸域名是否可信。它内部用 is_tld 和 is_domain_label 这类更小的格式检查。
调用图:调用 1 个内部函数(is_tld);被 1 处调用(is_bare_url_like)。
is_tld465–467 ↗
fn is_tld(label: &str) -> bool
作用:检查域名最后一段,也就是顶级域名,比如 com、org,格式是否合理。
数据流:输入标签 → 要求长度在 2 到 63 之间,并且全是英文字母 → 输出真假。
调用关系:is_domain_name 用它过滤不像真实域名结尾的字符串。
调用图:被 1 处调用(is_domain_name)。
is_domain_label469–485 ↗
fn is_domain_label(label: &str) -> bool
作用:检查域名中间每一段是否合法,比如 example 或 my-site。它不允许空段,也不允许横线开头或结尾。
数据流:输入域名标签 → 检查长度、首尾字符和每个字符 → 只有字母数字和中间横线符合要求才返回 true。
调用关系:它是 is_domain_name 的细粒度校验零件,用来避免把奇怪字符串误当域名。
url_preserving_wrap_options493–497 ↗
fn url_preserving_wrap_options(opts: RtOptions<'a>) -> RtOptions<'a>
作用:把普通换行选项改造成“保护网址”的换行选项。核心是不要在 /、- 或长词内部把网址拆开。
数据流:输入 RtOptions → 设置只按 ASCII 空格分词,关闭按连字符断词,关闭强制拆长词 → 输出新的 RtOptions。
调用关系:adaptive_wrap_line 在发现一行主要是网址时调用它,然后再交给 word_wrap_line 执行实际换行。
调用图:调用 1 个内部函数(word_separator);被 1 处调用(adaptive_wrap_line)。
adaptive_wrap_line508–518 ↗
fn adaptive_wrap_line(line: &'a Line<'a>, base: RtOptions<'a>) -> Vec<Line<'a>>
作用:这是单行的智能换行入口:没网址时照常换,有网址时保护链接不被拆坏。
数据流:输入一行和基础换行选项 → 先检查是否含网址 → 无网址就调用 word_wrap_line → 有网址且混有正文就调用 mixed_url_wrap_line → 纯网址类行就套用 url_preserving_wrap_options 后调用 word_wrap_line → 输出多行结果。
调用关系:很多终端渲染路径会调用它,比如命令输出、 transcript、历史链接显示等。它像分诊台,把不同文本送到标准换行或网址感知换行。
调用图:调用 5 个内部函数(line_contains_url_like, line_has_mixed_url_and_non_url_tokens, mixed_url_wrap_line, url_preserving_wrap_options, word_wrap_line);被 14 处调用(command_display_lines, exploring_display_lines, transcript_lines, user_shell_output_is_limited_by_screen_lines, insert_history_hyperlink_lines_with_mode_and_wrap_policy, flush_current_line, adaptive_wrap_hyperlink_lines, adaptive_wrap_lines, adaptive_wrap_line_keeps_long_url_like_token_intact, adaptive_wrap_line_mixed_line_counts_leading_spaces_before_first_word (+4 more))。
adaptive_wrap_lines528–554 ↗
fn adaptive_wrap_lines(
lines: I,
width_or_options: RtOptions<'a>,
) -> Vec<Line<'static>>
作用:对多行输入逐行做智能换行。第一行使用初始缩进,后面的输入行都用后续缩进。
数据流:输入一批行和换行选项 → 逐行转成 LineInput → 按是不是第一行调整缩进 → 调用 adaptive_wrap_line → 把结果转成拥有自己内容的静态行追加到输出 → 返回所有折行。
调用关系:这是多数历史单元格渲染会用的多行入口。它把单行判断交给 adaptive_wrap_line,把结果搬运交给 push_owned_lines。
调用图:调用 2 个内部函数(push_owned_lines, adaptive_wrap_line);被 7 处调用(install_confirmation_lines, as_renderable, push_section_header, as_renderable, transcript_lines, render_transcript_content_lines, display_lines);外部调用 2 个(into_iter, new)。
RtOptions::from584–586 ↗
fn from(width: usize) -> Self
作用:让调用者可以直接用一个数字宽度当作 RtOptions。这样写 word_wrap_line(&line, 80) 也能工作。
数据流:输入宽度数字 → 调用 RtOptions::new 生成默认换行配置 → 输出配置对象。
调用关系:它服务于所有接受 Into<RtOptions> 的函数,比如 word_wrap_line 和 word_wrap_lines,让 API 更省事。
调用图:调用 1 个内部函数(new)。
RtOptions::new591–602 ↗
fn new(width: usize) -> Self
作用:创建一套默认换行设置。默认会按给定宽度换行,允许长词拆开,并使用 textwrap 的常规分词方式。
数据流:输入显示宽度 → 填入换行符、空缩进、是否拆词、换行算法、分词器等默认值 → 输出 RtOptions。
调用关系:很多渲染代码和测试从它开始,再链式修改缩进、是否拆词等设置。RtOptions::from 也会调用它。
调用图:被 43 处调用(install_confirmation_lines, as_renderable, push_section_header, as_renderable, wrap_standard_row, wrap_styled_line, command_display_lines, exploring_display_lines, transcript_lines, user_shell_output_is_limited_by_screen_lines (+15 more));外部调用 2 个(default, new)。
RtOptions::line_ending604–609 ↗
fn line_ending(self, line_ending: textwrap::LineEnding) -> Self
作用:返回一份只改了换行符风格的新配置。换行符就是行与行之间用什么结尾。
数据流:输入原配置和新的 line_ending → 保留其他字段不变 → 输出更新后的配置。
调用关系:它是 RtOptions 的链式设置方法之一,供需要特殊行尾格式的调用者使用。
RtOptions::width611–613 ↗
fn width(self, width: usize) -> Self
作用:返回一份只改了目标宽度的新配置。宽度指终端里一行最多能放多少列。
数据流:输入原配置和新宽度 → 替换 width 字段 → 输出新配置。
调用关系:word_wrap_line 内部会根据缩进占掉的宽度临时改 width,再交给 wrap_ranges_trim。
RtOptions::initial_indent615–620 ↗
fn initial_indent(self, initial_indent: Line<'a>) -> Self
作用:设置第一条输出行前面的缩进,比如列表项开头的 - 。
数据流:输入原配置和一段 Line 作为首行缩进 → 替换 initial_indent → 输出新配置。
调用关系:word_wrap_lines 和 adaptive_wrap_lines 会用它确保只有整个块的第一行使用首行缩进,后续行使用 subsequent_indent。
RtOptions::subsequent_indent622–627 ↗
fn subsequent_indent(self, subsequent_indent: Line<'a>) -> Self
作用:设置换行后的后续行缩进,比如让列表项第二行对齐。
数据流:输入原配置和一段 Line 作为后续缩进 → 替换 subsequent_indent → 输出新配置。
调用关系:word_wrap_line 和 mixed_url_wrap_line 都会用这个缩进生成第二行及之后的行。
RtOptions::break_words629–634 ↗
fn break_words(self, break_words: bool) -> Self
作用:设置长词太宽时要不要硬拆开。关闭后,某些行可能会超过终端宽度,但网址能保持完整。
数据流:输入原配置和布尔值 → 替换 break_words 字段 → 输出新配置。
调用关系:url_preserving_wrap_options 会把它关掉;普通调用者也能手动设置。
RtOptions::word_separator636–641 ↗
fn word_separator(self, word_separator: textwrap::WordSeparator) -> RtOptions<'a>
作用:设置用什么规则把文字切成词。比如只按空格切,就不会把网址里的斜杠当断点。
数据流:输入原配置和新的 WordSeparator → 替换字段 → 输出新配置。
调用关系:url_preserving_wrap_options 调用它,把分词规则改成只按 ASCII 空格分隔。
调用图:被 1 处调用(url_preserving_wrap_options)。
RtOptions::wrap_algorithm643–648 ↗
fn wrap_algorithm(self, wrap_algorithm: textwrap::WrapAlgorithm) -> RtOptions<'a>
作用:设置使用哪种换行算法。算法就是决定词放到哪一行的策略。
数据流:输入原配置和算法值 → 替换 wrap_algorithm → 输出新配置。
调用关系:word_wrap_line 会把这个设置传给底层 textwrap 库。
RtOptions::word_splitter650–655 ↗
fn word_splitter(self, word_splitter: textwrap::WordSplitter) -> RtOptions<'a>
作用:设置一个词内部能不能、以及怎么拆。比如是否允许在连字符处断开。
数据流:输入原配置和新的 WordSplitter → 替换字段 → 输出新配置。
调用关系:url_preserving_wrap_options 会把它改成不按连字符断词;word_wrap_line 会把设置传到底层换行库。
word_wrap_line659–730 ↗
fn word_wrap_line(line: &'a Line<'a>, width_or_options: O) -> Vec<Line<'a>>
作用:这是标准单行换行函数。它能把带颜色、样式和缩进的 Line 折成多行,并尽量保留原来的样式。
数据流:输入一行和宽度/选项 → 先用 flatten_line 把多个 span 合成纯文字并记录样式范围 → 分别按首行缩进和后续缩进计算可用宽度 → 用 wrap_ranges_trim 找到每行文字范围 → 用 slice_line_spans 切回带样式片段并加上缩进 → 输出多条 Line。
调用关系:adaptive_wrap_line、word_wrap_lines、很多表格/单元格渲染都会调用它。它把“算哪里断”交给 wrap_ranges_trim,把“把样式贴回来”交给 slice_line_spans。
调用图:调用 3 个内部函数(flatten_line, slice_line_spans, wrap_ranges_trim);被 20 处调用(wrap_cell, render_stacked_field, wrap_cell, adaptive_wrap_line, ascii_space_separator_with_no_hyphenation_keeps_url_intact, break_words_false_allows_overflow_for_long_word, empty_initial_indent_subsequent_spaces, empty_input_yields_single_empty_line, hyphen_splitter_breaks_at_hyphen, indent_consumes_width_leaving_one_char_space (+10 more));外部调用 4 个(into, new, new, vec!)。
MixedUrlWord::width739–741 ↗
fn width(&self, text: &str) -> usize
作用:计算一个混合换行词片段在终端里占几列。emoji、中文等字符可能不止一列,所以不能只数字节。
数据流:输入一个 MixedUrlWord 和原文 → 取出该词范围内的字符串 → 用 display_width 计算显示宽度 → 输出列数。
调用关系:split_mixed_url_word 和混合网址换行流程依靠它判断一个词是否超过当前行宽。
调用图:被 1 处调用(split_mixed_url_word);外部调用 2 个(display_width, clone)。
mixed_url_wrap_line744–781 ↗
fn mixed_url_wrap_line(line: &'a Line<'a>, rt_opts: RtOptions<'a>) -> Vec<Line<'a>>
作用:处理“正文里夹着网址”的特殊换行。它保护网址不断开,同时让普通正文仍按合适宽度换行。
数据流:输入带样式行和 RtOptions → 摊平成纯文字并记录样式 → 扣除首行/后续缩进宽度 → 调用 mixed_url_wrap_ranges 算每行范围 → 用 slice_line_spans 切回原样式并加缩进 → 输出折好的 Line 列表。
调用关系:adaptive_wrap_line 在检测到混合网址和正文时调用它。它是比纯网址保护更精细的路径。
调用图:调用 3 个内部函数(flatten_line, mixed_url_wrap_ranges, slice_line_spans);被 1 处调用(adaptive_wrap_line);外部调用 2 个(new, vec!)。
mixed_url_wrap_ranges783–877 ↗
fn mixed_url_wrap_ranges(
text: &str,
initial_width: usize,
subsequent_width: usize,
) -> Vec<Range<usize>>
作用:为混合网址行计算每一行应该包含原文的哪一段。它的规则是:网址宁可超宽也不拆,普通超长词可以拆。
数据流:输入纯文字、首行可用宽度、后续行可用宽度 → 按空格找词并标记哪些是网址 → 对过长的非网址词调用 split_mixed_url_word 拆片 → 逐片尝试放入当前行,放不下就收尾开新行 → 输出原文字节范围列表。
调用关系:mixed_url_wrap_line 调用它拿到切分方案。它内部反复使用 is_url_like_token 和 split_mixed_url_word。
调用图:调用 2 个内部函数(is_url_like_token, split_mixed_url_word);被 1 处调用(mixed_url_wrap_line);外部调用 1 个(new)。
split_mixed_url_word879–896 ↗
fn split_mixed_url_word(text: &str, word: MixedUrlWord, line_limit: usize) -> Vec<MixedUrlWord>
作用:把混合行里的超长非网址词拆成能放进一行的小片。网址不会被它拆开。
数据流:输入原文、一个词范围、当前行宽 → 如果是网址或已经够短就原样返回 → 否则用 textwrap 的 Word.break_apart 按显示宽度拆分 → 输出多个非网址片段。
调用关系:mixed_url_wrap_ranges 在遇到放不下的普通长词时调用它,避免普通长词把整行排版撑坏。
调用图:调用 1 个内部函数(width);被 1 处调用(mixed_url_wrap_ranges);外部调用 3 个(new, from, vec!)。
flatten_line898–910 ↗
fn flatten_line(line: &Line<'_>) -> (String, Vec<(Range<usize>, ratatui::style::Style)>)
作用:把 ratatui 的一行多个 span 拼成一条纯字符串,同时记住每一段原本的样式范围。
数据流:输入 Line → 依次读取每个 span 的内容和样式 → 拼接成 flat 字符串,并记录每段起止字节和样式 → 输出纯文字和样式边界表。
调用关系:word_wrap_line 和 mixed_url_wrap_line 都先用它把复杂带样式行变成方便计算的形式,之后再用 slice_line_spans 还原。
调用图:被 2 处调用(mixed_url_wrap_line, word_wrap_line);外部调用 2 个(new, new)。
LineInput::as_ref920–925 ↗
fn as_ref(&self) -> &Line<'a>
作用:把 LineInput 统一看成 Line 的引用。这样不管里面是借来的行还是拥有的行,后续代码都能一样读取。
数据流:输入 LineInput 自身 → 根据枚举分支取出 Borrowed 或 Owned 里的 Line 引用 → 输出 &Line。
调用关系:word_wrap_lines 和 adaptive_wrap_lines 在把各种输入转成 LineInput 后,用它交给单行换行函数。
Line::into_line_input946–948 ↗
fn into_line_input(self) -> LineInput<'a>
作用:把 Line 引用或可变引用转成 LineInput,表示这行是借来的,不需要复制拥有权。
数据流:输入 Line 的引用 → 包装成 LineInput::Borrowed → 输出统一输入类型。
调用关系:word_wrap_lines 和 adaptive_wrap_lines 的泛型入口会自动调用它,让调用者可以直接传 Line。
调用图:外部调用 2 个(Borrowed, Owned)。
String::into_line_input952–954 ↗
fn into_line_input(self) -> LineInput<'a>
作用:把 String 转成可换行的 LineInput。调用者传普通字符串也能走同一套排版流程。
数据流:输入 String → 转成 ratatui 的 Line → 包装成 LineInput::Owned → 输出统一输入类型。
调用关系:word_wrap_lines 和 adaptive_wrap_lines 通过这个实现支持拥有的字符串输入。
调用图:外部调用 2 个(from, Owned)。
str::into_line_input958–960 ↗
fn into_line_input(self) -> LineInput<'a>
作用:把字符串切片 &str 转成 LineInput。这样传字面量或借用字符串也能直接换行。
数据流:输入 &str → 构造成 Line → 包装为 LineInput::Owned → 输出统一输入类型。
调用关系:word_wrap_lines 的测试会验证它能接受字符串切片数组。
调用图:外部调用 2 个(from, Owned)。
Cow::into_line_input964–966 ↗
fn into_line_input(self) -> LineInput<'a>
作用:把 Cow 字符串转成 LineInput。Cow 是“可能借用、也可能拥有”的字符串容器。
数据流:输入 Cow<str> → 构造成 Line → 包装成 LineInput::Owned → 输出统一输入类型。
调用关系:它让多行换行入口能接收更灵活的字符串来源。
调用图:外部调用 2 个(from, Owned)。
Span::into_line_input970–972 ↗
fn into_line_input(self) -> LineInput<'a>
作用:把单个带样式片段 Span 转成一整行输入。这样一段有颜色的文字也能直接换行。
数据流:输入 Span → 转成 Line → 包装为 LineInput::Owned → 输出统一输入类型。
调用关系:它扩展了 word_wrap_lines 和 adaptive_wrap_lines 可接受的输入形态。
调用图:外部调用 2 个(from, Owned)。
Vec::into_line_input976–978 ↗
fn into_line_input(self) -> LineInput<'a>
作用:把一组 Span 转成一整行输入。适合一行里有多段不同样式的文字。
数据流:输入 Vec<Span> → 构造成 Line → 包装为 LineInput::Owned → 输出统一输入类型。
调用关系:它让多行换行函数可以直接处理由多个样式片段组成的行。
调用图:外部调用 2 个(from, Owned)。
word_wrap_lines984–1008 ↗
fn word_wrap_lines(lines: I, width_or_options: O) -> Vec<Line<'static>>
作用:对多行输入做标准换行,并保证首行缩进只用一次。后续所有输出行都用后续缩进。
数据流:输入一批可转成 LineInput 的内容和选项 → 第一条输入使用原始首行缩进,之后把首行缩进改为后续缩进 → 对每行调用 word_wrap_line → 用 push_owned_lines 收集成静态输出 → 返回所有行。
调用关系:普通历史详情、markdown 单元格等渲染会调用它。它是标准多行换行入口,对应智能版本 adaptive_wrap_lines。
调用图:调用 2 个内部函数(push_owned_lines, word_wrap_line);被 8 处调用(agent_markdown_cell_survives_insert_history_rewrap, e2e_stream_blockquote_wrap_preserves_green_style, display_lines, wrapped_details_lines, wrap_lines_accepts_borrowed_iterators, wrap_lines_accepts_str_slices, wrap_lines_applies_initial_indent_only_once, wrap_lines_without_indents_is_concat_of_single_wraps);外部调用 3 个(into_iter, into, new)。
word_wrap_lines_borrowed1011–1031 ↗
fn word_wrap_lines_borrowed(lines: I, width_or_options: O) -> Vec<Line<'a>>
作用:对一批借用的 Line 做标准换行,返回仍可借用原内容的结果。它避免把内容复制成静态拥有版本。
数据流:输入 Line 引用迭代器和选项 → 第一行用原选项,后面把首行缩进换成后续缩进 → 逐行调用 word_wrap_line 并追加结果 → 输出借用生命周期内的 Line 列表。
调用关系:主要由测试和需要借用结果的场景使用。它的行为和 word_wrap_lines 类似,但不调用 push_owned_lines。
调用图:调用 1 个内部函数(word_wrap_line);被 3 处调用(word_wrap_does_not_split_words_simple_english, wrap_lines_borrowed_applies_initial_indent_only_once, wrap_lines_borrowed_without_indents_is_concat_of_single_wraps);外部调用 3 个(into_iter, into, new)。
slice_line_spans1033–1071 ↗
fn slice_line_spans(
original: &'a Line<'a>,
span_bounds: &[(Range<usize>, ratatui::style::Style)],
range: &Range<usize>,
) -> Line<'a>
作用:从原始带样式行里切出指定字节范围,并保留这段文字原来的样式。它像按范围剪彩色纸条。
数据流:输入原始 Line、每个 span 的边界和目标范围 → 找出与目标范围重叠的 span → 在每个 span 内容里切出局部字符串 → 用原样式构造新 span → 输出新的 Line。
调用关系:word_wrap_line 和 mixed_url_wrap_line 算出每行范围后都靠它把纯文字范围变回可渲染的带样式行。
调用图:被 2 处调用(mixed_url_wrap_line, word_wrap_line);外部调用 3 个(new, Borrowed, iter)。
tests::concat_line1082–1087 ↗
fn concat_line(line: &Line) -> String
作用:测试辅助函数,把一行的所有 span 内容拼成普通字符串。这样测试可以只关心显示出来的文字。
数据流:输入 Line → 遍历每个 span 的 content → 拼接成 String → 输出纯文字。
调用关系:大量测试用它比较换行结果,不必每次手写 span 拼接。
tests::trivial_unstyled_no_indents_wide_width1090–1095 ↗
fn trivial_unstyled_no_indents_wide_width()
作用:测试简单短文本在足够宽时不会被换行。它确认最基本情况没有被复杂逻辑破坏。
数据流:构造 hello → 用宽度 10 调 word_wrap_line → 检查只输出一行且内容不变。
调用关系:它直接覆盖 word_wrap_line 的正常路径,是最小冒烟测试。
调用图:调用 1 个内部函数(word_wrap_line);外部调用 2 个(from, assert_eq!)。
tests::simple_unstyled_wrap_narrow_width1098–1104 ↗
fn simple_unstyled_wrap_narrow_width()
作用:测试普通英文在窄宽度下会按空格拆成两行。它确认基础换行能工作。
数据流:输入 hello world 和宽度 5 → 调 word_wrap_line → 期待输出 hello、world 两行。
调用关系:它验证 word_wrap_line 与 wrap_ranges_trim 的基本配合。
调用图:调用 1 个内部函数(word_wrap_line);外部调用 2 个(from, assert_eq!)。
tests::simple_styled_wrap_preserves_styles1107–1119 ↗
fn simple_styled_wrap_preserves_styles()
作用:测试换行后颜色样式不会丢。特别是第一段红色文字换到输出行后仍然是红色。
数据流:构造带红色 hello 和普通 world 的行 → 宽度 6 换行 → 检查文字分布和每行 span 的颜色。
调用关系:它主要验证 flatten_line 与 slice_line_spans 能正确保存样式。
调用图:调用 1 个内部函数(word_wrap_line);外部调用 3 个(from, assert_eq!, vec!)。
tests::with_initial_and_subsequent_indents1122–1136 ↗
fn with_initial_and_subsequent_indents()
作用:测试首行缩进和后续缩进是否分别生效。比如列表第一行是 - ,后面是两个空格。
数据流:创建带两种缩进的 RtOptions → 换行 hello world foo → 检查三行的前缀和内容。
调用关系:它覆盖 RtOptions::new、initial_indent、subsequent_indent 和 word_wrap_line 的缩进逻辑。
调用图:调用 2 个内部函数(new, word_wrap_line);外部调用 3 个(from, assert!, assert_eq!)。
tests::empty_initial_indent_subsequent_spaces1139–1149 ↗
fn empty_initial_indent_subsequent_spaces()
作用:测试首行无缩进、后续行有空格缩进时的行为。它防止后续缩进被忽略。
数据流:设置空首行缩进和四空格后续缩进 → 换行一段文字 → 检查第一行从正文开始,后续行都以四空格开始。
调用关系:它验证 word_wrap_line 对不同缩进宽度的处理。
调用图:调用 2 个内部函数(new, word_wrap_line);外部调用 2 个(from, assert!)。
tests::empty_input_yields_single_empty_line1152–1157 ↗
fn empty_input_yields_single_empty_line()
作用:测试空输入也会得到一条空行,而不是没有输出。界面渲染通常需要这一行占位。
数据流:输入空字符串 Line → 调 word_wrap_line → 检查输出长度为 1 且内容为空。
调用关系:它覆盖 word_wrap_line 里没有折行范围时的兜底返回。
调用图:调用 1 个内部函数(word_wrap_line);外部调用 2 个(from, assert_eq!)。
tests::leading_spaces_preserved_on_first_line1160–1165 ↗
fn leading_spaces_preserved_on_first_line()
作用:测试首行开头空格会被保留。这样用户或工具输出的缩进不会被吞掉。
数据流:输入带三个前导空格的文本 → 宽度足够时换行 → 检查输出仍包含这些空格。
调用关系:它验证 wrap_ranges_trim 和 word_wrap_line 对首行空白的处理。
调用图:调用 1 个内部函数(word_wrap_line);外部调用 2 个(from, assert_eq!)。
tests::multiple_spaces_between_words_dont_start_next_line_with_spaces1168–1174 ↗
fn multiple_spaces_between_words_dont_start_next_line_with_spaces()
作用:测试多个单词间空格不会跑到下一行行首。下一行应从单词开始,显示更干净。
数据流:输入 hello world → 宽度 8 换行 → 检查第一行是 hello,第二行是 world。
调用关系:它验证 word_wrap_line 在处理剩余文本时会跳过行首多余空格。
调用图:调用 1 个内部函数(word_wrap_line);外部调用 2 个(from, assert_eq!)。
tests::break_words_false_allows_overflow_for_long_word1177–1183 ↗
fn break_words_false_allows_overflow_for_long_word()
作用:测试关闭拆长词后,超长单词会保持一整行,即使超过宽度。
数据流:创建 width 5 且 break_words false 的选项 → 换行超长单词 → 检查只输出一行完整内容。
调用关系:它覆盖 RtOptions::break_words 传到底层换行库的效果。
调用图:调用 2 个内部函数(new, word_wrap_line);外部调用 2 个(from, assert_eq!)。
tests::hyphen_splitter_breaks_at_hyphen1186–1192 ↗
fn hyphen_splitter_breaks_at_hyphen()
作用:测试默认设置会在连字符处断开普通词。这个行为正是网址保护模式要避免的。
数据流:输入 hello-world 和宽度 7 → 标准换行 → 检查输出为 hello- 和 world。
调用关系:它说明 word_wrap_line 的默认行为,也为 url_preserving_wrap_options 的必要性提供对照。
调用图:调用 1 个内部函数(word_wrap_line);外部调用 2 个(from, assert_eq!)。
tests::indent_consumes_width_leaving_one_char_space1195–1205 ↗
fn indent_consumes_width_leaving_one_char_space()
作用:测试缩进本身会占用行宽,即使缩进已经很宽,仍至少给正文留一个字符位置。
数据流:设置总宽 4、首行缩进 >>>>、后续缩进 -- → 换行 hello → 检查字符按剩余宽度拆成三行。
调用关系:它覆盖 word_wrap_line 中 saturating_sub 和 max(1) 这类防止零宽的逻辑。
调用图:调用 2 个内部函数(new, word_wrap_line);外部调用 2 个(from, assert_eq!)。
tests::wide_unicode_wraps_by_display_width1208–1214 ↗
fn wide_unicode_wraps_by_display_width()
作用:测试 emoji 这类宽字符按显示宽度换行,而不是按字符数。一个 emoji 通常占两列。
数据流:输入三个 emoji,宽度 4 → 调 word_wrap_line → 检查前两枚在第一行,第三枚在第二行。
调用关系:它验证底层显示宽度计算在 word_wrap_line 路径里生效。
调用图:调用 1 个内部函数(word_wrap_line);外部调用 2 个(from, assert_eq!)。
tests::styled_split_within_span_preserves_style1217–1228 ↗
fn styled_split_within_span_preserves_style()
作用:测试同一个带样式 span 被从中间拆开后,两边都保留样式。
数据流:输入红色 abcd,宽度 2 → 换成两行 → 检查两行都是红色,内容分别是 ab 和 cd。
调用关系:它重点检验 slice_line_spans 能在 span 内部切片且不丢样式。
调用图:调用 1 个内部函数(word_wrap_line);外部调用 3 个(from, assert_eq!, vec!)。
tests::wrap_lines_applies_initial_indent_only_once1231–1246 ↗
fn wrap_lines_applies_initial_indent_only_once()
作用:测试多行输入时,首行缩进只出现在整个输出的第一行。第二个输入行不能重新用首行缩进。
数据流:构造两条输入行和带首/后续缩进的选项 → 调 word_wrap_lines → 检查第一行以 - 开头,其余都以后续缩进开头。
调用关系:它验证 word_wrap_lines 对多输入行的缩进切换逻辑。
调用图:调用 2 个内部函数(new, word_wrap_lines);外部调用 3 个(from, assert!, vec!)。
tests::wrap_lines_without_indents_is_concat_of_single_wraps1249–1254 ↗
fn wrap_lines_without_indents_is_concat_of_single_wraps()
作用:测试没有缩进时,多行换行就像把每行单独处理后拼起来。
数据流:输入 hello 和 world! → 宽度足够 → 调 word_wrap_lines → 检查输出仍是两行原文。
调用关系:它覆盖 word_wrap_lines 的简单路径。
调用图:调用 1 个内部函数(word_wrap_lines);外部调用 2 个(assert_eq!, vec!)。
tests::wrap_lines_borrowed_applies_initial_indent_only_once1257–1270 ↗
fn wrap_lines_borrowed_applies_initial_indent_only_once()
作用:测试借用版多行换行也只使用一次首行缩进。
数据流:准备 Line 数组并传迭代器引用 → 调 word_wrap_lines_borrowed → 检查第一行和后续行前缀。
调用关系:它对应 word_wrap_lines_applies_initial_indent_only_once,但覆盖借用返回版本。
调用图:调用 2 个内部函数(new, word_wrap_lines_borrowed);外部调用 2 个(from, assert!)。
tests::wrap_lines_borrowed_without_indents_is_concat_of_single_wraps1273–1278 ↗
fn wrap_lines_borrowed_without_indents_is_concat_of_single_wraps()
作用:测试借用版在无缩进时能保持简单输入不变。
数据流:输入两个 Line 引用 → 调 word_wrap_lines_borrowed → 检查输出纯文字列表等于原输入。
调用关系:它验证 word_wrap_lines_borrowed 的基本行为。
调用图:调用 1 个内部函数(word_wrap_lines_borrowed);外部调用 2 个(from, assert_eq!)。
tests::wrap_lines_accepts_borrowed_iterators1281–1286 ↗
fn wrap_lines_accepts_borrowed_iterators()
作用:测试 word_wrap_lines 可以接受常见集合里的行输入。调用者不必手动转换类型。
数据流:输入 Line 数组或向量 → 调 word_wrap_lines → 检查按宽度折出的文本列表。
调用关系:它覆盖 IntoLineInput 相关实现和 word_wrap_lines 的泛型入口。
调用图:调用 1 个内部函数(word_wrap_lines);外部调用 2 个(from, assert_eq!)。
tests::wrap_lines_accepts_str_slices1289–1294 ↗
fn wrap_lines_accepts_str_slices()
作用:测试 word_wrap_lines 可以直接处理 &str。这让 API 对普通字符串很友好。
数据流:输入字符串切片数组 → 宽度 12 换行 → 检查输出为预期三行。
调用关系:它覆盖 str::into_line_input 和 word_wrap_lines 的配合。
调用图:调用 1 个内部函数(word_wrap_lines);外部调用 1 个(assert_eq!)。
tests::line_height_counts_double_width_emoji1297–1302 ↗
fn line_height_counts_double_width_emoji()
作用:测试包含 emoji 的文字高度计算符合终端列宽。宽度不同,折行数量也应不同。
数据流:把三个 emoji 当作一行 → 分别用宽度 4、2、6 换行 → 检查输出行数为 2、3、1。
调用关系:它间接验证 word_wrap_line 对宽字符显示宽度的处理。
调用图:外部调用 1 个(assert_eq!)。
tests::word_wrap_does_not_split_words_simple_english1305–1321 ↗
fn word_wrap_does_not_split_words_simple_english()
作用:测试普通英文段落会优先按单词边界换行,而不是随便切断单词。
数据流:输入一段英文故事 → 调 word_wrap_lines_borrowed,宽度 40 → 把结果用换行拼起来 → 和预期排版全文比较。
调用关系:它覆盖标准换行的大段真实文本场景。
调用图:调用 1 个内部函数(word_wrap_lines_borrowed);外部调用 2 个(from, assert_eq!)。
tests::ascii_space_separator_with_no_hyphenation_keeps_url_intact1324–1340 ↗
fn ascii_space_separator_with_no_hyphenation_keeps_url_intact()
作用:测试只按空格分词、禁用连字符断词、禁用拆词时,长网址不会被拆开。
数据流:输入很长的 URL → 创建保护网址的换行选项 → 调 word_wrap_line → 检查只输出一整行 URL。
调用关系:它验证 url_preserving_wrap_options 所采用的底层设置确实有效,虽然这里手动构造选项。
调用图:调用 2 个内部函数(new, word_wrap_line);外部调用 2 个(from, assert_eq!)。
tests::text_contains_url_like_matches_expected_tokens1343–1360 ↗
fn text_contains_url_like_matches_expected_tokens()
作用:测试网址识别能认出常见正例,包括完整 URL、www 域名、localhost、IPv4 和带括号的 URL。
数据流:遍历一组应被识别的字符串 → 调 text_contains_url_like → 每个都断言为 true。
调用关系:它覆盖 text_contains_url_like 以及 is_url_like_token 下游多种判断。
调用图:外部调用 1 个(assert!)。
tests::text_contains_url_like_rejects_non_urls1363–1378 ↗
fn text_contains_url_like_rejects_non_urls()
作用:测试网址识别不会把普通文件路径、键值、带横线文字、普通点号词误当网址。
数据流:遍历一组非网址字符串 → 调 text_contains_url_like → 每个都断言为 false。
调用关系:它保证启发式判断足够保守,避免太多误报导致普通文本换行变差。
调用图:外部调用 1 个(assert!)。
tests::line_contains_url_like_checks_across_spans1381–1389 ↗
tests::line_has_mixed_url_and_non_url_tokens_detects_prose_plus_url1392–1395 ↗
tests::line_has_mixed_url_and_non_url_tokens_ignores_pipe_prefix1398–1401 ↗
tests::line_has_mixed_url_and_non_url_tokens_ignores_ordered_list_marker1404–1407 ↗
tests::text_contains_url_like_accepts_custom_scheme_with_separator1410–1412 ↗
fn text_contains_url_like_accepts_custom_scheme_with_separator()
作用:测试自定义协议链接也能被识别,比如 myapp://...。
数据流:输入自定义 scheme 的链接 → 调 text_contains_url_like → 断言为 true。
调用关系:它覆盖 is_absolute_url_like 解析失败后的 has_valid_scheme_prefix 兜底路径。
调用图:外部调用 1 个(assert!)。
tests::text_contains_url_like_rejects_invalid_ports1415–1418 ↗
fn text_contains_url_like_rejects_invalid_ports()
作用:测试非法端口不会被误判为网址。
数据流:输入端口过大和端口非数字的地址 → 调 text_contains_url_like → 都断言为 false。
调用关系:它验证 is_bare_url_like 会调用 is_valid_port 严格检查端口。
调用图:外部调用 1 个(assert!)。
tests::adaptive_wrap_line_keeps_long_url_like_token_intact1421–1429 ↗
fn adaptive_wrap_line_keeps_long_url_like_token_intact()
作用:测试智能换行会保持长网址完整,不因为宽度小就拆开。
数据流:输入很长的裸域名 URL → 宽度 20 调 adaptive_wrap_line → 检查只输出一行完整内容。
调用关系:它覆盖 adaptive_wrap_line 发现网址后走网址保护选项的路径。
调用图:调用 2 个内部函数(new, adaptive_wrap_line);外部调用 2 个(from, assert_eq!)。
tests::adaptive_wrap_line_preserves_default_behavior_for_non_url_tokens1432–1439 ↗
fn adaptive_wrap_line_preserves_default_behavior_for_non_url_tokens()
作用:测试没有网址时,智能换行不会改变默认行为。普通超长 token 仍可按默认规则拆开。
数据流:输入一个非网址长 token → 宽度 20 调 adaptive_wrap_line → 检查输出超过一行。
调用关系:它确保 adaptive_wrap_line 的“无网址就交给 word_wrap_line”路径正常。
调用图:调用 2 个内部函数(new, adaptive_wrap_line);外部调用 2 个(from, assert!)。
tests::adaptive_wrap_line_mixed_line_keeps_regular_words_intact1442–1453 ↗
fn adaptive_wrap_line_mixed_line_keeps_regular_words_intact()
作用:测试正文夹网址时,网址保持完整,普通词也按单词边界漂亮换行。
数据流:输入一段含 URL 的句子 → 宽度 36 调 adaptive_wrap_line → 把结果拼成多行字符串并和预期比较。
调用关系:它覆盖 adaptive_wrap_line 进入 mixed_url_wrap_line 的主路径。
调用图:调用 2 个内部函数(new, adaptive_wrap_line);外部调用 2 个(from, assert_eq!)。
tests::adaptive_wrap_line_mixed_line_wraps_long_non_url_token1456–1471 ↗
fn adaptive_wrap_line_mixed_line_wraps_long_non_url_token()
作用:测试混合行里过长的非网址 token 可以被拆开,而 URL 仍保留。
数据流:输入 see URL 超长普通token → 宽度 24 智能换行 → 检查某行含 URL,且没有任何行含完整未拆的长普通 token。
调用关系:它验证 mixed_url_wrap_ranges 和 split_mixed_url_word 的协作。
调用图:调用 2 个内部函数(new, adaptive_wrap_line);外部调用 3 个(from, assert!, format!)。
tests::adaptive_wrap_line_mixed_line_counts_leading_spaces_before_first_word1474–1486 ↗
fn adaptive_wrap_line_mixed_line_counts_leading_spaces_before_first_word()
作用:测试混合行开头空格也会占用首行宽度。否则首行可能放太多字符。
数据流:输入带多个前导空格、普通词和 URL 的行 → 设置后续缩进 → 调 adaptive_wrap_line → 检查前两行拆分结果。
调用关系:它覆盖 mixed_url_wrap_ranges 对 leading_space_width 的处理。
调用图:调用 2 个内部函数(new, adaptive_wrap_line);外部调用 2 个(from, assert_eq!)。
tests::adaptive_wrap_line_mixed_line_resplits_long_token_for_continuation_width1489–1505 ↗
fn adaptive_wrap_line_mixed_line_resplits_long_token_for_continuation_width()
作用:测试首行和后续行宽度不同的时候,长普通词会按后续行宽重新拆。
数据流:输入长普通词加 URL → 后续缩进占用宽度 → 智能换行 → 检查第二、三行按更窄宽度拆分。
调用关系:它验证 mixed_url_wrap_ranges 在换到后续行后会更新 line_limit,并可重新拆分待处理片段。
调用图:调用 2 个内部函数(new, adaptive_wrap_line);外部调用 2 个(from, assert_eq!)。
tests::map_owned_wrapped_line_to_range_recovers_on_non_prefix_mismatch1508–1513 ↗
fn map_owned_wrapped_line_to_range_recovers_on_non_prefix_mismatch()
作用:测试映射新造换行文本时,遇到中途不匹配也能返回已确认的前缀范围,而不是崩溃。
数据流:输入原文 hello world 和包装文本 helloX → 调 map_owned_wrapped_line_to_range → 检查返回 0..5。
调用关系:它直接覆盖 map_owned_wrapped_line_to_range 的容错分支。
调用图:调用 1 个内部函数(map_owned_wrapped_line_to_range);外部调用 1 个(assert_eq!)。
tests::borrowed_slice_range_rejects_slices_outside_source_text1516–1524 ↗
fn borrowed_slice_range_rejects_slices_outside_source_text()
作用:测试 borrowed_slice_range 会拒绝不来自原文内存的字符串切片,并验证兜底映射还能工作。
数据流:创建原文和另一个独立 String test → borrowed_slice_range 应返回 None → 再用 map_owned_wrapped_line_to_range 映射为 0..4。
调用关系:它覆盖 wrap_ranges 在遇到外部 owned 内容时需要走兜底映射的原因。
调用图:调用 1 个内部函数(map_owned_wrapped_line_to_range);外部调用 2 个(from, assert_eq!)。
tests::map_owned_wrapped_line_to_range_indent_coincides_with_source1527–1542 ↗
fn map_owned_wrapped_line_to_range_indent_coincides_with_source()
作用:测试人工缩进和原文开头字符相同时,映射器不会把缩进误当原文。
数据流:原文以 - 开头,包装文本也有 - 缩进 → 调 map_owned_wrapped_line_to_range 并提供 synthetic_prefix → 检查映射到完整 - item one。
调用关系:它防止 map_owned_wrapped_line_to_range 在缩进场景中过早消耗原文字节。
调用图:调用 1 个内部函数(map_owned_wrapped_line_to_range);外部调用 1 个(assert_eq!)。
tests::wrap_ranges_indent_prefix_coincides_with_source_char1545–1568 ↗
fn wrap_ranges_indent_prefix_coincides_with_source_char()
作用:端到端测试:当缩进前缀和原文开头相同,wrap_ranges 仍能覆盖完整原文。
数据流:输入以 - 开头的长文本和同样 - 的缩进选项 → 调 wrap_ranges → 按范围重建原文 → 检查重建结果等于原文。
调用关系:它把 wrap_ranges 和 map_owned_wrapped_line_to_range 放在一起验证。
调用图:调用 1 个内部函数(wrap_ranges);外部调用 3 个(new, assert!, assert_eq!)。
tests::map_owned_wrapped_line_to_range_repro_overconsumes_repeated_prefix_patterns1571–1594 ↗
fn map_owned_wrapped_line_to_range_repro_overconsumes_repeated_prefix_patterns()
作用:测试重复前缀模式下映射器不会多吃原文。这个场景容易把缩进和内容混淆。
数据流:构造 - - foo 和带 - 缩进的 textwrap 输出 → 调 map_owned_wrapped_line_to_range → 检查映射长度不超过去掉缩进后的行长。
调用关系:它是针对曾经或可能出现的过度消费问题的回归测试。
调用图:调用 1 个内部函数(map_owned_wrapped_line_to_range);外部调用 4 个(assert!, panic!, new, wrap)。
tests::wrap_ranges_recovers_with_non_space_indents1597–1634 ↗
fn wrap_ranges_recovers_with_non_space_indents()
作用:测试有非空格缩进时,wrap_ranges 仍能从换行库的 owned 行恢复原文范围。
数据流:输入一句话和 * 、两个空格缩进 → 先确认 textwrap 会产生 owned 行 → 调 wrap_ranges → 用范围重建原文 → 检查完全一致。
调用关系:它覆盖 wrap_ranges 对人工缩进和 owned 输出的恢复能力。
调用图:调用 1 个内部函数(wrap_ranges);外部调用 5 个(new, assert!, assert_eq!, new, wrap)。
tests::wrap_ranges_trim_handles_owned_lines_with_penalty_char1637–1656 ↗
fn wrap_ranges_trim_handles_owned_lines_with_penalty_char()
作用:测试 wrap_ranges_trim 能处理换行库插入的惩罚字符,比如末尾连字符,而不把它算进原文。
数据流:设置自定义拆词器让长 token 被拆成 owned 行 → 调 wrap_ranges_trim → 用范围拼回原文 → 检查等于原文且确实产生多段范围。
调用关系:它验证 wrap_ranges_trim 和 map_owned_wrapped_line_to_range 对人工插入字符的处理。
调用图:调用 1 个内部函数(wrap_ranges_trim);外部调用 4 个(new, assert!, assert_eq!, Custom)。
流式与可滚动换行视图
这些组件以增量方式应用换行,或将换行后的行缓存到滚动模型中,用于长文本和 diff 显示。
tui/src/live_wrap.rs源码 ↗
这个文件像一个“自动换行裁纸机”。输入可能是一小段一小段来的文字,也可能中间带换行符;它会按照终端能显示的宽度,把文字切成视觉上的行。这里的“宽度”不是字数,而是屏幕格子数:英文通常占 1 格,中文和表情常占 2 格。Row 表示已经切好的一行,并记录它是不是因为用户真的输入了换行而结束。RowBuilder 是主要工具,它先把当前还没结束的一行存在缓冲里,够宽了就切出去;遇到真正的换行就把这一行定稿。它还能在窗口宽度变化时,把已有内容重新切一遍。还有一个小工具 take_prefix_by_width,专门负责从字符串开头取出“不超过多少格”的部分。文件底部的测试用例检查英文、中文、表情、分片输入、换行和改宽度这些容易出错的情况。
Row::width13–15 ↗
fn width(&self) -> usize
作用:这个函数告诉调用者,这一行文字在终端里实际会占多少格。它不是简单数有几个字,而是按显示宽度算。
数据流:进去的是一个 Row 自己保存的 text → 它用 unicode_width 这类工具按终端显示规则计算宽度 → 出来的是一个数字,表示这行在屏幕上占几列;它不改动任何内容。
调用关系:它是 Row 这个结果对象上的便捷查询。测试里会用它确认重新换行之后,每一行都没有超过新的宽度。
RowBuilder::new30–36 ↗
fn new(target_width: usize) -> Self
作用:这个函数创建一个新的换行器,告诉它每行最多能放多宽。外部想开始处理一段实时文字时,通常先调用它。
数据流:进去的是目标宽度 target_width → 它把宽度至少修正为 1,避免出现 0 宽这种没法工作的情况,并准备空的当前行和空的结果列表 → 出来的是一个干净的 RowBuilder,可以开始接收文字。
调用关系:它是整个换行流程的起点。测试和其他实时显示场景会先用它建好 RowBuilder,之后再把文字交给 RowBuilder::push_fragment、RowBuilder::set_width 等函数处理。
调用图:被 6 处调用(fragmentation_invariance_long_token, newline_splits_rows, rewrap_on_width_change, rows_do_not_exceed_width_ascii, rows_do_not_exceed_width_emoji_cjk, live_001_commit_on_overflow);外部调用 2 个(new, new)。
RowBuilder::width38–40 ↗
fn width(&self) -> usize
作用:这个函数返回当前设置的目标行宽。调用者可以用它知道这个换行器现在按多宽在切行。
数据流:进去的是 RowBuilder 自己保存的 target_width → 它直接读出这个数字 → 出来的是当前每行允许的最大显示宽度;不会修改内部状态。
调用关系:它是一个简单的查看入口,通常用于外部代码检查当前配置,而不是推动换行流程。
RowBuilder::set_width42–55 ↗
fn set_width(&mut self, width: usize)
作用:这个函数用来改变每行最大宽度,比如终端窗口被用户拉宽或缩窄时。它会把已经积累的文字重新按新宽度切行,避免旧行宽造成显示不准。
数据流:进去的是新的 width,以及 RowBuilder 已经保存的 rows 和 current_line → 它先把宽度修正到至少 1,再把旧行和当前未完成行拼回一整段文字,保留真正的换行符,然后清空旧状态并重新喂给 RowBuilder::push_fragment → 出来的是按新宽度重新生成的行;内部 rows、current_line 和 target_width 都可能变化。
调用关系:它在窗口尺寸变化时使用。它自己不直接完成所有切行,而是把拼回来的文字交给 RowBuilder::push_fragment,让正常的输入处理流程重新跑一遍。
调用图:调用 1 个内部函数(push_fragment);外部调用 1 个(new)。
RowBuilder::push_fragment58–77 ↗
fn push_fragment(&mut self, fragment: &str)
作用:这个函数接收新来的文字片段。片段可以很短,也可以包含换行;它会把这些文字接到当前内容后面,并在需要时自动切成显示行。
数据流:进去的是一个 fragment 字符串 → 它先跳过空字符串,然后逐个检查里面有没有 '\n';遇到换行前的文字就追加到 current_line,再调用 RowBuilder::flush_current_line 把这一行定稿;剩下没有换行的尾巴会追加到 current_line,并调用 RowBuilder::wrap_current_line 尝试按宽度切出完整行 → 出来的是更新后的 rows 和 current_line。
调用关系:它是实时输入的主入口,也被 RowBuilder::set_width 用来重建内容。它遇到显式换行时把活交给 RowBuilder::flush_current_line,普通变长后的自动换行则交给 RowBuilder::wrap_current_line。
调用图:调用 2 个内部函数(flush_current_line, wrap_current_line);被 1 处调用(set_width)。
RowBuilder::end_line80–82 ↗
fn end_line(&mut self)
作用:这个函数手动告诉换行器:当前逻辑行结束了,就像输入了一个换行符。适合外部已经知道一行结束,但不想真的拼一个 '\n' 字符的情况。
数据流:进去的是 RowBuilder 当前积累的 current_line → 它调用 RowBuilder::flush_current_line,并标记这是一次明确的换行结束 → 出来的是当前行被整理进 rows,current_line 被清空。
调用关系:它是 RowBuilder::push_fragment 遇到 '\n' 时那套流程的手动版本。实际收尾工作交给 RowBuilder::flush_current_line。
调用图:调用 1 个内部函数(flush_current_line)。
RowBuilder::rows85–87 ↗
fn rows(&self) -> &[Row]
作用:这个函数返回已经正式产出的行,但不会拿走它们。调用者可以用它查看“已经确定”的换行结果。
数据流:进去的是 RowBuilder 内部保存的 rows → 它直接给出这些行的只读引用 → 出来的是一个不可修改的行列表视图;current_line 里还没定稿的部分不会包含在里面。
调用关系:它常用于检查或读取已完成的行。和 RowBuilder::display_rows 不同,它不会临时加上当前还没结束的那一行。
RowBuilder::display_rows90–99 ↗
fn display_rows(&self) -> Vec<Row>
作用:这个函数给出当前适合显示在屏幕上的所有行,包括还没正式结束的那一行。实时界面刷新时会需要这种“现在看起来是什么样”的结果。
数据流:进去的是 rows 和 current_line → 它先复制已经产出的 rows;如果 current_line 不为空,就临时把它也包装成一个 Row,并标记不是显式换行结束 → 出来的是一个新的 Vec<Row>;它不会改变 RowBuilder 自己保存的内容。
调用关系:它面向显示层,适合每次重画屏幕时调用。它不承担切行工作,只把已完成行和当前临时行合在一起给外部看。
RowBuilder::drain_commit_ready103–115 ↗
fn drain_commit_ready(&mut self, max_keep: usize) -> Vec<Row>
作用:这个函数把太旧、已经不需要留在实时显示区里的行取走。它常用于“只保留最后几行在屏幕上,其余提交到历史记录或上层存储”的场景。
数据流:进去的是 max_keep,以及当前 rows 加上可能存在的 current_line 的显示行数 → 它算出总显示行数是否超过保留上限;如果超过,就从 rows 的最前面移走一批已经确定的旧行,但不会移走 current_line → 出来的是被移走的旧 Row 列表;内部 rows 会减少。
调用关系:它位于实时输出和历史提交之间。调用者用它控制内存和屏幕保留量;它不会调用换行逻辑,只处理已经产出的行。
调用图:外部调用 2 个(new, with_capacity)。
RowBuilder::flush_current_line117–141 ↗
fn flush_current_line(&mut self, explicit_break: bool)
作用:这个内部函数把当前逻辑行收尾。它会先把太长的部分按宽度切好,再把最后一段标记为真正换行结束。
数据流:进去的是 explicit_break 标记,以及 current_line 中未处理的文字 → 它先调用 RowBuilder::wrap_current_line 切出所有能确定的自动换行;如果这是显式换行,就把剩余文字作为带 explicit_break 的 Row 放入 rows;如果刚好卡在宽度边界导致没有剩余文字,也会放入一个空的显式换行行,用来保留“这里确实有换行”这个事实 → 出来的是 rows 增加,current_line 被清空。
调用关系:它被 RowBuilder::push_fragment 在遇到 '\n' 时调用,也被 RowBuilder::end_line 调用。它自己会先请 RowBuilder::wrap_current_line 处理宽度问题,再负责换行这个语义。
调用图:调用 1 个内部函数(wrap_current_line);被 2 处调用(end_line, push_fragment);外部调用 2 个(new, swap)。
RowBuilder::wrap_current_line143–177 ↗
fn wrap_current_line(&mut self)
作用:这个内部函数负责自动换行:只要当前缓冲里的文字超过目标宽度,就从前面切出一行放进结果里。
数据流:进去的是 current_line 和 target_width → 它反复调用 take_prefix_by_width 找到能放进一行的前缀;如果后面还有剩余,就把前缀作为非显式换行的 Row 放入 rows,并把剩余部分留在 current_line 继续处理;如果整段都能放下,就保留在 current_line,等后续文字或换行再决定 → 出来的是 rows 可能增加,current_line 可能变短。
调用关系:它是自动换行的核心,被 RowBuilder::push_fragment 和 RowBuilder::flush_current_line 调用。真正按屏幕宽度切字符串的细活交给 take_prefix_by_width。
调用图:调用 1 个内部函数(take_prefix_by_width);被 2 处调用(flush_current_line, push_fragment)。
take_prefix_by_width182–202 ↗
fn take_prefix_by_width(text: &str, max_cols: usize) -> (String, &str, usize)
作用:这个函数从一段文字开头取出尽量多的内容,但保证显示宽度不超过给定列数。它解决了中文、表情和英文宽度不同导致不能按字节或字符硬切的问题。
数据流:进去的是 text 和 max_cols → 它从前往后看每个字符,计算这个字符在终端里占几格;如果再加一个字符会超宽,就停下;最后把文字分成 prefix 和 suffix,并记录 prefix 的显示宽度 → 出来的是三样东西:可放进当前行的前缀、剩下的后缀、前缀占用的列数;它不修改原字符串。
调用关系:它是更底层的切割工具,被 RowBuilder::wrap_current_line 用来生成实时行,也会被 render_lines 之类的渲染流程使用。它不关心换行语义,只关心“这一段最多能拿多少宽”。
调用图:被 2 处调用(render_lines, wrap_current_line);外部调用 2 个(new, width)。
tests::rows_do_not_exceed_width_ascii210–227 ↗
fn rows_do_not_exceed_width_ascii()
作用:这个测试确认普通英文文本会按指定宽度切行。它防止最基础的自动换行功能退化。
数据流:进去的是一段英文句子和宽度 10 的 RowBuilder → 测试把句子推入换行器,然后读取已经产出的 rows → 出来的是断言结果:前两行应当正好是预期的两段文字,且都是自动换行而不是显式换行。
调用关系:它通过 RowBuilder::new 创建换行器,再间接检查 RowBuilder::push_fragment 和内部换行流程的效果。失败时说明基础英文切行规则被改坏了。
调用图:调用 1 个内部函数(new);外部调用 1 个(assert_eq!)。
tests::rows_do_not_exceed_width_emoji_cjk230–245 ↗
fn rows_do_not_exceed_width_emoji_cjk()
作用:这个测试确认表情符号和中文这种较宽字符不会被当成普通英文来算。它保护终端显示对齐这一点。
数据流:进去的是包含两个表情、空格和中文的字符串,以及宽度 6 的 RowBuilder → 测试推入文字后查看 rows → 出来的是断言结果:第一行只能容纳两个表情和一个空格,中文因为再放会超宽所以留下来。
调用关系:它主要验证 take_prefix_by_width 经由 RowBuilder 的实际效果。若这个测试失败,说明宽字符处理可能会让界面错位。
调用图:调用 1 个内部函数(new);外部调用 1 个(assert_eq!)。
tests::fragmentation_invariance_long_token248–262 ↗
fn fragmentation_invariance_long_token()
作用:这个测试确认同一段文字一次性输入,和分成很多小片输入,最后切出来的正式行一样。实时输出经常是一段段来的,所以这个性质很重要。
数据流:进去的是同一个 26 个字母的字符串 → 测试先一次性推入一个 RowBuilder,再每 3 个字符分片推入另一个 RowBuilder → 出来的是断言结果:两个 RowBuilder 产出的 rows 必须完全相同。
调用关系:它用 RowBuilder::new 建两个换行器,重点检查 RowBuilder::push_fragment 的增量处理不会因为输入切片方式不同而产生不同结果。
调用图:调用 1 个内部函数(new);外部调用 1 个(assert_eq!)。
tests::newline_splits_rows265–273 ↗
fn newline_splits_rows()
作用:这个测试确认输入里的换行符真的会把行分开,并且会被记录成显式换行。它防止系统把用户原本的换行误当成普通自动折行。
数据流:进去的是字符串 "hello\nworld" 和宽度 10 的 RowBuilder → 测试推入文字后调用 RowBuilder::display_rows 查看可显示结果 → 出来的是断言结果:存在 explicit_break 为真的行,第一行是 hello,并且后面有以 world 开头的行。
调用关系:它覆盖 RowBuilder::push_fragment 遇到 '\n' 时调用 RowBuilder::flush_current_line 的流程,也检查 RowBuilder::display_rows 能把当前未结束的 world 一并显示出来。
调用图:调用 1 个内部函数(new);外部调用 2 个(assert!, assert_eq!)。
tests::rewrap_on_width_change276–284 ↗
fn rewrap_on_width_change()
作用:这个测试确认改变宽度后,已经处理过的内容会被重新按新宽度换行。它对应用户调整终端窗口大小的真实场景。
数据流:进去的是一段 11 个字符的文本,先用宽度 10 处理,再把宽度改成 5 → 测试遍历新 rows,并用 Row::width 检查每一行 → 出来的是断言结果:所有正式行宽度都不超过 5。
调用关系:它通过 RowBuilder::set_width 触发重排,间接验证 set_width 会把旧内容拼回去并重新交给 RowBuilder::push_fragment 处理。
cloud-tasks/src/scrollable_diff.rs源码 ↗
可以把这个文件想成一个小型的“卷轴阅读器”。外面给它一堆原始文本行,它会按当前窗口宽度把长行拆成多行,缓存起来,之后界面绘制时就不用每次重新算。它还记住当前滚到哪里、可见区域有多高、内容总共有多高,并在窗口变小或内容变少时自动把滚动位置拉回合法范围,避免出现空白或越界。这里特别注意了 Unicode 字符宽度,也就是中文、表情等字符在终端里可能占两格,不按普通字节数算,否则换行会错位。ScrollableDiff 是主要容器,ScrollViewState 是它里面记录滚动状态的小结构。整体上,它不负责真正画到屏幕上,只负责把“原始文字”变成“适合当前宽度显示的文字”,并提供滚动所需的信息。
ScrollViewState::clamp13–18 ↗
fn clamp(&mut self)
作用:把当前滚动位置限制在合理范围内,防止滚到内容之外。它就像电梯的限位器,不能让你按到不存在的楼层。
数据流:进去的是当前的 scroll、viewport_h 和 content_h → 它算出最多能往下滚多少 → 如果 scroll 太大,就把它改成最大允许值;如果没超,就不动。
调用关系:当窗口宽度变化导致内容重新换行后,ScrollableDiff::set_width 会调用它;当可见高度变化时,ScrollableDiff::set_viewport 也会调用它,保证界面尺寸变化后滚动位置仍然安全。
调用图:被 2 处调用(set_viewport, set_width)。
ScrollableDiff::new35–37 ↗
ScrollableDiff::set_content40–47 ↗
fn set_content(&mut self, lines: Vec<String>)
作用:替换要显示的原始文本内容。比如用户选中了另一段对话或 diff,就用它把旧内容换成新内容。
数据流:进去是一组原始字符串行 → 它保存这些行,清空旧的换行结果和来源索引,把内容高度归零,并标记下次必须重新按宽度换行 → 出来没有返回值,但对象内部已经换成新内容。
调用关系:它会被 apply_selection_to_fields 调用,通常发生在界面选择变化时。它只换内容,不立刻换行;后面需要 ScrollableDiff::set_width 根据当前宽度重新生成可显示的行。
调用图:被 1 处调用(apply_selection_to_fields)。
ScrollableDiff::set_width50–57 ↗
fn set_width(&mut self, width: u16)
作用:告诉这个文本视图当前能显示多宽,并在宽度变了时重新换行。没有它,长行可能会冲出屏幕,或者窗口变窄后显示乱掉。
数据流:进去的是新的宽度 width → 它先检查宽度是不是和上次一样;如果一样就直接结束,如果变了就保存新宽度、调用 rewrap 重新拆行,再调用 clamp 修正滚动位置 → 出来没有返回值,但缓存的显示行和内容高度会更新。
调用关系:它是内容适配窗口宽度的关键步骤。它把真正拆行的活交给 ScrollableDiff::rewrap,再让 ScrollViewState::clamp 做滚动位置收尾。
ScrollableDiff::set_viewport60–63 ↗
fn set_viewport(&mut self, height: u16)
作用:更新可见区域的高度,也就是屏幕上一次能看到多少行。窗口变高变矮时需要调用它。
数据流:进去的是新的高度 height → 它写入 state.viewport_h → 然后调用 clamp,确保当前滚动位置没有因为可见区域变化而越界。
调用关系:它和 ScrollableDiff::set_width 一起处理界面尺寸变化。set_width 管宽度和换行,set_viewport 管高度和滚动范围。
调用图:调用 1 个内部函数(clamp)。
ScrollableDiff::wrapped_lines66–68 ↗
fn wrapped_lines(&self) -> &[String]
作用:把已经按宽度拆好的显示行交给外面读取。界面绘制代码会用它来拿真正要显示的文本。
数据流:进去没有额外数据 → 它读取内部 wrapped 缓存 → 出来是一段只读的字符串列表引用,不会改动内容。
调用关系:style_conversation_lines 会调用它来给对话或 diff 的每一行做样式处理。它依赖之前已经通过 ScrollableDiff::set_width 生成好换行结果。
调用图:被 1 处调用(style_conversation_lines)。
ScrollableDiff::wrapped_src_indices70–72 ↗
fn wrapped_src_indices(&self) -> &[usize]
作用:告诉外面每一条“换行后的显示行”来自哪一条原始行。这样即使一条原文被拆成多行,外面也能追溯来源。
数据流:进去没有额外数据 → 它读取 wrapped_src_idx 缓存 → 出来是一组原始行编号的只读引用。
调用关系:style_conversation_lines 会和 wrapped_lines 一起使用它。显示层可以根据来源行判断该怎么上色、标记或取回原始内容。
调用图:被 1 处调用(style_conversation_lines)。
ScrollableDiff::raw_line_at74–76 ↗
fn raw_line_at(&self, idx: usize) -> &str
作用:按编号取回某一条原始文本行。它提供了一个安全兜底:编号不存在时返回空字符串,而不是直接出错。
数据流:进去的是原始行编号 idx → 它尝试从 raw 里取这一行 → 如果有就返回这行文本,如果没有就返回空字符串;内部数据不变。
调用关系:style_conversation_lines 会调用它,用来从换行后的显示行追溯到原始行。它常和 wrapped_src_indices 配合使用。
调用图:被 1 处调用(style_conversation_lines)。
ScrollableDiff::scroll_by79–82 ↗
fn scroll_by(&mut self, delta: i16)
作用:让视图上下滚动指定行数。正数通常表示往下,负数表示往上,并且不会滚出内容范围。
数据流:进去的是一个带正负号的滚动量 delta → 它把当前 scroll 加上 delta,再用 max_scroll 算出的最大值做限制 → 最后写回新的 scroll;没有返回值。
调用关系:ScrollableDiff::page_by 会直接把翻页动作交给它。它自己会调用 ScrollableDiff::max_scroll 来知道底部在哪里。
调用图:调用 1 个内部函数(max_scroll);被 1 处调用(page_by)。
ScrollableDiff::page_by85–87 ↗
fn page_by(&mut self, delta: i16)
作用:按“一页”的感觉滚动,其实就是把给定的行数交给 scroll_by。通常外面会传入接近可见高度的数,比如一屏少一行。
数据流:进去的是翻页距离 delta → 它不单独计算,只转交给 ScrollableDiff::scroll_by → 结果是滚动位置按这个距离变化,并被限制在合法范围内。
调用关系:它是给翻页按键准备的简化入口。真正的滚动和边界检查都由 ScrollableDiff::scroll_by 完成。
调用图:调用 1 个内部函数(scroll_by)。
ScrollableDiff::scroll_to_top89–91 ↗
fn scroll_to_top(&mut self)
作用:直接跳到内容最顶部。用户按 Home 键或类似操作时会用到这种能力。
数据流:进去没有额外数据 → 它把 state.scroll 设为 0 → 出来没有返回值,视图下次显示时会从第一行开始。
调用关系:它是一个直接操作滚动位置的小入口,不需要调用其他函数,因为顶部位置固定就是 0。
ScrollableDiff::scroll_to_bottom93–95 ↗
fn scroll_to_bottom(&mut self)
作用:直接跳到内容最底部。查看长日志、长 diff 的末尾时会用到它。
数据流:进去没有额外数据 → 它调用 max_scroll 算出最多能滚到哪里 → 把 state.scroll 设成这个值;没有返回值。
调用关系:它依赖 ScrollableDiff::max_scroll 来处理内容比窗口短、或窗口高度变化后的情况,避免自己算错底部位置。
调用图:调用 1 个内部函数(max_scroll)。
ScrollableDiff::percent_scrolled98–108 ↗
fn percent_scrolled(&self) -> Option<u8>
作用:计算当前大概看到了全文的百分之多少,可用于显示类似“75%”这样的滚动提示。内容不够长或尺寸未知时,它会选择不返回百分比。
数据流:进去没有额外数据 → 它读取内容高度、可见高度和当前滚动位置 → 如果无法计算或根本不需要滚动,就返回 None;否则按可见底部位置除以总高度算百分比,四舍五入并限制在 0 到 100 之间。
调用关系:它不调用本文件其他函数,也不改动状态。它通常会被界面层在绘制滚动提示时读取。
ScrollableDiff::max_scroll110–112 ↗
fn max_scroll(&self) -> u16
作用:算出最多可以往下滚多少行。它是防止滚动越界的基础小工具。
数据流:进去没有额外数据 → 它读取 content_h 和 viewport_h → 用“内容高度减去可见高度”得到最大滚动值;如果内容比窗口短,就安全地返回 0。
调用关系:ScrollableDiff::scroll_by 和 ScrollableDiff::scroll_to_bottom 都会调用它。它把“底部在哪里”这件事集中到一个地方,减少重复计算。
调用图:被 2 处调用(scroll_by, scroll_to_bottom)。
ScrollableDiff::rewrap114–175 ↗
fn rewrap(&mut self, width: u16)
作用:按给定宽度把原始文本重新拆成适合显示的多行。这是整个文件里最核心的排版步骤。
数据流:进去的是显示宽度 width,以及对象里已有的 raw 原始行 → 如果宽度为 0,就简单复制原始行;否则逐行处理,把制表符换成 4 个空格,按 Unicode 字符显示宽度累计列数,超过宽度时尽量在空白或标点处断开,实在不行就硬切 → 最后写入 wrapped、wrapped_src_idx,并更新 content_h。
调用关系:它只由 ScrollableDiff::set_width 调用,因为只有宽度变化或内容被标记需要重算时才应该重新换行。它会用到外部库提供的字符宽度计算,也会用标准工具把当前行取出或新建列表。
调用图:被 1 处调用(set_width);外部调用 6 个(new, width, width, new, matches!, take)。