1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3
4use crossbeam_channel::{bounded, unbounded, Receiver, RecvTimeoutError, Sender, TrySendError};
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::alert_state::AcceptedDiagnosticSnapshot;
15use crate::config::Config;
16use crate::lsp::child_registry::LspChildRegistry;
17use crate::lsp::client::{LspClient, LspEvent, ServerState};
18use crate::lsp::diagnostics::{
19 from_lsp_diagnostics, DiagnosticEntry, DiagnosticsStore, StoredDiagnostic,
20};
21use crate::lsp::document::DocumentStore;
22use crate::lsp::position::{uri_for_path, uri_to_path};
23use crate::lsp::pull_params::{
24 AftDocumentDiagnosticParams, AftDocumentDiagnosticRequest, AftWorkspaceDiagnosticParams,
25 AftWorkspaceDiagnosticRequest,
26};
27use crate::lsp::registry::{resolve_lsp_binary, servers_for_file, ServerDef, ServerKind};
28use crate::lsp::roots::ServerKey;
29use crate::lsp::LspError;
30use crate::slog_error;
31
32const STDERR_REASON_BYTES: usize = 2 * 1024;
33
34fn server_key_for_definition(
35 def: &ServerDef,
36 file_path: &Path,
37 config: &Config,
38) -> Option<ServerKey> {
39 def.workspace_root_for_file_with_project_root(file_path, config.project_root.as_deref())
40 .map(|root| ServerKey {
41 kind: def.kind.clone(),
42 root,
43 })
44}
45
46fn server_key_sort(left: &ServerKey, right: &ServerKey) -> std::cmp::Ordering {
47 left.kind
48 .id_str()
49 .cmp(right.kind.id_str())
50 .then(left.root.cmp(&right.root))
51}
52
53#[derive(Debug, Clone)]
58pub enum ServerAttemptResult {
59 Ok { server_key: ServerKey },
61 NoRootMarker { looked_for: Vec<String> },
64 BinaryNotInstalled { binary: String },
67 SpawnFailed { binary: String, reason: String },
69}
70
71#[derive(Debug, Clone)]
73pub struct ServerAttempt {
74 pub server_id: String,
76 pub server_name: String,
78 pub result: ServerAttemptResult,
79}
80
81#[derive(Debug, Clone, Default)]
87pub struct EnsureServerOutcomes {
88 pub successful: Vec<ServerKey>,
90 pub attempts: Vec<ServerAttempt>,
93}
94
95#[derive(Clone, Debug)]
100pub struct ApplicableServerSnapshot {
101 pub server_keys: Vec<ServerKey>,
102 candidates: Vec<ApplicableServerCandidate>,
103 producer_failures: Vec<ApplicableServerFailure>,
104}
105
106#[derive(Clone, Debug)]
107struct ApplicableServerCandidate {
108 key: ServerKey,
109 definition: ServerDef,
110 source_file: PathBuf,
111}
112
113#[derive(Clone, Debug)]
114pub enum ApplicabilityResolutionError {
115 RootUnreadable { root: PathBuf, reason: String },
116}
117
118#[derive(Clone, Debug)]
119pub struct ApplicableServerFailure {
120 pub server_key: ServerKey,
121 pub result: ServerAttemptResult,
122}
123
124impl ApplicableServerFailure {
125 pub fn reason(&self) -> String {
126 self.result.failure_reason()
127 }
128}
129
130#[derive(Clone, Debug, Default)]
131pub struct ApplicableServerStartOutcomes {
132 pub successful: Vec<ServerKey>,
133 pub failures: Vec<ApplicableServerFailure>,
134}
135
136impl ServerAttemptResult {
137 pub fn failure_reason(&self) -> String {
138 match self {
139 Self::BinaryNotInstalled { binary } => format!("{binary} is unavailable"),
140 Self::SpawnFailed { reason, .. } => reason.clone(),
141 Self::NoRootMarker { looked_for } => {
142 format!(
143 "no workspace root marker found (looked for {})",
144 looked_for.join(", ")
145 )
146 }
147 Self::Ok { .. } => "server started successfully".to_string(),
148 }
149 }
150}
151
152impl EnsureServerOutcomes {
153 pub fn no_server_registered(&self) -> bool {
155 self.attempts.is_empty()
156 }
157
158 pub fn only_inapplicable_root_markers(&self) -> bool {
166 self.successful.is_empty()
167 && !self.attempts.is_empty()
168 && self
169 .attempts
170 .iter()
171 .all(|attempt| matches!(attempt.result, ServerAttemptResult::NoRootMarker { .. }))
172 }
173}
174
175#[derive(Debug, Clone, Default)]
185pub struct PostEditWaitOutcome {
186 pub accepted_snapshots: Vec<AcceptedDiagnosticSnapshot>,
191 pub diagnostics: Vec<StoredDiagnostic>,
195 pub pending_servers: Vec<ServerKey>,
199 pub exited_servers: Vec<ServerKey>,
203}
204
205#[derive(Debug, Clone, Copy, Default)]
207pub struct PreEditSnapshot {
208 pub epoch: u64,
209 pub document_version_at_capture: Option<i32>,
210}
211
212#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
213pub struct StaleDiagnosticsMark {
214 pub had_entries: bool,
215 pub changed: bool,
216}
217
218pub fn post_edit_entry_is_fresh(
219 entry: &DiagnosticEntry,
220 target_version: i32,
221 pre: PreEditSnapshot,
222) -> bool {
223 if entry.stale || entry.epoch <= pre.epoch {
224 return false;
225 }
226
227 match entry.version {
228 Some(version) => version >= target_version,
229 None => false,
234 }
235}
236
237impl PostEditWaitOutcome {
238 pub fn complete(&self) -> bool {
241 self.pending_servers.is_empty() && self.exited_servers.is_empty()
242 }
243}
244
245#[derive(Debug, Clone)]
247pub enum PullFileOutcome {
248 Full { diagnostic_count: usize },
250 Unchanged,
252 PartialNotSupported,
255 PullNotSupported,
258 RequestFailed { reason: String },
260}
261
262#[derive(Debug, Clone, Default)]
264pub struct EnsureFileOpenResult {
265 pub server_keys: Vec<ServerKey>,
266 pub newly_opened: Vec<ServerKey>,
268}
269
270impl EnsureFileOpenResult {
271 pub fn is_empty(&self) -> bool {
272 self.server_keys.is_empty()
273 }
274}
275
276#[derive(Debug, Clone)]
278pub struct PullFileResult {
279 pub server_key: ServerKey,
280 pub outcome: PullFileOutcome,
281}
282
283#[derive(Debug, Clone)]
285pub struct PullWorkspaceResult {
286 pub server_key: ServerKey,
287 pub files_reported: Vec<PathBuf>,
291 pub complete: bool,
293 pub cancelled: bool,
295 pub supports_workspace: bool,
299}
300
301pub struct DrainedLspEvents {
302 pub events: Vec<LspEvent>,
303 pub diagnostics_changed: bool,
304 pub accepted_snapshots: Vec<AcceptedDiagnosticSnapshot>,
310 pub has_more: bool,
311}
312
313pub(crate) struct PostEditDiagnosticsWait {
318 lookup_path: PathBuf,
319 expected_versions: Vec<(ServerKey, i32)>,
320 pre_snapshot: HashMap<ServerKey, PreEditSnapshot>,
321 event_rx: Receiver<LspEvent>,
322 wake_rx: Receiver<()>,
323 waiter_id: u64,
324 deadline: std::time::Instant,
325 fresh: HashMap<ServerKey, Vec<StoredDiagnostic>>,
326 exited: Vec<ServerKey>,
327}
328
329impl PostEditDiagnosticsWait {
330 pub(crate) fn deadline_reached(&self) -> bool {
331 std::time::Instant::now() >= self.deadline
332 }
333
334 pub(crate) fn next_event(&self) -> Option<LspEvent> {
335 let remaining = self
336 .deadline
337 .saturating_duration_since(std::time::Instant::now());
338 if remaining.is_zero() {
339 return None;
340 }
341
342 crossbeam_channel::select! {
343 recv(self.event_rx) -> event => event.ok(),
344 recv(self.wake_rx) -> _ => None,
345 default(remaining) => None,
346 }
347 }
348}
349
350impl IntoIterator for DrainedLspEvents {
351 type Item = LspEvent;
352 type IntoIter = std::vec::IntoIter<LspEvent>;
353
354 fn into_iter(self) -> Self::IntoIter {
355 self.events.into_iter()
356 }
357}
358
359pub struct LspManager {
360 clients: HashMap<ServerKey, LspClient>,
362 server_binaries: HashMap<ServerKey, String>,
366 documents: HashMap<ServerKey, DocumentStore>,
368 diagnostics: DiagnosticsStore,
370 event_tx: Sender<LspEvent>,
372 event_rx: Receiver<LspEvent>,
373 post_edit_waiters: HashMap<u64, Sender<()>>,
377 next_post_edit_waiter_id: u64,
378 binary_overrides: HashMap<ServerKind, PathBuf>,
380 extra_env: HashMap<String, String>,
384 failed_spawns: HashMap<ServerKey, ServerAttemptResult>,
399 watched_file_skip_logged: HashSet<ServerKey>,
402 #[cfg(windows)]
405 last_watched_file_notification_trace: String,
406 child_registry: LspChildRegistry,
410}
411
412impl LspManager {
413 pub fn new() -> Self {
414 let (event_tx, event_rx) = unbounded();
415 Self {
416 clients: HashMap::new(),
417 server_binaries: HashMap::new(),
418 documents: HashMap::new(),
419 diagnostics: DiagnosticsStore::new(),
420 event_tx,
421 event_rx,
422 post_edit_waiters: HashMap::new(),
423 next_post_edit_waiter_id: 0,
424 binary_overrides: HashMap::new(),
425 extra_env: HashMap::new(),
426 failed_spawns: HashMap::new(),
427 watched_file_skip_logged: HashSet::new(),
428 #[cfg(windows)]
429 last_watched_file_notification_trace: "no watched-file notification attempted"
430 .to_string(),
431 child_registry: LspChildRegistry::new(),
432 }
433 }
434
435 pub fn set_child_registry(&mut self, registry: LspChildRegistry) {
437 self.child_registry = registry;
438 }
439
440 pub fn set_extra_env(&mut self, key: &str, value: &str) {
444 self.extra_env.insert(key.to_string(), value.to_string());
445 }
446
447 pub fn server_count(&self) -> usize {
449 self.clients.len()
450 }
451
452 pub fn estimated_memory(&self) -> crate::memory::MemoryEstimate {
456 let mut bytes = 0u64;
457 let mut document_count = 0u64;
458 for documents in self.documents.values() {
459 let estimate = documents.estimated_memory();
460 bytes = bytes.saturating_add(estimate.estimated_bytes.unwrap_or(0));
461 document_count = document_count
462 .saturating_add(estimate.counts.get("documents").copied().unwrap_or(0));
463 }
464 let diagnostics = self.diagnostics.estimated_memory();
465 bytes = bytes.saturating_add(diagnostics.estimated_bytes.unwrap_or(0));
466 crate::memory::MemoryEstimate::estimated(bytes)
467 .count("servers", self.clients.len())
468 .count("document_stores", self.documents.len())
469 .count_u64("documents", document_count)
470 .count_u64(
471 "diagnostic_entries",
472 diagnostics
473 .counts
474 .get("diagnostic_entries")
475 .copied()
476 .unwrap_or(0),
477 )
478 .count_u64(
479 "diagnostics",
480 diagnostics.counts.get("diagnostics").copied().unwrap_or(0),
481 )
482 }
483
484 pub fn set_diagnostic_capacity(&mut self, capacity: usize) {
488 self.diagnostics.set_capacity(capacity);
489 }
490
491 pub fn override_binary(&mut self, kind: ServerKind, binary_path: PathBuf) {
493 self.binary_overrides.insert(kind, binary_path);
494 }
495
496 pub fn resolve_applicable_servers_for_root(
503 &self,
504 project_root: &Path,
505 config: &Config,
506 ) -> Result<ApplicableServerSnapshot, ApplicabilityResolutionError> {
507 if !project_root.is_dir() {
508 return Err(ApplicabilityResolutionError::RootUnreadable {
509 root: project_root.to_path_buf(),
510 reason: "project root is not a directory".to_string(),
511 });
512 }
513
514 let mut candidates = HashMap::<ServerKey, ApplicableServerCandidate>::new();
515 let mut producer_failures = HashMap::<ServerKey, ApplicableServerFailure>::new();
516 let walker = ignore::WalkBuilder::new(project_root)
517 .standard_filters(true)
518 .add_custom_ignore_filename(".aftignore")
519 .filter_entry(|entry| {
520 !matches!(
521 entry.file_name().to_string_lossy().as_ref(),
522 ".git" | "node_modules" | "target" | "dist" | "build" | ".next" | ".turbo"
523 )
524 })
525 .build();
526
527 for entry in walker {
528 let entry = entry.map_err(|error| ApplicabilityResolutionError::RootUnreadable {
529 root: project_root.to_path_buf(),
530 reason: error.to_string(),
531 })?;
532 if !entry
533 .file_type()
534 .is_some_and(|file_type| file_type.is_file())
535 {
536 continue;
537 }
538 let file = entry.path();
539 for definition in servers_for_file(file, config) {
540 let Some(key) = server_key_for_definition(&definition, file, config) else {
541 continue;
542 };
543 if candidates.contains_key(&key) || producer_failures.contains_key(&key) {
544 continue;
545 }
546 if let Some(result) = self.failed_spawns.get(&key) {
547 producer_failures.insert(
548 key.clone(),
549 ApplicableServerFailure {
550 server_key: key,
551 result: result.clone(),
552 },
553 );
554 continue;
555 }
556 if self.resolve_binary(&definition, config).is_err() {
557 producer_failures.insert(
558 key.clone(),
559 ApplicableServerFailure {
560 server_key: key,
561 result: ServerAttemptResult::BinaryNotInstalled {
562 binary: definition.binary.clone(),
563 },
564 },
565 );
566 continue;
567 }
568 candidates.insert(
569 key.clone(),
570 ApplicableServerCandidate {
571 key,
572 definition,
573 source_file: file.to_path_buf(),
574 },
575 );
576 }
577 }
578
579 let mut candidates = candidates.into_values().collect::<Vec<_>>();
580 candidates.sort_by(|left, right| server_key_sort(&left.key, &right.key));
581 let mut producer_failures = producer_failures.into_values().collect::<Vec<_>>();
582 producer_failures
583 .sort_by(|left, right| server_key_sort(&left.server_key, &right.server_key));
584 let mut server_keys = candidates
585 .iter()
586 .map(|candidate| candidate.key.clone())
587 .chain(
588 producer_failures
589 .iter()
590 .map(|failure| failure.server_key.clone()),
591 )
592 .collect::<Vec<_>>();
593 server_keys.sort_by(server_key_sort);
594 Ok(ApplicableServerSnapshot {
595 server_keys,
596 candidates,
597 producer_failures,
598 })
599 }
600
601 pub fn start_applicable_servers(
607 &mut self,
608 snapshot: &ApplicableServerSnapshot,
609 config: &Config,
610 ) -> ApplicableServerStartOutcomes {
611 let mut outcomes = ApplicableServerStartOutcomes {
612 failures: snapshot.producer_failures.clone(),
613 ..ApplicableServerStartOutcomes::default()
614 };
615 for candidate in &snapshot.candidates {
616 if self.clients.contains_key(&candidate.key) {
617 outcomes.successful.push(candidate.key.clone());
618 continue;
619 }
620 match self.spawn_server(
621 &candidate.definition,
622 &candidate.key.root,
623 &candidate.source_file,
624 config,
625 ) {
626 Ok(client) => {
627 self.clients.insert(candidate.key.clone(), client);
628 self.server_binaries
629 .insert(candidate.key.clone(), candidate.definition.binary.clone());
630 self.documents.entry(candidate.key.clone()).or_default();
631 outcomes.successful.push(candidate.key.clone());
632 }
633 Err(error) => {
634 let result = classify_spawn_error(&candidate.definition.binary, &error);
635 self.failed_spawns
636 .insert(candidate.key.clone(), result.clone());
637 outcomes.failures.push(ApplicableServerFailure {
638 server_key: candidate.key.clone(),
639 result,
640 });
641 }
642 }
643 }
644 outcomes
645 }
646
647 pub fn ensure_server_for_file(&mut self, file_path: &Path, config: &Config) -> Vec<ServerKey> {
654 self.ensure_server_for_file_detailed(file_path, config)
655 .successful
656 }
657
658 fn running_server_keys_for_file(&self, file_path: &Path, config: &Config) -> Vec<ServerKey> {
659 servers_for_file(file_path, config)
660 .into_iter()
661 .filter_map(|def| server_key_for_definition(&def, file_path, config))
662 .filter(|key| self.clients.contains_key(key))
663 .collect()
664 }
665
666 pub fn ensure_server_for_file_detailed(
674 &mut self,
675 file_path: &Path,
676 config: &Config,
677 ) -> EnsureServerOutcomes {
678 let defs = servers_for_file(file_path, config);
679 let mut outcomes = EnsureServerOutcomes::default();
680
681 for def in defs {
682 let server_id = def.kind.id_str().to_string();
683 let server_name = def.name.to_string();
684
685 let Some(key) = server_key_for_definition(&def, file_path, config) else {
686 outcomes.attempts.push(ServerAttempt {
687 server_id,
688 server_name,
689 result: ServerAttemptResult::NoRootMarker {
690 looked_for: def.root_markers.iter().map(|s| s.to_string()).collect(),
691 },
692 });
693 continue;
694 };
695
696 if !self.clients.contains_key(&key) {
697 if let Some(cached) = self.failed_spawns.get(&key) {
704 outcomes.attempts.push(ServerAttempt {
705 server_id,
706 server_name,
707 result: cached.clone(),
708 });
709 continue;
710 }
711
712 match self.spawn_server(&def, &key.root, file_path, config) {
713 Ok(client) => {
714 self.clients.insert(key.clone(), client);
715 self.server_binaries.insert(key.clone(), def.binary.clone());
716 self.documents.entry(key.clone()).or_default();
717 }
718 Err(err) => {
719 slog_error!("failed to spawn {}: {}", def.name, err);
720 let result = classify_spawn_error(&def.binary, &err);
721 self.failed_spawns.insert(key.clone(), result.clone());
725 outcomes.attempts.push(ServerAttempt {
726 server_id,
727 server_name,
728 result,
729 });
730 continue;
731 }
732 }
733 }
734
735 outcomes.attempts.push(ServerAttempt {
736 server_id,
737 server_name,
738 result: ServerAttemptResult::Ok {
739 server_key: key.clone(),
740 },
741 });
742 outcomes.successful.push(key);
743 }
744
745 outcomes
746 }
747
748 pub fn ensure_server_for_file_default(&mut self, file_path: &Path) -> Vec<ServerKey> {
751 self.ensure_server_for_file(file_path, &Config::default())
752 }
753 pub fn ensure_file_open(
758 &mut self,
759 file_path: &Path,
760 config: &Config,
761 ) -> Result<EnsureFileOpenResult, LspError> {
762 let canonical_path = canonicalize_for_lsp(file_path)?;
763 let server_keys = self.ensure_server_for_file(&canonical_path, config);
764 if server_keys.is_empty() {
765 return Ok(EnsureFileOpenResult::default());
766 }
767
768 let uri = uri_for_path(&canonical_path)?;
769 let language_id = language_id_for_extension(
770 canonical_path
771 .extension()
772 .and_then(|ext| ext.to_str())
773 .unwrap_or_default(),
774 )
775 .to_string();
776 let needs_content = server_keys.iter().any(|key| {
777 !self
778 .documents
779 .get(key)
780 .is_some_and(|store| store.is_open(&canonical_path))
781 });
782 let initial_content = needs_content
783 .then(|| std::fs::read_to_string(&canonical_path).map_err(LspError::Io))
784 .transpose()?;
785 let mut newly_opened = Vec::new();
786
787 for key in &server_keys {
788 let already_open = self
789 .documents
790 .get(key)
791 .is_some_and(|store| store.is_open(&canonical_path));
792
793 if !already_open {
794 let content = initial_content
795 .as_ref()
796 .expect("content is loaded when any server needs didOpen");
797 let send_result = if let Some(client) = self.clients.get_mut(key) {
798 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
799 text_document: TextDocumentItem::new(
800 uri.clone(),
801 language_id.clone(),
802 0,
803 content.clone(),
804 ),
805 })
806 } else {
807 Ok(())
808 };
809 if let Err(err) = send_result {
810 let _ = self.close_file_for_servers(&canonical_path, &newly_opened);
811 return Err(err);
812 }
813 self.documents
814 .entry(key.clone())
815 .or_default()
816 .open(canonical_path.clone());
817 newly_opened.push(key.clone());
818 continue;
819 }
820
821 let drifted = self
831 .documents
832 .get(key)
833 .is_some_and(|store| store.is_stale_on_disk(&canonical_path));
834 if drifted {
835 let content = match std::fs::read_to_string(&canonical_path) {
836 Ok(content) => content,
837 Err(err) => {
838 let _ = self.close_file_for_servers(&canonical_path, &newly_opened);
839 return Err(LspError::Io(err));
840 }
841 };
842 let next_version = self
843 .documents
844 .get(key)
845 .and_then(|store| store.version(&canonical_path))
846 .map(|v| v + 1)
847 .unwrap_or(1);
848 let send_result = if let Some(client) = self.clients.get_mut(key) {
849 client.send_notification::<DidChangeTextDocument>(DidChangeTextDocumentParams {
850 text_document: VersionedTextDocumentIdentifier::new(
851 uri.clone(),
852 next_version,
853 ),
854 content_changes: vec![TextDocumentContentChangeEvent {
855 range: None,
856 range_length: None,
857 text: content,
858 }],
859 })
860 } else {
861 Ok(())
862 };
863 if let Err(err) = send_result {
864 let _ = self.close_file_for_servers(&canonical_path, &newly_opened);
865 return Err(err);
866 }
867 if let Some(store) = self.documents.get_mut(key) {
868 store.bump_version(&canonical_path);
869 }
870 }
871 }
872
873 Ok(EnsureFileOpenResult {
874 server_keys,
875 newly_opened,
876 })
877 }
878
879 pub fn ensure_file_open_default(
880 &mut self,
881 file_path: &Path,
882 ) -> Result<EnsureFileOpenResult, LspError> {
883 self.ensure_file_open(file_path, &Config::default())
884 }
885
886 pub fn notify_file_changed(
892 &mut self,
893 file_path: &Path,
894 content: &str,
895 config: &Config,
896 ) -> Result<(), LspError> {
897 self.notify_file_changed_versioned(file_path, content, config)
898 .map(|_| ())
899 }
900
901 pub fn notify_file_changed_versioned(
912 &mut self,
913 file_path: &Path,
914 content: &str,
915 config: &Config,
916 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
917 let canonical_path = canonicalize_for_lsp(file_path)?;
918 let server_keys = self.ensure_server_for_file(&canonical_path, config);
919 self.notify_file_changed_for_server_keys(canonical_path, content, server_keys)
920 }
921
922 pub fn notify_file_changed_if_running(
928 &mut self,
929 file_path: &Path,
930 content: &str,
931 config: &Config,
932 ) -> Result<(), LspError> {
933 self.notify_file_changed_if_running_versioned(file_path, content, config)
934 .map(|_| ())
935 }
936
937 pub fn notify_file_changed_if_running_versioned(
941 &mut self,
942 file_path: &Path,
943 content: &str,
944 config: &Config,
945 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
946 let canonical_path = canonicalize_for_lsp(file_path)?;
947 let server_keys = self.running_server_keys_for_file(&canonical_path, config);
948 self.notify_file_changed_for_server_keys(canonical_path, content, server_keys)
949 }
950
951 fn notify_file_changed_for_server_keys(
952 &mut self,
953 canonical_path: PathBuf,
954 content: &str,
955 server_keys: Vec<ServerKey>,
956 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
957 if server_keys.is_empty() {
958 return Ok(Vec::new());
959 }
960
961 let uri = uri_for_path(&canonical_path)?;
962 let language_id = language_id_for_extension(
963 canonical_path
964 .extension()
965 .and_then(|ext| ext.to_str())
966 .unwrap_or_default(),
967 )
968 .to_string();
969
970 let mut versions: Vec<(ServerKey, i32)> = Vec::with_capacity(server_keys.len());
971
972 for key in server_keys {
973 let current_version = self
974 .documents
975 .get(&key)
976 .and_then(|store| store.version(&canonical_path));
977
978 if let Some(version) = current_version {
979 let next_version = version + 1;
980 if let Some(client) = self.clients.get_mut(&key) {
981 client.send_notification::<DidChangeTextDocument>(
982 DidChangeTextDocumentParams {
983 text_document: VersionedTextDocumentIdentifier::new(
984 uri.clone(),
985 next_version,
986 ),
987 content_changes: vec![TextDocumentContentChangeEvent {
988 range: None,
989 range_length: None,
990 text: content.to_string(),
991 }],
992 },
993 )?;
994 }
995 if let Some(store) = self.documents.get_mut(&key) {
996 store.bump_version(&canonical_path);
997 }
998 versions.push((key, next_version));
999 continue;
1000 }
1001
1002 if let Some(client) = self.clients.get_mut(&key) {
1003 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
1004 text_document: TextDocumentItem::new(
1005 uri.clone(),
1006 language_id.clone(),
1007 0,
1008 content.to_string(),
1009 ),
1010 })?;
1011 }
1012 self.documents
1013 .entry(key.clone())
1014 .or_default()
1015 .open(canonical_path.clone());
1016 versions.push((key, 0));
1019 }
1020
1021 Ok(versions)
1022 }
1023
1024 pub fn notify_file_changed_default(
1025 &mut self,
1026 file_path: &Path,
1027 content: &str,
1028 ) -> Result<(), LspError> {
1029 self.notify_file_changed(file_path, content, &Config::default())
1030 }
1031
1032 pub fn notify_files_watched_changed(
1038 &mut self,
1039 paths: &[(PathBuf, FileChangeType)],
1040 _config: &Config,
1041 ) -> Result<(), LspError> {
1042 #[cfg(windows)]
1043 let mut trace = vec![format!(
1044 "input_paths={paths:?}; active_keys={:?}",
1045 self.clients.keys().collect::<Vec<_>>()
1046 )];
1047
1048 if paths.is_empty() {
1049 #[cfg(windows)]
1050 {
1051 trace.push("outcome=no-input-paths".to_string());
1052 self.last_watched_file_notification_trace = trace.join("\n");
1053 }
1054 return Ok(());
1055 }
1056
1057 let mut canonical_events = Vec::with_capacity(paths.len());
1058 for (path, typ) in paths {
1059 let canonical_path = resolve_for_lsp_uri(path);
1060 canonical_events.push((canonical_path, *typ));
1061 }
1062 #[cfg(windows)]
1063 trace.push(format!("resolved_events={canonical_events:?}"));
1064
1065 let keys: Vec<ServerKey> = self.clients.keys().cloned().collect();
1066 #[cfg(windows)]
1067 if keys.is_empty() {
1068 trace.push("outcome=no-active-client".to_string());
1069 }
1070 for key in keys {
1071 let mut changes = Vec::new();
1072 for (path, typ) in &canonical_events {
1073 if !path.starts_with(&key.root) {
1074 continue;
1075 }
1076 changes.push(FileEvent::new(uri_for_path(path)?, *typ));
1077 }
1078
1079 if changes.is_empty() {
1080 #[cfg(windows)]
1081 trace.push(format!("key={key:?}; outcome=outside-root"));
1082 continue;
1083 }
1084
1085 if let Some(client) = self.clients.get_mut(&key) {
1086 let supports_static_watched_files = client.supports_watched_files();
1092 let has_dynamic_registration = client.has_watched_file_registration();
1093 if !(supports_static_watched_files || has_dynamic_registration) {
1094 #[cfg(windows)]
1095 trace.push(format!(
1096 "key={key:?}; changes={changes:?}; outcome=unsupported; static={supports_static_watched_files}; dynamic={has_dynamic_registration}"
1097 ));
1098 if self.watched_file_skip_logged.insert(key.clone()) {
1099 log::debug!(
1100 "skipping didChangeWatchedFiles for {:?} (not supported or registered)",
1101 key
1102 );
1103 }
1104 continue;
1105 }
1106 #[cfg(windows)]
1107 trace.push(format!("key={key:?}; changes={changes:?}; action=send"));
1108 let send_result = client.send_notification::<DidChangeWatchedFiles>(
1109 DidChangeWatchedFilesParams { changes },
1110 );
1111 #[cfg(windows)]
1112 trace.push(format!(
1113 "key={key:?}; outcome={}",
1114 if send_result.is_ok() {
1115 "sent"
1116 } else {
1117 "send-error"
1118 }
1119 ));
1120 if let Err(error) = send_result {
1121 #[cfg(windows)]
1122 {
1123 self.last_watched_file_notification_trace = trace.join("\n");
1124 }
1125 return Err(error);
1126 }
1127 }
1128 }
1129
1130 #[cfg(windows)]
1131 {
1132 self.last_watched_file_notification_trace = trace.join("\n");
1133 }
1134 Ok(())
1135 }
1136
1137 pub fn notify_file_closed(&mut self, file_path: &Path) -> Result<(), LspError> {
1139 let canonical_path = canonicalize_for_lsp(file_path)?;
1140 let keys = self
1141 .documents
1142 .iter()
1143 .filter(|(_, store)| store.is_open(&canonical_path))
1144 .map(|(key, _)| key.clone())
1145 .collect::<Vec<_>>();
1146 self.close_file_for_servers(&canonical_path, &keys)
1147 }
1148
1149 pub(crate) fn close_file_for_servers(
1154 &mut self,
1155 file_path: &Path,
1156 server_keys: &[ServerKey],
1157 ) -> Result<(), LspError> {
1158 let canonical_path = canonicalize_for_lsp(file_path)?;
1159 let uri = uri_for_path(&canonical_path)?;
1160 let mut first_error = None;
1161
1162 for key in server_keys {
1163 let was_open = self
1164 .documents
1165 .get(key)
1166 .is_some_and(|store| store.is_open(&canonical_path));
1167 if !was_open {
1168 continue;
1169 }
1170
1171 if let Some(client) = self.clients.get_mut(key) {
1172 if let Err(err) =
1173 client.send_notification::<DidCloseTextDocument>(DidCloseTextDocumentParams {
1174 text_document: TextDocumentIdentifier::new(uri.clone()),
1175 })
1176 {
1177 if first_error.is_none() {
1178 first_error = Some(err);
1179 }
1180 }
1181 }
1182
1183 if let Some(store) = self.documents.get_mut(key) {
1184 store.close(&canonical_path);
1185 }
1186 self.diagnostics.clear_for_server_file(key, &canonical_path);
1187 }
1188
1189 match first_error {
1190 Some(err) => Err(err),
1191 None => Ok(()),
1192 }
1193 }
1194
1195 pub fn client_for_file(&self, file_path: &Path, config: &Config) -> Option<&LspClient> {
1197 let key = self.server_key_for_file(file_path, config)?;
1198 self.clients.get(&key)
1199 }
1200
1201 pub fn client_for_file_default(&self, file_path: &Path) -> Option<&LspClient> {
1202 self.client_for_file(file_path, &Config::default())
1203 }
1204
1205 pub fn client_for_file_mut(
1207 &mut self,
1208 file_path: &Path,
1209 config: &Config,
1210 ) -> Option<&mut LspClient> {
1211 let key = self.server_key_for_file(file_path, config)?;
1212 self.clients.get_mut(&key)
1213 }
1214
1215 pub fn client_for_file_mut_default(&mut self, file_path: &Path) -> Option<&mut LspClient> {
1216 self.client_for_file_mut(file_path, &Config::default())
1217 }
1218
1219 pub fn active_client_count(&self) -> usize {
1221 self.clients.len()
1222 }
1223
1224 pub fn drain_events(&mut self) -> DrainedLspEvents {
1226 self.drain_events_bounded(usize::MAX)
1227 }
1228
1229 pub fn has_pending_events(&self) -> bool {
1232 !self.event_rx.is_empty()
1233 }
1234
1235 pub fn drain_events_bounded(&mut self, max_events: usize) -> DrainedLspEvents {
1236 let mut events = Vec::new();
1237 let mut diagnostics_changed = false;
1238 let mut accepted_snapshots = Vec::new();
1239 while events.len() < max_events {
1240 let Ok(event) = self.event_rx.try_recv() else {
1241 break;
1242 };
1243 if self.handle_event(&event).is_some() {
1244 diagnostics_changed = true;
1245 }
1246 if let Some(snapshot) = self.accepted_live_publish_snapshot(&event) {
1247 accepted_snapshots.push(snapshot);
1248 }
1249 events.push(event);
1250 }
1251 let has_more = events.len() >= max_events && !self.event_rx.is_empty();
1252 DrainedLspEvents {
1253 events,
1254 diagnostics_changed,
1255 accepted_snapshots,
1256 has_more,
1257 }
1258 }
1259
1260 pub fn wait_for_diagnostics(
1262 &mut self,
1263 file_path: &Path,
1264 config: &Config,
1265 timeout: std::time::Duration,
1266 ) -> Vec<StoredDiagnostic> {
1267 let deadline = std::time::Instant::now() + timeout;
1268 self.wait_for_file_diagnostics(file_path, config, deadline)
1269 }
1270
1271 pub fn wait_for_diagnostics_default(
1272 &mut self,
1273 file_path: &Path,
1274 timeout: std::time::Duration,
1275 ) -> Vec<StoredDiagnostic> {
1276 self.wait_for_diagnostics(file_path, &Config::default(), timeout)
1277 }
1278
1279 #[doc(hidden)]
1284 pub fn diagnostics_store_for_test(&self) -> &DiagnosticsStore {
1285 &self.diagnostics
1286 }
1287
1288 #[doc(hidden)]
1289 pub fn diagnostics_store_mut_for_test(&mut self) -> &mut DiagnosticsStore {
1290 &mut self.diagnostics
1291 }
1292
1293 #[doc(hidden)]
1294 pub fn post_edit_outcome_for_entry_for_test(
1295 key: ServerKey,
1296 entry: &DiagnosticEntry,
1297 target_version: i32,
1298 pre: PreEditSnapshot,
1299 ) -> PostEditWaitOutcome {
1300 Self::post_edit_outcome_for_entry(key, entry, target_version, pre)
1301 }
1302
1303 fn post_edit_outcome_for_entry(
1304 key: ServerKey,
1305 entry: &DiagnosticEntry,
1306 target_version: i32,
1307 pre: PreEditSnapshot,
1308 ) -> PostEditWaitOutcome {
1309 let mut fresh = HashMap::new();
1310 if let Some(diagnostics) =
1311 Self::authoritative_post_edit_diagnostics(entry, target_version, pre)
1312 {
1313 fresh.insert(key.clone(), diagnostics);
1314 }
1315 Self::post_edit_outcome(vec![(key, target_version)], fresh, Vec::new())
1316 }
1317
1318 fn authoritative_post_edit_diagnostics(
1319 entry: &DiagnosticEntry,
1320 target_version: i32,
1321 pre: PreEditSnapshot,
1322 ) -> Option<Vec<StoredDiagnostic>> {
1323 (!entry.provisional && post_edit_entry_is_fresh(entry, target_version, pre))
1324 .then(|| entry.diagnostics.clone())
1325 }
1326
1327 #[doc(hidden)]
1328 pub fn enqueue_event_for_test(&self, event: LspEvent) {
1329 self.event_tx
1330 .send(event)
1331 .expect("LSP event receiver should remain connected");
1332 }
1333
1334 #[doc(hidden)]
1335 pub fn pending_event_count_for_test(&self) -> usize {
1336 self.event_rx.len()
1337 }
1338
1339 #[doc(hidden)]
1340 pub fn document_is_open_for_test(&self, file_path: &Path) -> bool {
1341 canonicalize_for_lsp(file_path).is_ok_and(|canonical_path| {
1342 self.documents
1343 .values()
1344 .any(|store| store.is_open(&canonical_path))
1345 })
1346 }
1347
1348 pub fn warm_error_warning_counts(&self) -> (usize, usize) {
1352 self.diagnostics.error_warning_counts()
1353 }
1354
1355 pub fn warm_error_warning_counts_with_provisional(&self) -> ((usize, usize), bool) {
1356 self.diagnostics.error_warning_counts_with_provisional()
1357 }
1358
1359 pub fn diagnostics_generation(&self) -> u64 {
1360 self.diagnostics.generation()
1361 }
1362
1363 pub fn filtered_error_warning_counts(
1368 &self,
1369 keep: impl FnMut(&std::path::Path) -> bool,
1370 ) -> (usize, usize) {
1371 self.diagnostics.filtered_error_warning_counts(keep)
1372 }
1373
1374 pub fn filtered_error_warning_counts_with_provisional(
1378 &self,
1379 keep: impl FnMut(&std::path::Path) -> bool,
1380 ) -> ((usize, usize), bool) {
1381 self.diagnostics
1382 .filtered_error_warning_counts_with_provisional(keep)
1383 }
1384
1385 pub fn provisional_server_keys(&self) -> Vec<ServerKey> {
1389 self.clients
1390 .iter()
1391 .filter(|(_, client)| client.diagnostics_are_provisional())
1392 .map(|(key, _)| key.clone())
1393 .collect()
1394 }
1395
1396 pub fn snapshot_diagnostic_epochs(&self, file_path: &Path) -> HashMap<ServerKey, u64> {
1401 let lookup_path = normalize_lookup_path(file_path);
1402 self.diagnostics
1403 .entries_for_file(&lookup_path)
1404 .into_iter()
1405 .map(|(key, entry)| (key.clone(), entry.epoch))
1406 .collect()
1407 }
1408
1409 pub fn snapshot_pre_edit_state(&self, file_path: &Path) -> HashMap<ServerKey, PreEditSnapshot> {
1412 let lookup_path = normalize_lookup_path(file_path);
1413 let mut snapshots: HashMap<ServerKey, PreEditSnapshot> = self
1414 .diagnostics
1415 .entries_for_file(&lookup_path)
1416 .into_iter()
1417 .map(|(key, entry)| {
1418 (
1419 key.clone(),
1420 PreEditSnapshot {
1421 epoch: entry.epoch,
1422 document_version_at_capture: None,
1423 },
1424 )
1425 })
1426 .collect();
1427
1428 for (key, store) in &self.documents {
1429 if let Some(version) = store.version(&lookup_path) {
1430 snapshots
1431 .entry(key.clone())
1432 .or_default()
1433 .document_version_at_capture = Some(version);
1434 }
1435 }
1436
1437 snapshots
1438 }
1439
1440 pub fn diagnostic_entry_is_fresh_for_document(
1448 &self,
1449 file_path: &Path,
1450 server_key: &ServerKey,
1451 pre: PreEditSnapshot,
1452 ) -> bool {
1453 let lookup_path = normalize_lookup_path(file_path);
1454 let Some(entry) = self
1455 .diagnostics
1456 .entries_for_file(&lookup_path)
1457 .into_iter()
1458 .find_map(|(key, entry)| if key == server_key { Some(entry) } else { None })
1459 else {
1460 return false;
1461 };
1462
1463 if entry.stale {
1464 return false;
1465 }
1466
1467 let target_version = self
1468 .documents
1469 .get(server_key)
1470 .and_then(|store| store.version(&lookup_path))
1471 .or(pre.document_version_at_capture)
1472 .unwrap_or(0);
1473
1474 matches!(entry.version, Some(version) if version >= target_version)
1475 }
1476
1477 pub(crate) fn start_post_edit_diagnostics_wait(
1481 &mut self,
1482 file_path: &Path,
1483 expected_versions: &[(ServerKey, i32)],
1484 pre_snapshot: &HashMap<ServerKey, PreEditSnapshot>,
1485 timeout: std::time::Duration,
1486 ) -> PostEditDiagnosticsWait {
1487 let lookup_path = normalize_lookup_path(file_path);
1488
1489 let _ = self.drain_events_for_file(&lookup_path);
1492
1493 let waiter_id = self.next_post_edit_waiter_id;
1494 self.next_post_edit_waiter_id = self.next_post_edit_waiter_id.wrapping_add(1);
1495 let (wake_tx, wake_rx) = bounded(1);
1496 self.post_edit_waiters.insert(waiter_id, wake_tx);
1497
1498 PostEditDiagnosticsWait {
1499 lookup_path,
1500 expected_versions: expected_versions.to_vec(),
1501 pre_snapshot: pre_snapshot.clone(),
1502 event_rx: self.event_rx.clone(),
1503 wake_rx,
1504 waiter_id,
1505 deadline: std::time::Instant::now() + timeout,
1506 fresh: HashMap::new(),
1507 exited: Vec::new(),
1508 }
1509 }
1510
1511 pub(crate) fn poll_post_edit_diagnostics_wait(
1512 &mut self,
1513 wait: &mut PostEditDiagnosticsWait,
1514 event: Option<LspEvent>,
1515 ) -> bool {
1516 if let Some(event) = event {
1517 self.handle_event(&event);
1518 }
1519
1520 for (key, target_version) in &wait.expected_versions {
1521 if wait.fresh.contains_key(key) || wait.exited.contains(key) {
1522 continue;
1523 }
1524 if !self.clients.contains_key(key) {
1525 wait.exited.push(key.clone());
1526 continue;
1527 }
1528 if let Some(entry) = self
1529 .diagnostics
1530 .entries_for_file(&wait.lookup_path)
1531 .into_iter()
1532 .find_map(|(stored_key, entry)| (stored_key == key).then_some(entry))
1533 {
1534 let pre = wait.pre_snapshot.get(key).copied().unwrap_or_default();
1535 if let Some(diagnostics) =
1536 Self::authoritative_post_edit_diagnostics(entry, *target_version, pre)
1537 {
1538 wait.fresh.insert(key.clone(), diagnostics);
1539 }
1540 }
1541 }
1542
1543 wait.fresh.len() + wait.exited.len() == wait.expected_versions.len()
1544 }
1545
1546 pub(crate) fn finish_post_edit_diagnostics_wait(
1547 &mut self,
1548 wait: PostEditDiagnosticsWait,
1549 ) -> PostEditWaitOutcome {
1550 self.post_edit_waiters.remove(&wait.waiter_id);
1551 Self::post_edit_outcome(wait.expected_versions, wait.fresh, wait.exited)
1552 }
1553
1554 pub fn wait_for_post_edit_diagnostics(
1562 &mut self,
1563 file_path: &Path,
1564 _config: &Config,
1568 expected_versions: &[(ServerKey, i32)],
1569 pre_snapshot: &HashMap<ServerKey, PreEditSnapshot>,
1570 timeout: std::time::Duration,
1571 ) -> PostEditWaitOutcome {
1572 let mut wait = self.start_post_edit_diagnostics_wait(
1573 file_path,
1574 expected_versions,
1575 pre_snapshot,
1576 timeout,
1577 );
1578 let mut complete = self.poll_post_edit_diagnostics_wait(&mut wait, None);
1579
1580 while !complete && !wait.deadline_reached() {
1581 let event = wait.next_event();
1582 complete = self.poll_post_edit_diagnostics_wait(&mut wait, event);
1583 }
1584
1585 self.finish_post_edit_diagnostics_wait(wait)
1586 }
1587
1588 fn post_edit_outcome(
1589 mut expected: Vec<(ServerKey, i32)>,
1590 mut fresh: HashMap<ServerKey, Vec<StoredDiagnostic>>,
1591 exited: Vec<ServerKey>,
1592 ) -> PostEditWaitOutcome {
1593 expected.sort_by(|(left, _), (right, _)| server_key_sort(left, right));
1594
1595 let mut accepted_snapshots = Vec::new();
1596 let mut pending_servers = Vec::new();
1597 for (server_key, document_version) in expected {
1598 if let Some(diagnostics) = fresh.remove(&server_key) {
1599 accepted_snapshots.push(AcceptedDiagnosticSnapshot::new(
1600 server_key,
1601 document_version,
1602 diagnostics,
1603 ));
1604 } else if !exited.contains(&server_key) {
1605 pending_servers.push(server_key);
1606 }
1607 }
1608
1609 let mut diagnostics = accepted_snapshots
1610 .iter()
1611 .flat_map(|snapshot| snapshot.diagnostics.iter().cloned())
1612 .collect::<Vec<_>>();
1613 diagnostics.sort_by(|left, right| {
1614 left.file
1615 .cmp(&right.file)
1616 .then(left.line.cmp(&right.line))
1617 .then(left.column.cmp(&right.column))
1618 .then(left.message.cmp(&right.message))
1619 });
1620
1621 PostEditWaitOutcome {
1622 accepted_snapshots,
1623 diagnostics,
1624 pending_servers,
1625 exited_servers: exited,
1626 }
1627 }
1628
1629 pub fn wait_for_file_diagnostics(
1635 &mut self,
1636 file_path: &Path,
1637 config: &Config,
1638 deadline: std::time::Instant,
1639 ) -> Vec<StoredDiagnostic> {
1640 let lookup_path = normalize_lookup_path(file_path);
1641
1642 if self.server_key_for_file(&lookup_path, config).is_none() {
1643 return Vec::new();
1644 }
1645
1646 loop {
1647 if self.drain_events_for_file(&lookup_path) {
1648 break;
1649 }
1650
1651 let now = std::time::Instant::now();
1652 if now >= deadline {
1653 break;
1654 }
1655
1656 let timeout = deadline.saturating_duration_since(now);
1657 match self.event_rx.recv_timeout(timeout) {
1658 Ok(event) => {
1659 if matches!(
1660 self.handle_event(&event),
1661 Some(ref published_file) if published_file.as_path() == lookup_path.as_path()
1662 ) {
1663 break;
1664 }
1665 }
1666 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
1667 }
1668 }
1669
1670 self.get_diagnostics_for_file(&lookup_path)
1671 .into_iter()
1672 .cloned()
1673 .collect()
1674 }
1675
1676 pub const PULL_FILE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
1682
1683 pub fn pull_file_timeout() -> std::time::Duration {
1685 Self::PULL_FILE_TIMEOUT
1686 }
1687
1688 const PULL_WORKSPACE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
1692
1693 pub fn pull_file_diagnostics(
1704 &mut self,
1705 file_path: &Path,
1706 config: &Config,
1707 ) -> Result<Vec<PullFileResult>, LspError> {
1708 let canonical_path = canonicalize_for_lsp(file_path)?;
1709 let opened = self.ensure_file_open(&canonical_path, config)?;
1712 if opened.server_keys.is_empty() {
1713 return Ok(Vec::new());
1714 }
1715
1716 let uri = uri_for_path(&canonical_path)?;
1717 let mut results = Vec::with_capacity(opened.server_keys.len());
1718
1719 for key in opened.server_keys {
1720 let supports_pull = self
1721 .clients
1722 .get(&key)
1723 .and_then(|c| c.diagnostic_capabilities())
1724 .is_some_and(|caps| caps.pull_diagnostics);
1725
1726 if !supports_pull {
1727 results.push(PullFileResult {
1728 server_key: key.clone(),
1729 outcome: PullFileOutcome::PullNotSupported,
1730 });
1731 continue;
1732 }
1733
1734 let previous_result_id = self
1736 .diagnostics
1737 .entries_for_file(&canonical_path)
1738 .into_iter()
1739 .find(|(k, _)| **k == key)
1740 .and_then(|(_, entry)| entry.result_id.clone());
1741
1742 let identifier = self
1743 .clients
1744 .get(&key)
1745 .and_then(|c| c.diagnostic_capabilities())
1746 .and_then(|caps| caps.identifier.clone());
1747
1748 let params = AftDocumentDiagnosticParams {
1749 text_document: lsp_types::TextDocumentIdentifier { uri: uri.clone() },
1750 identifier,
1751 previous_result_id,
1752 work_done_progress_params: Default::default(),
1753 partial_result_params: Default::default(),
1754 };
1755
1756 let outcome = match self.send_pull_request(&key, params) {
1757 Ok(report) => {
1758 if matches!(
1759 &report,
1760 lsp_types::DocumentDiagnosticReportResult::Report(
1761 lsp_types::DocumentDiagnosticReport::Full(_)
1762 )
1763 ) {
1764 self.drain_events();
1769 }
1770 self.ingest_document_report(&key, &canonical_path, report)
1771 }
1772 Err(err) => {
1773 if let Some(result) = self.cache_post_initialize_exit(&key, &err) {
1774 PullFileOutcome::RequestFailed {
1775 reason: server_attempt_result_reason(&result),
1776 }
1777 } else if recoverable_pull_rejection(&err)
1778 && self.clients.get(&key).is_some_and(|client| {
1779 matches!(
1780 client.state(),
1781 ServerState::Ready | ServerState::Initializing
1782 )
1783 })
1784 {
1785 PullFileOutcome::RequestFailed {
1786 reason: format!("pull_rejected_push_fallback: {err}"),
1787 }
1788 } else {
1789 PullFileOutcome::RequestFailed {
1790 reason: err.to_string(),
1791 }
1792 }
1793 }
1794 };
1795
1796 results.push(PullFileResult {
1797 server_key: key,
1798 outcome,
1799 });
1800 }
1801
1802 Ok(results)
1803 }
1804
1805 pub fn pull_workspace_diagnostics(
1810 &mut self,
1811 server_key: &ServerKey,
1812 timeout: Option<std::time::Duration>,
1813 ) -> Result<PullWorkspaceResult, LspError> {
1814 let timeout = timeout.unwrap_or(Self::PULL_WORKSPACE_TIMEOUT);
1815
1816 let supports_workspace = self
1817 .clients
1818 .get(server_key)
1819 .and_then(|c| c.diagnostic_capabilities())
1820 .is_some_and(|caps| caps.workspace_diagnostics);
1821
1822 if !supports_workspace {
1823 return Ok(PullWorkspaceResult {
1824 server_key: server_key.clone(),
1825 files_reported: Vec::new(),
1826 complete: false,
1827 cancelled: false,
1828 supports_workspace: false,
1829 });
1830 }
1831
1832 let identifier = self
1833 .clients
1834 .get(server_key)
1835 .and_then(|c| c.diagnostic_capabilities())
1836 .and_then(|caps| caps.identifier.clone());
1837
1838 let params = AftWorkspaceDiagnosticParams {
1839 identifier,
1840 previous_result_ids: Vec::new(),
1841 work_done_progress_params: Default::default(),
1842 partial_result_params: Default::default(),
1843 };
1844
1845 let result = match self
1846 .clients
1847 .get_mut(server_key)
1848 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?
1849 .send_request_with_timeout::<AftWorkspaceDiagnosticRequest>(params, timeout)
1850 {
1851 Ok(result) => result,
1852 Err(LspError::Timeout(_)) => {
1853 return Ok(PullWorkspaceResult {
1854 server_key: server_key.clone(),
1855 files_reported: Vec::new(),
1856 complete: false,
1857 cancelled: true,
1858 supports_workspace: true,
1859 });
1860 }
1861 Err(err) => {
1862 if let Some(result) = self.cache_post_initialize_exit(server_key, &err) {
1863 return Err(LspError::ServerNotReady(server_attempt_result_reason(
1864 &result,
1865 )));
1866 }
1867 return Err(err);
1868 }
1869 };
1870
1871 let (items, complete) = match result {
1875 lsp_types::WorkspaceDiagnosticReportResult::Report(report) => (report.items, true),
1876 lsp_types::WorkspaceDiagnosticReportResult::Partial(partial) => (partial.items, false),
1877 };
1878
1879 let mut files_reported = Vec::with_capacity(items.len());
1881 for item in items {
1882 match item {
1883 lsp_types::WorkspaceDocumentDiagnosticReport::Full(full) => {
1884 if let Some(file) = uri_to_path(&full.uri) {
1885 let stored = from_lsp_diagnostics(
1886 file.clone(),
1887 full.full_document_diagnostic_report.items.clone(),
1888 );
1889 self.diagnostics.publish_with_result_id(
1890 server_key.clone(),
1891 file.clone(),
1892 stored,
1893 full.full_document_diagnostic_report.result_id.clone(),
1894 );
1895 files_reported.push(file);
1896 }
1897 }
1898 lsp_types::WorkspaceDocumentDiagnosticReport::Unchanged(_unchanged) => {
1899 }
1902 }
1903 }
1904
1905 Ok(PullWorkspaceResult {
1906 server_key: server_key.clone(),
1907 files_reported,
1908 complete,
1909 cancelled: false,
1910 supports_workspace: true,
1911 })
1912 }
1913
1914 fn cache_post_initialize_exit(
1915 &mut self,
1916 key: &ServerKey,
1917 err: &LspError,
1918 ) -> Option<ServerAttemptResult> {
1919 let binary = self
1920 .server_binaries
1921 .get(key)
1922 .cloned()
1923 .unwrap_or_else(|| key.kind.id_str().to_string());
1924 let (status, stderr_tail) = {
1925 let client = self.clients.get_mut(key)?;
1926 let mut status = client.child_exit_status();
1927 for _ in 0..10 {
1928 if status.is_some() {
1929 break;
1930 }
1931 std::thread::sleep(std::time::Duration::from_millis(10));
1932 status = client.child_exit_status();
1933 }
1934 let status = status?;
1935 wait_for_stderr_tail(client);
1936 (status, client.stderr_tail())
1937 };
1938 let reason = format_post_initialize_exit_reason(&binary, status, &stderr_tail, err);
1939 let result = ServerAttemptResult::SpawnFailed { binary, reason };
1940 self.clients.remove(key);
1941 self.server_binaries.remove(key);
1942 self.documents.remove(key);
1943 self.diagnostics.clear_for_server(key);
1944 self.failed_spawns.insert(key.clone(), result.clone());
1945 Some(result)
1946 }
1947
1948 fn send_pull_request(
1950 &mut self,
1951 key: &ServerKey,
1952 params: AftDocumentDiagnosticParams,
1953 ) -> Result<lsp_types::DocumentDiagnosticReportResult, LspError> {
1954 let client = self
1955 .clients
1956 .get_mut(key)
1957 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?;
1958 client.send_request_with_timeout::<AftDocumentDiagnosticRequest>(
1962 params,
1963 Self::PULL_FILE_TIMEOUT,
1964 )
1965 }
1966
1967 fn ingest_document_report(
1970 &mut self,
1971 key: &ServerKey,
1972 canonical_path: &Path,
1973 result: lsp_types::DocumentDiagnosticReportResult,
1974 ) -> PullFileOutcome {
1975 let report = match result {
1976 lsp_types::DocumentDiagnosticReportResult::Report(report) => report,
1977 lsp_types::DocumentDiagnosticReportResult::Partial(_) => {
1978 return PullFileOutcome::PartialNotSupported;
1982 }
1983 };
1984
1985 match report {
1986 lsp_types::DocumentDiagnosticReport::Full(full) => {
1987 let result_id = full.full_document_diagnostic_report.result_id.clone();
1988 let stored = from_lsp_diagnostics(
1989 canonical_path.to_path_buf(),
1990 full.full_document_diagnostic_report.items.clone(),
1991 );
1992 let count = stored.len();
1993 let provisional = self
1994 .clients
1995 .get(key)
1996 .is_some_and(|client| client.diagnostics_are_provisional());
1997 self.diagnostics.publish_full_with_provisional(
1998 key.clone(),
1999 canonical_path.to_path_buf(),
2000 stored,
2001 result_id,
2002 None,
2003 provisional,
2004 );
2005 PullFileOutcome::Full {
2006 diagnostic_count: count,
2007 }
2008 }
2009 lsp_types::DocumentDiagnosticReport::Unchanged(_unchanged) => {
2010 if self
2017 .diagnostics
2018 .has_report_for_server_file(key, canonical_path)
2019 {
2020 self.diagnostics
2021 .mark_fresh_for_server_file(key, canonical_path);
2022 let authoritative = self
2023 .clients
2024 .get(key)
2025 .map_or(true, |client| !client.diagnostics_are_provisional());
2026 if authoritative {
2027 self.diagnostics
2028 .clear_provisional_for_server_file(key, canonical_path);
2029 }
2030 PullFileOutcome::Unchanged
2031 } else {
2032 PullFileOutcome::RequestFailed {
2033 reason: "no_cache_for_unchanged".to_string(),
2034 }
2035 }
2036 }
2037 }
2038 }
2039
2040 pub fn shutdown_all(&mut self) {
2042 for (key, mut client) in self.clients.drain() {
2043 if let Err(err) = client.shutdown() {
2044 slog_error!("error shutting down {:?}: {}", key, err);
2045 }
2046 }
2047 self.server_binaries.clear();
2048 self.documents.clear();
2049 self.diagnostics = DiagnosticsStore::new();
2050 }
2051
2052 pub fn has_active_servers(&self) -> bool {
2054 self.clients
2055 .values()
2056 .any(|client| client.state() == ServerState::Ready)
2057 }
2058
2059 pub fn active_server_keys(&self) -> Vec<ServerKey> {
2062 self.clients.keys().cloned().collect()
2063 }
2064
2065 #[cfg(windows)]
2069 #[doc(hidden)]
2070 pub fn watched_file_notification_trace_for_test(&self) -> &str {
2071 &self.last_watched_file_notification_trace
2072 }
2073
2074 pub fn get_diagnostics_for_file(&self, file: &Path) -> Vec<&StoredDiagnostic> {
2075 let normalized = normalize_lookup_path(file);
2076 self.diagnostics.for_file(&normalized)
2077 }
2078
2079 pub fn get_diagnostics_for_file_with_provisional(
2080 &self,
2081 file: &Path,
2082 ) -> Vec<(&StoredDiagnostic, bool)> {
2083 let normalized = normalize_lookup_path(file);
2084 self.diagnostics.for_file_with_provisional(&normalized)
2085 }
2086
2087 pub fn clear_failed_spawns(&mut self) -> usize {
2108 let n = self.failed_spawns.len();
2109 self.failed_spawns.clear();
2110 n
2111 }
2112
2113 #[cfg(test)]
2114 pub(crate) fn insert_failed_spawn_for_test(&mut self) {
2115 let key = ServerKey {
2116 kind: crate::lsp::registry::ServerKind::Rust,
2117 root: std::path::PathBuf::from("/tmp/test-root"),
2118 };
2119 self.failed_spawns.insert(
2120 key,
2121 ServerAttemptResult::SpawnFailed {
2122 binary: "rust-analyzer".to_string(),
2123 reason: "test".to_string(),
2124 },
2125 );
2126 }
2127
2128 pub fn clear_diagnostics_for_file(&mut self, file: &Path) -> bool {
2129 diagnostic_path_candidates(file)
2130 .into_iter()
2131 .fold(false, |removed, candidate| {
2132 removed | self.diagnostics.clear_for_file(&candidate)
2133 })
2134 }
2135
2136 pub fn mark_diagnostics_stale_for_file(&mut self, file: &Path) -> StaleDiagnosticsMark {
2140 let mut result = StaleDiagnosticsMark::default();
2141 for candidate in diagnostic_path_candidates(file) {
2142 let (had_entries, changed) = self.diagnostics.mark_stale_for_file(&candidate);
2143 result.had_entries |= had_entries;
2144 result.changed |= changed;
2145 }
2146 result
2147 }
2148
2149 pub fn get_diagnostics_for_directory(&self, dir: &Path) -> Vec<&StoredDiagnostic> {
2150 let normalized = normalize_lookup_path(dir);
2151 self.diagnostics.for_directory(&normalized)
2152 }
2153
2154 pub fn get_diagnostics_for_directory_with_provisional(
2155 &self,
2156 dir: &Path,
2157 ) -> Vec<(&StoredDiagnostic, bool)> {
2158 let normalized = normalize_lookup_path(dir);
2159 self.diagnostics.for_directory_with_provisional(&normalized)
2160 }
2161
2162 pub fn get_all_diagnostics(&self) -> Vec<&StoredDiagnostic> {
2163 self.diagnostics.all()
2164 }
2165
2166 pub fn get_all_diagnostics_with_provisional(&self) -> Vec<(&StoredDiagnostic, bool)> {
2167 self.diagnostics.all_with_provisional()
2168 }
2169
2170 pub fn has_any_diagnostic_reports(&self) -> bool {
2175 self.diagnostics.has_any_fresh_report()
2176 }
2177
2178 pub fn has_diagnostic_report_for_file(&self, file: &Path) -> bool {
2182 let normalized = normalize_lookup_path(file);
2183 self.diagnostics.has_any_fresh_report_for_file(&normalized)
2184 }
2185
2186 pub fn has_diagnostic_report_for_server_file(&self, server: &ServerKey, file: &Path) -> bool {
2190 let normalized = normalize_lookup_path(file);
2191 self.diagnostics
2192 .has_fresh_report_for_server_file(server, &normalized)
2193 }
2194
2195 pub fn has_authoritative_report_for_file(&self, file: &Path) -> bool {
2200 let normalized = normalize_lookup_path(file);
2201 self.diagnostics
2202 .has_authoritative_report_for_file(&normalized)
2203 }
2204
2205 pub fn has_authoritative_report_for_server(&self, server: &ServerKey) -> bool {
2210 self.diagnostics.has_authoritative_report_for_server(server)
2211 }
2212
2213 pub fn server_is_warming(&self, server: &ServerKey) -> bool {
2218 self.clients
2219 .get(server)
2220 .is_some_and(|client| client.diagnostics_are_provisional())
2221 }
2222
2223 fn drain_events_for_file(&mut self, file_path: &Path) -> bool {
2224 let mut saw_file_diagnostics = false;
2225 while let Ok(event) = self.event_rx.try_recv() {
2226 if matches!(
2227 self.handle_event(&event),
2228 Some(ref published_file) if published_file.as_path() == file_path
2229 ) {
2230 saw_file_diagnostics = true;
2231 }
2232 }
2233 saw_file_diagnostics
2234 }
2235
2236 fn accepted_live_publish_snapshot(
2237 &self,
2238 event: &LspEvent,
2239 ) -> Option<AcceptedDiagnosticSnapshot> {
2240 let LspEvent::Notification {
2241 server_kind,
2242 root,
2243 method,
2244 params: Some(params),
2245 } = event
2246 else {
2247 return None;
2248 };
2249 if method != "textDocument/publishDiagnostics" {
2250 return None;
2251 }
2252
2253 let publish_params =
2254 serde_json::from_value::<lsp_types::PublishDiagnosticsParams>(params.clone()).ok()?;
2255 let file = uri_to_path(&publish_params.uri)?;
2256 let server_key = ServerKey {
2257 kind: server_kind.clone(),
2258 root: root.clone(),
2259 };
2260 let client = self.clients.get(&server_key)?;
2261 if client.state() != ServerState::Ready || client.diagnostics_are_provisional() {
2262 return None;
2263 }
2264 let document_version = self.documents.get(&server_key)?.version(&file)?;
2265 let entry = self
2266 .diagnostics
2267 .entries_for_file(&file)
2268 .into_iter()
2269 .find_map(|(stored_key, entry)| (stored_key == &server_key).then_some(entry))?;
2270 if entry.stale || entry.provisional || entry.version != Some(document_version) {
2271 return None;
2272 }
2273
2274 Some(AcceptedDiagnosticSnapshot::new(
2275 server_key,
2276 document_version,
2277 entry.diagnostics.clone(),
2278 ))
2279 }
2280
2281 fn handle_event(&mut self, event: &LspEvent) -> Option<PathBuf> {
2282 let published_file = match event {
2283 LspEvent::Notification {
2284 server_kind,
2285 root,
2286 method,
2287 params: Some(params),
2288 } if method == "textDocument/publishDiagnostics" => {
2289 self.handle_publish_diagnostics(server_kind.clone(), root.clone(), params)
2290 }
2291 LspEvent::Notification {
2292 server_kind,
2293 root,
2294 method,
2295 params: Some(params),
2296 } if method == "experimental/serverStatus" => {
2297 self.handle_server_status(server_kind.clone(), root.clone(), params);
2298 None
2299 }
2300 LspEvent::ServerExited { server_kind, root } => {
2301 let key = ServerKey {
2302 kind: server_kind.clone(),
2303 root: root.clone(),
2304 };
2305 self.clients.remove(&key);
2306 self.server_binaries.remove(&key);
2307 self.documents.remove(&key);
2308 self.diagnostics.clear_for_server(&key);
2309 None
2310 }
2311 _ => None,
2312 };
2313 self.wake_post_edit_waiters();
2314 published_file
2315 }
2316
2317 fn wake_post_edit_waiters(&mut self) {
2318 Self::wake_waiters(&mut self.post_edit_waiters);
2319 }
2320
2321 fn wake_waiters(waiters: &mut HashMap<u64, Sender<()>>) {
2322 waiters.retain(|_, sender| match sender.try_send(()) {
2323 Ok(()) | Err(TrySendError::Full(())) => true,
2324 Err(TrySendError::Disconnected(())) => false,
2325 });
2326 }
2327
2328 fn handle_publish_diagnostics(
2329 &mut self,
2330 server: ServerKind,
2331 root: PathBuf,
2332 params: &serde_json::Value,
2333 ) -> Option<PathBuf> {
2334 if let Ok(publish_params) =
2335 serde_json::from_value::<lsp_types::PublishDiagnosticsParams>(params.clone())
2336 {
2337 let file = uri_to_path(&publish_params.uri)?;
2338 let stored = from_lsp_diagnostics(file.clone(), publish_params.diagnostics);
2339 let key = ServerKey { kind: server, root };
2344 let provisional = self
2345 .clients
2346 .get(&key)
2347 .is_some_and(|client| client.diagnostics_are_provisional());
2348 self.diagnostics.publish_full_with_provisional(
2349 key,
2350 file.clone(),
2351 stored,
2352 None,
2353 publish_params.version,
2354 provisional,
2355 );
2356 return Some(file);
2357 }
2358 None
2359 }
2360
2361 fn handle_server_status(
2362 &mut self,
2363 server: ServerKind,
2364 root: PathBuf,
2365 params: &serde_json::Value,
2366 ) {
2367 if !matches!(&server, ServerKind::Rust)
2368 || params.get("quiescent").and_then(serde_json::Value::as_bool) != Some(true)
2369 {
2370 return;
2371 }
2372
2373 let key = ServerKey { kind: server, root };
2374 let became_quiescent = self
2375 .clients
2376 .get_mut(&key)
2377 .is_some_and(|client| client.set_rust_analyzer_quiescent(true));
2378 if became_quiescent {
2379 self.diagnostics.promote_provisional_for_server(&key);
2380 }
2381 }
2382
2383 fn spawn_server(
2384 &self,
2385 def: &ServerDef,
2386 root: &Path,
2387 source_file: &Path,
2388 config: &Config,
2389 ) -> Result<LspClient, LspError> {
2390 let initialization_options =
2391 initialization_options_for_spawn(def, source_file, root, config)?;
2392 let binary = self.resolve_binary(def, config)?;
2393
2394 let mut merged_env = def.env.clone();
2398 for (key, value) in &self.extra_env {
2399 merged_env.insert(key.clone(), value.clone());
2400 }
2401
2402 let reclaim_root = config
2407 .project_root
2408 .as_deref()
2409 .map(crate::inspect::job::canonicalize_normalized)
2410 .filter(|project_root| root.starts_with(project_root))
2411 .unwrap_or_else(|| root.to_path_buf());
2412
2413 let mut client = LspClient::spawn_with_reclaim_root(
2414 def.kind.clone(),
2415 root.to_path_buf(),
2416 &binary,
2417 &def.args,
2418 &merged_env,
2419 self.event_tx.clone(),
2420 self.child_registry.clone(),
2421 Some(&reclaim_root),
2422 )?;
2423 if let Err(err) = client.initialize(root, initialization_options) {
2424 wait_for_stderr_tail(&mut client);
2425 let stderr_tail = client.stderr_tail();
2426 let reason = if client.child_exited() || !stderr_tail.is_empty() {
2427 format_initialize_failure_reason(&def.binary, &stderr_tail, &err)
2428 } else {
2429 format!("server failed during initialize: {err}")
2430 };
2431 return Err(LspError::ServerNotReady(reason));
2432 }
2433 Ok(client)
2434 }
2435
2436 fn resolve_binary(&self, def: &ServerDef, config: &Config) -> Result<PathBuf, LspError> {
2437 if let Some(path) = self.binary_overrides.get(&def.kind) {
2438 if path.exists() {
2439 return Ok(path.clone());
2440 }
2441 return Err(LspError::NotFound(format!(
2442 "override binary for {:?} not found: {}",
2443 def.kind,
2444 path.display()
2445 )));
2446 }
2447
2448 if let Some(path) = env_binary_override(&def.kind) {
2449 if path.exists() {
2450 return Ok(path);
2451 }
2452 return Err(LspError::NotFound(format!(
2453 "environment override binary for {:?} not found: {}",
2454 def.kind,
2455 path.display()
2456 )));
2457 }
2458
2459 resolve_lsp_binary(
2464 &def.binary,
2465 config.project_root.as_deref(),
2466 &config.lsp_paths_extra,
2467 )
2468 .ok_or_else(|| {
2469 LspError::NotFound(format!(
2470 "language server binary '{}' not found in node_modules/.bin, lsp_paths_extra, or PATH",
2471 def.binary
2472 ))
2473 })
2474 }
2475
2476 fn server_key_for_file(&self, file_path: &Path, config: &Config) -> Option<ServerKey> {
2477 for def in servers_for_file(file_path, config) {
2478 let key = server_key_for_definition(&def, file_path, config)?;
2479 if self.clients.contains_key(&key) {
2480 return Some(key);
2481 }
2482 }
2483 None
2484 }
2485}
2486
2487impl Default for LspManager {
2488 fn default() -> Self {
2489 Self::new()
2490 }
2491}
2492
2493const ASTRO_TSDK_UNAVAILABLE: &str = "astro-ls requires a project TypeScript install; none found";
2494
2495fn initialization_options_for_spawn(
2496 def: &ServerDef,
2497 source_file: &Path,
2498 server_root: &Path,
2499 config: &Config,
2500) -> Result<Option<serde_json::Value>, LspError> {
2501 if def.kind != ServerKind::Astro {
2502 return Ok(def.initialization_options.clone());
2503 }
2504
2505 if def
2506 .initialization_options
2507 .as_ref()
2508 .and_then(|options| options.pointer("/typescript/tsdk"))
2509 .and_then(serde_json::Value::as_str)
2510 .is_some_and(|tsdk| !tsdk.is_empty())
2511 {
2512 return Ok(def.initialization_options.clone());
2513 }
2514
2515 let project_root = config.project_root.as_deref().unwrap_or(server_root);
2519 let boundary = if source_file.starts_with(project_root) {
2520 project_root
2521 } else {
2522 server_root
2523 };
2524 let tsdk = find_project_typescript_sdk(source_file, boundary)
2525 .ok_or_else(|| LspError::ServerNotReady(ASTRO_TSDK_UNAVAILABLE.to_string()))?;
2526 let mut options = serde_json::json!({
2527 "typescript": {
2528 "tsdk": tsdk.to_string_lossy(),
2529 }
2530 });
2531 if let Some(configured) = def.initialization_options.clone() {
2532 merge_json_override(&mut options, configured);
2533 }
2534 Ok(Some(options))
2535}
2536
2537fn find_project_typescript_sdk(source_file: &Path, project_root: &Path) -> Option<PathBuf> {
2538 let mut directory = source_file.parent()?;
2539 loop {
2540 let lib = directory
2541 .join("node_modules")
2542 .join("typescript")
2543 .join("lib");
2544 if lib.join("tsserverlibrary.js").is_file() || lib.join("typescript.js").is_file() {
2545 return Some(lib);
2546 }
2547 if directory == project_root {
2548 return None;
2549 }
2550 let parent = directory.parent()?;
2551 if !parent.starts_with(project_root) {
2552 return None;
2553 }
2554 directory = parent;
2555 }
2556}
2557
2558fn merge_json_override(base: &mut serde_json::Value, override_value: serde_json::Value) {
2559 match (base, override_value) {
2560 (serde_json::Value::Object(base), serde_json::Value::Object(override_fields)) => {
2561 for (key, value) in override_fields {
2562 if let Some(existing) = base.get_mut(&key) {
2563 merge_json_override(existing, value);
2564 } else {
2565 base.insert(key, value);
2566 }
2567 }
2568 }
2569 (base, value) => *base = value,
2570 }
2571}
2572
2573fn wait_for_stderr_tail(client: &mut LspClient) {
2574 for _ in 0..10 {
2575 if !client.stderr_tail().is_empty() {
2576 break;
2577 }
2578 std::thread::sleep(std::time::Duration::from_millis(10));
2579 }
2580}
2581
2582fn recoverable_pull_rejection(err: &LspError) -> bool {
2583 matches!(
2584 err,
2585 LspError::ServerError {
2586 code: -32601 | -32602,
2587 ..
2588 }
2589 )
2590}
2591
2592fn server_attempt_result_reason(result: &ServerAttemptResult) -> String {
2593 match result {
2594 ServerAttemptResult::SpawnFailed { binary, reason } => {
2595 format!("spawn_failed: {binary} ({reason})")
2596 }
2597 ServerAttemptResult::BinaryNotInstalled { binary } => {
2598 format!("binary_not_installed: {binary}")
2599 }
2600 ServerAttemptResult::NoRootMarker { looked_for } => {
2601 format!("no_root_marker (looked for: {})", looked_for.join(", "))
2602 }
2603 ServerAttemptResult::Ok { .. } => "ok".to_string(),
2604 }
2605}
2606
2607fn format_stderr_tail_for_reason(stderr_tail: &str) -> String {
2608 truncate_stderr_tail_for_reason(stderr_tail)
2609 .lines()
2610 .map(|line| format!(" {line}"))
2611 .collect::<Vec<_>>()
2612 .join("\n")
2613}
2614
2615fn truncate_stderr_tail_for_reason(stderr_tail: &str) -> String {
2616 if stderr_tail.len() <= STDERR_REASON_BYTES {
2617 return stderr_tail.to_string();
2618 }
2619
2620 let ellipsis = "...";
2621 let target_len = STDERR_REASON_BYTES.saturating_sub(ellipsis.len());
2622 let mut start = stderr_tail.len() - target_len;
2623 while start < stderr_tail.len() && !stderr_tail.is_char_boundary(start) {
2624 start += 1;
2625 }
2626 format!("{ellipsis}{}", &stderr_tail[start..])
2627}
2628
2629fn format_initialize_failure_reason(binary: &str, stderr_tail: &str, err: &LspError) -> String {
2630 let mut reason = format!("server crashed during initialize: {err}");
2631 if !stderr_tail.is_empty() {
2632 reason.push_str("; stderr (last 64 lines):\n");
2633 reason.push_str(&format_stderr_tail_for_reason(stderr_tail));
2634 reason.push_str("\n\n");
2635 reason.push_str(&failure_hint(binary, stderr_tail));
2636 }
2637 reason
2638}
2639
2640fn format_post_initialize_exit_reason(
2641 binary: &str,
2642 status: std::process::ExitStatus,
2643 stderr_tail: &str,
2644 err: &LspError,
2645) -> String {
2646 let code = status
2647 .code()
2648 .map(|c| c.to_string())
2649 .unwrap_or_else(|| "signal/unknown".to_string());
2650 let mut reason = format!("server exited after initialize (code {code}): {err}");
2651 if !stderr_tail.is_empty() {
2652 reason.push_str("; stderr (last 64 lines):\n");
2653 reason.push_str(&format_stderr_tail_for_reason(stderr_tail));
2654 reason.push_str("\n\n");
2655 reason.push_str(&failure_hint(binary, stderr_tail));
2656 }
2657 reason
2658}
2659
2660fn failure_hint(binary: &str, stderr_tail: &str) -> String {
2661 if stderr_tail.contains("MODULE_NOT_FOUND") || stderr_tail.contains("Cannot find module") {
2662 let package_manager = infer_package_manager(stderr_tail);
2663 format!(
2664 "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."
2665 )
2666 } else if let Some(component) = rustup_missing_component(stderr_tail) {
2667 format!("'{component}' is a rustup proxy but the component is not installed. Install it: rustup component add {component}")
2672 } else {
2673 format!("Hint: see stderr above for '{binary}' failure details.")
2674 }
2675}
2676
2677fn rustup_missing_component(stderr_tail: &str) -> Option<String> {
2683 let marker = "Unknown binary '";
2684 let start = stderr_tail.find(marker)? + marker.len();
2685 let rest = &stderr_tail[start..];
2686 let end = rest.find('\'')?;
2687 let name = &rest[..end];
2688 if name.is_empty() || !stderr_tail.contains("toolchain") {
2691 return None;
2692 }
2693 Some(name.to_string())
2694}
2695
2696fn infer_package_manager(stderr_tail: &str) -> &'static str {
2697 let lower = stderr_tail.to_ascii_lowercase();
2698 if lower.contains(".pnpm/") || lower.contains(".pnpm\\") || lower.contains("/pnpm/") {
2699 "pnpm"
2700 } else if lower.contains(".yarn/")
2701 || lower.contains(".yarn\\")
2702 || lower.contains("/yarn/")
2703 || lower.contains("yarn")
2704 {
2705 "yarn"
2706 } else {
2707 "npm"
2708 }
2709}
2710
2711fn canonicalize_for_lsp(file_path: &Path) -> Result<PathBuf, LspError> {
2712 std::fs::canonicalize(file_path)
2719 .map(|canonical| crate::inspect::job::normalize_path(&canonical))
2720 .map_err(LspError::from)
2721}
2722
2723fn resolve_for_lsp_uri(file_path: &Path) -> PathBuf {
2724 if let Ok(path) = std::fs::canonicalize(file_path) {
2727 return crate::inspect::job::normalize_path(&path);
2728 }
2729
2730 let mut existing = file_path.to_path_buf();
2731 let mut missing = Vec::new();
2732 while !existing.exists() {
2733 let Some(name) = existing.file_name() else {
2734 break;
2735 };
2736 missing.push(name.to_owned());
2737 let Some(parent) = existing.parent() else {
2738 break;
2739 };
2740 existing = parent.to_path_buf();
2741 }
2742
2743 let mut resolved = std::fs::canonicalize(&existing)
2744 .map(|canonical| crate::inspect::job::normalize_path(&canonical))
2745 .unwrap_or(existing);
2746 for segment in missing.into_iter().rev() {
2747 resolved.push(segment);
2748 }
2749 resolved
2750}
2751
2752fn language_id_for_extension(ext: &str) -> &'static str {
2753 match ext {
2754 "ts" => "typescript",
2755 "tsx" => "typescriptreact",
2756 "js" | "mjs" | "cjs" => "javascript",
2757 "jsx" => "javascriptreact",
2758 "py" | "pyi" => "python",
2759 "rs" => "rust",
2760 "go" => "go",
2761 "html" | "htm" => "html",
2762 _ => "plaintext",
2763 }
2764}
2765
2766fn normalize_lookup_path(path: &Path) -> PathBuf {
2767 std::fs::canonicalize(path)
2771 .map(|canonical| crate::inspect::job::normalize_path(&canonical))
2772 .unwrap_or_else(|_| path.to_path_buf())
2773}
2774
2775fn diagnostic_path_candidates(file: &Path) -> Vec<PathBuf> {
2776 let mut candidates = Vec::with_capacity(4);
2777 let mut add = |candidate: PathBuf| {
2778 if !candidates.iter().any(|existing| existing == &candidate) {
2779 candidates.push(candidate);
2780 }
2781 };
2782
2783 add(file.to_path_buf());
2787 add(normalize_lookup_path(file));
2788
2789 if let (Some(parent), Some(name)) = (file.parent(), file.file_name()) {
2793 if let Ok(canonical_parent) = std::fs::canonicalize(parent) {
2794 let reconstructed = canonical_parent.join(name);
2795 add(reconstructed.clone());
2796 add(crate::inspect::job::normalize_path(&reconstructed));
2797 }
2798 }
2799
2800 candidates
2801}
2802
2803fn classify_spawn_error(binary: &str, err: &LspError) -> ServerAttemptResult {
2810 match err {
2811 LspError::NotFound(_) => ServerAttemptResult::BinaryNotInstalled {
2816 binary: binary.to_string(),
2817 },
2818 other => ServerAttemptResult::SpawnFailed {
2819 binary: binary.to_string(),
2820 reason: other.to_string(),
2821 },
2822 }
2823}
2824
2825fn env_binary_override(kind: &ServerKind) -> Option<PathBuf> {
2826 let id = kind.id_str();
2827 let suffix: String = id
2828 .chars()
2829 .map(|ch| {
2830 if ch.is_ascii_alphanumeric() {
2831 ch.to_ascii_uppercase()
2832 } else {
2833 '_'
2834 }
2835 })
2836 .collect();
2837 let key = format!("AFT_LSP_{suffix}_BINARY");
2838 std::env::var_os(key).map(PathBuf::from)
2839}
2840
2841#[cfg(all(test, windows))]
2842mod windows_server_key_tests {
2843 use std::fs;
2844 use std::os::windows::ffi::OsStrExt;
2845
2846 use super::{canonicalize_for_lsp, server_key_for_definition};
2847 use crate::config::{Config, UserServerDef};
2848 use crate::lsp::registry::servers_for_file;
2849
2850 #[test]
2851 fn normalized_and_verbatim_inputs_produce_identical_server_key_material() {
2852 let temp_dir = tempfile::tempdir().expect("tempdir");
2853 let root = temp_dir.path().join("workspace");
2854 let source = root.join("src").join("main.customts");
2855 fs::create_dir_all(source.parent().expect("source parent")).expect("create source dir");
2856 fs::write(root.join("custom-root.json"), "{}\n").expect("write root marker");
2857 fs::write(&source, "export const value = 1;\n").expect("write source");
2858
2859 let config = Config {
2860 project_root: Some(root),
2861 lsp_servers: vec![UserServerDef {
2862 id: "custom-ts".to_string(),
2863 extensions: vec!["customts".to_string()],
2864 binary: "custom-ts-lsp".to_string(),
2865 args: Vec::new(),
2866 root_markers: vec!["custom-root.json".to_string()],
2867 env: Default::default(),
2868 initialization_options: None,
2869 disabled: false,
2870 }],
2871 ..Config::default()
2872 };
2873
2874 let normalized_input = canonicalize_for_lsp(&source).expect("normalized source path");
2875 let bare_canonical_input = fs::canonicalize(&source).expect("canonical source path");
2876 let key_for = |path: &std::path::Path| {
2877 let def = servers_for_file(path, &config)
2878 .into_iter()
2879 .find(|def| def.kind.id_str() == "custom-ts")
2880 .expect("custom server definition");
2881 server_key_for_definition(&def, path, &config).expect("custom server root")
2882 };
2883
2884 let key_material = |key: &crate::lsp::roots::ServerKey| {
2885 let root_bytes = key
2886 .root
2887 .as_os_str()
2888 .encode_wide()
2889 .flat_map(u16::to_le_bytes)
2890 .collect::<Vec<_>>();
2891 (key.kind.id_str().to_string(), root_bytes)
2892 };
2893 let ensure_key = key_for(&normalized_input);
2894 let running_lookup_key = key_for(&bare_canonical_input);
2895
2896 assert_eq!(key_material(&ensure_key), key_material(&running_lookup_key));
2897 }
2898}
2899
2900#[cfg(test)]
2901mod failure_hint_tests {
2902 use super::{failure_hint, rustup_missing_component};
2903
2904 #[test]
2905 fn detects_rustup_proxy_without_component() {
2906 let stderr = "error: Unknown binary 'rust-analyzer' in official toolchain 'stable-aarch64-apple-darwin'.";
2908 assert_eq!(
2909 rustup_missing_component(stderr).as_deref(),
2910 Some("rust-analyzer")
2911 );
2912 let hint = failure_hint("rust-analyzer", stderr);
2913 assert!(
2914 hint.contains("rustup component add rust-analyzer"),
2915 "expected actionable rustup hint, got: {hint}"
2916 );
2917 }
2918
2919 #[test]
2920 fn ignores_unknown_binary_without_toolchain_phrasing() {
2921 let stderr = "fatal: Unknown binary 'foo' was requested by the linker.";
2924 assert_eq!(rustup_missing_component(stderr), None);
2925 assert!(failure_hint("foo", stderr).starts_with("Hint: see stderr"));
2926 }
2927
2928 #[test]
2929 fn npm_module_not_found_still_wins() {
2930 let stderr = "Error: Cannot find module '/x/typescript-language-server/lib/cli.mjs'";
2932 let hint = failure_hint("typescript-language-server", stderr);
2933 assert!(hint.contains("install -g"), "got: {hint}");
2934 }
2935}
2936
2937#[cfg(test)]
2938mod diagnostic_capacity_tests {
2939 use std::fs;
2940
2941 use super::LspManager;
2942 use crate::config::Config;
2943
2944 #[test]
2949 fn set_diagnostic_capacity_propagates_to_store() {
2950 let mut manager = LspManager::new();
2951 manager.set_diagnostic_capacity(7);
2952 assert_eq!(manager.diagnostics_store_for_test().capacity_for_test(), 7);
2953 manager.set_diagnostic_capacity(0); assert_eq!(manager.diagnostics_store_for_test().capacity_for_test(), 0);
2955 }
2956
2957 #[test]
2960 fn clear_failed_spawns_empties_the_cache() {
2961 let mut manager = LspManager::new();
2962 assert_eq!(manager.clear_failed_spawns(), 0);
2963 manager.insert_failed_spawn_for_test();
2964 assert_eq!(manager.clear_failed_spawns(), 1);
2965 assert_eq!(manager.clear_failed_spawns(), 0);
2966 }
2967
2968 #[test]
2969 fn post_write_notification_does_not_start_a_cold_server() {
2970 let dir = tempfile::tempdir().unwrap();
2971 let file = dir.path().join("main.ts");
2972 fs::write(dir.path().join("package.json"), "{}").unwrap();
2973 fs::write(&file, "export const value = 1;\n").unwrap();
2974
2975 let mut manager = LspManager::new();
2976 manager
2977 .notify_file_changed_if_running(&file, "export const value = 1;\n", &Config::default())
2978 .unwrap();
2979 assert!(manager.clients.is_empty());
2980 }
2981}
2982
2983#[cfg(test)]
2984mod post_edit_waiter_tests {
2985 use std::collections::HashMap;
2986 use std::path::PathBuf;
2987 use std::time::{Duration, Instant};
2988
2989 use super::LspManager;
2990 use crate::lsp::client::LspEvent;
2991 use crate::lsp::registry::ServerKind;
2992
2993 #[test]
2994 fn draining_an_event_wakes_registered_post_edit_waiter() {
2995 let mut manager = LspManager::new();
2996 let mut wait = manager.start_post_edit_diagnostics_wait(
2997 PathBuf::from("/workspace/src/main.rs").as_path(),
2998 &[],
2999 &HashMap::new(),
3000 Duration::from_secs(2),
3001 );
3002 manager.enqueue_event_for_test(LspEvent::Notification {
3003 server_kind: ServerKind::Rust,
3004 root: PathBuf::from("/workspace"),
3005 method: "custom/drainedElsewhere".to_string(),
3006 params: None,
3007 });
3008
3009 assert_eq!(manager.drain_events().events.len(), 1);
3010 let started = Instant::now();
3011 assert!(wait.next_event().is_none());
3012 assert!(
3013 started.elapsed() < Duration::from_millis(250),
3014 "a competing drain did not wake the parked post-edit waiter"
3015 );
3016 let _ = manager.poll_post_edit_diagnostics_wait(&mut wait, None);
3017 let _ = manager.finish_post_edit_diagnostics_wait(wait);
3018 }
3019}
3020
3021#[cfg(test)]
3022mod clear_diagnostics_tests {
3023 use std::path::PathBuf;
3024
3025 use super::LspManager;
3026 use crate::lsp::client::LspEvent;
3027 use crate::lsp::diagnostics::{DiagnosticSeverity, StoredDiagnostic};
3028 use crate::lsp::position::uri_for_path;
3029 use crate::lsp::registry::ServerKind;
3030 use crate::lsp::roots::ServerKey;
3031
3032 fn err_diag(file: &PathBuf) -> StoredDiagnostic {
3033 StoredDiagnostic {
3034 file: file.clone(),
3035 line: 1,
3036 column: 1,
3037 end_line: 1,
3038 end_column: 2,
3039 severity: DiagnosticSeverity::Error,
3040 message: "boom".into(),
3041 code: None,
3042 source: None,
3043 }
3044 }
3045
3046 #[test]
3051 fn clear_diagnostics_for_deleted_file_matches_canonical_key() {
3052 let dir = tempfile::tempdir().unwrap();
3053 let canonical_dir = std::fs::canonicalize(dir.path()).unwrap();
3055 let canonical_file = canonical_dir.join("gone.ts");
3056 std::fs::write(&canonical_file, "x").unwrap();
3059
3060 let mut manager = LspManager::new();
3061 let key = ServerKey {
3062 kind: ServerKind::TypeScript,
3063 root: canonical_dir.clone(),
3064 };
3065 manager.diagnostics_store_mut_for_test().publish(
3066 key,
3067 canonical_file.clone(),
3068 vec![err_diag(&canonical_file)],
3069 );
3070 assert_eq!(manager.warm_error_warning_counts(), (1, 0));
3071
3072 std::fs::remove_file(&canonical_file).unwrap();
3073
3074 let watcher_path = dir.path().join("gone.ts");
3077 let removed = manager.clear_diagnostics_for_file(&watcher_path);
3078
3079 assert!(removed, "expected the deleted file's diagnostic to clear");
3080 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
3081 }
3082
3083 #[cfg(windows)]
3084 #[test]
3085 fn clear_diagnostics_for_deleted_file_matches_normalized_publish_key() {
3086 let dir = tempfile::tempdir().unwrap();
3087 let file = dir.path().join("normalized-gone.ts");
3088 std::fs::write(&file, "x").unwrap();
3089 let normalized_file = crate::inspect::job::canonicalize_normalized(&file);
3090
3091 let mut manager = LspManager::new();
3092 let key = ServerKey {
3093 kind: ServerKind::TypeScript,
3094 root: normalized_file.parent().unwrap().to_path_buf(),
3095 };
3096 manager.diagnostics_store_mut_for_test().publish(
3097 key,
3098 normalized_file.clone(),
3099 vec![err_diag(&normalized_file)],
3100 );
3101 std::fs::remove_file(&file).unwrap();
3102
3103 assert!(manager.clear_diagnostics_for_file(&file));
3104 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
3105 }
3106
3107 #[cfg(windows)]
3108 #[test]
3109 fn stale_diagnostics_for_deleted_file_matches_normalized_publish_key() {
3110 let dir = tempfile::tempdir().unwrap();
3111 let file = dir.path().join("normalized-stale.ts");
3112 std::fs::write(&file, "x").unwrap();
3113 let normalized_file = crate::inspect::job::canonicalize_normalized(&file);
3114
3115 let mut manager = LspManager::new();
3116 let key = ServerKey {
3117 kind: ServerKind::TypeScript,
3118 root: normalized_file.parent().unwrap().to_path_buf(),
3119 };
3120 manager.diagnostics_store_mut_for_test().publish(
3121 key,
3122 normalized_file.clone(),
3123 vec![err_diag(&normalized_file)],
3124 );
3125 std::fs::remove_file(&file).unwrap();
3126
3127 let result = manager.mark_diagnostics_stale_for_file(&file);
3128 assert!(result.had_entries);
3129 assert!(result.changed);
3130 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
3131 }
3132
3133 #[test]
3134 fn clear_diagnostics_for_unknown_file_is_noop() {
3135 let mut manager = LspManager::new();
3136 assert!(!manager.clear_diagnostics_for_file(&PathBuf::from("/nope/missing.ts")));
3137 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
3138 }
3139
3140 #[test]
3141 fn drain_events_reports_publish_diagnostics_updates() {
3142 let dir = tempfile::tempdir().unwrap();
3143 let root = std::fs::canonicalize(dir.path()).unwrap();
3144 let file = root.join("main.ts");
3145 std::fs::write(&file, "const x: number = 'nope';").unwrap();
3146
3147 let mut manager = LspManager::new();
3148 let diagnostic = lsp_types::Diagnostic {
3149 range: lsp_types::Range {
3150 start: lsp_types::Position {
3151 line: 0,
3152 character: 0,
3153 },
3154 end: lsp_types::Position {
3155 line: 0,
3156 character: 1,
3157 },
3158 },
3159 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
3160 code: None,
3161 code_description: None,
3162 source: Some("test".into()),
3163 message: "boom".into(),
3164 related_information: None,
3165 tags: None,
3166 data: None,
3167 };
3168 let params = serde_json::to_value(lsp_types::PublishDiagnosticsParams {
3169 uri: uri_for_path(&file).unwrap(),
3170 diagnostics: vec![diagnostic],
3171 version: Some(1),
3172 })
3173 .unwrap();
3174 manager
3175 .event_tx
3176 .send(LspEvent::Notification {
3177 server_kind: ServerKind::TypeScript,
3178 root,
3179 method: "textDocument/publishDiagnostics".into(),
3180 params: Some(params),
3181 })
3182 .unwrap();
3183
3184 let drained = manager.drain_events();
3185
3186 assert!(drained.diagnostics_changed);
3187 assert_eq!(drained.events.len(), 1);
3188 assert_eq!(manager.warm_error_warning_counts(), (1, 0));
3189 }
3190}
3191
3192#[cfg(test)]
3193mod inspect_path_tests {
3194 use super::LspManager;
3195 use crate::config::{Config, UserServerDef};
3196 use crate::lsp::registry::ServerKind;
3197
3198 #[test]
3199 fn applicability_resolution_does_not_start_or_open_a_server() {
3200 let temp_dir = tempfile::tempdir().expect("tempdir");
3201 let root = temp_dir.path().join("project");
3202 std::fs::create_dir_all(&root).expect("project root");
3203 std::fs::write(root.join("inspect-root.json"), "{}\n").expect("root marker");
3204 std::fs::write(root.join("input.inspectlang"), "value\n").expect("source file");
3205
3206 let config = Config {
3207 project_root: Some(root.clone()),
3208 lsp_servers: vec![UserServerDef {
3209 id: "inspect-test".to_string(),
3210 extensions: vec!["inspectlang".to_string()],
3211 binary: "inspect-test-lsp".to_string(),
3212 args: Vec::new(),
3213 root_markers: vec!["inspect-root.json".to_string()],
3214 env: Default::default(),
3215 initialization_options: None,
3216 disabled: false,
3217 }],
3218 ..Config::default()
3219 };
3220 let mut manager = LspManager::new();
3221 manager.override_binary(
3222 ServerKind::Custom("inspect-test".into()),
3223 std::env::current_exe().expect("current executable"),
3224 );
3225
3226 let snapshot = manager
3227 .resolve_applicable_servers_for_root(&root, &config)
3228 .expect("resolution succeeds without spawning");
3229 assert_eq!(snapshot.server_keys.len(), 1);
3230 assert_eq!(snapshot.server_keys[0].kind.id_str(), "inspect-test");
3231 assert_eq!(manager.server_count(), 0);
3232 assert!(!manager.document_is_open_for_test(&root.join("input.inspectlang")));
3233 }
3234
3235 #[test]
3236 fn applicability_resolution_preserves_an_empty_snapshot() {
3237 let temp_dir = tempfile::tempdir().expect("tempdir");
3238 let root = temp_dir.path().join("project");
3239 std::fs::create_dir_all(&root).expect("project root");
3240 std::fs::write(root.join("notes.txt"), "plain text\n").expect("fixture file");
3241
3242 let snapshot = LspManager::new()
3243 .resolve_applicable_servers_for_root(&root, &Config::default())
3244 .expect("an empty applicability set is valid");
3245
3246 assert!(snapshot.server_keys.is_empty());
3247 assert!(snapshot.candidates.is_empty());
3248 }
3249}