Files
codex/codex-rs/tui/src/streaming/math_tests.rs
T
Eric Traut a8964cb1ba Render standalone display math in the TUI (#45612)
## Why

The TUI renders supported inline math as Unicode but leaves display equations to ordinary Markdown rendering. Display fractions need spatial layout, and streamed equations must remain mutable while their closing delimiter is pending.

## What changed

- Render supported standalone `$$...$$` and `\[...\]` equations as Unicode, with stacked fractions and aligned baselines.
- Fall back to source for unsupported expressions, nested fractions, and spatial layouts that exceed the available width or occur inside lists or block quotes.
- Preview unfinished display math as wrapped source and hold it out of scrollback within the conversion budget, then render it when closed.
- Preserve display delimiter boundaries across incremental rendering and bound lookahead for rejected openers so shell `echo $$` does not retain the entire response.

## Testing

Add snapshots and regression tests for display layouts, narrow widths, literal fallback, streaming closure, resize reflow, raw source preservation, shell dollars, and bounded incremental rendering.

GitOrigin-RevId: ab54675f2e27a243f5d7165c76d9d7355f9fabd1
2026-09-15 05:44:41 +00:00

196 lines
6.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::StreamCore;
use super::render_source;
use crate::history_cell::HistoryRenderMode;
use crate::markdown::render_streaming_markdown_agent_with_links_and_cwd;
use pretty_assertions::assert_eq;
#[test]
fn unicode_math_stream_holds_display_until_closed_and_preserves_source() {
let cwd = std::env::temp_dir();
for mode in [HistoryRenderMode::Rich, HistoryRenderMode::Raw] {
for (delimiters, prefix) in [(("$$", "$$"), "Before.\n\n"), (("\\[", "\\]"), "Before.\n")] {
let mut stream = StreamCore::new(
Some(60),
&cwd,
mode,
/*inline_visualization_context*/ None,
);
let mut emitted = Vec::new();
let mut source = String::new();
for chunk in [
prefix,
delimiters.0,
"\n",
"\\frac{a+b}{c}\n",
"\n",
delimiters.1,
"\n\n",
"After $\\alpha^2$.\n",
] {
source.push_str(chunk);
stream.push_delta(chunk);
emitted.extend(stream.tick_batch(usize::MAX));
if mode == HistoryRenderMode::Rich && source.ends_with("\\frac{a+b}{c}\n") {
assert_eq!(
emitted,
render_source(
prefix,
Some(60),
&cwd,
mode,
/*inline_visualization_context*/ None
)
);
}
}
let (remaining, raw) = stream.finalize_remaining();
emitted.extend(remaining);
assert_eq!(raw, source);
assert_eq!(
emitted,
render_source(
&source,
Some(60),
&cwd,
mode,
/*inline_visualization_context*/ None
)
);
}
}
}
#[test]
fn unicode_math_raw_preview_preserves_line_on_newline() {
let cwd = std::env::temp_dir();
for open in ["$$", "\\["] {
let mut stream = StreamCore::new(
Some(12),
&cwd,
HistoryRenderMode::Raw,
/*inline_visualization_context*/ None,
);
let source = format!("{open}\\frac{{abcdefghijk}}{{lmnop}}");
stream.push_delta(&source);
let preview = stream.current_tail_lines();
assert_eq!(preview.len(), 1);
assert_eq!(preview[0].line.to_string(), source);
if open == "$$" {
insta::assert_snapshot!("unicode_math_raw_preview", preview[0].line.to_string());
}
stream.push_delta("\n");
let (lines, raw) = stream.finalize_remaining();
assert_eq!(lines, preview);
assert_eq!(raw, format!("{source}\n"));
}
}
#[test]
fn unicode_math_unfinished_display_is_visible_and_reflows() {
let cwd = std::env::temp_dir();
for (open, close) in [("$$", "$$"), (r"\[", r"\]")] {
let mut stream = StreamCore::new(
Some(80),
&cwd,
HistoryRenderMode::Rich,
/*inline_visualization_context*/ None,
);
for (chunk, expected) in [(&open[..1], &open[..1]), (&open[1..], open)] {
stream.push_delta(chunk);
assert_eq!(stream.current_tail_lines()[0].line.to_string(), expected);
}
stream.push_delta("\n \\frac{|a+b+c+d+e+f|}{g+h}");
for width in [80, 20, 80] {
stream.set_width(Some(width));
let lines = stream.current_tail_lines();
assert!(
lines
.iter()
.any(|line| line.line.to_string().contains("frac"))
);
assert!(lines.iter().all(|line| line.line.width() <= width));
if open == "$$" && width == 20 {
insta::assert_snapshot!(
"unicode_math_live_source_narrow",
lines
.iter()
.map(|line| line.line.to_string())
.collect::<Vec<_>>()
.join("\n")
);
}
}
stream.push_delta(&format!("\n{close}\n"));
let (lines, source) = stream.finalize_remaining();
assert_eq!(
lines,
render_source(
&source,
Some(80),
&cwd,
HistoryRenderMode::Rich,
/*inline_visualization_context*/ None
)
);
}
}
#[test]
fn unicode_math_rejected_display_keeps_its_closer_and_following_text() {
let cwd = std::env::temp_dir();
for (open, close) in [("$$", "$$"), ("\\[", "\\]")] {
for (prefix, body) in [
("", "x".repeat(/*n*/ 5000)),
("", "`x`".to_owned()),
("", format!("`{close}\nx`")),
("", "[x](https://example.com)".to_owned()),
("Equation: ", "x^2".to_owned()),
] {
let mut render = super::super::render::StreamingRender::new();
let mut source = String::new();
for chunk in [
format!("{prefix}{open}\n\n"),
format!("{body}\n\n"),
format!("{close}\n\n"),
"After $\\alpha$.\n\n".to_owned(),
"$$\\beta$$\n".to_owned(),
] {
source.push_str(&chunk);
render.append(
&source,
&chunk,
Some(40),
&cwd,
HistoryRenderMode::Rich,
/*inline_visualization_context*/ None,
);
let expected = render_streaming_markdown_agent_with_links_and_cwd(
&source,
Some(40),
Some(&cwd),
);
assert_eq!(
(&render.lines, render.pending_math_start),
(&expected.lines, expected.pending_math_start)
);
if source.len() > 4096 || !prefix.is_empty() {
assert_eq!(render.pending_math_start, None);
}
}
assert!(
render
.lines
.iter()
.any(|line| line.line.to_string().contains("After α."))
);
assert!(
render
.lines
.iter()
.any(|line| line.line.to_string().contains('β'))
);
assert_eq!(render.pending_math_start, None);
}
}
}