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