- 桌面应用
- 开发者工具
- 人工智能
- AI 应用
- AI Agent
- 代码智能体
【免费下载链接】warp
Warp is an agentic development environment, born out of the terminal.
Mermaid 图表在 Warp 的 Markdown 渲染面(markdown viewer、可编辑 plan 等)中,过去可能长期停留在"Rendering Mermaid diagram…"占位状态,用户无法区分"正在渲染""语法错误""SVG 转换失败"还是"渲染器挂死"。本文基于 Warp 开源仓库中的产品规格与配套技术规格(specs/APP-4267/PRODUCT.md、specs/APP-4267/TECH.md),完整讲解该问题的解决思路:如何以显式失败 Callout 替换无限加载占位、如何保留底层 Mermaid 源码为唯一事实来源,并对照 crates/editor/src/render/element/mermaid.rs、crates/editor/src/content/mermaid_diagram.rs 与 crates/warpui_core/src/elements/gui/image.rs 等源码,深入剖析其渲染链路、失败态布局与超时机制。读完你可以掌握 Warp 富文本渲染层中"异步图片资产 + 状态机 + 备用元素"这套模式,并了解如何在类似渲染面中复刻"失败/超时显式提示"的能力。
一、问题定义:Mermaid 渲染为何会"永久卡住"
在 Warp 中,Mermaid 渲染被实现为叠加在富文本 Markdown 渲染器之上的异步图片资产(async image asset)。产品规格 specs/APP-4267/PRODUCT.md 的 Problem 一节描述得非常具体:
The markdown viewer can show a large Mermaid placeholder that remains stuck on "Rendering Mermaid diagram…" indefinitely.
也就是说,用户会看到一块**高度巨大(默认占位高度为 10 个行高)**的加载占位,且它可能无限期存在。用户无法判断图表是:
- 仍在渲染(只是慢);
- 因 Mermaid 语法无效而失败;
- 在 SVG/图片转换管线中失败;
- 内部渲染器挂起(renderer hang)。
这种"不确定状态"对用户体验的伤害在于:占位占据了大量版面,却不提供任何可操作信息。产品规格给出的核心诉求是:任何渲染失败或超时,都必须替换为一个清晰、可读的失败 Callout,同时保留底层的 Mermaid 源码。
技术规格 specs/APP-4267/TECH.md 进一步指出根因:资产缓存(AssetCache)只在后台 fetch future 真正 resolve 时才把资产从Loading推进到Loaded或FailedToLoad;而 Mermaid 渲染在 async fetch 体内是同步调用(mermaid_to_svg::render_mermaid_to_svg),一旦该调用不返回,资产就会永远停留在AssetState::Loading,占位也就永远不消失。这正是"超时"路径必须单独实现的直接依据。
二、整体目标与非目标:先划定边界
产品规格 specs/APP-4267/PRODUCT.md 以 Goals / Non-goals 的形式划定了本次迭代的边界,这组边界对理解实现取舍至关重要。
2.1 目标(Goals)
| 目标 | 说明 |
|---|---|
| 显式失败态 | 对失败或超时的 Mermaid 图显示清晰、可读的失败状态 |
| 保留成功渲染行为 | 成功渲染的 Mermaid 图仍以 SVG 正常展示,不因本次改动回归 |
| 保留源码事实 | 作者的 Mermaid Markdown 仍是编辑、复制、存储、导出的唯一事实来源 |
| 轻量实现 | 首个迭代复用现有 markdown/code-block 样式,不引入专用编辑器或重做 UI |
2.2 非目标(Non-goals)
- 不做专用 Mermaid 源码编辑器;
- 不做重试(retry)、"复制原文"(copy raw)、"打开源码"(open raw source)等交互控件;
- 不改Mermaid 语法支持、图表主题、布局算法或已渲染 SVG 保真度;
- 不改普通图片的加载行为(超出 Mermaid 失败所需的最小实现面)。
这些非目标直接解释了为什么最终实现的失败 Callout "没有任何交互控件、不产生键盘焦点"(对应 PRODUCT.md Behavior 12),也解释了为什么后续计划中才出现 retry 与 copy source 等 Follow-ups。
三、Behavior 规格逐条解读:14 条行为契约
产品规格的 Behavior 一节定义了 14 条行为契约,是整个实现的验收标准。下面逐条精读并与源码对应:
- 初始占位不变:当渲染面中出现 fenced Mermaid 代码块且 Mermaid 渲染启用时,先显示现有 pending 态 "Rendering Mermaid diagram…"。
- 进入失败态的三种条件:
- Mermaid→SVG 渲染返回错误;
- SVG/图片转换管线返回错误;
- 图在 Mermaid 渲染超时(初始为 10 秒)内仍未 resolve。
- 替换占位:进入失败态后,用可见 Callout 替换 "Rendering Mermaid diagram…",文案为"Failed to render Mermaid diagram"。
- 就地展示:Callout 出现在同一 diagram/code-block 容器内,使用主题派生的文本、边框、背景色,与现有渲染代码/Markdown 块一致。
- 紧凑高度:能确定渲染已永久失败时,Callout 不保留大占位高度;若只是 UI 超时(底层仍 unresolved),Callout 仍需在原占位区域内可见。
- 成功路径:成功渲染仍展示 SVG,而非 Callout。
- 超时后成功:若超时后最终 resolve 成功,用成功 SVG 替换超时 Callout,不得为同一渲染尝试切回 loading 占位。
- 源码变更 = 新渲染尝试:Mermaid 源码改变后视为新尝试,旧的 success/failure/timeout 状态不迁移到新图。
- 多图独立:同一文档中多个 Mermaid 图互不影响,一张失败不影响其它图的 loading/failure 状态。
- 源码即事实:复制、存储、导出、分享、撤销/重做、编辑都作用于原始 fenced Mermaid markdown,而非 Callout 文本。
- 禁用路径不变:Mermaid 渲染禁用或刻意显示原始代码块时,现有 raw code block 行为不变。
- 无交互、可读:Callout 无交互控件、不产生键盘焦点,但必须作为普通可见文本被无障碍(text-based accessibility)表面读取。
- 选区一致:围绕失败图的选择行为与现渲染 Mermaid 一致——用户操作该块时不应误选/误复制失败消息,而是选到作者 Mermaid 源码。
- 不打扰全局:失败态不弹出用户可见 toast 或 modal,错误仅局限在图表块内,文档其余部分保持可读。
对照源码:第 3、4 条对应 crates/editor/src/render/element/mermaid.rs 中timeout_notice与failure_notice两个文本元素;第 5 条对应 crates/editor/src/content/mermaid_diagram.rs 中的FAILED_MERMAID_HEIGHT_LINE_MULTIPLIER = 2.0;第 7 条对应 crates/warpui_core/src/elements/gui/image.rspaint中AssetState::Loaded分支会清空全部备用元素(before_load_element = None; failed_to_load_element = None; load_timeout = None;);第 14 条对应技术规格 §6 Logging 的约束。
四、渲染链路解剖:从 fenced block 到 SVG
要理解失败 Callout 落在哪里,先要理解 Mermaid 图的完整渲染链路。技术规格 specs/APP-4267/TECH.md 的 Context 部分给出了精确的调用链,可与源码一一对应:
Markdown 解析 └─ crates/editor/src/content/text.rs (721-729) 当 FeatureFlag::MarkdownMermaid 启用时, fenced Mermaid 代码块被分类为 CodeBlockType::Mermaid └─ crates/editor/src/content/edit.rs (671-726) 将 styled Mermaid 代码块转换为 LayoutTask::MermaidDiagram 并调用 mermaid_diagram_layout └─ crates/editor/src/content/edit.rs (1031-1067) 将该 layout task 转换为 BlockItem::MermaidDiagram (保留源块内容长度) └─ crates/editor/src/content/mermaid_diagram.rs mermaid_asset_source() 创建 AssetSource::Async, fetch future 调用 mermaid_to_svg::render_mermaid_to_svg └─ crates/editor/src/render/element/mermaid.rs RenderableMermaidDiagram 用 Image::new(asset_source, ...) .contain().before_load(placeholder) .on_load_failure(failure_notice) .on_load_timeout(10s, timeout_notice) 渲染其中两个关键实现值得展开:
4.1 资产源:按源码内容哈希去重
crates/editor/src/content/mermaid_diagram.rs 的mermaid_asset_source用DefaultHasher对源码字符串哈希生成稳定 ID(configured:{:x}),并以AsyncAssetId::new::<MermaidDiagramAsset>标识资产类型。这样:
- 相同源码的 Mermaid 图在资产缓存中共享同一份资产;
- fetch future 中执行
mermaid_to_svg::render_mermaid_to_svg(&source, None),返回的 SVG 字节写入缓存; - 源码一旦变化,哈希变化 → 新资产 ID → 新的渲染尝试,这正对应 Behavior 8"源码变更 = 新渲染尝试"。
4.2 图片元素的三态备用渲染
crates/warpui_core/src/elements/gui/image.rs 是共享图片元素,本次为它增加了两个可选的"备用元素":
before_load_element(已有):加载中/被驱逐/失败时兜底渲染;failed_to_load_element(新增,on_load_failure注入):AssetState::FailedToLoad时的专用渲染;load_timeout(新增,on_load_timeout注入):AssetState::Loading超过指定时长后的渲染。
paint中的状态机(image.rs 486-531 行):
| 资产状态 | 渲染行为 |
|---|---|
Loading | 请求 repaint;未超时画before_load_element,超时画 timeout 元素 |
Evicted | 清除超时记录,画before_load_element |
FailedToLoad | 清除超时记录;有failed_to_load_element则画之,否则回退before_load_element |
Loaded | 清除超时记录,丢弃全部备用元素,绘制真实图片 |
注意Loaded分支"丢弃备用元素"正是 Behavior 7 的实现基础:超时后若最终加载成功,直接替换为 SVG,绝不切回 loading 占位。
4.3 Mermaid 渲染元素:三种文案的组装
crates/editor/src/render/element/mermaid.rs 的RenderableMermaidDiagram::layout组装了三个文本元素:
- loading 占位:"Rendering Mermaid diagram…",使用
code_text字体族/字号/行高比 +placeholder_color,soft_wrap(false); - 失败通知(
failure_notice):"Error rendering Mermaid diagram. Please check syntax.",soft_wrap(true),由.on_load_failure(...)注入; - 超时通知(
timeout_notice):"Failed to render Mermaid diagram",soft_wrap(false),由.on_load_timeout(MERMAID_RENDER_TIMEOUT, ...)注入,其中MERMAID_RENDER_TIMEOUT = Duration::from_secs(10)(mermaid.rs 第 14 行)。
随后Image::new(asset_source, CacheOption::BySize).contain().before_load(placeholder).on_load_failure(failure_notice).on_load_timeout(10s, timeout_notice),并在paint中保持原有的圆角背景(8px 圆角)、code_border边框、code_background背景、选区覆盖与光标绘制——这正是 Behavior 4 要求的"与现有渲染代码块一致"的视觉来源。
五、失败态的紧凑布局:两行行高取代十行占位
产品规格 Behavior 5 要求失败图不得继续占用巨大的默认占位高度。技术规格 §1 与源码 crates/editor/src/content/mermaid_diagram.rs 给出两个常量:
const DEFAULT_MERMAID_HEIGHT_LINE_MULTIPLIER: f32 = 10.0; const FAILED_MERMAID_HEIGHT_LINE_MULTIPLIER: f32 = 2.0;布局函数mermaid_diagram_config的尺寸决策如下:
Loaded(SVG 已加载):用mermaid_diagram_size读取 SVG 内禀尺寸,按height = max_width * intrinsic_height / intrinsic_width保持宽高比铺满最大宽度(成功渲染路径不变);FailedToLoad:使用base_line_height * FAILED_MERMAID_HEIGHT_LINE_MULTIPLIER(2 个行高)的紧凑高度;Loading/Evicted:保持默认 pending 高度(10 个行高)。
对应函数mermaid_diagram_fallback_height_line_multiplier直接按资产状态返回 2.0 或 10.0。技术规格明确要求"保持 helper 只负责布局尺寸,不把渲染元素状态塞进 content model"——即布局层与渲染层解耦。
六、超时机制的实现细节与陷阱
这是本设计中最微妙的部分。技术规格 §3 给出了三个明确的技术约束,全部在源码中落地:
6.1 不能只用 future 级 timeout
技术规格明确警告:不要只依赖warpui::r#async::FutureExt::with_timeout作为唯一超时机制。因为该 helper 只在被包裹 futureyield时才生效,而 Mermaid 渲染在 async fetch 体内是同步调用,一旦卡死就不会 yield,future 级 timeout 形同虚设。超时必须放在渲染/呈现层(presentation layer),即Image元素的 paint 状态机里。
6.2 按资产源键记录开始时间,而非实例字段
image.rs 用全局IMAGE_LOAD_TIMEOUT_STARTED_AT: Mutex<HashMap<u64, Instant>>以"资产源哈希键"记录加载开始时刻(load_timeout_key= 对self.source哈希)。原因正如技术规格所说:富文本布局可能在该元素重建之前就完成,若把超时起点只存在单个Image实例字段上,元素重建会导致超时计时被重置。以资产源为键的全局记录保证了同一资产的超时起点在元素重建后仍然存活(对应测试"Timeout start time survives rebuilding an Image element for the same asset source")。
6.3 paint 中的超时判定与重绘调度
loading_backup_element_kind的判定逻辑:
- 未配置
load_timeout:始终返回BeforeLoad,行为与改动前完全一致(opt-in,保护既有调用方); - 已配置:
elapsed = now - load_started_at;若elapsed >= timeout返回LoadTimeout元素;否则返回BeforeLoad并返回timeout - elapsed作为剩余时长。
paint的Loading分支中,未超时会ctx.repaint_after(remaining)调度一次定时重绘,到点后自动切换为 timeout 元素——这就是"10 秒后占位被替换"的运行时机制。Evicted/FailedToLoad/Loaded三个分支都会clear_load_timeout_started_at(),避免状态已迁移后残留计时数据。
七、源码与选区语义:失败 Callout 不是文档内容
技术规格 §5 是产品规格 Behavior 10、13 的实现保障:
不改
BlockItem::MermaidDiagram的内容长度、markdown 序列化、复制行为、hidden block 处理或编辑器选区语义。失败 Callout 只是该图块的渲染态呈现,不是新的文档内容。
在 crates/editor/src/content/edit.rs 中,BlockItem::MermaidDiagram保留了源块内容长度;crates/editor/src/render/element/mermaid.rs 的paint仍按块的真实start_char_offset/end_char_offset判断选区与光标绘制(selected判定、is_selection_head光标绘制)。因此用户在失败图块上操作时,选中的仍是作者 Mermaid 源码,而非 Callout 文本——这与 Behavior 13 完全一致。
八、测试与验证策略
技术规格 §Testing and validation 给出了完整的验证矩阵,仓库中已实现对应测试:
8.1 单元测试(已存在)
crates/editor/src/content/mermaid_diagram_tests.rs 覆盖:
loading_mermaid_layout_uses_default_height:Loading 态使用默认 10 行高;failed_mermaid_layout_uses_compact_height:FailedToLoad态使用 2 行高紧凑高度(用一个不存在的AssetSource::Raw触发FailedToLoad);mermaid_asset_source_renders_frontmatter_formatting_directives:验证资产源能正确处理带 frontmatter 配置(theme、themeVariables、fontFamily、fontSize、flowchart 曲线与节点间距)的源码,渲染结果包含<svg且尊重#ff0000、Inter等配置。
crates/warpui_core/src/elements/gui/image_tests.rs 下的测试覆盖Image的失败元素渲染、无失败元素时回退 before-load、超时后切换、超时起点在元素重建后保留等场景;crates/editor/src/content/edit_tests.rs 保留了成功图的test_layout_mermaid_block_uses_loaded_svg_aspect_ratio覆盖,确保成功路径不回归。
8.2 手工/集成验证清单
技术规格列出的手工验证点包括:
- 在 markdown viewer 或可编辑 plan 中渲染合法 Mermaid 图,确认仍产出 SVG;
- 渲染非法 Mermaid 语法,确认显示 "Failed to render Mermaid diagram" 而非卡在 loading 占位;
- 用永不 resolve 的测试资产源模拟卡死渲染,确认 10 秒后 loading 文本被替换;
- 编辑 Mermaid 源码产生新渲染尝试,确认旧失败态不保留;
- 同一文档两张图可独立呈现成功与失败;
- 选择/复制/导出文档时仍保留 fenced Mermaid 源码而非 Callout 文本。
8.3 建议命令
# 聚焦编辑器侧 Mermaid 布局/渲染测试 cargo nextest run --no-fail-fast --workspace mermaid # 聚焦共享 Image 元素测试 cargo nextest run --no-fail-fast --workspace image # 实现完成后的编译检查 cargo check技术规格还提示:可并行推进——一个 agent 负责共享Image的失败/超时行为与单测,另一个 agent 负责 Mermaid 特有的布局与渲染接线,最后统一做手工 UI 验证。
九、风险与权衡:三个已知边界
技术规格 §Risks and mitigations 明确记录了三个实现取舍,读者在评估该方案时应当知晓:
9.1 超时不取消底层工作
UI 级超时只是呈现层的兜底:它避免了无限占位,但不会取消已在后台执行器上运行的同步 Mermaid 渲染。如果真实场景中出现卡死渲染占用执行器容量,需后续跟进渲染器级取消或进程隔离(已列入 Follow-ups)。
9.2 共享 Image 行为必须 opt-in
新增的on_load_failure/on_load_timeout必须显式启用。未配置的既有调用方行为应与改动前逐字节一致(image.rs 的loading_backup_element_kind在无 timeout 配置时直接返回BeforeLoad即为此保证),避免 Mermaid 的失败/超时行为波及全应用普通图片。
9.3 超时态布局高度的取舍
已知FailedToLoad的资产可在下一次 layout pass 使用紧凑布局;但UI 超时期间资产仍为Loading,因此首个迭代中超时 Callout 仍可能占据默认占位高度(10 行高)。技术规格明确将"紧凑超时布局"列为后续可选项——首个迭代优先解决"无限 loading 文本"这个用户可见问题,紧凑排版不阻塞发布。
十、后续计划(Follow-ups)
产品规格与技术规格为后续迭代预留了清晰方向:
- 重试入口(retry affordance):若用户需要在不编辑、不重开文档的前提下重试同一份源码;
- "复制 Mermaid 源码"入口(copy raw):失败态下需要一个显式的源码逃生通道;
- 渲染器级取消或隔离:若确认卡死渲染确实占用后台执行器容量。
这些后续项都建立在本次"失败 Callout + 超时呈现"的地基之上,且均不会改变"作者 Mermaid 源码为唯一事实来源"的核心契约。
结语:一种可复用的"异步资产失败呈现"模式
综合产品规格、技术规格与仓库实现,specs/APP-4267 为我们展示了一个清晰的模式:异步图片资产 + 资产状态机(Loading/Evicted/FailedToLoad/Loaded)+ 按状态的备用元素 + 按资产源键的呈现级超时。这套设计既让 Mermaid 用户告别"永远转圈",又通过紧凑失败高度、主题化 Callout、源码/选区语义保留与 opt-in 的ImageAPI,把对既有功能的冲击降到最低。若你正在为 Markdown 渲染器或富文本编辑器设计类似的图表/图片失败态,Warp 的这套实现(mermaid_diagram.rs、mermaid.rs、image.rs)是值得对照参考的范本。
- 桌面应用
- 开发者工具
- 人工智能
- AI 应用
- AI Agent
- 代码智能体
【免费下载链接】warp
Warp is an agentic development environment, born out of the terminal.
相关推荐
3分钟解决Hugo-PaperMod渲染失败:从报错到修复的实战指南
3分钟解决Hugo PaperMod渲染失败:从报错到修复的实战指南 Hugo PaperMod是一款快速、简洁且响应式的Hugo主题,深受博客搭建者喜爱。但新
前端Warp 垂直标签页 Summary Tab Item 模式:从 Pane 级行渲染到 Tab 级聚合的技术设计与实现
Warp 垂直标签页 Summary Tab Item 模式:从 Pane 级行渲染到 Tab 级聚合的技术设计与实现 导读 Warp 的垂直标签页(Verti
桌面应用开发者工具人工智能AI 应用AI Agent代码智能体Warp 记事本 Markdown 的 Mermaid 图表渲染:从代码块识别、异步 SVG 渲染到特性开关的完整实现解析
Warp 记事本 Markdown 的 Mermaid 图表渲染:从代码块识别、异步 SVG 渲染到特性开关的完整实现解析 本文围绕 Warp 开源仓库中 sp
桌面应用开发者工具人工智能AI 应用AI Agent代码智能体
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考