1use tokio::sync::mpsc;
24use tower_lsp_server::Client;
25use tower_lsp_server::jsonrpc::Result;
26use tower_lsp_server::ls_types::{
27 ProgressParams, ProgressParamsValue, ProgressToken, WorkDoneProgress, WorkDoneProgressBegin,
28 WorkDoneProgressEnd, WorkDoneProgressReport,
29};
30
31const PROGRESS_CHANNEL_CAPACITY: usize = 8;
34
35#[derive(Clone)]
40pub struct ProgressSender {
41 tx: mpsc::Sender<ProgressUpdate>,
42 total: usize,
43}
44
45struct ProgressUpdate {
46 fetched: usize,
47 total: usize,
48}
49
50impl ProgressSender {
51 pub fn send(&self, fetched: usize) {
57 let _ = self.tx.try_send(ProgressUpdate {
58 fetched,
59 total: self.total,
60 });
61 }
62}
63
64pub struct RegistryProgress {
69 client: Client,
70 token: ProgressToken,
71 active: bool,
72 _consumer_handle: tokio::task::JoinHandle<()>,
75}
76
77impl RegistryProgress {
78 pub async fn start(
91 client: Client,
92 uri: &str,
93 total_deps: usize,
94 ) -> Result<(Self, ProgressSender)> {
95 let token = ProgressToken::String(format!("deps-fetch-{}", uri));
96
97 client
99 .send_request::<tower_lsp_server::ls_types::request::WorkDoneProgressCreate>(
100 tower_lsp_server::ls_types::WorkDoneProgressCreateParams {
101 token: token.clone(),
102 },
103 )
104 .await?;
105
106 client
108 .send_notification::<tower_lsp_server::ls_types::notification::Progress>(
109 ProgressParams {
110 token: token.clone(),
111 value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(
112 WorkDoneProgressBegin {
113 title: "Fetching package versions".to_string(),
114 message: Some(format!("Loading {} dependencies...", total_deps)),
115 cancellable: Some(false),
116 percentage: Some(0),
117 },
118 )),
119 },
120 )
121 .await;
122
123 let (tx, rx) = mpsc::channel(PROGRESS_CHANNEL_CAPACITY);
124
125 let consumer_client = client.clone();
127 let consumer_token = token.clone();
128 let consumer_handle = tokio::spawn(async move {
129 consume_progress_updates(rx, consumer_client, consumer_token).await;
130 });
131
132 let sender = ProgressSender {
133 tx,
134 total: total_deps,
135 };
136
137 Ok((
138 Self {
139 client,
140 token,
141 active: true,
142 _consumer_handle: consumer_handle,
143 },
144 sender,
145 ))
146 }
147
148 pub async fn end(mut self, success: bool) {
150 if !self.active {
151 return;
152 }
153
154 self.active = false;
155
156 self._consumer_handle.abort();
158
159 let message = if success {
160 "Package versions loaded"
161 } else {
162 "Failed to fetch some versions"
163 };
164
165 self.client
166 .send_notification::<tower_lsp_server::ls_types::notification::Progress>(
167 ProgressParams {
168 token: self.token.clone(),
169 value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(
170 WorkDoneProgressEnd {
171 message: Some(message.to_string()),
172 },
173 )),
174 },
175 )
176 .await;
177 }
178}
179
180async fn consume_progress_updates(
182 mut rx: mpsc::Receiver<ProgressUpdate>,
183 client: Client,
184 token: ProgressToken,
185) {
186 while let Some(update) = rx.recv().await {
187 let percentage = if update.total > 0 {
188 ((update.fetched as f64 / update.total as f64) * 100.0) as u32
189 } else {
190 0
191 };
192
193 client
194 .send_notification::<tower_lsp_server::ls_types::notification::Progress>(
195 ProgressParams {
196 token: token.clone(),
197 value: ProgressParamsValue::WorkDone(WorkDoneProgress::Report(
198 WorkDoneProgressReport {
199 message: Some(format!(
200 "Fetched {}/{} packages",
201 update.fetched, update.total
202 )),
203 percentage: Some(percentage),
204 cancellable: Some(false),
205 },
206 )),
207 },
208 )
209 .await;
210 }
211}
212
213impl Drop for RegistryProgress {
215 fn drop(&mut self) {
216 if self.active {
217 tracing::warn!(
218 token = ?self.token,
219 "RegistryProgress dropped without explicit end() - spawning cleanup"
220 );
221 self._consumer_handle.abort();
222 let client = self.client.clone();
223 let token = self.token.clone();
224 tokio::spawn(async move {
225 client
226 .send_notification::<tower_lsp_server::ls_types::notification::Progress>(
227 ProgressParams {
228 token,
229 value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(
230 WorkDoneProgressEnd { message: None },
231 )),
232 },
233 )
234 .await;
235 });
236 }
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 #[test]
243 fn test_progress_token_format() {
244 let uri = "file:///test/Cargo.toml";
245 let token = format!("deps-fetch-{}", uri);
246 assert_eq!(token, "deps-fetch-file:///test/Cargo.toml");
247 }
248
249 #[test]
250 fn test_percentage_calculation() {
251 let calculate = |fetched: usize, total: usize| -> u32 {
252 if total == 0 {
253 return 0;
254 }
255 ((fetched as f64 / total as f64) * 100.0) as u32
256 };
257
258 assert_eq!(calculate(0, 10), 0);
259 assert_eq!(calculate(5, 10), 50);
260 assert_eq!(calculate(10, 10), 100);
261 assert_eq!(calculate(7, 10), 70);
262 assert_eq!(calculate(0, 0), 0);
263 }
264
265 #[test]
266 fn test_progress_message_format() {
267 let format_message = |fetched: usize, total: usize| -> String {
268 format!("Fetched {}/{} packages", fetched, total)
269 };
270
271 assert_eq!(format_message(5, 10), "Fetched 5/10 packages");
272 assert_eq!(format_message(0, 15), "Fetched 0/15 packages");
273 assert_eq!(format_message(20, 20), "Fetched 20/20 packages");
274 }
275
276 #[tokio::test]
277 async fn test_progress_sender_try_send_on_closed_channel() {
278 use super::*;
279
280 let (tx, rx) = mpsc::channel(1);
281 let sender = ProgressSender { tx, total: 10 };
282
283 drop(rx);
285
286 sender.send(5);
288 }
289
290 #[tokio::test]
291 async fn test_progress_sender_try_send_on_full_channel() {
292 use super::*;
293
294 let (tx, _rx) = mpsc::channel(1);
295 let sender = ProgressSender { tx, total: 10 };
296
297 sender.send(1);
299 sender.send(2);
301 sender.send(3);
302 }
303}