[ty] Add background request task support (#19041)

## Summary

This PR adds a new trait to support running a request in the background.

Currently, there exists a `BackgroundDocumentRequestHandler` trait which
is similar but is scoped to a specific document (file in an editor
context). The new trait `BackgroundRequestHandler` is not tied to a
specific document nor a specific project but it's for the entire
workspace.

This is added to support running workspace wide requests like computing
the [workspace
diagnostics](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_diagnostic)
or [workspace
symbols](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_symbol).

**Note:** There's a slight difference with what a "workspace" means
between the server and ty. Currently, there's a 1-1 relationship between
a workspace in an editor and the project database corresponding to that
workspace in ty but this could change in the future when Micha adds
support for multiple workspaces or multi-root workspaces.

The data that would be required by the request handler (based on
implementing workspace diagnostics) is the list of databases
(`ProjectDatabse`) corresponding to the projects in the workspace and
the index (`Index`) that contains the open documents. The
`WorkspaceSnapshot` represents this and is passed to the handler similar
to `DocumentSnapshot`.

## Test Plan

This is used in implementing the workspace diagnostics which is where
this is tested.
This commit is contained in:
Dhruv Manilawala 2025-07-03 16:31:10 +05:30 committed by GitHub
parent e212dc2e8e
commit a95c18a8e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 136 additions and 26 deletions

View file

@ -28,23 +28,23 @@ pub(super) fn request(req: server::Request) -> Task {
let id = req.id.clone();
match req.method.as_str() {
requests::DocumentDiagnosticRequestHandler::METHOD => background_request_task::<
requests::DocumentDiagnosticRequestHandler::METHOD => background_document_request_task::<
requests::DocumentDiagnosticRequestHandler,
>(
req, BackgroundSchedule::Worker
),
requests::GotoTypeDefinitionRequestHandler::METHOD => background_request_task::<
requests::GotoTypeDefinitionRequestHandler::METHOD => background_document_request_task::<
requests::GotoTypeDefinitionRequestHandler,
>(
req, BackgroundSchedule::Worker
),
requests::HoverRequestHandler::METHOD => background_request_task::<
requests::HoverRequestHandler::METHOD => background_document_request_task::<
requests::HoverRequestHandler,
>(req, BackgroundSchedule::Worker),
requests::InlayHintRequestHandler::METHOD => background_request_task::<
requests::InlayHintRequestHandler::METHOD => background_document_request_task::<
requests::InlayHintRequestHandler,
>(req, BackgroundSchedule::Worker),
requests::CompletionRequestHandler::METHOD => background_request_task::<
requests::CompletionRequestHandler::METHOD => background_document_request_task::<
requests::CompletionRequestHandler,
>(
req, BackgroundSchedule::LatencySensitive
@ -135,7 +135,52 @@ where
}))
}
fn background_request_task<R: traits::BackgroundDocumentRequestHandler>(
#[expect(dead_code)]
fn background_request_task<R: traits::BackgroundRequestHandler>(
req: server::Request,
schedule: BackgroundSchedule,
) -> Result<Task>
where
<<R as RequestHandler>::RequestType as Request>::Params: UnwindSafe,
{
let retry = R::RETRY_ON_CANCELLATION.then(|| req.clone());
let (id, params) = cast_request::<R>(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 snapshot = session.take_workspace_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::<R>(&id, client, result, retry) {
respond::<R>(&id, response, client);
}
})
}))
}
fn background_document_request_task<R: traits::BackgroundDocumentRequestHandler>(
req: server::Request,
schedule: BackgroundSchedule,
) -> Result<Task>
@ -168,7 +213,7 @@ where
};
let Some(snapshot) = session.take_snapshot(url) else {
tracing::warn!("Ignoring request because snapshot for path `{path:?}` doesn't exist.");
tracing::warn!("Ignoring request because snapshot for path `{path:?}` doesn't exist");
return Box::new(|_| {});
};
@ -209,7 +254,7 @@ fn request_result_to_response<R>(
request: Option<lsp_server::Request>,
) -> Option<Result<<<R as RequestHandler>::RequestType as Request>::Result>>
where
R: traits::BackgroundDocumentRequestHandler,
R: traits::RetriableRequestHandler,
{
match result {
Ok(response) => Some(response),

View file

@ -8,7 +8,9 @@ use ty_project::ProjectDatabase;
use crate::DocumentSnapshot;
use crate::document::PositionExt;
use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler};
use crate::server::api::traits::{
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
};
use crate::session::client::Client;
pub(crate) struct CompletionRequestHandler;
@ -18,8 +20,6 @@ impl RequestHandler for CompletionRequestHandler {
}
impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
const RETRY_ON_CANCELLATION: bool = true;
fn document_url(params: &CompletionParams) -> Cow<Url> {
Cow::Borrowed(&params.text_document_position.text_document.uri)
}
@ -65,3 +65,7 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
Ok(Some(response))
}
}
impl RetriableRequestHandler for CompletionRequestHandler {
const RETRY_ON_CANCELLATION: bool = true;
}

View file

@ -8,7 +8,9 @@ use lsp_types::{
use crate::server::Result;
use crate::server::api::diagnostics::{Diagnostics, compute_diagnostics};
use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler};
use crate::server::api::traits::{
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
};
use crate::session::DocumentSnapshot;
use crate::session::client::Client;
use ty_project::ProjectDatabase;
@ -43,7 +45,9 @@ impl BackgroundDocumentRequestHandler for DocumentDiagnosticRequestHandler {
}),
))
}
}
impl RetriableRequestHandler for DocumentDiagnosticRequestHandler {
fn salsa_cancellation_error() -> lsp_server::ResponseError {
lsp_server::ResponseError {
code: lsp_server::ErrorCode::ServerCancelled as i32,

View file

@ -8,7 +8,9 @@ use ty_project::ProjectDatabase;
use crate::DocumentSnapshot;
use crate::document::{PositionExt, ToLink};
use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler};
use crate::server::api::traits::{
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
};
use crate::session::client::Client;
pub(crate) struct GotoTypeDefinitionRequestHandler;
@ -70,3 +72,5 @@ impl BackgroundDocumentRequestHandler for GotoTypeDefinitionRequestHandler {
}
}
}
impl RetriableRequestHandler for GotoTypeDefinitionRequestHandler {}

