Skip to main content

matrixcode_core/lsp/
progress.rs

1//! LSP Progress Tracking Module
2//!
3//! Tracks LSP initialization progress and provides callbacks for UI updates.
4//! Separated from core LSP logic to keep the library independent of UI frameworks.
5
6use std::sync::Arc;
7use std::time::Instant;
8
9/// LSP initialization status
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum LspInitStatus {
12    /// Not started yet
13    NotStarted,
14    /// Process is spawning
15    Spawning,
16    /// Server is initializing (building indexes, loading workspace)
17    Initializing,
18    /// Server is ready for requests
19    Ready,
20    /// Initialization failed
21    Failed,
22}
23
24impl LspInitStatus {
25    /// Get display label for the status
26    pub fn label(&self) -> &'static str {
27        match self {
28            Self::NotStarted => "Not Started",
29            Self::Spawning => "Starting Process",
30            Self::Initializing => "Initializing",
31            Self::Ready => "Ready",
32            Self::Failed => "Failed",
33        }
34    }
35
36    /// Check if initialization is complete (success or failure)
37    pub fn is_complete(&self) -> bool {
38        matches!(self, Self::Ready | Self::Failed)
39    }
40
41    /// Check if server is ready
42    pub fn is_ready(&self) -> bool {
43        matches!(self, Self::Ready)
44    }
45
46    /// Check if initialization is in progress
47    pub fn is_in_progress(&self) -> bool {
48        matches!(self, Self::Spawning | Self::Initializing)
49    }
50}
51
52/// LSP progress tracker
53///
54/// Tracks the initialization progress of a single LSP server.
55pub struct LspProgressTracker {
56    /// Language name (e.g., "rust", "typescript")
57    language: String,
58    /// Server name (e.g., "rust-analyzer", "typescript-language-server")
59    server_name: String,
60    /// Current status
61    status: LspInitStatus,
62    /// Progress percentage (0.0 - 1.0)
63    progress: f32,
64    /// Human-readable progress message
65    message: String,
66    /// Timestamp when initialization started
67    started_at: Option<Instant>,
68}
69
70impl LspProgressTracker {
71    /// Create a new progress tracker
72    pub fn new(language: impl Into<String>, server_name: impl Into<String>) -> Self {
73        Self {
74            language: language.into(),
75            server_name: server_name.into(),
76            status: LspInitStatus::NotStarted,
77            progress: 0.0,
78            message: "Waiting to start...".to_string(),
79            started_at: None,
80        }
81    }
82
83    /// Get language name
84    pub fn language(&self) -> &str {
85        &self.language
86    }
87
88    /// Get server name
89    pub fn server_name(&self) -> &str {
90        &self.server_name
91    }
92
93    /// Get current status
94    pub fn status(&self) -> LspInitStatus {
95        self.status
96    }
97
98    /// Get progress percentage (0.0 - 1.0)
99    pub fn progress(&self) -> f32 {
100        self.progress
101    }
102
103    /// Get progress message
104    pub fn message(&self) -> &str {
105        &self.message
106    }
107
108    /// Get elapsed time since initialization started
109    pub fn elapsed_secs(&self) -> Option<f64> {
110        self.started_at.map(|t| t.elapsed().as_secs_f64())
111    }
112
113    /// Check if server is ready
114    pub fn is_ready(&self) -> bool {
115        self.status.is_ready()
116    }
117
118    /// Mark initialization as started
119    pub fn start(&mut self) {
120        self.status = LspInitStatus::Spawning;
121        self.progress = 0.0;
122        self.message = "Starting process...".to_string();
123        self.started_at = Some(Instant::now());
124    }
125
126    /// Update progress during initialization
127    pub fn update(&mut self, progress: f32, message: impl Into<String>) {
128        if progress > 0.0 && progress <= 1.0 {
129            self.progress = progress;
130            self.message = message.into();
131            
132            // Auto-update status based on progress
133            if progress < 0.3 {
134                self.status = LspInitStatus::Spawning;
135            } else if progress < 1.0 {
136                self.status = LspInitStatus::Initializing;
137            }
138        }
139    }
140
141    /// Mark initialization as complete (success)
142    pub fn complete(&mut self) {
143        self.status = LspInitStatus::Ready;
144        self.progress = 1.0;
145        self.message = "Ready".to_string();
146    }
147
148    /// Mark initialization as failed
149    pub fn fail(&mut self, error: impl Into<String>) {
150        self.status = LspInitStatus::Failed;
151        self.progress = 0.0;
152        self.message = error.into();
153    }
154}
155
156/// Progress callback trait for UI updates
157///
158/// Implement this trait to receive progress updates during LSP initialization.
159/// The callback is called from async context, so implementations must be `Send + Sync`.
160pub trait LspProgressCallback: Send + Sync {
161    /// Called when progress updates
162    ///
163    /// - `progress`: Percentage (0.0 - 1.0)
164    /// - `message`: Human-readable status message
165    fn on_progress(&self, progress: f32, message: &str);
166
167    /// Called when initialization completes successfully
168    fn on_complete(&self);
169
170    /// Called when initialization fails
171    ///
172    /// - `error`: Error message
173    fn on_error(&self, error: &str);
174}
175
176/// No-op callback for testing or when progress updates are not needed
177pub struct NoOpProgressCallback;
178
179impl LspProgressCallback for NoOpProgressCallback {
180    fn on_progress(&self, _progress: f32, _message: &str) {}
181    fn on_complete(&self) {}
182    fn on_error(&self, _error: &str) {}
183}
184
185/// Logging callback for CLI environments
186///
187/// Prints progress updates to the console.
188pub struct LoggingProgressCallback;
189
190impl LspProgressCallback for LoggingProgressCallback {
191    fn on_progress(&self, progress: f32, message: &str) {
192        log::info!("LSP Progress: {:.0}% - {}", progress * 100.0, message);
193    }
194
195    fn on_complete(&self) {
196        log::info!("LSP Initialization complete");
197    }
198
199    fn on_error(&self, error: &str) {
200        log::error!("LSP Initialization failed: {}", error);
201    }
202}
203
204/// Multi-callback for combining multiple callbacks
205///
206/// Allows registering multiple callbacks (e.g., logging + UI update).
207pub struct MultiProgressCallback {
208    callbacks: Vec<Arc<dyn LspProgressCallback>>,
209}
210
211impl MultiProgressCallback {
212    /// Create a new multi-callback
213    pub fn new(callbacks: Vec<Arc<dyn LspProgressCallback>>) -> Self {
214        Self { callbacks }
215    }
216
217    /// Add a callback
218    pub fn add(&mut self, callback: Arc<dyn LspProgressCallback>) {
219        self.callbacks.push(callback);
220    }
221}
222
223impl LspProgressCallback for MultiProgressCallback {
224    fn on_progress(&self, progress: f32, message: &str) {
225        for callback in &self.callbacks {
226            callback.on_progress(progress, message);
227        }
228    }
229
230    fn on_complete(&self) {
231        for callback in &self.callbacks {
232            callback.on_complete();
233        }
234    }
235
236    fn on_error(&self, error: &str) {
237        for callback in &self.callbacks {
238            callback.on_error(error);
239        }
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn test_status_labels() {
249        assert_eq!(LspInitStatus::NotStarted.label(), "Not Started");
250        assert_eq!(LspInitStatus::Spawning.label(), "Starting Process");
251        assert_eq!(LspInitStatus::Initializing.label(), "Initializing");
252        assert_eq!(LspInitStatus::Ready.label(), "Ready");
253        assert_eq!(LspInitStatus::Failed.label(), "Failed");
254    }
255
256    #[test]
257    fn test_status_checks() {
258        assert!(LspInitStatus::Ready.is_complete());
259        assert!(LspInitStatus::Failed.is_complete());
260        assert!(!LspInitStatus::Initializing.is_complete());
261
262        assert!(LspInitStatus::Ready.is_ready());
263        assert!(!LspInitStatus::Failed.is_ready());
264
265        assert!(LspInitStatus::Spawning.is_in_progress());
266        assert!(LspInitStatus::Initializing.is_in_progress());
267        assert!(!LspInitStatus::Ready.is_in_progress());
268    }
269
270    #[test]
271    fn test_progress_tracker_lifecycle() {
272        let mut tracker = LspProgressTracker::new("rust", "rust-analyzer");
273
274        // Initial state
275        assert_eq!(tracker.status(), LspInitStatus::NotStarted);
276        assert_eq!(tracker.progress(), 0.0);
277        assert_eq!(tracker.message(), "Waiting to start...");
278        assert!(tracker.elapsed_secs().is_none());
279
280        // Start
281        tracker.start();
282        assert_eq!(tracker.status(), LspInitStatus::Spawning);
283        assert_eq!(tracker.progress(), 0.0);
284        assert!(tracker.elapsed_secs().is_some());
285
286        // Update
287        tracker.update(0.5, "Loading workspace...");
288        assert_eq!(tracker.status(), LspInitStatus::Initializing);
289        assert_eq!(tracker.progress(), 0.5);
290        assert_eq!(tracker.message(), "Loading workspace...");
291
292        // Complete
293        tracker.complete();
294        assert_eq!(tracker.status(), LspInitStatus::Ready);
295        assert_eq!(tracker.progress(), 1.0);
296        assert_eq!(tracker.message(), "Ready");
297    }
298
299    #[test]
300    fn test_progress_tracker_failure() {
301        let mut tracker = LspProgressTracker::new("typescript", "typescript-language-server");
302        tracker.start();
303        tracker.update(0.3, "Initializing...");
304
305        tracker.fail("Process spawn failed");
306        assert_eq!(tracker.status(), LspInitStatus::Failed);
307        assert_eq!(tracker.message(), "Process spawn failed");
308        assert!(!tracker.is_ready());
309    }
310
311    #[test]
312    fn test_no_op_callback() {
313        let callback = NoOpProgressCallback;
314        callback.on_progress(0.5, "test");
315        callback.on_complete();
316        callback.on_error("test error");
317        // No assertions needed - just verify it doesn't crash
318    }
319
320    #[test]
321    fn test_multi_callback() {
322        use std::sync::Mutex;
323
324        struct TestCallback {
325            progress_calls: Mutex<Vec<(f32, String)>>,
326        }
327
328        impl LspProgressCallback for TestCallback {
329            fn on_progress(&self, progress: f32, message: &str) {
330                self.progress_calls.lock().unwrap().push((progress, message.to_string()));
331            }
332            fn on_complete(&self) {}
333            fn on_error(&self, _error: &str) {}
334        }
335
336        let cb1 = Arc::new(TestCallback {
337            progress_calls: Mutex::new(Vec::new()),
338        });
339        let cb2 = Arc::new(NoOpProgressCallback);
340
341        let multi = MultiProgressCallback::new(vec![cb1.clone(), cb2]);
342        multi.on_progress(0.5, "test");
343
344        let calls = cb1.progress_calls.lock().unwrap();
345        assert_eq!(calls.len(), 1);
346        assert_eq!(calls[0], (0.5, "test".to_string()));
347    }
348}