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::Buffer::lines`] always returns the underlying logical text.
14//!
15//! Add / remove / toggle goes through
16//! [`crate::Buffer::add_fold`] / [`crate::Buffer::remove_fold_at`] /
17//! [`crate::Buffer::toggle_fold_at`]. Open-all / close-all (`zR` / `zM`)
18//! go through [`crate::Buffer::open_all_folds`] /
19//! [`crate::Buffer::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::Buffer::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::Buffer::add_fold`] set this to `false`.
42    ///
43    /// Used by [`crate::Buffer::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::Buffer {
67    /// Returns a snapshot of all folds as an owned `Vec<Fold>`.
68    ///
69    /// Owned rather than `&[Fold]` because a `Buffer` is a per-window
70    /// view onto a shared `Content`; 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    /// Register a new fold. If an existing fold has the same
77    /// `start_row`, it's replaced; otherwise the new one is inserted
78    /// in start-row order. Empty / inverted ranges are rejected.
79    pub fn add_fold(&mut self, start_row: usize, end_row: usize, closed: bool) {
80        if end_row < start_row {
81            return;
82        }
83        let last = self.row_count().saturating_sub(1);
84        if start_row > last {
85            return;
86        }
87        let end_row = end_row.min(last);
88        let fold = Fold {
89            start_row,
90            end_row,
91            closed,
92            auto_generated: false,
93        };
94        {
95            let mut c = self.content_lock_mut();
96            if let Some(idx) = c.folds.iter().position(|f| f.start_row == start_row) {
97                c.folds[idx] = fold;
98            } else {
99                let pos = c
100                    .folds
101                    .iter()
102                    .position(|f| f.start_row > start_row)
103                    .unwrap_or(c.folds.len());
104                c.folds.insert(pos, fold);
105            }
106        }
107        self.dirty_gen_bump();
108    }
109
110    /// Replace all auto-generated folds with a new set derived from
111    /// `ranges`, while leaving manual folds untouched.
112    ///
113    /// ## Algorithm (O(N) — bounded by `ranges.len()`, no unbounded growth)
114    ///
115    /// 1. Snapshot `start_row → closed` for every existing auto fold so
116    ///    open/closed state survives a reparse.
117    /// 2. Retain only manual folds (`auto_generated == false`).
118    /// 3. Insert one new `Fold` per range, re-using the snapshotted closed
119    ///    state when the start_row existed before, else `default_closed`.
120    ///
121    /// Invariants preserved:
122    /// - Folds stay sorted by `start_row` (same ordering as `add_fold`).
123    /// - Duplicate start_rows: the last range in `ranges` wins (consistent
124    ///   with `add_fold`'s replace-on-same-start-row semantics). In practice
125    ///   TS query ranges are already deduplicated.
126    /// - Empty / inverted ranges (end_row < start_row) are silently skipped.
127    /// - `end_row` is clamped to the last valid row, same as `add_fold`.
128    pub fn set_auto_folds(&mut self, ranges: &[(usize, usize)], default_closed: bool) {
129        // 1. Snapshot closed state of existing auto folds by start_row.
130        let prev_closed: std::collections::HashMap<usize, bool> = self
131            .content_lock()
132            .folds
133            .iter()
134            .filter(|f| f.auto_generated)
135            .map(|f| (f.start_row, f.closed))
136            .collect();
137
138        // 2. Retain manual folds only.
139        {
140            let mut c = self.content_lock_mut();
141            c.folds.retain(|f| !f.auto_generated);
142        }
143
144        // 3. Insert new auto folds in sorted order.
145        let last = self.row_count().saturating_sub(1);
146        for &(start_row, end_row) in ranges {
147            // Skip empty/inverted and out-of-bounds ranges.
148            if end_row < start_row || start_row > last {
149                continue;
150            }
151            let end_row = end_row.min(last);
152            // Only folds spanning more than one row are meaningful.
153            if end_row == start_row {
154                continue;
155            }
156            let closed = prev_closed
157                .get(&start_row)
158                .copied()
159                .unwrap_or(default_closed);
160            let fold = Fold {
161                start_row,
162                end_row,
163                closed,
164                auto_generated: true,
165            };
166            let mut c = self.content_lock_mut();
167            // Replace any existing fold at this start_row (manual or auto).
168            if let Some(idx) = c.folds.iter().position(|f| f.start_row == start_row) {
169                c.folds[idx] = fold;
170            } else {
171                let pos = c
172                    .folds
173                    .iter()
174                    .position(|f| f.start_row > start_row)
175                    .unwrap_or(c.folds.len());
176                c.folds.insert(pos, fold);
177            }
178        }
179
180        self.dirty_gen_bump();
181    }
182
183    /// Drop the fold whose range covers `row`. Returns `true` when a
184    /// fold was actually removed.
185    pub fn remove_fold_at(&mut self, row: usize) -> bool {
186        // Remove the INNERMOST fold containing `row` (largest start_row), so
187        // `zd` on a nested fold drops the inner one, not the enclosing block.
188        let idx = self
189            .content_lock()
190            .folds
191            .iter()
192            .enumerate()
193            .filter(|(_, f)| f.contains(row))
194            .max_by_key(|(_, f)| f.start_row)
195            .map(|(i, _)| i);
196        let Some(idx) = idx else {
197            return false;
198        };
199        self.content_lock_mut().folds.remove(idx);
200        self.dirty_gen_bump();
201        true
202    }
203
204    /// Open the fold at `row` (no-op if already open or no fold).
205    pub fn open_fold_at(&mut self, row: usize) -> bool {
206        let changed = {
207            let mut c = self.content_lock_mut();
208            let Some(f) = c
209                .folds
210                .iter_mut()
211                .filter(|f| f.contains(row))
212                .max_by_key(|f| f.start_row)
213            else {
214                return false;
215            };
216            if !f.closed {
217                return false;
218            }
219            f.closed = false;
220            true
221        };
222        if changed {
223            self.dirty_gen_bump();
224        }
225        changed
226    }
227
228    /// Close the fold at `row` (no-op if already closed or no fold).
229    pub fn close_fold_at(&mut self, row: usize) -> bool {
230        let changed = {
231            let mut c = self.content_lock_mut();
232            let Some(f) = c
233                .folds
234                .iter_mut()
235                .filter(|f| f.contains(row))
236                .max_by_key(|f| f.start_row)
237            else {
238                return false;
239            };
240            if f.closed {
241                return false;
242            }
243            f.closed = true;
244            true
245        };
246        if changed {
247            self.dirty_gen_bump();
248        }
249        changed
250    }
251
252    /// Flip the closed/open state of the fold containing `row`.
253    pub fn toggle_fold_at(&mut self, row: usize) -> bool {
254        let changed = {
255            let mut c = self.content_lock_mut();
256            let Some(f) = c
257                .folds
258                .iter_mut()
259                .filter(|f| f.contains(row))
260                .max_by_key(|f| f.start_row)
261            else {
262                return false;
263            };
264            f.closed = !f.closed;
265            true
266        };
267        if changed {
268            self.dirty_gen_bump();
269        }
270        changed
271    }
272
273    /// `zR` — open every fold.
274    pub fn open_all_folds(&mut self) {
275        let changed = {
276            let mut c = self.content_lock_mut();
277            let mut any = false;
278            for f in c.folds.iter_mut() {
279                if f.closed {
280                    f.closed = false;
281                    any = true;
282                }
283            }
284            any
285        };
286        if changed {
287            self.dirty_gen_bump();
288        }
289    }
290
291    /// `zE` — eliminate every fold.
292    pub fn clear_all_folds(&mut self) {
293        let was_nonempty = !self.content_lock().folds.is_empty();
294        if was_nonempty {
295            self.content_lock_mut().folds.clear();
296            self.dirty_gen_bump();
297        }
298    }
299
300    /// `zM` — close every fold.
301    pub fn close_all_folds(&mut self) {
302        let changed = {
303            let mut c = self.content_lock_mut();
304            let mut any = false;
305            for f in c.folds.iter_mut() {
306                if !f.closed {
307                    f.closed = true;
308                    any = true;
309                }
310            }
311            any
312        };
313        if changed {
314            self.dirty_gen_bump();
315        }
316    }
317
318    /// First fold whose range contains `row`. Useful for the host's
319    /// `za`/`zo`/`zc` handlers.
320    pub fn fold_at_row(&self, row: usize) -> Option<Fold> {
321        // Innermost fold containing `row`: with nested folds, the one with the
322        // largest `start_row` is the most-deeply-nested. Folds are stored in
323        // start-row order, so a plain `.find` would return the OUTERMOST fold
324        // and `zc`/`za`/`zo` would act on the wrong level.
325        self.content_lock()
326            .folds
327            .iter()
328            .filter(|f| f.contains(row))
329            .max_by_key(|f| f.start_row)
330            .copied()
331    }
332
333    /// True iff `row` is hidden by a closed fold (any fold).
334    pub fn is_row_hidden(&self, row: usize) -> bool {
335        self.folds().iter().any(|f| f.hides(row))
336    }
337
338    /// Open every closed fold whose body hides `row`, so the row becomes
339    /// visible. Handles nested folds in a single pass — unlike
340    /// `open_fold_at` / `FoldOp::OpenAt`, which only act on the first fold
341    /// containing the row and so can never reach a nested inner fold.
342    /// Used by `goto_line` so a jump into a folded region reveals the
343    /// target line instead of stranding the cursor on a hidden row.
344    /// Returns `true` if any fold was opened.
345    pub fn reveal_row(&mut self, row: usize) -> bool {
346        let changed = {
347            let mut c = self.content_lock_mut();
348            let mut any = false;
349            for f in c.folds.iter_mut() {
350                if f.hides(row) {
351                    f.closed = false;
352                    any = true;
353                }
354            }
355            any
356        };
357        if changed {
358            self.dirty_gen_bump();
359        }
360        changed
361    }
362
363    /// First visible row strictly after `row`, skipping any rows hidden
364    /// by closed folds. Returns `None` past the end of the buffer.
365    pub fn next_visible_row(&self, row: usize) -> Option<usize> {
366        let last = self.row_count().saturating_sub(1);
367        if last == 0 && row == 0 {
368            return None;
369        }
370        let mut r = row.checked_add(1)?;
371        while r <= last && self.is_row_hidden(r) {
372            r += 1;
373        }
374        (r <= last).then_some(r)
375    }
376
377    /// First visible row strictly before `row`, skipping hidden rows.
378    pub fn prev_visible_row(&self, row: usize) -> Option<usize> {
379        let mut r = row.checked_sub(1)?;
380        while self.is_row_hidden(r) {
381            r = r.checked_sub(1)?;
382        }
383        Some(r)
384    }
385
386    /// Drop every fold that touches `[start_row, end_row]`.
387    pub fn invalidate_folds_in_range(&mut self, start_row: usize, end_row: usize) {
388        let before = self.content_lock().folds.len();
389        invalidate_folds(&mut self.content_lock_mut().folds, start_row, end_row);
390        if self.content_lock().folds.len() != before {
391            self.dirty_gen_bump();
392        }
393    }
394
395    /// Replace the entire fold set wholesale. Used to install a per-window fold
396    /// snapshot into the shared buffer on focus change (window-level folds): the
397    /// app keeps each window's open/closed state and swaps it in before dispatch,
398    /// so motions/render/`z`-ops operate on the focused window's folds.
399    pub fn set_folds(&mut self, folds: &[Fold]) {
400        {
401            let mut c = self.content_lock_mut();
402            if c.folds.as_slice() == folds {
403                return; // no-op — avoid a spurious dirty_gen bump
404            }
405            c.folds = folds.to_vec();
406        }
407        self.dirty_gen_bump();
408    }
409}
410
411/// Drop every fold in `folds` that touches `[start_row, end_row]`, in place.
412///
413/// Free helper so both [`crate::Buffer::invalidate_folds_in_range`] (operating
414/// on the shared content) and the app's window-level edit-coherence pass
415/// (operating on a sibling window's owned `Vec<Fold>`) share one rule — vim
416/// opens/forgets any fold the edit overlapped.
417pub fn invalidate_folds(folds: &mut Vec<Fold>, start_row: usize, end_row: usize) {
418    folds.retain(|f| f.end_row < start_row || f.start_row > end_row);
419}
420
421#[cfg(test)]
422mod tests {
423    use crate::Buffer;
424
425    fn b() -> Buffer {
426        Buffer::from_str("a\nb\nc\nd\ne")
427    }
428
429    #[test]
430    fn add_keeps_folds_in_start_row_order() {
431        let mut buf = b();
432        buf.add_fold(2, 3, true);
433        buf.add_fold(0, 1, false);
434        let starts: Vec<usize> = buf.folds().iter().map(|f| f.start_row).collect();
435        assert_eq!(starts, vec![0, 2]);
436    }
437
438    #[test]
439    fn set_folds_replaces_wholesale() {
440        let mut buf = b();
441        buf.add_fold(0, 1, false);
442        // Install a different per-window snapshot.
443        let snapshot = vec![super::Fold {
444            start_row: 2,
445            end_row: 3,
446            closed: true,
447            auto_generated: false,
448        }];
449        buf.set_folds(&snapshot);
450        assert_eq!(buf.folds(), snapshot);
451        // Idempotent: re-installing the same set is a no-op (no dirty bump).
452        let dg = buf.dirty_gen();
453        buf.set_folds(&snapshot);
454        assert_eq!(buf.dirty_gen(), dg);
455    }
456
457    #[test]
458    fn invalidate_folds_helper_drops_overlapping() {
459        let f = |s, e| super::Fold {
460            start_row: s,
461            end_row: e,
462            closed: true,
463            auto_generated: false,
464        };
465        let mut folds = vec![f(0, 2), f(4, 6), f(8, 10)];
466        // Edit touches rows 5..5 → only the [4,6] fold overlaps.
467        super::invalidate_folds(&mut folds, 5, 5);
468        let starts: Vec<usize> = folds.iter().map(|x| x.start_row).collect();
469        assert_eq!(starts, vec![0, 8]);
470    }
471
472    #[test]
473    fn add_replaces_existing_with_same_start_row() {
474        let mut buf = b();
475        buf.add_fold(1, 2, true);
476        buf.add_fold(1, 4, false);
477        assert_eq!(buf.folds().len(), 1);
478        assert_eq!(buf.folds()[0].end_row, 4);
479        assert!(!buf.folds()[0].closed);
480    }
481
482    #[test]
483    fn add_clamps_end_row_to_buffer_bounds() {
484        let mut buf = b();
485        buf.add_fold(2, 99, true);
486        assert_eq!(buf.folds()[0].end_row, 4);
487    }
488
489    #[test]
490    fn add_rejects_inverted_range() {
491        let mut buf = b();
492        buf.add_fold(3, 1, true);
493        assert!(buf.folds().is_empty());
494    }
495
496    #[test]
497    fn toggle_flips_state() {
498        let mut buf = b();
499        buf.add_fold(1, 3, false);
500        assert!(!buf.folds()[0].closed);
501        assert!(buf.toggle_fold_at(2));
502        assert!(buf.folds()[0].closed);
503        assert!(buf.toggle_fold_at(2));
504        assert!(!buf.folds()[0].closed);
505    }
506
507    #[test]
508    fn is_row_hidden_excludes_start_row() {
509        let mut buf = b();
510        buf.add_fold(1, 3, true);
511        assert!(!buf.is_row_hidden(0));
512        assert!(!buf.is_row_hidden(1)); // start row stays visible
513        assert!(buf.is_row_hidden(2));
514        assert!(buf.is_row_hidden(3));
515        assert!(!buf.is_row_hidden(4));
516    }
517
518    #[test]
519    fn open_close_all_changes_every_fold() {
520        let mut buf = b();
521        buf.add_fold(0, 1, false);
522        buf.add_fold(2, 3, true);
523        buf.close_all_folds();
524        assert!(buf.folds().iter().all(|f| f.closed));
525        buf.open_all_folds();
526        assert!(buf.folds().iter().all(|f| !f.closed));
527    }
528
529    #[test]
530    fn invalidate_drops_overlapping_folds() {
531        let mut buf = b();
532        buf.add_fold(0, 1, true);
533        buf.add_fold(2, 3, true);
534        buf.add_fold(4, 4, true);
535        buf.invalidate_folds_in_range(2, 3);
536        let starts: Vec<usize> = buf.folds().iter().map(|f| f.start_row).collect();
537        assert_eq!(starts, vec![0, 4]);
538    }
539
540    // ── auto_generated flag + set_auto_folds ─────────────────────────────────
541
542    #[test]
543    fn add_fold_sets_auto_generated_false() {
544        let mut buf = b();
545        buf.add_fold(1, 3, false);
546        assert!(
547            !buf.folds()[0].auto_generated,
548            "manual add_fold must have auto_generated=false"
549        );
550    }
551
552    #[test]
553    fn set_auto_folds_adds_auto_folds() {
554        let mut buf = b();
555        buf.set_auto_folds(&[(0, 2), (3, 4)], false);
556        let folds = buf.folds();
557        assert_eq!(folds.len(), 2);
558        assert!(folds[0].auto_generated);
559        assert!(folds[1].auto_generated);
560        assert_eq!(folds[0].start_row, 0);
561        assert_eq!(folds[1].start_row, 3);
562    }
563
564    #[test]
565    fn set_auto_folds_second_call_replaces_first() {
566        let mut buf = b();
567        buf.set_auto_folds(&[(0, 2), (3, 4)], false);
568        assert_eq!(buf.folds().len(), 2);
569        // Replace with a different set.
570        buf.set_auto_folds(&[(1, 4)], false);
571        let folds = buf.folds();
572        assert_eq!(folds.len(), 1, "second call must replace first set");
573        assert_eq!(folds[0].start_row, 1);
574        assert!(folds[0].auto_generated);
575    }
576
577    #[test]
578    fn set_auto_folds_preserves_manual_folds() {
579        let mut buf = b();
580        // Add a manual fold.
581        buf.add_fold(0, 1, true);
582        // Auto-fold the remaining range.
583        buf.set_auto_folds(&[(2, 4)], false);
584        let folds = buf.folds();
585        assert_eq!(folds.len(), 2, "manual fold must survive set_auto_folds");
586        let manual = folds.iter().find(|f| f.start_row == 0).unwrap();
587        assert!(!manual.auto_generated, "manual fold flag must stay false");
588        let auto = folds.iter().find(|f| f.start_row == 2).unwrap();
589        assert!(auto.auto_generated);
590    }
591
592    #[test]
593    fn set_auto_folds_preserves_open_closed_state_by_start_row() {
594        let mut buf = b();
595        // First auto-fold pass: create a closed fold at row 0.
596        buf.set_auto_folds(&[(0, 2)], true); // default_closed=true → starts closed
597        assert!(buf.folds()[0].closed, "fold must start closed per default");
598
599        // User opens the fold (simulated by toggle).
600        buf.toggle_fold_at(0);
601        assert!(!buf.folds()[0].closed, "fold must now be open");
602
603        // Second auto-fold pass with same start_row — must preserve open state.
604        buf.set_auto_folds(&[(0, 2)], true); // default_closed=true but prev was open
605        assert!(
606            !buf.folds()[0].closed,
607            "open/closed state must be preserved across set_auto_folds"
608        );
609    }
610
611    #[test]
612    fn set_auto_folds_skips_single_row_and_inverted_ranges() {
613        let mut buf = b();
614        buf.set_auto_folds(&[(1, 1), (3, 2)], false);
615        assert!(
616            buf.folds().is_empty(),
617            "single-row and inverted ranges must be skipped"
618        );
619    }
620
621    #[test]
622    fn set_auto_folds_new_folds_use_default_closed() {
623        let mut buf = b();
624        buf.set_auto_folds(&[(0, 4)], true);
625        assert!(
626            buf.folds()[0].closed,
627            "new auto fold must use default_closed=true"
628        );
629
630        // Clear and re-run with default_closed=false.
631        buf.set_auto_folds(&[(0, 4)], false);
632        // This is a *new* start_row (it was removed + re-added), BUT the
633        // snapshot preserved the previous state (closed=true from above)
634        // because the start_row is the same.
635        // Wait — the test verifies the preservation path, not the default path.
636        // Let's use a fresh start_row to test the default path:
637        let mut buf2 = b();
638        buf2.set_auto_folds(&[(2, 4)], false);
639        assert!(
640            !buf2.folds()[0].closed,
641            "brand-new auto fold must start open when default_closed=false"
642        );
643    }
644}