tower_lsp_server/service/client/progress.rs
1//! Types for emitting `$/progress` notifications to the client.
2
3use std::fmt::{self, Debug, Formatter};
4use std::marker::PhantomData;
5
6use lsp_types::{
7 ProgressParams, ProgressParamsValue, ProgressToken, WorkDoneProgress, WorkDoneProgressBegin,
8 WorkDoneProgressReport, notification::Progress as ProgressNotification,
9};
10
11use super::Client;
12
13/// Indicates the progress stream is bounded from 0-100%.
14#[doc(hidden)]
15#[derive(Debug)]
16pub enum Bounded {}
17
18/// Indicates the progress stream is unbounded.
19#[doc(hidden)]
20#[derive(Debug)]
21pub enum Unbounded {}
22
23/// Indicates the progress stream may be canceled by the client.
24#[doc(hidden)]
25#[derive(Debug)]
26pub enum Cancellable {}
27
28/// Indicates the progress stream cannot be canceled by the client.
29#[doc(hidden)]
30#[derive(Debug)]
31pub enum NotCancellable {}
32
33/// A builder for a new `$/progress` stream.
34///
35/// This progress stream is initially assumed to be _unbounded_ and _not cancellable_.
36///
37/// This struct is created by [`Client::progress`]. See its documentation for more.
38#[must_use = "progress is not reported until `.begin()` is called"]
39pub struct Progress<B = Unbounded, C = NotCancellable> {
40 client: Client,
41 token: ProgressToken,
42 begin_msg: WorkDoneProgressBegin,
43 _kind: PhantomData<(B, C)>,
44}
45
46impl Progress {
47 pub(crate) const fn new(client: Client, token: ProgressToken, title: String) -> Self {
48 Self {
49 client,
50 token,
51 begin_msg: WorkDoneProgressBegin {
52 title,
53 cancellable: Some(false),
54 message: None,
55 percentage: None,
56 },
57 _kind: PhantomData,
58 }
59 }
60}
61
62impl<C> Progress<Unbounded, C> {
63 /// Sets the optional progress percentage to display in the client UI.
64 ///
65 /// This percentage value is initially `start_percentage`, where a value of `100` for example
66 /// is considered 100% by the client. If this method is not called, unbounded progress is
67 /// assumed.
68 pub fn with_percentage(self, start_percentage: u32) -> Progress<Bounded, C> {
69 Progress {
70 client: self.client,
71 token: self.token,
72 begin_msg: WorkDoneProgressBegin {
73 percentage: Some(start_percentage),
74 ..self.begin_msg
75 },
76 _kind: PhantomData,
77 }
78 }
79}
80
81impl<B> Progress<B, NotCancellable> {
82 /// Indicates that a "cancel" button should be displayed in the client UI.
83 ///
84 /// Clients that don’t support cancellation are allowed to ignore this setting. If this method
85 /// is not called, the user will not be presented with an option to cancel this operation.
86 pub fn with_cancel_button(self) -> Progress<B, Cancellable> {
87 Progress {
88 client: self.client,
89 token: self.token,
90 begin_msg: WorkDoneProgressBegin {
91 cancellable: Some(true),
92 ..self.begin_msg
93 },
94 _kind: PhantomData,
95 }
96 }
97}
98
99impl<B, C> Progress<B, C> {
100 /// Includes an optional more detailed progress message.
101 ///
102 /// This message is expected to contain information complementary to the `title` string passed
103 /// into [`Client::progress`], such as `"3/25 files"`, `"project/src/module2"`, or
104 /// `"node_modules/some_dep"`.
105 pub fn with_message<M>(mut self, message: M) -> Self
106 where
107 M: Into<String>,
108 {
109 self.begin_msg.message = Some(message.into());
110 self
111 }
112
113 /// Starts reporting progress to the client, returning an [`OngoingProgress`] handle.
114 ///
115 /// # Initialization
116 ///
117 /// This notification will only be sent if the server is initialized.
118 pub async fn begin(self) -> OngoingProgress<B, C> {
119 self.client
120 .send_notification::<ProgressNotification>(ProgressParams {
121 token: self.token.clone(),
122 value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(self.begin_msg)),
123 })
124 .await;
125
126 OngoingProgress {
127 client: self.client,
128 token: self.token,
129 _kind: PhantomData,
130 }
131 }
132}
133
134impl<B, C> Debug for Progress<B, C> {
135 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
136 f.debug_struct(stringify!(Progress))
137 .field("token", &self.token)
138 .field("properties", &self.begin_msg)
139 .finish_non_exhaustive()
140 }
141}
142
143/// An ongoing stream of progress being reported to the client.
144///
145/// This struct is created by [`Progress::begin`]. See its documentation for more.
146#[must_use = "ongoing progress is not reported until `.report()` and/or `.finish()` is called"]
147pub struct OngoingProgress<B, C> {
148 client: Client,
149 token: ProgressToken,
150 _kind: PhantomData<(B, C)>,
151}
152
153impl<B: Sync, C: Sync> OngoingProgress<B, C> {
154 async fn send_progress_report(&self, report: WorkDoneProgressReport) {
155 self.client
156 .send_notification::<ProgressNotification>(ProgressParams {
157 token: self.token.clone(),
158 value: ProgressParamsValue::WorkDone(WorkDoneProgress::Report(report)),
159 })
160 .await;
161 }
162}
163
164impl OngoingProgress<Unbounded, NotCancellable> {
165 /// Updates the secondary progress message visible in the client UI.
166 ///
167 /// This message is expected to contain information complementary to the `title` string passed
168 /// into [`Client::progress`], such as `"3/25 files"`, `"project/src/module2"`, or
169 /// `"node_modules/some_dep"`.
170 ///
171 /// # Initialization
172 ///
173 /// This notification will only be sent if the server is initialized.
174 pub async fn report<M>(&self, message: M)
175 where
176 M: Into<String>,
177 {
178 self.send_progress_report(WorkDoneProgressReport {
179 message: Some(message.into()),
180 ..Default::default()
181 })
182 .await;
183 }
184}
185
186impl OngoingProgress<Unbounded, Cancellable> {
187 /// Enables or disables the "cancel" button in the client UI.
188 ///
189 /// # Initialization
190 ///
191 /// This notification will only be sent if the server is initialized.
192 pub async fn report(&self, enable_cancel_btn: bool) {
193 self.send_progress_report(WorkDoneProgressReport {
194 cancellable: Some(enable_cancel_btn),
195 ..Default::default()
196 })
197 .await;
198 }
199
200 /// Updates the secondary progress message visible in the client UI and optionally
201 /// enables/disables the "cancel" button.
202 ///
203 /// This message is expected to contain information complementary to the `title` string passed
204 /// into [`Client::progress`], such as `"3/25 files"`, `"project/src/module2"`, or
205 /// `"node_modules/some_dep"`.
206 ///
207 /// If `enable_cancel_btn` is `None`, the state of the "cancel" button in the UI is unchanged.
208 ///
209 /// # Initialization
210 ///
211 /// This notification will only be sent if the server is initialized.
212 pub async fn report_with_message<M>(&self, message: M, enable_cancel_btn: Option<bool>)
213 where
214 M: Into<String>,
215 {
216 self.send_progress_report(WorkDoneProgressReport {
217 cancellable: enable_cancel_btn,
218 message: Some(message.into()),
219 ..Default::default()
220 })
221 .await;
222 }
223}
224
225impl OngoingProgress<Bounded, NotCancellable> {
226 /// Updates the progress percentage displayed in the client UI, where a value of `100` for
227 /// example is considered 100% by the client.
228 ///
229 /// # Initialization
230 ///
231 /// This notification will only be sent if the server is initialized.
232 pub async fn report(&self, percentage: u32) {
233 self.send_progress_report(WorkDoneProgressReport {
234 percentage: Some(percentage),
235 ..Default::default()
236 })
237 .await;
238 }
239
240 /// Same as [`OngoingProgress::report`](OngoingProgress#method.report-2), except it also
241 /// displays an optional more detailed progress message.
242 ///
243 /// This message is expected to contain information complementary to the `title` string passed
244 /// into [`Client::progress`], such as `"3/25 files"`, `"project/src/module2"`, or
245 /// `"node_modules/some_dep"`.
246 ///
247 /// # Initialization
248 ///
249 /// This notification will only be sent if the server is initialized.
250 pub async fn report_with_message<M>(&self, message: M, percentage: u32)
251 where
252 M: Into<String>,
253 {
254 self.send_progress_report(WorkDoneProgressReport {
255 message: Some(message.into()),
256 percentage: Some(percentage),
257 ..Default::default()
258 })
259 .await;
260 }
261}
262
263impl OngoingProgress<Bounded, Cancellable> {
264 /// Updates the progress percentage displayed in the client UI, where a value of `100` for
265 /// example is considered 100% by the client.
266 ///
267 /// If `enable_cancel_btn` is `None`, the state of the "cancel" button in the UI is unchanged.
268 ///
269 /// # Initialization
270 ///
271 /// This notification will only be sent if the server is initialized.
272 pub async fn report(&self, percentage: u32, enable_cancel_btn: Option<bool>) {
273 self.send_progress_report(WorkDoneProgressReport {
274 cancellable: enable_cancel_btn,
275 message: None,
276 percentage: Some(percentage),
277 })
278 .await;
279 }
280
281 /// Same as [`OngoingProgress::report`](OngoingProgress#method.report-3), except it also
282 /// displays an optional more detailed progress message.
283 ///
284 /// This message is expected to contain information complementary to the `title` string passed
285 /// into [`Client::progress`], such as `"3/25 files"`, `"project/src/module2"`, or
286 /// `"node_modules/some_dep"`.
287 ///
288 /// # Initialization
289 ///
290 /// This notification will only be sent if the server is initialized.
291 pub async fn report_with_message<M>(
292 &self,
293 message: M,
294 percentage: u32,
295 enable_cancel_btn: Option<bool>,
296 ) where
297 M: Into<String>,
298 {
299 self.send_progress_report(WorkDoneProgressReport {
300 cancellable: enable_cancel_btn,
301 message: Some(message.into()),
302 percentage: Some(percentage),
303 })
304 .await;
305 }
306}
307
308impl<C> OngoingProgress<Bounded, C> {
309 /// Discards the progress bound associated with this `OngoingProgress`.
310 ///
311 /// All subsequent progress reports will no longer show a percentage value.
312 pub fn into_unbounded(self) -> OngoingProgress<Unbounded, C> {
313 OngoingProgress {
314 client: self.client,
315 token: self.token,
316 _kind: PhantomData,
317 }
318 }
319}
320
321impl<B, C> OngoingProgress<B, C> {
322 /// Indicates this long-running operation is complete.
323 ///
324 /// # Initialization
325 ///
326 /// This notification will only be sent if the server is initialized.
327 pub async fn finish(self) {
328 self.finish_inner(None).await;
329 }
330
331 /// Same as [`OngoingProgress::finish`], except it also displays an optional more detailed
332 /// progress message.
333 ///
334 /// This message is expected to contain information complementary to the `title` string passed
335 /// into [`Client::progress`], such as `"3/25 files"`, `"project/src/module2"`, or
336 /// `"node_modules/some_dep"`.
337 ///
338 /// # Initialization
339 ///
340 /// This notification will only be sent if the server is initialized.
341 pub async fn finish_with_message<M>(self, message: M)
342 where
343 M: Into<String>,
344 {
345 self.finish_inner(Some(message.into())).await;
346 }
347
348 async fn finish_inner(self, message: Option<String>) {
349 self.client
350 .send_notification::<ProgressNotification>(ProgressParams {
351 token: self.token,
352 value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(
353 lsp_types::WorkDoneProgressEnd { message },
354 )),
355 })
356 .await;
357 }
358
359 /// Returns the `ProgressToken` associated with this long-running operation.
360 #[must_use]
361 pub const fn token(&self) -> &ProgressToken {
362 &self.token
363 }
364}
365
366impl<B, C> Debug for OngoingProgress<B, C> {
367 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
368 f.debug_struct(stringify!(OngoingProgress))
369 .field("token", &self.token)
370 .finish_non_exhaustive()
371 }
372}