use crate::server::schedule::Task; use crate::session::Session; use crate::system::AnySystemPath; use anyhow::anyhow; use lsp_server as server; use lsp_server::RequestId; use lsp_types::notification::Notification; use lsp_types::request::Request; use std::panic::{AssertUnwindSafe, UnwindSafe}; mod diagnostics; mod notifications; mod requests; mod semantic_tokens; mod traits; use self::traits::{NotificationHandler, RequestHandler}; use super::{Result, schedule::BackgroundSchedule}; use crate::session::client::Client; pub(crate) use diagnostics::publish_settings_diagnostics; use ruff_db::panic::PanicError; /// 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() { requests::DocumentDiagnosticRequestHandler::METHOD => background_document_request_task::< requests::DocumentDiagnosticRequestHandler, >( req, BackgroundSchedule::Worker ), requests::WorkspaceDiagnosticRequestHandler::METHOD => background_request_task::< requests::WorkspaceDiagnosticRequestHandler, >( req, BackgroundSchedule::Worker ), requests::GotoTypeDefinitionRequestHandler::METHOD => background_document_request_task::< requests::GotoTypeDefinitionRequestHandler, >( req, BackgroundSchedule::Worker ), requests::GotoDeclarationRequestHandler::METHOD => background_document_request_task::< requests::GotoDeclarationRequestHandler, >( req, BackgroundSchedule::Worker ), requests::GotoDefinitionRequestHandler::METHOD => background_document_request_task::< requests::GotoDefinitionRequestHandler, >(req, BackgroundSchedule::Worker), requests::HoverRequestHandler::METHOD => background_document_request_task::< requests::HoverRequestHandler, >(req, BackgroundSchedule::Worker), requests::ReferencesRequestHandler::METHOD => background_document_request_task::< requests::ReferencesRequestHandler, >(req, BackgroundSchedule::Worker), requests::InlayHintRequestHandler::METHOD => background_document_request_task::< requests::InlayHintRequestHandler, >(req, BackgroundSchedule::Worker), requests::SemanticTokensRequestHandler::METHOD => background_document_request_task::< requests::SemanticTokensRequestHandler, >(req, BackgroundSchedule::Worker), requests::SemanticTokensRangeRequestHandler::METHOD => background_document_request_task::< requests::SemanticTokensRangeRequestHandler, >( req, BackgroundSchedule::Worker ), requests::SignatureHelpRequestHandler::METHOD => background_document_request_task::< requests::SignatureHelpRequestHandler, >(req, BackgroundSchedule::Worker), requests::CompletionRequestHandler::METHOD => background_document_request_task::< requests::CompletionRequestHandler, >( req, BackgroundSchedule::LatencySensitive ), 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( "ty 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() { notifications::DidCloseTextDocumentHandler::METHOD => { sync_notification_task::(notif) } notifications::DidOpenTextDocumentHandler::METHOD => { sync_notification_task::(notif) } notifications::DidChangeTextDocumentHandler::METHOD => { sync_notification_task::(notif) } notifications::DidOpenNotebookHandler::METHOD => { sync_notification_task::(notif) } notifications::DidCloseNotebookHandler::METHOD => { sync_notification_task::(notif) } notifications::DidChangeWatchedFiles::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( "ty 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 retry = R::RETRY_ON_CANCELLATION.then(|| req.clone()); 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"); // SAFETY: The `snapshot` is safe to move across the unwind boundary because it is not used // after unwinding. let snapshot = AssertUnwindSafe(session.take_session_snapshot()); 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 = ruff_db::panic::catch_unwind(|| R::run(snapshot, client, params)); if let Some(response) = request_result_to_response::(&id, client, result, retry) { respond::(&id, response, client); } }) })) } fn background_document_request_task( req: server::Request, schedule: BackgroundSchedule, ) -> Result where <::RequestType as Request>::Params: UnwindSafe, { let retry = R::RETRY_ON_CANCELLATION.then(|| req.clone()); 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 Ok(path) = AnySystemPath::try_from_url(&url) else { let reason = format!("URL `{url}` isn't a valid system path"); tracing::warn!( "Ignoring request id={id} method={} because {reason}", R::METHOD ); return Box::new(|client| { respond_silent_error( id, client, lsp_server::ResponseError { code: lsp_server::ErrorCode::InvalidParams as i32, message: reason, data: None, }, ); }); }; let db = session.project_db(&path).clone(); let snapshot = session.take_document_snapshot(url); 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 = ruff_db::panic::catch_unwind(|| { R::run_with_snapshot(&db, snapshot, client, params) }); if let Some(response) = request_result_to_response::(&id, client, result, retry) { respond::(&id, response, client); } }) })) } fn request_result_to_response( id: &RequestId, client: &Client, result: std::result::Result< Result<<::RequestType as Request>::Result>, PanicError, >, request: Option, ) -> Option::RequestType as Request>::Result>> where R: traits::RetriableRequestHandler, { match result { Ok(response) => Some(response), Err(error) => { // Check if the request was canceled due to some modifications to the salsa database. if error.payload.downcast_ref::().is_some() { // If the query supports retry, re-queue the request. // The query is still likely to succeed if the user modified any other document. if let Some(request) = request { tracing::trace!( "request id={} method={} was cancelled by salsa, re-queueing for retry", request.id, request.method ); client.retry(request); } else { tracing::trace!( "request id={} was cancelled by salsa, sending content modified", id ); respond_silent_error(id.clone(), client, R::salsa_cancellation_error()); } None } else { Some(Err(Error { code: lsp_server::ErrorCode::InternalError, error: anyhow!("request handler {error}"), })) } } } } 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("ty encountered a problem. Check the logs for more details."); } })) } #[expect(dead_code)] fn background_notification_thread( req: server::Notification, schedule: BackgroundSchedule, ) -> Result where N: traits::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 snapshot = session.take_document_snapshot((*url).clone()); Box::new(move |client| { let _span = tracing::debug_span!("notification", method = N::METHOD).entered(); let result = match ruff_db::panic::catch_unwind(|| { N::run_with_snapshot(snapshot, client, params) }) { Ok(result) => result, Err(panic) => { tracing::error!("An error occurred while running {id}: {panic}"); client.show_error_message( "ty 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( "ty 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("ty encountered a problem. Check the logs for more details."); } client.respond(id, result); } /// 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) { client.respond_err(id, error); } /// 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) } }