Skip to main content

aft/lsp/
manager.rs

1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3use std::str::FromStr;
4
5use crossbeam_channel::{unbounded, Receiver, Sender};
6use lsp_types::notification::{DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument};
7use lsp_types::{
8    DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
9    TextDocumentContentChangeEvent, TextDocumentIdentifier, TextDocumentItem,
10    VersionedTextDocumentIdentifier,
11};
12
13use crate::lsp::client::{LspClient, LspEvent, ServerState};
14use crate::lsp::diagnostics::{from_lsp_diagnostics, DiagnosticsStore, StoredDiagnostic};
15use crate::lsp::document::DocumentStore;
16use crate::lsp::registry::{servers_for_file, ServerDef, ServerKind};
17use crate::lsp::roots::{find_workspace_root, ServerKey};
18use crate::lsp::LspError;
19
20pub struct LspManager {
21    /// Active server instances, keyed by (ServerKind, workspace_root).
22    clients: HashMap<ServerKey, LspClient>,
23    /// Tracks opened documents and versions per active server.
24    documents: HashMap<ServerKey, DocumentStore>,
25    /// Stored publishDiagnostics payloads across all servers.
26    diagnostics: DiagnosticsStore,
27    /// Unified event channel — all server reader threads send here.
28    event_tx: Sender<LspEvent>,
29    event_rx: Receiver<LspEvent>,
30    /// Optional binary path overrides used by integration tests.
31    binary_overrides: HashMap<ServerKind, PathBuf>,
32}
33
34impl LspManager {
35    pub fn new() -> Self {
36        let (event_tx, event_rx) = unbounded();
37        Self {
38            clients: HashMap::new(),
39            documents: HashMap::new(),
40            diagnostics: DiagnosticsStore::new(),
41            event_tx,
42            event_rx,
43            binary_overrides: HashMap::new(),
44        }
45    }
46
47    /// For testing: override the binary for a server kind.
48    pub fn override_binary(&mut self, kind: ServerKind, binary_path: PathBuf) {
49        self.binary_overrides.insert(kind, binary_path);
50    }
51
52    /// Ensure a server is running for the given file. Spawns if needed.
53    /// Returns the active server keys for the file, or an empty vec if none match.
54    pub fn ensure_server_for_file(&mut self, file_path: &Path) -> Vec<ServerKey> {
55        let defs = servers_for_file(file_path);
56        let mut keys = Vec::new();
57
58        for def in defs {
59            let Some(root) = find_workspace_root(file_path, def.root_markers) else {
60                continue;
61            };
62
63            let key = ServerKey {
64                kind: def.kind,
65                root,
66            };
67
68            if !self.clients.contains_key(&key) {
69                match self.spawn_server(def, &key.root) {
70                    Ok(client) => {
71                        self.clients.insert(key.clone(), client);
72                        self.documents.entry(key.clone()).or_default();
73                    }
74                    Err(err) => {
75                        log::error!("failed to spawn {}: {}", def.name, err);
76                        continue;
77                    }
78                }
79            }
80
81            keys.push(key);
82        }
83
84        keys
85    }
86    /// Ensure that servers are running for the file and that the document is open
87    /// in each server's DocumentStore. Reads file content from disk if not already open.
88    /// Returns the server keys for the file.
89    pub fn ensure_file_open(&mut self, file_path: &Path) -> Result<Vec<ServerKey>, LspError> {
90        let canonical_path = canonicalize_for_lsp(file_path)?;
91        let server_keys = self.ensure_server_for_file(&canonical_path);
92        if server_keys.is_empty() {
93            return Ok(server_keys);
94        }
95
96        let uri = uri_for_path(&canonical_path)?;
97        let language_id = language_id_for_extension(
98            canonical_path
99                .extension()
100                .and_then(|ext| ext.to_str())
101                .unwrap_or_default(),
102        )
103        .to_string();
104
105        for key in &server_keys {
106            let already_open = self
107                .documents
108                .get(key)
109                .map_or(false, |store| store.is_open(&canonical_path));
110
111            if !already_open {
112                let content = std::fs::read_to_string(&canonical_path).map_err(LspError::Io)?;
113                if let Some(client) = self.clients.get_mut(key) {
114                    client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
115                        text_document: TextDocumentItem::new(
116                            uri.clone(),
117                            language_id.clone(),
118                            0,
119                            content,
120                        ),
121                    })?;
122                }
123                self.documents
124                    .entry(key.clone())
125                    .or_default()
126                    .open(canonical_path.clone());
127            }
128        }
129
130        Ok(server_keys)
131    }
132
133    /// Notify relevant LSP servers that a file has been written/changed.
134    /// This is the main hook called after every file write in AFT.
135    ///
136    /// If the file's server isn't running yet, starts it (lazy spawn).
137    /// If the file isn't open in LSP yet, sends didOpen. Otherwise sends didChange.
138    pub fn notify_file_changed(&mut self, file_path: &Path, content: &str) -> Result<(), LspError> {
139        let canonical_path = canonicalize_for_lsp(file_path)?;
140        let server_keys = self.ensure_server_for_file(&canonical_path);
141        if server_keys.is_empty() {
142            return Ok(());
143        }
144
145        let uri = uri_for_path(&canonical_path)?;
146        let language_id = language_id_for_extension(
147            canonical_path
148                .extension()
149                .and_then(|ext| ext.to_str())
150                .unwrap_or_default(),
151        )
152        .to_string();
153
154        for key in server_keys {
155            let current_version = self
156                .documents
157                .get(&key)
158                .and_then(|store| store.version(&canonical_path));
159
160            if let Some(version) = current_version {
161                let next_version = version + 1;
162                if let Some(client) = self.clients.get_mut(&key) {
163                    client.send_notification::<DidChangeTextDocument>(
164                        DidChangeTextDocumentParams {
165                            text_document: VersionedTextDocumentIdentifier::new(
166                                uri.clone(),
167                                next_version,
168                            ),
169                            content_changes: vec![TextDocumentContentChangeEvent {
170                                range: None,
171                                range_length: None,
172                                text: content.to_string(),
173                            }],
174                        },
175                    )?;
176                }
177                if let Some(store) = self.documents.get_mut(&key) {
178                    store.bump_version(&canonical_path);
179                }
180                continue;
181            }
182
183            if let Some(client) = self.clients.get_mut(&key) {
184                client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
185                    text_document: TextDocumentItem::new(
186                        uri.clone(),
187                        language_id.clone(),
188                        0,
189                        content.to_string(),
190                    ),
191                })?;
192            }
193            self.documents
194                .entry(key)
195                .or_default()
196                .open(canonical_path.clone());
197        }
198
199        Ok(())
200    }
201
202    /// Close a document in all servers that have it open.
203    pub fn notify_file_closed(&mut self, file_path: &Path) -> Result<(), LspError> {
204        let canonical_path = canonicalize_for_lsp(file_path)?;
205        let uri = uri_for_path(&canonical_path)?;
206        let keys: Vec<ServerKey> = self.documents.keys().cloned().collect();
207
208        for key in keys {
209            let was_open = self
210                .documents
211                .get(&key)
212                .map(|store| store.is_open(&canonical_path))
213                .unwrap_or(false);
214            if !was_open {
215                continue;
216            }
217
218            if let Some(client) = self.clients.get_mut(&key) {
219                client.send_notification::<DidCloseTextDocument>(DidCloseTextDocumentParams {
220                    text_document: TextDocumentIdentifier::new(uri.clone()),
221                })?;
222            }
223
224            if let Some(store) = self.documents.get_mut(&key) {
225                store.close(&canonical_path);
226            }
227        }
228
229        Ok(())
230    }
231
232    /// Get an active client for a file path, if one exists.
233    pub fn client_for_file(&self, file_path: &Path) -> Option<&LspClient> {
234        let key = self.server_key_for_file(file_path)?;
235        self.clients.get(&key)
236    }
237
238    /// Get a mutable active client for a file path, if one exists.
239    pub fn client_for_file_mut(&mut self, file_path: &Path) -> Option<&mut LspClient> {
240        let key = self.server_key_for_file(file_path)?;
241        self.clients.get_mut(&key)
242    }
243
244    /// Number of tracked server clients.
245    pub fn active_client_count(&self) -> usize {
246        self.clients.len()
247    }
248
249    /// Drain all pending LSP events. Call from the main loop.
250    pub fn drain_events(&mut self) -> Vec<LspEvent> {
251        let mut events = Vec::new();
252        while let Ok(event) = self.event_rx.try_recv() {
253            match &event {
254                LspEvent::Notification {
255                    server_kind,
256                    method,
257                    params,
258                    ..
259                } if method == "textDocument/publishDiagnostics" => {
260                    if let Some(params) = params {
261                        self.handle_publish_diagnostics(*server_kind, params);
262                    }
263                }
264                LspEvent::ServerExited { server_kind, root } => {
265                    let key = ServerKey {
266                        kind: *server_kind,
267                        root: root.clone(),
268                    };
269                    self.clients.remove(&key);
270                    self.documents.remove(&key);
271                    self.diagnostics.clear_server(*server_kind);
272                }
273                _ => {}
274            }
275            events.push(event);
276        }
277        events
278    }
279
280    /// Wait briefly for diagnostics to arrive for a specific file after a change.
281    ///
282    /// This mirrors the existing `lsp_diagnostics` command behavior: sleep for a
283    /// short interval, drain queued LSP notifications, then read diagnostics from
284    /// the store using the canonicalized file path.
285    pub fn wait_for_diagnostics(
286        &mut self,
287        file_path: &Path,
288        timeout: std::time::Duration,
289    ) -> Vec<StoredDiagnostic> {
290        let lookup_path = normalize_lookup_path(file_path);
291
292        if !timeout.is_zero() {
293            std::thread::sleep(timeout);
294        }
295        self.drain_events();
296
297        self.diagnostics
298            .for_file(&lookup_path)
299            .into_iter()
300            .cloned()
301            .collect()
302    }
303
304    /// Shutdown all servers gracefully.
305    pub fn shutdown_all(&mut self) {
306        for (key, mut client) in self.clients.drain() {
307            if let Err(err) = client.shutdown() {
308                log::error!("error shutting down {:?}: {}", key, err);
309            }
310        }
311        self.documents.clear();
312        self.diagnostics = DiagnosticsStore::new();
313    }
314
315    /// Check if any server is active.
316    pub fn has_active_servers(&self) -> bool {
317        self.clients
318            .values()
319            .any(|client| client.state() == ServerState::Ready)
320    }
321
322    pub fn get_diagnostics_for_file(&self, file: &Path) -> Vec<&StoredDiagnostic> {
323        let normalized = normalize_lookup_path(file);
324        self.diagnostics.for_file(&normalized)
325    }
326
327    pub fn get_diagnostics_for_directory(&self, dir: &Path) -> Vec<&StoredDiagnostic> {
328        let normalized = normalize_lookup_path(dir);
329        self.diagnostics.for_directory(&normalized)
330    }
331
332    pub fn get_all_diagnostics(&self) -> Vec<&StoredDiagnostic> {
333        self.diagnostics.all()
334    }
335
336    fn handle_publish_diagnostics(&mut self, server: ServerKind, params: &serde_json::Value) {
337        if let Ok(publish_params) =
338            serde_json::from_value::<lsp_types::PublishDiagnosticsParams>(params.clone())
339        {
340            let Some(file) = uri_to_path(&publish_params.uri) else {
341                return;
342            };
343            let stored = from_lsp_diagnostics(file.clone(), publish_params.diagnostics);
344            self.diagnostics.publish(server, file, stored);
345        }
346    }
347
348    fn spawn_server(&self, def: &ServerDef, root: &Path) -> Result<LspClient, LspError> {
349        let binary = self.resolve_binary(def)?;
350        let mut client = LspClient::spawn(
351            def.kind,
352            root.to_path_buf(),
353            &binary,
354            def.args,
355            self.event_tx.clone(),
356        )?;
357        client.initialize(root)?;
358        Ok(client)
359    }
360
361    fn resolve_binary(&self, def: &ServerDef) -> Result<PathBuf, LspError> {
362        if let Some(path) = self.binary_overrides.get(&def.kind) {
363            if path.exists() {
364                return Ok(path.clone());
365            }
366            return Err(LspError::NotFound(format!(
367                "override binary for {:?} not found: {}",
368                def.kind,
369                path.display()
370            )));
371        }
372
373        if let Some(path) = env_binary_override(def.kind) {
374            if path.exists() {
375                return Ok(path);
376            }
377            return Err(LspError::NotFound(format!(
378                "environment override binary for {:?} not found: {}",
379                def.kind,
380                path.display()
381            )));
382        }
383
384        which::which(def.binary).map_err(|_| {
385            LspError::NotFound(format!(
386                "language server binary '{}' not found on PATH",
387                def.binary
388            ))
389        })
390    }
391
392    fn server_key_for_file(&self, file_path: &Path) -> Option<ServerKey> {
393        for def in servers_for_file(file_path) {
394            let root = find_workspace_root(file_path, def.root_markers)?;
395            let key = ServerKey {
396                kind: def.kind,
397                root,
398            };
399            if self.clients.contains_key(&key) {
400                return Some(key);
401            }
402        }
403        None
404    }
405}
406
407impl Default for LspManager {
408    fn default() -> Self {
409        Self::new()
410    }
411}
412
413fn canonicalize_for_lsp(file_path: &Path) -> Result<PathBuf, LspError> {
414    std::fs::canonicalize(file_path).map_err(LspError::from)
415}
416
417fn uri_for_path(path: &Path) -> Result<lsp_types::Uri, LspError> {
418    let url = url::Url::from_file_path(path).map_err(|_| {
419        LspError::NotFound(format!(
420            "failed to convert '{}' to file URI",
421            path.display()
422        ))
423    })?;
424    lsp_types::Uri::from_str(url.as_str()).map_err(|_| {
425        LspError::NotFound(format!("failed to parse file URI for '{}'", path.display()))
426    })
427}
428
429fn language_id_for_extension(ext: &str) -> &'static str {
430    match ext {
431        "ts" => "typescript",
432        "tsx" => "typescriptreact",
433        "js" | "mjs" | "cjs" => "javascript",
434        "jsx" => "javascriptreact",
435        "py" | "pyi" => "python",
436        "rs" => "rust",
437        "go" => "go",
438        _ => "plaintext",
439    }
440}
441
442fn normalize_lookup_path(path: &Path) -> PathBuf {
443    std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
444}
445
446fn uri_to_path(uri: &lsp_types::Uri) -> Option<PathBuf> {
447    let url = url::Url::parse(uri.as_str()).ok()?;
448    url.to_file_path()
449        .ok()
450        .map(|path| normalize_lookup_path(&path))
451}
452
453fn env_binary_override(kind: ServerKind) -> Option<PathBuf> {
454    let key = match kind {
455        ServerKind::TypeScript => "AFT_LSP_TYPESCRIPT_BINARY",
456        ServerKind::Python => "AFT_LSP_PYTHON_BINARY",
457        ServerKind::Rust => "AFT_LSP_RUST_BINARY",
458        ServerKind::Go => "AFT_LSP_GO_BINARY",
459    };
460    std::env::var_os(key).map(PathBuf::from)
461}