use rustledger_core::Directive;
use rustledger_parser::ParseResult;
use crate::types::{EditorDocumentSymbol, EditorRange, SymbolKind};
use super::line_index::{EditorCache, LineIndex};
pub fn get_document_symbols_cached(
parse_result: &ParseResult,
cache: &EditorCache,
) -> Vec<EditorDocumentSymbol> {
parse_result
.directives
.iter()
.filter_map(|spanned| {
directive_to_symbol_cached(
&spanned.value,
spanned.span.start,
spanned.span.end,
&cache.line_index,
)
})
.collect()
}
#[cfg(test)]
pub fn get_document_symbols(source: &str, parse_result: &ParseResult) -> Vec<EditorDocumentSymbol> {
let line_index = LineIndex::new(source);
parse_result
.directives
.iter()
.filter_map(|spanned| {
directive_to_symbol_cached(
&spanned.value,
spanned.span.start,
spanned.span.end,
&line_index,
)
})
.collect()
}
fn directive_to_symbol_cached(
directive: &Directive,
start_offset: usize,
end_offset: usize,
line_index: &LineIndex,
) -> Option<EditorDocumentSymbol> {
let (start_line, start_col) = line_index.offset_to_position(start_offset);
let (end_line, end_col) = line_index.offset_to_position(end_offset);
let range = EditorRange {
start_line,
start_character: start_col,
end_line,
end_character: end_col,
};
match directive {
Directive::Transaction(txn) => {
let date = txn.date;
let name = if let Some(ref payee) = txn.payee {
format!("{date} {payee}")
} else if !txn.narration.is_empty() {
let narration = &txn.narration;
format!("{date} {narration}")
} else {
format!("{date} Transaction")
};
let detail = if txn.narration.is_empty() {
None
} else {
Some(txn.narration.to_string())
};
let children: Vec<EditorDocumentSymbol> = txn
.postings
.iter()
.enumerate()
.map(|(i, posting)| {
let posting_name = posting.account.to_string();
let posting_detail = posting.units.as_ref().map(|u| {
if let (Some(num), Some(curr)) = (u.number(), u.currency()) {
format!("{num} {curr}")
} else if let Some(num) = u.number() {
num.to_string()
} else {
String::new()
}
});
let posting_line = start_line + 1 + i as u32;
let posting_range = EditorRange {
start_line: posting_line,
start_character: 2,
end_line: posting_line,
end_character: 50,
};
EditorDocumentSymbol {
name: posting_name,
detail: posting_detail,
kind: SymbolKind::Posting,
range: posting_range,
children: None,
deprecated: None,
}
})
.collect();
Some(EditorDocumentSymbol {
name,
detail,
kind: SymbolKind::Transaction,
range,
children: if children.is_empty() {
None
} else {
Some(children)
},
deprecated: None,
})
}
Directive::Open(open) => {
let account = &open.account;
Some(EditorDocumentSymbol {
name: format!("open {account}"),
detail: if open.currencies.is_empty() {
None
} else {
Some(
open.currencies
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>()
.join(", "),
)
},
kind: SymbolKind::Account,
range,
children: None,
deprecated: None,
})
}
Directive::Close(close) => {
let account = &close.account;
Some(EditorDocumentSymbol {
name: format!("close {account}"),
detail: None,
kind: SymbolKind::Account,
range,
children: None,
deprecated: Some(true),
})
}
Directive::Balance(bal) => {
let account = &bal.account;
let number = &bal.amount.number;
let currency = &bal.amount.currency;
Some(EditorDocumentSymbol {
name: format!("balance {account}"),
detail: Some(format!("{number} {currency}")),
kind: SymbolKind::Balance,
range,
children: None,
deprecated: None,
})
}
Directive::Pad(pad) => {
let account = &pad.account;
let source_account = &pad.source_account;
Some(EditorDocumentSymbol {
name: format!("pad {account}"),
detail: Some(format!("from {source_account}")),
kind: SymbolKind::Pad,
range,
children: None,
deprecated: None,
})
}
Directive::Commodity(comm) => {
let currency = &comm.currency;
Some(EditorDocumentSymbol {
name: format!("commodity {currency}"),
detail: None,
kind: SymbolKind::Commodity,
range,
children: None,
deprecated: None,
})
}
Directive::Event(event) => {
let event_type = &event.event_type;
Some(EditorDocumentSymbol {
name: format!("event \"{event_type}\""),
detail: Some(event.value.clone()),
kind: SymbolKind::Event,
range,
children: None,
deprecated: None,
})
}
Directive::Note(note) => {
let account = ¬e.account;
Some(EditorDocumentSymbol {
name: format!("note {account}"),
detail: Some(note.comment.clone()),
kind: SymbolKind::Note,
range,
children: None,
deprecated: None,
})
}
Directive::Document(doc) => {
let account = &doc.account;
Some(EditorDocumentSymbol {
name: format!("document {account}"),
detail: Some(doc.path.clone()),
kind: SymbolKind::Document,
range,
children: None,
deprecated: None,
})
}
Directive::Price(price) => {
let currency = &price.currency;
let number = &price.amount.number;
let amount_currency = &price.amount.currency;
Some(EditorDocumentSymbol {
name: format!("price {currency}"),
detail: Some(format!("{number} {amount_currency}")),
kind: SymbolKind::Price,
range,
children: None,
deprecated: None,
})
}
Directive::Query(query) => {
let name = &query.name;
Some(EditorDocumentSymbol {
name: format!("query \"{name}\""),
detail: None,
kind: SymbolKind::Query,
range,
children: None,
deprecated: None,
})
}
Directive::Custom(custom) => {
let custom_type = &custom.custom_type;
Some(EditorDocumentSymbol {
name: format!("custom \"{custom_type}\""),
detail: None,
kind: SymbolKind::Custom,
range,
children: None,
deprecated: None,
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rustledger_parser::parse;
#[test]
fn test_get_document_symbols() {
let source = r#"2024-01-01 open Assets:Bank USD
2024-01-15 * "Coffee Shop" "Morning coffee"
Assets:Bank -5.00 USD
Expenses:Food
"#;
let result = parse(source);
let symbols = get_document_symbols(source, &result);
assert_eq!(symbols.len(), 2);
assert!(symbols[0].name.contains("open"));
assert_eq!(symbols[0].kind, SymbolKind::Account);
assert!(symbols[1].name.contains("Coffee"));
assert_eq!(symbols[1].kind, SymbolKind::Transaction);
assert!(symbols[1].children.is_some());
assert_eq!(symbols[1].children.as_ref().unwrap().len(), 2);
}
#[test]
fn test_document_symbols_all_directive_types() {
let source = r#"2024-01-01 open Assets:Bank USD
2024-01-01 close Assets:OldBank
2024-01-01 commodity BTC
2024-01-01 balance Assets:Bank 100.00 USD
2024-01-01 pad Assets:Bank Equity:Opening
2024-01-01 event "location" "New York"
2024-01-01 note Assets:Bank "Test note"
2024-01-01 document Assets:Bank "/path/to/doc.pdf"
2024-01-01 price BTC 50000.00 USD
2024-01-01 query "test_query" "SELECT *"
2024-01-01 custom "budget" Expenses 500.00 USD
2024-01-15 * "Coffee"
Assets:Bank -5.00 USD
Expenses:Food
"#;
let result = parse(source);
let symbols = get_document_symbols(source, &result);
assert!(symbols.len() >= 12);
let kinds: Vec<_> = symbols.iter().map(|s| &s.kind).collect();
assert!(kinds.contains(&&SymbolKind::Account)); assert!(kinds.contains(&&SymbolKind::Commodity));
assert!(kinds.contains(&&SymbolKind::Balance));
assert!(kinds.contains(&&SymbolKind::Pad));
assert!(kinds.contains(&&SymbolKind::Event));
assert!(kinds.contains(&&SymbolKind::Note));
assert!(kinds.contains(&&SymbolKind::Document));
assert!(kinds.contains(&&SymbolKind::Price));
assert!(kinds.contains(&&SymbolKind::Query));
assert!(kinds.contains(&&SymbolKind::Custom));
assert!(kinds.contains(&&SymbolKind::Transaction));
}
}