use crate::diff::text_range::{cell_width_of, floor_char_boundary};
pub const TAB_WIDTH: usize = 4;
pub fn char_display_width(ch: char, column: usize) -> usize {
if ch == '\t' {
TAB_WIDTH - column % TAB_WIDTH
} else {
cell_width_of(ch).get()
}
}
pub fn display_column(line: &str, byte_column: usize) -> usize {
let end = floor_char_boundary(line, byte_column);
line[..end]
.chars()
.fold(0, |column, ch| column + char_display_width(ch, column))
}
pub fn display_width(line: &str) -> usize {
display_column(line, line.len())
}
pub fn byte_column_at_display(line: &str, target: usize) -> usize {
let mut column = 0;
for (index, ch) in line.char_indices() {
let width = char_display_width(ch, column);
if target < column + width {
return index;
}
column += width;
}
line.len()
}
pub fn char_column(line: &str, byte_column: usize) -> usize {
line[..floor_char_boundary(line, byte_column)]
.chars()
.count()
}
pub fn push_expanded(out: &mut String, text: &str, column: &mut usize) {
for ch in text.chars() {
let width = char_display_width(ch, *column);
if ch == '\t' {
out.extend(std::iter::repeat_n(' ', width));
} else {
out.push(ch);
}
*column += width;
}
}
pub fn expand_tabs(line: &str) -> String {
let mut out = String::with_capacity(line.len());
push_expanded(&mut out, line, &mut 0);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_tab_reaches_the_next_tab_stop() {
assert_eq!(expand_tabs("\tx"), " x");
assert_eq!(expand_tabs("ab\tx"), "ab x");
assert_eq!(
expand_tabs("abcd\tx"),
"abcd x",
"at a stop, a tab is a whole stop"
);
assert_eq!(expand_tabs("\t\tx"), " x");
}
#[test]
fn display_column_counts_tab_stops_and_cell_widths_not_bytes() {
let line = "\té漢x";
assert_eq!(display_column(line, 0), 0);
assert_eq!(display_column(line, 1), 4, "after the tab");
assert_eq!(display_column(line, 3), 5, "after the two-byte é, one cell");
assert_eq!(
display_column(line, 6),
7,
"after the three-byte 漢, two cells"
);
assert_eq!(display_column(line, 7), 8);
assert_eq!(
display_column(line, 2),
4,
"inside é rounds down to its start"
);
assert_eq!(display_column(line, 99), 8, "past the end clamps");
assert_eq!(display_width(line), 8);
}
#[test]
fn byte_column_at_display_inverts_display_column_on_every_covered_cell() {
let line = "\té漢x";
for (display, byte) in [(0, 0), (3, 0), (4, 1), (5, 3), (6, 3), (7, 6), (8, 7)] {
assert_eq!(
byte_column_at_display(line, display),
byte,
"display column {display}"
);
}
for byte in [0, 1, 3, 6, 7] {
assert_eq!(
byte_column_at_display(line, display_column(line, byte)),
byte
);
}
}
#[test]
fn char_column_counts_characters_before_a_byte_column() {
assert_eq!(char_column("é = x", 5), 4);
assert_eq!(char_column("\tx", 1), 1);
assert_eq!(char_column("é", 1), 0, "inside é rounds down");
}
#[test]
fn pieces_expanded_in_order_line_up_with_the_whole_row() {
let line = "a\tbc\td";
let mut out = String::new();
let mut column = 0;
push_expanded(&mut out, &line[..2], &mut column);
push_expanded(&mut out, &line[2..], &mut column);
assert_eq!(out, expand_tabs(line));
assert_eq!(column, display_width(line));
}
}