use std::panic::UnwindSafe; use anyhow::anyhow; use lsp_server::{self as server, RequestId}; use lsp_types::{notification::Notification, request::Request}; use notifications as notification; use requests as request; use crate::{ server::{ api::traits::{ BackgroundDocumentNotificationHandler, BackgroundDocumentRequestHandler, SyncNotificationHandler, }, schedule::Task, }, session::{Client, Session}, }; mod diagnostics; mod notifications; mod requests; mod traits; use self::traits::{NotificationHandler, RequestHandler}; use super::{Result, schedule::BackgroundSchedule}; /// Defines the `document_url` method for implementers of [`Notification`] and [`Request`], given /// the request or notification parameter type. /// /// This would only work if the parameter type has a `text_document` field with a `uri` field /// that is of type [`lsp_types::Url`]. macro_rules! define_document_url { ($params:ident: &$p:ty) => { fn document_url($params: &$p) -> std::borrow::Cow<'_, lsp_types::Url> { std::borrow::Cow::Borrowed(&$params.text_document.uri) } }; } use define_document_url; /// Processes a request from the client to the server. /// /// The LSP specification requires that each request has exactly one response. Therefore, /// it's crucial that all paths in this method call [`Client::respond`] exactly once. /// The only exception to this is requests that were cancelled by the client. In this case, /// the response was already sent by the [`notification::CancelNotificationHandler`]. pub(super) fn request(req: server::Request) -> Task { let id = req.id.clone(); match req.method.as_str() { request::CodeActions::METHOD => { background_request_task::(req, BackgroundSchedule::Worker) } request::CodeActionResolve::METHOD => { background_request_task::(req, BackgroundSchedule::Worker) } request::DocumentDiagnostic::METHOD => { background_request_task::(req, BackgroundSchedule::Worker) } request::ExecuteCommand::METHOD => sync_request_task::(req), request::Format::METHOD => { background_request_task::(req, BackgroundSchedule::Fmt) } request::FormatRange::METHOD => { background_request_task::(req, BackgroundSchedule::Fmt) } request::Hover::METHOD => { background_request_task::(req, BackgroundSchedule::Worker) } lsp_types::request::Shutdown::METHOD => sync_request_task::(req), method => { tracing::warn!("Received request {method} which does not have a handler"); let result: Result<()> = Err(Error::new( anyhow!("Unknown request: {method}"), server::ErrorCode::MethodNotFound, )); return Task::immediate(id, result); } } .unwrap_or_else(|err| { tracing::error!("Encountered error when routing request with ID {id}: {err}"); Task::sync(move |_session, client| { client.show_error_message( "Ruff failed to handle a request from the editor. Check the logs for more details.", ); respond_silent_error( id, client, lsp_server::ResponseError { code: err.code as i32, message: err.to_string(), data: None, }, ); }) }) } pub(super) fn notification(notif: server::Notification) -> Task { match notif.method.as_str() { notification::DidChange::METHOD => { sync_notification_task::(notif) } notification::DidChangeConfiguration::METHOD => { sync_notification_task::(notif) } notification::DidChangeWatchedFiles::METHOD => { sync_notification_task::(notif) } notification::DidChangeWorkspace::METHOD => { sync_notification_task::(notif) } notification::DidClose::METHOD => sync_notification_task::(notif), notification::DidOpen::METHOD => sync_notification_task::(notif), notification::DidOpenNotebook::METHOD => { sync_notification_task::(notif) } notification::DidChangeNotebook::METHOD => { sync_notification_task::(notif) } notification::DidCloseNotebook::METHOD => { sync_notification_task::(notif) } lsp_types::notification::Cancel::METHOD => { sync_notification_task::(notif) } lsp_types::notification::SetTrace::METHOD => { tracing::trace!("Ignoring `setTrace` notification"); return Task::nothing(); } method => { tracing::warn!("Received notification {method} which does not have a handler."); return Task::nothing(); } } .unwrap_or_else(|err| { tracing::error!("Encountered error when routing notification: {err}"); Task::sync(|_session, client| { client.show_error_message( "Ruff failed to handle a notification from the editor. Check the logs for more details." ); }) }) } fn sync_request_task(req: server::Request) -> Result where <::RequestType as Request>::Params: UnwindSafe, { let (id, params) = cast_request::(req)?; Ok(Task::sync(move |session, client: &Client| { let _span = tracing::debug_span!("request", %id, method = R::METHOD).entered(); let result = R::run(session, client, params); respond::(&id, result, client); })) } fn background_request_task( req: server::Request, schedule: BackgroundSchedule, ) -> Result where <::RequestType as Request>::Params: UnwindSafe, { let (id, params) = cast_request::(req)?; Ok(Task::background(schedule, move |session: &Session| { let cancellation_token = session .request_queue() .incoming() .cancellation_token(&id) .expect("request should have been tested for cancellation before scheduling"); let url = R::document_url(¶ms).into_owned(); let Some(snapshot) = session.take_snapshot(R::document_url(¶ms).into_owned()) else { tracing::warn!("Ignoring request because snapshot for path `{url:?}` doesn't exist."); return Box::new(|_| {}); }; Box::new(move |client| { let _span = tracing::debug_span!("request", %id, method = R::METHOD).entered(); // Test again if the request was cancelled since it was scheduled on the background task // and, if so, return early if cancellation_token.is_cancelled() { tracing::trace!( "Ignoring request id={id} method={} because it was cancelled", R::METHOD ); // We don't need to send a response here because the `cancel` notification // handler already responded with a message. return; } let result = std::panic::catch_unwind(|| R::run_with_snapshot(snapshot, client, params)); let response = request_result_to_response::(result); respond::(&id, response, client); }) })) } fn request_result_to_response( result: std::result::Result< Result<<::RequestType as Request>::Result>, Box, >, ) -> Result<<::RequestType as Request>::Result> where R: BackgroundDocumentRequestHandler, { match result { Ok(response) => response, Err(error) => { let message = if let Some(panic_message) = panic_message(&error) { format!("Request handler failed with: {panic_message}") } else { "Request handler failed".into() }; Err(Error { code: lsp_server::ErrorCode::InternalError, error: anyhow!(message), }) } } } fn sync_notification_task(notif: server::Notification) -> Result { let (id, params) = cast_notification::(notif)?; Ok(Task::sync(move |session, client| { let _span = tracing::debug_span!("notification", method = N::METHOD).entered(); if let Err(err) = N::run(session, client, params) { tracing::error!("An error occurred while running {id}: {err}"); client .show_error_message("Ruff encountered a problem. Check the logs for more details."); } })) } #[expect(dead_code)] fn background_notification_thread( req: server::Notification, schedule: BackgroundSchedule, ) -> Result where N: BackgroundDocumentNotificationHandler, <::NotificationType as Notification>::Params: UnwindSafe, { let (id, params) = cast_notification::(req)?; Ok(Task::background(schedule, move |session: &Session| { let url = N::document_url(¶ms); let Some(snapshot) = session.take_snapshot((*url).clone()) else { tracing::debug!( "Ignoring notification because snapshot for url `{url}` doesn't exist." ); return Box::new(|_| {}); }; Box::new(move |client| { let _span = tracing::debug_span!("notification", method = N::METHOD).entered(); let result = match std::panic::catch_unwind(|| N::run_with_snapshot(snapshot, client, params)) { Ok(result) => result, Err(panic) => { let message = if let Some(panic_message) = panic_message(&panic) { format!("notification handler for {id} failed with: {panic_message}") } else { format!("notification handler for {id} failed") }; tracing::error!(message); client.show_error_message( "Ruff encountered a panic. Check the logs for more details.", ); return; } }; if let Err(err) = result { tracing::error!("An error occurred while running {id}: {err}"); client.show_error_message( "Ruff encountered a problem. Check the logs for more details.", ); } }) })) } /// Tries to cast a serialized request from the server into /// a parameter type for a specific request handler. /// It is *highly* recommended to not override this function in your /// implementation. fn cast_request( request: server::Request, ) -> Result<( RequestId, <::RequestType as Request>::Params, )> where Req: RequestHandler, <::RequestType as Request>::Params: UnwindSafe, { request .extract(Req::METHOD) .map_err(|err| match err { json_err @ server::ExtractError::JsonError { .. } => { anyhow::anyhow!("JSON parsing failure:\n{json_err}") } server::ExtractError::MethodMismatch(_) => { unreachable!("A method mismatch should not be possible here unless you've used a different handler (`Req`) \ than the one whose method name was matched against earlier.") } }) .with_failure_code(server::ErrorCode::InternalError) } /// Sends back a response to the server, but only if the request wasn't cancelled. fn respond( id: &RequestId, result: Result<<::RequestType as Request>::Result>, client: &Client, ) where Req: RequestHandler, { if let Err(err) = &result { tracing::error!("An error occurred with request ID {id}: {err}"); client.show_error_message("Ruff encountered a problem. Check the logs for more details."); } if let Err(err) = client.respond(id, result) { tracing::error!("Failed to send response: {err}"); } } /// Sends back an error response to the server using a [`Client`] without showing a warning /// to the user. fn respond_silent_error(id: RequestId, client: &Client, error: lsp_server::ResponseError) { if let Err(err) = client.respond_err(id, error) { tracing::error!("Failed to send response: {err}"); } } /// Tries to cast a serialized request from the server into /// a parameter type for a specific request handler. fn cast_notification( notification: server::Notification, ) -> Result<( &'static str, <::NotificationType as Notification>::Params, )> where N: NotificationHandler, { Ok(( N::METHOD, notification .extract(N::METHOD) .map_err(|err| match err { json_err @ server::ExtractError::JsonError { .. } => { anyhow::anyhow!("JSON parsing failure:\n{json_err}") } server::ExtractError::MethodMismatch(_) => { unreachable!("A method mismatch should not be possible here unless you've used a different handler (`N`) \ than the one whose method name was matched against earlier.") } }) .with_failure_code(server::ErrorCode::InternalError)?, )) } pub(crate) struct Error { pub(crate) code: server::ErrorCode, pub(crate) error: anyhow::Error, } /// A trait to convert result types into the server result type, [`super::Result`]. trait LSPResult { fn with_failure_code(self, code: server::ErrorCode) -> super::Result; } impl> LSPResult for core::result::Result { fn with_failure_code(self, code: server::ErrorCode) -> super::Result { self.map_err(|err| Error::new(err.into(), code)) } } impl Error { pub(crate) fn new(err: anyhow::Error, code: server::ErrorCode) -> Self { Self { code, error: err } } } // Right now, we treat the error code as invisible data that won't // be printed. impl std::fmt::Debug for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.error.fmt(f) } } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.error.fmt(f) } } fn panic_message<'a>( err: &'a Box, ) -> Option> { if let Some(s) = err.downcast_ref::() { Some(s.into()) } else if let Some(&s) = err.downcast_ref::<&str>() { Some(s.into()) } else { None } }