use parking_lot::RwLock;
use rustledger_core::{Directive, PriceAnnotation};
use rustledger_loader::{Ledger, LoadOptions, load};
use rustledger_parser::Spanned;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
const COMMON_ROOT_NAMES: &[&str] = &[
"main.bean",
"main.beancount",
"ledger.bean",
"ledger.beancount",
"journal.bean",
"journal.beancount",
"index.bean",
"index.beancount",
];
pub fn discover_journal_file(workspace_root: &Path) -> Option<PathBuf> {
for name in COMMON_ROOT_NAMES {
let candidate = workspace_root.join(name);
if candidate.exists() && candidate.is_file() {
tracing::info!("Auto-discovered journal file: {}", candidate.display());
return Some(candidate);
}
}
tracing::debug!(
"No journal file found in workspace root: {}",
workspace_root.display()
);
None
}
fn extract_price_currency(price: &PriceAnnotation) -> Option<String> {
price
.amount
.as_ref()
.and_then(|inc| inc.currency())
.map(str::to_string)
}
#[derive(Debug, Clone, Default)]
pub struct LspConfig {
pub journal_file: Option<PathBuf>,
}
impl LspConfig {
pub fn from_init_options(options: Option<&serde_json::Value>) -> Self {
let mut config = Self::default();
if let Some(opts) = options {
if let Some(path) = opts
.get("journalFile")
.or_else(|| opts.get("journal_file"))
.and_then(|v| v.as_str())
{
config.journal_file = Some(PathBuf::from(path));
}
}
config
}
}
pub struct LedgerState {
ledger: Option<Ledger>,
included_files: HashSet<PathBuf>,
accounts: Vec<String>,
currencies: Vec<String>,
payees: Vec<String>,
tags: Vec<String>,
links: Vec<String>,
account_locations: HashMap<String, (PathBuf, u32)>,
}
impl Default for LedgerState {
fn default() -> Self {
Self::new()
}
}
impl LedgerState {
pub fn new() -> Self {
Self {
ledger: None,
included_files: HashSet::new(),
accounts: Vec::new(),
currencies: Vec::new(),
payees: Vec::new(),
tags: Vec::new(),
links: Vec::new(),
account_locations: HashMap::new(),
}
}
pub fn load(&mut self, journal_path: &Path) -> Result<HashSet<PathBuf>, String> {
tracing::info!("Loading journal file: {}", journal_path.display());
let options = LoadOptions {
validate: false,
..LoadOptions::default()
};
match load(journal_path, &options) {
Ok(ledger) => {
self.included_files.clear();
for file in ledger.source_map.files() {
let canonical = file
.path
.canonicalize()
.unwrap_or_else(|_| file.path.clone());
self.included_files.insert(canonical);
}
self.extract_completion_data(&ledger.directives);
self.extract_account_locations(&ledger);
let files = self.included_files.clone();
self.ledger = Some(ledger);
tracing::info!(
"Loaded {} files, {} accounts, {} currencies",
self.included_files.len(),
self.accounts.len(),
self.currencies.len()
);
Ok(files)
}
Err(e) => {
tracing::error!("Failed to load journal: {e}");
Err(e.to_string())
}
}
}
pub fn contains_file(&self, path: &Path) -> bool {
match path.canonicalize() {
Ok(canonical) => self.included_files.contains(&canonical),
Err(_) => self.included_files.contains(path),
}
}
pub fn accounts(&self) -> &[String] {
&self.accounts
}
pub fn currencies(&self) -> &[String] {
&self.currencies
}
pub fn payees(&self) -> &[String] {
&self.payees
}
pub fn tags(&self) -> &[String] {
&self.tags
}
pub fn links(&self) -> &[String] {
&self.links
}
pub fn directives(&self) -> Option<&[Spanned<Directive>]> {
self.ledger.as_ref().map(|l| l.directives.as_slice())
}
pub fn ledger(&self) -> Option<&Ledger> {
self.ledger.as_ref()
}
pub fn included_files(&self) -> &HashSet<PathBuf> {
&self.included_files
}
pub fn find_account_definition(&self, account: &str) -> Option<(PathBuf, u32)> {
self.account_locations.get(account).cloned()
}
fn extract_completion_data(&mut self, directives: &[Spanned<Directive>]) {
self.accounts.clear();
self.currencies.clear();
self.payees.clear();
self.tags.clear();
self.links.clear();
let mut accounts_set: HashSet<String> = HashSet::new();
let mut currencies_set: HashSet<String> = HashSet::new();
let mut payees_set: HashSet<String> = HashSet::new();
for spanned in directives {
match &spanned.value {
Directive::Open(open) => {
accounts_set.insert(open.account.to_string());
for currency in &open.currencies {
currencies_set.insert(currency.to_string());
}
}
Directive::Close(close) => {
accounts_set.insert(close.account.to_string());
}
Directive::Balance(balance) => {
accounts_set.insert(balance.account.to_string());
currencies_set.insert(balance.amount.currency.to_string());
}
Directive::Pad(pad) => {
accounts_set.insert(pad.account.to_string());
accounts_set.insert(pad.source_account.to_string());
}
Directive::Transaction(txn) => {
if let Some(payee) = &txn.payee {
payees_set.insert(payee.to_string());
}
for posting in &txn.postings {
accounts_set.insert(posting.account.to_string());
if let Some(units) = &posting.units
&& let Some(currency) = units.currency()
{
currencies_set.insert(currency.to_string());
}
if let Some(cost) = &posting.cost
&& let Some(currency) = &cost.currency
{
currencies_set.insert(currency.to_string());
}
if let Some(price) = &posting.price
&& let Some(currency) = extract_price_currency(price)
{
currencies_set.insert(currency);
}
}
}
Directive::Commodity(commodity) => {
currencies_set.insert(commodity.currency.to_string());
}
Directive::Document(doc) => {
accounts_set.insert(doc.account.to_string());
}
Directive::Note(note) => {
accounts_set.insert(note.account.to_string());
}
_ => {}
}
}
self.accounts = accounts_set.into_iter().collect();
self.accounts.sort();
self.currencies = currencies_set.into_iter().collect();
self.currencies.sort();
self.payees = payees_set.into_iter().collect();
self.payees.sort();
self.tags = rustledger_core::extract_tags_iter(directives.iter().map(|s| &s.value));
self.links = rustledger_core::extract_links_iter(directives.iter().map(|s| &s.value));
}
fn extract_account_locations(&mut self, ledger: &Ledger) {
self.account_locations.clear();
for spanned in &ledger.directives {
if let Directive::Open(open) = &spanned.value {
if let Some(file) = ledger.source_map.get(spanned.file_id as usize) {
let (line, _col) = file.line_col(spanned.span.start);
self.account_locations
.insert(open.account.to_string(), (file.path.clone(), line as u32));
}
}
}
}
}
pub type SharedLedgerState = Arc<RwLock<LedgerState>>;
pub fn new_shared_ledger_state() -> SharedLedgerState {
Arc::new(RwLock::new(LedgerState::new()))
}