use tower_lsp::lsp_types::{FoldingRange, FoldingRangeKind};
use logicaffeine_language::token::TokenType;
use crate::document::DocumentState;
pub fn folding_ranges(doc: &DocumentState) -> Vec<FoldingRange> {
let mut ranges = Vec::new();
for (_i, (_, _, span)) in doc.symbol_index.block_spans.iter().enumerate() {
let start_pos = doc.line_index.position(span.start);
let end_pos = doc.line_index.position(span.end);
if end_pos.line > start_pos.line {
ranges.push(FoldingRange {
start_line: start_pos.line,
start_character: Some(start_pos.character),
end_line: end_pos.line.saturating_sub(1),
end_character: None,
kind: Some(FoldingRangeKind::Region),
collapsed_text: None,
});
}
}
let mut indent_stack: Vec<u32> = Vec::new();
for token in &doc.tokens {
match &token.kind {
TokenType::Indent => {
let pos = doc.line_index.position(token.span.start);
indent_stack.push(pos.line);
}
TokenType::Dedent => {
if let Some(start_line) = indent_stack.pop() {
let end_pos = doc.line_index.position(token.span.start);
if end_pos.line > start_line {
ranges.push(FoldingRange {
start_line,
start_character: None,
end_line: end_pos.line.saturating_sub(1),
end_character: None,
kind: Some(FoldingRangeKind::Region),
collapsed_text: None,
});
}
}
}
_ => {}
}
}
ranges
}
#[cfg(test)]
mod tests {
use super::*;
use crate::document::DocumentState;
fn make_doc(source: &str) -> DocumentState {
DocumentState::new(source.to_string(), 1)
}
#[test]
fn folding_ranges_for_block() {
let doc = make_doc("## Main\n Let x be 5.\n Let y be 10.\n");
let ranges = folding_ranges(&doc);
assert!(!ranges.is_empty(), "Expected folding ranges for Main block");
}
#[test]
fn folding_ranges_empty_for_empty_doc() {
let doc = make_doc("");
let ranges = folding_ranges(&doc);
assert!(ranges.is_empty());
}
#[test]
fn folding_ranges_have_valid_lines() {
let doc = make_doc("## Main\n Let x be 5.\n Let y be 10.\n");
let ranges = folding_ranges(&doc);
for range in &ranges {
assert!(
range.start_line <= range.end_line,
"Folding range has start > end: {} > {}",
range.start_line, range.end_line
);
}
}
#[test]
fn folding_ranges_indent_dedent_folding() {
let doc = make_doc("## Main\n If 1 > 0:\n Let x be 5.\n");
let ranges = folding_ranges(&doc);
assert!(!ranges.is_empty(), "Expected folding ranges for indented block");
}
#[test]
fn folding_ranges_nested_blocks_multiple_ranges() {
let doc = make_doc("## Main\n Let x be 5.\n If x > 0:\n Show x.\n");
let ranges = folding_ranges(&doc);
assert!(ranges.len() >= 2,
"Expected at least 2 folding ranges (block + indent), got {}", ranges.len());
}
#[test]
fn folding_ranges_unmatched_indent_no_panic() {
let doc = make_doc("## Main\n");
let ranges = folding_ranges(&doc);
assert!(ranges.len() < 100, "Unreasonable number of ranges for a tiny doc");
}
#[test]
fn folding_ranges_are_region_kind() {
let doc = make_doc("## Main\n Let x be 5.\n Let y be 10.\n");
let ranges = folding_ranges(&doc);
for range in &ranges {
assert_eq!(
range.kind,
Some(FoldingRangeKind::Region),
"Folding ranges should be Region kind"
);
}
}
}