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_server_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, &key.root, 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 navigation_requires_deferred_execution(
671 &self,
672 file_path: &Path,
673 config: &Config,
674 ) -> bool {
675 let Ok(canonical_path) = canonicalize_for_lsp(file_path) else {
676 return false;
677 };
678 servers_for_file(&canonical_path, config)
679 .into_iter()
680 .filter_map(|definition| {
681 let key = server_key_for_definition(&definition, &canonical_path, config)?;
682 Some((definition, key))
683 })
684 .any(|(definition, key)| {
685 if let Some(client) = self.clients.get(&key) {
686 return client.state() != ServerState::Ready;
687 }
688 !self.failed_spawns.contains_key(&key)
689 && self.resolve_binary(&definition, &key.root, config).is_ok()
690 })
691 }
692
693 pub fn ensure_server_for_file_detailed(
701 &mut self,
702 file_path: &Path,
703 config: &Config,
704 ) -> EnsureServerOutcomes {
705 let defs = servers_for_file(file_path, config);
706 let mut outcomes = EnsureServerOutcomes::default();
707
708 for def in defs {
709 let server_id = def.kind.id_str().to_string();
710 let server_name = def.name.to_string();
711
712 let Some(key) = server_key_for_definition(&def, file_path, config) else {
713 outcomes.attempts.push(ServerAttempt {
714 server_id,
715 server_name,
716 result: ServerAttemptResult::NoRootMarker {
717 looked_for: def.root_markers.iter().map(|s| s.to_string()).collect(),
718 },
719 });
720 continue;
721 };
722
723 if !self.clients.contains_key(&key) {
724 if let Some(cached) = self.failed_spawns.get(&key) {
731 outcomes.attempts.push(ServerAttempt {
732 server_id,
733 server_name,
734 result: cached.clone(),
735 });
736 continue;
737 }
738
739 match self.spawn_server(&def, &key.root, file_path, config) {
740 Ok(client) => {
741 self.clients.insert(key.clone(), client);
742 self.server_binaries.insert(key.clone(), def.binary.clone());
743 self.documents.entry(key.clone()).or_default();
744 }
745 Err(err) => {
746 slog_error!("failed to spawn {}: {}", def.name, err);
747 let result = classify_spawn_error(&def.binary, &err);
748 self.failed_spawns.insert(key.clone(), result.clone());
752 outcomes.attempts.push(ServerAttempt {
753 server_id,
754 server_name,
755 result,
756 });
757 continue;
758 }
759 }
760 }
761
762 outcomes.attempts.push(ServerAttempt {
763 server_id,
764 server_name,
765 result: ServerAttemptResult::Ok {
766 server_key: key.clone(),
767 },
768 });
769 outcomes.successful.push(key);
770 }
771
772 outcomes
773 }
774
775 pub fn ensure_server_for_file_default(&mut self, file_path: &Path) -> Vec<ServerKey> {
778 self.ensure_server_for_file(file_path, &Config::default())
779 }
780 pub fn ensure_file_open(
785 &mut self,
786 file_path: &Path,
787 config: &Config,
788 ) -> Result<EnsureFileOpenResult, LspError> {
789 let canonical_path = canonicalize_for_lsp(file_path)?;
790 let server_keys = self.ensure_server_for_file(&canonical_path, config);
791 if server_keys.is_empty() {
792 return Ok(EnsureFileOpenResult::default());
793 }
794
795 let uri = uri_for_path(&canonical_path)?;
796 let language_id = language_id_for_extension(
797 canonical_path
798 .extension()
799 .and_then(|ext| ext.to_str())
800 .unwrap_or_default(),
801 )
802 .to_string();
803 let needs_content = server_keys.iter().any(|key| {
804 !self
805 .documents
806 .get(key)
807 .is_some_and(|store| store.is_open(&canonical_path))
808 });
809 let initial_content = needs_content
810 .then(|| std::fs::read_to_string(&canonical_path).map_err(LspError::Io))
811 .transpose()?;
812 let mut newly_opened = Vec::new();
813
814 for key in &server_keys {
815 let already_open = self
816 .documents
817 .get(key)
818 .is_some_and(|store| store.is_open(&canonical_path));
819
820 if !already_open {
821 let content = initial_content
822 .as_ref()
823 .expect("content is loaded when any server needs didOpen");
824 let send_result = if let Some(client) = self.clients.get_mut(key) {
825 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
826 text_document: TextDocumentItem::new(
827 uri.clone(),
828 language_id.clone(),
829 0,
830 content.clone(),
831 ),
832 })
833 } else {
834 Ok(())
835 };
836 if let Err(err) = send_result {
837 let _ = self.close_file_for_servers(&canonical_path, &newly_opened);
838 return Err(err);
839 }
840 self.documents
841 .entry(key.clone())
842 .or_default()
843 .open(canonical_path.clone());
844 newly_opened.push(key.clone());
845 continue;
846 }
847
848 let drifted = self
858 .documents
859 .get(key)
860 .is_some_and(|store| store.is_stale_on_disk(&canonical_path));
861 if drifted {
862 let content = match std::fs::read_to_string(&canonical_path) {
863 Ok(content) => content,
864 Err(err) => {
865 let _ = self.close_file_for_servers(&canonical_path, &newly_opened);
866 return Err(LspError::Io(err));
867 }
868 };
869 let next_version = self
870 .documents
871 .get(key)
872 .and_then(|store| store.version(&canonical_path))
873 .map(|v| v + 1)
874 .unwrap_or(1);
875 let send_result = if let Some(client) = self.clients.get_mut(key) {
876 client.send_notification::<DidChangeTextDocument>(DidChangeTextDocumentParams {
877 text_document: VersionedTextDocumentIdentifier::new(
878 uri.clone(),
879 next_version,
880 ),
881 content_changes: vec![TextDocumentContentChangeEvent {
882 range: None,
883 range_length: None,
884 text: content,
885 }],
886 })
887 } else {
888 Ok(())
889 };
890 if let Err(err) = send_result {
891 let _ = self.close_file_for_servers(&canonical_path, &newly_opened);
892 return Err(err);
893 }
894 if let Some(store) = self.documents.get_mut(key) {
895 store.bump_version(&canonical_path);
896 }
897 }
898 }
899
900 Ok(EnsureFileOpenResult {
901 server_keys,
902 newly_opened,
903 })
904 }
905
906 pub fn ensure_file_open_default(
907 &mut self,
908 file_path: &Path,
909 ) -> Result<EnsureFileOpenResult, LspError> {
910 self.ensure_file_open(file_path, &Config::default())
911 }
912
913 pub fn notify_file_changed(
919 &mut self,
920 file_path: &Path,
921 content: &str,
922 config: &Config,
923 ) -> Result<(), LspError> {
924 self.notify_file_changed_versioned(file_path, content, config)
925 .map(|_| ())
926 }
927
928 pub fn notify_file_changed_versioned(
939 &mut self,
940 file_path: &Path,
941 content: &str,
942 config: &Config,
943 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
944 let canonical_path = canonicalize_for_lsp(file_path)?;
945 let server_keys = self.ensure_server_for_file(&canonical_path, config);
946 self.notify_file_changed_for_server_keys(canonical_path, content, server_keys)
947 }
948
949 pub fn notify_file_changed_if_running(
955 &mut self,
956 file_path: &Path,
957 content: &str,
958 config: &Config,
959 ) -> Result<(), LspError> {
960 self.notify_file_changed_if_running_versioned(file_path, content, config)
961 .map(|_| ())
962 }
963
964 pub fn notify_file_changed_if_running_versioned(
968 &mut self,
969 file_path: &Path,
970 content: &str,
971 config: &Config,
972 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
973 let canonical_path = canonicalize_for_lsp(file_path)?;
974 let server_keys = self.running_server_keys_for_file(&canonical_path, config);
975 self.notify_file_changed_for_server_keys(canonical_path, content, server_keys)
976 }
977
978 fn notify_file_changed_for_server_keys(
979 &mut self,
980 canonical_path: PathBuf,
981 content: &str,
982 server_keys: Vec<ServerKey>,
983 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
984 if server_keys.is_empty() {
985 return Ok(Vec::new());
986 }
987
988 let uri = uri_for_path(&canonical_path)?;
989 let language_id = language_id_for_extension(
990 canonical_path
991 .extension()
992 .and_then(|ext| ext.to_str())
993 .unwrap_or_default(),
994 )
995 .to_string();
996
997 let mut versions: Vec<(ServerKey, i32)> = Vec::with_capacity(server_keys.len());
998
999 for key in server_keys {
1000 let current_version = self
1001 .documents
1002 .get(&key)
1003 .and_then(|store| store.version(&canonical_path));
1004
1005 if let Some(version) = current_version {
1006 let next_version = version + 1;
1007 if let Some(client) = self.clients.get_mut(&key) {
1008 client.send_notification::<DidChangeTextDocument>(
1009 DidChangeTextDocumentParams {
1010 text_document: VersionedTextDocumentIdentifier::new(
1011 uri.clone(),
1012 next_version,
1013 ),
1014 content_changes: vec![TextDocumentContentChangeEvent {
1015 range: None,
1016 range_length: None,
1017 text: content.to_string(),
1018 }],
1019 },
1020 )?;
1021 }
1022 if let Some(store) = self.documents.get_mut(&key) {
1023 store.bump_version(&canonical_path);
1024 }
1025 versions.push((key, next_version));
1026 continue;
1027 }
1028
1029 if let Some(client) = self.clients.get_mut(&key) {
1030 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
1031 text_document: TextDocumentItem::new(
1032 uri.clone(),
1033 language_id.clone(),
1034 0,
1035 content.to_string(),
1036 ),
1037 })?;
1038 }
1039 self.documents
1040 .entry(key.clone())
1041 .or_default()
1042 .open(canonical_path.clone());
1043 versions.push((key, 0));
1046 }
1047
1048 Ok(versions)
1049 }
1050
1051 pub fn notify_file_changed_default(
1052 &mut self,
1053 file_path: &Path,
1054 content: &str,
1055 ) -> Result<(), LspError> {
1056 self.notify_file_changed(file_path, content, &Config::default())
1057 }
1058
1059 pub fn notify_files_watched_changed(
1065 &mut self,
1066 paths: &[(PathBuf, FileChangeType)],
1067 _config: &Config,
1068 ) -> Result<(), LspError> {
1069 #[cfg(windows)]
1070 let mut trace = vec![format!(
1071 "input_paths={paths:?}; active_keys={:?}",
1072 self.clients.keys().collect::<Vec<_>>()
1073 )];
1074
1075 if paths.is_empty() {
1076 #[cfg(windows)]
1077 {
1078 trace.push("outcome=no-input-paths".to_string());
1079 self.last_watched_file_notification_trace = trace.join("\n");
1080 }
1081 return Ok(());
1082 }
1083
1084 let mut canonical_events = Vec::with_capacity(paths.len());
1085 for (path, typ) in paths {
1086 let canonical_path = resolve_for_lsp_uri(path);
1087 canonical_events.push((canonical_path, *typ));
1088 }
1089 #[cfg(windows)]
1090 trace.push(format!("resolved_events={canonical_events:?}"));
1091
1092 let keys: Vec<ServerKey> = self.clients.keys().cloned().collect();
1093 #[cfg(windows)]
1094 if keys.is_empty() {
1095 trace.push("outcome=no-active-client".to_string());
1096 }
1097 for key in keys {
1098 let mut changes = Vec::new();
1099 for (path, typ) in &canonical_events {
1100 if !path.starts_with(&key.root) {
1101 continue;
1102 }
1103 changes.push(FileEvent::new(uri_for_path(path)?, *typ));
1104 }
1105
1106 if changes.is_empty() {
1107 #[cfg(windows)]
1108 trace.push(format!("key={key:?}; outcome=outside-root"));
1109 continue;
1110 }
1111
1112 if let Some(client) = self.clients.get_mut(&key) {
1113 let supports_static_watched_files = client.supports_watched_files();
1119 let has_dynamic_registration = client.has_watched_file_registration();
1120 if !(supports_static_watched_files || has_dynamic_registration) {
1121 #[cfg(windows)]
1122 trace.push(format!(
1123 "key={key:?}; changes={changes:?}; outcome=unsupported; static={supports_static_watched_files}; dynamic={has_dynamic_registration}"
1124 ));
1125 if self.watched_file_skip_logged.insert(key.clone()) {
1126 log::debug!(
1127 "skipping didChangeWatchedFiles for {:?} (not supported or registered)",
1128 key
1129 );
1130 }
1131 continue;
1132 }
1133 #[cfg(windows)]
1134 trace.push(format!("key={key:?}; changes={changes:?}; action=send"));
1135 let send_result = client.send_notification::<DidChangeWatchedFiles>(
1136 DidChangeWatchedFilesParams { changes },
1137 );
1138 #[cfg(windows)]
1139 trace.push(format!(
1140 "key={key:?}; outcome={}",
1141 if send_result.is_ok() {
1142 "sent"
1143 } else {
1144 "send-error"
1145 }
1146 ));
1147 if let Err(error) = send_result {
1148 #[cfg(windows)]
1149 {
1150 self.last_watched_file_notification_trace = trace.join("\n");
1151 }
1152 return Err(error);
1153 }
1154 }
1155 }
1156
1157 #[cfg(windows)]
1158 {
1159 self.last_watched_file_notification_trace = trace.join("\n");
1160 }
1161 Ok(())
1162 }
1163
1164 pub fn notify_file_closed(&mut self, file_path: &Path) -> Result<(), LspError> {
1166 let canonical_path = canonicalize_for_lsp(file_path)?;
1167 let keys = self
1168 .documents
1169 .iter()
1170 .filter(|(_, store)| store.is_open(&canonical_path))
1171 .map(|(key, _)| key.clone())
1172 .collect::<Vec<_>>();
1173 self.close_file_for_servers(&canonical_path, &keys)
1174 }
1175
1176 pub(crate) fn close_file_for_servers(
1181 &mut self,
1182 file_path: &Path,
1183 server_keys: &[ServerKey],
1184 ) -> Result<(), LspError> {
1185 let canonical_path = canonicalize_for_lsp(file_path)?;
1186 let uri = uri_for_path(&canonical_path)?;
1187 let mut first_error = None;
1188
1189 for key in server_keys {
1190 let was_open = self
1191 .documents
1192 .get(key)
1193 .is_some_and(|store| store.is_open(&canonical_path));
1194 if !was_open {
1195 continue;
1196 }
1197
1198 if let Some(client) = self.clients.get_mut(key) {
1199 if let Err(err) =
1200 client.send_notification::<DidCloseTextDocument>(DidCloseTextDocumentParams {
1201 text_document: TextDocumentIdentifier::new(uri.clone()),
1202 })
1203 {
1204 if first_error.is_none() {
1205 first_error = Some(err);
1206 }
1207 }
1208 }
1209
1210 if let Some(store) = self.documents.get_mut(key) {
1211 store.close(&canonical_path);
1212 }
1213 self.diagnostics.clear_for_server_file(key, &canonical_path);
1214 }
1215
1216 match first_error {
1217 Some(err) => Err(err),
1218 None => Ok(()),
1219 }
1220 }
1221
1222 pub fn client_for_file(&self, file_path: &Path, config: &Config) -> Option<&LspClient> {
1224 let key = self.server_key_for_file(file_path, config)?;
1225 self.clients.get(&key)
1226 }
1227
1228 pub fn client_for_file_default(&self, file_path: &Path) -> Option<&LspClient> {
1229 self.client_for_file(file_path, &Config::default())
1230 }
1231
1232 pub fn client_for_file_mut(
1234 &mut self,
1235 file_path: &Path,
1236 config: &Config,
1237 ) -> Option<&mut LspClient> {
1238 let key = self.server_key_for_file(file_path, config)?;
1239 self.clients.get_mut(&key)
1240 }
1241
1242 pub fn client_for_file_mut_default(&mut self, file_path: &Path) -> Option<&mut LspClient> {
1243 self.client_for_file_mut(file_path, &Config::default())
1244 }
1245
1246 pub fn active_client_count(&self) -> usize {
1248 self.clients.len()
1249 }
1250
1251 pub fn drain_events(&mut self) -> DrainedLspEvents {
1253 self.drain_events_bounded(usize::MAX)
1254 }
1255
1256 pub fn has_pending_events(&self) -> bool {
1259 !self.event_rx.is_empty()
1260 }
1261
1262 pub fn drain_events_bounded(&mut self, max_events: usize) -> DrainedLspEvents {
1263 let mut events = Vec::new();
1264 let mut diagnostics_changed = false;
1265 let mut accepted_snapshots = Vec::new();
1266 while events.len() < max_events {
1267 let Ok(event) = self.event_rx.try_recv() else {
1268 break;
1269 };
1270 if self.handle_event(&event).is_some() {
1271 diagnostics_changed = true;
1272 }
1273 if let Some(snapshot) = self.accepted_live_publish_snapshot(&event) {
1274 accepted_snapshots.push(snapshot);
1275 }
1276 events.push(event);
1277 }
1278 let has_more = events.len() >= max_events && !self.event_rx.is_empty();
1279 DrainedLspEvents {
1280 events,
1281 diagnostics_changed,
1282 accepted_snapshots,
1283 has_more,
1284 }
1285 }
1286
1287 pub fn wait_for_diagnostics(
1289 &mut self,
1290 file_path: &Path,
1291 config: &Config,
1292 timeout: std::time::Duration,
1293 ) -> Vec<StoredDiagnostic> {
1294 let deadline = std::time::Instant::now() + timeout;
1295 self.wait_for_file_diagnostics(file_path, config, deadline)
1296 }
1297
1298 pub fn wait_for_diagnostics_default(
1299 &mut self,
1300 file_path: &Path,
1301 timeout: std::time::Duration,
1302 ) -> Vec<StoredDiagnostic> {
1303 self.wait_for_diagnostics(file_path, &Config::default(), timeout)
1304 }
1305
1306 #[doc(hidden)]
1311 pub fn diagnostics_store_for_test(&self) -> &DiagnosticsStore {
1312 &self.diagnostics
1313 }
1314
1315 #[doc(hidden)]
1316 pub fn diagnostics_store_mut_for_test(&mut self) -> &mut DiagnosticsStore {
1317 &mut self.diagnostics
1318 }
1319
1320 #[doc(hidden)]
1321 pub fn post_edit_outcome_for_entry_for_test(
1322 key: ServerKey,
1323 entry: &DiagnosticEntry,
1324 target_version: i32,
1325 pre: PreEditSnapshot,
1326 ) -> PostEditWaitOutcome {
1327 Self::post_edit_outcome_for_entry(key, entry, target_version, pre)
1328 }
1329
1330 fn post_edit_outcome_for_entry(
1331 key: ServerKey,
1332 entry: &DiagnosticEntry,
1333 target_version: i32,
1334 pre: PreEditSnapshot,
1335 ) -> PostEditWaitOutcome {
1336 let mut fresh = HashMap::new();
1337 if let Some(diagnostics) =
1338 Self::authoritative_post_edit_diagnostics(entry, target_version, pre)
1339 {
1340 fresh.insert(key.clone(), diagnostics);
1341 }
1342 Self::post_edit_outcome(vec![(key, target_version)], fresh, Vec::new())
1343 }
1344
1345 fn authoritative_post_edit_diagnostics(
1346 entry: &DiagnosticEntry,
1347 target_version: i32,
1348 pre: PreEditSnapshot,
1349 ) -> Option<Vec<StoredDiagnostic>> {
1350 (!entry.provisional && post_edit_entry_is_fresh(entry, target_version, pre))
1351 .then(|| entry.diagnostics.clone())
1352 }
1353
1354 #[doc(hidden)]
1355 pub fn enqueue_event_for_test(&self, event: LspEvent) {
1356 self.event_tx
1357 .send(event)
1358 .expect("LSP event receiver should remain connected");
1359 }
1360
1361 #[doc(hidden)]
1362 pub fn pending_event_count_for_test(&self) -> usize {
1363 self.event_rx.len()
1364 }
1365
1366 #[doc(hidden)]
1367 pub fn document_is_open_for_test(&self, file_path: &Path) -> bool {
1368 canonicalize_for_lsp(file_path).is_ok_and(|canonical_path| {
1369 self.documents
1370 .values()
1371 .any(|store| store.is_open(&canonical_path))
1372 })
1373 }
1374
1375 pub fn warm_error_warning_counts(&self) -> (usize, usize) {
1379 self.diagnostics.error_warning_counts()
1380 }
1381
1382 pub fn warm_error_warning_counts_with_provisional(&self) -> ((usize, usize), bool) {
1383 self.diagnostics.error_warning_counts_with_provisional()
1384 }
1385
1386 pub fn diagnostics_generation(&self) -> u64 {
1387 self.diagnostics.generation()
1388 }
1389
1390 pub fn filtered_error_warning_counts(
1395 &self,
1396 keep: impl FnMut(&std::path::Path) -> bool,
1397 ) -> (usize, usize) {
1398 self.diagnostics.filtered_error_warning_counts(keep)
1399 }
1400
1401 pub fn filtered_error_warning_counts_with_provisional(
1405 &self,
1406 keep: impl FnMut(&std::path::Path) -> bool,
1407 ) -> ((usize, usize), bool) {
1408 self.diagnostics
1409 .filtered_error_warning_counts_with_provisional(keep)
1410 }
1411
1412 pub fn provisional_server_keys(&self) -> Vec<ServerKey> {
1416 self.clients
1417 .iter()
1418 .filter(|(_, client)| client.diagnostics_are_provisional())
1419 .map(|(key, _)| key.clone())
1420 .collect()
1421 }
1422
1423 pub fn snapshot_diagnostic_epochs(&self, file_path: &Path) -> HashMap<ServerKey, u64> {
1428 let lookup_path = normalize_lookup_path(file_path);
1429 self.diagnostics
1430 .entries_for_file(&lookup_path)
1431 .into_iter()
1432 .map(|(key, entry)| (key.clone(), entry.epoch))
1433 .collect()
1434 }
1435
1436 pub fn snapshot_pre_edit_state(&self, file_path: &Path) -> HashMap<ServerKey, PreEditSnapshot> {
1439 let lookup_path = normalize_lookup_path(file_path);
1440 let mut snapshots: HashMap<ServerKey, PreEditSnapshot> = self
1441 .diagnostics
1442 .entries_for_file(&lookup_path)
1443 .into_iter()
1444 .map(|(key, entry)| {
1445 (
1446 key.clone(),
1447 PreEditSnapshot {
1448 epoch: entry.epoch,
1449 document_version_at_capture: None,
1450 },
1451 )
1452 })
1453 .collect();
1454
1455 for (key, store) in &self.documents {
1456 if let Some(version) = store.version(&lookup_path) {
1457 snapshots
1458 .entry(key.clone())
1459 .or_default()
1460 .document_version_at_capture = Some(version);
1461 }
1462 }
1463
1464 snapshots
1465 }
1466
1467 pub fn diagnostic_entry_is_fresh_for_document(
1475 &self,
1476 file_path: &Path,
1477 server_key: &ServerKey,
1478 pre: PreEditSnapshot,
1479 ) -> bool {
1480 let lookup_path = normalize_lookup_path(file_path);
1481 let Some(entry) = self
1482 .diagnostics
1483 .entries_for_file(&lookup_path)
1484 .into_iter()
1485 .find_map(|(key, entry)| if key == server_key { Some(entry) } else { None })
1486 else {
1487 return false;
1488 };
1489
1490 if entry.stale {
1491 return false;
1492 }
1493
1494 let target_version = self
1495 .documents
1496 .get(server_key)
1497 .and_then(|store| store.version(&lookup_path))
1498 .or(pre.document_version_at_capture)
1499 .unwrap_or(0);
1500
1501 matches!(entry.version, Some(version) if version >= target_version)
1502 }
1503
1504 pub(crate) fn start_post_edit_diagnostics_wait(
1508 &mut self,
1509 file_path: &Path,
1510 expected_versions: &[(ServerKey, i32)],
1511 pre_snapshot: &HashMap<ServerKey, PreEditSnapshot>,
1512 timeout: std::time::Duration,
1513 ) -> PostEditDiagnosticsWait {
1514 let lookup_path = normalize_lookup_path(file_path);
1515
1516 let _ = self.drain_events_for_file(&lookup_path);
1519
1520 let waiter_id = self.next_post_edit_waiter_id;
1521 self.next_post_edit_waiter_id = self.next_post_edit_waiter_id.wrapping_add(1);
1522 let (wake_tx, wake_rx) = bounded(1);
1523 self.post_edit_waiters.insert(waiter_id, wake_tx);
1524
1525 PostEditDiagnosticsWait {
1526 lookup_path,
1527 expected_versions: expected_versions.to_vec(),
1528 pre_snapshot: pre_snapshot.clone(),
1529 event_rx: self.event_rx.clone(),
1530 wake_rx,
1531 waiter_id,
1532 deadline: std::time::Instant::now() + timeout,
1533 fresh: HashMap::new(),
1534 exited: Vec::new(),
1535 }
1536 }
1537
1538 pub(crate) fn poll_post_edit_diagnostics_wait(
1539 &mut self,
1540 wait: &mut PostEditDiagnosticsWait,
1541 event: Option<LspEvent>,
1542 ) -> bool {
1543 if let Some(event) = event {
1544 self.handle_event(&event);
1545 }
1546
1547 for (key, target_version) in &wait.expected_versions {
1548 if wait.fresh.contains_key(key) || wait.exited.contains(key) {
1549 continue;
1550 }
1551 if !self.clients.contains_key(key) {
1552 wait.exited.push(key.clone());
1553 continue;
1554 }
1555 if let Some(entry) = self
1556 .diagnostics
1557 .entries_for_file(&wait.lookup_path)
1558 .into_iter()
1559 .find_map(|(stored_key, entry)| (stored_key == key).then_some(entry))
1560 {
1561 let pre = wait.pre_snapshot.get(key).copied().unwrap_or_default();
1562 if let Some(diagnostics) =
1563 Self::authoritative_post_edit_diagnostics(entry, *target_version, pre)
1564 {
1565 wait.fresh.insert(key.clone(), diagnostics);
1566 }
1567 }
1568 }
1569
1570 wait.fresh.len() + wait.exited.len() == wait.expected_versions.len()
1571 }
1572
1573 pub(crate) fn finish_post_edit_diagnostics_wait(
1574 &mut self,
1575 wait: PostEditDiagnosticsWait,
1576 ) -> PostEditWaitOutcome {
1577 self.post_edit_waiters.remove(&wait.waiter_id);
1578 Self::post_edit_outcome(wait.expected_versions, wait.fresh, wait.exited)
1579 }
1580
1581 pub fn wait_for_post_edit_diagnostics(
1589 &mut self,
1590 file_path: &Path,
1591 _config: &Config,
1595 expected_versions: &[(ServerKey, i32)],
1596 pre_snapshot: &HashMap<ServerKey, PreEditSnapshot>,
1597 timeout: std::time::Duration,
1598 ) -> PostEditWaitOutcome {
1599 let mut wait = self.start_post_edit_diagnostics_wait(
1600 file_path,
1601 expected_versions,
1602 pre_snapshot,
1603 timeout,
1604 );
1605 let mut complete = self.poll_post_edit_diagnostics_wait(&mut wait, None);
1606
1607 while !complete && !wait.deadline_reached() {
1608 let event = wait.next_event();
1609 complete = self.poll_post_edit_diagnostics_wait(&mut wait, event);
1610 }
1611
1612 self.finish_post_edit_diagnostics_wait(wait)
1613 }
1614
1615 fn post_edit_outcome(
1616 mut expected: Vec<(ServerKey, i32)>,
1617 mut fresh: HashMap<ServerKey, Vec<StoredDiagnostic>>,
1618 exited: Vec<ServerKey>,
1619 ) -> PostEditWaitOutcome {
1620 expected.sort_by(|(left, _), (right, _)| server_key_sort(left, right));
1621
1622 let mut accepted_snapshots = Vec::new();
1623 let mut pending_servers = Vec::new();
1624 for (server_key, document_version) in expected {
1625 if let Some(diagnostics) = fresh.remove(&server_key) {
1626 accepted_snapshots.push(AcceptedDiagnosticSnapshot::new(
1627 server_key,
1628 document_version,
1629 diagnostics,
1630 ));
1631 } else if !exited.contains(&server_key) {
1632 pending_servers.push(server_key);
1633 }
1634 }
1635
1636 let mut diagnostics = accepted_snapshots
1637 .iter()
1638 .flat_map(|snapshot| snapshot.diagnostics.iter().cloned())
1639 .collect::<Vec<_>>();
1640 diagnostics.sort_by(|left, right| {
1641 left.file
1642 .cmp(&right.file)
1643 .then(left.line.cmp(&right.line))
1644 .then(left.column.cmp(&right.column))
1645 .then(left.message.cmp(&right.message))
1646 });
1647
1648 PostEditWaitOutcome {
1649 accepted_snapshots,
1650 diagnostics,
1651 pending_servers,
1652 exited_servers: exited,
1653 }
1654 }
1655
1656 pub fn wait_for_file_diagnostics(
1662 &mut self,
1663 file_path: &Path,
1664 config: &Config,
1665 deadline: std::time::Instant,
1666 ) -> Vec<StoredDiagnostic> {
1667 let lookup_path = normalize_lookup_path(file_path);
1668
1669 if self.server_key_for_file(&lookup_path, config).is_none() {
1670 return Vec::new();
1671 }
1672
1673 loop {
1674 if self.drain_events_for_file(&lookup_path) {
1675 break;
1676 }
1677
1678 let now = std::time::Instant::now();
1679 if now >= deadline {
1680 break;
1681 }
1682
1683 let timeout = deadline.saturating_duration_since(now);
1684 match self.event_rx.recv_timeout(timeout) {
1685 Ok(event) => {
1686 if matches!(
1687 self.handle_event(&event),
1688 Some(ref published_file) if published_file.as_path() == lookup_path.as_path()
1689 ) {
1690 break;
1691 }
1692 }
1693 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
1694 }
1695 }
1696
1697 self.get_diagnostics_for_file(&lookup_path)
1698 .into_iter()
1699 .cloned()
1700 .collect()
1701 }
1702
1703 pub const PULL_FILE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
1709
1710 pub fn pull_file_timeout() -> std::time::Duration {
1712 Self::PULL_FILE_TIMEOUT
1713 }
1714
1715 const PULL_WORKSPACE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
1719
1720 pub fn pull_file_diagnostics(
1731 &mut self,
1732 file_path: &Path,
1733 config: &Config,
1734 ) -> Result<Vec<PullFileResult>, LspError> {
1735 let canonical_path = canonicalize_for_lsp(file_path)?;
1736 let opened = self.ensure_file_open(&canonical_path, config)?;
1739 if opened.server_keys.is_empty() {
1740 return Ok(Vec::new());
1741 }
1742
1743 let uri = uri_for_path(&canonical_path)?;
1744 let mut results = Vec::with_capacity(opened.server_keys.len());
1745
1746 for key in opened.server_keys {
1747 let supports_pull = self
1748 .clients
1749 .get(&key)
1750 .and_then(|c| c.diagnostic_capabilities())
1751 .is_some_and(|caps| caps.pull_diagnostics);
1752
1753 if !supports_pull {
1754 results.push(PullFileResult {
1755 server_key: key.clone(),
1756 outcome: PullFileOutcome::PullNotSupported,
1757 });
1758 continue;
1759 }
1760
1761 let previous_result_id = self
1763 .diagnostics
1764 .entries_for_file(&canonical_path)
1765 .into_iter()
1766 .find(|(k, _)| **k == key)
1767 .and_then(|(_, entry)| entry.result_id.clone());
1768
1769 let identifier = self
1770 .clients
1771 .get(&key)
1772 .and_then(|c| c.diagnostic_capabilities())
1773 .and_then(|caps| caps.identifier.clone());
1774
1775 let params = AftDocumentDiagnosticParams {
1776 text_document: lsp_types::TextDocumentIdentifier { uri: uri.clone() },
1777 identifier,
1778 previous_result_id,
1779 work_done_progress_params: Default::default(),
1780 partial_result_params: Default::default(),
1781 };
1782
1783 let outcome = match self.send_pull_request(&key, params) {
1784 Ok(report) => {
1785 if matches!(
1786 &report,
1787 lsp_types::DocumentDiagnosticReportResult::Report(
1788 lsp_types::DocumentDiagnosticReport::Full(_)
1789 )
1790 ) {
1791 self.drain_events();
1796 }
1797 self.ingest_document_report(&key, &canonical_path, report)
1798 }
1799 Err(err) => {
1800 if let Some(result) = self.cache_post_initialize_exit(&key, &err) {
1801 PullFileOutcome::RequestFailed {
1802 reason: server_attempt_result_reason(&result),
1803 }
1804 } else if recoverable_pull_rejection(&err)
1805 && self.clients.get(&key).is_some_and(|client| {
1806 matches!(
1807 client.state(),
1808 ServerState::Ready | ServerState::Initializing
1809 )
1810 })
1811 {
1812 PullFileOutcome::RequestFailed {
1813 reason: format!("pull_rejected_push_fallback: {err}"),
1814 }
1815 } else {
1816 PullFileOutcome::RequestFailed {
1817 reason: err.to_string(),
1818 }
1819 }
1820 }
1821 };
1822
1823 results.push(PullFileResult {
1824 server_key: key,
1825 outcome,
1826 });
1827 }
1828
1829 Ok(results)
1830 }
1831
1832 pub fn pull_workspace_diagnostics(
1837 &mut self,
1838 server_key: &ServerKey,
1839 timeout: Option<std::time::Duration>,
1840 ) -> Result<PullWorkspaceResult, LspError> {
1841 let timeout = timeout.unwrap_or(Self::PULL_WORKSPACE_TIMEOUT);
1842
1843 let supports_workspace = self
1844 .clients
1845 .get(server_key)
1846 .and_then(|c| c.diagnostic_capabilities())
1847 .is_some_and(|caps| caps.workspace_diagnostics);
1848
1849 if !supports_workspace {
1850 return Ok(PullWorkspaceResult {
1851 server_key: server_key.clone(),
1852 files_reported: Vec::new(),
1853 complete: false,
1854 cancelled: false,
1855 supports_workspace: false,
1856 });
1857 }
1858
1859 let identifier = self
1860 .clients
1861 .get(server_key)
1862 .and_then(|c| c.diagnostic_capabilities())
1863 .and_then(|caps| caps.identifier.clone());
1864
1865 let params = AftWorkspaceDiagnosticParams {
1866 identifier,
1867 previous_result_ids: Vec::new(),
1868 work_done_progress_params: Default::default(),
1869 partial_result_params: Default::default(),
1870 };
1871
1872 let result = match self
1873 .clients
1874 .get_mut(server_key)
1875 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?
1876 .send_request_with_timeout::<AftWorkspaceDiagnosticRequest>(params, timeout)
1877 {
1878 Ok(result) => result,
1879 Err(LspError::Timeout(_)) => {
1880 return Ok(PullWorkspaceResult {
1881 server_key: server_key.clone(),
1882 files_reported: Vec::new(),
1883 complete: false,
1884 cancelled: true,
1885 supports_workspace: true,
1886 });
1887 }
1888 Err(err) => {
1889 if let Some(result) = self.cache_post_initialize_exit(server_key, &err) {
1890 return Err(LspError::ServerNotReady(server_attempt_result_reason(
1891 &result,
1892 )));
1893 }
1894 return Err(err);
1895 }
1896 };
1897
1898 let (items, complete) = match result {
1902 lsp_types::WorkspaceDiagnosticReportResult::Report(report) => (report.items, true),
1903 lsp_types::WorkspaceDiagnosticReportResult::Partial(partial) => (partial.items, false),
1904 };
1905
1906 let mut files_reported = Vec::with_capacity(items.len());
1908 for item in items {
1909 match item {
1910 lsp_types::WorkspaceDocumentDiagnosticReport::Full(full) => {
1911 if let Some(file) = uri_to_path(&full.uri) {
1912 let stored = from_lsp_diagnostics(
1913 file.clone(),
1914 full.full_document_diagnostic_report.items.clone(),
1915 );
1916 self.diagnostics.publish_with_result_id(
1917 server_key.clone(),
1918 file.clone(),
1919 stored,
1920 full.full_document_diagnostic_report.result_id.clone(),
1921 );
1922 files_reported.push(file);
1923 }
1924 }
1925 lsp_types::WorkspaceDocumentDiagnosticReport::Unchanged(_unchanged) => {
1926 }
1929 }
1930 }
1931
1932 Ok(PullWorkspaceResult {
1933 server_key: server_key.clone(),
1934 files_reported,
1935 complete,
1936 cancelled: false,
1937 supports_workspace: true,
1938 })
1939 }
1940
1941 fn cache_post_initialize_exit(
1942 &mut self,
1943 key: &ServerKey,
1944 err: &LspError,
1945 ) -> Option<ServerAttemptResult> {
1946 let binary = self
1947 .server_binaries
1948 .get(key)
1949 .cloned()
1950 .unwrap_or_else(|| key.kind.id_str().to_string());
1951 let (status, stderr_tail) = {
1952 let client = self.clients.get_mut(key)?;
1953 let mut status = client.child_exit_status();
1954 for _ in 0..10 {
1955 if status.is_some() {
1956 break;
1957 }
1958 std::thread::sleep(std::time::Duration::from_millis(10));
1959 status = client.child_exit_status();
1960 }
1961 let status = status?;
1962 wait_for_stderr_tail(client);
1963 (status, client.stderr_tail())
1964 };
1965 let reason = format_post_initialize_exit_reason(&binary, status, &stderr_tail, err);
1966 let result = ServerAttemptResult::SpawnFailed { binary, reason };
1967 self.clients.remove(key);
1968 self.server_binaries.remove(key);
1969 self.documents.remove(key);
1970 self.diagnostics.clear_for_server(key);
1971 self.failed_spawns.insert(key.clone(), result.clone());
1972 Some(result)
1973 }
1974
1975 fn send_pull_request(
1977 &mut self,
1978 key: &ServerKey,
1979 params: AftDocumentDiagnosticParams,
1980 ) -> Result<lsp_types::DocumentDiagnosticReportResult, LspError> {
1981 let client = self
1982 .clients
1983 .get_mut(key)
1984 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?;
1985 client.send_request_with_timeout::<AftDocumentDiagnosticRequest>(
1989 params,
1990 Self::PULL_FILE_TIMEOUT,
1991 )
1992 }
1993
1994 fn ingest_document_report(
1997 &mut self,
1998 key: &ServerKey,
1999 canonical_path: &Path,
2000 result: lsp_types::DocumentDiagnosticReportResult,
2001 ) -> PullFileOutcome {
2002 let report = match result {
2003 lsp_types::DocumentDiagnosticReportResult::Report(report) => report,
2004 lsp_types::DocumentDiagnosticReportResult::Partial(_) => {
2005 return PullFileOutcome::PartialNotSupported;
2009 }
2010 };
2011
2012 match report {
2013 lsp_types::DocumentDiagnosticReport::Full(full) => {
2014 let result_id = full.full_document_diagnostic_report.result_id.clone();
2015 let stored = from_lsp_diagnostics(
2016 canonical_path.to_path_buf(),
2017 full.full_document_diagnostic_report.items.clone(),
2018 );
2019 let count = stored.len();
2020 let provisional = self
2021 .clients
2022 .get(key)
2023 .is_some_and(|client| client.diagnostics_are_provisional());
2024 self.diagnostics.publish_full_with_provisional(
2025 key.clone(),
2026 canonical_path.to_path_buf(),
2027 stored,
2028 result_id,
2029 None,
2030 provisional,
2031 );
2032 PullFileOutcome::Full {
2033 diagnostic_count: count,
2034 }
2035 }
2036 lsp_types::DocumentDiagnosticReport::Unchanged(_unchanged) => {
2037 if self
2044 .diagnostics
2045 .has_report_for_server_file(key, canonical_path)
2046 {
2047 self.diagnostics
2048 .mark_fresh_for_server_file(key, canonical_path);
2049 let authoritative = self
2050 .clients
2051 .get(key)
2052 .map_or(true, |client| !client.diagnostics_are_provisional());
2053 if authoritative {
2054 self.diagnostics
2055 .clear_provisional_for_server_file(key, canonical_path);
2056 }
2057 PullFileOutcome::Unchanged
2058 } else {
2059 PullFileOutcome::RequestFailed {
2060 reason: "no_cache_for_unchanged".to_string(),
2061 }
2062 }
2063 }
2064 }
2065 }
2066
2067 pub fn shutdown_all(&mut self) {
2069 for (key, mut client) in self.clients.drain() {
2070 if let Err(err) = client.shutdown() {
2071 slog_error!("error shutting down {:?}: {}", key, err);
2072 }
2073 }
2074 self.server_binaries.clear();
2075 self.documents.clear();
2076 self.diagnostics = DiagnosticsStore::new();
2077 }
2078
2079 pub fn has_active_servers(&self) -> bool {
2081 self.clients
2082 .values()
2083 .any(|client| client.state() == ServerState::Ready)
2084 }
2085
2086 pub fn active_server_keys(&self) -> Vec<ServerKey> {
2089 self.clients.keys().cloned().collect()
2090 }
2091
2092 #[cfg(windows)]
2096 #[doc(hidden)]
2097 pub fn watched_file_notification_trace_for_test(&self) -> &str {
2098 &self.last_watched_file_notification_trace
2099 }
2100
2101 pub fn get_diagnostics_for_file(&self, file: &Path) -> Vec<&StoredDiagnostic> {
2102 let normalized = normalize_lookup_path(file);
2103 self.diagnostics.for_file(&normalized)
2104 }
2105
2106 pub fn get_diagnostics_for_file_with_provisional(
2107 &self,
2108 file: &Path,
2109 ) -> Vec<(&StoredDiagnostic, bool)> {
2110 let normalized = normalize_lookup_path(file);
2111 self.diagnostics.for_file_with_provisional(&normalized)
2112 }
2113
2114 pub fn clear_failed_spawns(&mut self) -> usize {
2135 let n = self.failed_spawns.len();
2136 self.failed_spawns.clear();
2137 n
2138 }
2139
2140 #[cfg(test)]
2141 pub(crate) fn insert_failed_spawn_for_test(&mut self) {
2142 let key = ServerKey {
2143 kind: crate::lsp::registry::ServerKind::Rust,
2144 root: std::path::PathBuf::from("/tmp/test-root"),
2145 };
2146 self.failed_spawns.insert(
2147 key,
2148 ServerAttemptResult::SpawnFailed {
2149 binary: "rust-analyzer".to_string(),
2150 reason: "test".to_string(),
2151 },
2152 );
2153 }
2154
2155 pub fn clear_diagnostics_for_file(&mut self, file: &Path) -> bool {
2156 diagnostic_path_candidates(file)
2157 .into_iter()
2158 .fold(false, |removed, candidate| {
2159 removed | self.diagnostics.clear_for_file(&candidate)
2160 })
2161 }
2162
2163 pub fn mark_diagnostics_stale_for_file(&mut self, file: &Path) -> StaleDiagnosticsMark {
2167 let mut result = StaleDiagnosticsMark::default();
2168 for candidate in diagnostic_path_candidates(file) {
2169 let (had_entries, changed) = self.diagnostics.mark_stale_for_file(&candidate);
2170 result.had_entries |= had_entries;
2171 result.changed |= changed;
2172 }
2173 result
2174 }
2175
2176 pub fn get_diagnostics_for_directory(&self, dir: &Path) -> Vec<&StoredDiagnostic> {
2177 let normalized = normalize_lookup_path(dir);
2178 self.diagnostics.for_directory(&normalized)
2179 }
2180
2181 pub fn get_diagnostics_for_directory_with_provisional(
2182 &self,
2183 dir: &Path,
2184 ) -> Vec<(&StoredDiagnostic, bool)> {
2185 let normalized = normalize_lookup_path(dir);
2186 self.diagnostics.for_directory_with_provisional(&normalized)
2187 }
2188
2189 pub fn get_all_diagnostics(&self) -> Vec<&StoredDiagnostic> {
2190 self.diagnostics.all()
2191 }
2192
2193 pub fn get_all_diagnostics_with_provisional(&self) -> Vec<(&StoredDiagnostic, bool)> {
2194 self.diagnostics.all_with_provisional()
2195 }
2196
2197 pub fn has_any_diagnostic_reports(&self) -> bool {
2202 self.diagnostics.has_any_fresh_report()
2203 }
2204
2205 pub fn has_diagnostic_report_for_file(&self, file: &Path) -> bool {
2209 let normalized = normalize_lookup_path(file);
2210 self.diagnostics.has_any_fresh_report_for_file(&normalized)
2211 }
2212
2213 pub fn has_diagnostic_report_for_server_file(&self, server: &ServerKey, file: &Path) -> bool {
2217 let normalized = normalize_lookup_path(file);
2218 self.diagnostics
2219 .has_fresh_report_for_server_file(server, &normalized)
2220 }
2221
2222 pub fn has_authoritative_report_for_file(&self, file: &Path) -> bool {
2227 let normalized = normalize_lookup_path(file);
2228 self.diagnostics
2229 .has_authoritative_report_for_file(&normalized)
2230 }
2231
2232 pub fn has_authoritative_report_for_server(&self, server: &ServerKey) -> bool {
2237 self.diagnostics.has_authoritative_report_for_server(server)
2238 }
2239
2240 pub fn server_is_warming(&self, server: &ServerKey) -> bool {
2245 self.clients
2246 .get(server)
2247 .is_some_and(|client| client.diagnostics_are_provisional())
2248 }
2249
2250 pub fn producer_has_settled(&self, server: &ServerKey) -> bool {
2255 self.has_authoritative_report_for_server(server) || !self.server_is_warming(server)
2256 }
2257
2258 pub fn producers_settled(&self, expected: &[ServerKey]) -> bool {
2262 expected
2263 .iter()
2264 .all(|server| self.producer_has_settled(server))
2265 }
2266
2267 fn drain_events_for_file(&mut self, file_path: &Path) -> bool {
2268 let mut saw_file_diagnostics = false;
2269 while let Ok(event) = self.event_rx.try_recv() {
2270 if matches!(
2271 self.handle_event(&event),
2272 Some(ref published_file) if published_file.as_path() == file_path
2273 ) {
2274 saw_file_diagnostics = true;
2275 }
2276 }
2277 saw_file_diagnostics
2278 }
2279
2280 fn accepted_live_publish_snapshot(
2281 &self,
2282 event: &LspEvent,
2283 ) -> Option<AcceptedDiagnosticSnapshot> {
2284 let LspEvent::Notification {
2285 server_kind,
2286 root,
2287 method,
2288 params: Some(params),
2289 } = event
2290 else {
2291 return None;
2292 };
2293 if method != "textDocument/publishDiagnostics" {
2294 return None;
2295 }
2296
2297 let publish_params =
2298 serde_json::from_value::<lsp_types::PublishDiagnosticsParams>(params.clone()).ok()?;
2299 let file = uri_to_path(&publish_params.uri)?;
2300 let server_key = ServerKey {
2301 kind: server_kind.clone(),
2302 root: root.clone(),
2303 };
2304 let client = self.clients.get(&server_key)?;
2305 if client.state() != ServerState::Ready || client.diagnostics_are_provisional() {
2306 return None;
2307 }
2308 let document_version = self.documents.get(&server_key)?.version(&file)?;
2309 let entry = self
2310 .diagnostics
2311 .entries_for_file(&file)
2312 .into_iter()
2313 .find_map(|(stored_key, entry)| (stored_key == &server_key).then_some(entry))?;
2314 if entry.stale || entry.provisional || entry.version != Some(document_version) {
2315 return None;
2316 }
2317
2318 Some(AcceptedDiagnosticSnapshot::new(
2319 server_key,
2320 document_version,
2321 entry.diagnostics.clone(),
2322 ))
2323 }
2324
2325 fn handle_event(&mut self, event: &LspEvent) -> Option<PathBuf> {
2326 let published_file = match event {
2327 LspEvent::Notification {
2328 server_kind,
2329 root,
2330 method,
2331 params: Some(params),
2332 } if method == "textDocument/publishDiagnostics" => {
2333 self.handle_publish_diagnostics(server_kind.clone(), root.clone(), params)
2334 }
2335 LspEvent::Notification {
2336 server_kind,
2337 root,
2338 method,
2339 params: Some(params),
2340 } if method == "experimental/serverStatus" => {
2341 self.handle_server_status(server_kind.clone(), root.clone(), params);
2342 None
2343 }
2344 LspEvent::ServerExited { server_kind, root } => {
2345 let key = ServerKey {
2346 kind: server_kind.clone(),
2347 root: root.clone(),
2348 };
2349 self.clients.remove(&key);
2350 self.server_binaries.remove(&key);
2351 self.documents.remove(&key);
2352 self.diagnostics.clear_for_server(&key);
2353 None
2354 }
2355 _ => None,
2356 };
2357 self.wake_post_edit_waiters();
2358 published_file
2359 }
2360
2361 fn wake_post_edit_waiters(&mut self) {
2362 Self::wake_waiters(&mut self.post_edit_waiters);
2363 }
2364
2365 fn wake_waiters(waiters: &mut HashMap<u64, Sender<()>>) {
2366 waiters.retain(|_, sender| match sender.try_send(()) {
2367 Ok(()) | Err(TrySendError::Full(())) => true,
2368 Err(TrySendError::Disconnected(())) => false,
2369 });
2370 }
2371
2372 fn handle_publish_diagnostics(
2373 &mut self,
2374 server: ServerKind,
2375 root: PathBuf,
2376 params: &serde_json::Value,
2377 ) -> Option<PathBuf> {
2378 if let Ok(publish_params) =
2379 serde_json::from_value::<lsp_types::PublishDiagnosticsParams>(params.clone())
2380 {
2381 let file = uri_to_path(&publish_params.uri)?;
2382 let stored = from_lsp_diagnostics(file.clone(), publish_params.diagnostics);
2383 let key = ServerKey { kind: server, root };
2388 let provisional = self
2389 .clients
2390 .get(&key)
2391 .is_some_and(|client| client.diagnostics_are_provisional());
2392 self.diagnostics.publish_full_with_provisional(
2393 key,
2394 file.clone(),
2395 stored,
2396 None,
2397 publish_params.version,
2398 provisional,
2399 );
2400 return Some(file);
2401 }
2402 None
2403 }
2404
2405 fn handle_server_status(
2406 &mut self,
2407 server: ServerKind,
2408 root: PathBuf,
2409 params: &serde_json::Value,
2410 ) {
2411 if !matches!(&server, ServerKind::Rust)
2412 || params.get("quiescent").and_then(serde_json::Value::as_bool) != Some(true)
2413 {
2414 return;
2415 }
2416
2417 let key = ServerKey { kind: server, root };
2418 let became_quiescent = self
2419 .clients
2420 .get_mut(&key)
2421 .is_some_and(|client| client.set_rust_analyzer_quiescent(true));
2422 if became_quiescent {
2423 self.diagnostics.promote_provisional_for_server(&key);
2424 }
2425 }
2426
2427 fn spawn_server(
2428 &self,
2429 def: &ServerDef,
2430 root: &Path,
2431 source_file: &Path,
2432 config: &Config,
2433 ) -> Result<LspClient, LspError> {
2434 let initialization_options =
2435 initialization_options_for_spawn(def, source_file, root, config)?;
2436 let binary = self.resolve_binary(def, root, config)?;
2437
2438 let mut merged_env = def.env.clone();
2442 for (key, value) in &self.extra_env {
2443 merged_env.insert(key.clone(), value.clone());
2444 }
2445
2446 let reclaim_root = config
2451 .project_root
2452 .as_deref()
2453 .map(crate::inspect::job::canonicalize_normalized)
2454 .filter(|project_root| root.starts_with(project_root))
2455 .unwrap_or_else(|| root.to_path_buf());
2456
2457 let mut client = LspClient::spawn_with_reclaim_root(
2458 def.kind.clone(),
2459 root.to_path_buf(),
2460 &binary,
2461 &def.args,
2462 &merged_env,
2463 self.event_tx.clone(),
2464 self.child_registry.clone(),
2465 Some(&reclaim_root),
2466 )?;
2467 if let Err(err) = client.initialize(root, initialization_options) {
2468 wait_for_stderr_tail(&mut client);
2469 let stderr_tail = client.stderr_tail();
2470 let reason = if client.child_exited() || !stderr_tail.is_empty() {
2471 format_initialize_failure_reason(&def.binary, &stderr_tail, &err)
2472 } else {
2473 format!("server failed during initialize: {err}")
2474 };
2475 return Err(LspError::ServerNotReady(reason));
2476 }
2477 Ok(client)
2478 }
2479
2480 fn resolve_binary(
2481 &self,
2482 def: &ServerDef,
2483 root: &Path,
2484 config: &Config,
2485 ) -> Result<PathBuf, LspError> {
2486 if let Some(path) = self.binary_overrides.get(&def.kind) {
2487 if path.exists() {
2488 return Ok(path.clone());
2489 }
2490 return Err(LspError::NotFound(format!(
2491 "override binary for {:?} not found: {}",
2492 def.kind,
2493 path.display()
2494 )));
2495 }
2496
2497 if let Some(path) = env_binary_override(&def.kind) {
2498 if path.exists() {
2499 return Ok(path);
2500 }
2501 return Err(LspError::NotFound(format!(
2502 "environment override binary for {:?} not found: {}",
2503 def.kind,
2504 path.display()
2505 )));
2506 }
2507
2508 resolve_server_binary(def, Some(root), config).ok_or_else(|| {
2509 let searched = if matches!(def.kind, ServerKind::Python | ServerKind::Ty) {
2510 "the workspace virtualenv, node_modules/.bin, lsp_paths_extra, or PATH"
2511 } else {
2512 "node_modules/.bin, lsp_paths_extra, or PATH"
2513 };
2514 LspError::NotFound(format!(
2515 "language server binary '{}' not found in {searched}",
2516 def.binary,
2517 ))
2518 })
2519 }
2520
2521 fn server_key_for_file(&self, file_path: &Path, config: &Config) -> Option<ServerKey> {
2522 for def in servers_for_file(file_path, config) {
2523 let key = server_key_for_definition(&def, file_path, config)?;
2524 if self.clients.contains_key(&key) {
2525 return Some(key);
2526 }
2527 }
2528 None
2529 }
2530}
2531
2532impl Default for LspManager {
2533 fn default() -> Self {
2534 Self::new()
2535 }
2536}
2537
2538const ASTRO_TSDK_UNAVAILABLE: &str = "astro-ls requires a project TypeScript install; none found";
2539
2540fn initialization_options_for_spawn(
2541 def: &ServerDef,
2542 source_file: &Path,
2543 server_root: &Path,
2544 config: &Config,
2545) -> Result<Option<serde_json::Value>, LspError> {
2546 if def.kind != ServerKind::Astro {
2547 return Ok(def.initialization_options.clone());
2548 }
2549
2550 if def
2551 .initialization_options
2552 .as_ref()
2553 .and_then(|options| options.pointer("/typescript/tsdk"))
2554 .and_then(serde_json::Value::as_str)
2555 .is_some_and(|tsdk| !tsdk.is_empty())
2556 {
2557 return Ok(def.initialization_options.clone());
2558 }
2559
2560 let project_root = config.project_root.as_deref().unwrap_or(server_root);
2564 let boundary = if source_file.starts_with(project_root) {
2565 project_root
2566 } else {
2567 server_root
2568 };
2569 let tsdk = find_project_typescript_sdk(source_file, boundary)
2570 .ok_or_else(|| LspError::ServerNotReady(ASTRO_TSDK_UNAVAILABLE.to_string()))?;
2571 let mut options = serde_json::json!({
2572 "typescript": {
2573 "tsdk": tsdk.to_string_lossy(),
2574 }
2575 });
2576 if let Some(configured) = def.initialization_options.clone() {
2577 merge_json_override(&mut options, configured);
2578 }
2579 Ok(Some(options))
2580}
2581
2582fn find_project_typescript_sdk(source_file: &Path, project_root: &Path) -> Option<PathBuf> {
2583 let mut directory = source_file.parent()?;
2584 loop {
2585 let lib = directory
2586 .join("node_modules")
2587 .join("typescript")
2588 .join("lib");
2589 if lib.join("tsserverlibrary.js").is_file() || lib.join("typescript.js").is_file() {
2590 return Some(lib);
2591 }
2592 if directory == project_root {
2593 return None;
2594 }
2595 let parent = directory.parent()?;
2596 if !parent.starts_with(project_root) {
2597 return None;
2598 }
2599 directory = parent;
2600 }
2601}
2602
2603fn merge_json_override(base: &mut serde_json::Value, override_value: serde_json::Value) {
2604 match (base, override_value) {
2605 (serde_json::Value::Object(base), serde_json::Value::Object(override_fields)) => {
2606 for (key, value) in override_fields {
2607 if let Some(existing) = base.get_mut(&key) {
2608 merge_json_override(existing, value);
2609 } else {
2610 base.insert(key, value);
2611 }
2612 }
2613 }
2614 (base, value) => *base = value,
2615 }
2616}
2617
2618fn wait_for_stderr_tail(client: &mut LspClient) {
2619 for _ in 0..10 {
2620 if !client.stderr_tail().is_empty() {
2621 break;
2622 }
2623 std::thread::sleep(std::time::Duration::from_millis(10));
2624 }
2625}
2626
2627fn recoverable_pull_rejection(err: &LspError) -> bool {
2628 matches!(
2629 err,
2630 LspError::ServerError {
2631 code: -32601 | -32602,
2632 ..
2633 }
2634 )
2635}
2636
2637fn server_attempt_result_reason(result: &ServerAttemptResult) -> String {
2638 match result {
2639 ServerAttemptResult::SpawnFailed { binary, reason } => {
2640 format!("spawn_failed: {binary} ({reason})")
2641 }
2642 ServerAttemptResult::BinaryNotInstalled { binary } => {
2643 format!("binary_not_installed: {binary}")
2644 }
2645 ServerAttemptResult::NoRootMarker { looked_for } => {
2646 format!("no_root_marker (looked for: {})", looked_for.join(", "))
2647 }
2648 ServerAttemptResult::Ok { .. } => "ok".to_string(),
2649 }
2650}
2651
2652fn format_stderr_tail_for_reason(stderr_tail: &str) -> String {
2653 truncate_stderr_tail_for_reason(stderr_tail)
2654 .lines()
2655 .map(|line| format!(" {line}"))
2656 .collect::<Vec<_>>()
2657 .join("\n")
2658}
2659
2660fn truncate_stderr_tail_for_reason(stderr_tail: &str) -> String {
2661 if stderr_tail.len() <= STDERR_REASON_BYTES {
2662 return stderr_tail.to_string();
2663 }
2664
2665 let ellipsis = "...";
2666 let target_len = STDERR_REASON_BYTES.saturating_sub(ellipsis.len());
2667 let mut start = stderr_tail.len() - target_len;
2668 while start < stderr_tail.len() && !stderr_tail.is_char_boundary(start) {
2669 start += 1;
2670 }
2671 format!("{ellipsis}{}", &stderr_tail[start..])
2672}
2673
2674fn format_initialize_failure_reason(binary: &str, stderr_tail: &str, err: &LspError) -> String {
2675 let mut reason = format!("server crashed during initialize: {err}");
2676 if !stderr_tail.is_empty() {
2677 reason.push_str("; stderr (last 64 lines):\n");
2678 reason.push_str(&format_stderr_tail_for_reason(stderr_tail));
2679 reason.push_str("\n\n");
2680 reason.push_str(&failure_hint(binary, stderr_tail));
2681 }
2682 reason
2683}
2684
2685fn format_post_initialize_exit_reason(
2686 binary: &str,
2687 status: std::process::ExitStatus,
2688 stderr_tail: &str,
2689 err: &LspError,
2690) -> String {
2691 let code = status
2692 .code()
2693 .map(|c| c.to_string())
2694 .unwrap_or_else(|| "signal/unknown".to_string());
2695 let mut reason = format!("server exited after initialize (code {code}): {err}");
2696 if !stderr_tail.is_empty() {
2697 reason.push_str("; stderr (last 64 lines):\n");
2698 reason.push_str(&format_stderr_tail_for_reason(stderr_tail));
2699 reason.push_str("\n\n");
2700 reason.push_str(&failure_hint(binary, stderr_tail));
2701 }
2702 reason
2703}
2704
2705fn failure_hint(binary: &str, stderr_tail: &str) -> String {
2706 if stderr_tail.contains("MODULE_NOT_FOUND") || stderr_tail.contains("Cannot find module") {
2707 let package_manager = infer_package_manager(stderr_tail);
2708 format!(
2709 "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."
2710 )
2711 } else if let Some(component) = rustup_missing_component(stderr_tail) {
2712 format!("'{component}' is a rustup proxy but the component is not installed. Install it: rustup component add {component}")
2717 } else {
2718 format!("Hint: see stderr above for '{binary}' failure details.")
2719 }
2720}
2721
2722fn rustup_missing_component(stderr_tail: &str) -> Option<String> {
2728 let marker = "Unknown binary '";
2729 let start = stderr_tail.find(marker)? + marker.len();
2730 let rest = &stderr_tail[start..];
2731 let end = rest.find('\'')?;
2732 let name = &rest[..end];
2733 if name.is_empty() || !stderr_tail.contains("toolchain") {
2736 return None;
2737 }
2738 Some(name.to_string())
2739}
2740
2741fn infer_package_manager(stderr_tail: &str) -> &'static str {
2742 let lower = stderr_tail.to_ascii_lowercase();
2743 if lower.contains(".pnpm/") || lower.contains(".pnpm\\") || lower.contains("/pnpm/") {
2744 "pnpm"
2745 } else if lower.contains(".yarn/")
2746 || lower.contains(".yarn\\")
2747 || lower.contains("/yarn/")
2748 || lower.contains("yarn")
2749 {
2750 "yarn"
2751 } else {
2752 "npm"
2753 }
2754}
2755
2756fn canonicalize_for_lsp(file_path: &Path) -> Result<PathBuf, LspError> {
2757 std::fs::canonicalize(file_path)
2764 .map(|canonical| crate::inspect::job::normalize_path(&canonical))
2765 .map_err(LspError::from)
2766}
2767
2768fn resolve_for_lsp_uri(file_path: &Path) -> PathBuf {
2769 if let Ok(path) = std::fs::canonicalize(file_path) {
2772 return crate::inspect::job::normalize_path(&path);
2773 }
2774
2775 let mut existing = file_path.to_path_buf();
2776 let mut missing = Vec::new();
2777 while !existing.exists() {
2778 let Some(name) = existing.file_name() else {
2779 break;
2780 };
2781 missing.push(name.to_owned());
2782 let Some(parent) = existing.parent() else {
2783 break;
2784 };
2785 existing = parent.to_path_buf();
2786 }
2787
2788 let mut resolved = std::fs::canonicalize(&existing)
2789 .map(|canonical| crate::inspect::job::normalize_path(&canonical))
2790 .unwrap_or(existing);
2791 for segment in missing.into_iter().rev() {
2792 resolved.push(segment);
2793 }
2794 resolved
2795}
2796
2797fn language_id_for_extension(ext: &str) -> &'static str {
2798 match ext {
2799 "ts" => "typescript",
2800 "tsx" => "typescriptreact",
2801 "js" | "mjs" | "cjs" => "javascript",
2802 "jsx" => "javascriptreact",
2803 "py" | "pyi" => "python",
2804 "rs" => "rust",
2805 "go" => "go",
2806 "html" | "htm" => "html",
2807 _ => "plaintext",
2808 }
2809}
2810
2811fn normalize_lookup_path(path: &Path) -> PathBuf {
2812 std::fs::canonicalize(path)
2816 .map(|canonical| crate::inspect::job::normalize_path(&canonical))
2817 .unwrap_or_else(|_| path.to_path_buf())
2818}
2819
2820fn diagnostic_path_candidates(file: &Path) -> Vec<PathBuf> {
2821 let mut candidates = Vec::with_capacity(4);
2822 let mut add = |candidate: PathBuf| {
2823 if !candidates.iter().any(|existing| existing == &candidate) {
2824 candidates.push(candidate);
2825 }
2826 };
2827
2828 add(file.to_path_buf());
2832 add(normalize_lookup_path(file));
2833
2834 if let (Some(parent), Some(name)) = (file.parent(), file.file_name()) {
2838 if let Ok(canonical_parent) = std::fs::canonicalize(parent) {
2839 let reconstructed = canonical_parent.join(name);
2840 add(reconstructed.clone());
2841 add(crate::inspect::job::normalize_path(&reconstructed));
2842 }
2843 }
2844
2845 candidates
2846}
2847
2848fn classify_spawn_error(binary: &str, err: &LspError) -> ServerAttemptResult {
2855 match err {
2856 LspError::NotFound(_) => ServerAttemptResult::BinaryNotInstalled {
2861 binary: binary.to_string(),
2862 },
2863 other => ServerAttemptResult::SpawnFailed {
2864 binary: binary.to_string(),
2865 reason: other.to_string(),
2866 },
2867 }
2868}
2869
2870fn env_binary_override(kind: &ServerKind) -> Option<PathBuf> {
2871 let id = kind.id_str();
2872 let suffix: String = id
2873 .chars()
2874 .map(|ch| {
2875 if ch.is_ascii_alphanumeric() {
2876 ch.to_ascii_uppercase()
2877 } else {
2878 '_'
2879 }
2880 })
2881 .collect();
2882 let key = format!("AFT_LSP_{suffix}_BINARY");
2883 std::env::var_os(key).map(PathBuf::from)
2884}
2885
2886#[cfg(all(test, windows))]
2887mod windows_server_key_tests {
2888 use std::fs;
2889 use std::os::windows::ffi::OsStrExt;
2890
2891 use super::{canonicalize_for_lsp, server_key_for_definition};
2892 use crate::config::{Config, UserServerDef};
2893 use crate::lsp::registry::servers_for_file;
2894
2895 #[test]
2896 fn normalized_and_verbatim_inputs_produce_identical_server_key_material() {
2897 let temp_dir = tempfile::tempdir().expect("tempdir");
2898 let root = temp_dir.path().join("workspace");
2899 let source = root.join("src").join("main.customts");
2900 fs::create_dir_all(source.parent().expect("source parent")).expect("create source dir");
2901 fs::write(root.join("custom-root.json"), "{}\n").expect("write root marker");
2902 fs::write(&source, "export const value = 1;\n").expect("write source");
2903
2904 let config = Config {
2905 project_root: Some(root),
2906 lsp_servers: vec![UserServerDef {
2907 id: "custom-ts".to_string(),
2908 extensions: vec!["customts".to_string()],
2909 binary: "custom-ts-lsp".to_string(),
2910 args: Vec::new(),
2911 root_markers: vec!["custom-root.json".to_string()],
2912 env: Default::default(),
2913 initialization_options: None,
2914 disabled: false,
2915 }],
2916 ..Config::default()
2917 };
2918
2919 let normalized_input = canonicalize_for_lsp(&source).expect("normalized source path");
2920 let bare_canonical_input = fs::canonicalize(&source).expect("canonical source path");
2921 let key_for = |path: &std::path::Path| {
2922 let def = servers_for_file(path, &config)
2923 .into_iter()
2924 .find(|def| def.kind.id_str() == "custom-ts")
2925 .expect("custom server definition");
2926 server_key_for_definition(&def, path, &config).expect("custom server root")
2927 };
2928
2929 let key_material = |key: &crate::lsp::roots::ServerKey| {
2930 let root_bytes = key
2931 .root
2932 .as_os_str()
2933 .encode_wide()
2934 .flat_map(u16::to_le_bytes)
2935 .collect::<Vec<_>>();
2936 (key.kind.id_str().to_string(), root_bytes)
2937 };
2938 let ensure_key = key_for(&normalized_input);
2939 let running_lookup_key = key_for(&bare_canonical_input);
2940
2941 assert_eq!(key_material(&ensure_key), key_material(&running_lookup_key));
2942 }
2943}
2944
2945#[cfg(test)]
2946mod failure_hint_tests {
2947 use super::{failure_hint, rustup_missing_component};
2948
2949 #[test]
2950 fn detects_rustup_proxy_without_component() {
2951 let stderr = "error: Unknown binary 'rust-analyzer' in official toolchain 'stable-aarch64-apple-darwin'.";
2953 assert_eq!(
2954 rustup_missing_component(stderr).as_deref(),
2955 Some("rust-analyzer")
2956 );
2957 let hint = failure_hint("rust-analyzer", stderr);
2958 assert!(
2959 hint.contains("rustup component add rust-analyzer"),
2960 "expected actionable rustup hint, got: {hint}"
2961 );
2962 }
2963
2964 #[test]
2965 fn ignores_unknown_binary_without_toolchain_phrasing() {
2966 let stderr = "fatal: Unknown binary 'foo' was requested by the linker.";
2969 assert_eq!(rustup_missing_component(stderr), None);
2970 assert!(failure_hint("foo", stderr).starts_with("Hint: see stderr"));
2971 }
2972
2973 #[test]
2974 fn npm_module_not_found_still_wins() {
2975 let stderr = "Error: Cannot find module '/x/typescript-language-server/lib/cli.mjs'";
2977 let hint = failure_hint("typescript-language-server", stderr);
2978 assert!(hint.contains("install -g"), "got: {hint}");
2979 }
2980}
2981
2982#[cfg(test)]
2983mod diagnostic_capacity_tests {
2984 use std::fs;
2985
2986 use super::LspManager;
2987 use crate::config::Config;
2988
2989 #[test]
2994 fn set_diagnostic_capacity_propagates_to_store() {
2995 let mut manager = LspManager::new();
2996 manager.set_diagnostic_capacity(7);
2997 assert_eq!(manager.diagnostics_store_for_test().capacity_for_test(), 7);
2998 manager.set_diagnostic_capacity(0); assert_eq!(manager.diagnostics_store_for_test().capacity_for_test(), 0);
3000 }
3001
3002 #[test]
3005 fn clear_failed_spawns_empties_the_cache() {
3006 let mut manager = LspManager::new();
3007 assert_eq!(manager.clear_failed_spawns(), 0);
3008 manager.insert_failed_spawn_for_test();
3009 assert_eq!(manager.clear_failed_spawns(), 1);
3010 assert_eq!(manager.clear_failed_spawns(), 0);
3011 }
3012
3013 #[test]
3014 fn post_write_notification_does_not_start_a_cold_server() {
3015 let dir = tempfile::tempdir().unwrap();
3016 let file = dir.path().join("main.ts");
3017 fs::write(dir.path().join("package.json"), "{}").unwrap();
3018 fs::write(&file, "export const value = 1;\n").unwrap();
3019
3020 let mut manager = LspManager::new();
3021 manager
3022 .notify_file_changed_if_running(&file, "export const value = 1;\n", &Config::default())
3023 .unwrap();
3024 assert!(manager.clients.is_empty());
3025 }
3026}
3027
3028#[cfg(test)]
3029mod post_edit_waiter_tests {
3030 use std::collections::HashMap;
3031 use std::path::PathBuf;
3032 use std::time::{Duration, Instant};
3033
3034 use super::LspManager;
3035 use crate::lsp::client::LspEvent;
3036 use crate::lsp::registry::ServerKind;
3037
3038 #[test]
3039 fn draining_an_event_wakes_registered_post_edit_waiter() {
3040 let mut manager = LspManager::new();
3041 let mut wait = manager.start_post_edit_diagnostics_wait(
3042 PathBuf::from("/workspace/src/main.rs").as_path(),
3043 &[],
3044 &HashMap::new(),
3045 Duration::from_secs(2),
3046 );
3047 manager.enqueue_event_for_test(LspEvent::Notification {
3048 server_kind: ServerKind::Rust,
3049 root: PathBuf::from("/workspace"),
3050 method: "custom/drainedElsewhere".to_string(),
3051 params: None,
3052 });
3053
3054 assert_eq!(manager.drain_events().events.len(), 1);
3055 let started = Instant::now();
3056 assert!(wait.next_event().is_none());
3057 assert!(
3058 started.elapsed() < Duration::from_millis(250),
3059 "a competing drain did not wake the parked post-edit waiter"
3060 );
3061 let _ = manager.poll_post_edit_diagnostics_wait(&mut wait, None);
3062 let _ = manager.finish_post_edit_diagnostics_wait(wait);
3063 }
3064}
3065
3066#[cfg(test)]
3067mod clear_diagnostics_tests {
3068 use std::path::PathBuf;
3069
3070 use super::LspManager;
3071 use crate::lsp::client::LspEvent;
3072 use crate::lsp::diagnostics::{DiagnosticSeverity, StoredDiagnostic};
3073 use crate::lsp::position::uri_for_path;
3074 use crate::lsp::registry::ServerKind;
3075 use crate::lsp::roots::ServerKey;
3076
3077 fn err_diag(file: &PathBuf) -> StoredDiagnostic {
3078 StoredDiagnostic {
3079 file: file.clone(),
3080 line: 1,
3081 column: 1,
3082 end_line: 1,
3083 end_column: 2,
3084 severity: DiagnosticSeverity::Error,
3085 message: "boom".into(),
3086 code: None,
3087 source: None,
3088 }
3089 }
3090
3091 #[test]
3096 fn clear_diagnostics_for_deleted_file_matches_canonical_key() {
3097 let dir = tempfile::tempdir().unwrap();
3098 let canonical_dir = std::fs::canonicalize(dir.path()).unwrap();
3100 let canonical_file = canonical_dir.join("gone.ts");
3101 std::fs::write(&canonical_file, "x").unwrap();
3104
3105 let mut manager = LspManager::new();
3106 let key = ServerKey {
3107 kind: ServerKind::TypeScript,
3108 root: canonical_dir.clone(),
3109 };
3110 manager.diagnostics_store_mut_for_test().publish(
3111 key,
3112 canonical_file.clone(),
3113 vec![err_diag(&canonical_file)],
3114 );
3115 assert_eq!(manager.warm_error_warning_counts(), (1, 0));
3116
3117 std::fs::remove_file(&canonical_file).unwrap();
3118
3119 let watcher_path = dir.path().join("gone.ts");
3122 let removed = manager.clear_diagnostics_for_file(&watcher_path);
3123
3124 assert!(removed, "expected the deleted file's diagnostic to clear");
3125 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
3126 }
3127
3128 #[cfg(windows)]
3129 #[test]
3130 fn clear_diagnostics_for_deleted_file_matches_normalized_publish_key() {
3131 let dir = tempfile::tempdir().unwrap();
3132 let file = dir.path().join("normalized-gone.ts");
3133 std::fs::write(&file, "x").unwrap();
3134 let normalized_file = crate::inspect::job::canonicalize_normalized(&file);
3135
3136 let mut manager = LspManager::new();
3137 let key = ServerKey {
3138 kind: ServerKind::TypeScript,
3139 root: normalized_file.parent().unwrap().to_path_buf(),
3140 };
3141 manager.diagnostics_store_mut_for_test().publish(
3142 key,
3143 normalized_file.clone(),
3144 vec![err_diag(&normalized_file)],
3145 );
3146 std::fs::remove_file(&file).unwrap();
3147
3148 assert!(manager.clear_diagnostics_for_file(&file));
3149 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
3150 }
3151
3152 #[cfg(windows)]
3153 #[test]
3154 fn stale_diagnostics_for_deleted_file_matches_normalized_publish_key() {
3155 let dir = tempfile::tempdir().unwrap();
3156 let file = dir.path().join("normalized-stale.ts");
3157 std::fs::write(&file, "x").unwrap();
3158 let normalized_file = crate::inspect::job::canonicalize_normalized(&file);
3159
3160 let mut manager = LspManager::new();
3161 let key = ServerKey {
3162 kind: ServerKind::TypeScript,
3163 root: normalized_file.parent().unwrap().to_path_buf(),
3164 };
3165 manager.diagnostics_store_mut_for_test().publish(
3166 key,
3167 normalized_file.clone(),
3168 vec![err_diag(&normalized_file)],
3169 );
3170 std::fs::remove_file(&file).unwrap();
3171
3172 let result = manager.mark_diagnostics_stale_for_file(&file);
3173 assert!(result.had_entries);
3174 assert!(result.changed);
3175 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
3176 }
3177
3178 #[test]
3179 fn clear_diagnostics_for_unknown_file_is_noop() {
3180 let mut manager = LspManager::new();
3181 assert!(!manager.clear_diagnostics_for_file(&PathBuf::from("/nope/missing.ts")));
3182 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
3183 }
3184
3185 #[test]
3186 fn drain_events_reports_publish_diagnostics_updates() {
3187 let dir = tempfile::tempdir().unwrap();
3188 let root = std::fs::canonicalize(dir.path()).unwrap();
3189 let file = root.join("main.ts");
3190 std::fs::write(&file, "const x: number = 'nope';").unwrap();
3191
3192 let mut manager = LspManager::new();
3193 let diagnostic = lsp_types::Diagnostic {
3194 range: lsp_types::Range {
3195 start: lsp_types::Position {
3196 line: 0,
3197 character: 0,
3198 },
3199 end: lsp_types::Position {
3200 line: 0,
3201 character: 1,
3202 },
3203 },
3204 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
3205 code: None,
3206 code_description: None,
3207 source: Some("test".into()),
3208 message: "boom".into(),
3209 related_information: None,
3210 tags: None,
3211 data: None,
3212 };
3213 let params = serde_json::to_value(lsp_types::PublishDiagnosticsParams {
3214 uri: uri_for_path(&file).unwrap(),
3215 diagnostics: vec![diagnostic],
3216 version: Some(1),
3217 })
3218 .unwrap();
3219 manager
3220 .event_tx
3221 .send(LspEvent::Notification {
3222 server_kind: ServerKind::TypeScript,
3223 root,
3224 method: "textDocument/publishDiagnostics".into(),
3225 params: Some(params),
3226 })
3227 .unwrap();
3228
3229 let drained = manager.drain_events();
3230
3231 assert!(drained.diagnostics_changed);
3232 assert_eq!(drained.events.len(), 1);
3233 assert_eq!(manager.warm_error_warning_counts(), (1, 0));
3234 }
3235}
3236
3237#[cfg(test)]
3238mod inspect_path_tests {
3239 use super::LspManager;
3240 use crate::config::{Config, UserServerDef};
3241 use crate::lsp::registry::ServerKind;
3242
3243 #[test]
3244 fn applicability_resolution_does_not_start_or_open_a_server() {
3245 let temp_dir = tempfile::tempdir().expect("tempdir");
3246 let root = temp_dir.path().join("project");
3247 std::fs::create_dir_all(&root).expect("project root");
3248 std::fs::write(root.join("inspect-root.json"), "{}\n").expect("root marker");
3249 std::fs::write(root.join("input.inspectlang"), "value\n").expect("source file");
3250
3251 let config = Config {
3252 project_root: Some(root.clone()),
3253 lsp_servers: vec![UserServerDef {
3254 id: "inspect-test".to_string(),
3255 extensions: vec!["inspectlang".to_string()],
3256 binary: "inspect-test-lsp".to_string(),
3257 args: Vec::new(),
3258 root_markers: vec!["inspect-root.json".to_string()],
3259 env: Default::default(),
3260 initialization_options: None,
3261 disabled: false,
3262 }],
3263 ..Config::default()
3264 };
3265 let mut manager = LspManager::new();
3266 manager.override_binary(
3267 ServerKind::Custom("inspect-test".into()),
3268 std::env::current_exe().expect("current executable"),
3269 );
3270
3271 let snapshot = manager
3272 .resolve_applicable_servers_for_root(&root, &config)
3273 .expect("resolution succeeds without spawning");
3274 assert_eq!(snapshot.server_keys.len(), 1);
3275 assert_eq!(snapshot.server_keys[0].kind.id_str(), "inspect-test");
3276 assert_eq!(manager.server_count(), 0);
3277 assert!(!manager.document_is_open_for_test(&root.join("input.inspectlang")));
3278 }
3279
3280 #[test]
3281 fn applicability_resolution_preserves_an_empty_snapshot() {
3282 let temp_dir = tempfile::tempdir().expect("tempdir");
3283 let root = temp_dir.path().join("project");
3284 std::fs::create_dir_all(&root).expect("project root");
3285 std::fs::write(root.join("notes.txt"), "plain text\n").expect("fixture file");
3286
3287 let snapshot = LspManager::new()
3288 .resolve_applicable_servers_for_root(&root, &Config::default())
3289 .expect("an empty applicability set is valid");
3290
3291 assert!(snapshot.server_keys.is_empty());
3292 assert!(snapshot.candidates.is_empty());
3293 }
3294}