Client request sender and inbound response handling for ruff server (#10620)

## Summary

Fixes #10618.

This PR introduces a proper API for sending requests to the client and
handling any response sent back. Dynamic capability registration now
uses this new API, fixing an issue where a much more simplistic response
handler silently flushes a code action request that needed a response.

## Test Plan

#10618 can no longer be reproduced. No errors about unhandled responses
should appear in the extension output, and you should see this new log
when the server starts:
```
<DATE> <TIME> [info] <DURATION> INFO ruff_server::server Configuration file watcher successfully registered
```
This commit is contained in:
Jane Lewis 2024-03-26 13:53:56 -07:00 committed by GitHub
parent 72aa1ce00f
commit 4d59142255
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 193 additions and 73 deletions

View file

@ -1,11 +1,19 @@
use std::any::TypeId;
use lsp_server::{Notification, RequestId};
use rustc_hash::FxHashMap;
use serde_json::Value;
use super::schedule::Task;
pub(crate) type ClientSender = crossbeam::channel::Sender<lsp_server::Message>;
pub(crate) struct Client {
type ResponseBuilder<'s> = Box<dyn FnOnce(lsp_server::Response) -> Task<'s>>;
pub(crate) struct Client<'s> {
notifier: Notifier,
responder: Responder,
pub(super) requester: Requester<'s>,
}
#[derive(Clone)]
@ -14,11 +22,22 @@ pub(crate) struct Notifier(ClientSender);
#[derive(Clone)]
pub(crate) struct Responder(ClientSender);
impl Client {
pub(crate) struct Requester<'s> {
sender: ClientSender,
next_request_id: i32,
response_handlers: FxHashMap<lsp_server::RequestId, ResponseBuilder<'s>>,
}
impl<'s> Client<'s> {
pub(super) fn new(sender: &ClientSender) -> Self {
Self {
notifier: Notifier(sender.clone()),
responder: Responder(sender.clone()),
requester: Requester {
sender: sender.clone(),
next_request_id: 1,
response_handlers: FxHashMap::default(),
},
}
}
@ -74,3 +93,80 @@ impl Responder {
)?)
}
}
impl<'s> Requester<'s> {
/// Sends a request of kind `R` to the client, with associated parameters.
/// The task provided by `response_handler` will be dispatched as soon as the response
/// comes back from the client.
pub(crate) fn request<R>(
&mut self,
params: R::Params,
response_handler: impl Fn(R::Result) -> Task<'s> + 'static,
) -> crate::Result<()>
where
R: lsp_types::request::Request,
{
let serialized_params = serde_json::to_value(params)?;
self.response_handlers.insert(
self.next_request_id.into(),
Box::new(move |response: lsp_server::Response| {
match (response.error, response.result) {
(Some(err), _) => {
tracing::error!(
"Got an error from the client (code {}): {}",
err.code,
err.message
);
Task::nothing()
}
(None, Some(response)) => match serde_json::from_value(response) {
Ok(response) => response_handler(response),
Err(error) => {
tracing::error!("Failed to deserialize response from server: {error}");
Task::nothing()
}
},
(None, None) => {
if TypeId::of::<R::Result>() == TypeId::of::<()>() {
// We can't call `response_handler(())` directly here, but
// since we _know_ the type expected is `()`, we can use
// `from_value(Value::Null)`. `R::Result` implements `DeserializeOwned`,
// so this branch works in the general case but we'll only
// hit it if the concrete type is `()`, so the `unwrap()` is safe here.
response_handler(serde_json::from_value(Value::Null).unwrap());
} else {
tracing::error!(
"Server response was invalid: did not contain a result or error"
);
}
Task::nothing()
}
}
}),
);
self.sender
.send(lsp_server::Message::Request(lsp_server::Request {
id: self.next_request_id.into(),
method: R::METHOD.into(),
params: serialized_params,
}))?;
self.next_request_id += 1;
Ok(())
}
pub(crate) fn pop_response_task(&mut self, response: lsp_server::Response) -> Task<'s> {
if let Some(handler) = self.response_handlers.remove(&response.id) {
handler(response)
} else {
tracing::error!(
"Received a response with ID {}, which was not expected",
response.id
);
Task::nothing()
}
}
}