Skip to content

Remodel service as bidirectional streamΒ #177

@ebkalderon

Description

@ebkalderon

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:

  1. 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 as MessageStream and wrapping each outgoing message in an Option to get the behavior we want.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions