1#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
22pub struct FoldRange {
23 pub header: usize,
25 pub last_hidden: usize,
27}
28
29impl FoldRange {
30 pub fn hidden_count(&self) -> usize {
32 self.last_hidden.saturating_sub(self.header)
33 }
34
35 pub fn hides(&self, line: usize) -> bool {
37 line > self.header && line <= self.last_hidden
38 }
39}
40
41#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
42pub struct FoldState {
43 folds: Vec<FoldRange>,
45}
46
47impl FoldState {
48 pub fn is_hidden(&self, row: usize) -> bool {
50 self.folds.iter().any(|f| f.hides(row))
51 }
52
53 pub fn prune_invalid(&mut self, line_count: usize) {
55 self.folds.retain(|f| f.header < line_count);
56 }
57
58 pub fn is_folded_header(&self, row: usize) -> bool {
60 self.folds.iter().any(|f| f.header == row)
61 }
62
63 pub fn toggle(&mut self, header: usize, lines: &[String]) {
66 if let Some(pos) = self.folds.iter().position(|f| f.header == header) {
68 self.folds.remove(pos);
69 return;
70 }
71
72 if let Some(range) = detect_range(header, lines) {
74 self.folds
76 .retain(|f| !(f.header > range.header && f.header <= range.last_hidden));
77 let pos = self.folds.partition_point(|f| f.header < range.header);
78 self.folds.insert(pos, range);
79 }
80 }
81
82 #[allow(dead_code)]
87 pub fn logical_to_visual(&self, row: usize) -> Option<usize> {
88 if self.is_hidden(row) {
89 return None;
90 }
91 let mut visual = row;
92 for f in &self.folds {
93 if f.header >= row {
94 break;
95 }
96 visual -= f.hidden_count();
97 }
98 Some(visual)
99 }
100
101 #[allow(dead_code)]
103 pub fn visual_to_logical(&self, visual: usize) -> usize {
104 let mut logical = visual;
105 for f in &self.folds {
106 if f.header >= logical {
107 break;
108 }
109 logical += f.hidden_count();
110 }
112 logical
113 }
114
115 pub fn visible_lines<'a>(&self, lines: &'a [String]) -> Vec<(usize, &'a str)> {
117 lines
118 .iter()
119 .enumerate()
120 .filter(|(i, _)| !self.is_hidden(*i))
121 .map(|(i, s)| (i, s.as_str()))
122 .collect()
123 }
124
125 pub fn folds(&self) -> &[FoldRange] {
127 &self.folds
128 }
129}
130
131fn detect_range(header: usize, lines: &[String]) -> Option<FoldRange> {
138 let header_line = lines.get(header)?;
139 let trimmed = header_line.trim_end();
140
141 if trimmed.ends_with('{') || trimmed.ends_with('(') || trimmed.ends_with('[') {
142 brace_range(header, lines)
143 } else {
144 indent_range(header, lines)
145 }
146}
147
148fn brace_range(header: usize, lines: &[String]) -> Option<FoldRange> {
149 let open = lines[header].trim_end().chars().last()?;
150 let close = match open {
151 '{' => '}',
152 '(' => ')',
153 '[' => ']',
154 _ => return None,
155 };
156
157 let mut depth = 0i32;
158 for (i, line) in lines.iter().enumerate().skip(header) {
159 for ch in line.chars() {
160 if ch == open {
161 depth += 1;
162 }
163 if ch == close {
164 depth -= 1;
165 if depth == 0 && i > header {
166 return Some(FoldRange {
168 header,
169 last_hidden: i.saturating_sub(1),
170 });
171 }
172 }
173 }
174 }
175 None
176}
177
178fn indent_range(header: usize, lines: &[String]) -> Option<FoldRange> {
179 let header_indent = indent_level(&lines[header]);
180 let mut last = header;
181 for (i, line) in lines.iter().enumerate().skip(header + 1) {
182 if line.trim().is_empty() {
183 continue;
184 }
185 if indent_level(line) > header_indent {
186 last = i;
187 } else {
188 break;
189 }
190 }
191 if last == header {
192 return None;
193 }
194 Some(FoldRange {
195 header,
196 last_hidden: last,
197 })
198}
199
200fn indent_level(line: &str) -> usize {
201 line.chars().take_while(|c| *c == ' ').count()
202 + line.chars().take_while(|c| *c == '\t').count() * 4
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 fn lines(text: &str) -> Vec<String> {
210 text.lines().map(|l| l.to_string()).collect()
211 }
212
213 #[test]
214 fn brace_fold_hides_inner_lines() {
215 let src = lines("fn foo() {\n let x = 1;\n x\n}");
216 let mut fs = FoldState::default();
217 fs.toggle(0, &src);
218 assert!(!fs.is_hidden(0)); assert!(fs.is_hidden(1));
220 assert!(fs.is_hidden(2));
221 assert!(!fs.is_hidden(3)); }
223
224 #[test]
225 fn toggle_twice_unfolds() {
226 let src = lines("fn foo() {\n x\n}");
227 let mut fs = FoldState::default();
228 fs.toggle(0, &src);
229 assert!(fs.is_hidden(1));
230 fs.toggle(0, &src);
231 assert!(!fs.is_hidden(1));
232 }
233
234 #[test]
235 fn logical_to_visual_skips_hidden() {
236 let src = lines("fn foo() {\n x\n y\n}");
237 let mut fs = FoldState::default();
238 fs.toggle(0, &src);
239 assert_eq!(fs.logical_to_visual(0), Some(0));
241 assert_eq!(fs.logical_to_visual(1), None);
242 assert_eq!(fs.logical_to_visual(3), Some(1));
243 }
244
245 #[test]
246 fn visual_to_logical_round_trips() {
247 let src = lines("fn foo() {\n x\n y\n}\nlet z = 1;");
248 let mut fs = FoldState::default();
249 fs.toggle(0, &src);
250 assert_eq!(fs.visual_to_logical(0), 0);
252 assert_eq!(fs.visual_to_logical(1), 3);
253 assert_eq!(fs.visual_to_logical(2), 4);
254 }
255
256 #[test]
257 fn indent_fold() {
258 let src = lines("class Foo:\n def bar(self):\n pass\n\nclass Bar:");
259 let mut fs = FoldState::default();
260 fs.toggle(0, &src);
261 assert!(fs.is_hidden(1));
262 assert!(fs.is_hidden(2));
263 assert!(!fs.is_hidden(4));
264 }
265
266 #[test]
267 fn prune_invalid_removes_folds_beyond_line_count() {
268 let src = lines("fn foo() {\n x\n}\nfn bar() {\n y\n}");
269 let mut fs = FoldState::default();
270 fs.toggle(0, &src);
271 fs.toggle(3, &src);
272 assert!(fs.is_hidden(1));
273 assert!(fs.is_hidden(4));
274 fs.prune_invalid(3);
275 assert!(fs.is_hidden(1), "fold at 0 should still exist and hide line 1");
276 assert!(!fs.is_hidden(4), "fold at 3 should be removed since line_count is 3");
277 }
278}