docs/internal/unicode-width.md
This document lists all the places in the codebase where visual display width must be accounted for, particularly for:
src/primitives/display_width.rsThe central utility module for all visual width calculations. Uses the unicode-width crate.
Key functions:
char_width(c: char) -> usize - Returns visual width of a character (0, 1, or 2)str_width(s: &str) -> usize - Returns total visual width of a stringDisplayWidth trait - Extension trait for convenient .display_width() methodUsage: Import from crate::primitives::display_width::{char_width, str_width}
src/primitives/line_wrapping.rsLine wrapping must wrap based on visual width, not character count.
Key locations:
wrap_line() function (lines 111-127) - Tracks segment_visual_width using char_width(c)src/view/ui/split_rendering.rsThe main buffer rendering code that positions characters and cursors on screen.
Key locations:
visible_char_count += char_width(ch) - Tracks visual column during character iterationstr_width(&span.content) - Counts span width for cursor position detection (in test code)Important: The visible_char_count variable tracks visual columns, not character indices.
src/view/viewport.rsViewport scrolling calculations for horizontal scroll.
Key locations:
str_width(line_text) for line visual widthensure_column_visible_simple() uses visual widths for scroll calculationssrc/primitives/ansi.rsANSI escape code handling must exclude invisible escape sequences from width.
Key locations:
visible_char_count() function (lines ~302-319) - Returns visual width excluding ANSI codesstr_width() for fast path (no ANSI) and char_width() for character-by-character parsingsrc/view/ui/status_bar.rsStatus bar text truncation must account for visual width.
Key locations:
str_width() and char_width()str_width(&displayed_left)Truncation pattern:
let visual_width = str_width(&text);
if visual_width > max_width {
let mut width = 0;
let truncated: String = text
.chars()
.take_while(|ch| {
let w = char_width(*ch);
if width + w <= truncate_at {
width += w;
true
} else {
false
}
})
.collect();
format!("{}...", truncated)
}
src/view/ui/tabs.rsTab bar width calculations.
Key locations:
str_width(&tab_name_text) and str_width(close_text) for tab sizingsrc/view/ui/file_explorer.rsFile explorer tree view layout.
Key locations:
str_width(&node.entry.name) for name column widthstr_width(&size_text) for file size alignmentsrc/view/ui/file_browser.rsFile browser popup for Open File dialog.
Key locations:
str_width(label) + 2 for shortcut label width in navigation barsrc/view/ui/suggestions.rsAutocomplete/command palette suggestion rendering.
Key locations (all use visual-width-aware truncation):
Each column uses the truncation pattern shown above.
Some .chars().count() usages are intentionally character-based:
char_mappings vectors in split_rendering.rs (lines ~1003, ~2162, ~2271)
fancy_quote.chars().count() == 1)Byte length vs character count vs visual width:
s.len() - byte count (wrong for multi-byte UTF-8)s.chars().count() - character count (wrong for double-width)str_width(s) - visual width (correct for display)Truncation must use visual width:
// WRONG: truncates by character count
text.chars().take(max_width).collect()
// RIGHT: truncates by visual width
let mut width = 0;
text.chars().take_while(|ch| {
let w = char_width(*ch);
if width + w <= max_width { width += w; true } else { false }
}).collect()
Cursor positioning:
Line wrapping:
src/primitives/display_width.rs - Tests for width functionssrc/primitives/line_wrapping.rs - Tests including:
test_visual_width_calculationtest_wrap_line_double_width_characterstest_wrap_line_emoji_visual_widthtest_wrap_line_mixed_ascii_and_cjktest_chars_count_vs_visual_width_bug (regression test)src/primitives/line_wrapping.rs::proptests moduletests/e2e/multibyte_characters.rs - End-to-end tests for multi-byte character handlingunicode-width = "0.2" in Cargo.toml