use std::ops::Range;
use unicode_segmentation::UnicodeSegmentation;
use crate::ropetext::position::{Column, Position, Revision};
use crate::ropetext::text::Text;
use crate::ropetext::width::Metrics;
#[derive(Debug, Clone, Copy, Default)]
pub struct RowHints<'a> {
pub visible: &'a [bool],
pub inset: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VisualLine {
pub logical_row: usize,
pub chars: Range<usize>,
pub bytes: Range<usize>,
pub first: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cell {
pub row: usize,
pub column: usize,
}
#[derive(Debug, Clone)]
pub struct Layout {
lines: Vec<VisualLine>,
row_starts: Vec<usize>,
width: usize,
metrics: Metrics,
revision: Revision,
}
impl Layout {
pub fn describes(&self, text: &Text) -> bool {
self.revision == text.revision()
}
pub fn unwrapped(text: &Text) -> Self {
let mut lines = Vec::with_capacity(text.line_count());
let mut row_starts = Vec::with_capacity(text.line_count());
for row in 0..text.line_count() {
row_starts.push(lines.len());
let Some(source) = text.line(row) else {
continue;
};
lines.push(VisualLine {
logical_row: row,
chars: 0..source.chars().count(),
bytes: 0..source.len(),
first: true,
});
}
Self {
lines,
row_starts,
width: 0,
metrics: Metrics::default(),
revision: text.revision(),
}
}
pub fn compute(text: &Text, width: usize, metrics: Metrics, hints: &[RowHints<'_>]) -> Self {
let mut layout = Self {
lines: Vec::new(),
row_starts: Vec::with_capacity(text.line_count()),
width,
metrics,
revision: text.revision(),
};
let mut scratch = Vec::new();
for row in 0..text.line_count() {
layout.row_starts.push(layout.lines.len());
wrap_row(
text,
row,
width,
metrics,
hint_for(hints, row),
&mut scratch,
&mut layout.lines,
);
}
layout
}
pub fn relayout_rows(
&mut self,
text: &Text,
hints: &[RowHints<'_>],
rows: Range<usize>,
line_delta: isize,
) {
self.revision = text.revision();
let rows = rows.start.min(text.line_count())..rows.end.min(text.line_count());
let old_rows = {
let end = (rows.end as isize - line_delta).max(rows.start as isize) as usize;
rows.start.min(self.row_starts.len())..end.min(self.row_starts.len())
};
if rows.is_empty() && old_rows.is_empty() {
return;
}
let old_start = self
.row_starts
.get(old_rows.start)
.copied()
.unwrap_or(self.lines.len());
let old_end = self
.row_starts
.get(old_rows.end)
.copied()
.unwrap_or(self.lines.len());
let mut replacement = Vec::new();
let mut starts = Vec::with_capacity(rows.len());
let mut scratch = Vec::new();
for row in rows.clone() {
starts.push(old_start + replacement.len());
wrap_row(
text,
row,
self.width,
self.metrics,
hint_for(hints, row),
&mut scratch,
&mut replacement,
);
}
let added = replacement.len();
self.lines.splice(old_start..old_end, replacement);
if line_delta != 0 {
for line in &mut self.lines[old_start + added..] {
line.logical_row = (line.logical_row as isize + line_delta) as usize;
}
}
self.row_starts.splice(old_rows, starts);
let shift = added as isize - (old_end - old_start) as isize;
if shift != 0 {
let tail = rows.end.min(self.row_starts.len());
for start in &mut self.row_starts[tail..] {
*start = (*start as isize + shift) as usize;
}
}
debug_assert_eq!(
self.row_starts.len(),
text.line_count(),
"relayout left the layout describing a different number of rows"
);
}
pub fn visual_lines(&self) -> &[VisualLine] {
&self.lines
}
pub fn visual_line_count(&self) -> usize {
self.lines.len()
}
pub fn row_count(&self) -> usize {
self.row_starts.len()
}
pub fn width(&self) -> usize {
self.width
}
pub fn visual_row_of(&self, position: Position) -> usize {
let row = position.row().min(self.row_starts.len().saturating_sub(1));
let first = self.row_starts.get(row).copied().unwrap_or(0);
let column = position.column().get();
self.lines[first..]
.iter()
.take_while(|line| line.logical_row == row)
.enumerate()
.filter(|(_, line)| line.chars.start <= column)
.map(|(offset, _)| first + offset)
.last()
.unwrap_or(first)
}
pub fn cell_of(&self, text: &Text, hints: &[RowHints<'_>], position: Position) -> Cell {
debug_assert!(
self.describes(text),
"cell_of read against a text this layout does not describe"
);
let row = self.visual_row_of(position);
let line = &self.lines[row];
let hint = hint_for(hints, line.logical_row);
let Some(source) = text.line(line.logical_row) else {
return Cell {
row,
column: hint.inset,
};
};
let mut column = hint.inset;
let mut chars = line.chars.start;
for cluster in source[line.bytes.clone()].graphemes(true) {
if chars >= position.column().get() {
break;
}
if visible(&hint, chars) {
column += self.metrics.width_at(cluster, column - hint.inset);
}
chars += cluster.chars().count();
}
Cell { row, column }
}
pub fn position_at_cell(
&self,
text: &Text,
hints: &[RowHints<'_>],
cell: Cell,
) -> Option<Position> {
if !self.describes(text) {
return None;
}
let line = self.lines.get(cell.row)?;
let hint = hint_for(hints, line.logical_row);
let source = text.line(line.logical_row)?;
let mut column = hint.inset;
let mut chars = line.chars.start;
for cluster in source[line.bytes.clone()].graphemes(true) {
let width = if visible(&hint, chars) {
self.metrics.width_at(cluster, column - hint.inset)
} else {
0
};
if width > 0 && cell.column < column + width {
return text.position(line.logical_row, Column::new(chars));
}
column += width;
chars += cluster.chars().count();
}
text.position(line.logical_row, Column::new(line.chars.end))
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Viewport {
top: usize,
height: usize,
}
impl Viewport {
pub fn new(height: usize) -> Self {
Self { top: 0, height }
}
pub fn top(&self) -> usize {
self.top
}
pub fn height(&self) -> usize {
self.height
}
pub fn set_height(&mut self, height: usize) {
self.height = height;
}
pub fn rows(&self, layout: &Layout) -> Range<usize> {
let top = self.top.min(layout.visual_line_count().saturating_sub(1));
top..(top + self.height).min(layout.visual_line_count())
}
pub fn follow(&mut self, layout: &Layout, cursor: Position) -> bool {
if self.height == 0 {
return false;
}
let row = layout.visual_row_of(cursor);
let was = self.top;
if row < self.top {
self.top = row;
} else if row >= self.top + self.height {
self.top = row + 1 - self.height;
}
self.top != was
}
pub fn scroll_by(&mut self, layout: &Layout, delta: isize) -> bool {
let was = self.top;
let last = layout.visual_line_count().saturating_sub(1);
self.top = if delta >= 0 {
(self.top + delta.unsigned_abs()).min(last)
} else {
self.top.saturating_sub(delta.unsigned_abs())
};
self.top != was
}
}
struct Cluster {
chars: usize,
bytes: usize,
len: usize,
breakable: bool,
}
fn hint_for<'a>(hints: &'a [RowHints<'a>], row: usize) -> RowHints<'a> {
hints.get(row).copied().unwrap_or_default()
}
fn visible(hint: &RowHints<'_>, chars: usize) -> bool {
hint.visible.get(chars).copied().unwrap_or(true)
}
fn wrap_row(
text: &Text,
row: usize,
width: usize,
metrics: Metrics,
hint: RowHints<'_>,
scratch: &mut Vec<Cluster>,
out: &mut Vec<VisualLine>,
) {
let width = if hint.inset == 0 {
width
} else {
width.saturating_sub(hint.inset).max(1)
};
let Some(source) = text.line(row) else {
return;
};
scratch.clear();
let mut chars = 0;
for (bytes, cluster) in source.grapheme_indices(true) {
let len = cluster.chars().count();
scratch.push(Cluster {
chars,
bytes,
len: cluster.len(),
breakable: len == 1 && cluster.chars().next().is_some_and(char::is_whitespace),
});
chars += len;
}
let total_chars = chars;
let total_bytes = source.len();
if scratch.is_empty() || width == 0 {
out.push(VisualLine {
logical_row: row,
chars: 0..0,
bytes: 0..0,
first: true,
});
return;
}
let cell_width = |index: usize, column: usize| -> usize {
let cluster = &scratch[index];
if visible(&hint, cluster.chars) {
let at = cluster.bytes;
metrics.width_at(&source[at..at + cluster.len], column)
} else {
0
}
};
let char_at =
|index: usize| -> usize { scratch.get(index).map(|c| c.chars).unwrap_or(total_chars) };
let byte_at =
|index: usize| -> usize { scratch.get(index).map(|c| c.bytes).unwrap_or(total_bytes) };
let total = scratch.len();
let mut start = 0;
let mut first = true;
while start < total {
let fit_end = {
let mut column = 0;
let mut index = start;
while index < total {
let cells = cell_width(index, column);
if column + cells > width {
break;
}
column += cells;
index += 1;
}
if index == start { start + 1 } else { index }
};
if fit_end >= total {
out.push(VisualLine {
logical_row: row,
chars: char_at(start)..total_chars,
bytes: byte_at(start)..total_bytes,
first,
});
break;
}
let (content_end, next_start) = if scratch[fit_end].breakable {
(fit_end, fit_end + 1)
} else {
match scratch[start..fit_end]
.iter()
.enumerate()
.rev()
.find(|(_, cluster)| cluster.breakable)
{
Some((offset, _)) => (start + offset, start + offset + 1),
None => (fit_end, fit_end),
}
};
out.push(VisualLine {
logical_row: row,
chars: char_at(start)..char_at(content_end),
bytes: byte_at(start)..byte_at(content_end),
first,
});
start = next_start;
first = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn text(s: &str) -> Text {
Text::from(s)
}
fn plain(text: &Text, width: usize) -> Layout {
Layout::compute(text, width, Metrics::default(), &[])
}
fn drawn(text: &Text, layout: &Layout) -> Vec<String> {
layout
.visual_lines()
.iter()
.map(|line| {
let row = text.line(line.logical_row).expect("row exists");
row[line.bytes.clone()].to_string()
})
.collect()
}
fn at(text: &Text, row: usize, col: usize) -> Position {
text.position(row, Column::new(col)).expect("addressable")
}
#[test]
fn a_row_that_fits_is_one_visual_line() {
let t = text("short");
assert_eq!(drawn(&t, &plain(&t, 10)), ["short"]);
}
#[test]
fn wrapping_prefers_a_space() {
let t = text("aaaa bbbb");
assert_eq!(drawn(&t, &plain(&t, 6)), ["aaaa", "bbbb"]);
}
#[test]
fn a_word_longer_than_the_width_breaks_mid_word() {
let t = text("aaaaaaaa");
assert_eq!(drawn(&t, &plain(&t, 3)), ["aaa", "aaa", "aa"]);
}
#[test]
fn an_empty_row_is_still_a_visual_line() {
let t = text("a\n\nb");
let layout = plain(&t, 10);
assert_eq!(layout.visual_line_count(), 3);
assert_eq!(drawn(&t, &layout), ["a", "", "b"]);
}
#[test]
fn a_zero_width_pane_still_produces_one_line_per_row() {
let t = text("a\nb");
let layout = plain(&t, 0);
assert_eq!(layout.visual_line_count(), 2);
}
#[test]
fn a_cluster_wider_than_the_pane_still_advances() {
let t = text("\u{3042}\u{3042}");
let layout = plain(&t, 1);
assert_eq!(layout.visual_line_count(), 2);
}
#[test]
fn a_cluster_is_never_split_across_visual_lines() {
let family = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}";
let t = text(&format!("ab{family}cd"));
for width in 1..10 {
let layout = plain(&t, width);
let row = t.line(0).expect("one row");
for line in layout.visual_lines() {
assert!(
row.is_char_boundary(line.bytes.start) && row.is_char_boundary(line.bytes.end),
"width {width}: {:?} splits a character",
line.bytes
);
let intact: Vec<usize> = row.grapheme_indices(true).map(|(at, _)| at).collect();
assert!(
intact.contains(&line.bytes.start) || line.bytes.start == row.len(),
"width {width}: {:?} starts inside a cluster",
line.bytes
);
assert!(
intact.contains(&line.bytes.end) || line.bytes.end == row.len(),
"width {width}: {:?} ends inside a cluster",
line.bytes
);
}
}
}
#[test]
fn a_gutter_eats_into_the_width() {
let t = text("aaaa bbbb");
let hints = [RowHints {
visible: &[],
inset: 2,
}];
let layout = Layout::compute(&t, 9, Metrics::default(), &hints);
assert_eq!(
drawn(&t, &layout),
["aaaa", "bbbb"],
"nine cells less a two-cell gutter does not fit nine characters"
);
assert_eq!(
plain(&t, 9).visual_line_count(),
1,
"and without the gutter it does"
);
}
#[test]
fn a_gutter_as_wide_as_the_pane_still_makes_progress() {
let t = text("aaaa");
let hints = [RowHints {
visible: &[],
inset: 4,
}];
let layout = Layout::compute(&t, 4, Metrics::default(), &hints);
assert_eq!(
layout.visual_line_count(),
4,
"one cell per line, not a loop"
);
}
#[test]
fn undrawn_clusters_take_no_width() {
let t = text("## heading");
let visible = vec![
false, false, false, true, true, true, true, true, true, true,
];
let hints = [RowHints {
visible: &visible,
inset: 0,
}];
let layout = Layout::compute(&t, 7, Metrics::default(), &hints);
assert_eq!(layout.visual_line_count(), 1);
assert_eq!(
plain(&t, 7).visual_line_count(),
2,
"measuring the sigils would wrap it"
);
}
#[test]
fn a_tab_is_measured_to_its_stop_when_wrapping() {
let t = text("\tabcd");
assert_eq!(plain(&t, 8).visual_line_count(), 1);
assert_eq!(plain(&t, 7).visual_line_count(), 2);
}
#[test]
fn a_tab_is_itself_a_place_to_break() {
let t = text("ab cd\tef");
assert_eq!(drawn(&t, &plain(&t, 5)), ["ab cd", "ef"]);
}
#[test]
fn a_tab_measures_from_the_start_of_its_own_visual_line() {
let t = text("aaaa bb\tc");
assert_eq!(drawn(&t, &plain(&t, 4)), ["aaaa", "bb", "c"]);
}
#[test]
fn a_position_knows_which_visual_line_draws_it() {
let t = text("aaaa bbbb cccc");
let layout = plain(&t, 5);
assert_eq!(drawn(&t, &layout), ["aaaa", "bbbb", "cccc"]);
assert_eq!(layout.visual_row_of(at(&t, 0, 0)), 0);
assert_eq!(layout.visual_row_of(at(&t, 0, 5)), 1);
assert_eq!(layout.visual_row_of(at(&t, 0, 12)), 2);
}
#[test]
fn visual_rows_are_found_across_logical_rows() {
let t = text("aaaa bbbb\nsecond");
let layout = plain(&t, 5);
assert_eq!(drawn(&t, &layout), ["aaaa", "bbbb", "secon", "d"]);
assert_eq!(layout.visual_row_of(at(&t, 1, 0)), 2);
assert_eq!(layout.visual_row_of(at(&t, 1, 5)), 3);
}
#[test]
fn a_cell_accounts_for_the_gutter() {
let t = text("abc");
let hints = [RowHints {
visible: &[],
inset: 2,
}];
let layout = Layout::compute(&t, 10, Metrics::default(), &hints);
assert_eq!(
layout.cell_of(&t, &hints, at(&t, 0, 1)),
Cell { row: 0, column: 3 }
);
}
#[test]
fn a_cell_skips_hidden_clusters() {
let t = text("## heading");
let visible = vec![false, false, false];
let hints = [RowHints {
visible: &visible,
inset: 0,
}];
let layout = Layout::compute(&t, 40, Metrics::default(), &hints);
assert_eq!(
layout.cell_of(&t, &hints, at(&t, 0, 3)),
Cell { row: 0, column: 0 },
"the first drawn character is in the first cell"
);
}
#[test]
fn a_cell_counts_a_wide_cluster_as_two() {
let t = text("\u{3042}b");
let layout = plain(&t, 40);
assert_eq!(layout.cell_of(&t, &[], at(&t, 0, 1)).column, 2);
}
#[test]
fn a_click_inside_a_wide_cluster_means_that_cluster() {
let t = text("\u{3042}b");
let layout = plain(&t, 40);
for column in [0, 1] {
let landed = layout
.position_at_cell(&t, &[], Cell { row: 0, column })
.expect("inside the line");
assert_eq!(landed.column().get(), 0, "column {column}");
}
let landed = layout
.position_at_cell(&t, &[], Cell { row: 0, column: 2 })
.expect("inside the line");
assert_eq!(landed.column().get(), 1);
}
#[test]
fn a_click_past_the_end_of_a_visual_line_lands_at_its_end() {
let t = text("aaaa bbbb");
let layout = plain(&t, 5);
let landed = layout
.position_at_cell(&t, &[], Cell { row: 0, column: 99 })
.expect("inside the line");
assert_eq!(landed.column().get(), 4, "the end of the first visual line");
}
#[test]
fn a_click_in_the_gutter_lands_at_the_start_of_the_text() {
let t = text("abc");
let hints = [RowHints {
visible: &[],
inset: 3,
}];
let layout = Layout::compute(&t, 10, Metrics::default(), &hints);
let landed = layout
.position_at_cell(&t, &hints, Cell { row: 0, column: 1 })
.expect("inside the line");
assert_eq!(landed.column().get(), 0);
}
#[test]
fn a_click_below_the_text_finds_nothing() {
let t = text("abc");
let layout = plain(&t, 10);
assert!(
layout
.position_at_cell(&t, &[], Cell { row: 9, column: 0 })
.is_none()
);
}
#[test]
fn cells_and_positions_round_trip() {
let t = text("aaaa bbbb cccc");
let layout = plain(&t, 5);
for column in 0..14 {
let position = at(&t, 0, column);
let cell = layout.cell_of(&t, &[], position);
let back = layout
.position_at_cell(&t, &[], cell)
.expect("its own cell is inside the line");
assert_eq!(back, position, "column {column}");
}
}
#[test]
fn unwrapped_matches_row_count_and_describes_text() {
let t = text("short\na longer row that would wrap at a narrow width\nlast");
let layout = Layout::unwrapped(&t);
assert_eq!(layout.row_count(), t.line_count());
assert_eq!(layout.visual_line_count(), t.line_count());
assert!(
layout.describes(&t),
"unwrapped must describe the text it was built from"
);
assert_eq!(
drawn(&t, &layout),
[
"short",
"a longer row that would wrap at a narrow width",
"last"
],
"one unwrapped visual line per row"
);
}
#[test]
fn unwrapped_handles_an_empty_text() {
let t = text("");
let layout = Layout::unwrapped(&t);
assert_eq!(layout.row_count(), t.line_count());
assert!(layout.describes(&t));
}
#[test]
fn relayout_rewraps_only_what_changed() {
let mut buffer = crate::ropetext::EditBuffer::new(text("aaaa bbbb\nkeep\ntail"));
let mut layout = plain(buffer.text(), 5);
assert_eq!(layout.visual_line_count(), 4);
let end = buffer.text().position(0, Column::new(9)).unwrap();
let mut txn = buffer.begin();
txn.delete(buffer_span(&txn, 0, 4, 0, 9));
let change = txn.commit().expect("changed");
let _ = end;
layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
assert_eq!(drawn(buffer.text(), &layout), ["aaaa", "keep", "tail"]);
assert_eq!(layout.row_count(), buffer.text().line_count());
}
#[test]
fn relayout_follows_added_rows() {
let mut buffer = crate::ropetext::EditBuffer::new(text("one\ntwo"));
let mut layout = plain(buffer.text(), 10);
let at_end = buffer.text().position(0, Column::new(3)).unwrap();
let mut txn = buffer.begin();
txn.insert(at_end, "\nmiddle");
let change = txn.commit().expect("changed");
layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
assert_eq!(drawn(buffer.text(), &layout), ["one", "middle", "two"]);
assert_eq!(layout.row_count(), 3);
}
#[test]
fn relayout_follows_removed_rows() {
let mut buffer = crate::ropetext::EditBuffer::new(text("one\ntwo\nthree\nfour"));
let mut layout = plain(buffer.text(), 10);
let mut txn = buffer.begin();
txn.delete(buffer_span(&txn, 0, 3, 2, 5));
let change = txn.commit().expect("changed");
layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
assert_eq!(drawn(buffer.text(), &layout), ["one", "four"]);
assert_eq!(layout.row_count(), 2);
}
#[test]
fn relayout_matches_a_full_recompute() {
for (initial, row, col, inserted) in [
("aaaa bbbb\nkeep", 0, 4, " cccc"),
("one\ntwo\nthree", 1, 3, "\nsplit"),
("one\ntwo", 0, 0, "prefix "),
("wrapped line that is long\nnext", 0, 8, "\n"),
] {
let mut buffer = crate::ropetext::EditBuffer::new(text(initial));
let mut layout = plain(buffer.text(), 6);
let position = buffer.text().position(row, Column::new(col)).unwrap();
let mut txn = buffer.begin();
txn.insert(position, inserted);
let change = txn.commit().expect("changed");
layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
let fresh = plain(buffer.text(), 6);
assert_eq!(
layout.visual_lines(),
fresh.visual_lines(),
"relayout disagreed for {initial:?} + {inserted:?}"
);
}
}
fn buffer_span(
txn: &crate::ropetext::Txn<'_>,
r1: usize,
c1: usize,
r2: usize,
c2: usize,
) -> crate::ropetext::Span {
let text = txn.text();
let a = text.position(r1, Column::new(c1)).expect("addressable");
let b = text.position(r2, Column::new(c2)).expect("addressable");
text.span(a, b).expect("same text")
}
#[test]
fn a_viewport_shows_its_height_of_lines() {
let t = text("a\nb\nc\nd\ne");
let layout = plain(&t, 10);
let view = Viewport::new(3);
assert_eq!(view.rows(&layout), 0..3);
}
#[test]
fn a_viewport_clamps_to_what_there_is() {
let t = text("a\nb");
let layout = plain(&t, 10);
let view = Viewport::new(10);
assert_eq!(view.rows(&layout), 0..2);
}
#[test]
fn following_the_cursor_scrolls_the_least_it_can() {
let t = text("a\nb\nc\nd\ne");
let layout = plain(&t, 10);
let mut view = Viewport::new(3);
assert!(view.follow(&layout, at(&t, 4, 0)));
assert_eq!(view.top(), 2, "just enough to show the last row");
assert!(!view.follow(&layout, at(&t, 3, 0)), "already on screen");
assert!(view.follow(&layout, at(&t, 0, 0)));
assert_eq!(view.top(), 0);
}
#[test]
fn following_the_cursor_counts_visual_lines_not_rows() {
let t = text("aaaa bbbb cccc\nlast");
let layout = plain(&t, 5);
assert_eq!(layout.visual_line_count(), 4);
let mut view = Viewport::new(2);
view.follow(&layout, at(&t, 0, 12));
assert_eq!(view.top(), 1, "the third visual line of the first row");
}
#[test]
fn scrolling_does_not_run_past_the_end() {
let t = text("a\nb\nc");
let layout = plain(&t, 10);
let mut view = Viewport::new(2);
view.scroll_by(&layout, 99);
assert_eq!(view.top(), 2);
view.scroll_by(&layout, -99);
assert_eq!(view.top(), 0);
assert!(!view.scroll_by(&layout, -1), "already at the top");
}
#[test]
fn a_viewport_of_no_height_follows_nothing() {
let t = text("a\nb");
let layout = plain(&t, 10);
let mut view = Viewport::new(0);
assert!(!view.follow(&layout, at(&t, 1, 0)));
}
mod properties {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(200))]
#[test]
fn relayout_agrees_with_a_full_recompute(
initial in "[a-z \n]{0,40}",
inserted in "[a-z \n]{0,8}",
byte in 0usize..48,
width in 1usize..8,
) {
let mut buffer = crate::ropetext::EditBuffer::new(Text::from(initial.as_str()));
let Some(position) = buffer.text().position_at_byte(byte.min(buffer.text().len_bytes()))
else {
return Ok(());
};
let mut layout = Layout::compute(buffer.text(), width, Metrics::default(), &[]);
let mut txn = buffer.begin();
txn.insert(position, &inserted);
let Some(change) = txn.commit() else {
return Ok(());
};
layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
let fresh = Layout::compute(buffer.text(), width, Metrics::default(), &[]);
prop_assert_eq!(
layout.visual_lines(),
fresh.visual_lines(),
"{:?} + {:?} at byte {} width {}", initial, inserted, byte, width
);
prop_assert_eq!(layout.row_count(), fresh.row_count());
}
#[test]
fn relayout_agrees_after_a_deletion(
initial in "[a-z \n]{1,40}",
from in 0usize..48,
len in 0usize..12,
width in 1usize..8,
) {
let mut buffer = crate::ropetext::EditBuffer::new(Text::from(initial.as_str()));
let end = buffer.text().len_bytes();
let Some(start) = buffer.text().position_at_byte(from.min(end)) else {
return Ok(());
};
let Some(stop) = buffer.text().position_at_byte((from + len).min(end)) else {
return Ok(());
};
let span = buffer.text().span(start, stop).expect("same text");
let mut layout = Layout::compute(buffer.text(), width, Metrics::default(), &[]);
let mut txn = buffer.begin();
txn.delete(span);
let Some(change) = txn.commit() else {
return Ok(());
};
layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
let fresh = Layout::compute(buffer.text(), width, Metrics::default(), &[]);
prop_assert_eq!(
layout.visual_lines(),
fresh.visual_lines(),
"{:?} minus {}..{} at width {}", initial, from, from + len, width
);
}
#[test]
fn visual_lines_tile_their_rows(
initial in ".{0,40}",
width in 1usize..8,
) {
let t = Text::from(initial.as_str());
let layout = Layout::compute(&t, width, Metrics::default(), &[]);
prop_assert_eq!(layout.row_count(), t.line_count());
let mut seen_rows = 0;
let mut expected_row = 0;
let mut cursor = 0;
for line in layout.visual_lines() {
if line.first {
prop_assert_eq!(line.logical_row, expected_row, "rows must be in order");
expected_row += 1;
seen_rows += 1;
cursor = 0;
}
let row = t.line(line.logical_row).expect("a named row exists");
prop_assert!(line.bytes.end <= row.len(), "slice past the row");
prop_assert!(line.chars.start >= cursor, "a line went backwards");
prop_assert!(row.is_char_boundary(line.bytes.start));
prop_assert!(row.is_char_boundary(line.bytes.end));
let breaks: Vec<usize> = row
.grapheme_indices(true)
.map(|(at, _)| at)
.chain(std::iter::once(row.len()))
.collect();
prop_assert!(breaks.contains(&line.bytes.start), "start splits a cluster");
prop_assert!(breaks.contains(&line.bytes.end), "end splits a cluster");
cursor = line.chars.end;
}
prop_assert_eq!(seen_rows, t.line_count(), "every row is drawn");
}
}
}
}