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::pull_params::{
23 AftDocumentDiagnosticParams, AftDocumentDiagnosticRequest, AftWorkspaceDiagnosticParams,
24 AftWorkspaceDiagnosticRequest,
25};
26use crate::lsp::registry::{resolve_lsp_binary, servers_for_file, ServerDef, ServerKind};
27use crate::lsp::roots::ServerKey;
28use crate::lsp::LspError;
29use crate::slog_error;
30
31const STDERR_REASON_BYTES: usize = 2 * 1024;
32
33#[derive(Debug, Clone)]
38pub enum ServerAttemptResult {
39 Ok { server_key: ServerKey },
41 NoRootMarker { looked_for: Vec<String> },
44 BinaryNotInstalled { binary: String },
47 SpawnFailed { binary: String, reason: String },
49}
50
51#[derive(Debug, Clone)]
53pub struct ServerAttempt {
54 pub server_id: String,
56 pub server_name: String,
58 pub result: ServerAttemptResult,
59}
60
61#[derive(Debug, Clone, Default)]
67pub struct EnsureServerOutcomes {
68 pub successful: Vec<ServerKey>,
70 pub attempts: Vec<ServerAttempt>,
73}
74
75impl EnsureServerOutcomes {
76 pub fn no_server_registered(&self) -> bool {
78 self.attempts.is_empty()
79 }
80
81 pub fn only_inapplicable_root_markers(&self) -> bool {
89 self.successful.is_empty()
90 && !self.attempts.is_empty()
91 && self
92 .attempts
93 .iter()
94 .all(|attempt| matches!(attempt.result, ServerAttemptResult::NoRootMarker { .. }))
95 }
96}
97
98#[derive(Debug, Clone, Default)]
108pub struct PostEditWaitOutcome {
109 pub diagnostics: Vec<StoredDiagnostic>,
113 pub pending_servers: Vec<ServerKey>,
117 pub exited_servers: Vec<ServerKey>,
121}
122
123#[derive(Debug, Clone, Copy, Default)]
125pub struct PreEditSnapshot {
126 pub epoch: u64,
127 pub document_version_at_capture: Option<i32>,
128}
129
130#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
131pub struct StaleDiagnosticsMark {
132 pub had_entries: bool,
133 pub changed: bool,
134}
135
136pub fn post_edit_entry_is_fresh(
137 entry: &DiagnosticEntry,
138 target_version: i32,
139 pre: PreEditSnapshot,
140) -> bool {
141 if entry.stale || entry.epoch <= pre.epoch {
142 return false;
143 }
144
145 match entry.version {
146 Some(version) => version >= target_version,
147 None => false,
152 }
153}
154
155impl PostEditWaitOutcome {
156 pub fn complete(&self) -> bool {
159 self.pending_servers.is_empty() && self.exited_servers.is_empty()
160 }
161}
162
163#[derive(Debug, Clone)]
165pub enum PullFileOutcome {
166 Full { diagnostic_count: usize },
168 Unchanged,
170 PartialNotSupported,
173 PullNotSupported,
176 RequestFailed { reason: String },
178}
179
180#[derive(Debug, Clone, Default)]
182pub struct EnsureFileOpenResult {
183 pub server_keys: Vec<ServerKey>,
184 pub newly_opened: Vec<ServerKey>,
186}
187
188impl EnsureFileOpenResult {
189 pub fn is_empty(&self) -> bool {
190 self.server_keys.is_empty()
191 }
192}
193
194#[derive(Debug, Clone)]
196pub struct PullFileResult {
197 pub server_key: ServerKey,
198 pub outcome: PullFileOutcome,
199}
200
201pub(crate) struct TrackedPullFileResult {
202 pub results: Vec<PullFileResult>,
203 pub newly_opened: Vec<ServerKey>,
204}
205
206#[derive(Debug, Clone)]
208pub struct PullWorkspaceResult {
209 pub server_key: ServerKey,
210 pub files_reported: Vec<PathBuf>,
214 pub complete: bool,
216 pub cancelled: bool,
218 pub supports_workspace: bool,
222}
223
224pub struct DrainedLspEvents {
225 pub events: Vec<LspEvent>,
226 pub diagnostics_changed: bool,
227 pub has_more: bool,
228}
229
230impl IntoIterator for DrainedLspEvents {
231 type Item = LspEvent;
232 type IntoIter = std::vec::IntoIter<LspEvent>;
233
234 fn into_iter(self) -> Self::IntoIter {
235 self.events.into_iter()
236 }
237}
238
239pub struct LspManager {
240 clients: HashMap<ServerKey, LspClient>,
242 server_binaries: HashMap<ServerKey, String>,
246 documents: HashMap<ServerKey, DocumentStore>,
248 diagnostics: DiagnosticsStore,
250 event_tx: Sender<LspEvent>,
252 event_rx: Receiver<LspEvent>,
253 binary_overrides: HashMap<ServerKind, PathBuf>,
255 extra_env: HashMap<String, String>,
259 failed_spawns: HashMap<ServerKey, ServerAttemptResult>,
274 watched_file_skip_logged: HashSet<ServerKey>,
277 child_registry: LspChildRegistry,
281}
282
283impl LspManager {
284 pub fn new() -> Self {
285 let (event_tx, event_rx) = unbounded();
286 Self {
287 clients: HashMap::new(),
288 server_binaries: HashMap::new(),
289 documents: HashMap::new(),
290 diagnostics: DiagnosticsStore::new(),
291 event_tx,
292 event_rx,
293 binary_overrides: HashMap::new(),
294 extra_env: HashMap::new(),
295 failed_spawns: HashMap::new(),
296 watched_file_skip_logged: HashSet::new(),
297 child_registry: LspChildRegistry::new(),
298 }
299 }
300
301 pub fn set_child_registry(&mut self, registry: LspChildRegistry) {
303 self.child_registry = registry;
304 }
305
306 pub fn set_extra_env(&mut self, key: &str, value: &str) {
310 self.extra_env.insert(key.to_string(), value.to_string());
311 }
312
313 pub fn server_count(&self) -> usize {
315 self.clients.len()
316 }
317
318 pub fn estimated_memory(&self) -> crate::memory::MemoryEstimate {
322 let mut bytes = 0u64;
323 let mut document_count = 0u64;
324 for documents in self.documents.values() {
325 let estimate = documents.estimated_memory();
326 bytes = bytes.saturating_add(estimate.estimated_bytes.unwrap_or(0));
327 document_count = document_count
328 .saturating_add(estimate.counts.get("documents").copied().unwrap_or(0));
329 }
330 let diagnostics = self.diagnostics.estimated_memory();
331 bytes = bytes.saturating_add(diagnostics.estimated_bytes.unwrap_or(0));
332 crate::memory::MemoryEstimate::estimated(bytes)
333 .count("servers", self.clients.len())
334 .count("document_stores", self.documents.len())
335 .count_u64("documents", document_count)
336 .count_u64(
337 "diagnostic_entries",
338 diagnostics
339 .counts
340 .get("diagnostic_entries")
341 .copied()
342 .unwrap_or(0),
343 )
344 .count_u64(
345 "diagnostics",
346 diagnostics.counts.get("diagnostics").copied().unwrap_or(0),
347 )
348 }
349
350 pub fn set_diagnostic_capacity(&mut self, capacity: usize) {
354 self.diagnostics.set_capacity(capacity);
355 }
356
357 pub fn override_binary(&mut self, kind: ServerKind, binary_path: PathBuf) {
359 self.binary_overrides.insert(kind, binary_path);
360 }
361
362 pub fn ensure_server_for_file(&mut self, file_path: &Path, config: &Config) -> Vec<ServerKey> {
369 self.ensure_server_for_file_detailed(file_path, config)
370 .successful
371 }
372
373 fn running_server_keys_for_file(&self, file_path: &Path, config: &Config) -> Vec<ServerKey> {
374 let mut keys = Vec::new();
375 for def in servers_for_file(file_path, config) {
376 let Some(root) = def.workspace_root_for_file(file_path) else {
377 continue;
378 };
379 let key = ServerKey {
380 kind: def.kind.clone(),
381 root,
382 };
383 if self.clients.contains_key(&key) {
384 keys.push(key);
385 }
386 }
387 keys
388 }
389
390 pub fn ensure_server_for_file_detailed(
398 &mut self,
399 file_path: &Path,
400 config: &Config,
401 ) -> EnsureServerOutcomes {
402 let defs = servers_for_file(file_path, config);
403 let mut outcomes = EnsureServerOutcomes::default();
404
405 for def in defs {
406 let server_id = def.kind.id_str().to_string();
407 let server_name = def.name.to_string();
408
409 let Some(root) = def.workspace_root_for_file(file_path) else {
410 outcomes.attempts.push(ServerAttempt {
411 server_id,
412 server_name,
413 result: ServerAttemptResult::NoRootMarker {
414 looked_for: def.root_markers.iter().map(|s| s.to_string()).collect(),
415 },
416 });
417 continue;
418 };
419
420 let key = ServerKey {
421 kind: def.kind.clone(),
422 root,
423 };
424
425 if !self.clients.contains_key(&key) {
426 if let Some(cached) = self.failed_spawns.get(&key) {
433 outcomes.attempts.push(ServerAttempt {
434 server_id,
435 server_name,
436 result: cached.clone(),
437 });
438 continue;
439 }
440
441 match self.spawn_server(&def, &key.root, config) {
442 Ok(client) => {
443 self.clients.insert(key.clone(), client);
444 self.server_binaries.insert(key.clone(), def.binary.clone());
445 self.documents.entry(key.clone()).or_default();
446 }
447 Err(err) => {
448 slog_error!("failed to spawn {}: {}", def.name, err);
449 let result = classify_spawn_error(&def.binary, &err);
450 self.failed_spawns.insert(key.clone(), result.clone());
454 outcomes.attempts.push(ServerAttempt {
455 server_id,
456 server_name,
457 result,
458 });
459 continue;
460 }
461 }
462 }
463
464 outcomes.attempts.push(ServerAttempt {
465 server_id,
466 server_name,
467 result: ServerAttemptResult::Ok {
468 server_key: key.clone(),
469 },
470 });
471 outcomes.successful.push(key);
472 }
473
474 outcomes
475 }
476
477 pub fn ensure_server_for_file_default(&mut self, file_path: &Path) -> Vec<ServerKey> {
480 self.ensure_server_for_file(file_path, &Config::default())
481 }
482 pub fn ensure_file_open(
487 &mut self,
488 file_path: &Path,
489 config: &Config,
490 ) -> Result<EnsureFileOpenResult, LspError> {
491 let canonical_path = canonicalize_for_lsp(file_path)?;
492 let server_keys = self.ensure_server_for_file(&canonical_path, config);
493 if server_keys.is_empty() {
494 return Ok(EnsureFileOpenResult::default());
495 }
496
497 let uri = uri_for_path(&canonical_path)?;
498 let language_id = language_id_for_extension(
499 canonical_path
500 .extension()
501 .and_then(|ext| ext.to_str())
502 .unwrap_or_default(),
503 )
504 .to_string();
505 let needs_content = server_keys.iter().any(|key| {
506 !self
507 .documents
508 .get(key)
509 .is_some_and(|store| store.is_open(&canonical_path))
510 });
511 let initial_content = needs_content
512 .then(|| std::fs::read_to_string(&canonical_path).map_err(LspError::Io))
513 .transpose()?;
514 let mut newly_opened = Vec::new();
515
516 for key in &server_keys {
517 let already_open = self
518 .documents
519 .get(key)
520 .is_some_and(|store| store.is_open(&canonical_path));
521
522 if !already_open {
523 let content = initial_content
524 .as_ref()
525 .expect("content is loaded when any server needs didOpen");
526 let send_result = if let Some(client) = self.clients.get_mut(key) {
527 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
528 text_document: TextDocumentItem::new(
529 uri.clone(),
530 language_id.clone(),
531 0,
532 content.clone(),
533 ),
534 })
535 } else {
536 Ok(())
537 };
538 if let Err(err) = send_result {
539 let _ = self.close_file_for_servers(&canonical_path, &newly_opened);
540 return Err(err);
541 }
542 self.documents
543 .entry(key.clone())
544 .or_default()
545 .open(canonical_path.clone());
546 newly_opened.push(key.clone());
547 continue;
548 }
549
550 let drifted = self
560 .documents
561 .get(key)
562 .is_some_and(|store| store.is_stale_on_disk(&canonical_path));
563 if drifted {
564 let content = match std::fs::read_to_string(&canonical_path) {
565 Ok(content) => content,
566 Err(err) => {
567 let _ = self.close_file_for_servers(&canonical_path, &newly_opened);
568 return Err(LspError::Io(err));
569 }
570 };
571 let next_version = self
572 .documents
573 .get(key)
574 .and_then(|store| store.version(&canonical_path))
575 .map(|v| v + 1)
576 .unwrap_or(1);
577 let send_result = if let Some(client) = self.clients.get_mut(key) {
578 client.send_notification::<DidChangeTextDocument>(DidChangeTextDocumentParams {
579 text_document: VersionedTextDocumentIdentifier::new(
580 uri.clone(),
581 next_version,
582 ),
583 content_changes: vec![TextDocumentContentChangeEvent {
584 range: None,
585 range_length: None,
586 text: content,
587 }],
588 })
589 } else {
590 Ok(())
591 };
592 if let Err(err) = send_result {
593 let _ = self.close_file_for_servers(&canonical_path, &newly_opened);
594 return Err(err);
595 }
596 if let Some(store) = self.documents.get_mut(key) {
597 store.bump_version(&canonical_path);
598 }
599 }
600 }
601
602 Ok(EnsureFileOpenResult {
603 server_keys,
604 newly_opened,
605 })
606 }
607
608 pub fn ensure_file_open_default(
609 &mut self,
610 file_path: &Path,
611 ) -> Result<EnsureFileOpenResult, LspError> {
612 self.ensure_file_open(file_path, &Config::default())
613 }
614
615 pub fn notify_file_changed(
621 &mut self,
622 file_path: &Path,
623 content: &str,
624 config: &Config,
625 ) -> Result<(), LspError> {
626 self.notify_file_changed_versioned(file_path, content, config)
627 .map(|_| ())
628 }
629
630 pub fn notify_file_changed_versioned(
641 &mut self,
642 file_path: &Path,
643 content: &str,
644 config: &Config,
645 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
646 let canonical_path = canonicalize_for_lsp(file_path)?;
647 let server_keys = self.ensure_server_for_file(&canonical_path, config);
648 self.notify_file_changed_for_server_keys(canonical_path, content, server_keys)
649 }
650
651 pub fn notify_file_changed_if_running(
657 &mut self,
658 file_path: &Path,
659 content: &str,
660 config: &Config,
661 ) -> Result<(), LspError> {
662 let canonical_path = canonicalize_for_lsp(file_path)?;
663 let server_keys = self.running_server_keys_for_file(&canonical_path, config);
664 self.notify_file_changed_for_server_keys(canonical_path, content, server_keys)
665 .map(|_| ())
666 }
667
668 fn notify_file_changed_for_server_keys(
669 &mut self,
670 canonical_path: PathBuf,
671 content: &str,
672 server_keys: Vec<ServerKey>,
673 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
674 if server_keys.is_empty() {
675 return Ok(Vec::new());
676 }
677
678 let uri = uri_for_path(&canonical_path)?;
679 let language_id = language_id_for_extension(
680 canonical_path
681 .extension()
682 .and_then(|ext| ext.to_str())
683 .unwrap_or_default(),
684 )
685 .to_string();
686
687 let mut versions: Vec<(ServerKey, i32)> = Vec::with_capacity(server_keys.len());
688
689 for key in server_keys {
690 let current_version = self
691 .documents
692 .get(&key)
693 .and_then(|store| store.version(&canonical_path));
694
695 if let Some(version) = current_version {
696 let next_version = version + 1;
697 if let Some(client) = self.clients.get_mut(&key) {
698 client.send_notification::<DidChangeTextDocument>(
699 DidChangeTextDocumentParams {
700 text_document: VersionedTextDocumentIdentifier::new(
701 uri.clone(),
702 next_version,
703 ),
704 content_changes: vec![TextDocumentContentChangeEvent {
705 range: None,
706 range_length: None,
707 text: content.to_string(),
708 }],
709 },
710 )?;
711 }
712 if let Some(store) = self.documents.get_mut(&key) {
713 store.bump_version(&canonical_path);
714 }
715 versions.push((key, next_version));
716 continue;
717 }
718
719 if let Some(client) = self.clients.get_mut(&key) {
720 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
721 text_document: TextDocumentItem::new(
722 uri.clone(),
723 language_id.clone(),
724 0,
725 content.to_string(),
726 ),
727 })?;
728 }
729 self.documents
730 .entry(key.clone())
731 .or_default()
732 .open(canonical_path.clone());
733 versions.push((key, 0));
736 }
737
738 Ok(versions)
739 }
740
741 pub fn notify_file_changed_default(
742 &mut self,
743 file_path: &Path,
744 content: &str,
745 ) -> Result<(), LspError> {
746 self.notify_file_changed(file_path, content, &Config::default())
747 }
748
749 pub fn notify_files_watched_changed(
755 &mut self,
756 paths: &[(PathBuf, FileChangeType)],
757 _config: &Config,
758 ) -> Result<(), LspError> {
759 if paths.is_empty() {
760 return Ok(());
761 }
762
763 let mut canonical_events = Vec::with_capacity(paths.len());
764 for (path, typ) in paths {
765 let canonical_path = resolve_for_lsp_uri(path);
766 canonical_events.push((canonical_path, *typ));
767 }
768
769 let keys: Vec<ServerKey> = self.clients.keys().cloned().collect();
770 for key in keys {
771 let mut changes = Vec::new();
772 for (path, typ) in &canonical_events {
773 if !path.starts_with(&key.root) {
774 continue;
775 }
776 changes.push(FileEvent::new(uri_for_path(path)?, *typ));
777 }
778
779 if changes.is_empty() {
780 continue;
781 }
782
783 if let Some(client) = self.clients.get_mut(&key) {
784 let supports_static_watched_files = client.supports_watched_files();
790 let has_dynamic_registration = client.has_watched_file_registration();
791 if !(supports_static_watched_files || has_dynamic_registration) {
792 if self.watched_file_skip_logged.insert(key.clone()) {
793 log::debug!(
794 "skipping didChangeWatchedFiles for {:?} (not supported or registered)",
795 key
796 );
797 }
798 continue;
799 }
800 client.send_notification::<DidChangeWatchedFiles>(DidChangeWatchedFilesParams {
801 changes,
802 })?;
803 }
804 }
805
806 Ok(())
807 }
808
809 pub fn notify_file_closed(&mut self, file_path: &Path) -> Result<(), LspError> {
811 let canonical_path = canonicalize_for_lsp(file_path)?;
812 let keys = self
813 .documents
814 .iter()
815 .filter(|(_, store)| store.is_open(&canonical_path))
816 .map(|(key, _)| key.clone())
817 .collect::<Vec<_>>();
818 self.close_file_for_servers(&canonical_path, &keys)
819 }
820
821 pub(crate) fn close_file_for_servers(
826 &mut self,
827 file_path: &Path,
828 server_keys: &[ServerKey],
829 ) -> Result<(), LspError> {
830 let canonical_path = canonicalize_for_lsp(file_path)?;
831 let uri = uri_for_path(&canonical_path)?;
832 let mut first_error = None;
833
834 for key in server_keys {
835 let was_open = self
836 .documents
837 .get(key)
838 .is_some_and(|store| store.is_open(&canonical_path));
839 if !was_open {
840 continue;
841 }
842
843 if let Some(client) = self.clients.get_mut(key) {
844 if let Err(err) =
845 client.send_notification::<DidCloseTextDocument>(DidCloseTextDocumentParams {
846 text_document: TextDocumentIdentifier::new(uri.clone()),
847 })
848 {
849 if first_error.is_none() {
850 first_error = Some(err);
851 }
852 }
853 }
854
855 if let Some(store) = self.documents.get_mut(key) {
856 store.close(&canonical_path);
857 }
858 self.diagnostics.clear_for_server_file(key, &canonical_path);
859 }
860
861 match first_error {
862 Some(err) => Err(err),
863 None => Ok(()),
864 }
865 }
866
867 pub fn client_for_file(&self, file_path: &Path, config: &Config) -> Option<&LspClient> {
869 let key = self.server_key_for_file(file_path, config)?;
870 self.clients.get(&key)
871 }
872
873 pub fn client_for_file_default(&self, file_path: &Path) -> Option<&LspClient> {
874 self.client_for_file(file_path, &Config::default())
875 }
876
877 pub fn client_for_file_mut(
879 &mut self,
880 file_path: &Path,
881 config: &Config,
882 ) -> Option<&mut LspClient> {
883 let key = self.server_key_for_file(file_path, config)?;
884 self.clients.get_mut(&key)
885 }
886
887 pub fn client_for_file_mut_default(&mut self, file_path: &Path) -> Option<&mut LspClient> {
888 self.client_for_file_mut(file_path, &Config::default())
889 }
890
891 pub fn active_client_count(&self) -> usize {
893 self.clients.len()
894 }
895
896 pub fn drain_events(&mut self) -> DrainedLspEvents {
898 self.drain_events_bounded(usize::MAX)
899 }
900
901 pub fn has_pending_events(&self) -> bool {
904 !self.event_rx.is_empty()
905 }
906
907 pub fn drain_events_bounded(&mut self, max_events: usize) -> DrainedLspEvents {
908 let mut events = Vec::new();
909 let mut diagnostics_changed = false;
910 while events.len() < max_events {
911 let Ok(event) = self.event_rx.try_recv() else {
912 break;
913 };
914 if self.handle_event(&event).is_some() {
915 diagnostics_changed = true;
916 }
917 events.push(event);
918 }
919 let has_more = events.len() >= max_events && !self.event_rx.is_empty();
920 DrainedLspEvents {
921 events,
922 diagnostics_changed,
923 has_more,
924 }
925 }
926
927 pub fn wait_for_diagnostics(
929 &mut self,
930 file_path: &Path,
931 config: &Config,
932 timeout: std::time::Duration,
933 ) -> Vec<StoredDiagnostic> {
934 let deadline = std::time::Instant::now() + timeout;
935 self.wait_for_file_diagnostics(file_path, config, deadline)
936 }
937
938 pub fn wait_for_diagnostics_default(
939 &mut self,
940 file_path: &Path,
941 timeout: std::time::Duration,
942 ) -> Vec<StoredDiagnostic> {
943 self.wait_for_diagnostics(file_path, &Config::default(), timeout)
944 }
945
946 #[doc(hidden)]
951 pub fn diagnostics_store_for_test(&self) -> &DiagnosticsStore {
952 &self.diagnostics
953 }
954
955 #[doc(hidden)]
956 pub fn diagnostics_store_mut_for_test(&mut self) -> &mut DiagnosticsStore {
957 &mut self.diagnostics
958 }
959
960 #[doc(hidden)]
961 pub fn enqueue_event_for_test(&self, event: LspEvent) {
962 self.event_tx
963 .send(event)
964 .expect("LSP event receiver should remain connected");
965 }
966
967 #[doc(hidden)]
968 pub fn pending_event_count_for_test(&self) -> usize {
969 self.event_rx.len()
970 }
971
972 #[doc(hidden)]
973 pub fn document_is_open_for_test(&self, file_path: &Path) -> bool {
974 canonicalize_for_lsp(file_path).is_ok_and(|canonical_path| {
975 self.documents
976 .values()
977 .any(|store| store.is_open(&canonical_path))
978 })
979 }
980
981 pub fn warm_error_warning_counts(&self) -> (usize, usize) {
985 self.diagnostics.error_warning_counts()
986 }
987
988 pub fn warm_error_warning_counts_with_provisional(&self) -> ((usize, usize), bool) {
989 self.diagnostics.error_warning_counts_with_provisional()
990 }
991
992 pub fn diagnostics_generation(&self) -> u64 {
993 self.diagnostics.generation()
994 }
995
996 pub fn filtered_error_warning_counts(
1001 &self,
1002 keep: impl FnMut(&std::path::Path) -> bool,
1003 ) -> (usize, usize) {
1004 self.diagnostics.filtered_error_warning_counts(keep)
1005 }
1006
1007 pub fn filtered_error_warning_counts_with_provisional(
1011 &self,
1012 keep: impl FnMut(&std::path::Path) -> bool,
1013 ) -> ((usize, usize), bool) {
1014 self.diagnostics
1015 .filtered_error_warning_counts_with_provisional(keep)
1016 }
1017
1018 pub fn provisional_server_keys(&self) -> Vec<ServerKey> {
1022 self.clients
1023 .iter()
1024 .filter(|(_, client)| client.diagnostics_are_provisional())
1025 .map(|(key, _)| key.clone())
1026 .collect()
1027 }
1028
1029 pub fn snapshot_diagnostic_epochs(&self, file_path: &Path) -> HashMap<ServerKey, u64> {
1034 let lookup_path = normalize_lookup_path(file_path);
1035 self.diagnostics
1036 .entries_for_file(&lookup_path)
1037 .into_iter()
1038 .map(|(key, entry)| (key.clone(), entry.epoch))
1039 .collect()
1040 }
1041
1042 pub fn snapshot_pre_edit_state(&self, file_path: &Path) -> HashMap<ServerKey, PreEditSnapshot> {
1045 let lookup_path = normalize_lookup_path(file_path);
1046 let mut snapshots: HashMap<ServerKey, PreEditSnapshot> = self
1047 .diagnostics
1048 .entries_for_file(&lookup_path)
1049 .into_iter()
1050 .map(|(key, entry)| {
1051 (
1052 key.clone(),
1053 PreEditSnapshot {
1054 epoch: entry.epoch,
1055 document_version_at_capture: None,
1056 },
1057 )
1058 })
1059 .collect();
1060
1061 for (key, store) in &self.documents {
1062 if let Some(version) = store.version(&lookup_path) {
1063 snapshots
1064 .entry(key.clone())
1065 .or_default()
1066 .document_version_at_capture = Some(version);
1067 }
1068 }
1069
1070 snapshots
1071 }
1072
1073 pub fn diagnostic_entry_is_fresh_for_document(
1081 &self,
1082 file_path: &Path,
1083 server_key: &ServerKey,
1084 pre: PreEditSnapshot,
1085 ) -> bool {
1086 let lookup_path = normalize_lookup_path(file_path);
1087 let Some(entry) = self
1088 .diagnostics
1089 .entries_for_file(&lookup_path)
1090 .into_iter()
1091 .find_map(|(key, entry)| if key == server_key { Some(entry) } else { None })
1092 else {
1093 return false;
1094 };
1095
1096 if entry.stale {
1097 return false;
1098 }
1099
1100 let target_version = self
1101 .documents
1102 .get(server_key)
1103 .and_then(|store| store.version(&lookup_path))
1104 .or(pre.document_version_at_capture)
1105 .unwrap_or(0);
1106
1107 matches!(entry.version, Some(version) if version >= target_version)
1108 }
1109
1110 pub fn wait_for_post_edit_diagnostics(
1133 &mut self,
1134 file_path: &Path,
1135 _config: &Config,
1139 expected_versions: &[(ServerKey, i32)],
1140 pre_snapshot: &HashMap<ServerKey, PreEditSnapshot>,
1141 timeout: std::time::Duration,
1142 ) -> PostEditWaitOutcome {
1143 let lookup_path = normalize_lookup_path(file_path);
1144 let deadline = std::time::Instant::now() + timeout;
1145
1146 let _ = self.drain_events_for_file(&lookup_path);
1151
1152 let mut fresh: HashMap<ServerKey, Vec<StoredDiagnostic>> = HashMap::new();
1153 let mut exited: Vec<ServerKey> = Vec::new();
1154
1155 loop {
1156 for (key, target_version) in expected_versions {
1164 if fresh.contains_key(key) || exited.contains(key) {
1165 continue;
1166 }
1167 if !self.clients.contains_key(key) {
1168 exited.push(key.clone());
1169 continue;
1170 }
1171 if let Some(entry) = self
1172 .diagnostics
1173 .entries_for_file(&lookup_path)
1174 .into_iter()
1175 .find_map(|(k, e)| if k == key { Some(e) } else { None })
1176 {
1177 let pre = pre_snapshot.get(key).copied().unwrap_or_default();
1178 let is_fresh = post_edit_entry_is_fresh(entry, *target_version, pre);
1179 if is_fresh {
1180 fresh.insert(key.clone(), entry.diagnostics.clone());
1181 }
1182 }
1183 }
1184
1185 if fresh.len() + exited.len() == expected_versions.len() {
1187 break;
1188 }
1189
1190 let now = std::time::Instant::now();
1191 if now >= deadline {
1192 break;
1193 }
1194
1195 let timeout = deadline.saturating_duration_since(now);
1196 match self.event_rx.recv_timeout(timeout) {
1197 Ok(event) => {
1198 self.handle_event(&event);
1199 }
1200 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
1201 }
1202 }
1203
1204 let pending: Vec<ServerKey> = expected_versions
1206 .iter()
1207 .filter(|(k, _)| !fresh.contains_key(k) && !exited.contains(k))
1208 .map(|(k, _)| k.clone())
1209 .collect();
1210
1211 let mut diagnostics: Vec<StoredDiagnostic> = fresh
1214 .into_iter()
1215 .flat_map(|(_, diags)| diags.into_iter())
1216 .collect();
1217 diagnostics.sort_by(|a, b| {
1218 a.file
1219 .cmp(&b.file)
1220 .then(a.line.cmp(&b.line))
1221 .then(a.column.cmp(&b.column))
1222 .then(a.message.cmp(&b.message))
1223 });
1224
1225 PostEditWaitOutcome {
1226 diagnostics,
1227 pending_servers: pending,
1228 exited_servers: exited,
1229 }
1230 }
1231
1232 pub fn wait_for_file_diagnostics(
1238 &mut self,
1239 file_path: &Path,
1240 config: &Config,
1241 deadline: std::time::Instant,
1242 ) -> Vec<StoredDiagnostic> {
1243 let lookup_path = normalize_lookup_path(file_path);
1244
1245 if self.server_key_for_file(&lookup_path, config).is_none() {
1246 return Vec::new();
1247 }
1248
1249 loop {
1250 if self.drain_events_for_file(&lookup_path) {
1251 break;
1252 }
1253
1254 let now = std::time::Instant::now();
1255 if now >= deadline {
1256 break;
1257 }
1258
1259 let timeout = deadline.saturating_duration_since(now);
1260 match self.event_rx.recv_timeout(timeout) {
1261 Ok(event) => {
1262 if matches!(
1263 self.handle_event(&event),
1264 Some(ref published_file) if published_file.as_path() == lookup_path.as_path()
1265 ) {
1266 break;
1267 }
1268 }
1269 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
1270 }
1271 }
1272
1273 self.get_diagnostics_for_file(&lookup_path)
1274 .into_iter()
1275 .cloned()
1276 .collect()
1277 }
1278
1279 pub const PULL_FILE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
1285
1286 pub fn pull_file_timeout() -> std::time::Duration {
1288 Self::PULL_FILE_TIMEOUT
1289 }
1290
1291 const PULL_WORKSPACE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
1295
1296 pub fn pull_file_diagnostics(
1307 &mut self,
1308 file_path: &Path,
1309 config: &Config,
1310 ) -> Result<Vec<PullFileResult>, LspError> {
1311 self.pull_file_diagnostics_tracked(file_path, config)
1312 .map(|tracked| tracked.results)
1313 }
1314
1315 pub(crate) fn pull_file_diagnostics_tracked(
1316 &mut self,
1317 file_path: &Path,
1318 config: &Config,
1319 ) -> Result<TrackedPullFileResult, LspError> {
1320 let canonical_path = canonicalize_for_lsp(file_path)?;
1321 let opened = self.ensure_file_open(&canonical_path, config)?;
1324 if opened.server_keys.is_empty() {
1325 return Ok(TrackedPullFileResult {
1326 results: Vec::new(),
1327 newly_opened: opened.newly_opened,
1328 });
1329 }
1330
1331 let uri = uri_for_path(&canonical_path)?;
1332 let mut results = Vec::with_capacity(opened.server_keys.len());
1333
1334 for key in opened.server_keys {
1335 let supports_pull = self
1336 .clients
1337 .get(&key)
1338 .and_then(|c| c.diagnostic_capabilities())
1339 .is_some_and(|caps| caps.pull_diagnostics);
1340
1341 if !supports_pull {
1342 results.push(PullFileResult {
1343 server_key: key.clone(),
1344 outcome: PullFileOutcome::PullNotSupported,
1345 });
1346 continue;
1347 }
1348
1349 let previous_result_id = self
1351 .diagnostics
1352 .entries_for_file(&canonical_path)
1353 .into_iter()
1354 .find(|(k, _)| **k == key)
1355 .and_then(|(_, entry)| entry.result_id.clone());
1356
1357 let identifier = self
1358 .clients
1359 .get(&key)
1360 .and_then(|c| c.diagnostic_capabilities())
1361 .and_then(|caps| caps.identifier.clone());
1362
1363 let params = AftDocumentDiagnosticParams {
1364 text_document: lsp_types::TextDocumentIdentifier { uri: uri.clone() },
1365 identifier,
1366 previous_result_id,
1367 work_done_progress_params: Default::default(),
1368 partial_result_params: Default::default(),
1369 };
1370
1371 let outcome = match self.send_pull_request(&key, params) {
1372 Ok(report) => {
1373 if matches!(
1374 &report,
1375 lsp_types::DocumentDiagnosticReportResult::Report(
1376 lsp_types::DocumentDiagnosticReport::Full(_)
1377 )
1378 ) {
1379 self.drain_events();
1384 }
1385 self.ingest_document_report(&key, &canonical_path, report)
1386 }
1387 Err(err) => {
1388 if let Some(result) = self.cache_post_initialize_exit(&key, &err) {
1389 PullFileOutcome::RequestFailed {
1390 reason: server_attempt_result_reason(&result),
1391 }
1392 } else if recoverable_pull_rejection(&err)
1393 && self.clients.get(&key).is_some_and(|client| {
1394 matches!(
1395 client.state(),
1396 ServerState::Ready | ServerState::Initializing
1397 )
1398 })
1399 {
1400 PullFileOutcome::RequestFailed {
1401 reason: format!("pull_rejected_push_fallback: {err}"),
1402 }
1403 } else {
1404 PullFileOutcome::RequestFailed {
1405 reason: err.to_string(),
1406 }
1407 }
1408 }
1409 };
1410
1411 results.push(PullFileResult {
1412 server_key: key,
1413 outcome,
1414 });
1415 }
1416
1417 Ok(TrackedPullFileResult {
1418 results,
1419 newly_opened: opened.newly_opened,
1420 })
1421 }
1422
1423 pub fn pull_workspace_diagnostics(
1428 &mut self,
1429 server_key: &ServerKey,
1430 timeout: Option<std::time::Duration>,
1431 ) -> Result<PullWorkspaceResult, LspError> {
1432 let timeout = timeout.unwrap_or(Self::PULL_WORKSPACE_TIMEOUT);
1433
1434 let supports_workspace = self
1435 .clients
1436 .get(server_key)
1437 .and_then(|c| c.diagnostic_capabilities())
1438 .is_some_and(|caps| caps.workspace_diagnostics);
1439
1440 if !supports_workspace {
1441 return Ok(PullWorkspaceResult {
1442 server_key: server_key.clone(),
1443 files_reported: Vec::new(),
1444 complete: false,
1445 cancelled: false,
1446 supports_workspace: false,
1447 });
1448 }
1449
1450 let identifier = self
1451 .clients
1452 .get(server_key)
1453 .and_then(|c| c.diagnostic_capabilities())
1454 .and_then(|caps| caps.identifier.clone());
1455
1456 let params = AftWorkspaceDiagnosticParams {
1457 identifier,
1458 previous_result_ids: Vec::new(),
1459 work_done_progress_params: Default::default(),
1460 partial_result_params: Default::default(),
1461 };
1462
1463 let result = match self
1464 .clients
1465 .get_mut(server_key)
1466 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?
1467 .send_request_with_timeout::<AftWorkspaceDiagnosticRequest>(params, timeout)
1468 {
1469 Ok(result) => result,
1470 Err(LspError::Timeout(_)) => {
1471 return Ok(PullWorkspaceResult {
1472 server_key: server_key.clone(),
1473 files_reported: Vec::new(),
1474 complete: false,
1475 cancelled: true,
1476 supports_workspace: true,
1477 });
1478 }
1479 Err(err) => {
1480 if let Some(result) = self.cache_post_initialize_exit(server_key, &err) {
1481 return Err(LspError::ServerNotReady(server_attempt_result_reason(
1482 &result,
1483 )));
1484 }
1485 return Err(err);
1486 }
1487 };
1488
1489 let (items, complete) = match result {
1493 lsp_types::WorkspaceDiagnosticReportResult::Report(report) => (report.items, true),
1494 lsp_types::WorkspaceDiagnosticReportResult::Partial(partial) => (partial.items, false),
1495 };
1496
1497 let mut files_reported = Vec::with_capacity(items.len());
1499 for item in items {
1500 match item {
1501 lsp_types::WorkspaceDocumentDiagnosticReport::Full(full) => {
1502 if let Some(file) = uri_to_path(&full.uri) {
1503 let stored = from_lsp_diagnostics(
1504 file.clone(),
1505 full.full_document_diagnostic_report.items.clone(),
1506 );
1507 self.diagnostics.publish_with_result_id(
1508 server_key.clone(),
1509 file.clone(),
1510 stored,
1511 full.full_document_diagnostic_report.result_id.clone(),
1512 );
1513 files_reported.push(file);
1514 }
1515 }
1516 lsp_types::WorkspaceDocumentDiagnosticReport::Unchanged(_unchanged) => {
1517 }
1520 }
1521 }
1522
1523 Ok(PullWorkspaceResult {
1524 server_key: server_key.clone(),
1525 files_reported,
1526 complete,
1527 cancelled: false,
1528 supports_workspace: true,
1529 })
1530 }
1531
1532 fn cache_post_initialize_exit(
1533 &mut self,
1534 key: &ServerKey,
1535 err: &LspError,
1536 ) -> Option<ServerAttemptResult> {
1537 let binary = self
1538 .server_binaries
1539 .get(key)
1540 .cloned()
1541 .unwrap_or_else(|| key.kind.id_str().to_string());
1542 let (status, stderr_tail) = {
1543 let client = self.clients.get_mut(key)?;
1544 let mut status = client.child_exit_status();
1545 for _ in 0..10 {
1546 if status.is_some() {
1547 break;
1548 }
1549 std::thread::sleep(std::time::Duration::from_millis(10));
1550 status = client.child_exit_status();
1551 }
1552 let status = status?;
1553 wait_for_stderr_tail(client);
1554 (status, client.stderr_tail())
1555 };
1556 let reason = format_post_initialize_exit_reason(&binary, status, &stderr_tail, err);
1557 let result = ServerAttemptResult::SpawnFailed { binary, reason };
1558 self.clients.remove(key);
1559 self.server_binaries.remove(key);
1560 self.documents.remove(key);
1561 self.diagnostics.clear_for_server(key);
1562 self.failed_spawns.insert(key.clone(), result.clone());
1563 Some(result)
1564 }
1565
1566 fn send_pull_request(
1568 &mut self,
1569 key: &ServerKey,
1570 params: AftDocumentDiagnosticParams,
1571 ) -> Result<lsp_types::DocumentDiagnosticReportResult, LspError> {
1572 let client = self
1573 .clients
1574 .get_mut(key)
1575 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?;
1576 client.send_request_with_timeout::<AftDocumentDiagnosticRequest>(
1580 params,
1581 Self::PULL_FILE_TIMEOUT,
1582 )
1583 }
1584
1585 fn ingest_document_report(
1588 &mut self,
1589 key: &ServerKey,
1590 canonical_path: &Path,
1591 result: lsp_types::DocumentDiagnosticReportResult,
1592 ) -> PullFileOutcome {
1593 let report = match result {
1594 lsp_types::DocumentDiagnosticReportResult::Report(report) => report,
1595 lsp_types::DocumentDiagnosticReportResult::Partial(_) => {
1596 return PullFileOutcome::PartialNotSupported;
1600 }
1601 };
1602
1603 match report {
1604 lsp_types::DocumentDiagnosticReport::Full(full) => {
1605 let result_id = full.full_document_diagnostic_report.result_id.clone();
1606 let stored = from_lsp_diagnostics(
1607 canonical_path.to_path_buf(),
1608 full.full_document_diagnostic_report.items.clone(),
1609 );
1610 let count = stored.len();
1611 let provisional = self
1612 .clients
1613 .get(key)
1614 .is_some_and(|client| client.diagnostics_are_provisional());
1615 self.diagnostics.publish_full_with_provisional(
1616 key.clone(),
1617 canonical_path.to_path_buf(),
1618 stored,
1619 result_id,
1620 None,
1621 provisional,
1622 );
1623 PullFileOutcome::Full {
1624 diagnostic_count: count,
1625 }
1626 }
1627 lsp_types::DocumentDiagnosticReport::Unchanged(_unchanged) => {
1628 if self
1635 .diagnostics
1636 .has_report_for_server_file(key, canonical_path)
1637 {
1638 self.diagnostics
1639 .mark_fresh_for_server_file(key, canonical_path);
1640 let authoritative = self
1641 .clients
1642 .get(key)
1643 .map_or(true, |client| !client.diagnostics_are_provisional());
1644 if authoritative {
1645 self.diagnostics
1646 .clear_provisional_for_server_file(key, canonical_path);
1647 }
1648 PullFileOutcome::Unchanged
1649 } else {
1650 PullFileOutcome::RequestFailed {
1651 reason: "no_cache_for_unchanged".to_string(),
1652 }
1653 }
1654 }
1655 }
1656 }
1657
1658 pub fn shutdown_all(&mut self) {
1660 for (key, mut client) in self.clients.drain() {
1661 if let Err(err) = client.shutdown() {
1662 slog_error!("error shutting down {:?}: {}", key, err);
1663 }
1664 }
1665 self.server_binaries.clear();
1666 self.documents.clear();
1667 self.diagnostics = DiagnosticsStore::new();
1668 }
1669
1670 pub fn has_active_servers(&self) -> bool {
1672 self.clients
1673 .values()
1674 .any(|client| client.state() == ServerState::Ready)
1675 }
1676
1677 pub fn active_server_keys(&self) -> Vec<ServerKey> {
1680 self.clients.keys().cloned().collect()
1681 }
1682
1683 pub fn get_diagnostics_for_file(&self, file: &Path) -> Vec<&StoredDiagnostic> {
1684 let normalized = normalize_lookup_path(file);
1685 self.diagnostics.for_file(&normalized)
1686 }
1687
1688 pub fn get_diagnostics_for_file_with_provisional(
1689 &self,
1690 file: &Path,
1691 ) -> Vec<(&StoredDiagnostic, bool)> {
1692 let normalized = normalize_lookup_path(file);
1693 self.diagnostics.for_file_with_provisional(&normalized)
1694 }
1695
1696 pub fn clear_failed_spawns(&mut self) -> usize {
1717 let n = self.failed_spawns.len();
1718 self.failed_spawns.clear();
1719 n
1720 }
1721
1722 #[cfg(test)]
1723 pub(crate) fn insert_failed_spawn_for_test(&mut self) {
1724 let key = ServerKey {
1725 kind: crate::lsp::registry::ServerKind::Rust,
1726 root: std::path::PathBuf::from("/tmp/test-root"),
1727 };
1728 self.failed_spawns.insert(
1729 key,
1730 ServerAttemptResult::SpawnFailed {
1731 binary: "rust-analyzer".to_string(),
1732 reason: "test".to_string(),
1733 },
1734 );
1735 }
1736
1737 pub fn clear_diagnostics_for_file(&mut self, file: &Path) -> bool {
1738 let mut removed = self.diagnostics.clear_for_file(file);
1739
1740 let normalized = normalize_lookup_path(file);
1741 if normalized != file {
1742 removed |= self.diagnostics.clear_for_file(&normalized);
1743 }
1744
1745 if let (Some(parent), Some(name)) = (file.parent(), file.file_name()) {
1748 if let Ok(canonical_parent) = std::fs::canonicalize(parent) {
1749 let reconstructed = canonical_parent.join(name);
1750 if reconstructed != file && reconstructed != normalized {
1751 removed |= self.diagnostics.clear_for_file(&reconstructed);
1752 }
1753 }
1754 }
1755
1756 removed
1757 }
1758
1759 pub fn mark_diagnostics_stale_for_file(&mut self, file: &Path) -> StaleDiagnosticsMark {
1763 let mut candidates = vec![file.to_path_buf()];
1764 let normalized = normalize_lookup_path(file);
1765 if !candidates.iter().any(|candidate| candidate == &normalized) {
1766 candidates.push(normalized.clone());
1767 }
1768
1769 if let (Some(parent), Some(name)) = (file.parent(), file.file_name()) {
1770 if let Ok(canonical_parent) = std::fs::canonicalize(parent) {
1771 let reconstructed = canonical_parent.join(name);
1772 if !candidates
1773 .iter()
1774 .any(|candidate| candidate == &reconstructed)
1775 {
1776 candidates.push(reconstructed);
1777 }
1778 }
1779 }
1780
1781 let mut result = StaleDiagnosticsMark::default();
1782 for candidate in candidates {
1783 let (had_entries, changed) = self.diagnostics.mark_stale_for_file(&candidate);
1784 result.had_entries |= had_entries;
1785 result.changed |= changed;
1786 }
1787 result
1788 }
1789
1790 pub fn get_diagnostics_for_directory(&self, dir: &Path) -> Vec<&StoredDiagnostic> {
1791 let normalized = normalize_lookup_path(dir);
1792 self.diagnostics.for_directory(&normalized)
1793 }
1794
1795 pub fn get_diagnostics_for_directory_with_provisional(
1796 &self,
1797 dir: &Path,
1798 ) -> Vec<(&StoredDiagnostic, bool)> {
1799 let normalized = normalize_lookup_path(dir);
1800 self.diagnostics.for_directory_with_provisional(&normalized)
1801 }
1802
1803 pub fn get_all_diagnostics(&self) -> Vec<&StoredDiagnostic> {
1804 self.diagnostics.all()
1805 }
1806
1807 pub fn get_all_diagnostics_with_provisional(&self) -> Vec<(&StoredDiagnostic, bool)> {
1808 self.diagnostics.all_with_provisional()
1809 }
1810
1811 pub fn has_any_diagnostic_reports(&self) -> bool {
1816 self.diagnostics.has_any_fresh_report()
1817 }
1818
1819 pub fn has_diagnostic_report_for_file(&self, file: &Path) -> bool {
1823 let normalized = normalize_lookup_path(file);
1824 self.diagnostics.has_any_fresh_report_for_file(&normalized)
1825 }
1826
1827 pub fn has_diagnostic_report_for_server_file(&self, server: &ServerKey, file: &Path) -> bool {
1831 let normalized = normalize_lookup_path(file);
1832 self.diagnostics
1833 .has_fresh_report_for_server_file(server, &normalized)
1834 }
1835
1836 fn drain_events_for_file(&mut self, file_path: &Path) -> bool {
1837 let mut saw_file_diagnostics = false;
1838 while let Ok(event) = self.event_rx.try_recv() {
1839 if matches!(
1840 self.handle_event(&event),
1841 Some(ref published_file) if published_file.as_path() == file_path
1842 ) {
1843 saw_file_diagnostics = true;
1844 }
1845 }
1846 saw_file_diagnostics
1847 }
1848
1849 fn handle_event(&mut self, event: &LspEvent) -> Option<PathBuf> {
1850 match event {
1851 LspEvent::Notification {
1852 server_kind,
1853 root,
1854 method,
1855 params: Some(params),
1856 } if method == "textDocument/publishDiagnostics" => {
1857 self.handle_publish_diagnostics(server_kind.clone(), root.clone(), params)
1858 }
1859 LspEvent::Notification {
1860 server_kind,
1861 root,
1862 method,
1863 params: Some(params),
1864 } if method == "experimental/serverStatus" => {
1865 self.handle_server_status(server_kind.clone(), root.clone(), params);
1866 None
1867 }
1868 LspEvent::ServerExited { server_kind, root } => {
1869 let key = ServerKey {
1870 kind: server_kind.clone(),
1871 root: root.clone(),
1872 };
1873 self.clients.remove(&key);
1874 self.server_binaries.remove(&key);
1875 self.documents.remove(&key);
1876 self.diagnostics.clear_for_server(&key);
1877 None
1878 }
1879 _ => None,
1880 }
1881 }
1882
1883 fn handle_publish_diagnostics(
1884 &mut self,
1885 server: ServerKind,
1886 root: PathBuf,
1887 params: &serde_json::Value,
1888 ) -> Option<PathBuf> {
1889 if let Ok(publish_params) =
1890 serde_json::from_value::<lsp_types::PublishDiagnosticsParams>(params.clone())
1891 {
1892 let file = uri_to_path(&publish_params.uri)?;
1893 let stored = from_lsp_diagnostics(file.clone(), publish_params.diagnostics);
1894 let key = ServerKey { kind: server, root };
1900 let provisional = self
1901 .clients
1902 .get(&key)
1903 .is_some_and(|client| client.diagnostics_are_provisional());
1904 self.diagnostics.publish_full_with_provisional(
1905 key,
1906 file.clone(),
1907 stored,
1908 None,
1909 publish_params.version,
1910 provisional,
1911 );
1912 return Some(file);
1913 }
1914 None
1915 }
1916
1917 fn handle_server_status(
1918 &mut self,
1919 server: ServerKind,
1920 root: PathBuf,
1921 params: &serde_json::Value,
1922 ) {
1923 if !matches!(&server, ServerKind::Rust)
1924 || params.get("quiescent").and_then(serde_json::Value::as_bool) != Some(true)
1925 {
1926 return;
1927 }
1928
1929 let key = ServerKey { kind: server, root };
1930 let became_quiescent = self
1931 .clients
1932 .get_mut(&key)
1933 .is_some_and(|client| client.set_rust_analyzer_quiescent(true));
1934 if became_quiescent {
1935 self.diagnostics.mark_provisional_for_server_stale(&key);
1936 }
1937 }
1938
1939 fn spawn_server(
1940 &self,
1941 def: &ServerDef,
1942 root: &Path,
1943 config: &Config,
1944 ) -> Result<LspClient, LspError> {
1945 let binary = self.resolve_binary(def, config)?;
1946
1947 let mut merged_env = def.env.clone();
1951 for (key, value) in &self.extra_env {
1952 merged_env.insert(key.clone(), value.clone());
1953 }
1954
1955 let mut client = LspClient::spawn(
1956 def.kind.clone(),
1957 root.to_path_buf(),
1958 &binary,
1959 &def.args,
1960 &merged_env,
1961 self.event_tx.clone(),
1962 self.child_registry.clone(),
1963 )?;
1964 if let Err(err) = client.initialize(root, def.initialization_options.clone()) {
1965 wait_for_stderr_tail(&mut client);
1966 let stderr_tail = client.stderr_tail();
1967 let reason = if client.child_exited() || !stderr_tail.is_empty() {
1968 format_initialize_failure_reason(&def.binary, &stderr_tail, &err)
1969 } else {
1970 format!("server failed during initialize: {err}")
1971 };
1972 return Err(LspError::ServerNotReady(reason));
1973 }
1974 Ok(client)
1975 }
1976
1977 fn resolve_binary(&self, def: &ServerDef, config: &Config) -> Result<PathBuf, LspError> {
1978 if let Some(path) = self.binary_overrides.get(&def.kind) {
1979 if path.exists() {
1980 return Ok(path.clone());
1981 }
1982 return Err(LspError::NotFound(format!(
1983 "override binary for {:?} not found: {}",
1984 def.kind,
1985 path.display()
1986 )));
1987 }
1988
1989 if let Some(path) = env_binary_override(&def.kind) {
1990 if path.exists() {
1991 return Ok(path);
1992 }
1993 return Err(LspError::NotFound(format!(
1994 "environment override binary for {:?} not found: {}",
1995 def.kind,
1996 path.display()
1997 )));
1998 }
1999
2000 resolve_lsp_binary(
2005 &def.binary,
2006 config.project_root.as_deref(),
2007 &config.lsp_paths_extra,
2008 )
2009 .ok_or_else(|| {
2010 LspError::NotFound(format!(
2011 "language server binary '{}' not found in node_modules/.bin, lsp_paths_extra, or PATH",
2012 def.binary
2013 ))
2014 })
2015 }
2016
2017 fn server_key_for_file(&self, file_path: &Path, config: &Config) -> Option<ServerKey> {
2018 for def in servers_for_file(file_path, config) {
2019 let root = def.workspace_root_for_file(file_path)?;
2020 let key = ServerKey {
2021 kind: def.kind.clone(),
2022 root,
2023 };
2024 if self.clients.contains_key(&key) {
2025 return Some(key);
2026 }
2027 }
2028 None
2029 }
2030}
2031
2032impl Default for LspManager {
2033 fn default() -> Self {
2034 Self::new()
2035 }
2036}
2037
2038fn wait_for_stderr_tail(client: &mut LspClient) {
2039 for _ in 0..10 {
2040 if !client.stderr_tail().is_empty() {
2041 break;
2042 }
2043 std::thread::sleep(std::time::Duration::from_millis(10));
2044 }
2045}
2046
2047fn recoverable_pull_rejection(err: &LspError) -> bool {
2048 matches!(
2049 err,
2050 LspError::ServerError {
2051 code: -32601 | -32602,
2052 ..
2053 }
2054 )
2055}
2056
2057fn server_attempt_result_reason(result: &ServerAttemptResult) -> String {
2058 match result {
2059 ServerAttemptResult::SpawnFailed { binary, reason } => {
2060 format!("spawn_failed: {binary} ({reason})")
2061 }
2062 ServerAttemptResult::BinaryNotInstalled { binary } => {
2063 format!("binary_not_installed: {binary}")
2064 }
2065 ServerAttemptResult::NoRootMarker { looked_for } => {
2066 format!("no_root_marker (looked for: {})", looked_for.join(", "))
2067 }
2068 ServerAttemptResult::Ok { .. } => "ok".to_string(),
2069 }
2070}
2071
2072fn format_stderr_tail_for_reason(stderr_tail: &str) -> String {
2073 truncate_stderr_tail_for_reason(stderr_tail)
2074 .lines()
2075 .map(|line| format!(" {line}"))
2076 .collect::<Vec<_>>()
2077 .join("\n")
2078}
2079
2080fn truncate_stderr_tail_for_reason(stderr_tail: &str) -> String {
2081 if stderr_tail.len() <= STDERR_REASON_BYTES {
2082 return stderr_tail.to_string();
2083 }
2084
2085 let ellipsis = "...";
2086 let target_len = STDERR_REASON_BYTES.saturating_sub(ellipsis.len());
2087 let mut start = stderr_tail.len() - target_len;
2088 while start < stderr_tail.len() && !stderr_tail.is_char_boundary(start) {
2089 start += 1;
2090 }
2091 format!("{ellipsis}{}", &stderr_tail[start..])
2092}
2093
2094fn format_initialize_failure_reason(binary: &str, stderr_tail: &str, err: &LspError) -> String {
2095 let mut reason = format!("server crashed during initialize: {err}");
2096 if !stderr_tail.is_empty() {
2097 reason.push_str("; stderr (last 64 lines):\n");
2098 reason.push_str(&format_stderr_tail_for_reason(stderr_tail));
2099 reason.push_str("\n\n");
2100 reason.push_str(&failure_hint(binary, stderr_tail));
2101 }
2102 reason
2103}
2104
2105fn format_post_initialize_exit_reason(
2106 binary: &str,
2107 status: std::process::ExitStatus,
2108 stderr_tail: &str,
2109 err: &LspError,
2110) -> String {
2111 let code = status
2112 .code()
2113 .map(|c| c.to_string())
2114 .unwrap_or_else(|| "signal/unknown".to_string());
2115 let mut reason = format!("server exited after initialize (code {code}): {err}");
2116 if !stderr_tail.is_empty() {
2117 reason.push_str("; stderr (last 64 lines):\n");
2118 reason.push_str(&format_stderr_tail_for_reason(stderr_tail));
2119 reason.push_str("\n\n");
2120 reason.push_str(&failure_hint(binary, stderr_tail));
2121 }
2122 reason
2123}
2124
2125fn failure_hint(binary: &str, stderr_tail: &str) -> String {
2126 if stderr_tail.contains("MODULE_NOT_FOUND") || stderr_tail.contains("Cannot find module") {
2127 let package_manager = infer_package_manager(stderr_tail);
2128 format!(
2129 "Your package-manager shim resolves to a missing file. Try reinstalling: {package_manager} install -g {binary} --force. Common cause: hard-link breakage from fs migration or store prune."
2130 )
2131 } else if let Some(component) = rustup_missing_component(stderr_tail) {
2132 format!("'{component}' is a rustup proxy but the component is not installed. Install it: rustup component add {component}")
2137 } else {
2138 format!("Hint: see stderr above for '{binary}' failure details.")
2139 }
2140}
2141
2142fn rustup_missing_component(stderr_tail: &str) -> Option<String> {
2148 let marker = "Unknown binary '";
2149 let start = stderr_tail.find(marker)? + marker.len();
2150 let rest = &stderr_tail[start..];
2151 let end = rest.find('\'')?;
2152 let name = &rest[..end];
2153 if name.is_empty() || !stderr_tail.contains("toolchain") {
2156 return None;
2157 }
2158 Some(name.to_string())
2159}
2160
2161fn infer_package_manager(stderr_tail: &str) -> &'static str {
2162 let lower = stderr_tail.to_ascii_lowercase();
2163 if lower.contains(".pnpm/") || lower.contains(".pnpm\\") || lower.contains("/pnpm/") {
2164 "pnpm"
2165 } else if lower.contains(".yarn/")
2166 || lower.contains(".yarn\\")
2167 || lower.contains("/yarn/")
2168 || lower.contains("yarn")
2169 {
2170 "yarn"
2171 } else {
2172 "npm"
2173 }
2174}
2175
2176fn canonicalize_for_lsp(file_path: &Path) -> Result<PathBuf, LspError> {
2177 std::fs::canonicalize(file_path)
2184 .map(|canonical| crate::inspect::job::normalize_path(&canonical))
2185 .map_err(LspError::from)
2186}
2187
2188fn resolve_for_lsp_uri(file_path: &Path) -> PathBuf {
2189 if let Ok(path) = std::fs::canonicalize(file_path) {
2192 return crate::inspect::job::normalize_path(&path);
2193 }
2194
2195 let mut existing = file_path.to_path_buf();
2196 let mut missing = Vec::new();
2197 while !existing.exists() {
2198 let Some(name) = existing.file_name() else {
2199 break;
2200 };
2201 missing.push(name.to_owned());
2202 let Some(parent) = existing.parent() else {
2203 break;
2204 };
2205 existing = parent.to_path_buf();
2206 }
2207
2208 let mut resolved = std::fs::canonicalize(&existing)
2209 .map(|canonical| crate::inspect::job::normalize_path(&canonical))
2210 .unwrap_or(existing);
2211 for segment in missing.into_iter().rev() {
2212 resolved.push(segment);
2213 }
2214 resolved
2215}
2216
2217fn language_id_for_extension(ext: &str) -> &'static str {
2218 match ext {
2219 "ts" => "typescript",
2220 "tsx" => "typescriptreact",
2221 "js" | "mjs" | "cjs" => "javascript",
2222 "jsx" => "javascriptreact",
2223 "py" | "pyi" => "python",
2224 "rs" => "rust",
2225 "go" => "go",
2226 "html" | "htm" => "html",
2227 _ => "plaintext",
2228 }
2229}
2230
2231fn normalize_lookup_path(path: &Path) -> PathBuf {
2232 std::fs::canonicalize(path)
2236 .map(|canonical| crate::inspect::job::normalize_path(&canonical))
2237 .unwrap_or_else(|_| path.to_path_buf())
2238}
2239
2240fn classify_spawn_error(binary: &str, err: &LspError) -> ServerAttemptResult {
2247 match err {
2248 LspError::NotFound(_) => ServerAttemptResult::BinaryNotInstalled {
2253 binary: binary.to_string(),
2254 },
2255 other => ServerAttemptResult::SpawnFailed {
2256 binary: binary.to_string(),
2257 reason: other.to_string(),
2258 },
2259 }
2260}
2261
2262fn env_binary_override(kind: &ServerKind) -> Option<PathBuf> {
2263 let id = kind.id_str();
2264 let suffix: String = id
2265 .chars()
2266 .map(|ch| {
2267 if ch.is_ascii_alphanumeric() {
2268 ch.to_ascii_uppercase()
2269 } else {
2270 '_'
2271 }
2272 })
2273 .collect();
2274 let key = format!("AFT_LSP_{suffix}_BINARY");
2275 std::env::var_os(key).map(PathBuf::from)
2276}
2277
2278#[cfg(test)]
2279mod failure_hint_tests {
2280 use super::{failure_hint, rustup_missing_component};
2281
2282 #[test]
2283 fn detects_rustup_proxy_without_component() {
2284 let stderr = "error: Unknown binary 'rust-analyzer' in official toolchain 'stable-aarch64-apple-darwin'.";
2286 assert_eq!(
2287 rustup_missing_component(stderr).as_deref(),
2288 Some("rust-analyzer")
2289 );
2290 let hint = failure_hint("rust-analyzer", stderr);
2291 assert!(
2292 hint.contains("rustup component add rust-analyzer"),
2293 "expected actionable rustup hint, got: {hint}"
2294 );
2295 }
2296
2297 #[test]
2298 fn ignores_unknown_binary_without_toolchain_phrasing() {
2299 let stderr = "fatal: Unknown binary 'foo' was requested by the linker.";
2302 assert_eq!(rustup_missing_component(stderr), None);
2303 assert!(failure_hint("foo", stderr).starts_with("Hint: see stderr"));
2304 }
2305
2306 #[test]
2307 fn npm_module_not_found_still_wins() {
2308 let stderr = "Error: Cannot find module '/x/typescript-language-server/lib/cli.mjs'";
2310 let hint = failure_hint("typescript-language-server", stderr);
2311 assert!(hint.contains("install -g"), "got: {hint}");
2312 }
2313}
2314
2315#[cfg(test)]
2316mod diagnostic_capacity_tests {
2317 use std::fs;
2318
2319 use super::LspManager;
2320 use crate::config::Config;
2321
2322 #[test]
2327 fn set_diagnostic_capacity_propagates_to_store() {
2328 let mut manager = LspManager::new();
2329 manager.set_diagnostic_capacity(7);
2330 assert_eq!(manager.diagnostics_store_for_test().capacity_for_test(), 7);
2331 manager.set_diagnostic_capacity(0); assert_eq!(manager.diagnostics_store_for_test().capacity_for_test(), 0);
2333 }
2334
2335 #[test]
2338 fn clear_failed_spawns_empties_the_cache() {
2339 let mut manager = LspManager::new();
2340 assert_eq!(manager.clear_failed_spawns(), 0);
2341 manager.insert_failed_spawn_for_test();
2342 assert_eq!(manager.clear_failed_spawns(), 1);
2343 assert_eq!(manager.clear_failed_spawns(), 0);
2344 }
2345
2346 #[test]
2347 fn post_write_notification_does_not_start_a_cold_server() {
2348 let dir = tempfile::tempdir().unwrap();
2349 let file = dir.path().join("main.ts");
2350 fs::write(dir.path().join("package.json"), "{}").unwrap();
2351 fs::write(&file, "export const value = 1;\n").unwrap();
2352
2353 let mut manager = LspManager::new();
2354 manager
2355 .notify_file_changed_if_running(&file, "export const value = 1;\n", &Config::default())
2356 .unwrap();
2357 assert!(manager.clients.is_empty());
2358 }
2359}
2360
2361#[cfg(test)]
2362mod clear_diagnostics_tests {
2363 use std::path::PathBuf;
2364
2365 use super::LspManager;
2366 use crate::lsp::client::LspEvent;
2367 use crate::lsp::diagnostics::{DiagnosticSeverity, StoredDiagnostic};
2368 use crate::lsp::position::uri_for_path;
2369 use crate::lsp::registry::ServerKind;
2370 use crate::lsp::roots::ServerKey;
2371
2372 fn err_diag(file: &PathBuf) -> StoredDiagnostic {
2373 StoredDiagnostic {
2374 file: file.clone(),
2375 line: 1,
2376 column: 1,
2377 end_line: 1,
2378 end_column: 2,
2379 severity: DiagnosticSeverity::Error,
2380 message: "boom".into(),
2381 code: None,
2382 source: None,
2383 }
2384 }
2385
2386 #[test]
2391 fn clear_diagnostics_for_deleted_file_matches_canonical_key() {
2392 let dir = tempfile::tempdir().unwrap();
2393 let canonical_dir = std::fs::canonicalize(dir.path()).unwrap();
2395 let canonical_file = canonical_dir.join("gone.ts");
2396 std::fs::write(&canonical_file, "x").unwrap();
2399
2400 let mut manager = LspManager::new();
2401 let key = ServerKey {
2402 kind: ServerKind::TypeScript,
2403 root: canonical_dir.clone(),
2404 };
2405 manager.diagnostics_store_mut_for_test().publish(
2406 key,
2407 canonical_file.clone(),
2408 vec![err_diag(&canonical_file)],
2409 );
2410 assert_eq!(manager.warm_error_warning_counts(), (1, 0));
2411
2412 std::fs::remove_file(&canonical_file).unwrap();
2413
2414 let watcher_path = dir.path().join("gone.ts");
2417 let removed = manager.clear_diagnostics_for_file(&watcher_path);
2418
2419 assert!(removed, "expected the deleted file's diagnostic to clear");
2420 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
2421 }
2422
2423 #[test]
2424 fn clear_diagnostics_for_unknown_file_is_noop() {
2425 let mut manager = LspManager::new();
2426 assert!(!manager.clear_diagnostics_for_file(&PathBuf::from("/nope/missing.ts")));
2427 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
2428 }
2429
2430 #[test]
2431 fn drain_events_reports_publish_diagnostics_updates() {
2432 let dir = tempfile::tempdir().unwrap();
2433 let root = std::fs::canonicalize(dir.path()).unwrap();
2434 let file = root.join("main.ts");
2435 std::fs::write(&file, "const x: number = 'nope';").unwrap();
2436
2437 let mut manager = LspManager::new();
2438 let diagnostic = lsp_types::Diagnostic {
2439 range: lsp_types::Range {
2440 start: lsp_types::Position {
2441 line: 0,
2442 character: 0,
2443 },
2444 end: lsp_types::Position {
2445 line: 0,
2446 character: 1,
2447 },
2448 },
2449 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
2450 code: None,
2451 code_description: None,
2452 source: Some("test".into()),
2453 message: "boom".into(),
2454 related_information: None,
2455 tags: None,
2456 data: None,
2457 };
2458 let params = serde_json::to_value(lsp_types::PublishDiagnosticsParams {
2459 uri: uri_for_path(&file).unwrap(),
2460 diagnostics: vec![diagnostic],
2461 version: Some(1),
2462 })
2463 .unwrap();
2464 manager
2465 .event_tx
2466 .send(LspEvent::Notification {
2467 server_kind: ServerKind::TypeScript,
2468 root,
2469 method: "textDocument/publishDiagnostics".into(),
2470 params: Some(params),
2471 })
2472 .unwrap();
2473
2474 let drained = manager.drain_events();
2475
2476 assert!(drained.diagnostics_changed);
2477 assert_eq!(drained.events.len(), 1);
2478 assert_eq!(manager.warm_error_warning_counts(), (1, 0));
2479 }
2480}