diff --git a/src/tui_main/render_impl.rs b/src/tui_main/render_impl.rs index cd559cfd..37639430 100644 --- a/src/tui_main/render_impl.rs +++ b/src/tui_main/render_impl.rs @@ -2321,11 +2321,81 @@ mod tests { s } + /// #615: is `c` a Private Use Area codepoint? Nerd Font icons live in + /// the BMP PUA (`U+E000`-`U+F8FF`) and the Supplementary PUA-A/B planes + /// (`U+F0000`-`U+FFFFD`, `U+100000`-`U+10FFFD`). + fn is_pua(c: char) -> bool { + matches!(c as u32, 0xE000..=0xF8FF | 0xF0000..=0xFFFFD | 0x100000..=0x10FFFD) + } + + /// #615: Unicode classifies PUA codepoints — exactly the range Nerd + /// Font icons occupy — as "Ambiguous" width per UAX #11. Terminals and + /// width-calculation libraries are explicitly permitted to render them + /// as either 1 or 2 columns, and `quadraui::tui::cell_width` picks + /// between the two based on `unicode-width`'s classification of the + /// specific codepoint (see quadraui's `cell_width` doc comment) — a + /// classification that is not guaranteed identical across otherwise + /// dependency-compatible builds. A miscounted glyph shifts the + /// whitespace run touching it by exactly one column (statusline/tab-bar + /// segments elsewhere on the row are drawn at their own fixed + /// coordinates and are unaffected — see the CI-vs-local diff in #615, + /// where only the padding immediately around the glyph moved). + /// + /// A snapshot asserting the exact padding directly touching one of + /// these glyphs is therefore asserting on something the rendering code + /// cannot itself guarantee — see #615 for the full investigation + /// (toolchain version and `unicode-width` patch/version were both tried + /// locally and ruled out; the ambiguity is inherent to the codepoints, + /// not a resolvable dependency bug). Collapse only the whitespace run + /// immediately before and/or after each PUA glyph to a single space; + /// every other whitespace run on the row — including gutter padding, + /// indentation, and column alignment not touching a glyph — is left + /// untouched and still asserted byte-for-byte. + fn desensitize_glyph_width(line: &str) -> String { + if !line.chars().any(is_pua) { + return line.to_string(); + } + let chars: Vec = line.chars().collect(); + let mut out = String::with_capacity(line.len()); + let mut i = 0; + while i < chars.len() { + if chars[i] == ' ' { + let start = i; + let mut end = i; + while end < chars.len() && chars[end] == ' ' { + end += 1; + } + let touches_pua = (start > 0 && is_pua(chars[start - 1])) + || (end < chars.len() && is_pua(chars[end])); + if touches_pua { + out.push(' '); + } else { + out.extend(&chars[start..end]); + } + i = end; + } else { + out.push(chars[i]); + i += 1; + } + } + out + } + + /// Join rendered lines into the snapshot string, desensitizing rows + /// that contain nerd-font/PUA glyphs (#615). + fn snapshot_text(lines: &[String]) -> String { + lines + .iter() + .map(|l| desensitize_glyph_width(l)) + .collect::>() + .join("\n") + } + #[test] fn snapshot_normal_mode() { let e = test_engine("fn main() {\n println!(\"hello\");\n}\n"); let lines = render_tui(&e, 60, 12); - snap_settings().bind(|| insta::assert_snapshot!("normal_mode", lines.join("\n"))); + snap_settings().bind(|| insta::assert_snapshot!("normal_mode", snapshot_text(&lines))); } #[test] @@ -2333,7 +2403,7 @@ mod tests { let mut e = test_engine("hello world\n"); e.handle_key("i", Some('i'), false); let lines = render_tui(&e, 60, 12); - snap_settings().bind(|| insta::assert_snapshot!("insert_mode", lines.join("\n"))); + snap_settings().bind(|| insta::assert_snapshot!("insert_mode", snapshot_text(&lines))); } #[test] @@ -2344,7 +2414,7 @@ mod tests { e.handle_key("l", Some('l'), false); } let lines = render_tui(&e, 60, 12); - snap_settings().bind(|| insta::assert_snapshot!("visual_selection", lines.join("\n"))); + snap_settings().bind(|| insta::assert_snapshot!("visual_selection", snapshot_text(&lines))); } #[test] @@ -2355,7 +2425,7 @@ mod tests { e.handle_key("e", Some('e'), false); e.handle_key("t", Some('t'), false); let lines = render_tui(&e, 60, 12); - snap_settings().bind(|| insta::assert_snapshot!("command_line", lines.join("\n"))); + snap_settings().bind(|| insta::assert_snapshot!("command_line", snapshot_text(&lines))); } #[test] @@ -2363,7 +2433,7 @@ mod tests { let mut e = test_engine("left pane content\n"); e.open_editor_group(crate::core::window::SplitDirection::Vertical); let lines = render_tui(&e, 80, 16); - snap_settings().bind(|| insta::assert_snapshot!("split_panes", lines.join("\n"))); + snap_settings().bind(|| insta::assert_snapshot!("split_panes", snapshot_text(&lines))); } #[test] @@ -2371,7 +2441,7 @@ mod tests { let mut e = test_engine("alpha\nbeta\ngamma\ndelta\nepsilon\n"); e.settings.line_numbers = crate::core::settings::LineNumberMode::Absolute; let lines = render_tui(&e, 60, 12); - snap_settings().bind(|| insta::assert_snapshot!("line_numbers", lines.join("\n"))); + snap_settings().bind(|| insta::assert_snapshot!("line_numbers", snapshot_text(&lines))); } // ── :help render regression tests (#596) ───────────────────────────────── diff --git a/src/tui_main/snapshots/command_line.snap b/src/tui_main/snapshots/command_line.snap index 100cce8e..95452b9b 100644 --- a/src/tui_main/snapshots/command_line.snap +++ b/src/tui_main/snapshots/command_line.snap @@ -1,10 +1,10 @@ --- source: src/tui_main/render_impl.rs -expression: "lines.join(\"\\n\")" +expression: snapshot_text(&lines) --- - 󰍜 1: [No Name] × 󰤲  ⋯ + 󰍜 1: [No Name] × 󰤲  ⋯ ▎ -  buffer content +  buffer content    @@ -12,5 +12,5 @@ expression: "lines.join(\"\\n\")" - COMMAND [No Name] 󰘖 utf-8 LF Spaces: 4 Ln 1, Col 1 -  :set + COMMAND [No Name] 󰘖 utf-8 LF Spaces: 4 Ln 1, Col 1 +  :set diff --git a/src/tui_main/snapshots/insert_mode.snap b/src/tui_main/snapshots/insert_mode.snap index fd963a56..6bc014d8 100644 --- a/src/tui_main/snapshots/insert_mode.snap +++ b/src/tui_main/snapshots/insert_mode.snap @@ -1,10 +1,10 @@ --- source: src/tui_main/render_impl.rs -expression: "lines.join(\"\\n\")" +expression: snapshot_text(&lines) --- - 󰍜 1: [No Name] × 󰤲  ⋯ + 󰍜 1: [No Name] × 󰤲  ⋯ ▎ -  hello world +  hello world    @@ -12,5 +12,5 @@ expression: "lines.join(\"\\n\")" - INSERT [No Name] 󰘖 utf-8 LF Spaces: 4 Ln 1, Col 1 + INSERT [No Name] 󰘖 utf-8 LF Spaces: 4 Ln 1, Col 1  diff --git a/src/tui_main/snapshots/line_numbers.snap b/src/tui_main/snapshots/line_numbers.snap index a87ac2d4..2d346423 100644 --- a/src/tui_main/snapshots/line_numbers.snap +++ b/src/tui_main/snapshots/line_numbers.snap @@ -1,16 +1,16 @@ --- source: src/tui_main/render_impl.rs -expression: "lines.join(\"\\n\")" +expression: snapshot_text(&lines) --- - 󰍜 1: [No Name] × 󰤲  ⋯ + 󰍜 1: [No Name] × 󰤲  ⋯ ▎ -  1 alpha -  2 beta -  3 gamma -  4 delta -  5 epsilon +  1 alpha +  2 beta +  3 gamma +  4 delta +  5 epsilon - NORMAL [No Name] 󰘖 utf-8 LF Spaces: 4 Ln 1, Col 1 + NORMAL [No Name] 󰘖 utf-8 LF Spaces: 4 Ln 1, Col 1  diff --git a/src/tui_main/snapshots/normal_mode.snap b/src/tui_main/snapshots/normal_mode.snap index 312485d5..ced3feb4 100644 --- a/src/tui_main/snapshots/normal_mode.snap +++ b/src/tui_main/snapshots/normal_mode.snap @@ -1,16 +1,16 @@ --- source: src/tui_main/render_impl.rs -expression: "lines.join(\"\\n\")" +expression: snapshot_text(&lines) --- - 󰍜 1: [No Name] × 󰤲  ⋯ + 󰍜 1: [No Name] × 󰤲  ⋯ ▎ -  -fn main() { -  │ println!("hello"); -  } +  -fn main() { +  │ println!("hello"); +  }   - NORMAL [No Name] 󰘖 utf-8 LF Spaces: 4 Ln 1, Col 1 + NORMAL [No Name] 󰘖 utf-8 LF Spaces: 4 Ln 1, Col 1  diff --git a/src/tui_main/snapshots/split_panes.snap b/src/tui_main/snapshots/split_panes.snap index 4161e9be..92928209 100644 --- a/src/tui_main/snapshots/split_panes.snap +++ b/src/tui_main/snapshots/split_panes.snap @@ -1,14 +1,14 @@ --- source: src/tui_main/render_impl.rs -expression: "lines.join(\"\\n\")" +expression: snapshot_text(&lines) --- - 󰍜 1: [No Name] × ⋯ 1: [No Name] × 󰤲  ⋯ -▎ │ -  left pane content │ left pane content -  │ -  │ -  │ -  │ + 󰍜 1: [No Name] × ⋯ 1: [No Name] × 󰤲  ⋯ +▎ │ +  left pane content │ left pane content +  │ +  │ +  │ +  │ │ │ │ @@ -17,4 +17,4 @@ expression: "lines.join(\"\\n\")" │ │ [No Name] Ln 1, Col 1│ NORMAL [No Name] Ln 1, Col 1 -  Editor split +  Editor split diff --git a/src/tui_main/snapshots/visual_selection.snap b/src/tui_main/snapshots/visual_selection.snap index db705afc..4fbae1d4 100644 --- a/src/tui_main/snapshots/visual_selection.snap +++ b/src/tui_main/snapshots/visual_selection.snap @@ -1,16 +1,16 @@ --- source: src/tui_main/render_impl.rs -expression: "lines.join(\"\\n\")" +expression: snapshot_text(&lines) --- - 󰍜 1: [No Name] × 󰤲  ⋯ + 󰍜 1: [No Name] × 󰤲  ⋯ ▎ -  select this text -  and this too +  select this text +  and this too    - VISUAL [No Name] 󰘖 utf-8 LF Spaces: 4 Ln 1, Col 11 + VISUAL [No Name] 󰘖 utf-8 LF Spaces: 4 Ln 1, Col 11 