1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3use std::str::FromStr;
4
5use crossbeam_channel::{unbounded, Receiver, RecvTimeoutError, Sender};
6use lsp_types::notification::{
7 DidChangeTextDocument, DidChangeWatchedFiles, DidCloseTextDocument, DidOpenTextDocument,
8};
9use lsp_types::{
10 DidChangeTextDocumentParams, DidChangeWatchedFilesParams, DidCloseTextDocumentParams,
11 DidOpenTextDocumentParams, FileChangeType, FileEvent, TextDocumentContentChangeEvent,
12 TextDocumentIdentifier, TextDocumentItem, VersionedTextDocumentIdentifier,
13};
14
15use crate::config::Config;
16use crate::lsp::child_registry::LspChildRegistry;
17use crate::lsp::client::{LspClient, LspEvent, ServerState};
18use crate::lsp::diagnostics::{
19 from_lsp_diagnostics, DiagnosticEntry, DiagnosticsStore, StoredDiagnostic,
20};
21use crate::lsp::document::DocumentStore;
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.supports_watched_files() {
602 if self.watched_file_skip_logged.insert(key.clone()) {
603 log::debug!(
604 "skipping didChangeWatchedFiles for {:?} (capability not declared)",
605 key
606 );
607 }
608 continue;
609 }
610 client.send_notification::<DidChangeWatchedFiles>(DidChangeWatchedFilesParams {
611 changes,
612 })?;
613 }
614 }
615
616 Ok(())
617 }
618
619 pub fn notify_file_closed(&mut self, file_path: &Path) -> Result<(), LspError> {
621 let canonical_path = canonicalize_for_lsp(file_path)?;
622 let uri = uri_for_path(&canonical_path)?;
623 let keys: Vec<ServerKey> = self.documents.keys().cloned().collect();
624
625 for key in keys {
626 let was_open = self
627 .documents
628 .get(&key)
629 .map(|store| store.is_open(&canonical_path))
630 .unwrap_or(false);
631 if !was_open {
632 continue;
633 }
634
635 if let Some(client) = self.clients.get_mut(&key) {
636 client.send_notification::<DidCloseTextDocument>(DidCloseTextDocumentParams {
637 text_document: TextDocumentIdentifier::new(uri.clone()),
638 })?;
639 }
640
641 if let Some(store) = self.documents.get_mut(&key) {
642 store.close(&canonical_path);
643 }
644 }
645
646 Ok(())
647 }
648
649 pub fn client_for_file(&self, file_path: &Path, config: &Config) -> Option<&LspClient> {
651 let key = self.server_key_for_file(file_path, config)?;
652 self.clients.get(&key)
653 }
654
655 pub fn client_for_file_default(&self, file_path: &Path) -> Option<&LspClient> {
656 self.client_for_file(file_path, &Config::default())
657 }
658
659 pub fn client_for_file_mut(
661 &mut self,
662 file_path: &Path,
663 config: &Config,
664 ) -> Option<&mut LspClient> {
665 let key = self.server_key_for_file(file_path, config)?;
666 self.clients.get_mut(&key)
667 }
668
669 pub fn client_for_file_mut_default(&mut self, file_path: &Path) -> Option<&mut LspClient> {
670 self.client_for_file_mut(file_path, &Config::default())
671 }
672
673 pub fn active_client_count(&self) -> usize {
675 self.clients.len()
676 }
677
678 pub fn drain_events(&mut self) -> Vec<LspEvent> {
680 let mut events = Vec::new();
681 while let Ok(event) = self.event_rx.try_recv() {
682 self.handle_event(&event);
683 events.push(event);
684 }
685 events
686 }
687
688 pub fn wait_for_diagnostics(
690 &mut self,
691 file_path: &Path,
692 config: &Config,
693 timeout: std::time::Duration,
694 ) -> Vec<StoredDiagnostic> {
695 let deadline = std::time::Instant::now() + timeout;
696 self.wait_for_file_diagnostics(file_path, config, deadline)
697 }
698
699 pub fn wait_for_diagnostics_default(
700 &mut self,
701 file_path: &Path,
702 timeout: std::time::Duration,
703 ) -> Vec<StoredDiagnostic> {
704 self.wait_for_diagnostics(file_path, &Config::default(), timeout)
705 }
706
707 #[doc(hidden)]
712 pub fn diagnostics_store_for_test(&self) -> &DiagnosticsStore {
713 &self.diagnostics
714 }
715
716 pub fn snapshot_diagnostic_epochs(&self, file_path: &Path) -> HashMap<ServerKey, u64> {
721 let lookup_path = normalize_lookup_path(file_path);
722 self.diagnostics
723 .entries_for_file(&lookup_path)
724 .into_iter()
725 .map(|(key, entry)| (key.clone(), entry.epoch))
726 .collect()
727 }
728
729 pub fn snapshot_pre_edit_state(&self, file_path: &Path) -> HashMap<ServerKey, PreEditSnapshot> {
732 let lookup_path = normalize_lookup_path(file_path);
733 let mut snapshots: HashMap<ServerKey, PreEditSnapshot> = self
734 .diagnostics
735 .entries_for_file(&lookup_path)
736 .into_iter()
737 .map(|(key, entry)| {
738 (
739 key.clone(),
740 PreEditSnapshot {
741 epoch: entry.epoch,
742 document_version_at_capture: None,
743 },
744 )
745 })
746 .collect();
747
748 for (key, store) in &self.documents {
749 if let Some(version) = store.version(&lookup_path) {
750 snapshots
751 .entry(key.clone())
752 .or_default()
753 .document_version_at_capture = Some(version);
754 }
755 }
756
757 snapshots
758 }
759
760 pub fn wait_for_post_edit_diagnostics(
783 &mut self,
784 file_path: &Path,
785 _config: &Config,
789 expected_versions: &[(ServerKey, i32)],
790 pre_snapshot: &HashMap<ServerKey, PreEditSnapshot>,
791 timeout: std::time::Duration,
792 ) -> PostEditWaitOutcome {
793 let lookup_path = normalize_lookup_path(file_path);
794 let deadline = std::time::Instant::now() + timeout;
795
796 let _ = self.drain_events_for_file(&lookup_path);
801
802 let mut fresh: HashMap<ServerKey, Vec<StoredDiagnostic>> = HashMap::new();
803 let mut exited: Vec<ServerKey> = Vec::new();
804
805 loop {
806 for (key, target_version) in expected_versions {
814 if fresh.contains_key(key) || exited.contains(key) {
815 continue;
816 }
817 if !self.clients.contains_key(key) {
818 exited.push(key.clone());
819 continue;
820 }
821 if let Some(entry) = self
822 .diagnostics
823 .entries_for_file(&lookup_path)
824 .into_iter()
825 .find_map(|(k, e)| if k == key { Some(e) } else { None })
826 {
827 let pre = pre_snapshot.get(key).copied().unwrap_or_default();
828 let is_fresh = post_edit_entry_is_fresh(entry, *target_version, pre);
829 if is_fresh {
830 fresh.insert(key.clone(), entry.diagnostics.clone());
831 }
832 }
833 }
834
835 if fresh.len() + exited.len() == expected_versions.len() {
837 break;
838 }
839
840 let now = std::time::Instant::now();
841 if now >= deadline {
842 break;
843 }
844
845 let timeout = deadline.saturating_duration_since(now);
846 match self.event_rx.recv_timeout(timeout) {
847 Ok(event) => {
848 self.handle_event(&event);
849 }
850 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
851 }
852 }
853
854 let pending: Vec<ServerKey> = expected_versions
856 .iter()
857 .filter(|(k, _)| !fresh.contains_key(k) && !exited.contains(k))
858 .map(|(k, _)| k.clone())
859 .collect();
860
861 let mut diagnostics: Vec<StoredDiagnostic> = fresh
864 .into_iter()
865 .flat_map(|(_, diags)| diags.into_iter())
866 .collect();
867 diagnostics.sort_by(|a, b| {
868 a.file
869 .cmp(&b.file)
870 .then(a.line.cmp(&b.line))
871 .then(a.column.cmp(&b.column))
872 .then(a.message.cmp(&b.message))
873 });
874
875 PostEditWaitOutcome {
876 diagnostics,
877 pending_servers: pending,
878 exited_servers: exited,
879 }
880 }
881
882 pub fn wait_for_file_diagnostics(
888 &mut self,
889 file_path: &Path,
890 config: &Config,
891 deadline: std::time::Instant,
892 ) -> Vec<StoredDiagnostic> {
893 let lookup_path = normalize_lookup_path(file_path);
894
895 if self.server_key_for_file(&lookup_path, config).is_none() {
896 return Vec::new();
897 }
898
899 loop {
900 if self.drain_events_for_file(&lookup_path) {
901 break;
902 }
903
904 let now = std::time::Instant::now();
905 if now >= deadline {
906 break;
907 }
908
909 let timeout = deadline.saturating_duration_since(now);
910 match self.event_rx.recv_timeout(timeout) {
911 Ok(event) => {
912 if matches!(
913 self.handle_event(&event),
914 Some(ref published_file) if published_file.as_path() == lookup_path.as_path()
915 ) {
916 break;
917 }
918 }
919 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
920 }
921 }
922
923 self.get_diagnostics_for_file(&lookup_path)
924 .into_iter()
925 .cloned()
926 .collect()
927 }
928
929 pub const PULL_FILE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
935
936 pub fn pull_file_timeout() -> std::time::Duration {
938 Self::PULL_FILE_TIMEOUT
939 }
940
941 const PULL_WORKSPACE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
945
946 pub fn pull_file_diagnostics(
957 &mut self,
958 file_path: &Path,
959 config: &Config,
960 ) -> Result<Vec<PullFileResult>, LspError> {
961 let canonical_path = canonicalize_for_lsp(file_path)?;
962 self.ensure_file_open(&canonical_path, config)?;
965
966 let server_keys = self.ensure_server_for_file(&canonical_path, config);
967 if server_keys.is_empty() {
968 return Ok(Vec::new());
969 }
970
971 let uri = uri_for_path(&canonical_path)?;
972 let mut results = Vec::with_capacity(server_keys.len());
973
974 for key in server_keys {
975 let supports_pull = self
976 .clients
977 .get(&key)
978 .and_then(|c| c.diagnostic_capabilities())
979 .is_some_and(|caps| caps.pull_diagnostics);
980
981 if !supports_pull {
982 results.push(PullFileResult {
983 server_key: key.clone(),
984 outcome: PullFileOutcome::PullNotSupported,
985 });
986 continue;
987 }
988
989 let previous_result_id = self
991 .diagnostics
992 .entries_for_file(&canonical_path)
993 .into_iter()
994 .find(|(k, _)| **k == key)
995 .and_then(|(_, entry)| entry.result_id.clone());
996
997 let identifier = self
998 .clients
999 .get(&key)
1000 .and_then(|c| c.diagnostic_capabilities())
1001 .and_then(|caps| caps.identifier.clone());
1002
1003 let params = lsp_types::DocumentDiagnosticParams {
1004 text_document: lsp_types::TextDocumentIdentifier { uri: uri.clone() },
1005 identifier,
1006 previous_result_id,
1007 work_done_progress_params: Default::default(),
1008 partial_result_params: Default::default(),
1009 };
1010
1011 let outcome = match self.send_pull_request(&key, params) {
1012 Ok(report) => self.ingest_document_report(&key, &canonical_path, report),
1013 Err(err) => PullFileOutcome::RequestFailed {
1014 reason: err.to_string(),
1015 },
1016 };
1017
1018 results.push(PullFileResult {
1019 server_key: key,
1020 outcome,
1021 });
1022 }
1023
1024 Ok(results)
1025 }
1026
1027 pub fn pull_workspace_diagnostics(
1032 &mut self,
1033 server_key: &ServerKey,
1034 timeout: Option<std::time::Duration>,
1035 ) -> Result<PullWorkspaceResult, LspError> {
1036 let _timeout = timeout.unwrap_or(Self::PULL_WORKSPACE_TIMEOUT);
1037
1038 let supports_workspace = self
1039 .clients
1040 .get(server_key)
1041 .and_then(|c| c.diagnostic_capabilities())
1042 .is_some_and(|caps| caps.workspace_diagnostics);
1043
1044 if !supports_workspace {
1045 return Ok(PullWorkspaceResult {
1046 server_key: server_key.clone(),
1047 files_reported: Vec::new(),
1048 complete: false,
1049 cancelled: false,
1050 supports_workspace: false,
1051 });
1052 }
1053
1054 let identifier = self
1055 .clients
1056 .get(server_key)
1057 .and_then(|c| c.diagnostic_capabilities())
1058 .and_then(|caps| caps.identifier.clone());
1059
1060 let params = lsp_types::WorkspaceDiagnosticParams {
1061 identifier,
1062 previous_result_ids: Vec::new(),
1063 work_done_progress_params: Default::default(),
1064 partial_result_params: Default::default(),
1065 };
1066
1067 let result = match self
1075 .clients
1076 .get_mut(server_key)
1077 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?
1078 .send_request::<lsp_types::request::WorkspaceDiagnosticRequest>(params)
1079 {
1080 Ok(result) => result,
1081 Err(LspError::Timeout(_)) => {
1082 return Ok(PullWorkspaceResult {
1083 server_key: server_key.clone(),
1084 files_reported: Vec::new(),
1085 complete: false,
1086 cancelled: true,
1087 supports_workspace: true,
1088 });
1089 }
1090 Err(err) => return Err(err),
1091 };
1092
1093 let items = match result {
1098 lsp_types::WorkspaceDiagnosticReportResult::Report(report) => report.items,
1099 lsp_types::WorkspaceDiagnosticReportResult::Partial(_) => Vec::new(),
1100 };
1101
1102 let mut files_reported = Vec::with_capacity(items.len());
1104 for item in items {
1105 match item {
1106 lsp_types::WorkspaceDocumentDiagnosticReport::Full(full) => {
1107 if let Some(file) = uri_to_path(&full.uri) {
1108 let stored = from_lsp_diagnostics(
1109 file.clone(),
1110 full.full_document_diagnostic_report.items.clone(),
1111 );
1112 self.diagnostics.publish_with_result_id(
1113 server_key.clone(),
1114 file.clone(),
1115 stored,
1116 full.full_document_diagnostic_report.result_id.clone(),
1117 );
1118 files_reported.push(file);
1119 }
1120 }
1121 lsp_types::WorkspaceDocumentDiagnosticReport::Unchanged(_unchanged) => {
1122 }
1125 }
1126 }
1127
1128 Ok(PullWorkspaceResult {
1129 server_key: server_key.clone(),
1130 files_reported,
1131 complete: true,
1132 cancelled: false,
1133 supports_workspace: true,
1134 })
1135 }
1136
1137 fn send_pull_request(
1139 &mut self,
1140 key: &ServerKey,
1141 params: lsp_types::DocumentDiagnosticParams,
1142 ) -> Result<lsp_types::DocumentDiagnosticReportResult, LspError> {
1143 let client = self
1144 .clients
1145 .get_mut(key)
1146 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?;
1147 client.send_request::<lsp_types::request::DocumentDiagnosticRequest>(params)
1148 }
1149
1150 fn ingest_document_report(
1153 &mut self,
1154 key: &ServerKey,
1155 canonical_path: &Path,
1156 result: lsp_types::DocumentDiagnosticReportResult,
1157 ) -> PullFileOutcome {
1158 let report = match result {
1159 lsp_types::DocumentDiagnosticReportResult::Report(report) => report,
1160 lsp_types::DocumentDiagnosticReportResult::Partial(_) => {
1161 return PullFileOutcome::PartialNotSupported;
1165 }
1166 };
1167
1168 match report {
1169 lsp_types::DocumentDiagnosticReport::Full(full) => {
1170 let result_id = full.full_document_diagnostic_report.result_id.clone();
1171 let stored = from_lsp_diagnostics(
1172 canonical_path.to_path_buf(),
1173 full.full_document_diagnostic_report.items.clone(),
1174 );
1175 let count = stored.len();
1176 self.diagnostics.publish_with_result_id(
1177 key.clone(),
1178 canonical_path.to_path_buf(),
1179 stored,
1180 result_id,
1181 );
1182 PullFileOutcome::Full {
1183 diagnostic_count: count,
1184 }
1185 }
1186 lsp_types::DocumentDiagnosticReport::Unchanged(_unchanged) => {
1187 PullFileOutcome::Unchanged
1190 }
1191 }
1192 }
1193
1194 pub fn shutdown_all(&mut self) {
1196 for (key, mut client) in self.clients.drain() {
1197 if let Err(err) = client.shutdown() {
1198 slog_error!("error shutting down {:?}: {}", key, err);
1199 }
1200 }
1201 self.documents.clear();
1202 self.diagnostics = DiagnosticsStore::new();
1203 }
1204
1205 pub fn has_active_servers(&self) -> bool {
1207 self.clients
1208 .values()
1209 .any(|client| client.state() == ServerState::Ready)
1210 }
1211
1212 pub fn active_server_keys(&self) -> Vec<ServerKey> {
1215 self.clients.keys().cloned().collect()
1216 }
1217
1218 pub fn get_diagnostics_for_file(&self, file: &Path) -> Vec<&StoredDiagnostic> {
1219 let normalized = normalize_lookup_path(file);
1220 self.diagnostics.for_file(&normalized)
1221 }
1222
1223 pub fn get_diagnostics_for_directory(&self, dir: &Path) -> Vec<&StoredDiagnostic> {
1224 let normalized = normalize_lookup_path(dir);
1225 self.diagnostics.for_directory(&normalized)
1226 }
1227
1228 pub fn get_all_diagnostics(&self) -> Vec<&StoredDiagnostic> {
1229 self.diagnostics.all()
1230 }
1231
1232 fn drain_events_for_file(&mut self, file_path: &Path) -> bool {
1233 let mut saw_file_diagnostics = false;
1234 while let Ok(event) = self.event_rx.try_recv() {
1235 if matches!(
1236 self.handle_event(&event),
1237 Some(ref published_file) if published_file.as_path() == file_path
1238 ) {
1239 saw_file_diagnostics = true;
1240 }
1241 }
1242 saw_file_diagnostics
1243 }
1244
1245 fn handle_event(&mut self, event: &LspEvent) -> Option<PathBuf> {
1246 match event {
1247 LspEvent::Notification {
1248 server_kind,
1249 root,
1250 method,
1251 params: Some(params),
1252 } if method == "textDocument/publishDiagnostics" => {
1253 self.handle_publish_diagnostics(server_kind.clone(), root.clone(), params)
1254 }
1255 LspEvent::ServerExited { server_kind, root } => {
1256 let key = ServerKey {
1257 kind: server_kind.clone(),
1258 root: root.clone(),
1259 };
1260 self.clients.remove(&key);
1261 self.documents.remove(&key);
1262 self.diagnostics.clear_for_server(&key);
1263 None
1264 }
1265 _ => None,
1266 }
1267 }
1268
1269 fn handle_publish_diagnostics(
1270 &mut self,
1271 server: ServerKind,
1272 root: PathBuf,
1273 params: &serde_json::Value,
1274 ) -> Option<PathBuf> {
1275 if let Ok(publish_params) =
1276 serde_json::from_value::<lsp_types::PublishDiagnosticsParams>(params.clone())
1277 {
1278 let file = uri_to_path(&publish_params.uri)?;
1279 let stored = from_lsp_diagnostics(file.clone(), publish_params.diagnostics);
1280 let key = ServerKey { kind: server, root };
1286 self.diagnostics
1287 .publish_full(key, file.clone(), stored, None, publish_params.version);
1288 return Some(file);
1289 }
1290 None
1291 }
1292
1293 fn spawn_server(
1294 &self,
1295 def: &ServerDef,
1296 root: &Path,
1297 config: &Config,
1298 ) -> Result<LspClient, LspError> {
1299 let binary = self.resolve_binary(def, config)?;
1300
1301 let mut merged_env = def.env.clone();
1305 for (key, value) in &self.extra_env {
1306 merged_env.insert(key.clone(), value.clone());
1307 }
1308
1309 let mut client = LspClient::spawn(
1310 def.kind.clone(),
1311 root.to_path_buf(),
1312 &binary,
1313 &def.args,
1314 &merged_env,
1315 self.event_tx.clone(),
1316 self.child_registry.clone(),
1317 )?;
1318 client.initialize(root, def.initialization_options.clone())?;
1319 Ok(client)
1320 }
1321
1322 fn resolve_binary(&self, def: &ServerDef, config: &Config) -> Result<PathBuf, LspError> {
1323 if let Some(path) = self.binary_overrides.get(&def.kind) {
1324 if path.exists() {
1325 return Ok(path.clone());
1326 }
1327 return Err(LspError::NotFound(format!(
1328 "override binary for {:?} not found: {}",
1329 def.kind,
1330 path.display()
1331 )));
1332 }
1333
1334 if let Some(path) = env_binary_override(&def.kind) {
1335 if path.exists() {
1336 return Ok(path);
1337 }
1338 return Err(LspError::NotFound(format!(
1339 "environment override binary for {:?} not found: {}",
1340 def.kind,
1341 path.display()
1342 )));
1343 }
1344
1345 resolve_lsp_binary(
1350 &def.binary,
1351 config.project_root.as_deref(),
1352 &config.lsp_paths_extra,
1353 )
1354 .ok_or_else(|| {
1355 LspError::NotFound(format!(
1356 "language server binary '{}' not found in node_modules/.bin, lsp_paths_extra, or PATH",
1357 def.binary
1358 ))
1359 })
1360 }
1361
1362 fn server_key_for_file(&self, file_path: &Path, config: &Config) -> Option<ServerKey> {
1363 for def in servers_for_file(file_path, config) {
1364 let root = find_workspace_root(file_path, &def.root_markers)?;
1365 let key = ServerKey {
1366 kind: def.kind.clone(),
1367 root,
1368 };
1369 if self.clients.contains_key(&key) {
1370 return Some(key);
1371 }
1372 }
1373 None
1374 }
1375}
1376
1377impl Default for LspManager {
1378 fn default() -> Self {
1379 Self::new()
1380 }
1381}
1382
1383fn canonicalize_for_lsp(file_path: &Path) -> Result<PathBuf, LspError> {
1384 std::fs::canonicalize(file_path).map_err(LspError::from)
1385}
1386
1387fn resolve_for_lsp_uri(file_path: &Path) -> PathBuf {
1388 if let Ok(path) = std::fs::canonicalize(file_path) {
1389 return path;
1390 }
1391
1392 let mut existing = file_path.to_path_buf();
1393 let mut missing = Vec::new();
1394 while !existing.exists() {
1395 let Some(name) = existing.file_name() else {
1396 break;
1397 };
1398 missing.push(name.to_owned());
1399 let Some(parent) = existing.parent() else {
1400 break;
1401 };
1402 existing = parent.to_path_buf();
1403 }
1404
1405 let mut resolved = std::fs::canonicalize(&existing).unwrap_or(existing);
1406 for segment in missing.into_iter().rev() {
1407 resolved.push(segment);
1408 }
1409 resolved
1410}
1411
1412fn uri_for_path(path: &Path) -> Result<lsp_types::Uri, LspError> {
1413 let url = url::Url::from_file_path(path).map_err(|_| {
1414 LspError::NotFound(format!(
1415 "failed to convert '{}' to file URI",
1416 path.display()
1417 ))
1418 })?;
1419 lsp_types::Uri::from_str(url.as_str()).map_err(|_| {
1420 LspError::NotFound(format!("failed to parse file URI for '{}'", path.display()))
1421 })
1422}
1423
1424fn language_id_for_extension(ext: &str) -> &'static str {
1425 match ext {
1426 "ts" => "typescript",
1427 "tsx" => "typescriptreact",
1428 "js" | "mjs" | "cjs" => "javascript",
1429 "jsx" => "javascriptreact",
1430 "py" | "pyi" => "python",
1431 "rs" => "rust",
1432 "go" => "go",
1433 "html" | "htm" => "html",
1434 _ => "plaintext",
1435 }
1436}
1437
1438fn normalize_lookup_path(path: &Path) -> PathBuf {
1439 std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
1440}
1441
1442fn uri_to_path(uri: &lsp_types::Uri) -> Option<PathBuf> {
1443 let url = url::Url::parse(uri.as_str()).ok()?;
1444 url.to_file_path()
1445 .ok()
1446 .map(|path| normalize_lookup_path(&path))
1447}
1448
1449fn classify_spawn_error(binary: &str, err: &LspError) -> ServerAttemptResult {
1456 match err {
1457 LspError::NotFound(_) => ServerAttemptResult::BinaryNotInstalled {
1462 binary: binary.to_string(),
1463 },
1464 other => ServerAttemptResult::SpawnFailed {
1465 binary: binary.to_string(),
1466 reason: other.to_string(),
1467 },
1468 }
1469}
1470
1471fn env_binary_override(kind: &ServerKind) -> Option<PathBuf> {
1472 let id = kind.id_str();
1473 let suffix: String = id
1474 .chars()
1475 .map(|ch| {
1476 if ch.is_ascii_alphanumeric() {
1477 ch.to_ascii_uppercase()
1478 } else {
1479 '_'
1480 }
1481 })
1482 .collect();
1483 let key = format!("AFT_LSP_{suffix}_BINARY");
1484 std::env::var_os(key).map(PathBuf::from)
1485}