use rustledger_core::Directive;
use rustledger_parser::ParseResult;
use crate::types::{EditorRange, EditorReference, EditorReferencesResult, ReferenceKind};
use super::helpers::{
find_nth_word_in_line, find_quoted_string_in_line, find_word_in_line, get_line,
get_word_at_position, is_account_type, is_currency_like,
};
use super::line_index::{EditorCache, LineIndex};
pub fn get_references_cached(
source: &str,
line: u32,
character: u32,
parse_result: &ParseResult,
cache: &EditorCache,
) -> Option<EditorReferencesResult> {
let word = get_word_at_position(source, line, character)?;
if word.contains(':') || is_account_type(&word) {
return Some(find_account_references(
&word,
source,
parse_result,
&cache.line_index,
));
}
if is_currency_like(&word) && cache.currencies.contains(&word) {
return Some(find_currency_references(
&word,
source,
parse_result,
&cache.line_index,
));
}
if cache.payees.contains(&word) {
return Some(find_payee_references(
&word,
source,
parse_result,
&cache.line_index,
));
}
None
}
fn find_account_references(
account: &str,
source: &str,
parse_result: &ParseResult,
line_index: &LineIndex,
) -> EditorReferencesResult {
let mut references = Vec::new();
for spanned_directive in &parse_result.directives {
let (start_line, _) = line_index.offset_to_position(spanned_directive.span.start);
let directive_line = get_line(source, start_line as usize);
match &spanned_directive.value {
Directive::Open(open) => {
let open_account = open.account.to_string();
if open_account == account || account.starts_with(&format!("{open_account}:")) {
if let Some(range) =
find_word_in_line(directive_line, &open_account, start_line)
{
references.push(EditorReference {
range,
kind: ReferenceKind::Account,
is_definition: true,
context: Some("open".to_string()),
});
}
}
}
Directive::Close(close) => {
let close_account = close.account.to_string();
if close_account == account {
if let Some(range) =
find_word_in_line(directive_line, &close_account, start_line)
{
references.push(EditorReference {
range,
kind: ReferenceKind::Account,
is_definition: false,
context: Some("close".to_string()),
});
}
}
}
Directive::Balance(bal) => {
let bal_account = bal.account.to_string();
if bal_account == account {
if let Some(range) = find_word_in_line(directive_line, &bal_account, start_line)
{
references.push(EditorReference {
range,
kind: ReferenceKind::Account,
is_definition: false,
context: Some("balance".to_string()),
});
}
}
}
Directive::Pad(pad) => {
let pad_account = pad.account.to_string();
let source_account = pad.source_account.to_string();
if pad_account == account {
if let Some(range) = find_word_in_line(directive_line, &pad_account, start_line)
{
references.push(EditorReference {
range,
kind: ReferenceKind::Account,
is_definition: false,
context: Some("pad".to_string()),
});
}
}
if source_account == account {
if let Some(range) =
find_word_in_line(directive_line, &source_account, start_line)
{
references.push(EditorReference {
range,
kind: ReferenceKind::Account,
is_definition: false,
context: Some("pad source".to_string()),
});
}
}
}
Directive::Note(note) => {
let note_account = note.account.to_string();
if note_account == account {
if let Some(range) =
find_word_in_line(directive_line, ¬e_account, start_line)
{
references.push(EditorReference {
range,
kind: ReferenceKind::Account,
is_definition: false,
context: Some("note".to_string()),
});
}
}
}
Directive::Document(doc) => {
let doc_account = doc.account.to_string();
if doc_account == account {
if let Some(range) = find_word_in_line(directive_line, &doc_account, start_line)
{
references.push(EditorReference {
range,
kind: ReferenceKind::Account,
is_definition: false,
context: Some("document".to_string()),
});
}
}
}
Directive::Transaction(txn) => {
for (i, posting) in txn.postings.iter().enumerate() {
let posting_account = posting.account.to_string();
if posting_account == account {
let posting_line = start_line + 1 + i as u32;
if let Some(line_text) = source.lines().nth(posting_line as usize) {
if let Some(range) =
find_word_in_line(line_text, &posting_account, posting_line)
{
references.push(EditorReference {
range,
kind: ReferenceKind::Account,
is_definition: false,
context: Some("posting".to_string()),
});
}
}
}
}
}
_ => {}
}
}
EditorReferencesResult {
symbol: account.to_string(),
kind: ReferenceKind::Account,
references,
}
}
fn find_currency_references(
currency: &str,
source: &str,
parse_result: &ParseResult,
line_index: &LineIndex,
) -> EditorReferencesResult {
let mut references = Vec::new();
for spanned_directive in &parse_result.directives {
let (start_line, _) = line_index.offset_to_position(spanned_directive.span.start);
let directive_line = get_line(source, start_line as usize);
match &spanned_directive.value {
Directive::Commodity(comm) => {
if comm.currency.as_ref() == currency {
if let Some(range) = find_word_in_line(directive_line, currency, start_line) {
references.push(EditorReference {
range,
kind: ReferenceKind::Currency,
is_definition: true,
context: Some("commodity".to_string()),
});
}
}
}
Directive::Open(open) => {
for curr in &open.currencies {
if curr.as_ref() == currency {
if let Some(range) = find_word_in_line(directive_line, currency, start_line)
{
references.push(EditorReference {
range,
kind: ReferenceKind::Currency,
is_definition: false,
context: Some("open".to_string()),
});
}
}
}
}
Directive::Balance(bal) => {
if bal.amount.currency.as_ref() == currency {
if let Some(range) = find_word_in_line(directive_line, currency, start_line) {
references.push(EditorReference {
range,
kind: ReferenceKind::Currency,
is_definition: false,
context: Some("balance".to_string()),
});
}
}
}
Directive::Price(price) => {
if price.currency.as_ref() == currency {
if let Some(range) = find_word_in_line(directive_line, currency, start_line) {
references.push(EditorReference {
range,
kind: ReferenceKind::Currency,
is_definition: false,
context: Some("price".to_string()),
});
}
}
if price.amount.currency.as_ref() == currency {
if let Some(range) =
find_nth_word_in_line(directive_line, currency, start_line, 1)
{
references.push(EditorReference {
range,
kind: ReferenceKind::Currency,
is_definition: false,
context: Some("price amount".to_string()),
});
}
}
}
Directive::Transaction(txn) => {
for (i, posting) in txn.postings.iter().enumerate() {
if let Some(ref units) = posting.units {
if let Some(curr) = units.currency() {
if curr == currency {
let posting_line = start_line + 1 + i as u32;
if let Some(line_text) = source.lines().nth(posting_line as usize) {
if let Some(range) =
find_word_in_line(line_text, currency, posting_line)
{
references.push(EditorReference {
range,
kind: ReferenceKind::Currency,
is_definition: false,
context: Some("posting".to_string()),
});
}
}
}
}
}
}
}
_ => {}
}
}
EditorReferencesResult {
symbol: currency.to_string(),
kind: ReferenceKind::Currency,
references,
}
}
fn find_payee_references(
payee: &str,
source: &str,
parse_result: &ParseResult,
line_index: &LineIndex,
) -> EditorReferencesResult {
let mut references = Vec::new();
for spanned_directive in &parse_result.directives {
if let Directive::Transaction(txn) = &spanned_directive.value {
if let Some(ref txn_payee) = txn.payee {
if txn_payee == payee {
let (start_line, _) =
line_index.offset_to_position(spanned_directive.span.start);
let line_text = get_line(source, start_line as usize);
let range = if let Some(range) =
find_quoted_string_in_line(line_text, payee, start_line)
{
range
} else {
EditorRange {
start_line,
start_character: 0,
end_line: start_line,
end_character: payee.len() as u32,
}
};
references.push(EditorReference {
range,
kind: ReferenceKind::Payee,
is_definition: references.is_empty(), context: Some("transaction".to_string()),
});
}
}
}
}
EditorReferencesResult {
symbol: payee.to_string(),
kind: ReferenceKind::Payee,
references,
}
}
#[cfg(test)]
mod tests {
use super::*;
use rustledger_parser::parse;
#[test]
fn test_get_references_account() {
let source = r#"2024-01-01 open Assets:Bank USD
2024-01-15 balance Assets:Bank 100.00 USD
2024-01-20 * "Transfer"
Assets:Bank 50.00 USD
Income:Salary
"#;
let result = parse(source);
let cache = EditorCache::new(source, &result);
let refs = get_references_cached(source, 0, 20, &result, &cache);
assert!(refs.is_some());
let refs = refs.unwrap();
assert_eq!(refs.symbol, "Assets:Bank");
assert_eq!(refs.kind, ReferenceKind::Account);
assert!(refs.references.len() >= 3); }
#[test]
fn test_get_references_currency() {
let source = r#"2024-01-01 commodity USD
2024-01-01 open Assets:Bank USD
2024-01-15 balance Assets:Bank 100.00 USD
"#;
let result = parse(source);
let cache = EditorCache::new(source, &result);
let refs = get_references_cached(source, 0, 21, &result, &cache);
assert!(refs.is_some());
let refs = refs.unwrap();
assert_eq!(refs.symbol, "USD");
assert_eq!(refs.kind, ReferenceKind::Currency);
assert!(refs.references.len() >= 3); }
}