Skip to content

Commit a95c18a

Browse files
authored
[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.
1 parent e212dc2 commit a95c18a

File tree

8 files changed

+136
-26
lines changed

8 files changed

+136
-26
lines changed

crates/ty_server/src/server/api.rs

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,23 +28,23 @@ pub(super) fn request(req: server::Request) -> Task {
2828
let id = req.id.clone();
2929

3030
match req.method.as_str() {
31-
requests::DocumentDiagnosticRequestHandler::METHOD => background_request_task::<
31+
requests::DocumentDiagnosticRequestHandler::METHOD => background_document_request_task::<
3232
requests::DocumentDiagnosticRequestHandler,
3333
>(
3434
req, BackgroundSchedule::Worker
3535
),
36-
requests::GotoTypeDefinitionRequestHandler::METHOD => background_request_task::<
36+
requests::GotoTypeDefinitionRequestHandler::METHOD => background_document_request_task::<
3737
requests::GotoTypeDefinitionRequestHandler,
3838
>(
3939
req, BackgroundSchedule::Worker
4040
),
41-
requests::HoverRequestHandler::METHOD => background_request_task::<
41+
requests::HoverRequestHandler::METHOD => background_document_request_task::<
4242
requests::HoverRequestHandler,
4343
>(req, BackgroundSchedule::Worker),
44-
requests::InlayHintRequestHandler::METHOD => background_request_task::<
44+
requests::InlayHintRequestHandler::METHOD => background_document_request_task::<
4545
requests::InlayHintRequestHandler,
4646
>(req, BackgroundSchedule::Worker),
47-
requests::CompletionRequestHandler::METHOD => background_request_task::<
47+
requests::CompletionRequestHandler::METHOD => background_document_request_task::<
4848
requests::CompletionRequestHandler,
4949
>(
5050
req, BackgroundSchedule::LatencySensitive
@@ -135,7 +135,52 @@ where
135135
}))
136136
}
137137

138-
fn background_request_task<R: traits::BackgroundDocumentRequestHandler>(
138+
#[expect(dead_code)]
139+
fn background_request_task<R: traits::BackgroundRequestHandler>(
140+
req: server::Request,
141+
schedule: BackgroundSchedule,
142+
) -> Result<Task>
143+
where
144+
<<R as RequestHandler>::RequestType as Request>::Params: UnwindSafe,
145+
{
146+
let retry = R::RETRY_ON_CANCELLATION.then(|| req.clone());
147+
let (id, params) = cast_request::<R>(req)?;
148+
149+
Ok(Task::background(schedule, move |session: &Session| {
150+
let cancellation_token = session
151+
.request_queue()
152+
.incoming()
153+
.cancellation_token(&id)
154+
.expect("request should have been tested for cancellation before scheduling");
155+
156+
let snapshot = session.take_workspace_snapshot();
157+
158+
Box::new(move |client| {
159+
let _span = tracing::debug_span!("request", %id, method = R::METHOD).entered();
160+
161+
// Test again if the request was cancelled since it was scheduled on the background task
162+
// and, if so, return early
163+
if cancellation_token.is_cancelled() {
164+
tracing::trace!(
165+
"Ignoring request id={id} method={} because it was cancelled",
166+
R::METHOD
167+
);
168+
169+
// We don't need to send a response here because the `cancel` notification
170+
// handler already responded with a message.
171+
return;
172+
}
173+
174+
let result = ruff_db::panic::catch_unwind(|| R::run(snapshot, client, params));
175+
176+
if let Some(response) = request_result_to_response::<R>(&id, client, result, retry) {
177+
respond::<R>(&id, response, client);
178+
}
179+
})
180+
}))
181+
}
182+
183+
fn background_document_request_task<R: traits::BackgroundDocumentRequestHandler>(
139184
req: server::Request,
140185
schedule: BackgroundSchedule,
141186
) -> Result<Task>
@@ -168,7 +213,7 @@ where
168213
};
169214

170215
let Some(snapshot) = session.take_snapshot(url) else {
171-
tracing::warn!("Ignoring request because snapshot for path `{path:?}` doesn't exist.");
216+
tracing::warn!("Ignoring request because snapshot for path `{path:?}` doesn't exist");
172217
return Box::new(|_| {});
173218
};
174219

