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