Skip to content

Commit 038c831

Browse files
committed
[ty] Request cancellation and retry
1 parent 28c3a27 commit 038c831

28 files changed

+883
-545
lines changed

crates/ty_server/src/client.rs

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
use crate::Session;
2+
use crate::server::{Action, ConnectionSender};
3+
use crate::server::{Event, MainLoopSender};
4+
use anyhow::{Context, anyhow};
5+
use lsp_server::{Message, Notification, RequestId};
6+
use serde_json::Value;
7+
use std::any::TypeId;
8+
use std::fmt::Display;
9+
10+
pub(crate) type ClientResponseHandler = Box<dyn FnOnce(&Session, lsp_server::Response) + Send>;
11+
12+
#[derive(Debug)]
13+
pub(crate) struct Client {
14+
/// Channel to send messages back to the main loop.
15+
main_loop_sender: MainLoopSender,
16+
/// Channel to send messages directly to the LSP client without going through the main loop.
17+
///
18+
/// This is generally preferred because it reduces pressure on the main loop but it may not always be
19+
/// possible if access to data on [`Session`] is required, which background tasks don't have.
20+
client_sender: ConnectionSender,
21+
}
22+
23+
impl Client {
24+
pub(crate) fn new(main_loop_sender: MainLoopSender, client_sender: ConnectionSender) -> Self {
25+
Self {
26+
main_loop_sender,
27+
client_sender,
28+
}
29+
}
30+
31+
/// Sends a request of kind `R` to the client, with associated parameters.
32+
///
33+
/// The request is sent immediately.
34+
/// The `response_handler` will be dispatched as soon as the client response
35+
/// is processed on the main-loop. The handler always runs on the main-loop thread.
36+
///
37+
/// # Note
38+
/// This method takes a `session` so that we can register the pending-request
39+
/// and send the response directly to the client. If this ever becomes too limiting (because we
40+
/// need to send a request from somewhere where we don't have access to session), consider introducing
41+
/// a new `send_deferred_request` method that doesn't take a session and instead sends
42+
/// an `Action` to the main loop to send the request (the main loop has always access to session).
43+
pub(crate) fn send_request<R>(
44+
&self,
45+
session: &Session,
46+
params: R::Params,
47+
response_handler: impl FnOnce(&Session, R::Result) + Send + 'static,
48+
) -> crate::Result<()>
49+
where
50+
R: lsp_types::request::Request,
51+
{
52+
let response_handler =
53+
Box::new(move |session: &Session, response: lsp_server::Response| {
54+
match (response.error, response.result) {
55+
(Some(err), _) => {
56+
tracing::error!(
57+
"Got an error from the client (code {}): {}",
58+
err.code,
59+
err.message
60+
);
61+
}
62+
(None, Some(response)) => match serde_json::from_value(response) {
63+
Ok(response) => response_handler(session, response),
64+
Err(error) => {
65+
tracing::error!("Failed to deserialize response from server: {error}");
66+
}
67+
},
68+
(None, None) => {
69+
if TypeId::of::<R::Result>() == TypeId::of::<()>() {
70+
// We can't call `response_handler(())` directly here, but
71+
// since we _know_ the type expected is `()`, we can use
72+
// `from_value(Value::Null)`. `R::Result` implements `DeserializeOwned`,
73+
// so this branch works in the general case but we'll only
74+
// hit it if the concrete type is `()`, so the `unwrap()` is safe here.
75+
response_handler(session, serde_json::from_value(Value::Null).unwrap());
76+
} else {
77+
tracing::error!(
78+
"Server response was invalid: did not contain a result or error"
79+
);
80+
}
81+
}
82+
}
83+
});
84+
85+
let id = session
86+
.request_queue()
87+
.outgoing()
88+
.register(response_handler);
89+
90+
self.client_sender
91+
.send(Message::Request(lsp_server::Request {
92+
id,
93+
method: R::METHOD.to_string(),
94+
params: serde_json::to_value(params).context("Failed to serialize params")?,
95+
}))?;
96+
97+
Ok(())
98+
}
99+
100+
/// Sends a notification to the client.
101+
pub(crate) fn send_notification<N>(&self, params: N::Params) -> crate::Result<()>
102+
where
103+
N: lsp_types::notification::Notification,
104+
{
105+
let method = N::METHOD.to_string();
106+
107+
self.client_sender
108+
.send(lsp_server::Message::Notification(Notification::new(
109+
method, params,
110+
)))
111+
.map_err(|error| anyhow!("Failed to send notification: {error}"))
112+
}
113+
114+
/// Sends a notification without any parameters to the client.
115+
///
116+
/// This is useful for notifications that don't require any data.
117+
#[expect(dead_code)]
118+
pub(crate) fn send_notification_no_params(&self, method: String) -> crate::Result<()> {
119+
self.client_sender
120+
.send(lsp_server::Message::Notification(Notification::new(
121+
method,
122+
Value::Null,
123+
)))
124+
.map_err(|error| anyhow!("Failed to send notification: {error}"))
125+
}
126+
127+
/// Sends a response to the client for a given request ID.
128+
///
129+
/// The response isn't sent immediately. Instead, it's queued up in the main loop
130+
/// and checked for cancellation (each request must have exactly one response).
131+
pub(crate) fn respond<R>(
132+
&self,
133+
id: RequestId,
134+
result: crate::server::Result<R>,
135+
) -> crate::Result<()>
136+
where
137+
R: serde::Serialize,
138+
{
139+
let response = match result {
140+
Ok(res) => lsp_server::Response::new_ok(id, res),
141+
Err(crate::server::Error { code, error }) => {
142+
lsp_server::Response::new_err(id, code as i32, error.to_string())
143+
}
144+
};
145+
146+
self.main_loop_sender
147+
.send(Event::Action(Action::SendResponse(response)))
148+
.map_err(|error| anyhow!("Failed to send response: {error}"))
149+
}
150+
151+
/// Sends an error response to the client for a given request ID.
152+
///
153+
/// The response isn't sent immediately. Instead, it's queued up in the main loop.
154+
pub(crate) fn respond_err(
155+
&self,
156+
id: RequestId,
157+
error: lsp_server::ResponseError,
158+
) -> crate::Result<()> {
159+
let response = lsp_server::Response::new_err(id, error.code, error.message);
160+
161+
self.main_loop_sender
162+
.send(Event::Action(Action::SendResponse(response)))
163+
.map_err(|error| anyhow!("Failed to send response: {error}"))
164+
}
165+
166+
/// Shows a message to the user.
167+
///
168+
/// This opens a pop up in VS Code showing `message`.
169+
pub(crate) fn show_message(
170+
&self,
171+
message: impl Display,
172+
message_type: lsp_types::MessageType,
173+
) -> crate::Result<()> {
174+
self.send_notification::<lsp_types::notification::ShowMessage>(
175+
lsp_types::ShowMessageParams {
176+
typ: message_type,
177+
message: message.to_string(),
178+
},
179+
)
180+
}
181+
182+
/// Re-queues this request after a salsa cancellation for a retry.
183+
///
184+
/// The main loop will skip the retry if the client cancelled the request in the meantime.
185+
pub(crate) fn retry(&self, request: lsp_server::Request) -> crate::Result<()> {
186+
self.main_loop_sender
187+
.send(Event::Action(Action::RetryRequest(request)))
188+
.map_err(|error| anyhow!("Failed to send retry request: {error}"))
189+
}
190+
}
191+
192+
/// Sends an error to the client with a formatted message. The error is sent in a
193+
/// `window/showMessage` notification.
194+
#[macro_export]
195+
macro_rules! show_err_msg {
196+
($client:expr, $msg:expr$(, $($arg:tt)*)?) => {{
197+
let result = $client.show_message(::core::format_args!($msg$(, $($arg)*)?), lsp_types::MessageType::ERROR);
198+
199+
if let Err(err) = result {
200+
tracing::error!("Failed to send error message to client: {err}");
201+
}
202+
}};
203+
}

