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        self.follow_row(layout.visual_row_of(cursor))
390    }
391
392    /// [`Self::follow`] for a visual row already resolved by the caller.
393    pub fn follow_row(&mut self, row: usize) -> bool {
394        if self.height == 0 {
395            return false;
396        }
397        let was = self.top;
398        if row < self.top {
399            self.top = row;
400        } else if row >= self.top + self.height {
401            self.top = row + 1 - self.height;
402        }
403        self.top != was
404    }
405
406    /// Scroll by `delta` visual lines, without moving the cursor, never past
407    /// [`Self::max_top`]. Returns whether it moved.
408    pub fn scroll_by(&mut self, layout: &Layout, delta: isize) -> bool {
409        let was = self.top;
410        self.top = self
411            .top
412            .saturating_add_signed(delta)
413            .min(self.max_top(layout));
414        self.top != was
415    }
416
417    /// The furthest the view may scroll: the last visual line on the bottom
418    /// row, so no blank rows show below the content while any sits above.
419    pub fn max_top(&self, layout: &Layout) -> usize {
420        layout.visual_line_count().saturating_sub(self.height)
421    }
422
423    /// Pull the view back within [`Self::max_top`] — after the pane grew or
424    /// the content shrank.
425    pub fn clamp(&mut self, layout: &Layout) {
426        self.top = self.top.min(self.max_top(layout));
427    }
428}
429
430// -- wrapping ----------------------------------------------------------------
431
432/// One cluster of a row, as wrapping sees it.
433struct Cluster {
434    chars: usize,
435    bytes: usize,
436    /// Byte length, so the cluster's text can be re-sliced to measure it.
437    len: usize,
438    /// A single whitespace scalar, and so a place a break may land. A cluster of
439    /// several scalars is never whitespace.
440    breakable: bool,
441}
442
443fn hint_for<'a>(hints: &'a [RowHints<'a>], row: usize) -> RowHints<'a> {
444    hints.get(row).copied().unwrap_or_default()
445}
446
447fn visible(hint: &RowHints<'_>, chars: usize) -> bool {
448    hint.visible.get(chars).copied().unwrap_or(true)
449}
450
451/// Wrap one logical row, appending at least one visual line.
452fn wrap_row(
453    text: &Text,
454    row: usize,
455    width: usize,
456    metrics: Metrics,
457    hint: RowHints<'_>,
458    scratch: &mut Vec<Cluster>,
459    out: &mut Vec<VisualLine>,
460) {
461    // The gutter eats into the width available for text. `.max(1)` keeps forward
462    // progress when the gutter is as wide as the pane; a genuinely zero-width pane
463    // is left at zero so it falls into the degenerate case below.
464    let width = if hint.inset == 0 {
465        width
466    } else {
467        width.saturating_sub(hint.inset).max(1)
468    };
469
470    let Some(source) = text.line(row) else {
471        return;
472    };
473
474    scratch.clear();
475    let mut chars = 0;
476    for (bytes, cluster) in source.grapheme_indices(true) {
477        let len = cluster.chars().count();
478        scratch.push(Cluster {
479            chars,
480            bytes,
481            len: cluster.len(),
482            breakable: len == 1 && cluster.chars().next().is_some_and(char::is_whitespace),
483        });
484        chars += len;
485    }
486    let total_chars = chars;
487    let total_bytes = source.len();
488
489    if scratch.is_empty() || width == 0 {
490        out.push(VisualLine {
491            logical_row: row,
492            chars: 0..0,
493            bytes: 0..0,
494            first: true,
495        });
496        return;
497    }
498
499    let cell_width = |index: usize, column: usize| -> usize {
500        let cluster = &scratch[index];
501        if visible(&hint, cluster.chars) {
502            let at = cluster.bytes;
503            metrics.width_at(&source[at..at + cluster.len], column)
504        } else {
505            0
506        }
507    };
508    let char_at =
509        |index: usize| -> usize { scratch.get(index).map(|c| c.chars).unwrap_or(total_chars) };
510    let byte_at =
511        |index: usize| -> usize { scratch.get(index).map(|c| c.bytes).unwrap_or(total_bytes) };
512
513    let total = scratch.len();
514    let mut start = 0;
515    let mut first = true;
516
517    while start < total {
518        // Where the row stops fitting. The column resets per visual line, so a tab
519        // on a continuation row measures from that row's own left edge — which is
520        // what the renderer draws.
521        let fit_end = {
522            let mut column = 0;
523            let mut index = start;
524            while index < total {
525                let cells = cell_width(index, column);
526                if column + cells > width {
527                    break;
528                }
529                column += cells;
530                index += 1;
531            }
532            // A single cluster wider than the pane must still advance, or the loop
533            // never ends.
534            if index == start { start + 1 } else { index }
535        };
536
537        if fit_end >= total {
538            out.push(VisualLine {
539                logical_row: row,
540                chars: char_at(start)..total_chars,
541                bytes: byte_at(start)..total_bytes,
542                first,
543            });
544            break;
545        }
546
547        // Prefer breaking at the last whitespace that fits; otherwise break mid
548        // word, always on a cluster boundary.
549        let (content_end, next_start) = if scratch[fit_end].breakable {
550            (fit_end, fit_end + 1)
551        } else {
552            match scratch[start..fit_end]
553                .iter()
554                .enumerate()
555                .rev()
556                .find(|(_, cluster)| cluster.breakable)
557            {
558                Some((offset, _)) => (start + offset, start + offset + 1),
559                None => (fit_end, fit_end),
560            }
561        };
562
563        out.push(VisualLine {
564            logical_row: row,
565            chars: char_at(start)..char_at(content_end),
566            bytes: byte_at(start)..byte_at(content_end),
567            first,
568        });
569        start = next_start;
570        first = false;
571    }
572}
573
574#[cfg(test)]
575mod tests {
576    use super::*;
577
578    fn text(s: &str) -> Text {
579        Text::from(s)
580    }
581
582    fn plain(text: &Text, width: usize) -> Layout {
583        Layout::compute(text, width, Metrics::default(), &[])
584    }
585
586    /// The drawn content of each visual line.
587    fn drawn(text: &Text, layout: &Layout) -> Vec<String> {
588        layout
589            .visual_lines()
590            .iter()
591            .map(|line| {
592                let row = text.line(line.logical_row).expect("row exists");
593                row[line.bytes.clone()].to_string()
594            })
595            .collect()
596    }
597
598    fn at(text: &Text, row: usize, col: usize) -> Position {
599        text.position(row, Column::new(col)).expect("addressable")
600    }
601
602    // -- wrapping -----------------------------------------------------------
603
604    #[test]
605    fn a_row_that_fits_is_one_visual_line() {
606        let t = text("short");
607        assert_eq!(drawn(&t, &plain(&t, 10)), ["short"]);
608    }
609
610    #[test]
611    fn wrapping_prefers_a_space() {
612        let t = text("aaaa bbbb");
613        assert_eq!(drawn(&t, &plain(&t, 6)), ["aaaa", "bbbb"]);
614    }
615
616    #[test]
617    fn a_word_longer_than_the_width_breaks_mid_word() {
618        let t = text("aaaaaaaa");
619        assert_eq!(drawn(&t, &plain(&t, 3)), ["aaa", "aaa", "aa"]);
620    }
621
622    #[test]
623    fn an_empty_row_is_still_a_visual_line() {
624        let t = text("a\n\nb");
625        let layout = plain(&t, 10);
626        assert_eq!(layout.visual_line_count(), 3);
627        assert_eq!(drawn(&t, &layout), ["a", "", "b"]);
628    }
629
630    #[test]
631    fn a_zero_width_pane_still_produces_one_line_per_row() {
632        let t = text("a\nb");
633        let layout = plain(&t, 0);
634        assert_eq!(layout.visual_line_count(), 2);
635    }
636
637    #[test]
638    fn a_cluster_wider_than_the_pane_still_advances() {
639        // A width-2 glyph in a width-1 pane cannot fit, and must not loop.
640        let t = text("\u{3042}\u{3042}");
641        let layout = plain(&t, 1);
642        assert_eq!(layout.visual_line_count(), 2);
643    }
644
645    #[test]
646    fn a_cluster_is_never_split_across_visual_lines() {
647        // A break landing inside a cluster would hand the renderer half a glyph,
648        // and the halves would reclusterl differently from the whole — so every
649        // column derived from either row would be wrong from that point on.
650        let family = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}";
651        let t = text(&format!("ab{family}cd"));
652        for width in 1..10 {
653            let layout = plain(&t, width);
654            let row = t.line(0).expect("one row");
655            for line in layout.visual_lines() {
656                assert!(
657                    row.is_char_boundary(line.bytes.start) && row.is_char_boundary(line.bytes.end),
658                    "width {width}: {:?} splits a character",
659                    line.bytes
660                );
661                let intact: Vec<usize> = row.grapheme_indices(true).map(|(at, _)| at).collect();
662                assert!(
663                    intact.contains(&line.bytes.start) || line.bytes.start == row.len(),
664                    "width {width}: {:?} starts inside a cluster",
665                    line.bytes
666                );
667                assert!(
668                    intact.contains(&line.bytes.end) || line.bytes.end == row.len(),
669                    "width {width}: {:?} ends inside a cluster",
670                    line.bytes
671                );
672            }
673        }
674    }
675
676    #[test]
677    fn a_gutter_eats_into_the_width() {
678        let t = text("aaaa bbbb");
679        let hints = [RowHints {
680            visible: &[],
681            inset: 2,
682        }];
683        let layout = Layout::compute(&t, 9, Metrics::default(), &hints);
684        assert_eq!(
685            drawn(&t, &layout),
686            ["aaaa", "bbbb"],
687            "nine cells less a two-cell gutter does not fit nine characters"
688        );
689        assert_eq!(
690            plain(&t, 9).visual_line_count(),
691            1,
692            "and without the gutter it does"
693        );
694    }
695
696    #[test]
697    fn a_gutter_as_wide_as_the_pane_still_makes_progress() {
698        let t = text("aaaa");
699        let hints = [RowHints {
700            visible: &[],
701            inset: 4,
702        }];
703        let layout = Layout::compute(&t, 4, Metrics::default(), &hints);
704        assert_eq!(
705            layout.visual_line_count(),
706            4,
707            "one cell per line, not a loop"
708        );
709    }
710
711    #[test]
712    fn undrawn_clusters_take_no_width() {
713        // "## " concealed, as a heading's sigils are: the row draws as "heading"
714        // and so fits a pane that its raw text would not.
715        let t = text("## heading");
716        let visible = vec![
717            false, false, false, true, true, true, true, true, true, true,
718        ];
719        let hints = [RowHints {
720            visible: &visible,
721            inset: 0,
722        }];
723        let layout = Layout::compute(&t, 7, Metrics::default(), &hints);
724        assert_eq!(layout.visual_line_count(), 1);
725        assert_eq!(
726            plain(&t, 7).visual_line_count(),
727            2,
728            "measuring the sigils would wrap it"
729        );
730    }
731
732    #[test]
733    fn a_tab_is_measured_to_its_stop_when_wrapping() {
734        // Four cells of tab plus four of text is eight; a seven-cell pane wraps.
735        let t = text("\tabcd");
736        assert_eq!(plain(&t, 8).visual_line_count(), 1);
737        assert_eq!(plain(&t, 7).visual_line_count(), 2);
738    }
739
740    #[test]
741    fn a_tab_is_itself_a_place_to_break() {
742        // A tab is whitespace, so when it does not fit it becomes the break rather
743        // than being pushed to the next line.
744        let t = text("ab cd\tef");
745        assert_eq!(drawn(&t, &plain(&t, 5)), ["ab cd", "ef"]);
746    }
747
748    #[test]
749    fn a_tab_measures_from_the_start_of_its_own_visual_line() {
750        // On the continuation line the tab sits at column 2, so it advances 2 cells
751        // to the next stop and "c" no longer fits. Measured from the logical row's
752        // column 7 it would advance only 1, and "c" would fit — so this is the
753        // assertion that pins which of the two models is in use.
754        let t = text("aaaa bb\tc");
755        assert_eq!(drawn(&t, &plain(&t, 4)), ["aaaa", "bb", "c"]);
756    }
757
758    // -- lookups ------------------------------------------------------------
759
760    #[test]
761    fn a_position_knows_which_visual_line_draws_it() {
762        let t = text("aaaa bbbb cccc");
763        let layout = plain(&t, 5);
764        assert_eq!(drawn(&t, &layout), ["aaaa", "bbbb", "cccc"]);
765        assert_eq!(layout.visual_row_of(at(&t, 0, 0)), 0);
766        assert_eq!(layout.visual_row_of(at(&t, 0, 5)), 1);
767        assert_eq!(layout.visual_row_of(at(&t, 0, 12)), 2);
768    }
769
770    #[test]
771    fn visual_rows_are_found_across_logical_rows() {
772        let t = text("aaaa bbbb\nsecond");
773        let layout = plain(&t, 5);
774        assert_eq!(drawn(&t, &layout), ["aaaa", "bbbb", "secon", "d"]);
775        assert_eq!(layout.visual_row_of(at(&t, 1, 0)), 2);
776        assert_eq!(layout.visual_row_of(at(&t, 1, 5)), 3);
777    }
778
779    #[test]
780    fn a_cell_accounts_for_the_gutter() {
781        let t = text("abc");
782        let hints = [RowHints {
783            visible: &[],
784            inset: 2,
785        }];
786        let layout = Layout::compute(&t, 10, Metrics::default(), &hints);
787        assert_eq!(
788            layout.cell_of(&t, &hints, at(&t, 0, 1)),
789            Cell { row: 0, column: 3 }
790        );
791    }
792
793    #[test]
794    fn a_cell_skips_hidden_clusters() {
795        let t = text("## heading");
796        let visible = vec![false, false, false];
797        let hints = [RowHints {
798            visible: &visible,
799            inset: 0,
800        }];
801        let layout = Layout::compute(&t, 40, Metrics::default(), &hints);
802        assert_eq!(
803            layout.cell_of(&t, &hints, at(&t, 0, 3)),
804            Cell { row: 0, column: 0 },
805            "the first drawn character is in the first cell"
806        );
807    }
808
809    #[test]
810    fn a_cell_counts_a_wide_cluster_as_two() {
811        let t = text("\u{3042}b");
812        let layout = plain(&t, 40);
813        assert_eq!(layout.cell_of(&t, &[], at(&t, 0, 1)).column, 2);
814    }
815
816    #[test]
817    fn a_click_inside_a_wide_cluster_means_that_cluster() {
818        let t = text("\u{3042}b");
819        let layout = plain(&t, 40);
820        for column in [0, 1] {
821            let landed = layout
822                .position_at_cell(&t, &[], Cell { row: 0, column })
823                .expect("inside the line");
824            assert_eq!(landed.column().get(), 0, "column {column}");
825        }
826        let landed = layout
827            .position_at_cell(&t, &[], Cell { row: 0, column: 2 })
828            .expect("inside the line");
829        assert_eq!(landed.column().get(), 1);
830    }
831
832    #[test]
833    fn a_click_past_the_end_of_a_visual_line_lands_at_its_end() {
834        let t = text("aaaa bbbb");
835        let layout = plain(&t, 5);
836        let landed = layout
837            .position_at_cell(&t, &[], Cell { row: 0, column: 99 })
838            .expect("inside the line");
839        assert_eq!(landed.column().get(), 4, "the end of the first visual line");
840    }
841
842    #[test]
843    fn a_click_in_the_gutter_lands_at_the_start_of_the_text() {
844        let t = text("abc");
845        let hints = [RowHints {
846            visible: &[],
847            inset: 3,
848        }];
849        let layout = Layout::compute(&t, 10, Metrics::default(), &hints);
850        let landed = layout
851            .position_at_cell(&t, &hints, Cell { row: 0, column: 1 })
852            .expect("inside the line");
853        assert_eq!(landed.column().get(), 0);
854    }
855
856    #[test]
857    fn a_click_below_the_text_finds_nothing() {
858        let t = text("abc");
859        let layout = plain(&t, 10);
860        assert!(
861            layout
862                .position_at_cell(&t, &[], Cell { row: 9, column: 0 })
863                .is_none()
864        );
865    }
866
867    #[test]
868    fn cells_and_positions_round_trip() {
869        let t = text("aaaa bbbb cccc");
870        let layout = plain(&t, 5);
871        for column in 0..14 {
872            let position = at(&t, 0, column);
873            let cell = layout.cell_of(&t, &[], position);
874            let back = layout
875                .position_at_cell(&t, &[], cell)
876                .expect("its own cell is inside the line");
877            assert_eq!(back, position, "column {column}");
878        }
879    }
880
881    // -- unwrapped ------------------------------------------------------------
882
883    #[test]
884    fn unwrapped_matches_row_count_and_describes_text() {
885        let t = text("short\na longer row that would wrap at a narrow width\nlast");
886        let layout = Layout::unwrapped(&t);
887        assert_eq!(layout.row_count(), t.line_count());
888        assert_eq!(layout.visual_line_count(), t.line_count());
889        assert!(
890            layout.describes(&t),
891            "unwrapped must describe the text it was built from"
892        );
893        assert_eq!(
894            drawn(&t, &layout),
895            [
896                "short",
897                "a longer row that would wrap at a narrow width",
898                "last"
899            ],
900            "one unwrapped visual line per row"
901        );
902    }
903
904    #[test]
905    fn unwrapped_handles_an_empty_text() {
906        let t = text("");
907        let layout = Layout::unwrapped(&t);
908        assert_eq!(layout.row_count(), t.line_count());
909        assert!(layout.describes(&t));
910    }
911
912    // -- relayout -----------------------------------------------------------
913
914    #[test]
915    fn relayout_rewraps_only_what_changed() {
916        let mut buffer = crate::ropetext::EditBuffer::new(text("aaaa bbbb\nkeep\ntail"));
917        let mut layout = plain(buffer.text(), 5);
918        assert_eq!(layout.visual_line_count(), 4);
919
920        let end = buffer.text().position(0, Column::new(9)).unwrap();
921        let mut txn = buffer.begin();
922        txn.delete(buffer_span(&txn, 0, 4, 0, 9));
923        let change = txn.commit().expect("changed");
924        let _ = end;
925
926        layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
927        assert_eq!(drawn(buffer.text(), &layout), ["aaaa", "keep", "tail"]);
928        assert_eq!(layout.row_count(), buffer.text().line_count());
929    }
930
931    #[test]
932    fn relayout_follows_added_rows() {
933        let mut buffer = crate::ropetext::EditBuffer::new(text("one\ntwo"));
934        let mut layout = plain(buffer.text(), 10);
935        let at_end = buffer.text().position(0, Column::new(3)).unwrap();
936        let mut txn = buffer.begin();
937        txn.insert(at_end, "\nmiddle");
938        let change = txn.commit().expect("changed");
939
940        layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
941        assert_eq!(drawn(buffer.text(), &layout), ["one", "middle", "two"]);
942        assert_eq!(layout.row_count(), 3);
943    }
944
945    #[test]
946    fn relayout_follows_removed_rows() {
947        let mut buffer = crate::ropetext::EditBuffer::new(text("one\ntwo\nthree\nfour"));
948        let mut layout = plain(buffer.text(), 10);
949        let mut txn = buffer.begin();
950        txn.delete(buffer_span(&txn, 0, 3, 2, 5));
951        let change = txn.commit().expect("changed");
952
953        layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
954        assert_eq!(drawn(buffer.text(), &layout), ["one", "four"]);
955        assert_eq!(layout.row_count(), 2);
956    }
957
958    #[test]
959    fn relayout_matches_a_full_recompute() {
960        // The cheap path and the honest path must agree, or an incremental
961        // relayout is a way to be quietly wrong for the rest of the session.
962        for (initial, row, col, inserted) in [
963            ("aaaa bbbb\nkeep", 0, 4, " cccc"),
964            ("one\ntwo\nthree", 1, 3, "\nsplit"),
965            ("one\ntwo", 0, 0, "prefix "),
966            ("wrapped line that is long\nnext", 0, 8, "\n"),
967        ] {
968            let mut buffer = crate::ropetext::EditBuffer::new(text(initial));
969            let mut layout = plain(buffer.text(), 6);
970            let position = buffer.text().position(row, Column::new(col)).unwrap();
971            let mut txn = buffer.begin();
972            txn.insert(position, inserted);
973            let change = txn.commit().expect("changed");
974
975            layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
976            let fresh = plain(buffer.text(), 6);
977            assert_eq!(
978                layout.visual_lines(),
979                fresh.visual_lines(),
980                "relayout disagreed for {initial:?} + {inserted:?}"
981            );
982        }
983    }
984
985    fn buffer_span(
986        txn: &crate::ropetext::Txn<'_>,
987        r1: usize,
988        c1: usize,
989        r2: usize,
990        c2: usize,
991    ) -> crate::ropetext::Span {
992        let text = txn.text();
993        let a = text.position(r1, Column::new(c1)).expect("addressable");
994        let b = text.position(r2, Column::new(c2)).expect("addressable");
995        text.span(a, b).expect("same text")
996    }
997
998    // -- viewport -----------------------------------------------------------
999
1000    #[test]
1001    fn a_viewport_shows_its_height_of_lines() {
1002        let t = text("a\nb\nc\nd\ne");
1003        let layout = plain(&t, 10);
1004        let view = Viewport::new(3);
1005        assert_eq!(view.rows(&layout), 0..3);
1006    }
1007
1008    #[test]
1009    fn a_viewport_clamps_to_what_there_is() {
1010        let t = text("a\nb");
1011        let layout = plain(&t, 10);
1012        let view = Viewport::new(10);
1013        assert_eq!(view.rows(&layout), 0..2);
1014    }
1015
1016    #[test]
1017    fn following_the_cursor_scrolls_the_least_it_can() {
1018        let t = text("a\nb\nc\nd\ne");
1019        let layout = plain(&t, 10);
1020        let mut view = Viewport::new(3);
1021        assert!(view.follow(&layout, at(&t, 4, 0)));
1022        assert_eq!(view.top(), 2, "just enough to show the last row");
1023        assert!(!view.follow(&layout, at(&t, 3, 0)), "already on screen");
1024        assert!(view.follow(&layout, at(&t, 0, 0)));
1025        assert_eq!(view.top(), 0);
1026    }
1027
1028    #[test]
1029    fn following_the_cursor_counts_visual_lines_not_rows() {
1030        let t = text("aaaa bbbb cccc\nlast");
1031        let layout = plain(&t, 5);
1032        assert_eq!(layout.visual_line_count(), 4);
1033        let mut view = Viewport::new(2);
1034        view.follow(&layout, at(&t, 0, 12));
1035        assert_eq!(view.top(), 1, "the third visual line of the first row");
1036    }
1037
1038    #[test]
1039    fn scrolling_does_not_run_past_the_end() {
1040        let t = text("a\nb\nc");
1041        let layout = plain(&t, 10);
1042        let mut view = Viewport::new(2);
1043        view.scroll_by(&layout, 99);
1044        assert_eq!(view.top(), 1, "the last line sits on the bottom row");
1045        assert!(!view.scroll_by(&layout, 1), "already at the bottom");
1046        view.scroll_by(&layout, -99);
1047        assert_eq!(view.top(), 0);
1048        assert!(!view.scroll_by(&layout, -1), "already at the top");
1049    }
1050
1051    #[test]
1052    fn a_taller_pane_pulls_the_content_back_down() {
1053        let t = text("a\nb\nc\nd\ne");
1054        let layout = plain(&t, 10);
1055        let mut view = Viewport::new(2);
1056        view.scroll_by(&layout, 99);
1057        assert_eq!(view.top(), 3);
1058        view.set_height(4);
1059        view.clamp(&layout);
1060        assert_eq!(view.top(), 1);
1061    }
1062
1063    #[test]
1064    fn a_viewport_of_no_height_follows_nothing() {
1065        let t = text("a\nb");
1066        let layout = plain(&t, 10);
1067        let mut view = Viewport::new(0);
1068        assert!(!view.follow(&layout, at(&t, 1, 0)));
1069    }
1070
1071    // -- properties ---------------------------------------------------------
1072
1073    mod properties {
1074        use super::*;
1075        use proptest::prelude::*;
1076
1077        proptest! {
1078            #![proptest_config(ProptestConfig::with_cases(200))]
1079
1080            /// The incremental relayout agrees with a full recompute, for any edit
1081            /// at any place and any width.
1082            ///
1083            /// This is the property the incremental path lives or dies by. A
1084            /// relayout that is merely *close* is a way to be quietly wrong for the
1085            /// rest of a session, and the failure shows up as text drawn from the
1086            /// wrong row rather than as anything that looks like a layout bug.
1087            #[test]
1088            fn relayout_agrees_with_a_full_recompute(
1089                initial in "[a-z \n]{0,40}",
1090                inserted in "[a-z \n]{0,8}",
1091                byte in 0usize..48,
1092                width in 1usize..8,
1093            ) {
1094                let mut buffer = crate::ropetext::EditBuffer::new(Text::from(initial.as_str()));
1095                let Some(position) = buffer.text().position_at_byte(byte.min(buffer.text().len_bytes()))
1096                else {
1097                    return Ok(());
1098                };
1099                let mut layout = Layout::compute(buffer.text(), width, Metrics::default(), &[]);
1100
1101                let mut txn = buffer.begin();
1102                txn.insert(position, &inserted);
1103                let Some(change) = txn.commit() else {
1104                    return Ok(());
1105                };
1106
1107                layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
1108                let fresh = Layout::compute(buffer.text(), width, Metrics::default(), &[]);
1109                prop_assert_eq!(
1110                    layout.visual_lines(),
1111                    fresh.visual_lines(),
1112                    "{:?} + {:?} at byte {} width {}", initial, inserted, byte, width
1113                );
1114                prop_assert_eq!(layout.row_count(), fresh.row_count());
1115            }
1116
1117            /// Same, for deletions.
1118            #[test]
1119            fn relayout_agrees_after_a_deletion(
1120                initial in "[a-z \n]{1,40}",
1121                from in 0usize..48,
1122                len in 0usize..12,
1123                width in 1usize..8,
1124            ) {
1125                let mut buffer = crate::ropetext::EditBuffer::new(Text::from(initial.as_str()));
1126                let end = buffer.text().len_bytes();
1127                let Some(start) = buffer.text().position_at_byte(from.min(end)) else {
1128                    return Ok(());
1129                };
1130                let Some(stop) = buffer.text().position_at_byte((from + len).min(end)) else {
1131                    return Ok(());
1132                };
1133                let span = buffer.text().span(start, stop).expect("same text");
1134                let mut layout = Layout::compute(buffer.text(), width, Metrics::default(), &[]);
1135
1136                let mut txn = buffer.begin();
1137                txn.delete(span);
1138                let Some(change) = txn.commit() else {
1139                    return Ok(());
1140                };
1141
1142                layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
1143                let fresh = Layout::compute(buffer.text(), width, Metrics::default(), &[]);
1144                prop_assert_eq!(
1145                    layout.visual_lines(),
1146                    fresh.visual_lines(),
1147                    "{:?} minus {}..{} at width {}", initial, from, from + len, width
1148                );
1149            }
1150
1151            /// Every visual line covers a real slice of the row it names, and the
1152            /// lines of one row cover the row in order without gaps.
1153            #[test]
1154            fn visual_lines_tile_their_rows(
1155                initial in ".{0,40}",
1156                width in 1usize..8,
1157            ) {
1158                let t = Text::from(initial.as_str());
1159                let layout = Layout::compute(&t, width, Metrics::default(), &[]);
1160                prop_assert_eq!(layout.row_count(), t.line_count());
1161                let mut seen_rows = 0;
1162                let mut expected_row = 0;
1163                let mut cursor = 0;
1164                for line in layout.visual_lines() {
1165                    if line.first {
1166                        prop_assert_eq!(line.logical_row, expected_row, "rows must be in order");
1167                        expected_row += 1;
1168                        seen_rows += 1;
1169                        cursor = 0;
1170                    }
1171                    let row = t.line(line.logical_row).expect("a named row exists");
1172                    prop_assert!(line.bytes.end <= row.len(), "slice past the row");
1173                    prop_assert!(line.chars.start >= cursor, "a line went backwards");
1174                    prop_assert!(row.is_char_boundary(line.bytes.start));
1175                    prop_assert!(row.is_char_boundary(line.bytes.end));
1176                    // Both ends land between grapheme clusters, so a visual line's
1177                    // slice reclusters exactly as the whole row does.
1178                    let breaks: Vec<usize> = row
1179                        .grapheme_indices(true)
1180                        .map(|(at, _)| at)
1181                        .chain(std::iter::once(row.len()))
1182                        .collect();
1183                    prop_assert!(breaks.contains(&line.bytes.start), "start splits a cluster");
1184                    prop_assert!(breaks.contains(&line.bytes.end), "end splits a cluster");
1185                    cursor = line.chars.end;
1186                }
1187                prop_assert_eq!(seen_rows, t.line_count(), "every row is drawn");
1188            }
1189        }
1190    }
1191}