1use tower_lsp_server::Client;
20use tower_lsp_server::jsonrpc::Result;
21use tower_lsp_server::ls_types::{
22 ProgressParams, ProgressParamsValue, ProgressToken, WorkDoneProgress, WorkDoneProgressBegin,
23 WorkDoneProgressEnd, WorkDoneProgressReport,
24};
25
26pub struct RegistryProgress {
31 client: Client,
32 token: ProgressToken,
33 active: bool,
34}
35
36impl RegistryProgress {
37 pub async fn start(client: Client, uri: &str, total_deps: usize) -> Result<Self> {
56 let token = ProgressToken::String(format!("deps-fetch-{}", uri));
57
58 client
60 .send_request::<tower_lsp_server::ls_types::request::WorkDoneProgressCreate>(
61 tower_lsp_server::ls_types::WorkDoneProgressCreateParams {
62 token: token.clone(),
63 },
64 )
65 .await?;
66
67 client
69 .send_notification::<tower_lsp_server::ls_types::notification::Progress>(
70 ProgressParams {
71 token: token.clone(),
72 value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(
73 WorkDoneProgressBegin {
74 title: "Fetching package versions".to_string(),
75 message: Some(format!("Loading {} dependencies...", total_deps)),
76 cancellable: Some(false),
77 percentage: Some(0),
78 },
79 )),
80 },
81 )
82 .await;
83
84 Ok(Self {
85 client,
86 token,
87 active: true,
88 })
89 }
90
91 pub async fn update(&self, fetched: usize, total: usize) {
103 if !self.active || total == 0 {
104 return;
105 }
106
107 let percentage = ((fetched as f64 / total as f64) * 100.0) as u32;
108 self.client
109 .send_notification::<tower_lsp_server::ls_types::notification::Progress>(
110 ProgressParams {
111 token: self.token.clone(),
112 value: ProgressParamsValue::WorkDone(WorkDoneProgress::Report(
113 WorkDoneProgressReport {
114 message: Some(format!("Fetched {}/{} packages", fetched, total)),
115 percentage: Some(percentage),
116 cancellable: Some(false),
117 },
118 )),
119 },
120 )
121 .await;
122 }
123
124 pub async fn end(mut self, success: bool) {
130 if !self.active {
131 return;
132 }
133
134 self.active = false;
135 let message = if success {
136 "Package versions loaded"
137 } else {
138 "Failed to fetch some versions"
139 };
140
141 self.client
142 .send_notification::<tower_lsp_server::ls_types::notification::Progress>(
143 ProgressParams {
144 token: self.token.clone(),
145 value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(
146 WorkDoneProgressEnd {
147 message: Some(message.to_string()),
148 },
149 )),
150 },
151 )
152 .await;
153 }
154}
155
156impl Drop for RegistryProgress {
158 fn drop(&mut self) {
159 if self.active {
160 tracing::warn!(
161 token = ?self.token,
162 "RegistryProgress dropped without explicit end() - spawning cleanup"
163 );
164 let client = self.client.clone();
166 let token = self.token.clone();
167 tokio::spawn(async move {
168 client
169 .send_notification::<tower_lsp_server::ls_types::notification::Progress>(
170 ProgressParams {
171 token,
172 value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(
173 WorkDoneProgressEnd { message: None },
174 )),
175 },
176 )
177 .await;
178 });
179 }
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 #[test]
186 fn test_progress_token_format() {
187 let uri = "file:///test/Cargo.toml";
188 let token = format!("deps-fetch-{}", uri);
189 assert_eq!(token, "deps-fetch-file:///test/Cargo.toml");
190 }
191
192 #[test]
193 fn test_percentage_calculation() {
194 let calculate = |fetched: usize, total: usize| -> u32 {
195 if total == 0 {
196 return 0;
197 }
198 ((fetched as f64 / total as f64) * 100.0) as u32
199 };
200
201 assert_eq!(calculate(0, 10), 0);
202 assert_eq!(calculate(5, 10), 50);
203 assert_eq!(calculate(10, 10), 100);
204 assert_eq!(calculate(7, 10), 70);
205 assert_eq!(calculate(0, 0), 0);
206 }
207
208 #[test]
209 fn test_progress_message_format() {
210 let format_message = |fetched: usize, total: usize| -> String {
211 format!("Fetched {}/{} packages", fetched, total)
212 };
213
214 assert_eq!(format_message(5, 10), "Fetched 5/10 packages");
215 assert_eq!(format_message(0, 15), "Fetched 0/15 packages");
216 assert_eq!(format_message(20, 20), "Fetched 20/20 packages");
217 }
218
219 #[test]
220 fn test_update_after_end_is_safe() {
221 let active = false;
223 let total = 10;
224
225 if !active || total == 0 {
227 return; }
229
230 panic!("Should have returned early");
231 }
232
233 #[test]
234 fn test_update_with_zero_total_returns_early() {
235 let active = true;
236 let total = 0;
237
238 if !active || total == 0 {
239 return; }
241
242 panic!("Should have returned early");
243 }
244
245 #[test]
246 fn test_end_idempotency_flag() {
247 let mut active = true;
249
250 assert!(active, "First call should proceed");
252 active = false;
253
254 assert!(!active, "Second call should be no-op due to inactive flag");
256 }
257
258 #[test]
259 fn test_drop_cleanup_active_flag_logic() {
260 let active = true;
262 let should_cleanup = active;
263 assert!(
264 should_cleanup,
265 "Active progress should trigger cleanup on drop"
266 );
267
268 let active = false;
269 let should_cleanup = active;
270 assert!(
271 !should_cleanup,
272 "Inactive progress should not trigger cleanup"
273 );
274 }
275}