iced_code_editor/canvas_editor/
folding.rs1use std::collections::HashSet;
13
14use crate::text_buffer::TextBuffer;
15
16use super::TAB_WIDTH;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct FoldRegion {
24 pub start_line: usize,
26 pub end_line: usize,
28}
29
30impl FoldRegion {
31 pub fn new(start_line: usize, end_line: usize) -> Self {
38 Self { start_line, end_line }
39 }
40}
41
42fn indent_width(line: &str) -> Option<usize> {
51 let mut width = 0;
52 for c in line.chars() {
53 match c {
54 '\t' => width += TAB_WIDTH,
55 ' ' => width += 1,
56 _ if c.is_whitespace() => width += 1,
57 _ => return Some(width),
58 }
59 }
60 None
62}
63
64pub fn compute_foldable_regions(buffer: &TextBuffer) -> Vec<FoldRegion> {
83 let line_count = buffer.line_count();
84 let indents: Vec<Option<usize>> =
86 (0..line_count).map(|i| indent_width(buffer.line(i))).collect();
87
88 let mut regions: Vec<FoldRegion> = Vec::new();
89 let mut open_regions: Vec<(usize, usize)> = Vec::new();
94 let mut non_blank = indents
95 .iter()
96 .enumerate()
97 .filter_map(|(line, indent)| indent.map(|width| (line, width)))
98 .peekable();
99
100 let mut previous_non_blank = None;
101 while let Some((line, indent)) = non_blank.next() {
102 while open_regions
103 .last()
104 .is_some_and(|(header_indent, _)| *header_indent >= indent)
105 {
106 if let Some((_, region_index)) = open_regions.pop()
107 && let Some(end_line) = previous_non_blank
108 && let Some(region) = regions.get_mut(region_index)
109 {
110 region.end_line = end_line;
111 }
112 }
113
114 if non_blank
115 .peek()
116 .is_some_and(|(_, next_indent)| *next_indent > indent)
117 {
118 let region_index = regions.len();
119 regions.push(FoldRegion::new(line, line));
120 open_regions.push((indent, region_index));
121 }
122
123 previous_non_blank = Some(line);
124 }
125
126 if let Some(end_line) = previous_non_blank {
127 for (_, region_index) in open_regions {
128 if let Some(region) = regions.get_mut(region_index) {
129 region.end_line = end_line;
130 }
131 }
132 }
133
134 regions
135}
136
137pub fn is_fold_header(regions: &[FoldRegion], line: usize) -> bool {
144 regions.binary_search_by_key(&line, |region| region.start_line).is_ok()
145}
146
147pub fn is_line_fold_header(buffer: &TextBuffer, line: usize) -> bool {
153 let Some(header_indent) = indent_width(buffer.line(line)) else {
154 return false;
155 };
156
157 (line.saturating_add(1)..buffer.line_count())
158 .find_map(|next_line| indent_width(buffer.line(next_line)))
159 .is_some_and(|next_indent| next_indent > header_indent)
160}
161
162pub fn hidden_lines(
177 regions: &[FoldRegion],
178 collapsed: &HashSet<usize>,
179) -> HashSet<usize> {
180 let mut hidden = HashSet::new();
181 for region in regions {
182 if collapsed.contains(®ion.start_line) {
183 hidden.extend((region.start_line + 1)..=region.end_line);
184 }
185 }
186 hidden
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn test_indent_width_blank_lines() {
195 assert_eq!(indent_width(""), None);
196 assert_eq!(indent_width(" "), None);
197 assert_eq!(indent_width("\t"), None);
198 }
199
200 #[test]
201 fn test_indent_width_expands_tabs() {
202 assert_eq!(indent_width("code"), Some(0));
203 assert_eq!(indent_width(" code"), Some(2));
204 assert_eq!(indent_width("\tcode"), Some(TAB_WIDTH));
205 assert_eq!(indent_width("\t code"), Some(TAB_WIDTH + 2));
206 }
207
208 #[test]
209 fn test_no_regions_for_flat_text() {
210 let buffer = TextBuffer::new("a\nb\nc");
211 assert!(compute_foldable_regions(&buffer).is_empty());
212 }
213
214 #[test]
215 fn test_simple_block() {
216 let buffer =
218 TextBuffer::new("fn main() {\n let x = 1;\n let y = 2;\n}");
219 let regions = compute_foldable_regions(&buffer);
220 assert_eq!(regions, vec![FoldRegion::new(0, 2)]);
221 }
222
223 #[test]
224 fn test_nested_blocks() {
225 let buffer = TextBuffer::new(
227 "outer:\n inner:\n deep\n deeper\n after_inner",
228 );
229 let regions = compute_foldable_regions(&buffer);
230 assert_eq!(regions, vec![FoldRegion::new(0, 4), FoldRegion::new(1, 3)]);
231 }
232
233 #[test]
234 fn test_blank_lines_inside_and_trailing() {
235 let buffer =
238 TextBuffer::new("def f():\n a = 1\n\n b = 2\n\ng = 3");
239 let regions = compute_foldable_regions(&buffer);
240 assert_eq!(regions, vec![FoldRegion::new(0, 3)]);
243 }
244
245 #[test]
246 fn test_hidden_lines_single_collapsed() {
247 let regions = vec![FoldRegion::new(0, 2)];
248 let collapsed: HashSet<usize> = [0].into_iter().collect();
249 let hidden = hidden_lines(®ions, &collapsed);
250 assert_eq!(hidden, [1, 2].into_iter().collect());
251 }
252
253 #[test]
254 fn test_hidden_lines_nested_union() {
255 let regions = vec![FoldRegion::new(0, 4), FoldRegion::new(1, 3)];
256 let collapsed: HashSet<usize> = [1].into_iter().collect();
258 assert_eq!(
259 hidden_lines(®ions, &collapsed),
260 [2, 3].into_iter().collect()
261 );
262
263 let collapsed: HashSet<usize> = [0, 1].into_iter().collect();
265 assert_eq!(
266 hidden_lines(®ions, &collapsed),
267 [1, 2, 3, 4].into_iter().collect()
268 );
269 }
270
271 #[test]
272 fn test_hidden_lines_ignores_unknown_collapsed() {
273 let regions = vec![FoldRegion::new(0, 2)];
275 let collapsed: HashSet<usize> = [99].into_iter().collect();
276 assert!(hidden_lines(®ions, &collapsed).is_empty());
277 }
278
279 #[test]
280 fn test_is_fold_header() {
281 let regions = vec![FoldRegion::new(0, 2), FoldRegion::new(5, 7)];
282 assert!(is_fold_header(®ions, 0));
283 assert!(is_fold_header(®ions, 5));
284 assert!(!is_fold_header(®ions, 1));
285 assert!(!is_fold_header(®ions, 3));
286 }
287
288 #[test]
289 fn test_is_line_fold_header_skips_blank_lines() {
290 let buffer = TextBuffer::new("header\n\n body\nsibling");
291 assert!(is_line_fold_header(&buffer, 0));
292 assert!(!is_line_fold_header(&buffer, 1));
293 assert!(!is_line_fold_header(&buffer, 2));
294 }
295}