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::config::Config;
15use crate::lsp::child_registry::LspChildRegistry;
16use crate::lsp::client::{LspClient, LspEvent, ServerState};
17use crate::lsp::diagnostics::{
18 from_lsp_diagnostics, DiagnosticEntry, DiagnosticsStore, StoredDiagnostic,
19};
20use crate::lsp::document::DocumentStore;
21use crate::lsp::position::{uri_for_path, uri_to_path};
22use crate::lsp::pull_params::{
23 AftDocumentDiagnosticParams, AftDocumentDiagnosticRequest, AftWorkspaceDiagnosticParams,
24 AftWorkspaceDiagnosticRequest,
25};
26use crate::lsp::registry::{resolve_lsp_binary, servers_for_file, ServerDef, ServerKind};
27use crate::lsp::roots::ServerKey;
28use crate::lsp::LspError;
29use crate::slog_error;
30
31const STDERR_REASON_BYTES: usize = 2 * 1024;
32
33fn server_key_for_definition(
34 def: &ServerDef,
35 file_path: &Path,
36 config: &Config,
37) -> Option<ServerKey> {
38 def.workspace_root_for_file_with_project_root(file_path, config.project_root.as_deref())
39 .map(|root| ServerKey {
40 kind: def.kind.clone(),
41 root,
42 })
43}
44
45#[derive(Debug, Clone)]
50pub enum ServerAttemptResult {
51 Ok { server_key: ServerKey },
53 NoRootMarker { looked_for: Vec<String> },
56 BinaryNotInstalled { binary: String },
59 SpawnFailed { binary: String, reason: String },
61}
62
63#[derive(Debug, Clone)]
65pub struct ServerAttempt {
66 pub server_id: String,
68 pub server_name: String,
70 pub result: ServerAttemptResult,
71}
72
73#[derive(Debug, Clone, Default)]
79pub struct EnsureServerOutcomes {
80 pub successful: Vec<ServerKey>,
82 pub attempts: Vec<ServerAttempt>,
85}
86
87impl EnsureServerOutcomes {
88 pub fn no_server_registered(&self) -> bool {
90 self.attempts.is_empty()
91 }
92
93 pub fn only_inapplicable_root_markers(&self) -> bool {
101 self.successful.is_empty()
102 && !self.attempts.is_empty()
103 && self
104 .attempts
105 .iter()
106 .all(|attempt| matches!(attempt.result, ServerAttemptResult::NoRootMarker { .. }))
107 }
108}
109
110#[derive(Debug, Clone, Default)]
120pub struct PostEditWaitOutcome {
121 pub diagnostics: Vec<StoredDiagnostic>,
125 pub pending_servers: Vec<ServerKey>,
129 pub exited_servers: Vec<ServerKey>,
133}
134
135#[derive(Debug, Clone, Copy, Default)]
137pub struct PreEditSnapshot {
138 pub epoch: u64,
139 pub document_version_at_capture: Option<i32>,
140}
141
142#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
143pub struct StaleDiagnosticsMark {
144 pub had_entries: bool,
145 pub changed: bool,
146}
147
148pub fn post_edit_entry_is_fresh(
149 entry: &DiagnosticEntry,
150 target_version: i32,
151 pre: PreEditSnapshot,
152) -> bool {
153 if entry.stale || entry.epoch <= pre.epoch {
154 return false;
155 }
156
157 match entry.version {
158 Some(version) => version >= target_version,
159 None => false,
164 }
165}
166
167impl PostEditWaitOutcome {
168 pub fn complete(&self) -> bool {
171 self.pending_servers.is_empty() && self.exited_servers.is_empty()
172 }
173}
174
175#[derive(Debug, Clone)]
177pub enum PullFileOutcome {
178 Full { diagnostic_count: usize },
180 Unchanged,
182 PartialNotSupported,
185 PullNotSupported,
188 RequestFailed { reason: String },
190}
191
192#[derive(Debug, Clone, Default)]
194pub struct EnsureFileOpenResult {
195 pub server_keys: Vec<ServerKey>,
196 pub newly_opened: Vec<ServerKey>,
198}
199
200impl EnsureFileOpenResult {
201 pub fn is_empty(&self) -> bool {
202 self.server_keys.is_empty()
203 }
204}
205
206#[derive(Debug, Clone)]
208pub struct PullFileResult {
209 pub server_key: ServerKey,
210 pub outcome: PullFileOutcome,
211}
212
213pub(crate) struct TrackedPullFileResult {
214 pub results: Vec<PullFileResult>,
215 pub newly_opened: Vec<ServerKey>,
216}
217
218#[derive(Debug, Clone)]
220pub struct PullWorkspaceResult {
221 pub server_key: ServerKey,
222 pub files_reported: Vec<PathBuf>,
226 pub complete: bool,
228 pub cancelled: bool,
230 pub supports_workspace: bool,
234}
235
236pub struct DrainedLspEvents {
237 pub events: Vec<LspEvent>,
238 pub diagnostics_changed: bool,
239 pub has_more: bool,
240}
241
242pub(crate) struct PostEditDiagnosticsWait {
247 lookup_path: PathBuf,
248 expected_versions: Vec<(ServerKey, i32)>,
249 pre_snapshot: HashMap<ServerKey, PreEditSnapshot>,
250 event_rx: Receiver<LspEvent>,
251 wake_rx: Receiver<()>,
252 waiter_id: u64,
253 deadline: std::time::Instant,
254 fresh: HashMap<ServerKey, Vec<StoredDiagnostic>>,
255 exited: Vec<ServerKey>,
256}
257
258impl PostEditDiagnosticsWait {
259 pub(crate) fn deadline_reached(&self) -> bool {
260 std::time::Instant::now() >= self.deadline
261 }
262
263 pub(crate) fn next_event(&self) -> Option<LspEvent> {
264 let remaining = self
265 .deadline
266 .saturating_duration_since(std::time::Instant::now());
267 if remaining.is_zero() {
268 return None;
269 }
270
271 crossbeam_channel::select! {
272 recv(self.event_rx) -> event => event.ok(),
273 recv(self.wake_rx) -> _ => None,
274 default(remaining) => None,
275 }
276 }
277}
278
279impl IntoIterator for DrainedLspEvents {
280 type Item = LspEvent;
281 type IntoIter = std::vec::IntoIter<LspEvent>;
282
283 fn into_iter(self) -> Self::IntoIter {
284 self.events.into_iter()
285 }
286}
287
288pub struct LspManager {
289 clients: HashMap<ServerKey, LspClient>,
291 server_binaries: HashMap<ServerKey, String>,
295 documents: HashMap<ServerKey, DocumentStore>,
297 diagnostics: DiagnosticsStore,
299 event_tx: Sender<LspEvent>,
301 event_rx: Receiver<LspEvent>,
302 post_edit_waiters: HashMap<u64, Sender<()>>,
306 next_post_edit_waiter_id: u64,
307 binary_overrides: HashMap<ServerKind, PathBuf>,
309 extra_env: HashMap<String, String>,
313 failed_spawns: HashMap<ServerKey, ServerAttemptResult>,
328 watched_file_skip_logged: HashSet<ServerKey>,
331 #[cfg(windows)]
334 last_watched_file_notification_trace: String,
335 child_registry: LspChildRegistry,
339}
340
341impl LspManager {
342 pub fn new() -> Self {
343 let (event_tx, event_rx) = unbounded();
344 Self {
345 clients: HashMap::new(),
346 server_binaries: HashMap::new(),
347 documents: HashMap::new(),
348 diagnostics: DiagnosticsStore::new(),
349 event_tx,
350 event_rx,
351 post_edit_waiters: HashMap::new(),
352 next_post_edit_waiter_id: 0,
353 binary_overrides: HashMap::new(),
354 extra_env: HashMap::new(),
355 failed_spawns: HashMap::new(),
356 watched_file_skip_logged: HashSet::new(),
357 #[cfg(windows)]
358 last_watched_file_notification_trace: "no watched-file notification attempted"
359 .to_string(),
360 child_registry: LspChildRegistry::new(),
361 }
362 }
363
364 pub fn set_child_registry(&mut self, registry: LspChildRegistry) {
366 self.child_registry = registry;
367 }
368
369 pub fn set_extra_env(&mut self, key: &str, value: &str) {
373 self.extra_env.insert(key.to_string(), value.to_string());
374 }
375
376 pub fn server_count(&self) -> usize {
378 self.clients.len()
379 }
380
381 pub fn estimated_memory(&self) -> crate::memory::MemoryEstimate {
385 let mut bytes = 0u64;
386 let mut document_count = 0u64;
387 for documents in self.documents.values() {
388 let estimate = documents.estimated_memory();
389 bytes = bytes.saturating_add(estimate.estimated_bytes.unwrap_or(0));
390 document_count = document_count
391 .saturating_add(estimate.counts.get("documents").copied().unwrap_or(0));
392 }
393 let diagnostics = self.diagnostics.estimated_memory();
394 bytes = bytes.saturating_add(diagnostics.estimated_bytes.unwrap_or(0));
395 crate::memory::MemoryEstimate::estimated(bytes)
396 .count("servers", self.clients.len())
397 .count("document_stores", self.documents.len())
398 .count_u64("documents", document_count)
399 .count_u64(
400 "diagnostic_entries",
401 diagnostics
402 .counts
403 .get("diagnostic_entries")
404 .copied()
405 .unwrap_or(0),
406 )
407 .count_u64(
408 "diagnostics",
409 diagnostics.counts.get("diagnostics").copied().unwrap_or(0),
410 )
411 }
412
413 pub fn set_diagnostic_capacity(&mut self, capacity: usize) {
417 self.diagnostics.set_capacity(capacity);
418 }
419
420 pub fn override_binary(&mut self, kind: ServerKind, binary_path: PathBuf) {
422 self.binary_overrides.insert(kind, binary_path);
423 }
424
425 pub fn ensure_server_for_file(&mut self, file_path: &Path, config: &Config) -> Vec<ServerKey> {
432 self.ensure_server_for_file_detailed(file_path, config)
433 .successful
434 }
435
436 fn running_server_keys_for_file(&self, file_path: &Path, config: &Config) -> Vec<ServerKey> {
437 servers_for_file(file_path, config)
438 .into_iter()
439 .filter_map(|def| server_key_for_definition(&def, file_path, config))
440 .filter(|key| self.clients.contains_key(key))
441 .collect()
442 }
443
444 pub fn ensure_server_for_file_detailed(
452 &mut self,
453 file_path: &Path,
454 config: &Config,
455 ) -> EnsureServerOutcomes {
456 let defs = servers_for_file(file_path, config);
457 let mut outcomes = EnsureServerOutcomes::default();
458
459 for def in defs {
460 let server_id = def.kind.id_str().to_string();
461 let server_name = def.name.to_string();
462
463 let Some(key) = server_key_for_definition(&def, file_path, config) else {
464 outcomes.attempts.push(ServerAttempt {
465 server_id,
466 server_name,
467 result: ServerAttemptResult::NoRootMarker {
468 looked_for: def.root_markers.iter().map(|s| s.to_string()).collect(),
469 },
470 });
471 continue;
472 };
473
474 if !self.clients.contains_key(&key) {
475 if let Some(cached) = self.failed_spawns.get(&key) {
482 outcomes.attempts.push(ServerAttempt {
483 server_id,
484 server_name,
485 result: cached.clone(),
486 });
487 continue;
488 }
489
490 match self.spawn_server(&def, &key.root, config) {
491 Ok(client) => {
492 self.clients.insert(key.clone(), client);
493 self.server_binaries.insert(key.clone(), def.binary.clone());
494 self.documents.entry(key.clone()).or_default();
495 }
496 Err(err) => {
497 slog_error!("failed to spawn {}: {}", def.name, err);
498 let result = classify_spawn_error(&def.binary, &err);
499 self.failed_spawns.insert(key.clone(), result.clone());
503 outcomes.attempts.push(ServerAttempt {
504 server_id,
505 server_name,
506 result,
507 });
508 continue;
509 }
510 }
511 }
512
513 outcomes.attempts.push(ServerAttempt {
514 server_id,
515 server_name,
516 result: ServerAttemptResult::Ok {
517 server_key: key.clone(),
518 },
519 });
520 outcomes.successful.push(key);
521 }
522
523 outcomes
524 }
525
526 pub fn ensure_server_for_file_default(&mut self, file_path: &Path) -> Vec<ServerKey> {
529 self.ensure_server_for_file(file_path, &Config::default())
530 }
531 pub fn ensure_file_open(
536 &mut self,
537 file_path: &Path,
538 config: &Config,
539 ) -> Result<EnsureFileOpenResult, LspError> {
540 let canonical_path = canonicalize_for_lsp(file_path)?;
541 let server_keys = self.ensure_server_for_file(&canonical_path, config);
542 if server_keys.is_empty() {
543 return Ok(EnsureFileOpenResult::default());
544 }
545
546 let uri = uri_for_path(&canonical_path)?;
547 let language_id = language_id_for_extension(
548 canonical_path
549 .extension()
550 .and_then(|ext| ext.to_str())
551 .unwrap_or_default(),
552 )
553 .to_string();
554 let needs_content = server_keys.iter().any(|key| {
555 !self
556 .documents
557 .get(key)
558 .is_some_and(|store| store.is_open(&canonical_path))
559 });
560 let initial_content = needs_content
561 .then(|| std::fs::read_to_string(&canonical_path).map_err(LspError::Io))
562 .transpose()?;
563 let mut newly_opened = Vec::new();
564
565 for key in &server_keys {
566 let already_open = self
567 .documents
568 .get(key)
569 .is_some_and(|store| store.is_open(&canonical_path));
570
571 if !already_open {
572 let content = initial_content
573 .as_ref()
574 .expect("content is loaded when any server needs didOpen");
575 let send_result = if let Some(client) = self.clients.get_mut(key) {
576 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
577 text_document: TextDocumentItem::new(
578 uri.clone(),
579 language_id.clone(),
580 0,
581 content.clone(),
582 ),
583 })
584 } else {
585 Ok(())
586 };
587 if let Err(err) = send_result {
588 let _ = self.close_file_for_servers(&canonical_path, &newly_opened);
589 return Err(err);
590 }
591 self.documents
592 .entry(key.clone())
593 .or_default()
594 .open(canonical_path.clone());
595 newly_opened.push(key.clone());
596 continue;
597 }
598
599 let drifted = self
609 .documents
610 .get(key)
611 .is_some_and(|store| store.is_stale_on_disk(&canonical_path));
612 if drifted {
613 let content = match std::fs::read_to_string(&canonical_path) {
614 Ok(content) => content,
615 Err(err) => {
616 let _ = self.close_file_for_servers(&canonical_path, &newly_opened);
617 return Err(LspError::Io(err));
618 }
619 };
620 let next_version = self
621 .documents
622 .get(key)
623 .and_then(|store| store.version(&canonical_path))
624 .map(|v| v + 1)
625 .unwrap_or(1);
626 let send_result = if let Some(client) = self.clients.get_mut(key) {
627 client.send_notification::<DidChangeTextDocument>(DidChangeTextDocumentParams {
628 text_document: VersionedTextDocumentIdentifier::new(
629 uri.clone(),
630 next_version,
631 ),
632 content_changes: vec![TextDocumentContentChangeEvent {
633 range: None,
634 range_length: None,
635 text: content,
636 }],
637 })
638 } else {
639 Ok(())
640 };
641 if let Err(err) = send_result {
642 let _ = self.close_file_for_servers(&canonical_path, &newly_opened);
643 return Err(err);
644 }
645 if let Some(store) = self.documents.get_mut(key) {
646 store.bump_version(&canonical_path);
647 }
648 }
649 }
650
651 Ok(EnsureFileOpenResult {
652 server_keys,
653 newly_opened,
654 })
655 }
656
657 pub fn ensure_file_open_default(
658 &mut self,
659 file_path: &Path,
660 ) -> Result<EnsureFileOpenResult, LspError> {
661 self.ensure_file_open(file_path, &Config::default())
662 }
663
664 pub fn notify_file_changed(
670 &mut self,
671 file_path: &Path,
672 content: &str,
673 config: &Config,
674 ) -> Result<(), LspError> {
675 self.notify_file_changed_versioned(file_path, content, config)
676 .map(|_| ())
677 }
678
679 pub fn notify_file_changed_versioned(
690 &mut self,
691 file_path: &Path,
692 content: &str,
693 config: &Config,
694 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
695 let canonical_path = canonicalize_for_lsp(file_path)?;
696 let server_keys = self.ensure_server_for_file(&canonical_path, config);
697 self.notify_file_changed_for_server_keys(canonical_path, content, server_keys)
698 }
699
700 pub fn notify_file_changed_if_running(
706 &mut self,
707 file_path: &Path,
708 content: &str,
709 config: &Config,
710 ) -> Result<(), LspError> {
711 let canonical_path = canonicalize_for_lsp(file_path)?;
712 let server_keys = self.running_server_keys_for_file(&canonical_path, config);
713 self.notify_file_changed_for_server_keys(canonical_path, content, server_keys)
714 .map(|_| ())
715 }
716
717 fn notify_file_changed_for_server_keys(
718 &mut self,
719 canonical_path: PathBuf,
720 content: &str,
721 server_keys: Vec<ServerKey>,
722 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
723 if server_keys.is_empty() {
724 return Ok(Vec::new());
725 }
726
727 let uri = uri_for_path(&canonical_path)?;
728 let language_id = language_id_for_extension(
729 canonical_path
730 .extension()
731 .and_then(|ext| ext.to_str())
732 .unwrap_or_default(),
733 )
734 .to_string();
735
736 let mut versions: Vec<(ServerKey, i32)> = Vec::with_capacity(server_keys.len());
737
738 for key in server_keys {
739 let current_version = self
740 .documents
741 .get(&key)
742 .and_then(|store| store.version(&canonical_path));
743
744 if let Some(version) = current_version {
745 let next_version = version + 1;
746 if let Some(client) = self.clients.get_mut(&key) {
747 client.send_notification::<DidChangeTextDocument>(
748 DidChangeTextDocumentParams {
749 text_document: VersionedTextDocumentIdentifier::new(
750 uri.clone(),
751 next_version,
752 ),
753 content_changes: vec![TextDocumentContentChangeEvent {
754 range: None,
755 range_length: None,
756 text: content.to_string(),
757 }],
758 },
759 )?;
760 }
761 if let Some(store) = self.documents.get_mut(&key) {
762 store.bump_version(&canonical_path);
763 }
764 versions.push((key, next_version));
765 continue;
766 }
767
768 if let Some(client) = self.clients.get_mut(&key) {
769 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
770 text_document: TextDocumentItem::new(
771 uri.clone(),
772 language_id.clone(),
773 0,
774 content.to_string(),
775 ),
776 })?;
777 }
778 self.documents
779 .entry(key.clone())
780 .or_default()
781 .open(canonical_path.clone());
782 versions.push((key, 0));
785 }
786
787 Ok(versions)
788 }
789
790 pub fn notify_file_changed_default(
791 &mut self,
792 file_path: &Path,
793 content: &str,
794 ) -> Result<(), LspError> {
795 self.notify_file_changed(file_path, content, &Config::default())
796 }
797
798 pub fn notify_files_watched_changed(
804 &mut self,
805 paths: &[(PathBuf, FileChangeType)],
806 _config: &Config,
807 ) -> Result<(), LspError> {
808 #[cfg(windows)]
809 let mut trace = vec![format!(
810 "input_paths={paths:?}; active_keys={:?}",
811 self.clients.keys().collect::<Vec<_>>()
812 )];
813
814 if paths.is_empty() {
815 #[cfg(windows)]
816 {
817 trace.push("outcome=no-input-paths".to_string());
818 self.last_watched_file_notification_trace = trace.join("\n");
819 }
820 return Ok(());
821 }
822
823 let mut canonical_events = Vec::with_capacity(paths.len());
824 for (path, typ) in paths {
825 let canonical_path = resolve_for_lsp_uri(path);
826 canonical_events.push((canonical_path, *typ));
827 }
828 #[cfg(windows)]
829 trace.push(format!("resolved_events={canonical_events:?}"));
830
831 let keys: Vec<ServerKey> = self.clients.keys().cloned().collect();
832 #[cfg(windows)]
833 if keys.is_empty() {
834 trace.push("outcome=no-active-client".to_string());
835 }
836 for key in keys {
837 let mut changes = Vec::new();
838 for (path, typ) in &canonical_events {
839 if !path.starts_with(&key.root) {
840 continue;
841 }
842 changes.push(FileEvent::new(uri_for_path(path)?, *typ));
843 }
844
845 if changes.is_empty() {
846 #[cfg(windows)]
847 trace.push(format!("key={key:?}; outcome=outside-root"));
848 continue;
849 }
850
851 if let Some(client) = self.clients.get_mut(&key) {
852 let supports_static_watched_files = client.supports_watched_files();
858 let has_dynamic_registration = client.has_watched_file_registration();
859 if !(supports_static_watched_files || has_dynamic_registration) {
860 #[cfg(windows)]
861 trace.push(format!(
862 "key={key:?}; changes={changes:?}; outcome=unsupported; static={supports_static_watched_files}; dynamic={has_dynamic_registration}"
863 ));
864 if self.watched_file_skip_logged.insert(key.clone()) {
865 log::debug!(
866 "skipping didChangeWatchedFiles for {:?} (not supported or registered)",
867 key
868 );
869 }
870 continue;
871 }
872 #[cfg(windows)]
873 trace.push(format!("key={key:?}; changes={changes:?}; action=send"));
874 let send_result = client.send_notification::<DidChangeWatchedFiles>(
875 DidChangeWatchedFilesParams { changes },
876 );
877 #[cfg(windows)]
878 trace.push(format!(
879 "key={key:?}; outcome={}",
880 if send_result.is_ok() {
881 "sent"
882 } else {
883 "send-error"
884 }
885 ));
886 if let Err(error) = send_result {
887 #[cfg(windows)]
888 {
889 self.last_watched_file_notification_trace = trace.join("\n");
890 }
891 return Err(error);
892 }
893 }
894 }
895
896 #[cfg(windows)]
897 {
898 self.last_watched_file_notification_trace = trace.join("\n");
899 }
900 Ok(())
901 }
902
903 pub fn notify_file_closed(&mut self, file_path: &Path) -> Result<(), LspError> {
905 let canonical_path = canonicalize_for_lsp(file_path)?;
906 let keys = self
907 .documents
908 .iter()
909 .filter(|(_, store)| store.is_open(&canonical_path))
910 .map(|(key, _)| key.clone())
911 .collect::<Vec<_>>();
912 self.close_file_for_servers(&canonical_path, &keys)
913 }
914
915 pub(crate) fn close_file_for_servers(
920 &mut self,
921 file_path: &Path,
922 server_keys: &[ServerKey],
923 ) -> Result<(), LspError> {
924 let canonical_path = canonicalize_for_lsp(file_path)?;
925 let uri = uri_for_path(&canonical_path)?;
926 let mut first_error = None;
927
928 for key in server_keys {
929 let was_open = self
930 .documents
931 .get(key)
932 .is_some_and(|store| store.is_open(&canonical_path));
933 if !was_open {
934 continue;
935 }
936
937 if let Some(client) = self.clients.get_mut(key) {
938 if let Err(err) =
939 client.send_notification::<DidCloseTextDocument>(DidCloseTextDocumentParams {
940 text_document: TextDocumentIdentifier::new(uri.clone()),
941 })
942 {
943 if first_error.is_none() {
944 first_error = Some(err);
945 }
946 }
947 }
948
949 if let Some(store) = self.documents.get_mut(key) {
950 store.close(&canonical_path);
951 }
952 self.diagnostics.clear_for_server_file(key, &canonical_path);
953 }
954
955 match first_error {
956 Some(err) => Err(err),
957 None => Ok(()),
958 }
959 }
960
961 pub fn client_for_file(&self, file_path: &Path, config: &Config) -> Option<&LspClient> {
963 let key = self.server_key_for_file(file_path, config)?;
964 self.clients.get(&key)
965 }
966
967 pub fn client_for_file_default(&self, file_path: &Path) -> Option<&LspClient> {
968 self.client_for_file(file_path, &Config::default())
969 }
970
971 pub fn client_for_file_mut(
973 &mut self,
974 file_path: &Path,
975 config: &Config,
976 ) -> Option<&mut LspClient> {
977 let key = self.server_key_for_file(file_path, config)?;
978 self.clients.get_mut(&key)
979 }
980
981 pub fn client_for_file_mut_default(&mut self, file_path: &Path) -> Option<&mut LspClient> {
982 self.client_for_file_mut(file_path, &Config::default())
983 }
984
985 pub fn active_client_count(&self) -> usize {
987 self.clients.len()
988 }
989
990 pub fn drain_events(&mut self) -> DrainedLspEvents {
992 self.drain_events_bounded(usize::MAX)
993 }
994
995 pub fn has_pending_events(&self) -> bool {
998 !self.event_rx.is_empty()
999 }
1000
1001 pub fn drain_events_bounded(&mut self, max_events: usize) -> DrainedLspEvents {
1002 let mut events = Vec::new();
1003 let mut diagnostics_changed = false;
1004 while events.len() < max_events {
1005 let Ok(event) = self.event_rx.try_recv() else {
1006 break;
1007 };
1008 if self.handle_event(&event).is_some() {
1009 diagnostics_changed = true;
1010 }
1011 events.push(event);
1012 }
1013 let has_more = events.len() >= max_events && !self.event_rx.is_empty();
1014 DrainedLspEvents {
1015 events,
1016 diagnostics_changed,
1017 has_more,
1018 }
1019 }
1020
1021 pub fn wait_for_diagnostics(
1023 &mut self,
1024 file_path: &Path,
1025 config: &Config,
1026 timeout: std::time::Duration,
1027 ) -> Vec<StoredDiagnostic> {
1028 let deadline = std::time::Instant::now() + timeout;
1029 self.wait_for_file_diagnostics(file_path, config, deadline)
1030 }
1031
1032 pub fn wait_for_diagnostics_default(
1033 &mut self,
1034 file_path: &Path,
1035 timeout: std::time::Duration,
1036 ) -> Vec<StoredDiagnostic> {
1037 self.wait_for_diagnostics(file_path, &Config::default(), timeout)
1038 }
1039
1040 #[doc(hidden)]
1045 pub fn diagnostics_store_for_test(&self) -> &DiagnosticsStore {
1046 &self.diagnostics
1047 }
1048
1049 #[doc(hidden)]
1050 pub fn diagnostics_store_mut_for_test(&mut self) -> &mut DiagnosticsStore {
1051 &mut self.diagnostics
1052 }
1053
1054 #[doc(hidden)]
1055 pub fn post_edit_outcome_for_entry_for_test(
1056 key: ServerKey,
1057 entry: &DiagnosticEntry,
1058 target_version: i32,
1059 pre: PreEditSnapshot,
1060 ) -> PostEditWaitOutcome {
1061 Self::post_edit_outcome_for_entry(key, entry, target_version, pre)
1062 }
1063
1064 fn post_edit_outcome_for_entry(
1065 key: ServerKey,
1066 entry: &DiagnosticEntry,
1067 target_version: i32,
1068 pre: PreEditSnapshot,
1069 ) -> PostEditWaitOutcome {
1070 let mut fresh = HashMap::new();
1071 if let Some(diagnostics) =
1072 Self::authoritative_post_edit_diagnostics(entry, target_version, pre)
1073 {
1074 fresh.insert(key.clone(), diagnostics);
1075 }
1076 Self::post_edit_outcome(vec![key], fresh, Vec::new())
1077 }
1078
1079 fn authoritative_post_edit_diagnostics(
1080 entry: &DiagnosticEntry,
1081 target_version: i32,
1082 pre: PreEditSnapshot,
1083 ) -> Option<Vec<StoredDiagnostic>> {
1084 (!entry.provisional && post_edit_entry_is_fresh(entry, target_version, pre))
1085 .then(|| entry.diagnostics.clone())
1086 }
1087
1088 #[doc(hidden)]
1089 pub fn enqueue_event_for_test(&self, event: LspEvent) {
1090 self.event_tx
1091 .send(event)
1092 .expect("LSP event receiver should remain connected");
1093 }
1094
1095 #[doc(hidden)]
1096 pub fn pending_event_count_for_test(&self) -> usize {
1097 self.event_rx.len()
1098 }
1099
1100 #[doc(hidden)]
1101 pub fn document_is_open_for_test(&self, file_path: &Path) -> bool {
1102 canonicalize_for_lsp(file_path).is_ok_and(|canonical_path| {
1103 self.documents
1104 .values()
1105 .any(|store| store.is_open(&canonical_path))
1106 })
1107 }
1108
1109 pub fn warm_error_warning_counts(&self) -> (usize, usize) {
1113 self.diagnostics.error_warning_counts()
1114 }
1115
1116 pub fn warm_error_warning_counts_with_provisional(&self) -> ((usize, usize), bool) {
1117 self.diagnostics.error_warning_counts_with_provisional()
1118 }
1119
1120 pub fn diagnostics_generation(&self) -> u64 {
1121 self.diagnostics.generation()
1122 }
1123
1124 pub fn filtered_error_warning_counts(
1129 &self,
1130 keep: impl FnMut(&std::path::Path) -> bool,
1131 ) -> (usize, usize) {
1132 self.diagnostics.filtered_error_warning_counts(keep)
1133 }
1134
1135 pub fn filtered_error_warning_counts_with_provisional(
1139 &self,
1140 keep: impl FnMut(&std::path::Path) -> bool,
1141 ) -> ((usize, usize), bool) {
1142 self.diagnostics
1143 .filtered_error_warning_counts_with_provisional(keep)
1144 }
1145
1146 pub fn provisional_server_keys(&self) -> Vec<ServerKey> {
1150 self.clients
1151 .iter()
1152 .filter(|(_, client)| client.diagnostics_are_provisional())
1153 .map(|(key, _)| key.clone())
1154 .collect()
1155 }
1156
1157 pub fn snapshot_diagnostic_epochs(&self, file_path: &Path) -> HashMap<ServerKey, u64> {
1162 let lookup_path = normalize_lookup_path(file_path);
1163 self.diagnostics
1164 .entries_for_file(&lookup_path)
1165 .into_iter()
1166 .map(|(key, entry)| (key.clone(), entry.epoch))
1167 .collect()
1168 }
1169
1170 pub fn snapshot_pre_edit_state(&self, file_path: &Path) -> HashMap<ServerKey, PreEditSnapshot> {
1173 let lookup_path = normalize_lookup_path(file_path);
1174 let mut snapshots: HashMap<ServerKey, PreEditSnapshot> = self
1175 .diagnostics
1176 .entries_for_file(&lookup_path)
1177 .into_iter()
1178 .map(|(key, entry)| {
1179 (
1180 key.clone(),
1181 PreEditSnapshot {
1182 epoch: entry.epoch,
1183 document_version_at_capture: None,
1184 },
1185 )
1186 })
1187 .collect();
1188
1189 for (key, store) in &self.documents {
1190 if let Some(version) = store.version(&lookup_path) {
1191 snapshots
1192 .entry(key.clone())
1193 .or_default()
1194 .document_version_at_capture = Some(version);
1195 }
1196 }
1197
1198 snapshots
1199 }
1200
1201 pub fn diagnostic_entry_is_fresh_for_document(
1209 &self,
1210 file_path: &Path,
1211 server_key: &ServerKey,
1212 pre: PreEditSnapshot,
1213 ) -> bool {
1214 let lookup_path = normalize_lookup_path(file_path);
1215 let Some(entry) = self
1216 .diagnostics
1217 .entries_for_file(&lookup_path)
1218 .into_iter()
1219 .find_map(|(key, entry)| if key == server_key { Some(entry) } else { None })
1220 else {
1221 return false;
1222 };
1223
1224 if entry.stale {
1225 return false;
1226 }
1227
1228 let target_version = self
1229 .documents
1230 .get(server_key)
1231 .and_then(|store| store.version(&lookup_path))
1232 .or(pre.document_version_at_capture)
1233 .unwrap_or(0);
1234
1235 matches!(entry.version, Some(version) if version >= target_version)
1236 }
1237
1238 pub(crate) fn start_post_edit_diagnostics_wait(
1242 &mut self,
1243 file_path: &Path,
1244 expected_versions: &[(ServerKey, i32)],
1245 pre_snapshot: &HashMap<ServerKey, PreEditSnapshot>,
1246 timeout: std::time::Duration,
1247 ) -> PostEditDiagnosticsWait {
1248 let lookup_path = normalize_lookup_path(file_path);
1249
1250 let _ = self.drain_events_for_file(&lookup_path);
1253
1254 let waiter_id = self.next_post_edit_waiter_id;
1255 self.next_post_edit_waiter_id = self.next_post_edit_waiter_id.wrapping_add(1);
1256 let (wake_tx, wake_rx) = bounded(1);
1257 self.post_edit_waiters.insert(waiter_id, wake_tx);
1258
1259 PostEditDiagnosticsWait {
1260 lookup_path,
1261 expected_versions: expected_versions.to_vec(),
1262 pre_snapshot: pre_snapshot.clone(),
1263 event_rx: self.event_rx.clone(),
1264 wake_rx,
1265 waiter_id,
1266 deadline: std::time::Instant::now() + timeout,
1267 fresh: HashMap::new(),
1268 exited: Vec::new(),
1269 }
1270 }
1271
1272 pub(crate) fn poll_post_edit_diagnostics_wait(
1273 &mut self,
1274 wait: &mut PostEditDiagnosticsWait,
1275 event: Option<LspEvent>,
1276 ) -> bool {
1277 if let Some(event) = event {
1278 self.handle_event(&event);
1279 }
1280
1281 for (key, target_version) in &wait.expected_versions {
1282 if wait.fresh.contains_key(key) || wait.exited.contains(key) {
1283 continue;
1284 }
1285 if !self.clients.contains_key(key) {
1286 wait.exited.push(key.clone());
1287 continue;
1288 }
1289 if let Some(entry) = self
1290 .diagnostics
1291 .entries_for_file(&wait.lookup_path)
1292 .into_iter()
1293 .find_map(|(stored_key, entry)| (stored_key == key).then_some(entry))
1294 {
1295 let pre = wait.pre_snapshot.get(key).copied().unwrap_or_default();
1296 if let Some(diagnostics) =
1297 Self::authoritative_post_edit_diagnostics(entry, *target_version, pre)
1298 {
1299 wait.fresh.insert(key.clone(), diagnostics);
1300 }
1301 }
1302 }
1303
1304 wait.fresh.len() + wait.exited.len() == wait.expected_versions.len()
1305 }
1306
1307 pub(crate) fn finish_post_edit_diagnostics_wait(
1308 &mut self,
1309 wait: PostEditDiagnosticsWait,
1310 ) -> PostEditWaitOutcome {
1311 self.post_edit_waiters.remove(&wait.waiter_id);
1312 Self::post_edit_outcome(
1313 wait.expected_versions
1314 .into_iter()
1315 .map(|(key, _)| key)
1316 .collect(),
1317 wait.fresh,
1318 wait.exited,
1319 )
1320 }
1321
1322 pub fn wait_for_post_edit_diagnostics(
1330 &mut self,
1331 file_path: &Path,
1332 _config: &Config,
1336 expected_versions: &[(ServerKey, i32)],
1337 pre_snapshot: &HashMap<ServerKey, PreEditSnapshot>,
1338 timeout: std::time::Duration,
1339 ) -> PostEditWaitOutcome {
1340 let mut wait = self.start_post_edit_diagnostics_wait(
1341 file_path,
1342 expected_versions,
1343 pre_snapshot,
1344 timeout,
1345 );
1346 let mut complete = self.poll_post_edit_diagnostics_wait(&mut wait, None);
1347
1348 while !complete && !wait.deadline_reached() {
1349 let event = wait.next_event();
1350 complete = self.poll_post_edit_diagnostics_wait(&mut wait, event);
1351 }
1352
1353 self.finish_post_edit_diagnostics_wait(wait)
1354 }
1355
1356 fn post_edit_outcome(
1357 expected: Vec<ServerKey>,
1358 fresh: HashMap<ServerKey, Vec<StoredDiagnostic>>,
1359 exited: Vec<ServerKey>,
1360 ) -> PostEditWaitOutcome {
1361 let pending = expected
1362 .into_iter()
1363 .filter(|key| !fresh.contains_key(key) && !exited.contains(key))
1364 .collect();
1365 let mut diagnostics = fresh.into_values().flatten().collect::<Vec<_>>();
1366 diagnostics.sort_by(|left, right| {
1367 left.file
1368 .cmp(&right.file)
1369 .then(left.line.cmp(&right.line))
1370 .then(left.column.cmp(&right.column))
1371 .then(left.message.cmp(&right.message))
1372 });
1373
1374 PostEditWaitOutcome {
1375 diagnostics,
1376 pending_servers: pending,
1377 exited_servers: exited,
1378 }
1379 }
1380
1381 pub fn wait_for_file_diagnostics(
1387 &mut self,
1388 file_path: &Path,
1389 config: &Config,
1390 deadline: std::time::Instant,
1391 ) -> Vec<StoredDiagnostic> {
1392 let lookup_path = normalize_lookup_path(file_path);
1393
1394 if self.server_key_for_file(&lookup_path, config).is_none() {
1395 return Vec::new();
1396 }
1397
1398 loop {
1399 if self.drain_events_for_file(&lookup_path) {
1400 break;
1401 }
1402
1403 let now = std::time::Instant::now();
1404 if now >= deadline {
1405 break;
1406 }
1407
1408 let timeout = deadline.saturating_duration_since(now);
1409 match self.event_rx.recv_timeout(timeout) {
1410 Ok(event) => {
1411 if matches!(
1412 self.handle_event(&event),
1413 Some(ref published_file) if published_file.as_path() == lookup_path.as_path()
1414 ) {
1415 break;
1416 }
1417 }
1418 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
1419 }
1420 }
1421
1422 self.get_diagnostics_for_file(&lookup_path)
1423 .into_iter()
1424 .cloned()
1425 .collect()
1426 }
1427
1428 pub const PULL_FILE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
1434
1435 pub fn pull_file_timeout() -> std::time::Duration {
1437 Self::PULL_FILE_TIMEOUT
1438 }
1439
1440 const PULL_WORKSPACE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
1444
1445 pub fn pull_file_diagnostics(
1456 &mut self,
1457 file_path: &Path,
1458 config: &Config,
1459 ) -> Result<Vec<PullFileResult>, LspError> {
1460 self.pull_file_diagnostics_tracked(file_path, config)
1461 .map(|tracked| tracked.results)
1462 }
1463
1464 pub(crate) fn pull_file_diagnostics_tracked(
1465 &mut self,
1466 file_path: &Path,
1467 config: &Config,
1468 ) -> Result<TrackedPullFileResult, LspError> {
1469 let canonical_path = canonicalize_for_lsp(file_path)?;
1470 let opened = self.ensure_file_open(&canonical_path, config)?;
1473 if opened.server_keys.is_empty() {
1474 return Ok(TrackedPullFileResult {
1475 results: Vec::new(),
1476 newly_opened: opened.newly_opened,
1477 });
1478 }
1479
1480 let uri = uri_for_path(&canonical_path)?;
1481 let mut results = Vec::with_capacity(opened.server_keys.len());
1482
1483 for key in opened.server_keys {
1484 let supports_pull = self
1485 .clients
1486 .get(&key)
1487 .and_then(|c| c.diagnostic_capabilities())
1488 .is_some_and(|caps| caps.pull_diagnostics);
1489
1490 if !supports_pull {
1491 results.push(PullFileResult {
1492 server_key: key.clone(),
1493 outcome: PullFileOutcome::PullNotSupported,
1494 });
1495 continue;
1496 }
1497
1498 let previous_result_id = self
1500 .diagnostics
1501 .entries_for_file(&canonical_path)
1502 .into_iter()
1503 .find(|(k, _)| **k == key)
1504 .and_then(|(_, entry)| entry.result_id.clone());
1505
1506 let identifier = self
1507 .clients
1508 .get(&key)
1509 .and_then(|c| c.diagnostic_capabilities())
1510 .and_then(|caps| caps.identifier.clone());
1511
1512 let params = AftDocumentDiagnosticParams {
1513 text_document: lsp_types::TextDocumentIdentifier { uri: uri.clone() },
1514 identifier,
1515 previous_result_id,
1516 work_done_progress_params: Default::default(),
1517 partial_result_params: Default::default(),
1518 };
1519
1520 let outcome = match self.send_pull_request(&key, params) {
1521 Ok(report) => {
1522 if matches!(
1523 &report,
1524 lsp_types::DocumentDiagnosticReportResult::Report(
1525 lsp_types::DocumentDiagnosticReport::Full(_)
1526 )
1527 ) {
1528 self.drain_events();
1533 }
1534 self.ingest_document_report(&key, &canonical_path, report)
1535 }
1536 Err(err) => {
1537 if let Some(result) = self.cache_post_initialize_exit(&key, &err) {
1538 PullFileOutcome::RequestFailed {
1539 reason: server_attempt_result_reason(&result),
1540 }
1541 } else if recoverable_pull_rejection(&err)
1542 && self.clients.get(&key).is_some_and(|client| {
1543 matches!(
1544 client.state(),
1545 ServerState::Ready | ServerState::Initializing
1546 )
1547 })
1548 {
1549 PullFileOutcome::RequestFailed {
1550 reason: format!("pull_rejected_push_fallback: {err}"),
1551 }
1552 } else {
1553 PullFileOutcome::RequestFailed {
1554 reason: err.to_string(),
1555 }
1556 }
1557 }
1558 };
1559
1560 results.push(PullFileResult {
1561 server_key: key,
1562 outcome,
1563 });
1564 }
1565
1566 Ok(TrackedPullFileResult {
1567 results,
1568 newly_opened: opened.newly_opened,
1569 })
1570 }
1571
1572 pub fn pull_workspace_diagnostics(
1577 &mut self,
1578 server_key: &ServerKey,
1579 timeout: Option<std::time::Duration>,
1580 ) -> Result<PullWorkspaceResult, LspError> {
1581 let timeout = timeout.unwrap_or(Self::PULL_WORKSPACE_TIMEOUT);
1582
1583 let supports_workspace = self
1584 .clients
1585 .get(server_key)
1586 .and_then(|c| c.diagnostic_capabilities())
1587 .is_some_and(|caps| caps.workspace_diagnostics);
1588
1589 if !supports_workspace {
1590 return Ok(PullWorkspaceResult {
1591 server_key: server_key.clone(),
1592 files_reported: Vec::new(),
1593 complete: false,
1594 cancelled: false,
1595 supports_workspace: false,
1596 });
1597 }
1598
1599 let identifier = self
1600 .clients
1601 .get(server_key)
1602 .and_then(|c| c.diagnostic_capabilities())
1603 .and_then(|caps| caps.identifier.clone());
1604
1605 let params = AftWorkspaceDiagnosticParams {
1606 identifier,
1607 previous_result_ids: Vec::new(),
1608 work_done_progress_params: Default::default(),
1609 partial_result_params: Default::default(),
1610 };
1611
1612 let result = match self
1613 .clients
1614 .get_mut(server_key)
1615 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?
1616 .send_request_with_timeout::<AftWorkspaceDiagnosticRequest>(params, timeout)
1617 {
1618 Ok(result) => result,
1619 Err(LspError::Timeout(_)) => {
1620 return Ok(PullWorkspaceResult {
1621 server_key: server_key.clone(),
1622 files_reported: Vec::new(),
1623 complete: false,
1624 cancelled: true,
1625 supports_workspace: true,
1626 });
1627 }
1628 Err(err) => {
1629 if let Some(result) = self.cache_post_initialize_exit(server_key, &err) {
1630 return Err(LspError::ServerNotReady(server_attempt_result_reason(
1631 &result,
1632 )));
1633 }
1634 return Err(err);
1635 }
1636 };
1637
1638 let (items, complete) = match result {
1642 lsp_types::WorkspaceDiagnosticReportResult::Report(report) => (report.items, true),
1643 lsp_types::WorkspaceDiagnosticReportResult::Partial(partial) => (partial.items, false),
1644 };
1645
1646 let mut files_reported = Vec::with_capacity(items.len());
1648 for item in items {
1649 match item {
1650 lsp_types::WorkspaceDocumentDiagnosticReport::Full(full) => {
1651 if let Some(file) = uri_to_path(&full.uri) {
1652 let stored = from_lsp_diagnostics(
1653 file.clone(),
1654 full.full_document_diagnostic_report.items.clone(),
1655 );
1656 self.diagnostics.publish_with_result_id(
1657 server_key.clone(),
1658 file.clone(),
1659 stored,
1660 full.full_document_diagnostic_report.result_id.clone(),
1661 );
1662 files_reported.push(file);
1663 }
1664 }
1665 lsp_types::WorkspaceDocumentDiagnosticReport::Unchanged(_unchanged) => {
1666 }
1669 }
1670 }
1671
1672 Ok(PullWorkspaceResult {
1673 server_key: server_key.clone(),
1674 files_reported,
1675 complete,
1676 cancelled: false,
1677 supports_workspace: true,
1678 })
1679 }
1680
1681 fn cache_post_initialize_exit(
1682 &mut self,
1683 key: &ServerKey,
1684 err: &LspError,
1685 ) -> Option<ServerAttemptResult> {
1686 let binary = self
1687 .server_binaries
1688 .get(key)
1689 .cloned()
1690 .unwrap_or_else(|| key.kind.id_str().to_string());
1691 let (status, stderr_tail) = {
1692 let client = self.clients.get_mut(key)?;
1693 let mut status = client.child_exit_status();
1694 for _ in 0..10 {
1695 if status.is_some() {
1696 break;
1697 }
1698 std::thread::sleep(std::time::Duration::from_millis(10));
1699 status = client.child_exit_status();
1700 }
1701 let status = status?;
1702 wait_for_stderr_tail(client);
1703 (status, client.stderr_tail())
1704 };
1705 let reason = format_post_initialize_exit_reason(&binary, status, &stderr_tail, err);
1706 let result = ServerAttemptResult::SpawnFailed { binary, reason };
1707 self.clients.remove(key);
1708 self.server_binaries.remove(key);
1709 self.documents.remove(key);
1710 self.diagnostics.clear_for_server(key);
1711 self.failed_spawns.insert(key.clone(), result.clone());
1712 Some(result)
1713 }
1714
1715 fn send_pull_request(
1717 &mut self,
1718 key: &ServerKey,
1719 params: AftDocumentDiagnosticParams,
1720 ) -> Result<lsp_types::DocumentDiagnosticReportResult, LspError> {
1721 let client = self
1722 .clients
1723 .get_mut(key)
1724 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?;
1725 client.send_request_with_timeout::<AftDocumentDiagnosticRequest>(
1729 params,
1730 Self::PULL_FILE_TIMEOUT,
1731 )
1732 }
1733
1734 fn ingest_document_report(
1737 &mut self,
1738 key: &ServerKey,
1739 canonical_path: &Path,
1740 result: lsp_types::DocumentDiagnosticReportResult,
1741 ) -> PullFileOutcome {
1742 let report = match result {
1743 lsp_types::DocumentDiagnosticReportResult::Report(report) => report,
1744 lsp_types::DocumentDiagnosticReportResult::Partial(_) => {
1745 return PullFileOutcome::PartialNotSupported;
1749 }
1750 };
1751
1752 match report {
1753 lsp_types::DocumentDiagnosticReport::Full(full) => {
1754 let result_id = full.full_document_diagnostic_report.result_id.clone();
1755 let stored = from_lsp_diagnostics(
1756 canonical_path.to_path_buf(),
1757 full.full_document_diagnostic_report.items.clone(),
1758 );
1759 let count = stored.len();
1760 let provisional = self
1761 .clients
1762 .get(key)
1763 .is_some_and(|client| client.diagnostics_are_provisional());
1764 self.diagnostics.publish_full_with_provisional(
1765 key.clone(),
1766 canonical_path.to_path_buf(),
1767 stored,
1768 result_id,
1769 None,
1770 provisional,
1771 );
1772 PullFileOutcome::Full {
1773 diagnostic_count: count,
1774 }
1775 }
1776 lsp_types::DocumentDiagnosticReport::Unchanged(_unchanged) => {
1777 if self
1784 .diagnostics
1785 .has_report_for_server_file(key, canonical_path)
1786 {
1787 self.diagnostics
1788 .mark_fresh_for_server_file(key, canonical_path);
1789 let authoritative = self
1790 .clients
1791 .get(key)
1792 .map_or(true, |client| !client.diagnostics_are_provisional());
1793 if authoritative {
1794 self.diagnostics
1795 .clear_provisional_for_server_file(key, canonical_path);
1796 }
1797 PullFileOutcome::Unchanged
1798 } else {
1799 PullFileOutcome::RequestFailed {
1800 reason: "no_cache_for_unchanged".to_string(),
1801 }
1802 }
1803 }
1804 }
1805 }
1806
1807 pub fn shutdown_all(&mut self) {
1809 for (key, mut client) in self.clients.drain() {
1810 if let Err(err) = client.shutdown() {
1811 slog_error!("error shutting down {:?}: {}", key, err);
1812 }
1813 }
1814 self.server_binaries.clear();
1815 self.documents.clear();
1816 self.diagnostics = DiagnosticsStore::new();
1817 }
1818
1819 pub fn has_active_servers(&self) -> bool {
1821 self.clients
1822 .values()
1823 .any(|client| client.state() == ServerState::Ready)
1824 }
1825
1826 pub fn active_server_keys(&self) -> Vec<ServerKey> {
1829 self.clients.keys().cloned().collect()
1830 }
1831
1832 #[cfg(windows)]
1836 #[doc(hidden)]
1837 pub fn watched_file_notification_trace_for_test(&self) -> &str {
1838 &self.last_watched_file_notification_trace
1839 }
1840
1841 pub fn get_diagnostics_for_file(&self, file: &Path) -> Vec<&StoredDiagnostic> {
1842 let normalized = normalize_lookup_path(file);
1843 self.diagnostics.for_file(&normalized)
1844 }
1845
1846 pub fn get_diagnostics_for_file_with_provisional(
1847 &self,
1848 file: &Path,
1849 ) -> Vec<(&StoredDiagnostic, bool)> {
1850 let normalized = normalize_lookup_path(file);
1851 self.diagnostics.for_file_with_provisional(&normalized)
1852 }
1853
1854 pub fn clear_failed_spawns(&mut self) -> usize {
1875 let n = self.failed_spawns.len();
1876 self.failed_spawns.clear();
1877 n
1878 }
1879
1880 #[cfg(test)]
1881 pub(crate) fn insert_failed_spawn_for_test(&mut self) {
1882 let key = ServerKey {
1883 kind: crate::lsp::registry::ServerKind::Rust,
1884 root: std::path::PathBuf::from("/tmp/test-root"),
1885 };
1886 self.failed_spawns.insert(
1887 key,
1888 ServerAttemptResult::SpawnFailed {
1889 binary: "rust-analyzer".to_string(),
1890 reason: "test".to_string(),
1891 },
1892 );
1893 }
1894
1895 pub fn clear_diagnostics_for_file(&mut self, file: &Path) -> bool {
1896 diagnostic_path_candidates(file)
1897 .into_iter()
1898 .fold(false, |removed, candidate| {
1899 removed | self.diagnostics.clear_for_file(&candidate)
1900 })
1901 }
1902
1903 pub fn mark_diagnostics_stale_for_file(&mut self, file: &Path) -> StaleDiagnosticsMark {
1907 let mut result = StaleDiagnosticsMark::default();
1908 for candidate in diagnostic_path_candidates(file) {
1909 let (had_entries, changed) = self.diagnostics.mark_stale_for_file(&candidate);
1910 result.had_entries |= had_entries;
1911 result.changed |= changed;
1912 }
1913 result
1914 }
1915
1916 pub fn get_diagnostics_for_directory(&self, dir: &Path) -> Vec<&StoredDiagnostic> {
1917 let normalized = normalize_lookup_path(dir);
1918 self.diagnostics.for_directory(&normalized)
1919 }
1920
1921 pub fn get_diagnostics_for_directory_with_provisional(
1922 &self,
1923 dir: &Path,
1924 ) -> Vec<(&StoredDiagnostic, bool)> {
1925 let normalized = normalize_lookup_path(dir);
1926 self.diagnostics.for_directory_with_provisional(&normalized)
1927 }
1928
1929 pub fn get_all_diagnostics(&self) -> Vec<&StoredDiagnostic> {
1930 self.diagnostics.all()
1931 }
1932
1933 pub fn get_all_diagnostics_with_provisional(&self) -> Vec<(&StoredDiagnostic, bool)> {
1934 self.diagnostics.all_with_provisional()
1935 }
1936
1937 pub fn has_any_diagnostic_reports(&self) -> bool {
1942 self.diagnostics.has_any_fresh_report()
1943 }
1944
1945 pub fn has_diagnostic_report_for_file(&self, file: &Path) -> bool {
1949 let normalized = normalize_lookup_path(file);
1950 self.diagnostics.has_any_fresh_report_for_file(&normalized)
1951 }
1952
1953 pub fn has_diagnostic_report_for_server_file(&self, server: &ServerKey, file: &Path) -> bool {
1957 let normalized = normalize_lookup_path(file);
1958 self.diagnostics
1959 .has_fresh_report_for_server_file(server, &normalized)
1960 }
1961
1962 fn drain_events_for_file(&mut self, file_path: &Path) -> bool {
1963 let mut saw_file_diagnostics = false;
1964 while let Ok(event) = self.event_rx.try_recv() {
1965 if matches!(
1966 self.handle_event(&event),
1967 Some(ref published_file) if published_file.as_path() == file_path
1968 ) {
1969 saw_file_diagnostics = true;
1970 }
1971 }
1972 saw_file_diagnostics
1973 }
1974
1975 fn handle_event(&mut self, event: &LspEvent) -> Option<PathBuf> {
1976 let published_file = match event {
1977 LspEvent::Notification {
1978 server_kind,
1979 root,
1980 method,
1981 params: Some(params),
1982 } if method == "textDocument/publishDiagnostics" => {
1983 self.handle_publish_diagnostics(server_kind.clone(), root.clone(), params)
1984 }
1985 LspEvent::Notification {
1986 server_kind,
1987 root,
1988 method,
1989 params: Some(params),
1990 } if method == "experimental/serverStatus" => {
1991 self.handle_server_status(server_kind.clone(), root.clone(), params);
1992 None
1993 }
1994 LspEvent::ServerExited { server_kind, root } => {
1995 let key = ServerKey {
1996 kind: server_kind.clone(),
1997 root: root.clone(),
1998 };
1999 self.clients.remove(&key);
2000 self.server_binaries.remove(&key);
2001 self.documents.remove(&key);
2002 self.diagnostics.clear_for_server(&key);
2003 None
2004 }
2005 _ => None,
2006 };
2007 self.wake_post_edit_waiters();
2008 published_file
2009 }
2010
2011 fn wake_post_edit_waiters(&mut self) {
2012 self.post_edit_waiters
2013 .retain(|_, sender| match sender.try_send(()) {
2014 Ok(()) | Err(TrySendError::Full(())) => true,
2015 Err(TrySendError::Disconnected(())) => false,
2016 });
2017 }
2018
2019 fn handle_publish_diagnostics(
2020 &mut self,
2021 server: ServerKind,
2022 root: PathBuf,
2023 params: &serde_json::Value,
2024 ) -> Option<PathBuf> {
2025 if let Ok(publish_params) =
2026 serde_json::from_value::<lsp_types::PublishDiagnosticsParams>(params.clone())
2027 {
2028 let file = uri_to_path(&publish_params.uri)?;
2029 let stored = from_lsp_diagnostics(file.clone(), publish_params.diagnostics);
2030 let key = ServerKey { kind: server, root };
2036 let provisional = self
2037 .clients
2038 .get(&key)
2039 .is_some_and(|client| client.diagnostics_are_provisional());
2040 self.diagnostics.publish_full_with_provisional(
2041 key,
2042 file.clone(),
2043 stored,
2044 None,
2045 publish_params.version,
2046 provisional,
2047 );
2048 return Some(file);
2049 }
2050 None
2051 }
2052
2053 fn handle_server_status(
2054 &mut self,
2055 server: ServerKind,
2056 root: PathBuf,
2057 params: &serde_json::Value,
2058 ) {
2059 if !matches!(&server, ServerKind::Rust)
2060 || params.get("quiescent").and_then(serde_json::Value::as_bool) != Some(true)
2061 {
2062 return;
2063 }
2064
2065 let key = ServerKey { kind: server, root };
2066 let became_quiescent = self
2067 .clients
2068 .get_mut(&key)
2069 .is_some_and(|client| client.set_rust_analyzer_quiescent(true));
2070 if became_quiescent {
2071 self.diagnostics.promote_provisional_for_server(&key);
2072 }
2073 }
2074
2075 fn spawn_server(
2076 &self,
2077 def: &ServerDef,
2078 root: &Path,
2079 config: &Config,
2080 ) -> Result<LspClient, LspError> {
2081 let binary = self.resolve_binary(def, config)?;
2082
2083 let mut merged_env = def.env.clone();
2087 for (key, value) in &self.extra_env {
2088 merged_env.insert(key.clone(), value.clone());
2089 }
2090
2091 let reclaim_root = config
2096 .project_root
2097 .as_deref()
2098 .map(crate::inspect::job::canonicalize_normalized)
2099 .filter(|project_root| root.starts_with(project_root))
2100 .unwrap_or_else(|| root.to_path_buf());
2101
2102 let mut client = LspClient::spawn_with_reclaim_root(
2103 def.kind.clone(),
2104 root.to_path_buf(),
2105 &binary,
2106 &def.args,
2107 &merged_env,
2108 self.event_tx.clone(),
2109 self.child_registry.clone(),
2110 Some(&reclaim_root),
2111 )?;
2112 if let Err(err) = client.initialize(root, def.initialization_options.clone()) {
2113 wait_for_stderr_tail(&mut client);
2114 let stderr_tail = client.stderr_tail();
2115 let reason = if client.child_exited() || !stderr_tail.is_empty() {
2116 format_initialize_failure_reason(&def.binary, &stderr_tail, &err)
2117 } else {
2118 format!("server failed during initialize: {err}")
2119 };
2120 return Err(LspError::ServerNotReady(reason));
2121 }
2122 Ok(client)
2123 }
2124
2125 fn resolve_binary(&self, def: &ServerDef, config: &Config) -> Result<PathBuf, LspError> {
2126 if let Some(path) = self.binary_overrides.get(&def.kind) {
2127 if path.exists() {
2128 return Ok(path.clone());
2129 }
2130 return Err(LspError::NotFound(format!(
2131 "override binary for {:?} not found: {}",
2132 def.kind,
2133 path.display()
2134 )));
2135 }
2136
2137 if let Some(path) = env_binary_override(&def.kind) {
2138 if path.exists() {
2139 return Ok(path);
2140 }
2141 return Err(LspError::NotFound(format!(
2142 "environment override binary for {:?} not found: {}",
2143 def.kind,
2144 path.display()
2145 )));
2146 }
2147
2148 resolve_lsp_binary(
2153 &def.binary,
2154 config.project_root.as_deref(),
2155 &config.lsp_paths_extra,
2156 )
2157 .ok_or_else(|| {
2158 LspError::NotFound(format!(
2159 "language server binary '{}' not found in node_modules/.bin, lsp_paths_extra, or PATH",
2160 def.binary
2161 ))
2162 })
2163 }
2164
2165 fn server_key_for_file(&self, file_path: &Path, config: &Config) -> Option<ServerKey> {
2166 for def in servers_for_file(file_path, config) {
2167 let key = server_key_for_definition(&def, file_path, config)?;
2168 if self.clients.contains_key(&key) {
2169 return Some(key);
2170 }
2171 }
2172 None
2173 }
2174}
2175
2176impl Default for LspManager {
2177 fn default() -> Self {
2178 Self::new()
2179 }
2180}
2181
2182fn wait_for_stderr_tail(client: &mut LspClient) {
2183 for _ in 0..10 {
2184 if !client.stderr_tail().is_empty() {
2185 break;
2186 }
2187 std::thread::sleep(std::time::Duration::from_millis(10));
2188 }
2189}
2190
2191fn recoverable_pull_rejection(err: &LspError) -> bool {
2192 matches!(
2193 err,
2194 LspError::ServerError {
2195 code: -32601 | -32602,
2196 ..
2197 }
2198 )
2199}
2200
2201fn server_attempt_result_reason(result: &ServerAttemptResult) -> String {
2202 match result {
2203 ServerAttemptResult::SpawnFailed { binary, reason } => {
2204 format!("spawn_failed: {binary} ({reason})")
2205 }
2206 ServerAttemptResult::BinaryNotInstalled { binary } => {
2207 format!("binary_not_installed: {binary}")
2208 }
2209 ServerAttemptResult::NoRootMarker { looked_for } => {
2210 format!("no_root_marker (looked for: {})", looked_for.join(", "))
2211 }
2212 ServerAttemptResult::Ok { .. } => "ok".to_string(),
2213 }
2214}
2215
2216fn format_stderr_tail_for_reason(stderr_tail: &str) -> String {
2217 truncate_stderr_tail_for_reason(stderr_tail)
2218 .lines()
2219 .map(|line| format!(" {line}"))
2220 .collect::<Vec<_>>()
2221 .join("\n")
2222}
2223
2224fn truncate_stderr_tail_for_reason(stderr_tail: &str) -> String {
2225 if stderr_tail.len() <= STDERR_REASON_BYTES {
2226 return stderr_tail.to_string();
2227 }
2228
2229 let ellipsis = "...";
2230 let target_len = STDERR_REASON_BYTES.saturating_sub(ellipsis.len());
2231 let mut start = stderr_tail.len() - target_len;
2232 while start < stderr_tail.len() && !stderr_tail.is_char_boundary(start) {
2233 start += 1;
2234 }
2235 format!("{ellipsis}{}", &stderr_tail[start..])
2236}
2237
2238fn format_initialize_failure_reason(binary: &str, stderr_tail: &str, err: &LspError) -> String {
2239 let mut reason = format!("server crashed during initialize: {err}");
2240 if !stderr_tail.is_empty() {
2241 reason.push_str("; stderr (last 64 lines):\n");
2242 reason.push_str(&format_stderr_tail_for_reason(stderr_tail));
2243 reason.push_str("\n\n");
2244 reason.push_str(&failure_hint(binary, stderr_tail));
2245 }
2246 reason
2247}
2248
2249fn format_post_initialize_exit_reason(
2250 binary: &str,
2251 status: std::process::ExitStatus,
2252 stderr_tail: &str,
2253 err: &LspError,
2254) -> String {
2255 let code = status
2256 .code()
2257 .map(|c| c.to_string())
2258 .unwrap_or_else(|| "signal/unknown".to_string());
2259 let mut reason = format!("server exited after initialize (code {code}): {err}");
2260 if !stderr_tail.is_empty() {
2261 reason.push_str("; stderr (last 64 lines):\n");
2262 reason.push_str(&format_stderr_tail_for_reason(stderr_tail));
2263 reason.push_str("\n\n");
2264 reason.push_str(&failure_hint(binary, stderr_tail));
2265 }
2266 reason
2267}
2268
2269fn failure_hint(binary: &str, stderr_tail: &str) -> String {
2270 if stderr_tail.contains("MODULE_NOT_FOUND") || stderr_tail.contains("Cannot find module") {
2271 let package_manager = infer_package_manager(stderr_tail);
2272 format!(
2273 "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."
2274 )
2275 } else if let Some(component) = rustup_missing_component(stderr_tail) {
2276 format!("'{component}' is a rustup proxy but the component is not installed. Install it: rustup component add {component}")
2281 } else {
2282 format!("Hint: see stderr above for '{binary}' failure details.")
2283 }
2284}
2285
2286fn rustup_missing_component(stderr_tail: &str) -> Option<String> {
2292 let marker = "Unknown binary '";
2293 let start = stderr_tail.find(marker)? + marker.len();
2294 let rest = &stderr_tail[start..];
2295 let end = rest.find('\'')?;
2296 let name = &rest[..end];
2297 if name.is_empty() || !stderr_tail.contains("toolchain") {
2300 return None;
2301 }
2302 Some(name.to_string())
2303}
2304
2305fn infer_package_manager(stderr_tail: &str) -> &'static str {
2306 let lower = stderr_tail.to_ascii_lowercase();
2307 if lower.contains(".pnpm/") || lower.contains(".pnpm\\") || lower.contains("/pnpm/") {
2308 "pnpm"
2309 } else if lower.contains(".yarn/")
2310 || lower.contains(".yarn\\")
2311 || lower.contains("/yarn/")
2312 || lower.contains("yarn")
2313 {
2314 "yarn"
2315 } else {
2316 "npm"
2317 }
2318}
2319
2320fn canonicalize_for_lsp(file_path: &Path) -> Result<PathBuf, LspError> {
2321 std::fs::canonicalize(file_path)
2328 .map(|canonical| crate::inspect::job::normalize_path(&canonical))
2329 .map_err(LspError::from)
2330}
2331
2332fn resolve_for_lsp_uri(file_path: &Path) -> PathBuf {
2333 if let Ok(path) = std::fs::canonicalize(file_path) {
2336 return crate::inspect::job::normalize_path(&path);
2337 }
2338
2339 let mut existing = file_path.to_path_buf();
2340 let mut missing = Vec::new();
2341 while !existing.exists() {
2342 let Some(name) = existing.file_name() else {
2343 break;
2344 };
2345 missing.push(name.to_owned());
2346 let Some(parent) = existing.parent() else {
2347 break;
2348 };
2349 existing = parent.to_path_buf();
2350 }
2351
2352 let mut resolved = std::fs::canonicalize(&existing)
2353 .map(|canonical| crate::inspect::job::normalize_path(&canonical))
2354 .unwrap_or(existing);
2355 for segment in missing.into_iter().rev() {
2356 resolved.push(segment);
2357 }
2358 resolved
2359}
2360
2361fn language_id_for_extension(ext: &str) -> &'static str {
2362 match ext {
2363 "ts" => "typescript",
2364 "tsx" => "typescriptreact",
2365 "js" | "mjs" | "cjs" => "javascript",
2366 "jsx" => "javascriptreact",
2367 "py" | "pyi" => "python",
2368 "rs" => "rust",
2369 "go" => "go",
2370 "html" | "htm" => "html",
2371 _ => "plaintext",
2372 }
2373}
2374
2375fn normalize_lookup_path(path: &Path) -> PathBuf {
2376 std::fs::canonicalize(path)
2380 .map(|canonical| crate::inspect::job::normalize_path(&canonical))
2381 .unwrap_or_else(|_| path.to_path_buf())
2382}
2383
2384fn diagnostic_path_candidates(file: &Path) -> Vec<PathBuf> {
2385 let mut candidates = Vec::with_capacity(4);
2386 let mut add = |candidate: PathBuf| {
2387 if !candidates.iter().any(|existing| existing == &candidate) {
2388 candidates.push(candidate);
2389 }
2390 };
2391
2392 add(file.to_path_buf());
2396 add(normalize_lookup_path(file));
2397
2398 if let (Some(parent), Some(name)) = (file.parent(), file.file_name()) {
2402 if let Ok(canonical_parent) = std::fs::canonicalize(parent) {
2403 let reconstructed = canonical_parent.join(name);
2404 add(reconstructed.clone());
2405 add(crate::inspect::job::normalize_path(&reconstructed));
2406 }
2407 }
2408
2409 candidates
2410}
2411
2412fn classify_spawn_error(binary: &str, err: &LspError) -> ServerAttemptResult {
2419 match err {
2420 LspError::NotFound(_) => ServerAttemptResult::BinaryNotInstalled {
2425 binary: binary.to_string(),
2426 },
2427 other => ServerAttemptResult::SpawnFailed {
2428 binary: binary.to_string(),
2429 reason: other.to_string(),
2430 },
2431 }
2432}
2433
2434fn env_binary_override(kind: &ServerKind) -> Option<PathBuf> {
2435 let id = kind.id_str();
2436 let suffix: String = id
2437 .chars()
2438 .map(|ch| {
2439 if ch.is_ascii_alphanumeric() {
2440 ch.to_ascii_uppercase()
2441 } else {
2442 '_'
2443 }
2444 })
2445 .collect();
2446 let key = format!("AFT_LSP_{suffix}_BINARY");
2447 std::env::var_os(key).map(PathBuf::from)
2448}
2449
2450#[cfg(all(test, windows))]
2451mod windows_server_key_tests {
2452 use std::fs;
2453 use std::os::windows::ffi::OsStrExt;
2454
2455 use super::{canonicalize_for_lsp, server_key_for_definition};
2456 use crate::config::{Config, UserServerDef};
2457 use crate::lsp::registry::servers_for_file;
2458
2459 #[test]
2460 fn normalized_and_verbatim_inputs_produce_identical_server_key_material() {
2461 let temp_dir = tempfile::tempdir().expect("tempdir");
2462 let root = temp_dir.path().join("workspace");
2463 let source = root.join("src").join("main.customts");
2464 fs::create_dir_all(source.parent().expect("source parent")).expect("create source dir");
2465 fs::write(root.join("custom-root.json"), "{}\n").expect("write root marker");
2466 fs::write(&source, "export const value = 1;\n").expect("write source");
2467
2468 let config = Config {
2469 project_root: Some(root),
2470 lsp_servers: vec![UserServerDef {
2471 id: "custom-ts".to_string(),
2472 extensions: vec!["customts".to_string()],
2473 binary: "custom-ts-lsp".to_string(),
2474 args: Vec::new(),
2475 root_markers: vec!["custom-root.json".to_string()],
2476 env: Default::default(),
2477 initialization_options: None,
2478 disabled: false,
2479 }],
2480 ..Config::default()
2481 };
2482
2483 let normalized_input = canonicalize_for_lsp(&source).expect("normalized source path");
2484 let bare_canonical_input = fs::canonicalize(&source).expect("canonical source path");
2485 let key_for = |path: &std::path::Path| {
2486 let def = servers_for_file(path, &config)
2487 .into_iter()
2488 .find(|def| def.kind.id_str() == "custom-ts")
2489 .expect("custom server definition");
2490 server_key_for_definition(&def, path, &config).expect("custom server root")
2491 };
2492
2493 let key_material = |key: &crate::lsp::roots::ServerKey| {
2494 let root_bytes = key
2495 .root
2496 .as_os_str()
2497 .encode_wide()
2498 .flat_map(u16::to_le_bytes)
2499 .collect::<Vec<_>>();
2500 (key.kind.id_str().to_string(), root_bytes)
2501 };
2502 let ensure_key = key_for(&normalized_input);
2503 let running_lookup_key = key_for(&bare_canonical_input);
2504
2505 assert_eq!(key_material(&ensure_key), key_material(&running_lookup_key));
2506 }
2507}
2508
2509#[cfg(test)]
2510mod failure_hint_tests {
2511 use super::{failure_hint, rustup_missing_component};
2512
2513 #[test]
2514 fn detects_rustup_proxy_without_component() {
2515 let stderr = "error: Unknown binary 'rust-analyzer' in official toolchain 'stable-aarch64-apple-darwin'.";
2517 assert_eq!(
2518 rustup_missing_component(stderr).as_deref(),
2519 Some("rust-analyzer")
2520 );
2521 let hint = failure_hint("rust-analyzer", stderr);
2522 assert!(
2523 hint.contains("rustup component add rust-analyzer"),
2524 "expected actionable rustup hint, got: {hint}"
2525 );
2526 }
2527
2528 #[test]
2529 fn ignores_unknown_binary_without_toolchain_phrasing() {
2530 let stderr = "fatal: Unknown binary 'foo' was requested by the linker.";
2533 assert_eq!(rustup_missing_component(stderr), None);
2534 assert!(failure_hint("foo", stderr).starts_with("Hint: see stderr"));
2535 }
2536
2537 #[test]
2538 fn npm_module_not_found_still_wins() {
2539 let stderr = "Error: Cannot find module '/x/typescript-language-server/lib/cli.mjs'";
2541 let hint = failure_hint("typescript-language-server", stderr);
2542 assert!(hint.contains("install -g"), "got: {hint}");
2543 }
2544}
2545
2546#[cfg(test)]
2547mod diagnostic_capacity_tests {
2548 use std::fs;
2549
2550 use super::LspManager;
2551 use crate::config::Config;
2552
2553 #[test]
2558 fn set_diagnostic_capacity_propagates_to_store() {
2559 let mut manager = LspManager::new();
2560 manager.set_diagnostic_capacity(7);
2561 assert_eq!(manager.diagnostics_store_for_test().capacity_for_test(), 7);
2562 manager.set_diagnostic_capacity(0); assert_eq!(manager.diagnostics_store_for_test().capacity_for_test(), 0);
2564 }
2565
2566 #[test]
2569 fn clear_failed_spawns_empties_the_cache() {
2570 let mut manager = LspManager::new();
2571 assert_eq!(manager.clear_failed_spawns(), 0);
2572 manager.insert_failed_spawn_for_test();
2573 assert_eq!(manager.clear_failed_spawns(), 1);
2574 assert_eq!(manager.clear_failed_spawns(), 0);
2575 }
2576
2577 #[test]
2578 fn post_write_notification_does_not_start_a_cold_server() {
2579 let dir = tempfile::tempdir().unwrap();
2580 let file = dir.path().join("main.ts");
2581 fs::write(dir.path().join("package.json"), "{}").unwrap();
2582 fs::write(&file, "export const value = 1;\n").unwrap();
2583
2584 let mut manager = LspManager::new();
2585 manager
2586 .notify_file_changed_if_running(&file, "export const value = 1;\n", &Config::default())
2587 .unwrap();
2588 assert!(manager.clients.is_empty());
2589 }
2590}
2591
2592#[cfg(test)]
2593mod post_edit_waiter_tests {
2594 use std::collections::HashMap;
2595 use std::path::PathBuf;
2596 use std::time::{Duration, Instant};
2597
2598 use super::LspManager;
2599 use crate::lsp::client::LspEvent;
2600 use crate::lsp::registry::ServerKind;
2601
2602 #[test]
2603 fn draining_an_event_wakes_registered_post_edit_waiter() {
2604 let mut manager = LspManager::new();
2605 let mut wait = manager.start_post_edit_diagnostics_wait(
2606 PathBuf::from("/workspace/src/main.rs").as_path(),
2607 &[],
2608 &HashMap::new(),
2609 Duration::from_secs(2),
2610 );
2611 manager.enqueue_event_for_test(LspEvent::Notification {
2612 server_kind: ServerKind::Rust,
2613 root: PathBuf::from("/workspace"),
2614 method: "custom/drainedElsewhere".to_string(),
2615 params: None,
2616 });
2617
2618 assert_eq!(manager.drain_events().events.len(), 1);
2619 let started = Instant::now();
2620 assert!(wait.next_event().is_none());
2621 assert!(
2622 started.elapsed() < Duration::from_millis(250),
2623 "a competing drain did not wake the parked post-edit waiter"
2624 );
2625 let _ = manager.poll_post_edit_diagnostics_wait(&mut wait, None);
2626 let _ = manager.finish_post_edit_diagnostics_wait(wait);
2627 }
2628}
2629
2630#[cfg(test)]
2631mod clear_diagnostics_tests {
2632 use std::path::PathBuf;
2633
2634 use super::LspManager;
2635 use crate::lsp::client::LspEvent;
2636 use crate::lsp::diagnostics::{DiagnosticSeverity, StoredDiagnostic};
2637 use crate::lsp::position::uri_for_path;
2638 use crate::lsp::registry::ServerKind;
2639 use crate::lsp::roots::ServerKey;
2640
2641 fn err_diag(file: &PathBuf) -> StoredDiagnostic {
2642 StoredDiagnostic {
2643 file: file.clone(),
2644 line: 1,
2645 column: 1,
2646 end_line: 1,
2647 end_column: 2,
2648 severity: DiagnosticSeverity::Error,
2649 message: "boom".into(),
2650 code: None,
2651 source: None,
2652 }
2653 }
2654
2655 #[test]
2660 fn clear_diagnostics_for_deleted_file_matches_canonical_key() {
2661 let dir = tempfile::tempdir().unwrap();
2662 let canonical_dir = std::fs::canonicalize(dir.path()).unwrap();
2664 let canonical_file = canonical_dir.join("gone.ts");
2665 std::fs::write(&canonical_file, "x").unwrap();
2668
2669 let mut manager = LspManager::new();
2670 let key = ServerKey {
2671 kind: ServerKind::TypeScript,
2672 root: canonical_dir.clone(),
2673 };
2674 manager.diagnostics_store_mut_for_test().publish(
2675 key,
2676 canonical_file.clone(),
2677 vec![err_diag(&canonical_file)],
2678 );
2679 assert_eq!(manager.warm_error_warning_counts(), (1, 0));
2680
2681 std::fs::remove_file(&canonical_file).unwrap();
2682
2683 let watcher_path = dir.path().join("gone.ts");
2686 let removed = manager.clear_diagnostics_for_file(&watcher_path);
2687
2688 assert!(removed, "expected the deleted file's diagnostic to clear");
2689 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
2690 }
2691
2692 #[cfg(windows)]
2693 #[test]
2694 fn clear_diagnostics_for_deleted_file_matches_normalized_publish_key() {
2695 let dir = tempfile::tempdir().unwrap();
2696 let file = dir.path().join("normalized-gone.ts");
2697 std::fs::write(&file, "x").unwrap();
2698 let normalized_file = crate::inspect::job::canonicalize_normalized(&file);
2699
2700 let mut manager = LspManager::new();
2701 let key = ServerKey {
2702 kind: ServerKind::TypeScript,
2703 root: normalized_file.parent().unwrap().to_path_buf(),
2704 };
2705 manager.diagnostics_store_mut_for_test().publish(
2706 key,
2707 normalized_file.clone(),
2708 vec![err_diag(&normalized_file)],
2709 );
2710 std::fs::remove_file(&file).unwrap();
2711
2712 assert!(manager.clear_diagnostics_for_file(&file));
2713 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
2714 }
2715
2716 #[cfg(windows)]
2717 #[test]
2718 fn stale_diagnostics_for_deleted_file_matches_normalized_publish_key() {
2719 let dir = tempfile::tempdir().unwrap();
2720 let file = dir.path().join("normalized-stale.ts");
2721 std::fs::write(&file, "x").unwrap();
2722 let normalized_file = crate::inspect::job::canonicalize_normalized(&file);
2723
2724 let mut manager = LspManager::new();
2725 let key = ServerKey {
2726 kind: ServerKind::TypeScript,
2727 root: normalized_file.parent().unwrap().to_path_buf(),
2728 };
2729 manager.diagnostics_store_mut_for_test().publish(
2730 key,
2731 normalized_file.clone(),
2732 vec![err_diag(&normalized_file)],
2733 );
2734 std::fs::remove_file(&file).unwrap();
2735
2736 let result = manager.mark_diagnostics_stale_for_file(&file);
2737 assert!(result.had_entries);
2738 assert!(result.changed);
2739 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
2740 }
2741
2742 #[test]
2743 fn clear_diagnostics_for_unknown_file_is_noop() {
2744 let mut manager = LspManager::new();
2745 assert!(!manager.clear_diagnostics_for_file(&PathBuf::from("/nope/missing.ts")));
2746 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
2747 }
2748
2749 #[test]
2750 fn drain_events_reports_publish_diagnostics_updates() {
2751 let dir = tempfile::tempdir().unwrap();
2752 let root = std::fs::canonicalize(dir.path()).unwrap();
2753 let file = root.join("main.ts");
2754 std::fs::write(&file, "const x: number = 'nope';").unwrap();
2755
2756 let mut manager = LspManager::new();
2757 let diagnostic = lsp_types::Diagnostic {
2758 range: lsp_types::Range {
2759 start: lsp_types::Position {
2760 line: 0,
2761 character: 0,
2762 },
2763 end: lsp_types::Position {
2764 line: 0,
2765 character: 1,
2766 },
2767 },
2768 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
2769 code: None,
2770 code_description: None,
2771 source: Some("test".into()),
2772 message: "boom".into(),
2773 related_information: None,
2774 tags: None,
2775 data: None,
2776 };
2777 let params = serde_json::to_value(lsp_types::PublishDiagnosticsParams {
2778 uri: uri_for_path(&file).unwrap(),
2779 diagnostics: vec![diagnostic],
2780 version: Some(1),
2781 })
2782 .unwrap();
2783 manager
2784 .event_tx
2785 .send(LspEvent::Notification {
2786 server_kind: ServerKind::TypeScript,
2787 root,
2788 method: "textDocument/publishDiagnostics".into(),
2789 params: Some(params),
2790 })
2791 .unwrap();
2792
2793 let drained = manager.drain_events();
2794
2795 assert!(drained.diagnostics_changed);
2796 assert_eq!(drained.events.len(), 1);
2797 assert_eq!(manager.warm_error_warning_counts(), (1, 0));
2798 }
2799}