crates/ty_server/src/lib.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
use crate::server::Server;
1+
use crate::server::{ConnectionInitializer, Server};
22
use anyhow::Context;
33
pub use document::{DocumentKey, NotebookDocument, PositionEncoding, TextDocument};
44
pub use session::{ClientSettings, DocumentQuery, DocumentSnapshot, Session};
55
use std::num::NonZeroUsize;
66

7-
#[macro_use]
8-
mod message;
9-
7+
mod client;
108
mod document;
119
mod logging;
1210
mod server;
@@ -30,11 +28,20 @@ pub fn run_server() -> anyhow::Result<()> {
3028
// by default, we set the number of worker threads to `num_cpus`, with a maximum of 4.
3129
let worker_threads = std::thread::available_parallelism()
3230
.unwrap_or(four)
33-
.min(four);
31+
.max(four);
32+
33+
let (connection, io_threads) = ConnectionInitializer::stdio();
3434

35-
Server::new(worker_threads)
35+
let server_result = Server::new(worker_threads, connection)
3636
.context("Failed to start server")?
37-
.run()?;
37+
.run();
38+
39+
let io_result = io_threads.join();
3840

39-
Ok(())
41+
match (server_result, io_result) {
42+
(Ok(()), Ok(())) => Ok(()),
43+
(Err(server), Err(io)) => Err(server).context(format!("IO thread error: {io}")),
44+
(Err(server), _) => Err(server),
45+
(_, Err(io)) => Err(io).context("IO thread error"),
46+
}
4047
}

crates/ty_server/src/message.rs

Lines changed: 0 additions & 46 deletions
This file was deleted.

0 commit comments

Comments
 (0)