View file

@ -2,7 +2,9 @@ use std::borrow::Cow;
use crate::DocumentSnapshot;
use crate::document::{PositionExt, ToRangeExt};
use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler};
use crate::server::api::traits::{
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
};
use crate::session::client::Client;
use lsp_types::request::HoverRequest;
use lsp_types::{HoverContents, HoverParams, MarkupContent, Url};
@ -73,3 +75,5 @@ impl BackgroundDocumentRequestHandler for HoverRequestHandler {
}))
}
}
impl RetriableRequestHandler for HoverRequestHandler {}

View file

@ -2,7 +2,9 @@ use std::borrow::Cow;
use crate::DocumentSnapshot;
use crate::document::{RangeExt, TextSizeExt};
use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler};
use crate::server::api::traits::{
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
};
use crate::session::client::Client;
use lsp_types::request::InlayHintRequest;
use lsp_types::{InlayHintParams, Url};
@ -64,3 +66,5 @@ impl BackgroundDocumentRequestHandler for InlayHintRequestHandler {
Ok(Some(inlay_hints))
}
}
impl RetriableRequestHandler for InlayHintRequestHandler {}

View file

@ -1,7 +1,7 @@
//! A stateful LSP implementation that calls into the ty API.
use crate::session::client::Client;
use crate::session::{DocumentSnapshot, Session};
use crate::session::{DocumentSnapshot, Session, WorkspaceSnapshot};
use lsp_types::notification::Notification as LSPNotification;
use lsp_types::request::Request;
@ -25,11 +25,24 @@ pub(super) trait SyncRequestHandler: RequestHandler {
) -> super::Result<<<Self as RequestHandler>::RequestType as Request>::Result>;
}
/// A request handler that can be run on a background thread.
pub(super) trait BackgroundDocumentRequestHandler: RequestHandler {
/// Whether this request be retried if it was cancelled due to a modification to the Salsa database.
pub(super) trait RetriableRequestHandler: RequestHandler {
/// Whether this request can be cancelled if the Salsa database is modified.
const RETRY_ON_CANCELLATION: bool = false;
/// The error to return if the request was cancelled due to a modification to the Salsa database.
fn salsa_cancellation_error() -> lsp_server::ResponseError {
lsp_server::ResponseError {
code: lsp_server::ErrorCode::ContentModified as i32,
message: "content modified".to_string(),
data: None,
}
}
}
/// A request handler that can be run on a background thread.
///
/// This handler is specific to requests that operate on a single document.
pub(super) trait BackgroundDocumentRequestHandler: RetriableRequestHandler {
fn document_url(
params: &<<Self as RequestHandler>::RequestType as Request>::Params,
) -> std::borrow::Cow<lsp_types::Url>;
@ -40,14 +53,15 @@ pub(super) trait BackgroundDocumentRequestHandler: RequestHandler {
client: &Client,
params: <<Self as RequestHandler>::RequestType as Request>::Params,
) -> super::Result<<<Self as RequestHandler>::RequestType as Request>::Result>;
}
fn salsa_cancellation_error() -> lsp_server::ResponseError {
lsp_server::ResponseError {
code: lsp_server::ErrorCode::ContentModified as i32,
message: "content modified".to_string(),
data: None,
}
}
/// A request handler that can be run on a background thread.
pub(super) trait BackgroundRequestHandler: RetriableRequestHandler {
fn run(
snapshot: WorkspaceSnapshot,
client: &Client,
params: <<Self as RequestHandler>::RequestType as Request>::Params,
) -> super::Result<<<Self as RequestHandler>::RequestType as Request>::Result>;
}
/// A supertrait for any server notification handler.

View file

@ -2,6 +2,7 @@
use std::collections::{BTreeMap, VecDeque};
use std::ops::{Deref, DerefMut};
use std::panic::AssertUnwindSafe;
use std::sync::Arc;
use anyhow::{Context, anyhow};
@ -223,6 +224,14 @@ impl Session {
self.index().key_from_url(url)
}
pub(crate) fn take_workspace_snapshot(&self) -> WorkspaceSnapshot {
WorkspaceSnapshot {
projects: AssertUnwindSafe(self.projects.values().cloned().collect()),
index: self.index.clone().unwrap(),
position_encoding: self.position_encoding,
}
}
pub(crate) fn initialize_workspaces(&mut self, workspace_settings: Vec<(Url, ClientOptions)>) {
assert!(!self.workspaces.all_initialized());
@ -453,6 +462,28 @@ impl DocumentSnapshot {
}
}
/// An immutable snapshot of the current state of [`Session`].
pub(crate) struct WorkspaceSnapshot {
projects: AssertUnwindSafe<Vec<ProjectDatabase>>,
index: Arc<index::Index>,
position_encoding: PositionEncoding,
}
#[expect(dead_code)]
impl WorkspaceSnapshot {
pub(crate) fn projects(&self) -> &[ProjectDatabase] {
&self.projects
}
pub(crate) fn index(&self) -> &index::Index {
&self.index
}
pub(crate) fn position_encoding(&self) -> PositionEncoding {
self.position_encoding
}
}
#[derive(Debug, Default)]
pub(crate) struct Workspaces {
workspaces: BTreeMap<Url, Workspace>,