diff --git a/codex-rs/tui/src/markdown_render.rs b/codex-rs/tui/src/markdown_render.rs index e61062f9a5..257c25cf34 100644 --- a/codex-rs/tui/src/markdown_render.rs +++ b/codex-rs/tui/src/markdown_render.rs @@ -63,7 +63,7 @@ use std::ops::Range; use std::path::Path; use std::path::PathBuf; use std::sync::LazyLock; -use unicode_width::UnicodeWidthStr; +use unicode_width::UnicodeWidthChar; use url::Url; struct MarkdownStyles { @@ -1172,7 +1172,7 @@ where if word_count > 0 { total_words += word_count; total_cells += 1; - total_cell_width += plain.width(); + total_cell_width += Self::terminal_cell_width(&plain); } } @@ -1182,7 +1182,7 @@ where total_words as f64 / total_cells as f64 }; let avg_cell_width = if total_cells == 0 { - header_plain.width() as f64 + Self::terminal_cell_width(&header_plain) as f64 } else { total_cell_width as f64 / total_cells as f64 }; @@ -1504,7 +1504,10 @@ where #[inline] fn spans_display_width(spans: &[Span<'_>]) -> usize { - spans.iter().map(|span| span.content.width()).sum() + spans + .iter() + .map(|span| Self::terminal_cell_width(&span.content)) + .sum() } #[inline] @@ -1523,7 +1526,17 @@ where #[inline] fn longest_token_width(text: &str) -> usize { - text.split_whitespace().map(str::width).max().unwrap_or(0) + text.split_whitespace() + .map(Self::terminal_cell_width) + .max() + .unwrap_or(0) + } + + #[inline] + fn terminal_cell_width(text: &str) -> usize { + text.chars() + .map(|ch| UnicodeWidthChar::width(ch).unwrap_or(0)) + .sum() } fn push_inline_style(&mut self, style: Style) { @@ -2176,6 +2189,15 @@ mod tests { } } + #[test] + fn terminal_cell_width_matches_xterm_table_cases() { + assert_eq!(W::terminal_cell_width("🅰️"), 1); + assert_eq!(W::terminal_cell_width("↔️"), 1); + assert_eq!(W::terminal_cell_width("1️⃣"), 1); + assert_eq!(W::terminal_cell_width("👩‍💻"), 4); + assert_eq!(W::terminal_cell_width("🔤"), 2); + } + // ===== Column-metrics unit tests ===== #[test] diff --git a/codex-rs/tui/src/markdown_render_tests.rs b/codex-rs/tui/src/markdown_render_tests.rs index 24e6e41935..0a788ad491 100644 --- a/codex-rs/tui/src/markdown_render_tests.rs +++ b/codex-rs/tui/src/markdown_render_tests.rs @@ -4,7 +4,7 @@ use ratatui::text::Line; use ratatui::text::Span; use ratatui::text::Text; use std::path::Path; -use unicode_width::UnicodeWidthStr; +use unicode_width::UnicodeWidthChar; use crate::markdown_render::COLON_LOCATION_SUFFIX_RE; use crate::markdown_render::HASH_LOCATION_SUFFIX_RE; @@ -16,6 +16,12 @@ fn render_markdown_text_for_cwd(input: &str, cwd: &Path) -> Text<'static> { render_markdown_text_with_width_and_cwd(input, /*width*/ None, Some(cwd)) } +fn terminal_cell_width(text: &str) -> usize { + text.chars() + .map(|ch| UnicodeWidthChar::width(ch).unwrap_or(0)) + .sum() +} + #[test] fn empty() { assert_eq!(render_markdown_text(""), Text::default()); @@ -1480,13 +1486,15 @@ fn table_with_emoji_sequence_cells_has_consistent_display_width() { }) .map(String::as_str) .collect(); - let expected_width = table_lines + let first_table_line = table_lines .first() - .expect("expected unicode table to render as a box") - .width(); + .expect("expected unicode table to render as a box"); + let expected_width = terminal_cell_width(first_table_line); assert!( - table_lines.iter().all(|line| line.width() == expected_width), + table_lines + .iter() + .all(|line| terminal_cell_width(line) == expected_width), "expected every rendered table row to have width {expected_width}: {table_lines:?}" ); assert!(