@@ -209,7 +254,7 @@ fn request_result_to_response<R>(
209254
request: Option<lsp_server::Request>,
210255
) -> Option<Result<<<R as RequestHandler>::RequestType as Request>::Result>>
211256
where
212-
R: traits::BackgroundDocumentRequestHandler,
257+
R: traits::RetriableRequestHandler,
213258
{
214259
match result {
215260
Ok(response) => Some(response),

crates/ty_server/src/server/api/requests/completion.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ use ty_project::ProjectDatabase;
88

99
use crate::DocumentSnapshot;
1010
use crate::document::PositionExt;
11-
use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler};
11+
use crate::server::api::traits::{
12+
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
13+
};
1214
use crate::session::client::Client;
1315

1416
pub(crate) struct CompletionRequestHandler;
@@ -18,8 +20,6 @@ impl RequestHandler for CompletionRequestHandler {
1820
}
1921

2022
impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
21-
const RETRY_ON_CANCELLATION: bool = true;
22-
2323
fn document_url(params: &CompletionParams) -> Cow<Url> {
2424
Cow::Borrowed(&params.text_document_position.text_document.uri)
2525
}
@@ -65,3 +65,7 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
6565
Ok(Some(response))
6666
}
6767
}
68+
69+
impl RetriableRequestHandler for CompletionRequestHandler {
70+
const RETRY_ON_CANCELLATION: bool = true;
71+
}

crates/ty_server/src/server/api/requests/diagnostic.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ use lsp_types::{
88

99
use crate::server::Result;
1010
use crate::server::api::diagnostics::{Diagnostics, compute_diagnostics};
11-
use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler};
11+
use crate::server::api::traits::{
12+
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
13+
};
1214
use crate::session::DocumentSnapshot;
1315
use crate::session::client::Client;
1416
use ty_project::ProjectDatabase;
@@ -43,7 +45,9 @@ impl BackgroundDocumentRequestHandler for DocumentDiagnosticRequestHandler {
4345
}),
4446
))
4547
}
48+
}
4649

50+
impl RetriableRequestHandler for DocumentDiagnosticRequestHandler {
4751
fn salsa_cancellation_error() -> lsp_server::ResponseError {
4852
lsp_server::ResponseError {
4953
code: lsp_server::ErrorCode::ServerCancelled as i32,

crates/ty_server/src/server/api/requests/goto_type_definition.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ use ty_project::ProjectDatabase;
88

99
use crate::DocumentSnapshot;
1010
use crate::document::{PositionExt, ToLink};
11-
use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler};
11+
use crate::server::api::traits::{
12+
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
13+
};
1214
use crate::session::client::Client;
1315

1416
pub(crate) struct GotoTypeDefinitionRequestHandler;
@@ -70,3 +72,5 @@ impl BackgroundDocumentRequestHandler for GotoTypeDefinitionRequestHandler {
7072
}
7173
}
7274
}
75+
76+
impl RetriableRequestHandler for GotoTypeDefinitionRequestHandler {}

crates/ty_server/src/server/api/requests/hover.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ use std::borrow::Cow;
22

33
use crate::DocumentSnapshot;
44
use crate::document::{PositionExt, ToRangeExt};
5-
use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler};
5+
use crate::server::api::traits::{
6+
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
7+
};
68
use crate::session::client::Client;
79
use lsp_types::request::HoverRequest;
810
use lsp_types::{HoverContents, HoverParams, MarkupContent, Url};
@@ -73,3 +75,5 @@ impl BackgroundDocumentRequestHandler for HoverRequestHandler {
7375
}))
7476
}
7577
}
78+
79+
impl RetriableRequestHandler for HoverRequestHandler {}

crates/ty_server/src/server/api/requests/inlay_hints.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ use std::borrow::Cow;
22

33
use crate::DocumentSnapshot;
44
use crate::document::{RangeExt, TextSizeExt};
5-
use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler};
5+
use crate::server::api::traits::{
6+
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
7+
};
68
use crate::session::client::Client;
79
use lsp_types::request::InlayHintRequest;
810
use lsp_types::{InlayHintParams, Url};
@@ -64,3 +66,5 @@ impl BackgroundDocumentRequestHandler for InlayHintRequestHandler {
6466
Ok(Some(inlay_hints))
6567
}
6668
}
69+
70+
impl RetriableRequestHandler for InlayHintRequestHandler {}

crates/ty_server/src/server/api/traits.rs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! A stateful LSP implementation that calls into the ty API.
22
33
use crate::session::client::Client;
4-
use crate::session::{DocumentSnapshot, Session};
4+
use crate::session::{DocumentSnapshot, Session, WorkspaceSnapshot};
55

