diff --git a/src/lib.rs b/src/lib.rs index fe2c204..d8d14c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -82,6 +82,9 @@ pub extern crate lsp_types; /// A re-export of [`async-trait`](https://docs.rs/async-trait) for convenience. pub use async_trait::async_trait; +pub use self::service::progress::{ + Bounded, Cancellable, NotCancellable, OngoingProgress, Progress, Unbounded, +}; pub use self::service::{Client, ClientSocket, ExitedError, LspService, LspServiceBuilder}; pub use self::transport::{Loopback, Server}; diff --git a/src/service.rs b/src/service.rs index d15daba..9c3c759 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,6 +1,6 @@ //! Service abstraction for language servers. -pub use self::client::{Client, ClientSocket, RequestStream, ResponseSink}; +pub use self::client::{progress, Client, ClientSocket, RequestStream, ResponseSink}; pub(crate) use self::pending::Pending; pub(crate) use self::state::{ServerState, State}; diff --git a/src/service/client.rs b/src/service/client.rs index f3ccdee..dd7e843 100644 --- a/src/service/client.rs +++ b/src/service/client.rs @@ -10,8 +10,6 @@ use std::task::{Context, Poll}; use futures::channel::mpsc::{self, Sender}; use futures::future::BoxFuture; use futures::sink::SinkExt; -use lsp_types::notification::*; -use lsp_types::request::*; use lsp_types::*; use serde::Serialize; use serde_json::Value; @@ -19,10 +17,13 @@ use tower::Service; use tracing::{error, trace}; use self::pending::Pending; +use self::progress::Progress; use super::state::{ServerState, State}; use super::ExitedError; use crate::jsonrpc::{self, Error, ErrorCode, Id, Request, Response}; +pub mod progress; + mod pending; mod socket; @@ -94,6 +95,7 @@ impl Client { &self, registrations: Vec, ) -> jsonrpc::Result<()> { + use lsp_types::request::RegisterCapability; self.send_request::(RegistrationParams { registrations }) .await } @@ -114,6 +116,7 @@ impl Client { &self, unregisterations: Vec, ) -> jsonrpc::Result<()> { + use lsp_types::request::UnregisterCapability; self.send_request::(UnregistrationParams { unregisterations }) .await } @@ -126,6 +129,7 @@ impl Client { /// /// [`window/showMessage`]: https://microsoft.github.io/language-server-protocol/specification#window_showMessage pub async fn show_message(&self, typ: MessageType, message: M) { + use lsp_types::notification::ShowMessage; self.send_notification_unchecked::(ShowMessageParams { typ, message: message.to_string(), @@ -147,6 +151,7 @@ impl Client { message: M, actions: Option>, ) -> jsonrpc::Result> { + use lsp_types::request::ShowMessageRequest; self.send_request_unchecked::(ShowMessageRequestParams { typ, message: message.to_string(), @@ -161,6 +166,7 @@ impl Client { /// /// [`window/logMessage`]: https://microsoft.github.io/language-server-protocol/specification#window_logMessage pub async fn log_message(&self, typ: MessageType, message: M) { + use lsp_types::notification::LogMessage; self.send_notification_unchecked::(LogMessageParams { typ, message: message.to_string(), @@ -187,6 +193,7 @@ impl Client { /// /// This request was introduced in specification version 3.16.0. pub async fn show_document(&self, params: ShowDocumentParams) -> jsonrpc::Result { + use lsp_types::request::ShowDocument; let response = self.send_request::(params).await?; Ok(response.success) } @@ -200,6 +207,7 @@ impl Client { /// /// [`telemetry/event`]: https://microsoft.github.io/language-server-protocol/specification#telemetry_event pub async fn telemetry_event(&self, data: S) { + use lsp_types::notification::TelemetryEvent; match serde_json::to_value(data) { Err(e) => error!("invalid JSON in `telemetry/event` notification: {}", e), Ok(mut value) => { @@ -236,6 +244,7 @@ impl Client { /// /// This request was introduced in specification version 3.16.0. pub async fn code_lens_refresh(&self) -> jsonrpc::Result<()> { + use lsp_types::request::CodeLensRefresh; self.send_request::(()).await } @@ -262,6 +271,7 @@ impl Client { /// /// This request was introduced in specification version 3.16.0. pub async fn semantic_tokens_refresh(&self) -> jsonrpc::Result<()> { + use lsp_types::request::SemanticTokensRefresh; self.send_request::(()).await } @@ -287,6 +297,7 @@ impl Client { /// /// This request was introduced in specification version 3.17.0. pub async fn inline_value_refresh(&self) -> jsonrpc::Result<()> { + use lsp_types::request::InlineValueRefreshRequest; self.send_request::(()).await } @@ -312,6 +323,7 @@ impl Client { /// /// This request was introduced in specification version 3.17.0. pub async fn inlay_hint_refresh(&self) -> jsonrpc::Result<()> { + use lsp_types::request::InlayHintRefreshRequest; self.send_request::(()).await } @@ -353,6 +365,7 @@ impl Client { diags: Vec, version: Option, ) { + use lsp_types::notification::PublishDiagnostics; self.send_notification::(PublishDiagnosticsParams::new( uri, diags, version, )) @@ -386,6 +399,7 @@ impl Client { &self, items: Vec, ) -> jsonrpc::Result> { + use lsp_types::request::WorkspaceConfiguration; self.send_request::(ConfigurationParams { items }) .await } @@ -410,6 +424,7 @@ impl Client { /// /// This request was introduced in specification version 3.6.0. pub async fn workspace_folders(&self) -> jsonrpc::Result>> { + use lsp_types::request::WorkspaceFoldersRequest; self.send_request::(()).await } @@ -430,10 +445,59 @@ impl Client { &self, edit: WorkspaceEdit, ) -> jsonrpc::Result { + use lsp_types::request::ApplyWorkspaceEdit; self.send_request::(ApplyWorkspaceEditParams { edit, label: None }) .await } + /// Starts a stream of `$/progress` notifications for a client-provided [`ProgressToken`]. + /// + /// This method also takes a `title` argument briefly describing the kind of operation being + /// performed, e.g. "Indexing" or "Linking Dependencies". + /// + /// [`ProgressToken`]: https://docs.rs/lsp-types/latest/lsp_types/type.ProgressToken.html + /// + /// # Initialization + /// + /// These notifications will only be sent if the server is initialized. + /// + /// # Examples + /// + /// ```no_run + /// # use tower_lsp::{lsp_types::*, Client}; + /// # + /// # struct Mock { + /// # client: Client, + /// # } + /// # + /// # impl Mock { + /// # async fn completion(&self, params: CompletionParams) { + /// # let work_done_token = ProgressToken::Number(1); + /// # + /// let progress = self + /// .client + /// .progress(work_done_token, "Progress Title") + /// .with_message("Working...") + /// .with_percentage(0) + /// .begin() + /// .await; + /// + /// for percent in 1..=100 { + /// let msg = format!("Working... [{percent}/100]"); + /// progress.report_with_message(msg, percent).await; + /// } + /// + /// progress.finish_with_message("Done!").await; + /// # } + /// # } + /// ``` + pub fn progress(&self, token: ProgressToken, title: T) -> Progress + where + T: Into, + { + Progress::new(self.clone(), token, title.into()) + } + /// Sends a custom notification to the client. /// /// # Initialization @@ -563,6 +627,7 @@ mod tests { use std::future::Future; use futures::stream::StreamExt; + use lsp_types::notification::{LogMessage, PublishDiagnostics, ShowMessage, TelemetryEvent}; use serde_json::json; use super::*; diff --git a/src/service/client/progress.rs b/src/service/client/progress.rs new file mode 100644 index 0000000..ca8bea9 --- /dev/null +++ b/src/service/client/progress.rs @@ -0,0 +1,371 @@ +//! Types for emitting `$/progress` notifications to the client. + +use std::fmt::{self, Debug, Formatter}; +use std::marker::PhantomData; + +use lsp_types::{ + notification::Progress as ProgressNotification, ProgressParams, ProgressParamsValue, + ProgressToken, WorkDoneProgress, WorkDoneProgressBegin, WorkDoneProgressReport, +}; + +use super::Client; + +/// Indicates the progress stream is bounded from 0-100%. +#[doc(hidden)] +#[derive(Debug)] +pub enum Bounded {} + +/// Indicates the progress stream is unbounded. +#[doc(hidden)] +#[derive(Debug)] +pub enum Unbounded {} + +/// Indicates the progress stream may be canceled by the client. +#[doc(hidden)] +#[derive(Debug)] +pub enum Cancellable {} + +/// Indicates the progress stream cannot be canceled by the client. +#[doc(hidden)] +#[derive(Debug)] +pub enum NotCancellable {} + +/// A builder for a new `$/progress` stream. +/// +/// This progress stream is initially assumed to be _unbounded_ and _not cancellable_. +/// +/// This struct is created by [`Client::progress`]. See its documentation for more. +#[must_use = "progress is not reported until `.begin()` is called"] +pub struct Progress { + client: Client, + token: ProgressToken, + begin_msg: WorkDoneProgressBegin, + _kind: PhantomData<(B, C)>, +} + +impl Progress { + pub(crate) fn new(client: Client, token: ProgressToken, title: String) -> Self { + Progress { + client, + token, + begin_msg: WorkDoneProgressBegin { + title, + cancellable: Some(false), + message: None, + percentage: None, + }, + _kind: PhantomData, + } + } +} + +impl Progress { + /// Sets the optional progress percentage to display in the client UI. + /// + /// This percentage value is initially `start_percentage`, where a value of `100` for example + /// is considered 100% by the client. If this method is not called, unbounded progress is + /// assumed. + pub fn with_percentage(self, start_percentage: u32) -> Progress { + Progress { + client: self.client, + token: self.token, + begin_msg: WorkDoneProgressBegin { + percentage: Some(start_percentage), + ..self.begin_msg + }, + _kind: PhantomData, + } + } +} + +impl Progress { + /// Indicates that a "cancel" button should be displayed in the client UI. + /// + /// Clients that don’t support cancellation are allowed to ignore this setting. If this method + /// is not called, the user will not be presented with an option to cancel this operation. + pub fn with_cancel_button(self) -> Progress { + Progress { + client: self.client, + token: self.token, + begin_msg: WorkDoneProgressBegin { + cancellable: Some(true), + ..self.begin_msg + }, + _kind: PhantomData, + } + } +} + +impl Progress { + /// Includes an optional more detailed progress message. + /// + /// This message is expected to contain information complementary to the `title` string passed + /// into [`Client::progress`], such as `"3/25 files"`, `"project/src/module2"`, or + /// `"node_modules/some_dep"`. + pub fn with_message(mut self, message: M) -> Self + where + M: Into, + { + self.begin_msg.message = Some(message.into()); + self + } + + /// Starts reporting progress to the client, returning an [`OngoingProgress`] handle. + /// + /// # Initialization + /// + /// This notification will only be sent if the server is initialized. + pub async fn begin(self) -> OngoingProgress { + self.client + .send_notification::(ProgressParams { + token: self.token.clone(), + value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(self.begin_msg)), + }) + .await; + + OngoingProgress { + client: self.client, + token: self.token, + _kind: PhantomData, + } + } +} + +impl Debug for Progress { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.debug_struct(stringify!(Progress)) + .field("token", &self.token) + .field("properties", &self.begin_msg) + .finish() + } +} + +/// An ongoing stream of progress being reported to the client. +/// +/// This struct is created by [`Progress::begin`]. See its documentation for more. +#[must_use = "ongoing progress is not reported until `.report()` and/or `.finish()` is called"] +pub struct OngoingProgress { + client: Client, + token: ProgressToken, + _kind: PhantomData<(B, C)>, +} + +impl OngoingProgress { + async fn send_progress_report(&self, report: WorkDoneProgressReport) { + self.client + .send_notification::(ProgressParams { + token: self.token.clone(), + value: ProgressParamsValue::WorkDone(WorkDoneProgress::Report(report)), + }) + .await; + } +} + +impl OngoingProgress { + /// Updates the secondary progress message visible in the client UI. + /// + /// This message is expected to contain information complementary to the `title` string passed + /// into [`Client::progress`], such as `"3/25 files"`, `"project/src/module2"`, or + /// `"node_modules/some_dep"`. + /// + /// # Initialization + /// + /// This notification will only be sent if the server is initialized. + pub async fn report(&self, message: M) + where + M: Into, + { + self.send_progress_report(WorkDoneProgressReport { + message: Some(message.into()), + ..Default::default() + }) + .await; + } +} + +impl OngoingProgress { + /// Enables or disables the "cancel" button in the client UI. + /// + /// # Initialization + /// + /// This notification will only be sent if the server is initialized. + pub async fn report(&self, enable_cancel_btn: bool) { + self.send_progress_report(WorkDoneProgressReport { + cancellable: Some(enable_cancel_btn), + ..Default::default() + }) + .await; + } + + /// Updates the secondary progress message visible in the client UI and optionally + /// enables/disables the "cancel" button. + /// + /// This message is expected to contain information complementary to the `title` string passed + /// into [`Client::progress`], such as `"3/25 files"`, `"project/src/module2"`, or + /// `"node_modules/some_dep"`. + /// + /// If `enable_cancel_btn` is `None`, the state of the "cancel" button in the UI is unchanged. + /// + /// # Initialization + /// + /// This notification will only be sent if the server is initialized. + pub async fn report_with_message(&self, message: M, enable_cancel_btn: Option) + where + M: Into, + { + self.send_progress_report(WorkDoneProgressReport { + cancellable: enable_cancel_btn, + message: Some(message.into()), + ..Default::default() + }) + .await; + } +} + +impl OngoingProgress { + /// Updates the progress percentage displayed in the client UI, where a value of `100` for + /// example is considered 100% by the client. + /// + /// # Initialization + /// + /// This notification will only be sent if the server is initialized. + pub async fn report(&self, percentage: u32) { + self.send_progress_report(WorkDoneProgressReport { + percentage: Some(percentage), + ..Default::default() + }) + .await; + } + + /// Same as [`OngoingProgress::report`](OngoingProgress#method.report-2), except it also + /// displays an optional more detailed progress message. + /// + /// This message is expected to contain information complementary to the `title` string passed + /// into [`Client::progress`], such as `"3/25 files"`, `"project/src/module2"`, or + /// `"node_modules/some_dep"`. + /// + /// # Initialization + /// + /// This notification will only be sent if the server is initialized. + pub async fn report_with_message(&self, message: M, percentage: u32) + where + M: Into, + { + self.send_progress_report(WorkDoneProgressReport { + message: Some(message.into()), + percentage: Some(percentage), + ..Default::default() + }) + .await; + } +} + +impl OngoingProgress { + /// Updates the progress percentage displayed in the client UI, where a value of `100` for + /// example is considered 100% by the client. + /// + /// If `enable_cancel_btn` is `None`, the state of the "cancel" button in the UI is unchanged. + /// + /// # Initialization + /// + /// This notification will only be sent if the server is initialized. + pub async fn report(&self, percentage: u32, enable_cancel_btn: Option) { + self.send_progress_report(WorkDoneProgressReport { + cancellable: enable_cancel_btn, + message: None, + percentage: Some(percentage), + }) + .await; + } + + /// Same as [`OngoingProgress::report`](OngoingProgress#method.report-3), except it also + /// displays an optional more detailed progress message. + /// + /// This message is expected to contain information complementary to the `title` string passed + /// into [`Client::progress`], such as `"3/25 files"`, `"project/src/module2"`, or + /// `"node_modules/some_dep"`. + /// + /// # Initialization + /// + /// This notification will only be sent if the server is initialized. + pub async fn report_with_message( + &self, + message: M, + percentage: u32, + enable_cancel_btn: Option, + ) where + M: Into, + { + self.send_progress_report(WorkDoneProgressReport { + cancellable: enable_cancel_btn, + message: Some(message.into()), + percentage: Some(percentage), + }) + .await; + } +} + +impl OngoingProgress { + /// Discards the progress bound associated with this `OngoingProgress`. + /// + /// All subsequent progress reports will no longer show a percentage value. + pub fn into_unbounded(self) -> OngoingProgress { + OngoingProgress { + client: self.client, + token: self.token, + _kind: PhantomData, + } + } +} + +impl OngoingProgress { + /// Indicates this long-running operation is complete. + /// + /// # Initialization + /// + /// This notification will only be sent if the server is initialized. + pub async fn finish(self) { + self.finish_inner(None).await; + } + + /// Same as [`OngoingProgress::finish`], except it also displays an optional more detailed + /// progress message. + /// + /// This message is expected to contain information complementary to the `title` string passed + /// into [`Client::progress`], such as `"3/25 files"`, `"project/src/module2"`, or + /// `"node_modules/some_dep"`. + /// + /// # Initialization + /// + /// This notification will only be sent if the server is initialized. + pub async fn finish_with_message(self, message: M) + where + M: Into, + { + self.finish_inner(Some(message.into())).await; + } + + async fn finish_inner(self, message: Option) { + self.client + .send_notification::(ProgressParams { + token: self.token, + value: ProgressParamsValue::WorkDone(WorkDoneProgress::End( + lsp_types::WorkDoneProgressEnd { message }, + )), + }) + .await; + } + + /// Returns the `ProgressToken` associated with this long-running operation. + pub fn token(&self) -> &ProgressToken { + &self.token + } +} + +impl Debug for OngoingProgress { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.debug_struct(stringify!(OngoingProgress)) + .field("token", &self.token) + .finish() + } +}