#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Fold {
pub start_row: usize,
pub end_row: usize,
pub closed: bool,
pub auto_generated: bool,
}
impl Fold {
pub fn contains(&self, row: usize) -> bool {
row >= self.start_row && row <= self.end_row
}
pub fn hides(&self, row: usize) -> bool {
self.closed && row > self.start_row && row <= self.end_row
}
pub fn line_count(&self) -> usize {
self.end_row.saturating_sub(self.start_row) + 1
}
}
impl crate::Buffer {
pub fn folds(&self) -> Vec<Fold> {
self.content_lock().folds.clone()
}
pub fn add_fold(&mut self, start_row: usize, end_row: usize, closed: bool) {
if end_row < start_row {
return;
}
let last = self.row_count().saturating_sub(1);
if start_row > last {
return;
}
let end_row = end_row.min(last);
let fold = Fold {
start_row,
end_row,
closed,
auto_generated: false,
};
{
let mut c = self.content_lock_mut();
if let Some(idx) = c.folds.iter().position(|f| f.start_row == start_row) {
c.folds[idx] = fold;
} else {
let pos = c
.folds
.iter()
.position(|f| f.start_row > start_row)
.unwrap_or(c.folds.len());
c.folds.insert(pos, fold);
}
}
self.dirty_gen_bump();
}
pub fn set_auto_folds(&mut self, ranges: &[(usize, usize)], default_closed: bool) {
let prev_closed: std::collections::HashMap<usize, bool> = self
.content_lock()
.folds
.iter()
.filter(|f| f.auto_generated)
.map(|f| (f.start_row, f.closed))
.collect();
{
let mut c = self.content_lock_mut();
c.folds.retain(|f| !f.auto_generated);
}
let last = self.row_count().saturating_sub(1);
for &(start_row, end_row) in ranges {
if end_row < start_row || start_row > last {
continue;
}
let end_row = end_row.min(last);
if end_row == start_row {
continue;
}
let closed = prev_closed
.get(&start_row)
.copied()
.unwrap_or(default_closed);
let fold = Fold {
start_row,
end_row,
closed,
auto_generated: true,
};
let mut c = self.content_lock_mut();
if let Some(idx) = c.folds.iter().position(|f| f.start_row == start_row) {
c.folds[idx] = fold;
} else {
let pos = c
.folds
.iter()
.position(|f| f.start_row > start_row)
.unwrap_or(c.folds.len());
c.folds.insert(pos, fold);
}
}
self.dirty_gen_bump();
}
pub fn remove_fold_at(&mut self, row: usize) -> bool {
let idx = self
.content_lock()
.folds
.iter()
.enumerate()
.filter(|(_, f)| f.contains(row))
.max_by_key(|(_, f)| f.start_row)
.map(|(i, _)| i);
let Some(idx) = idx else {
return false;
};
self.content_lock_mut().folds.remove(idx);
self.dirty_gen_bump();
true
}
pub fn open_fold_at(&mut self, row: usize) -> bool {
let changed = {
let mut c = self.content_lock_mut();
let Some(f) = c
.folds
.iter_mut()
.filter(|f| f.contains(row))
.max_by_key(|f| f.start_row)
else {
return false;
};
if !f.closed {
return false;
}
f.closed = false;
true
};
if changed {
self.dirty_gen_bump();
}
changed
}
pub fn close_fold_at(&mut self, row: usize) -> bool {
let changed = {
let mut c = self.content_lock_mut();
let Some(f) = c
.folds
.iter_mut()
.filter(|f| f.contains(row))
.max_by_key(|f| f.start_row)
else {
return false;
};
if f.closed {
return false;
}
f.closed = true;
true
};
if changed {
self.dirty_gen_bump();
}
changed
}
pub fn toggle_fold_at(&mut self, row: usize) -> bool {
let changed = {
let mut c = self.content_lock_mut();
let Some(f) = c
.folds
.iter_mut()
.filter(|f| f.contains(row))
.max_by_key(|f| f.start_row)
else {
return false;
};
f.closed = !f.closed;
true
};
if changed {
self.dirty_gen_bump();
}
changed
}
pub fn open_all_folds(&mut self) {
let changed = {
let mut c = self.content_lock_mut();
let mut any = false;
for f in c.folds.iter_mut() {
if f.closed {
f.closed = false;
any = true;
}
}
any
};
if changed {
self.dirty_gen_bump();
}
}
pub fn clear_all_folds(&mut self) {
let was_nonempty = !self.content_lock().folds.is_empty();
if was_nonempty {
self.content_lock_mut().folds.clear();
self.dirty_gen_bump();
}
}
pub fn close_all_folds(&mut self) {
let changed = {
let mut c = self.content_lock_mut();
let mut any = false;
for f in c.folds.iter_mut() {
if !f.closed {
f.closed = true;
any = true;
}
}
any
};
if changed {
self.dirty_gen_bump();
}
}
pub fn fold_at_row(&self, row: usize) -> Option<Fold> {
self.content_lock()
.folds
.iter()
.filter(|f| f.contains(row))
.max_by_key(|f| f.start_row)
.copied()
}
pub fn is_row_hidden(&self, row: usize) -> bool {
self.folds().iter().any(|f| f.hides(row))
}
pub fn reveal_row(&mut self, row: usize) -> bool {
let changed = {
let mut c = self.content_lock_mut();
let mut any = false;
for f in c.folds.iter_mut() {
if f.hides(row) {
f.closed = false;
any = true;
}
}
any
};
if changed {
self.dirty_gen_bump();
}
changed
}
pub fn next_visible_row(&self, row: usize) -> Option<usize> {
let last = self.row_count().saturating_sub(1);
if last == 0 && row == 0 {
return None;
}
let mut r = row.checked_add(1)?;
while r <= last && self.is_row_hidden(r) {
r += 1;
}
(r <= last).then_some(r)
}
pub fn prev_visible_row(&self, row: usize) -> Option<usize> {
let mut r = row.checked_sub(1)?;
while self.is_row_hidden(r) {
r = r.checked_sub(1)?;
}
Some(r)
}
pub fn invalidate_folds_in_range(&mut self, start_row: usize, end_row: usize) {
let before = self.content_lock().folds.len();
self.content_lock_mut()
.folds
.retain(|f| f.end_row < start_row || f.start_row > end_row);
if self.content_lock().folds.len() != before {
self.dirty_gen_bump();
}
}
}
#[cfg(test)]
mod tests {
use crate::Buffer;
fn b() -> Buffer {
Buffer::from_str("a\nb\nc\nd\ne")
}
#[test]
fn add_keeps_folds_in_start_row_order() {
let mut buf = b();
buf.add_fold(2, 3, true);
buf.add_fold(0, 1, false);
let starts: Vec<usize> = buf.folds().iter().map(|f| f.start_row).collect();
assert_eq!(starts, vec![0, 2]);
}
#[test]
fn add_replaces_existing_with_same_start_row() {
let mut buf = b();
buf.add_fold(1, 2, true);
buf.add_fold(1, 4, false);
assert_eq!(buf.folds().len(), 1);
assert_eq!(buf.folds()[0].end_row, 4);
assert!(!buf.folds()[0].closed);
}
#[test]
fn add_clamps_end_row_to_buffer_bounds() {
let mut buf = b();
buf.add_fold(2, 99, true);
assert_eq!(buf.folds()[0].end_row, 4);
}
#[test]
fn add_rejects_inverted_range() {
let mut buf = b();
buf.add_fold(3, 1, true);
assert!(buf.folds().is_empty());
}
#[test]
fn toggle_flips_state() {
let mut buf = b();
buf.add_fold(1, 3, false);
assert!(!buf.folds()[0].closed);
assert!(buf.toggle_fold_at(2));
assert!(buf.folds()[0].closed);
assert!(buf.toggle_fold_at(2));
assert!(!buf.folds()[0].closed);
}
#[test]
fn is_row_hidden_excludes_start_row() {
let mut buf = b();
buf.add_fold(1, 3, true);
assert!(!buf.is_row_hidden(0));
assert!(!buf.is_row_hidden(1)); assert!(buf.is_row_hidden(2));
assert!(buf.is_row_hidden(3));
assert!(!buf.is_row_hidden(4));
}
#[test]
fn open_close_all_changes_every_fold() {
let mut buf = b();
buf.add_fold(0, 1, false);
buf.add_fold(2, 3, true);
buf.close_all_folds();
assert!(buf.folds().iter().all(|f| f.closed));
buf.open_all_folds();
assert!(buf.folds().iter().all(|f| !f.closed));
}
#[test]
fn invalidate_drops_overlapping_folds() {
let mut buf = b();
buf.add_fold(0, 1, true);
buf.add_fold(2, 3, true);
buf.add_fold(4, 4, true);
buf.invalidate_folds_in_range(2, 3);
let starts: Vec<usize> = buf.folds().iter().map(|f| f.start_row).collect();
assert_eq!(starts, vec![0, 4]);
}
#[test]
fn add_fold_sets_auto_generated_false() {
let mut buf = b();
buf.add_fold(1, 3, false);
assert!(
!buf.folds()[0].auto_generated,
"manual add_fold must have auto_generated=false"
);
}
#[test]
fn set_auto_folds_adds_auto_folds() {
let mut buf = b();
buf.set_auto_folds(&[(0, 2), (3, 4)], false);
let folds = buf.folds();
assert_eq!(folds.len(), 2);
assert!(folds[0].auto_generated);
assert!(folds[1].auto_generated);
assert_eq!(folds[0].start_row, 0);
assert_eq!(folds[1].start_row, 3);
}
#[test]
fn set_auto_folds_second_call_replaces_first() {
let mut buf = b();
buf.set_auto_folds(&[(0, 2), (3, 4)], false);
assert_eq!(buf.folds().len(), 2);
buf.set_auto_folds(&[(1, 4)], false);
let folds = buf.folds();
assert_eq!(folds.len(), 1, "second call must replace first set");
assert_eq!(folds[0].start_row, 1);
assert!(folds[0].auto_generated);
}
#[test]
fn set_auto_folds_preserves_manual_folds() {
let mut buf = b();
buf.add_fold(0, 1, true);
buf.set_auto_folds(&[(2, 4)], false);
let folds = buf.folds();
assert_eq!(folds.len(), 2, "manual fold must survive set_auto_folds");
let manual = folds.iter().find(|f| f.start_row == 0).unwrap();
assert!(!manual.auto_generated, "manual fold flag must stay false");
let auto = folds.iter().find(|f| f.start_row == 2).unwrap();
assert!(auto.auto_generated);
}
#[test]
fn set_auto_folds_preserves_open_closed_state_by_start_row() {
let mut buf = b();
buf.set_auto_folds(&[(0, 2)], true); assert!(buf.folds()[0].closed, "fold must start closed per default");
buf.toggle_fold_at(0);
assert!(!buf.folds()[0].closed, "fold must now be open");
buf.set_auto_folds(&[(0, 2)], true); assert!(
!buf.folds()[0].closed,
"open/closed state must be preserved across set_auto_folds"
);
}
#[test]
fn set_auto_folds_skips_single_row_and_inverted_ranges() {
let mut buf = b();
buf.set_auto_folds(&[(1, 1), (3, 2)], false);
assert!(
buf.folds().is_empty(),
"single-row and inverted ranges must be skipped"
);
}
#[test]
fn set_auto_folds_new_folds_use_default_closed() {
let mut buf = b();
buf.set_auto_folds(&[(0, 4)], true);
assert!(
buf.folds()[0].closed,
"new auto fold must use default_closed=true"
);
buf.set_auto_folds(&[(0, 4)], false);
let mut buf2 = b();
buf2.set_auto_folds(&[(2, 4)], false);
assert!(
!buf2.folds()[0].closed,
"brand-new auto fold must start open when default_closed=false"
);
}
}