1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct Fold {
33 pub start_row: usize,
35 pub end_row: usize,
37 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 pub fn hides(&self, row: usize) -> bool {
49 self.closed && row > self.start_row && row <= self.end_row
50 }
51
52 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 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 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 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 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 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 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 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 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 pub fn fold_at_row(&self, row: usize) -> Option<&Fold> {
180 self.folds.iter().find(|f| f.contains(row))
181 }
182
183 pub fn is_row_hidden(&self, row: usize) -> bool {
185 self.folds.iter().any(|f| f.hides(row))
186 }
187
188 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 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 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)); 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}