1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3
4use crossbeam_channel::{unbounded, Receiver, RecvTimeoutError, Sender};
5use lsp_types::notification::{
6 DidChangeTextDocument, DidChangeWatchedFiles, DidCloseTextDocument, DidOpenTextDocument,
7};
8use lsp_types::{
9 DidChangeTextDocumentParams, DidChangeWatchedFilesParams, DidCloseTextDocumentParams,
10 DidOpenTextDocumentParams, FileChangeType, FileEvent, TextDocumentContentChangeEvent,
11 TextDocumentIdentifier, TextDocumentItem, VersionedTextDocumentIdentifier,
12};
13
14use crate::config::Config;
15use crate::lsp::child_registry::LspChildRegistry;
16use crate::lsp::client::{LspClient, LspEvent, ServerState};
17use crate::lsp::diagnostics::{
18 from_lsp_diagnostics, DiagnosticEntry, DiagnosticsStore, StoredDiagnostic,
19};
20use crate::lsp::document::DocumentStore;
21use crate::lsp::position::{uri_for_path, uri_to_path};
22use crate::lsp::registry::{resolve_lsp_binary, servers_for_file, ServerDef, ServerKind};
23use crate::lsp::roots::{find_workspace_root, ServerKey};
24use crate::lsp::LspError;
25use crate::slog_error;
26
27#[derive(Debug, Clone)]
32pub enum ServerAttemptResult {
33 Ok { server_key: ServerKey },
35 NoRootMarker { looked_for: Vec<String> },
38 BinaryNotInstalled { binary: String },
41 SpawnFailed { binary: String, reason: String },
43}
44
45#[derive(Debug, Clone)]
47pub struct ServerAttempt {
48 pub server_id: String,
50 pub server_name: String,
52 pub result: ServerAttemptResult,
53}
54
55#[derive(Debug, Clone, Default)]
61pub struct EnsureServerOutcomes {
62 pub successful: Vec<ServerKey>,
64 pub attempts: Vec<ServerAttempt>,
67}
68
69impl EnsureServerOutcomes {
70 pub fn no_server_registered(&self) -> bool {
72 self.attempts.is_empty()
73 }
74}
75
76#[derive(Debug, Clone, Default)]
86pub struct PostEditWaitOutcome {
87 pub diagnostics: Vec<StoredDiagnostic>,
91 pub pending_servers: Vec<ServerKey>,
95 pub exited_servers: Vec<ServerKey>,
99}
100
101#[derive(Debug, Clone, Copy, Default)]
103pub struct PreEditSnapshot {
104 pub epoch: u64,
105 pub document_version_at_capture: Option<i32>,
106}
107
108pub fn post_edit_entry_is_fresh(
109 entry: &DiagnosticEntry,
110 target_version: i32,
111 pre: PreEditSnapshot,
112) -> bool {
113 if entry.epoch <= pre.epoch {
114 return false;
115 }
116
117 match entry.version {
118 Some(version) => version >= target_version,
119 None => true,
120 }
121}
122
123impl PostEditWaitOutcome {
124 pub fn complete(&self) -> bool {
127 self.pending_servers.is_empty() && self.exited_servers.is_empty()
128 }
129}
130
131#[derive(Debug, Clone)]
133pub enum PullFileOutcome {
134 Full { diagnostic_count: usize },
136 Unchanged,
138 PartialNotSupported,
141 PullNotSupported,
144 RequestFailed { reason: String },
146}
147
148#[derive(Debug, Clone)]
150pub struct PullFileResult {
151 pub server_key: ServerKey,
152 pub outcome: PullFileOutcome,
153}
154
155#[derive(Debug, Clone)]
157pub struct PullWorkspaceResult {
158 pub server_key: ServerKey,
159 pub files_reported: Vec<PathBuf>,
163 pub complete: bool,
165 pub cancelled: bool,
167 pub supports_workspace: bool,
171}
172
173pub struct LspManager {
174 clients: HashMap<ServerKey, LspClient>,
176 documents: HashMap<ServerKey, DocumentStore>,
178 diagnostics: DiagnosticsStore,
180 event_tx: Sender<LspEvent>,
182 event_rx: Receiver<LspEvent>,
183 binary_overrides: HashMap<ServerKind, PathBuf>,
185 extra_env: HashMap<String, String>,
189 failed_spawns: HashMap<ServerKey, ServerAttemptResult>,
204 watched_file_skip_logged: HashSet<ServerKey>,
207 child_registry: LspChildRegistry,
211}
212
213impl LspManager {
214 pub fn new() -> Self {
215 let (event_tx, event_rx) = unbounded();
216 Self {
217 clients: HashMap::new(),
218 documents: HashMap::new(),
219 diagnostics: DiagnosticsStore::new(),
220 event_tx,
221 event_rx,
222 binary_overrides: HashMap::new(),
223 extra_env: HashMap::new(),
224 failed_spawns: HashMap::new(),
225 watched_file_skip_logged: HashSet::new(),
226 child_registry: LspChildRegistry::new(),
227 }
228 }
229
230 pub fn set_child_registry(&mut self, registry: LspChildRegistry) {
232 self.child_registry = registry;
233 }
234
235 pub fn set_extra_env(&mut self, key: &str, value: &str) {
239 self.extra_env.insert(key.to_string(), value.to_string());
240 }
241
242 pub fn server_count(&self) -> usize {
244 self.clients.len()
245 }
246
247 pub fn override_binary(&mut self, kind: ServerKind, binary_path: PathBuf) {
249 self.binary_overrides.insert(kind, binary_path);
250 }
251
252 pub fn ensure_server_for_file(&mut self, file_path: &Path, config: &Config) -> Vec<ServerKey> {
259 self.ensure_server_for_file_detailed(file_path, config)
260 .successful
261 }
262
263 pub fn ensure_server_for_file_detailed(
271 &mut self,
272 file_path: &Path,
273 config: &Config,
274 ) -> EnsureServerOutcomes {
275 let defs = servers_for_file(file_path, config);
276 let mut outcomes = EnsureServerOutcomes::default();
277
278 for def in defs {
279 let server_id = def.kind.id_str().to_string();
280 let server_name = def.name.to_string();
281
282 let Some(root) = find_workspace_root(file_path, &def.root_markers) else {
283 outcomes.attempts.push(ServerAttempt {
284 server_id,
285 server_name,
286 result: ServerAttemptResult::NoRootMarker {
287 looked_for: def.root_markers.iter().map(|s| s.to_string()).collect(),
288 },
289 });
290 continue;
291 };
292
293 let key = ServerKey {
294 kind: def.kind.clone(),
295 root,
296 };
297
298 if !self.clients.contains_key(&key) {
299 if let Some(cached) = self.failed_spawns.get(&key) {
306 outcomes.attempts.push(ServerAttempt {
307 server_id,
308 server_name,
309 result: cached.clone(),
310 });
311 continue;
312 }
313
314 match self.spawn_server(&def, &key.root, config) {
315 Ok(client) => {
316 self.clients.insert(key.clone(), client);
317 self.documents.entry(key.clone()).or_default();
318 }
319 Err(err) => {
320 slog_error!("failed to spawn {}: {}", def.name, err);
321 let result = classify_spawn_error(&def.binary, &err);
322 self.failed_spawns.insert(key.clone(), result.clone());
326 outcomes.attempts.push(ServerAttempt {
327 server_id,
328 server_name,
329 result,
330 });
331 continue;
332 }
333 }
334 }
335
336 outcomes.attempts.push(ServerAttempt {
337 server_id,
338 server_name,
339 result: ServerAttemptResult::Ok {
340 server_key: key.clone(),
341 },
342 });
343 outcomes.successful.push(key);
344 }
345
346 outcomes
347 }
348
349 pub fn ensure_server_for_file_default(&mut self, file_path: &Path) -> Vec<ServerKey> {
352 self.ensure_server_for_file(file_path, &Config::default())
353 }
354 pub fn ensure_file_open(
358 &mut self,
359 file_path: &Path,
360 config: &Config,
361 ) -> Result<Vec<ServerKey>, LspError> {
362 let canonical_path = canonicalize_for_lsp(file_path)?;
363 let server_keys = self.ensure_server_for_file(&canonical_path, config);
364 if server_keys.is_empty() {
365 return Ok(server_keys);
366 }
367
368 let uri = uri_for_path(&canonical_path)?;
369 let language_id = language_id_for_extension(
370 canonical_path
371 .extension()
372 .and_then(|ext| ext.to_str())
373 .unwrap_or_default(),
374 )
375 .to_string();
376
377 for key in &server_keys {
378 let already_open = self
379 .documents
380 .get(key)
381 .is_some_and(|store| store.is_open(&canonical_path));
382
383 if !already_open {
384 let content = std::fs::read_to_string(&canonical_path).map_err(LspError::Io)?;
385 if let Some(client) = self.clients.get_mut(key) {
386 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
387 text_document: TextDocumentItem::new(
388 uri.clone(),
389 language_id.clone(),
390 0,
391 content,
392 ),
393 })?;
394 }
395 self.documents
396 .entry(key.clone())
397 .or_default()
398 .open(canonical_path.clone());
399 continue;
400 }
401
402 let drifted = self
412 .documents
413 .get(key)
414 .is_some_and(|store| store.is_stale_on_disk(&canonical_path));
415 if drifted {
416 let content = std::fs::read_to_string(&canonical_path).map_err(LspError::Io)?;
417 let next_version = self
418 .documents
419 .get(key)
420 .and_then(|store| store.version(&canonical_path))
421 .map(|v| v + 1)
422 .unwrap_or(1);
423 if let Some(client) = self.clients.get_mut(key) {
424 client.send_notification::<DidChangeTextDocument>(
425 DidChangeTextDocumentParams {
426 text_document: VersionedTextDocumentIdentifier::new(
427 uri.clone(),
428 next_version,
429 ),
430 content_changes: vec![TextDocumentContentChangeEvent {
431 range: None,
432 range_length: None,
433 text: content,
434 }],
435 },
436 )?;
437 }
438 if let Some(store) = self.documents.get_mut(key) {
439 store.bump_version(&canonical_path);
440 }
441 }
442 }
443
444 Ok(server_keys)
445 }
446
447 pub fn ensure_file_open_default(
448 &mut self,
449 file_path: &Path,
450 ) -> Result<Vec<ServerKey>, LspError> {
451 self.ensure_file_open(file_path, &Config::default())
452 }
453
454 pub fn notify_file_changed(
460 &mut self,
461 file_path: &Path,
462 content: &str,
463 config: &Config,
464 ) -> Result<(), LspError> {
465 self.notify_file_changed_versioned(file_path, content, config)
466 .map(|_| ())
467 }
468
469 pub fn notify_file_changed_versioned(
480 &mut self,
481 file_path: &Path,
482 content: &str,
483 config: &Config,
484 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
485 let canonical_path = canonicalize_for_lsp(file_path)?;
486 let server_keys = self.ensure_server_for_file(&canonical_path, config);
487 if server_keys.is_empty() {
488 return Ok(Vec::new());
489 }
490
491 let uri = uri_for_path(&canonical_path)?;
492 let language_id = language_id_for_extension(
493 canonical_path
494 .extension()
495 .and_then(|ext| ext.to_str())
496 .unwrap_or_default(),
497 )
498 .to_string();
499
500 let mut versions: Vec<(ServerKey, i32)> = Vec::with_capacity(server_keys.len());
501
502 for key in server_keys {
503 let current_version = self
504 .documents
505 .get(&key)
506 .and_then(|store| store.version(&canonical_path));
507
508 if let Some(version) = current_version {
509 let next_version = version + 1;
510 if let Some(client) = self.clients.get_mut(&key) {
511 client.send_notification::<DidChangeTextDocument>(
512 DidChangeTextDocumentParams {
513 text_document: VersionedTextDocumentIdentifier::new(
514 uri.clone(),
515 next_version,
516 ),
517 content_changes: vec![TextDocumentContentChangeEvent {
518 range: None,
519 range_length: None,
520 text: content.to_string(),
521 }],
522 },
523 )?;
524 }
525 if let Some(store) = self.documents.get_mut(&key) {
526 store.bump_version(&canonical_path);
527 }
528 versions.push((key, next_version));
529 continue;
530 }
531
532 if let Some(client) = self.clients.get_mut(&key) {
533 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
534 text_document: TextDocumentItem::new(
535 uri.clone(),
536 language_id.clone(),
537 0,
538 content.to_string(),
539 ),
540 })?;
541 }
542 self.documents
543 .entry(key.clone())
544 .or_default()
545 .open(canonical_path.clone());
546 versions.push((key, 0));
549 }
550
551 Ok(versions)
552 }
553
554 pub fn notify_file_changed_default(
555 &mut self,
556 file_path: &Path,
557 content: &str,
558 ) -> Result<(), LspError> {
559 self.notify_file_changed(file_path, content, &Config::default())
560 }
561
562 pub fn notify_files_watched_changed(
568 &mut self,
569 paths: &[(PathBuf, FileChangeType)],
570 _config: &Config,
571 ) -> Result<(), LspError> {
572 if paths.is_empty() {
573 return Ok(());
574 }
575
576 let mut canonical_events = Vec::with_capacity(paths.len());
577 for (path, typ) in paths {
578 let canonical_path = resolve_for_lsp_uri(path);
579 canonical_events.push((canonical_path, *typ));
580 }
581
582 let keys: Vec<ServerKey> = self.clients.keys().cloned().collect();
583 for key in keys {
584 let mut changes = Vec::new();
585 for (path, typ) in &canonical_events {
586 if !path.starts_with(&key.root) {
587 continue;
588 }
589 changes.push(FileEvent::new(uri_for_path(path)?, *typ));
590 }
591
592 if changes.is_empty() {
593 continue;
594 }
595
596 if let Some(client) = self.clients.get_mut(&key) {
597 if !client.has_watched_file_registration() {
601 if self.watched_file_skip_logged.insert(key.clone()) {
602 log::debug!(
603 "skipping didChangeWatchedFiles for {:?} (not dynamically registered)",
604 key
605 );
606 }
607 continue;
608 }
609 client.send_notification::<DidChangeWatchedFiles>(DidChangeWatchedFilesParams {
610 changes,
611 })?;
612 }
613 }
614
615 Ok(())
616 }
617
618 pub fn notify_file_closed(&mut self, file_path: &Path) -> Result<(), LspError> {
620 let canonical_path = canonicalize_for_lsp(file_path)?;
621 let uri = uri_for_path(&canonical_path)?;
622 let keys: Vec<ServerKey> = self.documents.keys().cloned().collect();
623
624 for key in keys {
625 let was_open = self
626 .documents
627 .get(&key)
628 .map(|store| store.is_open(&canonical_path))
629 .unwrap_or(false);
630 if !was_open {
631 continue;
632 }
633
634 if let Some(client) = self.clients.get_mut(&key) {
635 client.send_notification::<DidCloseTextDocument>(DidCloseTextDocumentParams {
636 text_document: TextDocumentIdentifier::new(uri.clone()),
637 })?;
638 }
639
640 if let Some(store) = self.documents.get_mut(&key) {
641 store.close(&canonical_path);
642 }
643 }
644
645 Ok(())
646 }
647
648 pub fn client_for_file(&self, file_path: &Path, config: &Config) -> Option<&LspClient> {
650 let key = self.server_key_for_file(file_path, config)?;
651 self.clients.get(&key)
652 }
653
654 pub fn client_for_file_default(&self, file_path: &Path) -> Option<&LspClient> {
655 self.client_for_file(file_path, &Config::default())
656 }
657
658 pub fn client_for_file_mut(
660 &mut self,
661 file_path: &Path,
662 config: &Config,
663 ) -> Option<&mut LspClient> {
664 let key = self.server_key_for_file(file_path, config)?;
665 self.clients.get_mut(&key)
666 }
667
668 pub fn client_for_file_mut_default(&mut self, file_path: &Path) -> Option<&mut LspClient> {
669 self.client_for_file_mut(file_path, &Config::default())
670 }
671
672 pub fn active_client_count(&self) -> usize {
674 self.clients.len()
675 }
676
677 pub fn drain_events(&mut self) -> Vec<LspEvent> {
679 let mut events = Vec::new();
680 while let Ok(event) = self.event_rx.try_recv() {
681 self.handle_event(&event);
682 events.push(event);
683 }
684 events
685 }
686
687 pub fn wait_for_diagnostics(
689 &mut self,
690 file_path: &Path,
691 config: &Config,
692 timeout: std::time::Duration,
693 ) -> Vec<StoredDiagnostic> {
694 let deadline = std::time::Instant::now() + timeout;
695 self.wait_for_file_diagnostics(file_path, config, deadline)
696 }
697
698 pub fn wait_for_diagnostics_default(
699 &mut self,
700 file_path: &Path,
701 timeout: std::time::Duration,
702 ) -> Vec<StoredDiagnostic> {
703 self.wait_for_diagnostics(file_path, &Config::default(), timeout)
704 }
705
706 #[doc(hidden)]
711 pub fn diagnostics_store_for_test(&self) -> &DiagnosticsStore {
712 &self.diagnostics
713 }
714
715 pub fn snapshot_diagnostic_epochs(&self, file_path: &Path) -> HashMap<ServerKey, u64> {
720 let lookup_path = normalize_lookup_path(file_path);
721 self.diagnostics
722 .entries_for_file(&lookup_path)
723 .into_iter()
724 .map(|(key, entry)| (key.clone(), entry.epoch))
725 .collect()
726 }
727
728 pub fn snapshot_pre_edit_state(&self, file_path: &Path) -> HashMap<ServerKey, PreEditSnapshot> {
731 let lookup_path = normalize_lookup_path(file_path);
732 let mut snapshots: HashMap<ServerKey, PreEditSnapshot> = self
733 .diagnostics
734 .entries_for_file(&lookup_path)
735 .into_iter()
736 .map(|(key, entry)| {
737 (
738 key.clone(),
739 PreEditSnapshot {
740 epoch: entry.epoch,
741 document_version_at_capture: None,
742 },
743 )
744 })
745 .collect();
746
747 for (key, store) in &self.documents {
748 if let Some(version) = store.version(&lookup_path) {
749 snapshots
750 .entry(key.clone())
751 .or_default()
752 .document_version_at_capture = Some(version);
753 }
754 }
755
756 snapshots
757 }
758
759 pub fn wait_for_post_edit_diagnostics(
782 &mut self,
783 file_path: &Path,
784 _config: &Config,
788 expected_versions: &[(ServerKey, i32)],
789 pre_snapshot: &HashMap<ServerKey, PreEditSnapshot>,
790 timeout: std::time::Duration,
791 ) -> PostEditWaitOutcome {
792 let lookup_path = normalize_lookup_path(file_path);
793 let deadline = std::time::Instant::now() + timeout;
794
795 let _ = self.drain_events_for_file(&lookup_path);
800
801 let mut fresh: HashMap<ServerKey, Vec<StoredDiagnostic>> = HashMap::new();
802 let mut exited: Vec<ServerKey> = Vec::new();
803
804 loop {
805 for (key, target_version) in expected_versions {
813 if fresh.contains_key(key) || exited.contains(key) {
814 continue;
815 }
816 if !self.clients.contains_key(key) {
817 exited.push(key.clone());
818 continue;
819 }
820 if let Some(entry) = self
821 .diagnostics
822 .entries_for_file(&lookup_path)
823 .into_iter()
824 .find_map(|(k, e)| if k == key { Some(e) } else { None })
825 {
826 let pre = pre_snapshot.get(key).copied().unwrap_or_default();
827 let is_fresh = post_edit_entry_is_fresh(entry, *target_version, pre);
828 if is_fresh {
829 fresh.insert(key.clone(), entry.diagnostics.clone());
830 }
831 }
832 }
833
834 if fresh.len() + exited.len() == expected_versions.len() {
836 break;
837 }
838
839 let now = std::time::Instant::now();
840 if now >= deadline {
841 break;
842 }
843
844 let timeout = deadline.saturating_duration_since(now);
845 match self.event_rx.recv_timeout(timeout) {
846 Ok(event) => {
847 self.handle_event(&event);
848 }
849 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
850 }
851 }
852
853 let pending: Vec<ServerKey> = expected_versions
855 .iter()
856 .filter(|(k, _)| !fresh.contains_key(k) && !exited.contains(k))
857 .map(|(k, _)| k.clone())
858 .collect();
859
860 let mut diagnostics: Vec<StoredDiagnostic> = fresh
863 .into_iter()
864 .flat_map(|(_, diags)| diags.into_iter())
865 .collect();
866 diagnostics.sort_by(|a, b| {
867 a.file
868 .cmp(&b.file)
869 .then(a.line.cmp(&b.line))
870 .then(a.column.cmp(&b.column))
871 .then(a.message.cmp(&b.message))
872 });
873
874 PostEditWaitOutcome {
875 diagnostics,
876 pending_servers: pending,
877 exited_servers: exited,
878 }
879 }
880
881 pub fn wait_for_file_diagnostics(
887 &mut self,
888 file_path: &Path,
889 config: &Config,
890 deadline: std::time::Instant,
891 ) -> Vec<StoredDiagnostic> {
892 let lookup_path = normalize_lookup_path(file_path);
893
894 if self.server_key_for_file(&lookup_path, config).is_none() {
895 return Vec::new();
896 }
897
898 loop {
899 if self.drain_events_for_file(&lookup_path) {
900 break;
901 }
902
903 let now = std::time::Instant::now();
904 if now >= deadline {
905 break;
906 }
907
908 let timeout = deadline.saturating_duration_since(now);
909 match self.event_rx.recv_timeout(timeout) {
910 Ok(event) => {
911 if matches!(
912 self.handle_event(&event),
913 Some(ref published_file) if published_file.as_path() == lookup_path.as_path()
914 ) {
915 break;
916 }
917 }
918 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
919 }
920 }
921
922 self.get_diagnostics_for_file(&lookup_path)
923 .into_iter()
924 .cloned()
925 .collect()
926 }
927
928 pub const PULL_FILE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
934
935 pub fn pull_file_timeout() -> std::time::Duration {
937 Self::PULL_FILE_TIMEOUT
938 }
939
940 const PULL_WORKSPACE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
944
945 pub fn pull_file_diagnostics(
956 &mut self,
957 file_path: &Path,
958 config: &Config,
959 ) -> Result<Vec<PullFileResult>, LspError> {
960 let canonical_path = canonicalize_for_lsp(file_path)?;
961 self.ensure_file_open(&canonical_path, config)?;
964
965 let server_keys = self.ensure_server_for_file(&canonical_path, config);
966 if server_keys.is_empty() {
967 return Ok(Vec::new());
968 }
969
970 let uri = uri_for_path(&canonical_path)?;
971 let mut results = Vec::with_capacity(server_keys.len());
972
973 for key in server_keys {
974 let supports_pull = self
975 .clients
976 .get(&key)
977 .and_then(|c| c.diagnostic_capabilities())
978 .is_some_and(|caps| caps.pull_diagnostics);
979
980 if !supports_pull {
981 results.push(PullFileResult {
982 server_key: key.clone(),
983 outcome: PullFileOutcome::PullNotSupported,
984 });
985 continue;
986 }
987
988 let previous_result_id = self
990 .diagnostics
991 .entries_for_file(&canonical_path)
992 .into_iter()
993 .find(|(k, _)| **k == key)
994 .and_then(|(_, entry)| entry.result_id.clone());
995
996 let identifier = self
997 .clients
998 .get(&key)
999 .and_then(|c| c.diagnostic_capabilities())
1000 .and_then(|caps| caps.identifier.clone());
1001
1002 let params = lsp_types::DocumentDiagnosticParams {
1003 text_document: lsp_types::TextDocumentIdentifier { uri: uri.clone() },
1004 identifier,
1005 previous_result_id,
1006 work_done_progress_params: Default::default(),
1007 partial_result_params: Default::default(),
1008 };
1009
1010 let outcome = match self.send_pull_request(&key, params) {
1011 Ok(report) => self.ingest_document_report(&key, &canonical_path, report),
1012 Err(err) => PullFileOutcome::RequestFailed {
1013 reason: err.to_string(),
1014 },
1015 };
1016
1017 results.push(PullFileResult {
1018 server_key: key,
1019 outcome,
1020 });
1021 }
1022
1023 Ok(results)
1024 }
1025
1026 pub fn pull_workspace_diagnostics(
1031 &mut self,
1032 server_key: &ServerKey,
1033 timeout: Option<std::time::Duration>,
1034 ) -> Result<PullWorkspaceResult, LspError> {
1035 let timeout = timeout.unwrap_or(Self::PULL_WORKSPACE_TIMEOUT);
1036
1037 let supports_workspace = self
1038 .clients
1039 .get(server_key)
1040 .and_then(|c| c.diagnostic_capabilities())
1041 .is_some_and(|caps| caps.workspace_diagnostics);
1042
1043 if !supports_workspace {
1044 return Ok(PullWorkspaceResult {
1045 server_key: server_key.clone(),
1046 files_reported: Vec::new(),
1047 complete: false,
1048 cancelled: false,
1049 supports_workspace: false,
1050 });
1051 }
1052
1053 let identifier = self
1054 .clients
1055 .get(server_key)
1056 .and_then(|c| c.diagnostic_capabilities())
1057 .and_then(|caps| caps.identifier.clone());
1058
1059 let params = lsp_types::WorkspaceDiagnosticParams {
1060 identifier,
1061 previous_result_ids: Vec::new(),
1062 work_done_progress_params: Default::default(),
1063 partial_result_params: Default::default(),
1064 };
1065
1066 let result = match self
1067 .clients
1068 .get_mut(server_key)
1069 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?
1070 .send_request_with_timeout::<lsp_types::request::WorkspaceDiagnosticRequest>(
1071 params, timeout,
1072 ) {
1073 Ok(result) => result,
1074 Err(LspError::Timeout(_)) => {
1075 return Ok(PullWorkspaceResult {
1076 server_key: server_key.clone(),
1077 files_reported: Vec::new(),
1078 complete: false,
1079 cancelled: true,
1080 supports_workspace: true,
1081 });
1082 }
1083 Err(err) => return Err(err),
1084 };
1085
1086 let items = match result {
1091 lsp_types::WorkspaceDiagnosticReportResult::Report(report) => report.items,
1092 lsp_types::WorkspaceDiagnosticReportResult::Partial(_) => Vec::new(),
1093 };
1094
1095 let mut files_reported = Vec::with_capacity(items.len());
1097 for item in items {
1098 match item {
1099 lsp_types::WorkspaceDocumentDiagnosticReport::Full(full) => {
1100 if let Some(file) = uri_to_path(&full.uri) {
1101 let stored = from_lsp_diagnostics(
1102 file.clone(),
1103 full.full_document_diagnostic_report.items.clone(),
1104 );
1105 self.diagnostics.publish_with_result_id(
1106 server_key.clone(),
1107 file.clone(),
1108 stored,
1109 full.full_document_diagnostic_report.result_id.clone(),
1110 );
1111 files_reported.push(file);
1112 }
1113 }
1114 lsp_types::WorkspaceDocumentDiagnosticReport::Unchanged(_unchanged) => {
1115 }
1118 }
1119 }
1120
1121 Ok(PullWorkspaceResult {
1122 server_key: server_key.clone(),
1123 files_reported,
1124 complete: true,
1125 cancelled: false,
1126 supports_workspace: true,
1127 })
1128 }
1129
1130 fn send_pull_request(
1132 &mut self,
1133 key: &ServerKey,
1134 params: lsp_types::DocumentDiagnosticParams,
1135 ) -> Result<lsp_types::DocumentDiagnosticReportResult, LspError> {
1136 let client = self
1137 .clients
1138 .get_mut(key)
1139 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?;
1140 client.send_request::<lsp_types::request::DocumentDiagnosticRequest>(params)
1141 }
1142
1143 fn ingest_document_report(
1146 &mut self,
1147 key: &ServerKey,
1148 canonical_path: &Path,
1149 result: lsp_types::DocumentDiagnosticReportResult,
1150 ) -> PullFileOutcome {
1151 let report = match result {
1152 lsp_types::DocumentDiagnosticReportResult::Report(report) => report,
1153 lsp_types::DocumentDiagnosticReportResult::Partial(_) => {
1154 return PullFileOutcome::PartialNotSupported;
1158 }
1159 };
1160
1161 match report {
1162 lsp_types::DocumentDiagnosticReport::Full(full) => {
1163 let result_id = full.full_document_diagnostic_report.result_id.clone();
1164 let stored = from_lsp_diagnostics(
1165 canonical_path.to_path_buf(),
1166 full.full_document_diagnostic_report.items.clone(),
1167 );
1168 let count = stored.len();
1169 self.diagnostics.publish_with_result_id(
1170 key.clone(),
1171 canonical_path.to_path_buf(),
1172 stored,
1173 result_id,
1174 );
1175 PullFileOutcome::Full {
1176 diagnostic_count: count,
1177 }
1178 }
1179 lsp_types::DocumentDiagnosticReport::Unchanged(_unchanged) => {
1180 PullFileOutcome::Unchanged
1183 }
1184 }
1185 }
1186
1187 pub fn shutdown_all(&mut self) {
1189 for (key, mut client) in self.clients.drain() {
1190 if let Err(err) = client.shutdown() {
1191 slog_error!("error shutting down {:?}: {}", key, err);
1192 }
1193 }
1194 self.documents.clear();
1195 self.diagnostics = DiagnosticsStore::new();
1196 }
1197
1198 pub fn has_active_servers(&self) -> bool {
1200 self.clients
1201 .values()
1202 .any(|client| client.state() == ServerState::Ready)
1203 }
1204
1205 pub fn active_server_keys(&self) -> Vec<ServerKey> {
1208 self.clients.keys().cloned().collect()
1209 }
1210
1211 pub fn get_diagnostics_for_file(&self, file: &Path) -> Vec<&StoredDiagnostic> {
1212 let normalized = normalize_lookup_path(file);
1213 self.diagnostics.for_file(&normalized)
1214 }
1215
1216 pub fn get_diagnostics_for_directory(&self, dir: &Path) -> Vec<&StoredDiagnostic> {
1217 let normalized = normalize_lookup_path(dir);
1218 self.diagnostics.for_directory(&normalized)
1219 }
1220
1221 pub fn get_all_diagnostics(&self) -> Vec<&StoredDiagnostic> {
1222 self.diagnostics.all()
1223 }
1224
1225 fn drain_events_for_file(&mut self, file_path: &Path) -> bool {
1226 let mut saw_file_diagnostics = false;
1227 while let Ok(event) = self.event_rx.try_recv() {
1228 if matches!(
1229 self.handle_event(&event),
1230 Some(ref published_file) if published_file.as_path() == file_path
1231 ) {
1232 saw_file_diagnostics = true;
1233 }
1234 }
1235 saw_file_diagnostics
1236 }
1237
1238 fn handle_event(&mut self, event: &LspEvent) -> Option<PathBuf> {
1239 match event {
1240 LspEvent::Notification {
1241 server_kind,
1242 root,
1243 method,
1244 params: Some(params),
1245 } if method == "textDocument/publishDiagnostics" => {
1246 self.handle_publish_diagnostics(server_kind.clone(), root.clone(), params)
1247 }
1248 LspEvent::ServerExited { server_kind, root } => {
1249 let key = ServerKey {
1250 kind: server_kind.clone(),
1251 root: root.clone(),
1252 };
1253 self.clients.remove(&key);
1254 self.documents.remove(&key);
1255 self.diagnostics.clear_for_server(&key);
1256 None
1257 }
1258 _ => None,
1259 }
1260 }
1261
1262 fn handle_publish_diagnostics(
1263 &mut self,
1264 server: ServerKind,
1265 root: PathBuf,
1266 params: &serde_json::Value,
1267 ) -> Option<PathBuf> {
1268 if let Ok(publish_params) =
1269 serde_json::from_value::<lsp_types::PublishDiagnosticsParams>(params.clone())
1270 {
1271 let file = uri_to_path(&publish_params.uri)?;
1272 let stored = from_lsp_diagnostics(file.clone(), publish_params.diagnostics);
1273 let key = ServerKey { kind: server, root };
1279 self.diagnostics
1280 .publish_full(key, file.clone(), stored, None, publish_params.version);
1281 return Some(file);
1282 }
1283 None
1284 }
1285
1286 fn spawn_server(
1287 &self,
1288 def: &ServerDef,
1289 root: &Path,
1290 config: &Config,
1291 ) -> Result<LspClient, LspError> {
1292 let binary = self.resolve_binary(def, config)?;
1293
1294 let mut merged_env = def.env.clone();
1298 for (key, value) in &self.extra_env {
1299 merged_env.insert(key.clone(), value.clone());
1300 }
1301
1302 let mut client = LspClient::spawn(
1303 def.kind.clone(),
1304 root.to_path_buf(),
1305 &binary,
1306 &def.args,
1307 &merged_env,
1308 self.event_tx.clone(),
1309 self.child_registry.clone(),
1310 )?;
1311 client.initialize(root, def.initialization_options.clone())?;
1312 Ok(client)
1313 }
1314
1315 fn resolve_binary(&self, def: &ServerDef, config: &Config) -> Result<PathBuf, LspError> {
1316 if let Some(path) = self.binary_overrides.get(&def.kind) {
1317 if path.exists() {
1318 return Ok(path.clone());
1319 }
1320 return Err(LspError::NotFound(format!(
1321 "override binary for {:?} not found: {}",
1322 def.kind,
1323 path.display()
1324 )));
1325 }
1326
1327 if let Some(path) = env_binary_override(&def.kind) {
1328 if path.exists() {
1329 return Ok(path);
1330 }
1331 return Err(LspError::NotFound(format!(
1332 "environment override binary for {:?} not found: {}",
1333 def.kind,
1334 path.display()
1335 )));
1336 }
1337
1338 resolve_lsp_binary(
1343 &def.binary,
1344 config.project_root.as_deref(),
1345 &config.lsp_paths_extra,
1346 )
1347 .ok_or_else(|| {
1348 LspError::NotFound(format!(
1349 "language server binary '{}' not found in node_modules/.bin, lsp_paths_extra, or PATH",
1350 def.binary
1351 ))
1352 })
1353 }
1354
1355 fn server_key_for_file(&self, file_path: &Path, config: &Config) -> Option<ServerKey> {
1356 for def in servers_for_file(file_path, config) {
1357 let root = find_workspace_root(file_path, &def.root_markers)?;
1358 let key = ServerKey {
1359 kind: def.kind.clone(),
1360 root,
1361 };
1362 if self.clients.contains_key(&key) {
1363 return Some(key);
1364 }
1365 }
1366 None
1367 }
1368}
1369
1370impl Default for LspManager {
1371 fn default() -> Self {
1372 Self::new()
1373 }
1374}
1375
1376fn canonicalize_for_lsp(file_path: &Path) -> Result<PathBuf, LspError> {
1377 std::fs::canonicalize(file_path).map_err(LspError::from)
1378}
1379
1380fn resolve_for_lsp_uri(file_path: &Path) -> PathBuf {
1381 if let Ok(path) = std::fs::canonicalize(file_path) {
1382 return path;
1383 }
1384
1385 let mut existing = file_path.to_path_buf();
1386 let mut missing = Vec::new();
1387 while !existing.exists() {
1388 let Some(name) = existing.file_name() else {
1389 break;
1390 };
1391 missing.push(name.to_owned());
1392 let Some(parent) = existing.parent() else {
1393 break;
1394 };
1395 existing = parent.to_path_buf();
1396 }
1397
1398 let mut resolved = std::fs::canonicalize(&existing).unwrap_or(existing);
1399 for segment in missing.into_iter().rev() {
1400 resolved.push(segment);
1401 }
1402 resolved
1403}
1404
1405fn language_id_for_extension(ext: &str) -> &'static str {
1406 match ext {
1407 "ts" => "typescript",
1408 "tsx" => "typescriptreact",
1409 "js" | "mjs" | "cjs" => "javascript",
1410 "jsx" => "javascriptreact",
1411 "py" | "pyi" => "python",
1412 "rs" => "rust",
1413 "go" => "go",
1414 "html" | "htm" => "html",
1415 _ => "plaintext",
1416 }
1417}
1418
1419fn normalize_lookup_path(path: &Path) -> PathBuf {
1420 std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
1421}
1422
1423fn classify_spawn_error(binary: &str, err: &LspError) -> ServerAttemptResult {
1430 match err {
1431 LspError::NotFound(_) => ServerAttemptResult::BinaryNotInstalled {
1436 binary: binary.to_string(),
1437 },
1438 other => ServerAttemptResult::SpawnFailed {
1439 binary: binary.to_string(),
1440 reason: other.to_string(),
1441 },
1442 }
1443}
1444
1445fn env_binary_override(kind: &ServerKind) -> Option<PathBuf> {
1446 let id = kind.id_str();
1447 let suffix: String = id
1448 .chars()
1449 .map(|ch| {
1450 if ch.is_ascii_alphanumeric() {
1451 ch.to_ascii_uppercase()
1452 } else {
1453 '_'
1454 }
1455 })
1456 .collect();
1457 let key = format!("AFT_LSP_{suffix}_BINARY");
1458 std::env::var_os(key).map(PathBuf::from)
1459}