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::{find_workspace_root, 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
130pub fn post_edit_entry_is_fresh(
131 entry: &DiagnosticEntry,
132 target_version: i32,
133 pre: PreEditSnapshot,
134) -> bool {
135 if entry.epoch <= pre.epoch {
136 return false;
137 }
138
139 match entry.version {
140 Some(version) => version >= target_version,
141 None => false,
146 }
147}
148
149impl PostEditWaitOutcome {
150 pub fn complete(&self) -> bool {
153 self.pending_servers.is_empty() && self.exited_servers.is_empty()
154 }
155}
156
157#[derive(Debug, Clone)]
159pub enum PullFileOutcome {
160 Full { diagnostic_count: usize },
162 Unchanged,
164 PartialNotSupported,
167 PullNotSupported,
170 RequestFailed { reason: String },
172}
173
174#[derive(Debug, Clone)]
176pub struct PullFileResult {
177 pub server_key: ServerKey,
178 pub outcome: PullFileOutcome,
179}
180
181#[derive(Debug, Clone)]
183pub struct PullWorkspaceResult {
184 pub server_key: ServerKey,
185 pub files_reported: Vec<PathBuf>,
189 pub complete: bool,
191 pub cancelled: bool,
193 pub supports_workspace: bool,
197}
198
199pub struct DrainedLspEvents {
200 pub events: Vec<LspEvent>,
201 pub diagnostics_changed: bool,
202}
203
204impl IntoIterator for DrainedLspEvents {
205 type Item = LspEvent;
206 type IntoIter = std::vec::IntoIter<LspEvent>;
207
208 fn into_iter(self) -> Self::IntoIter {
209 self.events.into_iter()
210 }
211}
212
213pub struct LspManager {
214 clients: HashMap<ServerKey, LspClient>,
216 server_binaries: HashMap<ServerKey, String>,
220 documents: HashMap<ServerKey, DocumentStore>,
222 diagnostics: DiagnosticsStore,
224 event_tx: Sender<LspEvent>,
226 event_rx: Receiver<LspEvent>,
227 binary_overrides: HashMap<ServerKind, PathBuf>,
229 extra_env: HashMap<String, String>,
233 failed_spawns: HashMap<ServerKey, ServerAttemptResult>,
248 watched_file_skip_logged: HashSet<ServerKey>,
251 child_registry: LspChildRegistry,
255}
256
257impl LspManager {
258 pub fn new() -> Self {
259 let (event_tx, event_rx) = unbounded();
260 Self {
261 clients: HashMap::new(),
262 server_binaries: HashMap::new(),
263 documents: HashMap::new(),
264 diagnostics: DiagnosticsStore::new(),
265 event_tx,
266 event_rx,
267 binary_overrides: HashMap::new(),
268 extra_env: HashMap::new(),
269 failed_spawns: HashMap::new(),
270 watched_file_skip_logged: HashSet::new(),
271 child_registry: LspChildRegistry::new(),
272 }
273 }
274
275 pub fn set_child_registry(&mut self, registry: LspChildRegistry) {
277 self.child_registry = registry;
278 }
279
280 pub fn set_extra_env(&mut self, key: &str, value: &str) {
284 self.extra_env.insert(key.to_string(), value.to_string());
285 }
286
287 pub fn server_count(&self) -> usize {
289 self.clients.len()
290 }
291
292 pub fn set_diagnostic_capacity(&mut self, capacity: usize) {
296 self.diagnostics.set_capacity(capacity);
297 }
298
299 pub fn override_binary(&mut self, kind: ServerKind, binary_path: PathBuf) {
301 self.binary_overrides.insert(kind, binary_path);
302 }
303
304 pub fn ensure_server_for_file(&mut self, file_path: &Path, config: &Config) -> Vec<ServerKey> {
311 self.ensure_server_for_file_detailed(file_path, config)
312 .successful
313 }
314
315 pub fn ensure_server_for_file_detailed(
323 &mut self,
324 file_path: &Path,
325 config: &Config,
326 ) -> EnsureServerOutcomes {
327 let defs = servers_for_file(file_path, config);
328 let mut outcomes = EnsureServerOutcomes::default();
329
330 for def in defs {
331 let server_id = def.kind.id_str().to_string();
332 let server_name = def.name.to_string();
333
334 let Some(root) = find_workspace_root(file_path, &def.root_markers) else {
335 outcomes.attempts.push(ServerAttempt {
336 server_id,
337 server_name,
338 result: ServerAttemptResult::NoRootMarker {
339 looked_for: def.root_markers.iter().map(|s| s.to_string()).collect(),
340 },
341 });
342 continue;
343 };
344
345 let key = ServerKey {
346 kind: def.kind.clone(),
347 root,
348 };
349
350 if !self.clients.contains_key(&key) {
351 if let Some(cached) = self.failed_spawns.get(&key) {
358 outcomes.attempts.push(ServerAttempt {
359 server_id,
360 server_name,
361 result: cached.clone(),
362 });
363 continue;
364 }
365
366 match self.spawn_server(&def, &key.root, config) {
367 Ok(client) => {
368 self.clients.insert(key.clone(), client);
369 self.server_binaries.insert(key.clone(), def.binary.clone());
370 self.documents.entry(key.clone()).or_default();
371 }
372 Err(err) => {
373 slog_error!("failed to spawn {}: {}", def.name, err);
374 let result = classify_spawn_error(&def.binary, &err);
375 self.failed_spawns.insert(key.clone(), result.clone());
379 outcomes.attempts.push(ServerAttempt {
380 server_id,
381 server_name,
382 result,
383 });
384 continue;
385 }
386 }
387 }
388
389 outcomes.attempts.push(ServerAttempt {
390 server_id,
391 server_name,
392 result: ServerAttemptResult::Ok {
393 server_key: key.clone(),
394 },
395 });
396 outcomes.successful.push(key);
397 }
398
399 outcomes
400 }
401
402 pub fn ensure_server_for_file_default(&mut self, file_path: &Path) -> Vec<ServerKey> {
405 self.ensure_server_for_file(file_path, &Config::default())
406 }
407 pub fn ensure_file_open(
411 &mut self,
412 file_path: &Path,
413 config: &Config,
414 ) -> Result<Vec<ServerKey>, LspError> {
415 let canonical_path = canonicalize_for_lsp(file_path)?;
416 let server_keys = self.ensure_server_for_file(&canonical_path, config);
417 if server_keys.is_empty() {
418 return Ok(server_keys);
419 }
420
421 let uri = uri_for_path(&canonical_path)?;
422 let language_id = language_id_for_extension(
423 canonical_path
424 .extension()
425 .and_then(|ext| ext.to_str())
426 .unwrap_or_default(),
427 )
428 .to_string();
429
430 for key in &server_keys {
431 let already_open = self
432 .documents
433 .get(key)
434 .is_some_and(|store| store.is_open(&canonical_path));
435
436 if !already_open {
437 let content = std::fs::read_to_string(&canonical_path).map_err(LspError::Io)?;
438 if let Some(client) = self.clients.get_mut(key) {
439 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
440 text_document: TextDocumentItem::new(
441 uri.clone(),
442 language_id.clone(),
443 0,
444 content,
445 ),
446 })?;
447 }
448 self.documents
449 .entry(key.clone())
450 .or_default()
451 .open(canonical_path.clone());
452 continue;
453 }
454
455 let drifted = self
465 .documents
466 .get(key)
467 .is_some_and(|store| store.is_stale_on_disk(&canonical_path));
468 if drifted {
469 let content = std::fs::read_to_string(&canonical_path).map_err(LspError::Io)?;
470 let next_version = self
471 .documents
472 .get(key)
473 .and_then(|store| store.version(&canonical_path))
474 .map(|v| v + 1)
475 .unwrap_or(1);
476 if let Some(client) = self.clients.get_mut(key) {
477 client.send_notification::<DidChangeTextDocument>(
478 DidChangeTextDocumentParams {
479 text_document: VersionedTextDocumentIdentifier::new(
480 uri.clone(),
481 next_version,
482 ),
483 content_changes: vec![TextDocumentContentChangeEvent {
484 range: None,
485 range_length: None,
486 text: content,
487 }],
488 },
489 )?;
490 }
491 if let Some(store) = self.documents.get_mut(key) {
492 store.bump_version(&canonical_path);
493 }
494 }
495 }
496
497 Ok(server_keys)
498 }
499
500 pub fn ensure_file_open_default(
501 &mut self,
502 file_path: &Path,
503 ) -> Result<Vec<ServerKey>, LspError> {
504 self.ensure_file_open(file_path, &Config::default())
505 }
506
507 pub fn notify_file_changed(
513 &mut self,
514 file_path: &Path,
515 content: &str,
516 config: &Config,
517 ) -> Result<(), LspError> {
518 self.notify_file_changed_versioned(file_path, content, config)
519 .map(|_| ())
520 }
521
522 pub fn notify_file_changed_versioned(
533 &mut self,
534 file_path: &Path,
535 content: &str,
536 config: &Config,
537 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
538 let canonical_path = canonicalize_for_lsp(file_path)?;
539 let server_keys = self.ensure_server_for_file(&canonical_path, config);
540 if server_keys.is_empty() {
541 return Ok(Vec::new());
542 }
543
544 let uri = uri_for_path(&canonical_path)?;
545 let language_id = language_id_for_extension(
546 canonical_path
547 .extension()
548 .and_then(|ext| ext.to_str())
549 .unwrap_or_default(),
550 )
551 .to_string();
552
553 let mut versions: Vec<(ServerKey, i32)> = Vec::with_capacity(server_keys.len());
554
555 for key in server_keys {
556 let current_version = self
557 .documents
558 .get(&key)
559 .and_then(|store| store.version(&canonical_path));
560
561 if let Some(version) = current_version {
562 let next_version = version + 1;
563 if let Some(client) = self.clients.get_mut(&key) {
564 client.send_notification::<DidChangeTextDocument>(
565 DidChangeTextDocumentParams {
566 text_document: VersionedTextDocumentIdentifier::new(
567 uri.clone(),
568 next_version,
569 ),
570 content_changes: vec![TextDocumentContentChangeEvent {
571 range: None,
572 range_length: None,
573 text: content.to_string(),
574 }],
575 },
576 )?;
577 }
578 if let Some(store) = self.documents.get_mut(&key) {
579 store.bump_version(&canonical_path);
580 }
581 versions.push((key, next_version));
582 continue;
583 }
584
585 if let Some(client) = self.clients.get_mut(&key) {
586 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
587 text_document: TextDocumentItem::new(
588 uri.clone(),
589 language_id.clone(),
590 0,
591 content.to_string(),
592 ),
593 })?;
594 }
595 self.documents
596 .entry(key.clone())
597 .or_default()
598 .open(canonical_path.clone());
599 versions.push((key, 0));
602 }
603
604 Ok(versions)
605 }
606
607 pub fn notify_file_changed_default(
608 &mut self,
609 file_path: &Path,
610 content: &str,
611 ) -> Result<(), LspError> {
612 self.notify_file_changed(file_path, content, &Config::default())
613 }
614
615 pub fn notify_files_watched_changed(
621 &mut self,
622 paths: &[(PathBuf, FileChangeType)],
623 _config: &Config,
624 ) -> Result<(), LspError> {
625 if paths.is_empty() {
626 return Ok(());
627 }
628
629 let mut canonical_events = Vec::with_capacity(paths.len());
630 for (path, typ) in paths {
631 let canonical_path = resolve_for_lsp_uri(path);
632 canonical_events.push((canonical_path, *typ));
633 }
634
635 let keys: Vec<ServerKey> = self.clients.keys().cloned().collect();
636 for key in keys {
637 let mut changes = Vec::new();
638 for (path, typ) in &canonical_events {
639 if !path.starts_with(&key.root) {
640 continue;
641 }
642 changes.push(FileEvent::new(uri_for_path(path)?, *typ));
643 }
644
645 if changes.is_empty() {
646 continue;
647 }
648
649 if let Some(client) = self.clients.get_mut(&key) {
650 let supports_static_watched_files = client.supports_watched_files();
656 let has_dynamic_registration = client.has_watched_file_registration();
657 if !(supports_static_watched_files || has_dynamic_registration) {
658 if self.watched_file_skip_logged.insert(key.clone()) {
659 log::debug!(
660 "skipping didChangeWatchedFiles for {:?} (not supported or registered)",
661 key
662 );
663 }
664 continue;
665 }
666 client.send_notification::<DidChangeWatchedFiles>(DidChangeWatchedFilesParams {
667 changes,
668 })?;
669 }
670 }
671
672 Ok(())
673 }
674
675 pub fn notify_file_closed(&mut self, file_path: &Path) -> Result<(), LspError> {
677 let canonical_path = canonicalize_for_lsp(file_path)?;
678 let uri = uri_for_path(&canonical_path)?;
679 let keys: Vec<ServerKey> = self.documents.keys().cloned().collect();
680
681 for key in keys {
682 let was_open = self
683 .documents
684 .get(&key)
685 .map(|store| store.is_open(&canonical_path))
686 .unwrap_or(false);
687 if !was_open {
688 continue;
689 }
690
691 if let Some(client) = self.clients.get_mut(&key) {
692 client.send_notification::<DidCloseTextDocument>(DidCloseTextDocumentParams {
693 text_document: TextDocumentIdentifier::new(uri.clone()),
694 })?;
695 }
696
697 if let Some(store) = self.documents.get_mut(&key) {
698 store.close(&canonical_path);
699 }
700 self.diagnostics
701 .clear_for_server_file(&key, &canonical_path);
702 }
703
704 Ok(())
705 }
706
707 pub fn client_for_file(&self, file_path: &Path, config: &Config) -> Option<&LspClient> {
709 let key = self.server_key_for_file(file_path, config)?;
710 self.clients.get(&key)
711 }
712
713 pub fn client_for_file_default(&self, file_path: &Path) -> Option<&LspClient> {
714 self.client_for_file(file_path, &Config::default())
715 }
716
717 pub fn client_for_file_mut(
719 &mut self,
720 file_path: &Path,
721 config: &Config,
722 ) -> Option<&mut LspClient> {
723 let key = self.server_key_for_file(file_path, config)?;
724 self.clients.get_mut(&key)
725 }
726
727 pub fn client_for_file_mut_default(&mut self, file_path: &Path) -> Option<&mut LspClient> {
728 self.client_for_file_mut(file_path, &Config::default())
729 }
730
731 pub fn active_client_count(&self) -> usize {
733 self.clients.len()
734 }
735
736 pub fn drain_events(&mut self) -> DrainedLspEvents {
738 let mut events = Vec::new();
739 let mut diagnostics_changed = false;
740 while let Ok(event) = self.event_rx.try_recv() {
741 if self.handle_event(&event).is_some() {
742 diagnostics_changed = true;
743 }
744 events.push(event);
745 }
746 DrainedLspEvents {
747 events,
748 diagnostics_changed,
749 }
750 }
751
752 pub fn wait_for_diagnostics(
754 &mut self,
755 file_path: &Path,
756 config: &Config,
757 timeout: std::time::Duration,
758 ) -> Vec<StoredDiagnostic> {
759 let deadline = std::time::Instant::now() + timeout;
760 self.wait_for_file_diagnostics(file_path, config, deadline)
761 }
762
763 pub fn wait_for_diagnostics_default(
764 &mut self,
765 file_path: &Path,
766 timeout: std::time::Duration,
767 ) -> Vec<StoredDiagnostic> {
768 self.wait_for_diagnostics(file_path, &Config::default(), timeout)
769 }
770
771 #[doc(hidden)]
776 pub fn diagnostics_store_for_test(&self) -> &DiagnosticsStore {
777 &self.diagnostics
778 }
779
780 #[doc(hidden)]
781 pub fn diagnostics_store_mut_for_test(&mut self) -> &mut DiagnosticsStore {
782 &mut self.diagnostics
783 }
784
785 pub fn warm_error_warning_counts(&self) -> (usize, usize) {
789 self.diagnostics.error_warning_counts()
790 }
791
792 pub fn filtered_error_warning_counts(
797 &self,
798 keep: impl FnMut(&std::path::Path) -> bool,
799 ) -> (usize, usize) {
800 self.diagnostics.filtered_error_warning_counts(keep)
801 }
802
803 pub fn snapshot_diagnostic_epochs(&self, file_path: &Path) -> HashMap<ServerKey, u64> {
808 let lookup_path = normalize_lookup_path(file_path);
809 self.diagnostics
810 .entries_for_file(&lookup_path)
811 .into_iter()
812 .map(|(key, entry)| (key.clone(), entry.epoch))
813 .collect()
814 }
815
816 pub fn snapshot_pre_edit_state(&self, file_path: &Path) -> HashMap<ServerKey, PreEditSnapshot> {
819 let lookup_path = normalize_lookup_path(file_path);
820 let mut snapshots: HashMap<ServerKey, PreEditSnapshot> = self
821 .diagnostics
822 .entries_for_file(&lookup_path)
823 .into_iter()
824 .map(|(key, entry)| {
825 (
826 key.clone(),
827 PreEditSnapshot {
828 epoch: entry.epoch,
829 document_version_at_capture: None,
830 },
831 )
832 })
833 .collect();
834
835 for (key, store) in &self.documents {
836 if let Some(version) = store.version(&lookup_path) {
837 snapshots
838 .entry(key.clone())
839 .or_default()
840 .document_version_at_capture = Some(version);
841 }
842 }
843
844 snapshots
845 }
846
847 pub fn diagnostic_entry_is_fresh_for_document(
855 &self,
856 file_path: &Path,
857 server_key: &ServerKey,
858 pre: PreEditSnapshot,
859 ) -> bool {
860 let lookup_path = normalize_lookup_path(file_path);
861 let Some(entry) = self
862 .diagnostics
863 .entries_for_file(&lookup_path)
864 .into_iter()
865 .find_map(|(key, entry)| if key == server_key { Some(entry) } else { None })
866 else {
867 return false;
868 };
869
870 let target_version = self
871 .documents
872 .get(server_key)
873 .and_then(|store| store.version(&lookup_path))
874 .or(pre.document_version_at_capture)
875 .unwrap_or(0);
876
877 matches!(entry.version, Some(version) if version >= target_version)
878 }
879
880 pub fn wait_for_post_edit_diagnostics(
903 &mut self,
904 file_path: &Path,
905 _config: &Config,
909 expected_versions: &[(ServerKey, i32)],
910 pre_snapshot: &HashMap<ServerKey, PreEditSnapshot>,
911 timeout: std::time::Duration,
912 ) -> PostEditWaitOutcome {
913 let lookup_path = normalize_lookup_path(file_path);
914 let deadline = std::time::Instant::now() + timeout;
915
916 let _ = self.drain_events_for_file(&lookup_path);
921
922 let mut fresh: HashMap<ServerKey, Vec<StoredDiagnostic>> = HashMap::new();
923 let mut exited: Vec<ServerKey> = Vec::new();
924
925 loop {
926 for (key, target_version) in expected_versions {
934 if fresh.contains_key(key) || exited.contains(key) {
935 continue;
936 }
937 if !self.clients.contains_key(key) {
938 exited.push(key.clone());
939 continue;
940 }
941 if let Some(entry) = self
942 .diagnostics
943 .entries_for_file(&lookup_path)
944 .into_iter()
945 .find_map(|(k, e)| if k == key { Some(e) } else { None })
946 {
947 let pre = pre_snapshot.get(key).copied().unwrap_or_default();
948 let is_fresh = post_edit_entry_is_fresh(entry, *target_version, pre);
949 if is_fresh {
950 fresh.insert(key.clone(), entry.diagnostics.clone());
951 }
952 }
953 }
954
955 if fresh.len() + exited.len() == expected_versions.len() {
957 break;
958 }
959
960 let now = std::time::Instant::now();
961 if now >= deadline {
962 break;
963 }
964
965 let timeout = deadline.saturating_duration_since(now);
966 match self.event_rx.recv_timeout(timeout) {
967 Ok(event) => {
968 self.handle_event(&event);
969 }
970 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
971 }
972 }
973
974 let pending: Vec<ServerKey> = expected_versions
976 .iter()
977 .filter(|(k, _)| !fresh.contains_key(k) && !exited.contains(k))
978 .map(|(k, _)| k.clone())
979 .collect();
980
981 let mut diagnostics: Vec<StoredDiagnostic> = fresh
984 .into_iter()
985 .flat_map(|(_, diags)| diags.into_iter())
986 .collect();
987 diagnostics.sort_by(|a, b| {
988 a.file
989 .cmp(&b.file)
990 .then(a.line.cmp(&b.line))
991 .then(a.column.cmp(&b.column))
992 .then(a.message.cmp(&b.message))
993 });
994
995 PostEditWaitOutcome {
996 diagnostics,
997 pending_servers: pending,
998 exited_servers: exited,
999 }
1000 }
1001
1002 pub fn wait_for_file_diagnostics(
1008 &mut self,
1009 file_path: &Path,
1010 config: &Config,
1011 deadline: std::time::Instant,
1012 ) -> Vec<StoredDiagnostic> {
1013 let lookup_path = normalize_lookup_path(file_path);
1014
1015 if self.server_key_for_file(&lookup_path, config).is_none() {
1016 return Vec::new();
1017 }
1018
1019 loop {
1020 if self.drain_events_for_file(&lookup_path) {
1021 break;
1022 }
1023
1024 let now = std::time::Instant::now();
1025 if now >= deadline {
1026 break;
1027 }
1028
1029 let timeout = deadline.saturating_duration_since(now);
1030 match self.event_rx.recv_timeout(timeout) {
1031 Ok(event) => {
1032 if matches!(
1033 self.handle_event(&event),
1034 Some(ref published_file) if published_file.as_path() == lookup_path.as_path()
1035 ) {
1036 break;
1037 }
1038 }
1039 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
1040 }
1041 }
1042
1043 self.get_diagnostics_for_file(&lookup_path)
1044 .into_iter()
1045 .cloned()
1046 .collect()
1047 }
1048
1049 pub const PULL_FILE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
1055
1056 pub fn pull_file_timeout() -> std::time::Duration {
1058 Self::PULL_FILE_TIMEOUT
1059 }
1060
1061 const PULL_WORKSPACE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
1065
1066 pub fn pull_file_diagnostics(
1077 &mut self,
1078 file_path: &Path,
1079 config: &Config,
1080 ) -> Result<Vec<PullFileResult>, LspError> {
1081 let canonical_path = canonicalize_for_lsp(file_path)?;
1082 self.ensure_file_open(&canonical_path, config)?;
1085
1086 let server_keys = self.ensure_server_for_file(&canonical_path, config);
1087 if server_keys.is_empty() {
1088 return Ok(Vec::new());
1089 }
1090
1091 let uri = uri_for_path(&canonical_path)?;
1092 let mut results = Vec::with_capacity(server_keys.len());
1093
1094 for key in server_keys {
1095 let supports_pull = self
1096 .clients
1097 .get(&key)
1098 .and_then(|c| c.diagnostic_capabilities())
1099 .is_some_and(|caps| caps.pull_diagnostics);
1100
1101 if !supports_pull {
1102 results.push(PullFileResult {
1103 server_key: key.clone(),
1104 outcome: PullFileOutcome::PullNotSupported,
1105 });
1106 continue;
1107 }
1108
1109 let previous_result_id = self
1111 .diagnostics
1112 .entries_for_file(&canonical_path)
1113 .into_iter()
1114 .find(|(k, _)| **k == key)
1115 .and_then(|(_, entry)| entry.result_id.clone());
1116
1117 let identifier = self
1118 .clients
1119 .get(&key)
1120 .and_then(|c| c.diagnostic_capabilities())
1121 .and_then(|caps| caps.identifier.clone());
1122
1123 let params = AftDocumentDiagnosticParams {
1124 text_document: lsp_types::TextDocumentIdentifier { uri: uri.clone() },
1125 identifier,
1126 previous_result_id,
1127 work_done_progress_params: Default::default(),
1128 partial_result_params: Default::default(),
1129 };
1130
1131 let outcome = match self.send_pull_request(&key, params) {
1132 Ok(report) => self.ingest_document_report(&key, &canonical_path, report),
1133 Err(err) => {
1134 if let Some(result) = self.cache_post_initialize_exit(&key, &err) {
1135 PullFileOutcome::RequestFailed {
1136 reason: server_attempt_result_reason(&result),
1137 }
1138 } else if recoverable_pull_rejection(&err)
1139 && self.clients.get(&key).is_some_and(|client| {
1140 matches!(
1141 client.state(),
1142 ServerState::Ready | ServerState::Initializing
1143 )
1144 })
1145 {
1146 PullFileOutcome::RequestFailed {
1147 reason: format!("pull_rejected_push_fallback: {err}"),
1148 }
1149 } else {
1150 PullFileOutcome::RequestFailed {
1151 reason: err.to_string(),
1152 }
1153 }
1154 }
1155 };
1156
1157 results.push(PullFileResult {
1158 server_key: key,
1159 outcome,
1160 });
1161 }
1162
1163 Ok(results)
1164 }
1165
1166 pub fn pull_workspace_diagnostics(
1171 &mut self,
1172 server_key: &ServerKey,
1173 timeout: Option<std::time::Duration>,
1174 ) -> Result<PullWorkspaceResult, LspError> {
1175 let timeout = timeout.unwrap_or(Self::PULL_WORKSPACE_TIMEOUT);
1176
1177 let supports_workspace = self
1178 .clients
1179 .get(server_key)
1180 .and_then(|c| c.diagnostic_capabilities())
1181 .is_some_and(|caps| caps.workspace_diagnostics);
1182
1183 if !supports_workspace {
1184 return Ok(PullWorkspaceResult {
1185 server_key: server_key.clone(),
1186 files_reported: Vec::new(),
1187 complete: false,
1188 cancelled: false,
1189 supports_workspace: false,
1190 });
1191 }
1192
1193 let identifier = self
1194 .clients
1195 .get(server_key)
1196 .and_then(|c| c.diagnostic_capabilities())
1197 .and_then(|caps| caps.identifier.clone());
1198
1199 let params = AftWorkspaceDiagnosticParams {
1200 identifier,
1201 previous_result_ids: Vec::new(),
1202 work_done_progress_params: Default::default(),
1203 partial_result_params: Default::default(),
1204 };
1205
1206 let result = match self
1207 .clients
1208 .get_mut(server_key)
1209 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?
1210 .send_request_with_timeout::<AftWorkspaceDiagnosticRequest>(params, timeout)
1211 {
1212 Ok(result) => result,
1213 Err(LspError::Timeout(_)) => {
1214 return Ok(PullWorkspaceResult {
1215 server_key: server_key.clone(),
1216 files_reported: Vec::new(),
1217 complete: false,
1218 cancelled: true,
1219 supports_workspace: true,
1220 });
1221 }
1222 Err(err) => {
1223 if let Some(result) = self.cache_post_initialize_exit(server_key, &err) {
1224 return Err(LspError::ServerNotReady(server_attempt_result_reason(
1225 &result,
1226 )));
1227 }
1228 return Err(err);
1229 }
1230 };
1231
1232 let (items, complete) = match result {
1236 lsp_types::WorkspaceDiagnosticReportResult::Report(report) => (report.items, true),
1237 lsp_types::WorkspaceDiagnosticReportResult::Partial(partial) => (partial.items, false),
1238 };
1239
1240 let mut files_reported = Vec::with_capacity(items.len());
1242 for item in items {
1243 match item {
1244 lsp_types::WorkspaceDocumentDiagnosticReport::Full(full) => {
1245 if let Some(file) = uri_to_path(&full.uri) {
1246 let stored = from_lsp_diagnostics(
1247 file.clone(),
1248 full.full_document_diagnostic_report.items.clone(),
1249 );
1250 self.diagnostics.publish_with_result_id(
1251 server_key.clone(),
1252 file.clone(),
1253 stored,
1254 full.full_document_diagnostic_report.result_id.clone(),
1255 );
1256 files_reported.push(file);
1257 }
1258 }
1259 lsp_types::WorkspaceDocumentDiagnosticReport::Unchanged(_unchanged) => {
1260 }
1263 }
1264 }
1265
1266 Ok(PullWorkspaceResult {
1267 server_key: server_key.clone(),
1268 files_reported,
1269 complete,
1270 cancelled: false,
1271 supports_workspace: true,
1272 })
1273 }
1274
1275 fn cache_post_initialize_exit(
1276 &mut self,
1277 key: &ServerKey,
1278 err: &LspError,
1279 ) -> Option<ServerAttemptResult> {
1280 let binary = self
1281 .server_binaries
1282 .get(key)
1283 .cloned()
1284 .unwrap_or_else(|| key.kind.id_str().to_string());
1285 let (status, stderr_tail) = {
1286 let client = self.clients.get_mut(key)?;
1287 let mut status = client.child_exit_status();
1288 for _ in 0..10 {
1289 if status.is_some() {
1290 break;
1291 }
1292 std::thread::sleep(std::time::Duration::from_millis(10));
1293 status = client.child_exit_status();
1294 }
1295 let status = status?;
1296 wait_for_stderr_tail(client);
1297 (status, client.stderr_tail())
1298 };
1299 let reason = format_post_initialize_exit_reason(&binary, status, &stderr_tail, err);
1300 let result = ServerAttemptResult::SpawnFailed { binary, reason };
1301 self.clients.remove(key);
1302 self.server_binaries.remove(key);
1303 self.documents.remove(key);
1304 self.diagnostics.clear_for_server(key);
1305 self.failed_spawns.insert(key.clone(), result.clone());
1306 Some(result)
1307 }
1308
1309 fn send_pull_request(
1311 &mut self,
1312 key: &ServerKey,
1313 params: AftDocumentDiagnosticParams,
1314 ) -> Result<lsp_types::DocumentDiagnosticReportResult, LspError> {
1315 let client = self
1316 .clients
1317 .get_mut(key)
1318 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?;
1319 client.send_request_with_timeout::<AftDocumentDiagnosticRequest>(
1323 params,
1324 Self::PULL_FILE_TIMEOUT,
1325 )
1326 }
1327
1328 fn ingest_document_report(
1331 &mut self,
1332 key: &ServerKey,
1333 canonical_path: &Path,
1334 result: lsp_types::DocumentDiagnosticReportResult,
1335 ) -> PullFileOutcome {
1336 let report = match result {
1337 lsp_types::DocumentDiagnosticReportResult::Report(report) => report,
1338 lsp_types::DocumentDiagnosticReportResult::Partial(_) => {
1339 return PullFileOutcome::PartialNotSupported;
1343 }
1344 };
1345
1346 match report {
1347 lsp_types::DocumentDiagnosticReport::Full(full) => {
1348 let result_id = full.full_document_diagnostic_report.result_id.clone();
1349 let stored = from_lsp_diagnostics(
1350 canonical_path.to_path_buf(),
1351 full.full_document_diagnostic_report.items.clone(),
1352 );
1353 let count = stored.len();
1354 self.diagnostics.publish_with_result_id(
1355 key.clone(),
1356 canonical_path.to_path_buf(),
1357 stored,
1358 result_id,
1359 );
1360 PullFileOutcome::Full {
1361 diagnostic_count: count,
1362 }
1363 }
1364 lsp_types::DocumentDiagnosticReport::Unchanged(_unchanged) => {
1365 if self
1369 .diagnostics
1370 .has_report_for_server_file(key, canonical_path)
1371 {
1372 PullFileOutcome::Unchanged
1373 } else {
1374 PullFileOutcome::RequestFailed {
1375 reason: "no_cache_for_unchanged".to_string(),
1376 }
1377 }
1378 }
1379 }
1380 }
1381
1382 pub fn shutdown_all(&mut self) {
1384 for (key, mut client) in self.clients.drain() {
1385 if let Err(err) = client.shutdown() {
1386 slog_error!("error shutting down {:?}: {}", key, err);
1387 }
1388 }
1389 self.server_binaries.clear();
1390 self.documents.clear();
1391 self.diagnostics = DiagnosticsStore::new();
1392 }
1393
1394 pub fn has_active_servers(&self) -> bool {
1396 self.clients
1397 .values()
1398 .any(|client| client.state() == ServerState::Ready)
1399 }
1400
1401 pub fn active_server_keys(&self) -> Vec<ServerKey> {
1404 self.clients.keys().cloned().collect()
1405 }
1406
1407 pub fn get_diagnostics_for_file(&self, file: &Path) -> Vec<&StoredDiagnostic> {
1408 let normalized = normalize_lookup_path(file);
1409 self.diagnostics.for_file(&normalized)
1410 }
1411
1412 pub fn clear_failed_spawns(&mut self) -> usize {
1433 let n = self.failed_spawns.len();
1434 self.failed_spawns.clear();
1435 n
1436 }
1437
1438 #[cfg(test)]
1439 pub(crate) fn insert_failed_spawn_for_test(&mut self) {
1440 let key = ServerKey {
1441 kind: crate::lsp::registry::ServerKind::Rust,
1442 root: std::path::PathBuf::from("/tmp/test-root"),
1443 };
1444 self.failed_spawns.insert(
1445 key,
1446 ServerAttemptResult::SpawnFailed {
1447 binary: "rust-analyzer".to_string(),
1448 reason: "test".to_string(),
1449 },
1450 );
1451 }
1452
1453 pub fn clear_diagnostics_for_file(&mut self, file: &Path) -> bool {
1454 let mut removed = self.diagnostics.clear_for_file(file);
1455
1456 let normalized = normalize_lookup_path(file);
1457 if normalized != file {
1458 removed |= self.diagnostics.clear_for_file(&normalized);
1459 }
1460
1461 if let (Some(parent), Some(name)) = (file.parent(), file.file_name()) {
1464 if let Ok(canonical_parent) = std::fs::canonicalize(parent) {
1465 let reconstructed = canonical_parent.join(name);
1466 if reconstructed != file && reconstructed != normalized {
1467 removed |= self.diagnostics.clear_for_file(&reconstructed);
1468 }
1469 }
1470 }
1471
1472 removed
1473 }
1474
1475 pub fn get_diagnostics_for_directory(&self, dir: &Path) -> Vec<&StoredDiagnostic> {
1476 let normalized = normalize_lookup_path(dir);
1477 self.diagnostics.for_directory(&normalized)
1478 }
1479
1480 pub fn get_all_diagnostics(&self) -> Vec<&StoredDiagnostic> {
1481 self.diagnostics.all()
1482 }
1483
1484 pub fn has_any_diagnostic_reports(&self) -> bool {
1489 !self.diagnostics.is_empty()
1490 }
1491
1492 pub fn has_diagnostic_report_for_file(&self, file: &Path) -> bool {
1495 let normalized = normalize_lookup_path(file);
1496 self.diagnostics.has_any_report_for_file(&normalized)
1497 }
1498
1499 pub fn has_diagnostic_report_for_server_file(&self, server: &ServerKey, file: &Path) -> bool {
1502 let normalized = normalize_lookup_path(file);
1503 self.diagnostics
1504 .has_report_for_server_file(server, &normalized)
1505 }
1506
1507 fn drain_events_for_file(&mut self, file_path: &Path) -> bool {
1508 let mut saw_file_diagnostics = false;
1509 while let Ok(event) = self.event_rx.try_recv() {
1510 if matches!(
1511 self.handle_event(&event),
1512 Some(ref published_file) if published_file.as_path() == file_path
1513 ) {
1514 saw_file_diagnostics = true;
1515 }
1516 }
1517 saw_file_diagnostics
1518 }
1519
1520 fn handle_event(&mut self, event: &LspEvent) -> Option<PathBuf> {
1521 match event {
1522 LspEvent::Notification {
1523 server_kind,
1524 root,
1525 method,
1526 params: Some(params),
1527 } if method == "textDocument/publishDiagnostics" => {
1528 self.handle_publish_diagnostics(server_kind.clone(), root.clone(), params)
1529 }
1530 LspEvent::ServerExited { server_kind, root } => {
1531 let key = ServerKey {
1532 kind: server_kind.clone(),
1533 root: root.clone(),
1534 };
1535 self.clients.remove(&key);
1536 self.server_binaries.remove(&key);
1537 self.documents.remove(&key);
1538 self.diagnostics.clear_for_server(&key);
1539 None
1540 }
1541 _ => None,
1542 }
1543 }
1544
1545 fn handle_publish_diagnostics(
1546 &mut self,
1547 server: ServerKind,
1548 root: PathBuf,
1549 params: &serde_json::Value,
1550 ) -> Option<PathBuf> {
1551 if let Ok(publish_params) =
1552 serde_json::from_value::<lsp_types::PublishDiagnosticsParams>(params.clone())
1553 {
1554 let file = uri_to_path(&publish_params.uri)?;
1555 let stored = from_lsp_diagnostics(file.clone(), publish_params.diagnostics);
1556 let key = ServerKey { kind: server, root };
1562 self.diagnostics
1563 .publish_full(key, file.clone(), stored, None, publish_params.version);
1564 return Some(file);
1565 }
1566 None
1567 }
1568
1569 fn spawn_server(
1570 &self,
1571 def: &ServerDef,
1572 root: &Path,
1573 config: &Config,
1574 ) -> Result<LspClient, LspError> {
1575 let binary = self.resolve_binary(def, config)?;
1576
1577 let mut merged_env = def.env.clone();
1581 for (key, value) in &self.extra_env {
1582 merged_env.insert(key.clone(), value.clone());
1583 }
1584
1585 let mut client = LspClient::spawn(
1586 def.kind.clone(),
1587 root.to_path_buf(),
1588 &binary,
1589 &def.args,
1590 &merged_env,
1591 self.event_tx.clone(),
1592 self.child_registry.clone(),
1593 )?;
1594 if let Err(err) = client.initialize(root, def.initialization_options.clone()) {
1595 wait_for_stderr_tail(&mut client);
1596 let stderr_tail = client.stderr_tail();
1597 let reason = if client.child_exited() || !stderr_tail.is_empty() {
1598 format_initialize_failure_reason(&def.binary, &stderr_tail, &err)
1599 } else {
1600 format!("server failed during initialize: {err}")
1601 };
1602 return Err(LspError::ServerNotReady(reason));
1603 }
1604 Ok(client)
1605 }
1606
1607 fn resolve_binary(&self, def: &ServerDef, config: &Config) -> Result<PathBuf, LspError> {
1608 if let Some(path) = self.binary_overrides.get(&def.kind) {
1609 if path.exists() {
1610 return Ok(path.clone());
1611 }
1612 return Err(LspError::NotFound(format!(
1613 "override binary for {:?} not found: {}",
1614 def.kind,
1615 path.display()
1616 )));
1617 }
1618
1619 if let Some(path) = env_binary_override(&def.kind) {
1620 if path.exists() {
1621 return Ok(path);
1622 }
1623 return Err(LspError::NotFound(format!(
1624 "environment override binary for {:?} not found: {}",
1625 def.kind,
1626 path.display()
1627 )));
1628 }
1629
1630 resolve_lsp_binary(
1635 &def.binary,
1636 config.project_root.as_deref(),
1637 &config.lsp_paths_extra,
1638 )
1639 .ok_or_else(|| {
1640 LspError::NotFound(format!(
1641 "language server binary '{}' not found in node_modules/.bin, lsp_paths_extra, or PATH",
1642 def.binary
1643 ))
1644 })
1645 }
1646
1647 fn server_key_for_file(&self, file_path: &Path, config: &Config) -> Option<ServerKey> {
1648 for def in servers_for_file(file_path, config) {
1649 let root = find_workspace_root(file_path, &def.root_markers)?;
1650 let key = ServerKey {
1651 kind: def.kind.clone(),
1652 root,
1653 };
1654 if self.clients.contains_key(&key) {
1655 return Some(key);
1656 }
1657 }
1658 None
1659 }
1660}
1661
1662impl Default for LspManager {
1663 fn default() -> Self {
1664 Self::new()
1665 }
1666}
1667
1668fn wait_for_stderr_tail(client: &mut LspClient) {
1669 for _ in 0..10 {
1670 if !client.stderr_tail().is_empty() {
1671 break;
1672 }
1673 std::thread::sleep(std::time::Duration::from_millis(10));
1674 }
1675}
1676
1677fn recoverable_pull_rejection(err: &LspError) -> bool {
1678 matches!(
1679 err,
1680 LspError::ServerError {
1681 code: -32601 | -32602,
1682 ..
1683 }
1684 )
1685}
1686
1687fn server_attempt_result_reason(result: &ServerAttemptResult) -> String {
1688 match result {
1689 ServerAttemptResult::SpawnFailed { binary, reason } => {
1690 format!("spawn_failed: {binary} ({reason})")
1691 }
1692 ServerAttemptResult::BinaryNotInstalled { binary } => {
1693 format!("binary_not_installed: {binary}")
1694 }
1695 ServerAttemptResult::NoRootMarker { looked_for } => {
1696 format!("no_root_marker (looked for: {})", looked_for.join(", "))
1697 }
1698 ServerAttemptResult::Ok { .. } => "ok".to_string(),
1699 }
1700}
1701
1702fn format_stderr_tail_for_reason(stderr_tail: &str) -> String {
1703 truncate_stderr_tail_for_reason(stderr_tail)
1704 .lines()
1705 .map(|line| format!(" {line}"))
1706 .collect::<Vec<_>>()
1707 .join("\n")
1708}
1709
1710fn truncate_stderr_tail_for_reason(stderr_tail: &str) -> String {
1711 if stderr_tail.len() <= STDERR_REASON_BYTES {
1712 return stderr_tail.to_string();
1713 }
1714
1715 let ellipsis = "...";
1716 let target_len = STDERR_REASON_BYTES.saturating_sub(ellipsis.len());
1717 let mut start = stderr_tail.len() - target_len;
1718 while start < stderr_tail.len() && !stderr_tail.is_char_boundary(start) {
1719 start += 1;
1720 }
1721 format!("{ellipsis}{}", &stderr_tail[start..])
1722}
1723
1724fn format_initialize_failure_reason(binary: &str, stderr_tail: &str, err: &LspError) -> String {
1725 let mut reason = format!("server crashed during initialize: {err}");
1726 if !stderr_tail.is_empty() {
1727 reason.push_str("; stderr (last 64 lines):\n");
1728 reason.push_str(&format_stderr_tail_for_reason(stderr_tail));
1729 reason.push_str("\n\n");
1730 reason.push_str(&failure_hint(binary, stderr_tail));
1731 }
1732 reason
1733}
1734
1735fn format_post_initialize_exit_reason(
1736 binary: &str,
1737 status: std::process::ExitStatus,
1738 stderr_tail: &str,
1739 err: &LspError,
1740) -> String {
1741 let code = status
1742 .code()
1743 .map(|c| c.to_string())
1744 .unwrap_or_else(|| "signal/unknown".to_string());
1745 let mut reason = format!("server exited after initialize (code {code}): {err}");
1746 if !stderr_tail.is_empty() {
1747 reason.push_str("; stderr (last 64 lines):\n");
1748 reason.push_str(&format_stderr_tail_for_reason(stderr_tail));
1749 reason.push_str("\n\n");
1750 reason.push_str(&failure_hint(binary, stderr_tail));
1751 }
1752 reason
1753}
1754
1755fn failure_hint(binary: &str, stderr_tail: &str) -> String {
1756 if stderr_tail.contains("MODULE_NOT_FOUND") || stderr_tail.contains("Cannot find module") {
1757 let package_manager = infer_package_manager(stderr_tail);
1758 format!(
1759 "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."
1760 )
1761 } else if let Some(component) = rustup_missing_component(stderr_tail) {
1762 format!("'{component}' is a rustup proxy but the component is not installed. Install it: rustup component add {component}")
1767 } else {
1768 format!("Hint: see stderr above for '{binary}' failure details.")
1769 }
1770}
1771
1772fn rustup_missing_component(stderr_tail: &str) -> Option<String> {
1778 let marker = "Unknown binary '";
1779 let start = stderr_tail.find(marker)? + marker.len();
1780 let rest = &stderr_tail[start..];
1781 let end = rest.find('\'')?;
1782 let name = &rest[..end];
1783 if name.is_empty() || !stderr_tail.contains("toolchain") {
1786 return None;
1787 }
1788 Some(name.to_string())
1789}
1790
1791fn infer_package_manager(stderr_tail: &str) -> &'static str {
1792 let lower = stderr_tail.to_ascii_lowercase();
1793 if lower.contains(".pnpm/") || lower.contains(".pnpm\\") || lower.contains("/pnpm/") {
1794 "pnpm"
1795 } else if lower.contains(".yarn/")
1796 || lower.contains(".yarn\\")
1797 || lower.contains("/yarn/")
1798 || lower.contains("yarn")
1799 {
1800 "yarn"
1801 } else {
1802 "npm"
1803 }
1804}
1805
1806fn canonicalize_for_lsp(file_path: &Path) -> Result<PathBuf, LspError> {
1807 std::fs::canonicalize(file_path).map_err(LspError::from)
1808}
1809
1810fn resolve_for_lsp_uri(file_path: &Path) -> PathBuf {
1811 if let Ok(path) = std::fs::canonicalize(file_path) {
1812 return path;
1813 }
1814
1815 let mut existing = file_path.to_path_buf();
1816 let mut missing = Vec::new();
1817 while !existing.exists() {
1818 let Some(name) = existing.file_name() else {
1819 break;
1820 };
1821 missing.push(name.to_owned());
1822 let Some(parent) = existing.parent() else {
1823 break;
1824 };
1825 existing = parent.to_path_buf();
1826 }
1827
1828 let mut resolved = std::fs::canonicalize(&existing).unwrap_or(existing);
1829 for segment in missing.into_iter().rev() {
1830 resolved.push(segment);
1831 }
1832 resolved
1833}
1834
1835fn language_id_for_extension(ext: &str) -> &'static str {
1836 match ext {
1837 "ts" => "typescript",
1838 "tsx" => "typescriptreact",
1839 "js" | "mjs" | "cjs" => "javascript",
1840 "jsx" => "javascriptreact",
1841 "py" | "pyi" => "python",
1842 "rs" => "rust",
1843 "go" => "go",
1844 "html" | "htm" => "html",
1845 _ => "plaintext",
1846 }
1847}
1848
1849fn normalize_lookup_path(path: &Path) -> PathBuf {
1850 std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
1851}
1852
1853fn classify_spawn_error(binary: &str, err: &LspError) -> ServerAttemptResult {
1860 match err {
1861 LspError::NotFound(_) => ServerAttemptResult::BinaryNotInstalled {
1866 binary: binary.to_string(),
1867 },
1868 other => ServerAttemptResult::SpawnFailed {
1869 binary: binary.to_string(),
1870 reason: other.to_string(),
1871 },
1872 }
1873}
1874
1875fn env_binary_override(kind: &ServerKind) -> Option<PathBuf> {
1876 let id = kind.id_str();
1877 let suffix: String = id
1878 .chars()
1879 .map(|ch| {
1880 if ch.is_ascii_alphanumeric() {
1881 ch.to_ascii_uppercase()
1882 } else {
1883 '_'
1884 }
1885 })
1886 .collect();
1887 let key = format!("AFT_LSP_{suffix}_BINARY");
1888 std::env::var_os(key).map(PathBuf::from)
1889}
1890
1891#[cfg(test)]
1892mod failure_hint_tests {
1893 use super::{failure_hint, rustup_missing_component};
1894
1895 #[test]
1896 fn detects_rustup_proxy_without_component() {
1897 let stderr = "error: Unknown binary 'rust-analyzer' in official toolchain 'stable-aarch64-apple-darwin'.";
1899 assert_eq!(
1900 rustup_missing_component(stderr).as_deref(),
1901 Some("rust-analyzer")
1902 );
1903 let hint = failure_hint("rust-analyzer", stderr);
1904 assert!(
1905 hint.contains("rustup component add rust-analyzer"),
1906 "expected actionable rustup hint, got: {hint}"
1907 );
1908 }
1909
1910 #[test]
1911 fn ignores_unknown_binary_without_toolchain_phrasing() {
1912 let stderr = "fatal: Unknown binary 'foo' was requested by the linker.";
1915 assert_eq!(rustup_missing_component(stderr), None);
1916 assert!(failure_hint("foo", stderr).starts_with("Hint: see stderr"));
1917 }
1918
1919 #[test]
1920 fn npm_module_not_found_still_wins() {
1921 let stderr = "Error: Cannot find module '/x/typescript-language-server/lib/cli.mjs'";
1923 let hint = failure_hint("typescript-language-server", stderr);
1924 assert!(hint.contains("install -g"), "got: {hint}");
1925 }
1926}
1927
1928#[cfg(test)]
1929mod diagnostic_capacity_tests {
1930 use super::LspManager;
1931
1932 #[test]
1937 fn set_diagnostic_capacity_propagates_to_store() {
1938 let mut manager = LspManager::new();
1939 manager.set_diagnostic_capacity(7);
1940 assert_eq!(manager.diagnostics_store_for_test().capacity_for_test(), 7);
1941 manager.set_diagnostic_capacity(0); assert_eq!(manager.diagnostics_store_for_test().capacity_for_test(), 0);
1943 }
1944
1945 #[test]
1948 fn clear_failed_spawns_empties_the_cache() {
1949 let mut manager = LspManager::new();
1950 assert_eq!(manager.clear_failed_spawns(), 0);
1951 manager.insert_failed_spawn_for_test();
1952 assert_eq!(manager.clear_failed_spawns(), 1);
1953 assert_eq!(manager.clear_failed_spawns(), 0);
1954 }
1955}
1956
1957#[cfg(test)]
1958mod clear_diagnostics_tests {
1959 use std::path::PathBuf;
1960
1961 use super::LspManager;
1962 use crate::lsp::client::LspEvent;
1963 use crate::lsp::diagnostics::{DiagnosticSeverity, StoredDiagnostic};
1964 use crate::lsp::position::uri_for_path;
1965 use crate::lsp::registry::ServerKind;
1966 use crate::lsp::roots::ServerKey;
1967
1968 fn err_diag(file: &PathBuf) -> StoredDiagnostic {
1969 StoredDiagnostic {
1970 file: file.clone(),
1971 line: 1,
1972 column: 1,
1973 end_line: 1,
1974 end_column: 2,
1975 severity: DiagnosticSeverity::Error,
1976 message: "boom".into(),
1977 code: None,
1978 source: None,
1979 }
1980 }
1981
1982 #[test]
1987 fn clear_diagnostics_for_deleted_file_matches_canonical_key() {
1988 let dir = tempfile::tempdir().unwrap();
1989 let canonical_dir = std::fs::canonicalize(dir.path()).unwrap();
1991 let canonical_file = canonical_dir.join("gone.ts");
1992 std::fs::write(&canonical_file, "x").unwrap();
1995
1996 let mut manager = LspManager::new();
1997 let key = ServerKey {
1998 kind: ServerKind::TypeScript,
1999 root: canonical_dir.clone(),
2000 };
2001 manager.diagnostics_store_mut_for_test().publish(
2002 key,
2003 canonical_file.clone(),
2004 vec![err_diag(&canonical_file)],
2005 );
2006 assert_eq!(manager.warm_error_warning_counts(), (1, 0));
2007
2008 std::fs::remove_file(&canonical_file).unwrap();
2009
2010 let watcher_path = dir.path().join("gone.ts");
2013 let removed = manager.clear_diagnostics_for_file(&watcher_path);
2014
2015 assert!(removed, "expected the deleted file's diagnostic to clear");
2016 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
2017 }
2018
2019 #[test]
2020 fn clear_diagnostics_for_unknown_file_is_noop() {
2021 let mut manager = LspManager::new();
2022 assert!(!manager.clear_diagnostics_for_file(&PathBuf::from("/nope/missing.ts")));
2023 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
2024 }
2025
2026 #[test]
2027 fn drain_events_reports_publish_diagnostics_updates() {
2028 let dir = tempfile::tempdir().unwrap();
2029 let root = std::fs::canonicalize(dir.path()).unwrap();
2030 let file = root.join("main.ts");
2031 std::fs::write(&file, "const x: number = 'nope';").unwrap();
2032
2033 let mut manager = LspManager::new();
2034 let diagnostic = lsp_types::Diagnostic {
2035 range: lsp_types::Range {
2036 start: lsp_types::Position {
2037 line: 0,
2038 character: 0,
2039 },
2040 end: lsp_types::Position {
2041 line: 0,
2042 character: 1,
2043 },
2044 },
2045 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
2046 code: None,
2047 code_description: None,
2048 source: Some("test".into()),
2049 message: "boom".into(),
2050 related_information: None,
2051 tags: None,
2052 data: None,
2053 };
2054 let params = serde_json::to_value(lsp_types::PublishDiagnosticsParams {
2055 uri: uri_for_path(&file).unwrap(),
2056 diagnostics: vec![diagnostic],
2057 version: Some(1),
2058 })
2059 .unwrap();
2060 manager
2061 .event_tx
2062 .send(LspEvent::Notification {
2063 server_kind: ServerKind::TypeScript,
2064 root,
2065 method: "textDocument/publishDiagnostics".into(),
2066 params: Some(params),
2067 })
2068 .unwrap();
2069
2070 let drained = manager.drain_events();
2071
2072 assert!(drained.diagnostics_changed);
2073 assert_eq!(drained.events.len(), 1);
2074 assert_eq!(manager.warm_error_warning_counts(), (1, 0));
2075 }
2076}