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
66impl crate::View {
67    /// Returns a snapshot of all folds as an owned `Vec<Fold>`.
68    ///
69    /// Owned rather than `&[Fold]` because a `View` is a per-window
70    /// view onto a shared `Buffer`; another view could mutate the folds vec
71    /// between when this returns and when the caller reads the slice.
72    pub fn folds(&self) -> Vec<Fold> {
73        self.content_lock().folds.clone()
74    }
75
76    /// Run `f` against the fold list under a **single** content lock, with
77    /// no clone. The borrow-style twin of [`Self::folds`] — prefer it for
78    /// every read-only query (`hides` scans, `is_empty`, per-row loops);
79    /// keep [`Self::folds`] only where an owned snapshot must outlive the
80    /// lock (e.g. it is stored, or the buffer is re-borrowed mutably).
81    ///
82    /// The closure runs with the content mutex held: it must not call back
83    /// into any `&self` method of this `View` (they all re-lock, and the
84    /// mutex is not re-entrant). Hoist such reads — `row_count()`,
85    /// `cursor()`, … — above the call.
86    pub fn with_folds<T>(&self, f: impl FnOnce(&[Fold]) -> T) -> T {
87        f(&self.content_lock().folds)
88    }
89
90    /// True when at least one fold is defined (open or closed). One lock,
91    /// no clone — the cheap form of `!folds().is_empty()`.
92    pub fn has_folds(&self) -> bool {
93        !self.content_lock().folds.is_empty()
94    }
95
96    /// Monotonic fold-mutation generation. Bumps only when a fold mutator
97    /// actually changes the fold set; read-only queries and plain text
98    /// edits leave it alone. Hosts caching a fold snapshot (`hjkl`'s
99    /// per-window `window_folds`) compare this instead of re-cloning the
100    /// fold `Vec` every keystroke.
101    ///
102    /// Conservative in the same sense as [`Self::dirty_gen`]: "if it
103    /// changed, the folds **may** have changed" (a `rebase_folds` whose
104    /// row-shift happens to move nothing still bumps).
105    pub fn fold_gen(&self) -> u64 {
106        self.content_lock().fold_gen
107    }
108
109    /// Record a fold-set mutation: bump the fold generation *and* the
110    /// render-cache generation (a fold change repaints). Every mutator in
111    /// this module funnels through here.
112    fn folds_changed(&mut self) {
113        self.fold_gen_bump();
114        self.dirty_gen_bump();
115    }
116
117    /// Register a new fold. If an existing fold has the same
118    /// `start_row`, it's replaced; otherwise the new one is inserted
119    /// in start-row order. Empty / inverted ranges are rejected.
120    pub fn add_fold(&mut self, start_row: usize, end_row: usize, closed: bool) {
121        if end_row < start_row {
122            return;
123        }
124        let last = self.row_count().saturating_sub(1);
125        if start_row > last {
126            return;
127        }
128        let end_row = end_row.min(last);
129        let fold = Fold {
130            start_row,
131            end_row,
132            closed,
133            auto_generated: false,
134        };
135        {
136            let mut c = self.content_lock_mut();
137            if let Some(idx) = c.folds.iter().position(|f| f.start_row == start_row) {
138                c.folds[idx] = fold;
139            } else {
140                let pos = c
141                    .folds
142                    .iter()
143                    .position(|f| f.start_row > start_row)
144                    .unwrap_or(c.folds.len());
145                c.folds.insert(pos, fold);
146            }
147        }
148        self.folds_changed();
149    }
150
151    /// Replace all auto-generated folds with a new set derived from
152    /// `ranges`, while leaving manual folds untouched.
153    ///
154    /// ## Algorithm (O(N) — bounded by `ranges.len()`, no unbounded growth)
155    ///
156    /// 1. Snapshot `start_row → closed` for every existing auto fold so
157    ///    open/closed state survives a reparse.
158    /// 2. Retain only manual folds (`auto_generated == false`).
159    /// 3. Insert one new `Fold` per range, re-using the snapshotted closed
160    ///    state when the start_row existed before, else `default_closed`.
161    ///
162    /// Invariants preserved:
163    /// - Folds stay sorted by `start_row` (same ordering as `add_fold`).
164    /// - Duplicate start_rows: the last range in `ranges` wins (consistent
165    ///   with `add_fold`'s replace-on-same-start-row semantics). In practice
166    ///   TS query ranges are already deduplicated.
167    /// - Empty / inverted ranges (end_row < start_row) are silently skipped.
168    /// - `end_row` is clamped to the last valid row, same as `add_fold`.
169    pub fn set_auto_folds(&mut self, ranges: &[(usize, usize)], default_closed: bool) {
170        // 1. Snapshot closed state of existing auto folds by start_row.
171        let prev_closed: std::collections::HashMap<usize, bool> = self
172            .content_lock()
173            .folds
174            .iter()
175            .filter(|f| f.auto_generated)
176            .map(|f| (f.start_row, f.closed))
177            .collect();
178
179        // 2. Retain manual folds only.
180        {
181            let mut c = self.content_lock_mut();
182            c.folds.retain(|f| !f.auto_generated);
183        }
184
185        // 3. Insert new auto folds in sorted order.
186        let last = self.row_count().saturating_sub(1);
187        for &(start_row, end_row) in ranges {
188            // Skip empty/inverted and out-of-bounds ranges.
189            if end_row < start_row || start_row > last {
190                continue;
191            }
192            let end_row = end_row.min(last);
193            // Only folds spanning more than one row are meaningful.
194            if end_row == start_row {
195                continue;
196            }
197            let closed = prev_closed
198                .get(&start_row)
199                .copied()
200                .unwrap_or(default_closed);
201            let fold = Fold {
202                start_row,
203                end_row,
204                closed,
205                auto_generated: true,
206            };
207            let mut c = self.content_lock_mut();
208            // Replace any existing fold at this start_row (manual or auto).
209            if let Some(idx) = c.folds.iter().position(|f| f.start_row == start_row) {
210                c.folds[idx] = fold;
211            } else {
212                let pos = c
213                    .folds
214                    .iter()
215                    .position(|f| f.start_row > start_row)
216                    .unwrap_or(c.folds.len());
217                c.folds.insert(pos, fold);
218            }
219        }
220
221        self.folds_changed();
222    }
223
224    /// Drop the fold whose range covers `row`. Returns `true` when a
225    /// fold was actually removed.
226    pub fn remove_fold_at(&mut self, row: usize) -> bool {
227        // Remove the INNERMOST fold containing `row` (largest start_row), so
228        // `zd` on a nested fold drops the inner one, not the enclosing block.
229        let idx = self
230            .content_lock()
231            .folds
232            .iter()
233            .enumerate()
234            .filter(|(_, f)| f.contains(row))
235            .max_by_key(|(_, f)| f.start_row)
236            .map(|(i, _)| i);
237        let Some(idx) = idx else {
238            return false;
239        };
240        self.content_lock_mut().folds.remove(idx);
241        self.folds_changed();
242        true
243    }
244
245    /// Open the fold at `row` (no-op if already open or no fold).
246    pub fn open_fold_at(&mut self, row: usize) -> bool {
247        let changed = {
248            let mut c = self.content_lock_mut();
249            let Some(f) = c
250                .folds
251                .iter_mut()
252                .filter(|f| f.contains(row))
253                .max_by_key(|f| f.start_row)
254            else {
255                return false;
256            };
257            if !f.closed {
258                return false;
259            }
260            f.closed = false;
261            true
262        };
263        if changed {
264            self.folds_changed();
265        }
266        changed
267    }
268
269    /// Close the fold at `row` (no-op if already closed or no fold).
270    pub fn close_fold_at(&mut self, row: usize) -> bool {
271        let changed = {
272            let mut c = self.content_lock_mut();
273            let Some(f) = c
274                .folds
275                .iter_mut()
276                .filter(|f| f.contains(row))
277                .max_by_key(|f| f.start_row)
278            else {
279                return false;
280            };
281            if f.closed {
282                return false;
283            }
284            f.closed = true;
285            true
286        };
287        if changed {
288            self.folds_changed();
289        }
290        changed
291    }
292
293    /// Flip the closed/open state of the fold containing `row`.
294    pub fn toggle_fold_at(&mut self, row: usize) -> bool {
295        let changed = {
296            let mut c = self.content_lock_mut();
297            let Some(f) = c
298                .folds
299                .iter_mut()
300                .filter(|f| f.contains(row))
301                .max_by_key(|f| f.start_row)
302            else {
303                return false;
304            };
305            f.closed = !f.closed;
306            true
307        };
308        if changed {
309            self.folds_changed();
310        }
311        changed
312    }
313
314    /// `zR` — open every fold.
315    pub fn open_all_folds(&mut self) {
316        let changed = {
317            let mut c = self.content_lock_mut();
318            let mut any = false;
319            for f in c.folds.iter_mut() {
320                if f.closed {
321                    f.closed = false;
322                    any = true;
323                }
324            }
325            any
326        };
327        if changed {
328            self.folds_changed();
329        }
330    }
331
332    /// `zE` — eliminate every fold.
333    pub fn clear_all_folds(&mut self) {
334        let was_nonempty = !self.content_lock().folds.is_empty();
335        if was_nonempty {
336            self.content_lock_mut().folds.clear();
337            self.folds_changed();
338        }
339    }
340
341    /// `zM` — close every fold.
342    pub fn close_all_folds(&mut self) {
343        let changed = {
344            let mut c = self.content_lock_mut();
345            let mut any = false;
346            for f in c.folds.iter_mut() {
347                if !f.closed {
348                    f.closed = true;
349                    any = true;
350                }
351            }
352            any
353        };
354        if changed {
355            self.folds_changed();
356        }
357    }
358
359    /// First fold whose range contains `row`. Useful for the host's
360    /// `za`/`zo`/`zc` handlers.
361    pub fn fold_at_row(&self, row: usize) -> Option<Fold> {
362        // Innermost fold containing `row`: with nested folds, the one with the
363        // largest `start_row` is the most-deeply-nested. Folds are stored in
364        // start-row order, so a plain `.find` would return the OUTERMOST fold
365        // and `zc`/`za`/`zo` would act on the wrong level.
366        self.content_lock()
367            .folds
368            .iter()
369            .filter(|f| f.contains(row))
370            .max_by_key(|f| f.start_row)
371            .copied()
372    }
373
374    /// True iff `row` is hidden by a closed fold (any fold).
375    pub fn is_row_hidden(&self, row: usize) -> bool {
376        self.with_folds(|folds| folds.iter().any(|f| f.hides(row)))
377    }
378
379    /// Open every closed fold whose body hides `row`, so the row becomes
380    /// visible. Handles nested folds in a single pass — unlike
381    /// `open_fold_at` / `FoldOp::OpenAt`, which only act on the first fold
382    /// containing the row and so can never reach a nested inner fold.
383    /// Used by `goto_line` so a jump into a folded region reveals the
384    /// target line instead of stranding the cursor on a hidden row.
385    /// Returns `true` if any fold was opened.
386    pub fn reveal_row(&mut self, row: usize) -> bool {
387        let changed = {
388            let mut c = self.content_lock_mut();
389            let mut any = false;
390            for f in c.folds.iter_mut() {
391                if f.hides(row) {
392                    f.closed = false;
393                    any = true;
394                }
395            }
396            any
397        };
398        if changed {
399            self.folds_changed();
400        }
401        changed
402    }
403
404    /// Last row index containing real content — skips vim's single
405    /// phantom trailing empty row. `ropey`'s `len_lines()` always
406    /// synthesizes one extra empty final "line" when the buffer text
407    /// ends in `\n` (vim treats that `\n` as a terminator, not a
408    /// separator). Mirrors `hjkl_engine::motions::move_bottom`'s clamp
409    /// (`G`) so vertical motions agree with `G` on where the buffer
410    /// "ends". A buffer whose *real* last line happens to be empty
411    /// (e.g. `"foo\n\n"`, row 1) is untouched — only a single trailing
412    /// phantom row is ever skipped.
413    ///
414    /// The emptiness test reads the last row's byte length rather than
415    /// materializing the row as a `String`: for the final rope line the two
416    /// agree exactly (ropey never gives the last line a trailing `\n`, so
417    /// `rope_line_str` has nothing to strip and
418    /// `rope_line_bytes(last) == 0` iff `rope_line_str(last).is_empty()`).
419    pub fn last_content_row(&self) -> usize {
420        let raw_last = self.row_count().saturating_sub(1);
421        if raw_last > 0 {
422            let c = self.content_lock();
423            if crate::buffer::rope_line_bytes(&c.text, raw_last) == 0 {
424                return raw_last - 1;
425            }
426        }
427        raw_last
428    }
429
430    /// First visible row strictly after `row`, skipping any rows hidden
431    /// by closed folds. Returns `None` past the end of the buffer.
432    ///
433    /// Takes the content lock **once** for the whole walk (via
434    /// [`Self::with_folds`]) instead of once per skipped row — `j` over a
435    /// long closed fold used to pay a lock + full `Vec<Fold>` clone per row.
436    /// `last_content_row()` locks too, so it is resolved before the scan.
437    pub fn next_visible_row(&self, row: usize) -> Option<usize> {
438        let last = self.last_content_row();
439        if last == 0 && row == 0 {
440            return None;
441        }
442        let mut r = row.checked_add(1)?;
443        self.with_folds(|folds| {
444            while r <= last && folds.iter().any(|f| f.hides(r)) {
445                r += 1;
446            }
447            (r <= last).then_some(r)
448        })
449    }
450
451    /// First visible row strictly before `row`, skipping hidden rows.
452    ///
453    /// One lock for the whole walk, same as [`Self::next_visible_row`].
454    pub fn prev_visible_row(&self, row: usize) -> Option<usize> {
455        let mut r = row.checked_sub(1)?;
456        self.with_folds(|folds| {
457            while folds.iter().any(|f| f.hides(r)) {
458                r = r.checked_sub(1)?;
459            }
460            Some(r)
461        })
462    }
463
464    /// Drop every fold that touches `[start_row, end_row]`.
465    pub fn invalidate_folds_in_range(&mut self, start_row: usize, end_row: usize) {
466        let before = self.content_lock().folds.len();
467        invalidate_folds(&mut self.content_lock_mut().folds, start_row, end_row);
468        if self.content_lock().folds.len() != before {
469            self.folds_changed();
470        }
471    }
472
473    /// Shift every buffer fold by an edit's row-delta band. Mirrors
474    /// [`crate::buffer::View::rebase_marks`] for the shared fold storage —
475    /// see [`shift_folds_after_edit`] for the per-fold rules.
476    pub fn rebase_folds(
477        &mut self,
478        edit_start: usize,
479        drop_end: usize,
480        shift_threshold: usize,
481        delta: isize,
482    ) {
483        if delta == 0 {
484            return;
485        }
486        let touched = {
487            let mut c = self.content_lock_mut();
488            if c.folds.is_empty() {
489                false
490            } else {
491                shift_folds_after_edit(&mut c.folds, edit_start, drop_end, shift_threshold, delta);
492                true
493            }
494        };
495        if touched {
496            // Conservative: a shift that happened to move nothing (every
497            // fold entirely below the edit) still bumps. Matches the
498            // `dirty_gen` contract — "may have changed".
499            self.fold_gen_bump();
500        }
501    }
502
503    /// Replace the entire fold set wholesale. Used to install a per-window fold
504    /// snapshot into the shared buffer on focus change (window-level folds): the
505    /// app keeps each window's open/closed state and swaps it in before dispatch,
506    /// so motions/render/`z`-ops operate on the focused window's folds.
507    pub fn set_folds(&mut self, folds: &[Fold]) {
508        {
509            let mut c = self.content_lock_mut();
510            if c.folds.as_slice() == folds {
511                return; // no-op — avoid a spurious dirty_gen bump
512            }
513            c.folds = folds.to_vec();
514        }
515        self.folds_changed();
516    }
517}
518
519/// Drop every fold in `folds` that touches `[start_row, end_row]`, in place.
520///
521/// Free helper so both [`crate::View::invalidate_folds_in_range`] (operating
522/// on the shared content) and the app's window-level edit-coherence pass
523/// (operating on a sibling window's owned `Vec<Fold>`) share one rule — vim
524/// opens/forgets any fold the edit overlapped.
525pub fn invalidate_folds(folds: &mut Vec<Fold>, start_row: usize, end_row: usize) {
526    folds.retain(|f| f.end_row < start_row || f.start_row > end_row);
527}
528
529// ── Row-delta shifting (edit-coherence) ──────────────────────────────────
530//
531// A manual (`zf`) fold is a row-range that has to track the same
532// insert/delete row-shift the engine already applies to marks and the
533// jumplist (see `Editor::shift_marks_after_edit`). Without this, a fold
534// below an edit keeps stale row numbers and the renderer / fold-aware ops
535// (`dd`, `p`, …) act on the wrong rows (#audit-r2 fix 1).
536//
537// The four `(edit_start, drop_end, shift_threshold, delta)` parameters are
538// the exact same band description `Editor::shift_marks_after_edit` computes
539// for marks: `[edit_start, drop_end)` is the row band the edit deleted
540// (empty for inserts), and any row `>= shift_threshold` moves by `delta`.
541// Reusing the identical band keeps folds, marks, and jumplist entries
542// shifting in lockstep for the same edit.
543
544/// Shift a single fold's `start_row` / `end_row` by an edit's row-delta band.
545/// Returns `None` when the edit's deleted band fully consumes the fold.
546///
547/// Each endpoint is mapped independently through the same drop/shift rule
548/// [`crate::buffer::View::rebase_marks`] applies to a point mark. Mapping
549/// the two endpoints independently is what produces the vim-shaped "edit
550/// inside a fold" semantics for free:
551/// - Both endpoints below `shift_threshold` and outside the deleted band →
552///   fold untouched (edit happened entirely outside the fold).
553/// - `start_row` outside the deleted band but `end_row` inside it → the
554///   edit deleted the fold's tail; it clips to end at the last surviving
555///   row (`edit_start - 1`).
556/// - `start_row` inside the deleted band but `end_row` outside it → the
557///   edit deleted the fold's head; it clips to start at `edit_start` (the
558///   row the surviving tail now occupies).
559/// - Both endpoints inside the deleted band → the edit consumed the whole
560///   fold; it's dropped.
561/// - `start_row` below the threshold and `end_row` at/above it (an insert
562///   or a deletion landing strictly inside the fold) → `start_row` stays,
563///   `end_row` shifts by `delta`: the fold grows (insert) or shrinks
564///   (delete) around the edit, matching vim.
565/// - Both endpoints at/above the threshold → the fold shifts wholesale.
566pub fn shift_fold(
567    fold: Fold,
568    edit_start: usize,
569    drop_end: usize,
570    shift_threshold: usize,
571    delta: isize,
572) -> Option<Fold> {
573    if delta == 0 {
574        return Some(fold);
575    }
576    let map_row = |row: usize| -> Option<usize> {
577        if (edit_start..drop_end).contains(&row) {
578            None
579        } else if row >= shift_threshold {
580            Some(((row as isize) + delta).max(0) as usize)
581        } else {
582            Some(row)
583        }
584    };
585    let mapped_start = map_row(fold.start_row);
586    let mapped_end = map_row(fold.end_row);
587    if mapped_start.is_none() && mapped_end.is_none() {
588        return None;
589    }
590    let new_start = mapped_start.unwrap_or(edit_start);
591    let new_end = mapped_end.unwrap_or_else(|| edit_start.saturating_sub(1));
592    if new_end < new_start {
593        return None;
594    }
595    Some(Fold {
596        start_row: new_start,
597        end_row: new_end,
598        closed: fold.closed,
599        auto_generated: fold.auto_generated,
600    })
601}
602
603/// Shift every fold in `folds` by an edit's row-delta band, in place.
604/// Folds the edit's deleted band fully consumes are dropped (mirrors
605/// [`invalidate_folds`] for the folds that DO survive but move).
606///
607/// Shared by [`crate::View::rebase_folds`] (engine-side, the buffer's own
608/// fold storage) and the app's sibling-window fold snapshot shift, so both
609/// converge on the identical row-shift rule.
610pub fn shift_folds_after_edit(
611    folds: &mut Vec<Fold>,
612    edit_start: usize,
613    drop_end: usize,
614    shift_threshold: usize,
615    delta: isize,
616) {
617    if delta == 0 {
618        return;
619    }
620    folds.retain_mut(
621        |f| match shift_fold(*f, edit_start, drop_end, shift_threshold, delta) {
622            Some(shifted) => {
623                *f = shifted;
624                true
625            }
626            None => false,
627        },
628    );
629}
630
631#[cfg(test)]
632mod tests {
633    use crate::View;
634
635    fn b() -> View {
636        View::from_str("a\nb\nc\nd\ne")
637    }
638
639    #[test]
640    fn add_keeps_folds_in_start_row_order() {
641        let mut buf = b();
642        buf.add_fold(2, 3, true);
643        buf.add_fold(0, 1, false);
644        let starts: Vec<usize> = buf.folds().iter().map(|f| f.start_row).collect();
645        assert_eq!(starts, vec![0, 2]);
646    }
647
648    #[test]
649    fn set_folds_replaces_wholesale() {
650        let mut buf = b();
651        buf.add_fold(0, 1, false);
652        // Install a different per-window snapshot.
653        let snapshot = vec![super::Fold {
654            start_row: 2,
655            end_row: 3,
656            closed: true,
657            auto_generated: false,
658        }];
659        buf.set_folds(&snapshot);
660        assert_eq!(buf.folds(), snapshot);
661        // Idempotent: re-installing the same set is a no-op (no dirty bump).
662        let dg = buf.dirty_gen();
663        buf.set_folds(&snapshot);
664        assert_eq!(buf.dirty_gen(), dg);
665    }
666
667    #[test]
668    fn invalidate_folds_helper_drops_overlapping() {
669        let f = |s, e| super::Fold {
670            start_row: s,
671            end_row: e,
672            closed: true,
673            auto_generated: false,
674        };
675        let mut folds = vec![f(0, 2), f(4, 6), f(8, 10)];
676        // Edit touches rows 5..5 → only the [4,6] fold overlaps.
677        super::invalidate_folds(&mut folds, 5, 5);
678        let starts: Vec<usize> = folds.iter().map(|x| x.start_row).collect();
679        assert_eq!(starts, vec![0, 8]);
680    }
681
682    #[test]
683    fn add_replaces_existing_with_same_start_row() {
684        let mut buf = b();
685        buf.add_fold(1, 2, true);
686        buf.add_fold(1, 4, false);
687        assert_eq!(buf.folds().len(), 1);
688        assert_eq!(buf.folds()[0].end_row, 4);
689        assert!(!buf.folds()[0].closed);
690    }
691
692    #[test]
693    fn add_clamps_end_row_to_buffer_bounds() {
694        let mut buf = b();
695        buf.add_fold(2, 99, true);
696        assert_eq!(buf.folds()[0].end_row, 4);
697    }
698
699    #[test]
700    fn add_rejects_inverted_range() {
701        let mut buf = b();
702        buf.add_fold(3, 1, true);
703        assert!(buf.folds().is_empty());
704    }
705
706    #[test]
707    fn toggle_flips_state() {
708        let mut buf = b();
709        buf.add_fold(1, 3, false);
710        assert!(!buf.folds()[0].closed);
711        assert!(buf.toggle_fold_at(2));
712        assert!(buf.folds()[0].closed);
713        assert!(buf.toggle_fold_at(2));
714        assert!(!buf.folds()[0].closed);
715    }
716
717    #[test]
718    fn is_row_hidden_excludes_start_row() {
719        let mut buf = b();
720        buf.add_fold(1, 3, true);
721        assert!(!buf.is_row_hidden(0));
722        assert!(!buf.is_row_hidden(1)); // start row stays visible
723        assert!(buf.is_row_hidden(2));
724        assert!(buf.is_row_hidden(3));
725        assert!(!buf.is_row_hidden(4));
726    }
727
728    #[test]
729    fn open_close_all_changes_every_fold() {
730        let mut buf = b();
731        buf.add_fold(0, 1, false);
732        buf.add_fold(2, 3, true);
733        buf.close_all_folds();
734        assert!(buf.folds().iter().all(|f| f.closed));
735        buf.open_all_folds();
736        assert!(buf.folds().iter().all(|f| !f.closed));
737    }
738
739    #[test]
740    fn invalidate_drops_overlapping_folds() {
741        let mut buf = b();
742        buf.add_fold(0, 1, true);
743        buf.add_fold(2, 3, true);
744        buf.add_fold(4, 4, true);
745        buf.invalidate_folds_in_range(2, 3);
746        let starts: Vec<usize> = buf.folds().iter().map(|f| f.start_row).collect();
747        assert_eq!(starts, vec![0, 4]);
748    }
749
750    // ── auto_generated flag + set_auto_folds ─────────────────────────────────
751
752    #[test]
753    fn add_fold_sets_auto_generated_false() {
754        let mut buf = b();
755        buf.add_fold(1, 3, false);
756        assert!(
757            !buf.folds()[0].auto_generated,
758            "manual add_fold must have auto_generated=false"
759        );
760    }
761
762    #[test]
763    fn set_auto_folds_adds_auto_folds() {
764        let mut buf = b();
765        buf.set_auto_folds(&[(0, 2), (3, 4)], false);
766        let folds = buf.folds();
767        assert_eq!(folds.len(), 2);
768        assert!(folds[0].auto_generated);
769        assert!(folds[1].auto_generated);
770        assert_eq!(folds[0].start_row, 0);
771        assert_eq!(folds[1].start_row, 3);
772    }
773
774    #[test]
775    fn set_auto_folds_second_call_replaces_first() {
776        let mut buf = b();
777        buf.set_auto_folds(&[(0, 2), (3, 4)], false);
778        assert_eq!(buf.folds().len(), 2);
779        // Replace with a different set.
780        buf.set_auto_folds(&[(1, 4)], false);
781        let folds = buf.folds();
782        assert_eq!(folds.len(), 1, "second call must replace first set");
783        assert_eq!(folds[0].start_row, 1);
784        assert!(folds[0].auto_generated);
785    }
786
787    #[test]
788    fn set_auto_folds_preserves_manual_folds() {
789        let mut buf = b();
790        // Add a manual fold.
791        buf.add_fold(0, 1, true);
792        // Auto-fold the remaining range.
793        buf.set_auto_folds(&[(2, 4)], false);
794        let folds = buf.folds();
795        assert_eq!(folds.len(), 2, "manual fold must survive set_auto_folds");
796        let manual = folds.iter().find(|f| f.start_row == 0).unwrap();
797        assert!(!manual.auto_generated, "manual fold flag must stay false");
798        let auto = folds.iter().find(|f| f.start_row == 2).unwrap();
799        assert!(auto.auto_generated);
800    }
801
802    #[test]
803    fn set_auto_folds_preserves_open_closed_state_by_start_row() {
804        let mut buf = b();
805        // First auto-fold pass: create a closed fold at row 0.
806        buf.set_auto_folds(&[(0, 2)], true); // default_closed=true → starts closed
807        assert!(buf.folds()[0].closed, "fold must start closed per default");
808
809        // User opens the fold (simulated by toggle).
810        buf.toggle_fold_at(0);
811        assert!(!buf.folds()[0].closed, "fold must now be open");
812
813        // Second auto-fold pass with same start_row — must preserve open state.
814        buf.set_auto_folds(&[(0, 2)], true); // default_closed=true but prev was open
815        assert!(
816            !buf.folds()[0].closed,
817            "open/closed state must be preserved across set_auto_folds"
818        );
819    }
820
821    #[test]
822    fn set_auto_folds_skips_single_row_and_inverted_ranges() {
823        let mut buf = b();
824        buf.set_auto_folds(&[(1, 1), (3, 2)], false);
825        assert!(
826            buf.folds().is_empty(),
827            "single-row and inverted ranges must be skipped"
828        );
829    }
830
831    #[test]
832    fn set_auto_folds_new_folds_use_default_closed() {
833        let mut buf = b();
834        buf.set_auto_folds(&[(0, 4)], true);
835        assert!(
836            buf.folds()[0].closed,
837            "new auto fold must use default_closed=true"
838        );
839
840        // Clear and re-run with default_closed=false.
841        buf.set_auto_folds(&[(0, 4)], false);
842        // This is a *new* start_row (it was removed + re-added), BUT the
843        // snapshot preserved the previous state (closed=true from above)
844        // because the start_row is the same.
845        // Wait — the test verifies the preservation path, not the default path.
846        // Let's use a fresh start_row to test the default path:
847        let mut buf2 = b();
848        buf2.set_auto_folds(&[(2, 4)], false);
849        assert!(
850            !buf2.folds()[0].closed,
851            "brand-new auto fold must start open when default_closed=false"
852        );
853    }
854
855    // ── row-delta shifting (audit-r2 fix 1) ───────────────────────────────
856
857    fn fold(s: usize, e: usize) -> super::Fold {
858        super::Fold {
859            start_row: s,
860            end_row: e,
861            closed: true,
862            auto_generated: false,
863        }
864    }
865
866    #[test]
867    fn shift_fold_insert_above_shifts_down() {
868        // 10-line file, fold at rows 4..6, insert one row at row 0
869        // (`ggO x<Esc>`): vim shifts the fold to 5..7.
870        let f = fold(4, 6);
871        let shifted = super::shift_fold(f, 0, 0, 1, 1).unwrap();
872        assert_eq!((shifted.start_row, shifted.end_row), (5, 7));
873    }
874
875    #[test]
876    fn shift_fold_delete_above_shifts_up() {
877        // Fold at rows 4..6, one row deleted above at row 0.
878        let f = fold(4, 6);
879        let shifted = super::shift_fold(f, 0, 1, 1, -1).unwrap();
880        assert_eq!((shifted.start_row, shifted.end_row), (3, 5));
881    }
882
883    #[test]
884    fn shift_fold_delete_fully_overlapping_drops() {
885        // Fold at rows 4..6, deletion covers rows 4..6 entirely.
886        let f = fold(4, 6);
887        assert!(super::shift_fold(f, 4, 7, 7, -3).is_none());
888    }
889
890    #[test]
891    fn shift_fold_delete_overlapping_tail_clips() {
892        // Fold at rows 4..6, deletion of rows 6..8 (tail only) clips the
893        // fold to end at the last surviving row.
894        let f = fold(4, 6);
895        let shifted = super::shift_fold(f, 6, 9, 9, -3).unwrap();
896        assert_eq!((shifted.start_row, shifted.end_row), (4, 5));
897    }
898
899    #[test]
900    fn shift_fold_delete_overlapping_head_clips() {
901        // Fold at rows 4..6, deletion of rows 3..4 (head only) clips the
902        // fold to start where the surviving tail now sits.
903        let f = fold(4, 6);
904        let shifted = super::shift_fold(f, 3, 5, 5, -2).unwrap();
905        assert_eq!((shifted.start_row, shifted.end_row), (3, 4));
906    }
907
908    #[test]
909    fn shift_fold_edit_inside_grows_on_insert() {
910        // Fold at rows 4..6, a line inserted at row 5 (strictly inside):
911        // vim grows the fold's end, leaves the start alone.
912        let f = fold(4, 6);
913        let shifted = super::shift_fold(f, 5, 5, 6, 1).unwrap();
914        assert_eq!((shifted.start_row, shifted.end_row), (4, 7));
915    }
916
917    #[test]
918    fn shift_fold_edit_inside_shrinks_on_delete() {
919        // Fold at rows 4..8, rows 5..6 deleted (strictly inside): the fold
920        // shrinks around the deletion instead of clipping or dropping.
921        let f = fold(4, 8);
922        let shifted = super::shift_fold(f, 5, 7, 7, -2).unwrap();
923        assert_eq!((shifted.start_row, shifted.end_row), (4, 6));
924    }
925
926    #[test]
927    fn shift_fold_unaffected_when_entirely_before_edit() {
928        let f = fold(1, 2);
929        let shifted = super::shift_fold(f, 10, 11, 11, 1).unwrap();
930        assert_eq!((shifted.start_row, shifted.end_row), (1, 2));
931    }
932
933    #[test]
934    fn shift_fold_zero_delta_is_noop() {
935        let f = fold(4, 6);
936        let shifted = super::shift_fold(f, 0, 0, 0, 0).unwrap();
937        assert_eq!(shifted, f);
938    }
939
940    #[test]
941    fn shift_folds_after_edit_shifts_vec_in_place() {
942        let mut folds = vec![fold(4, 6), fold(1, 2)];
943        // Insert one row at row 0: both folds shift down.
944        super::shift_folds_after_edit(&mut folds, 0, 0, 1, 1);
945        let ranges: Vec<(usize, usize)> = folds.iter().map(|f| (f.start_row, f.end_row)).collect();
946        assert_eq!(ranges, vec![(5, 7), (2, 3)]);
947    }
948
949    #[test]
950    fn shift_folds_after_edit_drops_fully_consumed() {
951        let mut folds = vec![fold(4, 6)];
952        super::shift_folds_after_edit(&mut folds, 4, 7, 7, -3);
953        assert!(folds.is_empty());
954    }
955
956    // ── Borrow-style accessors + fold generation (round-2 perf item 10) ───
957
958    #[test]
959    fn with_folds_sees_the_same_data_as_folds() {
960        let mut buf = b();
961        // Empty case.
962        assert_eq!(buf.with_folds(<[super::Fold]>::to_vec), buf.folds());
963        assert!(!buf.has_folds());
964
965        buf.add_fold(0, 1, false);
966        buf.add_fold(2, 3, true);
967        assert_eq!(buf.with_folds(<[super::Fold]>::to_vec), buf.folds());
968        assert!(buf.has_folds());
969        // And the borrow-style scan agrees with the owning one row by row.
970        for row in 0..buf.row_count() {
971            assert_eq!(
972                buf.with_folds(|f| f.iter().any(|f| f.hides(row))),
973                buf.folds().iter().any(|f| f.hides(row)),
974                "row {row}"
975            );
976        }
977    }
978
979    #[test]
980    fn fold_gen_bumps_on_every_mutator() {
981        fn none(_: &mut View) {}
982        fn open_fold(b: &mut View) {
983            b.add_fold(1, 3, false);
984        }
985        fn closed_fold(b: &mut View) {
986            b.add_fold(1, 3, true);
987        }
988        /// (name, setup, mutation-that-must-bump)
989        type Case = (&'static str, fn(&mut View), fn(&mut View));
990        let cases: [Case; 13] = [
991            ("add_fold", none, |b| b.add_fold(1, 3, false)),
992            ("set_auto_folds", none, |b| {
993                b.set_auto_folds(&[(1, 3)], false)
994            }),
995            ("remove_fold_at", open_fold, |b| {
996                assert!(b.remove_fold_at(2));
997            }),
998            ("open_fold_at", closed_fold, |b| {
999                assert!(b.open_fold_at(2));
1000            }),
1001            ("close_fold_at", open_fold, |b| {
1002                assert!(b.close_fold_at(2));
1003            }),
1004            ("toggle_fold_at", open_fold, |b| {
1005                assert!(b.toggle_fold_at(2));
1006            }),
1007            ("open_all_folds", closed_fold, View::open_all_folds),
1008            ("close_all_folds", open_fold, View::close_all_folds),
1009            ("clear_all_folds", open_fold, View::clear_all_folds),
1010            ("reveal_row", closed_fold, |b| {
1011                assert!(b.reveal_row(2));
1012            }),
1013            ("invalidate_folds_in_range", closed_fold, |b| {
1014                b.invalidate_folds_in_range(2, 2);
1015            }),
1016            ("rebase_folds", closed_fold, |b| b.rebase_folds(0, 0, 1, 1)),
1017            ("set_folds", none, |b| {
1018                b.set_folds(&[super::Fold {
1019                    start_row: 1,
1020                    end_row: 3,
1021                    closed: true,
1022                    auto_generated: false,
1023                }]);
1024            }),
1025        ];
1026        for (name, setup, mutate) in cases {
1027            let mut buf = b();
1028            setup(&mut buf);
1029            let before = buf.fold_gen();
1030            mutate(&mut buf);
1031            assert!(
1032                buf.fold_gen() > before,
1033                "{name} mutated the fold set and must bump fold_gen"
1034            );
1035        }
1036    }
1037
1038    #[test]
1039    fn fold_gen_is_stable_across_reads_and_plain_text_edits() {
1040        let mut buf = b();
1041        buf.add_fold(1, 3, true);
1042        let fg = buf.fold_gen();
1043        assert!(fg > 0);
1044
1045        // Pure reads.
1046        let _ = buf.folds();
1047        let _ = buf.with_folds(<[super::Fold]>::to_vec);
1048        let _ = buf.has_folds();
1049        let _ = buf.is_row_hidden(2);
1050        let _ = buf.fold_at_row(2);
1051        let _ = buf.next_visible_row(1);
1052        let _ = buf.prev_visible_row(4);
1053        assert_eq!(buf.fold_gen(), fg, "read-only queries must not bump");
1054
1055        // No-op mutators (nothing actually changes).
1056        buf.close_all_folds(); // already closed
1057        buf.add_fold(9, 9, true); // out of bounds → rejected
1058        buf.rebase_folds(0, 0, 1, 0); // delta == 0 → early return
1059        let same = buf.folds();
1060        buf.set_folds(&same); // identical set
1061        assert_eq!(buf.fold_gen(), fg, "no-op mutators must not bump");
1062
1063        // A plain text edit must not masquerade as a fold change — that is
1064        // the whole reason `fold_gen` is separate from `dirty_gen`.
1065        let dg = buf.dirty_gen();
1066        buf.apply_edit(crate::Edit::InsertChar {
1067            at: crate::Position { row: 0, col: 0 },
1068            ch: 'x',
1069        });
1070        assert_ne!(buf.dirty_gen(), dg, "text edit must bump dirty_gen");
1071        assert_eq!(buf.fold_gen(), fg, "text edit must not bump fold_gen");
1072    }
1073
1074    #[test]
1075    fn rebase_folds_shifts_buffer_fold_storage() {
1076        let mut buf = View::from_str("0\n1\n2\n3\n4\n5\n6\n7\n8\n9");
1077        buf.add_fold(4, 6, true);
1078        buf.rebase_folds(0, 0, 1, 1);
1079        let folds = buf.folds();
1080        assert_eq!(folds.len(), 1);
1081        assert_eq!((folds[0].start_row, folds[0].end_row), (5, 7));
1082    }
1083}