1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3use std::time::{Duration, Instant};
4
5use crossbeam_channel::{bounded, unbounded, Receiver, RecvTimeoutError, Sender, TrySendError};
6use lsp_types::notification::{
7 DidChangeTextDocument, DidChangeWatchedFiles, DidCloseTextDocument, DidOpenTextDocument,
8};
9use lsp_types::{
10 DidChangeTextDocumentParams, DidChangeWatchedFilesParams, DidCloseTextDocumentParams,
11 DidOpenTextDocumentParams, FileChangeType, FileEvent, TextDocumentContentChangeEvent,
12 TextDocumentIdentifier, TextDocumentItem, VersionedTextDocumentIdentifier,
13};
14
15use crate::alert_state::AcceptedDiagnosticSnapshot;
16use crate::config::Config;
17use crate::lsp::child_registry::LspChildRegistry;
18use crate::lsp::client::{LspClient, LspEvent, ReaderExitReap, ServerState};
19use crate::lsp::diagnostics::{
20 from_lsp_diagnostics, DiagnosticEntry, DiagnosticsStore, StoredDiagnostic,
21};
22use crate::lsp::document::DocumentStore;
23use crate::lsp::position::{uri_for_path, uri_to_path};
24use crate::lsp::pull_params::{
25 AftDocumentDiagnosticParams, AftDocumentDiagnosticRequest, AftWorkspaceDiagnosticParams,
26 AftWorkspaceDiagnosticRequest,
27};
28use crate::lsp::registry::{resolve_server_binary, servers_for_file, ServerDef, ServerKind};
29use crate::lsp::roots::ServerKey;
30use crate::lsp::LspError;
31use crate::slog_error;
32use crate::slog_info;
33
34const STDERR_REASON_BYTES: usize = 2 * 1024;
35const LSP_SHUTDOWN_ALL_BUDGET: Duration = Duration::from_secs(5);
37
38fn server_key_for_definition(
39 def: &ServerDef,
40 file_path: &Path,
41 config: &Config,
42) -> Option<ServerKey> {
43 def.workspace_root_for_file_with_project_root(file_path, config.project_root.as_deref())
44 .map(|root| ServerKey {
45 kind: def.kind.clone(),
46 root,
47 })
48}
49
50fn server_key_sort(left: &ServerKey, right: &ServerKey) -> std::cmp::Ordering {
51 left.kind
52 .id_str()
53 .cmp(right.kind.id_str())
54 .then(left.root.cmp(&right.root))
55}
56
57#[derive(Debug, Clone)]
62pub enum ServerAttemptResult {
63 Ok { server_key: ServerKey },
65 NoRootMarker { looked_for: Vec<String> },
68 BinaryNotInstalled { binary: String },
71 SpawnFailed { binary: String, reason: String },
73}
74
75#[derive(Debug, Clone)]
77pub struct ServerAttempt {
78 pub server_id: String,
80 pub server_name: String,
82 pub result: ServerAttemptResult,
83}
84
85#[derive(Debug, Clone, Default)]
91pub struct EnsureServerOutcomes {
92 pub successful: Vec<ServerKey>,
94 pub attempts: Vec<ServerAttempt>,
97}
98
99#[derive(Clone, Debug)]
104pub struct ApplicableServerSnapshot {
105 pub server_keys: Vec<ServerKey>,
106 candidates: Vec<ApplicableServerCandidate>,
107 producer_failures: Vec<ApplicableServerFailure>,
108}
109
110#[derive(Clone, Debug)]
111struct ApplicableServerCandidate {
112 key: ServerKey,
113 definition: ServerDef,
114 source_file: PathBuf,
115}
116
117#[derive(Clone, Debug)]
118pub enum ApplicabilityResolutionError {
119 RootUnreadable { root: PathBuf, reason: String },
120 RequestDeadline { root: PathBuf },
121}
122
123#[derive(Clone, Debug)]
124pub struct ApplicableServerFailure {
125 pub server_key: ServerKey,
126 pub result: ServerAttemptResult,
127}
128
129impl ApplicableServerFailure {
130 pub fn reason(&self) -> String {
131 self.result.failure_reason()
132 }
133}
134
135#[derive(Clone, Debug, Default)]
136pub struct ApplicableServerStartOutcomes {
137 pub successful: Vec<ServerKey>,
138 pub failures: Vec<ApplicableServerFailure>,
139 pub deadline_exceeded: Option<ServerKey>,
142}
143
144impl ServerAttemptResult {
145 pub fn failure_reason(&self) -> String {
146 match self {
147 Self::BinaryNotInstalled { binary } => format!("{binary} is unavailable"),
148 Self::SpawnFailed { reason, .. } => reason.clone(),
149 Self::NoRootMarker { looked_for } => {
150 format!(
151 "no workspace root marker found (looked for {})",
152 looked_for.join(", ")
153 )
154 }
155 Self::Ok { .. } => "server started successfully".to_string(),
156 }
157 }
158}
159
160impl EnsureServerOutcomes {
161 pub fn no_server_registered(&self) -> bool {
163 self.attempts.is_empty()
164 }
165
166 pub fn only_inapplicable_root_markers(&self) -> bool {
174 self.successful.is_empty()
175 && !self.attempts.is_empty()
176 && self
177 .attempts
178 .iter()
179 .all(|attempt| matches!(attempt.result, ServerAttemptResult::NoRootMarker { .. }))
180 }
181}
182
183#[derive(Debug, Clone, Default)]
193pub struct PostEditWaitOutcome {
194 pub accepted_snapshots: Vec<AcceptedDiagnosticSnapshot>,
199 pub diagnostics: Vec<StoredDiagnostic>,
203 pub pending_servers: Vec<ServerKey>,
207 pub exited_servers: Vec<ServerKey>,
211}
212
213#[derive(Debug, Clone, Copy, Default)]
215pub struct PreEditSnapshot {
216 pub epoch: u64,
217 pub document_version_at_capture: Option<i32>,
218}
219
220#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
221pub struct StaleDiagnosticsMark {
222 pub had_entries: bool,
223 pub changed: bool,
224}
225
226pub fn post_edit_entry_is_fresh(
227 entry: &DiagnosticEntry,
228 target_version: i32,
229 pre: PreEditSnapshot,
230) -> bool {
231 if entry.stale || entry.epoch <= pre.epoch {
232 return false;
233 }
234
235 match entry.version {
236 Some(version) => version >= target_version,
237 None => false,
242 }
243}
244
245impl PostEditWaitOutcome {
246 pub fn complete(&self) -> bool {
249 self.pending_servers.is_empty() && self.exited_servers.is_empty()
250 }
251}
252
253#[derive(Debug, Clone)]
255pub enum PullFileOutcome {
256 Full { diagnostic_count: usize },
258 Unchanged,
260 PartialNotSupported,
263 PullNotSupported,
266 RequestFailed { reason: String },
268}
269
270#[derive(Debug, Clone, Default)]
272pub struct EnsureFileOpenResult {
273 pub server_keys: Vec<ServerKey>,
274 pub newly_opened: Vec<ServerKey>,
276}
277
278impl EnsureFileOpenResult {
279 pub fn is_empty(&self) -> bool {
280 self.server_keys.is_empty()
281 }
282}
283
284#[derive(Debug, Clone)]
286pub struct PullFileResult {
287 pub server_key: ServerKey,
288 pub outcome: PullFileOutcome,
289}
290
291#[derive(Debug, Clone)]
293pub struct PullWorkspaceResult {
294 pub server_key: ServerKey,
295 pub files_reported: Vec<PathBuf>,
299 pub complete: bool,
301 pub cancelled: bool,
303 pub supports_workspace: bool,
307}
308
309pub struct DrainedLspEvents {
310 pub events: Vec<LspEvent>,
311 pub diagnostics_changed: bool,
312 pub accepted_snapshots: Vec<AcceptedDiagnosticSnapshot>,
318 pub has_more: bool,
319}
320
321pub(crate) struct PostEditDiagnosticsWait {
326 lookup_path: PathBuf,
327 expected_versions: Vec<(ServerKey, i32)>,
328 pre_snapshot: HashMap<ServerKey, PreEditSnapshot>,
329 event_rx: Receiver<LspEvent>,
330 wake_rx: Receiver<()>,
331 waiter_id: u64,
332 deadline: std::time::Instant,
333 fresh: HashMap<ServerKey, Vec<StoredDiagnostic>>,
334 exited: Vec<ServerKey>,
335}
336
337impl PostEditDiagnosticsWait {
338 pub(crate) fn deadline_reached(&self) -> bool {
339 std::time::Instant::now() >= self.deadline
340 }
341
342 pub(crate) fn next_event(&self) -> Option<LspEvent> {
343 let remaining = self
344 .deadline
345 .saturating_duration_since(std::time::Instant::now());
346 if remaining.is_zero() {
347 return None;
348 }
349
350 crossbeam_channel::select! {
351 recv(self.event_rx) -> event => event.ok(),
352 recv(self.wake_rx) -> _ => None,
353 default(remaining) => None,
354 }
355 }
356}
357
358impl IntoIterator for DrainedLspEvents {
359 type Item = LspEvent;
360 type IntoIter = std::vec::IntoIter<LspEvent>;
361
362 fn into_iter(self) -> Self::IntoIter {
363 self.events.into_iter()
364 }
365}
366
367#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
370pub struct LspShutdownAllOutcome {
371 pub graceful: usize,
372 pub forced: usize,
373 pub elapsed: Duration,
374}
375
376pub struct LspManager {
377 clients: HashMap<ServerKey, LspClient>,
379 server_binaries: HashMap<ServerKey, String>,
383 documents: HashMap<ServerKey, DocumentStore>,
385 diagnostics: DiagnosticsStore,
387 event_tx: Sender<LspEvent>,
389 event_rx: Receiver<LspEvent>,
390 post_edit_waiters: HashMap<u64, Sender<()>>,
394 next_post_edit_waiter_id: u64,
395 binary_overrides: HashMap<ServerKind, PathBuf>,
397 pushed_search_paths: Option<Vec<PathBuf>>,
400 extra_env: HashMap<String, String>,
404 failed_spawns: HashMap<ServerKey, ServerAttemptResult>,
419 watched_file_skip_logged: HashSet<ServerKey>,
422 #[cfg(windows)]
425 last_watched_file_notification_trace: String,
426 child_registry: LspChildRegistry,
430}
431
432impl LspManager {
433 pub fn new() -> Self {
434 let (event_tx, event_rx) = unbounded();
435 Self {
436 clients: HashMap::new(),
437 server_binaries: HashMap::new(),
438 documents: HashMap::new(),
439 diagnostics: DiagnosticsStore::new(),
440 event_tx,
441 event_rx,
442 post_edit_waiters: HashMap::new(),
443 next_post_edit_waiter_id: 0,
444 binary_overrides: HashMap::new(),
445 pushed_search_paths: None,
446 extra_env: HashMap::new(),
447 failed_spawns: HashMap::new(),
448 watched_file_skip_logged: HashSet::new(),
449 #[cfg(windows)]
450 last_watched_file_notification_trace: "no watched-file notification attempted"
451 .to_string(),
452 child_registry: LspChildRegistry::new(),
453 }
454 }
455
456 pub fn set_child_registry(&mut self, registry: LspChildRegistry) {
458 self.child_registry = registry;
459 }
460
461 pub fn set_extra_env(&mut self, key: &str, value: &str) {
465 self.extra_env.insert(key.to_string(), value.to_string());
466 }
467
468 pub fn server_count(&self) -> usize {
470 self.clients.len()
471 }
472
473 pub fn estimated_memory(&self) -> crate::memory::MemoryEstimate {
477 let mut bytes = 0u64;
478 let mut document_count = 0u64;
479 for documents in self.documents.values() {
480 let estimate = documents.estimated_memory();
481 bytes = bytes.saturating_add(estimate.estimated_bytes.unwrap_or(0));
482 document_count = document_count
483 .saturating_add(estimate.counts.get("documents").copied().unwrap_or(0));
484 }
485 let diagnostics = self.diagnostics.estimated_memory();
486 bytes = bytes.saturating_add(diagnostics.estimated_bytes.unwrap_or(0));
487 crate::memory::MemoryEstimate::estimated(bytes)
488 .count("servers", self.clients.len())
489 .count("document_stores", self.documents.len())
490 .count_u64("documents", document_count)
491 .count_u64(
492 "diagnostic_entries",
493 diagnostics
494 .counts
495 .get("diagnostic_entries")
496 .copied()
497 .unwrap_or(0),
498 )
499 .count_u64(
500 "diagnostics",
501 diagnostics.counts.get("diagnostics").copied().unwrap_or(0),
502 )
503 }
504
505 pub fn set_diagnostic_capacity(&mut self, capacity: usize) {
509 self.diagnostics.set_capacity(capacity);
510 }
511
512 pub fn override_binary(&mut self, kind: ServerKind, binary_path: PathBuf) {
514 self.binary_overrides.insert(kind, binary_path);
515 }
516
517 pub fn set_search_paths(&mut self, paths: Vec<PathBuf>) -> bool {
521 if self.pushed_search_paths.as_ref() == Some(&paths) {
522 return false;
523 }
524 self.pushed_search_paths = Some(paths);
525 self.clear_failed_spawns();
526 true
527 }
528
529 pub fn resolve_applicable_servers_for_root(
531 &self,
532 project_root: &Path,
533 config: &Config,
534 ) -> Result<ApplicableServerSnapshot, ApplicabilityResolutionError> {
535 self.resolve_applicable_servers(project_root, None, config, None)
536 }
537
538 pub fn resolve_applicable_servers_for_inspect(
543 &self,
544 project_root: &Path,
545 scope_roots: Option<&[PathBuf]>,
546 config: &Config,
547 deadline: std::time::Instant,
548 ) -> Result<ApplicableServerSnapshot, ApplicabilityResolutionError> {
549 self.resolve_applicable_servers(project_root, scope_roots, config, Some(deadline))
550 }
551
552 fn resolve_applicable_servers(
553 &self,
554 project_root: &Path,
555 scope_roots: Option<&[PathBuf]>,
556 config: &Config,
557 deadline: Option<std::time::Instant>,
558 ) -> Result<ApplicableServerSnapshot, ApplicabilityResolutionError> {
559 if !project_root.is_dir() {
560 return Err(ApplicabilityResolutionError::RootUnreadable {
561 root: project_root.to_path_buf(),
562 reason: "project root is not a directory".to_string(),
563 });
564 }
565
566 let mut candidates = HashMap::<ServerKey, ApplicableServerCandidate>::new();
567 let mut producer_failures = HashMap::<ServerKey, ApplicableServerFailure>::new();
568 let walker = ignore::WalkBuilder::new(project_root)
570 .same_file_system(true)
571 .standard_filters(true)
572 .add_custom_ignore_filename(".aftignore")
573 .filter_entry(|entry| {
574 !matches!(
575 entry.file_name().to_string_lossy().as_ref(),
576 ".git" | "node_modules" | "target" | "dist" | "build" | ".next" | ".turbo"
577 )
578 })
579 .build();
580
581 for entry in walker {
582 if deadline.is_some_and(|deadline| std::time::Instant::now() >= deadline) {
583 return Err(ApplicabilityResolutionError::RequestDeadline {
584 root: project_root.to_path_buf(),
585 });
586 }
587 let entry = entry.map_err(|error| ApplicabilityResolutionError::RootUnreadable {
588 root: project_root.to_path_buf(),
589 reason: error.to_string(),
590 })?;
591 if !entry
592 .file_type()
593 .is_some_and(|file_type| file_type.is_file())
594 {
595 continue;
596 }
597 let file = entry.path();
598 for definition in servers_for_file(file, config) {
599 let Some(key) = server_key_for_definition(&definition, file, config) else {
600 continue;
601 };
602 if candidates.contains_key(&key) || producer_failures.contains_key(&key) {
603 continue;
604 }
605 if let Some(result) = self.failed_spawns.get(&key) {
606 producer_failures.insert(
607 key.clone(),
608 ApplicableServerFailure {
609 server_key: key,
610 result: result.clone(),
611 },
612 );
613 continue;
614 }
615 if self.resolve_binary(&definition, &key.root, config).is_err() {
616 producer_failures.insert(
617 key.clone(),
618 ApplicableServerFailure {
619 server_key: key,
620 result: ServerAttemptResult::BinaryNotInstalled {
621 binary: definition.binary.clone(),
622 },
623 },
624 );
625 continue;
626 }
627 candidates.insert(
628 key.clone(),
629 ApplicableServerCandidate {
630 key,
631 definition,
632 source_file: file.to_path_buf(),
633 },
634 );
635 }
636 }
637
638 if let Some(scope_roots) = scope_roots.filter(|roots| !roots.is_empty()) {
639 let rust_roots = candidates
640 .keys()
641 .chain(producer_failures.keys())
642 .filter(|key| key.kind == ServerKind::Rust)
643 .map(|key| key.root.clone())
644 .collect::<HashSet<_>>();
645 let mut owners = HashSet::new();
646 for scope_root in scope_roots {
647 if let Some(owner) = rust_roots
648 .iter()
649 .filter(|root| scope_root.starts_with(root))
650 .max_by_key(|root| root.components().count())
651 {
652 owners.insert(owner.clone());
653 }
654 }
655 candidates.retain(|key, _| key.kind != ServerKind::Rust || owners.contains(&key.root));
656 producer_failures
657 .retain(|key, _| key.kind != ServerKind::Rust || owners.contains(&key.root));
658 }
659
660 let mut candidates = candidates.into_values().collect::<Vec<_>>();
661 candidates.sort_by(|left, right| server_key_sort(&left.key, &right.key));
662 let mut producer_failures = producer_failures.into_values().collect::<Vec<_>>();
663 producer_failures
664 .sort_by(|left, right| server_key_sort(&left.server_key, &right.server_key));
665 let mut server_keys = candidates
666 .iter()
667 .map(|candidate| candidate.key.clone())
668 .chain(
669 producer_failures
670 .iter()
671 .map(|failure| failure.server_key.clone()),
672 )
673 .collect::<Vec<_>>();
674 server_keys.sort_by(server_key_sort);
675 Ok(ApplicableServerSnapshot {
676 server_keys,
677 candidates,
678 producer_failures,
679 })
680 }
681
682 pub fn start_applicable_servers(
688 &mut self,
689 snapshot: &ApplicableServerSnapshot,
690 config: &Config,
691 ) -> ApplicableServerStartOutcomes {
692 self.start_applicable_servers_inner(snapshot, config, None)
693 }
694
695 pub fn start_applicable_server_until(
699 &mut self,
700 snapshot: &ApplicableServerSnapshot,
701 server: &ServerKey,
702 config: &Config,
703 deadline: std::time::Instant,
704 ) -> ApplicableServerStartOutcomes {
705 let single = ApplicableServerSnapshot {
706 server_keys: vec![server.clone()],
707 candidates: snapshot
708 .candidates
709 .iter()
710 .filter(|candidate| candidate.key == *server)
711 .cloned()
712 .collect(),
713 producer_failures: snapshot
714 .producer_failures
715 .iter()
716 .filter(|failure| failure.server_key == *server)
717 .cloned()
718 .collect(),
719 };
720 self.start_applicable_servers_inner(&single, config, Some(deadline))
721 }
722
723 fn start_applicable_servers_inner(
724 &mut self,
725 snapshot: &ApplicableServerSnapshot,
726 config: &Config,
727 deadline: Option<std::time::Instant>,
728 ) -> ApplicableServerStartOutcomes {
729 let mut outcomes = ApplicableServerStartOutcomes {
730 failures: snapshot.producer_failures.clone(),
731 ..ApplicableServerStartOutcomes::default()
732 };
733 for candidate in &snapshot.candidates {
734 let initialize_timeout = deadline
735 .map(|deadline| deadline.saturating_duration_since(std::time::Instant::now()));
736 if initialize_timeout.is_some_and(|remaining| remaining.is_zero()) {
737 outcomes.deadline_exceeded = Some(candidate.key.clone());
738 break;
739 }
740 if self.clients.contains_key(&candidate.key) {
741 outcomes.successful.push(candidate.key.clone());
742 continue;
743 }
744 match self.spawn_server_with_timeout(
745 &candidate.definition,
746 &candidate.key.root,
747 &candidate.source_file,
748 config,
749 initialize_timeout,
750 ) {
751 Ok(client) => {
752 self.clients.insert(candidate.key.clone(), client);
753 self.server_binaries
754 .insert(candidate.key.clone(), candidate.definition.binary.clone());
755 self.documents.entry(candidate.key.clone()).or_default();
756 outcomes.successful.push(candidate.key.clone());
757 }
758 Err(error) => {
759 if deadline.is_some_and(|deadline| std::time::Instant::now() >= deadline) {
760 outcomes.deadline_exceeded = Some(candidate.key.clone());
763 break;
764 }
765 let result = classify_spawn_error(&candidate.definition.binary, &error);
766 self.failed_spawns
767 .insert(candidate.key.clone(), result.clone());
768 outcomes.failures.push(ApplicableServerFailure {
769 server_key: candidate.key.clone(),
770 result,
771 });
772 }
773 }
774 }
775 outcomes
776 }
777
778 pub fn ensure_server_for_file(&mut self, file_path: &Path, config: &Config) -> Vec<ServerKey> {
785 self.ensure_server_for_file_detailed(file_path, config)
786 .successful
787 }
788
789 fn running_server_keys_for_file(&self, file_path: &Path, config: &Config) -> Vec<ServerKey> {
790 servers_for_file(file_path, config)
791 .into_iter()
792 .filter_map(|def| server_key_for_definition(&def, file_path, config))
793 .filter(|key| self.clients.contains_key(key))
794 .collect()
795 }
796
797 pub fn navigation_requires_deferred_execution(
802 &self,
803 file_path: &Path,
804 config: &Config,
805 ) -> bool {
806 let Ok(canonical_path) = canonicalize_for_lsp(file_path) else {
807 return false;
808 };
809 servers_for_file(&canonical_path, config)
810 .into_iter()
811 .filter_map(|definition| {
812 let key = server_key_for_definition(&definition, &canonical_path, config)?;
813 Some((definition, key))
814 })
815 .any(|(definition, key)| {
816 if let Some(client) = self.clients.get(&key) {
817 return client.state() != ServerState::Ready;
818 }
819 !self.failed_spawns.contains_key(&key)
820 && self.resolve_binary(&definition, &key.root, config).is_ok()
821 })
822 }
823
824 pub fn ensure_server_for_file_detailed(
832 &mut self,
833 file_path: &Path,
834 config: &Config,
835 ) -> EnsureServerOutcomes {
836 let defs = servers_for_file(file_path, config);
837 let mut outcomes = EnsureServerOutcomes::default();
838
839 for def in defs {
840 let server_id = def.kind.id_str().to_string();
841 let server_name = def.name.to_string();
842
843 let Some(key) = server_key_for_definition(&def, file_path, config) else {
844 outcomes.attempts.push(ServerAttempt {
845 server_id,
846 server_name,
847 result: ServerAttemptResult::NoRootMarker {
848 looked_for: def.root_markers.iter().map(|s| s.to_string()).collect(),
849 },
850 });
851 continue;
852 };
853
854 if !self.clients.contains_key(&key) {
855 if let Some(cached) = self.failed_spawns.get(&key) {
862 outcomes.attempts.push(ServerAttempt {
863 server_id,
864 server_name,
865 result: cached.clone(),
866 });
867 continue;
868 }
869
870 self.reap_unreferenced_children_for(&key);
875
876 match self.spawn_server(&def, &key.root, file_path, config) {
877 Ok(client) => {
878 self.clients.insert(key.clone(), client);
879 self.server_binaries.insert(key.clone(), def.binary.clone());
880 self.documents.entry(key.clone()).or_default();
881 }
882 Err(err) => {
883 slog_error!("failed to spawn {}: {}", def.name, err);
884 let result = classify_spawn_error(&def.binary, &err);
885 self.failed_spawns.insert(key.clone(), result.clone());
889 outcomes.attempts.push(ServerAttempt {
890 server_id,
891 server_name,
892 result,
893 });
894 continue;
895 }
896 }
897 }
898
899 outcomes.attempts.push(ServerAttempt {
900 server_id,
901 server_name,
902 result: ServerAttemptResult::Ok {
903 server_key: key.clone(),
904 },
905 });
906 outcomes.successful.push(key);
907 }
908
909 outcomes
910 }
911
912 pub fn ensure_server_for_file_default(&mut self, file_path: &Path) -> Vec<ServerKey> {
915 self.ensure_server_for_file(file_path, &Config::default())
916 }
917 pub fn ensure_file_open(
922 &mut self,
923 file_path: &Path,
924 config: &Config,
925 ) -> Result<EnsureFileOpenResult, LspError> {
926 let canonical_path = canonicalize_for_lsp(file_path)?;
927 let server_keys = self.ensure_server_for_file(&canonical_path, config);
928 if server_keys.is_empty() {
929 return Ok(EnsureFileOpenResult::default());
930 }
931
932 let uri = uri_for_path(&canonical_path)?;
933 let language_id = language_id_for_extension(
934 canonical_path
935 .extension()
936 .and_then(|ext| ext.to_str())
937 .unwrap_or_default(),
938 )
939 .to_string();
940 let needs_content = server_keys.iter().any(|key| {
941 !self
942 .documents
943 .get(key)
944 .is_some_and(|store| store.is_open(&canonical_path))
945 });
946 let initial_content = needs_content
947 .then(|| std::fs::read_to_string(&canonical_path).map_err(LspError::Io))
948 .transpose()?;
949 let mut newly_opened = Vec::new();
950
951 for key in &server_keys {
952 let already_open = self
953 .documents
954 .get(key)
955 .is_some_and(|store| store.is_open(&canonical_path));
956
957 if !already_open {
958 let content = initial_content
959 .as_ref()
960 .expect("content is loaded when any server needs didOpen");
961 let (send_result, sent) = if let Some(client) = self.clients.get_mut(key) {
962 (
963 client.send_notification::<DidOpenTextDocument>(
964 DidOpenTextDocumentParams {
965 text_document: TextDocumentItem::new(
966 uri.clone(),
967 language_id.clone(),
968 0,
969 content.clone(),
970 ),
971 },
972 ),
973 true,
974 )
975 } else {
976 (Ok(()), false)
977 };
978 if let Err(err) = send_result {
979 let _ = self.close_file_for_servers(&canonical_path, &newly_opened);
980 return Err(err);
981 }
982 if sent {
983 log_did_open_sent(key, &canonical_path, &language_id);
984 }
985 self.documents
986 .entry(key.clone())
987 .or_default()
988 .open(canonical_path.clone());
989 newly_opened.push(key.clone());
990 continue;
991 }
992
993 let drifted = self
1003 .documents
1004 .get(key)
1005 .is_some_and(|store| store.is_stale_on_disk(&canonical_path));
1006 if drifted {
1007 let content = match std::fs::read_to_string(&canonical_path) {
1008 Ok(content) => content,
1009 Err(err) => {
1010 let _ = self.close_file_for_servers(&canonical_path, &newly_opened);
1011 return Err(LspError::Io(err));
1012 }
1013 };
1014 let next_version = self
1015 .documents
1016 .get(key)
1017 .and_then(|store| store.version(&canonical_path))
1018 .map(|v| v + 1)
1019 .unwrap_or(1);
1020 let send_result = if let Some(client) = self.clients.get_mut(key) {
1021 client.send_notification::<DidChangeTextDocument>(DidChangeTextDocumentParams {
1022 text_document: VersionedTextDocumentIdentifier::new(
1023 uri.clone(),
1024 next_version,
1025 ),
1026 content_changes: vec![TextDocumentContentChangeEvent {
1027 range: None,
1028 range_length: None,
1029 text: content,
1030 }],
1031 })
1032 } else {
1033 Ok(())
1034 };
1035 if let Err(err) = send_result {
1036 let _ = self.close_file_for_servers(&canonical_path, &newly_opened);
1037 return Err(err);
1038 }
1039 if let Some(store) = self.documents.get_mut(key) {
1040 store.bump_version(&canonical_path);
1041 }
1042 }
1043 }
1044
1045 Ok(EnsureFileOpenResult {
1046 server_keys,
1047 newly_opened,
1048 })
1049 }
1050
1051 pub fn ensure_file_open_default(
1052 &mut self,
1053 file_path: &Path,
1054 ) -> Result<EnsureFileOpenResult, LspError> {
1055 self.ensure_file_open(file_path, &Config::default())
1056 }
1057
1058 pub fn notify_file_changed(
1064 &mut self,
1065 file_path: &Path,
1066 content: &str,
1067 config: &Config,
1068 ) -> Result<(), LspError> {
1069 self.notify_file_changed_versioned(file_path, content, config)
1070 .map(|_| ())
1071 }
1072
1073 pub fn notify_file_changed_versioned(
1084 &mut self,
1085 file_path: &Path,
1086 content: &str,
1087 config: &Config,
1088 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
1089 let canonical_path = canonicalize_for_lsp(file_path)?;
1090 let server_keys = self.ensure_server_for_file(&canonical_path, config);
1091 self.notify_file_changed_for_server_keys(canonical_path, content, server_keys)
1092 }
1093
1094 pub fn notify_file_changed_if_running(
1100 &mut self,
1101 file_path: &Path,
1102 content: &str,
1103 config: &Config,
1104 ) -> Result<(), LspError> {
1105 self.notify_file_changed_if_running_versioned(file_path, content, config)
1106 .map(|_| ())
1107 }
1108
1109 pub fn notify_file_changed_if_running_versioned(
1113 &mut self,
1114 file_path: &Path,
1115 content: &str,
1116 config: &Config,
1117 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
1118 let canonical_path = canonicalize_for_lsp(file_path)?;
1119 let server_keys = self.running_server_keys_for_file(&canonical_path, config);
1120 self.notify_file_changed_for_server_keys(canonical_path, content, server_keys)
1121 }
1122
1123 fn notify_file_changed_for_server_keys(
1124 &mut self,
1125 canonical_path: PathBuf,
1126 content: &str,
1127 server_keys: Vec<ServerKey>,
1128 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
1129 if server_keys.is_empty() {
1130 return Ok(Vec::new());
1131 }
1132
1133 let uri = uri_for_path(&canonical_path)?;
1134 let language_id = language_id_for_extension(
1135 canonical_path
1136 .extension()
1137 .and_then(|ext| ext.to_str())
1138 .unwrap_or_default(),
1139 )
1140 .to_string();
1141
1142 let mut versions: Vec<(ServerKey, i32)> = Vec::with_capacity(server_keys.len());
1143
1144 for key in server_keys {
1145 let current_version = self
1146 .documents
1147 .get(&key)
1148 .and_then(|store| store.version(&canonical_path));
1149
1150 if let Some(version) = current_version {
1151 let next_version = version + 1;
1152 if let Some(client) = self.clients.get_mut(&key) {
1153 client.send_notification::<DidChangeTextDocument>(
1154 DidChangeTextDocumentParams {
1155 text_document: VersionedTextDocumentIdentifier::new(
1156 uri.clone(),
1157 next_version,
1158 ),
1159 content_changes: vec![TextDocumentContentChangeEvent {
1160 range: None,
1161 range_length: None,
1162 text: content.to_string(),
1163 }],
1164 },
1165 )?;
1166 }
1167 if let Some(store) = self.documents.get_mut(&key) {
1168 store.bump_version(&canonical_path);
1169 }
1170 versions.push((key, next_version));
1171 continue;
1172 }
1173
1174 if let Some(client) = self.clients.get_mut(&key) {
1175 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
1176 text_document: TextDocumentItem::new(
1177 uri.clone(),
1178 language_id.clone(),
1179 0,
1180 content.to_string(),
1181 ),
1182 })?;
1183 log_did_open_sent(&key, &canonical_path, &language_id);
1184 }
1185 self.documents
1186 .entry(key.clone())
1187 .or_default()
1188 .open(canonical_path.clone());
1189 versions.push((key, 0));
1192 }
1193
1194 Ok(versions)
1195 }
1196
1197 pub fn notify_file_changed_default(
1198 &mut self,
1199 file_path: &Path,
1200 content: &str,
1201 ) -> Result<(), LspError> {
1202 self.notify_file_changed(file_path, content, &Config::default())
1203 }
1204
1205 pub fn notify_files_watched_changed(
1211 &mut self,
1212 paths: &[(PathBuf, FileChangeType)],
1213 _config: &Config,
1214 ) -> Result<(), LspError> {
1215 #[cfg(windows)]
1216 let mut trace = vec![format!(
1217 "input_paths={paths:?}; active_keys={:?}",
1218 self.clients.keys().collect::<Vec<_>>()
1219 )];
1220
1221 if paths.is_empty() {
1222 #[cfg(windows)]
1223 {
1224 trace.push("outcome=no-input-paths".to_string());
1225 self.last_watched_file_notification_trace = trace.join("\n");
1226 }
1227 return Ok(());
1228 }
1229
1230 let mut canonical_events = Vec::with_capacity(paths.len());
1231 for (path, typ) in paths {
1232 let canonical_path = resolve_for_lsp_uri(path);
1233 canonical_events.push((canonical_path, *typ));
1234 }
1235 #[cfg(windows)]
1236 trace.push(format!("resolved_events={canonical_events:?}"));
1237
1238 let keys: Vec<ServerKey> = self.clients.keys().cloned().collect();
1239 #[cfg(windows)]
1240 if keys.is_empty() {
1241 trace.push("outcome=no-active-client".to_string());
1242 }
1243 for key in keys {
1244 let mut changes = Vec::new();
1245 for (path, typ) in &canonical_events {
1246 if !path.starts_with(&key.root) {
1247 continue;
1248 }
1249 changes.push(FileEvent::new(uri_for_path(path)?, *typ));
1250 }
1251
1252 if changes.is_empty() {
1253 #[cfg(windows)]
1254 trace.push(format!("key={key:?}; outcome=outside-root"));
1255 continue;
1256 }
1257
1258 if let Some(client) = self.clients.get_mut(&key) {
1259 let supports_static_watched_files = client.supports_watched_files();
1265 let has_dynamic_registration = client.has_watched_file_registration();
1266 if !(supports_static_watched_files || has_dynamic_registration) {
1267 #[cfg(windows)]
1268 trace.push(format!(
1269 "key={key:?}; changes={changes:?}; outcome=unsupported; static={supports_static_watched_files}; dynamic={has_dynamic_registration}"
1270 ));
1271 if self.watched_file_skip_logged.insert(key.clone()) {
1272 log::debug!(
1273 "skipping didChangeWatchedFiles for {:?} (not supported or registered)",
1274 key
1275 );
1276 }
1277 continue;
1278 }
1279 #[cfg(windows)]
1280 trace.push(format!("key={key:?}; changes={changes:?}; action=send"));
1281 let send_result = client.send_notification::<DidChangeWatchedFiles>(
1282 DidChangeWatchedFilesParams { changes },
1283 );
1284 #[cfg(windows)]
1285 trace.push(format!(
1286 "key={key:?}; outcome={}",
1287 if send_result.is_ok() {
1288 "sent"
1289 } else {
1290 "send-error"
1291 }
1292 ));
1293 if let Err(error) = send_result {
1294 #[cfg(windows)]
1295 {
1296 self.last_watched_file_notification_trace = trace.join("\n");
1297 }
1298 return Err(error);
1299 }
1300 }
1301 }
1302
1303 #[cfg(windows)]
1304 {
1305 self.last_watched_file_notification_trace = trace.join("\n");
1306 }
1307 Ok(())
1308 }
1309
1310 pub fn notify_file_closed(&mut self, file_path: &Path) -> Result<(), LspError> {
1312 let canonical_path = canonicalize_for_lsp(file_path)?;
1313 let keys = self
1314 .documents
1315 .iter()
1316 .filter(|(_, store)| store.is_open(&canonical_path))
1317 .map(|(key, _)| key.clone())
1318 .collect::<Vec<_>>();
1319 self.close_file_for_servers(&canonical_path, &keys)
1320 }
1321
1322 pub(crate) fn close_file_for_servers(
1327 &mut self,
1328 file_path: &Path,
1329 server_keys: &[ServerKey],
1330 ) -> Result<(), LspError> {
1331 let canonical_path = canonicalize_for_lsp(file_path)?;
1332 let uri = uri_for_path(&canonical_path)?;
1333 let mut first_error = None;
1334
1335 for key in server_keys {
1336 let was_open = self
1337 .documents
1338 .get(key)
1339 .is_some_and(|store| store.is_open(&canonical_path));
1340 if !was_open {
1341 continue;
1342 }
1343
1344 if let Some(client) = self.clients.get_mut(key) {
1345 if let Err(err) =
1346 client.send_notification::<DidCloseTextDocument>(DidCloseTextDocumentParams {
1347 text_document: TextDocumentIdentifier::new(uri.clone()),
1348 })
1349 {
1350 if first_error.is_none() {
1351 first_error = Some(err);
1352 }
1353 }
1354 }
1355
1356 if let Some(store) = self.documents.get_mut(key) {
1357 store.close(&canonical_path);
1358 }
1359 self.diagnostics.clear_for_server_file(key, &canonical_path);
1360 }
1361
1362 match first_error {
1363 Some(err) => Err(err),
1364 None => Ok(()),
1365 }
1366 }
1367
1368 pub fn client_for_file(&self, file_path: &Path, config: &Config) -> Option<&LspClient> {
1370 let key = self.server_key_for_file(file_path, config)?;
1371 self.clients.get(&key)
1372 }
1373
1374 pub fn client_for_file_default(&self, file_path: &Path) -> Option<&LspClient> {
1375 self.client_for_file(file_path, &Config::default())
1376 }
1377
1378 pub fn client_for_file_mut(
1380 &mut self,
1381 file_path: &Path,
1382 config: &Config,
1383 ) -> Option<&mut LspClient> {
1384 let key = self.server_key_for_file(file_path, config)?;
1385 self.clients.get_mut(&key)
1386 }
1387
1388 pub fn client_for_file_mut_default(&mut self, file_path: &Path) -> Option<&mut LspClient> {
1389 self.client_for_file_mut(file_path, &Config::default())
1390 }
1391
1392 pub fn active_client_count(&self) -> usize {
1394 self.clients.len()
1395 }
1396
1397 pub fn drain_events(&mut self) -> DrainedLspEvents {
1399 self.drain_events_bounded(usize::MAX)
1400 }
1401
1402 pub fn has_pending_events(&self) -> bool {
1405 !self.event_rx.is_empty()
1406 }
1407
1408 pub fn drain_events_bounded(&mut self, max_events: usize) -> DrainedLspEvents {
1409 let mut events = Vec::new();
1410 let mut diagnostics_changed = false;
1411 let mut accepted_snapshots = Vec::new();
1412 while events.len() < max_events {
1413 let Ok(event) = self.event_rx.try_recv() else {
1414 break;
1415 };
1416 if self.handle_event(&event).is_some() {
1417 diagnostics_changed = true;
1418 }
1419 if let Some(snapshot) = self.accepted_live_publish_snapshot(&event) {
1420 accepted_snapshots.push(snapshot);
1421 }
1422 events.push(event);
1423 }
1424 let has_more = events.len() >= max_events && !self.event_rx.is_empty();
1425 DrainedLspEvents {
1426 events,
1427 diagnostics_changed,
1428 accepted_snapshots,
1429 has_more,
1430 }
1431 }
1432
1433 pub fn wait_for_diagnostics(
1435 &mut self,
1436 file_path: &Path,
1437 config: &Config,
1438 timeout: std::time::Duration,
1439 ) -> Vec<StoredDiagnostic> {
1440 let deadline = std::time::Instant::now() + timeout;
1441 self.wait_for_file_diagnostics(file_path, config, deadline)
1442 }
1443
1444 pub fn wait_for_diagnostics_default(
1445 &mut self,
1446 file_path: &Path,
1447 timeout: std::time::Duration,
1448 ) -> Vec<StoredDiagnostic> {
1449 self.wait_for_diagnostics(file_path, &Config::default(), timeout)
1450 }
1451
1452 #[doc(hidden)]
1457 pub fn diagnostics_store_for_test(&self) -> &DiagnosticsStore {
1458 &self.diagnostics
1459 }
1460
1461 #[doc(hidden)]
1462 pub fn diagnostics_store_mut_for_test(&mut self) -> &mut DiagnosticsStore {
1463 &mut self.diagnostics
1464 }
1465
1466 #[doc(hidden)]
1467 pub fn post_edit_outcome_for_entry_for_test(
1468 key: ServerKey,
1469 entry: &DiagnosticEntry,
1470 target_version: i32,
1471 pre: PreEditSnapshot,
1472 ) -> PostEditWaitOutcome {
1473 Self::post_edit_outcome_for_entry(key, entry, target_version, pre)
1474 }
1475
1476 fn post_edit_outcome_for_entry(
1477 key: ServerKey,
1478 entry: &DiagnosticEntry,
1479 target_version: i32,
1480 pre: PreEditSnapshot,
1481 ) -> PostEditWaitOutcome {
1482 let mut fresh = HashMap::new();
1483 if let Some(diagnostics) =
1484 Self::authoritative_post_edit_diagnostics(entry, target_version, pre)
1485 {
1486 fresh.insert(key.clone(), diagnostics);
1487 }
1488 Self::post_edit_outcome(vec![(key, target_version)], fresh, Vec::new())
1489 }
1490
1491 fn authoritative_post_edit_diagnostics(
1492 entry: &DiagnosticEntry,
1493 target_version: i32,
1494 pre: PreEditSnapshot,
1495 ) -> Option<Vec<StoredDiagnostic>> {
1496 (!entry.provisional && post_edit_entry_is_fresh(entry, target_version, pre))
1497 .then(|| entry.diagnostics.clone())
1498 }
1499
1500 #[doc(hidden)]
1501 pub fn enqueue_event_for_test(&self, event: LspEvent) {
1502 self.event_tx
1503 .send(event)
1504 .expect("LSP event receiver should remain connected");
1505 }
1506
1507 #[cfg(all(test, unix))]
1509 pub(crate) fn event_sender_for_test(&self) -> Sender<LspEvent> {
1510 self.event_tx.clone()
1511 }
1512
1513 #[cfg(all(test, unix))]
1515 pub(crate) fn insert_client_for_test(&mut self, client: LspClient) {
1516 let key = ServerKey {
1517 kind: client.kind(),
1518 root: client.root().to_path_buf(),
1519 };
1520 self.clients.insert(key, client);
1521 }
1522
1523 #[doc(hidden)]
1524 pub fn pending_event_count_for_test(&self) -> usize {
1525 self.event_rx.len()
1526 }
1527
1528 #[doc(hidden)]
1529 pub fn document_is_open_for_test(&self, file_path: &Path) -> bool {
1530 canonicalize_for_lsp(file_path).is_ok_and(|canonical_path| {
1531 self.documents
1532 .values()
1533 .any(|store| store.is_open(&canonical_path))
1534 })
1535 }
1536
1537 pub fn warm_error_warning_counts(&self) -> (usize, usize) {
1541 self.diagnostics.error_warning_counts()
1542 }
1543
1544 pub fn warm_error_warning_counts_with_provisional(&self) -> ((usize, usize), bool) {
1545 self.diagnostics.error_warning_counts_with_provisional()
1546 }
1547
1548 pub fn diagnostics_generation(&self) -> u64 {
1549 self.diagnostics.generation()
1550 }
1551
1552 pub fn filtered_error_warning_counts(
1557 &self,
1558 keep: impl FnMut(&std::path::Path) -> bool,
1559 ) -> (usize, usize) {
1560 self.diagnostics.filtered_error_warning_counts(keep)
1561 }
1562
1563 pub fn filtered_error_warning_counts_with_provisional(
1567 &self,
1568 keep: impl FnMut(&std::path::Path) -> bool,
1569 ) -> ((usize, usize), bool) {
1570 self.diagnostics
1571 .filtered_error_warning_counts_with_provisional(keep)
1572 }
1573
1574 pub fn provisional_server_keys(&self) -> Vec<ServerKey> {
1578 self.clients
1579 .iter()
1580 .filter(|(_, client)| client.diagnostics_are_provisional())
1581 .map(|(key, _)| key.clone())
1582 .collect()
1583 }
1584
1585 pub fn snapshot_diagnostic_epochs(&self, file_path: &Path) -> HashMap<ServerKey, u64> {
1590 let lookup_path = normalize_lookup_path(file_path);
1591 self.diagnostics
1592 .entries_for_file(&lookup_path)
1593 .into_iter()
1594 .map(|(key, entry)| (key.clone(), entry.epoch))
1595 .collect()
1596 }
1597
1598 pub fn snapshot_pre_edit_state(&self, file_path: &Path) -> HashMap<ServerKey, PreEditSnapshot> {
1601 let lookup_path = normalize_lookup_path(file_path);
1602 let mut snapshots: HashMap<ServerKey, PreEditSnapshot> = self
1603 .diagnostics
1604 .entries_for_file(&lookup_path)
1605 .into_iter()
1606 .map(|(key, entry)| {
1607 (
1608 key.clone(),
1609 PreEditSnapshot {
1610 epoch: entry.epoch,
1611 document_version_at_capture: None,
1612 },
1613 )
1614 })
1615 .collect();
1616
1617 for (key, store) in &self.documents {
1618 if let Some(version) = store.version(&lookup_path) {
1619 snapshots
1620 .entry(key.clone())
1621 .or_default()
1622 .document_version_at_capture = Some(version);
1623 }
1624 }
1625
1626 snapshots
1627 }
1628
1629 pub fn diagnostic_entry_is_fresh_for_document(
1637 &self,
1638 file_path: &Path,
1639 server_key: &ServerKey,
1640 pre: PreEditSnapshot,
1641 ) -> bool {
1642 let lookup_path = normalize_lookup_path(file_path);
1643 let Some(entry) = self
1644 .diagnostics
1645 .entries_for_file(&lookup_path)
1646 .into_iter()
1647 .find_map(|(key, entry)| if key == server_key { Some(entry) } else { None })
1648 else {
1649 return false;
1650 };
1651
1652 if entry.stale {
1653 return false;
1654 }
1655
1656 let target_version = self
1657 .documents
1658 .get(server_key)
1659 .and_then(|store| store.version(&lookup_path))
1660 .or(pre.document_version_at_capture)
1661 .unwrap_or(0);
1662
1663 matches!(entry.version, Some(version) if version >= target_version)
1664 }
1665
1666 pub(crate) fn start_post_edit_diagnostics_wait(
1670 &mut self,
1671 file_path: &Path,
1672 expected_versions: &[(ServerKey, i32)],
1673 pre_snapshot: &HashMap<ServerKey, PreEditSnapshot>,
1674 timeout: std::time::Duration,
1675 ) -> PostEditDiagnosticsWait {
1676 let lookup_path = normalize_lookup_path(file_path);
1677
1678 let _ = self.drain_events_for_file(&lookup_path);
1681
1682 let waiter_id = self.next_post_edit_waiter_id;
1683 self.next_post_edit_waiter_id = self.next_post_edit_waiter_id.wrapping_add(1);
1684 let (wake_tx, wake_rx) = bounded(1);
1685 self.post_edit_waiters.insert(waiter_id, wake_tx);
1686
1687 PostEditDiagnosticsWait {
1688 lookup_path,
1689 expected_versions: expected_versions.to_vec(),
1690 pre_snapshot: pre_snapshot.clone(),
1691 event_rx: self.event_rx.clone(),
1692 wake_rx,
1693 waiter_id,
1694 deadline: std::time::Instant::now() + timeout,
1695 fresh: HashMap::new(),
1696 exited: Vec::new(),
1697 }
1698 }
1699
1700 pub(crate) fn poll_post_edit_diagnostics_wait(
1701 &mut self,
1702 wait: &mut PostEditDiagnosticsWait,
1703 event: Option<LspEvent>,
1704 ) -> bool {
1705 if let Some(event) = event {
1706 self.handle_event(&event);
1707 }
1708
1709 for (key, target_version) in &wait.expected_versions {
1710 if wait.fresh.contains_key(key) || wait.exited.contains(key) {
1711 continue;
1712 }
1713 if !self.clients.contains_key(key) {
1714 wait.exited.push(key.clone());
1715 continue;
1716 }
1717 if let Some(entry) = self
1718 .diagnostics
1719 .entries_for_file(&wait.lookup_path)
1720 .into_iter()
1721 .find_map(|(stored_key, entry)| (stored_key == key).then_some(entry))
1722 {
1723 let pre = wait.pre_snapshot.get(key).copied().unwrap_or_default();
1724 if let Some(diagnostics) =
1725 Self::authoritative_post_edit_diagnostics(entry, *target_version, pre)
1726 {
1727 wait.fresh.insert(key.clone(), diagnostics);
1728 }
1729 }
1730 }
1731
1732 wait.fresh.len() + wait.exited.len() == wait.expected_versions.len()
1733 }
1734
1735 pub(crate) fn finish_post_edit_diagnostics_wait(
1736 &mut self,
1737 wait: PostEditDiagnosticsWait,
1738 ) -> PostEditWaitOutcome {
1739 self.post_edit_waiters.remove(&wait.waiter_id);
1740 Self::post_edit_outcome(wait.expected_versions, wait.fresh, wait.exited)
1741 }
1742
1743 pub fn wait_for_post_edit_diagnostics(
1751 &mut self,
1752 file_path: &Path,
1753 _config: &Config,
1757 expected_versions: &[(ServerKey, i32)],
1758 pre_snapshot: &HashMap<ServerKey, PreEditSnapshot>,
1759 timeout: std::time::Duration,
1760 ) -> PostEditWaitOutcome {
1761 let mut wait = self.start_post_edit_diagnostics_wait(
1762 file_path,
1763 expected_versions,
1764 pre_snapshot,
1765 timeout,
1766 );
1767 let mut complete = self.poll_post_edit_diagnostics_wait(&mut wait, None);
1768
1769 while !complete && !wait.deadline_reached() {
1770 let event = wait.next_event();
1771 complete = self.poll_post_edit_diagnostics_wait(&mut wait, event);
1772 }
1773
1774 self.finish_post_edit_diagnostics_wait(wait)
1775 }
1776
1777 fn post_edit_outcome(
1778 mut expected: Vec<(ServerKey, i32)>,
1779 mut fresh: HashMap<ServerKey, Vec<StoredDiagnostic>>,
1780 exited: Vec<ServerKey>,
1781 ) -> PostEditWaitOutcome {
1782 expected.sort_by(|(left, _), (right, _)| server_key_sort(left, right));
1783
1784 let mut accepted_snapshots = Vec::new();
1785 let mut pending_servers = Vec::new();
1786 for (server_key, document_version) in expected {
1787 if let Some(diagnostics) = fresh.remove(&server_key) {
1788 accepted_snapshots.push(AcceptedDiagnosticSnapshot::new(
1789 server_key,
1790 document_version,
1791 diagnostics,
1792 ));
1793 } else if !exited.contains(&server_key) {
1794 pending_servers.push(server_key);
1795 }
1796 }
1797
1798 let mut diagnostics = accepted_snapshots
1799 .iter()
1800 .flat_map(|snapshot| snapshot.diagnostics.iter().cloned())
1801 .collect::<Vec<_>>();
1802 diagnostics.sort_by(|left, right| {
1803 left.file
1804 .cmp(&right.file)
1805 .then(left.line.cmp(&right.line))
1806 .then(left.column.cmp(&right.column))
1807 .then(left.message.cmp(&right.message))
1808 });
1809
1810 PostEditWaitOutcome {
1811 accepted_snapshots,
1812 diagnostics,
1813 pending_servers,
1814 exited_servers: exited,
1815 }
1816 }
1817
1818 pub fn wait_for_file_diagnostics(
1824 &mut self,
1825 file_path: &Path,
1826 config: &Config,
1827 deadline: std::time::Instant,
1828 ) -> Vec<StoredDiagnostic> {
1829 let lookup_path = normalize_lookup_path(file_path);
1830
1831 if self.server_key_for_file(&lookup_path, config).is_none() {
1832 return Vec::new();
1833 }
1834
1835 loop {
1836 if self.drain_events_for_file(&lookup_path) {
1837 break;
1838 }
1839
1840 let now = std::time::Instant::now();
1841 if now >= deadline {
1842 break;
1843 }
1844
1845 let timeout = deadline.saturating_duration_since(now);
1846 match self.event_rx.recv_timeout(timeout) {
1847 Ok(event) => {
1848 if matches!(
1849 self.handle_event(&event),
1850 Some(ref published_file) if published_file.as_path() == lookup_path.as_path()
1851 ) {
1852 break;
1853 }
1854 }
1855 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
1856 }
1857 }
1858
1859 self.get_diagnostics_for_file(&lookup_path)
1860 .into_iter()
1861 .cloned()
1862 .collect()
1863 }
1864
1865 pub const PULL_FILE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
1871
1872 pub fn pull_file_timeout() -> std::time::Duration {
1874 Self::PULL_FILE_TIMEOUT
1875 }
1876
1877 const PULL_WORKSPACE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
1881
1882 pub fn pull_file_diagnostics(
1893 &mut self,
1894 file_path: &Path,
1895 config: &Config,
1896 ) -> Result<Vec<PullFileResult>, LspError> {
1897 self.pull_file_diagnostics_inner(file_path, config, None)
1898 }
1899
1900 pub fn pull_file_diagnostics_with_timeout(
1904 &mut self,
1905 file_path: &Path,
1906 config: &Config,
1907 timeout: Duration,
1908 ) -> Result<Vec<PullFileResult>, LspError> {
1909 self.pull_file_diagnostics_inner(file_path, config, Some(Instant::now() + timeout))
1910 }
1911
1912 fn pull_file_diagnostics_inner(
1913 &mut self,
1914 file_path: &Path,
1915 config: &Config,
1916 deadline: Option<Instant>,
1917 ) -> Result<Vec<PullFileResult>, LspError> {
1918 let canonical_path = canonicalize_for_lsp(file_path)?;
1919 let opened = self.ensure_file_open(&canonical_path, config)?;
1922 if opened.server_keys.is_empty() {
1923 return Ok(Vec::new());
1924 }
1925
1926 let uri = uri_for_path(&canonical_path)?;
1927 let mut results = Vec::with_capacity(opened.server_keys.len());
1928
1929 for key in opened.server_keys {
1930 let supports_pull = self
1931 .clients
1932 .get(&key)
1933 .and_then(|c| c.diagnostic_capabilities())
1934 .is_some_and(|caps| caps.pull_diagnostics);
1935
1936 if !supports_pull {
1937 results.push(PullFileResult {
1938 server_key: key.clone(),
1939 outcome: PullFileOutcome::PullNotSupported,
1940 });
1941 continue;
1942 }
1943
1944 let previous_result_id = self
1946 .diagnostics
1947 .entries_for_file(&canonical_path)
1948 .into_iter()
1949 .find(|(k, _)| **k == key)
1950 .and_then(|(_, entry)| entry.result_id.clone());
1951
1952 let identifier = self
1953 .clients
1954 .get(&key)
1955 .and_then(|c| c.diagnostic_capabilities())
1956 .and_then(|caps| caps.identifier.clone());
1957
1958 let params = AftDocumentDiagnosticParams {
1959 text_document: lsp_types::TextDocumentIdentifier { uri: uri.clone() },
1960 identifier,
1961 previous_result_id,
1962 work_done_progress_params: Default::default(),
1963 partial_result_params: Default::default(),
1964 };
1965
1966 let request_timeout = deadline
1967 .map(|deadline| {
1968 deadline
1969 .saturating_duration_since(Instant::now())
1970 .min(Self::PULL_FILE_TIMEOUT)
1971 })
1972 .unwrap_or(Self::PULL_FILE_TIMEOUT);
1973 let document_version = self
1974 .documents
1975 .get(&key)
1976 .and_then(|store| store.version(&canonical_path));
1977 slog_info!(
1978 "lsp_protocol server={} root={} method=textDocument/diagnostic event=sent file={} document_version={}",
1979 key.kind.id_str(),
1980 key.root.display(),
1981 canonical_path.display(),
1982 document_version
1983 .map(|version| version.to_string())
1984 .unwrap_or_else(|| "none".to_string())
1985 );
1986 let outcome = match self.send_pull_request(&key, params, request_timeout) {
1987 Ok(report) => {
1988 let report_kind = match &report {
1989 lsp_types::DocumentDiagnosticReportResult::Report(
1990 lsp_types::DocumentDiagnosticReport::Full(_),
1991 ) => "full",
1992 lsp_types::DocumentDiagnosticReportResult::Report(
1993 lsp_types::DocumentDiagnosticReport::Unchanged(_),
1994 ) => "unchanged",
1995 lsp_types::DocumentDiagnosticReportResult::Partial(_) => "partial",
1996 };
1997 slog_info!(
1998 "lsp_protocol server={} root={} method=textDocument/diagnostic event=received file={} report_kind={}",
1999 key.kind.id_str(),
2000 key.root.display(),
2001 canonical_path.display(),
2002 report_kind
2003 );
2004 if matches!(
2005 &report,
2006 lsp_types::DocumentDiagnosticReportResult::Report(
2007 lsp_types::DocumentDiagnosticReport::Full(_)
2008 )
2009 ) {
2010 self.drain_events();
2015 }
2016 self.ingest_document_report(&key, &canonical_path, document_version, report)
2017 }
2018 Err(err) => {
2019 slog_info!(
2020 "lsp_protocol server={} root={} method=textDocument/diagnostic event=failed file={} error={}",
2021 key.kind.id_str(),
2022 key.root.display(),
2023 canonical_path.display(),
2024 err
2025 );
2026 if let Some(result) = self.cache_post_initialize_exit(&key, &err) {
2027 PullFileOutcome::RequestFailed {
2028 reason: server_attempt_result_reason(&result),
2029 }
2030 } else if recoverable_pull_rejection(&err)
2031 && self.clients.get(&key).is_some_and(|client| {
2032 matches!(
2033 client.state(),
2034 ServerState::Ready | ServerState::Initializing
2035 )
2036 })
2037 {
2038 PullFileOutcome::RequestFailed {
2039 reason: format!("pull_rejected_push_fallback: {err}"),
2040 }
2041 } else {
2042 PullFileOutcome::RequestFailed {
2043 reason: err.to_string(),
2044 }
2045 }
2046 }
2047 };
2048
2049 results.push(PullFileResult {
2050 server_key: key,
2051 outcome,
2052 });
2053 }
2054
2055 Ok(results)
2056 }
2057
2058 pub fn pull_workspace_diagnostics(
2063 &mut self,
2064 server_key: &ServerKey,
2065 timeout: Option<std::time::Duration>,
2066 ) -> Result<PullWorkspaceResult, LspError> {
2067 let timeout = timeout.unwrap_or(Self::PULL_WORKSPACE_TIMEOUT);
2068
2069 let supports_workspace = self
2070 .clients
2071 .get(server_key)
2072 .and_then(|c| c.diagnostic_capabilities())
2073 .is_some_and(|caps| caps.workspace_diagnostics);
2074
2075 if !supports_workspace {
2076 return Ok(PullWorkspaceResult {
2077 server_key: server_key.clone(),
2078 files_reported: Vec::new(),
2079 complete: false,
2080 cancelled: false,
2081 supports_workspace: false,
2082 });
2083 }
2084
2085 let identifier = self
2086 .clients
2087 .get(server_key)
2088 .and_then(|c| c.diagnostic_capabilities())
2089 .and_then(|caps| caps.identifier.clone());
2090
2091 let params = AftWorkspaceDiagnosticParams {
2092 identifier,
2093 previous_result_ids: Vec::new(),
2094 work_done_progress_params: Default::default(),
2095 partial_result_params: Default::default(),
2096 };
2097
2098 let result = match self
2099 .clients
2100 .get_mut(server_key)
2101 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?
2102 .send_request_with_timeout::<AftWorkspaceDiagnosticRequest>(params, timeout)
2103 {
2104 Ok(result) => result,
2105 Err(LspError::Timeout(_)) => {
2106 return Ok(PullWorkspaceResult {
2107 server_key: server_key.clone(),
2108 files_reported: Vec::new(),
2109 complete: false,
2110 cancelled: true,
2111 supports_workspace: true,
2112 });
2113 }
2114 Err(err) => {
2115 if let Some(result) = self.cache_post_initialize_exit(server_key, &err) {
2116 return Err(LspError::ServerNotReady(server_attempt_result_reason(
2117 &result,
2118 )));
2119 }
2120 return Err(err);
2121 }
2122 };
2123
2124 let (items, complete) = match result {
2128 lsp_types::WorkspaceDiagnosticReportResult::Report(report) => (report.items, true),
2129 lsp_types::WorkspaceDiagnosticReportResult::Partial(partial) => (partial.items, false),
2130 };
2131
2132 let mut files_reported = Vec::with_capacity(items.len());
2134 for item in items {
2135 match item {
2136 lsp_types::WorkspaceDocumentDiagnosticReport::Full(full) => {
2137 if let Some(file) = uri_to_path(&full.uri) {
2138 let stored = from_lsp_diagnostics(
2139 file.clone(),
2140 full.full_document_diagnostic_report.items.clone(),
2141 );
2142 self.diagnostics.publish_with_result_id(
2143 server_key.clone(),
2144 file.clone(),
2145 stored,
2146 full.full_document_diagnostic_report.result_id.clone(),
2147 );
2148 files_reported.push(file);
2149 }
2150 }
2151 lsp_types::WorkspaceDocumentDiagnosticReport::Unchanged(_unchanged) => {
2152 }
2155 }
2156 }
2157
2158 Ok(PullWorkspaceResult {
2159 server_key: server_key.clone(),
2160 files_reported,
2161 complete,
2162 cancelled: false,
2163 supports_workspace: true,
2164 })
2165 }
2166
2167 fn cache_post_initialize_exit(
2168 &mut self,
2169 key: &ServerKey,
2170 err: &LspError,
2171 ) -> Option<ServerAttemptResult> {
2172 let binary = self
2173 .server_binaries
2174 .get(key)
2175 .cloned()
2176 .unwrap_or_else(|| key.kind.id_str().to_string());
2177 let (status, stderr_tail) = {
2178 let client = self.clients.get_mut(key)?;
2179 let mut status = client.child_exit_status();
2180 for _ in 0..10 {
2181 if status.is_some() {
2182 break;
2183 }
2184 std::thread::sleep(std::time::Duration::from_millis(10));
2185 status = client.child_exit_status();
2186 }
2187 let status = status?;
2188 wait_for_stderr_tail(client);
2189 (status, client.stderr_tail())
2190 };
2191 let reason = format_post_initialize_exit_reason(&binary, status, &stderr_tail, err);
2192 let result = ServerAttemptResult::SpawnFailed { binary, reason };
2193 self.clients.remove(key);
2194 self.server_binaries.remove(key);
2195 self.documents.remove(key);
2196 self.diagnostics.clear_for_server(key);
2197 self.failed_spawns.insert(key.clone(), result.clone());
2198 Some(result)
2199 }
2200
2201 fn send_pull_request(
2203 &mut self,
2204 key: &ServerKey,
2205 params: AftDocumentDiagnosticParams,
2206 timeout: Duration,
2207 ) -> Result<lsp_types::DocumentDiagnosticReportResult, LspError> {
2208 let client = self
2209 .clients
2210 .get_mut(key)
2211 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?;
2212 client.send_request_with_timeout::<AftDocumentDiagnosticRequest>(params, timeout)
2215 }
2216
2217 fn ingest_document_report(
2220 &mut self,
2221 key: &ServerKey,
2222 canonical_path: &Path,
2223 document_version: Option<i32>,
2224 result: lsp_types::DocumentDiagnosticReportResult,
2225 ) -> PullFileOutcome {
2226 let report = match result {
2227 lsp_types::DocumentDiagnosticReportResult::Report(report) => report,
2228 lsp_types::DocumentDiagnosticReportResult::Partial(_) => {
2229 return PullFileOutcome::PartialNotSupported;
2233 }
2234 };
2235
2236 match report {
2237 lsp_types::DocumentDiagnosticReport::Full(full) => {
2238 let result_id = full.full_document_diagnostic_report.result_id.clone();
2239 let stored = from_lsp_diagnostics(
2240 canonical_path.to_path_buf(),
2241 full.full_document_diagnostic_report.items.clone(),
2242 );
2243 let count = stored.len();
2244 let provisional = self
2245 .clients
2246 .get(key)
2247 .is_some_and(|client| client.diagnostics_are_provisional());
2248 self.diagnostics.publish_full_with_provisional(
2249 key.clone(),
2250 canonical_path.to_path_buf(),
2251 stored,
2252 result_id,
2253 document_version,
2254 provisional,
2255 );
2256 PullFileOutcome::Full {
2257 diagnostic_count: count,
2258 }
2259 }
2260 lsp_types::DocumentDiagnosticReport::Unchanged(_unchanged) => {
2261 if self
2268 .diagnostics
2269 .has_report_for_server_file(key, canonical_path)
2270 {
2271 if let Some(version) = document_version {
2272 self.diagnostics.confirm_for_server_file_version(
2273 key,
2274 canonical_path,
2275 version,
2276 );
2277 } else {
2278 self.diagnostics
2279 .mark_fresh_for_server_file(key, canonical_path);
2280 }
2281 let authoritative = self
2282 .clients
2283 .get(key)
2284 .map_or(true, |client| !client.diagnostics_are_provisional());
2285 if authoritative {
2286 self.diagnostics
2287 .clear_provisional_for_server_file(key, canonical_path);
2288 }
2289 PullFileOutcome::Unchanged
2290 } else {
2291 PullFileOutcome::RequestFailed {
2292 reason: "no_cache_for_unchanged".to_string(),
2293 }
2294 }
2295 }
2296 }
2297 }
2298
2299 pub fn take_all_clients(&mut self) -> Vec<(ServerKey, LspClient)> {
2303 let clients: Vec<_> = self.clients.drain().collect();
2304 self.server_binaries.clear();
2305 self.documents.clear();
2306 self.diagnostics = DiagnosticsStore::new();
2307 clients
2308 }
2309
2310 pub fn shutdown_all(&mut self) -> LspShutdownAllOutcome {
2312 let clients = self.take_all_clients();
2313 Self::shutdown_all_clients(
2314 clients,
2315 self.child_registry.clone(),
2316 LSP_SHUTDOWN_ALL_BUDGET,
2317 )
2318 }
2319
2320 fn shutdown_all_clients(
2321 clients: Vec<(ServerKey, LspClient)>,
2322 child_registry: LspChildRegistry,
2323 budget: Duration,
2324 ) -> LspShutdownAllOutcome {
2325 let started = Instant::now();
2326 let mut pending_pids = clients
2327 .iter()
2328 .map(|(_, client)| client.child_pid())
2329 .collect::<HashSet<_>>();
2330 let (result_tx, result_rx) = unbounded();
2331
2332 for (key, mut client) in clients {
2333 let pid = client.child_pid();
2334 let result_tx = result_tx.clone();
2335 std::thread::spawn(move || {
2336 let result = client.shutdown();
2337 drop(client);
2341 let _ = result_tx.send((key, pid, result));
2342 });
2343 }
2344 drop(result_tx);
2345
2346 let deadline = started + budget;
2347 let mut outcome = LspShutdownAllOutcome::default();
2348 while !pending_pids.is_empty() {
2349 let remaining = deadline.saturating_duration_since(Instant::now());
2350 match result_rx.recv_timeout(remaining) {
2351 Ok((key, pid, result)) => {
2352 if !pending_pids.remove(&pid) {
2353 continue;
2354 }
2355 match result {
2356 Ok(()) => outcome.graceful += 1,
2357 Err(err) => {
2358 outcome.forced += 1;
2359 slog_error!("error shutting down {:?}: {}", key, err);
2360 }
2361 }
2362 }
2363 Err(RecvTimeoutError::Timeout | RecvTimeoutError::Disconnected) => break,
2364 }
2365 }
2366
2367 if !pending_pids.is_empty() {
2368 let pids = pending_pids.into_iter().collect::<Vec<_>>();
2369 outcome.forced += pids.len();
2370 child_registry.reap_pids(&pids);
2374 }
2375
2376 outcome.elapsed = started.elapsed();
2377 slog_info!(
2378 "lsp shutdown_all: graceful={} forced={} elapsed_ms={}",
2379 outcome.graceful,
2380 outcome.forced,
2381 outcome.elapsed.as_millis()
2382 );
2383 outcome
2384 }
2385
2386 pub(crate) fn spawn_idle_lsp_reap(clients: Vec<(ServerKey, LspClient)>) {
2390 if clients.is_empty() {
2391 return;
2392 }
2393 let spawn_result = std::thread::Builder::new()
2394 .name("aft-lsp-idle-reap".into())
2395 .spawn(move || {
2396 for (key, mut client) in clients {
2397 if let Err(err) = client.shutdown_for_idle_reap() {
2398 slog_error!("error shutting down {:?}: {}", key, err);
2399 }
2400 }
2401 });
2402 if let Err(err) = spawn_result {
2403 slog_error!("failed to spawn idle LSP reap thread: {err}");
2404 }
2405 }
2406
2407 pub fn has_active_servers(&self) -> bool {
2409 self.clients
2410 .values()
2411 .any(|client| client.state() == ServerState::Ready)
2412 }
2413
2414 pub fn active_server_keys(&self) -> Vec<ServerKey> {
2417 self.clients.keys().cloned().collect()
2418 }
2419
2420 #[cfg(windows)]
2424 #[doc(hidden)]
2425 pub fn watched_file_notification_trace_for_test(&self) -> &str {
2426 &self.last_watched_file_notification_trace
2427 }
2428
2429 pub fn get_diagnostics_for_file(&self, file: &Path) -> Vec<&StoredDiagnostic> {
2430 let normalized = normalize_lookup_path(file);
2431 self.diagnostics.for_file(&normalized)
2432 }
2433
2434 pub fn get_diagnostics_for_file_with_provisional(
2435 &self,
2436 file: &Path,
2437 ) -> Vec<(&StoredDiagnostic, bool)> {
2438 let normalized = normalize_lookup_path(file);
2439 self.diagnostics.for_file_with_provisional(&normalized)
2440 }
2441
2442 pub fn clear_failed_spawns(&mut self) -> usize {
2463 let n = self.failed_spawns.len();
2464 self.failed_spawns.clear();
2465 n
2466 }
2467
2468 #[cfg(test)]
2469 pub(crate) fn insert_failed_spawn_for_test(&mut self) {
2470 let key = ServerKey {
2471 kind: crate::lsp::registry::ServerKind::Rust,
2472 root: std::path::PathBuf::from("/tmp/test-root"),
2473 };
2474 self.failed_spawns.insert(
2475 key,
2476 ServerAttemptResult::SpawnFailed {
2477 binary: "rust-analyzer".to_string(),
2478 reason: "test".to_string(),
2479 },
2480 );
2481 }
2482
2483 pub fn clear_diagnostics_for_file(&mut self, file: &Path) -> bool {
2484 diagnostic_path_candidates(file)
2485 .into_iter()
2486 .fold(false, |removed, candidate| {
2487 removed | self.diagnostics.clear_for_file(&candidate)
2488 })
2489 }
2490
2491 pub fn mark_diagnostics_stale_for_file(&mut self, file: &Path) -> StaleDiagnosticsMark {
2495 let mut result = StaleDiagnosticsMark::default();
2496 for candidate in diagnostic_path_candidates(file) {
2497 let (had_entries, changed) = self.diagnostics.mark_stale_for_file(&candidate);
2498 result.had_entries |= had_entries;
2499 result.changed |= changed;
2500 }
2501 result
2502 }
2503
2504 pub fn get_diagnostics_for_directory(&self, dir: &Path) -> Vec<&StoredDiagnostic> {
2505 let normalized = normalize_lookup_path(dir);
2506 self.diagnostics.for_directory(&normalized)
2507 }
2508
2509 pub fn get_diagnostics_for_directory_with_provisional(
2510 &self,
2511 dir: &Path,
2512 ) -> Vec<(&StoredDiagnostic, bool)> {
2513 let normalized = normalize_lookup_path(dir);
2514 self.diagnostics.for_directory_with_provisional(&normalized)
2515 }
2516
2517 pub fn get_all_diagnostics(&self) -> Vec<&StoredDiagnostic> {
2518 self.diagnostics.all()
2519 }
2520
2521 pub fn get_all_diagnostics_with_provisional(&self) -> Vec<(&StoredDiagnostic, bool)> {
2522 self.diagnostics.all_with_provisional()
2523 }
2524
2525 pub fn has_any_diagnostic_reports(&self) -> bool {
2530 self.diagnostics.has_any_fresh_report()
2531 }
2532
2533 pub fn has_diagnostic_report_for_file(&self, file: &Path) -> bool {
2537 let normalized = normalize_lookup_path(file);
2538 self.diagnostics.has_any_fresh_report_for_file(&normalized)
2539 }
2540
2541 pub fn has_diagnostic_report_for_server_file(&self, server: &ServerKey, file: &Path) -> bool {
2545 let normalized = normalize_lookup_path(file);
2546 self.diagnostics
2547 .has_fresh_report_for_server_file(server, &normalized)
2548 }
2549
2550 pub fn has_authoritative_report_for_file(&self, file: &Path) -> bool {
2555 let normalized = normalize_lookup_path(file);
2556 self.diagnostics
2557 .has_authoritative_report_for_file(&normalized)
2558 }
2559
2560 pub fn has_authoritative_report_for_server(&self, server: &ServerKey) -> bool {
2565 self.diagnostics.has_authoritative_report_for_server(server)
2566 }
2567
2568 pub fn server_is_warming(&self, server: &ServerKey) -> bool {
2573 self.clients
2574 .get(server)
2575 .is_some_and(|client| client.diagnostics_are_provisional())
2576 }
2577
2578 pub fn producer_has_settled(&self, server: &ServerKey) -> bool {
2583 self.has_authoritative_report_for_server(server) || !self.server_is_warming(server)
2584 }
2585
2586 pub fn producers_settled(&self, expected: &[ServerKey]) -> bool {
2590 expected
2591 .iter()
2592 .all(|server| self.producer_has_settled(server))
2593 }
2594
2595 fn drain_events_for_file(&mut self, file_path: &Path) -> bool {
2596 let mut saw_file_diagnostics = false;
2597 while let Ok(event) = self.event_rx.try_recv() {
2598 if matches!(
2599 self.handle_event(&event),
2600 Some(ref published_file) if published_file.as_path() == file_path
2601 ) {
2602 saw_file_diagnostics = true;
2603 }
2604 }
2605 saw_file_diagnostics
2606 }
2607
2608 fn accepted_live_publish_snapshot(
2609 &self,
2610 event: &LspEvent,
2611 ) -> Option<AcceptedDiagnosticSnapshot> {
2612 let LspEvent::Notification {
2613 server_kind,
2614 root,
2615 method,
2616 params: Some(params),
2617 } = event
2618 else {
2619 return None;
2620 };
2621 if method != "textDocument/publishDiagnostics" {
2622 return None;
2623 }
2624
2625 let publish_params =
2626 serde_json::from_value::<lsp_types::PublishDiagnosticsParams>(params.clone()).ok()?;
2627 let file = uri_to_path(&publish_params.uri)?;
2628 let server_key = ServerKey {
2629 kind: server_kind.clone(),
2630 root: root.clone(),
2631 };
2632 if self.live_publish_drop_reason(&server_key, &file).is_some() {
2633 return None;
2634 }
2635 let document_version = self.documents.get(&server_key)?.version(&file)?;
2636 let entry = self
2637 .diagnostics
2638 .entries_for_file(&file)
2639 .into_iter()
2640 .find_map(|(stored_key, entry)| (stored_key == &server_key).then_some(entry))?;
2641
2642 Some(AcceptedDiagnosticSnapshot::new(
2643 server_key,
2644 document_version,
2645 entry.diagnostics.clone(),
2646 ))
2647 }
2648
2649 fn live_publish_drop_reason(&self, key: &ServerKey, file: &Path) -> Option<&'static str> {
2650 let Some(client) = self.clients.get(key) else {
2651 return Some("server-not-live");
2652 };
2653 if client.state() != ServerState::Ready {
2654 return Some("server-not-ready");
2655 }
2656 if client.diagnostics_are_provisional() {
2657 return Some("producer-warming");
2658 }
2659 let Some(document_version) = self
2660 .documents
2661 .get(key)
2662 .and_then(|documents| documents.version(file))
2663 else {
2664 return Some("document-not-open");
2665 };
2666 let Some(entry) = self
2667 .diagnostics
2668 .entries_for_file(file)
2669 .into_iter()
2670 .find_map(|(stored_key, entry)| (stored_key == key).then_some(entry))
2671 else {
2672 return Some("report-not-stored");
2673 };
2674 if entry.stale {
2675 return Some("report-stale");
2676 }
2677 if entry.provisional {
2678 return Some("report-provisional");
2679 }
2680 match entry.version {
2681 None => Some("missing-version"),
2682 Some(version) if version != document_version => Some("version-mismatch"),
2683 Some(_) => None,
2684 }
2685 }
2686
2687 fn handle_event(&mut self, event: &LspEvent) -> Option<PathBuf> {
2688 let published_file = match event {
2689 LspEvent::Notification {
2690 server_kind,
2691 root,
2692 method,
2693 params: Some(params),
2694 } if method == "textDocument/publishDiagnostics" => {
2695 self.handle_publish_diagnostics(server_kind.clone(), root.clone(), params)
2696 }
2697 LspEvent::Notification {
2698 server_kind,
2699 root,
2700 method,
2701 params: Some(params),
2702 } if method == "experimental/serverStatus" => {
2703 self.handle_server_status(server_kind.clone(), root.clone(), params);
2704 None
2705 }
2706 LspEvent::ServerExited {
2707 server_kind,
2708 root,
2709 reason,
2710 } => {
2711 let key = ServerKey {
2712 kind: server_kind.clone(),
2713 root: root.clone(),
2714 };
2715 if let Some(mut client) = self.clients.remove(&key) {
2716 match client.reap_after_reader_exit(reason) {
2717 ReaderExitReap::AlreadyExited(status) => {
2718 slog_info!(
2719 "exited {:?} {}: exit status {status} ({reason})",
2720 server_kind,
2721 root.display()
2722 );
2723 }
2724 ReaderExitReap::KilledWhileAlive => {
2725 slog_info!(
2726 "exited {:?} {}: reader ended while child alive: {reason}",
2727 server_kind,
2728 root.display()
2729 );
2730 }
2731 }
2732 }
2733 self.server_binaries.remove(&key);
2734 self.documents.remove(&key);
2735 self.diagnostics.clear_for_server(&key);
2736 None
2737 }
2738 _ => None,
2739 };
2740 self.wake_post_edit_waiters();
2741 published_file
2742 }
2743
2744 fn wake_post_edit_waiters(&mut self) {
2745 Self::wake_waiters(&mut self.post_edit_waiters);
2746 }
2747
2748 fn wake_waiters(waiters: &mut HashMap<u64, Sender<()>>) {
2749 waiters.retain(|_, sender| match sender.try_send(()) {
2750 Ok(()) | Err(TrySendError::Full(())) => true,
2751 Err(TrySendError::Disconnected(())) => false,
2752 });
2753 }
2754
2755 fn handle_publish_diagnostics(
2756 &mut self,
2757 server: ServerKind,
2758 root: PathBuf,
2759 params: &serde_json::Value,
2760 ) -> Option<PathBuf> {
2761 let publish_params = match serde_json::from_value::<lsp_types::PublishDiagnosticsParams>(
2762 params.clone(),
2763 ) {
2764 Ok(params) => params,
2765 Err(err) => {
2766 slog_info!(
2767 "lsp_protocol server={} root={} method=textDocument/publishDiagnostics event=dropped-because-invalid-params error={}",
2768 server.id_str(),
2769 root.display(),
2770 err
2771 );
2772 return None;
2773 }
2774 };
2775 let Some(file) = uri_to_path(&publish_params.uri) else {
2776 slog_info!(
2777 "lsp_protocol server={} root={} method=textDocument/publishDiagnostics event=dropped-because-invalid-uri uri={:?}",
2778 server.id_str(),
2779 root.display(),
2780 publish_params.uri
2781 );
2782 return None;
2783 };
2784 let diagnostic_count = publish_params.diagnostics.len();
2785 let version = publish_params.version;
2786 slog_info!(
2787 "lsp_protocol server={} root={} method=textDocument/publishDiagnostics event=received file={} version={} diagnostics={}",
2788 server.id_str(),
2789 root.display(),
2790 file.display(),
2791 version
2792 .map(|version| version.to_string())
2793 .unwrap_or_else(|| "none".to_string()),
2794 diagnostic_count
2795 );
2796 let stored = from_lsp_diagnostics(file.clone(), publish_params.diagnostics);
2797 let key = ServerKey { kind: server, root };
2802 let provisional = self
2803 .clients
2804 .get(&key)
2805 .is_some_and(|client| client.diagnostics_are_provisional());
2806 self.diagnostics.publish_full_with_provisional(
2807 key.clone(),
2808 file.clone(),
2809 stored,
2810 None,
2811 version,
2812 provisional,
2813 );
2814 if let Some(reason) = self.live_publish_drop_reason(&key, &file) {
2815 slog_info!(
2816 "lsp_protocol server={} root={} method=textDocument/publishDiagnostics event=dropped-because-{} file={} version={}",
2817 key.kind.id_str(),
2818 key.root.display(),
2819 reason,
2820 file.display(),
2821 version
2822 .map(|version| version.to_string())
2823 .unwrap_or_else(|| "none".to_string())
2824 );
2825 }
2826 Some(file)
2827 }
2828
2829 fn handle_server_status(
2830 &mut self,
2831 server: ServerKind,
2832 root: PathBuf,
2833 params: &serde_json::Value,
2834 ) {
2835 if !matches!(&server, ServerKind::Rust)
2836 || params.get("quiescent").and_then(serde_json::Value::as_bool) != Some(true)
2837 {
2838 return;
2839 }
2840
2841 let key = ServerKey { kind: server, root };
2842 let became_quiescent = self
2843 .clients
2844 .get_mut(&key)
2845 .is_some_and(|client| client.set_rust_analyzer_quiescent(true));
2846 if became_quiescent {
2847 self.diagnostics.promote_provisional_for_server(&key);
2848 }
2849 }
2850
2851 fn reap_unreferenced_children_for(&self, key: &ServerKey) {
2852 let live_pids = self
2853 .clients
2854 .values()
2855 .map(LspClient::child_pid)
2856 .collect::<HashSet<_>>();
2857 let orphans = self
2858 .child_registry
2859 .pids_for_server(&key.root, &key.kind)
2860 .into_iter()
2861 .filter(|pid| !live_pids.contains(pid))
2862 .collect::<Vec<_>>();
2863 if !orphans.is_empty() {
2864 self.child_registry.reap_pids(&orphans);
2865 }
2866 }
2867
2868 fn spawn_server(
2869 &self,
2870 def: &ServerDef,
2871 root: &Path,
2872 source_file: &Path,
2873 config: &Config,
2874 ) -> Result<LspClient, LspError> {
2875 self.spawn_server_with_timeout(def, root, source_file, config, None)
2876 }
2877
2878 fn spawn_server_with_timeout(
2879 &self,
2880 def: &ServerDef,
2881 root: &Path,
2882 source_file: &Path,
2883 config: &Config,
2884 initialize_timeout: Option<std::time::Duration>,
2885 ) -> Result<LspClient, LspError> {
2886 let initialization_options =
2887 initialization_options_for_spawn(def, source_file, root, config)?;
2888 let binary = self.resolve_binary(def, root, config)?;
2889
2890 let mut merged_env = def.env.clone();
2894 for (key, value) in &self.extra_env {
2895 merged_env.insert(key.clone(), value.clone());
2896 }
2897
2898 let reclaim_root = config
2903 .project_root
2904 .as_deref()
2905 .map(crate::inspect::job::canonicalize_normalized)
2906 .filter(|project_root| root.starts_with(project_root))
2907 .unwrap_or_else(|| root.to_path_buf());
2908
2909 let mut client = LspClient::spawn_with_reclaim_root(
2910 def.kind.clone(),
2911 root.to_path_buf(),
2912 &binary,
2913 &def.args,
2914 &merged_env,
2915 self.event_tx.clone(),
2916 self.child_registry.clone(),
2917 Some(&reclaim_root),
2918 )?;
2919 let initialize = match initialize_timeout {
2920 Some(timeout) => client.initialize_with_timeout(root, initialization_options, timeout),
2921 None => client.initialize(root, initialization_options),
2922 };
2923 if let Err(err) = initialize {
2924 wait_for_stderr_tail(&mut client);
2925 let stderr_tail = client.stderr_tail();
2926 let reason = if client.child_exited() || !stderr_tail.is_empty() {
2927 format_initialize_failure_reason(&def.binary, &stderr_tail, &err)
2928 } else {
2929 format!("server failed during initialize: {err}")
2930 };
2931 return Err(LspError::ServerNotReady(reason));
2932 }
2933 Ok(client)
2934 }
2935
2936 fn resolve_binary(
2937 &self,
2938 def: &ServerDef,
2939 root: &Path,
2940 config: &Config,
2941 ) -> Result<PathBuf, LspError> {
2942 if let Some(path) = self.binary_overrides.get(&def.kind) {
2943 if path.exists() {
2944 return Ok(path.clone());
2945 }
2946 return Err(LspError::NotFound(format!(
2947 "override binary for {:?} not found: {}",
2948 def.kind,
2949 path.display()
2950 )));
2951 }
2952
2953 if let Some(path) = env_binary_override(&def.kind) {
2954 if path.exists() {
2955 return Ok(path);
2956 }
2957 return Err(LspError::NotFound(format!(
2958 "environment override binary for {:?} not found: {}",
2959 def.kind,
2960 path.display()
2961 )));
2962 }
2963
2964 let mut pushed_config;
2965 let resolution_config = if let Some(paths) = self.pushed_search_paths.as_ref() {
2966 pushed_config = config.clone();
2967 pushed_config.lsp_paths_extra.clone_from(paths);
2968 &pushed_config
2969 } else {
2970 config
2971 };
2972 resolve_server_binary(def, Some(root), resolution_config).ok_or_else(|| {
2973 let searched = if matches!(def.kind, ServerKind::Python | ServerKind::Ty) {
2974 "the workspace virtualenv, node_modules/.bin, lsp_paths_extra, or PATH"
2975 } else {
2976 "node_modules/.bin, lsp_paths_extra, or PATH"
2977 };
2978 LspError::NotFound(format!(
2979 "language server binary '{}' not found in {searched}",
2980 def.binary,
2981 ))
2982 })
2983 }
2984
2985 fn server_key_for_file(&self, file_path: &Path, config: &Config) -> Option<ServerKey> {
2986 for def in servers_for_file(file_path, config) {
2987 let key = server_key_for_definition(&def, file_path, config)?;
2988 if self.clients.contains_key(&key) {
2989 return Some(key);
2990 }
2991 }
2992 None
2993 }
2994}
2995
2996impl Default for LspManager {
2997 fn default() -> Self {
2998 Self::new()
2999 }
3000}
3001
3002const ASTRO_TSDK_UNAVAILABLE: &str = "astro-ls requires a project TypeScript install; none found";
3003
3004fn initialization_options_for_spawn(
3005 def: &ServerDef,
3006 source_file: &Path,
3007 server_root: &Path,
3008 config: &Config,
3009) -> Result<Option<serde_json::Value>, LspError> {
3010 if def.kind != ServerKind::Astro {
3011 return Ok(def.initialization_options.clone());
3012 }
3013
3014 if def
3015 .initialization_options
3016 .as_ref()
3017 .and_then(|options| options.pointer("/typescript/tsdk"))
3018 .and_then(serde_json::Value::as_str)
3019 .is_some_and(|tsdk| !tsdk.is_empty())
3020 {
3021 return Ok(def.initialization_options.clone());
3022 }
3023
3024 let project_root = config.project_root.as_deref().unwrap_or(server_root);
3028 let boundary = if source_file.starts_with(project_root) {
3029 project_root
3030 } else {
3031 server_root
3032 };
3033 let tsdk = find_project_typescript_sdk(source_file, boundary)
3034 .ok_or_else(|| LspError::ServerNotReady(ASTRO_TSDK_UNAVAILABLE.to_string()))?;
3035 let mut options = serde_json::json!({
3036 "typescript": {
3037 "tsdk": tsdk.to_string_lossy(),
3038 }
3039 });
3040 if let Some(configured) = def.initialization_options.clone() {
3041 merge_json_override(&mut options, configured);
3042 }
3043 Ok(Some(options))
3044}
3045
3046fn find_project_typescript_sdk(source_file: &Path, project_root: &Path) -> Option<PathBuf> {
3047 let mut directory = source_file.parent()?;
3048 loop {
3049 let lib = directory
3050 .join("node_modules")
3051 .join("typescript")
3052 .join("lib");
3053 if lib.join("tsserverlibrary.js").is_file() || lib.join("typescript.js").is_file() {
3054 return Some(lib);
3055 }
3056 if directory == project_root {
3057 return None;
3058 }
3059 let parent = directory.parent()?;
3060 if !parent.starts_with(project_root) {
3061 return None;
3062 }
3063 directory = parent;
3064 }
3065}
3066
3067fn merge_json_override(base: &mut serde_json::Value, override_value: serde_json::Value) {
3068 match (base, override_value) {
3069 (serde_json::Value::Object(base), serde_json::Value::Object(override_fields)) => {
3070 for (key, value) in override_fields {
3071 if let Some(existing) = base.get_mut(&key) {
3072 merge_json_override(existing, value);
3073 } else {
3074 base.insert(key, value);
3075 }
3076 }
3077 }
3078 (base, value) => *base = value,
3079 }
3080}
3081
3082fn wait_for_stderr_tail(client: &mut LspClient) {
3083 for _ in 0..10 {
3084 if !client.stderr_tail().is_empty() {
3085 break;
3086 }
3087 std::thread::sleep(std::time::Duration::from_millis(10));
3088 }
3089}
3090
3091fn recoverable_pull_rejection(err: &LspError) -> bool {
3092 matches!(
3093 err,
3094 LspError::ServerError {
3095 code: -32601 | -32602,
3096 ..
3097 }
3098 )
3099}
3100
3101fn server_attempt_result_reason(result: &ServerAttemptResult) -> String {
3102 match result {
3103 ServerAttemptResult::SpawnFailed { binary, reason } => {
3104 format!("spawn_failed: {binary} ({reason})")
3105 }
3106 ServerAttemptResult::BinaryNotInstalled { binary } => {
3107 format!("binary_not_installed: {binary}")
3108 }
3109 ServerAttemptResult::NoRootMarker { looked_for } => {
3110 format!("no_root_marker (looked for: {})", looked_for.join(", "))
3111 }
3112 ServerAttemptResult::Ok { .. } => "ok".to_string(),
3113 }
3114}
3115
3116fn format_stderr_tail_for_reason(stderr_tail: &str) -> String {
3117 truncate_stderr_tail_for_reason(stderr_tail)
3118 .lines()
3119 .map(|line| format!(" {line}"))
3120 .collect::<Vec<_>>()
3121 .join("\n")
3122}
3123
3124fn truncate_stderr_tail_for_reason(stderr_tail: &str) -> String {
3125 if stderr_tail.len() <= STDERR_REASON_BYTES {
3126 return stderr_tail.to_string();
3127 }
3128
3129 let ellipsis = "...";
3130 let target_len = STDERR_REASON_BYTES.saturating_sub(ellipsis.len());
3131 let mut start = stderr_tail.len() - target_len;
3132 while start < stderr_tail.len() && !stderr_tail.is_char_boundary(start) {
3133 start += 1;
3134 }
3135 format!("{ellipsis}{}", &stderr_tail[start..])
3136}
3137
3138fn format_initialize_failure_reason(binary: &str, stderr_tail: &str, err: &LspError) -> String {
3139 let mut reason = format!("server crashed during initialize: {err}");
3140 if !stderr_tail.is_empty() {
3141 reason.push_str("; stderr (last 64 lines):\n");
3142 reason.push_str(&format_stderr_tail_for_reason(stderr_tail));
3143 reason.push_str("\n\n");
3144 reason.push_str(&failure_hint(binary, stderr_tail));
3145 }
3146 reason
3147}
3148
3149fn format_post_initialize_exit_reason(
3150 binary: &str,
3151 status: std::process::ExitStatus,
3152 stderr_tail: &str,
3153 err: &LspError,
3154) -> String {
3155 let code = status
3156 .code()
3157 .map(|c| c.to_string())
3158 .unwrap_or_else(|| "signal/unknown".to_string());
3159 let mut reason = format!("server exited after initialize (code {code}): {err}");
3160 if !stderr_tail.is_empty() {
3161 reason.push_str("; stderr (last 64 lines):\n");
3162 reason.push_str(&format_stderr_tail_for_reason(stderr_tail));
3163 reason.push_str("\n\n");
3164 reason.push_str(&failure_hint(binary, stderr_tail));
3165 }
3166 reason
3167}
3168
3169fn failure_hint(binary: &str, stderr_tail: &str) -> String {
3170 if stderr_tail.contains("MODULE_NOT_FOUND") || stderr_tail.contains("Cannot find module") {
3171 let package_manager = infer_package_manager(stderr_tail);
3172 format!(
3173 "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."
3174 )
3175 } else if let Some(component) = rustup_missing_component(stderr_tail) {
3176 format!("'{component}' is a rustup proxy but the component is not installed. Install it: rustup component add {component}")
3181 } else {
3182 format!("Hint: see stderr above for '{binary}' failure details.")
3183 }
3184}
3185
3186fn rustup_missing_component(stderr_tail: &str) -> Option<String> {
3192 let marker = "Unknown binary '";
3193 let start = stderr_tail.find(marker)? + marker.len();
3194 let rest = &stderr_tail[start..];
3195 let end = rest.find('\'')?;
3196 let name = &rest[..end];
3197 if name.is_empty() || !stderr_tail.contains("toolchain") {
3200 return None;
3201 }
3202 Some(name.to_string())
3203}
3204
3205fn infer_package_manager(stderr_tail: &str) -> &'static str {
3206 let lower = stderr_tail.to_ascii_lowercase();
3207 if lower.contains(".pnpm/") || lower.contains(".pnpm\\") || lower.contains("/pnpm/") {
3208 "pnpm"
3209 } else if lower.contains(".yarn/")
3210 || lower.contains(".yarn\\")
3211 || lower.contains("/yarn/")
3212 || lower.contains("yarn")
3213 {
3214 "yarn"
3215 } else {
3216 "npm"
3217 }
3218}
3219
3220fn canonicalize_for_lsp(file_path: &Path) -> Result<PathBuf, LspError> {
3221 std::fs::canonicalize(file_path)
3228 .map(|canonical| crate::inspect::job::normalize_path(&canonical))
3229 .map_err(LspError::from)
3230}
3231
3232fn resolve_for_lsp_uri(file_path: &Path) -> PathBuf {
3233 if let Ok(path) = std::fs::canonicalize(file_path) {
3236 return crate::inspect::job::normalize_path(&path);
3237 }
3238
3239 let mut existing = file_path.to_path_buf();
3240 let mut missing = Vec::new();
3241 while !existing.exists() {
3242 let Some(name) = existing.file_name() else {
3243 break;
3244 };
3245 missing.push(name.to_owned());
3246 let Some(parent) = existing.parent() else {
3247 break;
3248 };
3249 existing = parent.to_path_buf();
3250 }
3251
3252 let mut resolved = std::fs::canonicalize(&existing)
3253 .map(|canonical| crate::inspect::job::normalize_path(&canonical))
3254 .unwrap_or(existing);
3255 for segment in missing.into_iter().rev() {
3256 resolved.push(segment);
3257 }
3258 resolved
3259}
3260
3261fn language_id_for_extension(ext: &str) -> &'static str {
3262 match ext {
3263 "ts" => "typescript",
3264 "tsx" => "typescriptreact",
3265 "js" | "mjs" | "cjs" => "javascript",
3266 "jsx" => "javascriptreact",
3267 "py" | "pyi" => "python",
3268 "rs" => "rust",
3269 "go" => "go",
3270 "html" | "htm" => "html",
3271 "md" | "markdown" | "mdx" | "mkd" | "mkdn" | "mdown" | "mdwn" | "qmd" | "rmd" => "markdown",
3272 _ => "plaintext",
3273 }
3274}
3275
3276fn log_did_open_sent(key: &ServerKey, file: &Path, language_id: &str) {
3277 slog_info!(
3278 "lsp_protocol server={} root={} method=textDocument/didOpen event=sent file={} language_id={} version=0",
3279 key.kind.id_str(),
3280 key.root.display(),
3281 file.display(),
3282 language_id
3283 );
3284}
3285
3286fn normalize_lookup_path(path: &Path) -> PathBuf {
3287 std::fs::canonicalize(path)
3291 .map(|canonical| crate::inspect::job::normalize_path(&canonical))
3292 .unwrap_or_else(|_| path.to_path_buf())
3293}
3294
3295fn diagnostic_path_candidates(file: &Path) -> Vec<PathBuf> {
3296 let mut candidates = Vec::with_capacity(4);
3297 let mut add = |candidate: PathBuf| {
3298 if !candidates.iter().any(|existing| existing == &candidate) {
3299 candidates.push(candidate);
3300 }
3301 };
3302
3303 add(file.to_path_buf());
3307 add(normalize_lookup_path(file));
3308
3309 if let (Some(parent), Some(name)) = (file.parent(), file.file_name()) {
3313 if let Ok(canonical_parent) = std::fs::canonicalize(parent) {
3314 let reconstructed = canonical_parent.join(name);
3315 add(reconstructed.clone());
3316 add(crate::inspect::job::normalize_path(&reconstructed));
3317 }
3318 }
3319
3320 candidates
3321}
3322
3323fn classify_spawn_error(binary: &str, err: &LspError) -> ServerAttemptResult {
3330 match err {
3331 LspError::NotFound(_) => ServerAttemptResult::BinaryNotInstalled {
3336 binary: binary.to_string(),
3337 },
3338 other => ServerAttemptResult::SpawnFailed {
3339 binary: binary.to_string(),
3340 reason: other.to_string(),
3341 },
3342 }
3343}
3344
3345fn env_binary_override(kind: &ServerKind) -> Option<PathBuf> {
3346 env_binary_override_from(kind, |key| std::env::var_os(key))
3347}
3348
3349fn env_binary_override_from(
3350 kind: &ServerKind,
3351 lookup: impl FnOnce(&str) -> Option<std::ffi::OsString>,
3352) -> Option<PathBuf> {
3353 let id = kind.id_str();
3354 let suffix: String = id
3355 .chars()
3356 .map(|ch| {
3357 if ch.is_ascii_alphanumeric() {
3358 ch.to_ascii_uppercase()
3359 } else {
3360 '_'
3361 }
3362 })
3363 .collect();
3364 let key = format!("AFT_LSP_{suffix}_BINARY");
3365 lookup(&key)
3366 .filter(|value| !value.is_empty())
3367 .map(PathBuf::from)
3368}
3369
3370#[cfg(test)]
3371mod env_binary_override_tests {
3372 use super::*;
3373
3374 #[test]
3375 fn empty_lsp_binary_override_is_unset_without_mutating_the_process_environment() {
3376 let kind = ServerKind::TypeScript;
3377 assert_eq!(
3378 env_binary_override_from(&kind, |key| {
3379 assert_eq!(key, "AFT_LSP_TYPESCRIPT_BINARY");
3380 Some(std::ffi::OsString::new())
3381 }),
3382 None
3383 );
3384 assert_eq!(
3385 env_binary_override_from(&kind, |_| Some(std::ffi::OsString::from("/bin/lsp"))),
3386 Some(PathBuf::from("/bin/lsp"))
3387 );
3388 }
3389}
3390
3391#[cfg(test)]
3392mod language_id_tests {
3393 use super::language_id_for_extension;
3394
3395 #[test]
3396 fn markdown_extensions_use_markdown_language_id() {
3397 for extension in [
3398 "md", "markdown", "mdx", "mkd", "mkdn", "mdown", "mdwn", "qmd", "rmd",
3399 ] {
3400 assert_eq!(language_id_for_extension(extension), "markdown");
3401 }
3402 }
3403}
3404
3405#[cfg(all(test, windows))]
3406mod windows_server_key_tests {
3407 use std::fs;
3408 use std::os::windows::ffi::OsStrExt;
3409
3410 use super::{canonicalize_for_lsp, server_key_for_definition};
3411 use crate::config::{Config, UserServerDef};
3412 use crate::lsp::registry::servers_for_file;
3413
3414 #[test]
3415 fn normalized_and_verbatim_inputs_produce_identical_server_key_material() {
3416 let temp_dir = tempfile::tempdir().expect("tempdir");
3417 let root = temp_dir.path().join("workspace");
3418 let source = root.join("src").join("main.customts");
3419 fs::create_dir_all(source.parent().expect("source parent")).expect("create source dir");
3420 fs::write(root.join("custom-root.json"), "{}\n").expect("write root marker");
3421 fs::write(&source, "export const value = 1;\n").expect("write source");
3422
3423 let config = Config {
3424 project_root: Some(root),
3425 lsp_servers: vec![UserServerDef {
3426 id: "custom-ts".to_string(),
3427 extensions: vec!["customts".to_string()],
3428 binary: "custom-ts-lsp".to_string(),
3429 args: Vec::new(),
3430 root_markers: vec!["custom-root.json".to_string()],
3431 env: Default::default(),
3432 initialization_options: None,
3433 disabled: false,
3434 }],
3435 ..Config::default()
3436 };
3437
3438 let normalized_input = canonicalize_for_lsp(&source).expect("normalized source path");
3439 let bare_canonical_input = fs::canonicalize(&source).expect("canonical source path");
3440 let key_for = |path: &std::path::Path| {
3441 let def = servers_for_file(path, &config)
3442 .into_iter()
3443 .find(|def| def.kind.id_str() == "custom-ts")
3444 .expect("custom server definition");
3445 server_key_for_definition(&def, path, &config).expect("custom server root")
3446 };
3447
3448 let key_material = |key: &crate::lsp::roots::ServerKey| {
3449 let root_bytes = key
3450 .root
3451 .as_os_str()
3452 .encode_wide()
3453 .flat_map(u16::to_le_bytes)
3454 .collect::<Vec<_>>();
3455 (key.kind.id_str().to_string(), root_bytes)
3456 };
3457 let ensure_key = key_for(&normalized_input);
3458 let running_lookup_key = key_for(&bare_canonical_input);
3459
3460 assert_eq!(key_material(&ensure_key), key_material(&running_lookup_key));
3461 }
3462}
3463
3464#[cfg(test)]
3465mod failure_hint_tests {
3466 use super::{failure_hint, rustup_missing_component};
3467
3468 #[test]
3469 fn detects_rustup_proxy_without_component() {
3470 let stderr = "error: Unknown binary 'rust-analyzer' in official toolchain 'stable-aarch64-apple-darwin'.";
3472 assert_eq!(
3473 rustup_missing_component(stderr).as_deref(),
3474 Some("rust-analyzer")
3475 );
3476 let hint = failure_hint("rust-analyzer", stderr);
3477 assert!(
3478 hint.contains("rustup component add rust-analyzer"),
3479 "expected actionable rustup hint, got: {hint}"
3480 );
3481 }
3482
3483 #[test]
3484 fn ignores_unknown_binary_without_toolchain_phrasing() {
3485 let stderr = "fatal: Unknown binary 'foo' was requested by the linker.";
3488 assert_eq!(rustup_missing_component(stderr), None);
3489 assert!(failure_hint("foo", stderr).starts_with("Hint: see stderr"));
3490 }
3491
3492 #[test]
3493 fn npm_module_not_found_still_wins() {
3494 let stderr = "Error: Cannot find module '/x/typescript-language-server/lib/cli.mjs'";
3496 let hint = failure_hint("typescript-language-server", stderr);
3497 assert!(hint.contains("install -g"), "got: {hint}");
3498 }
3499}
3500
3501#[cfg(test)]
3502mod diagnostic_capacity_tests {
3503 use std::fs;
3504
3505 use super::LspManager;
3506 use crate::config::Config;
3507
3508 #[test]
3513 fn set_diagnostic_capacity_propagates_to_store() {
3514 let mut manager = LspManager::new();
3515 manager.set_diagnostic_capacity(7);
3516 assert_eq!(manager.diagnostics_store_for_test().capacity_for_test(), 7);
3517 manager.set_diagnostic_capacity(0); assert_eq!(manager.diagnostics_store_for_test().capacity_for_test(), 0);
3519 }
3520
3521 #[test]
3524 fn clear_failed_spawns_empties_the_cache() {
3525 let mut manager = LspManager::new();
3526 assert_eq!(manager.clear_failed_spawns(), 0);
3527 manager.insert_failed_spawn_for_test();
3528 assert_eq!(manager.clear_failed_spawns(), 1);
3529 assert_eq!(manager.clear_failed_spawns(), 0);
3530 }
3531
3532 #[test]
3533 fn pushed_search_paths_make_new_binary_visible_to_stale_config() {
3534 let root = tempfile::tempdir().unwrap();
3535 let bin_dir = tempfile::tempdir().unwrap();
3536 let binary_name = "aft-test-pushed-lsp";
3537 let binary = bin_dir.path().join(binary_name);
3538 fs::write(&binary, "#!/bin/sh\nexit 0\n").unwrap();
3539 #[cfg(unix)]
3540 {
3541 use std::os::unix::fs::PermissionsExt;
3542 fs::set_permissions(&binary, fs::Permissions::from_mode(0o755)).unwrap();
3543 }
3544
3545 let config = Config {
3546 project_root: Some(root.path().to_path_buf()),
3547 lsp_servers: vec![crate::config::UserServerDef {
3548 id: "pushed-search-path-test".to_string(),
3549 extensions: vec!["pushedpath".to_string()],
3550 binary: binary_name.to_string(),
3551 args: Vec::new(),
3552 root_markers: Vec::new(),
3553 env: Default::default(),
3554 initialization_options: None,
3555 disabled: false,
3556 }],
3557 ..Config::default()
3558 };
3559 let file = root.path().join("sample.pushedpath");
3560 fs::write(&file, "test\n").unwrap();
3561 let definition = crate::lsp::registry::servers_for_file(&file, &config)
3562 .into_iter()
3563 .find(|definition| definition.kind.id_str() == "pushed-search-path-test")
3564 .unwrap();
3565 let mut manager = LspManager::new();
3566
3567 assert!(manager
3568 .resolve_binary(&definition, root.path(), &config)
3569 .is_err());
3570 assert!(manager.set_search_paths(vec![bin_dir.path().to_path_buf()]));
3571 assert_eq!(
3572 manager
3573 .resolve_binary(&definition, root.path(), &config)
3574 .unwrap(),
3575 binary
3576 );
3577 }
3578
3579 #[test]
3580 fn post_write_notification_does_not_start_a_cold_server() {
3581 let dir = tempfile::tempdir().unwrap();
3582 let file = dir.path().join("main.ts");
3583 fs::write(dir.path().join("package.json"), "{}").unwrap();
3584 fs::write(&file, "export const value = 1;\n").unwrap();
3585
3586 let mut manager = LspManager::new();
3587 manager
3588 .notify_file_changed_if_running(&file, "export const value = 1;\n", &Config::default())
3589 .unwrap();
3590 assert!(manager.clients.is_empty());
3591 }
3592}
3593
3594#[cfg(test)]
3595mod post_edit_waiter_tests {
3596 use std::collections::HashMap;
3597 use std::path::PathBuf;
3598 use std::time::{Duration, Instant};
3599
3600 use super::LspManager;
3601 use crate::lsp::client::LspEvent;
3602 use crate::lsp::registry::ServerKind;
3603
3604 #[test]
3605 fn draining_an_event_wakes_registered_post_edit_waiter() {
3606 let mut manager = LspManager::new();
3607 let mut wait = manager.start_post_edit_diagnostics_wait(
3608 PathBuf::from("/workspace/src/main.rs").as_path(),
3609 &[],
3610 &HashMap::new(),
3611 Duration::from_secs(2),
3612 );
3613 manager.enqueue_event_for_test(LspEvent::Notification {
3614 server_kind: ServerKind::Rust,
3615 root: PathBuf::from("/workspace"),
3616 method: "custom/drainedElsewhere".to_string(),
3617 params: None,
3618 });
3619
3620 assert_eq!(manager.drain_events().events.len(), 1);
3621 let started = Instant::now();
3622 assert!(wait.next_event().is_none());
3623 assert!(
3624 started.elapsed() < Duration::from_millis(250),
3625 "a competing drain did not wake the parked post-edit waiter"
3626 );
3627 let _ = manager.poll_post_edit_diagnostics_wait(&mut wait, None);
3628 let _ = manager.finish_post_edit_diagnostics_wait(wait);
3629 }
3630}
3631
3632#[cfg(test)]
3633mod clear_diagnostics_tests {
3634 use std::path::PathBuf;
3635
3636 use super::LspManager;
3637 use crate::lsp::client::LspEvent;
3638 use crate::lsp::diagnostics::{DiagnosticSeverity, StoredDiagnostic};
3639 use crate::lsp::position::uri_for_path;
3640 use crate::lsp::registry::ServerKind;
3641 use crate::lsp::roots::ServerKey;
3642
3643 fn err_diag(file: &PathBuf) -> StoredDiagnostic {
3644 StoredDiagnostic {
3645 file: file.clone(),
3646 line: 1,
3647 column: 1,
3648 end_line: 1,
3649 end_column: 2,
3650 severity: DiagnosticSeverity::Error,
3651 message: "boom".into(),
3652 code: None,
3653 source: None,
3654 }
3655 }
3656
3657 #[test]
3662 fn clear_diagnostics_for_deleted_file_matches_canonical_key() {
3663 let dir = tempfile::tempdir().unwrap();
3664 let canonical_dir = std::fs::canonicalize(dir.path()).unwrap();
3666 let canonical_file = canonical_dir.join("gone.ts");
3667 std::fs::write(&canonical_file, "x").unwrap();
3670
3671 let mut manager = LspManager::new();
3672 let key = ServerKey {
3673 kind: ServerKind::TypeScript,
3674 root: canonical_dir.clone(),
3675 };
3676 manager.diagnostics_store_mut_for_test().publish(
3677 key,
3678 canonical_file.clone(),
3679 vec![err_diag(&canonical_file)],
3680 );
3681 assert_eq!(manager.warm_error_warning_counts(), (1, 0));
3682
3683 std::fs::remove_file(&canonical_file).unwrap();
3684
3685 let watcher_path = dir.path().join("gone.ts");
3688 let removed = manager.clear_diagnostics_for_file(&watcher_path);
3689
3690 assert!(removed, "expected the deleted file's diagnostic to clear");
3691 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
3692 }
3693
3694 #[cfg(windows)]
3695 #[test]
3696 fn clear_diagnostics_for_deleted_file_matches_normalized_publish_key() {
3697 let dir = tempfile::tempdir().unwrap();
3698 let file = dir.path().join("normalized-gone.ts");
3699 std::fs::write(&file, "x").unwrap();
3700 let normalized_file = crate::inspect::job::canonicalize_normalized(&file);
3701
3702 let mut manager = LspManager::new();
3703 let key = ServerKey {
3704 kind: ServerKind::TypeScript,
3705 root: normalized_file.parent().unwrap().to_path_buf(),
3706 };
3707 manager.diagnostics_store_mut_for_test().publish(
3708 key,
3709 normalized_file.clone(),
3710 vec![err_diag(&normalized_file)],
3711 );
3712 std::fs::remove_file(&file).unwrap();
3713
3714 assert!(manager.clear_diagnostics_for_file(&file));
3715 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
3716 }
3717
3718 #[cfg(windows)]
3719 #[test]
3720 fn stale_diagnostics_for_deleted_file_matches_normalized_publish_key() {
3721 let dir = tempfile::tempdir().unwrap();
3722 let file = dir.path().join("normalized-stale.ts");
3723 std::fs::write(&file, "x").unwrap();
3724 let normalized_file = crate::inspect::job::canonicalize_normalized(&file);
3725
3726 let mut manager = LspManager::new();
3727 let key = ServerKey {
3728 kind: ServerKind::TypeScript,
3729 root: normalized_file.parent().unwrap().to_path_buf(),
3730 };
3731 manager.diagnostics_store_mut_for_test().publish(
3732 key,
3733 normalized_file.clone(),
3734 vec![err_diag(&normalized_file)],
3735 );
3736 std::fs::remove_file(&file).unwrap();
3737
3738 let result = manager.mark_diagnostics_stale_for_file(&file);
3739 assert!(result.had_entries);
3740 assert!(result.changed);
3741 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
3742 }
3743
3744 #[test]
3745 fn clear_diagnostics_for_unknown_file_is_noop() {
3746 let mut manager = LspManager::new();
3747 assert!(!manager.clear_diagnostics_for_file(&PathBuf::from("/nope/missing.ts")));
3748 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
3749 }
3750
3751 #[test]
3752 fn drain_events_reports_publish_diagnostics_updates() {
3753 let dir = tempfile::tempdir().unwrap();
3754 let root = std::fs::canonicalize(dir.path()).unwrap();
3755 let file = root.join("main.ts");
3756 std::fs::write(&file, "const x: number = 'nope';").unwrap();
3757
3758 let mut manager = LspManager::new();
3759 let diagnostic = lsp_types::Diagnostic {
3760 range: lsp_types::Range {
3761 start: lsp_types::Position {
3762 line: 0,
3763 character: 0,
3764 },
3765 end: lsp_types::Position {
3766 line: 0,
3767 character: 1,
3768 },
3769 },
3770 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
3771 code: None,
3772 code_description: None,
3773 source: Some("test".into()),
3774 message: "boom".into(),
3775 related_information: None,
3776 tags: None,
3777 data: None,
3778 };
3779 let params = serde_json::to_value(lsp_types::PublishDiagnosticsParams {
3780 uri: uri_for_path(&file).unwrap(),
3781 diagnostics: vec![diagnostic],
3782 version: Some(1),
3783 })
3784 .unwrap();
3785 manager
3786 .event_tx
3787 .send(LspEvent::Notification {
3788 server_kind: ServerKind::TypeScript,
3789 root,
3790 method: "textDocument/publishDiagnostics".into(),
3791 params: Some(params),
3792 })
3793 .unwrap();
3794
3795 let drained = manager.drain_events();
3796
3797 assert!(drained.diagnostics_changed);
3798 assert_eq!(drained.events.len(), 1);
3799 assert_eq!(manager.warm_error_warning_counts(), (1, 0));
3800 }
3801}
3802
3803#[cfg(test)]
3804mod inspect_path_tests {
3805 use super::LspManager;
3806 use crate::config::{Config, UserServerDef};
3807 use crate::lsp::registry::ServerKind;
3808
3809 #[test]
3810 fn applicability_resolution_does_not_start_or_open_a_server() {
3811 let temp_dir = tempfile::tempdir().expect("tempdir");
3812 let root = temp_dir.path().join("project");
3813 std::fs::create_dir_all(&root).expect("project root");
3814 std::fs::write(root.join("inspect-root.json"), "{}\n").expect("root marker");
3815 std::fs::write(root.join("input.inspectlang"), "value\n").expect("source file");
3816
3817 let config = Config {
3818 project_root: Some(root.clone()),
3819 lsp_servers: vec![UserServerDef {
3820 id: "inspect-test".to_string(),
3821 extensions: vec!["inspectlang".to_string()],
3822 binary: "inspect-test-lsp".to_string(),
3823 args: Vec::new(),
3824 root_markers: vec!["inspect-root.json".to_string()],
3825 env: Default::default(),
3826 initialization_options: None,
3827 disabled: false,
3828 }],
3829 ..Config::default()
3830 };
3831 let mut manager = LspManager::new();
3832 manager.override_binary(
3833 ServerKind::Custom("inspect-test".into()),
3834 std::env::current_exe().expect("current executable"),
3835 );
3836
3837 let snapshot = manager
3838 .resolve_applicable_servers_for_root(&root, &config)
3839 .expect("resolution succeeds without spawning");
3840 assert_eq!(snapshot.server_keys.len(), 1);
3841 assert_eq!(snapshot.server_keys[0].kind.id_str(), "inspect-test");
3842 assert_eq!(manager.server_count(), 0);
3843 assert!(!manager.document_is_open_for_test(&root.join("input.inspectlang")));
3844 }
3845
3846 #[test]
3847 fn applicability_resolution_preserves_an_empty_snapshot() {
3848 let temp_dir = tempfile::tempdir().expect("tempdir");
3849 let root = temp_dir.path().join("project");
3850 std::fs::create_dir_all(&root).expect("project root");
3851 std::fs::write(root.join("notes.txt"), "plain text\n").expect("fixture file");
3852
3853 let snapshot = LspManager::new()
3854 .resolve_applicable_servers_for_root(&root, &Config::default())
3855 .expect("an empty applicability set is valid");
3856
3857 assert!(snapshot.server_keys.is_empty());
3858 assert!(snapshot.candidates.is_empty());
3859 }
3860}
3861
3862#[cfg(all(test, unix))]
3864mod server_exit_reap_tests {
3865 use std::collections::HashMap;
3866 use std::path::Path;
3867 use std::process::{Child, Command};
3868 use std::thread;
3869 use std::time::{Duration, Instant};
3870
3871 use super::LspManager;
3872 use crate::config::Config;
3873 use crate::lsp::child_registry::LspChildRegistry;
3874 use crate::lsp::client::{LspClient, LspEvent, ServerExitReason};
3875 use crate::lsp::registry::ServerKind;
3876
3877 #[cfg(unix)]
3878 fn spawn_session_sleep() -> Child {
3879 use std::os::unix::process::CommandExt;
3880
3881 let mut command = Command::new("sh");
3882 command.args(["-c", "exec sleep 60"]);
3883 unsafe {
3884 command.pre_exec(|| {
3885 if libc::setsid() == -1 {
3886 return Err(std::io::Error::last_os_error());
3887 }
3888 Ok(())
3889 });
3890 }
3891 command.spawn().expect("spawn session-leader sleep")
3892 }
3893
3894 #[cfg(unix)]
3895 fn wait_until_dead(pid: u32, timeout: Duration) -> bool {
3896 let started = Instant::now();
3897 while started.elapsed() < timeout {
3898 if !crate::bash_background::process::is_process_alive(pid) {
3899 return true;
3900 }
3901 thread::sleep(Duration::from_millis(20));
3902 }
3903 false
3904 }
3905
3906 #[cfg(unix)]
3907 fn spawn_malformed_then_sleep_client(
3908 event_tx: crossbeam_channel::Sender<LspEvent>,
3909 registry: LspChildRegistry,
3910 root: std::path::PathBuf,
3911 ) -> LspClient {
3912 LspClient::spawn(
3913 ServerKind::TypeScript,
3914 root,
3915 Path::new("sh"),
3916 &[
3917 "-c".to_string(),
3918 "printf 'Content-Length: 3\r\n\r\n{{{'; exec sleep 60".to_string(),
3919 ],
3920 &HashMap::new(),
3921 event_tx,
3922 registry,
3923 )
3924 .expect("spawn malformed-then-sleep stand-in")
3925 }
3926
3927 #[cfg(unix)]
3928 #[test]
3929 fn server_exited_handler_kills_live_child_and_untracks() {
3930 let registry = LspChildRegistry::new();
3931 let mut manager = LspManager::new();
3932 manager.set_child_registry(registry.clone());
3933 let tmp = tempfile::tempdir().unwrap();
3934 let root = tmp.path().to_path_buf();
3935 let mut client = spawn_malformed_then_sleep_client(
3936 manager.event_sender_for_test(),
3937 registry.clone(),
3938 root.clone(),
3939 );
3940 let pid = client.child_pid();
3941 client.suppress_kill_on_drop_for_test();
3942 manager.insert_client_for_test(client);
3943
3944 let deadline = Instant::now() + Duration::from_secs(5);
3945 let mut saw_exit = false;
3946 while Instant::now() < deadline {
3947 let drained = manager.drain_events();
3948 if drained.events.iter().any(|event| {
3949 matches!(
3950 event,
3951 LspEvent::ServerExited {
3952 reason: ServerExitReason::ReadError(_),
3953 ..
3954 }
3955 )
3956 }) {
3957 saw_exit = true;
3958 break;
3959 }
3960 thread::sleep(Duration::from_millis(20));
3961 }
3962 assert!(saw_exit, "reader must emit ServerExited with ReadError");
3963 assert!(
3964 wait_until_dead(pid, Duration::from_secs(5)),
3965 "ServerExited handler must kill the still-running child"
3966 );
3967 assert!(
3968 !registry.pids().contains(&pid),
3969 "ServerExited handler must untrack the child"
3970 );
3971 assert_eq!(manager.active_client_count(), 0);
3972 }
3973
3974 #[cfg(unix)]
3975 #[test]
3976 fn lifecycle_census_reports_dropped_client_until_reaper_cleans_child() {
3977 let registry = LspChildRegistry::new();
3978 let (events, _event_rx) = crossbeam_channel::unbounded();
3979 let root = tempfile::tempdir().expect("tempdir");
3980 let mut client =
3981 spawn_malformed_then_sleep_client(events, registry.clone(), root.path().to_path_buf());
3982 client.suppress_kill_on_drop_for_test();
3983 drop(client);
3984
3985 let leaked = registry.health_snapshot();
3986 assert_eq!(leaked.children_without_client, 1);
3987 assert_eq!(leaked.children_total, 1);
3988 assert_eq!(registry.reap_children_without_client(), 1);
3989 assert_eq!(registry.health_snapshot().children_without_client, 0);
3990 assert_eq!(registry.health_snapshot().children_total, 0);
3991 }
3992
3993 #[cfg(unix)]
3994 #[test]
3995 fn ensure_server_reaps_unreferenced_children_before_spawn() {
3996 let tmp = tempfile::tempdir().unwrap();
3997 let root = tmp.path().join("workspace");
3998 let src = root.join("src");
3999 std::fs::create_dir_all(&src).unwrap();
4000 std::fs::write(root.join("Cargo.toml"), "[package]\nname = \"demo\"\n").unwrap();
4001 let main_rs = src.join("main.rs");
4002 std::fs::write(&main_rs, "fn main() {}\n").unwrap();
4003 let config = Config::default();
4004 let rust_def = crate::lsp::registry::servers_for_file(&main_rs, &config)
4005 .into_iter()
4006 .find(|def| matches!(def.kind, ServerKind::Rust))
4007 .expect("rust server applies to main.rs");
4008 let key = super::server_key_for_definition(&rust_def, &main_rs, &config)
4009 .expect("rust workspace root");
4010
4011 let registry = LspChildRegistry::new();
4012 let mut first = spawn_session_sleep();
4013 let mut second = spawn_session_sleep();
4014 let pid1 = first.id();
4015 let pid2 = second.id();
4016 registry.track_child(pid1, Some(&key.root), Some(&key.root), Some(&key.kind));
4017 registry.track_child(pid2, Some(&key.root), Some(&key.root), Some(&key.kind));
4018
4019 let mut manager = LspManager::new();
4020 manager.set_child_registry(registry.clone());
4021 manager.override_binary(ServerKind::Rust, Path::new("false").to_path_buf());
4022 assert_eq!(
4023 registry.pids_for_server(&key.root, &key.kind).len(),
4024 2,
4025 "both orphans must be registered under the spawn key"
4026 );
4027 let _ = manager.ensure_server_for_file(&main_rs, &config);
4028
4029 let deadline = Instant::now() + Duration::from_secs(5);
4030 loop {
4031 let first_exited = first.try_wait().ok().flatten().is_some();
4032 let second_exited = second.try_wait().ok().flatten().is_some();
4033 if first_exited && second_exited {
4034 break;
4035 }
4036 assert!(
4037 Instant::now() < deadline,
4038 "orphans must exit after rebind reap (first alive={}, second alive={})",
4039 crate::bash_background::process::is_process_alive(pid1),
4040 crate::bash_background::process::is_process_alive(pid2)
4041 );
4042 thread::sleep(Duration::from_millis(20));
4043 }
4044 assert!(
4045 !registry.pids().contains(&pid1) && !registry.pids().contains(&pid2),
4046 "reaped orphans must be untracked before the new spawn is tracked"
4047 );
4048 }
4049}