Skip to main content

lattice_cells/
matrix.rs

1//! The matrix: an ordered sequence of chunks the renderer slices.
2//!
3//! `CellMatrix` is the contract between the cell-builder worker
4//! (S2 producer, in `lattice-host`) and the renderers (S3 TUI, S4
5//! GPU consumers). Published wait-free via ArcSwap inside
6//! `RenderState`; the paint loop reads it once per frame.
7//!
8//! See `docs/dev/architecture/cell-grid-renderer.md` § Paint loop
9//! for the per-frame contract.
10
11use std::sync::Arc;
12
13use crate::chunk::CellChunk;
14use crate::row::CellRow;
15use crate::version::MatrixVersion;
16use crate::virtual_rows::{AnchorPosition, VirtualRow, VirtualRowMatrix};
17
18/// Sentinel value of [`CellMatrix::chunk_size`] meaning whole-doc
19/// mode: the matrix has at most one chunk covering the entire
20/// document. Below `4 × viewport_height` lines the cell-builder
21/// uses whole-doc mode (see chunking policy in the design doc).
22pub const CHUNK_SIZE_WHOLE_DOC: u32 = 0;
23
24/// Soft-wrap (W.2, A2): number of display rows a row of `col_count`
25/// columns occupies when wrapped at `wrap_width` columns. Floored at
26/// `1` (an empty row still occupies one display row) and at `1` when
27/// `wrap_width == 0` (wrapping off). Shared by the host scroll model
28/// and both renderers so segment arithmetic is defined in exactly one
29/// place.
30pub fn wrap_segments(col_count: u32, wrap_width: u32) -> u32 {
31    if wrap_width == 0 {
32        return 1;
33    }
34    col_count.div_ceil(wrap_width).max(1)
35}
36
37/// The published cell matrix for a single buffer.
38///
39/// Immutable once built; cell-builder replaces the published Arc
40/// when versions change. Cheap to clone (Arc bump).
41#[derive(Clone, Debug)]
42pub struct CellMatrix {
43    /// Chunks ordered by `start_source_line`. Whole-doc mode has
44    /// exactly one chunk.
45    pub chunks: Arc<[Arc<CellChunk>]>,
46    /// Logical lines per chunk, or [`CHUNK_SIZE_WHOLE_DOC`] for
47    /// whole-doc mode.
48    pub chunk_size: u32,
49    /// Total logical lines in the source buffer (pre-fold).
50    pub source_line_count: u32,
51    /// Total matrix rows across all chunks (post-fold).
52    pub visible_line_count: u32,
53    /// Component-wise maximum of all chunks' captured versions.
54    pub version: MatrixVersion,
55    /// Soft-wrap (W.2, A2): the column width each source line is
56    /// wrapped at when `:set wrap` is on, or `0` when wrapping is
57    /// off (the historical default — every source line is exactly
58    /// one display row). The cells worker stamps this from the
59    /// pane's `viewport_width`; consumers derive display geometry
60    /// via [`Self::segment_count`] without the matrix storing
61    /// per-line segment data. One `CellRow` per source line is
62    /// preserved either way — see
63    /// `docs/dev/architecture/soft-wrap.md` (A2).
64    pub wrap_width: u32,
65}
66
67impl Default for CellMatrix {
68    /// Equivalent to [`Self::empty`]. Provided so containers like
69    /// `Arc<ArcSwap<CellMatrix>>` derive `Default` without
70    /// explicit-init plumbing at every call site.
71    fn default() -> Self {
72        Self::empty()
73    }
74}
75
76impl CellMatrix {
77    /// Empty matrix: no chunks, no rows. The initial published
78    /// value before the cell-builder finishes its first build.
79    pub fn empty() -> Self {
80        Self {
81            chunks: Arc::from([] as [Arc<CellChunk>; 0]),
82            chunk_size: CHUNK_SIZE_WHOLE_DOC,
83            source_line_count: 0,
84            visible_line_count: 0,
85            version: MatrixVersion::ZERO,
86            wrap_width: 0,
87        }
88    }
89
90    /// Construct a matrix in chunked mode. S2 is the production
91    /// caller; `chunk_size` must be > 0. Use [`Self::whole_doc`]
92    /// for the single-chunk mode.
93    ///
94    /// `version` is the aggregate stamp the renderer can inspect
95    /// to detect "is this matrix newer than the one I painted last
96    /// frame?" Comparison is `!=` (see `MatrixVersion::differs_from`)
97    /// because some axes are hash-style and don't admit ordering.
98    /// Production builds pass the publisher's current
99    /// `MatrixVersion` snapshot; defaults to all-zero for empty /
100    /// test cases.
101    pub fn chunked(
102        chunks: impl Into<Arc<[Arc<CellChunk>]>>,
103        chunk_size: u32,
104        source_line_count: u32,
105        version: MatrixVersion,
106    ) -> Self {
107        assert!(chunk_size > 0, "chunked mode requires chunk_size > 0");
108        let chunks: Arc<[Arc<CellChunk>]> = chunks.into();
109        let visible_line_count = chunks.iter().map(|c| c.row_count()).sum::<u32>();
110        Self {
111            chunks,
112            chunk_size,
113            source_line_count,
114            visible_line_count,
115            version,
116            wrap_width: 0,
117        }
118    }
119
120    /// Construct a matrix in whole-doc mode (one chunk).
121    pub fn whole_doc(chunk: Arc<CellChunk>, source_line_count: u32) -> Self {
122        let visible_line_count = chunk.row_count();
123        let version = chunk.version;
124        Self {
125            chunks: Arc::from(vec![chunk]),
126            chunk_size: CHUNK_SIZE_WHOLE_DOC,
127            source_line_count,
128            visible_line_count,
129            version,
130            wrap_width: 0,
131        }
132    }
133
134    /// `true` when running in whole-doc mode (one chunk covering
135    /// the entire document).
136    pub fn is_whole_doc(&self) -> bool {
137        self.chunk_size == CHUNK_SIZE_WHOLE_DOC
138    }
139
140    /// `true` when no chunks/rows are present.
141    pub fn is_empty(&self) -> bool {
142        self.visible_line_count == 0
143    }
144
145    /// Soft-wrap (W.2, A2): how many *display* rows the source line
146    /// `target` occupies. `1` when wrapping is off (`wrap_width ==
147    /// 0`) or the line is missing/folded; otherwise
148    /// `⌈col_count / wrap_width⌉`, floored at `1` (an empty line
149    /// still occupies one display row).
150    ///
151    /// This is the published geometry the host scroll model
152    /// (`bottom_anchored_scroll`) and both renderers read to expand
153    /// a source line into wrap segments — no per-line segment data
154    /// is stored on the matrix.
155    pub fn segment_count(&self, target: u32) -> u32 {
156        if self.wrap_width == 0 {
157            return 1;
158        }
159        match self.row_at_source_line(target) {
160            Some(row) => wrap_segments(row.col_count(), self.wrap_width),
161            None => 1,
162        }
163    }
164
165    /// S3.c.0 (2026-05-26): look up the row whose logical source
166    /// line equals `target`. Returns `None` when the target line
167    /// is folded (no visible row) or past the matrix's coverage.
168    ///
169    /// Walks chunks linearly; chunks are sorted by
170    /// `start_source_line`. Most callers ask for visible lines
171    /// (≤ viewport_height per frame); a typical 100K-line buffer
172    /// at `chunk_size = 128` has ~780 chunks, so the walk is
173    /// sub-µs even without an outer binary search.
174    pub fn row_at_source_line(&self, target: u32) -> Option<&CellRow> {
175        for chunk in self.chunks.iter() {
176            let start = chunk.start_source_line;
177            // Whole-doc mode (`chunk_size == 0`) — the single
178            // chunk covers the entire source. Chunked mode — the
179            // chunk covers `[start, start + chunk_size)`.
180            let end = if self.chunk_size == CHUNK_SIZE_WHOLE_DOC {
181                self.source_line_count
182            } else {
183                start.saturating_add(self.chunk_size)
184            };
185            if target < start {
186                // chunks are ordered; the rest are even further
187                // away.
188                return None;
189            }
190            if target < end {
191                return chunk.row_at_source_line(target);
192            }
193        }
194        None
195    }
196
197    /// H.3 (2026-06-04): first source line this matrix's chunks were
198    /// built to cover. `0` for whole-doc mode and full-coverage
199    /// chunked mode; the window's lower bound for a windowed
200    /// large-file matrix. Derived from the first chunk's
201    /// `start_source_line` (exact — chunks are ordered).
202    pub fn covered_start_line(&self) -> u32 {
203        self.chunks
204            .first()
205            .map(|c| c.start_source_line)
206            .unwrap_or(0)
207    }
208
209    /// H.3: exclusive upper bound of the source-line range this
210    /// matrix's chunks were built to cover. `source_line_count` in
211    /// whole-doc mode; otherwise the last chunk's
212    /// `start_source_line + chunk_size`, clamped to
213    /// `source_line_count`. Robust against fold elision (a fully
214    /// folded tail chunk still reports the source span it was built
215    /// over) and against a window that ends mid-`chunk_size`
216    /// (`build_matrix` aligns the window up to `chunk_size`, so the
217    /// last chunk's nominal end equals the window's upper bound).
218    pub fn covered_end_line(&self) -> u32 {
219        if self.is_whole_doc() {
220            return self.source_line_count;
221        }
222        self.chunks
223            .last()
224            .map(|c| {
225                c.start_source_line
226                    .saturating_add(self.chunk_size)
227                    .min(self.source_line_count)
228            })
229            .unwrap_or(0)
230    }
231
232    /// H.3: does this matrix cover the entire source-line range
233    /// `[lo, hi)`? The cells worker's cache-hit gate uses this to
234    /// keep a windowed large-file matrix from serving a viewport
235    /// that has scrolled past its covered range — when it returns
236    /// `false`, the worker rebuilds the window around the new
237    /// scroll. An empty matrix (no chunks) covers nothing.
238    pub fn covers(&self, lo: u32, hi: u32) -> bool {
239        if self.chunks.is_empty() {
240            return false;
241        }
242        self.covered_start_line() <= lo && hi <= self.covered_end_line()
243    }
244
245    /// Borrow the visible rows starting at matrix-row index
246    /// `scroll`, up to `height` rows. Returns a [`CellSlice`] that
247    /// iterates `&CellRow` references without allocating.
248    ///
249    /// If `scroll + height` exceeds `visible_line_count`, the
250    /// slice is naturally truncated (no panic, no padding). The
251    /// renderer must paint blank rows below the slice's end when
252    /// the viewport extends past EOF.
253    pub fn slice(&self, scroll: u32, height: u32) -> CellSlice<'_> {
254        let start = scroll.min(self.visible_line_count);
255        let end = scroll.saturating_add(height).min(self.visible_line_count);
256        CellSlice {
257            chunks: &self.chunks,
258            start,
259            end,
260        }
261    }
262
263    /// D.0a: borrow `height` display rows starting at display
264    /// row `scroll`, **interleaving** virtual rows from
265    /// `virtual_rows` with the document rows in this matrix.
266    ///
267    /// `scroll` and `height` are in *display-row* space —
268    /// they count both document rows and virtual rows.
269    /// Returns a [`DisplaySlice`] that iterates
270    /// [`DisplayRowEntry::Document`] for document rows and
271    /// [`DisplayRowEntry::Virtual`] for virtual rows in
272    /// natural top-to-bottom order.
273    ///
274    /// When `virtual_rows.is_empty()` the iterator degenerates
275    /// to the same yield order as [`Self::slice`]; renderers
276    /// can call `display_slice` unconditionally without
277    /// paying for the interleaver when no provider has
278    /// registered virtual rows.
279    ///
280    /// See `docs/dev/architecture/virtual-rows.md` for the
281    /// full ordering contract (Above-before-Cell-before-Below
282    /// at each anchor line, folded-line anchors emit at the
283    /// next visible line, past-EOF anchors emit at the end).
284    pub fn display_slice<'a>(
285        &'a self,
286        scroll: u32,
287        height: u32,
288        virtual_rows: &'a VirtualRowMatrix,
289    ) -> DisplaySlice<'a> {
290        DisplaySlice {
291            chunks: &self.chunks,
292            cell_total: self.visible_line_count,
293            virtual_rows,
294            scroll,
295            height,
296        }
297    }
298}
299
300/// Borrowed iterator over a slice of matrix rows. Created via
301/// [`CellMatrix::slice`].
302///
303/// The slice does not pre-resolve a flat `Vec<&CellRow>`; it walks
304/// chunks lazily during iteration so the renderer pays only for
305/// visible rows. Sub-microsecond for any realistic viewport.
306#[derive(Debug)]
307pub struct CellSlice<'a> {
308    chunks: &'a [Arc<CellChunk>],
309    /// Start index in *matrix-row* space (post-fold), inclusive.
310    start: u32,
311    /// End index in *matrix-row* space (post-fold), exclusive.
312    end: u32,
313}
314
315impl<'a> CellSlice<'a> {
316    /// Number of rows the slice will yield.
317    pub fn len(&self) -> u32 {
318        self.end.saturating_sub(self.start)
319    }
320
321    pub fn is_empty(&self) -> bool {
322        self.start >= self.end
323    }
324
325    /// Iterate matrix rows in order.
326    pub fn iter(&self) -> CellSliceIter<'a> {
327        CellSliceIter::new(self.chunks, self.start, self.end)
328    }
329}
330
331/// Iterator yielded by [`CellSlice::iter`]. Walks chunks from the
332/// one containing `start` and yields `&CellRow` refs through to
333/// `end`.
334pub struct CellSliceIter<'a> {
335    chunks: &'a [Arc<CellChunk>],
336    chunk_idx: usize,
337    /// Index into `chunks[chunk_idx].rows` for the next yield.
338    row_idx_in_chunk: u32,
339    /// Matrix-row index of the next yield (pre-increment).
340    next_matrix_row: u32,
341    /// Stop-at matrix-row index (exclusive).
342    end: u32,
343}
344
345impl<'a> CellSliceIter<'a> {
346    fn new(chunks: &'a [Arc<CellChunk>], start: u32, end: u32) -> Self {
347        // Find the chunk + in-chunk offset corresponding to `start`.
348        let mut acc: u32 = 0;
349        let mut chunk_idx = chunks.len();
350        let mut row_idx_in_chunk = 0u32;
351        for (i, c) in chunks.iter().enumerate() {
352            let next = acc.saturating_add(c.row_count());
353            if start < next {
354                chunk_idx = i;
355                row_idx_in_chunk = start - acc;
356                break;
357            }
358            acc = next;
359        }
360        Self {
361            chunks,
362            chunk_idx,
363            row_idx_in_chunk,
364            next_matrix_row: start,
365            end,
366        }
367    }
368}
369
370impl<'a> Iterator for CellSliceIter<'a> {
371    type Item = &'a CellRow;
372
373    fn next(&mut self) -> Option<Self::Item> {
374        if self.next_matrix_row >= self.end {
375            return None;
376        }
377        while self.chunk_idx < self.chunks.len() {
378            let chunk = &self.chunks[self.chunk_idx];
379            if (self.row_idx_in_chunk as usize) < chunk.rows.len() {
380                let row = &chunk.rows[self.row_idx_in_chunk as usize];
381                self.row_idx_in_chunk += 1;
382                self.next_matrix_row += 1;
383                return Some(row);
384            }
385            // Walk to next non-empty chunk.
386            self.chunk_idx += 1;
387            self.row_idx_in_chunk = 0;
388        }
389        None
390    }
391}
392
393// ============================================================
394// D.0a: display rows — interleaver over (CellMatrix,
395// VirtualRowMatrix). See
396// `docs/dev/architecture/virtual-rows.md`.
397// ============================================================
398
399/// One row yielded by [`DisplaySliceIter`].
400///
401/// `Document` rows reference a [`CellRow`] from the underlying
402/// [`CellMatrix`]; `Virtual` rows reference a [`VirtualRow`]
403/// from the sibling [`VirtualRowMatrix`]. Renderers paint both
404/// the same way (both carry `Arc<[Cell]>`), differing only in
405/// the cursor / motion treatment (vim's `j` / `k` step
406/// document rows only — virtual rows are visual-only).
407#[derive(Debug, Clone, Copy)]
408pub enum DisplayRowEntry<'a> {
409    Document(&'a CellRow),
410    Virtual(&'a VirtualRow),
411}
412
413/// Borrowed slice over the interleaved (document, virtual)
414/// display rows. Created via [`CellMatrix::display_slice`].
415///
416/// Holds the parameters; the actual interleaving work happens
417/// in [`Self::iter`] / [`DisplaySliceIter`]. Cheap to
418/// construct.
419#[derive(Debug)]
420pub struct DisplaySlice<'a> {
421    chunks: &'a [Arc<CellChunk>],
422    cell_total: u32,
423    virtual_rows: &'a VirtualRowMatrix,
424    scroll: u32,
425    height: u32,
426}
427
428impl<'a> DisplaySlice<'a> {
429    /// Iterate `height` display rows starting at `scroll`.
430    ///
431    /// When `virtual_rows.is_empty()` the iterator walks the
432    /// underlying `CellSliceIter` directly without
433    /// interleaver overhead.
434    pub fn iter(&self) -> DisplaySliceIter<'a> {
435        let cells = if self.virtual_rows.is_empty() {
436            // Fast path: no virtual rows, scroll counts cell
437            // rows 1:1. Reuse CellSliceIter's chunk-walk
438            // logic for the start position.
439            let start = self.scroll.min(self.cell_total);
440            CellSliceIter::new(self.chunks, start, self.cell_total)
441        } else {
442            CellSliceIter::new(self.chunks, 0, self.cell_total)
443        };
444
445        let mut it = DisplaySliceIter {
446            cells,
447            cell_peek: None,
448            virtual_rows: &self.virtual_rows.rows,
449            v_idx: 0,
450            after_cell_below_for: None,
451            remaining: u32::MAX,
452        };
453
454        // Skip `scroll` display rows when virtual rows are
455        // present. Naive O(scroll); for v1 viewport sizes
456        // (sub-frame budget at scroll < ~10k display rows)
457        // this is well inside the per-frame budget. Optimised
458        // skip via line_index lookup can replace this if a
459        // bench surfaces it.
460        if !self.virtual_rows.is_empty() {
461            for _ in 0..self.scroll {
462                if it.next().is_none() {
463                    break;
464                }
465            }
466        }
467        it.remaining = self.height;
468        it
469    }
470}
471
472/// Iterator yielded by [`DisplaySlice::iter`]. Walks the
473/// underlying [`CellSliceIter`] in tandem with the virtual
474/// rows in `virtual_rows`, emitting them in the order:
475///
476/// 1. Virtual rows whose anchor is strictly less than the
477///    next document row's source line.
478/// 2. Virtual rows whose anchor equals the next document
479///    row's source line and `position == Above`.
480/// 3. The document row.
481/// 4. Virtual rows whose anchor equals that document row's
482///    source line and `position == Below`.
483/// 5. Repeat from (1) with the next document row.
484/// 6. After the last document row, any remaining virtual
485///    rows are emitted in their sorted order (covers both
486///    past-EOF anchors and anchors on folded-out trailing
487///    lines).
488pub struct DisplaySliceIter<'a> {
489    cells: CellSliceIter<'a>,
490    cell_peek: Option<&'a CellRow>,
491    virtual_rows: &'a [VirtualRow],
492    v_idx: usize,
493    /// When `Some(line)`, the iterator just emitted the
494    /// document row for `line` and is now draining its
495    /// `Below(line)` virtual rows before peeking the next
496    /// document row.
497    after_cell_below_for: Option<u32>,
498    remaining: u32,
499}
500
501impl<'a> Iterator for DisplaySliceIter<'a> {
502    type Item = DisplayRowEntry<'a>;
503
504    fn next(&mut self) -> Option<Self::Item> {
505        if self.remaining == 0 {
506            return None;
507        }
508
509        // Each `next()` emits exactly one entry on a single straight-line
510        // pass: phase A returns a queued `Below` row, else phase B/C/D
511        // peeks the next document row and returns either the document row
512        // or a virtual row anchored at/before it. No path loops back, so
513        // this is straight-line, not a `loop` (clippy::never_loop).
514
515        // Phase A: drain `Below(line)` virtual rows for the
516        // most-recently emitted document row.
517        if let Some(line) = self.after_cell_below_for {
518            if let Some(vrow) = self.virtual_rows.get(self.v_idx)
519                && vrow.anchor_line == line
520                && vrow.position == AnchorPosition::Below
521            {
522                self.v_idx += 1;
523                self.remaining -= 1;
524                return Some(DisplayRowEntry::Virtual(vrow));
525            }
526            // No more Below(line) entries — exit phase A.
527            self.after_cell_below_for = None;
528        }
529
530        // Phase B: peek next document row if we haven't.
531        if self.cell_peek.is_none() {
532            self.cell_peek = self.cells.next();
533        }
534
535        match self.cell_peek {
536            Some(crow) => {
537                let line = crow.source_line;
538                // Phase C: emit any virtual row whose anchor sits at or
539                // before `line` with `Above`-or-earlier semantics.
540                if let Some(vrow) = self.virtual_rows.get(self.v_idx) {
541                    let v_anchor = vrow.anchor_line;
542                    let emits_before_cell = v_anchor < line
543                        || (v_anchor == line && vrow.position == AnchorPosition::Above);
544                    if emits_before_cell {
545                        self.v_idx += 1;
546                        self.remaining -= 1;
547                        return Some(DisplayRowEntry::Virtual(vrow));
548                    }
549                }
550                // Phase D: emit the document row; queue `Below(line)` for
551                // phase A on the next call.
552                self.cell_peek = None;
553                self.after_cell_below_for = Some(line);
554                self.remaining -= 1;
555                Some(DisplayRowEntry::Document(crow))
556            }
557            None => {
558                // No more document rows; emit any remaining virtual rows
559                // in sorted order.
560                if let Some(vrow) = self.virtual_rows.get(self.v_idx) {
561                    self.v_idx += 1;
562                    self.remaining -= 1;
563                    return Some(DisplayRowEntry::Virtual(vrow));
564                }
565                None
566            }
567        }
568    }
569}
570
571#[cfg(test)]
572mod tests {
573    use super::*;
574    use crate::cell::Cell;
575
576    fn row(source_line: u32, ch: u8) -> CellRow {
577        CellRow::new(
578            vec![Cell::with_codepoint(ch as u32)],
579            source_line,
580            Vec::<crate::row::InlayOffset>::new(),
581        )
582    }
583
584    fn chunk(start: u32, rows: Vec<CellRow>) -> Arc<CellChunk> {
585        Arc::new(CellChunk::new(start, rows, MatrixVersion::ZERO))
586    }
587
588    #[test]
589    fn wrap_segments_arithmetic() {
590        // Wrap off ⇒ always one display row.
591        assert_eq!(wrap_segments(0, 0), 1);
592        assert_eq!(wrap_segments(200, 0), 1);
593        // Empty / short rows ⇒ one row.
594        assert_eq!(wrap_segments(0, 80), 1);
595        assert_eq!(wrap_segments(1, 80), 1);
596        assert_eq!(wrap_segments(80, 80), 1);
597        // Exact multiples + remainders.
598        assert_eq!(wrap_segments(81, 80), 2);
599        assert_eq!(wrap_segments(160, 80), 2);
600        assert_eq!(wrap_segments(161, 80), 3);
601    }
602
603    #[test]
604    fn segment_count_reads_wrap_width() {
605        let c = chunk(0, vec![row(0, b'a'), row(1, b'b')]);
606        let mut m = CellMatrix::whole_doc(c, 2);
607        // Wrap off ⇒ 1 per line regardless of content.
608        assert_eq!(m.segment_count(0), 1);
609        // Turn wrap on at width 1: each 1-cell row is exactly one
610        // segment; a missing line is neutral (1).
611        m.wrap_width = 1;
612        assert_eq!(m.segment_count(0), 1);
613        assert_eq!(m.segment_count(99), 1);
614    }
615
616    #[test]
617    fn empty_matrix_reports_empty() {
618        let m = CellMatrix::empty();
619        assert!(m.is_empty());
620        assert!(m.is_whole_doc());
621        assert_eq!(m.visible_line_count, 0);
622        let s = m.slice(0, 10);
623        assert!(s.is_empty());
624        assert_eq!(s.iter().count(), 0);
625    }
626
627    #[test]
628    fn whole_doc_holds_single_chunk() {
629        let c = chunk(0, vec![row(0, b'a'), row(1, b'b'), row(2, b'c')]);
630        let m = CellMatrix::whole_doc(c, 3);
631        assert!(m.is_whole_doc());
632        assert_eq!(m.visible_line_count, 3);
633        assert_eq!(m.source_line_count, 3);
634        let s = m.slice(0, 10);
635        let chars: Vec<u32> = s
636            .iter()
637            .map(|r| r.cells.first().map(|c| c.codepoint).unwrap_or(0))
638            .collect();
639        assert_eq!(chars, vec![b'a' as u32, b'b' as u32, b'c' as u32]);
640    }
641
642    #[test]
643    fn slice_truncates_past_eof() {
644        let c = chunk(0, vec![row(0, b'a'), row(1, b'b')]);
645        let m = CellMatrix::whole_doc(c, 2);
646        let s = m.slice(0, 5);
647        assert_eq!(s.len(), 2);
648        assert_eq!(s.iter().count(), 2);
649    }
650
651    #[test]
652    fn slice_with_scroll_past_eof_is_empty() {
653        let c = chunk(0, vec![row(0, b'a')]);
654        let m = CellMatrix::whole_doc(c, 1);
655        let s = m.slice(10, 5);
656        assert!(s.is_empty());
657        assert_eq!(s.iter().count(), 0);
658    }
659
660    #[test]
661    fn chunked_stores_passed_version() {
662        let v = MatrixVersion {
663            text: 3,
664            syntax: 7,
665            inlay_hints: 1,
666            folds: 0,
667            theme: 0,
668            whitespace: 0,
669            indent: 0,
670            conceal: 0,
671        };
672        let c1 = Arc::new(CellChunk::new(0, vec![row(0, b'a')], v));
673        let c2 = Arc::new(CellChunk::new(1, vec![row(1, b'b')], v));
674        let m = CellMatrix::chunked(vec![c1, c2], 1, 2, v);
675        assert_eq!(m.version, v);
676        assert_eq!(m.visible_line_count, 2);
677    }
678
679    #[test]
680    fn slice_walks_across_chunks() {
681        // Three chunks of one row each.
682        let c1 = chunk(0, vec![row(0, b'a')]);
683        let c2 = chunk(1, vec![row(1, b'b')]);
684        let c3 = chunk(2, vec![row(2, b'c')]);
685        let m = CellMatrix::chunked(vec![c1, c2, c3], 1, 3, MatrixVersion::ZERO);
686        let s = m.slice(0, 3);
687        let chars: Vec<u32> = s.iter().map(|r| r.cells[0].codepoint).collect();
688        assert_eq!(chars, vec![b'a' as u32, b'b' as u32, b'c' as u32]);
689    }
690
691    #[test]
692    fn slice_starting_mid_chunk() {
693        let c1 = chunk(0, vec![row(0, b'a'), row(1, b'b'), row(2, b'c')]);
694        let c2 = chunk(3, vec![row(3, b'd'), row(4, b'e')]);
695        let m = CellMatrix::chunked(vec![c1, c2], 3, 5, MatrixVersion::ZERO);
696        // Scroll past first two rows of chunk1; take 3 rows.
697        let s = m.slice(2, 3);
698        let chars: Vec<u32> = s.iter().map(|r| r.cells[0].codepoint).collect();
699        assert_eq!(chars, vec![b'c' as u32, b'd' as u32, b'e' as u32]);
700    }
701
702    #[test]
703    fn slice_handles_folded_rows_via_chunk_row_count() {
704        // Chunk covers source lines 0..3 but only lines 0, 2 are
705        // visible (line 1 is folded).
706        let c = chunk(0, vec![row(0, b'a'), row(2, b'c')]);
707        let m = CellMatrix::chunked(vec![c], 3, 3, MatrixVersion::ZERO);
708        assert_eq!(m.visible_line_count, 2); // post-fold count
709        let s = m.slice(0, 5);
710        let source_lines: Vec<u32> = s.iter().map(|r| r.source_line).collect();
711        assert_eq!(source_lines, vec![0, 2]);
712    }
713
714    #[test]
715    fn slice_len_matches_iter_count() {
716        let c1 = chunk(0, vec![row(0, b'a'), row(1, b'b')]);
717        let c2 = chunk(2, vec![row(2, b'c'), row(3, b'd'), row(4, b'e')]);
718        let m = CellMatrix::chunked(vec![c1, c2], 2, 5, MatrixVersion::ZERO);
719        for (scroll, height) in [(0, 5), (1, 3), (3, 10), (2, 2), (5, 5)] {
720            let s = m.slice(scroll, height);
721            assert_eq!(
722                s.len() as usize,
723                s.iter().count(),
724                "scroll={scroll} height={height}"
725            );
726        }
727    }
728
729    #[test]
730    fn slice_iter_walks_past_empty_chunks() {
731        // Middle chunk is empty (e.g. covers a fully-folded range).
732        let c1 = chunk(0, vec![row(0, b'a')]);
733        let c2 = Arc::new(CellChunk::empty(1, MatrixVersion::ZERO));
734        let c3 = chunk(2, vec![row(2, b'c')]);
735        let m = CellMatrix::chunked(vec![c1, c2, c3], 1, 3, MatrixVersion::ZERO);
736        let s = m.slice(0, 5);
737        let chars: Vec<u32> = s.iter().map(|r| r.cells[0].codepoint).collect();
738        assert_eq!(chars, vec![b'a' as u32, b'c' as u32]);
739    }
740
741    #[test]
742    fn chunk_size_whole_doc_constant() {
743        assert_eq!(CHUNK_SIZE_WHOLE_DOC, 0);
744    }
745
746    // ---- S3.c.0 — row_at_source_line ----
747
748    /// Whole-doc matrix: every source line in `[0, count)` looks
749    /// up to a row. Targets past `count` return `None`. Folded
750    /// lines (absent from the chunk's rows vec) return `None`.
751    #[test]
752    fn row_at_source_line_whole_doc() {
753        // Lines 0, 1, 3 visible; line 2 folded.
754        let c = chunk(0, vec![row(0, b'a'), row(1, b'b'), row(3, b'd')]);
755        let m = CellMatrix::whole_doc(c, 4);
756        assert!(m.is_whole_doc());
757
758        assert_eq!(m.row_at_source_line(0).unwrap().source_line, 0);
759        assert_eq!(m.row_at_source_line(1).unwrap().source_line, 1);
760        // Folded — present in the source range but absent from
761        // the chunk's row vector.
762        assert!(m.row_at_source_line(2).is_none());
763        assert_eq!(m.row_at_source_line(3).unwrap().source_line, 3);
764        // Past EOF.
765        assert!(m.row_at_source_line(4).is_none());
766        assert!(m.row_at_source_line(100).is_none());
767    }
768
769    /// Chunked matrix: lookup walks chunks to the right one and
770    /// then binary-searches inside. Spans across chunk boundaries.
771    #[test]
772    fn row_at_source_line_chunked_walks_chunks() {
773        // Three chunks of width 2 covering lines 0..6.
774        let c1 = chunk(0, vec![row(0, b'a'), row(1, b'b')]);
775        let c2 = chunk(2, vec![row(2, b'c'), row(3, b'd')]);
776        let c3 = chunk(4, vec![row(4, b'e'), row(5, b'f')]);
777        let m = CellMatrix::chunked(vec![c1, c2, c3], 2, 6, MatrixVersion::ZERO);
778
779        for line in 0u32..6 {
780            let r = m
781                .row_at_source_line(line)
782                .unwrap_or_else(|| panic!("expected row for line {line}"));
783            assert_eq!(r.source_line, line);
784        }
785        // Past EOF.
786        assert!(m.row_at_source_line(6).is_none());
787    }
788
789    /// Empty matrix never resolves any target.
790    #[test]
791    fn row_at_source_line_empty_matrix_returns_none() {
792        let m = CellMatrix::empty();
793        assert!(m.row_at_source_line(0).is_none());
794        assert!(m.row_at_source_line(42).is_none());
795    }
796
797    // ============================================================
798    // D.0a: display_slice / interleaver tests
799    // ============================================================
800
801    use crate::virtual_rows::{AnchorPosition, VirtualRow, VirtualRowMatrix, VirtualRowVersion};
802
803    fn vrow(anchor: u32, position: AnchorPosition) -> VirtualRow {
804        VirtualRow {
805            media: None,
806            anchor_line: anchor,
807            position,
808            cells: Arc::from([] as [Cell; 0]),
809            height: 1,
810            kind: crate::VirtualRowKind::Generic,
811            bg: None,
812            scales: None,
813            gutter_line: None,
814            gutter_fg: None,
815        }
816    }
817
818    /// Collect a DisplaySlice as a Vec<(kind, anchor_or_line)>
819    /// for ergonomic test assertions. `kind` is 'D' for
820    /// Document or 'V' for Virtual.
821    fn collect(slice: &DisplaySlice<'_>) -> Vec<(char, u32)> {
822        slice
823            .iter()
824            .map(|e| match e {
825                DisplayRowEntry::Document(r) => ('D', r.source_line),
826                DisplayRowEntry::Virtual(r) => ('V', r.anchor_line),
827            })
828            .collect()
829    }
830
831    #[test]
832    fn display_slice_empty_virtual_matches_slice() {
833        let c = chunk(0, vec![row(0, b'a'), row(1, b'b'), row(2, b'c')]);
834        let m = CellMatrix::whole_doc(c, 3);
835        let v = VirtualRowMatrix::empty();
836        let ds = m.display_slice(0, 10, &v);
837        assert_eq!(collect(&ds), vec![('D', 0), ('D', 1), ('D', 2)]);
838    }
839
840    #[test]
841    fn display_slice_above_emits_before_document_row() {
842        let c = chunk(0, vec![row(0, b'a'), row(1, b'b')]);
843        let m = CellMatrix::whole_doc(c, 2);
844        let v = VirtualRowMatrix::build(
845            vec![vrow(1, AnchorPosition::Above)],
846            2,
847            VirtualRowVersion(1),
848        );
849        let ds = m.display_slice(0, 10, &v);
850        assert_eq!(collect(&ds), vec![('D', 0), ('V', 1), ('D', 1)]);
851    }
852
853    #[test]
854    fn display_slice_below_emits_after_document_row() {
855        let c = chunk(0, vec![row(0, b'a'), row(1, b'b')]);
856        let m = CellMatrix::whole_doc(c, 2);
857        let v = VirtualRowMatrix::build(
858            vec![vrow(0, AnchorPosition::Below)],
859            2,
860            VirtualRowVersion(1),
861        );
862        let ds = m.display_slice(0, 10, &v);
863        assert_eq!(collect(&ds), vec![('D', 0), ('V', 0), ('D', 1)]);
864    }
865
866    #[test]
867    fn display_slice_multiple_at_same_anchor_sorted_above_then_below() {
868        let c = chunk(0, vec![row(0, b'a'), row(1, b'b')]);
869        let m = CellMatrix::whole_doc(c, 2);
870        let v = VirtualRowMatrix::build(
871            vec![
872                vrow(1, AnchorPosition::Below),
873                vrow(1, AnchorPosition::Above),
874                vrow(1, AnchorPosition::Above),
875                vrow(1, AnchorPosition::Below),
876            ],
877            2,
878            VirtualRowVersion(1),
879        );
880        let ds = m.display_slice(0, 20, &v);
881        // Expected ordering at anchor=1: two Above, then doc
882        // row 1, then two Below.
883        assert_eq!(
884            collect(&ds),
885            vec![
886                ('D', 0),
887                ('V', 1), // Above
888                ('V', 1), // Above
889                ('D', 1),
890                ('V', 1), // Below
891                ('V', 1), // Below
892            ]
893        );
894    }
895
896    #[test]
897    fn display_slice_anchor_past_eof_emits_at_end() {
898        let c = chunk(0, vec![row(0, b'a'), row(1, b'b')]);
899        let m = CellMatrix::whole_doc(c, 2);
900        // VirtualRowMatrix::build clamps past-EOF anchors to
901        // source_line_count, which sorts after the last
902        // document row.
903        let v = VirtualRowMatrix::build(
904            vec![vrow(99, AnchorPosition::Above)],
905            2,
906            VirtualRowVersion(1),
907        );
908        let ds = m.display_slice(0, 20, &v);
909        assert_eq!(collect(&ds), vec![('D', 0), ('D', 1), ('V', 2)]);
910    }
911
912    #[test]
913    fn display_slice_folded_line_emits_at_next_visible_row() {
914        // Matrix has source lines [0, 2, 4] (lines 1 and 3
915        // folded). Virtual rows anchored at 1 (Above) and 3
916        // (Below) must emit at the next visible row -- they
917        // can't sit at their original folded source line.
918        let c = chunk(0, vec![row(0, b'a'), row(2, b'b'), row(4, b'c')]);
919        let m = CellMatrix::whole_doc(c, 5);
920        let v = VirtualRowMatrix::build(
921            vec![
922                vrow(1, AnchorPosition::Above),
923                vrow(3, AnchorPosition::Below),
924            ],
925            5,
926            VirtualRowVersion(1),
927        );
928        let ds = m.display_slice(0, 20, &v);
929        // (1, Above) emits before the next visible doc row
930        // (source 2). (3, Below) emits before the next visible
931        // doc row (source 4) because the would-be anchor line
932        // 3 is folded out.
933        assert_eq!(
934            collect(&ds),
935            vec![('D', 0), ('V', 1), ('D', 2), ('V', 3), ('D', 4),]
936        );
937    }
938
939    #[test]
940    fn display_slice_scroll_skips_display_rows() {
941        let c = chunk(0, vec![row(0, b'a'), row(1, b'b'), row(2, b'c')]);
942        let m = CellMatrix::whole_doc(c, 3);
943        let v = VirtualRowMatrix::build(
944            vec![vrow(0, AnchorPosition::Below)],
945            3,
946            VirtualRowVersion(1),
947        );
948        // Unsliced order: D0, V0, D1, D2.
949        let ds_0 = m.display_slice(0, 10, &v);
950        assert_eq!(collect(&ds_0), vec![('D', 0), ('V', 0), ('D', 1), ('D', 2)]);
951
952        // Scroll past D0 and V0: 2-row skip starts at D1.
953        let ds_2 = m.display_slice(2, 10, &v);
954        assert_eq!(collect(&ds_2), vec![('D', 1), ('D', 2)]);
955    }
956
957    #[test]
958    fn display_slice_height_bounds_yielded_rows() {
959        let c = chunk(0, vec![row(0, b'a'), row(1, b'b'), row(2, b'c')]);
960        let m = CellMatrix::whole_doc(c, 3);
961        let v = VirtualRowMatrix::build(
962            vec![vrow(0, AnchorPosition::Below)],
963            3,
964            VirtualRowVersion(1),
965        );
966        // Unsliced order: D0, V0, D1, D2. height=2 yields the
967        // first two.
968        let ds = m.display_slice(0, 2, &v);
969        assert_eq!(collect(&ds), vec![('D', 0), ('V', 0)]);
970    }
971
972    #[test]
973    fn display_slice_empty_matrix_with_virtual_rows_emits_only_virtual() {
974        let m = CellMatrix::empty();
975        let v = VirtualRowMatrix::build(
976            vec![
977                vrow(0, AnchorPosition::Above),
978                vrow(0, AnchorPosition::Below),
979            ],
980            0,
981            VirtualRowVersion(1),
982        );
983        let ds = m.display_slice(0, 10, &v);
984        assert_eq!(collect(&ds), vec![('V', 0), ('V', 0)]);
985    }
986
987    #[test]
988    fn display_slice_chunked_matrix_interleaves_correctly() {
989        // Two chunks of size 2: rows [0, 1] and [2, 3].
990        let c1 = chunk(0, vec![row(0, b'a'), row(1, b'b')]);
991        let c2 = chunk(2, vec![row(2, b'c'), row(3, b'd')]);
992        let m = CellMatrix::chunked(vec![c1, c2], 2, 4, MatrixVersion::ZERO);
993        let v = VirtualRowMatrix::build(
994            vec![
995                vrow(1, AnchorPosition::Below),
996                vrow(2, AnchorPosition::Above),
997            ],
998            4,
999            VirtualRowVersion(1),
1000        );
1001        let ds = m.display_slice(0, 20, &v);
1002        assert_eq!(
1003            collect(&ds),
1004            vec![
1005                ('D', 0),
1006                ('D', 1),
1007                ('V', 1), // Below(1)
1008                ('V', 2), // Above(2)
1009                ('D', 2),
1010                ('D', 3),
1011            ]
1012        );
1013    }
1014}