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)]
182pub struct PullFileResult {
183 pub server_key: ServerKey,
184 pub outcome: PullFileOutcome,
185}
186
187#[derive(Debug, Clone)]
189pub struct PullWorkspaceResult {
190 pub server_key: ServerKey,
191 pub files_reported: Vec<PathBuf>,
195 pub complete: bool,
197 pub cancelled: bool,
199 pub supports_workspace: bool,
203}
204
205pub struct DrainedLspEvents {
206 pub events: Vec<LspEvent>,
207 pub diagnostics_changed: bool,
208 pub has_more: bool,
209}
210
211impl IntoIterator for DrainedLspEvents {
212 type Item = LspEvent;
213 type IntoIter = std::vec::IntoIter<LspEvent>;
214
215 fn into_iter(self) -> Self::IntoIter {
216 self.events.into_iter()
217 }
218}
219
220pub struct LspManager {
221 clients: HashMap<ServerKey, LspClient>,
223 server_binaries: HashMap<ServerKey, String>,
227 documents: HashMap<ServerKey, DocumentStore>,
229 diagnostics: DiagnosticsStore,
231 event_tx: Sender<LspEvent>,
233 event_rx: Receiver<LspEvent>,
234 binary_overrides: HashMap<ServerKind, PathBuf>,
236 extra_env: HashMap<String, String>,
240 failed_spawns: HashMap<ServerKey, ServerAttemptResult>,
255 watched_file_skip_logged: HashSet<ServerKey>,
258 child_registry: LspChildRegistry,
262}
263
264impl LspManager {
265 pub fn new() -> Self {
266 let (event_tx, event_rx) = unbounded();
267 Self {
268 clients: HashMap::new(),
269 server_binaries: HashMap::new(),
270 documents: HashMap::new(),
271 diagnostics: DiagnosticsStore::new(),
272 event_tx,
273 event_rx,
274 binary_overrides: HashMap::new(),
275 extra_env: HashMap::new(),
276 failed_spawns: HashMap::new(),
277 watched_file_skip_logged: HashSet::new(),
278 child_registry: LspChildRegistry::new(),
279 }
280 }
281
282 pub fn set_child_registry(&mut self, registry: LspChildRegistry) {
284 self.child_registry = registry;
285 }
286
287 pub fn set_extra_env(&mut self, key: &str, value: &str) {
291 self.extra_env.insert(key.to_string(), value.to_string());
292 }
293
294 pub fn server_count(&self) -> usize {
296 self.clients.len()
297 }
298
299 pub fn set_diagnostic_capacity(&mut self, capacity: usize) {
303 self.diagnostics.set_capacity(capacity);
304 }
305
306 pub fn override_binary(&mut self, kind: ServerKind, binary_path: PathBuf) {
308 self.binary_overrides.insert(kind, binary_path);
309 }
310
311 pub fn ensure_server_for_file(&mut self, file_path: &Path, config: &Config) -> Vec<ServerKey> {
318 self.ensure_server_for_file_detailed(file_path, config)
319 .successful
320 }
321
322 pub fn ensure_server_for_file_detailed(
330 &mut self,
331 file_path: &Path,
332 config: &Config,
333 ) -> EnsureServerOutcomes {
334 let defs = servers_for_file(file_path, config);
335 let mut outcomes = EnsureServerOutcomes::default();
336
337 for def in defs {
338 let server_id = def.kind.id_str().to_string();
339 let server_name = def.name.to_string();
340
341 let Some(root) = def.workspace_root_for_file(file_path) else {
342 outcomes.attempts.push(ServerAttempt {
343 server_id,
344 server_name,
345 result: ServerAttemptResult::NoRootMarker {
346 looked_for: def.root_markers.iter().map(|s| s.to_string()).collect(),
347 },
348 });
349 continue;
350 };
351
352 let key = ServerKey {
353 kind: def.kind.clone(),
354 root,
355 };
356
357 if !self.clients.contains_key(&key) {
358 if let Some(cached) = self.failed_spawns.get(&key) {
365 outcomes.attempts.push(ServerAttempt {
366 server_id,
367 server_name,
368 result: cached.clone(),
369 });
370 continue;
371 }
372
373 match self.spawn_server(&def, &key.root, config) {
374 Ok(client) => {
375 self.clients.insert(key.clone(), client);
376 self.server_binaries.insert(key.clone(), def.binary.clone());
377 self.documents.entry(key.clone()).or_default();
378 }
379 Err(err) => {
380 slog_error!("failed to spawn {}: {}", def.name, err);
381 let result = classify_spawn_error(&def.binary, &err);
382 self.failed_spawns.insert(key.clone(), result.clone());
386 outcomes.attempts.push(ServerAttempt {
387 server_id,
388 server_name,
389 result,
390 });
391 continue;
392 }
393 }
394 }
395
396 outcomes.attempts.push(ServerAttempt {
397 server_id,
398 server_name,
399 result: ServerAttemptResult::Ok {
400 server_key: key.clone(),
401 },
402 });
403 outcomes.successful.push(key);
404 }
405
406 outcomes
407 }
408
409 pub fn ensure_server_for_file_default(&mut self, file_path: &Path) -> Vec<ServerKey> {
412 self.ensure_server_for_file(file_path, &Config::default())
413 }
414 pub fn ensure_file_open(
418 &mut self,
419 file_path: &Path,
420 config: &Config,
421 ) -> Result<Vec<ServerKey>, LspError> {
422 let canonical_path = canonicalize_for_lsp(file_path)?;
423 let server_keys = self.ensure_server_for_file(&canonical_path, config);
424 if server_keys.is_empty() {
425 return Ok(server_keys);
426 }
427
428 let uri = uri_for_path(&canonical_path)?;
429 let language_id = language_id_for_extension(
430 canonical_path
431 .extension()
432 .and_then(|ext| ext.to_str())
433 .unwrap_or_default(),
434 )
435 .to_string();
436
437 for key in &server_keys {
438 let already_open = self
439 .documents
440 .get(key)
441 .is_some_and(|store| store.is_open(&canonical_path));
442
443 if !already_open {
444 let content = std::fs::read_to_string(&canonical_path).map_err(LspError::Io)?;
445 if let Some(client) = self.clients.get_mut(key) {
446 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
447 text_document: TextDocumentItem::new(
448 uri.clone(),
449 language_id.clone(),
450 0,
451 content,
452 ),
453 })?;
454 }
455 self.documents
456 .entry(key.clone())
457 .or_default()
458 .open(canonical_path.clone());
459 continue;
460 }
461
462 let drifted = self
472 .documents
473 .get(key)
474 .is_some_and(|store| store.is_stale_on_disk(&canonical_path));
475 if drifted {
476 let content = std::fs::read_to_string(&canonical_path).map_err(LspError::Io)?;
477 let next_version = self
478 .documents
479 .get(key)
480 .and_then(|store| store.version(&canonical_path))
481 .map(|v| v + 1)
482 .unwrap_or(1);
483 if let Some(client) = self.clients.get_mut(key) {
484 client.send_notification::<DidChangeTextDocument>(
485 DidChangeTextDocumentParams {
486 text_document: VersionedTextDocumentIdentifier::new(
487 uri.clone(),
488 next_version,
489 ),
490 content_changes: vec![TextDocumentContentChangeEvent {
491 range: None,
492 range_length: None,
493 text: content,
494 }],
495 },
496 )?;
497 }
498 if let Some(store) = self.documents.get_mut(key) {
499 store.bump_version(&canonical_path);
500 }
501 }
502 }
503
504 Ok(server_keys)
505 }
506
507 pub fn ensure_file_open_default(
508 &mut self,
509 file_path: &Path,
510 ) -> Result<Vec<ServerKey>, LspError> {
511 self.ensure_file_open(file_path, &Config::default())
512 }
513
514 pub fn notify_file_changed(
520 &mut self,
521 file_path: &Path,
522 content: &str,
523 config: &Config,
524 ) -> Result<(), LspError> {
525 self.notify_file_changed_versioned(file_path, content, config)
526 .map(|_| ())
527 }
528
529 pub fn notify_file_changed_versioned(
540 &mut self,
541 file_path: &Path,
542 content: &str,
543 config: &Config,
544 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
545 let canonical_path = canonicalize_for_lsp(file_path)?;
546 let server_keys = self.ensure_server_for_file(&canonical_path, config);
547 if server_keys.is_empty() {
548 return Ok(Vec::new());
549 }
550
551 let uri = uri_for_path(&canonical_path)?;
552 let language_id = language_id_for_extension(
553 canonical_path
554 .extension()
555 .and_then(|ext| ext.to_str())
556 .unwrap_or_default(),
557 )
558 .to_string();
559
560 let mut versions: Vec<(ServerKey, i32)> = Vec::with_capacity(server_keys.len());
561
562 for key in server_keys {
563 let current_version = self
564 .documents
565 .get(&key)
566 .and_then(|store| store.version(&canonical_path));
567
568 if let Some(version) = current_version {
569 let next_version = version + 1;
570 if let Some(client) = self.clients.get_mut(&key) {
571 client.send_notification::<DidChangeTextDocument>(
572 DidChangeTextDocumentParams {
573 text_document: VersionedTextDocumentIdentifier::new(
574 uri.clone(),
575 next_version,
576 ),
577 content_changes: vec![TextDocumentContentChangeEvent {
578 range: None,
579 range_length: None,
580 text: content.to_string(),
581 }],
582 },
583 )?;
584 }
585 if let Some(store) = self.documents.get_mut(&key) {
586 store.bump_version(&canonical_path);
587 }
588 versions.push((key, next_version));
589 continue;
590 }
591
592 if let Some(client) = self.clients.get_mut(&key) {
593 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
594 text_document: TextDocumentItem::new(
595 uri.clone(),
596 language_id.clone(),
597 0,
598 content.to_string(),
599 ),
600 })?;
601 }
602 self.documents
603 .entry(key.clone())
604 .or_default()
605 .open(canonical_path.clone());
606 versions.push((key, 0));
609 }
610
611 Ok(versions)
612 }
613
614 pub fn notify_file_changed_default(
615 &mut self,
616 file_path: &Path,
617 content: &str,
618 ) -> Result<(), LspError> {
619 self.notify_file_changed(file_path, content, &Config::default())
620 }
621
622 pub fn notify_files_watched_changed(
628 &mut self,
629 paths: &[(PathBuf, FileChangeType)],
630 _config: &Config,
631 ) -> Result<(), LspError> {
632 if paths.is_empty() {
633 return Ok(());
634 }
635
636 let mut canonical_events = Vec::with_capacity(paths.len());
637 for (path, typ) in paths {
638 let canonical_path = resolve_for_lsp_uri(path);
639 canonical_events.push((canonical_path, *typ));
640 }
641
642 let keys: Vec<ServerKey> = self.clients.keys().cloned().collect();
643 for key in keys {
644 let mut changes = Vec::new();
645 for (path, typ) in &canonical_events {
646 if !path.starts_with(&key.root) {
647 continue;
648 }
649 changes.push(FileEvent::new(uri_for_path(path)?, *typ));
650 }
651
652 if changes.is_empty() {
653 continue;
654 }
655
656 if let Some(client) = self.clients.get_mut(&key) {
657 let supports_static_watched_files = client.supports_watched_files();
663 let has_dynamic_registration = client.has_watched_file_registration();
664 if !(supports_static_watched_files || has_dynamic_registration) {
665 if self.watched_file_skip_logged.insert(key.clone()) {
666 log::debug!(
667 "skipping didChangeWatchedFiles for {:?} (not supported or registered)",
668 key
669 );
670 }
671 continue;
672 }
673 client.send_notification::<DidChangeWatchedFiles>(DidChangeWatchedFilesParams {
674 changes,
675 })?;
676 }
677 }
678
679 Ok(())
680 }
681
682 pub fn notify_file_closed(&mut self, file_path: &Path) -> Result<(), LspError> {
684 let canonical_path = canonicalize_for_lsp(file_path)?;
685 let uri = uri_for_path(&canonical_path)?;
686 let keys: Vec<ServerKey> = self.documents.keys().cloned().collect();
687
688 for key in keys {
689 let was_open = self
690 .documents
691 .get(&key)
692 .map(|store| store.is_open(&canonical_path))
693 .unwrap_or(false);
694 if !was_open {
695 continue;
696 }
697
698 if let Some(client) = self.clients.get_mut(&key) {
699 client.send_notification::<DidCloseTextDocument>(DidCloseTextDocumentParams {
700 text_document: TextDocumentIdentifier::new(uri.clone()),
701 })?;
702 }
703
704 if let Some(store) = self.documents.get_mut(&key) {
705 store.close(&canonical_path);
706 }
707 self.diagnostics
708 .clear_for_server_file(&key, &canonical_path);
709 }
710
711 Ok(())
712 }
713
714 pub fn client_for_file(&self, file_path: &Path, config: &Config) -> Option<&LspClient> {
716 let key = self.server_key_for_file(file_path, config)?;
717 self.clients.get(&key)
718 }
719
720 pub fn client_for_file_default(&self, file_path: &Path) -> Option<&LspClient> {
721 self.client_for_file(file_path, &Config::default())
722 }
723
724 pub fn client_for_file_mut(
726 &mut self,
727 file_path: &Path,
728 config: &Config,
729 ) -> Option<&mut LspClient> {
730 let key = self.server_key_for_file(file_path, config)?;
731 self.clients.get_mut(&key)
732 }
733
734 pub fn client_for_file_mut_default(&mut self, file_path: &Path) -> Option<&mut LspClient> {
735 self.client_for_file_mut(file_path, &Config::default())
736 }
737
738 pub fn active_client_count(&self) -> usize {
740 self.clients.len()
741 }
742
743 pub fn drain_events(&mut self) -> DrainedLspEvents {
745 self.drain_events_bounded(usize::MAX)
746 }
747
748 pub fn drain_events_bounded(&mut self, max_events: usize) -> DrainedLspEvents {
749 let mut events = Vec::new();
750 let mut diagnostics_changed = false;
751 while events.len() < max_events {
752 let Ok(event) = self.event_rx.try_recv() else {
753 break;
754 };
755 if self.handle_event(&event).is_some() {
756 diagnostics_changed = true;
757 }
758 events.push(event);
759 }
760 let has_more = events.len() >= max_events && !self.event_rx.is_empty();
761 DrainedLspEvents {
762 events,
763 diagnostics_changed,
764 has_more,
765 }
766 }
767
768 pub fn wait_for_diagnostics(
770 &mut self,
771 file_path: &Path,
772 config: &Config,
773 timeout: std::time::Duration,
774 ) -> Vec<StoredDiagnostic> {
775 let deadline = std::time::Instant::now() + timeout;
776 self.wait_for_file_diagnostics(file_path, config, deadline)
777 }
778
779 pub fn wait_for_diagnostics_default(
780 &mut self,
781 file_path: &Path,
782 timeout: std::time::Duration,
783 ) -> Vec<StoredDiagnostic> {
784 self.wait_for_diagnostics(file_path, &Config::default(), timeout)
785 }
786
787 #[doc(hidden)]
792 pub fn diagnostics_store_for_test(&self) -> &DiagnosticsStore {
793 &self.diagnostics
794 }
795
796 #[doc(hidden)]
797 pub fn diagnostics_store_mut_for_test(&mut self) -> &mut DiagnosticsStore {
798 &mut self.diagnostics
799 }
800
801 pub fn warm_error_warning_counts(&self) -> (usize, usize) {
805 self.diagnostics.error_warning_counts()
806 }
807
808 pub fn filtered_error_warning_counts(
813 &self,
814 keep: impl FnMut(&std::path::Path) -> bool,
815 ) -> (usize, usize) {
816 self.diagnostics.filtered_error_warning_counts(keep)
817 }
818
819 pub fn snapshot_diagnostic_epochs(&self, file_path: &Path) -> HashMap<ServerKey, u64> {
824 let lookup_path = normalize_lookup_path(file_path);
825 self.diagnostics
826 .entries_for_file(&lookup_path)
827 .into_iter()
828 .map(|(key, entry)| (key.clone(), entry.epoch))
829 .collect()
830 }
831
832 pub fn snapshot_pre_edit_state(&self, file_path: &Path) -> HashMap<ServerKey, PreEditSnapshot> {
835 let lookup_path = normalize_lookup_path(file_path);
836 let mut snapshots: HashMap<ServerKey, PreEditSnapshot> = self
837 .diagnostics
838 .entries_for_file(&lookup_path)
839 .into_iter()
840 .map(|(key, entry)| {
841 (
842 key.clone(),
843 PreEditSnapshot {
844 epoch: entry.epoch,
845 document_version_at_capture: None,
846 },
847 )
848 })
849 .collect();
850
851 for (key, store) in &self.documents {
852 if let Some(version) = store.version(&lookup_path) {
853 snapshots
854 .entry(key.clone())
855 .or_default()
856 .document_version_at_capture = Some(version);
857 }
858 }
859
860 snapshots
861 }
862
863 pub fn diagnostic_entry_is_fresh_for_document(
871 &self,
872 file_path: &Path,
873 server_key: &ServerKey,
874 pre: PreEditSnapshot,
875 ) -> bool {
876 let lookup_path = normalize_lookup_path(file_path);
877 let Some(entry) = self
878 .diagnostics
879 .entries_for_file(&lookup_path)
880 .into_iter()
881 .find_map(|(key, entry)| if key == server_key { Some(entry) } else { None })
882 else {
883 return false;
884 };
885
886 if entry.stale {
887 return false;
888 }
889
890 let target_version = self
891 .documents
892 .get(server_key)
893 .and_then(|store| store.version(&lookup_path))
894 .or(pre.document_version_at_capture)
895 .unwrap_or(0);
896
897 matches!(entry.version, Some(version) if version >= target_version)
898 }
899
900 pub fn wait_for_post_edit_diagnostics(
923 &mut self,
924 file_path: &Path,
925 _config: &Config,
929 expected_versions: &[(ServerKey, i32)],
930 pre_snapshot: &HashMap<ServerKey, PreEditSnapshot>,
931 timeout: std::time::Duration,
932 ) -> PostEditWaitOutcome {
933 let lookup_path = normalize_lookup_path(file_path);
934 let deadline = std::time::Instant::now() + timeout;
935
936 let _ = self.drain_events_for_file(&lookup_path);
941
942 let mut fresh: HashMap<ServerKey, Vec<StoredDiagnostic>> = HashMap::new();
943 let mut exited: Vec<ServerKey> = Vec::new();
944
945 loop {
946 for (key, target_version) in expected_versions {
954 if fresh.contains_key(key) || exited.contains(key) {
955 continue;
956 }
957 if !self.clients.contains_key(key) {
958 exited.push(key.clone());
959 continue;
960 }
961 if let Some(entry) = self
962 .diagnostics
963 .entries_for_file(&lookup_path)
964 .into_iter()
965 .find_map(|(k, e)| if k == key { Some(e) } else { None })
966 {
967 let pre = pre_snapshot.get(key).copied().unwrap_or_default();
968 let is_fresh = post_edit_entry_is_fresh(entry, *target_version, pre);
969 if is_fresh {
970 fresh.insert(key.clone(), entry.diagnostics.clone());
971 }
972 }
973 }
974
975 if fresh.len() + exited.len() == expected_versions.len() {
977 break;
978 }
979
980 let now = std::time::Instant::now();
981 if now >= deadline {
982 break;
983 }
984
985 let timeout = deadline.saturating_duration_since(now);
986 match self.event_rx.recv_timeout(timeout) {
987 Ok(event) => {
988 self.handle_event(&event);
989 }
990 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
991 }
992 }
993
994 let pending: Vec<ServerKey> = expected_versions
996 .iter()
997 .filter(|(k, _)| !fresh.contains_key(k) && !exited.contains(k))
998 .map(|(k, _)| k.clone())
999 .collect();
1000
1001 let mut diagnostics: Vec<StoredDiagnostic> = fresh
1004 .into_iter()
1005 .flat_map(|(_, diags)| diags.into_iter())
1006 .collect();
1007 diagnostics.sort_by(|a, b| {
1008 a.file
1009 .cmp(&b.file)
1010 .then(a.line.cmp(&b.line))
1011 .then(a.column.cmp(&b.column))
1012 .then(a.message.cmp(&b.message))
1013 });
1014
1015 PostEditWaitOutcome {
1016 diagnostics,
1017 pending_servers: pending,
1018 exited_servers: exited,
1019 }
1020 }
1021
1022 pub fn wait_for_file_diagnostics(
1028 &mut self,
1029 file_path: &Path,
1030 config: &Config,
1031 deadline: std::time::Instant,
1032 ) -> Vec<StoredDiagnostic> {
1033 let lookup_path = normalize_lookup_path(file_path);
1034
1035 if self.server_key_for_file(&lookup_path, config).is_none() {
1036 return Vec::new();
1037 }
1038
1039 loop {
1040 if self.drain_events_for_file(&lookup_path) {
1041 break;
1042 }
1043
1044 let now = std::time::Instant::now();
1045 if now >= deadline {
1046 break;
1047 }
1048
1049 let timeout = deadline.saturating_duration_since(now);
1050 match self.event_rx.recv_timeout(timeout) {
1051 Ok(event) => {
1052 if matches!(
1053 self.handle_event(&event),
1054 Some(ref published_file) if published_file.as_path() == lookup_path.as_path()
1055 ) {
1056 break;
1057 }
1058 }
1059 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
1060 }
1061 }
1062
1063 self.get_diagnostics_for_file(&lookup_path)
1064 .into_iter()
1065 .cloned()
1066 .collect()
1067 }
1068
1069 pub const PULL_FILE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
1075
1076 pub fn pull_file_timeout() -> std::time::Duration {
1078 Self::PULL_FILE_TIMEOUT
1079 }
1080
1081 const PULL_WORKSPACE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
1085
1086 pub fn pull_file_diagnostics(
1097 &mut self,
1098 file_path: &Path,
1099 config: &Config,
1100 ) -> Result<Vec<PullFileResult>, LspError> {
1101 let canonical_path = canonicalize_for_lsp(file_path)?;
1102 self.ensure_file_open(&canonical_path, config)?;
1105
1106 let server_keys = self.ensure_server_for_file(&canonical_path, config);
1107 if server_keys.is_empty() {
1108 return Ok(Vec::new());
1109 }
1110
1111 let uri = uri_for_path(&canonical_path)?;
1112 let mut results = Vec::with_capacity(server_keys.len());
1113
1114 for key in server_keys {
1115 let supports_pull = self
1116 .clients
1117 .get(&key)
1118 .and_then(|c| c.diagnostic_capabilities())
1119 .is_some_and(|caps| caps.pull_diagnostics);
1120
1121 if !supports_pull {
1122 results.push(PullFileResult {
1123 server_key: key.clone(),
1124 outcome: PullFileOutcome::PullNotSupported,
1125 });
1126 continue;
1127 }
1128
1129 let previous_result_id = self
1131 .diagnostics
1132 .entries_for_file(&canonical_path)
1133 .into_iter()
1134 .find(|(k, _)| **k == key)
1135 .and_then(|(_, entry)| entry.result_id.clone());
1136
1137 let identifier = self
1138 .clients
1139 .get(&key)
1140 .and_then(|c| c.diagnostic_capabilities())
1141 .and_then(|caps| caps.identifier.clone());
1142
1143 let params = AftDocumentDiagnosticParams {
1144 text_document: lsp_types::TextDocumentIdentifier { uri: uri.clone() },
1145 identifier,
1146 previous_result_id,
1147 work_done_progress_params: Default::default(),
1148 partial_result_params: Default::default(),
1149 };
1150
1151 let outcome = match self.send_pull_request(&key, params) {
1152 Ok(report) => self.ingest_document_report(&key, &canonical_path, report),
1153 Err(err) => {
1154 if let Some(result) = self.cache_post_initialize_exit(&key, &err) {
1155 PullFileOutcome::RequestFailed {
1156 reason: server_attempt_result_reason(&result),
1157 }
1158 } else if recoverable_pull_rejection(&err)
1159 && self.clients.get(&key).is_some_and(|client| {
1160 matches!(
1161 client.state(),
1162 ServerState::Ready | ServerState::Initializing
1163 )
1164 })
1165 {
1166 PullFileOutcome::RequestFailed {
1167 reason: format!("pull_rejected_push_fallback: {err}"),
1168 }
1169 } else {
1170 PullFileOutcome::RequestFailed {
1171 reason: err.to_string(),
1172 }
1173 }
1174 }
1175 };
1176
1177 results.push(PullFileResult {
1178 server_key: key,
1179 outcome,
1180 });
1181 }
1182
1183 Ok(results)
1184 }
1185
1186 pub fn pull_workspace_diagnostics(
1191 &mut self,
1192 server_key: &ServerKey,
1193 timeout: Option<std::time::Duration>,
1194 ) -> Result<PullWorkspaceResult, LspError> {
1195 let timeout = timeout.unwrap_or(Self::PULL_WORKSPACE_TIMEOUT);
1196
1197 let supports_workspace = self
1198 .clients
1199 .get(server_key)
1200 .and_then(|c| c.diagnostic_capabilities())
1201 .is_some_and(|caps| caps.workspace_diagnostics);
1202
1203 if !supports_workspace {
1204 return Ok(PullWorkspaceResult {
1205 server_key: server_key.clone(),
1206 files_reported: Vec::new(),
1207 complete: false,
1208 cancelled: false,
1209 supports_workspace: false,
1210 });
1211 }
1212
1213 let identifier = self
1214 .clients
1215 .get(server_key)
1216 .and_then(|c| c.diagnostic_capabilities())
1217 .and_then(|caps| caps.identifier.clone());
1218
1219 let params = AftWorkspaceDiagnosticParams {
1220 identifier,
1221 previous_result_ids: Vec::new(),
1222 work_done_progress_params: Default::default(),
1223 partial_result_params: Default::default(),
1224 };
1225
1226 let result = match self
1227 .clients
1228 .get_mut(server_key)
1229 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?
1230 .send_request_with_timeout::<AftWorkspaceDiagnosticRequest>(params, timeout)
1231 {
1232 Ok(result) => result,
1233 Err(LspError::Timeout(_)) => {
1234 return Ok(PullWorkspaceResult {
1235 server_key: server_key.clone(),
1236 files_reported: Vec::new(),
1237 complete: false,
1238 cancelled: true,
1239 supports_workspace: true,
1240 });
1241 }
1242 Err(err) => {
1243 if let Some(result) = self.cache_post_initialize_exit(server_key, &err) {
1244 return Err(LspError::ServerNotReady(server_attempt_result_reason(
1245 &result,
1246 )));
1247 }
1248 return Err(err);
1249 }
1250 };
1251
1252 let (items, complete) = match result {
1256 lsp_types::WorkspaceDiagnosticReportResult::Report(report) => (report.items, true),
1257 lsp_types::WorkspaceDiagnosticReportResult::Partial(partial) => (partial.items, false),
1258 };
1259
1260 let mut files_reported = Vec::with_capacity(items.len());
1262 for item in items {
1263 match item {
1264 lsp_types::WorkspaceDocumentDiagnosticReport::Full(full) => {
1265 if let Some(file) = uri_to_path(&full.uri) {
1266 let stored = from_lsp_diagnostics(
1267 file.clone(),
1268 full.full_document_diagnostic_report.items.clone(),
1269 );
1270 self.diagnostics.publish_with_result_id(
1271 server_key.clone(),
1272 file.clone(),
1273 stored,
1274 full.full_document_diagnostic_report.result_id.clone(),
1275 );
1276 files_reported.push(file);
1277 }
1278 }
1279 lsp_types::WorkspaceDocumentDiagnosticReport::Unchanged(_unchanged) => {
1280 }
1283 }
1284 }
1285
1286 Ok(PullWorkspaceResult {
1287 server_key: server_key.clone(),
1288 files_reported,
1289 complete,
1290 cancelled: false,
1291 supports_workspace: true,
1292 })
1293 }
1294
1295 fn cache_post_initialize_exit(
1296 &mut self,
1297 key: &ServerKey,
1298 err: &LspError,
1299 ) -> Option<ServerAttemptResult> {
1300 let binary = self
1301 .server_binaries
1302 .get(key)
1303 .cloned()
1304 .unwrap_or_else(|| key.kind.id_str().to_string());
1305 let (status, stderr_tail) = {
1306 let client = self.clients.get_mut(key)?;
1307 let mut status = client.child_exit_status();
1308 for _ in 0..10 {
1309 if status.is_some() {
1310 break;
1311 }
1312 std::thread::sleep(std::time::Duration::from_millis(10));
1313 status = client.child_exit_status();
1314 }
1315 let status = status?;
1316 wait_for_stderr_tail(client);
1317 (status, client.stderr_tail())
1318 };
1319 let reason = format_post_initialize_exit_reason(&binary, status, &stderr_tail, err);
1320 let result = ServerAttemptResult::SpawnFailed { binary, reason };
1321 self.clients.remove(key);
1322 self.server_binaries.remove(key);
1323 self.documents.remove(key);
1324 self.diagnostics.clear_for_server(key);
1325 self.failed_spawns.insert(key.clone(), result.clone());
1326 Some(result)
1327 }
1328
1329 fn send_pull_request(
1331 &mut self,
1332 key: &ServerKey,
1333 params: AftDocumentDiagnosticParams,
1334 ) -> Result<lsp_types::DocumentDiagnosticReportResult, LspError> {
1335 let client = self
1336 .clients
1337 .get_mut(key)
1338 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?;
1339 client.send_request_with_timeout::<AftDocumentDiagnosticRequest>(
1343 params,
1344 Self::PULL_FILE_TIMEOUT,
1345 )
1346 }
1347
1348 fn ingest_document_report(
1351 &mut self,
1352 key: &ServerKey,
1353 canonical_path: &Path,
1354 result: lsp_types::DocumentDiagnosticReportResult,
1355 ) -> PullFileOutcome {
1356 let report = match result {
1357 lsp_types::DocumentDiagnosticReportResult::Report(report) => report,
1358 lsp_types::DocumentDiagnosticReportResult::Partial(_) => {
1359 return PullFileOutcome::PartialNotSupported;
1363 }
1364 };
1365
1366 match report {
1367 lsp_types::DocumentDiagnosticReport::Full(full) => {
1368 let result_id = full.full_document_diagnostic_report.result_id.clone();
1369 let stored = from_lsp_diagnostics(
1370 canonical_path.to_path_buf(),
1371 full.full_document_diagnostic_report.items.clone(),
1372 );
1373 let count = stored.len();
1374 self.diagnostics.publish_with_result_id(
1375 key.clone(),
1376 canonical_path.to_path_buf(),
1377 stored,
1378 result_id,
1379 );
1380 PullFileOutcome::Full {
1381 diagnostic_count: count,
1382 }
1383 }
1384 lsp_types::DocumentDiagnosticReport::Unchanged(_unchanged) => {
1385 if self
1392 .diagnostics
1393 .has_report_for_server_file(key, canonical_path)
1394 {
1395 self.diagnostics
1396 .mark_fresh_for_server_file(key, canonical_path);
1397 PullFileOutcome::Unchanged
1398 } else {
1399 PullFileOutcome::RequestFailed {
1400 reason: "no_cache_for_unchanged".to_string(),
1401 }
1402 }
1403 }
1404 }
1405 }
1406
1407 pub fn shutdown_all(&mut self) {
1409 for (key, mut client) in self.clients.drain() {
1410 if let Err(err) = client.shutdown() {
1411 slog_error!("error shutting down {:?}: {}", key, err);
1412 }
1413 }
1414 self.server_binaries.clear();
1415 self.documents.clear();
1416 self.diagnostics = DiagnosticsStore::new();
1417 }
1418
1419 pub fn has_active_servers(&self) -> bool {
1421 self.clients
1422 .values()
1423 .any(|client| client.state() == ServerState::Ready)
1424 }
1425
1426 pub fn active_server_keys(&self) -> Vec<ServerKey> {
1429 self.clients.keys().cloned().collect()
1430 }
1431
1432 pub fn get_diagnostics_for_file(&self, file: &Path) -> Vec<&StoredDiagnostic> {
1433 let normalized = normalize_lookup_path(file);
1434 self.diagnostics.for_file(&normalized)
1435 }
1436
1437 pub fn clear_failed_spawns(&mut self) -> usize {
1458 let n = self.failed_spawns.len();
1459 self.failed_spawns.clear();
1460 n
1461 }
1462
1463 #[cfg(test)]
1464 pub(crate) fn insert_failed_spawn_for_test(&mut self) {
1465 let key = ServerKey {
1466 kind: crate::lsp::registry::ServerKind::Rust,
1467 root: std::path::PathBuf::from("/tmp/test-root"),
1468 };
1469 self.failed_spawns.insert(
1470 key,
1471 ServerAttemptResult::SpawnFailed {
1472 binary: "rust-analyzer".to_string(),
1473 reason: "test".to_string(),
1474 },
1475 );
1476 }
1477
1478 pub fn clear_diagnostics_for_file(&mut self, file: &Path) -> bool {
1479 let mut removed = self.diagnostics.clear_for_file(file);
1480
1481 let normalized = normalize_lookup_path(file);
1482 if normalized != file {
1483 removed |= self.diagnostics.clear_for_file(&normalized);
1484 }
1485
1486 if let (Some(parent), Some(name)) = (file.parent(), file.file_name()) {
1489 if let Ok(canonical_parent) = std::fs::canonicalize(parent) {
1490 let reconstructed = canonical_parent.join(name);
1491 if reconstructed != file && reconstructed != normalized {
1492 removed |= self.diagnostics.clear_for_file(&reconstructed);
1493 }
1494 }
1495 }
1496
1497 removed
1498 }
1499
1500 pub fn mark_diagnostics_stale_for_file(&mut self, file: &Path) -> StaleDiagnosticsMark {
1504 let mut candidates = vec![file.to_path_buf()];
1505 let normalized = normalize_lookup_path(file);
1506 if !candidates.iter().any(|candidate| candidate == &normalized) {
1507 candidates.push(normalized.clone());
1508 }
1509
1510 if let (Some(parent), Some(name)) = (file.parent(), file.file_name()) {
1511 if let Ok(canonical_parent) = std::fs::canonicalize(parent) {
1512 let reconstructed = canonical_parent.join(name);
1513 if !candidates
1514 .iter()
1515 .any(|candidate| candidate == &reconstructed)
1516 {
1517 candidates.push(reconstructed);
1518 }
1519 }
1520 }
1521
1522 let mut result = StaleDiagnosticsMark::default();
1523 for candidate in candidates {
1524 let (had_entries, changed) = self.diagnostics.mark_stale_for_file(&candidate);
1525 result.had_entries |= had_entries;
1526 result.changed |= changed;
1527 }
1528 result
1529 }
1530
1531 pub fn get_diagnostics_for_directory(&self, dir: &Path) -> Vec<&StoredDiagnostic> {
1532 let normalized = normalize_lookup_path(dir);
1533 self.diagnostics.for_directory(&normalized)
1534 }
1535
1536 pub fn get_all_diagnostics(&self) -> Vec<&StoredDiagnostic> {
1537 self.diagnostics.all()
1538 }
1539
1540 pub fn has_any_diagnostic_reports(&self) -> bool {
1545 self.diagnostics.has_any_fresh_report()
1546 }
1547
1548 pub fn has_diagnostic_report_for_file(&self, file: &Path) -> bool {
1552 let normalized = normalize_lookup_path(file);
1553 self.diagnostics.has_any_fresh_report_for_file(&normalized)
1554 }
1555
1556 pub fn has_diagnostic_report_for_server_file(&self, server: &ServerKey, file: &Path) -> bool {
1560 let normalized = normalize_lookup_path(file);
1561 self.diagnostics
1562 .has_fresh_report_for_server_file(server, &normalized)
1563 }
1564
1565 fn drain_events_for_file(&mut self, file_path: &Path) -> bool {
1566 let mut saw_file_diagnostics = false;
1567 while let Ok(event) = self.event_rx.try_recv() {
1568 if matches!(
1569 self.handle_event(&event),
1570 Some(ref published_file) if published_file.as_path() == file_path
1571 ) {
1572 saw_file_diagnostics = true;
1573 }
1574 }
1575 saw_file_diagnostics
1576 }
1577
1578 fn handle_event(&mut self, event: &LspEvent) -> Option<PathBuf> {
1579 match event {
1580 LspEvent::Notification {
1581 server_kind,
1582 root,
1583 method,
1584 params: Some(params),
1585 } if method == "textDocument/publishDiagnostics" => {
1586 self.handle_publish_diagnostics(server_kind.clone(), root.clone(), params)
1587 }
1588 LspEvent::ServerExited { server_kind, root } => {
1589 let key = ServerKey {
1590 kind: server_kind.clone(),
1591 root: root.clone(),
1592 };
1593 self.clients.remove(&key);
1594 self.server_binaries.remove(&key);
1595 self.documents.remove(&key);
1596 self.diagnostics.clear_for_server(&key);
1597 None
1598 }
1599 _ => None,
1600 }
1601 }
1602
1603 fn handle_publish_diagnostics(
1604 &mut self,
1605 server: ServerKind,
1606 root: PathBuf,
1607 params: &serde_json::Value,
1608 ) -> Option<PathBuf> {
1609 if let Ok(publish_params) =
1610 serde_json::from_value::<lsp_types::PublishDiagnosticsParams>(params.clone())
1611 {
1612 let file = uri_to_path(&publish_params.uri)?;
1613 let stored = from_lsp_diagnostics(file.clone(), publish_params.diagnostics);
1614 let key = ServerKey { kind: server, root };
1620 self.diagnostics
1621 .publish_full(key, file.clone(), stored, None, publish_params.version);
1622 return Some(file);
1623 }
1624 None
1625 }
1626
1627 fn spawn_server(
1628 &self,
1629 def: &ServerDef,
1630 root: &Path,
1631 config: &Config,
1632 ) -> Result<LspClient, LspError> {
1633 let binary = self.resolve_binary(def, config)?;
1634
1635 let mut merged_env = def.env.clone();
1639 for (key, value) in &self.extra_env {
1640 merged_env.insert(key.clone(), value.clone());
1641 }
1642
1643 let mut client = LspClient::spawn(
1644 def.kind.clone(),
1645 root.to_path_buf(),
1646 &binary,
1647 &def.args,
1648 &merged_env,
1649 self.event_tx.clone(),
1650 self.child_registry.clone(),
1651 )?;
1652 if let Err(err) = client.initialize(root, def.initialization_options.clone()) {
1653 wait_for_stderr_tail(&mut client);
1654 let stderr_tail = client.stderr_tail();
1655 let reason = if client.child_exited() || !stderr_tail.is_empty() {
1656 format_initialize_failure_reason(&def.binary, &stderr_tail, &err)
1657 } else {
1658 format!("server failed during initialize: {err}")
1659 };
1660 return Err(LspError::ServerNotReady(reason));
1661 }
1662 Ok(client)
1663 }
1664
1665 fn resolve_binary(&self, def: &ServerDef, config: &Config) -> Result<PathBuf, LspError> {
1666 if let Some(path) = self.binary_overrides.get(&def.kind) {
1667 if path.exists() {
1668 return Ok(path.clone());
1669 }
1670 return Err(LspError::NotFound(format!(
1671 "override binary for {:?} not found: {}",
1672 def.kind,
1673 path.display()
1674 )));
1675 }
1676
1677 if let Some(path) = env_binary_override(&def.kind) {
1678 if path.exists() {
1679 return Ok(path);
1680 }
1681 return Err(LspError::NotFound(format!(
1682 "environment override binary for {:?} not found: {}",
1683 def.kind,
1684 path.display()
1685 )));
1686 }
1687
1688 resolve_lsp_binary(
1693 &def.binary,
1694 config.project_root.as_deref(),
1695 &config.lsp_paths_extra,
1696 )
1697 .ok_or_else(|| {
1698 LspError::NotFound(format!(
1699 "language server binary '{}' not found in node_modules/.bin, lsp_paths_extra, or PATH",
1700 def.binary
1701 ))
1702 })
1703 }
1704
1705 fn server_key_for_file(&self, file_path: &Path, config: &Config) -> Option<ServerKey> {
1706 for def in servers_for_file(file_path, config) {
1707 let root = def.workspace_root_for_file(file_path)?;
1708 let key = ServerKey {
1709 kind: def.kind.clone(),
1710 root,
1711 };
1712 if self.clients.contains_key(&key) {
1713 return Some(key);
1714 }
1715 }
1716 None
1717 }
1718}
1719
1720impl Default for LspManager {
1721 fn default() -> Self {
1722 Self::new()
1723 }
1724}
1725
1726fn wait_for_stderr_tail(client: &mut LspClient) {
1727 for _ in 0..10 {
1728 if !client.stderr_tail().is_empty() {
1729 break;
1730 }
1731 std::thread::sleep(std::time::Duration::from_millis(10));
1732 }
1733}
1734
1735fn recoverable_pull_rejection(err: &LspError) -> bool {
1736 matches!(
1737 err,
1738 LspError::ServerError {
1739 code: -32601 | -32602,
1740 ..
1741 }
1742 )
1743}
1744
1745fn server_attempt_result_reason(result: &ServerAttemptResult) -> String {
1746 match result {
1747 ServerAttemptResult::SpawnFailed { binary, reason } => {
1748 format!("spawn_failed: {binary} ({reason})")
1749 }
1750 ServerAttemptResult::BinaryNotInstalled { binary } => {
1751 format!("binary_not_installed: {binary}")
1752 }
1753 ServerAttemptResult::NoRootMarker { looked_for } => {
1754 format!("no_root_marker (looked for: {})", looked_for.join(", "))
1755 }
1756 ServerAttemptResult::Ok { .. } => "ok".to_string(),
1757 }
1758}
1759
1760fn format_stderr_tail_for_reason(stderr_tail: &str) -> String {
1761 truncate_stderr_tail_for_reason(stderr_tail)
1762 .lines()
1763 .map(|line| format!(" {line}"))
1764 .collect::<Vec<_>>()
1765 .join("\n")
1766}
1767
1768fn truncate_stderr_tail_for_reason(stderr_tail: &str) -> String {
1769 if stderr_tail.len() <= STDERR_REASON_BYTES {
1770 return stderr_tail.to_string();
1771 }
1772
1773 let ellipsis = "...";
1774 let target_len = STDERR_REASON_BYTES.saturating_sub(ellipsis.len());
1775 let mut start = stderr_tail.len() - target_len;
1776 while start < stderr_tail.len() && !stderr_tail.is_char_boundary(start) {
1777 start += 1;
1778 }
1779 format!("{ellipsis}{}", &stderr_tail[start..])
1780}
1781
1782fn format_initialize_failure_reason(binary: &str, stderr_tail: &str, err: &LspError) -> String {
1783 let mut reason = format!("server crashed during initialize: {err}");
1784 if !stderr_tail.is_empty() {
1785 reason.push_str("; stderr (last 64 lines):\n");
1786 reason.push_str(&format_stderr_tail_for_reason(stderr_tail));
1787 reason.push_str("\n\n");
1788 reason.push_str(&failure_hint(binary, stderr_tail));
1789 }
1790 reason
1791}
1792
1793fn format_post_initialize_exit_reason(
1794 binary: &str,
1795 status: std::process::ExitStatus,
1796 stderr_tail: &str,
1797 err: &LspError,
1798) -> String {
1799 let code = status
1800 .code()
1801 .map(|c| c.to_string())
1802 .unwrap_or_else(|| "signal/unknown".to_string());
1803 let mut reason = format!("server exited after initialize (code {code}): {err}");
1804 if !stderr_tail.is_empty() {
1805 reason.push_str("; stderr (last 64 lines):\n");
1806 reason.push_str(&format_stderr_tail_for_reason(stderr_tail));
1807 reason.push_str("\n\n");
1808 reason.push_str(&failure_hint(binary, stderr_tail));
1809 }
1810 reason
1811}
1812
1813fn failure_hint(binary: &str, stderr_tail: &str) -> String {
1814 if stderr_tail.contains("MODULE_NOT_FOUND") || stderr_tail.contains("Cannot find module") {
1815 let package_manager = infer_package_manager(stderr_tail);
1816 format!(
1817 "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."
1818 )
1819 } else if let Some(component) = rustup_missing_component(stderr_tail) {
1820 format!("'{component}' is a rustup proxy but the component is not installed. Install it: rustup component add {component}")
1825 } else {
1826 format!("Hint: see stderr above for '{binary}' failure details.")
1827 }
1828}
1829
1830fn rustup_missing_component(stderr_tail: &str) -> Option<String> {
1836 let marker = "Unknown binary '";
1837 let start = stderr_tail.find(marker)? + marker.len();
1838 let rest = &stderr_tail[start..];
1839 let end = rest.find('\'')?;
1840 let name = &rest[..end];
1841 if name.is_empty() || !stderr_tail.contains("toolchain") {
1844 return None;
1845 }
1846 Some(name.to_string())
1847}
1848
1849fn infer_package_manager(stderr_tail: &str) -> &'static str {
1850 let lower = stderr_tail.to_ascii_lowercase();
1851 if lower.contains(".pnpm/") || lower.contains(".pnpm\\") || lower.contains("/pnpm/") {
1852 "pnpm"
1853 } else if lower.contains(".yarn/")
1854 || lower.contains(".yarn\\")
1855 || lower.contains("/yarn/")
1856 || lower.contains("yarn")
1857 {
1858 "yarn"
1859 } else {
1860 "npm"
1861 }
1862}
1863
1864fn canonicalize_for_lsp(file_path: &Path) -> Result<PathBuf, LspError> {
1865 std::fs::canonicalize(file_path).map_err(LspError::from)
1866}
1867
1868fn resolve_for_lsp_uri(file_path: &Path) -> PathBuf {
1869 if let Ok(path) = std::fs::canonicalize(file_path) {
1870 return path;
1871 }
1872
1873 let mut existing = file_path.to_path_buf();
1874 let mut missing = Vec::new();
1875 while !existing.exists() {
1876 let Some(name) = existing.file_name() else {
1877 break;
1878 };
1879 missing.push(name.to_owned());
1880 let Some(parent) = existing.parent() else {
1881 break;
1882 };
1883 existing = parent.to_path_buf();
1884 }
1885
1886 let mut resolved = std::fs::canonicalize(&existing).unwrap_or(existing);
1887 for segment in missing.into_iter().rev() {
1888 resolved.push(segment);
1889 }
1890 resolved
1891}
1892
1893fn language_id_for_extension(ext: &str) -> &'static str {
1894 match ext {
1895 "ts" => "typescript",
1896 "tsx" => "typescriptreact",
1897 "js" | "mjs" | "cjs" => "javascript",
1898 "jsx" => "javascriptreact",
1899 "py" | "pyi" => "python",
1900 "rs" => "rust",
1901 "go" => "go",
1902 "html" | "htm" => "html",
1903 _ => "plaintext",
1904 }
1905}
1906
1907fn normalize_lookup_path(path: &Path) -> PathBuf {
1908 std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
1909}
1910
1911fn classify_spawn_error(binary: &str, err: &LspError) -> ServerAttemptResult {
1918 match err {
1919 LspError::NotFound(_) => ServerAttemptResult::BinaryNotInstalled {
1924 binary: binary.to_string(),
1925 },
1926 other => ServerAttemptResult::SpawnFailed {
1927 binary: binary.to_string(),
1928 reason: other.to_string(),
1929 },
1930 }
1931}
1932
1933fn env_binary_override(kind: &ServerKind) -> Option<PathBuf> {
1934 let id = kind.id_str();
1935 let suffix: String = id
1936 .chars()
1937 .map(|ch| {
1938 if ch.is_ascii_alphanumeric() {
1939 ch.to_ascii_uppercase()
1940 } else {
1941 '_'
1942 }
1943 })
1944 .collect();
1945 let key = format!("AFT_LSP_{suffix}_BINARY");
1946 std::env::var_os(key).map(PathBuf::from)
1947}
1948
1949#[cfg(test)]
1950mod failure_hint_tests {
1951 use super::{failure_hint, rustup_missing_component};
1952
1953 #[test]
1954 fn detects_rustup_proxy_without_component() {
1955 let stderr = "error: Unknown binary 'rust-analyzer' in official toolchain 'stable-aarch64-apple-darwin'.";
1957 assert_eq!(
1958 rustup_missing_component(stderr).as_deref(),
1959 Some("rust-analyzer")
1960 );
1961 let hint = failure_hint("rust-analyzer", stderr);
1962 assert!(
1963 hint.contains("rustup component add rust-analyzer"),
1964 "expected actionable rustup hint, got: {hint}"
1965 );
1966 }
1967
1968 #[test]
1969 fn ignores_unknown_binary_without_toolchain_phrasing() {
1970 let stderr = "fatal: Unknown binary 'foo' was requested by the linker.";
1973 assert_eq!(rustup_missing_component(stderr), None);
1974 assert!(failure_hint("foo", stderr).starts_with("Hint: see stderr"));
1975 }
1976
1977 #[test]
1978 fn npm_module_not_found_still_wins() {
1979 let stderr = "Error: Cannot find module '/x/typescript-language-server/lib/cli.mjs'";
1981 let hint = failure_hint("typescript-language-server", stderr);
1982 assert!(hint.contains("install -g"), "got: {hint}");
1983 }
1984}
1985
1986#[cfg(test)]
1987mod diagnostic_capacity_tests {
1988 use super::LspManager;
1989
1990 #[test]
1995 fn set_diagnostic_capacity_propagates_to_store() {
1996 let mut manager = LspManager::new();
1997 manager.set_diagnostic_capacity(7);
1998 assert_eq!(manager.diagnostics_store_for_test().capacity_for_test(), 7);
1999 manager.set_diagnostic_capacity(0); assert_eq!(manager.diagnostics_store_for_test().capacity_for_test(), 0);
2001 }
2002
2003 #[test]
2006 fn clear_failed_spawns_empties_the_cache() {
2007 let mut manager = LspManager::new();
2008 assert_eq!(manager.clear_failed_spawns(), 0);
2009 manager.insert_failed_spawn_for_test();
2010 assert_eq!(manager.clear_failed_spawns(), 1);
2011 assert_eq!(manager.clear_failed_spawns(), 0);
2012 }
2013}
2014
2015#[cfg(test)]
2016mod clear_diagnostics_tests {
2017 use std::path::PathBuf;
2018
2019 use super::LspManager;
2020 use crate::lsp::client::LspEvent;
2021 use crate::lsp::diagnostics::{DiagnosticSeverity, StoredDiagnostic};
2022 use crate::lsp::position::uri_for_path;
2023 use crate::lsp::registry::ServerKind;
2024 use crate::lsp::roots::ServerKey;
2025
2026 fn err_diag(file: &PathBuf) -> StoredDiagnostic {
2027 StoredDiagnostic {
2028 file: file.clone(),
2029 line: 1,
2030 column: 1,
2031 end_line: 1,
2032 end_column: 2,
2033 severity: DiagnosticSeverity::Error,
2034 message: "boom".into(),
2035 code: None,
2036 source: None,
2037 }
2038 }
2039
2040 #[test]
2045 fn clear_diagnostics_for_deleted_file_matches_canonical_key() {
2046 let dir = tempfile::tempdir().unwrap();
2047 let canonical_dir = std::fs::canonicalize(dir.path()).unwrap();
2049 let canonical_file = canonical_dir.join("gone.ts");
2050 std::fs::write(&canonical_file, "x").unwrap();
2053
2054 let mut manager = LspManager::new();
2055 let key = ServerKey {
2056 kind: ServerKind::TypeScript,
2057 root: canonical_dir.clone(),
2058 };
2059 manager.diagnostics_store_mut_for_test().publish(
2060 key,
2061 canonical_file.clone(),
2062 vec![err_diag(&canonical_file)],
2063 );
2064 assert_eq!(manager.warm_error_warning_counts(), (1, 0));
2065
2066 std::fs::remove_file(&canonical_file).unwrap();
2067
2068 let watcher_path = dir.path().join("gone.ts");
2071 let removed = manager.clear_diagnostics_for_file(&watcher_path);
2072
2073 assert!(removed, "expected the deleted file's diagnostic to clear");
2074 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
2075 }
2076
2077 #[test]
2078 fn clear_diagnostics_for_unknown_file_is_noop() {
2079 let mut manager = LspManager::new();
2080 assert!(!manager.clear_diagnostics_for_file(&PathBuf::from("/nope/missing.ts")));
2081 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
2082 }
2083
2084 #[test]
2085 fn drain_events_reports_publish_diagnostics_updates() {
2086 let dir = tempfile::tempdir().unwrap();
2087 let root = std::fs::canonicalize(dir.path()).unwrap();
2088 let file = root.join("main.ts");
2089 std::fs::write(&file, "const x: number = 'nope';").unwrap();
2090
2091 let mut manager = LspManager::new();
2092 let diagnostic = lsp_types::Diagnostic {
2093 range: lsp_types::Range {
2094 start: lsp_types::Position {
2095 line: 0,
2096 character: 0,
2097 },
2098 end: lsp_types::Position {
2099 line: 0,
2100 character: 1,
2101 },
2102 },
2103 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
2104 code: None,
2105 code_description: None,
2106 source: Some("test".into()),
2107 message: "boom".into(),
2108 related_information: None,
2109 tags: None,
2110 data: None,
2111 };
2112 let params = serde_json::to_value(lsp_types::PublishDiagnosticsParams {
2113 uri: uri_for_path(&file).unwrap(),
2114 diagnostics: vec![diagnostic],
2115 version: Some(1),
2116 })
2117 .unwrap();
2118 manager
2119 .event_tx
2120 .send(LspEvent::Notification {
2121 server_kind: ServerKind::TypeScript,
2122 root,
2123 method: "textDocument/publishDiagnostics".into(),
2124 params: Some(params),
2125 })
2126 .unwrap();
2127
2128 let drained = manager.drain_events();
2129
2130 assert!(drained.diagnostics_changed);
2131 assert_eq!(drained.events.len(), 1);
2132 assert_eq!(manager.warm_error_warning_counts(), (1, 0));
2133 }
2134}