-
Notifications
You must be signed in to change notification settings - Fork 72
Description
Problem statement
Currently, LspService
adopts a unary request/response model where <LspService as tower::Service>::call()
accepts a single Incoming
message and sends an Option<String>
response back.
The basic service definition looks like this:
impl tower::Service<Incoming> for LspService {
type Response = Option<String>;
type Error = ExitedError;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn call(&mut self, message: Incoming) -> Self::Future { ... }
}
This model is a poor fit for a Language Server Protocol implementation for several reasons:
- LSP is bidirectional, where the client and server can both send requests and responses to each other. The unary request/response model imposes a strong client-to-server ordering on the
LspService
when there really shouldn't be one, leading to hacks such asMessageStream
and wrapping each outgoing message in anOption
to get the behavior we want. - Client-to-server communication cannot be polled independently of server-to-client communication, since they are both being funneled through the
LspService::call()
method. This method must be called repeatedly in order to drive both independent streams of communication, which feels awkward to use and write tests for.
Proposal
For these reasons, I believe it would be better to adopt a bidirectional model structured like this:
impl tower::Service<MessageStream> for LspService {
type Response = MessageStream;
type Error = ExitedError;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn call(&mut self, stream: MessageStream) -> Self::Future { ... }
}
With the above model, the call()
method is only ever called once by the client, given a stream of incoming messages, producing a future which either resolves to Ok(MessageStream)
if all is well, or an Err(ExitedError)
if the language server has already shut down. Both message streams can then be polled independently from each other, in either direction, with no strict ordering. When the language server shuts down, the outgoing message stream will terminate and any subsequent attempt to call LspService::call()
to produce a new one will immediately return Err(ExitedError)
.
User impact
This change would have a minor impact on users which rely on tower_lsp::Server
to route communication over stdio
. It will result in a slightly different initialization process:
Before
let (service, messages) = LspService::new(Backend::default());
Server::new(stdin, stdout)
.interleave(messages)
.serve(service)
.await;
After
let service = LspService::new(Backend::default());
Server::new(stdin, stdout)
.serve(service)
.await;
This change would have a greater effect on users wrapping LspService
in tower
middleware in their own projects, since the service request and response types would have changed and would need to be handled differently.