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