logicaffeine_lsp/
folding.rs1use tower_lsp::lsp_types::{FoldingRange, FoldingRangeKind};
2
3use logicaffeine_language::token::TokenType;
4
5use crate::document::DocumentState;
6
7pub fn folding_ranges(doc: &DocumentState) -> Vec<FoldingRange> {
13 let mut ranges = Vec::new();
14
15 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 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 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 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 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 let doc = make_doc("## Main\n");
120 let ranges = folding_ranges(&doc);
121 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}