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        self.content_lock_mut()
390            .folds
391            .retain(|f| f.end_row < start_row || f.start_row > end_row);
392        if self.content_lock().folds.len() != before {
393            self.dirty_gen_bump();
394        }
395    }
396}
397
398#[cfg(test)]
399mod tests {
400    use crate::Buffer;
401
402    fn b() -> Buffer {
403        Buffer::from_str("a\nb\nc\nd\ne")
404    }
405
406    #[test]
407    fn add_keeps_folds_in_start_row_order() {
408        let mut buf = b();
409        buf.add_fold(2, 3, true);
410        buf.add_fold(0, 1, false);
411        let starts: Vec<usize> = buf.folds().iter().map(|f| f.start_row).collect();
412        assert_eq!(starts, vec![0, 2]);
413    }
414
415    #[test]
416    fn add_replaces_existing_with_same_start_row() {
417        let mut buf = b();
418        buf.add_fold(1, 2, true);
419        buf.add_fold(1, 4, false);
420        assert_eq!(buf.folds().len(), 1);
421        assert_eq!(buf.folds()[0].end_row, 4);
422        assert!(!buf.folds()[0].closed);
423    }
424
425    #[test]
426    fn add_clamps_end_row_to_buffer_bounds() {
427        let mut buf = b();
428        buf.add_fold(2, 99, true);
429        assert_eq!(buf.folds()[0].end_row, 4);
430    }
431
432    #[test]
433    fn add_rejects_inverted_range() {
434        let mut buf = b();
435        buf.add_fold(3, 1, true);
436        assert!(buf.folds().is_empty());
437    }
438
439    #[test]
440    fn toggle_flips_state() {
441        let mut buf = b();
442        buf.add_fold(1, 3, false);
443        assert!(!buf.folds()[0].closed);
444        assert!(buf.toggle_fold_at(2));
445        assert!(buf.folds()[0].closed);
446        assert!(buf.toggle_fold_at(2));
447        assert!(!buf.folds()[0].closed);
448    }
449
450    #[test]
451    fn is_row_hidden_excludes_start_row() {
452        let mut buf = b();
453        buf.add_fold(1, 3, true);
454        assert!(!buf.is_row_hidden(0));
455        assert!(!buf.is_row_hidden(1)); // start row stays visible
456        assert!(buf.is_row_hidden(2));
457        assert!(buf.is_row_hidden(3));
458        assert!(!buf.is_row_hidden(4));
459    }
460
461    #[test]
462    fn open_close_all_changes_every_fold() {
463        let mut buf = b();
464        buf.add_fold(0, 1, false);
465        buf.add_fold(2, 3, true);
466        buf.close_all_folds();
467        assert!(buf.folds().iter().all(|f| f.closed));
468        buf.open_all_folds();
469        assert!(buf.folds().iter().all(|f| !f.closed));
470    }
471
472    #[test]
473    fn invalidate_drops_overlapping_folds() {
474        let mut buf = b();
475        buf.add_fold(0, 1, true);
476        buf.add_fold(2, 3, true);
477        buf.add_fold(4, 4, true);
478        buf.invalidate_folds_in_range(2, 3);
479        let starts: Vec<usize> = buf.folds().iter().map(|f| f.start_row).collect();
480        assert_eq!(starts, vec![0, 4]);
481    }
482
483    // ── auto_generated flag + set_auto_folds ─────────────────────────────────
484
485    #[test]
486    fn add_fold_sets_auto_generated_false() {
487        let mut buf = b();
488        buf.add_fold(1, 3, false);
489        assert!(
490            !buf.folds()[0].auto_generated,
491            "manual add_fold must have auto_generated=false"
492        );
493    }
494
495    #[test]
496    fn set_auto_folds_adds_auto_folds() {
497        let mut buf = b();
498        buf.set_auto_folds(&[(0, 2), (3, 4)], false);
499        let folds = buf.folds();
500        assert_eq!(folds.len(), 2);
501        assert!(folds[0].auto_generated);
502        assert!(folds[1].auto_generated);
503        assert_eq!(folds[0].start_row, 0);
504        assert_eq!(folds[1].start_row, 3);
505    }
506
507    #[test]
508    fn set_auto_folds_second_call_replaces_first() {
509        let mut buf = b();
510        buf.set_auto_folds(&[(0, 2), (3, 4)], false);
511        assert_eq!(buf.folds().len(), 2);
512        // Replace with a different set.
513        buf.set_auto_folds(&[(1, 4)], false);
514        let folds = buf.folds();
515        assert_eq!(folds.len(), 1, "second call must replace first set");
516        assert_eq!(folds[0].start_row, 1);
517        assert!(folds[0].auto_generated);
518    }
519
520    #[test]
521    fn set_auto_folds_preserves_manual_folds() {
522        let mut buf = b();
523        // Add a manual fold.
524        buf.add_fold(0, 1, true);
525        // Auto-fold the remaining range.
526        buf.set_auto_folds(&[(2, 4)], false);
527        let folds = buf.folds();
528        assert_eq!(folds.len(), 2, "manual fold must survive set_auto_folds");
529        let manual = folds.iter().find(|f| f.start_row == 0).unwrap();
530        assert!(!manual.auto_generated, "manual fold flag must stay false");
531        let auto = folds.iter().find(|f| f.start_row == 2).unwrap();
532        assert!(auto.auto_generated);
533    }
534
535    #[test]
536    fn set_auto_folds_preserves_open_closed_state_by_start_row() {
537        let mut buf = b();
538        // First auto-fold pass: create a closed fold at row 0.
539        buf.set_auto_folds(&[(0, 2)], true); // default_closed=true → starts closed
540        assert!(buf.folds()[0].closed, "fold must start closed per default");
541
542        // User opens the fold (simulated by toggle).
543        buf.toggle_fold_at(0);
544        assert!(!buf.folds()[0].closed, "fold must now be open");
545
546        // Second auto-fold pass with same start_row — must preserve open state.
547        buf.set_auto_folds(&[(0, 2)], true); // default_closed=true but prev was open
548        assert!(
549            !buf.folds()[0].closed,
550            "open/closed state must be preserved across set_auto_folds"
551        );
552    }
553
554    #[test]
555    fn set_auto_folds_skips_single_row_and_inverted_ranges() {
556        let mut buf = b();
557        buf.set_auto_folds(&[(1, 1), (3, 2)], false);
558        assert!(
559            buf.folds().is_empty(),
560            "single-row and inverted ranges must be skipped"
561        );
562    }
563
564    #[test]
565    fn set_auto_folds_new_folds_use_default_closed() {
566        let mut buf = b();
567        buf.set_auto_folds(&[(0, 4)], true);
568        assert!(
569            buf.folds()[0].closed,
570            "new auto fold must use default_closed=true"
571        );
572
573        // Clear and re-run with default_closed=false.
574        buf.set_auto_folds(&[(0, 4)], false);
575        // This is a *new* start_row (it was removed + re-added), BUT the
576        // snapshot preserved the previous state (closed=true from above)
577        // because the start_row is the same.
578        // Wait — the test verifies the preservation path, not the default path.
579        // Let's use a fresh start_row to test the default path:
580        let mut buf2 = b();
581        buf2.set_auto_folds(&[(2, 4)], false);
582        assert!(
583            !buf2.folds()[0].closed,
584            "brand-new auto fold must start open when default_closed=false"
585        );
586    }
587}