Skip to main content

kimun_notes/ropetext/
layout.rs

1//! Where rows break, and which cell a position is drawn in.
2//!
3//! A layout is the visual lines a [`Text`] wraps into at a width, plus the
4//! mapping between a [`Position`] and a screen cell. It is derived, and it is
5//! derived from three things: the text, the width, and the caller's per-row
6//! [`RowHints`].
7//!
8//! A layout does not outlive a resize, which is why it is separate from the
9//! buffer, and it does not hold the text, which is why the queries that need to
10//! measure characters take the text again.
11//!
12//! # Why hints
13//!
14//! A syntax layer that conceals characters — markdown hiding the `#` of a
15//! heading — changes how wide a row draws without changing what it contains. A
16//! layout that measured the row's text would break lines in the wrong places. So
17//! the caller says, per row, which clusters are drawn and how far the row is
18//! inset, and this module never learns what a heading is.
19
20use std::ops::Range;
21
22use unicode_segmentation::UnicodeSegmentation;
23
24use crate::ropetext::position::{Column, Position, Revision};
25use crate::ropetext::text::Text;
26use crate::ropetext::width::Metrics;
27
28/// What a syntax layer tells the layout about one logical row.
29#[derive(Debug, Clone, Copy, Default)]
30pub struct RowHints<'a> {
31    /// Per Unicode scalar of the row: `false` where the renderer draws nothing.
32    /// A shorter slice than the row means the rest is visible, and an empty one
33    /// means all of it is — so a caller with no syntax layer passes nothing.
34    ///
35    /// Stated as *visible* rather than hidden because that is what a syntax layer
36    /// computes: it walks a row deciding what to draw. Inverting it here would cost
37    /// an allocation per row per frame to say the same thing.
38    pub visible: &'a [bool],
39    /// Cells of gutter the renderer draws before the row's text, on the first
40    /// visual line and every continuation of it.
41    pub inset: usize,
42}
43
44/// One drawn line: a slice of a logical row that fits the width.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct VisualLine {
47    pub logical_row: usize,
48    /// Scalar offsets within the logical row.
49    pub chars: Range<usize>,
50    /// Byte offsets within the logical row.
51    pub bytes: Range<usize>,
52    /// Whether this is the row's first visual line, as against a continuation.
53    pub first: bool,
54}
55
56/// A screen cell, relative to the top-left of the laid-out text.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub struct Cell {
59    /// Index into [`Layout::visual_lines`].
60    pub row: usize,
61    /// Cells from the left edge, gutter included.
62    pub column: usize,
63}
64
65/// Where a text's rows break at a given width.
66#[derive(Debug, Clone)]
67pub struct Layout {
68    lines: Vec<VisualLine>,
69    /// Logical row → index of its first visual line. Turns a lookup into a walk
70    /// over one row's wrap count rather than over every visual line.
71    row_starts: Vec<usize>,
72    width: usize,
73    metrics: Metrics,
74    /// Which text this describes.
75    ///
76    /// A [`VisualLine`] holds byte ranges into the text it was laid out from, and
77    /// reading one against a newer text slices out of bounds. Callers used to
78    /// guess at staleness by comparing row counts, which an edit within a single
79    /// row does not change — so shrinking a row and pressing an arrow before the
80    /// next frame panicked. The text already carries an identity; recording it is
81    /// what makes the question answerable rather than approximable.
82    revision: Revision,
83}
84
85impl Layout {
86    /// Whether this layout still describes `text`.
87    ///
88    /// The only safe precondition for anything that reads a [`VisualLine`]'s byte
89    /// range against a text — `cell_of`, `position_at_cell`, and any caller
90    /// slicing a row itself. A row count is not a substitute: an edit inside one
91    /// row leaves it unchanged while every byte range after the edit moves.
92    pub fn describes(&self, text: &Text) -> bool {
93        self.revision == text.revision()
94    }
95
96    /// Lay `text` out as one unwrapped visual line per row — no grapheme
97    /// segmentation, no width measurement, no break search.
98    ///
99    /// For a caller that needs *some* layout describing `text` right now and
100    /// cannot afford `compute`'s cost this instant (a large buffer, off the
101    /// keystroke that triggered a full rebuild). `describes` is true the
102    /// moment this returns, so nothing downstream has to know the wrap is
103    /// wrong — only that a genuinely long row will not soft-wrap until a
104    /// real `compute` replaces this one. `row_count` still matches `text`,
105    /// which is the invariant every other reader depends on.
106    pub fn unwrapped(text: &Text) -> Self {
107        let mut lines = Vec::with_capacity(text.line_count());
108        let mut row_starts = Vec::with_capacity(text.line_count());
109        for row in 0..text.line_count() {
110            row_starts.push(lines.len());
111            let Some(source) = text.line(row) else {
112                continue;
113            };
114            lines.push(VisualLine {
115                logical_row: row,
116                chars: 0..source.chars().count(),
117                bytes: 0..source.len(),
118                first: true,
119            });
120        }
121        Self {
122            lines,
123            row_starts,
124            width: 0,
125            metrics: Metrics::default(),
126            revision: text.revision(),
127        }
128    }
129
130    /// Lay `text` out at `width` cells.
131    pub fn compute(text: &Text, width: usize, metrics: Metrics, hints: &[RowHints<'_>]) -> Self {
132        let mut layout = Self {
133            lines: Vec::new(),
134            row_starts: Vec::with_capacity(text.line_count()),
135            width,
136            metrics,
137            revision: text.revision(),
138        };
139        let mut scratch = Vec::new();
140        for row in 0..text.line_count() {
141            layout.row_starts.push(layout.lines.len());
142            wrap_row(
143                text,
144                row,
145                width,
146                metrics,
147                hint_for(hints, row),
148                &mut scratch,
149                &mut layout.lines,
150            );
151        }
152        layout
153    }
154
155    /// Re-wrap `rows` in place, leaving the rest alone.
156    ///
157    /// For a caller holding a [`Change`](crate::ropetext::Change), whose `rows` is exactly
158    /// this argument. Rows outside the range must be unchanged in content and in
159    /// hints; rows inside it may have become any number of visual lines.
160    pub fn relayout_rows(
161        &mut self,
162        text: &Text,
163        hints: &[RowHints<'_>],
164        rows: Range<usize>,
165        line_delta: isize,
166    ) {
167        // Whatever else this does, afterwards the layout describes `text`.
168        self.revision = text.revision();
169        // `rows` is in the *new* text's numbering, because that is what a `Change`
170        // reports. The layout is still in the old text's, so the region being
171        // replaced has to be named twice: once to find what to throw away, once to
172        // say what replaces it.
173        let rows = rows.start.min(text.line_count())..rows.end.min(text.line_count());
174        let old_rows = {
175            let end = (rows.end as isize - line_delta).max(rows.start as isize) as usize;
176            rows.start.min(self.row_starts.len())..end.min(self.row_starts.len())
177        };
178        if rows.is_empty() && old_rows.is_empty() {
179            return;
180        }
181
182        let old_start = self
183            .row_starts
184            .get(old_rows.start)
185            .copied()
186            .unwrap_or(self.lines.len());
187        let old_end = self
188            .row_starts
189            .get(old_rows.end)
190            .copied()
191            .unwrap_or(self.lines.len());
192
193        let mut replacement = Vec::new();
194        let mut starts = Vec::with_capacity(rows.len());
195        let mut scratch = Vec::new();
196        for row in rows.clone() {
197            starts.push(old_start + replacement.len());
198            wrap_row(
199                text,
200                row,
201                self.width,
202                self.metrics,
203                hint_for(hints, row),
204                &mut scratch,
205                &mut replacement,
206            );
207        }
208
209        let added = replacement.len();
210        self.lines.splice(old_start..old_end, replacement);
211
212        // Every visual line after the replaced region belongs to a row that has
213        // moved. Renumbering them is what keeps a visual line pointing at the row
214        // it draws — without it, a row inserted above leaves every line below
215        // slicing the wrong row's text, which reads as corruption rather than as a
216        // stale layout.
217        if line_delta != 0 {
218            for line in &mut self.lines[old_start + added..] {
219                line.logical_row = (line.logical_row as isize + line_delta) as usize;
220            }
221        }
222
223        self.row_starts.splice(old_rows, starts);
224        let shift = added as isize - (old_end - old_start) as isize;
225        if shift != 0 {
226            let tail = rows.end.min(self.row_starts.len());
227            for start in &mut self.row_starts[tail..] {
228                *start = (*start as isize + shift) as usize;
229            }
230        }
231        debug_assert_eq!(
232            self.row_starts.len(),
233            text.line_count(),
234            "relayout left the layout describing a different number of rows"
235        );
236    }
237
238    pub fn visual_lines(&self) -> &[VisualLine] {
239        &self.lines
240    }
241
242    /// How many visual lines the text occupies. Never zero.
243    pub fn visual_line_count(&self) -> usize {
244        self.lines.len()
245    }
246
247    /// How many logical rows this layout was built for. A caller comparing this
248    /// with the text's row count is asking whether the layout is stale.
249    pub fn row_count(&self) -> usize {
250        self.row_starts.len()
251    }
252
253    pub fn width(&self) -> usize {
254        self.width
255    }
256
257    /// Which visual line `position` is drawn on.
258    pub fn visual_row_of(&self, position: Position) -> usize {
259        let row = position.row().min(self.row_starts.len().saturating_sub(1));
260        let first = self.row_starts.get(row).copied().unwrap_or(0);
261        let column = position.column().get();
262        self.lines[first..]
263            .iter()
264            .take_while(|line| line.logical_row == row)
265            .enumerate()
266            .filter(|(_, line)| line.chars.start <= column)
267            .map(|(offset, _)| first + offset)
268            .last()
269            .unwrap_or(first)
270    }
271
272    /// Which cell `position` is drawn in.
273    ///
274    /// Takes the text and the hints because the layout stores where rows break,
275    /// not what they contain, and a cell is a measurement of content.
276    pub fn cell_of(&self, text: &Text, hints: &[RowHints<'_>], position: Position) -> Cell {
277        // Returns a cell rather than an option, so it cannot refuse a stale text
278        // the way `position_at_cell` does — the caller has to have checked. This
279        // is what says so, and what catches a caller that has not.
280        debug_assert!(
281            self.describes(text),
282            "cell_of read against a text this layout does not describe"
283        );
284        let row = self.visual_row_of(position);
285        let line = &self.lines[row];
286        let hint = hint_for(hints, line.logical_row);
287        let Some(source) = text.line(line.logical_row) else {
288            return Cell {
289                row,
290                column: hint.inset,
291            };
292        };
293        let mut column = hint.inset;
294        let mut chars = line.chars.start;
295        for cluster in source[line.bytes.clone()].graphemes(true) {
296            if chars >= position.column().get() {
297                break;
298            }
299            if visible(&hint, chars) {
300                column += self.metrics.width_at(cluster, column - hint.inset);
301            }
302            chars += cluster.chars().count();
303        }
304        Cell { row, column }
305    }
306
307    /// The position drawn at `cell`, or `None` if there is no such visual line.
308    ///
309    /// A column past the end of a visual line lands at its end, and a column
310    /// inside a wide cluster lands on that cluster: a click between the halves of
311    /// a CJK character means the character.
312    pub fn position_at_cell(
313        &self,
314        text: &Text,
315        hints: &[RowHints<'_>],
316        cell: Cell,
317    ) -> Option<Position> {
318        // A visual line's byte range addresses the text this was laid out from.
319        // Read against a newer one it slices out of bounds, so a stale layout is
320        // refused here rather than trusted — see [`Self::describes`].
321        if !self.describes(text) {
322            return None;
323        }
324        let line = self.lines.get(cell.row)?;
325        let hint = hint_for(hints, line.logical_row);
326        let source = text.line(line.logical_row)?;
327        let mut column = hint.inset;
328        let mut chars = line.chars.start;
329        // No short circuit for a cell inside the inset. Returning the row's
330        // first char here would skip the loop that walks past the row's leading
331        // undrawn clusters, and a syntax layer that hides a marker *and* insets
332        // the row for it — a blockquote drawing a bar in place of `> ` — would
333        // land a click on the hidden marker rather than on the first drawn
334        // character. The loop already answers this: undrawn clusters measure
335        // zero, so a cell in the gutter falls into the first drawn cluster's
336        // span and resolves to it.
337        for cluster in source[line.bytes.clone()].graphemes(true) {
338            let width = if visible(&hint, chars) {
339                self.metrics.width_at(cluster, column - hint.inset)
340            } else {
341                0
342            };
343            if width > 0 && cell.column < column + width {
344                return text.position(line.logical_row, Column::new(chars));
345            }
346            column += width;
347            chars += cluster.chars().count();
348        }
349        text.position(line.logical_row, Column::new(line.chars.end))
350    }
351}
352
353/// The visible part of a scrolled layout.
354///
355/// Kept apart from [`Layout`] on purpose: a layout is thrown away and rebuilt
356/// when the pane is resized, and where the reader had scrolled to is not.
357#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
358pub struct Viewport {
359    top: usize,
360    height: usize,
361}
362
363impl Viewport {
364    pub fn new(height: usize) -> Self {
365        Self { top: 0, height }
366    }
367
368    pub fn top(&self) -> usize {
369        self.top
370    }
371
372    pub fn height(&self) -> usize {
373        self.height
374    }
375
376    pub fn set_height(&mut self, height: usize) {
377        self.height = height;
378    }
379
380    /// The visual lines on screen.
381    pub fn rows(&self, layout: &Layout) -> Range<usize> {
382        let top = self.top.min(layout.visual_line_count().saturating_sub(1));
383        top..(top + self.height).min(layout.visual_line_count())
384    }
385
386    /// Scroll the least amount that brings `cursor` on screen. Returns whether it
387    /// moved.
388    pub fn follow(&mut self, layout: &Layout, cursor: Position) -> bool {
389        if self.height == 0 {
390            return false;
391        }
392        let row = layout.visual_row_of(cursor);
393        let was = self.top;
394        if row < self.top {
395            self.top = row;
396        } else if row >= self.top + self.height {
397            self.top = row + 1 - self.height;
398        }
399        self.top != was
400    }
401
402    /// Scroll by `delta` visual lines, without moving the cursor. Returns whether
403    /// it moved.
404    pub fn scroll_by(&mut self, layout: &Layout, delta: isize) -> bool {
405        let was = self.top;
406        let last = layout.visual_line_count().saturating_sub(1);
407        self.top = if delta >= 0 {
408            (self.top + delta.unsigned_abs()).min(last)
409        } else {
410            self.top.saturating_sub(delta.unsigned_abs())
411        };
412        self.top != was
413    }
414}
415
416// -- wrapping ----------------------------------------------------------------
417
418/// One cluster of a row, as wrapping sees it.
419struct Cluster {
420    chars: usize,
421    bytes: usize,
422    /// Byte length, so the cluster's text can be re-sliced to measure it.
423    len: usize,
424    /// A single whitespace scalar, and so a place a break may land. A cluster of
425    /// several scalars is never whitespace.
426    breakable: bool,
427}
428
429fn hint_for<'a>(hints: &'a [RowHints<'a>], row: usize) -> RowHints<'a> {
430    hints.get(row).copied().unwrap_or_default()
431}
432
433fn visible(hint: &RowHints<'_>, chars: usize) -> bool {
434    hint.visible.get(chars).copied().unwrap_or(true)
435}
436
437/// Wrap one logical row, appending at least one visual line.
438fn wrap_row(
439    text: &Text,
440    row: usize,
441    width: usize,
442    metrics: Metrics,
443    hint: RowHints<'_>,
444    scratch: &mut Vec<Cluster>,
445    out: &mut Vec<VisualLine>,
446) {
447    // The gutter eats into the width available for text. `.max(1)` keeps forward
448    // progress when the gutter is as wide as the pane; a genuinely zero-width pane
449    // is left at zero so it falls into the degenerate case below.
450    let width = if hint.inset == 0 {
451        width
452    } else {
453        width.saturating_sub(hint.inset).max(1)
454    };
455
456    let Some(source) = text.line(row) else {
457        return;
458    };
459
460    scratch.clear();
461    let mut chars = 0;
462    for (bytes, cluster) in source.grapheme_indices(true) {
463        let len = cluster.chars().count();
464        scratch.push(Cluster {
465            chars,
466            bytes,
467            len: cluster.len(),
468            breakable: len == 1 && cluster.chars().next().is_some_and(char::is_whitespace),
469        });
470        chars += len;
471    }
472    let total_chars = chars;
473    let total_bytes = source.len();
474
475    if scratch.is_empty() || width == 0 {
476        out.push(VisualLine {
477            logical_row: row,
478            chars: 0..0,
479            bytes: 0..0,
480            first: true,
481        });
482        return;
483    }
484
485    let cell_width = |index: usize, column: usize| -> usize {
486        let cluster = &scratch[index];
487        if visible(&hint, cluster.chars) {
488            let at = cluster.bytes;
489            metrics.width_at(&source[at..at + cluster.len], column)
490        } else {
491            0
492        }
493    };
494    let char_at =
495        |index: usize| -> usize { scratch.get(index).map(|c| c.chars).unwrap_or(total_chars) };
496    let byte_at =
497        |index: usize| -> usize { scratch.get(index).map(|c| c.bytes).unwrap_or(total_bytes) };
498
499    let total = scratch.len();
500    let mut start = 0;
501    let mut first = true;
502
503    while start < total {
504        // Where the row stops fitting. The column resets per visual line, so a tab
505        // on a continuation row measures from that row's own left edge — which is
506        // what the renderer draws.
507        let fit_end = {
508            let mut column = 0;
509            let mut index = start;
510            while index < total {
511                let cells = cell_width(index, column);
512                if column + cells > width {
513                    break;
514                }
515                column += cells;
516                index += 1;
517            }
518            // A single cluster wider than the pane must still advance, or the loop
519            // never ends.
520            if index == start { start + 1 } else { index }
521        };
522
523        if fit_end >= total {
524            out.push(VisualLine {
525                logical_row: row,
526                chars: char_at(start)..total_chars,
527                bytes: byte_at(start)..total_bytes,
528                first,
529            });
530            break;
531        }
532
533        // Prefer breaking at the last whitespace that fits; otherwise break mid
534        // word, always on a cluster boundary.
535        let (content_end, next_start) = if scratch[fit_end].breakable {
536            (fit_end, fit_end + 1)
537        } else {
538            match scratch[start..fit_end]
539                .iter()
540                .enumerate()
541                .rev()
542                .find(|(_, cluster)| cluster.breakable)
543            {
544                Some((offset, _)) => (start + offset, start + offset + 1),
545                None => (fit_end, fit_end),
546            }
547        };
548
549        out.push(VisualLine {
550            logical_row: row,
551            chars: char_at(start)..char_at(content_end),
552            bytes: byte_at(start)..byte_at(content_end),
553            first,
554        });
555        start = next_start;
556        first = false;
557    }
558}
559
560#[cfg(test)]
561mod tests {
562    use super::*;
563
564    fn text(s: &str) -> Text {
565        Text::from(s)
566    }
567
568    fn plain(text: &Text, width: usize) -> Layout {
569        Layout::compute(text, width, Metrics::default(), &[])
570    }
571
572    /// The drawn content of each visual line.
573    fn drawn(text: &Text, layout: &Layout) -> Vec<String> {
574        layout
575            .visual_lines()
576            .iter()
577            .map(|line| {
578                let row = text.line(line.logical_row).expect("row exists");
579                row[line.bytes.clone()].to_string()
580            })
581            .collect()
582    }
583
584    fn at(text: &Text, row: usize, col: usize) -> Position {
585        text.position(row, Column::new(col)).expect("addressable")
586    }
587
588    // -- wrapping -----------------------------------------------------------
589
590    #[test]
591    fn a_row_that_fits_is_one_visual_line() {
592        let t = text("short");
593        assert_eq!(drawn(&t, &plain(&t, 10)), ["short"]);
594    }
595
596    #[test]
597    fn wrapping_prefers_a_space() {
598        let t = text("aaaa bbbb");
599        assert_eq!(drawn(&t, &plain(&t, 6)), ["aaaa", "bbbb"]);
600    }
601
602    #[test]
603    fn a_word_longer_than_the_width_breaks_mid_word() {
604        let t = text("aaaaaaaa");
605        assert_eq!(drawn(&t, &plain(&t, 3)), ["aaa", "aaa", "aa"]);
606    }
607
608    #[test]
609    fn an_empty_row_is_still_a_visual_line() {
610        let t = text("a\n\nb");
611        let layout = plain(&t, 10);
612        assert_eq!(layout.visual_line_count(), 3);
613        assert_eq!(drawn(&t, &layout), ["a", "", "b"]);
614    }
615
616    #[test]
617    fn a_zero_width_pane_still_produces_one_line_per_row() {
618        let t = text("a\nb");
619        let layout = plain(&t, 0);
620        assert_eq!(layout.visual_line_count(), 2);
621    }
622
623    #[test]
624    fn a_cluster_wider_than_the_pane_still_advances() {
625        // A width-2 glyph in a width-1 pane cannot fit, and must not loop.
626        let t = text("\u{3042}\u{3042}");
627        let layout = plain(&t, 1);
628        assert_eq!(layout.visual_line_count(), 2);
629    }
630
631    #[test]
632    fn a_cluster_is_never_split_across_visual_lines() {
633        // A break landing inside a cluster would hand the renderer half a glyph,
634        // and the halves would reclusterl differently from the whole — so every
635        // column derived from either row would be wrong from that point on.
636        let family = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}";
637        let t = text(&format!("ab{family}cd"));
638        for width in 1..10 {
639            let layout = plain(&t, width);
640            let row = t.line(0).expect("one row");
641            for line in layout.visual_lines() {
642                assert!(
643                    row.is_char_boundary(line.bytes.start) && row.is_char_boundary(line.bytes.end),
644                    "width {width}: {:?} splits a character",
645                    line.bytes
646                );
647                let intact: Vec<usize> = row.grapheme_indices(true).map(|(at, _)| at).collect();
648                assert!(
649                    intact.contains(&line.bytes.start) || line.bytes.start == row.len(),
650                    "width {width}: {:?} starts inside a cluster",
651                    line.bytes
652                );
653                assert!(
654                    intact.contains(&line.bytes.end) || line.bytes.end == row.len(),
655                    "width {width}: {:?} ends inside a cluster",
656                    line.bytes
657                );
658            }
659        }
660    }
661
662    #[test]
663    fn a_gutter_eats_into_the_width() {
664        let t = text("aaaa bbbb");
665        let hints = [RowHints {
666            visible: &[],
667            inset: 2,
668        }];
669        let layout = Layout::compute(&t, 9, Metrics::default(), &hints);
670        assert_eq!(
671            drawn(&t, &layout),
672            ["aaaa", "bbbb"],
673            "nine cells less a two-cell gutter does not fit nine characters"
674        );
675        assert_eq!(
676            plain(&t, 9).visual_line_count(),
677            1,
678            "and without the gutter it does"
679        );
680    }
681
682    #[test]
683    fn a_gutter_as_wide_as_the_pane_still_makes_progress() {
684        let t = text("aaaa");
685        let hints = [RowHints {
686            visible: &[],
687            inset: 4,
688        }];
689        let layout = Layout::compute(&t, 4, Metrics::default(), &hints);
690        assert_eq!(
691            layout.visual_line_count(),
692            4,
693            "one cell per line, not a loop"
694        );
695    }
696
697    #[test]
698    fn undrawn_clusters_take_no_width() {
699        // "## " concealed, as a heading's sigils are: the row draws as "heading"
700        // and so fits a pane that its raw text would not.
701        let t = text("## heading");
702        let visible = vec![
703            false, false, false, true, true, true, true, true, true, true,
704        ];
705        let hints = [RowHints {
706            visible: &visible,
707            inset: 0,
708        }];
709        let layout = Layout::compute(&t, 7, Metrics::default(), &hints);
710        assert_eq!(layout.visual_line_count(), 1);
711        assert_eq!(
712            plain(&t, 7).visual_line_count(),
713            2,
714            "measuring the sigils would wrap it"
715        );
716    }
717
718    #[test]
719    fn a_tab_is_measured_to_its_stop_when_wrapping() {
720        // Four cells of tab plus four of text is eight; a seven-cell pane wraps.
721        let t = text("\tabcd");
722        assert_eq!(plain(&t, 8).visual_line_count(), 1);
723        assert_eq!(plain(&t, 7).visual_line_count(), 2);
724    }
725
726    #[test]
727    fn a_tab_is_itself_a_place_to_break() {
728        // A tab is whitespace, so when it does not fit it becomes the break rather
729        // than being pushed to the next line.
730        let t = text("ab cd\tef");
731        assert_eq!(drawn(&t, &plain(&t, 5)), ["ab cd", "ef"]);
732    }
733
734    #[test]
735    fn a_tab_measures_from_the_start_of_its_own_visual_line() {
736        // On the continuation line the tab sits at column 2, so it advances 2 cells
737        // to the next stop and "c" no longer fits. Measured from the logical row's
738        // column 7 it would advance only 1, and "c" would fit — so this is the
739        // assertion that pins which of the two models is in use.
740        let t = text("aaaa bb\tc");
741        assert_eq!(drawn(&t, &plain(&t, 4)), ["aaaa", "bb", "c"]);
742    }
743
744    // -- lookups ------------------------------------------------------------
745
746    #[test]
747    fn a_position_knows_which_visual_line_draws_it() {
748        let t = text("aaaa bbbb cccc");
749        let layout = plain(&t, 5);
750        assert_eq!(drawn(&t, &layout), ["aaaa", "bbbb", "cccc"]);
751        assert_eq!(layout.visual_row_of(at(&t, 0, 0)), 0);
752        assert_eq!(layout.visual_row_of(at(&t, 0, 5)), 1);
753        assert_eq!(layout.visual_row_of(at(&t, 0, 12)), 2);
754    }
755
756    #[test]
757    fn visual_rows_are_found_across_logical_rows() {
758        let t = text("aaaa bbbb\nsecond");
759        let layout = plain(&t, 5);
760        assert_eq!(drawn(&t, &layout), ["aaaa", "bbbb", "secon", "d"]);
761        assert_eq!(layout.visual_row_of(at(&t, 1, 0)), 2);
762        assert_eq!(layout.visual_row_of(at(&t, 1, 5)), 3);
763    }
764
765    #[test]
766    fn a_cell_accounts_for_the_gutter() {
767        let t = text("abc");
768        let hints = [RowHints {
769            visible: &[],
770            inset: 2,
771        }];
772        let layout = Layout::compute(&t, 10, Metrics::default(), &hints);
773        assert_eq!(
774            layout.cell_of(&t, &hints, at(&t, 0, 1)),
775            Cell { row: 0, column: 3 }
776        );
777    }
778
779    #[test]
780    fn a_cell_skips_hidden_clusters() {
781        let t = text("## heading");
782        let visible = vec![false, false, false];
783        let hints = [RowHints {
784            visible: &visible,
785            inset: 0,
786        }];
787        let layout = Layout::compute(&t, 40, Metrics::default(), &hints);
788        assert_eq!(
789            layout.cell_of(&t, &hints, at(&t, 0, 3)),
790            Cell { row: 0, column: 0 },
791            "the first drawn character is in the first cell"
792        );
793    }
794
795    #[test]
796    fn a_cell_counts_a_wide_cluster_as_two() {
797        let t = text("\u{3042}b");
798        let layout = plain(&t, 40);
799        assert_eq!(layout.cell_of(&t, &[], at(&t, 0, 1)).column, 2);
800    }
801
802    #[test]
803    fn a_click_inside_a_wide_cluster_means_that_cluster() {
804        let t = text("\u{3042}b");
805        let layout = plain(&t, 40);
806        for column in [0, 1] {
807            let landed = layout
808                .position_at_cell(&t, &[], Cell { row: 0, column })
809                .expect("inside the line");
810            assert_eq!(landed.column().get(), 0, "column {column}");
811        }
812        let landed = layout
813            .position_at_cell(&t, &[], Cell { row: 0, column: 2 })
814            .expect("inside the line");
815        assert_eq!(landed.column().get(), 1);
816    }
817
818    #[test]
819    fn a_click_past_the_end_of_a_visual_line_lands_at_its_end() {
820        let t = text("aaaa bbbb");
821        let layout = plain(&t, 5);
822        let landed = layout
823            .position_at_cell(&t, &[], Cell { row: 0, column: 99 })
824            .expect("inside the line");
825        assert_eq!(landed.column().get(), 4, "the end of the first visual line");
826    }
827
828    #[test]
829    fn a_click_in_the_gutter_lands_at_the_start_of_the_text() {
830        let t = text("abc");
831        let hints = [RowHints {
832            visible: &[],
833            inset: 3,
834        }];
835        let layout = Layout::compute(&t, 10, Metrics::default(), &hints);
836        let landed = layout
837            .position_at_cell(&t, &hints, Cell { row: 0, column: 1 })
838            .expect("inside the line");
839        assert_eq!(landed.column().get(), 0);
840    }
841
842    #[test]
843    fn a_click_below_the_text_finds_nothing() {
844        let t = text("abc");
845        let layout = plain(&t, 10);
846        assert!(
847            layout
848                .position_at_cell(&t, &[], Cell { row: 9, column: 0 })
849                .is_none()
850        );
851    }
852
853    #[test]
854    fn cells_and_positions_round_trip() {
855        let t = text("aaaa bbbb cccc");
856        let layout = plain(&t, 5);
857        for column in 0..14 {
858            let position = at(&t, 0, column);
859            let cell = layout.cell_of(&t, &[], position);
860            let back = layout
861                .position_at_cell(&t, &[], cell)
862                .expect("its own cell is inside the line");
863            assert_eq!(back, position, "column {column}");
864        }
865    }
866
867    // -- unwrapped ------------------------------------------------------------
868
869    #[test]
870    fn unwrapped_matches_row_count_and_describes_text() {
871        let t = text("short\na longer row that would wrap at a narrow width\nlast");
872        let layout = Layout::unwrapped(&t);
873        assert_eq!(layout.row_count(), t.line_count());
874        assert_eq!(layout.visual_line_count(), t.line_count());
875        assert!(
876            layout.describes(&t),
877            "unwrapped must describe the text it was built from"
878        );
879        assert_eq!(
880            drawn(&t, &layout),
881            [
882                "short",
883                "a longer row that would wrap at a narrow width",
884                "last"
885            ],
886            "one unwrapped visual line per row"
887        );
888    }
889
890    #[test]
891    fn unwrapped_handles_an_empty_text() {
892        let t = text("");
893        let layout = Layout::unwrapped(&t);
894        assert_eq!(layout.row_count(), t.line_count());
895        assert!(layout.describes(&t));
896    }
897
898    // -- relayout -----------------------------------------------------------
899
900    #[test]
901    fn relayout_rewraps_only_what_changed() {
902        let mut buffer = crate::ropetext::EditBuffer::new(text("aaaa bbbb\nkeep\ntail"));
903        let mut layout = plain(buffer.text(), 5);
904        assert_eq!(layout.visual_line_count(), 4);
905
906        let end = buffer.text().position(0, Column::new(9)).unwrap();
907        let mut txn = buffer.begin();
908        txn.delete(buffer_span(&txn, 0, 4, 0, 9));
909        let change = txn.commit().expect("changed");
910        let _ = end;
911
912        layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
913        assert_eq!(drawn(buffer.text(), &layout), ["aaaa", "keep", "tail"]);
914        assert_eq!(layout.row_count(), buffer.text().line_count());
915    }
916
917    #[test]
918    fn relayout_follows_added_rows() {
919        let mut buffer = crate::ropetext::EditBuffer::new(text("one\ntwo"));
920        let mut layout = plain(buffer.text(), 10);
921        let at_end = buffer.text().position(0, Column::new(3)).unwrap();
922        let mut txn = buffer.begin();
923        txn.insert(at_end, "\nmiddle");
924        let change = txn.commit().expect("changed");
925
926        layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
927        assert_eq!(drawn(buffer.text(), &layout), ["one", "middle", "two"]);
928        assert_eq!(layout.row_count(), 3);
929    }
930
931    #[test]
932    fn relayout_follows_removed_rows() {
933        let mut buffer = crate::ropetext::EditBuffer::new(text("one\ntwo\nthree\nfour"));
934        let mut layout = plain(buffer.text(), 10);
935        let mut txn = buffer.begin();
936        txn.delete(buffer_span(&txn, 0, 3, 2, 5));
937        let change = txn.commit().expect("changed");
938
939        layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
940        assert_eq!(drawn(buffer.text(), &layout), ["one", "four"]);
941        assert_eq!(layout.row_count(), 2);
942    }
943
944    #[test]
945    fn relayout_matches_a_full_recompute() {
946        // The cheap path and the honest path must agree, or an incremental
947        // relayout is a way to be quietly wrong for the rest of the session.
948        for (initial, row, col, inserted) in [
949            ("aaaa bbbb\nkeep", 0, 4, " cccc"),
950            ("one\ntwo\nthree", 1, 3, "\nsplit"),
951            ("one\ntwo", 0, 0, "prefix "),
952            ("wrapped line that is long\nnext", 0, 8, "\n"),
953        ] {
954            let mut buffer = crate::ropetext::EditBuffer::new(text(initial));
955            let mut layout = plain(buffer.text(), 6);
956            let position = buffer.text().position(row, Column::new(col)).unwrap();
957            let mut txn = buffer.begin();
958            txn.insert(position, inserted);
959            let change = txn.commit().expect("changed");
960
961            layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
962            let fresh = plain(buffer.text(), 6);
963            assert_eq!(
964                layout.visual_lines(),
965                fresh.visual_lines(),
966                "relayout disagreed for {initial:?} + {inserted:?}"
967            );
968        }
969    }
970
971    fn buffer_span(
972        txn: &crate::ropetext::Txn<'_>,
973        r1: usize,
974        c1: usize,
975        r2: usize,
976        c2: usize,
977    ) -> crate::ropetext::Span {
978        let text = txn.text();
979        let a = text.position(r1, Column::new(c1)).expect("addressable");
980        let b = text.position(r2, Column::new(c2)).expect("addressable");
981        text.span(a, b).expect("same text")
982    }
983
984    // -- viewport -----------------------------------------------------------
985
986    #[test]
987    fn a_viewport_shows_its_height_of_lines() {
988        let t = text("a\nb\nc\nd\ne");
989        let layout = plain(&t, 10);
990        let view = Viewport::new(3);
991        assert_eq!(view.rows(&layout), 0..3);
992    }
993
994    #[test]
995    fn a_viewport_clamps_to_what_there_is() {
996        let t = text("a\nb");
997        let layout = plain(&t, 10);
998        let view = Viewport::new(10);
999        assert_eq!(view.rows(&layout), 0..2);
1000    }
1001
1002    #[test]
1003    fn following_the_cursor_scrolls_the_least_it_can() {
1004        let t = text("a\nb\nc\nd\ne");
1005        let layout = plain(&t, 10);
1006        let mut view = Viewport::new(3);
1007        assert!(view.follow(&layout, at(&t, 4, 0)));
1008        assert_eq!(view.top(), 2, "just enough to show the last row");
1009        assert!(!view.follow(&layout, at(&t, 3, 0)), "already on screen");
1010        assert!(view.follow(&layout, at(&t, 0, 0)));
1011        assert_eq!(view.top(), 0);
1012    }
1013
1014    #[test]
1015    fn following_the_cursor_counts_visual_lines_not_rows() {
1016        let t = text("aaaa bbbb cccc\nlast");
1017        let layout = plain(&t, 5);
1018        assert_eq!(layout.visual_line_count(), 4);
1019        let mut view = Viewport::new(2);
1020        view.follow(&layout, at(&t, 0, 12));
1021        assert_eq!(view.top(), 1, "the third visual line of the first row");
1022    }
1023
1024    #[test]
1025    fn scrolling_does_not_run_past_the_end() {
1026        let t = text("a\nb\nc");
1027        let layout = plain(&t, 10);
1028        let mut view = Viewport::new(2);
1029        view.scroll_by(&layout, 99);
1030        assert_eq!(view.top(), 2);
1031        view.scroll_by(&layout, -99);
1032        assert_eq!(view.top(), 0);
1033        assert!(!view.scroll_by(&layout, -1), "already at the top");
1034    }
1035
1036    #[test]
1037    fn a_viewport_of_no_height_follows_nothing() {
1038        let t = text("a\nb");
1039        let layout = plain(&t, 10);
1040        let mut view = Viewport::new(0);
1041        assert!(!view.follow(&layout, at(&t, 1, 0)));
1042    }
1043
1044    // -- properties ---------------------------------------------------------
1045
1046    mod properties {
1047        use super::*;
1048        use proptest::prelude::*;
1049
1050        proptest! {
1051            #![proptest_config(ProptestConfig::with_cases(200))]
1052
1053            /// The incremental relayout agrees with a full recompute, for any edit
1054            /// at any place and any width.
1055            ///
1056            /// This is the property the incremental path lives or dies by. A
1057            /// relayout that is merely *close* is a way to be quietly wrong for the
1058            /// rest of a session, and the failure shows up as text drawn from the
1059            /// wrong row rather than as anything that looks like a layout bug.
1060            #[test]
1061            fn relayout_agrees_with_a_full_recompute(
1062                initial in "[a-z \n]{0,40}",
1063                inserted in "[a-z \n]{0,8}",
1064                byte in 0usize..48,
1065                width in 1usize..8,
1066            ) {
1067                let mut buffer = crate::ropetext::EditBuffer::new(Text::from(initial.as_str()));
1068                let Some(position) = buffer.text().position_at_byte(byte.min(buffer.text().len_bytes()))
1069                else {
1070                    return Ok(());
1071                };
1072                let mut layout = Layout::compute(buffer.text(), width, Metrics::default(), &[]);
1073
1074                let mut txn = buffer.begin();
1075                txn.insert(position, &inserted);
1076                let Some(change) = txn.commit() else {
1077                    return Ok(());
1078                };
1079
1080                layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
1081                let fresh = Layout::compute(buffer.text(), width, Metrics::default(), &[]);
1082                prop_assert_eq!(
1083                    layout.visual_lines(),
1084                    fresh.visual_lines(),
1085                    "{:?} + {:?} at byte {} width {}", initial, inserted, byte, width
1086                );
1087                prop_assert_eq!(layout.row_count(), fresh.row_count());
1088            }
1089
1090            /// Same, for deletions.
1091            #[test]
1092            fn relayout_agrees_after_a_deletion(
1093                initial in "[a-z \n]{1,40}",
1094                from in 0usize..48,
1095                len in 0usize..12,
1096                width in 1usize..8,
1097            ) {
1098                let mut buffer = crate::ropetext::EditBuffer::new(Text::from(initial.as_str()));
1099                let end = buffer.text().len_bytes();
1100                let Some(start) = buffer.text().position_at_byte(from.min(end)) else {
1101                    return Ok(());
1102                };
1103                let Some(stop) = buffer.text().position_at_byte((from + len).min(end)) else {
1104                    return Ok(());
1105                };
1106                let span = buffer.text().span(start, stop).expect("same text");
1107                let mut layout = Layout::compute(buffer.text(), width, Metrics::default(), &[]);
1108
1109                let mut txn = buffer.begin();
1110                txn.delete(span);
1111                let Some(change) = txn.commit() else {
1112                    return Ok(());
1113                };
1114
1115                layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
1116                let fresh = Layout::compute(buffer.text(), width, Metrics::default(), &[]);
1117                prop_assert_eq!(
1118                    layout.visual_lines(),
1119                    fresh.visual_lines(),
1120                    "{:?} minus {}..{} at width {}", initial, from, from + len, width
1121                );
1122            }
1123
1124            /// Every visual line covers a real slice of the row it names, and the
1125            /// lines of one row cover the row in order without gaps.
1126            #[test]
1127            fn visual_lines_tile_their_rows(
1128                initial in ".{0,40}",
1129                width in 1usize..8,
1130            ) {
1131                let t = Text::from(initial.as_str());
1132                let layout = Layout::compute(&t, width, Metrics::default(), &[]);
1133                prop_assert_eq!(layout.row_count(), t.line_count());
1134                let mut seen_rows = 0;
1135                let mut expected_row = 0;
1136                let mut cursor = 0;
1137                for line in layout.visual_lines() {
1138                    if line.first {
1139                        prop_assert_eq!(line.logical_row, expected_row, "rows must be in order");
1140                        expected_row += 1;
1141                        seen_rows += 1;
1142                        cursor = 0;
1143                    }
1144                    let row = t.line(line.logical_row).expect("a named row exists");
1145                    prop_assert!(line.bytes.end <= row.len(), "slice past the row");
1146                    prop_assert!(line.chars.start >= cursor, "a line went backwards");
1147                    prop_assert!(row.is_char_boundary(line.bytes.start));
1148                    prop_assert!(row.is_char_boundary(line.bytes.end));
1149                    // Both ends land between grapheme clusters, so a visual line's
1150                    // slice reclusters exactly as the whole row does.
1151                    let breaks: Vec<usize> = row
1152                        .grapheme_indices(true)
1153                        .map(|(at, _)| at)
1154                        .chain(std::iter::once(row.len()))
1155                        .collect();
1156                    prop_assert!(breaks.contains(&line.bytes.start), "start splits a cluster");
1157                    prop_assert!(breaks.contains(&line.bytes.end), "end splits a cluster");
1158                    cursor = line.chars.end;
1159                }
1160                prop_assert_eq!(seen_rows, t.line_count(), "every row is drawn");
1161            }
1162        }
1163    }
1164}