use std::collections::BTreeSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FoldRange {
pub start_row: usize,
pub end_row: usize,
}
#[derive(Debug, Clone, Default)]
pub struct FoldState {
collapsed: BTreeSet<usize>,
}
impl FoldState {
pub fn new() -> Self {
Self::default()
}
pub fn toggle(&mut self, start_row: usize) -> bool {
if self.collapsed.remove(&start_row) {
false
} else {
self.collapsed.insert(start_row);
true
}
}
pub fn is_collapsed(&self, start_row: usize) -> bool {
self.collapsed.contains(&start_row)
}
pub fn retain(&mut self, ranges: &[FoldRange]) {
self.collapsed
.retain(|start| ranges.iter().any(|range| range.start_row == *start));
}
pub fn hidden_range_at<'a>(
&self,
ranges: &'a [FoldRange],
row: usize,
) -> Option<&'a FoldRange> {
self.collapsed.iter().find_map(|start| {
ranges
.iter()
.find(|range| range.start_row == *start && *start < row && row <= range.end_row)
})
}
pub fn is_row_hidden(&self, ranges: &[FoldRange], row: usize) -> bool {
self.hidden_range_at(ranges, row).is_some()
}
pub fn step_visible_row(
&self,
ranges: &[FoldRange],
row: usize,
total_lines: usize,
down: bool,
) -> usize {
if total_lines == 0 {
return 0;
}
let last = total_lines - 1;
if down {
let mut next = row.saturating_add(1);
while next <= last {
match self.hidden_range_at(ranges, next) {
Some(range) => next = range.end_row.saturating_add(1),
None => return next,
}
}
row
} else {
if row == 0 {
return 0;
}
let next = row - 1;
match self.hidden_range_at(ranges, next) {
Some(range) => range.start_row,
None => next,
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ranges() -> Vec<FoldRange> {
vec![
FoldRange {
start_row: 0,
end_row: 5,
},
FoldRange {
start_row: 1,
end_row: 3,
},
FoldRange {
start_row: 7,
end_row: 9,
},
]
}
#[test]
fn toggle_flips_membership() {
let mut state = FoldState::new();
assert!(state.toggle(0));
assert!(state.is_collapsed(0));
assert!(!state.toggle(0));
assert!(!state.is_collapsed(0));
}
#[test]
fn headers_stay_visible() {
let mut state = FoldState::new();
state.toggle(0);
assert!(!state.is_row_hidden(&ranges(), 0));
assert!(state.is_row_hidden(&ranges(), 1));
assert!(state.is_row_hidden(&ranges(), 5));
assert!(!state.is_row_hidden(&ranges(), 6));
}
#[test]
fn step_down_skips_collapsed_spans() {
let mut state = FoldState::new();
state.toggle(0);
assert_eq!(state.step_visible_row(&ranges(), 0, 10, true), 6);
assert_eq!(state.step_visible_row(&ranges(), 2, 10, true), 6);
state.toggle(7);
assert_eq!(state.step_visible_row(&ranges(), 8, 10, true), 8);
assert_eq!(state.step_visible_row(&ranges(), 6, 10, true), 7);
}
#[test]
fn step_up_lands_on_headers() {
let mut state = FoldState::new();
state.toggle(0);
assert_eq!(state.step_visible_row(&ranges(), 6, 10, false), 0);
assert_eq!(state.step_visible_row(&ranges(), 4, 10, false), 0);
assert_eq!(state.step_visible_row(&ranges(), 0, 10, false), 0);
state.toggle(7);
assert_eq!(state.step_visible_row(&ranges(), 9, 10, false), 7);
assert_eq!(state.step_visible_row(&ranges(), 8, 10, true), 8);
let open = FoldState::new();
assert_eq!(open.step_visible_row(&ranges(), 6, 10, false), 5);
assert_eq!(open.step_visible_row(&ranges(), 6, 10, true), 7);
}
#[test]
fn retain_drops_vanished_starts() {
let mut state = FoldState::new();
state.toggle(0);
state.toggle(7);
state.retain(&[FoldRange {
start_row: 7,
end_row: 9,
}]);
assert!(!state.is_collapsed(0));
assert!(state.is_collapsed(7));
}
#[test]
fn empty_state_hides_nothing() {
let state = FoldState::new();
for row in 0..10 {
assert!(!state.is_row_hidden(&ranges(), row));
}
}
}