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 override_binary(&mut self, kind: ServerKind, binary_path: PathBuf) {
294 self.binary_overrides.insert(kind, binary_path);
295 }
296
297 pub fn ensure_server_for_file(&mut self, file_path: &Path, config: &Config) -> Vec<ServerKey> {
304 self.ensure_server_for_file_detailed(file_path, config)
305 .successful
306 }
307
308 pub fn ensure_server_for_file_detailed(
316 &mut self,
317 file_path: &Path,
318 config: &Config,
319 ) -> EnsureServerOutcomes {
320 let defs = servers_for_file(file_path, config);
321 let mut outcomes = EnsureServerOutcomes::default();
322
323 for def in defs {
324 let server_id = def.kind.id_str().to_string();
325 let server_name = def.name.to_string();
326
327 let Some(root) = find_workspace_root(file_path, &def.root_markers) else {
328 outcomes.attempts.push(ServerAttempt {
329 server_id,
330 server_name,
331 result: ServerAttemptResult::NoRootMarker {
332 looked_for: def.root_markers.iter().map(|s| s.to_string()).collect(),
333 },
334 });
335 continue;
336 };
337
338 let key = ServerKey {
339 kind: def.kind.clone(),
340 root,
341 };
342
343 if !self.clients.contains_key(&key) {
344 if let Some(cached) = self.failed_spawns.get(&key) {
351 outcomes.attempts.push(ServerAttempt {
352 server_id,
353 server_name,
354 result: cached.clone(),
355 });
356 continue;
357 }
358
359 match self.spawn_server(&def, &key.root, config) {
360 Ok(client) => {
361 self.clients.insert(key.clone(), client);
362 self.server_binaries.insert(key.clone(), def.binary.clone());
363 self.documents.entry(key.clone()).or_default();
364 }
365 Err(err) => {
366 slog_error!("failed to spawn {}: {}", def.name, err);
367 let result = classify_spawn_error(&def.binary, &err);
368 self.failed_spawns.insert(key.clone(), result.clone());
372 outcomes.attempts.push(ServerAttempt {
373 server_id,
374 server_name,
375 result,
376 });
377 continue;
378 }
379 }
380 }
381
382 outcomes.attempts.push(ServerAttempt {
383 server_id,
384 server_name,
385 result: ServerAttemptResult::Ok {
386 server_key: key.clone(),
387 },
388 });
389 outcomes.successful.push(key);
390 }
391
392 outcomes
393 }
394
395 pub fn ensure_server_for_file_default(&mut self, file_path: &Path) -> Vec<ServerKey> {
398 self.ensure_server_for_file(file_path, &Config::default())
399 }
400 pub fn ensure_file_open(
404 &mut self,
405 file_path: &Path,
406 config: &Config,
407 ) -> Result<Vec<ServerKey>, LspError> {
408 let canonical_path = canonicalize_for_lsp(file_path)?;
409 let server_keys = self.ensure_server_for_file(&canonical_path, config);
410 if server_keys.is_empty() {
411 return Ok(server_keys);
412 }
413
414 let uri = uri_for_path(&canonical_path)?;
415 let language_id = language_id_for_extension(
416 canonical_path
417 .extension()
418 .and_then(|ext| ext.to_str())
419 .unwrap_or_default(),
420 )
421 .to_string();
422
423 for key in &server_keys {
424 let already_open = self
425 .documents
426 .get(key)
427 .is_some_and(|store| store.is_open(&canonical_path));
428
429 if !already_open {
430 let content = std::fs::read_to_string(&canonical_path).map_err(LspError::Io)?;
431 if let Some(client) = self.clients.get_mut(key) {
432 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
433 text_document: TextDocumentItem::new(
434 uri.clone(),
435 language_id.clone(),
436 0,
437 content,
438 ),
439 })?;
440 }
441 self.documents
442 .entry(key.clone())
443 .or_default()
444 .open(canonical_path.clone());
445 continue;
446 }
447
448 let drifted = self
458 .documents
459 .get(key)
460 .is_some_and(|store| store.is_stale_on_disk(&canonical_path));
461 if drifted {
462 let content = std::fs::read_to_string(&canonical_path).map_err(LspError::Io)?;
463 let next_version = self
464 .documents
465 .get(key)
466 .and_then(|store| store.version(&canonical_path))
467 .map(|v| v + 1)
468 .unwrap_or(1);
469 if let Some(client) = self.clients.get_mut(key) {
470 client.send_notification::<DidChangeTextDocument>(
471 DidChangeTextDocumentParams {
472 text_document: VersionedTextDocumentIdentifier::new(
473 uri.clone(),
474 next_version,
475 ),
476 content_changes: vec![TextDocumentContentChangeEvent {
477 range: None,
478 range_length: None,
479 text: content,
480 }],
481 },
482 )?;
483 }
484 if let Some(store) = self.documents.get_mut(key) {
485 store.bump_version(&canonical_path);
486 }
487 }
488 }
489
490 Ok(server_keys)
491 }
492
493 pub fn ensure_file_open_default(
494 &mut self,
495 file_path: &Path,
496 ) -> Result<Vec<ServerKey>, LspError> {
497 self.ensure_file_open(file_path, &Config::default())
498 }
499
500 pub fn notify_file_changed(
506 &mut self,
507 file_path: &Path,
508 content: &str,
509 config: &Config,
510 ) -> Result<(), LspError> {
511 self.notify_file_changed_versioned(file_path, content, config)
512 .map(|_| ())
513 }
514
515 pub fn notify_file_changed_versioned(
526 &mut self,
527 file_path: &Path,
528 content: &str,
529 config: &Config,
530 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
531 let canonical_path = canonicalize_for_lsp(file_path)?;
532 let server_keys = self.ensure_server_for_file(&canonical_path, config);
533 if server_keys.is_empty() {
534 return Ok(Vec::new());
535 }
536
537 let uri = uri_for_path(&canonical_path)?;
538 let language_id = language_id_for_extension(
539 canonical_path
540 .extension()
541 .and_then(|ext| ext.to_str())
542 .unwrap_or_default(),
543 )
544 .to_string();
545
546 let mut versions: Vec<(ServerKey, i32)> = Vec::with_capacity(server_keys.len());
547
548 for key in server_keys {
549 let current_version = self
550 .documents
551 .get(&key)
552 .and_then(|store| store.version(&canonical_path));
553
554 if let Some(version) = current_version {
555 let next_version = version + 1;
556 if let Some(client) = self.clients.get_mut(&key) {
557 client.send_notification::<DidChangeTextDocument>(
558 DidChangeTextDocumentParams {
559 text_document: VersionedTextDocumentIdentifier::new(
560 uri.clone(),
561 next_version,
562 ),
563 content_changes: vec![TextDocumentContentChangeEvent {
564 range: None,
565 range_length: None,
566 text: content.to_string(),
567 }],
568 },
569 )?;
570 }
571 if let Some(store) = self.documents.get_mut(&key) {
572 store.bump_version(&canonical_path);
573 }
574 versions.push((key, next_version));
575 continue;
576 }
577
578 if let Some(client) = self.clients.get_mut(&key) {
579 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
580 text_document: TextDocumentItem::new(
581 uri.clone(),
582 language_id.clone(),
583 0,
584 content.to_string(),
585 ),
586 })?;
587 }
588 self.documents
589 .entry(key.clone())
590 .or_default()
591 .open(canonical_path.clone());
592 versions.push((key, 0));
595 }
596
597 Ok(versions)
598 }
599
600 pub fn notify_file_changed_default(
601 &mut self,
602 file_path: &Path,
603 content: &str,
604 ) -> Result<(), LspError> {
605 self.notify_file_changed(file_path, content, &Config::default())
606 }
607
608 pub fn notify_files_watched_changed(
614 &mut self,
615 paths: &[(PathBuf, FileChangeType)],
616 _config: &Config,
617 ) -> Result<(), LspError> {
618 if paths.is_empty() {
619 return Ok(());
620 }
621
622 let mut canonical_events = Vec::with_capacity(paths.len());
623 for (path, typ) in paths {
624 let canonical_path = resolve_for_lsp_uri(path);
625 canonical_events.push((canonical_path, *typ));
626 }
627
628 let keys: Vec<ServerKey> = self.clients.keys().cloned().collect();
629 for key in keys {
630 let mut changes = Vec::new();
631 for (path, typ) in &canonical_events {
632 if !path.starts_with(&key.root) {
633 continue;
634 }
635 changes.push(FileEvent::new(uri_for_path(path)?, *typ));
636 }
637
638 if changes.is_empty() {
639 continue;
640 }
641
642 if let Some(client) = self.clients.get_mut(&key) {
643 let supports_static_watched_files = client.supports_watched_files();
649 let has_dynamic_registration = client.has_watched_file_registration();
650 if !(supports_static_watched_files || has_dynamic_registration) {
651 if self.watched_file_skip_logged.insert(key.clone()) {
652 log::debug!(
653 "skipping didChangeWatchedFiles for {:?} (not supported or registered)",
654 key
655 );
656 }
657 continue;
658 }
659 client.send_notification::<DidChangeWatchedFiles>(DidChangeWatchedFilesParams {
660 changes,
661 })?;
662 }
663 }
664
665 Ok(())
666 }
667
668 pub fn notify_file_closed(&mut self, file_path: &Path) -> Result<(), LspError> {
670 let canonical_path = canonicalize_for_lsp(file_path)?;
671 let uri = uri_for_path(&canonical_path)?;
672 let keys: Vec<ServerKey> = self.documents.keys().cloned().collect();
673
674 for key in keys {
675 let was_open = self
676 .documents
677 .get(&key)
678 .map(|store| store.is_open(&canonical_path))
679 .unwrap_or(false);
680 if !was_open {
681 continue;
682 }
683
684 if let Some(client) = self.clients.get_mut(&key) {
685 client.send_notification::<DidCloseTextDocument>(DidCloseTextDocumentParams {
686 text_document: TextDocumentIdentifier::new(uri.clone()),
687 })?;
688 }
689
690 if let Some(store) = self.documents.get_mut(&key) {
691 store.close(&canonical_path);
692 }
693 self.diagnostics
694 .clear_for_server_file(&key, &canonical_path);
695 }
696
697 Ok(())
698 }
699
700 pub fn client_for_file(&self, file_path: &Path, config: &Config) -> Option<&LspClient> {
702 let key = self.server_key_for_file(file_path, config)?;
703 self.clients.get(&key)
704 }
705
706 pub fn client_for_file_default(&self, file_path: &Path) -> Option<&LspClient> {
707 self.client_for_file(file_path, &Config::default())
708 }
709
710 pub fn client_for_file_mut(
712 &mut self,
713 file_path: &Path,
714 config: &Config,
715 ) -> Option<&mut LspClient> {
716 let key = self.server_key_for_file(file_path, config)?;
717 self.clients.get_mut(&key)
718 }
719
720 pub fn client_for_file_mut_default(&mut self, file_path: &Path) -> Option<&mut LspClient> {
721 self.client_for_file_mut(file_path, &Config::default())
722 }
723
724 pub fn active_client_count(&self) -> usize {
726 self.clients.len()
727 }
728
729 pub fn drain_events(&mut self) -> DrainedLspEvents {
731 let mut events = Vec::new();
732 let mut diagnostics_changed = false;
733 while let Ok(event) = self.event_rx.try_recv() {
734 if self.handle_event(&event).is_some() {
735 diagnostics_changed = true;
736 }
737 events.push(event);
738 }
739 DrainedLspEvents {
740 events,
741 diagnostics_changed,
742 }
743 }
744
745 pub fn wait_for_diagnostics(
747 &mut self,
748 file_path: &Path,
749 config: &Config,
750 timeout: std::time::Duration,
751 ) -> Vec<StoredDiagnostic> {
752 let deadline = std::time::Instant::now() + timeout;
753 self.wait_for_file_diagnostics(file_path, config, deadline)
754 }
755
756 pub fn wait_for_diagnostics_default(
757 &mut self,
758 file_path: &Path,
759 timeout: std::time::Duration,
760 ) -> Vec<StoredDiagnostic> {
761 self.wait_for_diagnostics(file_path, &Config::default(), timeout)
762 }
763
764 #[doc(hidden)]
769 pub fn diagnostics_store_for_test(&self) -> &DiagnosticsStore {
770 &self.diagnostics
771 }
772
773 #[doc(hidden)]
774 pub fn diagnostics_store_mut_for_test(&mut self) -> &mut DiagnosticsStore {
775 &mut self.diagnostics
776 }
777
778 pub fn warm_error_warning_counts(&self) -> (usize, usize) {
782 self.diagnostics.error_warning_counts()
783 }
784
785 pub fn filtered_error_warning_counts(
790 &self,
791 keep: impl FnMut(&std::path::Path) -> bool,
792 ) -> (usize, usize) {
793 self.diagnostics.filtered_error_warning_counts(keep)
794 }
795
796 pub fn snapshot_diagnostic_epochs(&self, file_path: &Path) -> HashMap<ServerKey, u64> {
801 let lookup_path = normalize_lookup_path(file_path);
802 self.diagnostics
803 .entries_for_file(&lookup_path)
804 .into_iter()
805 .map(|(key, entry)| (key.clone(), entry.epoch))
806 .collect()
807 }
808
809 pub fn snapshot_pre_edit_state(&self, file_path: &Path) -> HashMap<ServerKey, PreEditSnapshot> {
812 let lookup_path = normalize_lookup_path(file_path);
813 let mut snapshots: HashMap<ServerKey, PreEditSnapshot> = self
814 .diagnostics
815 .entries_for_file(&lookup_path)
816 .into_iter()
817 .map(|(key, entry)| {
818 (
819 key.clone(),
820 PreEditSnapshot {
821 epoch: entry.epoch,
822 document_version_at_capture: None,
823 },
824 )
825 })
826 .collect();
827
828 for (key, store) in &self.documents {
829 if let Some(version) = store.version(&lookup_path) {
830 snapshots
831 .entry(key.clone())
832 .or_default()
833 .document_version_at_capture = Some(version);
834 }
835 }
836
837 snapshots
838 }
839
840 pub fn diagnostic_entry_is_fresh_for_document(
848 &self,
849 file_path: &Path,
850 server_key: &ServerKey,
851 pre: PreEditSnapshot,
852 ) -> bool {
853 let lookup_path = normalize_lookup_path(file_path);
854 let Some(entry) = self
855 .diagnostics
856 .entries_for_file(&lookup_path)
857 .into_iter()
858 .find_map(|(key, entry)| if key == server_key { Some(entry) } else { None })
859 else {
860 return false;
861 };
862
863 let target_version = self
864 .documents
865 .get(server_key)
866 .and_then(|store| store.version(&lookup_path))
867 .or(pre.document_version_at_capture)
868 .unwrap_or(0);
869
870 matches!(entry.version, Some(version) if version >= target_version)
871 }
872
873 pub fn wait_for_post_edit_diagnostics(
896 &mut self,
897 file_path: &Path,
898 _config: &Config,
902 expected_versions: &[(ServerKey, i32)],
903 pre_snapshot: &HashMap<ServerKey, PreEditSnapshot>,
904 timeout: std::time::Duration,
905 ) -> PostEditWaitOutcome {
906 let lookup_path = normalize_lookup_path(file_path);
907 let deadline = std::time::Instant::now() + timeout;
908
909 let _ = self.drain_events_for_file(&lookup_path);
914
915 let mut fresh: HashMap<ServerKey, Vec<StoredDiagnostic>> = HashMap::new();
916 let mut exited: Vec<ServerKey> = Vec::new();
917
918 loop {
919 for (key, target_version) in expected_versions {
927 if fresh.contains_key(key) || exited.contains(key) {
928 continue;
929 }
930 if !self.clients.contains_key(key) {
931 exited.push(key.clone());
932 continue;
933 }
934 if let Some(entry) = self
935 .diagnostics
936 .entries_for_file(&lookup_path)
937 .into_iter()
938 .find_map(|(k, e)| if k == key { Some(e) } else { None })
939 {
940 let pre = pre_snapshot.get(key).copied().unwrap_or_default();
941 let is_fresh = post_edit_entry_is_fresh(entry, *target_version, pre);
942 if is_fresh {
943 fresh.insert(key.clone(), entry.diagnostics.clone());
944 }
945 }
946 }
947
948 if fresh.len() + exited.len() == expected_versions.len() {
950 break;
951 }
952
953 let now = std::time::Instant::now();
954 if now >= deadline {
955 break;
956 }
957
958 let timeout = deadline.saturating_duration_since(now);
959 match self.event_rx.recv_timeout(timeout) {
960 Ok(event) => {
961 self.handle_event(&event);
962 }
963 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
964 }
965 }
966
967 let pending: Vec<ServerKey> = expected_versions
969 .iter()
970 .filter(|(k, _)| !fresh.contains_key(k) && !exited.contains(k))
971 .map(|(k, _)| k.clone())
972 .collect();
973
974 let mut diagnostics: Vec<StoredDiagnostic> = fresh
977 .into_iter()
978 .flat_map(|(_, diags)| diags.into_iter())
979 .collect();
980 diagnostics.sort_by(|a, b| {
981 a.file
982 .cmp(&b.file)
983 .then(a.line.cmp(&b.line))
984 .then(a.column.cmp(&b.column))
985 .then(a.message.cmp(&b.message))
986 });
987
988 PostEditWaitOutcome {
989 diagnostics,
990 pending_servers: pending,
991 exited_servers: exited,
992 }
993 }
994
995 pub fn wait_for_file_diagnostics(
1001 &mut self,
1002 file_path: &Path,
1003 config: &Config,
1004 deadline: std::time::Instant,
1005 ) -> Vec<StoredDiagnostic> {
1006 let lookup_path = normalize_lookup_path(file_path);
1007
1008 if self.server_key_for_file(&lookup_path, config).is_none() {
1009 return Vec::new();
1010 }
1011
1012 loop {
1013 if self.drain_events_for_file(&lookup_path) {
1014 break;
1015 }
1016
1017 let now = std::time::Instant::now();
1018 if now >= deadline {
1019 break;
1020 }
1021
1022 let timeout = deadline.saturating_duration_since(now);
1023 match self.event_rx.recv_timeout(timeout) {
1024 Ok(event) => {
1025 if matches!(
1026 self.handle_event(&event),
1027 Some(ref published_file) if published_file.as_path() == lookup_path.as_path()
1028 ) {
1029 break;
1030 }
1031 }
1032 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
1033 }
1034 }
1035
1036 self.get_diagnostics_for_file(&lookup_path)
1037 .into_iter()
1038 .cloned()
1039 .collect()
1040 }
1041
1042 pub const PULL_FILE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
1048
1049 pub fn pull_file_timeout() -> std::time::Duration {
1051 Self::PULL_FILE_TIMEOUT
1052 }
1053
1054 const PULL_WORKSPACE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
1058
1059 pub fn pull_file_diagnostics(
1070 &mut self,
1071 file_path: &Path,
1072 config: &Config,
1073 ) -> Result<Vec<PullFileResult>, LspError> {
1074 let canonical_path = canonicalize_for_lsp(file_path)?;
1075 self.ensure_file_open(&canonical_path, config)?;
1078
1079 let server_keys = self.ensure_server_for_file(&canonical_path, config);
1080 if server_keys.is_empty() {
1081 return Ok(Vec::new());
1082 }
1083
1084 let uri = uri_for_path(&canonical_path)?;
1085 let mut results = Vec::with_capacity(server_keys.len());
1086
1087 for key in server_keys {
1088 let supports_pull = self
1089 .clients
1090 .get(&key)
1091 .and_then(|c| c.diagnostic_capabilities())
1092 .is_some_and(|caps| caps.pull_diagnostics);
1093
1094 if !supports_pull {
1095 results.push(PullFileResult {
1096 server_key: key.clone(),
1097 outcome: PullFileOutcome::PullNotSupported,
1098 });
1099 continue;
1100 }
1101
1102 let previous_result_id = self
1104 .diagnostics
1105 .entries_for_file(&canonical_path)
1106 .into_iter()
1107 .find(|(k, _)| **k == key)
1108 .and_then(|(_, entry)| entry.result_id.clone());
1109
1110 let identifier = self
1111 .clients
1112 .get(&key)
1113 .and_then(|c| c.diagnostic_capabilities())
1114 .and_then(|caps| caps.identifier.clone());
1115
1116 let params = AftDocumentDiagnosticParams {
1117 text_document: lsp_types::TextDocumentIdentifier { uri: uri.clone() },
1118 identifier,
1119 previous_result_id,
1120 work_done_progress_params: Default::default(),
1121 partial_result_params: Default::default(),
1122 };
1123
1124 let outcome = match self.send_pull_request(&key, params) {
1125 Ok(report) => self.ingest_document_report(&key, &canonical_path, report),
1126 Err(err) => {
1127 if let Some(result) = self.cache_post_initialize_exit(&key, &err) {
1128 PullFileOutcome::RequestFailed {
1129 reason: server_attempt_result_reason(&result),
1130 }
1131 } else if recoverable_pull_rejection(&err)
1132 && self.clients.get(&key).is_some_and(|client| {
1133 matches!(
1134 client.state(),
1135 ServerState::Ready | ServerState::Initializing
1136 )
1137 })
1138 {
1139 PullFileOutcome::RequestFailed {
1140 reason: format!("pull_rejected_push_fallback: {err}"),
1141 }
1142 } else {
1143 PullFileOutcome::RequestFailed {
1144 reason: err.to_string(),
1145 }
1146 }
1147 }
1148 };
1149
1150 results.push(PullFileResult {
1151 server_key: key,
1152 outcome,
1153 });
1154 }
1155
1156 Ok(results)
1157 }
1158
1159 pub fn pull_workspace_diagnostics(
1164 &mut self,
1165 server_key: &ServerKey,
1166 timeout: Option<std::time::Duration>,
1167 ) -> Result<PullWorkspaceResult, LspError> {
1168 let timeout = timeout.unwrap_or(Self::PULL_WORKSPACE_TIMEOUT);
1169
1170 let supports_workspace = self
1171 .clients
1172 .get(server_key)
1173 .and_then(|c| c.diagnostic_capabilities())
1174 .is_some_and(|caps| caps.workspace_diagnostics);
1175
1176 if !supports_workspace {
1177 return Ok(PullWorkspaceResult {
1178 server_key: server_key.clone(),
1179 files_reported: Vec::new(),
1180 complete: false,
1181 cancelled: false,
1182 supports_workspace: false,
1183 });
1184 }
1185
1186 let identifier = self
1187 .clients
1188 .get(server_key)
1189 .and_then(|c| c.diagnostic_capabilities())
1190 .and_then(|caps| caps.identifier.clone());
1191
1192 let params = AftWorkspaceDiagnosticParams {
1193 identifier,
1194 previous_result_ids: Vec::new(),
1195 work_done_progress_params: Default::default(),
1196 partial_result_params: Default::default(),
1197 };
1198
1199 let result = match self
1200 .clients
1201 .get_mut(server_key)
1202 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?
1203 .send_request_with_timeout::<AftWorkspaceDiagnosticRequest>(params, timeout)
1204 {
1205 Ok(result) => result,
1206 Err(LspError::Timeout(_)) => {
1207 return Ok(PullWorkspaceResult {
1208 server_key: server_key.clone(),
1209 files_reported: Vec::new(),
1210 complete: false,
1211 cancelled: true,
1212 supports_workspace: true,
1213 });
1214 }
1215 Err(err) => {
1216 if let Some(result) = self.cache_post_initialize_exit(server_key, &err) {
1217 return Err(LspError::ServerNotReady(server_attempt_result_reason(
1218 &result,
1219 )));
1220 }
1221 return Err(err);
1222 }
1223 };
1224
1225 let (items, complete) = match result {
1229 lsp_types::WorkspaceDiagnosticReportResult::Report(report) => (report.items, true),
1230 lsp_types::WorkspaceDiagnosticReportResult::Partial(partial) => (partial.items, false),
1231 };
1232
1233 let mut files_reported = Vec::with_capacity(items.len());
1235 for item in items {
1236 match item {
1237 lsp_types::WorkspaceDocumentDiagnosticReport::Full(full) => {
1238 if let Some(file) = uri_to_path(&full.uri) {
1239 let stored = from_lsp_diagnostics(
1240 file.clone(),
1241 full.full_document_diagnostic_report.items.clone(),
1242 );
1243 self.diagnostics.publish_with_result_id(
1244 server_key.clone(),
1245 file.clone(),
1246 stored,
1247 full.full_document_diagnostic_report.result_id.clone(),
1248 );
1249 files_reported.push(file);
1250 }
1251 }
1252 lsp_types::WorkspaceDocumentDiagnosticReport::Unchanged(_unchanged) => {
1253 }
1256 }
1257 }
1258
1259 Ok(PullWorkspaceResult {
1260 server_key: server_key.clone(),
1261 files_reported,
1262 complete,
1263 cancelled: false,
1264 supports_workspace: true,
1265 })
1266 }
1267
1268 fn cache_post_initialize_exit(
1269 &mut self,
1270 key: &ServerKey,
1271 err: &LspError,
1272 ) -> Option<ServerAttemptResult> {
1273 let binary = self
1274 .server_binaries
1275 .get(key)
1276 .cloned()
1277 .unwrap_or_else(|| key.kind.id_str().to_string());
1278 let (status, stderr_tail) = {
1279 let client = self.clients.get_mut(key)?;
1280 let mut status = client.child_exit_status();
1281 for _ in 0..10 {
1282 if status.is_some() {
1283 break;
1284 }
1285 std::thread::sleep(std::time::Duration::from_millis(10));
1286 status = client.child_exit_status();
1287 }
1288 let status = status?;
1289 wait_for_stderr_tail(client);
1290 (status, client.stderr_tail())
1291 };
1292 let reason = format_post_initialize_exit_reason(&binary, status, &stderr_tail, err);
1293 let result = ServerAttemptResult::SpawnFailed { binary, reason };
1294 self.clients.remove(key);
1295 self.server_binaries.remove(key);
1296 self.documents.remove(key);
1297 self.diagnostics.clear_for_server(key);
1298 self.failed_spawns.insert(key.clone(), result.clone());
1299 Some(result)
1300 }
1301
1302 fn send_pull_request(
1304 &mut self,
1305 key: &ServerKey,
1306 params: AftDocumentDiagnosticParams,
1307 ) -> Result<lsp_types::DocumentDiagnosticReportResult, LspError> {
1308 let client = self
1309 .clients
1310 .get_mut(key)
1311 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?;
1312 client.send_request::<AftDocumentDiagnosticRequest>(params)
1313 }
1314
1315 fn ingest_document_report(
1318 &mut self,
1319 key: &ServerKey,
1320 canonical_path: &Path,
1321 result: lsp_types::DocumentDiagnosticReportResult,
1322 ) -> PullFileOutcome {
1323 let report = match result {
1324 lsp_types::DocumentDiagnosticReportResult::Report(report) => report,
1325 lsp_types::DocumentDiagnosticReportResult::Partial(_) => {
1326 return PullFileOutcome::PartialNotSupported;
1330 }
1331 };
1332
1333 match report {
1334 lsp_types::DocumentDiagnosticReport::Full(full) => {
1335 let result_id = full.full_document_diagnostic_report.result_id.clone();
1336 let stored = from_lsp_diagnostics(
1337 canonical_path.to_path_buf(),
1338 full.full_document_diagnostic_report.items.clone(),
1339 );
1340 let count = stored.len();
1341 self.diagnostics.publish_with_result_id(
1342 key.clone(),
1343 canonical_path.to_path_buf(),
1344 stored,
1345 result_id,
1346 );
1347 PullFileOutcome::Full {
1348 diagnostic_count: count,
1349 }
1350 }
1351 lsp_types::DocumentDiagnosticReport::Unchanged(_unchanged) => {
1352 if self
1356 .diagnostics
1357 .has_report_for_server_file(key, canonical_path)
1358 {
1359 PullFileOutcome::Unchanged
1360 } else {
1361 PullFileOutcome::RequestFailed {
1362 reason: "no_cache_for_unchanged".to_string(),
1363 }
1364 }
1365 }
1366 }
1367 }
1368
1369 pub fn shutdown_all(&mut self) {
1371 for (key, mut client) in self.clients.drain() {
1372 if let Err(err) = client.shutdown() {
1373 slog_error!("error shutting down {:?}: {}", key, err);
1374 }
1375 }
1376 self.server_binaries.clear();
1377 self.documents.clear();
1378 self.diagnostics = DiagnosticsStore::new();
1379 }
1380
1381 pub fn has_active_servers(&self) -> bool {
1383 self.clients
1384 .values()
1385 .any(|client| client.state() == ServerState::Ready)
1386 }
1387
1388 pub fn active_server_keys(&self) -> Vec<ServerKey> {
1391 self.clients.keys().cloned().collect()
1392 }
1393
1394 pub fn get_diagnostics_for_file(&self, file: &Path) -> Vec<&StoredDiagnostic> {
1395 let normalized = normalize_lookup_path(file);
1396 self.diagnostics.for_file(&normalized)
1397 }
1398
1399 pub fn clear_diagnostics_for_file(&mut self, file: &Path) -> bool {
1413 let mut removed = self.diagnostics.clear_for_file(file);
1414
1415 let normalized = normalize_lookup_path(file);
1416 if normalized != file {
1417 removed |= self.diagnostics.clear_for_file(&normalized);
1418 }
1419
1420 if let (Some(parent), Some(name)) = (file.parent(), file.file_name()) {
1423 if let Ok(canonical_parent) = std::fs::canonicalize(parent) {
1424 let reconstructed = canonical_parent.join(name);
1425 if reconstructed != file && reconstructed != normalized {
1426 removed |= self.diagnostics.clear_for_file(&reconstructed);
1427 }
1428 }
1429 }
1430
1431 removed
1432 }
1433
1434 pub fn get_diagnostics_for_directory(&self, dir: &Path) -> Vec<&StoredDiagnostic> {
1435 let normalized = normalize_lookup_path(dir);
1436 self.diagnostics.for_directory(&normalized)
1437 }
1438
1439 pub fn get_all_diagnostics(&self) -> Vec<&StoredDiagnostic> {
1440 self.diagnostics.all()
1441 }
1442
1443 pub fn has_any_diagnostic_reports(&self) -> bool {
1448 !self.diagnostics.is_empty()
1449 }
1450
1451 pub fn has_diagnostic_report_for_file(&self, file: &Path) -> bool {
1454 let normalized = normalize_lookup_path(file);
1455 self.diagnostics.has_any_report_for_file(&normalized)
1456 }
1457
1458 pub fn has_diagnostic_report_for_server_file(&self, server: &ServerKey, file: &Path) -> bool {
1461 let normalized = normalize_lookup_path(file);
1462 self.diagnostics
1463 .has_report_for_server_file(server, &normalized)
1464 }
1465
1466 fn drain_events_for_file(&mut self, file_path: &Path) -> bool {
1467 let mut saw_file_diagnostics = false;
1468 while let Ok(event) = self.event_rx.try_recv() {
1469 if matches!(
1470 self.handle_event(&event),
1471 Some(ref published_file) if published_file.as_path() == file_path
1472 ) {
1473 saw_file_diagnostics = true;
1474 }
1475 }
1476 saw_file_diagnostics
1477 }
1478
1479 fn handle_event(&mut self, event: &LspEvent) -> Option<PathBuf> {
1480 match event {
1481 LspEvent::Notification {
1482 server_kind,
1483 root,
1484 method,
1485 params: Some(params),
1486 } if method == "textDocument/publishDiagnostics" => {
1487 self.handle_publish_diagnostics(server_kind.clone(), root.clone(), params)
1488 }
1489 LspEvent::ServerExited { server_kind, root } => {
1490 let key = ServerKey {
1491 kind: server_kind.clone(),
1492 root: root.clone(),
1493 };
1494 self.clients.remove(&key);
1495 self.server_binaries.remove(&key);
1496 self.documents.remove(&key);
1497 self.diagnostics.clear_for_server(&key);
1498 None
1499 }
1500 _ => None,
1501 }
1502 }
1503
1504 fn handle_publish_diagnostics(
1505 &mut self,
1506 server: ServerKind,
1507 root: PathBuf,
1508 params: &serde_json::Value,
1509 ) -> Option<PathBuf> {
1510 if let Ok(publish_params) =
1511 serde_json::from_value::<lsp_types::PublishDiagnosticsParams>(params.clone())
1512 {
1513 let file = uri_to_path(&publish_params.uri)?;
1514 let stored = from_lsp_diagnostics(file.clone(), publish_params.diagnostics);
1515 let key = ServerKey { kind: server, root };
1521 self.diagnostics
1522 .publish_full(key, file.clone(), stored, None, publish_params.version);
1523 return Some(file);
1524 }
1525 None
1526 }
1527
1528 fn spawn_server(
1529 &self,
1530 def: &ServerDef,
1531 root: &Path,
1532 config: &Config,
1533 ) -> Result<LspClient, LspError> {
1534 let binary = self.resolve_binary(def, config)?;
1535
1536 let mut merged_env = def.env.clone();
1540 for (key, value) in &self.extra_env {
1541 merged_env.insert(key.clone(), value.clone());
1542 }
1543
1544 let mut client = LspClient::spawn(
1545 def.kind.clone(),
1546 root.to_path_buf(),
1547 &binary,
1548 &def.args,
1549 &merged_env,
1550 self.event_tx.clone(),
1551 self.child_registry.clone(),
1552 )?;
1553 if let Err(err) = client.initialize(root, def.initialization_options.clone()) {
1554 wait_for_stderr_tail(&mut client);
1555 let stderr_tail = client.stderr_tail();
1556 let reason = if client.child_exited() || !stderr_tail.is_empty() {
1557 format_initialize_failure_reason(&def.binary, &stderr_tail, &err)
1558 } else {
1559 format!("server failed during initialize: {err}")
1560 };
1561 return Err(LspError::ServerNotReady(reason));
1562 }
1563 Ok(client)
1564 }
1565
1566 fn resolve_binary(&self, def: &ServerDef, config: &Config) -> Result<PathBuf, LspError> {
1567 if let Some(path) = self.binary_overrides.get(&def.kind) {
1568 if path.exists() {
1569 return Ok(path.clone());
1570 }
1571 return Err(LspError::NotFound(format!(
1572 "override binary for {:?} not found: {}",
1573 def.kind,
1574 path.display()
1575 )));
1576 }
1577
1578 if let Some(path) = env_binary_override(&def.kind) {
1579 if path.exists() {
1580 return Ok(path);
1581 }
1582 return Err(LspError::NotFound(format!(
1583 "environment override binary for {:?} not found: {}",
1584 def.kind,
1585 path.display()
1586 )));
1587 }
1588
1589 resolve_lsp_binary(
1594 &def.binary,
1595 config.project_root.as_deref(),
1596 &config.lsp_paths_extra,
1597 )
1598 .ok_or_else(|| {
1599 LspError::NotFound(format!(
1600 "language server binary '{}' not found in node_modules/.bin, lsp_paths_extra, or PATH",
1601 def.binary
1602 ))
1603 })
1604 }
1605
1606 fn server_key_for_file(&self, file_path: &Path, config: &Config) -> Option<ServerKey> {
1607 for def in servers_for_file(file_path, config) {
1608 let root = find_workspace_root(file_path, &def.root_markers)?;
1609 let key = ServerKey {
1610 kind: def.kind.clone(),
1611 root,
1612 };
1613 if self.clients.contains_key(&key) {
1614 return Some(key);
1615 }
1616 }
1617 None
1618 }
1619}
1620
1621impl Default for LspManager {
1622 fn default() -> Self {
1623 Self::new()
1624 }
1625}
1626
1627fn wait_for_stderr_tail(client: &mut LspClient) {
1628 for _ in 0..10 {
1629 if !client.stderr_tail().is_empty() {
1630 break;
1631 }
1632 std::thread::sleep(std::time::Duration::from_millis(10));
1633 }
1634}
1635
1636fn recoverable_pull_rejection(err: &LspError) -> bool {
1637 matches!(
1638 err,
1639 LspError::ServerError {
1640 code: -32601 | -32602,
1641 ..
1642 }
1643 )
1644}
1645
1646fn server_attempt_result_reason(result: &ServerAttemptResult) -> String {
1647 match result {
1648 ServerAttemptResult::SpawnFailed { binary, reason } => {
1649 format!("spawn_failed: {binary} ({reason})")
1650 }
1651 ServerAttemptResult::BinaryNotInstalled { binary } => {
1652 format!("binary_not_installed: {binary}")
1653 }
1654 ServerAttemptResult::NoRootMarker { looked_for } => {
1655 format!("no_root_marker (looked for: {})", looked_for.join(", "))
1656 }
1657 ServerAttemptResult::Ok { .. } => "ok".to_string(),
1658 }
1659}
1660
1661fn format_stderr_tail_for_reason(stderr_tail: &str) -> String {
1662 truncate_stderr_tail_for_reason(stderr_tail)
1663 .lines()
1664 .map(|line| format!(" {line}"))
1665 .collect::<Vec<_>>()
1666 .join("\n")
1667}
1668
1669fn truncate_stderr_tail_for_reason(stderr_tail: &str) -> String {
1670 if stderr_tail.len() <= STDERR_REASON_BYTES {
1671 return stderr_tail.to_string();
1672 }
1673
1674 let ellipsis = "...";
1675 let target_len = STDERR_REASON_BYTES.saturating_sub(ellipsis.len());
1676 let mut start = stderr_tail.len() - target_len;
1677 while start < stderr_tail.len() && !stderr_tail.is_char_boundary(start) {
1678 start += 1;
1679 }
1680 format!("{ellipsis}{}", &stderr_tail[start..])
1681}
1682
1683fn format_initialize_failure_reason(binary: &str, stderr_tail: &str, err: &LspError) -> String {
1684 let mut reason = format!("server crashed during initialize: {err}");
1685 if !stderr_tail.is_empty() {
1686 reason.push_str("; stderr (last 64 lines):\n");
1687 reason.push_str(&format_stderr_tail_for_reason(stderr_tail));
1688 reason.push_str("\n\n");
1689 reason.push_str(&failure_hint(binary, stderr_tail));
1690 }
1691 reason
1692}
1693
1694fn format_post_initialize_exit_reason(
1695 binary: &str,
1696 status: std::process::ExitStatus,
1697 stderr_tail: &str,
1698 err: &LspError,
1699) -> String {
1700 let code = status
1701 .code()
1702 .map(|c| c.to_string())
1703 .unwrap_or_else(|| "signal/unknown".to_string());
1704 let mut reason = format!("server exited after initialize (code {code}): {err}");
1705 if !stderr_tail.is_empty() {
1706 reason.push_str("; stderr (last 64 lines):\n");
1707 reason.push_str(&format_stderr_tail_for_reason(stderr_tail));
1708 reason.push_str("\n\n");
1709 reason.push_str(&failure_hint(binary, stderr_tail));
1710 }
1711 reason
1712}
1713
1714fn failure_hint(binary: &str, stderr_tail: &str) -> String {
1715 if stderr_tail.contains("MODULE_NOT_FOUND") || stderr_tail.contains("Cannot find module") {
1716 let package_manager = infer_package_manager(stderr_tail);
1717 format!(
1718 "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."
1719 )
1720 } else if let Some(component) = rustup_missing_component(stderr_tail) {
1721 format!("'{component}' is a rustup proxy but the component is not installed. Install it: rustup component add {component}")
1726 } else {
1727 format!("Hint: see stderr above for '{binary}' failure details.")
1728 }
1729}
1730
1731fn rustup_missing_component(stderr_tail: &str) -> Option<String> {
1737 let marker = "Unknown binary '";
1738 let start = stderr_tail.find(marker)? + marker.len();
1739 let rest = &stderr_tail[start..];
1740 let end = rest.find('\'')?;
1741 let name = &rest[..end];
1742 if name.is_empty() || !stderr_tail.contains("toolchain") {
1745 return None;
1746 }
1747 Some(name.to_string())
1748}
1749
1750fn infer_package_manager(stderr_tail: &str) -> &'static str {
1751 let lower = stderr_tail.to_ascii_lowercase();
1752 if lower.contains(".pnpm/") || lower.contains(".pnpm\\") || lower.contains("/pnpm/") {
1753 "pnpm"
1754 } else if lower.contains(".yarn/")
1755 || lower.contains(".yarn\\")
1756 || lower.contains("/yarn/")
1757 || lower.contains("yarn")
1758 {
1759 "yarn"
1760 } else {
1761 "npm"
1762 }
1763}
1764
1765fn canonicalize_for_lsp(file_path: &Path) -> Result<PathBuf, LspError> {
1766 std::fs::canonicalize(file_path).map_err(LspError::from)
1767}
1768
1769fn resolve_for_lsp_uri(file_path: &Path) -> PathBuf {
1770 if let Ok(path) = std::fs::canonicalize(file_path) {
1771 return path;
1772 }
1773
1774 let mut existing = file_path.to_path_buf();
1775 let mut missing = Vec::new();
1776 while !existing.exists() {
1777 let Some(name) = existing.file_name() else {
1778 break;
1779 };
1780 missing.push(name.to_owned());
1781 let Some(parent) = existing.parent() else {
1782 break;
1783 };
1784 existing = parent.to_path_buf();
1785 }
1786
1787 let mut resolved = std::fs::canonicalize(&existing).unwrap_or(existing);
1788 for segment in missing.into_iter().rev() {
1789 resolved.push(segment);
1790 }
1791 resolved
1792}
1793
1794fn language_id_for_extension(ext: &str) -> &'static str {
1795 match ext {
1796 "ts" => "typescript",
1797 "tsx" => "typescriptreact",
1798 "js" | "mjs" | "cjs" => "javascript",
1799 "jsx" => "javascriptreact",
1800 "py" | "pyi" => "python",
1801 "rs" => "rust",
1802 "go" => "go",
1803 "html" | "htm" => "html",
1804 _ => "plaintext",
1805 }
1806}
1807
1808fn normalize_lookup_path(path: &Path) -> PathBuf {
1809 std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
1810}
1811
1812fn classify_spawn_error(binary: &str, err: &LspError) -> ServerAttemptResult {
1819 match err {
1820 LspError::NotFound(_) => ServerAttemptResult::BinaryNotInstalled {
1825 binary: binary.to_string(),
1826 },
1827 other => ServerAttemptResult::SpawnFailed {
1828 binary: binary.to_string(),
1829 reason: other.to_string(),
1830 },
1831 }
1832}
1833
1834fn env_binary_override(kind: &ServerKind) -> Option<PathBuf> {
1835 let id = kind.id_str();
1836 let suffix: String = id
1837 .chars()
1838 .map(|ch| {
1839 if ch.is_ascii_alphanumeric() {
1840 ch.to_ascii_uppercase()
1841 } else {
1842 '_'
1843 }
1844 })
1845 .collect();
1846 let key = format!("AFT_LSP_{suffix}_BINARY");
1847 std::env::var_os(key).map(PathBuf::from)
1848}
1849
1850#[cfg(test)]
1851mod failure_hint_tests {
1852 use super::{failure_hint, rustup_missing_component};
1853
1854 #[test]
1855 fn detects_rustup_proxy_without_component() {
1856 let stderr = "error: Unknown binary 'rust-analyzer' in official toolchain 'stable-aarch64-apple-darwin'.";
1858 assert_eq!(
1859 rustup_missing_component(stderr).as_deref(),
1860 Some("rust-analyzer")
1861 );
1862 let hint = failure_hint("rust-analyzer", stderr);
1863 assert!(
1864 hint.contains("rustup component add rust-analyzer"),
1865 "expected actionable rustup hint, got: {hint}"
1866 );
1867 }
1868
1869 #[test]
1870 fn ignores_unknown_binary_without_toolchain_phrasing() {
1871 let stderr = "fatal: Unknown binary 'foo' was requested by the linker.";
1874 assert_eq!(rustup_missing_component(stderr), None);
1875 assert!(failure_hint("foo", stderr).starts_with("Hint: see stderr"));
1876 }
1877
1878 #[test]
1879 fn npm_module_not_found_still_wins() {
1880 let stderr = "Error: Cannot find module '/x/typescript-language-server/lib/cli.mjs'";
1882 let hint = failure_hint("typescript-language-server", stderr);
1883 assert!(hint.contains("install -g"), "got: {hint}");
1884 }
1885}
1886
1887#[cfg(test)]
1888mod clear_diagnostics_tests {
1889 use std::path::PathBuf;
1890
1891 use super::LspManager;
1892 use crate::lsp::client::LspEvent;
1893 use crate::lsp::diagnostics::{DiagnosticSeverity, StoredDiagnostic};
1894 use crate::lsp::position::uri_for_path;
1895 use crate::lsp::registry::ServerKind;
1896 use crate::lsp::roots::ServerKey;
1897
1898 fn err_diag(file: &PathBuf) -> StoredDiagnostic {
1899 StoredDiagnostic {
1900 file: file.clone(),
1901 line: 1,
1902 column: 1,
1903 end_line: 1,
1904 end_column: 2,
1905 severity: DiagnosticSeverity::Error,
1906 message: "boom".into(),
1907 code: None,
1908 source: None,
1909 }
1910 }
1911
1912 #[test]
1917 fn clear_diagnostics_for_deleted_file_matches_canonical_key() {
1918 let dir = tempfile::tempdir().unwrap();
1919 let canonical_dir = std::fs::canonicalize(dir.path()).unwrap();
1921 let canonical_file = canonical_dir.join("gone.ts");
1922 std::fs::write(&canonical_file, "x").unwrap();
1925
1926 let mut manager = LspManager::new();
1927 let key = ServerKey {
1928 kind: ServerKind::TypeScript,
1929 root: canonical_dir.clone(),
1930 };
1931 manager.diagnostics_store_mut_for_test().publish(
1932 key,
1933 canonical_file.clone(),
1934 vec![err_diag(&canonical_file)],
1935 );
1936 assert_eq!(manager.warm_error_warning_counts(), (1, 0));
1937
1938 std::fs::remove_file(&canonical_file).unwrap();
1939
1940 let watcher_path = dir.path().join("gone.ts");
1943 let removed = manager.clear_diagnostics_for_file(&watcher_path);
1944
1945 assert!(removed, "expected the deleted file's diagnostic to clear");
1946 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
1947 }
1948
1949 #[test]
1950 fn clear_diagnostics_for_unknown_file_is_noop() {
1951 let mut manager = LspManager::new();
1952 assert!(!manager.clear_diagnostics_for_file(&PathBuf::from("/nope/missing.ts")));
1953 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
1954 }
1955
1956 #[test]
1957 fn drain_events_reports_publish_diagnostics_updates() {
1958 let dir = tempfile::tempdir().unwrap();
1959 let root = std::fs::canonicalize(dir.path()).unwrap();
1960 let file = root.join("main.ts");
1961 std::fs::write(&file, "const x: number = 'nope';").unwrap();
1962
1963 let mut manager = LspManager::new();
1964 let diagnostic = lsp_types::Diagnostic {
1965 range: lsp_types::Range {
1966 start: lsp_types::Position {
1967 line: 0,
1968 character: 0,
1969 },
1970 end: lsp_types::Position {
1971 line: 0,
1972 character: 1,
1973 },
1974 },
1975 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
1976 code: None,
1977 code_description: None,
1978 source: Some("test".into()),
1979 message: "boom".into(),
1980 related_information: None,
1981 tags: None,
1982 data: None,
1983 };
1984 let params = serde_json::to_value(lsp_types::PublishDiagnosticsParams {
1985 uri: uri_for_path(&file).unwrap(),
1986 diagnostics: vec![diagnostic],
1987 version: Some(1),
1988 })
1989 .unwrap();
1990 manager
1991 .event_tx
1992 .send(LspEvent::Notification {
1993 server_kind: ServerKind::TypeScript,
1994 root,
1995 method: "textDocument/publishDiagnostics".into(),
1996 params: Some(params),
1997 })
1998 .unwrap();
1999
2000 let drained = manager.drain_events();
2001
2002 assert!(drained.diagnostics_changed);
2003 assert_eq!(drained.events.len(), 1);
2004 assert_eq!(manager.warm_error_warning_counts(), (1, 0));
2005 }
2006}