use crate::handlers::call_hierarchy::{
handle_incoming_calls, handle_outgoing_calls, handle_prepare_call_hierarchy,
};
use crate::handlers::code_actions::{handle_code_action_resolve, handle_code_actions};
use crate::handlers::code_lens::{handle_code_lens, handle_code_lens_resolve};
use crate::handlers::completion::handle_completion;
use crate::handlers::completion_resolve::handle_completion_resolve;
use crate::handlers::declaration::handle_goto_declaration;
use crate::handlers::definition::handle_goto_definition;
use crate::handlers::diagnostics::all_diagnostics;
use crate::handlers::document_color::{handle_color_presentation, handle_document_color};
use crate::handlers::document_highlight::handle_document_highlight;
use crate::handlers::document_links::{handle_document_link_resolve, handle_document_links};
use crate::handlers::execute_command::handle_execute_command;
use crate::handlers::folding::handle_folding_ranges;
use crate::handlers::formatting::handle_formatting;
use crate::handlers::hover::handle_hover;
use crate::handlers::inlay_hints::{handle_inlay_hint_resolve, handle_inlay_hints};
use crate::handlers::linked_editing::handle_linked_editing_range;
use crate::handlers::on_type_formatting::handle_on_type_formatting;
use crate::handlers::range_formatting::handle_range_formatting;
use crate::handlers::references::handle_references;
use crate::handlers::rename::{handle_prepare_rename, handle_rename};
use crate::handlers::selection_range::handle_selection_range;
use crate::handlers::semantic_tokens::{
handle_semantic_tokens, handle_semantic_tokens_delta, handle_semantic_tokens_range,
};
use crate::handlers::signature_help::handle_signature_help;
use crate::handlers::symbols::handle_document_symbols;
use crate::handlers::type_hierarchy::{
handle_prepare_type_hierarchy, handle_subtypes, handle_supertypes,
};
use crate::handlers::workspace_symbols::handle_workspace_symbols;
use crate::ledger_state::{SharedLedgerState, new_shared_ledger_state};
use crate::uri_to_path;
use crate::vfs::Vfs;
use crossbeam_channel::{Receiver, Sender};
use lsp_types::notification::{
DidChangeTextDocument, DidChangeWatchedFiles, DidCloseTextDocument, DidOpenTextDocument,
Notification, PublishDiagnostics,
};
use lsp_types::request::{
CallHierarchyIncomingCalls, CallHierarchyOutgoingCalls, CallHierarchyPrepare,
CodeActionRequest, CodeActionResolveRequest, CodeLensRequest, CodeLensResolve,
ColorPresentationRequest, Completion, DocumentColor, DocumentHighlightRequest,
DocumentLinkRequest, DocumentLinkResolve, DocumentSymbolRequest, ExecuteCommand,
FoldingRangeRequest, Formatting, GotoDeclaration, GotoDefinition, HoverRequest, Initialize,
InlayHintRequest, InlayHintResolveRequest, LinkedEditingRange, OnTypeFormatting,
PrepareRenameRequest, RangeFormatting, References, Rename, Request, ResolveCompletionItem,
SelectionRangeRequest, SemanticTokensFullDeltaRequest, SemanticTokensFullRequest,
SemanticTokensRangeRequest, Shutdown, SignatureHelpRequest, TypeHierarchyPrepare,
TypeHierarchySubtypes, TypeHierarchySupertypes, WorkspaceSymbolRequest,
};
use lsp_types::{
CallHierarchyIncomingCallsParams, CallHierarchyOutgoingCallsParams, CallHierarchyPrepareParams,
CodeAction, CodeActionParams, CodeLens, CodeLensParams, ColorPresentationParams,
CompletionItem, CompletionParams, DocumentColorParams, DocumentFormattingParams,
DocumentHighlightParams, DocumentLink, DocumentLinkParams, DocumentOnTypeFormattingParams,
DocumentRangeFormattingParams, DocumentSymbolParams, ExecuteCommandParams, FoldingRangeParams,
GotoDefinitionParams, HoverParams, InitializeParams, InlayHint, InlayHintParams,
LinkedEditingRangeParams, PublishDiagnosticsParams, ReferenceParams, RenameParams,
SelectionRangeParams, SemanticTokensDeltaParams, SemanticTokensParams,
SemanticTokensRangeParams, SignatureHelpParams, TextDocumentPositionParams,
TypeHierarchyPrepareParams, TypeHierarchySubtypesParams, TypeHierarchySupertypesParams, Uri,
WorkspaceSymbolParams,
};
use parking_lot::RwLock;
use rustledger_core::Directive;
use rustledger_parser::{ParseResult, Spanned, parse};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
#[derive(Debug)]
pub enum Event {
Message(Message),
Task(TaskResult),
}
#[derive(Debug)]
pub enum Message {
Request(lsp_server::Request),
Notification(lsp_server::Notification),
Response(lsp_server::Response),
}
#[derive(Debug)]
pub struct TaskResult {
pub request_id: lsp_server::RequestId,
pub result: Result<serde_json::Value, String>,
}
type BackgroundJob = Box<dyn FnOnce() + Send>;
#[derive(Debug)]
enum DispatchError {
MethodNotFound(String),
DuplicateInitialize,
Handler(String),
}
impl DispatchError {
fn error_code(&self) -> lsp_server::ErrorCode {
match self {
Self::MethodNotFound(_) => lsp_server::ErrorCode::MethodNotFound,
Self::DuplicateInitialize => lsp_server::ErrorCode::InvalidRequest,
Self::Handler(_) => lsp_server::ErrorCode::InternalError,
}
}
}
impl std::fmt::Display for DispatchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MethodNotFound(method) => write!(f, "Unhandled request: {method}"),
Self::DuplicateInitialize => write!(
f,
"initialize must be sent exactly once per LSP connection (LSP 3.17 \
§Lifecycle); this connection has already been initialized via \
server.rs::start_stdio."
),
Self::Handler(msg) => f.write_str(msg),
}
}
}
pub struct MainLoopState {
pub vfs: Arc<RwLock<Vfs>>,
pub sender: Sender<lsp_server::Message>,
pub diagnostics: HashMap<Uri, Vec<lsp_types::Diagnostic>>,
pub shutdown_requested: bool,
pub pending_exit_code: Option<i32>,
pub position_encoding: crate::handlers::utils::PositionEncoding,
pub ledger_state: SharedLedgerState,
pub journal_file: Option<PathBuf>,
pub task_sender: Sender<TaskResult>,
pub task_receiver: Receiver<TaskResult>,
pub job_sender: Sender<BackgroundJob>,
revision: Arc<std::sync::atomic::AtomicU64>,
exit_action: Option<Box<dyn FnOnce(i32) + Send>>,
}
fn empty_parse_result() -> Arc<ParseResult> {
Arc::new(parse(""))
}
impl MainLoopState {
pub fn new(sender: Sender<lsp_server::Message>, journal_file: Option<PathBuf>) -> Self {
let ledger_state = new_shared_ledger_state();
if let Some(ref path) = journal_file {
let mut state = ledger_state.write();
if let Err(e) = state.load(path) {
tracing::error!("Failed to load journal file: {e}");
}
}
let (task_sender, task_receiver) = crossbeam_channel::unbounded();
let (job_sender, job_receiver) = crossbeam_channel::unbounded::<BackgroundJob>();
std::thread::Builder::new()
.name("lsp-worker".into())
.spawn(move || {
for job in job_receiver {
job();
}
})
.expect("failed to spawn LSP worker thread");
Self {
vfs: Arc::new(RwLock::new(Vfs::new())),
sender,
diagnostics: HashMap::new(),
shutdown_requested: false,
pending_exit_code: None,
position_encoding: crate::handlers::utils::PositionEncoding::Utf16,
ledger_state,
journal_file,
task_sender,
task_receiver,
job_sender,
revision: Arc::new(std::sync::atomic::AtomicU64::new(0)),
exit_action: Some(Box::new(|_code| {})),
}
}
fn bump_revision(&self) -> u64 {
self.revision
.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
+ 1
}
#[must_use]
pub fn with_exit_action<F>(mut self, action: F) -> Self
where
F: FnOnce(i32) + Send + 'static,
{
self.exit_action = Some(Box::new(action));
self
}
pub fn reload_journal(&mut self) {
if let Some(ref path) = self.journal_file {
let mut state = self.ledger_state.write();
if let Err(e) = state.load(path) {
tracing::error!("Failed to reload journal file: {e}");
}
}
}
fn get_document_data(&self, uri: &Uri) -> (String, Arc<ParseResult>) {
if let Some(path) = uri_to_path(uri)
&& let Some((text, parse_result)) = self.vfs.write().get_document_data(&path)
{
return (text, parse_result);
}
(String::new(), empty_parse_result())
}
pub fn handle_event(&mut self, event: Event) {
match event {
Event::Message(msg) => self.handle_message(msg),
Event::Task(task_result) => {
let response = match task_result.result {
Ok(value) => lsp_server::Response::new_ok(task_result.request_id, value),
Err(msg) => lsp_server::Response::new_err(
task_result.request_id,
lsp_server::ErrorCode::InternalError as i32,
msg,
),
};
self.send(lsp_server::Message::Response(response));
}
}
}
fn dispatch_async(
&self,
request_id: lsp_server::RequestId,
handler: impl FnOnce() -> Result<serde_json::Value, String> + Send + 'static,
) {
self.dispatch_async_inner(request_id, handler, true);
}
fn dispatch_async_unconditional(
&self,
request_id: lsp_server::RequestId,
handler: impl FnOnce() -> Result<serde_json::Value, String> + Send + 'static,
) {
self.dispatch_async_inner(request_id, handler, false);
}
fn dispatch_async_inner(
&self,
request_id: lsp_server::RequestId,
handler: impl FnOnce() -> Result<serde_json::Value, String> + Send + 'static,
check_staleness: bool,
) {
let task_sender = self.task_sender.clone();
let revision_arc = self.revision.clone();
let dispatch_revision = if check_staleness {
Some(revision_arc.load(std::sync::atomic::Ordering::SeqCst))
} else {
None
};
let _ = self.job_sender.send(Box::new(move || {
let result = handler();
if let Some(rev) = dispatch_revision
&& revision_arc.load(std::sync::atomic::Ordering::SeqCst) != rev
{
tracing::debug!(
"Dropping stale result for request {:?} (revision changed)",
request_id
);
return;
}
let _ = task_sender.send(TaskResult { request_id, result });
}));
}
fn try_dispatch_async(&self, req: &lsp_server::Request) -> bool {
match req.method.as_str() {
CodeLensResolve::METHOD => {
let id = req.id.clone();
let lens: CodeLens = match serde_json::from_value(req.params.clone()) {
Ok(l) => l,
Err(e) => {
self.dispatch_async_unconditional(id, move || Err(e.to_string()));
return true;
}
};
self.dispatch_async_unconditional(id, move || {
let resolved = handle_code_lens_resolve(lens);
serde_json::to_value(resolved).map_err(|e| e.to_string())
});
true
}
SemanticTokensFullRequest::METHOD => {
let id = req.id.clone();
let params: SemanticTokensParams = match serde_json::from_value(req.params.clone())
{
Ok(p) => p,
Err(e) => {
self.dispatch_async(id, move || Err(e.to_string()));
return true;
}
};
let uri = ¶ms.text_document.uri;
let (text, parse_result) = self.get_document_data(uri);
let encoding = self.position_encoding;
self.dispatch_async(id, move || {
let response = handle_semantic_tokens(¶ms, &text, &parse_result, encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
});
true
}
_ => false,
}
}
fn handle_message(&mut self, msg: Message) {
match msg {
Message::Request(req) => self.handle_request(req),
Message::Notification(notif) => self.handle_notification(notif),
Message::Response(_resp) => {
}
}
}
fn handle_request(&mut self, req: lsp_server::Request) {
let id = req.id.clone();
if self.try_dispatch_async(&req) {
return; }
let response = match self.dispatch_sync(req) {
Ok(value) => lsp_server::Response::new_ok(id, value),
Err(err) => lsp_server::Response::new_err(id, err.error_code() as i32, err.to_string()),
};
self.send(lsp_server::Message::Response(response));
}
fn dispatch_sync(
&mut self,
req: lsp_server::Request,
) -> Result<serde_json::Value, DispatchError> {
if req.method == Initialize::METHOD {
let _params: InitializeParams = serde_json::from_value(req.params)
.map_err(|e| DispatchError::Handler(e.to_string()))?;
return Err(DispatchError::DuplicateInitialize);
}
let method = req.method.clone();
let inner: Result<serde_json::Value, String> = match method.as_str() {
Shutdown::METHOD => {
self.shutdown_requested = true;
Ok(serde_json::Value::Null)
}
Completion::METHOD => self.handle_completion_request(req),
GotoDefinition::METHOD => self.handle_goto_definition_request(req),
References::METHOD => self.handle_references_request(req),
HoverRequest::METHOD => self.handle_hover_request(req),
DocumentSymbolRequest::METHOD => self.handle_document_symbols_request(req),
SemanticTokensFullDeltaRequest::METHOD => {
self.handle_semantic_tokens_delta_request(req)
}
SemanticTokensRangeRequest::METHOD => self.handle_semantic_tokens_range_request(req),
CodeActionRequest::METHOD => self.handle_code_action_request(req),
CodeActionResolveRequest::METHOD => self.handle_code_action_resolve_request(req),
WorkspaceSymbolRequest::METHOD => self.handle_workspace_symbol_request(req),
PrepareRenameRequest::METHOD => self.handle_prepare_rename_request(req),
Rename::METHOD => self.handle_rename_request(req),
Formatting::METHOD => self.handle_formatting_request(req),
RangeFormatting::METHOD => self.handle_range_formatting_request(req),
DocumentLinkRequest::METHOD => self.handle_document_link_request(req),
DocumentLinkResolve::METHOD => self.handle_document_link_resolve_request(req),
InlayHintRequest::METHOD => self.handle_inlay_hint_request(req),
InlayHintResolveRequest::METHOD => self.handle_inlay_hint_resolve_request(req),
SelectionRangeRequest::METHOD => self.handle_selection_range_request(req),
FoldingRangeRequest::METHOD => self.handle_folding_range_request(req),
TypeHierarchyPrepare::METHOD => self.handle_prepare_type_hierarchy_request(req),
TypeHierarchySupertypes::METHOD => self.handle_type_hierarchy_supertypes_request(req),
TypeHierarchySubtypes::METHOD => self.handle_type_hierarchy_subtypes_request(req),
DocumentHighlightRequest::METHOD => self.handle_document_highlight_request(req),
LinkedEditingRange::METHOD => self.handle_linked_editing_range_request(req),
OnTypeFormatting::METHOD => self.handle_on_type_formatting_request(req),
CodeLensRequest::METHOD => self.handle_code_lens_request(req),
DocumentColor::METHOD => self.handle_document_color_request(req),
ColorPresentationRequest::METHOD => self.handle_color_presentation_request(req),
GotoDeclaration::METHOD => self.handle_goto_declaration_request(req),
CallHierarchyPrepare::METHOD => self.handle_prepare_call_hierarchy_request(req),
CallHierarchyIncomingCalls::METHOD => self.handle_incoming_calls_request(req),
CallHierarchyOutgoingCalls::METHOD => self.handle_outgoing_calls_request(req),
SignatureHelpRequest::METHOD => self.handle_signature_help_request(req),
ExecuteCommand::METHOD => self.handle_execute_command_request(req),
ResolveCompletionItem::METHOD => self.handle_completion_resolve_request(req),
_ => {
tracing::warn!("Unhandled request: {method}");
return Err(DispatchError::MethodNotFound(method));
}
};
inner.map_err(DispatchError::Handler)
}
fn handle_completion_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: CompletionParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.text_document_position.text_document.uri;
let (text, parse_result) = self.get_document_data(uri);
let ledger_guard = self.ledger_state.read();
let ledger_state = if ledger_guard.ledger().is_some() {
Some(&*ledger_guard)
} else {
None
};
let response = handle_completion(
¶ms,
&text,
&parse_result,
ledger_state,
self.position_encoding,
);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_goto_definition_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: GotoDefinitionParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.text_document_position_params.text_document.uri;
let (text, parse_result) = self.get_document_data(uri);
let response =
handle_goto_definition(¶ms, &text, &parse_result, uri, self.position_encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_references_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: ReferenceParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.text_document_position.text_document.uri;
let (text, parse_result) = self.get_document_data(uri);
let response =
handle_references(¶ms, &text, &parse_result, uri, self.position_encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_hover_request(&self, req: lsp_server::Request) -> Result<serde_json::Value, String> {
let params: HoverParams = serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.text_document_position_params.text_document.uri;
let (text, parse_result) = self.get_document_data(uri);
let response = handle_hover(¶ms, &text, &parse_result, self.position_encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_document_symbols_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: DocumentSymbolParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.text_document.uri;
let (text, parse_result) = self.get_document_data(uri);
let response =
handle_document_symbols(¶ms, &text, &parse_result, self.position_encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_semantic_tokens_delta_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: SemanticTokensDeltaParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.text_document.uri;
let (text, parse_result) = self.get_document_data(uri);
let response = handle_semantic_tokens_delta(
¶ms,
&text,
&parse_result,
None,
self.position_encoding,
);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_semantic_tokens_range_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: SemanticTokensRangeParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.text_document.uri;
let (text, parse_result) = self.get_document_data(uri);
let response =
handle_semantic_tokens_range(¶ms, &text, &parse_result, self.position_encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_code_action_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: CodeActionParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.text_document.uri;
let (text, parse_result) = self.get_document_data(uri);
let response = handle_code_actions(¶ms, &text, &parse_result, self.position_encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_code_action_resolve_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let action: CodeAction = serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri: Uri = if let Some(data) = &action.data {
data.get("uri")
.and_then(|v| v.as_str())
.and_then(|s| s.parse().ok())
.unwrap_or_else(|| "file:///unknown".parse().unwrap())
} else {
"file:///unknown".parse().unwrap()
};
let (text, parse_result) = self.get_document_data(&uri);
let resolved =
handle_code_action_resolve(action, &text, &parse_result, &uri, self.position_encoding);
serde_json::to_value(resolved).map_err(|e| e.to_string())
}
fn handle_workspace_symbol_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: WorkspaceSymbolParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let mut vfs = self.vfs.write();
let documents: Vec<_> = vfs
.iter_with_parse()
.map(|(path, content, parse_result)| {
let uri_str = format!("file://{}", path.display());
let uri: Uri = uri_str
.parse()
.unwrap_or_else(|_| "file:///".parse().unwrap());
(uri, content, parse_result)
})
.collect();
let response = handle_workspace_symbols(¶ms, &documents, self.position_encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_prepare_rename_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: TextDocumentPositionParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.text_document.uri;
let (text, parse_result) = self.get_document_data(uri);
let response = handle_prepare_rename(¶ms, &text, &parse_result, self.position_encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_rename_request(&self, req: lsp_server::Request) -> Result<serde_json::Value, String> {
let params: RenameParams = serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.text_document_position.text_document.uri;
let (text, parse_result) = self.get_document_data(uri);
let response = handle_rename(¶ms, &text, &parse_result, self.position_encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_formatting_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: DocumentFormattingParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.text_document.uri;
let (text, parse_result) = self.get_document_data(uri);
let response = handle_formatting(¶ms, &text, &parse_result, self.position_encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_folding_range_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: FoldingRangeParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.text_document.uri;
let (text, parse_result) = self.get_document_data(uri);
let response = handle_folding_ranges(¶ms, &text, &parse_result, self.position_encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_range_formatting_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: DocumentRangeFormattingParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.text_document.uri;
let (text, parse_result) = self.get_document_data(uri);
let response =
handle_range_formatting(¶ms, &text, &parse_result, self.position_encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_document_link_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: DocumentLinkParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.text_document.uri;
let (text, parse_result) = self.get_document_data(uri);
let response = handle_document_links(¶ms, &text, &parse_result, self.position_encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_document_link_resolve_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let link: DocumentLink = serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let resolved = handle_document_link_resolve(link);
serde_json::to_value(resolved).map_err(|e| e.to_string())
}
fn handle_inlay_hint_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: InlayHintParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.text_document.uri;
let (text, parse_result) = self.get_document_data(uri);
let response = handle_inlay_hints(¶ms, &text, &parse_result, self.position_encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_inlay_hint_resolve_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let hint: InlayHint = serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri: Uri = if let Some(data) = &hint.data {
data.get("uri")
.and_then(|v| v.as_str())
.and_then(|s| s.parse().ok())
.unwrap_or_else(|| "file:///unknown".parse().unwrap())
} else {
"file:///unknown".parse().unwrap()
};
let (_text, parse_result) = self.get_document_data(&uri);
let resolved = handle_inlay_hint_resolve(hint, &parse_result);
serde_json::to_value(resolved).map_err(|e| e.to_string())
}
fn handle_selection_range_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: SelectionRangeParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.text_document.uri;
let (text, parse_result) = self.get_document_data(uri);
let response =
handle_selection_range(¶ms, &text, &parse_result, self.position_encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_prepare_type_hierarchy_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: TypeHierarchyPrepareParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.text_document_position_params.text_document.uri;
let (text, parse_result) = self.get_document_data(uri);
let response = handle_prepare_type_hierarchy(
¶ms,
&text,
&parse_result,
uri,
self.position_encoding,
);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_type_hierarchy_supertypes_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: TypeHierarchySupertypesParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.item.uri;
let (text, parse_result) = self.get_document_data(uri);
let response =
handle_supertypes(¶ms, &text, &parse_result, uri, self.position_encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_type_hierarchy_subtypes_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: TypeHierarchySubtypesParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.item.uri;
let (text, parse_result) = self.get_document_data(uri);
let response = handle_subtypes(¶ms, &text, &parse_result, uri, self.position_encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_document_highlight_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: DocumentHighlightParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.text_document_position_params.text_document.uri;
let (text, parse_result) = self.get_document_data(uri);
let response =
handle_document_highlight(¶ms, &text, &parse_result, self.position_encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_linked_editing_range_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: LinkedEditingRangeParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.text_document_position_params.text_document.uri;
let (text, parse_result) = self.get_document_data(uri);
let response =
handle_linked_editing_range(¶ms, &text, &parse_result, self.position_encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_on_type_formatting_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: DocumentOnTypeFormattingParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.text_document_position.text_document.uri;
let text = if let Some(path) = uri_to_path(uri) {
self.vfs.read().get_content(&path).unwrap_or_default()
} else {
String::new()
};
let response = handle_on_type_formatting(¶ms, &text, self.position_encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_code_lens_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: CodeLensParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.text_document.uri;
let (text, parse_result) = self.get_document_data(uri);
let cached_diagnostics = self.diagnostics.get(uri).map(Vec::as_slice);
let response = handle_code_lens(
¶ms,
&text,
&parse_result,
cached_diagnostics,
self.position_encoding,
);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_document_color_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: DocumentColorParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.text_document.uri;
let (text, parse_result) = self.get_document_data(uri);
let response = handle_document_color(¶ms, &text, &parse_result, self.position_encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_color_presentation_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: ColorPresentationParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let response = handle_color_presentation(¶ms);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_goto_declaration_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: GotoDefinitionParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.text_document_position_params.text_document.uri;
let (text, parse_result) = self.get_document_data(uri);
let response =
handle_goto_declaration(¶ms, &text, &parse_result, uri, self.position_encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_prepare_call_hierarchy_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: CallHierarchyPrepareParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.text_document_position_params.text_document.uri;
let (text, parse_result) = self.get_document_data(uri);
let response = handle_prepare_call_hierarchy(
¶ms,
&text,
&parse_result,
uri,
self.position_encoding,
);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_incoming_calls_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: CallHierarchyIncomingCallsParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.item.uri;
let (text, parse_result) = self.get_document_data(uri);
let response =
handle_incoming_calls(¶ms, &text, &parse_result, uri, self.position_encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_outgoing_calls_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: CallHierarchyOutgoingCallsParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.item.uri;
let (text, parse_result) = self.get_document_data(uri);
let response =
handle_outgoing_calls(¶ms, &text, &parse_result, uri, self.position_encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_signature_help_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: SignatureHelpParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri = ¶ms.text_document_position_params.text_document.uri;
let text = if let Some(path) = uri_to_path(uri) {
self.vfs.read().get_content(&path).unwrap_or_default()
} else {
String::new()
};
let response = handle_signature_help(¶ms, &text, self.position_encoding);
serde_json::to_value(response).map_err(|e| e.to_string())
}
fn handle_execute_command_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let params: ExecuteCommandParams =
serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri_from_args: Option<Uri> = params
.arguments
.first()
.and_then(|arg| arg.get("uri"))
.and_then(|v| v.as_str())
.and_then(|s| s.parse().ok());
if let Some(uri) = uri_from_args {
let (text, parse_result) = self.get_document_data(&uri);
let result =
handle_execute_command(¶ms, &text, &parse_result, &uri, self.position_encoding);
self.send_show_message(result.show_message);
return Ok(result.response.unwrap_or(serde_json::Value::Null));
}
let first_path = self.vfs.read().paths().next().cloned();
let path = match first_path {
Some(p) => p,
None => {
return Ok(serde_json::json!({
"error": "No document open"
}));
}
};
#[cfg(not(windows))]
let uri: Uri = format!("file://{}", path.display())
.parse()
.map_err(|e| format!("{:?}", e))?;
#[cfg(windows)]
let uri: Uri = format!("file:///{}", path.display())
.parse()
.map_err(|e| format!("{:?}", e))?;
let (text, parse_result) = self.get_document_data(&uri);
let result =
handle_execute_command(¶ms, &text, &parse_result, &uri, self.position_encoding);
self.send_show_message(result.show_message);
Ok(result.response.unwrap_or(serde_json::Value::Null))
}
fn send_show_message(&self, params: Option<lsp_types::ShowMessageParams>) {
let Some(params) = params else { return };
let notif = lsp_server::Notification::new(
<lsp_types::notification::ShowMessage as lsp_types::notification::Notification>::METHOD
.to_string(),
params,
);
self.send(lsp_server::Message::Notification(notif));
}
fn handle_completion_resolve_request(
&self,
req: lsp_server::Request,
) -> Result<serde_json::Value, String> {
let item: CompletionItem = serde_json::from_value(req.params).map_err(|e| e.to_string())?;
let uri: Uri = if let Some(data) = &item.data {
data.get("uri")
.and_then(|v| v.as_str())
.and_then(|s| s.parse().ok())
.unwrap_or_else(|| "file:///unknown".parse().unwrap())
} else {
"file:///unknown".parse().unwrap()
};
let (_text, parse_result) = self.get_document_data(&uri);
let ledger_guard = self.ledger_state.read();
let directives = ledger_guard
.directives()
.unwrap_or_else(|| parse_result.directives.as_slice());
let resolved = handle_completion_resolve(item, directives);
serde_json::to_value(resolved).map_err(|e| e.to_string())
}
fn handle_notification(&mut self, notif: lsp_server::Notification) {
match notif.method.as_str() {
DidOpenTextDocument::METHOD => {
if let Ok(params) =
serde_json::from_value::<lsp_types::DidOpenTextDocumentParams>(notif.params)
{
self.on_did_open(params);
}
}
DidChangeTextDocument::METHOD => {
if let Ok(params) =
serde_json::from_value::<lsp_types::DidChangeTextDocumentParams>(notif.params)
{
self.on_did_change(params);
}
}
DidCloseTextDocument::METHOD => {
if let Ok(params) =
serde_json::from_value::<lsp_types::DidCloseTextDocumentParams>(notif.params)
{
self.on_did_close(params);
}
}
DidChangeWatchedFiles::METHOD => {
if let Ok(params) =
serde_json::from_value::<lsp_types::DidChangeWatchedFilesParams>(notif.params)
{
self.on_did_change_watched_files(params);
}
}
"initialized" => {
tracing::info!("Client initialized");
self.register_file_watchers();
}
"exit" => {
tracing::info!("Exit notification received");
let code = if self.shutdown_requested { 0 } else { 1 };
self.pending_exit_code = Some(code);
if let Some(action) = self.exit_action.take() {
action(code);
}
}
_ => {
tracing::debug!("Unhandled notification: {}", notif.method);
}
}
}
fn on_did_open(&mut self, params: lsp_types::DidOpenTextDocumentParams) {
let uri = params.text_document.uri;
let text = params.text_document.text;
let version = params.text_document.version;
tracing::info!("Document opened: {}", uri.as_str());
if let Some(path) = uri_to_path(&uri) {
self.vfs.write().open(path, text.clone(), version);
}
self.bump_revision();
self.publish_diagnostics(&uri, &text);
}
fn on_did_change(&mut self, params: lsp_types::DidChangeTextDocumentParams) {
let uri = params.text_document.uri;
let version = params.text_document.version;
if let Some(change) = params.content_changes.into_iter().last() {
let text = change.text;
tracing::debug!("Document changed: {}", uri.as_str());
if let Some(path) = uri_to_path(&uri) {
self.vfs.write().update(&path, text.clone(), version);
}
self.bump_revision();
self.publish_diagnostics(&uri, &text);
}
}
fn on_did_close(&mut self, params: lsp_types::DidCloseTextDocumentParams) {
let uri = params.text_document.uri;
tracing::info!("Document closed: {}", uri.as_str());
if let Some(path) = uri_to_path(&uri) {
self.vfs.write().close(&path);
}
self.diagnostics.remove(&uri);
self.send_diagnostics(&uri, vec![]);
}
fn on_did_change_watched_files(&mut self, params: lsp_types::DidChangeWatchedFilesParams) {
tracing::info!("Watched files changed: {} files", params.changes.len());
let mut should_reload_journal = false;
let mut should_revalidate = false;
for change in params.changes {
tracing::debug!("File {:?}: {:?}", change.uri.as_str(), change.typ);
if let Some(path) = uri_to_path(&change.uri) {
let ledger_guard = self.ledger_state.read();
if ledger_guard.contains_file(&path) {
should_reload_journal = true;
}
}
if change.uri.as_str().ends_with(".beancount") || change.uri.as_str().ends_with(".bean")
{
should_revalidate = true;
}
}
if should_reload_journal {
tracing::info!("Reloading journal due to external file changes");
self.reload_journal();
}
if should_revalidate {
self.revalidate_open_documents();
}
}
fn revalidate_open_documents(&mut self) {
let paths: Vec<_> = self.vfs.read().paths().cloned().collect();
let documents: Vec<_> = paths
.into_iter()
.filter_map(|path| {
let content = self.vfs.read().get_content(&path)?;
let uri_str = format!("file://{}", path.display());
let uri = uri_str.parse::<Uri>().ok()?;
Some((uri, content))
})
.collect();
for (uri, content) in documents {
tracing::debug!("Revalidating: {}", uri.as_str());
self.publish_diagnostics(&uri, &content);
}
}
fn register_file_watchers(&self) {
let watchers = vec![
lsp_types::FileSystemWatcher {
glob_pattern: lsp_types::GlobPattern::String("**/*.beancount".to_string()),
kind: Some(lsp_types::WatchKind::all()),
},
lsp_types::FileSystemWatcher {
glob_pattern: lsp_types::GlobPattern::String("**/*.bean".to_string()),
kind: Some(lsp_types::WatchKind::all()),
},
];
let registration = lsp_types::Registration {
id: "file-watcher".to_string(),
method: "workspace/didChangeWatchedFiles".to_string(),
register_options: Some(
serde_json::to_value(lsp_types::DidChangeWatchedFilesRegistrationOptions {
watchers,
})
.unwrap_or_default(),
),
};
let params = lsp_types::RegistrationParams {
registrations: vec![registration],
};
let request = lsp_server::Request::new(
lsp_server::RequestId::from("register-file-watchers".to_string()),
"client/registerCapability".to_string(),
params,
);
self.send(lsp_server::Message::Request(request));
tracing::info!("Registered file watchers for *.beancount and *.bean files");
}
fn publish_diagnostics(&mut self, uri: &Uri, text: &str) {
let result = parse(text);
let current_canonical_path = uri_to_path(uri).and_then(|p| p.canonicalize().ok());
let other_buffer_parses: Vec<(PathBuf, Arc<ParseResult>)> = {
let mut vfs = self.vfs.write();
vfs.iter_with_parse()
.filter_map(|(path, _text, parsed)| {
let canonical = path.canonicalize().ok()?;
if Some(&canonical) == current_canonical_path.as_ref() {
return None;
}
if !parsed.errors.is_empty() {
return None;
}
Some((canonical, parsed))
})
.collect()
};
let ledger_guard = self.ledger_state.read();
let (ledger_state, current_file_id) = if ledger_guard.ledger().is_some() {
let file_id = current_canonical_path.as_ref().and_then(|canonical| {
ledger_guard.ledger().and_then(|ledger| {
ledger.source_map.files().iter().find_map(|f| {
f.path
.canonicalize()
.ok()
.filter(|canonical_f| canonical_f == canonical)
.map(|_| f.id as u16)
})
})
});
(Some(&*ledger_guard), file_id)
} else {
(None, None)
};
let other_buffer_overlays: Vec<(u16, &[Spanned<Directive>])> =
if let Some(ls) = ledger_state {
let ledger = ls.ledger().expect("ledger_state.ledger() checked above");
other_buffer_parses
.iter()
.filter_map(|(canonical, parsed)| {
let fid = ledger.source_map.files().iter().find_map(|f| {
f.path
.canonicalize()
.ok()
.filter(|canonical_f| canonical_f == canonical)
.map(|_| f.id as u16)
})?;
Some((fid, parsed.directives.as_slice()))
})
.collect()
} else {
Vec::new()
};
let diagnostics = all_diagnostics(
&result,
text,
ledger_state,
current_file_id,
current_canonical_path.as_deref(),
&other_buffer_overlays,
self.position_encoding,
);
drop(ledger_guard);
tracing::debug!(
"Publishing {} diagnostics for {} (file_id: {:?})",
diagnostics.len(),
uri.as_str(),
current_file_id
);
self.diagnostics.insert(uri.clone(), diagnostics.clone());
self.send_diagnostics(uri, diagnostics);
}
fn send_diagnostics(&self, uri: &Uri, diagnostics: Vec<lsp_types::Diagnostic>) {
let params = PublishDiagnosticsParams {
uri: uri.clone(),
diagnostics,
version: None,
};
let notif = lsp_server::Notification::new(PublishDiagnostics::METHOD.to_string(), params);
self.send(lsp_server::Message::Notification(notif));
}
fn send(&self, msg: lsp_server::Message) {
if let Err(e) = self.sender.send(msg) {
tracing::error!("Failed to send message: {}", e);
}
}
}
#[must_use]
pub fn run_main_loop(
receiver: Receiver<lsp_server::Message>,
sender: Sender<lsp_server::Message>,
journal_file: Option<PathBuf>,
position_encoding: crate::handlers::utils::PositionEncoding,
) -> i32 {
run_main_loop_with_exit_action(
receiver,
sender,
journal_file,
position_encoding,
|_code| {},
)
}
#[must_use]
pub fn run_main_loop_with_exit_action<F>(
receiver: Receiver<lsp_server::Message>,
sender: Sender<lsp_server::Message>,
journal_file: Option<PathBuf>,
position_encoding: crate::handlers::utils::PositionEncoding,
exit_action: F,
) -> i32
where
F: FnOnce(i32) + Send + 'static,
{
let mut state = MainLoopState::new(sender, journal_file).with_exit_action(exit_action);
state.position_encoding = position_encoding;
let task_receiver = state.task_receiver.clone();
tracing::info!("Main loop started");
let exit_code = loop {
crossbeam_channel::select! {
recv(receiver) -> msg => {
let msg = match msg {
Ok(msg) => msg,
Err(_) => break 0, };
let event = match msg {
lsp_server::Message::Request(req) => Event::Message(Message::Request(req)),
lsp_server::Message::Notification(notif) => {
Event::Message(Message::Notification(notif))
}
lsp_server::Message::Response(resp) => Event::Message(Message::Response(resp)),
};
state.handle_event(event);
}
recv(task_receiver) -> task_result => {
if let Ok(result) = task_result {
state.handle_event(Event::Task(result));
}
}
}
if let Some(code) = state.pending_exit_code {
break code;
}
};
tracing::info!("Main loop ended (exit code {exit_code})");
exit_code
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dispatch_error_codes() {
assert_eq!(
DispatchError::MethodNotFound("foo/bar".into()).error_code() as i32,
lsp_server::ErrorCode::MethodNotFound as i32,
);
assert_eq!(
DispatchError::DuplicateInitialize.error_code() as i32,
lsp_server::ErrorCode::InvalidRequest as i32,
);
assert_eq!(
DispatchError::Handler("boom".into()).error_code() as i32,
lsp_server::ErrorCode::InternalError as i32,
);
}
#[test]
fn dispatch_error_display() {
let unhandled = DispatchError::MethodNotFound("foo/bar".into()).to_string();
assert!(
unhandled.contains("foo/bar"),
"MethodNotFound should include the method name: {unhandled}",
);
let dup_init = DispatchError::DuplicateInitialize.to_string();
assert!(
dup_init.contains("exactly once"),
"DuplicateInitialize message should cite the spec invariant: {dup_init}",
);
let handler = DispatchError::Handler("custom failure".into()).to_string();
assert_eq!(handler, "custom failure");
}
}