66
use lsp_types::notification::Notification as LSPNotification;
77
use lsp_types::request::Request;
@@ -25,11 +25,24 @@ pub(super) trait SyncRequestHandler: RequestHandler {
2525
) -> super::Result<<<Self as RequestHandler>::RequestType as Request>::Result>;
2626
}
2727

28-
/// A request handler that can be run on a background thread.
29-
pub(super) trait BackgroundDocumentRequestHandler: RequestHandler {
30-
/// Whether this request be retried if it was cancelled due to a modification to the Salsa database.
28+
pub(super) trait RetriableRequestHandler: RequestHandler {
29+
/// Whether this request can be cancelled if the Salsa database is modified.
3130
const RETRY_ON_CANCELLATION: bool = false;
3231

32+
/// The error to return if the request was cancelled due to a modification to the Salsa database.
33+
fn salsa_cancellation_error() -> lsp_server::ResponseError {
34+
lsp_server::ResponseError {
35+
code: lsp_server::ErrorCode::ContentModified as i32,
36+
message: "content modified".to_string(),
37+
data: None,
38+
}
39+
}
40+
}
41+
42+
/// A request handler that can be run on a background thread.
43+
///
44+
/// This handler is specific to requests that operate on a single document.
45+
pub(super) trait BackgroundDocumentRequestHandler: RetriableRequestHandler {
3346
fn document_url(
3447
params: &<<Self as RequestHandler>::RequestType as Request>::Params,
3548
) -> std::borrow::Cow<lsp_types::Url>;
@@ -40,14 +53,15 @@ pub(super) trait BackgroundDocumentRequestHandler: RequestHandler {
4053
client: &Client,
4154
params: <<Self as RequestHandler>::RequestType as Request>::Params,
4255
) -> super::Result<<<Self as RequestHandler>::RequestType as Request>::Result>;
56+
}
4357

44-
fn salsa_cancellation_error() -> lsp_server::ResponseError {
45-
lsp_server::ResponseError {
46-
code: lsp_server::ErrorCode::ContentModified as i32,
47-
message: "content modified".to_string(),
48-
data: None,
49-
}
50-
}
58+
/// A request handler that can be run on a background thread.
59+
pub(super) trait BackgroundRequestHandler: RetriableRequestHandler {
60+
fn run(
61+
snapshot: WorkspaceSnapshot,
62+
client: &Client,
63+
params: <<Self as RequestHandler>::RequestType as Request>::Params,
64+
) -> super::Result<<<Self as RequestHandler>::RequestType as Request>::Result>;
5165
}
5266

5367
/// A supertrait for any server notification handler.

crates/ty_server/src/session.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
use std::collections::{BTreeMap, VecDeque};
44
use std::ops::{Deref, DerefMut};
5+
use std::panic::AssertUnwindSafe;
56
use std::sync::Arc;
67

78
use anyhow::{Context, anyhow};
@@ -223,6 +224,14 @@ impl Session {
223224
self.index().key_from_url(url)
224225
}
225226

227+
pub(crate) fn take_workspace_snapshot(&self) -> WorkspaceSnapshot {
228+
WorkspaceSnapshot {
229+
projects: AssertUnwindSafe(self.projects.values().cloned().collect()),
230+
index: self.index.clone().unwrap(),
231+
position_encoding: self.position_encoding,
232+
}
233+
}
234+
226235
pub(crate) fn initialize_workspaces(&mut self, workspace_settings: Vec<(Url, ClientOptions)>) {
227236
assert!(!self.workspaces.all_initialized());
228237

@@ -453,6 +462,28 @@ impl DocumentSnapshot {
453462
}
454463
}
455464

465+
/// An immutable snapshot of the current state of [`Session`].
466+
pub(crate) struct WorkspaceSnapshot {
467+
projects: AssertUnwindSafe<Vec<ProjectDatabase>>,
468+
index: Arc<index::Index>,
469+
position_encoding: PositionEncoding,
470+
}
471+
472+
#[expect(dead_code)]
473+
impl WorkspaceSnapshot {
474+
pub(crate) fn projects(&self) -> &[ProjectDatabase] {
475+
&self.projects
476+
}
477+
478+
pub(crate) fn index(&self) -> &index::Index {
479+
&self.index
480+
}
481+
482+
pub(crate) fn position_encoding(&self) -> PositionEncoding {
483+
self.position_encoding
484+
}
485+
}
486+
456487
#[derive(Debug, Default)]
457488
pub(crate) struct Workspaces {
458489
workspaces: BTreeMap<Url, Workspace>,

0 commit comments

Comments
 (0)