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}
40
41impl Fold {
42    pub fn contains(&self, row: usize) -> bool {
43        row >= self.start_row && row <= self.end_row
44    }
45
46    /// True when `row` is hidden by a closed fold (i.e. inside the
47    /// fold but not on its `start_row` marker line).
48    pub fn hides(&self, row: usize) -> bool {
49        self.closed && row > self.start_row && row <= self.end_row
50    }
51
52    /// Number of rows the fold spans.
53    pub fn line_count(&self) -> usize {
54        self.end_row.saturating_sub(self.start_row) + 1
55    }
56}
57
58impl crate::Buffer {
59    pub fn folds(&self) -> &[Fold] {
60        &self.folds
61    }
62
63    /// Register a new fold. If an existing fold has the same
64    /// `start_row`, it's replaced; otherwise the new one is inserted
65    /// in start-row order. Empty / inverted ranges are rejected.
66    pub fn add_fold(&mut self, start_row: usize, end_row: usize, closed: bool) {
67        if end_row < start_row {
68            return;
69        }
70        let last = self.row_count().saturating_sub(1);
71        if start_row > last {
72            return;
73        }
74        let end_row = end_row.min(last);
75        let fold = Fold {
76            start_row,
77            end_row,
78            closed,
79        };
80        if let Some(idx) = self.folds.iter().position(|f| f.start_row == start_row) {
81            self.folds[idx] = fold;
82        } else {
83            let pos = self
84                .folds
85                .iter()
86                .position(|f| f.start_row > start_row)
87                .unwrap_or(self.folds.len());
88            self.folds.insert(pos, fold);
89        }
90        self.dirty_gen_bump();
91    }
92
93    /// Drop the fold whose range covers `row`. Returns `true` when a
94    /// fold was actually removed.
95    pub fn remove_fold_at(&mut self, row: usize) -> bool {
96        let Some(idx) = self.folds.iter().position(|f| f.contains(row)) else {
97            return false;
98        };
99        self.folds.remove(idx);
100        self.dirty_gen_bump();
101        true
102    }
103
104    /// Open the fold at `row` (no-op if already open or no fold).
105    pub fn open_fold_at(&mut self, row: usize) -> bool {
106        let Some(f) = self.folds.iter_mut().find(|f| f.contains(row)) else {
107            return false;
108        };
109        if !f.closed {
110            return false;
111        }
112        f.closed = false;
113        self.dirty_gen_bump();
114        true
115    }
116
117    /// Close the fold at `row` (no-op if already closed or no fold).
118    pub fn close_fold_at(&mut self, row: usize) -> bool {
119        let Some(f) = self.folds.iter_mut().find(|f| f.contains(row)) else {
120            return false;
121        };
122        if f.closed {
123            return false;
124        }
125        f.closed = true;
126        self.dirty_gen_bump();
127        true
128    }
129
130    /// Flip the closed/open state of the fold containing `row`.
131    pub fn toggle_fold_at(&mut self, row: usize) -> bool {
132        let Some(f) = self.folds.iter_mut().find(|f| f.contains(row)) else {
133            return false;
134        };
135        f.closed = !f.closed;
136        self.dirty_gen_bump();
137        true
138    }
139
140    /// `zR` — open every fold.
141    pub fn open_all_folds(&mut self) {
142        let mut changed = false;
143        for f in self.folds.iter_mut() {
144            if f.closed {
145                f.closed = false;
146                changed = true;
147            }
148        }
149        if changed {
150            self.dirty_gen_bump();
151        }
152    }
153
154    /// `zE` — eliminate every fold.
155    pub fn clear_all_folds(&mut self) {
156        if !self.folds.is_empty() {
157            self.folds.clear();
158            self.dirty_gen_bump();
159        }
160    }
161
162    /// `zM` — close every fold.
163    pub fn close_all_folds(&mut self) {
164        let mut changed = false;
165        for f in self.folds.iter_mut() {
166            if !f.closed {
167                f.closed = true;
168                changed = true;
169            }
170        }
171        if changed {
172            self.dirty_gen_bump();
173        }
174    }
175
176    /// First fold whose range contains `row` (most folds are
177    /// non-overlapping in vim's model, so the first match is the
178    /// only match). Useful for the host's `za`/`zo`/`zc` handlers.
179    pub fn fold_at_row(&self, row: usize) -> Option<&Fold> {
180        self.folds.iter().find(|f| f.contains(row))
181    }
182
183    /// True iff `row` is hidden by a closed fold (any fold).
184    pub fn is_row_hidden(&self, row: usize) -> bool {
185        self.folds.iter().any(|f| f.hides(row))
186    }
187
188    /// First visible row strictly after `row`, skipping any rows hidden
189    /// by closed folds. Returns `None` past the end of the buffer.
190    /// Drives fold-aware `j`: closed folds count as a single visual line.
191    pub fn next_visible_row(&self, row: usize) -> Option<usize> {
192        let last = self.row_count().saturating_sub(1);
193        if last == 0 && row == 0 {
194            return None;
195        }
196        let mut r = row.checked_add(1)?;
197        while r <= last && self.is_row_hidden(r) {
198            r += 1;
199        }
200        (r <= last).then_some(r)
201    }
202
203    /// First visible row strictly before `row`, skipping hidden rows.
204    /// Returns `None` past the top of the buffer.
205    pub fn prev_visible_row(&self, row: usize) -> Option<usize> {
206        let mut r = row.checked_sub(1)?;
207        while self.is_row_hidden(r) {
208            r = r.checked_sub(1)?;
209        }
210        Some(r)
211    }
212
213    /// Drop every fold that touches `[start_row, end_row]`. Edit
214    /// paths call this to invalidate folds whose contents the user
215    /// just mutated — vim's "edits inside a fold open it" behaviour.
216    pub fn invalidate_folds_in_range(&mut self, start_row: usize, end_row: usize) {
217        let before = self.folds.len();
218        self.folds
219            .retain(|f| f.end_row < start_row || f.start_row > end_row);
220        if self.folds.len() != before {
221            self.dirty_gen_bump();
222        }
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use crate::Buffer;
229
230    fn b() -> Buffer {
231        Buffer::from_str("a\nb\nc\nd\ne")
232    }
233
234    #[test]
235    fn add_keeps_folds_in_start_row_order() {
236        let mut buf = b();
237        buf.add_fold(2, 3, true);
238        buf.add_fold(0, 1, false);
239        let starts: Vec<usize> = buf.folds().iter().map(|f| f.start_row).collect();
240        assert_eq!(starts, vec![0, 2]);
241    }
242
243    #[test]
244    fn add_replaces_existing_with_same_start_row() {
245        let mut buf = b();
246        buf.add_fold(1, 2, true);
247        buf.add_fold(1, 4, false);
248        assert_eq!(buf.folds().len(), 1);
249        assert_eq!(buf.folds()[0].end_row, 4);
250        assert!(!buf.folds()[0].closed);
251    }
252
253    #[test]
254    fn add_clamps_end_row_to_buffer_bounds() {
255        let mut buf = b();
256        buf.add_fold(2, 99, true);
257        assert_eq!(buf.folds()[0].end_row, 4);
258    }
259
260    #[test]
261    fn add_rejects_inverted_range() {
262        let mut buf = b();
263        buf.add_fold(3, 1, true);
264        assert!(buf.folds().is_empty());
265    }
266
267    #[test]
268    fn toggle_flips_state() {
269        let mut buf = b();
270        buf.add_fold(1, 3, false);
271        assert!(!buf.folds()[0].closed);
272        assert!(buf.toggle_fold_at(2));
273        assert!(buf.folds()[0].closed);
274        assert!(buf.toggle_fold_at(2));
275        assert!(!buf.folds()[0].closed);
276    }
277
278    #[test]
279    fn is_row_hidden_excludes_start_row() {
280        let mut buf = b();
281        buf.add_fold(1, 3, true);
282        assert!(!buf.is_row_hidden(0));
283        assert!(!buf.is_row_hidden(1)); // start row stays visible
284        assert!(buf.is_row_hidden(2));
285        assert!(buf.is_row_hidden(3));
286        assert!(!buf.is_row_hidden(4));
287    }
288
289    #[test]
290    fn open_close_all_changes_every_fold() {
291        let mut buf = b();
292        buf.add_fold(0, 1, false);
293        buf.add_fold(2, 3, true);
294        buf.close_all_folds();
295        assert!(buf.folds().iter().all(|f| f.closed));
296        buf.open_all_folds();
297        assert!(buf.folds().iter().all(|f| !f.closed));
298    }
299
300    #[test]
301    fn invalidate_drops_overlapping_folds() {
302        let mut buf = b();
303        buf.add_fold(0, 1, true);
304        buf.add_fold(2, 3, true);
305        buf.add_fold(4, 4, true);
306        buf.invalidate_folds_in_range(2, 3);
307        let starts: Vec<usize> = buf.folds().iter().map(|f| f.start_row).collect();
308        assert_eq!(starts, vec![0, 4]);
309    }
310}