ebkalderon / tower-lsp

Language Server Protocol implementation written in Rust
Apache License 2.0
951 stars 54 forks source link

Implement support for client initiated $/progress #380

Closed ebkalderon closed 6 months ago

ebkalderon commented 1 year ago

Background

This task was split from #176.

According to the relevant section in the specification, client initiated progress is defined as follows:

  1. The client includes a .work_done_progress_params.work_done_token field in the "params" given to the server.
  2. The server sends back $/progress begin/report/end notifications to the client using this work_done_token value.

Implementation

To support this type of $/progress, we should do the following:

Draft API

impl Client {
    pub fn progress<T>(&self, token: ProgressToken, title: T) -> Progress
    where
        T: Into<String>,
}

enum Bounded {}
enum Unbounded {}
enum Cancellable {}
enum NotCancellable {}

pub struct Progress<B = Unbounded, C = NotCancellable> { ... }

impl<C> Progress<Unbounded, C> {
    pub fn bounded(self, start_percentage: u32) -> Progress<Bounded, C>;
}

impl<B> Progress<B, NotCancellable> {
    pub fn cancellable(self, show_cancel_btn: bool) -> Progress<B, Cancellable>;
}

impl<B, C> Progress<B, C> {
    pub fn with_message<M>(self, message: M) -> Self
    where
        M: Into<String>;

    pub async fn begin(self) -> OngoingProgress<B, C>;
}

pub struct OngoingProgress<B, C> { ... }

impl OngoingProgress<Unbounded, NotCancellable> {
    pub async fn report<M>(&self, message: M)
    where
        M: Into<Option<String>>;
}

impl OngoingProgress<Unbounded, Cancellable> {
    pub async fn report<M>(&self, message: M, show_cancel_btn: bool)
    where
        M: Into<Option<String>>;
}

impl OngoingProgress<Bounded, NotCancellable> {
    pub async fn report<M>(&self, percentage: u32, message: M)
    where
        M: Into<Option<String>>;
}

impl OngoingProgress<Bounded, Cancellable> {
    pub async fn report<M>(&self, percentage: u32, message: M, show_cancel_btn: bool)
    where
        M: Into<Option<String>>;
}

impl<C> OngoingProgress<Bounded, C> {
    // This is explicitly allowed by the official specification. Clients will treat all
    // subsequent calls to `report()` as unbounded progress.
    pub fn discard_bound(self) -> OngoingProgress<Unbounded, C>;
}

impl<B, C> OngoingProgress<B, C> {
    pub fn token(&self) -> &ProgressToken;

    pub async fn finish<M>(self, message: M)
    where
        M: Into<Option<String>>;
}

Usage

impl LanguageServer for MyServer {
    async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
        let token = params.work_done_progress_params.work_done_token.unwrap();

        let progress = self
            .client
            .progress(token, "retrieving completions")
            .bounded(0)
            .with_message("starting work...")
            .begin()
            .await;

        for x in something {
            // Do some work...
            let percentage = ...
            progress.report(percentage, "reticulating splines".to_owned()).await;
        }

        progress.finish("done!".to_owned()).await;

        // ...
    }

    // ...
}

Other Ideas

ebkalderon commented 1 year ago

I've opened #385 implementing a streamlined version of the API above. Public feedback and testing is most welcome! :heart: