codive_lsp/
facade.rs

1//! LSP facade providing high-level API
2//!
3//! This module provides a high-level interface to LSP operations with
4//! lazy initialization, client caching, and automatic server management.
5
6use crate::client::LspClient;
7use crate::server::{LspRegistry, LspServerInfo};
8use crate::types::*;
9use anyhow::{anyhow, Result};
10use std::collections::{HashMap, HashSet};
11use std::path::{Path, PathBuf};
12use std::sync::Arc;
13use tokio::sync::{Mutex, RwLock};
14use tracing::{debug, error, info, warn};
15
16/// LSP facade providing high-level operations
17pub struct Lsp {
18    /// Server registry
19    registry: LspRegistry,
20    /// Active clients keyed by (root, server_id)
21    clients: RwLock<HashMap<(PathBuf, String), Arc<LspClient>>>,
22    /// Servers that failed to start (to avoid retrying)
23    broken: RwLock<HashSet<(PathBuf, String)>>,
24    /// Clients currently being spawned
25    spawning: Mutex<HashMap<(PathBuf, String), tokio::sync::broadcast::Sender<()>>>,
26    /// Working directory (for relative paths)
27    working_dir: PathBuf,
28}
29
30impl Lsp {
31    /// Create a new LSP facade
32    pub fn new(working_dir: PathBuf) -> Self {
33        Self {
34            registry: LspRegistry::new(),
35            clients: RwLock::new(HashMap::new()),
36            broken: RwLock::new(HashSet::new()),
37            spawning: Mutex::new(HashMap::new()),
38            working_dir,
39        }
40    }
41
42    /// Create with custom registry
43    pub fn with_registry(working_dir: PathBuf, registry: LspRegistry) -> Self {
44        Self {
45            registry,
46            clients: RwLock::new(HashMap::new()),
47            broken: RwLock::new(HashSet::new()),
48            spawning: Mutex::new(HashMap::new()),
49            working_dir,
50        }
51    }
52
53    /// Get the working directory
54    pub fn working_dir(&self) -> &Path {
55        &self.working_dir
56    }
57
58    /// Check if there are any clients available for a file
59    pub async fn has_clients(&self, path: &Path) -> bool {
60        let path = self.resolve_path(path);
61        let ext = path
62            .extension()
63            .and_then(|e| e.to_str())
64            .map(|e| format!(".{}", e))
65            .unwrap_or_default();
66
67        for server in self.registry.servers_for_extension(&ext) {
68            if let Some(root) = server.detect_root(&path) {
69                let key = (root, server.id.clone());
70                let broken = self.broken.read().await;
71                if !broken.contains(&key) {
72                    return true;
73                }
74            }
75        }
76
77        false
78    }
79
80    /// Get clients for a file (spawning if necessary)
81    pub async fn clients_for_file(&self, path: &Path) -> Result<Vec<Arc<LspClient>>> {
82        let path = self.resolve_path(path);
83        let ext = path
84            .extension()
85            .and_then(|e| e.to_str())
86            .map(|e| format!(".{}", e))
87            .unwrap_or_default();
88
89        let mut result = Vec::new();
90
91        for server in self.registry.servers_for_extension(&ext) {
92            if let Some(client) = self.get_or_spawn_client(&path, server).await? {
93                result.push(client);
94            }
95        }
96
97        Ok(result)
98    }
99
100    /// Touch a file (notify LSP servers about it)
101    pub async fn touch_file(&self, path: &Path, wait_for_diagnostics: bool) -> Result<()> {
102        let path = self.resolve_path(path);
103        let clients = self.clients_for_file(&path).await?;
104
105        for client in clients {
106            if let Err(e) = client.open_file(&path).await {
107                warn!(
108                    server_id = %client.server_id(),
109                    path = ?path,
110                    error = ?e,
111                    "Failed to open file in LSP"
112                );
113            }
114        }
115
116        // TODO: Implement wait_for_diagnostics if needed
117        if wait_for_diagnostics {
118            // Give servers a moment to process
119            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
120        }
121
122        Ok(())
123    }
124
125    // ========================================================================
126    // LSP Operations
127    // ========================================================================
128
129    /// Get hover information
130    pub async fn hover(
131        &self,
132        path: &Path,
133        line: u32,
134        character: u32,
135    ) -> Result<Vec<Option<Hover>>> {
136        let path = self.resolve_path(path);
137        let path_clone = path.clone();
138        self.run_on_file(&path, move |client| {
139            let p = path_clone.clone();
140            async move { client.hover(&p, line, character).await }
141        })
142        .await
143    }
144
145    /// Go to definition
146    pub async fn definition(
147        &self,
148        path: &Path,
149        line: u32,
150        character: u32,
151    ) -> Result<Vec<Location>> {
152        let path = self.resolve_path(path);
153        let path_clone = path.clone();
154        let results = self
155            .run_on_file(&path, move |client| {
156                let p = path_clone.clone();
157                async move { client.definition(&p, line, character).await }
158            })
159            .await?;
160        Ok(results.into_iter().flatten().collect())
161    }
162
163    /// Find references
164    pub async fn references(
165        &self,
166        path: &Path,
167        line: u32,
168        character: u32,
169    ) -> Result<Vec<Location>> {
170        let path = self.resolve_path(path);
171        let path_clone = path.clone();
172        let results = self
173            .run_on_file(&path, move |client| {
174                let p = path_clone.clone();
175                async move { client.references(&p, line, character, true).await }
176            })
177            .await?;
178        Ok(results.into_iter().flatten().collect())
179    }
180
181    /// Go to implementation
182    pub async fn implementation(
183        &self,
184        path: &Path,
185        line: u32,
186        character: u32,
187    ) -> Result<Vec<Location>> {
188        let path = self.resolve_path(path);
189        let path_clone = path.clone();
190        let results = self
191            .run_on_file(&path, move |client| {
192                let p = path_clone.clone();
193                async move { client.implementation(&p, line, character).await }
194            })
195            .await?;
196        Ok(results.into_iter().flatten().collect())
197    }
198
199    /// Get document symbols
200    pub async fn document_symbols(&self, path: &Path) -> Result<Vec<DocumentSymbolResponse>> {
201        let path = self.resolve_path(path);
202        let path_clone = path.clone();
203        self.run_on_file(&path, move |client| {
204            let p = path_clone.clone();
205            async move { client.document_symbols(&p).await }
206        })
207        .await
208    }
209
210    /// Search workspace symbols
211    pub async fn workspace_symbols(&self, query: &str) -> Result<Vec<SymbolInformation>> {
212        let clients = self.clients.read().await;
213        let mut results = Vec::new();
214
215        for client in clients.values() {
216            match client.workspace_symbols(query).await {
217                Ok(symbols) => {
218                    // Filter to important symbol kinds and limit
219                    let filtered: Vec<_> = symbols
220                        .into_iter()
221                        .filter(|s| IMPORTANT_SYMBOL_KINDS.contains(&s.kind))
222                        .take(10)
223                        .collect();
224                    results.extend(filtered);
225                }
226                Err(e) => {
227                    warn!(
228                        server_id = %client.server_id(),
229                        error = ?e,
230                        "Failed to get workspace symbols"
231                    );
232                }
233            }
234        }
235
236        Ok(results)
237    }
238
239    /// Prepare call hierarchy
240    pub async fn prepare_call_hierarchy(
241        &self,
242        path: &Path,
243        line: u32,
244        character: u32,
245    ) -> Result<Vec<CallHierarchyItem>> {
246        let path = self.resolve_path(path);
247        let path_clone = path.clone();
248        let results = self
249            .run_on_file(&path, move |client| {
250                let p = path_clone.clone();
251                async move { client.prepare_call_hierarchy(&p, line, character).await }
252            })
253            .await?;
254        Ok(results.into_iter().flatten().collect())
255    }
256
257    /// Get incoming calls
258    pub async fn incoming_calls(
259        &self,
260        path: &Path,
261        line: u32,
262        character: u32,
263    ) -> Result<Vec<CallHierarchyIncomingCall>> {
264        let path = self.resolve_path(path);
265        let items = self.prepare_call_hierarchy(&path, line, character).await?;
266
267        if items.is_empty() {
268            return Ok(vec![]);
269        }
270
271        // Use the first item
272        let item = items.into_iter().next().unwrap();
273        let results = self
274            .run_on_file(&path, |client| {
275                let item = item.clone();
276                async move { client.incoming_calls(item).await }
277            })
278            .await?;
279        Ok(results.into_iter().flatten().collect())
280    }
281
282    /// Get outgoing calls
283    pub async fn outgoing_calls(
284        &self,
285        path: &Path,
286        line: u32,
287        character: u32,
288    ) -> Result<Vec<CallHierarchyOutgoingCall>> {
289        let path = self.resolve_path(path);
290        let items = self.prepare_call_hierarchy(&path, line, character).await?;
291
292        if items.is_empty() {
293            return Ok(vec![]);
294        }
295
296        // Use the first item
297        let item = items.into_iter().next().unwrap();
298        let results = self
299            .run_on_file(&path, |client| {
300                let item = item.clone();
301                async move { client.outgoing_calls(item).await }
302            })
303            .await?;
304        Ok(results.into_iter().flatten().collect())
305    }
306
307    /// Get all diagnostics
308    pub async fn diagnostics(&self) -> HashMap<PathBuf, Vec<Diagnostic>> {
309        let clients = self.clients.read().await;
310        let mut result = HashMap::new();
311
312        for client in clients.values() {
313            for (path, diags) in client.diagnostics() {
314                result
315                    .entry(path)
316                    .or_insert_with(Vec::new)
317                    .extend(diags);
318            }
319        }
320
321        result
322    }
323
324    /// Get diagnostics for a specific file
325    pub async fn diagnostics_for_file(&self, path: &Path) -> Vec<Diagnostic> {
326        let path = self.resolve_path(path);
327        let clients = self.clients.read().await;
328        let mut result = Vec::new();
329
330        for client in clients.values() {
331            result.extend(client.diagnostics_for_file(&path));
332        }
333
334        result
335    }
336
337    /// Get status of all active LSP servers
338    pub async fn status(&self) -> Vec<LspStatus> {
339        let clients = self.clients.read().await;
340        clients
341            .values()
342            .map(|client| LspStatus {
343                id: client.server_id().to_string(),
344                name: client.server_id().to_string(),
345                root: client
346                    .root()
347                    .strip_prefix(&self.working_dir)
348                    .unwrap_or(client.root())
349                    .to_string_lossy()
350                    .to_string(),
351                status: LspConnectionStatus::Connected,
352            })
353            .collect()
354    }
355
356    /// Shutdown all clients
357    pub async fn shutdown(self) {
358        info!("Shutting down all LSP clients");
359        let clients = self.clients.into_inner();
360        for (_, client) in clients {
361            if let Ok(client) = Arc::try_unwrap(client) {
362                client.shutdown().await;
363            }
364        }
365    }
366
367    // ========================================================================
368    // Internal Methods
369    // ========================================================================
370
371    /// Resolve a path relative to working directory
372    fn resolve_path(&self, path: &Path) -> PathBuf {
373        if path.is_absolute() {
374            path.to_path_buf()
375        } else {
376            self.working_dir.join(path)
377        }
378    }
379
380    /// Get or spawn a client for a file
381    async fn get_or_spawn_client(
382        &self,
383        path: &Path,
384        server: &LspServerInfo,
385    ) -> Result<Option<Arc<LspClient>>> {
386        let root = match server.detect_root(path) {
387            Some(root) => root,
388            None => return Ok(None),
389        };
390
391        let key = (root.clone(), server.id.clone());
392
393        // Check if broken
394        {
395            let broken = self.broken.read().await;
396            if broken.contains(&key) {
397                return Ok(None);
398            }
399        }
400
401        // Check if already exists
402        {
403            let clients = self.clients.read().await;
404            if let Some(client) = clients.get(&key) {
405                return Ok(Some(client.clone()));
406            }
407        }
408
409        // Check if currently spawning
410        {
411            let spawning = self.spawning.lock().await;
412            if let Some(tx) = spawning.get(&key) {
413                let mut rx = tx.subscribe();
414                drop(spawning);
415                let _ = rx.recv().await;
416
417                // Now check if client exists
418                let clients = self.clients.read().await;
419                return Ok(clients.get(&key).cloned());
420            }
421        }
422
423        // Spawn the client
424        info!(server_id = %server.id, root = ?root, "Spawning LSP server");
425
426        let (tx, _) = tokio::sync::broadcast::channel(1);
427        {
428            let mut spawning = self.spawning.lock().await;
429            spawning.insert(key.clone(), tx.clone());
430        }
431
432        let result = self.spawn_client(server, &root).await;
433
434        // Clean up spawning state
435        {
436            let mut spawning = self.spawning.lock().await;
437            spawning.remove(&key);
438        }
439        let _ = tx.send(());
440
441        match result {
442            Ok(client) => {
443                let client = Arc::new(client);
444                let mut clients = self.clients.write().await;
445                clients.insert(key, client.clone());
446                Ok(Some(client))
447            }
448            Err(e) => {
449                error!(
450                    server_id = %server.id,
451                    root = ?root,
452                    error = ?e,
453                    "Failed to spawn LSP server"
454                );
455                let mut broken = self.broken.write().await;
456                broken.insert(key);
457                Ok(None)
458            }
459        }
460    }
461
462    /// Spawn a new client
463    async fn spawn_client(&self, server: &LspServerInfo, root: &Path) -> Result<LspClient> {
464        let handle = server.spawn(root)?;
465        LspClient::new(&server.id, handle, root.to_path_buf()).await
466    }
467
468    /// Run an operation on all clients for a file
469    async fn run_on_file<F, Fut, T>(&self, path: &Path, f: F) -> Result<Vec<T>>
470    where
471        F: Fn(Arc<LspClient>) -> Fut,
472        Fut: std::future::Future<Output = Result<T>>,
473    {
474        let clients = self.clients_for_file(path).await?;
475
476        // Touch the file first
477        for client in &clients {
478            if let Err(e) = client.open_file(path).await {
479                warn!(
480                    server_id = %client.server_id(),
481                    path = ?path,
482                    error = ?e,
483                    "Failed to open file"
484                );
485            }
486        }
487
488        let mut results = Vec::new();
489        for client in clients {
490            match f(client.clone()).await {
491                Ok(result) => results.push(result),
492                Err(e) => {
493                    warn!(
494                        server_id = %client.server_id(),
495                        error = ?e,
496                        "LSP operation failed"
497                    );
498                }
499            }
500        }
501
502        Ok(results)
503    }
504}
505
506// ============================================================================
507// Global Instance
508// ============================================================================
509
510use std::sync::OnceLock;
511
512static LSP_INSTANCE: OnceLock<Lsp> = OnceLock::new();
513
514/// Initialize the global LSP instance
515pub fn init(working_dir: PathBuf) -> &'static Lsp {
516    LSP_INSTANCE.get_or_init(|| Lsp::new(working_dir))
517}
518
519/// Get the global LSP instance
520pub fn lsp() -> Option<&'static Lsp> {
521    LSP_INSTANCE.get()
522}
523
524#[cfg(test)]
525mod tests {
526    use super::*;
527    use tempfile::tempdir;
528
529    #[tokio::test]
530    async fn test_lsp_new() {
531        let temp = tempdir().unwrap();
532        let lsp = Lsp::new(temp.path().to_path_buf());
533        assert_eq!(lsp.working_dir(), temp.path());
534    }
535
536    #[tokio::test]
537    async fn test_has_clients_no_root() {
538        let temp = tempdir().unwrap();
539        let lsp = Lsp::new(temp.path().to_path_buf());
540
541        // File without a project root
542        let file = temp.path().join("orphan.rs");
543        std::fs::write(&file, "fn main() {}").unwrap();
544
545        assert!(!lsp.has_clients(&file).await);
546    }
547
548    #[tokio::test]
549    async fn test_status_empty() {
550        let temp = tempdir().unwrap();
551        let lsp = Lsp::new(temp.path().to_path_buf());
552
553        let status = lsp.status().await;
554        assert!(status.is_empty());
555    }
556}