1use std::collections::BTreeSet;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct FoldRange {
15 pub start_row: usize,
17 pub end_row: usize,
19}
20
21#[derive(Debug, Clone, Default)]
24pub struct FoldState {
25 collapsed: BTreeSet<usize>,
26}
27
28impl FoldState {
29 pub fn new() -> Self {
31 Self::default()
32 }
33
34 pub fn toggle(&mut self, start_row: usize) -> bool {
38 if self.collapsed.remove(&start_row) {
39 false
40 } else {
41 self.collapsed.insert(start_row);
42 true
43 }
44 }
45
46 pub fn is_collapsed(&self, start_row: usize) -> bool {
48 self.collapsed.contains(&start_row)
49 }
50
51 pub fn retain(&mut self, ranges: &[FoldRange]) {
54 self.collapsed
55 .retain(|start| ranges.iter().any(|range| range.start_row == *start));
56 }
57
58 pub fn hidden_range_at<'a>(
61 &self,
62 ranges: &'a [FoldRange],
63 row: usize,
64 ) -> Option<&'a FoldRange> {
65 self.collapsed.iter().find_map(|start| {
66 ranges
67 .iter()
68 .find(|range| range.start_row == *start && *start < row && row <= range.end_row)
69 })
70 }
71
72 pub fn is_row_hidden(&self, ranges: &[FoldRange], row: usize) -> bool {
74 self.hidden_range_at(ranges, row).is_some()
75 }
76
77 pub fn step_visible_row(
80 &self,
81 ranges: &[FoldRange],
82 row: usize,
83 total_lines: usize,
84 down: bool,
85 ) -> usize {
86 if total_lines == 0 {
87 return 0;
88 }
89 let last = total_lines - 1;
90 if down {
91 let mut next = row.saturating_add(1);
92 while next <= last {
93 match self.hidden_range_at(ranges, next) {
94 Some(range) => next = range.end_row.saturating_add(1),
95 None => return next,
96 }
97 }
98 row
99 } else {
100 if row == 0 {
101 return 0;
102 }
103 let next = row - 1;
104 match self.hidden_range_at(ranges, next) {
105 Some(range) => range.start_row,
107 None => next,
108 }
109 }
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 fn ranges() -> Vec<FoldRange> {
118 vec![
119 FoldRange {
120 start_row: 0,
121 end_row: 5,
122 },
123 FoldRange {
124 start_row: 1,
125 end_row: 3,
126 },
127 FoldRange {
128 start_row: 7,
129 end_row: 9,
130 },
131 ]
132 }
133
134 #[test]
135 fn toggle_flips_membership() {
136 let mut state = FoldState::new();
137 assert!(state.toggle(0));
138 assert!(state.is_collapsed(0));
139 assert!(!state.toggle(0));
140 assert!(!state.is_collapsed(0));
141 }
142
143 #[test]
144 fn headers_stay_visible() {
145 let mut state = FoldState::new();
146 state.toggle(0);
147 assert!(!state.is_row_hidden(&ranges(), 0));
148 assert!(state.is_row_hidden(&ranges(), 1));
149 assert!(state.is_row_hidden(&ranges(), 5));
150 assert!(!state.is_row_hidden(&ranges(), 6));
151 }
152
153 #[test]
154 fn step_down_skips_collapsed_spans() {
155 let mut state = FoldState::new();
156 state.toggle(0);
157 assert_eq!(state.step_visible_row(&ranges(), 0, 10, true), 6);
159 assert_eq!(state.step_visible_row(&ranges(), 2, 10, true), 6);
161 state.toggle(7);
164 assert_eq!(state.step_visible_row(&ranges(), 8, 10, true), 8);
165 assert_eq!(state.step_visible_row(&ranges(), 6, 10, true), 7);
166 }
167
168 #[test]
169 fn step_up_lands_on_headers() {
170 let mut state = FoldState::new();
171 state.toggle(0);
172 assert_eq!(state.step_visible_row(&ranges(), 6, 10, false), 0);
174 assert_eq!(state.step_visible_row(&ranges(), 4, 10, false), 0);
176 assert_eq!(state.step_visible_row(&ranges(), 0, 10, false), 0);
177 state.toggle(7);
179 assert_eq!(state.step_visible_row(&ranges(), 9, 10, false), 7);
180 assert_eq!(state.step_visible_row(&ranges(), 8, 10, true), 8);
181 let open = FoldState::new();
183 assert_eq!(open.step_visible_row(&ranges(), 6, 10, false), 5);
184 assert_eq!(open.step_visible_row(&ranges(), 6, 10, true), 7);
185 }
186
187 #[test]
188 fn retain_drops_vanished_starts() {
189 let mut state = FoldState::new();
190 state.toggle(0);
191 state.toggle(7);
192 state.retain(&[FoldRange {
193 start_row: 7,
194 end_row: 9,
195 }]);
196 assert!(!state.is_collapsed(0));
197 assert!(state.is_collapsed(7));
198 }
199
200 #[test]
201 fn empty_state_hides_nothing() {
202 let state = FoldState::new();
203 for row in 0..10 {
204 assert!(!state.is_row_hidden(&ranges(), row));
205 }
206 }
207}