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}