mirror of
https://github.com/openai/codex.git
synced 2026-10-21 05:22:21 +00:00
Explain Mermaid rendering fallbacks in the TUI (#47572)
## Why Mermaid diagrams that cannot be rendered currently fall back to source without explaining why the preview is unavailable. ## What changed Show a dimmed notice above the preserved source when a completed Mermaid block uses unsupported syntax, exceeds renderer size limits, or does not fit the terminal width. Wrap the notice to the available width and preserve indentation inside lists and blockquotes. Leave unfinished fences and disabled Mermaid rendering without notices. ## Testing Add coverage for fallback reasons, dimmed styling, source preservation, nested indentation, and suppression of notices for unfinished fences and disabled rendering. Update the streaming resize snapshot to include the width notice. GitOrigin-RevId: 6ac7914b398c855e7aed3775ee0de70fb4484b78
This commit is contained in:
@@ -1018,11 +1018,11 @@ where
|
||||
{
|
||||
let indent =
|
||||
Self::spans_display_width(&self.prefix_spans(self.pending_marker_line));
|
||||
mermaid::render(
|
||||
Some(mermaid::render(
|
||||
&code,
|
||||
self.wrap_width.map(|width| width.saturating_sub(indent)),
|
||||
¤t_syntax_theme(),
|
||||
)
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
//! Bounded, theme-aware Mermaid previews for completed Markdown fences.
|
||||
//!
|
||||
//! Source remains owned by the transcript. Unsupported syntax, resource limits, and terminal
|
||||
//! overflow retain the original code block; diagrams never emit terminal control sequences.
|
||||
//! overflow retain the original code block with a reason; diagrams never emit terminal control
|
||||
//! sequences. Callers only render completed fences when Mermaid rendering is enabled.
|
||||
|
||||
use crate::render::highlight::foreground_style_for_scopes_with_theme;
|
||||
use crate::render::highlight::highlight_code_to_lines;
|
||||
use codex_mermaid::RenderError;
|
||||
use codex_mermaid::Role;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::text::Span;
|
||||
use std::ops::Range;
|
||||
@@ -32,12 +36,30 @@ pub(super) fn has_closing_fence(input: &str, range: Range<usize>, content_end: u
|
||||
>= opening_len
|
||||
}
|
||||
|
||||
pub(super) fn render(
|
||||
source: &str,
|
||||
width: Option<usize>,
|
||||
theme: &Theme,
|
||||
) -> Option<Vec<Line<'static>>> {
|
||||
let diagram = codex_mermaid::render_spans(source, width.unwrap_or(120)).ok()?;
|
||||
pub(super) fn render(source: &str, width: Option<usize>, theme: &Theme) -> Vec<Line<'static>> {
|
||||
let width = width.unwrap_or(120);
|
||||
let diagram = match codex_mermaid::render_spans(source, width) {
|
||||
Ok(diagram) => diagram,
|
||||
Err(error) => {
|
||||
let notice = match error {
|
||||
RenderError::Unsupported => {
|
||||
"This Mermaid diagram uses features the terminal renderer doesn't support."
|
||||
}
|
||||
RenderError::TooWide => {
|
||||
"This Mermaid diagram doesn't fit the current terminal width."
|
||||
}
|
||||
RenderError::Limit => {
|
||||
"This Mermaid diagram exceeds the terminal renderer's size limits."
|
||||
}
|
||||
};
|
||||
let mut lines = textwrap::wrap(notice, width.max(/*other*/ 1))
|
||||
.into_iter()
|
||||
.map(|line| Line::from(line.into_owned().dim()))
|
||||
.collect::<Vec<_>>();
|
||||
lines.extend(highlight_code_to_lines(source, "mermaid"));
|
||||
return lines;
|
||||
}
|
||||
};
|
||||
let node = foreground_style_for_scopes_with_theme(
|
||||
theme,
|
||||
&["entity.name.type", "support.type", "variable"],
|
||||
@@ -45,25 +67,23 @@ pub(super) fn render(
|
||||
.unwrap_or_else(|| Style::default().cyan());
|
||||
let edge = foreground_style_for_scopes_with_theme(theme, &["comment"])
|
||||
.unwrap_or_else(|| Style::default().dim());
|
||||
Some(
|
||||
diagram
|
||||
.into_iter()
|
||||
.map(|line| {
|
||||
Line::from(
|
||||
line.into_iter()
|
||||
.map(|span| {
|
||||
let style = match span.role {
|
||||
Role::Node => node,
|
||||
Role::Edge => edge,
|
||||
Role::Text => Style::default(),
|
||||
};
|
||||
Span::styled(span.text, style)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
diagram
|
||||
.into_iter()
|
||||
.map(|line| {
|
||||
Line::from(
|
||||
line.into_iter()
|
||||
.map(|span| {
|
||||
let style = match span.role {
|
||||
Role::Node => node,
|
||||
Role::Edge => edge,
|
||||
Role::Text => Style::default(),
|
||||
};
|
||||
Span::styled(span.text, style)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -61,24 +61,20 @@ flowchart TD
|
||||
let output = markdown_text(source, /*width*/ 100);
|
||||
assert!(output.starts_with('╭'));
|
||||
assert_snapshot!(output);
|
||||
assert_eq!(
|
||||
markdown_text(source, /*width*/ 40),
|
||||
markdown_text(
|
||||
assert!(
|
||||
markdown_text(source, /*width*/ 40).ends_with(&markdown_text(
|
||||
&source.replacen("mermaid", "unknown", /*count*/ 1),
|
||||
/*width*/ 40,
|
||||
)
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mermaid_unclosed_invalid_unsupported_and_wide_blocks_keep_source() {
|
||||
fn mermaid_unclosed_blocks_keep_source_without_notice() {
|
||||
for (source, width) in [
|
||||
("```mermaid\nflowchart LR\nA --> B\n", 80),
|
||||
("```mermaid\nflowchart TD\nA[unfinished\n", 80),
|
||||
("````mermaid\nflowchart LR\nA --> B\n```\n", 80),
|
||||
("```mermaid\nflowchart LR\nA[unfinished\n```", 80),
|
||||
("```mermaid\nflowchart TD\nA([unfinished]\n```", 80),
|
||||
("```mermaid\npie\n\"Cats\": 2\n```", 80),
|
||||
("```mermaid\nflowchart LR\nA[Request] --> B[Reply]\n```", 8),
|
||||
("> ```mermaid\n> flowchart LR\n> A --> B\n", 80),
|
||||
] {
|
||||
assert_eq!(
|
||||
@@ -89,6 +85,52 @@ fn mermaid_unclosed_invalid_unsupported_and_wide_blocks_keep_source() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mermaid_fallback_notices_preserve_source() {
|
||||
let mut cases = Vec::new();
|
||||
for (name, source, width) in [
|
||||
("invalid", "```mermaid\nflowchart LR\nA[unfinished\n```", 80),
|
||||
("unsupported", "```mermaid\npie\n\"Cats\": 2\n```", 80),
|
||||
(
|
||||
"too wide",
|
||||
"```mermaid\nflowchart LR\nA[Request] --> B[Reply]\n```",
|
||||
8,
|
||||
),
|
||||
(
|
||||
"limit",
|
||||
"```mermaid\nflowchart TD\nA[This label exceeds the forty column limit]\n```",
|
||||
80,
|
||||
),
|
||||
] {
|
||||
let rendered = render_markdown_text_with_width(source, Some(width));
|
||||
assert!(
|
||||
rendered.lines[0]
|
||||
.spans
|
||||
.iter()
|
||||
.filter(|span| !span.content.is_empty())
|
||||
.all(|span| {
|
||||
span.style
|
||||
.add_modifier
|
||||
.contains(ratatui::style::Modifier::DIM)
|
||||
}),
|
||||
"{rendered:?}"
|
||||
);
|
||||
let output = rendered.to_string();
|
||||
assert!(output.ends_with(&markdown_text(
|
||||
&source.replacen("mermaid", "unknown", /*count*/ 1),
|
||||
width,
|
||||
)));
|
||||
cases.push(format!("{name}\n{output}"));
|
||||
}
|
||||
assert_snapshot!(cases.join("\n\n---\n\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mermaid_fallback_notices_follow_nested_indentation() {
|
||||
let source = "> ```mermaid\n> pie\n> \"Cats\": 2\n> ```\n\n- Diagram:\n\n ```mermaid\n pie\n \"Cats\": 2\n ```\n";
|
||||
assert_snapshot!(markdown_text(source, /*width*/ 40));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mermaid_styles_follow_the_supplied_theme() {
|
||||
use two_face::theme::EmbeddedThemeName;
|
||||
@@ -118,7 +160,6 @@ fn mermaid_styles_follow_the_supplied_theme() {
|
||||
/*width*/ Some(40),
|
||||
theme,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
let styled = lines
|
||||
.into_iter()
|
||||
|
||||
@@ -53,6 +53,21 @@ fn disabled_renderers_preserve_source_independently() {
|
||||
insta::assert_snapshot!(snapshot.join("\n\n---\n\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_mermaid_keeps_unsupported_source_without_notice() {
|
||||
init(TuiRendering {
|
||||
mermaid: false,
|
||||
..Default::default()
|
||||
});
|
||||
let source = "```mermaid\npie\n\"Cats\": 2\n```\n";
|
||||
let rendered = render_markdown_text(source);
|
||||
init(TuiRendering::default());
|
||||
assert_eq!(
|
||||
rendered.to_string(),
|
||||
render_markdown_text(&source.replacen("mermaid", "unknown", /*count*/ 1)).to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_tables_keep_markdown_fences_and_cell_markup() {
|
||||
init(TuiRendering {
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
---
|
||||
source: tui/src/markdown_render/mermaid_tests.rs
|
||||
expression: "markdown_text(source, 40)"
|
||||
---
|
||||
> This Mermaid diagram uses features the
|
||||
> terminal renderer doesn't support.
|
||||
> pie
|
||||
> "Cats": 2
|
||||
|
||||
• Diagram:
|
||||
|
||||
This Mermaid diagram uses features the
|
||||
terminal renderer doesn't support.
|
||||
pie
|
||||
"Cats": 2
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
---
|
||||
source: tui/src/markdown_render/mermaid_tests.rs
|
||||
expression: "cases.join(\"\\n\\n---\\n\\n\")"
|
||||
---
|
||||
invalid
|
||||
This Mermaid diagram uses features the terminal renderer doesn't support.
|
||||
flowchart LR
|
||||
A[unfinished
|
||||
|
||||
---
|
||||
|
||||
unsupported
|
||||
This Mermaid diagram uses features the terminal renderer doesn't support.
|
||||
pie
|
||||
"Cats": 2
|
||||
|
||||
---
|
||||
|
||||
too wide
|
||||
This
|
||||
Mermaid
|
||||
diagram
|
||||
doesn't
|
||||
fit the
|
||||
current
|
||||
terminal
|
||||
width.
|
||||
flowchart LR
|
||||
A[Request] --> B[Reply]
|
||||
|
||||
---
|
||||
|
||||
limit
|
||||
This Mermaid diagram exceeds the terminal renderer's size limits.
|
||||
flowchart TD
|
||||
A[This label exceeds the forty column limit]
|
||||
+8
-1
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: tui/src/streaming/mermaid_tests.rs
|
||||
assertion_line: 78
|
||||
expression: "stages.join(\"\\n\\n---\\n\\n\")"
|
||||
---
|
||||
Before.
|
||||
@@ -45,6 +44,14 @@ After.
|
||||
|
||||
Before.
|
||||
|
||||
This
|
||||
Mermaid
|
||||
diagram
|
||||
doesn't
|
||||
fit the
|
||||
current
|
||||
terminal
|
||||
width.
|
||||
flowchart LR
|
||||
A[Request] --> B[Reply]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user