scrive_core/fold_map.rs
1//! Code folding — **bracket-anchored**. A fold is a collapsed bracket
2//! *pair*, identified by the byte offset of its **opener** (`([{`). That offset
3//! is the only thing stored; it rides each committed transaction's [`Patch`] via
4//! [`Patch::map_offset`], and the fold's *extent* (which rows are hidden) is
5//! re-derived from the live [`Brackets`] pass every frame.
6//!
7//! Storing only the opener (never a hidden-interior byte range) is what keeps a
8//! fold from drifting onto an unrelated span: there is no interior offset to go
9//! stale. Reconcile is then trivial — a fold survives iff its opener is still a
10//! live, foldable open bracket. Rows are reconstructed each frame, so there is no
11//! persisted visual index to desync.
12//!
13//! **View state, not document state:** folds never enter the buffer or the undo
14//! stack; they compose over the buffer as a second display layer the
15//! render/movement/hit-test paths consult.
16
17use crate::bracket::Brackets;
18use crate::buffer::Buffer;
19use crate::coords::{Bias, Point};
20use crate::display_map::{BufferRow, DisplayRow};
21use crate::offset_set::OffsetSet;
22use crate::patch::Patch;
23use crate::sum_tree::{Dimension, Item, SumTree, Summary};
24
25// Op-count canary: the number of full `FoldMap::new` builds on this thread. The
26// per-keystroke analog of the widget's draw-budget gate — a document-scale test
27// asserts a plain keystroke / arrow key does NOT rebuild the whole map (which is
28// O(folds)); it shifts the cache in place or reads it. Thread-local so parallel
29// tests (which build fresh maps constantly) don't race the count. Debug/test
30// only; zero-cost in release.
31#[cfg(any(test, debug_assertions))]
32thread_local! {
33 pub(crate) static FOLD_BUILDS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
34}
35
36// Op-count canary: `FoldSet::reconcile` scans (the O(folds) retain) on this
37// thread. A test asserts a plain keystroke / Enter (no bracket char changed, so
38// no fold can lose its pair) does NOT scan the fold set, while a brace deletion
39// does. Debug/test only; zero-cost in release.
40#[cfg(any(test, debug_assertions))]
41thread_local! {
42 pub(crate) static RECONCILE_SCANS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
43}
44
45/// The persistent, document-owned set of active folds. Each entry is the byte
46/// offset of one collapsed pair's **opening bracket**, kept sorted and unique.
47/// Rides [`Patch`] remap in the commit path (see [`FoldSet::apply_patch`]); the
48/// extent is resolved from the [`Brackets`] pass in [`FoldMap::new`], so folds
49/// survive edits and undo/redo with no interior bookkeeping to drift.
50#[derive(Clone, Default, Debug)]
51pub struct FoldSet {
52 /// Collapsed pairs' opening-bracket byte offsets, on a delta-gap SumTree
53 /// ([`OffsetSet`]) so a text edit shifts the whole set without the flat Vec's
54 /// O(folds) rebase; membership / window / first-at are O(log n).
55 offsets: OffsetSet,
56 /// Bumped on every mutation — the memoization key for the document's cached
57 /// [`FoldMap`] (folds are view state, so an edit's buffer revision does NOT
58 /// cover a pure fold toggle; this does).
59 generation: u64,
60}
61
62impl FoldSet {
63 /// An empty fold set.
64 #[must_use]
65 pub fn new() -> Self {
66 Self::default()
67 }
68
69 /// Whether nothing is folded.
70 #[must_use]
71 pub fn is_empty(&self) -> bool {
72 self.offsets.is_empty()
73 }
74
75 /// The number of active folds.
76 #[must_use]
77 pub fn len(&self) -> usize {
78 self.offsets.len()
79 }
80
81 /// The smallest folded opener at byte `offset` or after, if any — an O(log)
82 /// probe into the opener-sorted set (e.g. "the fold headed on this row"),
83 /// not a scan of every fold.
84 #[must_use]
85 pub fn first_at_or_after(&self, offset: u32) -> Option<u32> {
86 self.offsets.first_at_or_after(offset)
87 }
88
89 /// The mutation counter — the document's `FoldMap` cache invalidates when it
90 /// changes (see the struct field).
91 #[must_use]
92 pub fn generation(&self) -> u64 {
93 self.generation
94 }
95
96 /// Note a mutation for the cache key.
97 fn bump(&mut self) {
98 self.generation = self.generation.wrapping_add(1);
99 }
100
101 /// Unfold everything.
102 pub fn clear(&mut self) {
103 self.offsets = OffsetSet::new();
104 self.bump();
105 }
106
107 /// Collapse the pair whose opener is at byte offset `open`. No-op returning
108 /// `false` if that opener is already folded. The caller vouches that `open` is
109 /// a foldable open bracket (see [`crate::Document::fold`]).
110 pub fn fold(&mut self, open: u32) -> bool {
111 if self.offsets.insert(open) {
112 self.bump();
113 true
114 } else {
115 false
116 }
117 }
118
119 /// Unfold the pair whose opener is at `open`. Returns whether one was removed.
120 pub fn unfold(&mut self, open: u32) -> bool {
121 if self.offsets.remove(open) {
122 self.bump();
123 true
124 } else {
125 false
126 }
127 }
128
129 /// Collapse every opener in `opens` in ONE pass — one set mutation for the
130 /// whole batch. A multi-caret `Ctrl+Shift+[` can collapse thousands of pairs at
131 /// once; calling [`Self::fold`] per item would be O(folds) each and O(folds²)
132 /// overall, so the batch path keeps a document-scale collapse linear. The
133 /// caller vouches each offset is a foldable open bracket.
134 pub fn fold_all(&mut self, opens: impl IntoIterator<Item = u32>) {
135 for open in opens {
136 self.offsets.insert(open); // idempotent; dedups
137 }
138 self.bump();
139 }
140
141 /// Unfold every opener in `opens` in ONE pass — the batch mirror of
142 /// [`Self::unfold`].
143 pub fn unfold_all(&mut self, opens: &[u32]) {
144 if opens.is_empty() {
145 return;
146 }
147 self.offsets.remove_all(opens);
148 self.bump();
149 }
150
151 /// Whether the pair opening at `open` is folded.
152 #[must_use]
153 pub fn is_folded(&self, open: u32) -> bool {
154 self.offsets.contains(open)
155 }
156
157 /// Fold the pair at `open` if not folded, else unfold it.
158 pub fn toggle(&mut self, open: u32) -> bool {
159 if self.is_folded(open) {
160 self.unfold(open)
161 } else {
162 self.fold(open)
163 }
164 }
165
166 /// Shift every opener through a committed patch. Bias `Right` keeps the
167 /// offset tracking its bracket char (an insertion at the bracket pushes both
168 /// right together). An edit that deletes the opener maps it into the deletion;
169 /// [`FoldSet::reconcile`] then drops it (it is no longer a live bracket).
170 pub fn apply_patch(&mut self, patch: &Patch) {
171 // Shift every opener through the patch (Bias::Right — the offset tracks its
172 // bracket char) on the delta-gap tree; a deleted opener maps into the
173 // deletion and reconcile drops it.
174 self.offsets.apply_patch(patch);
175 // Deliberately does NOT bump the generation: shifting openers through an
176 // edit is not a structural change to the fold SET, and the generation is
177 // the document's `FoldMap` cache key. The edit path shifts the cached
178 // map in lockstep via `FoldMap::apply_patch`, so a rebuild is reserved for
179 // real fold add/remove (fold/unfold/reconcile), which do bump.
180 }
181
182 /// Drop every fold whose opener is no longer a live *foldable* open bracket.
183 /// `is_foldable_opener` answers "is this byte offset an open bracket with a
184 /// matched partner forming a foldable range?" (from the live [`Brackets`]
185 /// pass — see `Document::reconcile_folds`). Because the extent is
186 /// re-derived, there is nothing else to validate or heal.
187 pub fn reconcile(&mut self, is_foldable_opener: impl Fn(u32) -> bool) {
188 #[cfg(any(test, debug_assertions))]
189 RECONCILE_SCANS.with(|c| c.set(c.get() + 1));
190 crate::perf::charge(self.offsets.len() as u64); // complexity gate: whole-set scan
191 let drop: Vec<u32> =
192 self.offsets.offsets().into_iter().filter(|&o| !is_foldable_opener(o)).collect();
193 self.offsets.remove_all(&drop);
194 self.bump();
195 }
196
197 /// The collapsed openers whose offset lies in `range`, ascending — the folds an
198 /// edit's re-matched region could directly touch. O(log n + hits).
199 #[must_use]
200 pub fn openers_in(&self, range: core::ops::Range<u32>) -> Vec<u32> {
201 self.offsets.in_range(range.start, range.end)
202 }
203
204 /// Windowed reconcile ([`reconcile`](Self::reconcile)'s twin): drop only the
205 /// `candidates` (a small, pre-narrowed set) that are no longer foldable. The
206 /// common case — a bracket edit that broke no fold — removes nothing and costs
207 /// only the O(candidates) predicate; the O(folds) `retain` runs (and is
208 /// counted) ONLY when a fold actually broke, which is the unavoidable Vec
209 /// compaction. Callers vouch every candidate is currently folded.
210 pub fn reconcile_only(&mut self, candidates: &[u32], is_foldable_opener: impl Fn(u32) -> bool) {
211 let drop: Vec<u32> = candidates.iter().copied().filter(|&o| !is_foldable_opener(o)).collect();
212 if drop.is_empty() {
213 return; // no fold broke — O(candidates), no whole-set touch
214 }
215 #[cfg(any(test, debug_assertions))]
216 RECONCILE_SCANS.with(|c| c.set(c.get() + 1));
217 crate::perf::charge(self.offsets.len() as u64); // complexity gate: fold-set touch
218 self.offsets.remove_all(&drop);
219 self.bump();
220 }
221
222 /// The collapsed openers (offsets), ascending — for [`FoldMap`] and tests.
223 pub fn iter(&self) -> impl Iterator<Item = u32> + '_ {
224 self.offsets.offsets().into_iter()
225 }
226}
227
228/// One resolved block fold: it hides buffer rows `header+1..=last`.
229#[derive(Clone, Copy, PartialEq, Eq, Debug)]
230struct FoldedRegion {
231 header: u32,
232 last: u32,
233}
234
235/// One resolved **inline** fold — a collapsed single-line bracket pair. It hides
236/// the bytes *between* the brackets (`open+1..close`) on one still-visible row,
237/// rendered `[ … ]`. The delimiters stay; only the interior collapses, so `close`
238/// remains a real, addressable buffer position.
239#[derive(Clone, Copy, PartialEq, Eq, Debug)]
240pub struct InlineFold {
241 /// The buffer row the pair sits on (opener and closer share it).
242 pub row: u32,
243 /// Byte offset of the opening bracket.
244 pub open: u32,
245 /// Byte offset of the closing bracket.
246 pub close: u32,
247}
248
249impl InlineFold {
250 /// The LEFT landable edge of the collapsed gap: just after the `[`.
251 #[must_use]
252 pub fn left_edge(&self) -> u32 {
253 crate::row_layout::gap_left_edge(self.open)
254 }
255
256 /// The RIGHT landable edge: the closing bracket itself.
257 #[must_use]
258 pub fn right_edge(&self) -> u32 {
259 self.close
260 }
261
262 /// Whether caret offset `off` is strictly inside the collapsed gap — the one
263 /// caret-boundary rule shared by movement, the document's fold-time caret
264 /// pull-out, and the widget's projections.
265 #[must_use]
266 pub fn hides_caret_at(&self, off: u32) -> bool {
267 crate::row_layout::gap_hides_caret(self.open, self.close, off)
268 }
269
270 /// Whether the *glyph* at `off` is hidden by this fold — the deliberately
271 /// off-by-one sibling of [`Self::hides_caret_at`]: the glyph at `open+1`
272 /// hides even though its caret slot stays landable.
273 #[must_use]
274 pub fn hides_glyph_at(&self, off: u32) -> bool {
275 crate::row_layout::gap_hides_glyph(self.open, self.close, off)
276 }
277}
278
279/// Reduce a laminar set of block regions to its **root** (outermost) members,
280/// sorted ascending by header and pairwise disjoint. A region nested inside
281/// another hides no extra rows and its header is itself hidden, so only roots
282/// drive the row arithmetic. Bracket pairs are naturally laminar (nested or
283/// disjoint, never crossing), so this never has to reject a crossing.
284fn root_regions(mut regions: Vec<FoldedRegion>) -> Vec<FoldedRegion> {
285 regions.sort_by_key(|r| (r.header, core::cmp::Reverse(r.last)));
286 let mut roots: Vec<FoldedRegion> = Vec::new();
287 for r in regions {
288 // `r.header > prev_root.last` ⇒ disjoint (a new root); otherwise `r` sits
289 // inside the current root's row span (laminar ⇒ fully contained) — skip.
290 if roots.last().is_none_or(|root| r.header > root.last) {
291 roots.push(r);
292 }
293 }
294 roots
295}
296
297/// Reduce inline folds to their **roots**: drop any inline fold nested inside
298/// another collapsed inline fold on the same row — its interior is already hidden
299/// in the outer fold's `…` chip, so rendering it separately would double-count the
300/// horizontal collapse (and, on the no-span path, slice a backwards range). Bracket
301/// pairs are laminar, so an inner fold is fully contained
302/// (`outer.open < inner.open && inner.close < outer.close`). The inline analog of
303/// [`root_regions`]: sort outer-before-inner, keep a fold unless the last root
304/// encloses it (the last root is always the tightest enclosing candidate, since a
305/// container and all its contents sort before any disjoint sibling).
306/// Delta-gap encode root-reduced, opener-ascending inline folds into the tree.
307fn build_inline(folds: Vec<InlineFold>) -> SumTree<InlineItem> {
308 let (mut prev_open, mut prev_row) = (0u32, 0u32);
309 let items = folds.into_iter().map(move |f| {
310 debug_assert!(f.open >= prev_open && f.row >= prev_row, "inline folds ascend");
311 let it = InlineItem { open_gap: f.open - prev_open, width: f.close - f.open, row_gap: f.row - prev_row };
312 prev_open = f.open;
313 prev_row = f.row;
314 it
315 });
316 SumTree::from_items(items)
317}
318
319fn root_inline(mut inline: Vec<InlineFold>) -> Vec<InlineFold> {
320 inline.sort_by(|a, b| a.open.cmp(&b.open).then(b.close.cmp(&a.close)));
321 let mut roots: Vec<InlineFold> = Vec::new();
322 for f in inline {
323 if roots.last().is_none_or(|r| !(r.row == f.row && r.open < f.open && f.close < r.close)) {
324 roots.push(f);
325 }
326 }
327 roots
328}
329
330/// A visible display row, resolved back to its buffer row — the render driver.
331#[derive(Clone, Copy, PartialEq, Eq, Debug)]
332pub struct VisibleRow {
333 /// The row's index in display space.
334 pub display_row: DisplayRow,
335 /// The buffer row it renders.
336 pub buffer_row: BufferRow,
337 /// Whether this row is a fold header (draw a chevron + `…` chip).
338 pub is_fold_header: bool,
339 /// On a header, the last folded row — `Some(last)` ⇒ this fold is collapsed.
340 pub last_folded: Option<BufferRow>,
341}
342
343/// One resolved block fold on the delta-gap region tree. `vgap` = this region's
344/// header row minus the PREVIOUS region's `last` (the visible rows between them;
345/// ≥ 1 for every region after the first, since roots are disjoint — 0 only for a
346/// fold headed on row 0). `span` = `last − header` (the hidden interior rows, ≥ 1
347/// for a block). Absolute header/last are prefix sums, so a line insert shifts a
348/// whole suffix by reanchoring ONE seam gap.
349#[derive(Clone, Copy, Debug)]
350struct Region {
351 vgap: u32,
352 span: u32,
353}
354
355/// Relative region summary. `vgap_sum` accumulates to a header's display row
356/// (`Σ vgap = header − hidden_before`), `span_sum` to hidden rows, and their sum to
357/// the absolute `last` row — the three dimensions the projections seek by.
358#[derive(Clone, Copy, Debug, Default)]
359struct RegionSummary {
360 vgap_sum: u32,
361 span_sum: u32,
362 count: u32,
363}
364
365impl Summary for RegionSummary {
366 fn add_summary(&mut self, o: &Self) {
367 self.vgap_sum += o.vgap_sum;
368 self.span_sum += o.span_sum;
369 self.count += o.count;
370 }
371}
372
373impl Item for Region {
374 type Summary = RegionSummary;
375 fn summary(&self) -> RegionSummary {
376 RegionSummary { vgap_sum: self.vgap, span_sum: self.span, count: 1 }
377 }
378}
379
380/// Absolute `last` row — `Σ (vgap + span)`; the cumulative end after region `i` is
381/// `last_i`, so `seek(LastDim(r))` finds the first region whose `last > r`.
382#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
383struct LastDim(u32);
384impl Dimension<RegionSummary> for LastDim {
385 fn add_summary(&mut self, s: &RegionSummary) {
386 self.0 += s.vgap_sum + s.span_sum;
387 }
388}
389
390/// A header's display row — `Σ vgap`; the cumulative end after region `i` is
391/// `header_display[i]` (strictly increasing), so `summary_before(DispDim(d)).count`
392/// = the headers displayed strictly above `d`.
393#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
394struct DispDim(u32);
395impl Dimension<RegionSummary> for DispDim {
396 fn add_summary(&mut self, s: &RegionSummary) {
397 self.0 += s.vgap_sum;
398 }
399}
400
401/// Hidden rows — `Σ span`; accumulated before region `i` it is `hidden_before[i]`.
402#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
403struct HiddenDim(u32);
404impl Dimension<RegionSummary> for HiddenDim {
405 fn add_summary(&mut self, s: &RegionSummary) {
406 self.0 += s.span_sum;
407 }
408}
409
410/// Region index.
411#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
412struct RCount(u32);
413impl Dimension<RegionSummary> for RCount {
414 fn add_summary(&mut self, s: &RegionSummary) {
415 self.0 += s.count;
416 }
417}
418
419/// A located region decoded to absolute rows plus the rows hidden before it — the
420/// shared result of the buffer-row → region seek (see [`FoldMap::locate`]).
421struct Located {
422 header: u32,
423 last: u32,
424 span: u32,
425 hidden_before: u32,
426}
427
428/// Delta-gap inline (single-line) fold leaf, on the same contract as [`Region`].
429/// `open_gap` = open − previous fold's open, `width` = close − open, `row_gap` =
430/// row − previous fold's row. Absolute open/row are prefix sums; close = open +
431/// width. So a text edit shifts the whole inline set by reanchoring one seam.
432#[derive(Clone, Copy, Debug)]
433struct InlineItem {
434 open_gap: u32,
435 width: u32,
436 row_gap: u32,
437}
438
439#[derive(Clone, Copy, Debug, Default)]
440struct InlineSummary {
441 open_span: u32,
442 row_span: u32,
443 count: u32,
444}
445
446impl Summary for InlineSummary {
447 fn add_summary(&mut self, o: &Self) {
448 self.open_span += o.open_span;
449 self.row_span += o.row_span;
450 self.count += o.count;
451 }
452}
453
454impl Item for InlineItem {
455 type Summary = InlineSummary;
456 fn summary(&self) -> InlineSummary {
457 InlineSummary { open_span: self.open_gap, row_span: self.row_gap, count: 1 }
458 }
459}
460
461/// Combined open-offset / buffer-row dimension: accumulates BOTH so a single seek
462/// or visit yields a fold's `open` and `row` together (one dimension slot). Ordered
463/// by `(open, row)` — open dominates, and rows rise monotonically with opens, so it
464/// is a total order in document order, and `summary_before(OpenRow { open: x, .. })`
465/// counts the folds with `open < x`.
466#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
467struct OpenRow {
468 open: u32,
469 row: u32,
470}
471impl Dimension<InlineSummary> for OpenRow {
472 fn add_summary(&mut self, s: &InlineSummary) {
473 self.open += s.open_span;
474 self.row += s.row_span;
475 }
476}
477
478/// Inline fold index.
479#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
480struct ICount(u32);
481impl Dimension<InlineSummary> for ICount {
482 fn add_summary(&mut self, s: &InlineSummary) {
483 self.0 += s.count;
484 }
485}
486
487/// A fold-aware row converter — buffer rows ↔ display rows with folded interiors
488/// hidden. Resolved from a [`FoldSet`] + the live [`Brackets`] + buffer,
489/// **memoized** on the [`Document`](crate::Document). A fold add/remove
490/// ([`FoldSet::generation`] bump) rebuilds it from scratch; every text edit shifts
491/// it in place through the patch ([`Self::apply_patch`]) — a no-line-change edit
492/// only moves inline offsets, and a line insert/remove reanchors the region tree's
493/// affected seam — so plain typing over a document-scale fold set never pays an
494/// O(folds) rebuild. Manual `PartialEq` (decoded regions) so a test can deep-equal
495/// the incrementally-shifted map against a fresh build (the drift oracle).
496#[derive(Debug)]
497pub struct FoldMap {
498 /// Resolved *root* block folds (sorted, disjoint by header) on a delta-gap
499 /// [`SumTree`] keyed by header row: a line insert/remove shifts a whole suffix
500 /// in O(log n) by reanchoring one seam gap, and every row projection is an
501 /// O(log n) cursor descent, not an O(folds) full rebuild.
502 regions: SumTree<Region>,
503 /// Resolved inline (single-line) folds on a delta-gap [`SumTree`], keyed by
504 /// opener offset (hence by row), so a text edit reanchors one seam in O(log n)
505 /// rather than an O(inline) rebase, and offset/row queries are O(log n).
506 inline: SumTree<InlineItem>,
507 buffer_row_count: u32,
508}
509
510impl PartialEq for FoldMap {
511 fn eq(&self, other: &Self) -> bool {
512 self.buffer_row_count == other.buffer_row_count
513 && self.decoded_regions() == other.decoded_regions()
514 && self.decoded_inline() == other.decoded_inline()
515 }
516}
517impl Eq for FoldMap {}
518
519impl FoldMap {
520 /// Resolve the collapsed openers into visible block-fold regions against the
521 /// live `brackets` + `buffer`. `O(folds log brackets)`. An opener that is no
522 /// longer a matched bracket, or whose pair is single-line, yields no block
523 /// region (the latter is a future inline fold, not a row-hider).
524 #[must_use]
525 pub fn new(folds: &FoldSet, brackets: &Brackets, buffer: &Buffer) -> Self {
526 #[cfg(any(test, debug_assertions))]
527 FOLD_BUILDS.with(|c| c.set(c.get() + 1));
528 let mut regions = Vec::new();
529 let mut inline = Vec::new();
530 for open in folds.iter() {
531 // `foldable_partner`, not `at().partner`: the depth-free lookup, so a
532 // rebuild over many folds never allocates a prefix stack per opener.
533 let Some(close) = brackets.foldable_partner(open) else { continue };
534 let header = buffer.offset_to_point(open).row;
535 let last = buffer.offset_to_point(close).row;
536 if last > header {
537 regions.push(FoldedRegion { header, last }); // multi-line ⇒ block
538 } else if crate::row_layout::pair_has_interior(open, close) {
539 inline.push(InlineFold { row: header, open, close }); // single-line ⇒ inline
540 }
541 }
542 Self::assemble(regions, inline, buffer.line_count())
543 }
544
545 /// Shift this map through a committed edit's `patch` IN PLACE, returning
546 /// `false` (→ the caller rebuilds instead) when the edit changed the line
547 /// count. A line insertion/removal moves block-region rows and the display
548 /// prefix sums, which is not a cheap in-place update; a **no-line-change**
549 /// edit (plain typing) leaves every block region, prefix sum, and the buffer
550 /// row count unchanged and only moves inline-fold offsets — so this is
551 /// O(inline folds), and O(0) for the common block-only "collapse all" case,
552 /// far cheaper than the O(folds) rebuild the per-keystroke edit path would
553 /// otherwise pay.
554 ///
555 /// A fold whose *interior* an edit touches is expanded (removed) before this
556 /// runs, so a surviving inline fold's interior is intact — only its (and its
557 /// `close`'s) offset shifts, exactly as [`FoldSet::apply_patch`] shifts the
558 /// opener, keeping the two in lockstep. Fold add/remove bumps the generation
559 /// and forces a rebuild, so this never has to reconcile the fold set.
560 #[must_use]
561 pub fn apply_patch(&mut self, patch: &Patch, buffer: &Buffer) -> bool {
562 let new_rows = buffer.line_count();
563 let d = i64::from(new_rows) - i64::from(self.buffer_row_count);
564 // A line change moves block-region rows. A SINGLE edit shifts every region
565 // (and inline fold) at/after the insertion row by `+d` — the region tree in
566 // O(log n) via one reanchored seam, inline offsets/rows in the loop below.
567 // A MULTI-edit line change is a piecewise row shift (multi-caret, rare) —
568 // rebuild. `os` bounds the inline row shift to the folds after the edit.
569 if d != 0 {
570 let [edit] = patch.edits() else { return false };
571 let (os, oe) = (edit.old.start, edit.old.end);
572 // A line change overlapping an inline fold's span can RECLASSIFY it
573 // (single-line ↔ multi-line block) — a shift can't express that, and
574 // `expand` doesn't catch an inline fold's byte interior, so rebuild.
575 if self.inline_overlaps(os, oe) {
576 return false;
577 }
578 // The edit's OLD range covered rows `[r_os, r_oe_old]`; content ending
579 // after `r_os` shifts by `d`. `r_oe_old = new-end row − d` recovers the
580 // old-end row without the old buffer (the net line delta `d` is the
581 // newlines the range gained). For an insertion `r_oe_old == r_os`.
582 let r_os = buffer.offset_to_point(edit.new.start).row;
583 let r_oe_old = (i64::from(buffer.offset_to_point(edit.new.end).row) - d) as u32;
584 // Regions that DON'T shift end at/before `r_os` (`last ≤ r_os`): a fold
585 // tailed on `r_os` has its opener earlier, so it is before the edit.
586 let k = self.regions.summary_before(&LastDim(r_os.saturating_add(1))).count;
587 let (before, suffix) = self.regions.split_at(&RCount(k));
588 if !suffix.is_empty() {
589 let last_prev = before.extent::<LastDim>().0; // last of the region before the seam
590 let (first, ..) = suffix.seek::<RCount, LastDim>(&RCount(0)).expect("non-empty");
591 let header = last_prev + first.vgap;
592 // If the edit's OLD range reached INTO the first shifted region
593 // (`header < r_oe_old`), a deletion shrank its span — `expand` misses
594 // that (it checks only the collapsed NEW endpoint, which can land on
595 // a still-visible row), and a rigid row shift can't express a span
596 // change — so rebuild. Later regions have larger headers, so probing
597 // the first suffices. (An insertion has `r_oe_old == r_os ≤ header`,
598 // so this never trips — surviving folds never straddle an insertion.)
599 if header < r_oe_old {
600 return false;
601 }
602 // Reanchor: the first shifted region's `vgap` absorbs `+d` (its
603 // header moves while its predecessor, in `before`, stayed); later
604 // gaps/spans are relative and unchanged. The straddle guard makes
605 // the new header ≥ r_os ≥ the predecessor's last, so `vgap` stays ≥ 0.
606 let new_vgap = i64::from(first.vgap) + d;
607 debug_assert!(new_vgap >= 0, "a non-straddling shift keeps vgap ≥ 0");
608 let fixed = Region { vgap: new_vgap as u32, span: first.span };
609 let suffix = suffix.replace(RCount(0)..RCount(1), std::iter::once(fixed));
610 self.regions = before.append(&suffix);
611 }
612 self.buffer_row_count = new_rows;
613 }
614 self.shift_inline(patch, d);
615 true
616 }
617
618 /// Shift the inline-fold tree through `patch`. A single edit whose range
619 /// overlaps no surviving inline fold reanchors ONE seam in O(log n) — folds
620 /// after the edit move `open += byte_delta`, `row += d`. Otherwise (a straddling
621 /// edit that changes a fold's width, or a multi-edit) it rides each fold through
622 /// the patch and rebuilds — O(inline), the rare fallback. A line-changing
623 /// straddle already forced a full rebuild upstream, so the fallback is `d == 0`.
624 fn shift_inline(&mut self, patch: &Patch, d: i64) {
625 if let [edit] = patch.edits() {
626 let (os, oe) = (edit.old.start, edit.old.end);
627 if !self.inline_overlaps(os, oe) {
628 let byte_delta = (i64::from(edit.new.end) - i64::from(edit.new.start))
629 - (i64::from(oe) - i64::from(os));
630 let k = self.inline.summary_before(&OpenRow { open: os, row: 0 }).count; // open < os
631 let (before, suffix) = self.inline.split_at(&ICount(k));
632 if !suffix.is_empty() {
633 let (it, ..) = suffix.seek::<ICount, OpenRow>(&ICount(0)).expect("non-empty");
634 let fixed = InlineItem {
635 open_gap: (i64::from(it.open_gap) + byte_delta) as u32,
636 width: it.width,
637 row_gap: (i64::from(it.row_gap) + d) as u32,
638 };
639 let suffix = suffix.replace(ICount(0)..ICount(1), std::iter::once(fixed));
640 self.inline = before.append(&suffix);
641 }
642 return;
643 }
644 }
645 debug_assert!(d == 0, "the inline fallback is only reached on a no-line-change edit");
646 let mut v = self.decoded_inline();
647 for f in &mut v {
648 f.open = patch.map_offset(f.open, Bias::Right);
649 f.close = patch.map_offset(f.close, Bias::Right);
650 }
651 self.inline = build_inline(v);
652 }
653
654 /// Reduce raw folds to roots and build the O(log) search prefixes (the
655 /// hidden-row prefix sum and each header's display row). The one owner of the
656 /// [`FoldMap`] invariants, shared by [`Self::new`] and the test constructor.
657 fn assemble(regions: Vec<FoldedRegion>, inline: Vec<InlineFold>, buffer_row_count: u32) -> Self {
658 let regions = root_regions(regions); // sorted, disjoint by header
659 // Delta-gap encode: vgap = header − previous region's last (visible rows
660 // between them), span = last − header (hidden interior rows).
661 let mut prev_last = 0u32;
662 let mut items = Vec::with_capacity(regions.len());
663 for r in ®ions {
664 items.push(Region { vgap: r.header - prev_last, span: r.last - r.header });
665 prev_last = r.last;
666 }
667 Self {
668 regions: SumTree::from_items(items),
669 inline: build_inline(root_inline(inline)),
670 buffer_row_count,
671 }
672 }
673
674 /// Decode the region tree back to absolute `(header, last)` pairs — the O(n)
675 /// reduce for `PartialEq` (the drift oracle) and tests.
676 fn decoded_regions(&self) -> Vec<(u32, u32)> {
677 let mut out = Vec::with_capacity(self.regions.summary().count as usize);
678 let mut last = 0u32;
679 for item in self.regions.items() {
680 let header = last + item.vgap;
681 last = header + item.span;
682 out.push((header, last));
683 }
684 out
685 }
686
687 /// Decode the inline-fold tree to absolute [`InlineFold`]s, ascending by opener
688 /// — the O(n) reduce for `PartialEq`, [`Self::inline_folds`], and tests.
689 fn decoded_inline(&self) -> Vec<InlineFold> {
690 let mut out = Vec::with_capacity(self.inline.summary().count as usize);
691 let (mut open, mut row) = (0u32, 0u32);
692 for it in self.inline.items() {
693 crate::perf::charge(1); // complexity gate: whole inline-set decode is O(F)
694 open += it.open_gap;
695 row += it.row_gap;
696 out.push(InlineFold { row, open, close: open + it.width });
697 }
698 out
699 }
700
701 /// The last inline fold opening strictly before `off` (the only one that can
702 /// hide `off`, since roots are disjoint) — O(log n). The offset-keyed lookup
703 /// movement / hit-testing / the caret-eject path use.
704 #[must_use]
705 pub fn inline_fold_before(&self, off: u32) -> Option<InlineFold> {
706 let j = self.inline.summary_before(&OpenRow { open: off, row: 0 }).count; // open < off
707 let (it, _c, OpenRow { open: ob, row: rb }) =
708 self.inline.seek::<ICount, OpenRow>(&ICount(j.checked_sub(1)?))?;
709 let open = ob + it.open_gap;
710 Some(InlineFold { row: rb + it.row_gap, open, close: open + it.width })
711 }
712
713 /// The root inline fold opening *exactly* at `opener`, or `None` — O(log n)
714 /// membership for the collapsed-chip hit-test. Roots are disjoint and
715 /// open-sorted, so [`Self::inline_fold_before`]`(opener+1)` returns the last
716 /// root with `open ≤ opener`; testing `open == opener` is then exact root
717 /// membership. `saturating_add` guards the u32 edge; a nested (non-root)
718 /// opener resolves to its enclosing root, whose `open != opener` → `None` —
719 /// exactly what a linear `inline_folds().binary_search` membership test yields.
720 #[must_use]
721 pub fn inline_fold_at(&self, opener: u32) -> Option<InlineFold> {
722 self.inline_fold_before(opener.saturating_add(1)).filter(|f| f.open == opener)
723 }
724
725 /// Whether any inline fold's `[open, close]` overlaps `[os, oe]` — O(log n). One
726 /// probe suffices: the fold with the greatest `open ≤ oe` has the greatest
727 /// `close` (disjoint, open-sorted ⇒ close ascending).
728 fn inline_overlaps(&self, os: u32, oe: u32) -> bool {
729 let j = self.inline.summary_before(&OpenRow { open: oe.saturating_add(1), row: 0 }).count;
730 let Some(k) = j.checked_sub(1) else { return false };
731 let Some((it, _c, OpenRow { open: ob, .. })) = self.inline.seek::<ICount, OpenRow>(&ICount(k))
732 else {
733 return false;
734 };
735 ob + it.open_gap + it.width >= os // close ≥ os
736 }
737
738 /// The region whose `[last_prev, last]` brackets buffer row `r` — the first with
739 /// `last > r`, or the last region as a fallback (then `r` is beyond every fold) —
740 /// decoded to absolute rows plus the hidden rows before it. `None` iff no folds.
741 /// The shared O(log n) descent under every buffer-row → display query.
742 fn locate(&self, r: u32) -> Option<Located> {
743 let (item, LastDim(last_prev), HiddenDim(hidden_before)) =
744 self.regions.seek::<LastDim, HiddenDim>(&LastDim(r))?;
745 let header = last_prev + item.vgap;
746 Some(Located { header, last: header + item.span, span: item.span, hidden_before })
747 }
748
749 /// Tail-inclusive [`Self::locate`]: the region with `last >= r` (via
750 /// `seek(last > r − 1)`), so a fold's TAIL row (`last == r`) resolves to ITS
751 /// fold, not the next one below it. `locate`'s `last > r` is right for the
752 /// display projections but off-by-one for containment (`is_folded` /
753 /// `fold_containing`), where the hidden tail row must still count as folded.
754 fn containing(&self, r: u32) -> Option<Located> {
755 let (item, LastDim(last_prev), HiddenDim(hidden_before)) =
756 self.regions.seek::<LastDim, HiddenDim>(&LastDim(r.saturating_sub(1)))?;
757 let header = last_prev + item.vgap;
758 Some(Located { header, last: header + item.span, span: item.span, hidden_before })
759 }
760
761 /// An empty (nothing-folded) map — the document's cache seed before its first
762 /// real build.
763 #[must_use]
764 pub(crate) fn empty() -> Self {
765 Self::assemble(Vec::new(), Vec::new(), 0)
766 }
767
768 /// The resolved inline (single-line) folds, sorted by opener offset — the
769 /// render / caret / hit-test consult these to collapse a mid-line span.
770 #[must_use]
771 pub fn inline_folds(&self) -> Vec<InlineFold> {
772 self.decoded_inline()
773 }
774
775 /// The inline folds on buffer `row`, ascending by opener — the per-row render
776 /// query (`RowLayout`). O(log n + hits): a row's folds are a contiguous window.
777 #[must_use]
778 pub fn inline_folds_on_row(&self, row: u32) -> Vec<InlineFold> {
779 let mut out = Vec::new();
780 self.inline.filter_visit::<OpenRow, _, _>(
781 &|before: &OpenRow, sum: &InlineSummary| {
782 before.row <= row && before.row + sum.row_span >= row
783 },
784 &mut |it: &InlineItem, before: &OpenRow| {
785 let r = before.row + it.row_gap;
786 if r == row {
787 let open = before.open + it.open_gap;
788 out.push(InlineFold { row: r, open, close: open + it.width });
789 }
790 },
791 );
792 out
793 }
794
795 /// Total hidden rows across all root folds (the tree root summary, O(1)).
796 fn hidden_total(&self) -> u32 {
797 self.regions.summary().span_sum
798 }
799
800 /// Number of display rows (buffer rows minus every hidden interior row).
801 #[must_use]
802 pub fn display_row_count(&self) -> u32 {
803 self.buffer_row_count - self.hidden_total()
804 }
805
806 /// The last valid display row.
807 #[must_use]
808 pub fn max_display_row(&self) -> DisplayRow {
809 DisplayRow(self.display_row_count().saturating_sub(1))
810 }
811
812 /// Buffer row → display row. A hidden interior row clips to its header's
813 /// display row, so a collapsed fold's interior renders at the header line.
814 #[must_use]
815 pub fn to_display_row(&self, row: BufferRow) -> DisplayRow {
816 let r = row.0;
817 let Some(l) = self.locate(r) else { return DisplayRow(r) };
818 if r > l.header && r <= l.last {
819 DisplayRow(l.header - l.hidden_before) // interior → clip to the header's display row
820 } else if r <= l.header {
821 DisplayRow(r - l.hidden_before) // visible, at/before this region's header
822 } else {
823 DisplayRow(r - l.hidden_before - l.span) // r beyond the last fold (seek fallback)
824 }
825 }
826
827 /// Display row → buffer row. Always a visible row (a header or an unfolded
828 /// line), never a hidden interior.
829 #[must_use]
830 pub fn to_buffer_row(&self, row: DisplayRow) -> BufferRow {
831 let d = row.0;
832 // Regions whose header displays strictly above `d` are hidden-above; add
833 // their hidden rows back. `summary_before(DispDim(d))` = exactly those
834 // (header_display[i] < d), `.span_sum` = their hidden rows (= hidden_before[j]).
835 BufferRow(d + self.regions.summary_before(&DispDim(d)).span_sum)
836 }
837
838 /// Whether `row` is a hidden interior row of some fold.
839 #[must_use]
840 pub fn is_folded(&self, row: BufferRow) -> bool {
841 let r = row.0;
842 self.containing(r).is_some_and(|l| l.header < r && r <= l.last)
843 }
844
845 /// If `offset` is HIDDEN inside a collapsed fold, the caret's eject target — a
846 /// block's header line end, or an inline gap's left landable edge; `None` when
847 /// visible. `O(log folds)` (regions and inline roots are sorted and disjoint),
848 /// so a multi-caret fold pulls every hidden caret out in one `O(carets·log
849 /// folds)` pass instead of an `O(carets·folds)` [`Self::display_position`]
850 /// probe each — the "collapse all" ejection at document scale.
851 #[must_use]
852 pub fn entry_edge_if_hidden(&self, buffer: &Buffer, offset: u32) -> Option<u32> {
853 // Block: is `offset`'s row STRICTLY inside a region's hidden interior?
854 // (The tail row `last` rides the header and stays visible, so `< last`.)
855 let row = buffer.offset_to_point(offset).row;
856 if let Some(l) = self.locate(row) {
857 if l.header < row && row < l.last {
858 let end = buffer.line_len(l.header);
859 return Some(buffer.point_to_offset(Point::new(l.header, end)));
860 }
861 }
862 // Inline: is `offset` strictly inside a collapsed single-line gap? Only the
863 // fold opening just before it can hide it (roots are disjoint).
864 if let Some(f) = self.inline_fold_before(offset) {
865 if f.hides_caret_at(offset) {
866 return Some(f.left_edge());
867 }
868 }
869 None
870 }
871
872 /// The collapsed root fold whose hidden interior contains `row` (i.e.
873 /// `header < row <= last`), as `(header, last)`. `None` if `row` is a header
874 /// or not folded. Used by fold-aware horizontal movement to hop the collapsed
875 /// gap between the header line and the inline closing tail.
876 #[must_use]
877 pub fn fold_containing(&self, row: BufferRow) -> Option<(BufferRow, BufferRow)> {
878 let r = row.0;
879 self.containing(r)
880 .filter(|l| l.header < r && r <= l.last)
881 .map(|l| (BufferRow(l.header), BufferRow(l.last)))
882 }
883
884 /// If `row` is a folded header, the fold's last row (for the chip/chevron and
885 /// placeholder). `None` if `row` is not a header of a collapsed fold.
886 #[must_use]
887 pub fn fold_at_header(&self, row: BufferRow) -> Option<BufferRow> {
888 // The region bracketing `row` is the one headed there iff its header == row
889 // (a header's own `last > row`, and the prior region's `last < row`).
890 self.locate(row.0).filter(|l| l.header == row.0).map(|l| BufferRow(l.last))
891 }
892
893 /// If `row` is the *last* (closing) row of a collapsed root fold, that fold's
894 /// header row — the display anchor for the fold's inline closing tail.
895 /// `None` otherwise. Roots are disjoint, so at most one fold ends on `row`.
896 /// The inverse direction of [`Self::fold_at_header`], for placing a caret /
897 /// hit-testing a click on the collapsed line's real closing bracket.
898 #[must_use]
899 pub fn header_of_tail(&self, row: BufferRow) -> Option<BufferRow> {
900 // Roots are disjoint ⇒ `last` is strictly increasing. `j` regions end before
901 // `row`; the j-th (first with `last >= row`) is tailed on `row` iff its
902 // `last == row`, and then its header is `last − span`.
903 let r = row.0;
904 let j = self.regions.summary_before(&LastDim(r)).count;
905 let (item, RCount(_), LastDim(last_prev)) = self.regions.seek::<RCount, LastDim>(&RCount(j))?;
906 let last = last_prev + item.vgap + item.span;
907 (last == r).then(|| BufferRow(last - item.span))
908 }
909
910 /// The display-row window covering fractional rows `[top_rows, bottom_rows)`
911 /// from the content top: floor the first visible row, ceil past the last,
912 /// clamp to the row count. THE one owner of the render window rule — the
913 /// widget derives the fractional rows from pixels ([`Self::display_row_at`]
914 /// is its single-row sibling) and never floors/clamps itself.
915 /// (`f64` like [`Self::display_row_at`]: exact for every u32 row count,
916 /// where `f32` fractional rows degrade past ~2²³.)
917 #[must_use]
918 pub fn display_window(&self, top_rows: f64, bottom_rows: f64) -> core::ops::Range<u32> {
919 let first = top_rows.floor().max(0.0) as u32;
920 let last = (bottom_rows.ceil().max(0.0) as u32).min(self.display_row_count());
921 first..last.max(first)
922 }
923
924 /// The visible display rows in `window` (see [`Self::display_window`]),
925 /// each resolved to its buffer row with the fold-header flag — the render
926 /// loop's driver. Hidden interior rows are simply not produced.
927 pub fn visible_rows(&self, window: core::ops::Range<u32>) -> impl Iterator<Item = VisibleRow> + '_ {
928 window.map(move |d| {
929 let buffer_row = self.to_buffer_row(DisplayRow(d));
930 let last_folded = self.fold_at_header(buffer_row);
931 VisibleRow {
932 display_row: DisplayRow(d),
933 buffer_row,
934 is_fold_header: last_folded.is_some(),
935 last_folded,
936 }
937 })
938 }
939
940 /// Build a `FoldMap` straight from `(header, last)` block regions — the
941 /// row-arithmetic under test, without needing a bracketed buffer.
942 #[cfg(test)]
943 pub(crate) fn from_rows(regions: impl IntoIterator<Item = (u32, u32)>, row_count: u32) -> Self {
944 let regions = regions
945 .into_iter()
946 .filter(|&(h, l)| l > h)
947 .map(|(header, last)| FoldedRegion { header, last })
948 .collect();
949 Self::assemble(regions, Vec::new(), row_count)
950 }
951
952 /// Build a `FoldMap` from `(row, open, close)` inline folds — the inline
953 /// row/offset arithmetic under test, without a bracketed buffer.
954 #[cfg(test)]
955 pub(crate) fn from_inline(
956 folds: impl IntoIterator<Item = (u32, u32, u32)>,
957 row_count: u32,
958 ) -> Self {
959 let inline = folds.into_iter().map(|(row, open, close)| InlineFold { row, open, close }).collect();
960 Self::assemble(Vec::new(), inline, row_count)
961 }
962}
963
964#[cfg(test)]
965mod tests {
966 use super::*;
967 use crate::patch::{Edit, Patch};
968
969 // ── FoldSet: opener-keyed set that rides the patch mover ──
970
971 #[test]
972 fn fold_toggle_and_duplicate() {
973 let mut f = FoldSet::new();
974 assert!(f.fold(10));
975 assert!(!f.fold(10), "same opener already folded");
976 assert!(f.is_folded(10));
977 assert!(f.toggle(10)); // unfolds
978 assert!(!f.is_folded(10));
979 assert!(f.is_empty());
980 }
981
982 #[test]
983 fn apply_patch_shifts_openers() {
984 let mut f = FoldSet::new();
985 f.fold(5);
986 f.fold(20);
987 // Insert 3 bytes at the top → both openers shift right by 3.
988 f.apply_patch(&Patch::single(Edit { old: 0..0, new: 0..3 }));
989 assert_eq!(f.iter().collect::<Vec<_>>(), vec![8, 23]);
990 }
991
992 #[test]
993 fn reconcile_drops_openers_that_are_no_longer_foldable() {
994 let mut f = FoldSet::new();
995 f.fold(8);
996 f.fold(23);
997 // Only offset 8 is still a live foldable open bracket.
998 f.reconcile(|o| o == 8);
999 assert_eq!(f.iter().collect::<Vec<_>>(), vec![8]);
1000 }
1001
1002 #[test]
1003 fn deleting_the_opener_reconciles_away() {
1004 let mut f = FoldSet::new();
1005 f.fold(4);
1006 // Delete the bracket char at [4,5); the opener maps into the deletion.
1007 f.apply_patch(&Patch::single(Edit { old: 4..5, new: 4..4 }));
1008 f.reconcile(|_| false); // nothing foldable there anymore
1009 assert!(f.is_empty());
1010 }
1011
1012 // ── FoldMap: row arithmetic over resolved block regions ──
1013
1014 #[test]
1015 fn empty_is_identity() {
1016 let m = FoldMap::from_rows([], 6);
1017 assert_eq!(m.display_row_count(), 6);
1018 for r in 0..6 {
1019 assert_eq!(m.to_display_row(BufferRow(r)), DisplayRow(r));
1020 assert_eq!(m.to_buffer_row(DisplayRow(r)), BufferRow(r));
1021 assert!(!m.is_folded(BufferRow(r)));
1022 }
1023 }
1024
1025 #[test]
1026 fn hides_interior_rows() {
1027 let m = FoldMap::from_rows([(2, 5)], 10); // hides rows 3,4,5
1028 assert_eq!(m.display_row_count(), 7);
1029 assert!(m.is_folded(BufferRow(3)) && m.is_folded(BufferRow(5)));
1030 assert!(!m.is_folded(BufferRow(2)) && !m.is_folded(BufferRow(6)));
1031 assert_eq!(m.fold_at_header(BufferRow(2)), Some(BufferRow(5)));
1032 }
1033
1034 #[test]
1035 fn display_and_buffer_row_mapping() {
1036 let m = FoldMap::from_rows([(2, 5)], 10);
1037 for r in [3, 4, 5] {
1038 assert_eq!(m.to_display_row(BufferRow(r)), m.to_display_row(BufferRow(2)));
1039 }
1040 assert_eq!(m.to_display_row(BufferRow(6)), DisplayRow(3)); // 6 − 3 hidden
1041 assert_eq!(m.to_buffer_row(DisplayRow(2)), BufferRow(2)); // the header
1042 assert_eq!(m.to_buffer_row(DisplayRow(3)), BufferRow(6)); // interior skipped
1043 assert_eq!(m.to_buffer_row(DisplayRow(6)), BufferRow(9));
1044 }
1045
1046 #[test]
1047 fn roundtrip_visible_rows() {
1048 let m = FoldMap::from_rows([(2, 5)], 10);
1049 let all: Vec<_> = m.visible_rows(0..m.display_row_count()).collect();
1050 assert_eq!(all.iter().map(|v| v.buffer_row.0).collect::<Vec<_>>(), vec![0, 1, 2, 6, 7, 8, 9]);
1051 let header = all.iter().find(|v| v.is_fold_header).unwrap();
1052 assert_eq!((header.buffer_row, header.last_folded), (BufferRow(2), Some(BufferRow(5))));
1053 }
1054
1055 #[test]
1056 fn nested_reduces_to_root() {
1057 let m = FoldMap::from_rows([(1, 8), (3, 5)], 10); // inner (3,5) ⊆ outer (1,8)
1058 assert_eq!(m.display_row_count(), 3); // rows 2..=8 hidden ⇒ visible 0,1,9
1059 assert_eq!(m.fold_at_header(BufferRow(1)), Some(BufferRow(8)));
1060 assert_eq!(m.fold_at_header(BufferRow(3)), None, "inner header is hidden, not a root");
1061 let visible: Vec<_> = m.visible_rows(0..m.display_row_count()).map(|v| v.buffer_row.0).collect();
1062 assert_eq!(visible, vec![0, 1, 9]);
1063 }
1064
1065 #[test]
1066 fn three_level_nesting_reduces_to_root() {
1067 let m = FoldMap::from_rows([(0, 10), (2, 8), (4, 6)], 12);
1068 assert_eq!(m.display_row_count(), 2); // root (0,10) hides 1..=10 ⇒ visible 0,11
1069 assert_eq!(m.to_buffer_row(DisplayRow(1)), BufferRow(11));
1070 }
1071
1072 #[test]
1073 fn disjoint_siblings_both_hide() {
1074 let m = FoldMap::from_rows([(2, 5), (6, 8)], 10);
1075 assert_eq!(m.display_row_count(), 10 - 3 - 2);
1076 assert_eq!(m.fold_at_header(BufferRow(2)), Some(BufferRow(5)));
1077 assert_eq!(m.fold_at_header(BufferRow(6)), Some(BufferRow(8)));
1078 }
1079
1080 #[test]
1081 fn fold_at_eof() {
1082 let m = FoldMap::from_rows([(3, 5)], 6);
1083 assert_eq!(m.display_row_count(), 4); // rows 4,5 hidden
1084 assert_eq!(m.fold_at_header(BufferRow(3)), Some(BufferRow(5)));
1085 }
1086
1087 #[test]
1088 fn header_of_tail_is_the_inverse_of_fold_at_header() {
1089 let m = FoldMap::from_rows([(2, 5)], 10);
1090 assert_eq!(m.fold_at_header(BufferRow(2)), Some(BufferRow(5)));
1091 assert_eq!(m.header_of_tail(BufferRow(5)), Some(BufferRow(2)));
1092 assert_eq!(m.header_of_tail(BufferRow(2)), None, "the header is not a tail");
1093 assert_eq!(m.header_of_tail(BufferRow(4)), None, "an interior row is not the last");
1094 }
1095
1096 #[test]
1097 fn fold_containing_finds_the_enclosing_root() {
1098 let m = FoldMap::from_rows([(2, 5)], 10);
1099 assert_eq!(m.fold_containing(BufferRow(4)), Some((BufferRow(2), BufferRow(5))));
1100 assert_eq!(m.fold_containing(BufferRow(2)), None, "the header is visible, not contained");
1101 assert_eq!(m.fold_containing(BufferRow(6)), None);
1102 }
1103
1104 #[test]
1105 fn inline_shift_is_sublinear_in_fold_count() {
1106 use crate::sum_tree::NODE_ALLOCS;
1107 // N inline folds, one per row. A single front insert shifts ALL of them
1108 // (open += 1) — one reanchored delta-gap seam, O(log N) node allocs, not the
1109 // O(N) rebuild the flat Vec (or the fallback) would do. A `d == 0` edit keeps
1110 // it on the inline path only (no block regions, no offset_to_point).
1111 let allocs_for = |n: u32| -> f64 {
1112 let mut m = FoldMap::from_inline((0..n).map(|i| (i, i * 10 + 100, i * 10 + 103)), n);
1113 let buf = Buffer::new(&"\n".repeat((n - 1) as usize)).unwrap(); // line_count == n
1114 let patch = Patch::single(Edit { old: 5..5, new: 5..6 }); // insert 1 byte, no newline
1115 NODE_ALLOCS.with(|c| c.set(0));
1116 let _ = m.apply_patch(&patch, &buf);
1117 NODE_ALLOCS.with(std::cell::Cell::get) as f64
1118 };
1119 let (small, big) = (allocs_for(1000), allocs_for(4000));
1120 eprintln!("[fold_map] inline shift node allocs {small} -> {big} ({:.2}x)", big / small);
1121 assert!(
1122 big <= small * 2.0,
1123 "inline shift allocates superlinearly ({small} -> {big} nodes): it rebuilt the \
1124 whole inline set instead of reanchoring the suffix at one seam"
1125 );
1126 }
1127
1128 #[test]
1129 fn a_hidden_tail_row_counts_as_folded_with_a_fold_below() {
1130 // `is_folded`/`fold_containing` use the TAIL-inclusive lookup so a fold's
1131 // hidden tail row still counts as folded even when a second root sits
1132 // directly below it. Row 5 is fold (2,5)'s hidden tail; root (6,8) follows.
1133 // A plain `last > r` lookup would resolve row 5 to (6,8) and report it visible.
1134 let m = FoldMap::from_rows([(2, 5), (6, 8)], 10);
1135 assert!(m.is_folded(BufferRow(5)), "a fold's tail row is hidden");
1136 assert_eq!(m.fold_containing(BufferRow(5)), Some((BufferRow(2), BufferRow(5))));
1137 // The display projection agrees — row 5 collapses onto the header (row 2).
1138 assert_eq!(m.to_display_row(BufferRow(5)), m.to_display_row(BufferRow(2)));
1139 assert!(!m.is_folded(BufferRow(2)), "the header is visible");
1140 assert!(m.is_folded(BufferRow(7)) && m.is_folded(BufferRow(8)), "second fold hidden");
1141 }
1142
1143 // ── inline root reduction: a collapsed inline pair nested in another ──
1144
1145 #[test]
1146 fn nested_inline_folds_reduce_to_the_outer_root() {
1147 // One row: `( … )` at [4,30] with `[ … ]` at [20,28] nested inside it.
1148 let roots = root_inline(vec![
1149 InlineFold { row: 0, open: 20, close: 28 },
1150 InlineFold { row: 0, open: 4, close: 30 },
1151 ]);
1152 assert_eq!(roots.iter().map(|f| f.open).collect::<Vec<_>>(), vec![4], "only the outer chip renders");
1153 }
1154
1155 #[test]
1156 fn disjoint_inline_folds_both_survive() {
1157 let roots = root_inline(vec![
1158 InlineFold { row: 0, open: 12, close: 18 },
1159 InlineFold { row: 0, open: 4, close: 8 },
1160 ]);
1161 assert_eq!(roots.iter().map(|f| f.open).collect::<Vec<_>>(), vec![4, 12]);
1162 }
1163
1164 #[test]
1165 fn triple_nested_inline_reduces_to_root() {
1166 // A(0..40) ⊃ B(5..30) ⊃ C(10..20), all on row 0 → only A survives.
1167 let roots = root_inline(vec![
1168 InlineFold { row: 0, open: 10, close: 20 },
1169 InlineFold { row: 0, open: 5, close: 30 },
1170 InlineFold { row: 0, open: 0, close: 40 },
1171 ]);
1172 assert_eq!(roots.iter().map(|f| f.open).collect::<Vec<_>>(), vec![0]);
1173 }
1174
1175 // ── the collapsed-chip inline-membership hit-test ───────────────────────────
1176
1177 #[test]
1178 fn inline_fold_at_matches_the_linear_membership() {
1179 // Parity oracle: over random inline-fold sets (disjoint + nested, several
1180 // rows) the O(log F) offset-keyed `inline_fold_at` descent must agree with the
1181 // linear `inline_folds()` membership scan — presence bit AND resolved fold —
1182 // at EVERY candidate offset (root openers, nested/reduced openers, non-openers).
1183 // This pins the `+1` and the `f.open == opener` filter (drop either and a
1184 // root/non-opener boundary diverges here). Row bands (open = row*100 + …) keep
1185 // opener-ascending ⟺ row-ascending, the real-buffer invariant `from_inline`
1186 // asserts; openers are deduped because a bracket char occupies one offset.
1187 let mut seed = 0x9E37_79B9_7F4A_7C15u64;
1188 let mut rng = || {
1189 seed ^= seed << 13;
1190 seed ^= seed >> 7;
1191 seed ^= seed << 17;
1192 seed
1193 };
1194 for _ in 0..300 {
1195 let n = (rng() % 9) as usize;
1196 let mut raw: Vec<(u32, u32, u32)> = Vec::new();
1197 for _ in 0..n {
1198 let row = (rng() % 4) as u32;
1199 let open = row * 100 + (rng() % 80) as u32;
1200 let width = 1 + (rng() % 18) as u32;
1201 raw.push((row, open, open + width));
1202 }
1203 raw.sort_unstable();
1204 raw.dedup_by_key(|&mut (_, o, _)| o); // unique openers, as real brackets are
1205 let m = FoldMap::from_inline(raw.iter().copied(), 4);
1206 let roots = m.inline_folds();
1207 for o in 0..=400u32 {
1208 let linear = roots.iter().find(|f| f.open == o).copied();
1209 let via_log = m.inline_fold_at(o);
1210 assert_eq!(
1211 via_log, linear,
1212 "inline_fold_at({o}) = {via_log:?} but linear membership = {linear:?}\nroots = {roots:?}"
1213 );
1214 }
1215 // The u32 edge: `saturating_add(1)` must not panic or false-match at MAX.
1216 assert_eq!(m.inline_fold_at(u32::MAX), roots.iter().find(|f| f.open == u32::MAX).copied());
1217 }
1218 }
1219
1220 #[test]
1221 fn inline_membership_is_fold_count_independent() {
1222 // `inline_fold_at` is an O(log F) offset-keyed descent, so its work meter
1223 // must stay flat as the inline-fold count doubles. A membership test that
1224 // decoded every inline leaf (`inline_folds().binary_search`) would charge F
1225 // per item in `decoded_inline`; the `sum_tree` seek under `inline_fold_before`
1226 // is unmetered, so the O(log) path charges ~0. One disjoint fold per row;
1227 // probe the middle opener (a real opener, so both the seek and a whole-set
1228 // scan do their full work).
1229 let meter_for = |f: u32| -> u64 {
1230 let m = FoldMap::from_inline((0..f).map(|i| (i, i * 10 + 100, i * 10 + 104)), f);
1231 let mid = (f / 2) * 10 + 100;
1232 crate::perf::reset();
1233 let _ = m.inline_fold_at(mid);
1234 crate::perf::meter()
1235 };
1236 let (small, big) = (meter_for(1000), meter_for(2000));
1237 eprintln!("[fold_map] inline membership meter {small} -> {big}");
1238 assert!(
1239 big <= small + small / 4 + 256,
1240 "inline_fold_at charged {small} -> {big}: membership decoded the whole inline set \
1241 instead of an O(log F) offset-keyed descent"
1242 );
1243 }
1244}