docs/internal/visual-layout-unification.md
The codebase has inconsistent handling of visual column calculations across different flows:
Rendering/cursor detection uses ViewLine.char_mappings indexed by visual column
Mouse clicks use char_mappings[visual_col] for O(1) lookup
MoveUp/MoveDown use str_width() and byte_offset_at_visual_column() on raw buffer
[, 2, m as width 1 each!)// Per-line mappings that support all operations
pub struct LineMappings {
// Per-CHARACTER (indexed by char position in text)
// Length == text.chars().count()
pub char_source_bytes: Vec<Option<usize>>,
// Per-VISUAL-COLUMN (indexed by visual column)
// Length == visual width of line
pub visual_to_char: Vec<usize>,
}
// primitives/visual_layout.rs
/// Calculate visual width handling ANSI escapes, tabs, zero-width chars
pub fn visual_width(s: &str, start_col: usize) -> usize;
/// Convert byte offset to visual column (ANSI-aware, tab-aware)
pub fn byte_to_visual_col(s: &str, byte_offset: usize) -> usize;
/// Convert visual column to byte offset (ANSI-aware, tab-aware)
pub fn visual_col_to_byte(s: &str, visual_col: usize) -> usize;
/// Build complete per-char and per-visual-col mappings
pub fn build_line_mappings(text: &str, source_bytes: &[Option<usize>]) -> LineMappings;
| Operation | How | Complexity |
|---|---|---|
| Mouse click at visual col V | char_idx = visual_to_char[V] | |
byte = char_source_bytes[char_idx] | O(1) | |
| Cursor render at char I | byte = char_source_bytes[I] | O(1) |
| MoveUp/Down | Use shared byte_to_visual_col() and visual_col_to_byte() | O(n) per line |
Note: MoveUp/Down is O(n) but only processes one line at a time, and navigation is infrequent compared to rendering.
primitives/visual_layout.rsbuild_line_mappings() functionbyte_to_visual_col() and visual_col_to_byte() with ANSI/tab supportViewLine in view_pipeline.rschar_mappings: Vec<Option<usize>> (per visual col)char_source_bytes: Vec<Option<usize>> (per char)visual_to_char: Vec<usize> (per visual col)char_styles but make it per-char instead of per-visual-colview_pipeline.rs iteratorsplit_rendering.rschar_source_bytes[char_index] instead of char_mappings[col_offset]push_span_with_map: update to build per-char mappingsinput.rs mouse click handlingscreen_to_buffer_position: use visual_to_char[col] then char_source_bytes[char_idx]ViewLineMapping struct to use new formatactions.rs MoveUp/MoveDownstr_width() with ANSI-aware byte_to_visual_col()byte_offset_at_visual_column() with ANSI-aware visual_col_to_byte()display_width.rs functions can remain for simple cases