Skip to main content

fresh/view/
folding.rs

1//! Folding range infrastructure
2//!
3//! Provides a marker-based system for tracking collapsed folding ranges.
4//! Fold ranges are stored as byte markers so they auto-adjust on edits.
5
6use crate::model::buffer::Buffer;
7use crate::model::marker::{MarkerId, MarkerList};
8
9/// A collapsed fold range tracked by markers.
10#[derive(Debug, Clone)]
11pub struct FoldRange {
12    /// Marker at the first hidden byte (start of line after header)
13    start_marker: MarkerId,
14    /// Marker at the end of the hidden range (start of line after fold end)
15    end_marker: MarkerId,
16    /// Optional placeholder text for the folded range
17    placeholder: Option<String>,
18}
19
20/// A resolved fold range with computed line/byte info.
21#[derive(Debug, Clone)]
22pub struct ResolvedFoldRange {
23    /// Header line number (the visible line that owns the fold)
24    pub header_line: usize,
25    /// First hidden line number (header_line + 1)
26    pub start_line: usize,
27    /// Last hidden line number (inclusive)
28    pub end_line: usize,
29    /// Start byte of hidden range
30    pub start_byte: usize,
31    /// End byte of hidden range (exclusive)
32    pub end_byte: usize,
33    /// Line-start byte of the fold header
34    pub header_byte: usize,
35    /// Optional placeholder text
36    pub placeholder: Option<String>,
37}
38
39/// Collapsed fold range represented by line numbers for persistence/cloning.
40#[derive(Debug, Clone)]
41pub struct CollapsedFoldLineRange {
42    /// Header line number (visible line that owns the fold)
43    pub header_line: usize,
44    /// Last hidden line number (inclusive)
45    pub end_line: usize,
46    /// Optional placeholder text
47    pub placeholder: Option<String>,
48}
49
50/// Manages collapsed fold ranges for a buffer.
51#[derive(Debug, Clone)]
52pub struct FoldManager {
53    ranges: Vec<FoldRange>,
54}
55
56impl FoldManager {
57    /// Create a new empty fold manager.
58    pub fn new() -> Self {
59        Self { ranges: Vec::new() }
60    }
61
62    /// Returns true if there are no collapsed folds.
63    pub fn is_empty(&self) -> bool {
64        self.ranges.is_empty()
65    }
66
67    /// Add a collapsed fold range.
68    pub fn add(
69        &mut self,
70        marker_list: &mut MarkerList,
71        start: usize,
72        end: usize,
73        placeholder: Option<String>,
74    ) {
75        if end <= start {
76            return;
77        }
78
79        let start_marker = marker_list.create(start, true); // left affinity
80        let end_marker = marker_list.create(end, false); // right affinity
81
82        self.ranges.push(FoldRange {
83            start_marker,
84            end_marker,
85            placeholder,
86        });
87    }
88
89    /// Remove all fold ranges and their markers.
90    pub fn clear(&mut self, marker_list: &mut MarkerList) {
91        for range in &self.ranges {
92            marker_list.delete(range.start_marker);
93            marker_list.delete(range.end_marker);
94        }
95        self.ranges.clear();
96    }
97
98    /// Remove any fold that contains the given byte position.
99    /// Returns true if a fold was removed.
100    pub fn remove_if_contains_byte(&mut self, marker_list: &mut MarkerList, byte: usize) -> bool {
101        let mut to_delete = Vec::new();
102
103        self.ranges.retain(|range| {
104            let Some(start_byte) = marker_list.get_position(range.start_marker) else {
105                return true;
106            };
107            let Some(end_byte) = marker_list.get_position(range.end_marker) else {
108                return true;
109            };
110            if start_byte <= byte && byte < end_byte {
111                to_delete.push((range.start_marker, range.end_marker));
112                false
113            } else {
114                true
115            }
116        });
117
118        for (start, end) in &to_delete {
119            marker_list.delete(*start);
120            marker_list.delete(*end);
121        }
122
123        !to_delete.is_empty()
124    }
125
126    /// Resolve all fold ranges into line/byte ranges, filtering invalid entries.
127    pub fn resolved_ranges(
128        &self,
129        buffer: &Buffer,
130        marker_list: &MarkerList,
131    ) -> Vec<ResolvedFoldRange> {
132        let mut ranges = Vec::new();
133
134        for range in &self.ranges {
135            let Some(start_byte) = marker_list.get_position(range.start_marker) else {
136                continue;
137            };
138            let Some(end_byte) = marker_list.get_position(range.end_marker) else {
139                continue;
140            };
141            if end_byte <= start_byte {
142                continue;
143            }
144
145            let start_line = buffer.get_line_number(start_byte);
146            if start_line == 0 {
147                continue;
148            }
149            let end_line = buffer.get_line_number(end_byte.saturating_sub(1));
150            if end_line < start_line {
151                continue;
152            }
153
154            let header_byte =
155                indent_folding::find_line_start_byte(buffer, start_byte.saturating_sub(1));
156
157            ranges.push(ResolvedFoldRange {
158                header_line: start_line - 1,
159                start_line,
160                end_line,
161                start_byte,
162                end_byte,
163                header_byte,
164                placeholder: range.placeholder.clone(),
165            });
166        }
167
168        ranges
169    }
170
171    /// Return a map of header_byte -> placeholder for collapsed folds.
172    pub fn collapsed_header_bytes(
173        &self,
174        buffer: &Buffer,
175        marker_list: &MarkerList,
176    ) -> std::collections::BTreeMap<usize, Option<String>> {
177        let mut map = std::collections::BTreeMap::new();
178        for range in self.resolved_ranges(buffer, marker_list) {
179            map.insert(range.header_byte, range.placeholder);
180        }
181        map
182    }
183
184    /// Remove the fold range whose header byte matches `target_header_byte`.
185    /// Returns true if a fold was removed.
186    pub fn remove_by_header_byte(
187        &mut self,
188        buffer: &Buffer,
189        marker_list: &mut MarkerList,
190        target_header_byte: usize,
191    ) -> bool {
192        let mut to_delete = Vec::new();
193
194        self.ranges.retain(|range| {
195            let Some(start_byte) = marker_list.get_position(range.start_marker) else {
196                return true;
197            };
198            let current_header =
199                indent_folding::find_line_start_byte(buffer, start_byte.saturating_sub(1));
200            if current_header == target_header_byte {
201                to_delete.push((range.start_marker, range.end_marker));
202                false
203            } else {
204                true
205            }
206        });
207
208        for (start, end) in &to_delete {
209            marker_list.delete(*start);
210            marker_list.delete(*end);
211        }
212
213        !to_delete.is_empty()
214    }
215
216    /// Return collapsed fold ranges as line-based data (for persistence/cloning).
217    pub fn collapsed_line_ranges(
218        &self,
219        buffer: &Buffer,
220        marker_list: &MarkerList,
221    ) -> Vec<CollapsedFoldLineRange> {
222        self.resolved_ranges(buffer, marker_list)
223            .into_iter()
224            .map(|range| CollapsedFoldLineRange {
225                header_line: range.header_line,
226                end_line: range.end_line,
227                placeholder: range.placeholder,
228            })
229            .collect()
230    }
231
232    /// Count total hidden lines for folds with headers in the given range.
233    pub fn hidden_line_count_in_range(
234        &self,
235        buffer: &Buffer,
236        marker_list: &MarkerList,
237        start_line: usize,
238        end_line: usize,
239    ) -> usize {
240        let mut hidden = 0usize;
241        for range in self.resolved_ranges(buffer, marker_list) {
242            if range.header_line >= start_line && range.header_line <= end_line {
243                hidden = hidden.saturating_add(range.end_line.saturating_sub(range.start_line) + 1);
244            }
245        }
246        hidden
247    }
248}
249
250impl Default for FoldManager {
251    fn default() -> Self {
252        Self::new()
253    }
254}
255
256/// Indent-based folding fallback for when LSP folding ranges are not available.
257///
258/// Computes foldable ranges by analyzing indentation levels, reusing the same
259/// indent measurement logic as the auto-indent feature
260/// ([`PatternIndentCalculator::count_leading_indent`]).
261pub mod indent_folding {
262    use crate::model::buffer::Buffer;
263    use crate::primitives::indent_pattern::PatternIndentCalculator;
264
265    /// Find the byte offset of the start of the line containing `pos`.
266    /// Scans backward for `\n` (or returns 0).
267    pub fn find_line_start_byte(buffer: &Buffer, pos: usize) -> usize {
268        if pos == 0 {
269            return 0;
270        }
271        let mut p = pos.min(buffer.len()).saturating_sub(1);
272        loop {
273            match PatternIndentCalculator::byte_at(buffer, p) {
274                Some(b'\n') => return p + 1,
275                None => return 0,
276                _ => {
277                    if p == 0 {
278                        return 0;
279                    }
280                    p -= 1;
281                }
282            }
283        }
284    }
285
286    /// Measure leading indent of a line given as a byte slice (no trailing `\n`).
287    fn slice_indent(line: &[u8], tab_size: usize) -> (usize, bool) {
288        let mut indent = 0;
289        let mut all_blank = true;
290        for &b in line {
291            match b {
292                b' ' => indent += 1,
293                b'\t' => {
294                    if tab_size > 0 {
295                        indent += tab_size - (indent % tab_size);
296                    } else {
297                        indent += 1;
298                    }
299                }
300                b'\r' => {}
301                _ => {
302                    all_blank = false;
303                    break;
304                }
305            }
306        }
307        (indent, all_blank)
308    }
309
310    /// Identify foldable lines in a raw byte slice by analysing indentation.
311    ///
312    /// Works without any line metadata, so it can be used on large files whose
313    /// piece tree has not been scanned for line feeds.
314    ///
315    /// `max_lookahead` limits how many lines *ahead* of each candidate we scan
316    /// to decide foldability.
317    ///
318    /// Returns an iterator of 0-based line indices (within the slice) that are
319    /// foldable.
320    pub fn foldable_lines_in_bytes(
321        bytes: &[u8],
322        tab_size: usize,
323        max_lookahead: usize,
324    ) -> Vec<usize> {
325        // Split into lines (preserving empty trailing line if present).
326        let lines: Vec<&[u8]> = bytes.split(|&b| b == b'\n').collect();
327        let line_count = lines.len();
328        let mut result = Vec::new();
329
330        for i in 0..line_count {
331            let (header_indent, header_blank) = slice_indent(lines[i], tab_size);
332            if header_blank {
333                continue;
334            }
335
336            // Find next non-blank line within lookahead.
337            let limit = line_count.min(i + 1 + max_lookahead);
338            let mut next = i + 1;
339            while next < limit {
340                let (_, blank) = slice_indent(lines[next], tab_size);
341                if !blank {
342                    break;
343                }
344                next += 1;
345            }
346            if next >= limit {
347                continue;
348            }
349
350            let (next_indent, _) = slice_indent(lines[next], tab_size);
351            if next_indent > header_indent {
352                result.push(i);
353            }
354        }
355
356        result
357    }
358
359    /// Byte-based fold-end search for a single header line.
360    ///
361    /// Reads up to `max_scan_bytes` forward from `header_byte` and determines
362    /// whether the line at that offset is foldable (next non-blank line is more
363    /// indented).  Returns `Some(end_byte)` where `end_byte` is the start of
364    /// the last non-blank line still inside the fold, or `None`.
365    pub fn indent_fold_end_byte(
366        buffer: &Buffer,
367        header_byte: usize,
368        tab_size: usize,
369        max_scan_bytes: usize,
370    ) -> Option<usize> {
371        let buf_len = buffer.len();
372        let end = buf_len.min(header_byte.saturating_add(max_scan_bytes));
373        let bytes = buffer.slice_bytes(header_byte..end);
374        if bytes.is_empty() {
375            return None;
376        }
377
378        let lines: Vec<&[u8]> = bytes.split(|&b| b == b'\n').collect();
379        if lines.is_empty() {
380            return None;
381        }
382
383        let (header_indent, header_blank) = slice_indent(lines[0], tab_size);
384        if header_blank {
385            return None;
386        }
387
388        // Find next non-blank line.
389        let mut next = 1;
390        while next < lines.len() {
391            let (_, blank) = slice_indent(lines[next], tab_size);
392            if !blank {
393                break;
394            }
395            next += 1;
396        }
397        if next >= lines.len() {
398            return None;
399        }
400
401        let (next_indent, _) = slice_indent(lines[next], tab_size);
402        if next_indent <= header_indent {
403            return None;
404        }
405
406        // Scan forward for fold boundary.
407        let mut last_non_blank_line = next;
408        let mut current = next + 1;
409        while current < lines.len() {
410            let (indent, blank) = slice_indent(lines[current], tab_size);
411            if blank {
412                current += 1;
413                continue;
414            }
415            if indent <= header_indent {
416                break;
417            }
418            last_non_blank_line = current;
419            current += 1;
420        }
421
422        if last_non_blank_line < 1 {
423            return None;
424        }
425
426        // Convert line index back to byte offset: sum lengths of lines 0..last_non_blank_line
427        // (each line was separated by a `\n`).
428        let mut byte_offset = 0;
429        for i in 0..last_non_blank_line {
430            byte_offset += lines[i].len() + 1; // +1 for the \n
431        }
432        Some(header_byte + byte_offset)
433    }
434
435    /// Find the byte offset of the start of the *next* line after `pos`.
436    /// Scans forward for `\n` and returns the byte after it. If no `\n` is
437    /// found, returns `buffer.len()`.
438    pub fn find_next_line_start_byte(buffer: &Buffer, pos: usize) -> usize {
439        let mut p = pos;
440        let len = buffer.len();
441        while p < len {
442            match PatternIndentCalculator::byte_at(buffer, p) {
443                Some(b'\n') => return p + 1,
444                None => return len,
445                _ => p += 1,
446            }
447        }
448        len
449    }
450
451    /// Byte-range of a fold that contains `target_byte`.
452    ///
453    /// Walks backward (up to `max_upward_lines` lines) from the line
454    /// containing `target_byte`, trying each candidate as a fold header via
455    /// [`indent_fold_end_byte`].  When a fold is found whose hidden range
456    /// reaches at least `target_byte`, returns `(header_byte, start_byte,
457    /// end_byte)` where:
458    ///
459    /// * `header_byte` – first byte of the fold header line
460    /// * `start_byte`  – first hidden byte (start of the line after the header)
461    /// * `end_byte`    – one past the last hidden byte (start of the line
462    ///   *after* the last hidden line, or `buffer.len()`)
463    ///
464    /// Returns `None` if no enclosing fold is found within the search limit.
465    pub fn find_fold_range_at_byte(
466        buffer: &Buffer,
467        target_byte: usize,
468        tab_size: usize,
469        max_scan_bytes: usize,
470        max_upward_lines: usize,
471    ) -> Option<(usize, usize, usize)> {
472        let mut header_byte = find_line_start_byte(buffer, target_byte);
473
474        for _ in 0..=max_upward_lines {
475            if let Some(fold_end_byte) =
476                indent_fold_end_byte(buffer, header_byte, tab_size, max_scan_bytes)
477            {
478                if fold_end_byte >= target_byte {
479                    let eb = find_next_line_start_byte(buffer, fold_end_byte);
480                    let sb = find_next_line_start_byte(buffer, header_byte);
481                    if sb < eb {
482                        return Some((header_byte, sb, eb));
483                    }
484                }
485            }
486            if header_byte == 0 {
487                break;
488            }
489            header_byte = find_line_start_byte(buffer, header_byte.saturating_sub(1));
490        }
491
492        None
493    }
494
495    #[cfg(test)]
496    mod tests {
497        use super::*;
498
499        #[test]
500        fn test_slice_indent_spaces() {
501            assert_eq!(slice_indent(b"    hello", 4), (4, false));
502            assert_eq!(slice_indent(b"hello", 4), (0, false));
503            assert_eq!(slice_indent(b"        deep", 4), (8, false));
504        }
505
506        #[test]
507        fn test_slice_indent_tabs() {
508            assert_eq!(slice_indent(b"\thello", 4), (4, false));
509            assert_eq!(slice_indent(b"\t\thello", 4), (8, false));
510            // Mixed: 2 spaces + tab (tab_size=4) → 2 + (4-2) = 4
511            assert_eq!(slice_indent(b"  \thello", 4), (4, false));
512        }
513
514        #[test]
515        fn test_slice_indent_blank() {
516            assert_eq!(slice_indent(b"", 4), (0, true));
517            assert_eq!(slice_indent(b"   ", 4), (3, true));
518            assert_eq!(slice_indent(b"  \r", 4), (2, true));
519        }
520
521        #[test]
522        fn test_foldable_lines_basic() {
523            let text = b"fn main() {\n    println!();\n}\n";
524            let foldable = foldable_lines_in_bytes(text, 4, 50);
525            assert_eq!(foldable, vec![0]); // line 0 is foldable
526        }
527
528        #[test]
529        fn test_foldable_lines_nested() {
530            let text = b"fn main() {\n    if true {\n        x();\n    }\n}\n";
531            let foldable = foldable_lines_in_bytes(text, 4, 50);
532            assert_eq!(foldable, vec![0, 1]); // both fn and if are foldable
533        }
534
535        #[test]
536        fn test_foldable_lines_not_foldable() {
537            let text = b"line1\nline2\nline3\n";
538            let foldable = foldable_lines_in_bytes(text, 4, 50);
539            assert!(foldable.is_empty());
540        }
541
542        #[test]
543        fn test_foldable_lines_blank_lines_skipped() {
544            // Blank line between header and indented line should still be foldable
545            let text = b"fn main() {\n\n    println!();\n}\n";
546            let foldable = foldable_lines_in_bytes(text, 4, 50);
547            assert_eq!(foldable, vec![0]);
548        }
549
550        #[test]
551        fn test_foldable_lines_max_lookahead() {
552            // With max_lookahead=1, a blank line between header and content means
553            // the lookahead can't reach the indented line.
554            let text = b"fn main() {\n\n\n    println!();\n}\n";
555            let foldable_short = foldable_lines_in_bytes(text, 4, 1);
556            assert!(foldable_short.is_empty());
557
558            let foldable_long = foldable_lines_in_bytes(text, 4, 50);
559            assert_eq!(foldable_long, vec![0]);
560        }
561    }
562}