Skip to main content

lattice_cells/
row.rs

1//! One renderable row of cells.
2//!
3//! A `CellRow` corresponds to one *visible* row in the matrix.
4//! When the buffer has folds, folded source lines do **not**
5//! produce a row — the chunk's row vector skips them. Use
6//! [`CellRow::source_line`] to recover the logical buffer line.
7
8use std::sync::Arc;
9
10use crate::cell::Cell;
11
12/// Inlay-hint position record. `(orig_byte, char_width)`:
13///
14/// - `orig_byte` — utf-8 byte offset INTO THE ORIGINAL (pre-splice)
15///   line where the inlay was inserted.
16/// - `char_width` — number of cells the inlay occupies in the
17///   spliced row.
18///
19/// Overlays (cursor, selection, diagnostic underline) take byte
20/// positions in the source rope. To map a source byte → cell
21/// column, the overlay walker scans this list and accumulates
22/// `char_width` for every entry with `orig_byte ≤ target`. The
23/// representation mirrors the TUI's existing inlay offset arrays
24/// so S3's cutover is a structural swap, not a logic change.
25pub type InlayOffset = (u32, u32);
26
27/// One row of cells in the matrix. Immutable once built; cheap to
28/// share via `Arc<CellRow>` across panes and frames.
29#[derive(Clone, Debug, PartialEq, Eq, Hash)]
30pub struct CellRow {
31    /// Body cells in left-to-right order, including any spliced
32    /// inlay cells. Length is the rendered column count for this
33    /// row (post-inlay).
34    pub cells: Arc<[Cell]>,
35    /// Logical line in the source buffer (0-based, pre-fold).
36    /// Stable across edits *within* the line; rebuilt when the
37    /// line itself changes.
38    pub source_line: u32,
39    /// Inlay positions for byte↔column remap (see [`InlayOffset`]).
40    /// Empty when the row has no inlays.
41    pub inlay_offsets: Arc<[InlayOffset]>,
42}
43
44impl CellRow {
45    /// Construct a row from already-built cells + metadata. The
46    /// cell-builder worker (S2) is the production caller; tests
47    /// use this directly.
48    pub fn new(
49        cells: impl Into<Arc<[Cell]>>,
50        source_line: u32,
51        inlay_offsets: impl Into<Arc<[InlayOffset]>>,
52    ) -> Self {
53        Self {
54            cells: cells.into(),
55            source_line,
56            inlay_offsets: inlay_offsets.into(),
57        }
58    }
59
60    /// Empty row at a given source line. Used for visually-empty
61    /// lines (truly-empty source lines, or fold-marker placeholders).
62    pub fn empty(source_line: u32) -> Self {
63        Self {
64            cells: Arc::from([] as [Cell; 0]),
65            source_line,
66            inlay_offsets: Arc::from([] as [InlayOffset; 0]),
67        }
68    }
69
70    /// Clone-with-shifted-source-line. The `cells` and
71    /// `inlay_offsets` Arcs are reused (cheap refcount bump — the
72    /// cell payload is shared); only the `source_line` field
73    /// changes.
74    ///
75    /// Used by the cell-builder's incremental rebuild path
76    /// (S2.4.b) to reuse cached row content when an edit shifts
77    /// downstream lines without changing their contents.
78    /// `new_source_line` is the row's logical line in the
79    /// post-edit document.
80    pub fn with_source_line(&self, new_source_line: u32) -> Self {
81        Self {
82            cells: Arc::clone(&self.cells),
83            source_line: new_source_line,
84            inlay_offsets: Arc::clone(&self.inlay_offsets),
85        }
86    }
87
88    /// Column count = post-inlay cell count.
89    pub fn col_count(&self) -> u32 {
90        self.cells.len() as u32
91    }
92
93    /// Soft-wrap (W.2/A2): the cell sub-slice for display segment
94    /// `seg` when this row is wrapped at `width` columns. Segment 0
95    /// covers columns `[0, width)`, segment 1 `[width, 2·width)`,
96    /// and so on. The last segment is short when `col_count` isn't a
97    /// multiple of `width`; out-of-range segments return an empty
98    /// slice.
99    ///
100    /// `width == 0` means wrapping is off: segment 0 is the whole
101    /// row, every other segment is empty. Pure borrow — no
102    /// allocation, and the cells (with their resolved syntax
103    /// colours/flags) are untouched, so highlighting is preserved
104    /// verbatim. Shared by both renderers so segment geometry is
105    /// defined in one place.
106    pub fn segment(&self, seg: u32, width: u32) -> &[Cell] {
107        if width == 0 {
108            return if seg == 0 { &self.cells } else { &[] };
109        }
110        let len = self.cells.len();
111        let start = (seg as usize).saturating_mul(width as usize).min(len);
112        let end = start.saturating_add(width as usize).min(len);
113        &self.cells[start..end]
114    }
115
116    /// `true` when the row has no cells. Visually-empty source
117    /// lines produce empty rows.
118    pub fn is_empty(&self) -> bool {
119        self.cells.is_empty()
120    }
121
122    /// Map a source-byte (already char-resolved, see note) →
123    /// combined cell column for this row. Used by overlay
124    /// decoration computation to position cursor / selection /
125    /// diagnostic underline quads. Returns the column *after* any
126    /// inlays spliced before `byte`.
127    ///
128    /// Walks `inlay_offsets` linearly — fine for the typical
129    /// 0–3 inlays per row. Pre-sort assumption: `inlay_offsets`
130    /// must be sorted ascending by `orig_byte`. Construction in
131    /// S2 maintains that invariant.
132    ///
133    /// Note on byte vs char: the cell-grid renderer's design
134    /// target is ASCII source code where byte == char-column. For
135    /// non-ASCII content the cell-builder pre-resolves byte → char
136    /// position before calling overlays, so `byte` here is
137    /// effectively a char-column count. This fn only adds inlay
138    /// shifts.
139    pub fn byte_to_combined_col(&self, byte: u32) -> u32 {
140        let mut col = byte;
141        for (orig_byte, width) in self.inlay_offsets.iter() {
142            if *orig_byte <= byte {
143                col = col.saturating_add(*width);
144            } else {
145                break;
146            }
147        }
148        col
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    fn ascii(b: u8) -> Cell {
157        Cell::with_codepoint(b as u32)
158    }
159
160    #[test]
161    fn empty_row_at_source_line() {
162        let r = CellRow::empty(42);
163        assert_eq!(r.source_line, 42);
164        assert!(r.is_empty());
165        assert_eq!(r.col_count(), 0);
166        assert!(r.inlay_offsets.is_empty());
167    }
168
169    #[test]
170    fn segment_slices_at_width() {
171        let cells: Vec<Cell> = (0..10u8).map(|i| ascii(b'0' + i)).collect();
172        let r = CellRow::new(cells, 0, Vec::<InlayOffset>::new());
173        // Wrap off ⇒ seg 0 is the whole row, others empty.
174        assert_eq!(r.segment(0, 0).len(), 10);
175        assert_eq!(r.segment(1, 0).len(), 0);
176        // Wrap at 4 ⇒ segments [0,4), [4,8), [8,10) (short last).
177        assert_eq!(r.segment(0, 4).len(), 4);
178        assert_eq!(r.segment(0, 4)[0].codepoint, b'0' as u32);
179        assert_eq!(r.segment(1, 4).len(), 4);
180        assert_eq!(r.segment(1, 4)[0].codepoint, b'4' as u32);
181        assert_eq!(r.segment(2, 4).len(), 2);
182        assert_eq!(r.segment(2, 4)[0].codepoint, b'8' as u32);
183        // Out-of-range segment ⇒ empty, no panic.
184        assert_eq!(r.segment(3, 4).len(), 0);
185        assert_eq!(r.segment(99, 4).len(), 0);
186    }
187
188    #[test]
189    fn new_builds_from_slices() {
190        let cells = vec![ascii(b'a'), ascii(b'b'), ascii(b'c')];
191        let inlays = vec![(2u32, 1u32)];
192        let r = CellRow::new(cells.clone(), 7, inlays.clone());
193        assert_eq!(r.source_line, 7);
194        assert_eq!(r.col_count(), 3);
195        assert_eq!(r.cells.as_ref(), cells.as_slice());
196        assert_eq!(r.inlay_offsets.as_ref(), inlays.as_slice());
197    }
198
199    #[test]
200    fn byte_to_col_identity_without_inlays() {
201        let r = CellRow::new(vec![ascii(b'h'), ascii(b'i')], 0, Vec::<InlayOffset>::new());
202        assert_eq!(r.byte_to_combined_col(0), 0);
203        assert_eq!(r.byte_to_combined_col(1), 1);
204        assert_eq!(r.byte_to_combined_col(2), 2);
205    }
206
207    #[test]
208    fn byte_to_col_shifts_by_inlay_width() {
209        // Inlay of width 3 inserted before byte 2.
210        let r = CellRow::new(
211            vec![
212                ascii(b'a'),
213                ascii(b'b'),
214                ascii(b'?'),
215                ascii(b'?'),
216                ascii(b'?'),
217                ascii(b'c'),
218            ],
219            0,
220            vec![(2u32, 3u32)],
221        );
222        // Bytes before the inlay are unshifted.
223        assert_eq!(r.byte_to_combined_col(0), 0);
224        assert_eq!(r.byte_to_combined_col(1), 1);
225        // Byte at the inlay position is shifted by the inlay width.
226        assert_eq!(r.byte_to_combined_col(2), 5);
227        assert_eq!(r.byte_to_combined_col(3), 6);
228    }
229
230    #[test]
231    fn byte_to_col_handles_multiple_inlays() {
232        let r = CellRow::new(vec![ascii(b'x'); 10], 0, vec![(1u32, 2u32), (3u32, 1u32)]);
233        assert_eq!(r.byte_to_combined_col(0), 0);
234        assert_eq!(r.byte_to_combined_col(1), 3); // +2 from first inlay
235        assert_eq!(r.byte_to_combined_col(2), 4); // still +2
236        assert_eq!(r.byte_to_combined_col(3), 6); // +3 (both)
237        assert_eq!(r.byte_to_combined_col(5), 8);
238    }
239
240    /// S2.4.b: `with_source_line` keeps the cell + inlay-offset
241    /// payloads (shared `Arc` identity) and only changes the row's
242    /// logical-line position.
243    #[test]
244    fn with_source_line_shares_cell_arcs() {
245        let cells = vec![ascii(b'a'), ascii(b'b'), ascii(b'c')];
246        let inlays = vec![(2u32, 1u32)];
247        let r = CellRow::new(cells, 5, inlays);
248        let shifted = r.with_source_line(12);
249        assert_eq!(shifted.source_line, 12);
250        // Arc payloads must be shared — cheap refcount-only clone.
251        assert!(Arc::ptr_eq(&r.cells, &shifted.cells));
252        assert!(Arc::ptr_eq(&r.inlay_offsets, &shifted.inlay_offsets));
253        // The original is unchanged.
254        assert_eq!(r.source_line, 5);
255    }
256}