use lsp_types::{FoldingRange, FoldingRangeKind, FoldingRangeParams};
use rustledger_core::Directive;
use rustledger_parser::ParseResult;
use super::utils::LineIndex;
pub fn handle_folding_ranges(
_params: &FoldingRangeParams,
source: &str,
parse_result: &ParseResult,
) -> Option<Vec<FoldingRange>> {
let mut ranges = Vec::new();
let line_index = LineIndex::new(source);
for spanned in &parse_result.directives {
if let Directive::Transaction(txn) = &spanned.value
&& !txn.postings.is_empty()
{
let (start_line, _) = line_index.offset_to_position(spanned.span.start);
let (end_line, _) = line_index.offset_to_position(spanned.span.end);
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_transaction_summary(txn)),
});
}
}
}
let lines: Vec<&str> = source.lines().collect();
let mut section_start: Option<(u32, &str)> = None;
for (line_num, line) in lines.iter().enumerate() {
let trimmed = line.trim();
if is_section_header(trimmed) {
if let Some((start, _title)) = section_start
&& line_num as u32 > start + 1
{
ranges.push(FoldingRange {
start_line: start,
start_character: None,
end_line: line_num as u32 - 1,
end_character: None,
kind: Some(FoldingRangeKind::Region),
collapsed_text: None,
});
}
section_start = Some((line_num as u32, trimmed));
}
}
if let Some((start, _title)) = section_start {
let end = lines.len() as u32;
if end > start + 1 {
ranges.push(FoldingRange {
start_line: start,
start_character: None,
end_line: end - 1,
end_character: None,
kind: Some(FoldingRangeKind::Region),
collapsed_text: None,
});
}
}
let mut comment_start: Option<u32> = None;
for (line_num, line) in lines.iter().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with(';') && !is_section_header(trimmed) {
if comment_start.is_none() {
comment_start = Some(line_num as u32);
}
} else if let Some(start) = comment_start {
let end = line_num as u32 - 1;
if end > start + 2 {
ranges.push(FoldingRange {
start_line: start,
start_character: None,
end_line: end,
end_character: None,
kind: Some(FoldingRangeKind::Comment),
collapsed_text: None,
});
}
comment_start = None;
}
}
ranges.sort_by_key(|a| a.start_line);
ranges.dedup_by(|a, b| a.start_line == b.start_line && a.end_line == b.end_line);
if ranges.is_empty() {
None
} else {
Some(ranges)
}
}
fn format_transaction_summary(txn: &rustledger_core::Transaction) -> String {
let date = txn.date.to_string();
if let Some(ref payee) = txn.payee {
format!("{} {} ...", date, payee)
} else if !txn.narration.is_empty() {
let narration = txn.narration.to_string();
let truncated = if narration.len() > 30 {
format!("{}...", &narration[..30])
} else {
narration
};
format!("{} {} ...", date, truncated)
} else {
format!("{} Transaction ({} postings)", date, txn.postings.len())
}
}
fn is_section_header(line: &str) -> bool {
if !line.starts_with(';') {
return false;
}
let content = line.trim_start_matches(';').trim();
content.starts_with("===")
|| content.starts_with("---")
|| content.starts_with("###")
|| content.starts_with("***")
|| (content.len() > 3 && content.chars().take(3).all(|c| c == '='))
}
#[cfg(test)]
mod tests {
use super::*;
use rustledger_parser::parse;
#[test]
fn test_folding_transaction() {
let source = r#"2024-01-15 * "Coffee Shop" "Morning coffee"
Assets:Bank -5.00 USD
Expenses:Food
"#;
let result = parse(source);
let params = FoldingRangeParams {
text_document: lsp_types::TextDocumentIdentifier {
uri: "file:///test.beancount".parse().unwrap(),
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let ranges = handle_folding_ranges(¶ms, source, &result);
assert!(ranges.is_some());
let ranges = ranges.unwrap();
assert!(!ranges.is_empty());
let txn_fold = ranges.iter().find(|r| r.start_line == 0);
assert!(txn_fold.is_some());
}
#[test]
fn test_is_section_header() {
assert!(is_section_header("; === Expenses ==="));
assert!(is_section_header("; --- Income ---"));
assert!(is_section_header("; ### Assets"));
assert!(!is_section_header("; Just a comment"));
assert!(!is_section_header("2024-01-01 open Assets:Bank"));
}
}