use unicode_width::UnicodeWidthChar;
use crate::buffer::LineBuffer;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Segment {
pub line_idx: usize,
pub byte_start: usize,
pub byte_end: usize,
}
pub struct Layout {
segments: Vec<Segment>,
width: u16,
chop: bool,
}
impl Layout {
pub fn build(buf: &LineBuffer, width: u16, chop: bool) -> Self {
let mut segments = Vec::with_capacity(buf.lines().len());
for (line_idx, line) in buf.lines().iter().enumerate() {
split_line(line, line_idx, width, chop, &mut segments);
}
Self { segments, width, chop }
}
pub fn rebuild(&mut self, buf: &LineBuffer, width: u16, chop: bool) {
if self.width == width && self.chop == chop {
return;
}
*self = Self::build(buf, width, chop);
}
pub fn len(&self) -> usize {
self.segments.len()
}
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.segments.is_empty()
}
pub fn segment(&self, idx: usize) -> Option<&Segment> {
self.segments.get(idx)
}
pub fn first_segment_of(&self, line_idx: usize) -> Option<usize> {
self.segments.iter().position(|s| s.line_idx == line_idx)
}
}
fn split_line(line: &str, line_idx: usize, width: u16, chop: bool, out: &mut Vec<Segment>) {
let max_cols = width.max(1) as usize;
let mut cols = 0usize;
let mut seg_start = 0usize;
let mut chars = line.char_indices().peekable();
while let Some((i, c)) = chars.next() {
if c == '\x1b' {
if let Some(&(_, '[')) = chars.peek() {
chars.next();
while let Some(&(_, p)) = chars.peek() {
let v = p as u32;
if (0x20..=0x3F).contains(&v) {
chars.next();
} else {
break;
}
}
if let Some(&(_, p)) = chars.peek() {
let v = p as u32;
if (0x40..=0x7E).contains(&v) {
chars.next();
}
}
continue;
}
}
let w = c.width().unwrap_or(0);
if cols + w > max_cols {
out.push(Segment { line_idx, byte_start: seg_start, byte_end: i });
if chop {
return;
}
seg_start = i;
cols = w;
} else {
cols += w;
}
}
out.push(Segment {
line_idx,
byte_start: seg_start,
byte_end: line.len(),
});
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn buf_from(input: &[u8]) -> LineBuffer {
let mut buf = LineBuffer::new(Cursor::new(input.to_vec()));
buf.fill_all().unwrap();
buf
}
#[test]
fn chop_truncates_long_line() {
let buf = buf_from(b"short\nthis is too long\n");
let layout = Layout::build(&buf, 5, true);
assert_eq!(layout.len(), 2);
let s1 = layout.segment(1).unwrap();
assert_eq!(s1.line_idx, 1);
assert_eq!(s1.byte_start, 0);
assert_eq!(s1.byte_end, 5);
}
#[test]
fn wrap_creates_multiple_segments() {
let buf = buf_from(b"0123456789\n");
let layout = Layout::build(&buf, 4, false);
assert_eq!(layout.len(), 3);
assert_eq!(layout.segment(0).unwrap().byte_end, 4);
assert_eq!(layout.segment(1).unwrap().byte_start, 4);
assert_eq!(layout.segment(1).unwrap().byte_end, 8);
assert_eq!(layout.segment(2).unwrap().byte_start, 8);
assert_eq!(layout.segment(2).unwrap().byte_end, 10);
}
#[test]
fn empty_line_yields_one_segment() {
let buf = buf_from(b"\nfoo\n");
let layout = Layout::build(&buf, 80, false);
assert_eq!(layout.len(), 2);
let s0 = layout.segment(0).unwrap();
assert_eq!(s0.byte_start, 0);
assert_eq!(s0.byte_end, 0);
}
#[test]
fn sgr_doesnt_count_toward_width() {
let buf = buf_from(b"\x1b[31mhello\x1b[0m\n");
let layout = Layout::build(&buf, 5, false);
assert_eq!(layout.len(), 1);
}
#[test]
fn first_segment_of_logical_line() {
let buf = buf_from(b"a\nlong line one two three\n");
let layout = Layout::build(&buf, 5, false);
assert_eq!(layout.first_segment_of(0), Some(0));
assert_eq!(layout.first_segment_of(1), Some(1));
}
#[test]
fn rebuild_noop_when_unchanged() {
let buf = buf_from(b"abc\n");
let mut layout = Layout::build(&buf, 80, false);
let n = layout.len();
layout.rebuild(&buf, 80, false);
assert_eq!(layout.len(), n);
}
#[test]
fn rebuild_recomputes_on_width_change() {
let buf = buf_from(b"0123456789\n");
let mut layout = Layout::build(&buf, 80, false);
assert_eq!(layout.len(), 1);
layout.rebuild(&buf, 5, false);
assert_eq!(layout.len(), 2);
}
#[test]
fn rebuild_recomputes_on_chop_change() {
let buf = buf_from(b"0123456789\n");
let mut layout = Layout::build(&buf, 4, false);
assert_eq!(layout.len(), 3);
layout.rebuild(&buf, 4, true);
assert_eq!(layout.len(), 1);
}
#[test]
fn wide_chars_count_as_two_cols() {
let buf = buf_from("你好\n".as_bytes());
let layout = Layout::build(&buf, 3, false);
assert_eq!(layout.len(), 2);
}
}