use crate::server::schedule::Task; use crate::session::Session; use crate::system::{url_to_any_system_path, AnySystemPath}; use lsp_server as server; use lsp_types::notification::Notification; mod diagnostics; mod notifications; mod requests; mod traits; use notifications as notification; use requests as request; use self::traits::{NotificationHandler, RequestHandler}; use super::{client::Responder, schedule::BackgroundSchedule, Result}; pub(super) fn request<'a>(req: server::Request) -> Task<'a> { let id = req.id.clone(); match req.method.as_str() { request::DocumentDiagnosticRequestHandler::METHOD => { background_request_task::( req, BackgroundSchedule::LatencySensitive, ) } request::GotoTypeDefinitionRequestHandler::METHOD => { background_request_task::( req, BackgroundSchedule::LatencySensitive, ) } method => { tracing::warn!("Received request {method} which does not have a handler"); return Task::nothing(); } } .unwrap_or_else(|err| { tracing::error!("Encountered error when routing request with ID {id}: {err}"); show_err_msg!( "Ruff failed to handle a request from the editor. Check the logs for more details." ); let result: Result<()> = Err(err); Task::immediate(id, result) }) } pub(super) fn notification<'a>(notif: server::Notification) -> Task<'a> { match notif.method.as_str() { notification::DidCloseTextDocumentHandler::METHOD => local_notification_task::(notif), notification::DidOpenTextDocumentHandler::METHOD => local_notification_task::(notif), notification::DidChangeTextDocumentHandler::METHOD => local_notification_task::(notif), notification::DidOpenNotebookHandler::METHOD => { local_notification_task::(notif) } notification::DidCloseNotebookHandler::METHOD => { local_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}"); show_err_msg!("Ruff failed to handle a notification from the editor. Check the logs for more details."); Task::nothing() }) } fn _local_request_task<'a, R: traits::SyncRequestHandler>( req: server::Request, ) -> super::Result> { let (id, params) = cast_request::(req)?; Ok(Task::local(|session, notifier, requester, responder| { let _span = tracing::trace_span!("request", %id, method = R::METHOD).entered(); let result = R::run(session, notifier, requester, params); respond::(id, result, &responder); })) } // TODO(micha): Calls to `db` could panic if the db gets mutated while this task is running. // We should either wrap `R::run_with_snapshot` with a salsa catch cancellation handler or // use `SemanticModel` instead of passing `db` which uses a Result for all it's methods // that propagate cancellations. fn background_request_task<'a, R: traits::BackgroundDocumentRequestHandler>( req: server::Request, schedule: BackgroundSchedule, ) -> super::Result> { let (id, params) = cast_request::(req)?; Ok(Task::background(schedule, move |session: &Session| { let url = R::document_url(¶ms).into_owned(); let Ok(path) = url_to_any_system_path(&url) else { return Box::new(|_, _| {}); }; let db = match path { AnySystemPath::System(path) => match session.project_db_for_path(path.as_std_path()) { Some(db) => db.clone(), None => session.default_project_db().clone(), }, AnySystemPath::SystemVirtual(_) => session.default_project_db().clone(), }; let Some(snapshot) = session.take_snapshot(url) else { return Box::new(|_, _| {}); }; Box::new(move |notifier, responder| { let _span = tracing::trace_span!("request", %id, method = R::METHOD).entered(); let result = R::run_with_snapshot(snapshot, db, notifier, params); respond::(id, result, &responder); }) })) } fn local_notification_task<'a, N: traits::SyncNotificationHandler>( notif: server::Notification, ) -> super::Result> { let (id, params) = cast_notification::(notif)?; Ok(Task::local(move |session, notifier, requester, _| { let _span = tracing::trace_span!("notification", method = N::METHOD).entered(); if let Err(err) = N::run(session, notifier, requester, params) { tracing::error!("An error occurred while running {id}: {err}"); show_err_msg!("Ruff encountered a problem. Check the logs for more details."); } })) } #[allow(dead_code)] fn background_notification_thread<'a, N: traits::BackgroundDocumentNotificationHandler>( req: server::Notification, schedule: BackgroundSchedule, ) -> super::Result> { let (id, params) = cast_notification::(req)?; Ok(Task::background(schedule, move |session: &Session| { // TODO(jane): we should log an error if we can't take a snapshot. let Some(snapshot) = session.take_snapshot(N::document_url(¶ms).into_owned()) else { return Box::new(|_, _| {}); }; Box::new(move |notifier, _| { let _span = tracing::trace_span!("notification", method = N::METHOD).entered(); if let Err(err) = N::run_with_snapshot(snapshot, notifier, params) { tracing::error!("An error occurred while running {id}: {err}"); show_err_msg!("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, ) -> super::Result<( server::RequestId, <::RequestType as lsp_types::request::Request>::Params, )> where Req: traits::RequestHandler, { 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 using a [`Responder`]. fn respond( id: server::RequestId, result: crate::server::Result< <::RequestType as lsp_types::request::Request>::Result, >, responder: &Responder, ) where Req: traits::RequestHandler, { if let Err(err) = &result { tracing::error!("An error occurred with request ID {id}: {err}"); show_err_msg!("Ruff encountered a problem. Check the logs for more details."); } if let Err(err) = responder.respond(id, result) { 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, ) -> super::Result< ( &'static str, <::NotificationType as lsp_types::notification::Notification>::Params, )> where N: traits::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) } }