use std::collections::HashMap;
use lsp_types::{FoldingRange, FoldingRangeKind, FoldingRangeParams, Uri};
use crate::ir::ast::{ClassDefinition, Equation, Statement};
use crate::lsp::utils::parse_document;
pub fn handle_folding_range(
documents: &HashMap<Uri, String>,
params: FoldingRangeParams,
) -> Option<Vec<FoldingRange>> {
let uri = ¶ms.text_document.uri;
let text = documents.get(uri)?;
let path = uri.path().as_str();
let mut ranges = Vec::new();
collect_comment_ranges(text, &mut ranges);
collect_annotation_ranges(text, &mut ranges);
if let Some(ast) = parse_document(text, path) {
for class in ast.class_list.values() {
collect_class_ranges(class, text, &mut ranges);
}
}
Some(ranges)
}
fn collect_comment_ranges(text: &str, ranges: &mut Vec<FoldingRange>) {
let lines: Vec<&str> = text.lines().collect();
let mut i = 0;
while i < lines.len() {
let line = lines[i].trim();
if line.contains("/*") && !line.contains("*/") {
let start_line = i as u32;
let mut end_line = start_line;
for (j, line) in lines.iter().enumerate().skip(i + 1) {
if line.contains("*/") {
end_line = j as u32;
break;
}
}
if end_line > start_line {
ranges.push(FoldingRange {
start_line,
start_character: None,
end_line,
end_character: None,
kind: Some(FoldingRangeKind::Comment),
collapsed_text: None,
});
}
i = end_line as usize + 1;
continue;
}
if line.starts_with("//") {
let start_line = i as u32;
let mut end_line = start_line;
for (j, line) in lines.iter().enumerate().skip(i + 1) {
if line.trim().starts_with("//") {
end_line = j as u32;
} else {
break;
}
}
if end_line > start_line {
ranges.push(FoldingRange {
start_line,
start_character: None,
end_line,
end_character: None,
kind: Some(FoldingRangeKind::Comment),
collapsed_text: None,
});
}
i = end_line as usize + 1;
continue;
}
i += 1;
}
}
fn collect_annotation_ranges(text: &str, ranges: &mut Vec<FoldingRange>) {
let lines: Vec<&str> = text.lines().collect();
for (line_idx, line) in lines.iter().enumerate() {
if let Some(ann_pos) = find_annotation_start(line) {
let before_ann = &line[..ann_pos];
if before_ann.contains("//") || is_inside_string(line, ann_pos) {
continue;
}
let start_line = line_idx as u32;
let after_ann = &line[ann_pos..];
if let Some(paren_offset) = after_ann.find('(') {
let paren_pos = ann_pos + paren_offset + 1;
if let Some(end_line) = find_matching_paren(&lines, line_idx, paren_pos) {
if end_line > start_line {
ranges.push(FoldingRange {
start_line,
start_character: Some(ann_pos as u32),
end_line,
end_character: None,
kind: Some(FoldingRangeKind::Imports),
collapsed_text: Some("annotation(...)".to_string()),
});
}
}
}
}
}
}
fn find_annotation_start(line: &str) -> Option<usize> {
let mut search_start = 0;
while let Some(pos) = line[search_start..].find("annotation") {
let abs_pos = search_start + pos;
if abs_pos > 0 {
let prev_char = line[..abs_pos].chars().last().unwrap();
if prev_char.is_alphanumeric() || prev_char == '_' {
search_start = abs_pos + 10; continue;
}
}
let after = &line[abs_pos + 10..];
let trimmed = after.trim_start();
if trimmed.starts_with('(') {
return Some(abs_pos);
}
search_start = abs_pos + 10;
}
None
}
fn is_inside_string(line: &str, pos: usize) -> bool {
let before = &line[..pos];
let quote_count = before.matches('"').count() - before.matches("\\\"").count();
quote_count % 2 == 1
}
fn find_matching_paren(lines: &[&str], start_line: usize, start_col: usize) -> Option<u32> {
let mut depth = 1; let mut in_string = false;
let first_line = lines.get(start_line)?;
for ch in first_line[start_col..].chars() {
match ch {
'"' => in_string = !in_string,
'(' if !in_string => depth += 1,
')' if !in_string => {
depth -= 1;
if depth == 0 {
return Some(start_line as u32);
}
}
_ => {}
}
}
for (offset, line) in lines.iter().enumerate().skip(start_line + 1) {
for ch in line.chars() {
match ch {
'"' => in_string = !in_string,
'(' if !in_string => depth += 1,
')' if !in_string => {
depth -= 1;
if depth == 0 {
return Some(offset as u32);
}
}
_ => {}
}
}
}
None
}
fn collect_class_ranges(class: &ClassDefinition, text: &str, ranges: &mut Vec<FoldingRange>) {
let class_name = &class.name.text;
let start_line = class.location.start_line.saturating_sub(1);
let end_line = class.location.end_line.saturating_sub(1);
if end_line > start_line {
ranges.push(FoldingRange {
start_line,
start_character: None,
end_line,
end_character: None,
kind: Some(FoldingRangeKind::Region),
collapsed_text: Some(format!("{:?} {} ...", class.class_type, class_name)),
});
}
if let Some(ref eq_kw) = class.equation_keyword {
let eq_start = eq_kw.location.start_line.saturating_sub(1);
let eq_end = find_section_end(class, eq_start, end_line);
if eq_end > eq_start {
ranges.push(FoldingRange {
start_line: eq_start,
start_character: None,
end_line: eq_end,
end_character: None,
kind: Some(FoldingRangeKind::Region),
collapsed_text: Some("equation ...".to_string()),
});
}
}
if let Some(ref eq_kw) = class.initial_equation_keyword {
let eq_start = eq_kw.location.start_line.saturating_sub(1);
let eq_end = find_section_end(class, eq_start, end_line);
if eq_end > eq_start {
ranges.push(FoldingRange {
start_line: eq_start,
start_character: None,
end_line: eq_end,
end_character: None,
kind: Some(FoldingRangeKind::Region),
collapsed_text: Some("initial equation ...".to_string()),
});
}
}
if let Some(ref alg_kw) = class.algorithm_keyword {
let alg_start = alg_kw.location.start_line.saturating_sub(1);
let alg_end = find_section_end(class, alg_start, end_line);
if alg_end > alg_start {
ranges.push(FoldingRange {
start_line: alg_start,
start_character: None,
end_line: alg_end,
end_character: None,
kind: Some(FoldingRangeKind::Region),
collapsed_text: Some("algorithm ...".to_string()),
});
}
}
if let Some(ref alg_kw) = class.initial_algorithm_keyword {
let alg_start = alg_kw.location.start_line.saturating_sub(1);
let alg_end = find_section_end(class, alg_start, end_line);
if alg_end > alg_start {
ranges.push(FoldingRange {
start_line: alg_start,
start_character: None,
end_line: alg_end,
end_character: None,
kind: Some(FoldingRangeKind::Region),
collapsed_text: Some("initial algorithm ...".to_string()),
});
}
}
for eq in &class.equations {
collect_equation_ranges(eq, text, ranges);
}
for eq in &class.initial_equations {
collect_equation_ranges(eq, text, ranges);
}
for algo in &class.algorithms {
for stmt in algo {
collect_statement_ranges(stmt, text, ranges);
}
}
for algo in &class.initial_algorithms {
for stmt in algo {
collect_statement_ranges(stmt, text, ranges);
}
}
for nested_class in class.classes.values() {
collect_class_ranges(nested_class, text, ranges);
}
}
fn find_section_end(class: &ClassDefinition, section_start: u32, class_end: u32) -> u32 {
let mut section_starts: Vec<u32> = vec![];
if let Some(ref kw) = class.equation_keyword {
section_starts.push(kw.location.start_line.saturating_sub(1));
}
if let Some(ref kw) = class.initial_equation_keyword {
section_starts.push(kw.location.start_line.saturating_sub(1));
}
if let Some(ref kw) = class.algorithm_keyword {
section_starts.push(kw.location.start_line.saturating_sub(1));
}
if let Some(ref kw) = class.initial_algorithm_keyword {
section_starts.push(kw.location.start_line.saturating_sub(1));
}
section_starts.sort();
for &start in §ion_starts {
if start > section_start {
return start.saturating_sub(1);
}
}
class_end.saturating_sub(1)
}
fn collect_equation_ranges(eq: &Equation, text: &str, ranges: &mut Vec<FoldingRange>) {
match eq {
Equation::If {
cond_blocks,
else_block: _,
} => {
if let Some(first_block) = cond_blocks.first()
&& let Some(loc) = first_block.cond.get_location()
{
let start_line = loc.start_line.saturating_sub(1);
if let Some(end_line) = find_end_keyword(text, start_line, "if") {
ranges.push(FoldingRange {
start_line,
start_character: None,
end_line,
end_character: None,
kind: Some(FoldingRangeKind::Region),
collapsed_text: Some("if ... end if".to_string()),
});
}
}
for block in cond_blocks {
for inner_eq in &block.eqs {
collect_equation_ranges(inner_eq, text, ranges);
}
}
}
Equation::For { indices, equations } => {
if let Some(first_index) = indices.first() {
let start_line = first_index.ident.location.start_line.saturating_sub(1);
if let Some(end_line) = find_end_keyword(text, start_line, "for") {
ranges.push(FoldingRange {
start_line,
start_character: None,
end_line,
end_character: None,
kind: Some(FoldingRangeKind::Region),
collapsed_text: Some("for ... end for".to_string()),
});
}
}
for inner_eq in equations {
collect_equation_ranges(inner_eq, text, ranges);
}
}
Equation::When(blocks) => {
if let Some(first_block) = blocks.first()
&& let Some(loc) = first_block.cond.get_location()
{
let start_line = loc.start_line.saturating_sub(1);
if let Some(end_line) = find_end_keyword(text, start_line, "when") {
ranges.push(FoldingRange {
start_line,
start_character: None,
end_line,
end_character: None,
kind: Some(FoldingRangeKind::Region),
collapsed_text: Some("when ... end when".to_string()),
});
}
}
for block in blocks {
for inner_eq in &block.eqs {
collect_equation_ranges(inner_eq, text, ranges);
}
}
}
_ => {}
}
}
fn collect_statement_ranges(stmt: &Statement, text: &str, ranges: &mut Vec<FoldingRange>) {
match stmt {
Statement::For { indices, equations } => {
if let Some(first_index) = indices.first() {
let start_line = first_index.ident.location.start_line.saturating_sub(1);
if let Some(end_line) = find_end_keyword(text, start_line, "for") {
ranges.push(FoldingRange {
start_line,
start_character: None,
end_line,
end_character: None,
kind: Some(FoldingRangeKind::Region),
collapsed_text: Some("for ... end for".to_string()),
});
}
}
for inner_stmt in equations {
collect_statement_ranges(inner_stmt, text, ranges);
}
}
Statement::While(block) => {
if let Some(loc) = block.cond.get_location() {
let start_line = loc.start_line.saturating_sub(1);
if let Some(end_line) = find_end_keyword(text, start_line, "while") {
ranges.push(FoldingRange {
start_line,
start_character: None,
end_line,
end_character: None,
kind: Some(FoldingRangeKind::Region),
collapsed_text: Some("while ... end while".to_string()),
});
}
}
for inner_stmt in &block.stmts {
collect_statement_ranges(inner_stmt, text, ranges);
}
}
Statement::If {
cond_blocks,
else_block,
} => {
if let Some(first_block) = cond_blocks.first()
&& let Some(loc) = first_block.cond.get_location()
{
let start_line = loc.start_line.saturating_sub(1);
if let Some(end_line) = find_end_keyword(text, start_line, "if") {
ranges.push(FoldingRange {
start_line,
start_character: None,
end_line,
end_character: None,
kind: Some(FoldingRangeKind::Region),
collapsed_text: Some("if ... end if".to_string()),
});
}
}
for block in cond_blocks {
for inner_stmt in &block.stmts {
collect_statement_ranges(inner_stmt, text, ranges);
}
}
if let Some(else_stmts) = else_block {
for inner_stmt in else_stmts {
collect_statement_ranges(inner_stmt, text, ranges);
}
}
}
Statement::When(blocks) => {
if let Some(first_block) = blocks.first()
&& let Some(loc) = first_block.cond.get_location()
{
let start_line = loc.start_line.saturating_sub(1);
if let Some(end_line) = find_end_keyword(text, start_line, "when") {
ranges.push(FoldingRange {
start_line,
start_character: None,
end_line,
end_character: None,
kind: Some(FoldingRangeKind::Region),
collapsed_text: Some("when ... end when".to_string()),
});
}
}
for block in blocks {
for inner_stmt in &block.stmts {
collect_statement_ranges(inner_stmt, text, ranges);
}
}
}
_ => {}
}
}
fn find_end_keyword(text: &str, start_line: u32, keyword: &str) -> Option<u32> {
let lines: Vec<&str> = text.lines().collect();
let end_pattern = format!("end {}", keyword);
let mut depth = 0;
for (i, line) in lines.iter().enumerate().skip(start_line as usize) {
let trimmed = line.trim();
if trimmed.starts_with(keyword) && !trimmed.starts_with("end") {
depth += 1;
}
if trimmed.starts_with(&end_pattern) || trimmed == &end_pattern[..end_pattern.len()] {
if depth <= 1 {
return Some(i as u32);
}
depth -= 1;
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_collect_comment_ranges_multiline() {
let text = "/* This is\na multi-line\ncomment */\ncode here";
let mut ranges = Vec::new();
collect_comment_ranges(text, &mut ranges);
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].start_line, 0);
assert_eq!(ranges[0].end_line, 2);
}
#[test]
fn test_collect_comment_ranges_consecutive() {
let text = "// Comment 1\n// Comment 2\n// Comment 3\ncode";
let mut ranges = Vec::new();
collect_comment_ranges(text, &mut ranges);
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].start_line, 0);
assert_eq!(ranges[0].end_line, 2);
}
#[test]
fn test_find_end_keyword() {
let text = "for i in 1:10 loop\n x := i;\nend for;";
let end_line = find_end_keyword(text, 0, "for");
assert_eq!(end_line, Some(2));
}
#[test]
fn test_collect_annotation_ranges_single_line() {
let text = "x = 1 annotation(Evaluate=true);";
let mut ranges = Vec::new();
collect_annotation_ranges(text, &mut ranges);
assert_eq!(ranges.len(), 0);
}
#[test]
fn test_collect_annotation_ranges_multiline() {
let text = r#" annotation(
Documentation(info="<html>
<p>Test</p>
</html>"));"#;
let mut ranges = Vec::new();
collect_annotation_ranges(text, &mut ranges);
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].start_line, 0);
assert_eq!(ranges[0].end_line, 3);
assert_eq!(
ranges[0].collapsed_text,
Some("annotation(...)".to_string())
);
}
#[test]
fn test_collect_annotation_ranges_nested_parens() {
let text = r#"annotation(
Placement(transformation(extent={{-80,70},{-60,90}})),
Documentation(info="text"))"#;
let mut ranges = Vec::new();
collect_annotation_ranges(text, &mut ranges);
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].start_line, 0);
assert_eq!(ranges[0].end_line, 2);
}
#[test]
fn test_collect_annotation_ranges_with_whitespace() {
let text = r#" annotation (
Documentation(info="<html>
<p>Test</p>
</html>"));"#;
let mut ranges = Vec::new();
collect_annotation_ranges(text, &mut ranges);
assert_eq!(
ranges.len(),
1,
"Expected 1 folding range for 'annotation ('"
);
assert_eq!(ranges[0].start_line, 0);
assert_eq!(ranges[0].end_line, 3);
}
#[test]
fn test_find_annotation_start() {
assert_eq!(find_annotation_start(" annotation(x=1)"), Some(2));
assert_eq!(find_annotation_start(" annotation (x=1)"), Some(2));
assert_eq!(find_annotation_start("annotation (x=1)"), Some(0));
assert_eq!(find_annotation_start("annotation\t(x=1)"), Some(0));
assert_eq!(find_annotation_start("annotation x"), None);
assert_eq!(find_annotation_start("myannotation(x=1)"), None);
}
#[test]
fn test_algorithm_section_folding() {
use lsp_types::Uri;
use std::collections::HashMap;
let text = r#"model Test
Real x;
algorithm
x := 1;
x := x + 1;
x := x * 2;
end Test;
"#;
let uri: Uri = "file:///tmp/test.mo".parse().unwrap();
let mut documents = HashMap::new();
documents.insert(uri.clone(), text.to_string());
let params = FoldingRangeParams {
text_document: lsp_types::TextDocumentIdentifier { uri },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let ranges = handle_folding_range(&documents, params).unwrap();
let algo_folds: Vec<_> = ranges
.iter()
.filter(|r| r.collapsed_text == Some("algorithm ...".to_string()))
.collect();
assert!(
!algo_folds.is_empty(),
"Expected algorithm folding range, got: {:?}",
ranges
);
}
}