1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3
4use crossbeam_channel::{unbounded, Receiver, RecvTimeoutError, Sender};
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
242impl IntoIterator for DrainedLspEvents {
243 type Item = LspEvent;
244 type IntoIter = std::vec::IntoIter<LspEvent>;
245
246 fn into_iter(self) -> Self::IntoIter {
247 self.events.into_iter()
248 }
249}
250
251pub struct LspManager {
252 clients: HashMap<ServerKey, LspClient>,
254 server_binaries: HashMap<ServerKey, String>,
258 documents: HashMap<ServerKey, DocumentStore>,
260 diagnostics: DiagnosticsStore,
262 event_tx: Sender<LspEvent>,
264 event_rx: Receiver<LspEvent>,
265 binary_overrides: HashMap<ServerKind, PathBuf>,
267 extra_env: HashMap<String, String>,
271 failed_spawns: HashMap<ServerKey, ServerAttemptResult>,
286 watched_file_skip_logged: HashSet<ServerKey>,
289 #[cfg(windows)]
292 last_watched_file_notification_trace: String,
293 child_registry: LspChildRegistry,
297}
298
299impl LspManager {
300 pub fn new() -> Self {
301 let (event_tx, event_rx) = unbounded();
302 Self {
303 clients: HashMap::new(),
304 server_binaries: HashMap::new(),
305 documents: HashMap::new(),
306 diagnostics: DiagnosticsStore::new(),
307 event_tx,
308 event_rx,
309 binary_overrides: HashMap::new(),
310 extra_env: HashMap::new(),
311 failed_spawns: HashMap::new(),
312 watched_file_skip_logged: HashSet::new(),
313 #[cfg(windows)]
314 last_watched_file_notification_trace: "no watched-file notification attempted"
315 .to_string(),
316 child_registry: LspChildRegistry::new(),
317 }
318 }
319
320 pub fn set_child_registry(&mut self, registry: LspChildRegistry) {
322 self.child_registry = registry;
323 }
324
325 pub fn set_extra_env(&mut self, key: &str, value: &str) {
329 self.extra_env.insert(key.to_string(), value.to_string());
330 }
331
332 pub fn server_count(&self) -> usize {
334 self.clients.len()
335 }
336
337 pub fn estimated_memory(&self) -> crate::memory::MemoryEstimate {
341 let mut bytes = 0u64;
342 let mut document_count = 0u64;
343 for documents in self.documents.values() {
344 let estimate = documents.estimated_memory();
345 bytes = bytes.saturating_add(estimate.estimated_bytes.unwrap_or(0));
346 document_count = document_count
347 .saturating_add(estimate.counts.get("documents").copied().unwrap_or(0));
348 }
349 let diagnostics = self.diagnostics.estimated_memory();
350 bytes = bytes.saturating_add(diagnostics.estimated_bytes.unwrap_or(0));
351 crate::memory::MemoryEstimate::estimated(bytes)
352 .count("servers", self.clients.len())
353 .count("document_stores", self.documents.len())
354 .count_u64("documents", document_count)
355 .count_u64(
356 "diagnostic_entries",
357 diagnostics
358 .counts
359 .get("diagnostic_entries")
360 .copied()
361 .unwrap_or(0),
362 )
363 .count_u64(
364 "diagnostics",
365 diagnostics.counts.get("diagnostics").copied().unwrap_or(0),
366 )
367 }
368
369 pub fn set_diagnostic_capacity(&mut self, capacity: usize) {
373 self.diagnostics.set_capacity(capacity);
374 }
375
376 pub fn override_binary(&mut self, kind: ServerKind, binary_path: PathBuf) {
378 self.binary_overrides.insert(kind, binary_path);
379 }
380
381 pub fn ensure_server_for_file(&mut self, file_path: &Path, config: &Config) -> Vec<ServerKey> {
388 self.ensure_server_for_file_detailed(file_path, config)
389 .successful
390 }
391
392 fn running_server_keys_for_file(&self, file_path: &Path, config: &Config) -> Vec<ServerKey> {
393 servers_for_file(file_path, config)
394 .into_iter()
395 .filter_map(|def| server_key_for_definition(&def, file_path, config))
396 .filter(|key| self.clients.contains_key(key))
397 .collect()
398 }
399
400 pub fn ensure_server_for_file_detailed(
408 &mut self,
409 file_path: &Path,
410 config: &Config,
411 ) -> EnsureServerOutcomes {
412 let defs = servers_for_file(file_path, config);
413 let mut outcomes = EnsureServerOutcomes::default();
414
415 for def in defs {
416 let server_id = def.kind.id_str().to_string();
417 let server_name = def.name.to_string();
418
419 let Some(key) = server_key_for_definition(&def, file_path, config) else {
420 outcomes.attempts.push(ServerAttempt {
421 server_id,
422 server_name,
423 result: ServerAttemptResult::NoRootMarker {
424 looked_for: def.root_markers.iter().map(|s| s.to_string()).collect(),
425 },
426 });
427 continue;
428 };
429
430 if !self.clients.contains_key(&key) {
431 if let Some(cached) = self.failed_spawns.get(&key) {
438 outcomes.attempts.push(ServerAttempt {
439 server_id,
440 server_name,
441 result: cached.clone(),
442 });
443 continue;
444 }
445
446 match self.spawn_server(&def, &key.root, config) {
447 Ok(client) => {
448 self.clients.insert(key.clone(), client);
449 self.server_binaries.insert(key.clone(), def.binary.clone());
450 self.documents.entry(key.clone()).or_default();
451 }
452 Err(err) => {
453 slog_error!("failed to spawn {}: {}", def.name, err);
454 let result = classify_spawn_error(&def.binary, &err);
455 self.failed_spawns.insert(key.clone(), result.clone());
459 outcomes.attempts.push(ServerAttempt {
460 server_id,
461 server_name,
462 result,
463 });
464 continue;
465 }
466 }
467 }
468
469 outcomes.attempts.push(ServerAttempt {
470 server_id,
471 server_name,
472 result: ServerAttemptResult::Ok {
473 server_key: key.clone(),
474 },
475 });
476 outcomes.successful.push(key);
477 }
478
479 outcomes
480 }
481
482 pub fn ensure_server_for_file_default(&mut self, file_path: &Path) -> Vec<ServerKey> {
485 self.ensure_server_for_file(file_path, &Config::default())
486 }
487 pub fn ensure_file_open(
492 &mut self,
493 file_path: &Path,
494 config: &Config,
495 ) -> Result<EnsureFileOpenResult, LspError> {
496 let canonical_path = canonicalize_for_lsp(file_path)?;
497 let server_keys = self.ensure_server_for_file(&canonical_path, config);
498 if server_keys.is_empty() {
499 return Ok(EnsureFileOpenResult::default());
500 }
501
502 let uri = uri_for_path(&canonical_path)?;
503 let language_id = language_id_for_extension(
504 canonical_path
505 .extension()
506 .and_then(|ext| ext.to_str())
507 .unwrap_or_default(),
508 )
509 .to_string();
510 let needs_content = server_keys.iter().any(|key| {
511 !self
512 .documents
513 .get(key)
514 .is_some_and(|store| store.is_open(&canonical_path))
515 });
516 let initial_content = needs_content
517 .then(|| std::fs::read_to_string(&canonical_path).map_err(LspError::Io))
518 .transpose()?;
519 let mut newly_opened = Vec::new();
520
521 for key in &server_keys {
522 let already_open = self
523 .documents
524 .get(key)
525 .is_some_and(|store| store.is_open(&canonical_path));
526
527 if !already_open {
528 let content = initial_content
529 .as_ref()
530 .expect("content is loaded when any server needs didOpen");
531 let send_result = if let Some(client) = self.clients.get_mut(key) {
532 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
533 text_document: TextDocumentItem::new(
534 uri.clone(),
535 language_id.clone(),
536 0,
537 content.clone(),
538 ),
539 })
540 } else {
541 Ok(())
542 };
543 if let Err(err) = send_result {
544 let _ = self.close_file_for_servers(&canonical_path, &newly_opened);
545 return Err(err);
546 }
547 self.documents
548 .entry(key.clone())
549 .or_default()
550 .open(canonical_path.clone());
551 newly_opened.push(key.clone());
552 continue;
553 }
554
555 let drifted = self
565 .documents
566 .get(key)
567 .is_some_and(|store| store.is_stale_on_disk(&canonical_path));
568 if drifted {
569 let content = match std::fs::read_to_string(&canonical_path) {
570 Ok(content) => content,
571 Err(err) => {
572 let _ = self.close_file_for_servers(&canonical_path, &newly_opened);
573 return Err(LspError::Io(err));
574 }
575 };
576 let next_version = self
577 .documents
578 .get(key)
579 .and_then(|store| store.version(&canonical_path))
580 .map(|v| v + 1)
581 .unwrap_or(1);
582 let send_result = if let Some(client) = self.clients.get_mut(key) {
583 client.send_notification::<DidChangeTextDocument>(DidChangeTextDocumentParams {
584 text_document: VersionedTextDocumentIdentifier::new(
585 uri.clone(),
586 next_version,
587 ),
588 content_changes: vec![TextDocumentContentChangeEvent {
589 range: None,
590 range_length: None,
591 text: content,
592 }],
593 })
594 } else {
595 Ok(())
596 };
597 if let Err(err) = send_result {
598 let _ = self.close_file_for_servers(&canonical_path, &newly_opened);
599 return Err(err);
600 }
601 if let Some(store) = self.documents.get_mut(key) {
602 store.bump_version(&canonical_path);
603 }
604 }
605 }
606
607 Ok(EnsureFileOpenResult {
608 server_keys,
609 newly_opened,
610 })
611 }
612
613 pub fn ensure_file_open_default(
614 &mut self,
615 file_path: &Path,
616 ) -> Result<EnsureFileOpenResult, LspError> {
617 self.ensure_file_open(file_path, &Config::default())
618 }
619
620 pub fn notify_file_changed(
626 &mut self,
627 file_path: &Path,
628 content: &str,
629 config: &Config,
630 ) -> Result<(), LspError> {
631 self.notify_file_changed_versioned(file_path, content, config)
632 .map(|_| ())
633 }
634
635 pub fn notify_file_changed_versioned(
646 &mut self,
647 file_path: &Path,
648 content: &str,
649 config: &Config,
650 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
651 let canonical_path = canonicalize_for_lsp(file_path)?;
652 let server_keys = self.ensure_server_for_file(&canonical_path, config);
653 self.notify_file_changed_for_server_keys(canonical_path, content, server_keys)
654 }
655
656 pub fn notify_file_changed_if_running(
662 &mut self,
663 file_path: &Path,
664 content: &str,
665 config: &Config,
666 ) -> Result<(), LspError> {
667 let canonical_path = canonicalize_for_lsp(file_path)?;
668 let server_keys = self.running_server_keys_for_file(&canonical_path, config);
669 self.notify_file_changed_for_server_keys(canonical_path, content, server_keys)
670 .map(|_| ())
671 }
672
673 fn notify_file_changed_for_server_keys(
674 &mut self,
675 canonical_path: PathBuf,
676 content: &str,
677 server_keys: Vec<ServerKey>,
678 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
679 if server_keys.is_empty() {
680 return Ok(Vec::new());
681 }
682
683 let uri = uri_for_path(&canonical_path)?;
684 let language_id = language_id_for_extension(
685 canonical_path
686 .extension()
687 .and_then(|ext| ext.to_str())
688 .unwrap_or_default(),
689 )
690 .to_string();
691
692 let mut versions: Vec<(ServerKey, i32)> = Vec::with_capacity(server_keys.len());
693
694 for key in server_keys {
695 let current_version = self
696 .documents
697 .get(&key)
698 .and_then(|store| store.version(&canonical_path));
699
700 if let Some(version) = current_version {
701 let next_version = version + 1;
702 if let Some(client) = self.clients.get_mut(&key) {
703 client.send_notification::<DidChangeTextDocument>(
704 DidChangeTextDocumentParams {
705 text_document: VersionedTextDocumentIdentifier::new(
706 uri.clone(),
707 next_version,
708 ),
709 content_changes: vec![TextDocumentContentChangeEvent {
710 range: None,
711 range_length: None,
712 text: content.to_string(),
713 }],
714 },
715 )?;
716 }
717 if let Some(store) = self.documents.get_mut(&key) {
718 store.bump_version(&canonical_path);
719 }
720 versions.push((key, next_version));
721 continue;
722 }
723
724 if let Some(client) = self.clients.get_mut(&key) {
725 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
726 text_document: TextDocumentItem::new(
727 uri.clone(),
728 language_id.clone(),
729 0,
730 content.to_string(),
731 ),
732 })?;
733 }
734 self.documents
735 .entry(key.clone())
736 .or_default()
737 .open(canonical_path.clone());
738 versions.push((key, 0));
741 }
742
743 Ok(versions)
744 }
745
746 pub fn notify_file_changed_default(
747 &mut self,
748 file_path: &Path,
749 content: &str,
750 ) -> Result<(), LspError> {
751 self.notify_file_changed(file_path, content, &Config::default())
752 }
753
754 pub fn notify_files_watched_changed(
760 &mut self,
761 paths: &[(PathBuf, FileChangeType)],
762 _config: &Config,
763 ) -> Result<(), LspError> {
764 #[cfg(windows)]
765 let mut trace = vec![format!(
766 "input_paths={paths:?}; active_keys={:?}",
767 self.clients.keys().collect::<Vec<_>>()
768 )];
769
770 if paths.is_empty() {
771 #[cfg(windows)]
772 {
773 trace.push("outcome=no-input-paths".to_string());
774 self.last_watched_file_notification_trace = trace.join("\n");
775 }
776 return Ok(());
777 }
778
779 let mut canonical_events = Vec::with_capacity(paths.len());
780 for (path, typ) in paths {
781 let canonical_path = resolve_for_lsp_uri(path);
782 canonical_events.push((canonical_path, *typ));
783 }
784 #[cfg(windows)]
785 trace.push(format!("resolved_events={canonical_events:?}"));
786
787 let keys: Vec<ServerKey> = self.clients.keys().cloned().collect();
788 #[cfg(windows)]
789 if keys.is_empty() {
790 trace.push("outcome=no-active-client".to_string());
791 }
792 for key in keys {
793 let mut changes = Vec::new();
794 for (path, typ) in &canonical_events {
795 if !path.starts_with(&key.root) {
796 continue;
797 }
798 changes.push(FileEvent::new(uri_for_path(path)?, *typ));
799 }
800
801 if changes.is_empty() {
802 #[cfg(windows)]
803 trace.push(format!("key={key:?}; outcome=outside-root"));
804 continue;
805 }
806
807 if let Some(client) = self.clients.get_mut(&key) {
808 let supports_static_watched_files = client.supports_watched_files();
814 let has_dynamic_registration = client.has_watched_file_registration();
815 if !(supports_static_watched_files || has_dynamic_registration) {
816 #[cfg(windows)]
817 trace.push(format!(
818 "key={key:?}; changes={changes:?}; outcome=unsupported; static={supports_static_watched_files}; dynamic={has_dynamic_registration}"
819 ));
820 if self.watched_file_skip_logged.insert(key.clone()) {
821 log::debug!(
822 "skipping didChangeWatchedFiles for {:?} (not supported or registered)",
823 key
824 );
825 }
826 continue;
827 }
828 #[cfg(windows)]
829 trace.push(format!("key={key:?}; changes={changes:?}; action=send"));
830 let send_result = client.send_notification::<DidChangeWatchedFiles>(
831 DidChangeWatchedFilesParams { changes },
832 );
833 #[cfg(windows)]
834 trace.push(format!(
835 "key={key:?}; outcome={}",
836 if send_result.is_ok() {
837 "sent"
838 } else {
839 "send-error"
840 }
841 ));
842 if let Err(error) = send_result {
843 #[cfg(windows)]
844 {
845 self.last_watched_file_notification_trace = trace.join("\n");
846 }
847 return Err(error);
848 }
849 }
850 }
851
852 #[cfg(windows)]
853 {
854 self.last_watched_file_notification_trace = trace.join("\n");
855 }
856 Ok(())
857 }
858
859 pub fn notify_file_closed(&mut self, file_path: &Path) -> Result<(), LspError> {
861 let canonical_path = canonicalize_for_lsp(file_path)?;
862 let keys = self
863 .documents
864 .iter()
865 .filter(|(_, store)| store.is_open(&canonical_path))
866 .map(|(key, _)| key.clone())
867 .collect::<Vec<_>>();
868 self.close_file_for_servers(&canonical_path, &keys)
869 }
870
871 pub(crate) fn close_file_for_servers(
876 &mut self,
877 file_path: &Path,
878 server_keys: &[ServerKey],
879 ) -> Result<(), LspError> {
880 let canonical_path = canonicalize_for_lsp(file_path)?;
881 let uri = uri_for_path(&canonical_path)?;
882 let mut first_error = None;
883
884 for key in server_keys {
885 let was_open = self
886 .documents
887 .get(key)
888 .is_some_and(|store| store.is_open(&canonical_path));
889 if !was_open {
890 continue;
891 }
892
893 if let Some(client) = self.clients.get_mut(key) {
894 if let Err(err) =
895 client.send_notification::<DidCloseTextDocument>(DidCloseTextDocumentParams {
896 text_document: TextDocumentIdentifier::new(uri.clone()),
897 })
898 {
899 if first_error.is_none() {
900 first_error = Some(err);
901 }
902 }
903 }
904
905 if let Some(store) = self.documents.get_mut(key) {
906 store.close(&canonical_path);
907 }
908 self.diagnostics.clear_for_server_file(key, &canonical_path);
909 }
910
911 match first_error {
912 Some(err) => Err(err),
913 None => Ok(()),
914 }
915 }
916
917 pub fn client_for_file(&self, file_path: &Path, config: &Config) -> Option<&LspClient> {
919 let key = self.server_key_for_file(file_path, config)?;
920 self.clients.get(&key)
921 }
922
923 pub fn client_for_file_default(&self, file_path: &Path) -> Option<&LspClient> {
924 self.client_for_file(file_path, &Config::default())
925 }
926
927 pub fn client_for_file_mut(
929 &mut self,
930 file_path: &Path,
931 config: &Config,
932 ) -> Option<&mut LspClient> {
933 let key = self.server_key_for_file(file_path, config)?;
934 self.clients.get_mut(&key)
935 }
936
937 pub fn client_for_file_mut_default(&mut self, file_path: &Path) -> Option<&mut LspClient> {
938 self.client_for_file_mut(file_path, &Config::default())
939 }
940
941 pub fn active_client_count(&self) -> usize {
943 self.clients.len()
944 }
945
946 pub fn drain_events(&mut self) -> DrainedLspEvents {
948 self.drain_events_bounded(usize::MAX)
949 }
950
951 pub fn has_pending_events(&self) -> bool {
954 !self.event_rx.is_empty()
955 }
956
957 pub fn drain_events_bounded(&mut self, max_events: usize) -> DrainedLspEvents {
958 let mut events = Vec::new();
959 let mut diagnostics_changed = false;
960 while events.len() < max_events {
961 let Ok(event) = self.event_rx.try_recv() else {
962 break;
963 };
964 if self.handle_event(&event).is_some() {
965 diagnostics_changed = true;
966 }
967 events.push(event);
968 }
969 let has_more = events.len() >= max_events && !self.event_rx.is_empty();
970 DrainedLspEvents {
971 events,
972 diagnostics_changed,
973 has_more,
974 }
975 }
976
977 pub fn wait_for_diagnostics(
979 &mut self,
980 file_path: &Path,
981 config: &Config,
982 timeout: std::time::Duration,
983 ) -> Vec<StoredDiagnostic> {
984 let deadline = std::time::Instant::now() + timeout;
985 self.wait_for_file_diagnostics(file_path, config, deadline)
986 }
987
988 pub fn wait_for_diagnostics_default(
989 &mut self,
990 file_path: &Path,
991 timeout: std::time::Duration,
992 ) -> Vec<StoredDiagnostic> {
993 self.wait_for_diagnostics(file_path, &Config::default(), timeout)
994 }
995
996 #[doc(hidden)]
1001 pub fn diagnostics_store_for_test(&self) -> &DiagnosticsStore {
1002 &self.diagnostics
1003 }
1004
1005 #[doc(hidden)]
1006 pub fn diagnostics_store_mut_for_test(&mut self) -> &mut DiagnosticsStore {
1007 &mut self.diagnostics
1008 }
1009
1010 #[doc(hidden)]
1011 pub fn enqueue_event_for_test(&self, event: LspEvent) {
1012 self.event_tx
1013 .send(event)
1014 .expect("LSP event receiver should remain connected");
1015 }
1016
1017 #[doc(hidden)]
1018 pub fn pending_event_count_for_test(&self) -> usize {
1019 self.event_rx.len()
1020 }
1021
1022 #[doc(hidden)]
1023 pub fn document_is_open_for_test(&self, file_path: &Path) -> bool {
1024 canonicalize_for_lsp(file_path).is_ok_and(|canonical_path| {
1025 self.documents
1026 .values()
1027 .any(|store| store.is_open(&canonical_path))
1028 })
1029 }
1030
1031 pub fn warm_error_warning_counts(&self) -> (usize, usize) {
1035 self.diagnostics.error_warning_counts()
1036 }
1037
1038 pub fn warm_error_warning_counts_with_provisional(&self) -> ((usize, usize), bool) {
1039 self.diagnostics.error_warning_counts_with_provisional()
1040 }
1041
1042 pub fn diagnostics_generation(&self) -> u64 {
1043 self.diagnostics.generation()
1044 }
1045
1046 pub fn filtered_error_warning_counts(
1051 &self,
1052 keep: impl FnMut(&std::path::Path) -> bool,
1053 ) -> (usize, usize) {
1054 self.diagnostics.filtered_error_warning_counts(keep)
1055 }
1056
1057 pub fn filtered_error_warning_counts_with_provisional(
1061 &self,
1062 keep: impl FnMut(&std::path::Path) -> bool,
1063 ) -> ((usize, usize), bool) {
1064 self.diagnostics
1065 .filtered_error_warning_counts_with_provisional(keep)
1066 }
1067
1068 pub fn provisional_server_keys(&self) -> Vec<ServerKey> {
1072 self.clients
1073 .iter()
1074 .filter(|(_, client)| client.diagnostics_are_provisional())
1075 .map(|(key, _)| key.clone())
1076 .collect()
1077 }
1078
1079 pub fn snapshot_diagnostic_epochs(&self, file_path: &Path) -> HashMap<ServerKey, u64> {
1084 let lookup_path = normalize_lookup_path(file_path);
1085 self.diagnostics
1086 .entries_for_file(&lookup_path)
1087 .into_iter()
1088 .map(|(key, entry)| (key.clone(), entry.epoch))
1089 .collect()
1090 }
1091
1092 pub fn snapshot_pre_edit_state(&self, file_path: &Path) -> HashMap<ServerKey, PreEditSnapshot> {
1095 let lookup_path = normalize_lookup_path(file_path);
1096 let mut snapshots: HashMap<ServerKey, PreEditSnapshot> = self
1097 .diagnostics
1098 .entries_for_file(&lookup_path)
1099 .into_iter()
1100 .map(|(key, entry)| {
1101 (
1102 key.clone(),
1103 PreEditSnapshot {
1104 epoch: entry.epoch,
1105 document_version_at_capture: None,
1106 },
1107 )
1108 })
1109 .collect();
1110
1111 for (key, store) in &self.documents {
1112 if let Some(version) = store.version(&lookup_path) {
1113 snapshots
1114 .entry(key.clone())
1115 .or_default()
1116 .document_version_at_capture = Some(version);
1117 }
1118 }
1119
1120 snapshots
1121 }
1122
1123 pub fn diagnostic_entry_is_fresh_for_document(
1131 &self,
1132 file_path: &Path,
1133 server_key: &ServerKey,
1134 pre: PreEditSnapshot,
1135 ) -> bool {
1136 let lookup_path = normalize_lookup_path(file_path);
1137 let Some(entry) = self
1138 .diagnostics
1139 .entries_for_file(&lookup_path)
1140 .into_iter()
1141 .find_map(|(key, entry)| if key == server_key { Some(entry) } else { None })
1142 else {
1143 return false;
1144 };
1145
1146 if entry.stale {
1147 return false;
1148 }
1149
1150 let target_version = self
1151 .documents
1152 .get(server_key)
1153 .and_then(|store| store.version(&lookup_path))
1154 .or(pre.document_version_at_capture)
1155 .unwrap_or(0);
1156
1157 matches!(entry.version, Some(version) if version >= target_version)
1158 }
1159
1160 pub fn wait_for_post_edit_diagnostics(
1183 &mut self,
1184 file_path: &Path,
1185 _config: &Config,
1189 expected_versions: &[(ServerKey, i32)],
1190 pre_snapshot: &HashMap<ServerKey, PreEditSnapshot>,
1191 timeout: std::time::Duration,
1192 ) -> PostEditWaitOutcome {
1193 let lookup_path = normalize_lookup_path(file_path);
1194 let deadline = std::time::Instant::now() + timeout;
1195
1196 let _ = self.drain_events_for_file(&lookup_path);
1201
1202 let mut fresh: HashMap<ServerKey, Vec<StoredDiagnostic>> = HashMap::new();
1203 let mut exited: Vec<ServerKey> = Vec::new();
1204
1205 loop {
1206 for (key, target_version) in expected_versions {
1214 if fresh.contains_key(key) || exited.contains(key) {
1215 continue;
1216 }
1217 if !self.clients.contains_key(key) {
1218 exited.push(key.clone());
1219 continue;
1220 }
1221 if let Some(entry) = self
1222 .diagnostics
1223 .entries_for_file(&lookup_path)
1224 .into_iter()
1225 .find_map(|(k, e)| if k == key { Some(e) } else { None })
1226 {
1227 let pre = pre_snapshot.get(key).copied().unwrap_or_default();
1228 let is_fresh = post_edit_entry_is_fresh(entry, *target_version, pre);
1229 if is_fresh {
1230 fresh.insert(key.clone(), entry.diagnostics.clone());
1231 }
1232 }
1233 }
1234
1235 if fresh.len() + exited.len() == expected_versions.len() {
1237 break;
1238 }
1239
1240 let now = std::time::Instant::now();
1241 if now >= deadline {
1242 break;
1243 }
1244
1245 let timeout = deadline.saturating_duration_since(now);
1246 match self.event_rx.recv_timeout(timeout) {
1247 Ok(event) => {
1248 self.handle_event(&event);
1249 }
1250 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
1251 }
1252 }
1253
1254 let pending: Vec<ServerKey> = expected_versions
1256 .iter()
1257 .filter(|(k, _)| !fresh.contains_key(k) && !exited.contains(k))
1258 .map(|(k, _)| k.clone())
1259 .collect();
1260
1261 let mut diagnostics: Vec<StoredDiagnostic> = fresh
1264 .into_iter()
1265 .flat_map(|(_, diags)| diags.into_iter())
1266 .collect();
1267 diagnostics.sort_by(|a, b| {
1268 a.file
1269 .cmp(&b.file)
1270 .then(a.line.cmp(&b.line))
1271 .then(a.column.cmp(&b.column))
1272 .then(a.message.cmp(&b.message))
1273 });
1274
1275 PostEditWaitOutcome {
1276 diagnostics,
1277 pending_servers: pending,
1278 exited_servers: exited,
1279 }
1280 }
1281
1282 pub fn wait_for_file_diagnostics(
1288 &mut self,
1289 file_path: &Path,
1290 config: &Config,
1291 deadline: std::time::Instant,
1292 ) -> Vec<StoredDiagnostic> {
1293 let lookup_path = normalize_lookup_path(file_path);
1294
1295 if self.server_key_for_file(&lookup_path, config).is_none() {
1296 return Vec::new();
1297 }
1298
1299 loop {
1300 if self.drain_events_for_file(&lookup_path) {
1301 break;
1302 }
1303
1304 let now = std::time::Instant::now();
1305 if now >= deadline {
1306 break;
1307 }
1308
1309 let timeout = deadline.saturating_duration_since(now);
1310 match self.event_rx.recv_timeout(timeout) {
1311 Ok(event) => {
1312 if matches!(
1313 self.handle_event(&event),
1314 Some(ref published_file) if published_file.as_path() == lookup_path.as_path()
1315 ) {
1316 break;
1317 }
1318 }
1319 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
1320 }
1321 }
1322
1323 self.get_diagnostics_for_file(&lookup_path)
1324 .into_iter()
1325 .cloned()
1326 .collect()
1327 }
1328
1329 pub const PULL_FILE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
1335
1336 pub fn pull_file_timeout() -> std::time::Duration {
1338 Self::PULL_FILE_TIMEOUT
1339 }
1340
1341 const PULL_WORKSPACE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
1345
1346 pub fn pull_file_diagnostics(
1357 &mut self,
1358 file_path: &Path,
1359 config: &Config,
1360 ) -> Result<Vec<PullFileResult>, LspError> {
1361 self.pull_file_diagnostics_tracked(file_path, config)
1362 .map(|tracked| tracked.results)
1363 }
1364
1365 pub(crate) fn pull_file_diagnostics_tracked(
1366 &mut self,
1367 file_path: &Path,
1368 config: &Config,
1369 ) -> Result<TrackedPullFileResult, LspError> {
1370 let canonical_path = canonicalize_for_lsp(file_path)?;
1371 let opened = self.ensure_file_open(&canonical_path, config)?;
1374 if opened.server_keys.is_empty() {
1375 return Ok(TrackedPullFileResult {
1376 results: Vec::new(),
1377 newly_opened: opened.newly_opened,
1378 });
1379 }
1380
1381 let uri = uri_for_path(&canonical_path)?;
1382 let mut results = Vec::with_capacity(opened.server_keys.len());
1383
1384 for key in opened.server_keys {
1385 let supports_pull = self
1386 .clients
1387 .get(&key)
1388 .and_then(|c| c.diagnostic_capabilities())
1389 .is_some_and(|caps| caps.pull_diagnostics);
1390
1391 if !supports_pull {
1392 results.push(PullFileResult {
1393 server_key: key.clone(),
1394 outcome: PullFileOutcome::PullNotSupported,
1395 });
1396 continue;
1397 }
1398
1399 let previous_result_id = self
1401 .diagnostics
1402 .entries_for_file(&canonical_path)
1403 .into_iter()
1404 .find(|(k, _)| **k == key)
1405 .and_then(|(_, entry)| entry.result_id.clone());
1406
1407 let identifier = self
1408 .clients
1409 .get(&key)
1410 .and_then(|c| c.diagnostic_capabilities())
1411 .and_then(|caps| caps.identifier.clone());
1412
1413 let params = AftDocumentDiagnosticParams {
1414 text_document: lsp_types::TextDocumentIdentifier { uri: uri.clone() },
1415 identifier,
1416 previous_result_id,
1417 work_done_progress_params: Default::default(),
1418 partial_result_params: Default::default(),
1419 };
1420
1421 let outcome = match self.send_pull_request(&key, params) {
1422 Ok(report) => {
1423 if matches!(
1424 &report,
1425 lsp_types::DocumentDiagnosticReportResult::Report(
1426 lsp_types::DocumentDiagnosticReport::Full(_)
1427 )
1428 ) {
1429 self.drain_events();
1434 }
1435 self.ingest_document_report(&key, &canonical_path, report)
1436 }
1437 Err(err) => {
1438 if let Some(result) = self.cache_post_initialize_exit(&key, &err) {
1439 PullFileOutcome::RequestFailed {
1440 reason: server_attempt_result_reason(&result),
1441 }
1442 } else if recoverable_pull_rejection(&err)
1443 && self.clients.get(&key).is_some_and(|client| {
1444 matches!(
1445 client.state(),
1446 ServerState::Ready | ServerState::Initializing
1447 )
1448 })
1449 {
1450 PullFileOutcome::RequestFailed {
1451 reason: format!("pull_rejected_push_fallback: {err}"),
1452 }
1453 } else {
1454 PullFileOutcome::RequestFailed {
1455 reason: err.to_string(),
1456 }
1457 }
1458 }
1459 };
1460
1461 results.push(PullFileResult {
1462 server_key: key,
1463 outcome,
1464 });
1465 }
1466
1467 Ok(TrackedPullFileResult {
1468 results,
1469 newly_opened: opened.newly_opened,
1470 })
1471 }
1472
1473 pub fn pull_workspace_diagnostics(
1478 &mut self,
1479 server_key: &ServerKey,
1480 timeout: Option<std::time::Duration>,
1481 ) -> Result<PullWorkspaceResult, LspError> {
1482 let timeout = timeout.unwrap_or(Self::PULL_WORKSPACE_TIMEOUT);
1483
1484 let supports_workspace = self
1485 .clients
1486 .get(server_key)
1487 .and_then(|c| c.diagnostic_capabilities())
1488 .is_some_and(|caps| caps.workspace_diagnostics);
1489
1490 if !supports_workspace {
1491 return Ok(PullWorkspaceResult {
1492 server_key: server_key.clone(),
1493 files_reported: Vec::new(),
1494 complete: false,
1495 cancelled: false,
1496 supports_workspace: false,
1497 });
1498 }
1499
1500 let identifier = self
1501 .clients
1502 .get(server_key)
1503 .and_then(|c| c.diagnostic_capabilities())
1504 .and_then(|caps| caps.identifier.clone());
1505
1506 let params = AftWorkspaceDiagnosticParams {
1507 identifier,
1508 previous_result_ids: Vec::new(),
1509 work_done_progress_params: Default::default(),
1510 partial_result_params: Default::default(),
1511 };
1512
1513 let result = match self
1514 .clients
1515 .get_mut(server_key)
1516 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?
1517 .send_request_with_timeout::<AftWorkspaceDiagnosticRequest>(params, timeout)
1518 {
1519 Ok(result) => result,
1520 Err(LspError::Timeout(_)) => {
1521 return Ok(PullWorkspaceResult {
1522 server_key: server_key.clone(),
1523 files_reported: Vec::new(),
1524 complete: false,
1525 cancelled: true,
1526 supports_workspace: true,
1527 });
1528 }
1529 Err(err) => {
1530 if let Some(result) = self.cache_post_initialize_exit(server_key, &err) {
1531 return Err(LspError::ServerNotReady(server_attempt_result_reason(
1532 &result,
1533 )));
1534 }
1535 return Err(err);
1536 }
1537 };
1538
1539 let (items, complete) = match result {
1543 lsp_types::WorkspaceDiagnosticReportResult::Report(report) => (report.items, true),
1544 lsp_types::WorkspaceDiagnosticReportResult::Partial(partial) => (partial.items, false),
1545 };
1546
1547 let mut files_reported = Vec::with_capacity(items.len());
1549 for item in items {
1550 match item {
1551 lsp_types::WorkspaceDocumentDiagnosticReport::Full(full) => {
1552 if let Some(file) = uri_to_path(&full.uri) {
1553 let stored = from_lsp_diagnostics(
1554 file.clone(),
1555 full.full_document_diagnostic_report.items.clone(),
1556 );
1557 self.diagnostics.publish_with_result_id(
1558 server_key.clone(),
1559 file.clone(),
1560 stored,
1561 full.full_document_diagnostic_report.result_id.clone(),
1562 );
1563 files_reported.push(file);
1564 }
1565 }
1566 lsp_types::WorkspaceDocumentDiagnosticReport::Unchanged(_unchanged) => {
1567 }
1570 }
1571 }
1572
1573 Ok(PullWorkspaceResult {
1574 server_key: server_key.clone(),
1575 files_reported,
1576 complete,
1577 cancelled: false,
1578 supports_workspace: true,
1579 })
1580 }
1581
1582 fn cache_post_initialize_exit(
1583 &mut self,
1584 key: &ServerKey,
1585 err: &LspError,
1586 ) -> Option<ServerAttemptResult> {
1587 let binary = self
1588 .server_binaries
1589 .get(key)
1590 .cloned()
1591 .unwrap_or_else(|| key.kind.id_str().to_string());
1592 let (status, stderr_tail) = {
1593 let client = self.clients.get_mut(key)?;
1594 let mut status = client.child_exit_status();
1595 for _ in 0..10 {
1596 if status.is_some() {
1597 break;
1598 }
1599 std::thread::sleep(std::time::Duration::from_millis(10));
1600 status = client.child_exit_status();
1601 }
1602 let status = status?;
1603 wait_for_stderr_tail(client);
1604 (status, client.stderr_tail())
1605 };
1606 let reason = format_post_initialize_exit_reason(&binary, status, &stderr_tail, err);
1607 let result = ServerAttemptResult::SpawnFailed { binary, reason };
1608 self.clients.remove(key);
1609 self.server_binaries.remove(key);
1610 self.documents.remove(key);
1611 self.diagnostics.clear_for_server(key);
1612 self.failed_spawns.insert(key.clone(), result.clone());
1613 Some(result)
1614 }
1615
1616 fn send_pull_request(
1618 &mut self,
1619 key: &ServerKey,
1620 params: AftDocumentDiagnosticParams,
1621 ) -> Result<lsp_types::DocumentDiagnosticReportResult, LspError> {
1622 let client = self
1623 .clients
1624 .get_mut(key)
1625 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?;
1626 client.send_request_with_timeout::<AftDocumentDiagnosticRequest>(
1630 params,
1631 Self::PULL_FILE_TIMEOUT,
1632 )
1633 }
1634
1635 fn ingest_document_report(
1638 &mut self,
1639 key: &ServerKey,
1640 canonical_path: &Path,
1641 result: lsp_types::DocumentDiagnosticReportResult,
1642 ) -> PullFileOutcome {
1643 let report = match result {
1644 lsp_types::DocumentDiagnosticReportResult::Report(report) => report,
1645 lsp_types::DocumentDiagnosticReportResult::Partial(_) => {
1646 return PullFileOutcome::PartialNotSupported;
1650 }
1651 };
1652
1653 match report {
1654 lsp_types::DocumentDiagnosticReport::Full(full) => {
1655 let result_id = full.full_document_diagnostic_report.result_id.clone();
1656 let stored = from_lsp_diagnostics(
1657 canonical_path.to_path_buf(),
1658 full.full_document_diagnostic_report.items.clone(),
1659 );
1660 let count = stored.len();
1661 let provisional = self
1662 .clients
1663 .get(key)
1664 .is_some_and(|client| client.diagnostics_are_provisional());
1665 self.diagnostics.publish_full_with_provisional(
1666 key.clone(),
1667 canonical_path.to_path_buf(),
1668 stored,
1669 result_id,
1670 None,
1671 provisional,
1672 );
1673 PullFileOutcome::Full {
1674 diagnostic_count: count,
1675 }
1676 }
1677 lsp_types::DocumentDiagnosticReport::Unchanged(_unchanged) => {
1678 if self
1685 .diagnostics
1686 .has_report_for_server_file(key, canonical_path)
1687 {
1688 self.diagnostics
1689 .mark_fresh_for_server_file(key, canonical_path);
1690 let authoritative = self
1691 .clients
1692 .get(key)
1693 .map_or(true, |client| !client.diagnostics_are_provisional());
1694 if authoritative {
1695 self.diagnostics
1696 .clear_provisional_for_server_file(key, canonical_path);
1697 }
1698 PullFileOutcome::Unchanged
1699 } else {
1700 PullFileOutcome::RequestFailed {
1701 reason: "no_cache_for_unchanged".to_string(),
1702 }
1703 }
1704 }
1705 }
1706 }
1707
1708 pub fn shutdown_all(&mut self) {
1710 for (key, mut client) in self.clients.drain() {
1711 if let Err(err) = client.shutdown() {
1712 slog_error!("error shutting down {:?}: {}", key, err);
1713 }
1714 }
1715 self.server_binaries.clear();
1716 self.documents.clear();
1717 self.diagnostics = DiagnosticsStore::new();
1718 }
1719
1720 pub fn has_active_servers(&self) -> bool {
1722 self.clients
1723 .values()
1724 .any(|client| client.state() == ServerState::Ready)
1725 }
1726
1727 pub fn active_server_keys(&self) -> Vec<ServerKey> {
1730 self.clients.keys().cloned().collect()
1731 }
1732
1733 #[cfg(windows)]
1737 #[doc(hidden)]
1738 pub fn watched_file_notification_trace_for_test(&self) -> &str {
1739 &self.last_watched_file_notification_trace
1740 }
1741
1742 pub fn get_diagnostics_for_file(&self, file: &Path) -> Vec<&StoredDiagnostic> {
1743 let normalized = normalize_lookup_path(file);
1744 self.diagnostics.for_file(&normalized)
1745 }
1746
1747 pub fn get_diagnostics_for_file_with_provisional(
1748 &self,
1749 file: &Path,
1750 ) -> Vec<(&StoredDiagnostic, bool)> {
1751 let normalized = normalize_lookup_path(file);
1752 self.diagnostics.for_file_with_provisional(&normalized)
1753 }
1754
1755 pub fn clear_failed_spawns(&mut self) -> usize {
1776 let n = self.failed_spawns.len();
1777 self.failed_spawns.clear();
1778 n
1779 }
1780
1781 #[cfg(test)]
1782 pub(crate) fn insert_failed_spawn_for_test(&mut self) {
1783 let key = ServerKey {
1784 kind: crate::lsp::registry::ServerKind::Rust,
1785 root: std::path::PathBuf::from("/tmp/test-root"),
1786 };
1787 self.failed_spawns.insert(
1788 key,
1789 ServerAttemptResult::SpawnFailed {
1790 binary: "rust-analyzer".to_string(),
1791 reason: "test".to_string(),
1792 },
1793 );
1794 }
1795
1796 pub fn clear_diagnostics_for_file(&mut self, file: &Path) -> bool {
1797 diagnostic_path_candidates(file)
1798 .into_iter()
1799 .fold(false, |removed, candidate| {
1800 removed | self.diagnostics.clear_for_file(&candidate)
1801 })
1802 }
1803
1804 pub fn mark_diagnostics_stale_for_file(&mut self, file: &Path) -> StaleDiagnosticsMark {
1808 let mut result = StaleDiagnosticsMark::default();
1809 for candidate in diagnostic_path_candidates(file) {
1810 let (had_entries, changed) = self.diagnostics.mark_stale_for_file(&candidate);
1811 result.had_entries |= had_entries;
1812 result.changed |= changed;
1813 }
1814 result
1815 }
1816
1817 pub fn get_diagnostics_for_directory(&self, dir: &Path) -> Vec<&StoredDiagnostic> {
1818 let normalized = normalize_lookup_path(dir);
1819 self.diagnostics.for_directory(&normalized)
1820 }
1821
1822 pub fn get_diagnostics_for_directory_with_provisional(
1823 &self,
1824 dir: &Path,
1825 ) -> Vec<(&StoredDiagnostic, bool)> {
1826 let normalized = normalize_lookup_path(dir);
1827 self.diagnostics.for_directory_with_provisional(&normalized)
1828 }
1829
1830 pub fn get_all_diagnostics(&self) -> Vec<&StoredDiagnostic> {
1831 self.diagnostics.all()
1832 }
1833
1834 pub fn get_all_diagnostics_with_provisional(&self) -> Vec<(&StoredDiagnostic, bool)> {
1835 self.diagnostics.all_with_provisional()
1836 }
1837
1838 pub fn has_any_diagnostic_reports(&self) -> bool {
1843 self.diagnostics.has_any_fresh_report()
1844 }
1845
1846 pub fn has_diagnostic_report_for_file(&self, file: &Path) -> bool {
1850 let normalized = normalize_lookup_path(file);
1851 self.diagnostics.has_any_fresh_report_for_file(&normalized)
1852 }
1853
1854 pub fn has_diagnostic_report_for_server_file(&self, server: &ServerKey, file: &Path) -> bool {
1858 let normalized = normalize_lookup_path(file);
1859 self.diagnostics
1860 .has_fresh_report_for_server_file(server, &normalized)
1861 }
1862
1863 fn drain_events_for_file(&mut self, file_path: &Path) -> bool {
1864 let mut saw_file_diagnostics = false;
1865 while let Ok(event) = self.event_rx.try_recv() {
1866 if matches!(
1867 self.handle_event(&event),
1868 Some(ref published_file) if published_file.as_path() == file_path
1869 ) {
1870 saw_file_diagnostics = true;
1871 }
1872 }
1873 saw_file_diagnostics
1874 }
1875
1876 fn handle_event(&mut self, event: &LspEvent) -> Option<PathBuf> {
1877 match event {
1878 LspEvent::Notification {
1879 server_kind,
1880 root,
1881 method,
1882 params: Some(params),
1883 } if method == "textDocument/publishDiagnostics" => {
1884 self.handle_publish_diagnostics(server_kind.clone(), root.clone(), params)
1885 }
1886 LspEvent::Notification {
1887 server_kind,
1888 root,
1889 method,
1890 params: Some(params),
1891 } if method == "experimental/serverStatus" => {
1892 self.handle_server_status(server_kind.clone(), root.clone(), params);
1893 None
1894 }
1895 LspEvent::ServerExited { server_kind, root } => {
1896 let key = ServerKey {
1897 kind: server_kind.clone(),
1898 root: root.clone(),
1899 };
1900 self.clients.remove(&key);
1901 self.server_binaries.remove(&key);
1902 self.documents.remove(&key);
1903 self.diagnostics.clear_for_server(&key);
1904 None
1905 }
1906 _ => None,
1907 }
1908 }
1909
1910 fn handle_publish_diagnostics(
1911 &mut self,
1912 server: ServerKind,
1913 root: PathBuf,
1914 params: &serde_json::Value,
1915 ) -> Option<PathBuf> {
1916 if let Ok(publish_params) =
1917 serde_json::from_value::<lsp_types::PublishDiagnosticsParams>(params.clone())
1918 {
1919 let file = uri_to_path(&publish_params.uri)?;
1920 let stored = from_lsp_diagnostics(file.clone(), publish_params.diagnostics);
1921 let key = ServerKey { kind: server, root };
1927 let provisional = self
1928 .clients
1929 .get(&key)
1930 .is_some_and(|client| client.diagnostics_are_provisional());
1931 self.diagnostics.publish_full_with_provisional(
1932 key,
1933 file.clone(),
1934 stored,
1935 None,
1936 publish_params.version,
1937 provisional,
1938 );
1939 return Some(file);
1940 }
1941 None
1942 }
1943
1944 fn handle_server_status(
1945 &mut self,
1946 server: ServerKind,
1947 root: PathBuf,
1948 params: &serde_json::Value,
1949 ) {
1950 if !matches!(&server, ServerKind::Rust)
1951 || params.get("quiescent").and_then(serde_json::Value::as_bool) != Some(true)
1952 {
1953 return;
1954 }
1955
1956 let key = ServerKey { kind: server, root };
1957 let became_quiescent = self
1958 .clients
1959 .get_mut(&key)
1960 .is_some_and(|client| client.set_rust_analyzer_quiescent(true));
1961 if became_quiescent {
1962 self.diagnostics.promote_provisional_for_server(&key);
1963 }
1964 }
1965
1966 fn spawn_server(
1967 &self,
1968 def: &ServerDef,
1969 root: &Path,
1970 config: &Config,
1971 ) -> Result<LspClient, LspError> {
1972 let binary = self.resolve_binary(def, config)?;
1973
1974 let mut merged_env = def.env.clone();
1978 for (key, value) in &self.extra_env {
1979 merged_env.insert(key.clone(), value.clone());
1980 }
1981
1982 let reclaim_root = config
1987 .project_root
1988 .as_deref()
1989 .map(crate::inspect::job::canonicalize_normalized)
1990 .filter(|project_root| root.starts_with(project_root))
1991 .unwrap_or_else(|| root.to_path_buf());
1992
1993 let mut client = LspClient::spawn_with_reclaim_root(
1994 def.kind.clone(),
1995 root.to_path_buf(),
1996 &binary,
1997 &def.args,
1998 &merged_env,
1999 self.event_tx.clone(),
2000 self.child_registry.clone(),
2001 Some(&reclaim_root),
2002 )?;
2003 if let Err(err) = client.initialize(root, def.initialization_options.clone()) {
2004 wait_for_stderr_tail(&mut client);
2005 let stderr_tail = client.stderr_tail();
2006 let reason = if client.child_exited() || !stderr_tail.is_empty() {
2007 format_initialize_failure_reason(&def.binary, &stderr_tail, &err)
2008 } else {
2009 format!("server failed during initialize: {err}")
2010 };
2011 return Err(LspError::ServerNotReady(reason));
2012 }
2013 Ok(client)
2014 }
2015
2016 fn resolve_binary(&self, def: &ServerDef, config: &Config) -> Result<PathBuf, LspError> {
2017 if let Some(path) = self.binary_overrides.get(&def.kind) {
2018 if path.exists() {
2019 return Ok(path.clone());
2020 }
2021 return Err(LspError::NotFound(format!(
2022 "override binary for {:?} not found: {}",
2023 def.kind,
2024 path.display()
2025 )));
2026 }
2027
2028 if let Some(path) = env_binary_override(&def.kind) {
2029 if path.exists() {
2030 return Ok(path);
2031 }
2032 return Err(LspError::NotFound(format!(
2033 "environment override binary for {:?} not found: {}",
2034 def.kind,
2035 path.display()
2036 )));
2037 }
2038
2039 resolve_lsp_binary(
2044 &def.binary,
2045 config.project_root.as_deref(),
2046 &config.lsp_paths_extra,
2047 )
2048 .ok_or_else(|| {
2049 LspError::NotFound(format!(
2050 "language server binary '{}' not found in node_modules/.bin, lsp_paths_extra, or PATH",
2051 def.binary
2052 ))
2053 })
2054 }
2055
2056 fn server_key_for_file(&self, file_path: &Path, config: &Config) -> Option<ServerKey> {
2057 for def in servers_for_file(file_path, config) {
2058 let key = server_key_for_definition(&def, file_path, config)?;
2059 if self.clients.contains_key(&key) {
2060 return Some(key);
2061 }
2062 }
2063 None
2064 }
2065}
2066
2067impl Default for LspManager {
2068 fn default() -> Self {
2069 Self::new()
2070 }
2071}
2072
2073fn wait_for_stderr_tail(client: &mut LspClient) {
2074 for _ in 0..10 {
2075 if !client.stderr_tail().is_empty() {
2076 break;
2077 }
2078 std::thread::sleep(std::time::Duration::from_millis(10));
2079 }
2080}
2081
2082fn recoverable_pull_rejection(err: &LspError) -> bool {
2083 matches!(
2084 err,
2085 LspError::ServerError {
2086 code: -32601 | -32602,
2087 ..
2088 }
2089 )
2090}
2091
2092fn server_attempt_result_reason(result: &ServerAttemptResult) -> String {
2093 match result {
2094 ServerAttemptResult::SpawnFailed { binary, reason } => {
2095 format!("spawn_failed: {binary} ({reason})")
2096 }
2097 ServerAttemptResult::BinaryNotInstalled { binary } => {
2098 format!("binary_not_installed: {binary}")
2099 }
2100 ServerAttemptResult::NoRootMarker { looked_for } => {
2101 format!("no_root_marker (looked for: {})", looked_for.join(", "))
2102 }
2103 ServerAttemptResult::Ok { .. } => "ok".to_string(),
2104 }
2105}
2106
2107fn format_stderr_tail_for_reason(stderr_tail: &str) -> String {
2108 truncate_stderr_tail_for_reason(stderr_tail)
2109 .lines()
2110 .map(|line| format!(" {line}"))
2111 .collect::<Vec<_>>()
2112 .join("\n")
2113}
2114
2115fn truncate_stderr_tail_for_reason(stderr_tail: &str) -> String {
2116 if stderr_tail.len() <= STDERR_REASON_BYTES {
2117 return stderr_tail.to_string();
2118 }
2119
2120 let ellipsis = "...";
2121 let target_len = STDERR_REASON_BYTES.saturating_sub(ellipsis.len());
2122 let mut start = stderr_tail.len() - target_len;
2123 while start < stderr_tail.len() && !stderr_tail.is_char_boundary(start) {
2124 start += 1;
2125 }
2126 format!("{ellipsis}{}", &stderr_tail[start..])
2127}
2128
2129fn format_initialize_failure_reason(binary: &str, stderr_tail: &str, err: &LspError) -> String {
2130 let mut reason = format!("server crashed during initialize: {err}");
2131 if !stderr_tail.is_empty() {
2132 reason.push_str("; stderr (last 64 lines):\n");
2133 reason.push_str(&format_stderr_tail_for_reason(stderr_tail));
2134 reason.push_str("\n\n");
2135 reason.push_str(&failure_hint(binary, stderr_tail));
2136 }
2137 reason
2138}
2139
2140fn format_post_initialize_exit_reason(
2141 binary: &str,
2142 status: std::process::ExitStatus,
2143 stderr_tail: &str,
2144 err: &LspError,
2145) -> String {
2146 let code = status
2147 .code()
2148 .map(|c| c.to_string())
2149 .unwrap_or_else(|| "signal/unknown".to_string());
2150 let mut reason = format!("server exited after initialize (code {code}): {err}");
2151 if !stderr_tail.is_empty() {
2152 reason.push_str("; stderr (last 64 lines):\n");
2153 reason.push_str(&format_stderr_tail_for_reason(stderr_tail));
2154 reason.push_str("\n\n");
2155 reason.push_str(&failure_hint(binary, stderr_tail));
2156 }
2157 reason
2158}
2159
2160fn failure_hint(binary: &str, stderr_tail: &str) -> String {
2161 if stderr_tail.contains("MODULE_NOT_FOUND") || stderr_tail.contains("Cannot find module") {
2162 let package_manager = infer_package_manager(stderr_tail);
2163 format!(
2164 "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."
2165 )
2166 } else if let Some(component) = rustup_missing_component(stderr_tail) {
2167 format!("'{component}' is a rustup proxy but the component is not installed. Install it: rustup component add {component}")
2172 } else {
2173 format!("Hint: see stderr above for '{binary}' failure details.")
2174 }
2175}
2176
2177fn rustup_missing_component(stderr_tail: &str) -> Option<String> {
2183 let marker = "Unknown binary '";
2184 let start = stderr_tail.find(marker)? + marker.len();
2185 let rest = &stderr_tail[start..];
2186 let end = rest.find('\'')?;
2187 let name = &rest[..end];
2188 if name.is_empty() || !stderr_tail.contains("toolchain") {
2191 return None;
2192 }
2193 Some(name.to_string())
2194}
2195
2196fn infer_package_manager(stderr_tail: &str) -> &'static str {
2197 let lower = stderr_tail.to_ascii_lowercase();
2198 if lower.contains(".pnpm/") || lower.contains(".pnpm\\") || lower.contains("/pnpm/") {
2199 "pnpm"
2200 } else if lower.contains(".yarn/")
2201 || lower.contains(".yarn\\")
2202 || lower.contains("/yarn/")
2203 || lower.contains("yarn")
2204 {
2205 "yarn"
2206 } else {
2207 "npm"
2208 }
2209}
2210
2211fn canonicalize_for_lsp(file_path: &Path) -> Result<PathBuf, LspError> {
2212 std::fs::canonicalize(file_path)
2219 .map(|canonical| crate::inspect::job::normalize_path(&canonical))
2220 .map_err(LspError::from)
2221}
2222
2223fn resolve_for_lsp_uri(file_path: &Path) -> PathBuf {
2224 if let Ok(path) = std::fs::canonicalize(file_path) {
2227 return crate::inspect::job::normalize_path(&path);
2228 }
2229
2230 let mut existing = file_path.to_path_buf();
2231 let mut missing = Vec::new();
2232 while !existing.exists() {
2233 let Some(name) = existing.file_name() else {
2234 break;
2235 };
2236 missing.push(name.to_owned());
2237 let Some(parent) = existing.parent() else {
2238 break;
2239 };
2240 existing = parent.to_path_buf();
2241 }
2242
2243 let mut resolved = std::fs::canonicalize(&existing)
2244 .map(|canonical| crate::inspect::job::normalize_path(&canonical))
2245 .unwrap_or(existing);
2246 for segment in missing.into_iter().rev() {
2247 resolved.push(segment);
2248 }
2249 resolved
2250}
2251
2252fn language_id_for_extension(ext: &str) -> &'static str {
2253 match ext {
2254 "ts" => "typescript",
2255 "tsx" => "typescriptreact",
2256 "js" | "mjs" | "cjs" => "javascript",
2257 "jsx" => "javascriptreact",
2258 "py" | "pyi" => "python",
2259 "rs" => "rust",
2260 "go" => "go",
2261 "html" | "htm" => "html",
2262 _ => "plaintext",
2263 }
2264}
2265
2266fn normalize_lookup_path(path: &Path) -> PathBuf {
2267 std::fs::canonicalize(path)
2271 .map(|canonical| crate::inspect::job::normalize_path(&canonical))
2272 .unwrap_or_else(|_| path.to_path_buf())
2273}
2274
2275fn diagnostic_path_candidates(file: &Path) -> Vec<PathBuf> {
2276 let mut candidates = Vec::with_capacity(4);
2277 let mut add = |candidate: PathBuf| {
2278 if !candidates.iter().any(|existing| existing == &candidate) {
2279 candidates.push(candidate);
2280 }
2281 };
2282
2283 add(file.to_path_buf());
2287 add(normalize_lookup_path(file));
2288
2289 if let (Some(parent), Some(name)) = (file.parent(), file.file_name()) {
2293 if let Ok(canonical_parent) = std::fs::canonicalize(parent) {
2294 let reconstructed = canonical_parent.join(name);
2295 add(reconstructed.clone());
2296 add(crate::inspect::job::normalize_path(&reconstructed));
2297 }
2298 }
2299
2300 candidates
2301}
2302
2303fn classify_spawn_error(binary: &str, err: &LspError) -> ServerAttemptResult {
2310 match err {
2311 LspError::NotFound(_) => ServerAttemptResult::BinaryNotInstalled {
2316 binary: binary.to_string(),
2317 },
2318 other => ServerAttemptResult::SpawnFailed {
2319 binary: binary.to_string(),
2320 reason: other.to_string(),
2321 },
2322 }
2323}
2324
2325fn env_binary_override(kind: &ServerKind) -> Option<PathBuf> {
2326 let id = kind.id_str();
2327 let suffix: String = id
2328 .chars()
2329 .map(|ch| {
2330 if ch.is_ascii_alphanumeric() {
2331 ch.to_ascii_uppercase()
2332 } else {
2333 '_'
2334 }
2335 })
2336 .collect();
2337 let key = format!("AFT_LSP_{suffix}_BINARY");
2338 std::env::var_os(key).map(PathBuf::from)
2339}
2340
2341#[cfg(all(test, windows))]
2342mod windows_server_key_tests {
2343 use std::fs;
2344 use std::os::windows::ffi::OsStrExt;
2345
2346 use super::{canonicalize_for_lsp, server_key_for_definition};
2347 use crate::config::{Config, UserServerDef};
2348 use crate::lsp::registry::servers_for_file;
2349
2350 #[test]
2351 fn normalized_and_verbatim_inputs_produce_identical_server_key_material() {
2352 let temp_dir = tempfile::tempdir().expect("tempdir");
2353 let root = temp_dir.path().join("workspace");
2354 let source = root.join("src").join("main.customts");
2355 fs::create_dir_all(source.parent().expect("source parent")).expect("create source dir");
2356 fs::write(root.join("custom-root.json"), "{}\n").expect("write root marker");
2357 fs::write(&source, "export const value = 1;\n").expect("write source");
2358
2359 let config = Config {
2360 project_root: Some(root),
2361 lsp_servers: vec![UserServerDef {
2362 id: "custom-ts".to_string(),
2363 extensions: vec!["customts".to_string()],
2364 binary: "custom-ts-lsp".to_string(),
2365 args: Vec::new(),
2366 root_markers: vec!["custom-root.json".to_string()],
2367 env: Default::default(),
2368 initialization_options: None,
2369 disabled: false,
2370 }],
2371 ..Config::default()
2372 };
2373
2374 let normalized_input = canonicalize_for_lsp(&source).expect("normalized source path");
2375 let bare_canonical_input = fs::canonicalize(&source).expect("canonical source path");
2376 let key_for = |path: &std::path::Path| {
2377 let def = servers_for_file(path, &config)
2378 .into_iter()
2379 .find(|def| def.kind.id_str() == "custom-ts")
2380 .expect("custom server definition");
2381 server_key_for_definition(&def, path, &config).expect("custom server root")
2382 };
2383
2384 let key_material = |key: &crate::lsp::roots::ServerKey| {
2385 let root_bytes = key
2386 .root
2387 .as_os_str()
2388 .encode_wide()
2389 .flat_map(u16::to_le_bytes)
2390 .collect::<Vec<_>>();
2391 (key.kind.id_str().to_string(), root_bytes)
2392 };
2393 let ensure_key = key_for(&normalized_input);
2394 let running_lookup_key = key_for(&bare_canonical_input);
2395
2396 assert_eq!(key_material(&ensure_key), key_material(&running_lookup_key));
2397 }
2398}
2399
2400#[cfg(test)]
2401mod failure_hint_tests {
2402 use super::{failure_hint, rustup_missing_component};
2403
2404 #[test]
2405 fn detects_rustup_proxy_without_component() {
2406 let stderr = "error: Unknown binary 'rust-analyzer' in official toolchain 'stable-aarch64-apple-darwin'.";
2408 assert_eq!(
2409 rustup_missing_component(stderr).as_deref(),
2410 Some("rust-analyzer")
2411 );
2412 let hint = failure_hint("rust-analyzer", stderr);
2413 assert!(
2414 hint.contains("rustup component add rust-analyzer"),
2415 "expected actionable rustup hint, got: {hint}"
2416 );
2417 }
2418
2419 #[test]
2420 fn ignores_unknown_binary_without_toolchain_phrasing() {
2421 let stderr = "fatal: Unknown binary 'foo' was requested by the linker.";
2424 assert_eq!(rustup_missing_component(stderr), None);
2425 assert!(failure_hint("foo", stderr).starts_with("Hint: see stderr"));
2426 }
2427
2428 #[test]
2429 fn npm_module_not_found_still_wins() {
2430 let stderr = "Error: Cannot find module '/x/typescript-language-server/lib/cli.mjs'";
2432 let hint = failure_hint("typescript-language-server", stderr);
2433 assert!(hint.contains("install -g"), "got: {hint}");
2434 }
2435}
2436
2437#[cfg(test)]
2438mod diagnostic_capacity_tests {
2439 use std::fs;
2440
2441 use super::LspManager;
2442 use crate::config::Config;
2443
2444 #[test]
2449 fn set_diagnostic_capacity_propagates_to_store() {
2450 let mut manager = LspManager::new();
2451 manager.set_diagnostic_capacity(7);
2452 assert_eq!(manager.diagnostics_store_for_test().capacity_for_test(), 7);
2453 manager.set_diagnostic_capacity(0); assert_eq!(manager.diagnostics_store_for_test().capacity_for_test(), 0);
2455 }
2456
2457 #[test]
2460 fn clear_failed_spawns_empties_the_cache() {
2461 let mut manager = LspManager::new();
2462 assert_eq!(manager.clear_failed_spawns(), 0);
2463 manager.insert_failed_spawn_for_test();
2464 assert_eq!(manager.clear_failed_spawns(), 1);
2465 assert_eq!(manager.clear_failed_spawns(), 0);
2466 }
2467
2468 #[test]
2469 fn post_write_notification_does_not_start_a_cold_server() {
2470 let dir = tempfile::tempdir().unwrap();
2471 let file = dir.path().join("main.ts");
2472 fs::write(dir.path().join("package.json"), "{}").unwrap();
2473 fs::write(&file, "export const value = 1;\n").unwrap();
2474
2475 let mut manager = LspManager::new();
2476 manager
2477 .notify_file_changed_if_running(&file, "export const value = 1;\n", &Config::default())
2478 .unwrap();
2479 assert!(manager.clients.is_empty());
2480 }
2481}
2482
2483#[cfg(test)]
2484mod clear_diagnostics_tests {
2485 use std::path::PathBuf;
2486
2487 use super::LspManager;
2488 use crate::lsp::client::LspEvent;
2489 use crate::lsp::diagnostics::{DiagnosticSeverity, StoredDiagnostic};
2490 use crate::lsp::position::uri_for_path;
2491 use crate::lsp::registry::ServerKind;
2492 use crate::lsp::roots::ServerKey;
2493
2494 fn err_diag(file: &PathBuf) -> StoredDiagnostic {
2495 StoredDiagnostic {
2496 file: file.clone(),
2497 line: 1,
2498 column: 1,
2499 end_line: 1,
2500 end_column: 2,
2501 severity: DiagnosticSeverity::Error,
2502 message: "boom".into(),
2503 code: None,
2504 source: None,
2505 }
2506 }
2507
2508 #[test]
2513 fn clear_diagnostics_for_deleted_file_matches_canonical_key() {
2514 let dir = tempfile::tempdir().unwrap();
2515 let canonical_dir = std::fs::canonicalize(dir.path()).unwrap();
2517 let canonical_file = canonical_dir.join("gone.ts");
2518 std::fs::write(&canonical_file, "x").unwrap();
2521
2522 let mut manager = LspManager::new();
2523 let key = ServerKey {
2524 kind: ServerKind::TypeScript,
2525 root: canonical_dir.clone(),
2526 };
2527 manager.diagnostics_store_mut_for_test().publish(
2528 key,
2529 canonical_file.clone(),
2530 vec![err_diag(&canonical_file)],
2531 );
2532 assert_eq!(manager.warm_error_warning_counts(), (1, 0));
2533
2534 std::fs::remove_file(&canonical_file).unwrap();
2535
2536 let watcher_path = dir.path().join("gone.ts");
2539 let removed = manager.clear_diagnostics_for_file(&watcher_path);
2540
2541 assert!(removed, "expected the deleted file's diagnostic to clear");
2542 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
2543 }
2544
2545 #[cfg(windows)]
2546 #[test]
2547 fn clear_diagnostics_for_deleted_file_matches_normalized_publish_key() {
2548 let dir = tempfile::tempdir().unwrap();
2549 let file = dir.path().join("normalized-gone.ts");
2550 std::fs::write(&file, "x").unwrap();
2551 let normalized_file = crate::inspect::job::canonicalize_normalized(&file);
2552
2553 let mut manager = LspManager::new();
2554 let key = ServerKey {
2555 kind: ServerKind::TypeScript,
2556 root: normalized_file.parent().unwrap().to_path_buf(),
2557 };
2558 manager.diagnostics_store_mut_for_test().publish(
2559 key,
2560 normalized_file.clone(),
2561 vec![err_diag(&normalized_file)],
2562 );
2563 std::fs::remove_file(&file).unwrap();
2564
2565 assert!(manager.clear_diagnostics_for_file(&file));
2566 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
2567 }
2568
2569 #[cfg(windows)]
2570 #[test]
2571 fn stale_diagnostics_for_deleted_file_matches_normalized_publish_key() {
2572 let dir = tempfile::tempdir().unwrap();
2573 let file = dir.path().join("normalized-stale.ts");
2574 std::fs::write(&file, "x").unwrap();
2575 let normalized_file = crate::inspect::job::canonicalize_normalized(&file);
2576
2577 let mut manager = LspManager::new();
2578 let key = ServerKey {
2579 kind: ServerKind::TypeScript,
2580 root: normalized_file.parent().unwrap().to_path_buf(),
2581 };
2582 manager.diagnostics_store_mut_for_test().publish(
2583 key,
2584 normalized_file.clone(),
2585 vec![err_diag(&normalized_file)],
2586 );
2587 std::fs::remove_file(&file).unwrap();
2588
2589 let result = manager.mark_diagnostics_stale_for_file(&file);
2590 assert!(result.had_entries);
2591 assert!(result.changed);
2592 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
2593 }
2594
2595 #[test]
2596 fn clear_diagnostics_for_unknown_file_is_noop() {
2597 let mut manager = LspManager::new();
2598 assert!(!manager.clear_diagnostics_for_file(&PathBuf::from("/nope/missing.ts")));
2599 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
2600 }
2601
2602 #[test]
2603 fn drain_events_reports_publish_diagnostics_updates() {
2604 let dir = tempfile::tempdir().unwrap();
2605 let root = std::fs::canonicalize(dir.path()).unwrap();
2606 let file = root.join("main.ts");
2607 std::fs::write(&file, "const x: number = 'nope';").unwrap();
2608
2609 let mut manager = LspManager::new();
2610 let diagnostic = lsp_types::Diagnostic {
2611 range: lsp_types::Range {
2612 start: lsp_types::Position {
2613 line: 0,
2614 character: 0,
2615 },
2616 end: lsp_types::Position {
2617 line: 0,
2618 character: 1,
2619 },
2620 },
2621 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
2622 code: None,
2623 code_description: None,
2624 source: Some("test".into()),
2625 message: "boom".into(),
2626 related_information: None,
2627 tags: None,
2628 data: None,
2629 };
2630 let params = serde_json::to_value(lsp_types::PublishDiagnosticsParams {
2631 uri: uri_for_path(&file).unwrap(),
2632 diagnostics: vec![diagnostic],
2633 version: Some(1),
2634 })
2635 .unwrap();
2636 manager
2637 .event_tx
2638 .send(LspEvent::Notification {
2639 server_kind: ServerKind::TypeScript,
2640 root,
2641 method: "textDocument/publishDiagnostics".into(),
2642 params: Some(params),
2643 })
2644 .unwrap();
2645
2646 let drained = manager.drain_events();
2647
2648 assert!(drained.diagnostics_changed);
2649 assert_eq!(drained.events.len(), 1);
2650 assert_eq!(manager.warm_error_warning_counts(), (1, 0));
2651 }
2652}