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}