lattice_cells/virtual_rows.rs
1//! Virtual rows: a sibling lane to [`crate::CellMatrix`] for
2//! rows that **displace** content vertically without belonging
3//! to the source rope.
4//!
5//! Examples of virtual rows: diff deletion blocks (D.3),
6//! multibuffer excerpt headers and separators (M.2), LSP inlay
7//! hints that occupy a row of their own, code-lens summaries
8//! above a function declaration. Each is a row of [`Cell`]s
9//! that appears at a specific anchor source line, either
10//! immediately *above* or *below* that line.
11//!
12//! D.0a (this module) ships the primitive's data layer + an
13//! interleaving [`crate::DisplaySliceIter`] over
14//! [`CellMatrix`]. The first production consumer is D.3
15//! (inline diff overlay) per
16//! `docs/dev/architecture/diff-system.md`; the multibuffer
17//! consumer is M.2 per `multibuffer-views.md`. D.0a itself
18//! has no production renderer caller -- the iterator is
19//! validated end-to-end by tests + bench against real
20//! [`CellMatrix`] inputs.
21//!
22//! Design anchor:
23//! `docs/dev/architecture/virtual-rows.md`.
24
25use std::sync::Arc;
26
27use crate::cell::Cell;
28
29/// Where a virtual row sits relative to its anchor source
30/// line.
31#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
32pub enum AnchorPosition {
33 /// The virtual row paints immediately above the anchor
34 /// source line. Multiple `Above` rows at the same anchor
35 /// paint in their `VirtualRowMatrix.rows` insertion
36 /// order.
37 Above,
38 /// The virtual row paints immediately below the anchor
39 /// source line. Multiple `Below` rows at the same anchor
40 /// paint in their `VirtualRowMatrix.rows` insertion
41 /// order.
42 Below,
43}
44
45/// D.6.i (2026-05-31): which kind of virtual row this is,
46/// for renderer-side backdrop / decoration discrimination.
47///
48/// Two production kinds today:
49/// - `DeletionBlock` — a diff deletion-block row (D.3 inline
50/// overlay) carrying baseline content that's gone from
51/// the current side. Painted with the
52/// `host_theme.diff_deletion_block_bg` backdrop (default:
53/// faint dark red) so the user sees "this content
54/// existed in baseline but is gone in current".
55/// - `Filler` — a blank padding row (D.4.c / D.6.b
56/// side-by-side alignment) on the shorter side of a hunk
57/// so parallel rows line up across panes. Should paint
58/// with **no backdrop** (or a neutral one) — fillers are
59/// visual padding, not content; the deletion-block red
60/// would mis-read them as "deleted lines."
61///
62/// `Generic` is the default for any other virtual-row
63/// source (future code-lens, inlay-line, multibuffer
64/// excerpt header). Renderers treat it like a deletion
65/// block for backdrop purposes today; the variant exists
66/// so future kinds can join the discriminator without a
67/// breaking change.
68#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
69pub enum VirtualRowKind {
70 /// Default — any virtual row not explicitly tagged.
71 /// Painted with the deletion-block backdrop today.
72 #[default]
73 Generic,
74 /// Diff deletion-block row (D.3). Baseline content
75 /// that was removed from the current side; paints
76 /// with the deletion-block backdrop.
77 DeletionBlock,
78 /// Side-by-side alignment filler (D.4.c / D.6.b).
79 /// Blank padding; no backdrop.
80 Filler,
81 /// Sticky header row — always rendered at the top of the pane
82 /// regardless of scroll position. Excluded from the per-line
83 /// `virtual_rows_at` pass so it is never double-painted when its
84 /// anchor line is in the viewport. Use `VirtualRow::bg` to supply
85 /// a background colour; falls back to no backdrop if `bg` is `None`.
86 Sticky,
87 /// MG.26b: an annotation row — content that scrolls with its
88 /// anchor and paints **no backdrop** of its own.
89 ///
90 /// Distinct from [`Generic`](Self::Generic), which carries the
91 /// diff deletion-block backdrop: a blame chunk heading painted
92 /// with that would read as a removed line. Distinct from
93 /// [`Filler`](Self::Filler), which is blank alignment padding
94 /// rather than something to read, and from
95 /// [`Sticky`](Self::Sticky), which pins to the top of the pane —
96 /// an annotation belongs *with* the lines it describes and has to
97 /// scroll away with them. `VirtualRow::bg` still supplies a
98 /// background where one is wanted.
99 Annotation,
100 /// Dashboard branding block (DB.4-gpui). A contiguous group of
101 /// these rows carries the mark's block cells + the wordmark/tagline
102 /// text; the **GPUI peer** intercepts the group and paints a 2-D
103 /// composition instead of the flat cells — the mark as crisp square
104 /// quads (corner cuts preserved) and the "Lattice" wordmark shaped
105 /// large, vertically centred beside the mark. The **TUI peer** paints
106 /// the cells normally (its terminal-art treatment). A *paint*
107 /// discriminant only — never a motion/scroll/cursor branch.
108 BrandingBlock,
109 /// IM.3: an inline media block — an image drawn where it appears in the
110 /// buffer. A contiguous group of these rows carries the alt text as
111 /// ordinary cells; the **GPUI peer** intercepts the group and paints the
112 /// image over the region, the **TUI peer** paints the cells it was given
113 /// and needs no code of its own. The `BrandingBlock` treatment exactly,
114 /// which is what keeps the TUI a first-class peer for a feature it
115 /// cannot render.
116 ///
117 /// Scrolls with its anchor (an image belongs *with* the line that
118 /// references it), so unlike `BrandingBlock` it is NOT pinned. A *paint*
119 /// discriminant plus a height contribution — never a motion or cursor
120 /// branch.
121 MediaBlock,
122}
123
124impl VirtualRowKind {
125 /// Whether rows of this kind are *pinned* to the top of the pane
126 /// (rendered in the sticky pre-pass, excluded from the scrolling
127 /// per-line pass, and reserved out of the visible window) rather than
128 /// scrolling with the document.
129 ///
130 /// `Sticky` is the general headerline (multibuffer excerpt headers,
131 /// async-status HUD). `BrandingBlock` — the dashboard logo — is pinned
132 /// too: it is a masthead that should stay put while the sections
133 /// beneath it scroll, and it keeps its 2-D paint treatment either way.
134 pub fn is_pinned(self) -> bool {
135 matches!(self, VirtualRowKind::Sticky | VirtualRowKind::BrandingBlock)
136 }
137}
138
139/// One virtual row's anchor + content.
140///
141/// `anchor_line` is the 0-based source line this row attaches
142/// to. `position` selects Above or Below the anchor.
143/// `cells` is the rendered row content -- same `Arc<[Cell]>`
144/// shape that backs a document [`crate::CellRow`], so
145/// renderers can paint virtual rows through the same fast
146/// path with no special casing.
147///
148/// `height` is the row's vertical span in matrix-row units
149/// (`1` for the common case; values > 1 reserved for
150/// multi-line code-lens / signature-preview blocks that paint
151/// taller than one cell row).
152///
153/// `kind` (D.6.i) tags the row's provenance so renderers
154/// pick the right backdrop / decoration treatment without
155/// guessing from cell content.
156///
157/// `bg` overrides the kind-based default background when
158/// `Some(rgb_u32)` (`0xRRGGBB`). `None` → renderer picks from
159/// `kind` (deletion-block red for `DeletionBlock`/`Generic`,
160/// transparent for `Filler`/`Sticky`).
161#[derive(Clone, Debug)]
162pub struct VirtualRow {
163 pub anchor_line: u32,
164 pub position: AnchorPosition,
165 pub cells: Arc<[Cell]>,
166 pub height: u16,
167 pub kind: VirtualRowKind,
168 pub bg: Option<u32>,
169 /// F.3 (Thread F): per-display-column font scale in
170 /// **hundredths** (`100` = 1.0×, the base size), parallel to
171 /// [`Self::cells`] by display column. `None` ⇒ the whole row
172 /// is base size (the common case — zero cost, no allocation).
173 ///
174 /// This extends the variable-font commitment (per-token
175 /// scaling, the emacs markdown-heading model — only the title
176 /// scales, not the leading markers) from document rows to
177 /// virtual rows. A renderer coalesces contiguous equal scales
178 /// into runs (mirroring how it coalesces per-cell `fg`), shapes
179 /// each run at `font_size × scale/100` on a **shared baseline**,
180 /// and grows the row height to the tallest run. The dashboard
181 /// branding block (DB.4-gpui) is the first consumer: the
182 /// "Lattice" wordmark scales while the mark blocks stay base.
183 ///
184 /// The GPUI peer honors it; the TUI peer ignores it (a terminal
185 /// cell grid cannot vary font size). When present, its length
186 /// matches `cells`; a shorter/absent entry defaults to `100`.
187 pub scales: Option<Arc<[u16]>>,
188 /// IM.3: the media block this row belongs to, when
189 /// [`kind`](Self::kind) is
190 /// [`MediaBlock`](VirtualRowKind::MediaBlock).
191 ///
192 /// Shared across every row of a group, so a renderer that meets any row
193 /// of the block can paint the whole thing without reassembling it from
194 /// the cells. `None` for every other kind — one `Option<Arc>` per row,
195 /// which is what `scales` already costs.
196 pub media: Option<crate::media::MediaBlockRef>,
197 /// TC.8: the SOURCE line this row should show in its gutter (0-based;
198 /// renderers add one, as they do for document rows). `None` — the default
199 /// for every kind — paints the blank gutter virtual rows have always had.
200 ///
201 /// Deliberately separate from [`Self::anchor_line`]. Anchoring answers
202 /// "where does this row sit"; this answers "what number does it show", and
203 /// for most virtual rows the honest answer is *nothing*: a deletion block
204 /// has no current-side line, and a filler row has no line at all. A sticky
205 /// context row is the case where the two differ in the other direction —
206 /// it is anchored above the viewport but shows its own place in the file.
207 ///
208 /// It is a field rather than a renderer-side `match vrow.kind` because the
209 /// renderers must not branch on kind: any producer that knows a real line
210 /// number can set this and get the document gutter for free.
211 pub gutter_line: Option<u32>,
212 /// TC.11: foreground for [`Self::gutter_line`]'s digits (`0xRRGGBB`).
213 /// `None` — the default — leaves the renderer's own gutter colour, which
214 /// is what document rows use.
215 pub gutter_fg: Option<u32>,
216}
217
218/// F.3 (Thread F): one contiguous run of display columns rendered
219/// at a single font scale, produced by [`coalesce_scales`]. The
220/// renderer shapes each run at `font_size × scale/100`.
221#[derive(Copy, Clone, Debug, PartialEq, Eq)]
222pub struct ScaleRun {
223 /// First display column of the run (0-based, into the row's
224 /// cells).
225 pub start_col: u32,
226 /// Number of display columns the run spans.
227 pub cols: u32,
228 /// Font scale in hundredths (`100` = 1.0×).
229 pub scale: u16,
230}
231
232/// Base font scale in hundredths (`100` = 1.0×). A column with this
233/// scale (or no scale entry) renders at the base font size.
234pub const BASE_SCALE: u16 = 100;
235
236/// F.3 (Thread F): coalesce a per-column `scales` slice into
237/// contiguous same-scale [`ScaleRun`]s across `total_cols` columns,
238/// exactly as a renderer coalesces per-cell `fg` into text runs.
239///
240/// Columns beyond `scales.len()` (or when `scales` is empty) default
241/// to [`BASE_SCALE`]. The returned runs cover `0..total_cols`
242/// contiguously, in column order, and never split two adjacent
243/// columns that share a scale. `total_cols == 0` ⇒ empty. Pure and
244/// allocation-light (O(total_cols)); unit-testable without a
245/// renderer.
246pub fn coalesce_scales(scales: &[u16], total_cols: u32) -> Vec<ScaleRun> {
247 let mut runs: Vec<ScaleRun> = Vec::new();
248 if total_cols == 0 {
249 return runs;
250 }
251 let scale_at = |col: u32| -> u16 {
252 scales
253 .get(col as usize)
254 .copied()
255 .filter(|s| *s != 0)
256 .unwrap_or(BASE_SCALE)
257 };
258 let mut start = 0u32;
259 let mut cur = scale_at(0);
260 for col in 1..total_cols {
261 let s = scale_at(col);
262 if s != cur {
263 runs.push(ScaleRun {
264 start_col: start,
265 cols: col - start,
266 scale: cur,
267 });
268 start = col;
269 cur = s;
270 }
271 }
272 runs.push(ScaleRun {
273 start_col: start,
274 cols: total_cols - start,
275 scale: cur,
276 });
277 runs
278}
279
280/// A monotonically-increasing counter; bumped by the
281/// publisher whenever the [`VirtualRowMatrix`] is replaced.
282///
283/// Consumers compare versions across frames to invalidate
284/// caches. A single `u64` is sufficient because virtual rows
285/// have only one source of change (provider mutation); unlike
286/// [`crate::MatrixVersion`], they don't need multiple axes.
287#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
288pub struct VirtualRowVersion(pub u64);
289
290impl VirtualRowVersion {
291 pub const ZERO: Self = Self(0);
292
293 /// Returns the next version (wrapping on overflow, which
294 /// won't happen in any realistic session: 1 publish per
295 /// frame at 240Hz for 2 billion years would be needed).
296 pub fn next(self) -> Self {
297 Self(self.0.wrapping_add(1))
298 }
299}
300
301/// The published virtual-row lane for one document.
302///
303/// Immutable once built; the publisher replaces the `Arc<…>`
304/// when providers mutate. Cheap to clone (Arc bump).
305///
306/// `rows` is sorted by `(anchor_line, position)` with
307/// `Above` < `Below` at the same line. `line_index[i]` is the
308/// index of the first row in `rows` whose `anchor_line >= i`;
309/// length is `source_line_count + 1`. The line index turns
310/// "how many virtual rows are anchored before line L" into a
311/// constant-time array lookup, which the
312/// [`crate::DisplaySliceIter`] uses to fast-forward past
313/// scrolled-off virtual rows in O(1) instead of O(V).
314#[derive(Clone, Debug)]
315pub struct VirtualRowMatrix {
316 pub rows: Arc<[VirtualRow]>,
317 pub line_index: Arc<[u32]>,
318 pub source_line_count: u32,
319 pub version: VirtualRowVersion,
320}
321
322impl Default for VirtualRowMatrix {
323 fn default() -> Self {
324 Self::empty()
325 }
326}
327
328impl VirtualRowMatrix {
329 /// The empty matrix. The initial published value before
330 /// any provider has emitted.
331 pub fn empty() -> Self {
332 Self {
333 rows: Arc::from([] as [VirtualRow; 0]),
334 line_index: Arc::from([0u32]),
335 source_line_count: 0,
336 version: VirtualRowVersion::ZERO,
337 }
338 }
339
340 /// `true` when no virtual rows are present. The
341 /// [`crate::CellMatrix::display_slice`] fast path detects
342 /// this to skip interleaver overhead entirely.
343 pub fn is_empty(&self) -> bool {
344 self.rows.is_empty()
345 }
346
347 pub fn len(&self) -> usize {
348 self.rows.len()
349 }
350
351 /// Build a `VirtualRowMatrix` from an unsorted list. The
352 /// rows are sorted by `(anchor_line, position)` and the
353 /// `line_index` is computed.
354 ///
355 /// `source_line_count` should match the document's line
356 /// count (so the line-index sentinel covers EOF). If a
357 /// virtual row anchors past `source_line_count`, it is
358 /// clamped to anchor at `source_line_count` (treated as
359 /// "past EOF" by the interleaver, which emits it after
360 /// the last document row).
361 pub fn build(
362 mut rows: Vec<VirtualRow>,
363 source_line_count: u32,
364 version: VirtualRowVersion,
365 ) -> Self {
366 for row in &mut rows {
367 if row.anchor_line > source_line_count {
368 row.anchor_line = source_line_count;
369 }
370 }
371 rows.sort_by(|a, b| {
372 a.anchor_line
373 .cmp(&b.anchor_line)
374 .then_with(|| position_rank(a.position).cmp(&position_rank(b.position)))
375 });
376
377 let line_index_len = source_line_count.saturating_add(1) as usize;
378 let mut line_index = Vec::with_capacity(line_index_len);
379 let mut row_idx: u32 = 0;
380 for line in 0..line_index_len as u32 {
381 while (row_idx as usize) < rows.len() && rows[row_idx as usize].anchor_line < line {
382 row_idx += 1;
383 }
384 line_index.push(row_idx);
385 }
386
387 Self {
388 rows: Arc::from(rows),
389 line_index: Arc::from(line_index),
390 source_line_count,
391 version,
392 }
393 }
394
395 /// Index of the first row in `rows` whose `anchor_line >=
396 /// line`. Returns `rows.len() as u32` when every row
397 /// anchors strictly below `line`.
398 ///
399 /// O(1) array lookup when `line <= source_line_count`;
400 /// returns `rows.len()` for queries past EOF.
401 pub fn first_row_at_or_after(&self, line: u32) -> u32 {
402 let idx = (line as usize).min(self.line_index.len().saturating_sub(1));
403 self.line_index[idx]
404 }
405
406 /// Number of virtual rows whose anchor sits in the inclusive
407 /// document-line range `[lo, hi]`, regardless of
408 /// [`AnchorPosition`]. Returns `0` when `lo > hi`.
409 ///
410 /// O(1) — two [`Self::first_row_at_or_after`] lookups. This is
411 /// the geometry primitive the host's scroll model uses to
412 /// answer "how many *display* rows does the document-line span
413 /// `[lo, hi]` occupy", since each interleaved virtual row
414 /// consumes a display row without being a document line. The
415 /// count is position-agnostic on purpose: a bottom-anchored
416 /// scroll over-reserves by at most the cursor line's own
417 /// `Below` rows, which is the safe direction (the last line is
418 /// guaranteed clear of the modeline rather than flush against
419 /// it).
420 ///
421 /// [`VirtualRowKind::Sticky`] rows are excluded — they are
422 /// rendered at the pane top outside the scroll window and do
423 /// not displace content rows.
424 pub fn virtual_rows_in_line_range(&self, lo: u32, hi: u32) -> u32 {
425 if lo > hi {
426 return 0;
427 }
428 let end = self.first_row_at_or_after(hi.saturating_add(1));
429 let start = self.first_row_at_or_after(lo);
430 let total = end.saturating_sub(start);
431 let sticky = self.rows[start as usize..end as usize]
432 .iter()
433 .filter(|r| r.kind.is_pinned())
434 .count() as u32;
435 total.saturating_sub(sticky)
436 }
437
438 /// Iterator over all pinned rows in the matrix (see
439 /// [`VirtualRowKind::is_pinned`] — `Sticky` headerlines + the
440 /// `BrandingBlock` masthead). Used by renderers to paint the fixed top
441 /// strip before the scrollable content window.
442 /// IM.5: the [`RowWeights`](crate::RowWeights) implied by this matrix's
443 /// media blocks.
444 ///
445 /// A source line carrying a media block costs its own row plus the
446 /// block's DRAWN height, which is fractional and generally differs from
447 /// the rows the block reserved. Everything else is absent from the map
448 /// and therefore costs exactly what it always did.
449 ///
450 /// `draws_media` is the renderer's answer to "do I paint pixels". The TUI
451 /// passes `false` and gets a uniform map, so its scroll arithmetic is
452 /// untouched — it renders the block's alt-text cells in the reserved rows
453 /// and those rows are one line tall, which is the truth for a terminal.
454 /// Only one renderer runs in a process, so the two answers never collide.
455 pub fn media_row_weights(&self, draws_media: bool) -> crate::RowWeights {
456 let mut weights = crate::RowWeights::uniform();
457 if !draws_media {
458 return weights;
459 }
460 // Group by anchor line: a block's rows all share one anchor, and the
461 // line's cost is its own row plus the block's drawn height.
462 let mut seen: std::collections::HashMap<u32, (f32, u32)> = std::collections::HashMap::new();
463 for row in self.rows.iter() {
464 if row.kind != VirtualRowKind::MediaBlock {
465 continue;
466 }
467 let Some(block) = row.media.as_ref() else {
468 continue;
469 };
470 let entry = seen.entry(row.anchor_line).or_insert((0.0, 0));
471 entry.1 += 1;
472 // Every row of a group carries the same descriptor, so the drawn
473 // height is recorded once rather than accumulated per row.
474 entry.0 = block.line_heights(0);
475 }
476 for (line, (drawn, rows)) in seen {
477 // `line_heights(0)` returns 0.0 when the block has no resolved
478 // height yet — before its size is known it costs its reserved
479 // rows, which is what stops a resolving image from reflowing the
480 // document.
481 let cost = if drawn > 0.0 { drawn } else { rows as f32 };
482 weights.set(line, 1.0 + cost);
483 }
484 weights
485 }
486
487 pub fn sticky_rows(&self) -> impl Iterator<Item = &VirtualRow> {
488 self.rows.iter().filter(|r| r.kind.is_pinned())
489 }
490}
491
492/// Sort-order helper: `Above` < `Below` at the same anchor
493/// line.
494const fn position_rank(p: AnchorPosition) -> u8 {
495 match p {
496 AnchorPosition::Above => 0,
497 AnchorPosition::Below => 1,
498 }
499}
500
501/// Stable identity for a [`VirtualRowProvider`]. Issued by
502/// the worker / subsystem that owns the provider registry.
503pub type ProviderId = u64;
504
505/// A producer of virtual rows.
506///
507/// Producers are registered with the (future) virtual-rows
508/// worker, which calls [`Self::collect`] when rebuilding the
509/// published [`VirtualRowMatrix`]. The worker merges the
510/// outputs of all registered providers, sorts, and publishes
511/// via `ArcSwap`.
512///
513/// D.0a ships the trait; the worker itself lands in D.0a.1
514/// (or as part of D.3 when the first production consumer
515/// appears, whichever ships first). Tests build
516/// `VirtualRowMatrix` directly via [`VirtualRowMatrix::build`]
517/// rather than through a provider registry.
518pub trait VirtualRowProvider: Send + Sync + std::fmt::Debug {
519 /// A stable id for this provider. Used by the worker to
520 /// deduplicate registration + route mutation
521 /// notifications.
522 fn id(&self) -> ProviderId;
523
524 /// Monotonic version counter — the provider bumps it
525 /// whenever the rows [`Self::collect`] would emit have
526 /// changed. The worker uses the combined fingerprint of all
527 /// providers' versions to short-circuit on the cache-hit
528 /// path without paying for the (potentially expensive)
529 /// `collect` calls.
530 ///
531 /// D.0a.1 introduces this. Implementations whose row set is
532 /// truly static may return `0` forever — the worker will
533 /// then cache-hit unless some other provider's version
534 /// changes or the document's line count changes.
535 fn version(&self) -> u64;
536
537 /// Emit the current set of virtual rows for the
538 /// associated document.
539 ///
540 /// Called by the worker on its rebuild path. Providers
541 /// must not block; non-trivial computation belongs in the
542 /// provider's own background task with the result cached
543 /// here.
544 fn collect(&self) -> Vec<VirtualRow>;
545}
546
547#[cfg(test)]
548mod tests {
549 use super::*;
550
551 fn row(anchor: u32, pos: AnchorPosition) -> VirtualRow {
552 VirtualRow {
553 media: None,
554 anchor_line: anchor,
555 position: pos,
556 cells: Arc::from([] as [Cell; 0]),
557 height: 1,
558 kind: VirtualRowKind::Generic,
559 bg: None,
560 scales: None,
561 gutter_line: None,
562 gutter_fg: None,
563 }
564 }
565
566 #[test]
567 fn coalesce_scales_empty_is_empty() {
568 assert!(coalesce_scales(&[], 0).is_empty());
569 assert!(coalesce_scales(&[150, 150], 0).is_empty());
570 }
571
572 #[test]
573 fn coalesce_scales_no_scales_is_one_base_run() {
574 // No per-column scales ⇒ one base-size run spanning the row.
575 let runs = coalesce_scales(&[], 5);
576 assert_eq!(
577 runs,
578 vec![ScaleRun {
579 start_col: 0,
580 cols: 5,
581 scale: BASE_SCALE
582 }]
583 );
584 }
585
586 #[test]
587 fn coalesce_scales_splits_only_on_transition() {
588 // The markdown-heading shape: base markers, scaled title —
589 // "## " at base, the rest at 1.6×. Two runs, split at col 3.
590 let scales = [100, 100, 100, 160, 160, 160];
591 let runs = coalesce_scales(&scales, 6);
592 assert_eq!(
593 runs,
594 vec![
595 ScaleRun {
596 start_col: 0,
597 cols: 3,
598 scale: 100
599 },
600 ScaleRun {
601 start_col: 3,
602 cols: 3,
603 scale: 160
604 },
605 ]
606 );
607 }
608
609 #[test]
610 fn coalesce_scales_handles_multiple_runs_and_zero_sentinel() {
611 // A general per-token row: base, scaled, base again — plus a
612 // `0` sentinel column that defaults to base, and a trailing
613 // column past `scales.len()` that also defaults to base.
614 let scales = [100, 250, 250, 0];
615 let runs = coalesce_scales(&scales, 6);
616 assert_eq!(
617 runs,
618 vec![
619 ScaleRun {
620 start_col: 0,
621 cols: 1,
622 scale: 100
623 },
624 ScaleRun {
625 start_col: 1,
626 cols: 2,
627 scale: 250
628 },
629 ScaleRun {
630 start_col: 3,
631 cols: 3,
632 scale: 100
633 },
634 ]
635 );
636 }
637
638 #[test]
639 fn empty_matrix_basics() {
640 let m = VirtualRowMatrix::empty();
641 assert!(m.is_empty());
642 assert_eq!(m.len(), 0);
643 assert_eq!(m.source_line_count, 0);
644 assert_eq!(m.version, VirtualRowVersion::ZERO);
645 // line_index has one sentinel entry.
646 assert_eq!(m.line_index.len(), 1);
647 assert_eq!(m.first_row_at_or_after(0), 0);
648 assert_eq!(m.first_row_at_or_after(100), 0);
649 }
650
651 #[test]
652 fn build_sorts_by_anchor_and_position() {
653 // Insertion order: (5, Below), (3, Above), (5, Above), (3, Below).
654 // Sorted: (3, Above), (3, Below), (5, Above), (5, Below).
655 let rows = vec![
656 row(5, AnchorPosition::Below),
657 row(3, AnchorPosition::Above),
658 row(5, AnchorPosition::Above),
659 row(3, AnchorPosition::Below),
660 ];
661 let m = VirtualRowMatrix::build(rows, 10, VirtualRowVersion(1));
662 assert_eq!(m.len(), 4);
663 assert_eq!(m.rows[0].anchor_line, 3);
664 assert_eq!(m.rows[0].position, AnchorPosition::Above);
665 assert_eq!(m.rows[1].anchor_line, 3);
666 assert_eq!(m.rows[1].position, AnchorPosition::Below);
667 assert_eq!(m.rows[2].anchor_line, 5);
668 assert_eq!(m.rows[2].position, AnchorPosition::Above);
669 assert_eq!(m.rows[3].anchor_line, 5);
670 assert_eq!(m.rows[3].position, AnchorPosition::Below);
671 }
672
673 #[test]
674 fn line_index_locates_rows() {
675 let rows = vec![
676 row(2, AnchorPosition::Above),
677 row(2, AnchorPosition::Below),
678 row(5, AnchorPosition::Above),
679 row(7, AnchorPosition::Below),
680 ];
681 let m = VirtualRowMatrix::build(rows, 10, VirtualRowVersion(1));
682
683 // No rows anchor before line 0..2 ⇒ index 0 (first row
684 // is at line 2).
685 assert_eq!(m.first_row_at_or_after(0), 0);
686 assert_eq!(m.first_row_at_or_after(2), 0);
687 // Past line 2's two rows ⇒ index 2 (next is line 5).
688 assert_eq!(m.first_row_at_or_after(3), 2);
689 assert_eq!(m.first_row_at_or_after(5), 2);
690 // Past line 5's row ⇒ index 3 (next is line 7).
691 assert_eq!(m.first_row_at_or_after(6), 3);
692 assert_eq!(m.first_row_at_or_after(7), 3);
693 // Past everything ⇒ index 4 (= rows.len()).
694 assert_eq!(m.first_row_at_or_after(8), 4);
695 }
696
697 #[test]
698 fn anchor_past_eof_clamps_to_line_count() {
699 let rows = vec![
700 row(100, AnchorPosition::Above),
701 row(5, AnchorPosition::Above),
702 ];
703 let m = VirtualRowMatrix::build(rows, 10, VirtualRowVersion(1));
704 assert_eq!(m.len(), 2);
705 // (100) clamped to 10; (5) stays. After sort: (5),
706 // (10).
707 assert_eq!(m.rows[0].anchor_line, 5);
708 assert_eq!(m.rows[1].anchor_line, 10);
709 }
710
711 #[test]
712 fn virtual_rows_in_line_range_counts_inclusive() {
713 // anchors at lines 2 (x2), 5, 7.
714 let rows = vec![
715 row(2, AnchorPosition::Above),
716 row(2, AnchorPosition::Below),
717 row(5, AnchorPosition::Above),
718 row(7, AnchorPosition::Below),
719 ];
720 let m = VirtualRowMatrix::build(rows, 10, VirtualRowVersion(1));
721
722 // Empty / inverted ranges.
723 assert_eq!(m.virtual_rows_in_line_range(3, 2), 0);
724 // Range below every anchor.
725 assert_eq!(m.virtual_rows_in_line_range(0, 1), 0);
726 // Inclusive of both endpoints: [2, 7] covers all four.
727 assert_eq!(m.virtual_rows_in_line_range(2, 7), 4);
728 // Endpoint inclusivity: [2, 2] captures both line-2 rows.
729 assert_eq!(m.virtual_rows_in_line_range(2, 2), 2);
730 // Mid-range: [3, 5] captures only the line-5 row.
731 assert_eq!(m.virtual_rows_in_line_range(3, 5), 1);
732 // [6, 7] captures only the line-7 row.
733 assert_eq!(m.virtual_rows_in_line_range(6, 7), 1);
734 // Range past EOF is clamped, never panics.
735 assert_eq!(m.virtual_rows_in_line_range(8, u32::MAX), 0);
736 // The empty matrix reports zero for any range.
737 assert_eq!(
738 VirtualRowMatrix::empty().virtual_rows_in_line_range(0, u32::MAX),
739 0
740 );
741 }
742
743 #[test]
744 fn version_next_increments() {
745 let v = VirtualRowVersion::ZERO;
746 assert_eq!(v.next(), VirtualRowVersion(1));
747 assert_eq!(v.next().next(), VirtualRowVersion(2));
748 }
749}