Skip to main content

rustledger_lsp/
lib.rs

1//! Language Server Protocol implementation for Beancount.
2//!
3//! This crate provides an LSP server for Beancount files, enabling IDE features like:
4//! - Real-time syntax error diagnostics
5//! - Autocompletion for accounts, currencies, payees
6//! - Go-to-definition for accounts
7//! - Hover information
8//! - Document symbols (outline view)
9//!
10//! # Architecture
11//!
12//! The server follows rust-analyzer's architecture:
13//! - **Main loop**: Handles LSP messages, applies changes, dispatches requests
14//! - **Query database**: Salsa-inspired incremental computation
15//! - **Handlers**: Process LSP requests against immutable snapshots
16//!
17//! # Example
18//!
19//! ```ignore
20//! fn main() -> std::process::ExitCode {
21//!     rustledger_lsp::start_stdio().map(|()| std::process::ExitCode::SUCCESS)
22//!         .unwrap_or(std::process::ExitCode::FAILURE)
23//! }
24//! ```
25
26#![warn(missing_docs)]
27#![warn(clippy::all)]
28
29use lsp_types::Uri;
30use std::path::PathBuf;
31
32pub mod handlers;
33pub mod ledger_state;
34pub mod main_loop;
35
36/// Convert an LSP URI to a file path.
37///
38/// Handles both Unix and Windows paths, as well as percent-encoded characters.
39#[cfg(not(windows))]
40pub fn uri_to_path(uri: &Uri) -> Option<PathBuf> {
41    let path_str = uri.as_str().strip_prefix("file://")?;
42    // Decode percent-encoded characters (e.g., %20 -> space)
43    let decoded = percent_decode(path_str);
44    Some(PathBuf::from(decoded))
45}
46
47/// Convert an LSP URI to a file path (Windows version).
48#[cfg(windows)]
49pub fn uri_to_path(uri: &Uri) -> Option<PathBuf> {
50    let path_str = uri.as_str().strip_prefix("file://")?;
51    // Handle Windows paths like file:///C:/...
52    let path_str = path_str.strip_prefix('/').unwrap_or(path_str);
53    // Decode percent-encoded characters (e.g., %20 -> space)
54    let decoded = percent_decode(path_str);
55    Some(PathBuf::from(decoded))
56}
57
58/// Decode percent-encoded characters in a string.
59fn percent_decode(s: &str) -> String {
60    let mut result = String::with_capacity(s.len());
61    let mut chars = s.chars().peekable();
62
63    while let Some(c) = chars.next() {
64        if c == '%' {
65            // Try to read two hex digits
66            let hex: String = chars.by_ref().take(2).collect();
67            if hex.len() == 2
68                && let Ok(byte) = u8::from_str_radix(&hex, 16)
69            {
70                result.push(byte as char);
71                continue;
72            }
73            // Failed to decode, keep original
74            result.push('%');
75            result.push_str(&hex);
76        } else {
77            result.push(c);
78        }
79    }
80    result
81}
82
83mod server;
84mod snapshot;
85mod vfs;
86
87pub use ledger_state::{LedgerState, LspConfig, SharedLedgerState, new_shared_ledger_state};
88pub use main_loop::{run_main_loop, run_main_loop_with_exit_action};
89pub use server::{Server, start_stdio};
90pub use snapshot::Snapshot;
91pub use vfs::Vfs;
92
93/// LSP server version.
94pub const VERSION: &str = env!("CARGO_PKG_VERSION");