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