1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3
4use crossbeam_channel::{bounded, unbounded, Receiver, RecvTimeoutError, Sender, TrySendError};
5use lsp_types::notification::{
6 DidChangeTextDocument, DidChangeWatchedFiles, DidCloseTextDocument, DidOpenTextDocument,
7};
8use lsp_types::{
9 DidChangeTextDocumentParams, DidChangeWatchedFilesParams, DidCloseTextDocumentParams,
10 DidOpenTextDocumentParams, FileChangeType, FileEvent, TextDocumentContentChangeEvent,
11 TextDocumentIdentifier, TextDocumentItem, VersionedTextDocumentIdentifier,
12};
13
14use crate::alert_state::AcceptedDiagnosticSnapshot;
15use crate::config::Config;
16use crate::lsp::child_registry::LspChildRegistry;
17use crate::lsp::client::{LspClient, LspEvent, ServerState};
18use crate::lsp::diagnostics::{
19 from_lsp_diagnostics, DiagnosticEntry, DiagnosticsStore, StoredDiagnostic,
20};
21use crate::lsp::document::DocumentStore;
22use crate::lsp::position::{uri_for_path, uri_to_path};
23use crate::lsp::pull_params::{
24 AftDocumentDiagnosticParams, AftDocumentDiagnosticRequest, AftWorkspaceDiagnosticParams,
25 AftWorkspaceDiagnosticRequest,
26};
27use crate::lsp::registry::{resolve_server_binary, servers_for_file, ServerDef, ServerKind};
28use crate::lsp::roots::ServerKey;
29use crate::lsp::LspError;
30use crate::slog_error;
31
32const STDERR_REASON_BYTES: usize = 2 * 1024;
33
34fn server_key_for_definition(
35 def: &ServerDef,
36 file_path: &Path,
37 config: &Config,
38) -> Option<ServerKey> {
39 def.workspace_root_for_file_with_project_root(file_path, config.project_root.as_deref())
40 .map(|root| ServerKey {
41 kind: def.kind.clone(),
42 root,
43 })
44}
45
46fn server_key_sort(left: &ServerKey, right: &ServerKey) -> std::cmp::Ordering {
47 left.kind
48 .id_str()
49 .cmp(right.kind.id_str())
50 .then(left.root.cmp(&right.root))
51}
52
53#[derive(Debug, Clone)]
58pub enum ServerAttemptResult {
59 Ok { server_key: ServerKey },
61 NoRootMarker { looked_for: Vec<String> },
64 BinaryNotInstalled { binary: String },
67 SpawnFailed { binary: String, reason: String },
69}
70
71#[derive(Debug, Clone)]
73pub struct ServerAttempt {
74 pub server_id: String,
76 pub server_name: String,
78 pub result: ServerAttemptResult,
79}
80
81#[derive(Debug, Clone, Default)]
87pub struct EnsureServerOutcomes {
88 pub successful: Vec<ServerKey>,
90 pub attempts: Vec<ServerAttempt>,
93}
94
95#[derive(Clone, Debug)]
100pub struct ApplicableServerSnapshot {
101 pub server_keys: Vec<ServerKey>,
102 candidates: Vec<ApplicableServerCandidate>,
103 producer_failures: Vec<ApplicableServerFailure>,
104}
105
106#[derive(Clone, Debug)]
107struct ApplicableServerCandidate {
108 key: ServerKey,
109 definition: ServerDef,
110 source_file: PathBuf,
111}
112
113#[derive(Clone, Debug)]
114pub enum ApplicabilityResolutionError {
115 RootUnreadable { root: PathBuf, reason: String },
116 RequestDeadline { root: PathBuf },
117}
118
119#[derive(Clone, Debug)]
120pub struct ApplicableServerFailure {
121 pub server_key: ServerKey,
122 pub result: ServerAttemptResult,
123}
124
125impl ApplicableServerFailure {
126 pub fn reason(&self) -> String {
127 self.result.failure_reason()
128 }
129}
130
131#[derive(Clone, Debug, Default)]
132pub struct ApplicableServerStartOutcomes {
133 pub successful: Vec<ServerKey>,
134 pub failures: Vec<ApplicableServerFailure>,
135 pub deadline_exceeded: Option<ServerKey>,
138}
139
140impl ServerAttemptResult {
141 pub fn failure_reason(&self) -> String {
142 match self {
143 Self::BinaryNotInstalled { binary } => format!("{binary} is unavailable"),
144 Self::SpawnFailed { reason, .. } => reason.clone(),
145 Self::NoRootMarker { looked_for } => {
146 format!(
147 "no workspace root marker found (looked for {})",
148 looked_for.join(", ")
149 )
150 }
151 Self::Ok { .. } => "server started successfully".to_string(),
152 }
153 }
154}
155
156impl EnsureServerOutcomes {
157 pub fn no_server_registered(&self) -> bool {
159 self.attempts.is_empty()
160 }
161
162 pub fn only_inapplicable_root_markers(&self) -> bool {
170 self.successful.is_empty()
171 && !self.attempts.is_empty()
172 && self
173 .attempts
174 .iter()
175 .all(|attempt| matches!(attempt.result, ServerAttemptResult::NoRootMarker { .. }))
176 }
177}
178
179#[derive(Debug, Clone, Default)]
189pub struct PostEditWaitOutcome {
190 pub accepted_snapshots: Vec<AcceptedDiagnosticSnapshot>,
195 pub diagnostics: Vec<StoredDiagnostic>,
199 pub pending_servers: Vec<ServerKey>,
203 pub exited_servers: Vec<ServerKey>,
207}
208
209#[derive(Debug, Clone, Copy, Default)]
211pub struct PreEditSnapshot {
212 pub epoch: u64,
213 pub document_version_at_capture: Option<i32>,
214}
215
216#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
217pub struct StaleDiagnosticsMark {
218 pub had_entries: bool,
219 pub changed: bool,
220}
221
222pub fn post_edit_entry_is_fresh(
223 entry: &DiagnosticEntry,
224 target_version: i32,
225 pre: PreEditSnapshot,
226) -> bool {
227 if entry.stale || entry.epoch <= pre.epoch {
228 return false;
229 }
230
231 match entry.version {
232 Some(version) => version >= target_version,
233 None => false,
238 }
239}
240
241impl PostEditWaitOutcome {
242 pub fn complete(&self) -> bool {
245 self.pending_servers.is_empty() && self.exited_servers.is_empty()
246 }
247}
248
249#[derive(Debug, Clone)]
251pub enum PullFileOutcome {
252 Full { diagnostic_count: usize },
254 Unchanged,
256 PartialNotSupported,
259 PullNotSupported,
262 RequestFailed { reason: String },
264}
265
266#[derive(Debug, Clone, Default)]
268pub struct EnsureFileOpenResult {
269 pub server_keys: Vec<ServerKey>,
270 pub newly_opened: Vec<ServerKey>,
272}
273
274impl EnsureFileOpenResult {
275 pub fn is_empty(&self) -> bool {
276 self.server_keys.is_empty()
277 }
278}
279
280#[derive(Debug, Clone)]
282pub struct PullFileResult {
283 pub server_key: ServerKey,
284 pub outcome: PullFileOutcome,
285}
286
287#[derive(Debug, Clone)]
289pub struct PullWorkspaceResult {
290 pub server_key: ServerKey,
291 pub files_reported: Vec<PathBuf>,
295 pub complete: bool,
297 pub cancelled: bool,
299 pub supports_workspace: bool,
303}
304
305pub struct DrainedLspEvents {
306 pub events: Vec<LspEvent>,
307 pub diagnostics_changed: bool,
308 pub accepted_snapshots: Vec<AcceptedDiagnosticSnapshot>,
314 pub has_more: bool,
315}
316
317pub(crate) struct PostEditDiagnosticsWait {
322 lookup_path: PathBuf,
323 expected_versions: Vec<(ServerKey, i32)>,
324 pre_snapshot: HashMap<ServerKey, PreEditSnapshot>,
325 event_rx: Receiver<LspEvent>,
326 wake_rx: Receiver<()>,
327 waiter_id: u64,
328 deadline: std::time::Instant,
329 fresh: HashMap<ServerKey, Vec<StoredDiagnostic>>,
330 exited: Vec<ServerKey>,
331}
332
333impl PostEditDiagnosticsWait {
334 pub(crate) fn deadline_reached(&self) -> bool {
335 std::time::Instant::now() >= self.deadline
336 }
337
338 pub(crate) fn next_event(&self) -> Option<LspEvent> {
339 let remaining = self
340 .deadline
341 .saturating_duration_since(std::time::Instant::now());
342 if remaining.is_zero() {
343 return None;
344 }
345
346 crossbeam_channel::select! {
347 recv(self.event_rx) -> event => event.ok(),
348 recv(self.wake_rx) -> _ => None,
349 default(remaining) => None,
350 }
351 }
352}
353
354impl IntoIterator for DrainedLspEvents {
355 type Item = LspEvent;
356 type IntoIter = std::vec::IntoIter<LspEvent>;
357
358 fn into_iter(self) -> Self::IntoIter {
359 self.events.into_iter()
360 }
361}
362
363pub struct LspManager {
364 clients: HashMap<ServerKey, LspClient>,
366 server_binaries: HashMap<ServerKey, String>,
370 documents: HashMap<ServerKey, DocumentStore>,
372 diagnostics: DiagnosticsStore,
374 event_tx: Sender<LspEvent>,
376 event_rx: Receiver<LspEvent>,
377 post_edit_waiters: HashMap<u64, Sender<()>>,
381 next_post_edit_waiter_id: u64,
382 binary_overrides: HashMap<ServerKind, PathBuf>,
384 extra_env: HashMap<String, String>,
388 failed_spawns: HashMap<ServerKey, ServerAttemptResult>,
403 watched_file_skip_logged: HashSet<ServerKey>,
406 #[cfg(windows)]
409 last_watched_file_notification_trace: String,
410 child_registry: LspChildRegistry,
414}
415
416impl LspManager {
417 pub fn new() -> Self {
418 let (event_tx, event_rx) = unbounded();
419 Self {
420 clients: HashMap::new(),
421 server_binaries: HashMap::new(),
422 documents: HashMap::new(),
423 diagnostics: DiagnosticsStore::new(),
424 event_tx,
425 event_rx,
426 post_edit_waiters: HashMap::new(),
427 next_post_edit_waiter_id: 0,
428 binary_overrides: HashMap::new(),
429 extra_env: HashMap::new(),
430 failed_spawns: HashMap::new(),
431 watched_file_skip_logged: HashSet::new(),
432 #[cfg(windows)]
433 last_watched_file_notification_trace: "no watched-file notification attempted"
434 .to_string(),
435 child_registry: LspChildRegistry::new(),
436 }
437 }
438
439 pub fn set_child_registry(&mut self, registry: LspChildRegistry) {
441 self.child_registry = registry;
442 }
443
444 pub fn set_extra_env(&mut self, key: &str, value: &str) {
448 self.extra_env.insert(key.to_string(), value.to_string());
449 }
450
451 pub fn server_count(&self) -> usize {
453 self.clients.len()
454 }
455
456 pub fn estimated_memory(&self) -> crate::memory::MemoryEstimate {
460 let mut bytes = 0u64;
461 let mut document_count = 0u64;
462 for documents in self.documents.values() {
463 let estimate = documents.estimated_memory();
464 bytes = bytes.saturating_add(estimate.estimated_bytes.unwrap_or(0));
465 document_count = document_count
466 .saturating_add(estimate.counts.get("documents").copied().unwrap_or(0));
467 }
468 let diagnostics = self.diagnostics.estimated_memory();
469 bytes = bytes.saturating_add(diagnostics.estimated_bytes.unwrap_or(0));
470 crate::memory::MemoryEstimate::estimated(bytes)
471 .count("servers", self.clients.len())
472 .count("document_stores", self.documents.len())
473 .count_u64("documents", document_count)
474 .count_u64(
475 "diagnostic_entries",
476 diagnostics
477 .counts
478 .get("diagnostic_entries")
479 .copied()
480 .unwrap_or(0),
481 )
482 .count_u64(
483 "diagnostics",
484 diagnostics.counts.get("diagnostics").copied().unwrap_or(0),
485 )
486 }
487
488 pub fn set_diagnostic_capacity(&mut self, capacity: usize) {
492 self.diagnostics.set_capacity(capacity);
493 }
494
495 pub fn override_binary(&mut self, kind: ServerKind, binary_path: PathBuf) {
497 self.binary_overrides.insert(kind, binary_path);
498 }
499
500 pub fn resolve_applicable_servers_for_root(
502 &self,
503 project_root: &Path,
504 config: &Config,
505 ) -> Result<ApplicableServerSnapshot, ApplicabilityResolutionError> {
506 self.resolve_applicable_servers(project_root, None, config, None)
507 }
508
509 pub fn resolve_applicable_servers_for_inspect(
514 &self,
515 project_root: &Path,
516 scope_roots: Option<&[PathBuf]>,
517 config: &Config,
518 deadline: std::time::Instant,
519 ) -> Result<ApplicableServerSnapshot, ApplicabilityResolutionError> {
520 self.resolve_applicable_servers(project_root, scope_roots, config, Some(deadline))
521 }
522
523 fn resolve_applicable_servers(
524 &self,
525 project_root: &Path,
526 scope_roots: Option<&[PathBuf]>,
527 config: &Config,
528 deadline: Option<std::time::Instant>,
529 ) -> Result<ApplicableServerSnapshot, ApplicabilityResolutionError> {
530 if !project_root.is_dir() {
531 return Err(ApplicabilityResolutionError::RootUnreadable {
532 root: project_root.to_path_buf(),
533 reason: "project root is not a directory".to_string(),
534 });
535 }
536
537 let mut candidates = HashMap::<ServerKey, ApplicableServerCandidate>::new();
538 let mut producer_failures = HashMap::<ServerKey, ApplicableServerFailure>::new();
539 let walker = ignore::WalkBuilder::new(project_root)
541 .same_file_system(true)
542 .standard_filters(true)
543 .add_custom_ignore_filename(".aftignore")
544 .filter_entry(|entry| {
545 !matches!(
546 entry.file_name().to_string_lossy().as_ref(),
547 ".git" | "node_modules" | "target" | "dist" | "build" | ".next" | ".turbo"
548 )
549 })
550 .build();
551
552 for entry in walker {
553 if deadline.is_some_and(|deadline| std::time::Instant::now() >= deadline) {
554 return Err(ApplicabilityResolutionError::RequestDeadline {
555 root: project_root.to_path_buf(),
556 });
557 }
558 let entry = entry.map_err(|error| ApplicabilityResolutionError::RootUnreadable {
559 root: project_root.to_path_buf(),
560 reason: error.to_string(),
561 })?;
562 if !entry
563 .file_type()
564 .is_some_and(|file_type| file_type.is_file())
565 {
566 continue;
567 }
568 let file = entry.path();
569 for definition in servers_for_file(file, config) {
570 let Some(key) = server_key_for_definition(&definition, file, config) else {
571 continue;
572 };
573 if candidates.contains_key(&key) || producer_failures.contains_key(&key) {
574 continue;
575 }
576 if let Some(result) = self.failed_spawns.get(&key) {
577 producer_failures.insert(
578 key.clone(),
579 ApplicableServerFailure {
580 server_key: key,
581 result: result.clone(),
582 },
583 );
584 continue;
585 }
586 if self.resolve_binary(&definition, &key.root, config).is_err() {
587 producer_failures.insert(
588 key.clone(),
589 ApplicableServerFailure {
590 server_key: key,
591 result: ServerAttemptResult::BinaryNotInstalled {
592 binary: definition.binary.clone(),
593 },
594 },
595 );
596 continue;
597 }
598 candidates.insert(
599 key.clone(),
600 ApplicableServerCandidate {
601 key,
602 definition,
603 source_file: file.to_path_buf(),
604 },
605 );
606 }
607 }
608
609 if let Some(scope_roots) = scope_roots.filter(|roots| !roots.is_empty()) {
610 let rust_roots = candidates
611 .keys()
612 .chain(producer_failures.keys())
613 .filter(|key| key.kind == ServerKind::Rust)
614 .map(|key| key.root.clone())
615 .collect::<HashSet<_>>();
616 let mut owners = HashSet::new();
617 for scope_root in scope_roots {
618 if let Some(owner) = rust_roots
619 .iter()
620 .filter(|root| scope_root.starts_with(root))
621 .max_by_key(|root| root.components().count())
622 {
623 owners.insert(owner.clone());
624 }
625 }
626 candidates.retain(|key, _| key.kind != ServerKind::Rust || owners.contains(&key.root));
627 producer_failures
628 .retain(|key, _| key.kind != ServerKind::Rust || owners.contains(&key.root));
629 }
630
631 let mut candidates = candidates.into_values().collect::<Vec<_>>();
632 candidates.sort_by(|left, right| server_key_sort(&left.key, &right.key));
633 let mut producer_failures = producer_failures.into_values().collect::<Vec<_>>();
634 producer_failures
635 .sort_by(|left, right| server_key_sort(&left.server_key, &right.server_key));
636 let mut server_keys = candidates
637 .iter()
638 .map(|candidate| candidate.key.clone())
639 .chain(
640 producer_failures
641 .iter()
642 .map(|failure| failure.server_key.clone()),
643 )
644 .collect::<Vec<_>>();
645 server_keys.sort_by(server_key_sort);
646 Ok(ApplicableServerSnapshot {
647 server_keys,
648 candidates,
649 producer_failures,
650 })
651 }
652
653 pub fn start_applicable_servers(
659 &mut self,
660 snapshot: &ApplicableServerSnapshot,
661 config: &Config,
662 ) -> ApplicableServerStartOutcomes {
663 self.start_applicable_servers_inner(snapshot, config, None)
664 }
665
666 pub fn start_applicable_server_until(
670 &mut self,
671 snapshot: &ApplicableServerSnapshot,
672 server: &ServerKey,
673 config: &Config,
674 deadline: std::time::Instant,
675 ) -> ApplicableServerStartOutcomes {
676 let single = ApplicableServerSnapshot {
677 server_keys: vec![server.clone()],
678 candidates: snapshot
679 .candidates
680 .iter()
681 .filter(|candidate| candidate.key == *server)
682 .cloned()
683 .collect(),
684 producer_failures: snapshot
685 .producer_failures
686 .iter()
687 .filter(|failure| failure.server_key == *server)
688 .cloned()
689 .collect(),
690 };
691 self.start_applicable_servers_inner(&single, config, Some(deadline))
692 }
693
694 fn start_applicable_servers_inner(
695 &mut self,
696 snapshot: &ApplicableServerSnapshot,
697 config: &Config,
698 deadline: Option<std::time::Instant>,
699 ) -> ApplicableServerStartOutcomes {
700 let mut outcomes = ApplicableServerStartOutcomes {
701 failures: snapshot.producer_failures.clone(),
702 ..ApplicableServerStartOutcomes::default()
703 };
704 for candidate in &snapshot.candidates {
705 let initialize_timeout = deadline
706 .map(|deadline| deadline.saturating_duration_since(std::time::Instant::now()));
707 if initialize_timeout.is_some_and(|remaining| remaining.is_zero()) {
708 outcomes.deadline_exceeded = Some(candidate.key.clone());
709 break;
710 }
711 if self.clients.contains_key(&candidate.key) {
712 outcomes.successful.push(candidate.key.clone());
713 continue;
714 }
715 match self.spawn_server_with_timeout(
716 &candidate.definition,
717 &candidate.key.root,
718 &candidate.source_file,
719 config,
720 initialize_timeout,
721 ) {
722 Ok(client) => {
723 self.clients.insert(candidate.key.clone(), client);
724 self.server_binaries
725 .insert(candidate.key.clone(), candidate.definition.binary.clone());
726 self.documents.entry(candidate.key.clone()).or_default();
727 outcomes.successful.push(candidate.key.clone());
728 }
729 Err(error) => {
730 if deadline.is_some_and(|deadline| std::time::Instant::now() >= deadline) {
731 outcomes.deadline_exceeded = Some(candidate.key.clone());
734 break;
735 }
736 let result = classify_spawn_error(&candidate.definition.binary, &error);
737 self.failed_spawns
738 .insert(candidate.key.clone(), result.clone());
739 outcomes.failures.push(ApplicableServerFailure {
740 server_key: candidate.key.clone(),
741 result,
742 });
743 }
744 }
745 }
746 outcomes
747 }
748
749 pub fn ensure_server_for_file(&mut self, file_path: &Path, config: &Config) -> Vec<ServerKey> {
756 self.ensure_server_for_file_detailed(file_path, config)
757 .successful
758 }
759
760 fn running_server_keys_for_file(&self, file_path: &Path, config: &Config) -> Vec<ServerKey> {
761 servers_for_file(file_path, config)
762 .into_iter()
763 .filter_map(|def| server_key_for_definition(&def, file_path, config))
764 .filter(|key| self.clients.contains_key(key))
765 .collect()
766 }
767
768 pub fn navigation_requires_deferred_execution(
773 &self,
774 file_path: &Path,
775 config: &Config,
776 ) -> bool {
777 let Ok(canonical_path) = canonicalize_for_lsp(file_path) else {
778 return false;
779 };
780 servers_for_file(&canonical_path, config)
781 .into_iter()
782 .filter_map(|definition| {
783 let key = server_key_for_definition(&definition, &canonical_path, config)?;
784 Some((definition, key))
785 })
786 .any(|(definition, key)| {
787 if let Some(client) = self.clients.get(&key) {
788 return client.state() != ServerState::Ready;
789 }
790 !self.failed_spawns.contains_key(&key)
791 && self.resolve_binary(&definition, &key.root, config).is_ok()
792 })
793 }
794
795 pub fn ensure_server_for_file_detailed(
803 &mut self,
804 file_path: &Path,
805 config: &Config,
806 ) -> EnsureServerOutcomes {
807 let defs = servers_for_file(file_path, config);
808 let mut outcomes = EnsureServerOutcomes::default();
809
810 for def in defs {
811 let server_id = def.kind.id_str().to_string();
812 let server_name = def.name.to_string();
813
814 let Some(key) = server_key_for_definition(&def, file_path, config) else {
815 outcomes.attempts.push(ServerAttempt {
816 server_id,
817 server_name,
818 result: ServerAttemptResult::NoRootMarker {
819 looked_for: def.root_markers.iter().map(|s| s.to_string()).collect(),
820 },
821 });
822 continue;
823 };
824
825 if !self.clients.contains_key(&key) {
826 if let Some(cached) = self.failed_spawns.get(&key) {
833 outcomes.attempts.push(ServerAttempt {
834 server_id,
835 server_name,
836 result: cached.clone(),
837 });
838 continue;
839 }
840
841 match self.spawn_server(&def, &key.root, file_path, config) {
842 Ok(client) => {
843 self.clients.insert(key.clone(), client);
844 self.server_binaries.insert(key.clone(), def.binary.clone());
845 self.documents.entry(key.clone()).or_default();
846 }
847 Err(err) => {
848 slog_error!("failed to spawn {}: {}", def.name, err);
849 let result = classify_spawn_error(&def.binary, &err);
850 self.failed_spawns.insert(key.clone(), result.clone());
854 outcomes.attempts.push(ServerAttempt {
855 server_id,
856 server_name,
857 result,
858 });
859 continue;
860 }
861 }
862 }
863
864 outcomes.attempts.push(ServerAttempt {
865 server_id,
866 server_name,
867 result: ServerAttemptResult::Ok {
868 server_key: key.clone(),
869 },
870 });
871 outcomes.successful.push(key);
872 }
873
874 outcomes
875 }
876
877 pub fn ensure_server_for_file_default(&mut self, file_path: &Path) -> Vec<ServerKey> {
880 self.ensure_server_for_file(file_path, &Config::default())
881 }
882 pub fn ensure_file_open(
887 &mut self,
888 file_path: &Path,
889 config: &Config,
890 ) -> Result<EnsureFileOpenResult, LspError> {
891 let canonical_path = canonicalize_for_lsp(file_path)?;
892 let server_keys = self.ensure_server_for_file(&canonical_path, config);
893 if server_keys.is_empty() {
894 return Ok(EnsureFileOpenResult::default());
895 }
896
897 let uri = uri_for_path(&canonical_path)?;
898 let language_id = language_id_for_extension(
899 canonical_path
900 .extension()
901 .and_then(|ext| ext.to_str())
902 .unwrap_or_default(),
903 )
904 .to_string();
905 let needs_content = server_keys.iter().any(|key| {
906 !self
907 .documents
908 .get(key)
909 .is_some_and(|store| store.is_open(&canonical_path))
910 });
911 let initial_content = needs_content
912 .then(|| std::fs::read_to_string(&canonical_path).map_err(LspError::Io))
913 .transpose()?;
914 let mut newly_opened = Vec::new();
915
916 for key in &server_keys {
917 let already_open = self
918 .documents
919 .get(key)
920 .is_some_and(|store| store.is_open(&canonical_path));
921
922 if !already_open {
923 let content = initial_content
924 .as_ref()
925 .expect("content is loaded when any server needs didOpen");
926 let send_result = if let Some(client) = self.clients.get_mut(key) {
927 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
928 text_document: TextDocumentItem::new(
929 uri.clone(),
930 language_id.clone(),
931 0,
932 content.clone(),
933 ),
934 })
935 } else {
936 Ok(())
937 };
938 if let Err(err) = send_result {
939 let _ = self.close_file_for_servers(&canonical_path, &newly_opened);
940 return Err(err);
941 }
942 self.documents
943 .entry(key.clone())
944 .or_default()
945 .open(canonical_path.clone());
946 newly_opened.push(key.clone());
947 continue;
948 }
949
950 let drifted = self
960 .documents
961 .get(key)
962 .is_some_and(|store| store.is_stale_on_disk(&canonical_path));
963 if drifted {
964 let content = match std::fs::read_to_string(&canonical_path) {
965 Ok(content) => content,
966 Err(err) => {
967 let _ = self.close_file_for_servers(&canonical_path, &newly_opened);
968 return Err(LspError::Io(err));
969 }
970 };
971 let next_version = self
972 .documents
973 .get(key)
974 .and_then(|store| store.version(&canonical_path))
975 .map(|v| v + 1)
976 .unwrap_or(1);
977 let send_result = if let Some(client) = self.clients.get_mut(key) {
978 client.send_notification::<DidChangeTextDocument>(DidChangeTextDocumentParams {
979 text_document: VersionedTextDocumentIdentifier::new(
980 uri.clone(),
981 next_version,
982 ),
983 content_changes: vec![TextDocumentContentChangeEvent {
984 range: None,
985 range_length: None,
986 text: content,
987 }],
988 })
989 } else {
990 Ok(())
991 };
992 if let Err(err) = send_result {
993 let _ = self.close_file_for_servers(&canonical_path, &newly_opened);
994 return Err(err);
995 }
996 if let Some(store) = self.documents.get_mut(key) {
997 store.bump_version(&canonical_path);
998 }
999 }
1000 }
1001
1002 Ok(EnsureFileOpenResult {
1003 server_keys,
1004 newly_opened,
1005 })
1006 }
1007
1008 pub fn ensure_file_open_default(
1009 &mut self,
1010 file_path: &Path,
1011 ) -> Result<EnsureFileOpenResult, LspError> {
1012 self.ensure_file_open(file_path, &Config::default())
1013 }
1014
1015 pub fn notify_file_changed(
1021 &mut self,
1022 file_path: &Path,
1023 content: &str,
1024 config: &Config,
1025 ) -> Result<(), LspError> {
1026 self.notify_file_changed_versioned(file_path, content, config)
1027 .map(|_| ())
1028 }
1029
1030 pub fn notify_file_changed_versioned(
1041 &mut self,
1042 file_path: &Path,
1043 content: &str,
1044 config: &Config,
1045 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
1046 let canonical_path = canonicalize_for_lsp(file_path)?;
1047 let server_keys = self.ensure_server_for_file(&canonical_path, config);
1048 self.notify_file_changed_for_server_keys(canonical_path, content, server_keys)
1049 }
1050
1051 pub fn notify_file_changed_if_running(
1057 &mut self,
1058 file_path: &Path,
1059 content: &str,
1060 config: &Config,
1061 ) -> Result<(), LspError> {
1062 self.notify_file_changed_if_running_versioned(file_path, content, config)
1063 .map(|_| ())
1064 }
1065
1066 pub fn notify_file_changed_if_running_versioned(
1070 &mut self,
1071 file_path: &Path,
1072 content: &str,
1073 config: &Config,
1074 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
1075 let canonical_path = canonicalize_for_lsp(file_path)?;
1076 let server_keys = self.running_server_keys_for_file(&canonical_path, config);
1077 self.notify_file_changed_for_server_keys(canonical_path, content, server_keys)
1078 }
1079
1080 fn notify_file_changed_for_server_keys(
1081 &mut self,
1082 canonical_path: PathBuf,
1083 content: &str,
1084 server_keys: Vec<ServerKey>,
1085 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
1086 if server_keys.is_empty() {
1087 return Ok(Vec::new());
1088 }
1089
1090 let uri = uri_for_path(&canonical_path)?;
1091 let language_id = language_id_for_extension(
1092 canonical_path
1093 .extension()
1094 .and_then(|ext| ext.to_str())
1095 .unwrap_or_default(),
1096 )
1097 .to_string();
1098
1099 let mut versions: Vec<(ServerKey, i32)> = Vec::with_capacity(server_keys.len());
1100
1101 for key in server_keys {
1102 let current_version = self
1103 .documents
1104 .get(&key)
1105 .and_then(|store| store.version(&canonical_path));
1106
1107 if let Some(version) = current_version {
1108 let next_version = version + 1;
1109 if let Some(client) = self.clients.get_mut(&key) {
1110 client.send_notification::<DidChangeTextDocument>(
1111 DidChangeTextDocumentParams {
1112 text_document: VersionedTextDocumentIdentifier::new(
1113 uri.clone(),
1114 next_version,
1115 ),
1116 content_changes: vec![TextDocumentContentChangeEvent {
1117 range: None,
1118 range_length: None,
1119 text: content.to_string(),
1120 }],
1121 },
1122 )?;
1123 }
1124 if let Some(store) = self.documents.get_mut(&key) {
1125 store.bump_version(&canonical_path);
1126 }
1127 versions.push((key, next_version));
1128 continue;
1129 }
1130
1131 if let Some(client) = self.clients.get_mut(&key) {
1132 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
1133 text_document: TextDocumentItem::new(
1134 uri.clone(),
1135 language_id.clone(),
1136 0,
1137 content.to_string(),
1138 ),
1139 })?;
1140 }
1141 self.documents
1142 .entry(key.clone())
1143 .or_default()
1144 .open(canonical_path.clone());
1145 versions.push((key, 0));
1148 }
1149
1150 Ok(versions)
1151 }
1152
1153 pub fn notify_file_changed_default(
1154 &mut self,
1155 file_path: &Path,
1156 content: &str,
1157 ) -> Result<(), LspError> {
1158 self.notify_file_changed(file_path, content, &Config::default())
1159 }
1160
1161 pub fn notify_files_watched_changed(
1167 &mut self,
1168 paths: &[(PathBuf, FileChangeType)],
1169 _config: &Config,
1170 ) -> Result<(), LspError> {
1171 #[cfg(windows)]
1172 let mut trace = vec![format!(
1173 "input_paths={paths:?}; active_keys={:?}",
1174 self.clients.keys().collect::<Vec<_>>()
1175 )];
1176
1177 if paths.is_empty() {
1178 #[cfg(windows)]
1179 {
1180 trace.push("outcome=no-input-paths".to_string());
1181 self.last_watched_file_notification_trace = trace.join("\n");
1182 }
1183 return Ok(());
1184 }
1185
1186 let mut canonical_events = Vec::with_capacity(paths.len());
1187 for (path, typ) in paths {
1188 let canonical_path = resolve_for_lsp_uri(path);
1189 canonical_events.push((canonical_path, *typ));
1190 }
1191 #[cfg(windows)]
1192 trace.push(format!("resolved_events={canonical_events:?}"));
1193
1194 let keys: Vec<ServerKey> = self.clients.keys().cloned().collect();
1195 #[cfg(windows)]
1196 if keys.is_empty() {
1197 trace.push("outcome=no-active-client".to_string());
1198 }
1199 for key in keys {
1200 let mut changes = Vec::new();
1201 for (path, typ) in &canonical_events {
1202 if !path.starts_with(&key.root) {
1203 continue;
1204 }
1205 changes.push(FileEvent::new(uri_for_path(path)?, *typ));
1206 }
1207
1208 if changes.is_empty() {
1209 #[cfg(windows)]
1210 trace.push(format!("key={key:?}; outcome=outside-root"));
1211 continue;
1212 }
1213
1214 if let Some(client) = self.clients.get_mut(&key) {
1215 let supports_static_watched_files = client.supports_watched_files();
1221 let has_dynamic_registration = client.has_watched_file_registration();
1222 if !(supports_static_watched_files || has_dynamic_registration) {
1223 #[cfg(windows)]
1224 trace.push(format!(
1225 "key={key:?}; changes={changes:?}; outcome=unsupported; static={supports_static_watched_files}; dynamic={has_dynamic_registration}"
1226 ));
1227 if self.watched_file_skip_logged.insert(key.clone()) {
1228 log::debug!(
1229 "skipping didChangeWatchedFiles for {:?} (not supported or registered)",
1230 key
1231 );
1232 }
1233 continue;
1234 }
1235 #[cfg(windows)]
1236 trace.push(format!("key={key:?}; changes={changes:?}; action=send"));
1237 let send_result = client.send_notification::<DidChangeWatchedFiles>(
1238 DidChangeWatchedFilesParams { changes },
1239 );
1240 #[cfg(windows)]
1241 trace.push(format!(
1242 "key={key:?}; outcome={}",
1243 if send_result.is_ok() {
1244 "sent"
1245 } else {
1246 "send-error"
1247 }
1248 ));
1249 if let Err(error) = send_result {
1250 #[cfg(windows)]
1251 {
1252 self.last_watched_file_notification_trace = trace.join("\n");
1253 }
1254 return Err(error);
1255 }
1256 }
1257 }
1258
1259 #[cfg(windows)]
1260 {
1261 self.last_watched_file_notification_trace = trace.join("\n");
1262 }
1263 Ok(())
1264 }
1265
1266 pub fn notify_file_closed(&mut self, file_path: &Path) -> Result<(), LspError> {
1268 let canonical_path = canonicalize_for_lsp(file_path)?;
1269 let keys = self
1270 .documents
1271 .iter()
1272 .filter(|(_, store)| store.is_open(&canonical_path))
1273 .map(|(key, _)| key.clone())
1274 .collect::<Vec<_>>();
1275 self.close_file_for_servers(&canonical_path, &keys)
1276 }
1277
1278 pub(crate) fn close_file_for_servers(
1283 &mut self,
1284 file_path: &Path,
1285 server_keys: &[ServerKey],
1286 ) -> Result<(), LspError> {
1287 let canonical_path = canonicalize_for_lsp(file_path)?;
1288 let uri = uri_for_path(&canonical_path)?;
1289 let mut first_error = None;
1290
1291 for key in server_keys {
1292 let was_open = self
1293 .documents
1294 .get(key)
1295 .is_some_and(|store| store.is_open(&canonical_path));
1296 if !was_open {
1297 continue;
1298 }
1299
1300 if let Some(client) = self.clients.get_mut(key) {
1301 if let Err(err) =
1302 client.send_notification::<DidCloseTextDocument>(DidCloseTextDocumentParams {
1303 text_document: TextDocumentIdentifier::new(uri.clone()),
1304 })
1305 {
1306 if first_error.is_none() {
1307 first_error = Some(err);
1308 }
1309 }
1310 }
1311
1312 if let Some(store) = self.documents.get_mut(key) {
1313 store.close(&canonical_path);
1314 }
1315 self.diagnostics.clear_for_server_file(key, &canonical_path);
1316 }
1317
1318 match first_error {
1319 Some(err) => Err(err),
1320 None => Ok(()),
1321 }
1322 }
1323
1324 pub fn client_for_file(&self, file_path: &Path, config: &Config) -> Option<&LspClient> {
1326 let key = self.server_key_for_file(file_path, config)?;
1327 self.clients.get(&key)
1328 }
1329
1330 pub fn client_for_file_default(&self, file_path: &Path) -> Option<&LspClient> {
1331 self.client_for_file(file_path, &Config::default())
1332 }
1333
1334 pub fn client_for_file_mut(
1336 &mut self,
1337 file_path: &Path,
1338 config: &Config,
1339 ) -> Option<&mut LspClient> {
1340 let key = self.server_key_for_file(file_path, config)?;
1341 self.clients.get_mut(&key)
1342 }
1343
1344 pub fn client_for_file_mut_default(&mut self, file_path: &Path) -> Option<&mut LspClient> {
1345 self.client_for_file_mut(file_path, &Config::default())
1346 }
1347
1348 pub fn active_client_count(&self) -> usize {
1350 self.clients.len()
1351 }
1352
1353 pub fn drain_events(&mut self) -> DrainedLspEvents {
1355 self.drain_events_bounded(usize::MAX)
1356 }
1357
1358 pub fn has_pending_events(&self) -> bool {
1361 !self.event_rx.is_empty()
1362 }
1363
1364 pub fn drain_events_bounded(&mut self, max_events: usize) -> DrainedLspEvents {
1365 let mut events = Vec::new();
1366 let mut diagnostics_changed = false;
1367 let mut accepted_snapshots = Vec::new();
1368 while events.len() < max_events {
1369 let Ok(event) = self.event_rx.try_recv() else {
1370 break;
1371 };
1372 if self.handle_event(&event).is_some() {
1373 diagnostics_changed = true;
1374 }
1375 if let Some(snapshot) = self.accepted_live_publish_snapshot(&event) {
1376 accepted_snapshots.push(snapshot);
1377 }
1378 events.push(event);
1379 }
1380 let has_more = events.len() >= max_events && !self.event_rx.is_empty();
1381 DrainedLspEvents {
1382 events,
1383 diagnostics_changed,
1384 accepted_snapshots,
1385 has_more,
1386 }
1387 }
1388
1389 pub fn wait_for_diagnostics(
1391 &mut self,
1392 file_path: &Path,
1393 config: &Config,
1394 timeout: std::time::Duration,
1395 ) -> Vec<StoredDiagnostic> {
1396 let deadline = std::time::Instant::now() + timeout;
1397 self.wait_for_file_diagnostics(file_path, config, deadline)
1398 }
1399
1400 pub fn wait_for_diagnostics_default(
1401 &mut self,
1402 file_path: &Path,
1403 timeout: std::time::Duration,
1404 ) -> Vec<StoredDiagnostic> {
1405 self.wait_for_diagnostics(file_path, &Config::default(), timeout)
1406 }
1407
1408 #[doc(hidden)]
1413 pub fn diagnostics_store_for_test(&self) -> &DiagnosticsStore {
1414 &self.diagnostics
1415 }
1416
1417 #[doc(hidden)]
1418 pub fn diagnostics_store_mut_for_test(&mut self) -> &mut DiagnosticsStore {
1419 &mut self.diagnostics
1420 }
1421
1422 #[doc(hidden)]
1423 pub fn post_edit_outcome_for_entry_for_test(
1424 key: ServerKey,
1425 entry: &DiagnosticEntry,
1426 target_version: i32,
1427 pre: PreEditSnapshot,
1428 ) -> PostEditWaitOutcome {
1429 Self::post_edit_outcome_for_entry(key, entry, target_version, pre)
1430 }
1431
1432 fn post_edit_outcome_for_entry(
1433 key: ServerKey,
1434 entry: &DiagnosticEntry,
1435 target_version: i32,
1436 pre: PreEditSnapshot,
1437 ) -> PostEditWaitOutcome {
1438 let mut fresh = HashMap::new();
1439 if let Some(diagnostics) =
1440 Self::authoritative_post_edit_diagnostics(entry, target_version, pre)
1441 {
1442 fresh.insert(key.clone(), diagnostics);
1443 }
1444 Self::post_edit_outcome(vec![(key, target_version)], fresh, Vec::new())
1445 }
1446
1447 fn authoritative_post_edit_diagnostics(
1448 entry: &DiagnosticEntry,
1449 target_version: i32,
1450 pre: PreEditSnapshot,
1451 ) -> Option<Vec<StoredDiagnostic>> {
1452 (!entry.provisional && post_edit_entry_is_fresh(entry, target_version, pre))
1453 .then(|| entry.diagnostics.clone())
1454 }
1455
1456 #[doc(hidden)]
1457 pub fn enqueue_event_for_test(&self, event: LspEvent) {
1458 self.event_tx
1459 .send(event)
1460 .expect("LSP event receiver should remain connected");
1461 }
1462
1463 #[doc(hidden)]
1464 pub fn pending_event_count_for_test(&self) -> usize {
1465 self.event_rx.len()
1466 }
1467
1468 #[doc(hidden)]
1469 pub fn document_is_open_for_test(&self, file_path: &Path) -> bool {
1470 canonicalize_for_lsp(file_path).is_ok_and(|canonical_path| {
1471 self.documents
1472 .values()
1473 .any(|store| store.is_open(&canonical_path))
1474 })
1475 }
1476
1477 pub fn warm_error_warning_counts(&self) -> (usize, usize) {
1481 self.diagnostics.error_warning_counts()
1482 }
1483
1484 pub fn warm_error_warning_counts_with_provisional(&self) -> ((usize, usize), bool) {
1485 self.diagnostics.error_warning_counts_with_provisional()
1486 }
1487
1488 pub fn diagnostics_generation(&self) -> u64 {
1489 self.diagnostics.generation()
1490 }
1491
1492 pub fn filtered_error_warning_counts(
1497 &self,
1498 keep: impl FnMut(&std::path::Path) -> bool,
1499 ) -> (usize, usize) {
1500 self.diagnostics.filtered_error_warning_counts(keep)
1501 }
1502
1503 pub fn filtered_error_warning_counts_with_provisional(
1507 &self,
1508 keep: impl FnMut(&std::path::Path) -> bool,
1509 ) -> ((usize, usize), bool) {
1510 self.diagnostics
1511 .filtered_error_warning_counts_with_provisional(keep)
1512 }
1513
1514 pub fn provisional_server_keys(&self) -> Vec<ServerKey> {
1518 self.clients
1519 .iter()
1520 .filter(|(_, client)| client.diagnostics_are_provisional())
1521 .map(|(key, _)| key.clone())
1522 .collect()
1523 }
1524
1525 pub fn snapshot_diagnostic_epochs(&self, file_path: &Path) -> HashMap<ServerKey, u64> {
1530 let lookup_path = normalize_lookup_path(file_path);
1531 self.diagnostics
1532 .entries_for_file(&lookup_path)
1533 .into_iter()
1534 .map(|(key, entry)| (key.clone(), entry.epoch))
1535 .collect()
1536 }
1537
1538 pub fn snapshot_pre_edit_state(&self, file_path: &Path) -> HashMap<ServerKey, PreEditSnapshot> {
1541 let lookup_path = normalize_lookup_path(file_path);
1542 let mut snapshots: HashMap<ServerKey, PreEditSnapshot> = self
1543 .diagnostics
1544 .entries_for_file(&lookup_path)
1545 .into_iter()
1546 .map(|(key, entry)| {
1547 (
1548 key.clone(),
1549 PreEditSnapshot {
1550 epoch: entry.epoch,
1551 document_version_at_capture: None,
1552 },
1553 )
1554 })
1555 .collect();
1556
1557 for (key, store) in &self.documents {
1558 if let Some(version) = store.version(&lookup_path) {
1559 snapshots
1560 .entry(key.clone())
1561 .or_default()
1562 .document_version_at_capture = Some(version);
1563 }
1564 }
1565
1566 snapshots
1567 }
1568
1569 pub fn diagnostic_entry_is_fresh_for_document(
1577 &self,
1578 file_path: &Path,
1579 server_key: &ServerKey,
1580 pre: PreEditSnapshot,
1581 ) -> bool {
1582 let lookup_path = normalize_lookup_path(file_path);
1583 let Some(entry) = self
1584 .diagnostics
1585 .entries_for_file(&lookup_path)
1586 .into_iter()
1587 .find_map(|(key, entry)| if key == server_key { Some(entry) } else { None })
1588 else {
1589 return false;
1590 };
1591
1592 if entry.stale {
1593 return false;
1594 }
1595
1596 let target_version = self
1597 .documents
1598 .get(server_key)
1599 .and_then(|store| store.version(&lookup_path))
1600 .or(pre.document_version_at_capture)
1601 .unwrap_or(0);
1602
1603 matches!(entry.version, Some(version) if version >= target_version)
1604 }
1605
1606 pub(crate) fn start_post_edit_diagnostics_wait(
1610 &mut self,
1611 file_path: &Path,
1612 expected_versions: &[(ServerKey, i32)],
1613 pre_snapshot: &HashMap<ServerKey, PreEditSnapshot>,
1614 timeout: std::time::Duration,
1615 ) -> PostEditDiagnosticsWait {
1616 let lookup_path = normalize_lookup_path(file_path);
1617
1618 let _ = self.drain_events_for_file(&lookup_path);
1621
1622 let waiter_id = self.next_post_edit_waiter_id;
1623 self.next_post_edit_waiter_id = self.next_post_edit_waiter_id.wrapping_add(1);
1624 let (wake_tx, wake_rx) = bounded(1);
1625 self.post_edit_waiters.insert(waiter_id, wake_tx);
1626
1627 PostEditDiagnosticsWait {
1628 lookup_path,
1629 expected_versions: expected_versions.to_vec(),
1630 pre_snapshot: pre_snapshot.clone(),
1631 event_rx: self.event_rx.clone(),
1632 wake_rx,
1633 waiter_id,
1634 deadline: std::time::Instant::now() + timeout,
1635 fresh: HashMap::new(),
1636 exited: Vec::new(),
1637 }
1638 }
1639
1640 pub(crate) fn poll_post_edit_diagnostics_wait(
1641 &mut self,
1642 wait: &mut PostEditDiagnosticsWait,
1643 event: Option<LspEvent>,
1644 ) -> bool {
1645 if let Some(event) = event {
1646 self.handle_event(&event);
1647 }
1648
1649 for (key, target_version) in &wait.expected_versions {
1650 if wait.fresh.contains_key(key) || wait.exited.contains(key) {
1651 continue;
1652 }
1653 if !self.clients.contains_key(key) {
1654 wait.exited.push(key.clone());
1655 continue;
1656 }
1657 if let Some(entry) = self
1658 .diagnostics
1659 .entries_for_file(&wait.lookup_path)
1660 .into_iter()
1661 .find_map(|(stored_key, entry)| (stored_key == key).then_some(entry))
1662 {
1663 let pre = wait.pre_snapshot.get(key).copied().unwrap_or_default();
1664 if let Some(diagnostics) =
1665 Self::authoritative_post_edit_diagnostics(entry, *target_version, pre)
1666 {
1667 wait.fresh.insert(key.clone(), diagnostics);
1668 }
1669 }
1670 }
1671
1672 wait.fresh.len() + wait.exited.len() == wait.expected_versions.len()
1673 }
1674
1675 pub(crate) fn finish_post_edit_diagnostics_wait(
1676 &mut self,
1677 wait: PostEditDiagnosticsWait,
1678 ) -> PostEditWaitOutcome {
1679 self.post_edit_waiters.remove(&wait.waiter_id);
1680 Self::post_edit_outcome(wait.expected_versions, wait.fresh, wait.exited)
1681 }
1682
1683 pub fn wait_for_post_edit_diagnostics(
1691 &mut self,
1692 file_path: &Path,
1693 _config: &Config,
1697 expected_versions: &[(ServerKey, i32)],
1698 pre_snapshot: &HashMap<ServerKey, PreEditSnapshot>,
1699 timeout: std::time::Duration,
1700 ) -> PostEditWaitOutcome {
1701 let mut wait = self.start_post_edit_diagnostics_wait(
1702 file_path,
1703 expected_versions,
1704 pre_snapshot,
1705 timeout,
1706 );
1707 let mut complete = self.poll_post_edit_diagnostics_wait(&mut wait, None);
1708
1709 while !complete && !wait.deadline_reached() {
1710 let event = wait.next_event();
1711 complete = self.poll_post_edit_diagnostics_wait(&mut wait, event);
1712 }
1713
1714 self.finish_post_edit_diagnostics_wait(wait)
1715 }
1716
1717 fn post_edit_outcome(
1718 mut expected: Vec<(ServerKey, i32)>,
1719 mut fresh: HashMap<ServerKey, Vec<StoredDiagnostic>>,
1720 exited: Vec<ServerKey>,
1721 ) -> PostEditWaitOutcome {
1722 expected.sort_by(|(left, _), (right, _)| server_key_sort(left, right));
1723
1724 let mut accepted_snapshots = Vec::new();
1725 let mut pending_servers = Vec::new();
1726 for (server_key, document_version) in expected {
1727 if let Some(diagnostics) = fresh.remove(&server_key) {
1728 accepted_snapshots.push(AcceptedDiagnosticSnapshot::new(
1729 server_key,
1730 document_version,
1731 diagnostics,
1732 ));
1733 } else if !exited.contains(&server_key) {
1734 pending_servers.push(server_key);
1735 }
1736 }
1737
1738 let mut diagnostics = accepted_snapshots
1739 .iter()
1740 .flat_map(|snapshot| snapshot.diagnostics.iter().cloned())
1741 .collect::<Vec<_>>();
1742 diagnostics.sort_by(|left, right| {
1743 left.file
1744 .cmp(&right.file)
1745 .then(left.line.cmp(&right.line))
1746 .then(left.column.cmp(&right.column))
1747 .then(left.message.cmp(&right.message))
1748 });
1749
1750 PostEditWaitOutcome {
1751 accepted_snapshots,
1752 diagnostics,
1753 pending_servers,
1754 exited_servers: exited,
1755 }
1756 }
1757
1758 pub fn wait_for_file_diagnostics(
1764 &mut self,
1765 file_path: &Path,
1766 config: &Config,
1767 deadline: std::time::Instant,
1768 ) -> Vec<StoredDiagnostic> {
1769 let lookup_path = normalize_lookup_path(file_path);
1770
1771 if self.server_key_for_file(&lookup_path, config).is_none() {
1772 return Vec::new();
1773 }
1774
1775 loop {
1776 if self.drain_events_for_file(&lookup_path) {
1777 break;
1778 }
1779
1780 let now = std::time::Instant::now();
1781 if now >= deadline {
1782 break;
1783 }
1784
1785 let timeout = deadline.saturating_duration_since(now);
1786 match self.event_rx.recv_timeout(timeout) {
1787 Ok(event) => {
1788 if matches!(
1789 self.handle_event(&event),
1790 Some(ref published_file) if published_file.as_path() == lookup_path.as_path()
1791 ) {
1792 break;
1793 }
1794 }
1795 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
1796 }
1797 }
1798
1799 self.get_diagnostics_for_file(&lookup_path)
1800 .into_iter()
1801 .cloned()
1802 .collect()
1803 }
1804
1805 pub const PULL_FILE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
1811
1812 pub fn pull_file_timeout() -> std::time::Duration {
1814 Self::PULL_FILE_TIMEOUT
1815 }
1816
1817 const PULL_WORKSPACE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
1821
1822 pub fn pull_file_diagnostics(
1833 &mut self,
1834 file_path: &Path,
1835 config: &Config,
1836 ) -> Result<Vec<PullFileResult>, LspError> {
1837 let canonical_path = canonicalize_for_lsp(file_path)?;
1838 let opened = self.ensure_file_open(&canonical_path, config)?;
1841 if opened.server_keys.is_empty() {
1842 return Ok(Vec::new());
1843 }
1844
1845 let uri = uri_for_path(&canonical_path)?;
1846 let mut results = Vec::with_capacity(opened.server_keys.len());
1847
1848 for key in opened.server_keys {
1849 let supports_pull = self
1850 .clients
1851 .get(&key)
1852 .and_then(|c| c.diagnostic_capabilities())
1853 .is_some_and(|caps| caps.pull_diagnostics);
1854
1855 if !supports_pull {
1856 results.push(PullFileResult {
1857 server_key: key.clone(),
1858 outcome: PullFileOutcome::PullNotSupported,
1859 });
1860 continue;
1861 }
1862
1863 let previous_result_id = self
1865 .diagnostics
1866 .entries_for_file(&canonical_path)
1867 .into_iter()
1868 .find(|(k, _)| **k == key)
1869 .and_then(|(_, entry)| entry.result_id.clone());
1870
1871 let identifier = self
1872 .clients
1873 .get(&key)
1874 .and_then(|c| c.diagnostic_capabilities())
1875 .and_then(|caps| caps.identifier.clone());
1876
1877 let params = AftDocumentDiagnosticParams {
1878 text_document: lsp_types::TextDocumentIdentifier { uri: uri.clone() },
1879 identifier,
1880 previous_result_id,
1881 work_done_progress_params: Default::default(),
1882 partial_result_params: Default::default(),
1883 };
1884
1885 let outcome = match self.send_pull_request(&key, params) {
1886 Ok(report) => {
1887 if matches!(
1888 &report,
1889 lsp_types::DocumentDiagnosticReportResult::Report(
1890 lsp_types::DocumentDiagnosticReport::Full(_)
1891 )
1892 ) {
1893 self.drain_events();
1898 }
1899 self.ingest_document_report(&key, &canonical_path, report)
1900 }
1901 Err(err) => {
1902 if let Some(result) = self.cache_post_initialize_exit(&key, &err) {
1903 PullFileOutcome::RequestFailed {
1904 reason: server_attempt_result_reason(&result),
1905 }
1906 } else if recoverable_pull_rejection(&err)
1907 && self.clients.get(&key).is_some_and(|client| {
1908 matches!(
1909 client.state(),
1910 ServerState::Ready | ServerState::Initializing
1911 )
1912 })
1913 {
1914 PullFileOutcome::RequestFailed {
1915 reason: format!("pull_rejected_push_fallback: {err}"),
1916 }
1917 } else {
1918 PullFileOutcome::RequestFailed {
1919 reason: err.to_string(),
1920 }
1921 }
1922 }
1923 };
1924
1925 results.push(PullFileResult {
1926 server_key: key,
1927 outcome,
1928 });
1929 }
1930
1931 Ok(results)
1932 }
1933
1934 pub fn pull_workspace_diagnostics(
1939 &mut self,
1940 server_key: &ServerKey,
1941 timeout: Option<std::time::Duration>,
1942 ) -> Result<PullWorkspaceResult, LspError> {
1943 let timeout = timeout.unwrap_or(Self::PULL_WORKSPACE_TIMEOUT);
1944
1945 let supports_workspace = self
1946 .clients
1947 .get(server_key)
1948 .and_then(|c| c.diagnostic_capabilities())
1949 .is_some_and(|caps| caps.workspace_diagnostics);
1950
1951 if !supports_workspace {
1952 return Ok(PullWorkspaceResult {
1953 server_key: server_key.clone(),
1954 files_reported: Vec::new(),
1955 complete: false,
1956 cancelled: false,
1957 supports_workspace: false,
1958 });
1959 }
1960
1961 let identifier = self
1962 .clients
1963 .get(server_key)
1964 .and_then(|c| c.diagnostic_capabilities())
1965 .and_then(|caps| caps.identifier.clone());
1966
1967 let params = AftWorkspaceDiagnosticParams {
1968 identifier,
1969 previous_result_ids: Vec::new(),
1970 work_done_progress_params: Default::default(),
1971 partial_result_params: Default::default(),
1972 };
1973
1974 let result = match self
1975 .clients
1976 .get_mut(server_key)
1977 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?
1978 .send_request_with_timeout::<AftWorkspaceDiagnosticRequest>(params, timeout)
1979 {
1980 Ok(result) => result,
1981 Err(LspError::Timeout(_)) => {
1982 return Ok(PullWorkspaceResult {
1983 server_key: server_key.clone(),
1984 files_reported: Vec::new(),
1985 complete: false,
1986 cancelled: true,
1987 supports_workspace: true,
1988 });
1989 }
1990 Err(err) => {
1991 if let Some(result) = self.cache_post_initialize_exit(server_key, &err) {
1992 return Err(LspError::ServerNotReady(server_attempt_result_reason(
1993 &result,
1994 )));
1995 }
1996 return Err(err);
1997 }
1998 };
1999
2000 let (items, complete) = match result {
2004 lsp_types::WorkspaceDiagnosticReportResult::Report(report) => (report.items, true),
2005 lsp_types::WorkspaceDiagnosticReportResult::Partial(partial) => (partial.items, false),
2006 };
2007
2008 let mut files_reported = Vec::with_capacity(items.len());
2010 for item in items {
2011 match item {
2012 lsp_types::WorkspaceDocumentDiagnosticReport::Full(full) => {
2013 if let Some(file) = uri_to_path(&full.uri) {
2014 let stored = from_lsp_diagnostics(
2015 file.clone(),
2016 full.full_document_diagnostic_report.items.clone(),
2017 );
2018 self.diagnostics.publish_with_result_id(
2019 server_key.clone(),
2020 file.clone(),
2021 stored,
2022 full.full_document_diagnostic_report.result_id.clone(),
2023 );
2024 files_reported.push(file);
2025 }
2026 }
2027 lsp_types::WorkspaceDocumentDiagnosticReport::Unchanged(_unchanged) => {
2028 }
2031 }
2032 }
2033
2034 Ok(PullWorkspaceResult {
2035 server_key: server_key.clone(),
2036 files_reported,
2037 complete,
2038 cancelled: false,
2039 supports_workspace: true,
2040 })
2041 }
2042
2043 fn cache_post_initialize_exit(
2044 &mut self,
2045 key: &ServerKey,
2046 err: &LspError,
2047 ) -> Option<ServerAttemptResult> {
2048 let binary = self
2049 .server_binaries
2050 .get(key)
2051 .cloned()
2052 .unwrap_or_else(|| key.kind.id_str().to_string());
2053 let (status, stderr_tail) = {
2054 let client = self.clients.get_mut(key)?;
2055 let mut status = client.child_exit_status();
2056 for _ in 0..10 {
2057 if status.is_some() {
2058 break;
2059 }
2060 std::thread::sleep(std::time::Duration::from_millis(10));
2061 status = client.child_exit_status();
2062 }
2063 let status = status?;
2064 wait_for_stderr_tail(client);
2065 (status, client.stderr_tail())
2066 };
2067 let reason = format_post_initialize_exit_reason(&binary, status, &stderr_tail, err);
2068 let result = ServerAttemptResult::SpawnFailed { binary, reason };
2069 self.clients.remove(key);
2070 self.server_binaries.remove(key);
2071 self.documents.remove(key);
2072 self.diagnostics.clear_for_server(key);
2073 self.failed_spawns.insert(key.clone(), result.clone());
2074 Some(result)
2075 }
2076
2077 fn send_pull_request(
2079 &mut self,
2080 key: &ServerKey,
2081 params: AftDocumentDiagnosticParams,
2082 ) -> Result<lsp_types::DocumentDiagnosticReportResult, LspError> {
2083 let client = self
2084 .clients
2085 .get_mut(key)
2086 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?;
2087 client.send_request_with_timeout::<AftDocumentDiagnosticRequest>(
2091 params,
2092 Self::PULL_FILE_TIMEOUT,
2093 )
2094 }
2095
2096 fn ingest_document_report(
2099 &mut self,
2100 key: &ServerKey,
2101 canonical_path: &Path,
2102 result: lsp_types::DocumentDiagnosticReportResult,
2103 ) -> PullFileOutcome {
2104 let report = match result {
2105 lsp_types::DocumentDiagnosticReportResult::Report(report) => report,
2106 lsp_types::DocumentDiagnosticReportResult::Partial(_) => {
2107 return PullFileOutcome::PartialNotSupported;
2111 }
2112 };
2113
2114 match report {
2115 lsp_types::DocumentDiagnosticReport::Full(full) => {
2116 let result_id = full.full_document_diagnostic_report.result_id.clone();
2117 let stored = from_lsp_diagnostics(
2118 canonical_path.to_path_buf(),
2119 full.full_document_diagnostic_report.items.clone(),
2120 );
2121 let count = stored.len();
2122 let provisional = self
2123 .clients
2124 .get(key)
2125 .is_some_and(|client| client.diagnostics_are_provisional());
2126 self.diagnostics.publish_full_with_provisional(
2127 key.clone(),
2128 canonical_path.to_path_buf(),
2129 stored,
2130 result_id,
2131 None,
2132 provisional,
2133 );
2134 PullFileOutcome::Full {
2135 diagnostic_count: count,
2136 }
2137 }
2138 lsp_types::DocumentDiagnosticReport::Unchanged(_unchanged) => {
2139 if self
2146 .diagnostics
2147 .has_report_for_server_file(key, canonical_path)
2148 {
2149 self.diagnostics
2150 .mark_fresh_for_server_file(key, canonical_path);
2151 let authoritative = self
2152 .clients
2153 .get(key)
2154 .map_or(true, |client| !client.diagnostics_are_provisional());
2155 if authoritative {
2156 self.diagnostics
2157 .clear_provisional_for_server_file(key, canonical_path);
2158 }
2159 PullFileOutcome::Unchanged
2160 } else {
2161 PullFileOutcome::RequestFailed {
2162 reason: "no_cache_for_unchanged".to_string(),
2163 }
2164 }
2165 }
2166 }
2167 }
2168
2169 pub fn shutdown_all(&mut self) {
2171 for (key, mut client) in self.clients.drain() {
2172 if let Err(err) = client.shutdown() {
2173 slog_error!("error shutting down {:?}: {}", key, err);
2174 }
2175 }
2176 self.server_binaries.clear();
2177 self.documents.clear();
2178 self.diagnostics = DiagnosticsStore::new();
2179 }
2180
2181 pub fn has_active_servers(&self) -> bool {
2183 self.clients
2184 .values()
2185 .any(|client| client.state() == ServerState::Ready)
2186 }
2187
2188 pub fn active_server_keys(&self) -> Vec<ServerKey> {
2191 self.clients.keys().cloned().collect()
2192 }
2193
2194 #[cfg(windows)]
2198 #[doc(hidden)]
2199 pub fn watched_file_notification_trace_for_test(&self) -> &str {
2200 &self.last_watched_file_notification_trace
2201 }
2202
2203 pub fn get_diagnostics_for_file(&self, file: &Path) -> Vec<&StoredDiagnostic> {
2204 let normalized = normalize_lookup_path(file);
2205 self.diagnostics.for_file(&normalized)
2206 }
2207
2208 pub fn get_diagnostics_for_file_with_provisional(
2209 &self,
2210 file: &Path,
2211 ) -> Vec<(&StoredDiagnostic, bool)> {
2212 let normalized = normalize_lookup_path(file);
2213 self.diagnostics.for_file_with_provisional(&normalized)
2214 }
2215
2216 pub fn clear_failed_spawns(&mut self) -> usize {
2237 let n = self.failed_spawns.len();
2238 self.failed_spawns.clear();
2239 n
2240 }
2241
2242 #[cfg(test)]
2243 pub(crate) fn insert_failed_spawn_for_test(&mut self) {
2244 let key = ServerKey {
2245 kind: crate::lsp::registry::ServerKind::Rust,
2246 root: std::path::PathBuf::from("/tmp/test-root"),
2247 };
2248 self.failed_spawns.insert(
2249 key,
2250 ServerAttemptResult::SpawnFailed {
2251 binary: "rust-analyzer".to_string(),
2252 reason: "test".to_string(),
2253 },
2254 );
2255 }
2256
2257 pub fn clear_diagnostics_for_file(&mut self, file: &Path) -> bool {
2258 diagnostic_path_candidates(file)
2259 .into_iter()
2260 .fold(false, |removed, candidate| {
2261 removed | self.diagnostics.clear_for_file(&candidate)
2262 })
2263 }
2264
2265 pub fn mark_diagnostics_stale_for_file(&mut self, file: &Path) -> StaleDiagnosticsMark {
2269 let mut result = StaleDiagnosticsMark::default();
2270 for candidate in diagnostic_path_candidates(file) {
2271 let (had_entries, changed) = self.diagnostics.mark_stale_for_file(&candidate);
2272 result.had_entries |= had_entries;
2273 result.changed |= changed;
2274 }
2275 result
2276 }
2277
2278 pub fn get_diagnostics_for_directory(&self, dir: &Path) -> Vec<&StoredDiagnostic> {
2279 let normalized = normalize_lookup_path(dir);
2280 self.diagnostics.for_directory(&normalized)
2281 }
2282
2283 pub fn get_diagnostics_for_directory_with_provisional(
2284 &self,
2285 dir: &Path,
2286 ) -> Vec<(&StoredDiagnostic, bool)> {
2287 let normalized = normalize_lookup_path(dir);
2288 self.diagnostics.for_directory_with_provisional(&normalized)
2289 }
2290
2291 pub fn get_all_diagnostics(&self) -> Vec<&StoredDiagnostic> {
2292 self.diagnostics.all()
2293 }
2294
2295 pub fn get_all_diagnostics_with_provisional(&self) -> Vec<(&StoredDiagnostic, bool)> {
2296 self.diagnostics.all_with_provisional()
2297 }
2298
2299 pub fn has_any_diagnostic_reports(&self) -> bool {
2304 self.diagnostics.has_any_fresh_report()
2305 }
2306
2307 pub fn has_diagnostic_report_for_file(&self, file: &Path) -> bool {
2311 let normalized = normalize_lookup_path(file);
2312 self.diagnostics.has_any_fresh_report_for_file(&normalized)
2313 }
2314
2315 pub fn has_diagnostic_report_for_server_file(&self, server: &ServerKey, file: &Path) -> bool {
2319 let normalized = normalize_lookup_path(file);
2320 self.diagnostics
2321 .has_fresh_report_for_server_file(server, &normalized)
2322 }
2323
2324 pub fn has_authoritative_report_for_file(&self, file: &Path) -> bool {
2329 let normalized = normalize_lookup_path(file);
2330 self.diagnostics
2331 .has_authoritative_report_for_file(&normalized)
2332 }
2333
2334 pub fn has_authoritative_report_for_server(&self, server: &ServerKey) -> bool {
2339 self.diagnostics.has_authoritative_report_for_server(server)
2340 }
2341
2342 pub fn server_is_warming(&self, server: &ServerKey) -> bool {
2347 self.clients
2348 .get(server)
2349 .is_some_and(|client| client.diagnostics_are_provisional())
2350 }
2351
2352 pub fn producer_has_settled(&self, server: &ServerKey) -> bool {
2357 self.has_authoritative_report_for_server(server) || !self.server_is_warming(server)
2358 }
2359
2360 pub fn producers_settled(&self, expected: &[ServerKey]) -> bool {
2364 expected
2365 .iter()
2366 .all(|server| self.producer_has_settled(server))
2367 }
2368
2369 fn drain_events_for_file(&mut self, file_path: &Path) -> bool {
2370 let mut saw_file_diagnostics = false;
2371 while let Ok(event) = self.event_rx.try_recv() {
2372 if matches!(
2373 self.handle_event(&event),
2374 Some(ref published_file) if published_file.as_path() == file_path
2375 ) {
2376 saw_file_diagnostics = true;
2377 }
2378 }
2379 saw_file_diagnostics
2380 }
2381
2382 fn accepted_live_publish_snapshot(
2383 &self,
2384 event: &LspEvent,
2385 ) -> Option<AcceptedDiagnosticSnapshot> {
2386 let LspEvent::Notification {
2387 server_kind,
2388 root,
2389 method,
2390 params: Some(params),
2391 } = event
2392 else {
2393 return None;
2394 };
2395 if method != "textDocument/publishDiagnostics" {
2396 return None;
2397 }
2398
2399 let publish_params =
2400 serde_json::from_value::<lsp_types::PublishDiagnosticsParams>(params.clone()).ok()?;
2401 let file = uri_to_path(&publish_params.uri)?;
2402 let server_key = ServerKey {
2403 kind: server_kind.clone(),
2404 root: root.clone(),
2405 };
2406 let client = self.clients.get(&server_key)?;
2407 if client.state() != ServerState::Ready || client.diagnostics_are_provisional() {
2408 return None;
2409 }
2410 let document_version = self.documents.get(&server_key)?.version(&file)?;
2411 let entry = self
2412 .diagnostics
2413 .entries_for_file(&file)
2414 .into_iter()
2415 .find_map(|(stored_key, entry)| (stored_key == &server_key).then_some(entry))?;
2416 if entry.stale || entry.provisional || entry.version != Some(document_version) {
2417 return None;
2418 }
2419
2420 Some(AcceptedDiagnosticSnapshot::new(
2421 server_key,
2422 document_version,
2423 entry.diagnostics.clone(),
2424 ))
2425 }
2426
2427 fn handle_event(&mut self, event: &LspEvent) -> Option<PathBuf> {
2428 let published_file = match event {
2429 LspEvent::Notification {
2430 server_kind,
2431 root,
2432 method,
2433 params: Some(params),
2434 } if method == "textDocument/publishDiagnostics" => {
2435 self.handle_publish_diagnostics(server_kind.clone(), root.clone(), params)
2436 }
2437 LspEvent::Notification {
2438 server_kind,
2439 root,
2440 method,
2441 params: Some(params),
2442 } if method == "experimental/serverStatus" => {
2443 self.handle_server_status(server_kind.clone(), root.clone(), params);
2444 None
2445 }
2446 LspEvent::ServerExited { server_kind, root } => {
2447 let key = ServerKey {
2448 kind: server_kind.clone(),
2449 root: root.clone(),
2450 };
2451 self.clients.remove(&key);
2452 self.server_binaries.remove(&key);
2453 self.documents.remove(&key);
2454 self.diagnostics.clear_for_server(&key);
2455 None
2456 }
2457 _ => None,
2458 };
2459 self.wake_post_edit_waiters();
2460 published_file
2461 }
2462
2463 fn wake_post_edit_waiters(&mut self) {
2464 Self::wake_waiters(&mut self.post_edit_waiters);
2465 }
2466
2467 fn wake_waiters(waiters: &mut HashMap<u64, Sender<()>>) {
2468 waiters.retain(|_, sender| match sender.try_send(()) {
2469 Ok(()) | Err(TrySendError::Full(())) => true,
2470 Err(TrySendError::Disconnected(())) => false,
2471 });
2472 }
2473
2474 fn handle_publish_diagnostics(
2475 &mut self,
2476 server: ServerKind,
2477 root: PathBuf,
2478 params: &serde_json::Value,
2479 ) -> Option<PathBuf> {
2480 if let Ok(publish_params) =
2481 serde_json::from_value::<lsp_types::PublishDiagnosticsParams>(params.clone())
2482 {
2483 let file = uri_to_path(&publish_params.uri)?;
2484 let stored = from_lsp_diagnostics(file.clone(), publish_params.diagnostics);
2485 let key = ServerKey { kind: server, root };
2490 let provisional = self
2491 .clients
2492 .get(&key)
2493 .is_some_and(|client| client.diagnostics_are_provisional());
2494 self.diagnostics.publish_full_with_provisional(
2495 key,
2496 file.clone(),
2497 stored,
2498 None,
2499 publish_params.version,
2500 provisional,
2501 );
2502 return Some(file);
2503 }
2504 None
2505 }
2506
2507 fn handle_server_status(
2508 &mut self,
2509 server: ServerKind,
2510 root: PathBuf,
2511 params: &serde_json::Value,
2512 ) {
2513 if !matches!(&server, ServerKind::Rust)
2514 || params.get("quiescent").and_then(serde_json::Value::as_bool) != Some(true)
2515 {
2516 return;
2517 }
2518
2519 let key = ServerKey { kind: server, root };
2520 let became_quiescent = self
2521 .clients
2522 .get_mut(&key)
2523 .is_some_and(|client| client.set_rust_analyzer_quiescent(true));
2524 if became_quiescent {
2525 self.diagnostics.promote_provisional_for_server(&key);
2526 }
2527 }
2528
2529 fn spawn_server(
2530 &self,
2531 def: &ServerDef,
2532 root: &Path,
2533 source_file: &Path,
2534 config: &Config,
2535 ) -> Result<LspClient, LspError> {
2536 self.spawn_server_with_timeout(def, root, source_file, config, None)
2537 }
2538
2539 fn spawn_server_with_timeout(
2540 &self,
2541 def: &ServerDef,
2542 root: &Path,
2543 source_file: &Path,
2544 config: &Config,
2545 initialize_timeout: Option<std::time::Duration>,
2546 ) -> Result<LspClient, LspError> {
2547 let initialization_options =
2548 initialization_options_for_spawn(def, source_file, root, config)?;
2549 let binary = self.resolve_binary(def, root, config)?;
2550
2551 let mut merged_env = def.env.clone();
2555 for (key, value) in &self.extra_env {
2556 merged_env.insert(key.clone(), value.clone());
2557 }
2558
2559 let reclaim_root = config
2564 .project_root
2565 .as_deref()
2566 .map(crate::inspect::job::canonicalize_normalized)
2567 .filter(|project_root| root.starts_with(project_root))
2568 .unwrap_or_else(|| root.to_path_buf());
2569
2570 let mut client = LspClient::spawn_with_reclaim_root(
2571 def.kind.clone(),
2572 root.to_path_buf(),
2573 &binary,
2574 &def.args,
2575 &merged_env,
2576 self.event_tx.clone(),
2577 self.child_registry.clone(),
2578 Some(&reclaim_root),
2579 )?;
2580 let initialize = match initialize_timeout {
2581 Some(timeout) => client.initialize_with_timeout(root, initialization_options, timeout),
2582 None => client.initialize(root, initialization_options),
2583 };
2584 if let Err(err) = initialize {
2585 wait_for_stderr_tail(&mut client);
2586 let stderr_tail = client.stderr_tail();
2587 let reason = if client.child_exited() || !stderr_tail.is_empty() {
2588 format_initialize_failure_reason(&def.binary, &stderr_tail, &err)
2589 } else {
2590 format!("server failed during initialize: {err}")
2591 };
2592 return Err(LspError::ServerNotReady(reason));
2593 }
2594 Ok(client)
2595 }
2596
2597 fn resolve_binary(
2598 &self,
2599 def: &ServerDef,
2600 root: &Path,
2601 config: &Config,
2602 ) -> Result<PathBuf, LspError> {
2603 if let Some(path) = self.binary_overrides.get(&def.kind) {
2604 if path.exists() {
2605 return Ok(path.clone());
2606 }
2607 return Err(LspError::NotFound(format!(
2608 "override binary for {:?} not found: {}",
2609 def.kind,
2610 path.display()
2611 )));
2612 }
2613
2614 if let Some(path) = env_binary_override(&def.kind) {
2615 if path.exists() {
2616 return Ok(path);
2617 }
2618 return Err(LspError::NotFound(format!(
2619 "environment override binary for {:?} not found: {}",
2620 def.kind,
2621 path.display()
2622 )));
2623 }
2624
2625 resolve_server_binary(def, Some(root), config).ok_or_else(|| {
2626 let searched = if matches!(def.kind, ServerKind::Python | ServerKind::Ty) {
2627 "the workspace virtualenv, node_modules/.bin, lsp_paths_extra, or PATH"
2628 } else {
2629 "node_modules/.bin, lsp_paths_extra, or PATH"
2630 };
2631 LspError::NotFound(format!(
2632 "language server binary '{}' not found in {searched}",
2633 def.binary,
2634 ))
2635 })
2636 }
2637
2638 fn server_key_for_file(&self, file_path: &Path, config: &Config) -> Option<ServerKey> {
2639 for def in servers_for_file(file_path, config) {
2640 let key = server_key_for_definition(&def, file_path, config)?;
2641 if self.clients.contains_key(&key) {
2642 return Some(key);
2643 }
2644 }
2645 None
2646 }
2647}
2648
2649impl Default for LspManager {
2650 fn default() -> Self {
2651 Self::new()
2652 }
2653}
2654
2655const ASTRO_TSDK_UNAVAILABLE: &str = "astro-ls requires a project TypeScript install; none found";
2656
2657fn initialization_options_for_spawn(
2658 def: &ServerDef,
2659 source_file: &Path,
2660 server_root: &Path,
2661 config: &Config,
2662) -> Result<Option<serde_json::Value>, LspError> {
2663 if def.kind != ServerKind::Astro {
2664 return Ok(def.initialization_options.clone());
2665 }
2666
2667 if def
2668 .initialization_options
2669 .as_ref()
2670 .and_then(|options| options.pointer("/typescript/tsdk"))
2671 .and_then(serde_json::Value::as_str)
2672 .is_some_and(|tsdk| !tsdk.is_empty())
2673 {
2674 return Ok(def.initialization_options.clone());
2675 }
2676
2677 let project_root = config.project_root.as_deref().unwrap_or(server_root);
2681 let boundary = if source_file.starts_with(project_root) {
2682 project_root
2683 } else {
2684 server_root
2685 };
2686 let tsdk = find_project_typescript_sdk(source_file, boundary)
2687 .ok_or_else(|| LspError::ServerNotReady(ASTRO_TSDK_UNAVAILABLE.to_string()))?;
2688 let mut options = serde_json::json!({
2689 "typescript": {
2690 "tsdk": tsdk.to_string_lossy(),
2691 }
2692 });
2693 if let Some(configured) = def.initialization_options.clone() {
2694 merge_json_override(&mut options, configured);
2695 }
2696 Ok(Some(options))
2697}
2698
2699fn find_project_typescript_sdk(source_file: &Path, project_root: &Path) -> Option<PathBuf> {
2700 let mut directory = source_file.parent()?;
2701 loop {
2702 let lib = directory
2703 .join("node_modules")
2704 .join("typescript")
2705 .join("lib");
2706 if lib.join("tsserverlibrary.js").is_file() || lib.join("typescript.js").is_file() {
2707 return Some(lib);
2708 }
2709 if directory == project_root {
2710 return None;
2711 }
2712 let parent = directory.parent()?;
2713 if !parent.starts_with(project_root) {
2714 return None;
2715 }
2716 directory = parent;
2717 }
2718}
2719
2720fn merge_json_override(base: &mut serde_json::Value, override_value: serde_json::Value) {
2721 match (base, override_value) {
2722 (serde_json::Value::Object(base), serde_json::Value::Object(override_fields)) => {
2723 for (key, value) in override_fields {
2724 if let Some(existing) = base.get_mut(&key) {
2725 merge_json_override(existing, value);
2726 } else {
2727 base.insert(key, value);
2728 }
2729 }
2730 }
2731 (base, value) => *base = value,
2732 }
2733}
2734
2735fn wait_for_stderr_tail(client: &mut LspClient) {
2736 for _ in 0..10 {
2737 if !client.stderr_tail().is_empty() {
2738 break;
2739 }
2740 std::thread::sleep(std::time::Duration::from_millis(10));
2741 }
2742}
2743
2744fn recoverable_pull_rejection(err: &LspError) -> bool {
2745 matches!(
2746 err,
2747 LspError::ServerError {
2748 code: -32601 | -32602,
2749 ..
2750 }
2751 )
2752}
2753
2754fn server_attempt_result_reason(result: &ServerAttemptResult) -> String {
2755 match result {
2756 ServerAttemptResult::SpawnFailed { binary, reason } => {
2757 format!("spawn_failed: {binary} ({reason})")
2758 }
2759 ServerAttemptResult::BinaryNotInstalled { binary } => {
2760 format!("binary_not_installed: {binary}")
2761 }
2762 ServerAttemptResult::NoRootMarker { looked_for } => {
2763 format!("no_root_marker (looked for: {})", looked_for.join(", "))
2764 }
2765 ServerAttemptResult::Ok { .. } => "ok".to_string(),
2766 }
2767}
2768
2769fn format_stderr_tail_for_reason(stderr_tail: &str) -> String {
2770 truncate_stderr_tail_for_reason(stderr_tail)
2771 .lines()
2772 .map(|line| format!(" {line}"))
2773 .collect::<Vec<_>>()
2774 .join("\n")
2775}
2776
2777fn truncate_stderr_tail_for_reason(stderr_tail: &str) -> String {
2778 if stderr_tail.len() <= STDERR_REASON_BYTES {
2779 return stderr_tail.to_string();
2780 }
2781
2782 let ellipsis = "...";
2783 let target_len = STDERR_REASON_BYTES.saturating_sub(ellipsis.len());
2784 let mut start = stderr_tail.len() - target_len;
2785 while start < stderr_tail.len() && !stderr_tail.is_char_boundary(start) {
2786 start += 1;
2787 }
2788 format!("{ellipsis}{}", &stderr_tail[start..])
2789}
2790
2791fn format_initialize_failure_reason(binary: &str, stderr_tail: &str, err: &LspError) -> String {
2792 let mut reason = format!("server crashed during initialize: {err}");
2793 if !stderr_tail.is_empty() {
2794 reason.push_str("; stderr (last 64 lines):\n");
2795 reason.push_str(&format_stderr_tail_for_reason(stderr_tail));
2796 reason.push_str("\n\n");
2797 reason.push_str(&failure_hint(binary, stderr_tail));
2798 }
2799 reason
2800}
2801
2802fn format_post_initialize_exit_reason(
2803 binary: &str,
2804 status: std::process::ExitStatus,
2805 stderr_tail: &str,
2806 err: &LspError,
2807) -> String {
2808 let code = status
2809 .code()
2810 .map(|c| c.to_string())
2811 .unwrap_or_else(|| "signal/unknown".to_string());
2812 let mut reason = format!("server exited after initialize (code {code}): {err}");
2813 if !stderr_tail.is_empty() {
2814 reason.push_str("; stderr (last 64 lines):\n");
2815 reason.push_str(&format_stderr_tail_for_reason(stderr_tail));
2816 reason.push_str("\n\n");
2817 reason.push_str(&failure_hint(binary, stderr_tail));
2818 }
2819 reason
2820}
2821
2822fn failure_hint(binary: &str, stderr_tail: &str) -> String {
2823 if stderr_tail.contains("MODULE_NOT_FOUND") || stderr_tail.contains("Cannot find module") {
2824 let package_manager = infer_package_manager(stderr_tail);
2825 format!(
2826 "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."
2827 )
2828 } else if let Some(component) = rustup_missing_component(stderr_tail) {
2829 format!("'{component}' is a rustup proxy but the component is not installed. Install it: rustup component add {component}")
2834 } else {
2835 format!("Hint: see stderr above for '{binary}' failure details.")
2836 }
2837}
2838
2839fn rustup_missing_component(stderr_tail: &str) -> Option<String> {
2845 let marker = "Unknown binary '";
2846 let start = stderr_tail.find(marker)? + marker.len();
2847 let rest = &stderr_tail[start..];
2848 let end = rest.find('\'')?;
2849 let name = &rest[..end];
2850 if name.is_empty() || !stderr_tail.contains("toolchain") {
2853 return None;
2854 }
2855 Some(name.to_string())
2856}
2857
2858fn infer_package_manager(stderr_tail: &str) -> &'static str {
2859 let lower = stderr_tail.to_ascii_lowercase();
2860 if lower.contains(".pnpm/") || lower.contains(".pnpm\\") || lower.contains("/pnpm/") {
2861 "pnpm"
2862 } else if lower.contains(".yarn/")
2863 || lower.contains(".yarn\\")
2864 || lower.contains("/yarn/")
2865 || lower.contains("yarn")
2866 {
2867 "yarn"
2868 } else {
2869 "npm"
2870 }
2871}
2872
2873fn canonicalize_for_lsp(file_path: &Path) -> Result<PathBuf, LspError> {
2874 std::fs::canonicalize(file_path)
2881 .map(|canonical| crate::inspect::job::normalize_path(&canonical))
2882 .map_err(LspError::from)
2883}
2884
2885fn resolve_for_lsp_uri(file_path: &Path) -> PathBuf {
2886 if let Ok(path) = std::fs::canonicalize(file_path) {
2889 return crate::inspect::job::normalize_path(&path);
2890 }
2891
2892 let mut existing = file_path.to_path_buf();
2893 let mut missing = Vec::new();
2894 while !existing.exists() {
2895 let Some(name) = existing.file_name() else {
2896 break;
2897 };
2898 missing.push(name.to_owned());
2899 let Some(parent) = existing.parent() else {
2900 break;
2901 };
2902 existing = parent.to_path_buf();
2903 }
2904
2905 let mut resolved = std::fs::canonicalize(&existing)
2906 .map(|canonical| crate::inspect::job::normalize_path(&canonical))
2907 .unwrap_or(existing);
2908 for segment in missing.into_iter().rev() {
2909 resolved.push(segment);
2910 }
2911 resolved
2912}
2913
2914fn language_id_for_extension(ext: &str) -> &'static str {
2915 match ext {
2916 "ts" => "typescript",
2917 "tsx" => "typescriptreact",
2918 "js" | "mjs" | "cjs" => "javascript",
2919 "jsx" => "javascriptreact",
2920 "py" | "pyi" => "python",
2921 "rs" => "rust",
2922 "go" => "go",
2923 "html" | "htm" => "html",
2924 _ => "plaintext",
2925 }
2926}
2927
2928fn normalize_lookup_path(path: &Path) -> PathBuf {
2929 std::fs::canonicalize(path)
2933 .map(|canonical| crate::inspect::job::normalize_path(&canonical))
2934 .unwrap_or_else(|_| path.to_path_buf())
2935}
2936
2937fn diagnostic_path_candidates(file: &Path) -> Vec<PathBuf> {
2938 let mut candidates = Vec::with_capacity(4);
2939 let mut add = |candidate: PathBuf| {
2940 if !candidates.iter().any(|existing| existing == &candidate) {
2941 candidates.push(candidate);
2942 }
2943 };
2944
2945 add(file.to_path_buf());
2949 add(normalize_lookup_path(file));
2950
2951 if let (Some(parent), Some(name)) = (file.parent(), file.file_name()) {
2955 if let Ok(canonical_parent) = std::fs::canonicalize(parent) {
2956 let reconstructed = canonical_parent.join(name);
2957 add(reconstructed.clone());
2958 add(crate::inspect::job::normalize_path(&reconstructed));
2959 }
2960 }
2961
2962 candidates
2963}
2964
2965fn classify_spawn_error(binary: &str, err: &LspError) -> ServerAttemptResult {
2972 match err {
2973 LspError::NotFound(_) => ServerAttemptResult::BinaryNotInstalled {
2978 binary: binary.to_string(),
2979 },
2980 other => ServerAttemptResult::SpawnFailed {
2981 binary: binary.to_string(),
2982 reason: other.to_string(),
2983 },
2984 }
2985}
2986
2987fn env_binary_override(kind: &ServerKind) -> Option<PathBuf> {
2988 let id = kind.id_str();
2989 let suffix: String = id
2990 .chars()
2991 .map(|ch| {
2992 if ch.is_ascii_alphanumeric() {
2993 ch.to_ascii_uppercase()
2994 } else {
2995 '_'
2996 }
2997 })
2998 .collect();
2999 let key = format!("AFT_LSP_{suffix}_BINARY");
3000 std::env::var_os(key).map(PathBuf::from)
3001}
3002
3003#[cfg(all(test, windows))]
3004mod windows_server_key_tests {
3005 use std::fs;
3006 use std::os::windows::ffi::OsStrExt;
3007
3008 use super::{canonicalize_for_lsp, server_key_for_definition};
3009 use crate::config::{Config, UserServerDef};
3010 use crate::lsp::registry::servers_for_file;
3011
3012 #[test]
3013 fn normalized_and_verbatim_inputs_produce_identical_server_key_material() {
3014 let temp_dir = tempfile::tempdir().expect("tempdir");
3015 let root = temp_dir.path().join("workspace");
3016 let source = root.join("src").join("main.customts");
3017 fs::create_dir_all(source.parent().expect("source parent")).expect("create source dir");
3018 fs::write(root.join("custom-root.json"), "{}\n").expect("write root marker");
3019 fs::write(&source, "export const value = 1;\n").expect("write source");
3020
3021 let config = Config {
3022 project_root: Some(root),
3023 lsp_servers: vec![UserServerDef {
3024 id: "custom-ts".to_string(),
3025 extensions: vec!["customts".to_string()],
3026 binary: "custom-ts-lsp".to_string(),
3027 args: Vec::new(),
3028 root_markers: vec!["custom-root.json".to_string()],
3029 env: Default::default(),
3030 initialization_options: None,
3031 disabled: false,
3032 }],
3033 ..Config::default()
3034 };
3035
3036 let normalized_input = canonicalize_for_lsp(&source).expect("normalized source path");
3037 let bare_canonical_input = fs::canonicalize(&source).expect("canonical source path");
3038 let key_for = |path: &std::path::Path| {
3039 let def = servers_for_file(path, &config)
3040 .into_iter()
3041 .find(|def| def.kind.id_str() == "custom-ts")
3042 .expect("custom server definition");
3043 server_key_for_definition(&def, path, &config).expect("custom server root")
3044 };
3045
3046 let key_material = |key: &crate::lsp::roots::ServerKey| {
3047 let root_bytes = key
3048 .root
3049 .as_os_str()
3050 .encode_wide()
3051 .flat_map(u16::to_le_bytes)
3052 .collect::<Vec<_>>();
3053 (key.kind.id_str().to_string(), root_bytes)
3054 };
3055 let ensure_key = key_for(&normalized_input);
3056 let running_lookup_key = key_for(&bare_canonical_input);
3057
3058 assert_eq!(key_material(&ensure_key), key_material(&running_lookup_key));
3059 }
3060}
3061
3062#[cfg(test)]
3063mod failure_hint_tests {
3064 use super::{failure_hint, rustup_missing_component};
3065
3066 #[test]
3067 fn detects_rustup_proxy_without_component() {
3068 let stderr = "error: Unknown binary 'rust-analyzer' in official toolchain 'stable-aarch64-apple-darwin'.";
3070 assert_eq!(
3071 rustup_missing_component(stderr).as_deref(),
3072 Some("rust-analyzer")
3073 );
3074 let hint = failure_hint("rust-analyzer", stderr);
3075 assert!(
3076 hint.contains("rustup component add rust-analyzer"),
3077 "expected actionable rustup hint, got: {hint}"
3078 );
3079 }
3080
3081 #[test]
3082 fn ignores_unknown_binary_without_toolchain_phrasing() {
3083 let stderr = "fatal: Unknown binary 'foo' was requested by the linker.";
3086 assert_eq!(rustup_missing_component(stderr), None);
3087 assert!(failure_hint("foo", stderr).starts_with("Hint: see stderr"));
3088 }
3089
3090 #[test]
3091 fn npm_module_not_found_still_wins() {
3092 let stderr = "Error: Cannot find module '/x/typescript-language-server/lib/cli.mjs'";
3094 let hint = failure_hint("typescript-language-server", stderr);
3095 assert!(hint.contains("install -g"), "got: {hint}");
3096 }
3097}
3098
3099#[cfg(test)]
3100mod diagnostic_capacity_tests {
3101 use std::fs;
3102
3103 use super::LspManager;
3104 use crate::config::Config;
3105
3106 #[test]
3111 fn set_diagnostic_capacity_propagates_to_store() {
3112 let mut manager = LspManager::new();
3113 manager.set_diagnostic_capacity(7);
3114 assert_eq!(manager.diagnostics_store_for_test().capacity_for_test(), 7);
3115 manager.set_diagnostic_capacity(0); assert_eq!(manager.diagnostics_store_for_test().capacity_for_test(), 0);
3117 }
3118
3119 #[test]
3122 fn clear_failed_spawns_empties_the_cache() {
3123 let mut manager = LspManager::new();
3124 assert_eq!(manager.clear_failed_spawns(), 0);
3125 manager.insert_failed_spawn_for_test();
3126 assert_eq!(manager.clear_failed_spawns(), 1);
3127 assert_eq!(manager.clear_failed_spawns(), 0);
3128 }
3129
3130 #[test]
3131 fn post_write_notification_does_not_start_a_cold_server() {
3132 let dir = tempfile::tempdir().unwrap();
3133 let file = dir.path().join("main.ts");
3134 fs::write(dir.path().join("package.json"), "{}").unwrap();
3135 fs::write(&file, "export const value = 1;\n").unwrap();
3136
3137 let mut manager = LspManager::new();
3138 manager
3139 .notify_file_changed_if_running(&file, "export const value = 1;\n", &Config::default())
3140 .unwrap();
3141 assert!(manager.clients.is_empty());
3142 }
3143}
3144
3145#[cfg(test)]
3146mod post_edit_waiter_tests {
3147 use std::collections::HashMap;
3148 use std::path::PathBuf;
3149 use std::time::{Duration, Instant};
3150
3151 use super::LspManager;
3152 use crate::lsp::client::LspEvent;
3153 use crate::lsp::registry::ServerKind;
3154
3155 #[test]
3156 fn draining_an_event_wakes_registered_post_edit_waiter() {
3157 let mut manager = LspManager::new();
3158 let mut wait = manager.start_post_edit_diagnostics_wait(
3159 PathBuf::from("/workspace/src/main.rs").as_path(),
3160 &[],
3161 &HashMap::new(),
3162 Duration::from_secs(2),
3163 );
3164 manager.enqueue_event_for_test(LspEvent::Notification {
3165 server_kind: ServerKind::Rust,
3166 root: PathBuf::from("/workspace"),
3167 method: "custom/drainedElsewhere".to_string(),
3168 params: None,
3169 });
3170
3171 assert_eq!(manager.drain_events().events.len(), 1);
3172 let started = Instant::now();
3173 assert!(wait.next_event().is_none());
3174 assert!(
3175 started.elapsed() < Duration::from_millis(250),
3176 "a competing drain did not wake the parked post-edit waiter"
3177 );
3178 let _ = manager.poll_post_edit_diagnostics_wait(&mut wait, None);
3179 let _ = manager.finish_post_edit_diagnostics_wait(wait);
3180 }
3181}
3182
3183#[cfg(test)]
3184mod clear_diagnostics_tests {
3185 use std::path::PathBuf;
3186
3187 use super::LspManager;
3188 use crate::lsp::client::LspEvent;
3189 use crate::lsp::diagnostics::{DiagnosticSeverity, StoredDiagnostic};
3190 use crate::lsp::position::uri_for_path;
3191 use crate::lsp::registry::ServerKind;
3192 use crate::lsp::roots::ServerKey;
3193
3194 fn err_diag(file: &PathBuf) -> StoredDiagnostic {
3195 StoredDiagnostic {
3196 file: file.clone(),
3197 line: 1,
3198 column: 1,
3199 end_line: 1,
3200 end_column: 2,
3201 severity: DiagnosticSeverity::Error,
3202 message: "boom".into(),
3203 code: None,
3204 source: None,
3205 }
3206 }
3207
3208 #[test]
3213 fn clear_diagnostics_for_deleted_file_matches_canonical_key() {
3214 let dir = tempfile::tempdir().unwrap();
3215 let canonical_dir = std::fs::canonicalize(dir.path()).unwrap();
3217 let canonical_file = canonical_dir.join("gone.ts");
3218 std::fs::write(&canonical_file, "x").unwrap();
3221
3222 let mut manager = LspManager::new();
3223 let key = ServerKey {
3224 kind: ServerKind::TypeScript,
3225 root: canonical_dir.clone(),
3226 };
3227 manager.diagnostics_store_mut_for_test().publish(
3228 key,
3229 canonical_file.clone(),
3230 vec![err_diag(&canonical_file)],
3231 );
3232 assert_eq!(manager.warm_error_warning_counts(), (1, 0));
3233
3234 std::fs::remove_file(&canonical_file).unwrap();
3235
3236 let watcher_path = dir.path().join("gone.ts");
3239 let removed = manager.clear_diagnostics_for_file(&watcher_path);
3240
3241 assert!(removed, "expected the deleted file's diagnostic to clear");
3242 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
3243 }
3244
3245 #[cfg(windows)]
3246 #[test]
3247 fn clear_diagnostics_for_deleted_file_matches_normalized_publish_key() {
3248 let dir = tempfile::tempdir().unwrap();
3249 let file = dir.path().join("normalized-gone.ts");
3250 std::fs::write(&file, "x").unwrap();
3251 let normalized_file = crate::inspect::job::canonicalize_normalized(&file);
3252
3253 let mut manager = LspManager::new();
3254 let key = ServerKey {
3255 kind: ServerKind::TypeScript,
3256 root: normalized_file.parent().unwrap().to_path_buf(),
3257 };
3258 manager.diagnostics_store_mut_for_test().publish(
3259 key,
3260 normalized_file.clone(),
3261 vec![err_diag(&normalized_file)],
3262 );
3263 std::fs::remove_file(&file).unwrap();
3264
3265 assert!(manager.clear_diagnostics_for_file(&file));
3266 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
3267 }
3268
3269 #[cfg(windows)]
3270 #[test]
3271 fn stale_diagnostics_for_deleted_file_matches_normalized_publish_key() {
3272 let dir = tempfile::tempdir().unwrap();
3273 let file = dir.path().join("normalized-stale.ts");
3274 std::fs::write(&file, "x").unwrap();
3275 let normalized_file = crate::inspect::job::canonicalize_normalized(&file);
3276
3277 let mut manager = LspManager::new();
3278 let key = ServerKey {
3279 kind: ServerKind::TypeScript,
3280 root: normalized_file.parent().unwrap().to_path_buf(),
3281 };
3282 manager.diagnostics_store_mut_for_test().publish(
3283 key,
3284 normalized_file.clone(),
3285 vec![err_diag(&normalized_file)],
3286 );
3287 std::fs::remove_file(&file).unwrap();
3288
3289 let result = manager.mark_diagnostics_stale_for_file(&file);
3290 assert!(result.had_entries);
3291 assert!(result.changed);
3292 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
3293 }
3294
3295 #[test]
3296 fn clear_diagnostics_for_unknown_file_is_noop() {
3297 let mut manager = LspManager::new();
3298 assert!(!manager.clear_diagnostics_for_file(&PathBuf::from("/nope/missing.ts")));
3299 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
3300 }
3301
3302 #[test]
3303 fn drain_events_reports_publish_diagnostics_updates() {
3304 let dir = tempfile::tempdir().unwrap();
3305 let root = std::fs::canonicalize(dir.path()).unwrap();
3306 let file = root.join("main.ts");
3307 std::fs::write(&file, "const x: number = 'nope';").unwrap();
3308
3309 let mut manager = LspManager::new();
3310 let diagnostic = lsp_types::Diagnostic {
3311 range: lsp_types::Range {
3312 start: lsp_types::Position {
3313 line: 0,
3314 character: 0,
3315 },
3316 end: lsp_types::Position {
3317 line: 0,
3318 character: 1,
3319 },
3320 },
3321 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
3322 code: None,
3323 code_description: None,
3324 source: Some("test".into()),
3325 message: "boom".into(),
3326 related_information: None,
3327 tags: None,
3328 data: None,
3329 };
3330 let params = serde_json::to_value(lsp_types::PublishDiagnosticsParams {
3331 uri: uri_for_path(&file).unwrap(),
3332 diagnostics: vec![diagnostic],
3333 version: Some(1),
3334 })
3335 .unwrap();
3336 manager
3337 .event_tx
3338 .send(LspEvent::Notification {
3339 server_kind: ServerKind::TypeScript,
3340 root,
3341 method: "textDocument/publishDiagnostics".into(),
3342 params: Some(params),
3343 })
3344 .unwrap();
3345
3346 let drained = manager.drain_events();
3347
3348 assert!(drained.diagnostics_changed);
3349 assert_eq!(drained.events.len(), 1);
3350 assert_eq!(manager.warm_error_warning_counts(), (1, 0));
3351 }
3352}
3353
3354#[cfg(test)]
3355mod inspect_path_tests {
3356 use super::LspManager;
3357 use crate::config::{Config, UserServerDef};
3358 use crate::lsp::registry::ServerKind;
3359
3360 #[test]
3361 fn applicability_resolution_does_not_start_or_open_a_server() {
3362 let temp_dir = tempfile::tempdir().expect("tempdir");
3363 let root = temp_dir.path().join("project");
3364 std::fs::create_dir_all(&root).expect("project root");
3365 std::fs::write(root.join("inspect-root.json"), "{}\n").expect("root marker");
3366 std::fs::write(root.join("input.inspectlang"), "value\n").expect("source file");
3367
3368 let config = Config {
3369 project_root: Some(root.clone()),
3370 lsp_servers: vec![UserServerDef {
3371 id: "inspect-test".to_string(),
3372 extensions: vec!["inspectlang".to_string()],
3373 binary: "inspect-test-lsp".to_string(),
3374 args: Vec::new(),
3375 root_markers: vec!["inspect-root.json".to_string()],
3376 env: Default::default(),
3377 initialization_options: None,
3378 disabled: false,
3379 }],
3380 ..Config::default()
3381 };
3382 let mut manager = LspManager::new();
3383 manager.override_binary(
3384 ServerKind::Custom("inspect-test".into()),
3385 std::env::current_exe().expect("current executable"),
3386 );
3387
3388 let snapshot = manager
3389 .resolve_applicable_servers_for_root(&root, &config)
3390 .expect("resolution succeeds without spawning");
3391 assert_eq!(snapshot.server_keys.len(), 1);
3392 assert_eq!(snapshot.server_keys[0].kind.id_str(), "inspect-test");
3393 assert_eq!(manager.server_count(), 0);
3394 assert!(!manager.document_is_open_for_test(&root.join("input.inspectlang")));
3395 }
3396
3397 #[test]
3398 fn applicability_resolution_preserves_an_empty_snapshot() {
3399 let temp_dir = tempfile::tempdir().expect("tempdir");
3400 let root = temp_dir.path().join("project");
3401 std::fs::create_dir_all(&root).expect("project root");
3402 std::fs::write(root.join("notes.txt"), "plain text\n").expect("fixture file");
3403
3404 let snapshot = LspManager::new()
3405 .resolve_applicable_servers_for_root(&root, &Config::default())
3406 .expect("an empty applicability set is valid");
3407
3408 assert!(snapshot.server_keys.is_empty());
3409 assert!(snapshot.candidates.is_empty());
3410 }
3411}