use unicode_width::UnicodeWidthChar;
#[inline]
fn cell_width(ch: char, visual: usize, tab_w: usize) -> usize {
if ch == '\t' {
tab_w - (visual % tab_w)
} else {
ch.width().unwrap_or(1)
}
}
pub fn visual_col_to_char_col(line: &str, visual_col: usize, tab_width: usize) -> usize {
let tab_w = if tab_width == 0 { 1 } else { tab_width };
if visual_col == 0 {
return 0;
}
let mut visual = 0usize;
for (i, ch) in line.chars().enumerate() {
let advance = cell_width(ch, visual, tab_w);
if advance == 0 {
continue;
}
if visual + advance > visual_col {
return i;
}
visual += advance;
}
line.chars().count()
}
pub fn char_col_to_visual_col(line: &str, char_col: usize, tab_width: usize) -> usize {
let tab_w = if tab_width == 0 { 1 } else { tab_width };
let mut visual = 0usize;
for (i, ch) in line.chars().enumerate() {
if i == char_col {
return visual;
}
visual += cell_width(ch, visual, tab_w);
}
visual
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_exact_visual_col() {
assert_eq!(visual_col_to_char_col("hello", 0, 4), 0);
assert_eq!(visual_col_to_char_col("hello", 1, 4), 1);
assert_eq!(visual_col_to_char_col("hello", 3, 4), 3);
assert_eq!(visual_col_to_char_col("hello", 4, 4), 4);
}
#[test]
fn tab_expansion_click_inside_run_lands_on_tab_char() {
let line = "x\tyz";
assert_eq!(visual_col_to_char_col(line, 1, 4), 1); assert_eq!(visual_col_to_char_col(line, 2, 4), 1); assert_eq!(visual_col_to_char_col(line, 3, 4), 1); assert_eq!(visual_col_to_char_col(line, 4, 4), 2); assert_eq!(visual_col_to_char_col(line, 5, 4), 3); }
#[test]
fn tab_at_column_boundary() {
let line = "abcd\tefg";
assert_eq!(visual_col_to_char_col(line, 4, 4), 4); assert_eq!(visual_col_to_char_col(line, 5, 4), 4); assert_eq!(visual_col_to_char_col(line, 7, 4), 4); assert_eq!(visual_col_to_char_col(line, 8, 4), 5); }
#[test]
fn past_eol_clamps_to_char_count() {
assert_eq!(visual_col_to_char_col("hi", 99, 4), 2);
assert_eq!(visual_col_to_char_col("x", 100, 4), 1);
}
#[test]
fn empty_line_always_zero() {
assert_eq!(visual_col_to_char_col("", 0, 4), 0);
assert_eq!(visual_col_to_char_col("", 5, 4), 0);
}
#[test]
fn multibyte_single_cell_chars() {
let line = "αβγδε"; assert_eq!(visual_col_to_char_col(line, 0, 4), 0);
assert_eq!(visual_col_to_char_col(line, 2, 4), 2);
assert_eq!(visual_col_to_char_col(line, 4, 4), 4);
assert_eq!(visual_col_to_char_col(line, 5, 4), 5); }
#[test]
fn tab_width_one_treats_tab_as_single_cell() {
let line = "a\tb";
assert_eq!(visual_col_to_char_col(line, 0, 1), 0); assert_eq!(visual_col_to_char_col(line, 1, 1), 1); assert_eq!(visual_col_to_char_col(line, 2, 1), 2); }
#[test]
fn tab_width_zero_treated_as_one() {
let line = "a\tb";
assert_eq!(visual_col_to_char_col(line, 0, 0), 0);
assert_eq!(visual_col_to_char_col(line, 1, 0), 1);
assert_eq!(visual_col_to_char_col(line, 2, 0), 2);
}
#[test]
fn leading_tab_then_text() {
let line = "\thello";
assert_eq!(visual_col_to_char_col(line, 0, 4), 0); assert_eq!(visual_col_to_char_col(line, 3, 4), 0); assert_eq!(visual_col_to_char_col(line, 4, 4), 1); assert_eq!(visual_col_to_char_col(line, 8, 4), 5); }
#[test]
fn cjk_char_col_to_visual_col() {
let line = "ab世界cd";
assert_eq!(char_col_to_visual_col(line, 0, 4), 0); assert_eq!(char_col_to_visual_col(line, 1, 4), 1); assert_eq!(char_col_to_visual_col(line, 2, 4), 2); assert_eq!(char_col_to_visual_col(line, 3, 4), 4); assert_eq!(char_col_to_visual_col(line, 4, 4), 6); assert_eq!(char_col_to_visual_col(line, 5, 4), 7); assert_eq!(char_col_to_visual_col(line, 6, 4), 8); }
#[test]
fn cjk_visual_col_to_char_col() {
let line = "ab世界cd";
assert_eq!(visual_col_to_char_col(line, 0, 4), 0); assert_eq!(visual_col_to_char_col(line, 1, 4), 1); assert_eq!(visual_col_to_char_col(line, 2, 4), 2); assert_eq!(visual_col_to_char_col(line, 3, 4), 2); assert_eq!(visual_col_to_char_col(line, 4, 4), 3); assert_eq!(visual_col_to_char_col(line, 5, 4), 3); assert_eq!(visual_col_to_char_col(line, 6, 4), 4); assert_eq!(visual_col_to_char_col(line, 7, 4), 5); assert_eq!(visual_col_to_char_col(line, 8, 4), 6); }
#[test]
fn emoji_is_two_cells() {
let line = "a🦀b";
assert_eq!(char_col_to_visual_col(line, 1, 4), 1);
assert_eq!(char_col_to_visual_col(line, 2, 4), 3);
assert_eq!(char_col_to_visual_col(line, 3, 4), 4);
assert_eq!(visual_col_to_char_col(line, 1, 4), 1); assert_eq!(visual_col_to_char_col(line, 2, 4), 1); assert_eq!(visual_col_to_char_col(line, 3, 4), 2); }
#[test]
fn variation_selector_is_zero_width() {
let line = "a\u{2764}\u{fe0f}b";
assert_eq!(char_col_to_visual_col(line, 1, 4), 1); assert_eq!(char_col_to_visual_col(line, 2, 4), 2); assert_eq!(char_col_to_visual_col(line, 3, 4), 2); assert_eq!(char_col_to_visual_col(line, 4, 4), 3);
assert_eq!(visual_col_to_char_col(line, 1, 4), 1);
assert_eq!(visual_col_to_char_col(line, 2, 4), 3);
}
#[test]
fn combining_mark_is_zero_width() {
let line = "ae\u{301}b";
assert_eq!(char_col_to_visual_col(line, 0, 4), 0); assert_eq!(char_col_to_visual_col(line, 1, 4), 1); assert_eq!(char_col_to_visual_col(line, 2, 4), 2); assert_eq!(char_col_to_visual_col(line, 3, 4), 2); assert_eq!(char_col_to_visual_col(line, 4, 4), 3);
assert_eq!(visual_col_to_char_col(line, 0, 4), 0); assert_eq!(visual_col_to_char_col(line, 1, 4), 1); assert_eq!(visual_col_to_char_col(line, 2, 4), 3); assert_eq!(visual_col_to_char_col(line, 3, 4), 4); }
#[test]
fn tab_stop_measured_from_visual_not_char_column() {
let line = "世\tx";
assert_eq!(char_col_to_visual_col(line, 0, 4), 0); assert_eq!(char_col_to_visual_col(line, 1, 4), 2); assert_eq!(char_col_to_visual_col(line, 2, 4), 4); assert_eq!(char_col_to_visual_col(line, 3, 4), 5);
assert_eq!(visual_col_to_char_col(line, 0, 4), 0); assert_eq!(visual_col_to_char_col(line, 1, 4), 0); assert_eq!(visual_col_to_char_col(line, 2, 4), 1); assert_eq!(visual_col_to_char_col(line, 3, 4), 1); assert_eq!(visual_col_to_char_col(line, 4, 4), 2); }
#[test]
fn wide_char_before_tab_shifts_the_stop() {
let line = "ab世\tx";
assert_eq!(char_col_to_visual_col(line, 3, 4), 4); assert_eq!(char_col_to_visual_col(line, 4, 4), 8); assert_eq!(visual_col_to_char_col(line, 8, 4), 4); assert_eq!(visual_col_to_char_col(line, 7, 4), 3); }
#[test]
fn control_char_is_one_cell_matching_the_renderer() {
let line = "a\u{1}b";
assert_eq!(char_col_to_visual_col(line, 1, 4), 1); assert_eq!(char_col_to_visual_col(line, 2, 4), 2); assert_eq!(visual_col_to_char_col(line, 1, 4), 1);
assert_eq!(visual_col_to_char_col(line, 2, 4), 2);
}
#[test]
fn round_trip_char_to_visual_to_char() {
for line in ["ab世界cd", "世\tx", "a🦀b", "\tab世", "abcd"] {
let mut visual = 0usize;
for (i, ch) in line.chars().enumerate() {
let v = char_col_to_visual_col(line, i, 4);
assert_eq!(v, visual, "start col for char {i} of {line:?}");
let w = if ch == '\t' {
4 - (visual % 4)
} else {
unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1)
};
if w > 0 {
assert_eq!(
visual_col_to_char_col(line, v, 4),
i,
"round trip for char {i} of {line:?}"
);
}
visual += w;
}
}
}
}