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