#[derive(Debug, Clone, PartialEq)]
pub struct VisualLine {
pub logical_row: usize,
pub start_col: usize,
pub end_col: usize,
pub start_byte: usize,
pub end_byte: usize,
pub is_first_visual_line: bool,
}
impl VisualLine {
pub fn content<'a>(&self, source: &'a str) -> &'a str {
&source[self.start_byte..self.end_byte]
}
}
pub struct WordWrapLayout {
visual_lines: Vec<VisualLine>,
row_starts: Vec<usize>,
}
impl WordWrapLayout {
pub fn compute(lines: &[String], width: u16, rendered: &[Vec<bool>]) -> Self {
let width = width as usize;
let mut visual_lines = Vec::new();
let mut row_starts = Vec::with_capacity(lines.len());
if lines.is_empty() {
return Self::default();
}
for (row, line) in lines.iter().enumerate() {
let ci: Vec<(usize, char)> = line.char_indices().collect();
if ci.is_empty() || width == 0 {
row_starts.push(visual_lines.len());
visual_lines.push(VisualLine {
logical_row: row,
start_col: 0,
end_col: 0,
start_byte: 0,
end_byte: 0,
is_first_visual_line: true,
});
continue;
}
let flags: &[bool] = rendered.get(row).map(|v| v.as_slice()).unwrap_or(&[]);
let is_rendered = |pos: usize| -> bool {
if pos < flags.len() { flags[pos] } else { true }
};
let byte_at = |pos: usize| -> usize {
if pos < ci.len() { ci[pos].0 } else { line.len() }
};
let total = ci.len();
let mut start = 0;
let mut is_first = true;
while start < total {
if is_first {
row_starts.push(visual_lines.len());
}
let fit_end = {
let mut rcount = 0usize;
let mut pos = start;
while pos < total {
let r = is_rendered(pos) as usize;
if rcount + r > width {
break;
}
rcount += r;
pos += 1;
}
pos
};
if fit_end == total {
visual_lines.push(VisualLine {
logical_row: row,
start_col: start,
end_col: total,
start_byte: byte_at(start),
end_byte: line.len(),
is_first_visual_line: is_first,
});
break;
}
let (content_end, next_start) =
if fit_end < total && ci[fit_end].1.is_whitespace() {
(fit_end, fit_end + 1)
} else {
match ci[start..fit_end]
.iter()
.enumerate()
.rev()
.find(|(_, (_, c))| c.is_whitespace())
{
Some((i, _)) => (start + i, start + i + 1),
None => (fit_end, fit_end), }
};
visual_lines.push(VisualLine {
logical_row: row,
start_col: start,
end_col: content_end,
start_byte: byte_at(start),
end_byte: byte_at(content_end),
is_first_visual_line: is_first,
});
start = next_start;
is_first = false;
}
}
Self { visual_lines, row_starts }
}
pub fn total_visual_lines(&self) -> usize {
self.visual_lines.len()
}
pub fn visual_lines(&self) -> &[VisualLine] {
&self.visual_lines
}
pub fn logical_to_visual(&self, row: usize, col: usize) -> (usize, usize) {
let row = row.min(self.row_starts.len().saturating_sub(1));
let first = self.row_starts.get(row).copied().unwrap_or(0);
let vrow = self.visual_lines[first..]
.iter()
.enumerate()
.take_while(|(_, vl)| vl.logical_row == row)
.filter(|(_, vl)| vl.start_col <= col)
.last()
.map(|(i, _)| first + i)
.unwrap_or(first);
let vl = &self.visual_lines[vrow];
(vrow, col.saturating_sub(vl.start_col))
}
pub fn visual_to_logical(&self, vrow: usize, vcol: usize) -> (usize, usize) {
let vrow = vrow.min(self.visual_lines.len().saturating_sub(1));
let vl = &self.visual_lines[vrow];
let col = (vl.start_col + vcol).min(vl.end_col);
(vl.logical_row, col)
}
}
impl Default for WordWrapLayout {
fn default() -> Self {
Self {
visual_lines: vec![VisualLine {
logical_row: 0,
start_col: 0,
end_col: 0,
start_byte: 0,
end_byte: 0,
is_first_visual_line: true,
}],
row_starts: vec![0],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ls(s: &str) -> Vec<String> {
s.lines().map(str::to_owned).collect()
}
fn content_of<'a>(vl: &VisualLine, source: &'a str) -> &'a str {
vl.content(source)
}
#[test]
fn empty_input_produces_one_visual_line() {
let layout = WordWrapLayout::compute(&[], 40, &[]);
assert_eq!(layout.total_visual_lines(), 1);
assert_eq!(layout.visual_lines()[0].logical_row, 0);
assert!(layout.visual_lines()[0].is_first_visual_line);
}
#[test]
fn empty_string_produces_one_visual_line() {
let src = String::new();
let layout = WordWrapLayout::compute(&[src.clone()], 40, &[]);
assert_eq!(layout.total_visual_lines(), 1);
assert_eq!(content_of(&layout.visual_lines()[0], &src), "");
assert!(layout.visual_lines()[0].is_first_visual_line);
}
#[test]
fn short_line_fits_on_one_visual_line() {
let lines = ls("hello world");
let layout = WordWrapLayout::compute(&lines, 40, &[]);
assert_eq!(layout.total_visual_lines(), 1);
assert_eq!(content_of(&layout.visual_lines()[0], &lines[0]), "hello world");
assert!(layout.visual_lines()[0].is_first_visual_line);
}
#[test]
fn long_line_wraps_at_whitespace() {
let lines = ls("hello world foo");
let layout = WordWrapLayout::compute(&lines, 11, &[]);
assert_eq!(layout.total_visual_lines(), 2);
assert_eq!(content_of(&layout.visual_lines()[0], &lines[0]), "hello world");
assert_eq!(content_of(&layout.visual_lines()[1], &lines[0]), "foo");
assert!(layout.visual_lines()[0].is_first_visual_line);
assert!(!layout.visual_lines()[1].is_first_visual_line);
}
#[test]
fn long_word_hard_breaks_at_width() {
let lines = vec!["abcdefgh".to_string()];
let layout = WordWrapLayout::compute(&lines, 4, &[]);
assert_eq!(layout.total_visual_lines(), 2);
assert_eq!(content_of(&layout.visual_lines()[0], &lines[0]), "abcd");
assert_eq!(content_of(&layout.visual_lines()[1], &lines[0]), "efgh");
}
#[test]
fn two_logical_lines_have_correct_logical_rows() {
let layout = WordWrapLayout::compute(&ls("abc\nxyz"), 10, &[]);
assert_eq!(layout.total_visual_lines(), 2);
assert_eq!(layout.visual_lines()[0].logical_row, 0);
assert_eq!(layout.visual_lines()[1].logical_row, 1);
}
#[test]
fn unicode_chars_counted_not_bytes() {
let lines = vec!["あいう".to_string()];
let layout = WordWrapLayout::compute(&lines, 2, &[]);
assert_eq!(layout.total_visual_lines(), 2);
assert_eq!(content_of(&layout.visual_lines()[0], &lines[0]), "あい");
assert_eq!(content_of(&layout.visual_lines()[1], &lines[0]), "う");
}
#[test]
fn logical_to_visual_start_of_line() {
let layout = WordWrapLayout::compute(&ls("hello world"), 40, &[]);
assert_eq!(layout.logical_to_visual(0, 0), (0, 0));
}
#[test]
fn logical_to_visual_wrapped_cursor() {
let layout = WordWrapLayout::compute(&ls("hello world foo"), 11, &[]);
let (vrow, vcol) = layout.logical_to_visual(0, 12);
assert_eq!(vrow, 1);
assert_eq!(vcol, 0);
}
#[test]
fn visual_to_logical_first_line() {
let layout = WordWrapLayout::compute(&ls("hello"), 40, &[]);
assert_eq!(layout.visual_to_logical(0, 3), (0, 3));
}
#[test]
fn visual_to_logical_accounts_for_start_col() {
let layout = WordWrapLayout::compute(&ls("hello world foo"), 11, &[]);
let (row, col) = layout.visual_to_logical(1, 0);
assert_eq!(row, 0);
assert_eq!(col, 12);
}
#[test]
fn row_starts_index_multi_line_multi_wrap() {
let lines = vec![
"abc".to_string(),
"hello world foo".to_string(),
"xyz".to_string(),
];
let layout = WordWrapLayout::compute(&lines, 11, &[]);
assert_eq!(layout.row_starts, vec![0, 1, 3]);
assert_eq!(layout.logical_to_visual(2, 0), (3, 0));
}
#[test]
fn coordinate_roundtrip_vrow_zero() {
let layout = WordWrapLayout::compute(&ls("hello world foo"), 11, &[]);
let (row, col) = layout.visual_to_logical(0, 3);
let (vrow2, vcol2) = layout.logical_to_visual(row, col);
assert_eq!((vrow2, vcol2), (0, 3));
}
#[test]
fn byte_offsets_correct_for_unicode() {
let lines = vec!["あいう".to_string()];
let layout = WordWrapLayout::compute(&lines, 2, &[]);
let vl0 = &layout.visual_lines()[0];
let vl1 = &layout.visual_lines()[1];
assert_eq!((vl0.start_byte, vl0.end_byte), (0, 6)); assert_eq!((vl1.start_byte, vl1.end_byte), (6, 9)); }
}