Skip to main content

hjkl_buffer/
folds.rs

1//! Manual folds: contiguous row ranges that the host can collapse
2//! to a single visible "fold marker" line.
3//!
4//! Phase 9 of the migration plan unlocks this — vim users get
5//! `zo`/`zc`/`za`/`zR`/`zM` over the same buffer the editor is
6//! mutating, no separate fold tracker required.
7//!
8//! ## Fold semantics
9//!
10//! Folds are **row-range** spans, not byte spans. [`Fold`] covers
11//! `[start_row, end_row]` inclusive. The host renders folds as collapsed
12//! single-line stubs; the buffer never elides them on its own —
13//! [`crate::View::lines`] always returns the underlying logical text.
14//!
15//! Add / remove / toggle goes through
16//! [`crate::View::add_fold`] / [`crate::View::remove_fold_at`] /
17//! [`crate::View::toggle_fold_at`]. Open-all / close-all (`zR` / `zM`)
18//! go through [`crate::View::open_all_folds`] /
19//! [`crate::View::close_all_folds`]; folds keep their definitions across
20//! open/close cycles.
21
22/// A contiguous range of rows that the host can collapse to a single
23/// fold-marker line.
24///
25/// Folds are row-range spans: `[start_row, end_row]` inclusive. The buffer
26/// never elides content — [`crate::View::lines`] always returns the full
27/// logical text regardless of fold state. It is the host's render path that
28/// skips hidden rows and replaces them with a stub.
29///
30/// See the `folds` module documentation for the full invariant description.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct Fold {
33    /// First row of the folded range (visible when closed).
34    pub start_row: usize,
35    /// Last row of the folded range, inclusive.
36    pub end_row: usize,
37    /// `true` = collapsed (rows after `start_row` are hidden).
38    pub closed: bool,
39    /// `true` when this fold was created by the auto-fold engine
40    /// (tree-sitter foldmethod=expr). Manual folds created via `zf` /
41    /// [`crate::View::add_fold`] set this to `false`.
42    ///
43    /// Used by [`crate::View::set_auto_folds`] to distinguish auto
44    /// folds (which it manages) from manual folds (which it leaves
45    /// untouched).
46    pub auto_generated: bool,
47}
48
49impl Fold {
50    pub fn contains(&self, row: usize) -> bool {
51        row >= self.start_row && row <= self.end_row
52    }
53
54    /// True when `row` is hidden by a closed fold (i.e. inside the
55    /// fold but not on its `start_row` marker line).
56    pub fn hides(&self, row: usize) -> bool {
57        self.closed && row > self.start_row && row <= self.end_row
58    }
59
60    /// Number of rows the fold spans.
61    pub fn line_count(&self) -> usize {
62        self.end_row.saturating_sub(self.start_row) + 1
63    }
64}
65
66/// Sorted in canonical `(start_row ASC, end_row DESC)` order over a fold slice
67/// for O(log F) "is this row hidden by a closed fold" queries.
68///
69/// The O(log F) twin of `folds.iter().any(|f| f.hides(row))`. Closed
70/// folds' `(start_row, end_row]` hidden ranges are merged into a disjoint
71/// union, and one `partition_point` over the merged ranges answers the
72/// query. Merging (not just sorting) is what keeps this correct for
73/// nested folds: a fold that starts inside an enclosing one can end
74/// before it while the enclosing fold still covers the query row, so
75/// "the last fold with `start_row <= row`" alone is not enough — that
76/// fold may have already ended while an earlier, longer one still hides
77/// the row.
78///
79/// Mirrors the TUI renderer's `FoldIndex` (`hjkl-buffer-tui`
80/// render.rs) — the same merge, the same query, the same answers. That
81/// copy sorts its input defensively because its fold source is not
82/// guaranteed ordered; this one relies on the buffer's canonical
83/// `(start_row ASC, end_row DESC)` ordering invariant (see
84/// [`crate::View::add_fold`] and [`crate::View::set_auto_folds`]) and skips the
85/// sort so the build stays O(F) — it runs per keystroke / per frame on hot
86/// paths. Debug builds assert the invariant, so feeding unsorted folds fails
87/// loudly in tests rather than silently mis-answering in release.
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct FoldIndex {
90    /// Merged, disjoint half-open intervals `(start, end]` covering rows
91    /// hidden by at least one closed fold, sorted by `start`. A row `r` is
92    /// hidden iff `start < r <= end` for the interval with the greatest
93    /// `start <= r`.
94    hidden_ranges: Vec<(usize, usize)>,
95}
96
97impl FoldIndex {
98    /// Build the merged hidden-range index from a fold slice in canonical
99    /// `(start_row ASC, end_row DESC)` order (the buffer invariant — see the
100    /// type docs).
101    ///
102    /// O(F): one filter pass over the folds, then a single merge pass.
103    pub fn new(folds: &[Fold]) -> Self {
104        debug_assert!(
105            folds
106                .windows(2)
107                .all(|window| compare_folds(&window[0], &window[1]).is_le()),
108            "FoldIndex::new requires folds in canonical order"
109        );
110        // Merge the closed folds' (start, end] intervals into a disjoint
111        // union. `hides` membership is "in the union", so a single binary
112        // search over the merged ranges is correct even with nesting.
113        let mut hidden_ranges: Vec<(usize, usize)> = Vec::with_capacity(folds.len());
114        for f in folds.iter().filter(|f| f.closed) {
115            let (s, e) = (f.start_row, f.end_row);
116            match hidden_ranges.last_mut() {
117                // Overlaps the previous interval (or abuts it exactly —
118                // `s <= last_end` leaves no uncovered row between them) →
119                // extend. A gap needs `s > last_end`, e.g. (0,5] then (7,9]:
120                // row 6 is covered by neither, so they stay separate.
121                Some((_, last_end)) if s <= *last_end => {
122                    *last_end = (*last_end).max(e);
123                }
124                _ => hidden_ranges.push((s, e)),
125            }
126        }
127        Self { hidden_ranges }
128    }
129
130    /// True when `row` is hidden by a closed fold — the O(log F) twin of
131    /// `folds.iter().any(|f| f.hides(row))`.
132    pub fn hides_row(&self, row: usize) -> bool {
133        let idx = self.hidden_ranges.partition_point(|&(s, _)| s <= row);
134        if idx == 0 {
135            return false;
136        }
137        let (s, e) = self.hidden_ranges[idx - 1];
138        row > s && row <= e
139    }
140}
141
142/// Compare folds in canonical order: earliest start first, then widest range
143/// first when ranges share a start row.
144fn compare_folds(left: &Fold, right: &Fold) -> std::cmp::Ordering {
145    left.start_row
146        .cmp(&right.start_row)
147        .then_with(|| right.end_row.cmp(&left.end_row))
148}
149
150/// Index of the deepest fold containing `row`: latest start first, then the
151/// shortest range for folds that share a start row.
152fn innermost_containing_fold_index(folds: &[Fold], row: usize) -> Option<usize> {
153    folds
154        .iter()
155        .enumerate()
156        .filter(|(_, fold)| fold.contains(row))
157        .max_by_key(|(_, fold)| (fold.start_row, std::cmp::Reverse(fold.end_row)))
158        .map(|(index, _)| index)
159}
160
161/// Index of the recursive fold target at `row`. A fold that starts on the
162/// cursor row takes precedence; among same-start folds, that is the widest
163/// range so `zC` / `zO` / `zA` include the complete same-start nesting.
164/// Otherwise, use the innermost containing fold, matching the single-fold
165/// commands' target selection.
166fn recursive_fold_target_index(folds: &[Fold], row: usize) -> Option<usize> {
167    folds
168        .iter()
169        .position(|fold| fold.start_row == row)
170        .or_else(|| innermost_containing_fold_index(folds, row))
171}
172
173impl crate::View {
174    /// Returns a snapshot of all folds as an owned `Vec<Fold>`.
175    ///
176    /// Owned rather than `&[Fold]` because a `View` is a per-window
177    /// view onto a shared `Buffer`; another view could mutate the folds vec
178    /// between when this returns and when the caller reads the slice.
179    pub fn folds(&self) -> Vec<Fold> {
180        self.content_lock().folds.clone()
181    }
182
183    /// Run `f` against the fold list under a **single** content lock, with
184    /// no clone. The borrow-style twin of [`Self::folds`] — prefer it for
185    /// every read-only query (`hides` scans, `is_empty`, per-row loops);
186    /// keep [`Self::folds`] only where an owned snapshot must outlive the
187    /// lock (e.g. it is stored, or the buffer is re-borrowed mutably).
188    ///
189    /// The closure runs with the content mutex held: it must not call back
190    /// into any `&self` method of this `View` (they all re-lock, and the
191    /// mutex is not re-entrant). Hoist such reads — `row_count()`,
192    /// `cursor()`, … — above the call.
193    pub fn with_folds<T>(&self, f: impl FnOnce(&[Fold]) -> T) -> T {
194        f(&self.content_lock().folds)
195    }
196
197    /// True when at least one fold is defined (open or closed). One lock,
198    /// no clone — the cheap form of `!folds().is_empty()`.
199    pub fn has_folds(&self) -> bool {
200        !self.content_lock().folds.is_empty()
201    }
202
203    /// Monotonic fold-mutation generation. Bumps only when a fold mutator
204    /// actually changes the fold set; read-only queries and plain text
205    /// edits leave it alone. Hosts caching a fold snapshot (`hjkl`'s
206    /// per-window `window_folds`) compare this instead of re-cloning the
207    /// fold `Vec` every keystroke.
208    ///
209    /// Conservative in the same sense as [`Self::dirty_gen`]: "if it
210    /// changed, the folds **may** have changed" (a `rebase_folds` whose
211    /// row-shift happens to move nothing still bumps).
212    pub fn fold_gen(&self) -> u64 {
213        self.content_lock().fold_gen
214    }
215
216    /// Record a fold-set mutation: bump the fold generation *and* the
217    /// render-cache generation (a fold change repaints). Every mutator in
218    /// this module funnels through here.
219    fn folds_changed(&mut self) {
220        self.fold_gen_bump();
221        self.dirty_gen_bump();
222    }
223
224    /// Register a new fold. An existing fold with the exact same inclusive
225    /// range is replaced; distinct ranges sharing a start row are retained.
226    /// Folds stay in canonical `(start_row ASC, end_row DESC)` order. Empty /
227    /// inverted ranges are rejected.
228    pub fn add_fold(&mut self, start_row: usize, end_row: usize, closed: bool) {
229        if end_row < start_row {
230            return;
231        }
232        let last = self.row_count().saturating_sub(1);
233        if start_row > last {
234            return;
235        }
236        let end_row = end_row.min(last);
237        let fold = Fold {
238            start_row,
239            end_row,
240            closed,
241            auto_generated: false,
242        };
243        {
244            let mut c = self.content_lock_mut();
245            if let Some(idx) = c
246                .folds
247                .iter()
248                .position(|f| (f.start_row, f.end_row) == (start_row, end_row))
249            {
250                c.folds[idx] = fold;
251            } else {
252                let pos = c
253                    .folds
254                    .partition_point(|existing| compare_folds(existing, &fold).is_lt());
255                c.folds.insert(pos, fold);
256            }
257        }
258        self.folds_changed();
259    }
260
261    /// Replace all auto-generated folds with a new set derived from
262    /// `ranges`, while leaving manual folds untouched.
263    ///
264    /// ## `foldlevelstart`, not a boolean
265    ///
266    /// The second parameter is vim's `'foldlevelstart'` verbatim, and the rule
267    /// it implements is vim's: **a fold whose (1-based) nesting level is
268    /// greater than `foldlevelstart` starts closed**, everything at or above
269    /// that level starts open. So `0` closes everything, `1` leaves top-level
270    /// folds open and closes what is inside them, `99` (hjkl's default) opens
271    /// everything.
272    ///
273    /// It takes the raw option rather than a `default_closed: bool` — the
274    /// shape it replaced, which could only express "all closed" / "all open" —
275    /// and rather than a per-range level supplied by the caller, because:
276    ///
277    /// - **Nesting level is derivable from the ranges alone** (a fold's level
278    ///   is one more than the number of other ranges that strictly contain
279    ///   it), so a caller passing levels would be passing information this
280    ///   function can compute — and would have to recompute identically at
281    ///   every call site, marker scan and tree-sitter query alike.
282    /// - **Only this function knows the final set.** Levels have to be counted
283    ///   over the ranges that actually become folds, and that is decided
284    ///   *here*: single-row, inverted and out-of-range entries are dropped,
285    ///   `end_row` is clamped, duplicate start rows collapse, and rows owned
286    ///   by a manual fold are skipped. A level computed by the caller over the
287    ///   raw ranges would be counting folds that do not exist.
288    ///
289    /// Levels are counted over the **auto** folds only: a `zf` fold enclosing
290    /// an auto one is the user's own structure, and letting it push the auto
291    /// fold a level deeper would make the auto fold's start state depend on
292    /// unrelated manual folding.
293    ///
294    /// vim's own default for the option is `-1`, meaning "do nothing, leave
295    /// `'foldlevel'` alone" (which, with `'foldlevel'` at its own default of
296    /// `0`, ends up closing everything). hjkl's `foldlevelstart` is a `u32`
297    /// defaulting to `99`, so that mode does not exist here and no value is
298    /// reserved for it — every value is a real level.
299    ///
300    /// ## Algorithm (O(N log N) — bounded by `ranges.len()`, no unbounded growth)
301    ///
302    /// 1. Snapshot `(start_row, end_row) → closed` for every existing auto fold
303    ///    so each range's open/closed state survives a reparse.
304    /// 2. Retain only manual folds (`auto_generated == false`).
305    /// 3. Normalise `ranges` into the set that will actually become folds.
306    /// 4. Assign each of those a nesting level by a containment sweep.
307    /// 5. Insert one new `Fold` per surviving range, re-using the snapshotted
308    ///    closed state when its exact range existed before, else
309    ///    `level > foldlevelstart`.
310    ///
311    /// Invariants preserved:
312    /// - Folds stay in canonical `(start_row ASC, end_row DESC)` order.
313    /// - Duplicate exact ranges are deduplicated deterministically. Distinct
314    ///   ranges sharing a start row are retained.
315    /// - Empty / inverted ranges (end_row < start_row) are silently skipped.
316    /// - `end_row` is clamped to the last valid row, same as `add_fold`.
317    /// - A MANUAL fold's start_row is never taken over: the auto range for
318    ///   that row is dropped instead. `zf` is an explicit choice of extent,
319    ///   and converting it to an auto fold both changed it and handed it to
320    ///   the auto engine to overwrite on the next reparse.
321    /// - An auto fold with an existing exact range keeps its open / closed
322    ///   state: `foldlevelstart` decides how a fold *starts*, so it applies to
323    ///   newly-appearing ranges only and never re-closes a fold the user opened.
324    /// - When the resulting fold set is identical to the current one, nothing
325    ///   is written and NO generation bumps — [`Self::fold_gen`] promises to
326    ///   move only on a real change, and `dirty_gen` moving every pass made
327    ///   the caller's "recompute once per edit" guard fire every frame (and
328    ///   with it a full re-highlight, since `dirty_gen` keys that cache).
329    pub fn set_auto_folds(&mut self, ranges: &[(usize, usize)], foldlevelstart: u32) {
330        // 1. Snapshot closed state of existing auto folds by exact range.
331        let prev_closed: std::collections::HashMap<(usize, usize), bool> = self
332            .content_lock()
333            .folds
334            .iter()
335            .filter(|f| f.auto_generated)
336            .map(|f| ((f.start_row, f.end_row), f.closed))
337            .collect();
338
339        // 2. Start from the manual folds — they survive untouched, and their
340        //    start rows are off-limits to the ranges below.
341        let mut next: Vec<Fold> = self
342            .content_lock()
343            .folds
344            .iter()
345            .filter(|f| !f.auto_generated)
346            .copied()
347            .collect();
348        let manual_starts: std::collections::HashSet<usize> =
349            next.iter().map(|f| f.start_row).collect();
350
351        // 3. Normalise: drop what will never become a fold, then sort and
352        //    deduplicate exact ranges so the level sweep counts only real folds.
353        let last = self.row_count().saturating_sub(1);
354        let mut accepted: Vec<(usize, usize)> = Vec::with_capacity(ranges.len());
355        for &(start_row, end_row) in ranges {
356            // Skip empty/inverted and out-of-bounds ranges.
357            if end_row < start_row || start_row > last {
358                continue;
359            }
360            let end_row = end_row.min(last);
361            // Only folds spanning more than one row are meaningful.
362            if end_row == start_row || manual_starts.contains(&start_row) {
363                continue;
364            }
365            accepted.push((start_row, end_row));
366        }
367        accepted
368            .sort_unstable_by_key(|&(start_row, end_row)| (start_row, std::cmp::Reverse(end_row)));
369        accepted.dedup();
370
371        // 4. Nesting level per accepted range, by a single containment sweep.
372        //    Visiting outermost-first makes `stack` the chain of enclosing
373        //    folds: pop everything that ends before this range does — that
374        //    covers both a sibling that already closed and a partial overlap,
375        //    neither of which encloses it — and what is left is exactly the
376        //    enclosing chain.
377        let mut levels = Vec::with_capacity(accepted.len());
378        let mut stack: Vec<usize> = Vec::new();
379        for &(_, end_row) in &accepted {
380            while stack.last().is_some_and(|&open_end| open_end < end_row) {
381                stack.pop();
382            }
383            // 1-based, matching vim: an unnested fold is level 1.
384            levels.push(stack.len() as u32 + 1);
385            stack.push(end_row);
386        }
387
388        // 5. Insert the new auto folds, then restore canonical ordering.
389        next.extend(
390            accepted
391                .iter()
392                .zip(levels)
393                .map(|(&(start_row, end_row), level)| Fold {
394                    start_row,
395                    end_row,
396                    closed: prev_closed
397                        .get(&(start_row, end_row))
398                        .copied()
399                        .unwrap_or(level > foldlevelstart),
400                    auto_generated: true,
401                }),
402        );
403        next.sort_by(compare_folds);
404
405        // 6. No-op when nothing actually changed — see the invariant above.
406        if self.content_lock().folds == next {
407            return;
408        }
409        self.content_lock_mut().folds = next;
410        self.folds_changed();
411    }
412
413    /// Drop the fold whose range covers `row`. Returns `true` when a
414    /// fold was actually removed.
415    pub fn remove_fold_at(&mut self, row: usize) -> bool {
416        // Remove the innermost fold containing `row`, so `zd` on a nested
417        // fold drops the inner one, not the enclosing block.
418        let idx = innermost_containing_fold_index(&self.content_lock().folds, row);
419        let Some(idx) = idx else {
420            return false;
421        };
422        self.content_lock_mut().folds.remove(idx);
423        self.folds_changed();
424        true
425    }
426
427    /// Open the fold at `row` (no-op if already open or no fold).
428    pub fn open_fold_at(&mut self, row: usize) -> bool {
429        let Some(idx) = innermost_containing_fold_index(&self.content_lock().folds, row) else {
430            return false;
431        };
432        let changed = {
433            let mut c = self.content_lock_mut();
434            let f = &mut c.folds[idx];
435            if !f.closed {
436                return false;
437            }
438            f.closed = false;
439            true
440        };
441        if changed {
442            self.folds_changed();
443        }
444        changed
445    }
446
447    /// Close the fold at `row` (no-op if already closed or no fold).
448    pub fn close_fold_at(&mut self, row: usize) -> bool {
449        let Some(idx) = innermost_containing_fold_index(&self.content_lock().folds, row) else {
450            return false;
451        };
452        let changed = {
453            let mut c = self.content_lock_mut();
454            let f = &mut c.folds[idx];
455            if f.closed {
456                return false;
457            }
458            f.closed = true;
459            true
460        };
461        if changed {
462            self.folds_changed();
463        }
464        changed
465    }
466
467    /// Flip the closed/open state of the fold containing `row`.
468    pub fn toggle_fold_at(&mut self, row: usize) -> bool {
469        let Some(idx) = innermost_containing_fold_index(&self.content_lock().folds, row) else {
470            return false;
471        };
472        let changed = {
473            let mut c = self.content_lock_mut();
474            let f = &mut c.folds[idx];
475            f.closed = !f.closed;
476            true
477        };
478        if changed {
479            self.folds_changed();
480        }
481        changed
482    }
483
484    /// `zC` — close the fold subtree at `row`. Folds that start on `row`
485    /// select their widest range, so same-start nested folds close together;
486    /// otherwise the innermost containing fold and its descendants close.
487    pub fn close_folds_recursively_at(&mut self, row: usize) -> bool {
488        let changed = {
489            let mut c = self.content_lock_mut();
490            let Some(target) = recursive_fold_target_index(&c.folds, row).map(|idx| c.folds[idx])
491            else {
492                return false;
493            };
494            let mut any = false;
495            for fold in &mut c.folds {
496                if fold.start_row >= target.start_row
497                    && fold.end_row <= target.end_row
498                    && !fold.closed
499                {
500                    fold.closed = true;
501                    any = true;
502                }
503            }
504            any
505        };
506        if changed {
507            self.folds_changed();
508        }
509        changed
510    }
511
512    /// `zO` — open the fold subtree at `row`. Target selection matches
513    /// [`Self::close_folds_recursively_at`].
514    pub fn open_folds_recursively_at(&mut self, row: usize) -> bool {
515        let changed = {
516            let mut c = self.content_lock_mut();
517            let Some(target) = recursive_fold_target_index(&c.folds, row).map(|idx| c.folds[idx])
518            else {
519                return false;
520            };
521            let mut any = false;
522            for fold in &mut c.folds {
523                if fold.start_row >= target.start_row
524                    && fold.end_row <= target.end_row
525                    && fold.closed
526                {
527                    fold.closed = false;
528                    any = true;
529                }
530            }
531            any
532        };
533        if changed {
534            self.folds_changed();
535        }
536        changed
537    }
538
539    /// `zA` — toggle the fold subtree at `row`. If every fold in the subtree
540    /// is open, close it; otherwise open the entire subtree.
541    pub fn toggle_folds_recursively_at(&mut self, row: usize) -> bool {
542        let changed = {
543            let mut c = self.content_lock_mut();
544            let Some(target) = recursive_fold_target_index(&c.folds, row).map(|idx| c.folds[idx])
545            else {
546                return false;
547            };
548            let closed = !c.folds.iter().any(|fold| {
549                fold.start_row >= target.start_row && fold.end_row <= target.end_row && fold.closed
550            });
551            for fold in &mut c.folds {
552                if fold.start_row >= target.start_row && fold.end_row <= target.end_row {
553                    fold.closed = closed;
554                }
555            }
556            true
557        };
558        if changed {
559            self.folds_changed();
560        }
561        changed
562    }
563
564    /// `zR` — open every fold.
565    pub fn open_all_folds(&mut self) {
566        let changed = {
567            let mut c = self.content_lock_mut();
568            let mut any = false;
569            for f in c.folds.iter_mut() {
570                if f.closed {
571                    f.closed = false;
572                    any = true;
573                }
574            }
575            any
576        };
577        if changed {
578            self.folds_changed();
579        }
580    }
581
582    /// `zE` — eliminate every fold.
583    pub fn clear_all_folds(&mut self) {
584        let was_nonempty = !self.content_lock().folds.is_empty();
585        if was_nonempty {
586            self.content_lock_mut().folds.clear();
587            self.folds_changed();
588        }
589    }
590
591    /// `zM` — close every fold.
592    pub fn close_all_folds(&mut self) {
593        let changed = {
594            let mut c = self.content_lock_mut();
595            let mut any = false;
596            for f in c.folds.iter_mut() {
597                if !f.closed {
598                    f.closed = true;
599                    any = true;
600                }
601            }
602            any
603        };
604        if changed {
605            self.folds_changed();
606        }
607    }
608
609    /// Deepest fold whose range contains `row`. Useful for the host's
610    /// `za`/`zo`/`zc` handlers. Ranges sharing a start row select the shortest
611    /// one.
612    pub fn fold_at_row(&self, row: usize) -> Option<Fold> {
613        let folds = self.content_lock();
614        innermost_containing_fold_index(&folds.folds, row).map(|idx| folds.folds[idx])
615    }
616
617    /// True iff `row` is hidden by a closed fold (any fold).
618    pub fn is_row_hidden(&self, row: usize) -> bool {
619        self.with_folds(|folds| folds.iter().any(|f| f.hides(row)))
620    }
621
622    /// Open every closed fold whose body hides `row`, so the row becomes
623    /// visible. Handles nested folds in a single pass — unlike
624    /// `open_fold_at` / `FoldOp::OpenAt`, which only act on the first fold
625    /// containing the row and so can never reach a nested inner fold.
626    /// Used by `goto_line` so a jump into a folded region reveals the
627    /// target line instead of stranding the cursor on a hidden row.
628    /// Returns `true` if any fold was opened.
629    pub fn reveal_row(&mut self, row: usize) -> bool {
630        self.open_folds_where(|f| f.hides(row))
631    }
632
633    /// Open every closed fold that contains `row`, its marker line included
634    /// — vim's `foldOpenCursor()`, which `'foldopen'` runs after a motion.
635    /// Unlike [`Self::reveal_row`], a cursor sitting on a closed fold's first
636    /// row opens that fold too; unlike `zO`, nested folds that do not contain
637    /// `row` stay as they are. Returns `true` if any fold was opened.
638    pub fn open_folds_containing(&mut self, row: usize) -> bool {
639        self.open_folds_where(|f| f.closed && f.contains(row))
640    }
641
642    fn open_folds_where(&mut self, pred: impl Fn(&Fold) -> bool) -> bool {
643        let changed = {
644            let mut c = self.content_lock_mut();
645            let mut any = false;
646            for f in c.folds.iter_mut() {
647                if pred(f) {
648                    f.closed = false;
649                    any = true;
650                }
651            }
652            any
653        };
654        if changed {
655            self.folds_changed();
656        }
657        changed
658    }
659
660    /// Last row index containing real content — skips vim's single
661    /// phantom trailing empty row. `ropey`'s `len_lines()` always
662    /// synthesizes one extra empty final "line" when the buffer text
663    /// ends in `\n` (vim treats that `\n` as a terminator, not a
664    /// separator). Mirrors `hjkl_engine::motions::move_bottom`'s clamp
665    /// (`G`) so vertical motions agree with `G` on where the buffer
666    /// "ends". A buffer whose *real* last line happens to be empty
667    /// (e.g. `"foo\n\n"`, row 1) is untouched — only a single trailing
668    /// phantom row is ever skipped.
669    ///
670    /// The emptiness test reads the last row's byte length rather than
671    /// materializing the row as a `String`: for the final rope line the two
672    /// agree exactly (ropey never gives the last line a trailing `\n`, so
673    /// `rope_line_str` has nothing to strip and
674    /// `rope_line_bytes(last) == 0` iff `rope_line_str(last).is_empty()`).
675    pub fn last_content_row(&self) -> usize {
676        let raw_last = self.row_count().saturating_sub(1);
677        if raw_last > 0 {
678            let c = self.content_lock();
679            if crate::buffer::rope_line_bytes(&c.text, raw_last) == 0 {
680                return raw_last - 1;
681            }
682        }
683        raw_last
684    }
685
686    /// First visible row strictly after `row`, skipping any rows hidden
687    /// by closed folds. Returns `None` past the end of the buffer.
688    ///
689    /// Takes the content lock **once** for the whole walk (via
690    /// [`Self::with_folds`]) instead of once per skipped row — `j` over a
691    /// long closed fold used to pay a lock + full `Vec<Fold>` clone per row.
692    /// `last_content_row()` locks too, so it is resolved before the scan.
693    pub fn next_visible_row(&self, row: usize) -> Option<usize> {
694        let last = self.last_content_row();
695        if last == 0 && row == 0 {
696            return None;
697        }
698        let mut r = row.checked_add(1)?;
699        self.with_folds(|folds| {
700            let index = FoldIndex::new(folds);
701            while r <= last && index.hides_row(r) {
702                r += 1;
703            }
704            (r <= last).then_some(r)
705        })
706    }
707
708    /// First visible row strictly before `row`, skipping hidden rows.
709    ///
710    /// One lock for the whole walk, same as [`Self::next_visible_row`].
711    pub fn prev_visible_row(&self, row: usize) -> Option<usize> {
712        let mut r = row.checked_sub(1)?;
713        self.with_folds(|folds| {
714            let index = FoldIndex::new(folds);
715            while index.hides_row(r) {
716                r = r.checked_sub(1)?;
717            }
718            Some(r)
719        })
720    }
721
722    /// Drop every fold that touches `[start_row, end_row]`.
723    pub fn invalidate_folds_in_range(&mut self, start_row: usize, end_row: usize) {
724        let before = self.content_lock().folds.len();
725        invalidate_folds(&mut self.content_lock_mut().folds, start_row, end_row);
726        if self.content_lock().folds.len() != before {
727            self.folds_changed();
728        }
729    }
730
731    /// Shift every buffer fold by an edit's row-delta band. Mirrors
732    /// [`crate::buffer::View::rebase_marks`] for the shared fold storage —
733    /// see [`shift_folds_after_edit`] for the per-fold rules.
734    pub fn rebase_folds(
735        &mut self,
736        edit_start: usize,
737        drop_end: usize,
738        shift_threshold: usize,
739        delta: isize,
740    ) {
741        if delta == 0 {
742            return;
743        }
744        let touched = {
745            let mut c = self.content_lock_mut();
746            if c.folds.is_empty() {
747                false
748            } else {
749                shift_folds_after_edit(&mut c.folds, edit_start, drop_end, shift_threshold, delta);
750                true
751            }
752        };
753        if touched {
754            // Conservative: a shift that happened to move nothing (every
755            // fold entirely below the edit) still bumps. Matches the
756            // `dirty_gen` contract — "may have changed".
757            self.fold_gen_bump();
758        }
759    }
760
761    /// Replace the entire fold set wholesale. Used to install a per-window fold
762    /// snapshot into the shared buffer on focus change (window-level folds): the
763    /// app keeps each window's open/closed state and swaps it in before dispatch,
764    /// so motions/render/`z`-ops operate on the focused window's folds. Input
765    /// is normalized to canonical `(start_row ASC, end_row DESC)` order, with
766    /// exact-range duplicates collapsed.
767    pub fn set_folds(&mut self, folds: &[Fold]) {
768        let mut next = folds.to_vec();
769        next.sort_by(compare_folds);
770        next.dedup_by_key(|fold| (fold.start_row, fold.end_row));
771        {
772            let mut c = self.content_lock_mut();
773            if c.folds == next {
774                return; // no-op — avoid a spurious dirty_gen bump
775            }
776            c.folds = next;
777        }
778        self.folds_changed();
779    }
780}
781
782/// Drop every fold in `folds` that touches `[start_row, end_row]`, in place.
783///
784/// Free helper so both [`crate::View::invalidate_folds_in_range`] (operating
785/// on the shared content) and the app's window-level edit-coherence pass
786/// (operating on a sibling window's owned `Vec<Fold>`) share one rule — vim
787/// opens/forgets any fold the edit overlapped.
788pub fn invalidate_folds(folds: &mut Vec<Fold>, start_row: usize, end_row: usize) {
789    folds.retain(|f| f.end_row < start_row || f.start_row > end_row);
790}
791
792// ── Row-delta shifting (edit-coherence) ──────────────────────────────────
793//
794// A manual (`zf`) fold is a row-range that has to track the same
795// insert/delete row-shift the engine already applies to marks and the
796// jumplist (see `Editor::shift_marks_after_edit`). Without this, a fold
797// below an edit keeps stale row numbers and the renderer / fold-aware ops
798// (`dd`, `p`, …) act on the wrong rows (#audit-r2 fix 1).
799//
800// The four `(edit_start, drop_end, shift_threshold, delta)` parameters are
801// the exact same band description `Editor::shift_marks_after_edit` computes
802// for marks: `[edit_start, drop_end)` is the row band the edit deleted
803// (empty for inserts), and any row `>= shift_threshold` moves by `delta`.
804// Reusing the identical band keeps folds, marks, and jumplist entries
805// shifting in lockstep for the same edit.
806
807/// Shift a single fold's `start_row` / `end_row` by an edit's row-delta band.
808/// Returns `None` when the edit's deleted band fully consumes the fold.
809///
810/// Each endpoint is mapped independently through the same drop/shift rule
811/// [`crate::buffer::View::rebase_marks`] applies to a point mark. Mapping
812/// the two endpoints independently is what produces the vim-shaped "edit
813/// inside a fold" semantics for free:
814/// - Both endpoints below `shift_threshold` and outside the deleted band →
815///   fold untouched (edit happened entirely outside the fold).
816/// - `start_row` outside the deleted band but `end_row` inside it → the
817///   edit deleted the fold's tail; it clips to end at the last surviving
818///   row (`edit_start - 1`).
819/// - `start_row` inside the deleted band but `end_row` outside it → the
820///   edit deleted the fold's head; it clips to start at `edit_start` (the
821///   row the surviving tail now occupies).
822/// - Both endpoints inside the deleted band → the edit consumed the whole
823///   fold; it's dropped.
824/// - `start_row` below the threshold and `end_row` at/above it (an insert
825///   or a deletion landing strictly inside the fold) → `start_row` stays,
826///   `end_row` shifts by `delta`: the fold grows (insert) or shrinks
827///   (delete) around the edit, matching vim.
828/// - Both endpoints at/above the threshold → the fold shifts wholesale.
829pub fn shift_fold(
830    fold: Fold,
831    edit_start: usize,
832    drop_end: usize,
833    shift_threshold: usize,
834    delta: isize,
835) -> Option<Fold> {
836    if delta == 0 {
837        return Some(fold);
838    }
839    let map_row = |row: usize| -> Option<usize> {
840        if (edit_start..drop_end).contains(&row) {
841            None
842        } else if row >= shift_threshold {
843            Some(((row as isize) + delta).max(0) as usize)
844        } else {
845            Some(row)
846        }
847    };
848    let mapped_start = map_row(fold.start_row);
849    let mapped_end = map_row(fold.end_row);
850    if mapped_start.is_none() && mapped_end.is_none() {
851        return None;
852    }
853    let new_start = mapped_start.unwrap_or(edit_start);
854    let new_end = mapped_end.unwrap_or_else(|| edit_start.saturating_sub(1));
855    if new_end < new_start {
856        return None;
857    }
858    Some(Fold {
859        start_row: new_start,
860        end_row: new_end,
861        closed: fold.closed,
862        auto_generated: fold.auto_generated,
863    })
864}
865
866/// Shift every fold in `folds` by an edit's row-delta band, in place.
867/// Folds the edit's deleted band fully consumes are dropped (mirrors
868/// [`invalidate_folds`] for the folds that DO survive but move). The surviving
869/// folds are normalized to canonical `(start_row ASC, end_row DESC)` order and
870/// exact-range collisions retain the first stable canonical fold.
871///
872/// Shared by [`crate::View::rebase_folds`] (engine-side, the buffer's own
873/// fold storage) and the app's sibling-window fold snapshot shift, so both
874/// converge on the identical row-shift rule.
875pub fn shift_folds_after_edit(
876    folds: &mut Vec<Fold>,
877    edit_start: usize,
878    drop_end: usize,
879    shift_threshold: usize,
880    delta: isize,
881) {
882    if delta == 0 {
883        return;
884    }
885    folds.retain_mut(
886        |f| match shift_fold(*f, edit_start, drop_end, shift_threshold, delta) {
887            Some(shifted) => {
888                *f = shifted;
889                true
890            }
891            None => false,
892        },
893    );
894    folds.sort_by(compare_folds);
895    folds.dedup_by_key(|fold| (fold.start_row, fold.end_row));
896}
897
898#[cfg(test)]
899mod tests {
900    use crate::View;
901
902    fn b() -> View {
903        View::from_str("a\nb\nc\nd\ne")
904    }
905
906    #[test]
907    fn add_keeps_folds_in_start_row_order() {
908        let mut buf = b();
909        buf.add_fold(2, 3, true);
910        buf.add_fold(0, 1, false);
911        let starts: Vec<usize> = buf.folds().iter().map(|f| f.start_row).collect();
912        assert_eq!(starts, vec![0, 2]);
913    }
914
915    #[test]
916    fn set_folds_replaces_wholesale() {
917        let mut buf = b();
918        buf.add_fold(0, 1, false);
919        // Install a different per-window snapshot.
920        let snapshot = vec![super::Fold {
921            start_row: 2,
922            end_row: 3,
923            closed: true,
924            auto_generated: false,
925        }];
926        buf.set_folds(&snapshot);
927        assert_eq!(buf.folds(), snapshot);
928        // Idempotent: re-installing the same set is a no-op (no dirty bump).
929        let dg = buf.dirty_gen();
930        buf.set_folds(&snapshot);
931        assert_eq!(buf.dirty_gen(), dg);
932    }
933
934    #[test]
935    fn invalidate_folds_helper_drops_overlapping() {
936        let f = |s, e| super::Fold {
937            start_row: s,
938            end_row: e,
939            closed: true,
940            auto_generated: false,
941        };
942        let mut folds = vec![f(0, 2), f(4, 6), f(8, 10)];
943        // Edit touches rows 5..5 → only the [4,6] fold overlaps.
944        super::invalidate_folds(&mut folds, 5, 5);
945        let starts: Vec<usize> = folds.iter().map(|x| x.start_row).collect();
946        assert_eq!(starts, vec![0, 8]);
947    }
948
949    #[test]
950    fn add_fold_retains_same_start_ranges_and_replaces_exact_duplicate() {
951        let mut buf = b();
952        buf.add_fold(1, 4, false);
953        buf.add_fold(1, 2, false);
954        buf.add_fold(1, 2, true);
955
956        assert_eq!(
957            buf.folds()
958                .iter()
959                .map(|f| (f.start_row, f.end_row, f.closed))
960                .collect::<Vec<_>>(),
961            vec![(1, 4, false), (1, 2, true)]
962        );
963    }
964
965    #[test]
966    fn add_clamps_end_row_to_buffer_bounds() {
967        let mut buf = b();
968        buf.add_fold(2, 99, true);
969        assert_eq!(buf.folds()[0].end_row, 4);
970    }
971
972    #[test]
973    fn add_rejects_inverted_range() {
974        let mut buf = b();
975        buf.add_fold(3, 1, true);
976        assert!(buf.folds().is_empty());
977    }
978
979    #[test]
980    fn open_folds_containing_opens_the_marker_row_fold_and_nothing_else() {
981        let mut buf = b();
982        buf.add_fold(0, 4, true);
983        buf.add_fold(0, 1, true);
984        buf.add_fold(3, 4, true);
985        // `reveal_row` leaves a fold whose marker line the row is on.
986        assert!(!buf.reveal_row(0));
987        assert!(buf.open_folds_containing(0));
988        for f in buf.folds() {
989            assert_eq!(f.closed, !f.contains(0), "{f:?}");
990        }
991        assert!(!buf.open_folds_containing(0));
992    }
993
994    #[test]
995    fn toggle_flips_state() {
996        let mut buf = b();
997        buf.add_fold(1, 3, false);
998        assert!(!buf.folds()[0].closed);
999        assert!(buf.toggle_fold_at(2));
1000        assert!(buf.folds()[0].closed);
1001        assert!(buf.toggle_fold_at(2));
1002        assert!(!buf.folds()[0].closed);
1003    }
1004
1005    #[test]
1006    fn same_start_selectors_target_the_innermost_fold() {
1007        let mut buf = b();
1008        buf.add_fold(0, 4, false);
1009        buf.add_fold(0, 2, true);
1010        assert!(buf.open_fold_at(1));
1011        assert!(!buf.is_row_hidden(1));
1012        assert!(!buf.is_row_hidden(2));
1013        assert!(!buf.is_row_hidden(3));
1014        assert!(!buf.is_row_hidden(4));
1015
1016        assert!(buf.toggle_fold_at(1));
1017        assert!(buf.is_row_hidden(1));
1018        assert!(buf.is_row_hidden(2));
1019        assert!(!buf.is_row_hidden(3));
1020        assert!(!buf.is_row_hidden(4));
1021        assert_eq!(buf.fold_at_row(1).map(|fold| fold.end_row), Some(2));
1022
1023        assert!(buf.remove_fold_at(1));
1024        assert!(!buf.is_row_hidden(1));
1025        assert!(!buf.is_row_hidden(2));
1026        assert!(!buf.is_row_hidden(3));
1027        assert!(!buf.is_row_hidden(4));
1028    }
1029
1030    #[test]
1031    fn recursive_fold_commands_apply_to_only_the_target_subtree() {
1032        let mut buf = View::from_str("0\n1\n2\n3\n4\n5\n6\n7\n8");
1033        buf.add_fold(0, 6, false);
1034        buf.add_fold(2, 4, false);
1035        buf.add_fold(3, 3, false);
1036        buf.add_fold(5, 6, false);
1037
1038        assert!(buf.close_folds_recursively_at(2));
1039        assert_eq!(
1040            buf.folds()
1041                .iter()
1042                .map(|fold| (fold.start_row, fold.end_row, fold.closed))
1043                .collect::<Vec<_>>(),
1044            vec![(0, 6, false), (2, 4, true), (3, 3, true), (5, 6, false)]
1045        );
1046        assert_eq!(buf.next_visible_row(2), Some(5));
1047
1048        assert!(buf.open_folds_recursively_at(2));
1049        assert!(buf.folds().iter().all(|fold| !fold.closed));
1050    }
1051
1052    #[test]
1053    fn recursive_toggle_uses_the_same_start_subtree_state() {
1054        let mut buf = View::from_str("a\nb\nc\nd\ne\nf");
1055        buf.add_fold(0, 4, false);
1056        buf.add_fold(0, 2, false);
1057
1058        assert!(buf.toggle_folds_recursively_at(0));
1059        assert_eq!(
1060            buf.folds()
1061                .iter()
1062                .map(|fold| (fold.start_row, fold.end_row, fold.closed))
1063                .collect::<Vec<_>>(),
1064            vec![(0, 4, true), (0, 2, true)]
1065        );
1066        assert!((1..=4).all(|row| buf.is_row_hidden(row)));
1067        assert_eq!(buf.next_visible_row(0), Some(5));
1068
1069        assert!(buf.toggle_folds_recursively_at(0));
1070        assert_eq!(
1071            buf.folds()
1072                .iter()
1073                .map(|fold| (fold.start_row, fold.end_row, fold.closed))
1074                .collect::<Vec<_>>(),
1075            vec![(0, 4, false), (0, 2, false)]
1076        );
1077        assert!((0..5).all(|row| !buf.is_row_hidden(row)));
1078
1079        assert!(buf.close_fold_at(0));
1080        assert_eq!(
1081            buf.folds()
1082                .iter()
1083                .map(|fold| (fold.start_row, fold.end_row, fold.closed))
1084                .collect::<Vec<_>>(),
1085            vec![(0, 4, false), (0, 2, true)]
1086        );
1087        assert!(buf.is_row_hidden(1));
1088        assert!(buf.is_row_hidden(2));
1089        assert!(!buf.is_row_hidden(3));
1090        assert!(!buf.is_row_hidden(4));
1091        assert_eq!(buf.next_visible_row(0), Some(3));
1092
1093        assert!(buf.toggle_folds_recursively_at(0));
1094        assert_eq!(
1095            buf.folds()
1096                .iter()
1097                .map(|fold| (fold.start_row, fold.end_row, fold.closed))
1098                .collect::<Vec<_>>(),
1099            vec![(0, 4, false), (0, 2, false)]
1100        );
1101        assert!((0..5).all(|row| !buf.is_row_hidden(row)));
1102    }
1103
1104    #[test]
1105    fn is_row_hidden_excludes_start_row() {
1106        let mut buf = b();
1107        buf.add_fold(1, 3, true);
1108        assert!(!buf.is_row_hidden(0));
1109        assert!(!buf.is_row_hidden(1)); // start row stays visible
1110        assert!(buf.is_row_hidden(2));
1111        assert!(buf.is_row_hidden(3));
1112        assert!(!buf.is_row_hidden(4));
1113    }
1114
1115    #[test]
1116    fn fold_index_hides_row_matches_naive_scan() {
1117        // The cases a naive "last fold with start_row <= row" binary search
1118        // gets wrong, pinned against the linear scan it replaces:
1119        // - a fold starting well before the queried row: row 99/100 are
1120        //   covered by [0,100], but the last start_row <= 99/100 is [50,60];
1121        // - a nested fold ending before the enclosing one: row 7 is covered
1122        //   by [1,10], but the last start_row <= 7 is [5,6] which ended at 6;
1123        // - row 10: covered by [1,10] and [0,100], while the last start_row
1124        //   <= 10 is [8,9], which ends at 9.
1125        let f = |s, e, closed| super::Fold {
1126            start_row: s,
1127            end_row: e,
1128            closed,
1129            auto_generated: false,
1130        };
1131        let folds = vec![
1132            f(0, 100, true),  // starts well before the queried rows
1133            f(1, 10, true),   // encloses the nested folds below
1134            f(5, 6, true),    // nested; ends before rows 7..9
1135            f(8, 9, true),    // nested; covers rows 8..9
1136            f(20, 20, true),  // degenerate: hides nothing itself
1137            f(30, 40, false), // open: hides nothing
1138            f(50, 60, true),
1139        ];
1140        let index = super::FoldIndex::new(&folds);
1141        for row in 0..200 {
1142            let naive = folds.iter().any(|f| f.hides(row));
1143            assert_eq!(index.hides_row(row), naive, "row {row}");
1144        }
1145    }
1146
1147    #[test]
1148    fn fold_index_agrees_with_buffer_folds_across_nested_and_open_mixes() {
1149        // Same guarantee through the real buffer API: `add_fold` keeps the
1150        // list in canonical order regardless of insertion order, and the
1151        // index must answer identically to the buffer's linear scan for a
1152        // nesting of closed/open/degenerate folds.
1153        let mut buf = View::from_str(&"x\n".repeat(60));
1154        buf.add_fold(0, 55, true);
1155        buf.add_fold(2, 3, true);
1156        buf.add_fold(5, 6, false); // open
1157        buf.add_fold(7, 7, true); // degenerate — hides nothing itself
1158        buf.add_fold(10, 20, true);
1159        buf.add_fold(15, 16, true); // nested inside [10,20]
1160        for row in 0..buf.row_count() {
1161            let naive = buf.folds().iter().any(|f| f.hides(row));
1162            let via_index = buf.with_folds(|folds| super::FoldIndex::new(folds).hides_row(row));
1163            assert_eq!(via_index, naive, "row {row}");
1164        }
1165    }
1166
1167    #[test]
1168    fn open_close_all_changes_every_fold() {
1169        let mut buf = b();
1170        buf.add_fold(0, 1, false);
1171        buf.add_fold(2, 3, true);
1172        buf.close_all_folds();
1173        assert!(buf.folds().iter().all(|f| f.closed));
1174        buf.open_all_folds();
1175        assert!(buf.folds().iter().all(|f| !f.closed));
1176    }
1177
1178    #[test]
1179    fn invalidate_drops_overlapping_folds() {
1180        let mut buf = b();
1181        buf.add_fold(0, 1, true);
1182        buf.add_fold(2, 3, true);
1183        buf.add_fold(4, 4, true);
1184        buf.invalidate_folds_in_range(2, 3);
1185        let starts: Vec<usize> = buf.folds().iter().map(|f| f.start_row).collect();
1186        assert_eq!(starts, vec![0, 4]);
1187    }
1188
1189    // ── auto_generated flag + set_auto_folds ─────────────────────────────────
1190
1191    #[test]
1192    fn add_fold_sets_auto_generated_false() {
1193        let mut buf = b();
1194        buf.add_fold(1, 3, false);
1195        assert!(
1196            !buf.folds()[0].auto_generated,
1197            "manual add_fold must have auto_generated=false"
1198        );
1199    }
1200
1201    #[test]
1202    fn set_auto_folds_retains_same_start_nested_ranges() {
1203        let mut buf = b();
1204        buf.set_auto_folds(&[(0, 4), (0, 2)], 99);
1205
1206        assert_eq!(
1207            buf.folds()
1208                .iter()
1209                .map(|f| (f.start_row, f.end_row))
1210                .collect::<Vec<_>>(),
1211            vec![(0, 4), (0, 2)]
1212        );
1213        assert!(buf.close_fold_at(1));
1214        assert!(buf.is_row_hidden(1));
1215        assert!(buf.is_row_hidden(2));
1216        assert!(!buf.is_row_hidden(3));
1217        assert!(!buf.is_row_hidden(4));
1218        assert_eq!(buf.next_visible_row(0), Some(3));
1219    }
1220
1221    #[test]
1222    fn set_auto_folds_deduplicates_same_start_exact_ranges() {
1223        let mut buf = b();
1224        buf.set_auto_folds(&[(0, 4), (0, 2), (0, 4)], 99);
1225        assert_eq!(
1226            buf.folds()
1227                .iter()
1228                .map(|fold| (fold.start_row, fold.end_row))
1229                .collect::<Vec<_>>(),
1230            vec![(0, 4), (0, 2)]
1231        );
1232    }
1233
1234    #[test]
1235    fn set_auto_folds_adds_auto_folds() {
1236        let mut buf = b();
1237        buf.set_auto_folds(&[(0, 2), (3, 4)], 99);
1238        let folds = buf.folds();
1239        assert_eq!(folds.len(), 2);
1240        assert!(folds[0].auto_generated);
1241        assert!(folds[1].auto_generated);
1242        assert_eq!(folds[0].start_row, 0);
1243        assert_eq!(folds[1].start_row, 3);
1244    }
1245
1246    #[test]
1247    fn set_auto_folds_second_call_replaces_first() {
1248        let mut buf = b();
1249        buf.set_auto_folds(&[(0, 2), (3, 4)], 99);
1250        assert_eq!(buf.folds().len(), 2);
1251        // Replace with a different set.
1252        buf.set_auto_folds(&[(1, 4)], 99);
1253        let folds = buf.folds();
1254        assert_eq!(folds.len(), 1, "second call must replace first set");
1255        assert_eq!(folds[0].start_row, 1);
1256        assert!(folds[0].auto_generated);
1257    }
1258
1259    #[test]
1260    fn set_auto_folds_preserves_manual_folds() {
1261        let mut buf = b();
1262        // Add a manual fold.
1263        buf.add_fold(0, 1, true);
1264        // Auto-fold the remaining range.
1265        buf.set_auto_folds(&[(2, 4)], 99);
1266        let folds = buf.folds();
1267        assert_eq!(folds.len(), 2, "manual fold must survive set_auto_folds");
1268        let manual = folds.iter().find(|f| f.start_row == 0).unwrap();
1269        assert!(!manual.auto_generated, "manual fold flag must stay false");
1270        let auto = folds.iter().find(|f| f.start_row == 2).unwrap();
1271        assert!(auto.auto_generated);
1272    }
1273
1274    #[test]
1275    fn set_auto_folds_preserves_same_start_range_states_after_reordering() {
1276        let mut buf = b();
1277        buf.set_auto_folds(&[(0, 4), (0, 2)], 99);
1278        assert!(buf.close_fold_at(1));
1279
1280        buf.set_auto_folds(&[(0, 2), (0, 4)], 99);
1281        assert_eq!(
1282            buf.folds()
1283                .iter()
1284                .map(|fold| (fold.start_row, fold.end_row, fold.closed))
1285                .collect::<Vec<_>>(),
1286            vec![(0, 4, false), (0, 2, true)]
1287        );
1288    }
1289
1290    #[test]
1291    fn set_auto_folds_preserves_open_closed_state_by_exact_range() {
1292        let mut buf = b();
1293        // First auto-fold pass: create a closed fold at row 0.
1294        buf.set_auto_folds(&[(0, 2)], 0); // foldlevelstart=0 → starts closed
1295        assert!(buf.folds()[0].closed, "fold must start closed per default");
1296
1297        // User opens the fold (simulated by toggle).
1298        buf.toggle_fold_at(0);
1299        assert!(!buf.folds()[0].closed, "fold must now be open");
1300
1301        // Second auto-fold pass with the same exact range preserves open state.
1302        buf.set_auto_folds(&[(0, 2)], 0); // foldlevelstart=0 but prev was open
1303        assert!(
1304            !buf.folds()[0].closed,
1305            "open/closed state must be preserved across set_auto_folds"
1306        );
1307    }
1308
1309    #[test]
1310    fn set_auto_folds_skips_single_row_and_inverted_ranges() {
1311        let mut buf = b();
1312        buf.set_auto_folds(&[(1, 1), (3, 2)], 99);
1313        assert!(
1314            buf.folds().is_empty(),
1315            "single-row and inverted ranges must be skipped"
1316        );
1317    }
1318
1319    #[test]
1320    fn set_auto_folds_new_folds_start_per_foldlevelstart() {
1321        let mut buf = b();
1322        buf.set_auto_folds(&[(0, 4)], 0);
1323        assert!(
1324            buf.folds()[0].closed,
1325            "new auto fold must start closed at foldlevelstart=0"
1326        );
1327
1328        // Re-running the same exact range at foldlevelstart=99 must NOT reopen
1329        // it: the snapshot preserves the state the fold already has, and
1330        // `foldlevelstart` only decides how a fold *starts*.
1331        buf.set_auto_folds(&[(0, 4)], 99);
1332        assert!(
1333            buf.folds()[0].closed,
1334            "an existing fold keeps its state — foldlevelstart is start-only"
1335        );
1336
1337        // A brand-new start row takes the option: level 1 <= 99 → open.
1338        let mut buf2 = b();
1339        buf2.set_auto_folds(&[(2, 4)], 99);
1340        assert!(
1341            !buf2.folds()[0].closed,
1342            "brand-new level-1 auto fold must start open at foldlevelstart=99"
1343        );
1344    }
1345
1346    // ── foldlevelstart: level semantics, not a boolean ────────────────────
1347    //
1348    // The expectations below are neovim 0.12.4's, measured rather than
1349    // reasoned: the same nesting opened with `foldmethod=expr` +
1350    // `v:lua.vim.treesitter.foldexpr()` and `foldlevelstart` set to each
1351    // value, with the closed folds enumerated via `foldclosed` /
1352    // `foldclosedend` (`foldlevel()` merges adjacent siblings and reads as a
1353    // false difference). Converted from vim's 1-based lines to 0-based rows.
1354    //
1355    //  row  source                        fold        level
1356    //    2  function M.outer(a, b)        2..16         1
1357    //    3    if a > b then               3..14         2
1358    //    4      local t = {               4..7          3
1359    //   10      for i = 1, 10 do         10..13         3
1360    //   18  function M.second(c)         18..23         1
1361    //   19    while c > 0 do             19..21         2
1362    const NVIM_LUA_RANGES: &[(usize, usize)] =
1363        &[(2, 16), (3, 14), (4, 7), (10, 13), (18, 23), (19, 21)];
1364
1365    /// Closed auto folds, as `(start_row, end_row)`, after one pass at `fls`.
1366    fn closed_at(ranges: &[(usize, usize)], fls: u32) -> Vec<(usize, usize)> {
1367        let mut buf = View::from_str(&"x\n".repeat(26));
1368        buf.set_auto_folds(ranges, fls);
1369        buf.folds()
1370            .iter()
1371            .filter(|f| f.closed)
1372            .map(|f| (f.start_row, f.end_row))
1373            .collect()
1374    }
1375
1376    #[test]
1377    fn set_auto_folds_closes_folds_deeper_than_foldlevelstart() {
1378        // fls=0 → every fold closed (vim: level 1 > 0).
1379        assert_eq!(
1380            closed_at(NVIM_LUA_RANGES, 0),
1381            vec![(2, 16), (3, 14), (4, 7), (10, 13), (18, 23), (19, 21)],
1382            "foldlevelstart=0 must close every fold"
1383        );
1384        // fls=1 → level 1 open, levels 2+ closed. nvim closed 4..15 / 20..22
1385        // (1-based), i.e. the level-2 folds became the outermost closed ones.
1386        assert_eq!(
1387            closed_at(NVIM_LUA_RANGES, 1),
1388            vec![(3, 14), (4, 7), (10, 13), (19, 21)],
1389            "foldlevelstart=1 must open level 1 and close levels 2+"
1390        );
1391        // fls=2 → nvim closed 5..8 / 11..14 (1-based): the level-3 folds only.
1392        assert_eq!(
1393            closed_at(NVIM_LUA_RANGES, 2),
1394            vec![(4, 7), (10, 13)],
1395            "foldlevelstart=2 must close only level 3 and deeper"
1396        );
1397        // fls=3 and fls=99 → nvim closed nothing (max level here is 3).
1398        assert!(
1399            closed_at(NVIM_LUA_RANGES, 3).is_empty(),
1400            "foldlevelstart=3 must leave a 3-level nesting fully open"
1401        );
1402        assert!(
1403            closed_at(NVIM_LUA_RANGES, 99).is_empty(),
1404            "foldlevelstart=99 (hjkl's default) must open everything"
1405        );
1406    }
1407
1408    #[test]
1409    fn set_auto_folds_levels_are_independent_of_range_order() {
1410        // Tree-sitter query order is capture order, not document order: the
1411        // level sweep must sort, not trust the caller.
1412        let mut shuffled = NVIM_LUA_RANGES.to_vec();
1413        shuffled.reverse();
1414        assert_eq!(
1415            closed_at(&shuffled, 1),
1416            closed_at(NVIM_LUA_RANGES, 1),
1417            "nesting level must come from containment, not from range order"
1418        );
1419    }
1420
1421    #[test]
1422    fn set_auto_folds_levels_ignore_dropped_ranges() {
1423        // A single-row range never becomes a fold, so it must not push the
1424        // ranges around it a level deeper.
1425        let mut with_noise = NVIM_LUA_RANGES.to_vec();
1426        with_noise.push((5, 5)); // single row → dropped
1427        with_noise.push((9, 8)); // inverted → dropped
1428        assert_eq!(
1429            closed_at(&with_noise, 1),
1430            closed_at(NVIM_LUA_RANGES, 1),
1431            "ranges that never become folds must not count as a nesting level"
1432        );
1433    }
1434
1435    #[test]
1436    fn set_auto_folds_manual_folds_do_not_add_a_nesting_level() {
1437        // A `zf` fold wrapping the whole file must not make every auto fold
1438        // one level deeper — the auto set's levels are its own.
1439        let mut buf = View::from_str(&"x\n".repeat(26));
1440        buf.add_fold(0, 25, false);
1441        buf.set_auto_folds(NVIM_LUA_RANGES, 1);
1442        let closed: Vec<(usize, usize)> = buf
1443            .folds()
1444            .iter()
1445            .filter(|f| f.auto_generated && f.closed)
1446            .map(|f| (f.start_row, f.end_row))
1447            .collect();
1448        assert_eq!(
1449            closed,
1450            vec![(3, 14), (4, 7), (10, 13), (19, 21)],
1451            "an enclosing manual fold must not shift auto fold levels"
1452        );
1453    }
1454
1455    #[test]
1456    fn set_auto_folds_at_a_level_settles_without_bumping_generations() {
1457        // The mixed open/closed set a non-zero `foldlevelstart` produces has
1458        // to compare equal on the next pass, or the caller's once-per-edit
1459        // guard fires every frame.
1460        let mut buf = View::from_str(&"x\n".repeat(26));
1461        buf.set_auto_folds(NVIM_LUA_RANGES, 1);
1462        let fg = buf.fold_gen();
1463        let dg = buf.dirty_gen();
1464        for _ in 0..5 {
1465            buf.set_auto_folds(NVIM_LUA_RANGES, 1);
1466        }
1467        assert_eq!(
1468            buf.fold_gen(),
1469            fg,
1470            "unchanged fold set must not bump fold_gen"
1471        );
1472        assert_eq!(
1473            buf.dirty_gen(),
1474            dg,
1475            "unchanged fold set must not bump dirty_gen"
1476        );
1477    }
1478
1479    // ── row-delta shifting (audit-r2 fix 1) ───────────────────────────────
1480
1481    fn fold(s: usize, e: usize) -> super::Fold {
1482        super::Fold {
1483            start_row: s,
1484            end_row: e,
1485            closed: true,
1486            auto_generated: false,
1487        }
1488    }
1489
1490    #[test]
1491    fn shift_fold_insert_above_shifts_down() {
1492        // 10-line file, fold at rows 4..6, insert one row at row 0
1493        // (`ggO x<Esc>`): vim shifts the fold to 5..7.
1494        let f = fold(4, 6);
1495        let shifted = super::shift_fold(f, 0, 0, 1, 1).unwrap();
1496        assert_eq!((shifted.start_row, shifted.end_row), (5, 7));
1497    }
1498
1499    #[test]
1500    fn shift_fold_delete_above_shifts_up() {
1501        // Fold at rows 4..6, one row deleted above at row 0.
1502        let f = fold(4, 6);
1503        let shifted = super::shift_fold(f, 0, 1, 1, -1).unwrap();
1504        assert_eq!((shifted.start_row, shifted.end_row), (3, 5));
1505    }
1506
1507    #[test]
1508    fn shift_fold_delete_fully_overlapping_drops() {
1509        // Fold at rows 4..6, deletion covers rows 4..6 entirely.
1510        let f = fold(4, 6);
1511        assert!(super::shift_fold(f, 4, 7, 7, -3).is_none());
1512    }
1513
1514    #[test]
1515    fn shift_fold_delete_overlapping_tail_clips() {
1516        // Fold at rows 4..6, deletion of rows 6..8 (tail only) clips the
1517        // fold to end at the last surviving row.
1518        let f = fold(4, 6);
1519        let shifted = super::shift_fold(f, 6, 9, 9, -3).unwrap();
1520        assert_eq!((shifted.start_row, shifted.end_row), (4, 5));
1521    }
1522
1523    #[test]
1524    fn shift_fold_delete_overlapping_head_clips() {
1525        // Fold at rows 4..6, deletion of rows 3..4 (head only) clips the
1526        // fold to start where the surviving tail now sits.
1527        let f = fold(4, 6);
1528        let shifted = super::shift_fold(f, 3, 5, 5, -2).unwrap();
1529        assert_eq!((shifted.start_row, shifted.end_row), (3, 4));
1530    }
1531
1532    #[test]
1533    fn shift_fold_edit_inside_grows_on_insert() {
1534        // Fold at rows 4..6, a line inserted at row 5 (strictly inside):
1535        // vim grows the fold's end, leaves the start alone.
1536        let f = fold(4, 6);
1537        let shifted = super::shift_fold(f, 5, 5, 6, 1).unwrap();
1538        assert_eq!((shifted.start_row, shifted.end_row), (4, 7));
1539    }
1540
1541    #[test]
1542    fn shift_fold_edit_inside_shrinks_on_delete() {
1543        // Fold at rows 4..8, rows 5..6 deleted (strictly inside): the fold
1544        // shrinks around the deletion instead of clipping or dropping.
1545        let f = fold(4, 8);
1546        let shifted = super::shift_fold(f, 5, 7, 7, -2).unwrap();
1547        assert_eq!((shifted.start_row, shifted.end_row), (4, 6));
1548    }
1549
1550    #[test]
1551    fn shift_fold_unaffected_when_entirely_before_edit() {
1552        let f = fold(1, 2);
1553        let shifted = super::shift_fold(f, 10, 11, 11, 1).unwrap();
1554        assert_eq!((shifted.start_row, shifted.end_row), (1, 2));
1555    }
1556
1557    #[test]
1558    fn shift_fold_zero_delta_is_noop() {
1559        let f = fold(4, 6);
1560        let shifted = super::shift_fold(f, 0, 0, 0, 0).unwrap();
1561        assert_eq!(shifted, f);
1562    }
1563
1564    #[test]
1565    fn shift_folds_after_edit_shifts_vec_in_place() {
1566        let mut folds = vec![fold(4, 6), fold(1, 2)];
1567        // Insert one row at row 0: both folds shift down.
1568        super::shift_folds_after_edit(&mut folds, 0, 0, 1, 1);
1569        let ranges: Vec<(usize, usize)> = folds.iter().map(|f| (f.start_row, f.end_row)).collect();
1570        assert_eq!(ranges, vec![(2, 3), (5, 7)]);
1571    }
1572
1573    #[test]
1574    fn shift_folds_after_edit_restores_canonical_order_after_start_collapse() {
1575        let mut folds = vec![fold(0, 3), fold(1, 5)];
1576
1577        super::shift_folds_after_edit(&mut folds, 0, 2, 2, -2);
1578
1579        assert_eq!(
1580            folds
1581                .iter()
1582                .map(|fold| (fold.start_row, fold.end_row))
1583                .collect::<Vec<_>>(),
1584            vec![(0, 3), (0, 1)]
1585        );
1586        let index = super::FoldIndex::new(&folds);
1587        assert!(index.hides_row(2));
1588    }
1589
1590    #[test]
1591    fn shift_folds_after_edit_deduplicates_exact_range_collisions() {
1592        let mut folds = vec![
1593            super::Fold {
1594                start_row: 0,
1595                end_row: 3,
1596                closed: false,
1597                auto_generated: false,
1598            },
1599            super::Fold {
1600                start_row: 1,
1601                end_row: 3,
1602                closed: true,
1603                auto_generated: true,
1604            },
1605        ];
1606
1607        super::shift_folds_after_edit(&mut folds, 0, 2, 2, -2);
1608
1609        assert_eq!(
1610            folds,
1611            vec![super::Fold {
1612                start_row: 0,
1613                end_row: 1,
1614                closed: false,
1615                auto_generated: false,
1616            }]
1617        );
1618    }
1619
1620    #[test]
1621    fn shift_folds_after_edit_drops_fully_consumed() {
1622        let mut folds = vec![fold(4, 6)];
1623        super::shift_folds_after_edit(&mut folds, 4, 7, 7, -3);
1624        assert!(folds.is_empty());
1625    }
1626
1627    // ── Borrow-style accessors + fold generation (round-2 perf item 10) ───
1628
1629    #[test]
1630    fn with_folds_sees_the_same_data_as_folds() {
1631        let mut buf = b();
1632        // Empty case.
1633        assert_eq!(buf.with_folds(<[super::Fold]>::to_vec), buf.folds());
1634        assert!(!buf.has_folds());
1635
1636        buf.add_fold(0, 1, false);
1637        buf.add_fold(2, 3, true);
1638        assert_eq!(buf.with_folds(<[super::Fold]>::to_vec), buf.folds());
1639        assert!(buf.has_folds());
1640        // And the borrow-style scan agrees with the owning one row by row.
1641        for row in 0..buf.row_count() {
1642            assert_eq!(
1643                buf.with_folds(|f| f.iter().any(|f| f.hides(row))),
1644                buf.folds().iter().any(|f| f.hides(row)),
1645                "row {row}"
1646            );
1647        }
1648    }
1649
1650    #[test]
1651    fn fold_gen_bumps_on_every_mutator() {
1652        fn none(_: &mut View) {}
1653        fn open_fold(b: &mut View) {
1654            b.add_fold(1, 3, false);
1655        }
1656        fn closed_fold(b: &mut View) {
1657            b.add_fold(1, 3, true);
1658        }
1659        /// (name, setup, mutation-that-must-bump)
1660        type Case = (&'static str, fn(&mut View), fn(&mut View));
1661        let cases: [Case; 14] = [
1662            ("add_fold", none, |b| b.add_fold(1, 3, false)),
1663            ("set_auto_folds", none, |b| b.set_auto_folds(&[(1, 3)], 99)),
1664            ("remove_fold_at", open_fold, |b| {
1665                assert!(b.remove_fold_at(2));
1666            }),
1667            ("open_fold_at", closed_fold, |b| {
1668                assert!(b.open_fold_at(2));
1669            }),
1670            ("close_fold_at", open_fold, |b| {
1671                assert!(b.close_fold_at(2));
1672            }),
1673            ("toggle_fold_at", open_fold, |b| {
1674                assert!(b.toggle_fold_at(2));
1675            }),
1676            ("open_all_folds", closed_fold, View::open_all_folds),
1677            ("close_all_folds", open_fold, View::close_all_folds),
1678            ("clear_all_folds", open_fold, View::clear_all_folds),
1679            ("reveal_row", closed_fold, |b| {
1680                assert!(b.reveal_row(2));
1681            }),
1682            ("open_folds_containing", closed_fold, |b| {
1683                assert!(b.open_folds_containing(1));
1684            }),
1685            ("invalidate_folds_in_range", closed_fold, |b| {
1686                b.invalidate_folds_in_range(2, 2);
1687            }),
1688            ("rebase_folds", closed_fold, |b| b.rebase_folds(0, 0, 1, 1)),
1689            ("set_folds", none, |b| {
1690                b.set_folds(&[super::Fold {
1691                    start_row: 1,
1692                    end_row: 3,
1693                    closed: true,
1694                    auto_generated: false,
1695                }]);
1696            }),
1697        ];
1698        for (name, setup, mutate) in cases {
1699            let mut buf = b();
1700            setup(&mut buf);
1701            let before = buf.fold_gen();
1702            mutate(&mut buf);
1703            assert!(
1704                buf.fold_gen() > before,
1705                "{name} mutated the fold set and must bump fold_gen"
1706            );
1707        }
1708    }
1709
1710    #[test]
1711    fn fold_gen_is_stable_across_reads_and_plain_text_edits() {
1712        let mut buf = b();
1713        buf.add_fold(1, 3, true);
1714        let fg = buf.fold_gen();
1715        assert!(fg > 0);
1716
1717        // Pure reads.
1718        let _ = buf.folds();
1719        let _ = buf.with_folds(<[super::Fold]>::to_vec);
1720        let _ = buf.has_folds();
1721        let _ = buf.is_row_hidden(2);
1722        let _ = buf.fold_at_row(2);
1723        let _ = buf.next_visible_row(1);
1724        let _ = buf.prev_visible_row(4);
1725        assert_eq!(buf.fold_gen(), fg, "read-only queries must not bump");
1726
1727        // No-op mutators (nothing actually changes).
1728        buf.close_all_folds(); // already closed
1729        buf.add_fold(9, 9, true); // out of bounds → rejected
1730        buf.rebase_folds(0, 0, 1, 0); // delta == 0 → early return
1731        let same = buf.folds();
1732        buf.set_folds(&same); // identical set
1733        assert_eq!(buf.fold_gen(), fg, "no-op mutators must not bump");
1734
1735        // A plain text edit must not masquerade as a fold change — that is
1736        // the whole reason `fold_gen` is separate from `dirty_gen`.
1737        let dg = buf.dirty_gen();
1738        buf.apply_edit(crate::Edit::InsertChar {
1739            at: crate::Position { row: 0, col: 0 },
1740            ch: 'x',
1741        });
1742        assert_ne!(buf.dirty_gen(), dg, "text edit must bump dirty_gen");
1743        assert_eq!(buf.fold_gen(), fg, "text edit must not bump fold_gen");
1744    }
1745
1746    #[test]
1747    fn rebase_folds_shifts_buffer_fold_storage() {
1748        let mut buf = View::from_str("0\n1\n2\n3\n4\n5\n6\n7\n8\n9");
1749        buf.add_fold(4, 6, true);
1750        buf.rebase_folds(0, 0, 1, 1);
1751        let folds = buf.folds();
1752        assert_eq!(folds.len(), 1);
1753        assert_eq!((folds[0].start_row, folds[0].end_row), (5, 7));
1754    }
1755}