Skip to main content

scrive_core/
row_layout.rs

1//! Per-row horizontal projection — the **one owner** of byte-column ↔
2//! display-cell math on rows with collapsed inline folds, and of the collapsed
3//! block header's `head … tail` one-line layout.
4//!
5//! Every consumer that needs to place something horizontally — render, caret
6//! placement, hit testing, selection, and movement — consults the functions
7//! here rather than recomputing the inline-collapse shift, its inverse, the
8//! chip-center formula, the caret/glyph boundary predicates, or the header
9//! width that feeds a collapsed block's inline tail. Because each of those
10//! facts has exactly one definition, a change to the chip layout or a boundary
11//! rule is a one-site edit and every site stays in agreement by construction.
12//!
13//! Everything here is in **cells and columns** — GUI-free. The widget's only
14//! remaining job is `x = origin + cell × advance` (and its inverse).
15
16use std::borrow::Cow;
17
18use crate::buffer::Buffer;
19use crate::coords::{Bias, Point};
20use crate::display_map::{self, BufferRow, DisplayRow};
21use crate::fold_map::{FoldMap, InlineFold};
22
23// Op-count canary: counts `display_position` probes on this thread so a test
24// can assert `expand_folds_touched` calls it O(edit points) per commit — once
25// per point, for the hidden-gap check — and never O(candidates · edits), which
26// would make a document-scale multi-caret edit over a folded document cost the
27// product of the two. Debug/test only; zero-cost in release.
28#[cfg(any(test, debug_assertions))]
29thread_local! {
30    pub(crate) static DISPLAY_POSITION_PROBES: std::cell::Cell<u64> =
31        const { std::cell::Cell::new(0) };
32}
33
34/// Display cells a collapsed inline fold's `…` chip occupies. The chip
35/// starts one cell past the opening bracket; the interior collapses to this.
36pub const INLINE_CHIP_CELLS: u32 = 3;
37
38/// Display cells between a collapsed block header's end and its inline closing
39/// tail — the ` … ` placeholder gap.
40pub const FOLD_PLACEHOLDER_CELLS: u32 = 4;
41
42/// Where a caret at some byte column renders horizontally: a whole display
43/// cell, or — for a column hidden inside a collapsed inline fold — the center
44/// of that fold's `…` chip. The chip center is the **only** fractional cell in
45/// the system; fencing it in this enum keeps `f32` out of the inverse maps.
46#[derive(Copy, Clone, PartialEq, Debug)]
47pub enum CaretCell {
48    /// An exact display cell.
49    Cell(u32),
50    /// The fractional cell at a collapsed chip's visual center.
51    ChipCenter(f32),
52}
53
54impl CaretCell {
55    /// The (possibly fractional) display-cell value, for pixel projection.
56    #[must_use]
57    pub fn cells(self) -> f32 {
58        match self {
59            Self::Cell(c) => c as f32,
60            Self::ChipCenter(c) => c,
61        }
62    }
63}
64
65/// Where a buffer offset renders: its display row plus its horizontal
66/// [`CaretCell`]. THE owner of "where does offset O show on screen" — a caret,
67/// selection endpoint, squiggle bound, popup anchor, and autoscroll target all
68/// read the same value (see [`FoldMap::display_position`]).
69#[derive(Copy, Clone, PartialEq, Debug)]
70pub struct DisplayPosition {
71    /// The visible display row (a hidden closing tail resolves to its header's).
72    pub row: DisplayRow,
73    /// The horizontal position on that row.
74    pub x: CaretCell,
75}
76
77/// One collapsed inline fold's `…` chip on a row, in display cells, as named
78/// fields the widget destructures to render and hit-test the chip.
79#[derive(Copy, Clone, PartialEq, Debug)]
80pub struct Chip {
81    /// The chip's first display cell (one past the opening bracket).
82    pub cell: u32,
83    /// The chip's visual center, in fractional display cells.
84    pub center: f32,
85    /// Byte column of the opening bracket on its line.
86    pub open_col: u32,
87    /// Byte column of the closing bracket on its line.
88    pub close_col: u32,
89}
90
91/// One visible glyph of a collapsed block's inline closing tail, resolved to
92/// the display cell it occupies on the *header* row.
93#[derive(Copy, Clone, PartialEq, Debug)]
94pub struct TailGlyph {
95    /// Byte column on the fold's *last* buffer row.
96    pub col: u32,
97    /// Display cell on the header's display row.
98    pub cell: u32,
99    /// The character itself.
100    pub ch: char,
101}
102
103/// Which region of a collapsed block header's display line a cell falls in.
104#[derive(Copy, Clone, PartialEq, Debug)]
105pub enum HeaderHit {
106    /// On the header's own text — resolve against the header row's [`RowLayout`].
107    Head,
108    /// In the ` … ` placeholder gap — clamps to the header line's end.
109    Gap,
110    /// On the inline closing tail: the byte column on the fold's *last* row.
111    Tail(u32),
112}
113
114/// The caret slot just after a pair's opening bracket — the LEFT landable edge
115/// of a collapsed inline gap. The one place `open + 1` is written.
116#[must_use]
117pub fn gap_left_edge(open: u32) -> u32 {
118    open + 1
119}
120
121/// Whether caret offset `off` is strictly inside the collapsed gap of the pair
122/// `(open, close)`. THE caret-boundary rule: `open+1` and `close` are
123/// landable; strictly between them hides. (Its off-by-one sibling is
124/// [`gap_hides_glyph`] — a *glyph* at `open+1` hides even though the caret slot
125/// there is landable. The two variants exist as exactly these two functions.)
126#[must_use]
127pub fn gap_hides_caret(open: u32, close: u32, off: u32) -> bool {
128    off > gap_left_edge(open) && off < close
129}
130
131/// Whether the glyph at offset `off` is hidden by the collapsed pair
132/// `(open, close)`: everything strictly between the brackets.
133#[must_use]
134pub fn gap_hides_glyph(open: u32, close: u32, off: u32) -> bool {
135    off > open && off < close
136}
137
138/// Byte column where a line's visible content starts (its leading whitespace
139/// length) — where a collapsed block's closing tail begins on its last row.
140/// The one `trim_start` rule, shared by movement and the widget.
141#[must_use]
142pub fn tail_start_col(line: &str) -> u32 {
143    (line.len() - line.trim_start().len()) as u32
144}
145
146/// Whether a bracket pair `(open, close)` has a *non-empty interior* — the one
147/// foldability rule: there must be something between the brackets to
148/// hide. Shared by the document's foldable-pair enumeration and the fold map's
149/// inline/block resolution.
150#[must_use]
151pub fn pair_has_interior(open: u32, close: u32) -> bool {
152    close > open + 1
153}
154
155/// Round a fractional display cell to the nearest cell boundary, floored at 0
156/// — the quantization for a column (box) selection's *virtual* corner cells,
157/// which may lie past any line's content and so can't resolve through
158/// [`RowLayout::hit`] yet. The same round-half-up rule `hit` applies over real
159/// content, so a box edge and a click at the same pixel agree on the boundary.
160#[must_use]
161pub fn virtual_cell(cell: f32) -> u32 {
162    cell.round().max(0.0) as u32
163}
164
165/// One collapsed inline fold on a row, with its bracket cells precomputed.
166#[derive(Copy, Clone, Debug)]
167struct InlineSpan {
168    /// Tab-expanded (pre-collapse) cell of the opening bracket.
169    open_cell: u32,
170    /// Tab-expanded (pre-collapse) cell of the closing bracket.
171    close_cell: u32,
172    fold: InlineFold,
173}
174
175/// A visible buffer row's horizontal projection: byte column ↔ display cell,
176/// with tab expansion *and* the horizontal collapse of every root inline fold
177/// on the row. Built per use by [`FoldMap::row_layout`]; holds only the row's
178/// text (a [`Cow`] — borrowed straight off the backing when the row is stored
179/// contiguously, owned only when it spans a chunk boundary) and copies of the
180/// row's folds, so it carries no derived state that could drift out of sync
181/// with the document. Like [`FoldMap`], it is cheap to rebuild and never
182/// stored.
183pub struct RowLayout<'a> {
184    line: Cow<'a, str>,
185    /// Byte offset of the row's first character (column 0), for offset-space
186    /// boundary predicates.
187    row_start: u32,
188    tab: u32,
189    /// This row's root inline folds, sorted by opening cell.
190    spans: Vec<InlineSpan>,
191}
192
193impl<'a> RowLayout<'a> {
194    fn new(fold_map: &FoldMap, buffer: &'a Buffer, row: BufferRow, tab: u32) -> Self {
195        let line = buffer.line(row.0);
196        let row_start = buffer.point_to_offset(Point::new(row.0, 0));
197        // This row's inline folds — an O(log n + hits) windowed descent into the
198        // fold tree, not an O(all inline folds) scan/materialize per row per frame.
199        let mut spans: Vec<InlineSpan> = fold_map
200            .inline_folds_on_row(row.0)
201            .into_iter()
202            .map(|fold| InlineSpan {
203                open_cell: display_map::expand(&line, fold.open - row_start, tab),
204                close_cell: display_map::expand(&line, fold.close - row_start, tab),
205                fold,
206            })
207            .collect();
208        spans.sort_by_key(|s| s.open_cell);
209        Self { line, row_start, tab, spans }
210    }
211
212    /// Whether the row has no collapsed inline folds (the identity projection).
213    #[must_use]
214    pub fn is_plain(&self) -> bool {
215        self.spans.is_empty()
216    }
217
218    /// Byte offset of the row's column 0 — for turning a chip's byte columns back
219    /// into document offsets (e.g. to test a chip against the selection set).
220    #[must_use]
221    pub fn row_start(&self) -> u32 {
222        self.row_start
223    }
224
225    /// Collapse shift at pre-collapse cell `cell`: every fold whose closing
226    /// bracket sits at/before it has hidden `close−open−1` cells behind an
227    /// [`INLINE_CHIP_CELLS`]-wide chip.
228    fn shift_at(&self, cell: u32) -> i32 {
229        self.spans
230            .iter()
231            .filter(|s| cell >= s.close_cell)
232            .map(|s| (s.close_cell as i32 - s.open_cell as i32 - 1) - INLINE_CHIP_CELLS as i32)
233            .sum()
234    }
235
236    /// Pre-collapse (tab-expanded) cell → post-collapse display cell.
237    fn cell_of(&self, raw_cell: u32) -> u32 {
238        (raw_cell as i32 - self.shift_at(raw_cell)).max(0) as u32
239    }
240
241    /// Byte column → display cell (tab-expanded, inline-collapsed). Total and
242    /// monotone; a column hidden inside a chip maps into the chip's span (use
243    /// [`Self::caret_cell`] for caret placement, which clips to the center).
244    #[must_use]
245    pub fn display_cell(&self, col: u32) -> u32 {
246        self.cell_of(display_map::expand(&self.line, col, self.tab))
247    }
248
249    /// Caret placement for a byte column: its display cell, or the chip center
250    /// when the column is hidden inside a collapsed inline fold's gap.
251    #[must_use]
252    pub fn caret_cell(&self, col: u32) -> CaretCell {
253        let off = self.row_start + col;
254        match self.spans.iter().find(|s| s.fold.hides_caret_at(off)) {
255            Some(s) => CaretCell::ChipCenter(
256                self.cell_of(s.open_cell + 1) as f32 + INLINE_CHIP_CELLS as f32 / 2.0,
257            ),
258            None => CaretCell::Cell(self.display_cell(col)),
259        }
260    }
261
262    /// Whether the glyph at byte column `col` is hidden inside a collapsed
263    /// inline fold (the deliberately-different sibling of the caret rule: the
264    /// glyph at `open+1` hides while its caret slot stays landable).
265    #[must_use]
266    pub fn glyph_hidden(&self, col: u32) -> bool {
267        let off = self.row_start + col;
268        self.spans.iter().any(|s| s.fold.hides_glyph_at(off))
269    }
270
271    /// Inverse projection: a (fractional, unrounded) display cell → the byte
272    /// column a click there lands on. Rounding policy lives HERE, not at call
273    /// sites. A cell on a chip resolves to just after the opening bracket;
274    /// past-EOL clamps; mid-tab snaps by `bias`.
275    #[must_use]
276    pub fn hit(&self, cell: f32, bias: Bias) -> u32 {
277        let dc = cell.round().max(0.0) as u32;
278        let mut extra = 0i32;
279        for s in &self.spans {
280            // Compare in DISPLAY space: prior collapsed folds shift this
281            // fold's bracket cells left before the click can be tested.
282            let d_open = self.cell_of(s.open_cell);
283            let d_chip_end = d_open + 1 + INLINE_CHIP_CELLS;
284            if dc >= d_chip_end {
285                extra += (s.close_cell as i32 - s.open_cell as i32 - 1) - INLINE_CHIP_CELLS as i32;
286            } else if dc > d_open {
287                return s.fold.left_edge() - self.row_start; // on the chip → just after `[`
288            }
289        }
290        // Widened: a virtual cell past i32::MAX (a host box corner) would wrap
291        // negative as i32 and hit column 0 instead of the line end.
292        let raw_cell = (i64::from(dc) + i64::from(extra)).clamp(0, i64::from(u32::MAX)) as u32;
293        display_map::collapse(&self.line, raw_cell, self.tab, bias)
294    }
295
296    /// The row's rendered display width in cells (tab-expanded, collapsed).
297    /// A collapsed block's inline tail begins [`FOLD_PLACEHOLDER_CELLS`] past
298    /// this — see [`HeaderLayout::tail_cell`].
299    #[must_use]
300    pub fn width(&self) -> u32 {
301        self.display_cell(self.line.len() as u32)
302    }
303
304    /// The row's collapsed chips, in display order.
305    pub fn chips(&self) -> impl Iterator<Item = Chip> + '_ {
306        self.spans.iter().map(|s| {
307            let cell = self.cell_of(s.open_cell + 1);
308            Chip {
309                cell,
310                center: cell as f32 + INLINE_CHIP_CELLS as f32 / 2.0,
311                open_col: s.fold.open - self.row_start,
312                close_col: s.fold.close - self.row_start,
313            }
314        })
315    }
316}
317
318/// A collapsed block fold's one-line display layout: the (inline-fold-
319/// aware) header text, the ` … ` placeholder gap, then the fold's real closing
320/// tail — `fn main() { … }`. The single source for the placeholder render,
321/// caret placement on the tail, selection washes, hit-testing, the hover-chip
322/// rect, and the preview anchor, so they agree to the pixel by construction.
323pub struct HeaderLayout<'a> {
324    /// The header row's own horizontal projection.
325    head: RowLayout<'a>,
326    /// The fold's last buffer row — the line the tail glyphs come from.
327    last: BufferRow,
328    tail_line: Cow<'a, str>,
329    /// Byte column where the visible tail starts on `last` (its leading ws).
330    tail_lead: u32,
331    tab: u32,
332}
333
334impl<'a> HeaderLayout<'a> {
335    /// The header row's projection (for hits resolving to [`HeaderHit::Head`]).
336    #[must_use]
337    pub fn head(&self) -> &RowLayout<'a> {
338        &self.head
339    }
340
341    /// Rendered display width of the header text — where the placeholder gap
342    /// begins. Inline-fold aware: a collapsed inline fold before the block's
343    /// opener shrinks it.
344    #[must_use]
345    pub fn head_cells(&self) -> u32 {
346        self.head.width()
347    }
348
349    /// The fractional cell at the center of the ` … ` gap, where the chip and
350    /// its `…` glyph both center.
351    #[must_use]
352    pub fn gap_center(&self) -> f32 {
353        self.head_cells() as f32 + FOLD_PLACEHOLDER_CELLS as f32 / 2.0
354    }
355
356    /// The display cell where the inline closing tail begins.
357    #[must_use]
358    pub fn tail_cell(&self) -> u32 {
359        self.head_cells() + FOLD_PLACEHOLDER_CELLS
360    }
361
362    /// The fold's last buffer row (the tail's real home).
363    #[must_use]
364    pub fn last_row(&self) -> BufferRow {
365        self.last
366    }
367
368    /// Byte column on the last row where the visible tail starts.
369    #[must_use]
370    pub fn tail_start_col(&self) -> u32 {
371        self.tail_lead
372    }
373
374    /// Tab-expanded cell of the tail's first visible column on its own row.
375    fn lead_cells(&self) -> u32 {
376        display_map::expand(&self.tail_line, self.tail_lead, self.tab)
377    }
378
379    /// Byte column on the *last* row → display cell on the header row, using
380    /// full-line tab stops (the caret/hit/selection convention). `None` for a
381    /// column in the leading whitespace before the visible tail.
382    #[must_use]
383    pub fn tail_col_cell(&self, col: u32) -> Option<u32> {
384        (col >= self.tail_lead)
385            .then(|| self.tail_cell() + display_map::expand(&self.tail_line, col, self.tab) - self.lead_cells())
386    }
387
388    /// The tail's rendered width in cells.
389    #[must_use]
390    pub fn tail_cells(&self) -> u32 {
391        display_map::expand(&self.tail_line, self.tail_line.len() as u32, self.tab) - self.lead_cells()
392    }
393
394    /// The collapsed line's total rendered width (head + gap + tail), in cells.
395    #[must_use]
396    pub fn width(&self) -> u32 {
397        self.tail_cell() + self.tail_cells()
398    }
399
400    /// The visible tail glyphs at their header-row display cells.
401    pub fn tail_glyphs(&self) -> impl Iterator<Item = TailGlyph> + '_ {
402        self.tail_line[self.tail_lead as usize..].char_indices().map(move |(i, ch)| {
403            let col = self.tail_lead + i as u32;
404            TailGlyph {
405                col,
406                cell: self.tail_col_cell(col).expect("tail glyph is at/after the lead"),
407                ch,
408            }
409        })
410    }
411
412    /// Resolve a (fractional) display cell on the collapsed header line to the
413    /// region it falls in, with the ±half-cell boundaries the hit-test has
414    /// always used: at/after the tail (minus half a cell) → the tail column;
415    /// past the header text (plus half a cell) → the gap; else the head.
416    #[must_use]
417    pub fn hit(&self, cell: f32, bias: Bias) -> HeaderHit {
418        if cell >= self.tail_cell() as f32 - 0.5 {
419            let cell_in_tail = (cell - self.tail_cell() as f32).round().max(0.0) as u32;
420            let col = display_map::collapse(&self.tail_line, self.lead_cells() + cell_in_tail, self.tab, bias);
421            HeaderHit::Tail(col)
422        } else if cell > self.head_cells() as f32 + 0.5 {
423            HeaderHit::Gap
424        } else {
425            HeaderHit::Head
426        }
427    }
428}
429
430impl FoldMap {
431    /// The horizontal projection of visible buffer `row` — built per use,
432    /// like the `FoldMap` itself; never store it.
433    #[must_use]
434    pub fn row_layout<'a>(&self, buffer: &'a Buffer, row: BufferRow, tab: u32) -> RowLayout<'a> {
435        RowLayout::new(self, buffer, row, tab)
436    }
437
438    /// The `head … tail` one-line layout of `row`, iff it is a collapsed block
439    /// fold's header.
440    #[must_use]
441    pub fn header_layout<'a>(&self, buffer: &'a Buffer, row: BufferRow, tab: u32) -> Option<HeaderLayout<'a>> {
442        let last = self.fold_at_header(row)?;
443        let head = self.row_layout(buffer, row, tab);
444        let tail_line = buffer.line(last.0);
445        let tail_lead = tail_start_col(&tail_line);
446        Some(HeaderLayout { head, last, tail_line, tail_lead, tab })
447    }
448
449    /// THE owner of "where does buffer `offset` render": its display row and
450    /// horizontal cell. Follows a collapsed block's closing tail to the header
451    /// row; clips a column hidden in an inline fold to its chip center. `None`
452    /// iff the offset is genuinely hidden — inside a block fold's gap, or on
453    /// the last row before the visible tail.
454    #[must_use]
455    pub fn display_position(&self, buffer: &Buffer, offset: u32, tab: u32) -> Option<DisplayPosition> {
456        #[cfg(any(test, debug_assertions))]
457        DISPLAY_POSITION_PROBES.with(|c| c.set(c.get() + 1));
458        crate::perf::charge(1); // complexity gate: one display-map probe
459        let p = buffer.offset_to_point(offset);
460        let row = BufferRow(p.row);
461        if !self.is_folded(row) {
462            let layout = self.row_layout(buffer, row, tab);
463            return Some(DisplayPosition { row: self.to_display_row(row), x: layout.caret_cell(p.col) });
464        }
465        // Hidden row: only a collapsed fold's closing tail is representable —
466        // it rides the header's display line.
467        let hdr = self.header_of_tail(row)?;
468        let layout = self.header_layout(buffer, hdr, tab)?;
469        let cell = layout.tail_col_cell(p.col)?;
470        Some(DisplayPosition { row: self.to_display_row(hdr), x: CaretCell::Cell(cell) })
471    }
472
473    /// Inverse of [`Self::display_position`] on one visible row: a (fractional,
474    /// unrounded) display cell → the byte offset a click there lands on,
475    /// resolving a collapsed header's gap (→ header line end) and tail
476    /// (→ the last row's column) before the plain row projection.
477    #[must_use]
478    pub fn hit_row(&self, buffer: &Buffer, row: BufferRow, cell: f32, bias: Bias, tab: u32) -> u32 {
479        if let Some(layout) = self.header_layout(buffer, row, tab) {
480            match layout.hit(cell, bias) {
481                HeaderHit::Tail(col) => return buffer.point_to_offset(Point::new(layout.last_row().0, col)),
482                HeaderHit::Gap => return buffer.point_to_offset(Point::new(row.0, buffer.line_len(row.0))),
483                HeaderHit::Head => {}
484            }
485        }
486        let layout = self.row_layout(buffer, row, tab);
487        buffer.point_to_offset(Point::new(row.0, layout.hit(cell, bias)))
488    }
489
490    /// THE pixel-y inversion policy, in row units: a (fractional) count of
491    /// display rows from the content top → the display row it falls on,
492    /// floored and clamped to the valid range. Every y-driven hit (clicks,
493    /// hover, gutter, boxes) routes through this one rule.
494    ///
495    /// `f64`, deliberately: `f32` holds fractional rows exactly only below
496    /// ~2²³ rows — past that, hits land on the wrong line and (via the
497    /// widget's px↔row maps) rendered rows visibly skip. `f64` is exact for
498    /// every u32-addressable document.
499    #[must_use]
500    pub fn display_row_at(&self, rows_from_top: f64) -> DisplayRow {
501        DisplayRow((rows_from_top.floor().max(0.0) as u32).min(self.display_row_count().saturating_sub(1)))
502    }
503}
504
505#[cfg(test)]
506mod tests {
507    use super::*;
508    use crate::document::Document;
509
510    /// A document with the inline pair at `inline_at` and/or the block pair at
511    /// `block_at` collapsed (each the byte offset of an opening bracket).
512    fn doc_with_folds(text: &str, fold_openers: &[u32]) -> Document {
513        let mut doc = Document::new(text).expect("test doc fits");
514        for &o in fold_openers {
515            assert!(doc.toggle_fold_opener(o), "opener {o} must be foldable");
516        }
517        doc
518    }
519
520    fn fold_map(doc: &Document) -> FoldMap {
521        FoldMap::new(doc.folds(), doc.brackets(), doc.buffer())
522    }
523
524    // ── RowLayout: the caret boundary rule — open+1 lands on the chip's left
525    //    edge (a cell), never its center ──
526
527    #[test]
528    fn caret_cell_lands_on_chip_edge_not_center() {
529        // `a[bcdef]g` — fold the [..] pair (opener at byte 1).
530        let doc = doc_with_folds("a[bcdef]g", &[1]);
531        let fm = fold_map(&doc);
532        let rl = fm.row_layout(doc.buffer(), BufferRow(0), 4);
533        // col 2 == open+1: the LEFT landable edge — a cell, not the center.
534        assert_eq!(rl.caret_cell(2), CaretCell::Cell(2), "open+1 is landable at the chip's left edge");
535        // col 3 (strict interior) clips to the chip center: cell 2 + 1.5.
536        assert_eq!(rl.caret_cell(3), CaretCell::ChipCenter(2.0 + INLINE_CHIP_CELLS as f32 / 2.0));
537        // col 7 == close: the RIGHT landable edge.
538        assert_eq!(rl.caret_cell(7), CaretCell::Cell(rl.display_cell(7)));
539        // Glyphs: open+1's glyph hides even though its caret slot is landable.
540        assert!(rl.glyph_hidden(2));
541        assert!(!rl.glyph_hidden(7), "the closing bracket stays visible");
542        assert!(!rl.glyph_hidden(1), "the opening bracket stays visible");
543    }
544
545    // ── RowLayout: the inverse round-trips on multi-chip rows — the hit-test
546    //    compares in display space, so a later chip resolves correctly ──
547
548    #[test]
549    fn hit_round_trips_display_cell_on_multi_chip_rows() {
550        // Two inline pairs on one row, a tab up front, a multibyte char after.
551        let text = "\tf([aa], [bb]) é";
552        let open1 = text.find("[aa").unwrap() as u32;
553        let open2 = text.find("[bb").unwrap() as u32;
554        let doc = doc_with_folds(text, &[open1, open2]);
555        let fm = fold_map(&doc);
556        let rl = fm.row_layout(doc.buffer(), BufferRow(0), 4);
557        assert_eq!(rl.chips().count(), 2);
558        // Every landable char-boundary column round-trips through the inverse.
559        let line = doc.buffer().line(0);
560        for (i, _) in line.char_indices() {
561            let col = i as u32;
562            if rl.glyph_hidden(col) {
563                continue; // interior columns resolve to the chip instead
564            }
565            assert_eq!(rl.hit(rl.display_cell(col) as f32, Bias::Left), col, "round-trip col {col}");
566        }
567        // Every cell strictly on a chip resolves to just after its `[`. The
568        // second chip is the discriminating case: it resolves correctly only
569        // because the compare is done in display space, not buffer space.
570        for chip in rl.chips().collect::<Vec<_>>() {
571            for dc in chip.cell..chip.cell + INLINE_CHIP_CELLS {
572                assert_eq!(rl.hit(dc as f32, Bias::Left), chip.open_col + 1, "chip cell {dc}");
573            }
574        }
575    }
576
577    // ── HeaderLayout: an inline fold preceding a block fold shrinks the head,
578    //    so the tail is placed against the collapsed width, not the raw one ──
579
580    #[test]
581    fn header_layout_shrinks_with_preceding_inline_fold() {
582        let text = "\tcall([a, b, c]) {\n\tbody\n\t}\n";
583        let inline_open = text.find('[').unwrap() as u32;
584        let block_open = text.find('{').unwrap() as u32;
585        let doc = doc_with_folds(text, &[inline_open, block_open]);
586        let fm = fold_map(&doc);
587        let hl = fm.header_layout(doc.buffer(), BufferRow(0), 4).expect("row 0 is a collapsed header");
588        let raw = display_map::expand(&doc.buffer().line(0), doc.buffer().line(0).len() as u32, 4);
589        // The rendered head shrank: the [..] interior collapsed to a chip.
590        assert!(hl.head_cells() < raw, "head {} must be < raw {raw}", hl.head_cells());
591        assert_eq!(hl.head_cells(), fm.row_layout(doc.buffer(), BufferRow(0), 4).width());
592        assert_eq!(hl.tail_cell(), hl.head_cells() + FOLD_PLACEHOLDER_CELLS);
593        // The tail's `}` cell agrees between the glyph list and the col map.
594        let g: Vec<TailGlyph> = hl.tail_glyphs().collect();
595        assert_eq!(g.len(), 1);
596        assert_eq!(g[0].ch, '}');
597        assert_eq!(Some(g[0].cell), hl.tail_col_cell(g[0].col));
598    }
599
600    #[test]
601    fn tail_glyph_cells_match_tail_col_cell_with_tab_in_tail() {
602        // A tab INSIDE the visible tail: full-line tab stops, not trimmed-line.
603        let text = "f() {\nbody\n}\tx\n";
604        let block_open = text.find('{').unwrap() as u32;
605        let doc = doc_with_folds(text, &[block_open]);
606        let fm = fold_map(&doc);
607        let hl = fm.header_layout(doc.buffer(), BufferRow(0), 4).expect("collapsed header");
608        for g in hl.tail_glyphs() {
609            assert_eq!(Some(g.cell), hl.tail_col_cell(g.col), "glyph at col {} agrees with the col map", g.col);
610        }
611        // And the hit resolution lands back on each glyph's column.
612        for g in hl.tail_glyphs() {
613            assert_eq!(hl.hit(g.cell as f32, Bias::Left), HeaderHit::Tail(g.col));
614        }
615    }
616
617    // ── display_position: the one offset→(row, x) owner — an offset below a
618    //    fold resolves onto its visible display-space row ──
619
620    #[test]
621    fn display_position_follows_tail_and_hides_gap() {
622        let text = "a {\nhidden\n} tail\nafter\n";
623        let block_open = text.find('{').unwrap() as u32;
624        let doc = doc_with_folds(text, &[block_open]);
625        let fm = fold_map(&doc);
626        let buffer = doc.buffer();
627        // An offset inside the fold's gap is unrepresentable.
628        let hidden = buffer.point_to_offset(Point::new(1, 2));
629        assert_eq!(fm.display_position(buffer, hidden, 4), None);
630        // The tail `}` rides the header's display row at the tail cell.
631        let tail = buffer.point_to_offset(Point::new(2, 0));
632        let p = fm.display_position(buffer, tail, 4).expect("tail is visible");
633        assert_eq!(p.row, DisplayRow(0));
634        let hl = fm.header_layout(buffer, BufferRow(0), 4).unwrap();
635        assert_eq!(p.x, CaretCell::Cell(hl.tail_cell()));
636        // A row below the fold is shifted up by the hidden count.
637        let after = buffer.point_to_offset(Point::new(3, 0));
638        let p = fm.display_position(buffer, after, 4).expect("visible");
639        assert_eq!(p.row, DisplayRow(1), "rows 1..=2 hidden ⇒ row 3 displays at 1");
640    }
641
642    #[test]
643    fn hit_row_resolves_head_gap_and_tail() {
644        let text = "ab {\nhidden\n}\n";
645        let block_open = text.find('{').unwrap() as u32;
646        let doc = doc_with_folds(text, &[block_open]);
647        let fm = fold_map(&doc);
648        let buffer = doc.buffer();
649        let hl = fm.header_layout(buffer, BufferRow(0), 4).unwrap();
650        // Head: cell 0 → offset 0.
651        assert_eq!(fm.hit_row(buffer, BufferRow(0), 0.0, Bias::Left, 4), 0);
652        // Gap: between head end and tail → clamps to the header line's end.
653        let gap_cell = hl.head_cells() as f32 + FOLD_PLACEHOLDER_CELLS as f32 / 2.0;
654        assert_eq!(fm.hit_row(buffer, BufferRow(0), gap_cell, Bias::Left, 4), buffer.line_len(0));
655        // Tail: the tail cell → the `}` on the last row.
656        let tail_off = buffer.point_to_offset(Point::new(2, 0));
657        assert_eq!(fm.hit_row(buffer, BufferRow(0), hl.tail_cell() as f32, Bias::Left, 4), tail_off);
658    }
659
660    #[test]
661    fn display_row_at_floors_and_clamps() {
662        let doc = doc_with_folds("a\nb\nc\n", &[]);
663        let fm = fold_map(&doc);
664        assert_eq!(fm.display_row_at(-2.0), DisplayRow(0));
665        assert_eq!(fm.display_row_at(0.9), DisplayRow(0));
666        assert_eq!(fm.display_row_at(1.0), DisplayRow(1));
667        assert_eq!(fm.display_row_at(99.0), fm.max_display_row());
668    }
669
670    // ── plain rows: the projection is the identity over tab expansion ──
671
672    #[test]
673    fn plain_row_layout_is_tab_expansion() {
674        let doc = doc_with_folds("\tx = 1\n", &[]);
675        let fm = fold_map(&doc);
676        let rl = fm.row_layout(doc.buffer(), BufferRow(0), 4);
677        assert!(rl.is_plain());
678        assert_eq!(rl.display_cell(0), 0);
679        assert_eq!(rl.display_cell(1), 4, "tab expands to the stop");
680        assert_eq!(rl.width(), 4 + "x = 1".len() as u32);
681        assert_eq!(rl.hit(4.0, Bias::Left), 1);
682    }
683}