Skip to main content

logicaffeine_lsp/
folding.rs

1use tower_lsp::lsp_types::{FoldingRange, FoldingRangeKind};
2
3use logicaffeine_language::token::TokenType;
4
5use crate::document::DocumentState;
6
7/// Handle folding range request.
8///
9/// Provides folding for:
10/// - Block headers (## Main, ## To, etc.)
11/// - Indent/Dedent pairs (indented blocks)
12pub fn folding_ranges(doc: &DocumentState) -> Vec<FoldingRange> {
13    let mut ranges = Vec::new();
14
15    // Block header folding
16    for (_i, (_, _, span)) in doc.symbol_index.block_spans.iter().enumerate() {
17        let start_pos = doc.line_index.position(span.start);
18        let end_pos = doc.line_index.position(span.end);
19
20        // Don't create zero-line folds
21        if end_pos.line > start_pos.line {
22            ranges.push(FoldingRange {
23                start_line: start_pos.line,
24                start_character: Some(start_pos.character),
25                end_line: end_pos.line.saturating_sub(1),
26                end_character: None,
27                kind: Some(FoldingRangeKind::Region),
28                collapsed_text: None,
29            });
30        }
31    }
32
33    // Indent/Dedent block folding
34    let mut indent_stack: Vec<u32> = Vec::new();
35    for token in &doc.tokens {
36        match &token.kind {
37            TokenType::Indent => {
38                let pos = doc.line_index.position(token.span.start);
39                indent_stack.push(pos.line);
40            }
41            TokenType::Dedent => {
42                if let Some(start_line) = indent_stack.pop() {
43                    let end_pos = doc.line_index.position(token.span.start);
44                    if end_pos.line > start_line {
45                        ranges.push(FoldingRange {
46                            start_line,
47                            start_character: None,
48                            end_line: end_pos.line.saturating_sub(1),
49                            end_character: None,
50                            kind: Some(FoldingRangeKind::Region),
51                            collapsed_text: None,
52                        });
53                    }
54                }
55            }
56            _ => {}
57        }
58    }
59
60    ranges
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use crate::document::DocumentState;
67
68    fn make_doc(source: &str) -> DocumentState {
69        DocumentState::new(source.to_string(), 1)
70    }
71
72    #[test]
73    fn folding_ranges_for_block() {
74        let doc = make_doc("## Main\n    Let x be 5.\n    Let y be 10.\n");
75        let ranges = folding_ranges(&doc);
76        // Should have at least one folding range for the Main block
77        assert!(!ranges.is_empty(), "Expected folding ranges for Main block");
78    }
79
80    #[test]
81    fn folding_ranges_empty_for_empty_doc() {
82        let doc = make_doc("");
83        let ranges = folding_ranges(&doc);
84        assert!(ranges.is_empty());
85    }
86
87    #[test]
88    fn folding_ranges_have_valid_lines() {
89        let doc = make_doc("## Main\n    Let x be 5.\n    Let y be 10.\n");
90        let ranges = folding_ranges(&doc);
91        for range in &ranges {
92            assert!(
93                range.start_line <= range.end_line,
94                "Folding range has start > end: {} > {}",
95                range.start_line, range.end_line
96            );
97        }
98    }
99
100    #[test]
101    fn folding_ranges_indent_dedent_folding() {
102        let doc = make_doc("## Main\n    If 1 > 0:\n        Let x be 5.\n");
103        let ranges = folding_ranges(&doc);
104        // Should have at least one indent/dedent folding range
105        assert!(!ranges.is_empty(), "Expected folding ranges for indented block");
106    }
107
108    #[test]
109    fn folding_ranges_nested_blocks_multiple_ranges() {
110        let doc = make_doc("## Main\n    Let x be 5.\n    If x > 0:\n        Show x.\n");
111        let ranges = folding_ranges(&doc);
112        assert!(ranges.len() >= 2,
113            "Expected at least 2 folding ranges (block + indent), got {}", ranges.len());
114    }
115
116    #[test]
117    fn folding_ranges_unmatched_indent_no_panic() {
118        // Malformed document — more indents than dedents
119        let doc = make_doc("## Main\n");
120        let ranges = folding_ranges(&doc);
121        // Should not panic on malformed doc — ranges may or may not be produced
122        assert!(ranges.len() < 100, "Unreasonable number of ranges for a tiny doc");
123    }
124
125    #[test]
126    fn folding_ranges_are_region_kind() {
127        let doc = make_doc("## Main\n    Let x be 5.\n    Let y be 10.\n");
128        let ranges = folding_ranges(&doc);
129        for range in &ranges {
130            assert_eq!(
131                range.kind,
132                Some(FoldingRangeKind::Region),
133                "Folding ranges should be Region kind"
134            );
135        }
136    }
137}