use crate::handlers::execute_command::COMMANDS;
use crate::handlers::on_type_formatting::{FIRST_TRIGGER_CHARACTER, MORE_TRIGGER_CHARACTERS};
use crate::handlers::semantic_tokens::get_capabilities as get_semantic_tokens_capabilities;
use crate::handlers::signature_help::TRIGGER_CHARACTERS as SIGNATURE_TRIGGER_CHARACTERS;
use crate::ledger_state::{LspConfig, discover_journal_file};
use crate::main_loop::run_main_loop;
use crate::uri_to_path;
use lsp_server::Connection;
use lsp_types::InitializeParams;
pub struct Server {
connection: Connection,
init_params: InitializeParams,
config: LspConfig,
}
impl Server {
pub fn new(connection: Connection, init_params: InitializeParams) -> Self {
let config = LspConfig::from_init_options(init_params.initialization_options.as_ref());
if let Some(ref journal) = config.journal_file {
tracing::info!("Journal file configured: {}", journal.display());
}
Self {
connection,
init_params,
config,
}
}
pub fn run(self) {
tracing::info!("Starting Beancount Language Server v{}", crate::VERSION);
let journal_file = self.resolve_journal_path();
if let Some(ref path) = journal_file {
tracing::info!("Using journal file: {}", path.display());
}
if let Some(folders) = &self.init_params.workspace_folders
&& let Some(folder) = folders.first()
{
tracing::info!("Workspace root: {}", folder.uri.as_str());
}
let (sender, receiver) = (self.connection.sender, self.connection.receiver);
run_main_loop(receiver, sender, journal_file);
tracing::info!("Server shutdown complete");
}
fn resolve_journal_path(&self) -> Option<std::path::PathBuf> {
let workspace_root = self.get_workspace_root();
if let Some(journal) = &self.config.journal_file {
return self.resolve_explicit_journal(journal, workspace_root.as_deref());
}
if let Some(root) = workspace_root
&& let Some(discovered) = discover_journal_file(&root)
{
return Some(discovered);
}
if let Ok(cwd) = std::env::current_dir()
&& let Some(discovered) = discover_journal_file(&cwd)
{
return Some(discovered);
}
tracing::debug!("No journal file configured or discovered");
None
}
fn get_workspace_root(&self) -> Option<std::path::PathBuf> {
self.init_params
.workspace_folders
.as_ref()
.and_then(|folders| folders.first())
.and_then(|folder| uri_to_path(&folder.uri))
}
fn resolve_explicit_journal(
&self,
journal: &std::path::Path,
workspace_root: Option<&std::path::Path>,
) -> Option<std::path::PathBuf> {
if journal.is_absolute() {
return Some(journal.to_path_buf());
}
if let Some(root) = workspace_root {
let resolved = root.join(journal);
if resolved.exists() {
return Some(resolved);
}
}
let resolved = std::env::current_dir()
.ok()
.map(|cwd| cwd.join(journal))
.filter(|p| p.exists());
if resolved.is_none() {
tracing::warn!(
"Journal file '{}' not found relative to workspace or current directory",
journal.display()
);
}
resolved
}
}
pub fn start_stdio() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
tracing::info!("Starting LSP server on stdio");
let (connection, io_threads) = Connection::stdio();
let (id, params) = connection.initialize_start()?;
let init_params: InitializeParams = serde_json::from_value(params)?;
let capabilities = lsp_types::ServerCapabilities {
text_document_sync: Some(lsp_types::TextDocumentSyncCapability::Kind(
lsp_types::TextDocumentSyncKind::FULL,
)),
completion_provider: Some(lsp_types::CompletionOptions {
trigger_characters: Some(vec![
":".to_string(), " ".to_string(), "\"".to_string(), ]),
resolve_provider: Some(true), ..Default::default()
}),
definition_provider: Some(lsp_types::OneOf::Left(true)),
references_provider: Some(lsp_types::OneOf::Left(true)),
hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)),
document_symbol_provider: Some(lsp_types::OneOf::Left(true)),
semantic_tokens_provider: Some(get_semantic_tokens_capabilities()),
code_action_provider: Some(lsp_types::CodeActionProviderCapability::Options(
lsp_types::CodeActionOptions {
code_action_kinds: Some(vec![
lsp_types::CodeActionKind::QUICKFIX,
lsp_types::CodeActionKind::REFACTOR,
]),
resolve_provider: Some(true), work_done_progress_options: Default::default(),
},
)),
workspace_symbol_provider: Some(lsp_types::OneOf::Left(true)),
rename_provider: Some(lsp_types::OneOf::Right(lsp_types::RenameOptions {
prepare_provider: Some(true),
work_done_progress_options: Default::default(),
})),
document_formatting_provider: Some(lsp_types::OneOf::Left(true)),
document_range_formatting_provider: Some(lsp_types::OneOf::Left(true)),
document_link_provider: Some(lsp_types::DocumentLinkOptions {
resolve_provider: Some(true), work_done_progress_options: Default::default(),
}),
inlay_hint_provider: Some(lsp_types::OneOf::Right(
lsp_types::InlayHintServerCapabilities::Options(lsp_types::InlayHintOptions {
resolve_provider: Some(true), work_done_progress_options: Default::default(),
}),
)),
selection_range_provider: Some(lsp_types::SelectionRangeProviderCapability::Simple(true)),
folding_range_provider: Some(lsp_types::FoldingRangeProviderCapability::Simple(true)),
document_highlight_provider: Some(lsp_types::OneOf::Left(true)),
linked_editing_range_provider: Some(
lsp_types::LinkedEditingRangeServerCapabilities::Simple(true),
),
document_on_type_formatting_provider: Some(lsp_types::DocumentOnTypeFormattingOptions {
first_trigger_character: FIRST_TRIGGER_CHARACTER.to_string(),
more_trigger_character: Some(
MORE_TRIGGER_CHARACTERS
.iter()
.map(|s| s.to_string())
.collect(),
),
}),
code_lens_provider: Some(lsp_types::CodeLensOptions {
resolve_provider: Some(true), }),
color_provider: Some(lsp_types::ColorProviderCapability::Simple(true)),
declaration_provider: Some(lsp_types::DeclarationCapability::Simple(true)),
call_hierarchy_provider: Some(lsp_types::CallHierarchyServerCapability::Simple(true)),
signature_help_provider: Some(lsp_types::SignatureHelpOptions {
trigger_characters: Some(
SIGNATURE_TRIGGER_CHARACTERS
.iter()
.map(|s| s.to_string())
.collect(),
),
retrigger_characters: None,
work_done_progress_options: Default::default(),
}),
execute_command_provider: Some(lsp_types::ExecuteCommandOptions {
commands: COMMANDS.iter().map(|s| s.to_string()).collect(),
work_done_progress_options: Default::default(),
}),
experimental: Some(serde_json::json!({
"typeHierarchyProvider": true
})),
workspace: Some(lsp_types::WorkspaceServerCapabilities {
workspace_folders: Some(lsp_types::WorkspaceFoldersServerCapabilities {
supported: Some(true),
change_notifications: Some(lsp_types::OneOf::Left(true)),
}),
file_operations: None, }),
..Default::default()
};
let server_info = lsp_types::ServerInfo {
name: "rledger-lsp".to_string(),
version: Some(crate::VERSION.to_string()),
};
let init_result = lsp_types::InitializeResult {
capabilities,
server_info: Some(server_info),
};
connection.initialize_finish(id, serde_json::to_value(init_result)?)?;
tracing::info!("LSP initialized successfully");
let server = Server::new(connection, init_params);
server.run();
io_threads.join()?;
Ok(())
}