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) -> Vec<Fold> {
65 self.content_lock().folds.clone()
66 }
67
68 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 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 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 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 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 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 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 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 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 pub fn is_row_hidden(&self, row: usize) -> bool {
228 self.folds().iter().any(|f| f.hides(row))
229 }
230
231 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 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 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)); 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}