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