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");