use crate::primitives::ansi::AnsiParser;
use crate::primitives::display_width::char_width;
pub const TAB_WIDTH: usize = 8;
#[inline]
pub fn tab_expansion_width(col: usize) -> usize {
TAB_WIDTH - (col % TAB_WIDTH)
}
#[derive(Debug, Clone, Default)]
pub struct LineMappings {
pub char_source_bytes: Vec<Option<usize>>,
pub char_visual_cols: Vec<usize>,
pub visual_to_char: Vec<usize>,
pub total_visual_width: usize,
}
impl LineMappings {
#[inline]
pub fn source_byte_at_char(&self, char_idx: usize) -> Option<usize> {
self.char_source_bytes.get(char_idx).copied().flatten()
}
#[inline]
pub fn visual_col_at_char(&self, char_idx: usize) -> usize {
self.char_visual_cols.get(char_idx).copied().unwrap_or(0)
}
#[inline]
pub fn char_at_visual_col(&self, visual_col: usize) -> usize {
self.visual_to_char
.get(visual_col)
.copied()
.unwrap_or_else(|| {
self.char_source_bytes.len().saturating_sub(1)
})
}
#[inline]
pub fn source_byte_at_visual_col(&self, visual_col: usize) -> Option<usize> {
let char_idx = self.char_at_visual_col(visual_col);
self.source_byte_at_char(char_idx)
}
#[inline]
pub fn line_end_byte(&self) -> usize {
self.char_source_bytes
.iter()
.rev()
.find_map(|&b| b)
.map(|b| b + 1) .unwrap_or(0)
}
}
#[derive(Debug)]
pub struct LineMappingsBuilder {
mappings: LineMappings,
current_visual_col: usize,
ansi_parser: Option<AnsiParser>,
}
impl LineMappingsBuilder {
pub fn new(has_ansi: bool) -> Self {
Self {
mappings: LineMappings::default(),
current_visual_col: 0,
ansi_parser: if has_ansi {
Some(AnsiParser::new())
} else {
None
},
}
}
pub fn add_char(&mut self, ch: char, source_byte: Option<usize>) -> usize {
if let Some(ref mut parser) = self.ansi_parser {
if parser.parse_char(ch).is_none() {
let _char_idx = self.mappings.char_source_bytes.len();
self.mappings.char_source_bytes.push(source_byte);
self.mappings.char_visual_cols.push(self.current_visual_col);
return 0;
}
}
let width = if ch == '\t' {
tab_expansion_width(self.current_visual_col)
} else {
char_width(ch)
};
let char_idx = self.mappings.char_source_bytes.len();
self.mappings.char_source_bytes.push(source_byte);
self.mappings.char_visual_cols.push(self.current_visual_col);
for _ in 0..width {
self.mappings.visual_to_char.push(char_idx);
}
self.current_visual_col += width;
width
}
pub fn add_tab(&mut self, source_byte: Option<usize>) -> usize {
let width = tab_expansion_width(self.current_visual_col);
let char_idx = self.mappings.char_source_bytes.len();
self.mappings.char_source_bytes.push(source_byte);
self.mappings.char_visual_cols.push(self.current_visual_col);
for _ in 0..width {
self.mappings.visual_to_char.push(char_idx);
}
self.current_visual_col += width;
width
}
pub fn current_visual_col(&self) -> usize {
self.current_visual_col
}
pub fn finish(mut self) -> LineMappings {
self.mappings.total_visual_width = self.current_visual_col;
self.mappings
}
}
pub fn visual_width(s: &str, start_col: usize) -> usize {
if !s.contains('\x1b') && !s.contains('\t') {
return crate::primitives::display_width::str_width(s);
}
let mut col = start_col;
let mut parser = AnsiParser::new();
for ch in s.chars() {
if parser.parse_char(ch).is_none() {
continue; }
if ch == '\t' {
col += tab_expansion_width(col);
} else {
col += char_width(ch);
}
}
col - start_col
}
pub fn byte_to_visual_col(s: &str, byte_offset: usize) -> usize {
let clamped_offset = byte_offset.min(s.len());
if !s.contains('\x1b') && !s.contains('\t') {
return crate::primitives::display_width::str_width(&s[..clamped_offset]);
}
let mut col = 0;
let mut current_byte = 0;
let mut parser = AnsiParser::new();
for ch in s.chars() {
if current_byte >= clamped_offset {
break;
}
if parser.parse_char(ch).is_some() {
if ch == '\t' {
col += tab_expansion_width(col);
} else {
col += char_width(ch);
}
}
current_byte += ch.len_utf8();
}
col
}
pub fn visual_col_to_byte(s: &str, target_visual_col: usize) -> usize {
if !s.contains('\x1b') && !s.contains('\t') {
let mut col = 0;
for (byte_idx, ch) in s.char_indices() {
let width = char_width(ch);
if target_visual_col < col + width {
return byte_idx;
}
col += width;
}
return s.len();
}
let mut col = 0;
let mut parser = AnsiParser::new();
for (byte_idx, ch) in s.char_indices() {
if parser.parse_char(ch).is_some() {
let width = if ch == '\t' {
tab_expansion_width(col)
} else {
char_width(ch)
};
if target_visual_col < col + width {
return byte_idx;
}
col += width;
}
}
s.len()
}
pub fn build_line_mappings(
text: &str,
source_bytes: impl Iterator<Item = Option<usize>>,
has_ansi: bool,
) -> LineMappings {
let mut builder = LineMappingsBuilder::new(has_ansi);
let mut source_iter = source_bytes;
for ch in text.chars() {
let source_byte = source_iter.next().flatten();
builder.add_char(ch, source_byte);
}
builder.finish()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_visual_width_ascii() {
assert_eq!(visual_width("Hello", 0), 5);
assert_eq!(visual_width("", 0), 0);
}
#[test]
fn test_visual_width_with_tabs() {
assert_eq!(visual_width("\t", 0), 8);
assert_eq!(visual_width("1234\t", 0), 8);
assert_eq!(visual_width("12\t", 0), 8);
}
#[test]
fn test_visual_width_with_ansi() {
assert_eq!(visual_width("\x1b[31mRed\x1b[0m", 0), 3);
assert_eq!(visual_width("\x1b[1;31;4mBold\x1b[0m", 0), 4);
}
#[test]
fn test_visual_width_cjk() {
assert_eq!(visual_width("你好", 0), 4);
assert_eq!(visual_width("Hello你好", 0), 9);
}
#[test]
fn test_byte_to_visual_col_simple() {
let s = "Hello";
assert_eq!(byte_to_visual_col(s, 0), 0);
assert_eq!(byte_to_visual_col(s, 1), 1);
assert_eq!(byte_to_visual_col(s, 5), 5);
}
#[test]
fn test_byte_to_visual_col_with_ansi() {
let s = "\x1b[31mRed";
assert_eq!(byte_to_visual_col(s, 0), 0); assert_eq!(byte_to_visual_col(s, 5), 0); assert_eq!(byte_to_visual_col(s, 6), 1); assert_eq!(byte_to_visual_col(s, 8), 3); }
#[test]
fn test_byte_to_visual_col_with_cjk() {
let s = "a你b";
assert_eq!(byte_to_visual_col(s, 0), 0); assert_eq!(byte_to_visual_col(s, 1), 1); assert_eq!(byte_to_visual_col(s, 4), 3); }
#[test]
fn test_visual_col_to_byte_simple() {
let s = "Hello";
assert_eq!(visual_col_to_byte(s, 0), 0);
assert_eq!(visual_col_to_byte(s, 3), 3);
assert_eq!(visual_col_to_byte(s, 5), 5);
assert_eq!(visual_col_to_byte(s, 10), 5); }
#[test]
fn test_visual_col_to_byte_with_ansi() {
let s = "\x1b[31mRed";
assert_eq!(visual_col_to_byte(s, 0), 5); assert_eq!(visual_col_to_byte(s, 1), 6); assert_eq!(visual_col_to_byte(s, 3), 8); }
#[test]
fn test_visual_col_to_byte_with_cjk() {
let s = "a你b";
assert_eq!(visual_col_to_byte(s, 0), 0); assert_eq!(visual_col_to_byte(s, 1), 1); assert_eq!(visual_col_to_byte(s, 2), 1); assert_eq!(visual_col_to_byte(s, 3), 4); }
#[test]
fn test_line_mappings_builder_simple() {
let mut builder = LineMappingsBuilder::new(false);
builder.add_char('H', Some(0));
builder.add_char('i', Some(1));
let mappings = builder.finish();
assert_eq!(mappings.char_source_bytes.len(), 2);
assert_eq!(mappings.visual_to_char.len(), 2);
assert_eq!(mappings.source_byte_at_char(0), Some(0));
assert_eq!(mappings.source_byte_at_char(1), Some(1));
assert_eq!(mappings.char_at_visual_col(0), 0);
assert_eq!(mappings.char_at_visual_col(1), 1);
}
#[test]
fn test_line_mappings_builder_with_cjk() {
let mut builder = LineMappingsBuilder::new(false);
builder.add_char('a', Some(0)); builder.add_char('你', Some(1)); builder.add_char('b', Some(4));
let mappings = builder.finish();
assert_eq!(mappings.char_source_bytes.len(), 3);
assert_eq!(mappings.visual_to_char.len(), 4);
assert_eq!(mappings.source_byte_at_visual_col(0), Some(0));
assert_eq!(mappings.source_byte_at_visual_col(1), Some(1));
assert_eq!(mappings.source_byte_at_visual_col(2), Some(1));
assert_eq!(mappings.source_byte_at_visual_col(3), Some(4));
}
#[test]
fn test_line_mappings_builder_with_ansi() {
let mut builder = LineMappingsBuilder::new(true);
builder.add_char('\x1b', Some(0));
builder.add_char('[', Some(1));
builder.add_char('3', Some(2));
builder.add_char('1', Some(3));
builder.add_char('m', Some(4));
builder.add_char('A', Some(5));
let mappings = builder.finish();
assert_eq!(mappings.char_source_bytes.len(), 6);
assert_eq!(mappings.visual_to_char.len(), 1);
assert_eq!(mappings.total_visual_width, 1);
assert_eq!(mappings.source_byte_at_char(0), Some(0)); assert_eq!(mappings.source_byte_at_char(5), Some(5));
assert_eq!(mappings.char_at_visual_col(0), 5);
assert_eq!(mappings.source_byte_at_visual_col(0), Some(5));
}
#[test]
fn test_line_mappings_cursor_on_ansi() {
let mut builder = LineMappingsBuilder::new(true);
builder.add_char('\x1b', Some(0));
builder.add_char('[', Some(1));
builder.add_char('3', Some(2));
builder.add_char('1', Some(3));
builder.add_char('m', Some(4));
builder.add_char('H', Some(5));
builder.add_char('i', Some(6));
let mappings = builder.finish();
assert_eq!(mappings.source_byte_at_char(0), Some(0)); assert_eq!(mappings.source_byte_at_char(1), Some(1));
assert_eq!(mappings.visual_col_at_char(0), 0);
assert_eq!(mappings.visual_col_at_char(4), 0);
assert_eq!(mappings.visual_col_at_char(5), 0); assert_eq!(mappings.visual_col_at_char(6), 1); }
}