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    /// Returns a snapshot of all folds as an owned `Vec<Fold>`.
60    ///
61    /// Owned rather than `&[Fold]` because a `Buffer` is a per-window
62    /// view onto a shared `Content`; another view could mutate the folds vec
63    /// between when this returns and when the caller reads the slice.
64    pub fn folds(&self) -> Vec<Fold> {
65        self.content_lock().folds.clone()
66    }
67
68    /// Register a new fold. If an existing fold has the same
69    /// `start_row`, it's replaced; otherwise the new one is inserted
70    /// in start-row order. Empty / inverted ranges are rejected.
71    pub fn add_fold(&mut self, start_row: usize, end_row: usize, closed: bool) {
72        if end_row < start_row {
73            return;
74        }
75        let last = self.row_count().saturating_sub(1);
76        if start_row > last {
77            return;
78        }
79        let end_row = end_row.min(last);
80        let fold = Fold {
81            start_row,
82            end_row,
83            closed,
84        };
85        {
86            let mut c = self.content_lock_mut();
87            if let Some(idx) = c.folds.iter().position(|f| f.start_row == start_row) {
88                c.folds[idx] = fold;
89            } else {
90                let pos = c
91                    .folds
92                    .iter()
93                    .position(|f| f.start_row > start_row)
94                    .unwrap_or(c.folds.len());
95                c.folds.insert(pos, fold);
96            }
97        }
98        self.dirty_gen_bump();
99    }
100
101    /// Drop the fold whose range covers `row`. Returns `true` when a
102    /// fold was actually removed.
103    pub fn remove_fold_at(&mut self, row: usize) -> bool {
104        let idx = self
105            .content_lock()
106            .folds
107            .iter()
108            .position(|f| f.contains(row));
109        let Some(idx) = idx else {
110            return false;
111        };
112        self.content_lock_mut().folds.remove(idx);
113        self.dirty_gen_bump();
114        true
115    }
116
117    /// Open the fold at `row` (no-op if already open or no fold).
118    pub fn open_fold_at(&mut self, row: usize) -> bool {
119        let changed = {
120            let mut c = self.content_lock_mut();
121            let Some(f) = c.folds.iter_mut().find(|f| f.contains(row)) else {
122                return false;
123            };
124            if !f.closed {
125                return false;
126            }
127            f.closed = false;
128            true
129        };
130        if changed {
131            self.dirty_gen_bump();
132        }
133        changed
134    }
135
136    /// Close the fold at `row` (no-op if already closed or no fold).
137    pub fn close_fold_at(&mut self, row: usize) -> bool {
138        let changed = {
139            let mut c = self.content_lock_mut();
140            let Some(f) = c.folds.iter_mut().find(|f| f.contains(row)) else {
141                return false;
142            };
143            if f.closed {
144                return false;
145            }
146            f.closed = true;
147            true
148        };
149        if changed {
150            self.dirty_gen_bump();
151        }
152        changed
153    }
154
155    /// Flip the closed/open state of the fold containing `row`.
156    pub fn toggle_fold_at(&mut self, row: usize) -> bool {
157        let changed = {
158            let mut c = self.content_lock_mut();
159            let Some(f) = c.folds.iter_mut().find(|f| f.contains(row)) else {
160                return false;
161            };
162            f.closed = !f.closed;
163            true
164        };
165        if changed {
166            self.dirty_gen_bump();
167        }
168        changed
169    }
170
171    /// `zR` — open every fold.
172    pub fn open_all_folds(&mut self) {
173        let changed = {
174            let mut c = self.content_lock_mut();
175            let mut any = false;
176            for f in c.folds.iter_mut() {
177                if f.closed {
178                    f.closed = false;
179                    any = true;
180                }
181            }
182            any
183        };
184        if changed {
185            self.dirty_gen_bump();
186        }
187    }
188
189    /// `zE` — eliminate every fold.
190    pub fn clear_all_folds(&mut self) {
191        let was_nonempty = !self.content_lock().folds.is_empty();
192        if was_nonempty {
193            self.content_lock_mut().folds.clear();
194            self.dirty_gen_bump();
195        }
196    }
197
198    /// `zM` — close every fold.
199    pub fn close_all_folds(&mut self) {
200        let changed = {
201            let mut c = self.content_lock_mut();
202            let mut any = false;
203            for f in c.folds.iter_mut() {
204                if !f.closed {
205                    f.closed = true;
206                    any = true;
207                }
208            }
209            any
210        };
211        if changed {
212            self.dirty_gen_bump();
213        }
214    }
215
216    /// First fold whose range contains `row`. Useful for the host's
217    /// `za`/`zo`/`zc` handlers.
218    pub fn fold_at_row(&self, row: usize) -> Option<Fold> {
219        self.content_lock()
220            .folds
221            .iter()
222            .find(|f| f.contains(row))
223            .copied()
224    }
225
226    /// True iff `row` is hidden by a closed fold (any fold).
227    pub fn is_row_hidden(&self, row: usize) -> bool {
228        self.folds().iter().any(|f| f.hides(row))
229    }
230
231    /// First visible row strictly after `row`, skipping any rows hidden
232    /// by closed folds. Returns `None` past the end of the buffer.
233    pub fn next_visible_row(&self, row: usize) -> Option<usize> {
234        let last = self.row_count().saturating_sub(1);
235        if last == 0 && row == 0 {
236            return None;
237        }
238        let mut r = row.checked_add(1)?;
239        while r <= last && self.is_row_hidden(r) {
240            r += 1;
241        }
242        (r <= last).then_some(r)
243    }
244
245    /// First visible row strictly before `row`, skipping hidden rows.
246    pub fn prev_visible_row(&self, row: usize) -> Option<usize> {
247        let mut r = row.checked_sub(1)?;
248        while self.is_row_hidden(r) {
249            r = r.checked_sub(1)?;
250        }
251        Some(r)
252    }
253
254    /// Drop every fold that touches `[start_row, end_row]`.
255    pub fn invalidate_folds_in_range(&mut self, start_row: usize, end_row: usize) {
256        let before = self.content_lock().folds.len();
257        self.content_lock_mut()
258            .folds
259            .retain(|f| f.end_row < start_row || f.start_row > end_row);
260        if self.content_lock().folds.len() != before {
261            self.dirty_gen_bump();
262        }
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use crate::Buffer;
269
270    fn b() -> Buffer {
271        Buffer::from_str("a\nb\nc\nd\ne")
272    }
273
274    #[test]
275    fn add_keeps_folds_in_start_row_order() {
276        let mut buf = b();
277        buf.add_fold(2, 3, true);
278        buf.add_fold(0, 1, false);
279        let starts: Vec<usize> = buf.folds().iter().map(|f| f.start_row).collect();
280        assert_eq!(starts, vec![0, 2]);
281    }
282
283    #[test]
284    fn add_replaces_existing_with_same_start_row() {
285        let mut buf = b();
286        buf.add_fold(1, 2, true);
287        buf.add_fold(1, 4, false);
288        assert_eq!(buf.folds().len(), 1);
289        assert_eq!(buf.folds()[0].end_row, 4);
290        assert!(!buf.folds()[0].closed);
291    }
292
293    #[test]
294    fn add_clamps_end_row_to_buffer_bounds() {
295        let mut buf = b();
296        buf.add_fold(2, 99, true);
297        assert_eq!(buf.folds()[0].end_row, 4);
298    }
299
300    #[test]
301    fn add_rejects_inverted_range() {
302        let mut buf = b();
303        buf.add_fold(3, 1, true);
304        assert!(buf.folds().is_empty());
305    }
306
307    #[test]
308    fn toggle_flips_state() {
309        let mut buf = b();
310        buf.add_fold(1, 3, false);
311        assert!(!buf.folds()[0].closed);
312        assert!(buf.toggle_fold_at(2));
313        assert!(buf.folds()[0].closed);
314        assert!(buf.toggle_fold_at(2));
315        assert!(!buf.folds()[0].closed);
316    }
317
318    #[test]
319    fn is_row_hidden_excludes_start_row() {
320        let mut buf = b();
321        buf.add_fold(1, 3, true);
322        assert!(!buf.is_row_hidden(0));
323        assert!(!buf.is_row_hidden(1)); // start row stays visible
324        assert!(buf.is_row_hidden(2));
325        assert!(buf.is_row_hidden(3));
326        assert!(!buf.is_row_hidden(4));
327    }
328
329    #[test]
330    fn open_close_all_changes_every_fold() {
331        let mut buf = b();
332        buf.add_fold(0, 1, false);
333        buf.add_fold(2, 3, true);
334        buf.close_all_folds();
335        assert!(buf.folds().iter().all(|f| f.closed));
336        buf.open_all_folds();
337        assert!(buf.folds().iter().all(|f| !f.closed));
338    }
339
340    #[test]
341    fn invalidate_drops_overlapping_folds() {
342        let mut buf = b();
343        buf.add_fold(0, 1, true);
344        buf.add_fold(2, 3, true);
345        buf.add_fold(4, 4, true);
346        buf.invalidate_folds_in_range(2, 3);
347        let starts: Vec<usize> = buf.folds().iter().map(|f| f.start_row).collect();
348        assert_eq!(starts, vec![0, 4]);
349    }
350}