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(def: &ServerDef, file_path: &Path) -> Option<ServerKey> {
34 def.workspace_root_for_file(file_path)
35 .map(|root| ServerKey {
36 kind: def.kind.clone(),
37 root,
38 })
39}
40
41#[derive(Debug, Clone)]
46pub enum ServerAttemptResult {
47 Ok { server_key: ServerKey },
49 NoRootMarker { looked_for: Vec<String> },
52 BinaryNotInstalled { binary: String },
55 SpawnFailed { binary: String, reason: String },
57}
58
59#[derive(Debug, Clone)]
61pub struct ServerAttempt {
62 pub server_id: String,
64 pub server_name: String,
66 pub result: ServerAttemptResult,
67}
68
69#[derive(Debug, Clone, Default)]
75pub struct EnsureServerOutcomes {
76 pub successful: Vec<ServerKey>,
78 pub attempts: Vec<ServerAttempt>,
81}
82
83impl EnsureServerOutcomes {
84 pub fn no_server_registered(&self) -> bool {
86 self.attempts.is_empty()
87 }
88
89 pub fn only_inapplicable_root_markers(&self) -> bool {
97 self.successful.is_empty()
98 && !self.attempts.is_empty()
99 && self
100 .attempts
101 .iter()
102 .all(|attempt| matches!(attempt.result, ServerAttemptResult::NoRootMarker { .. }))
103 }
104}
105
106#[derive(Debug, Clone, Default)]
116pub struct PostEditWaitOutcome {
117 pub diagnostics: Vec<StoredDiagnostic>,
121 pub pending_servers: Vec<ServerKey>,
125 pub exited_servers: Vec<ServerKey>,
129}
130
131#[derive(Debug, Clone, Copy, Default)]
133pub struct PreEditSnapshot {
134 pub epoch: u64,
135 pub document_version_at_capture: Option<i32>,
136}
137
138#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
139pub struct StaleDiagnosticsMark {
140 pub had_entries: bool,
141 pub changed: bool,
142}
143
144pub fn post_edit_entry_is_fresh(
145 entry: &DiagnosticEntry,
146 target_version: i32,
147 pre: PreEditSnapshot,
148) -> bool {
149 if entry.stale || entry.epoch <= pre.epoch {
150 return false;
151 }
152
153 match entry.version {
154 Some(version) => version >= target_version,
155 None => false,
160 }
161}
162
163impl PostEditWaitOutcome {
164 pub fn complete(&self) -> bool {
167 self.pending_servers.is_empty() && self.exited_servers.is_empty()
168 }
169}
170
171#[derive(Debug, Clone)]
173pub enum PullFileOutcome {
174 Full { diagnostic_count: usize },
176 Unchanged,
178 PartialNotSupported,
181 PullNotSupported,
184 RequestFailed { reason: String },
186}
187
188#[derive(Debug, Clone, Default)]
190pub struct EnsureFileOpenResult {
191 pub server_keys: Vec<ServerKey>,
192 pub newly_opened: Vec<ServerKey>,
194}
195
196impl EnsureFileOpenResult {
197 pub fn is_empty(&self) -> bool {
198 self.server_keys.is_empty()
199 }
200}
201
202#[derive(Debug, Clone)]
204pub struct PullFileResult {
205 pub server_key: ServerKey,
206 pub outcome: PullFileOutcome,
207}
208
209pub(crate) struct TrackedPullFileResult {
210 pub results: Vec<PullFileResult>,
211 pub newly_opened: Vec<ServerKey>,
212}
213
214#[derive(Debug, Clone)]
216pub struct PullWorkspaceResult {
217 pub server_key: ServerKey,
218 pub files_reported: Vec<PathBuf>,
222 pub complete: bool,
224 pub cancelled: bool,
226 pub supports_workspace: bool,
230}
231
232pub struct DrainedLspEvents {
233 pub events: Vec<LspEvent>,
234 pub diagnostics_changed: bool,
235 pub has_more: bool,
236}
237
238impl IntoIterator for DrainedLspEvents {
239 type Item = LspEvent;
240 type IntoIter = std::vec::IntoIter<LspEvent>;
241
242 fn into_iter(self) -> Self::IntoIter {
243 self.events.into_iter()
244 }
245}
246
247pub struct LspManager {
248 clients: HashMap<ServerKey, LspClient>,
250 server_binaries: HashMap<ServerKey, String>,
254 documents: HashMap<ServerKey, DocumentStore>,
256 diagnostics: DiagnosticsStore,
258 event_tx: Sender<LspEvent>,
260 event_rx: Receiver<LspEvent>,
261 binary_overrides: HashMap<ServerKind, PathBuf>,
263 extra_env: HashMap<String, String>,
267 failed_spawns: HashMap<ServerKey, ServerAttemptResult>,
282 watched_file_skip_logged: HashSet<ServerKey>,
285 #[cfg(windows)]
288 last_watched_file_notification_trace: String,
289 child_registry: LspChildRegistry,
293}
294
295impl LspManager {
296 pub fn new() -> Self {
297 let (event_tx, event_rx) = unbounded();
298 Self {
299 clients: HashMap::new(),
300 server_binaries: HashMap::new(),
301 documents: HashMap::new(),
302 diagnostics: DiagnosticsStore::new(),
303 event_tx,
304 event_rx,
305 binary_overrides: HashMap::new(),
306 extra_env: HashMap::new(),
307 failed_spawns: HashMap::new(),
308 watched_file_skip_logged: HashSet::new(),
309 #[cfg(windows)]
310 last_watched_file_notification_trace: "no watched-file notification attempted"
311 .to_string(),
312 child_registry: LspChildRegistry::new(),
313 }
314 }
315
316 pub fn set_child_registry(&mut self, registry: LspChildRegistry) {
318 self.child_registry = registry;
319 }
320
321 pub fn set_extra_env(&mut self, key: &str, value: &str) {
325 self.extra_env.insert(key.to_string(), value.to_string());
326 }
327
328 pub fn server_count(&self) -> usize {
330 self.clients.len()
331 }
332
333 pub fn estimated_memory(&self) -> crate::memory::MemoryEstimate {
337 let mut bytes = 0u64;
338 let mut document_count = 0u64;
339 for documents in self.documents.values() {
340 let estimate = documents.estimated_memory();
341 bytes = bytes.saturating_add(estimate.estimated_bytes.unwrap_or(0));
342 document_count = document_count
343 .saturating_add(estimate.counts.get("documents").copied().unwrap_or(0));
344 }
345 let diagnostics = self.diagnostics.estimated_memory();
346 bytes = bytes.saturating_add(diagnostics.estimated_bytes.unwrap_or(0));
347 crate::memory::MemoryEstimate::estimated(bytes)
348 .count("servers", self.clients.len())
349 .count("document_stores", self.documents.len())
350 .count_u64("documents", document_count)
351 .count_u64(
352 "diagnostic_entries",
353 diagnostics
354 .counts
355 .get("diagnostic_entries")
356 .copied()
357 .unwrap_or(0),
358 )
359 .count_u64(
360 "diagnostics",
361 diagnostics.counts.get("diagnostics").copied().unwrap_or(0),
362 )
363 }
364
365 pub fn set_diagnostic_capacity(&mut self, capacity: usize) {
369 self.diagnostics.set_capacity(capacity);
370 }
371
372 pub fn override_binary(&mut self, kind: ServerKind, binary_path: PathBuf) {
374 self.binary_overrides.insert(kind, binary_path);
375 }
376
377 pub fn ensure_server_for_file(&mut self, file_path: &Path, config: &Config) -> Vec<ServerKey> {
384 self.ensure_server_for_file_detailed(file_path, config)
385 .successful
386 }
387
388 fn running_server_keys_for_file(&self, file_path: &Path, config: &Config) -> Vec<ServerKey> {
389 servers_for_file(file_path, config)
390 .into_iter()
391 .filter_map(|def| server_key_for_definition(&def, file_path))
392 .filter(|key| self.clients.contains_key(key))
393 .collect()
394 }
395
396 pub fn ensure_server_for_file_detailed(
404 &mut self,
405 file_path: &Path,
406 config: &Config,
407 ) -> EnsureServerOutcomes {
408 let defs = servers_for_file(file_path, config);
409 let mut outcomes = EnsureServerOutcomes::default();
410
411 for def in defs {
412 let server_id = def.kind.id_str().to_string();
413 let server_name = def.name.to_string();
414
415 let Some(key) = server_key_for_definition(&def, file_path) else {
416 outcomes.attempts.push(ServerAttempt {
417 server_id,
418 server_name,
419 result: ServerAttemptResult::NoRootMarker {
420 looked_for: def.root_markers.iter().map(|s| s.to_string()).collect(),
421 },
422 });
423 continue;
424 };
425
426 if !self.clients.contains_key(&key) {
427 if let Some(cached) = self.failed_spawns.get(&key) {
434 outcomes.attempts.push(ServerAttempt {
435 server_id,
436 server_name,
437 result: cached.clone(),
438 });
439 continue;
440 }
441
442 match self.spawn_server(&def, &key.root, config) {
443 Ok(client) => {
444 self.clients.insert(key.clone(), client);
445 self.server_binaries.insert(key.clone(), def.binary.clone());
446 self.documents.entry(key.clone()).or_default();
447 }
448 Err(err) => {
449 slog_error!("failed to spawn {}: {}", def.name, err);
450 let result = classify_spawn_error(&def.binary, &err);
451 self.failed_spawns.insert(key.clone(), result.clone());
455 outcomes.attempts.push(ServerAttempt {
456 server_id,
457 server_name,
458 result,
459 });
460 continue;
461 }
462 }
463 }
464
465 outcomes.attempts.push(ServerAttempt {
466 server_id,
467 server_name,
468 result: ServerAttemptResult::Ok {
469 server_key: key.clone(),
470 },
471 });
472 outcomes.successful.push(key);
473 }
474
475 outcomes
476 }
477
478 pub fn ensure_server_for_file_default(&mut self, file_path: &Path) -> Vec<ServerKey> {
481 self.ensure_server_for_file(file_path, &Config::default())
482 }
483 pub fn ensure_file_open(
488 &mut self,
489 file_path: &Path,
490 config: &Config,
491 ) -> Result<EnsureFileOpenResult, LspError> {
492 let canonical_path = canonicalize_for_lsp(file_path)?;
493 let server_keys = self.ensure_server_for_file(&canonical_path, config);
494 if server_keys.is_empty() {
495 return Ok(EnsureFileOpenResult::default());
496 }
497
498 let uri = uri_for_path(&canonical_path)?;
499 let language_id = language_id_for_extension(
500 canonical_path
501 .extension()
502 .and_then(|ext| ext.to_str())
503 .unwrap_or_default(),
504 )
505 .to_string();
506 let needs_content = server_keys.iter().any(|key| {
507 !self
508 .documents
509 .get(key)
510 .is_some_and(|store| store.is_open(&canonical_path))
511 });
512 let initial_content = needs_content
513 .then(|| std::fs::read_to_string(&canonical_path).map_err(LspError::Io))
514 .transpose()?;
515 let mut newly_opened = Vec::new();
516
517 for key in &server_keys {
518 let already_open = self
519 .documents
520 .get(key)
521 .is_some_and(|store| store.is_open(&canonical_path));
522
523 if !already_open {
524 let content = initial_content
525 .as_ref()
526 .expect("content is loaded when any server needs didOpen");
527 let send_result = if let Some(client) = self.clients.get_mut(key) {
528 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
529 text_document: TextDocumentItem::new(
530 uri.clone(),
531 language_id.clone(),
532 0,
533 content.clone(),
534 ),
535 })
536 } else {
537 Ok(())
538 };
539 if let Err(err) = send_result {
540 let _ = self.close_file_for_servers(&canonical_path, &newly_opened);
541 return Err(err);
542 }
543 self.documents
544 .entry(key.clone())
545 .or_default()
546 .open(canonical_path.clone());
547 newly_opened.push(key.clone());
548 continue;
549 }
550
551 let drifted = self
561 .documents
562 .get(key)
563 .is_some_and(|store| store.is_stale_on_disk(&canonical_path));
564 if drifted {
565 let content = match std::fs::read_to_string(&canonical_path) {
566 Ok(content) => content,
567 Err(err) => {
568 let _ = self.close_file_for_servers(&canonical_path, &newly_opened);
569 return Err(LspError::Io(err));
570 }
571 };
572 let next_version = self
573 .documents
574 .get(key)
575 .and_then(|store| store.version(&canonical_path))
576 .map(|v| v + 1)
577 .unwrap_or(1);
578 let send_result = if let Some(client) = self.clients.get_mut(key) {
579 client.send_notification::<DidChangeTextDocument>(DidChangeTextDocumentParams {
580 text_document: VersionedTextDocumentIdentifier::new(
581 uri.clone(),
582 next_version,
583 ),
584 content_changes: vec![TextDocumentContentChangeEvent {
585 range: None,
586 range_length: None,
587 text: content,
588 }],
589 })
590 } else {
591 Ok(())
592 };
593 if let Err(err) = send_result {
594 let _ = self.close_file_for_servers(&canonical_path, &newly_opened);
595 return Err(err);
596 }
597 if let Some(store) = self.documents.get_mut(key) {
598 store.bump_version(&canonical_path);
599 }
600 }
601 }
602
603 Ok(EnsureFileOpenResult {
604 server_keys,
605 newly_opened,
606 })
607 }
608
609 pub fn ensure_file_open_default(
610 &mut self,
611 file_path: &Path,
612 ) -> Result<EnsureFileOpenResult, LspError> {
613 self.ensure_file_open(file_path, &Config::default())
614 }
615
616 pub fn notify_file_changed(
622 &mut self,
623 file_path: &Path,
624 content: &str,
625 config: &Config,
626 ) -> Result<(), LspError> {
627 self.notify_file_changed_versioned(file_path, content, config)
628 .map(|_| ())
629 }
630
631 pub fn notify_file_changed_versioned(
642 &mut self,
643 file_path: &Path,
644 content: &str,
645 config: &Config,
646 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
647 let canonical_path = canonicalize_for_lsp(file_path)?;
648 let server_keys = self.ensure_server_for_file(&canonical_path, config);
649 self.notify_file_changed_for_server_keys(canonical_path, content, server_keys)
650 }
651
652 pub fn notify_file_changed_if_running(
658 &mut self,
659 file_path: &Path,
660 content: &str,
661 config: &Config,
662 ) -> Result<(), LspError> {
663 let canonical_path = canonicalize_for_lsp(file_path)?;
664 let server_keys = self.running_server_keys_for_file(&canonical_path, config);
665 self.notify_file_changed_for_server_keys(canonical_path, content, server_keys)
666 .map(|_| ())
667 }
668
669 fn notify_file_changed_for_server_keys(
670 &mut self,
671 canonical_path: PathBuf,
672 content: &str,
673 server_keys: Vec<ServerKey>,
674 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
675 if server_keys.is_empty() {
676 return Ok(Vec::new());
677 }
678
679 let uri = uri_for_path(&canonical_path)?;
680 let language_id = language_id_for_extension(
681 canonical_path
682 .extension()
683 .and_then(|ext| ext.to_str())
684 .unwrap_or_default(),
685 )
686 .to_string();
687
688 let mut versions: Vec<(ServerKey, i32)> = Vec::with_capacity(server_keys.len());
689
690 for key in server_keys {
691 let current_version = self
692 .documents
693 .get(&key)
694 .and_then(|store| store.version(&canonical_path));
695
696 if let Some(version) = current_version {
697 let next_version = version + 1;
698 if let Some(client) = self.clients.get_mut(&key) {
699 client.send_notification::<DidChangeTextDocument>(
700 DidChangeTextDocumentParams {
701 text_document: VersionedTextDocumentIdentifier::new(
702 uri.clone(),
703 next_version,
704 ),
705 content_changes: vec![TextDocumentContentChangeEvent {
706 range: None,
707 range_length: None,
708 text: content.to_string(),
709 }],
710 },
711 )?;
712 }
713 if let Some(store) = self.documents.get_mut(&key) {
714 store.bump_version(&canonical_path);
715 }
716 versions.push((key, next_version));
717 continue;
718 }
719
720 if let Some(client) = self.clients.get_mut(&key) {
721 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
722 text_document: TextDocumentItem::new(
723 uri.clone(),
724 language_id.clone(),
725 0,
726 content.to_string(),
727 ),
728 })?;
729 }
730 self.documents
731 .entry(key.clone())
732 .or_default()
733 .open(canonical_path.clone());
734 versions.push((key, 0));
737 }
738
739 Ok(versions)
740 }
741
742 pub fn notify_file_changed_default(
743 &mut self,
744 file_path: &Path,
745 content: &str,
746 ) -> Result<(), LspError> {
747 self.notify_file_changed(file_path, content, &Config::default())
748 }
749
750 pub fn notify_files_watched_changed(
756 &mut self,
757 paths: &[(PathBuf, FileChangeType)],
758 _config: &Config,
759 ) -> Result<(), LspError> {
760 #[cfg(windows)]
761 let mut trace = vec![format!(
762 "input_paths={paths:?}; active_keys={:?}",
763 self.clients.keys().collect::<Vec<_>>()
764 )];
765
766 if paths.is_empty() {
767 #[cfg(windows)]
768 {
769 trace.push("outcome=no-input-paths".to_string());
770 self.last_watched_file_notification_trace = trace.join("\n");
771 }
772 return Ok(());
773 }
774
775 let mut canonical_events = Vec::with_capacity(paths.len());
776 for (path, typ) in paths {
777 let canonical_path = resolve_for_lsp_uri(path);
778 canonical_events.push((canonical_path, *typ));
779 }
780 #[cfg(windows)]
781 trace.push(format!("resolved_events={canonical_events:?}"));
782
783 let keys: Vec<ServerKey> = self.clients.keys().cloned().collect();
784 #[cfg(windows)]
785 if keys.is_empty() {
786 trace.push("outcome=no-active-client".to_string());
787 }
788 for key in keys {
789 let mut changes = Vec::new();
790 for (path, typ) in &canonical_events {
791 if !path.starts_with(&key.root) {
792 continue;
793 }
794 changes.push(FileEvent::new(uri_for_path(path)?, *typ));
795 }
796
797 if changes.is_empty() {
798 #[cfg(windows)]
799 trace.push(format!("key={key:?}; outcome=outside-root"));
800 continue;
801 }
802
803 if let Some(client) = self.clients.get_mut(&key) {
804 let supports_static_watched_files = client.supports_watched_files();
810 let has_dynamic_registration = client.has_watched_file_registration();
811 if !(supports_static_watched_files || has_dynamic_registration) {
812 #[cfg(windows)]
813 trace.push(format!(
814 "key={key:?}; changes={changes:?}; outcome=unsupported; static={supports_static_watched_files}; dynamic={has_dynamic_registration}"
815 ));
816 if self.watched_file_skip_logged.insert(key.clone()) {
817 log::debug!(
818 "skipping didChangeWatchedFiles for {:?} (not supported or registered)",
819 key
820 );
821 }
822 continue;
823 }
824 #[cfg(windows)]
825 trace.push(format!("key={key:?}; changes={changes:?}; action=send"));
826 let send_result = client.send_notification::<DidChangeWatchedFiles>(
827 DidChangeWatchedFilesParams { changes },
828 );
829 #[cfg(windows)]
830 trace.push(format!(
831 "key={key:?}; outcome={}",
832 if send_result.is_ok() {
833 "sent"
834 } else {
835 "send-error"
836 }
837 ));
838 if let Err(error) = send_result {
839 #[cfg(windows)]
840 {
841 self.last_watched_file_notification_trace = trace.join("\n");
842 }
843 return Err(error);
844 }
845 }
846 }
847
848 #[cfg(windows)]
849 {
850 self.last_watched_file_notification_trace = trace.join("\n");
851 }
852 Ok(())
853 }
854
855 pub fn notify_file_closed(&mut self, file_path: &Path) -> Result<(), LspError> {
857 let canonical_path = canonicalize_for_lsp(file_path)?;
858 let keys = self
859 .documents
860 .iter()
861 .filter(|(_, store)| store.is_open(&canonical_path))
862 .map(|(key, _)| key.clone())
863 .collect::<Vec<_>>();
864 self.close_file_for_servers(&canonical_path, &keys)
865 }
866
867 pub(crate) fn close_file_for_servers(
872 &mut self,
873 file_path: &Path,
874 server_keys: &[ServerKey],
875 ) -> Result<(), LspError> {
876 let canonical_path = canonicalize_for_lsp(file_path)?;
877 let uri = uri_for_path(&canonical_path)?;
878 let mut first_error = None;
879
880 for key in server_keys {
881 let was_open = self
882 .documents
883 .get(key)
884 .is_some_and(|store| store.is_open(&canonical_path));
885 if !was_open {
886 continue;
887 }
888
889 if let Some(client) = self.clients.get_mut(key) {
890 if let Err(err) =
891 client.send_notification::<DidCloseTextDocument>(DidCloseTextDocumentParams {
892 text_document: TextDocumentIdentifier::new(uri.clone()),
893 })
894 {
895 if first_error.is_none() {
896 first_error = Some(err);
897 }
898 }
899 }
900
901 if let Some(store) = self.documents.get_mut(key) {
902 store.close(&canonical_path);
903 }
904 self.diagnostics.clear_for_server_file(key, &canonical_path);
905 }
906
907 match first_error {
908 Some(err) => Err(err),
909 None => Ok(()),
910 }
911 }
912
913 pub fn client_for_file(&self, file_path: &Path, config: &Config) -> Option<&LspClient> {
915 let key = self.server_key_for_file(file_path, config)?;
916 self.clients.get(&key)
917 }
918
919 pub fn client_for_file_default(&self, file_path: &Path) -> Option<&LspClient> {
920 self.client_for_file(file_path, &Config::default())
921 }
922
923 pub fn client_for_file_mut(
925 &mut self,
926 file_path: &Path,
927 config: &Config,
928 ) -> Option<&mut LspClient> {
929 let key = self.server_key_for_file(file_path, config)?;
930 self.clients.get_mut(&key)
931 }
932
933 pub fn client_for_file_mut_default(&mut self, file_path: &Path) -> Option<&mut LspClient> {
934 self.client_for_file_mut(file_path, &Config::default())
935 }
936
937 pub fn active_client_count(&self) -> usize {
939 self.clients.len()
940 }
941
942 pub fn drain_events(&mut self) -> DrainedLspEvents {
944 self.drain_events_bounded(usize::MAX)
945 }
946
947 pub fn has_pending_events(&self) -> bool {
950 !self.event_rx.is_empty()
951 }
952
953 pub fn drain_events_bounded(&mut self, max_events: usize) -> DrainedLspEvents {
954 let mut events = Vec::new();
955 let mut diagnostics_changed = false;
956 while events.len() < max_events {
957 let Ok(event) = self.event_rx.try_recv() else {
958 break;
959 };
960 if self.handle_event(&event).is_some() {
961 diagnostics_changed = true;
962 }
963 events.push(event);
964 }
965 let has_more = events.len() >= max_events && !self.event_rx.is_empty();
966 DrainedLspEvents {
967 events,
968 diagnostics_changed,
969 has_more,
970 }
971 }
972
973 pub fn wait_for_diagnostics(
975 &mut self,
976 file_path: &Path,
977 config: &Config,
978 timeout: std::time::Duration,
979 ) -> Vec<StoredDiagnostic> {
980 let deadline = std::time::Instant::now() + timeout;
981 self.wait_for_file_diagnostics(file_path, config, deadline)
982 }
983
984 pub fn wait_for_diagnostics_default(
985 &mut self,
986 file_path: &Path,
987 timeout: std::time::Duration,
988 ) -> Vec<StoredDiagnostic> {
989 self.wait_for_diagnostics(file_path, &Config::default(), timeout)
990 }
991
992 #[doc(hidden)]
997 pub fn diagnostics_store_for_test(&self) -> &DiagnosticsStore {
998 &self.diagnostics
999 }
1000
1001 #[doc(hidden)]
1002 pub fn diagnostics_store_mut_for_test(&mut self) -> &mut DiagnosticsStore {
1003 &mut self.diagnostics
1004 }
1005
1006 #[doc(hidden)]
1007 pub fn enqueue_event_for_test(&self, event: LspEvent) {
1008 self.event_tx
1009 .send(event)
1010 .expect("LSP event receiver should remain connected");
1011 }
1012
1013 #[doc(hidden)]
1014 pub fn pending_event_count_for_test(&self) -> usize {
1015 self.event_rx.len()
1016 }
1017
1018 #[doc(hidden)]
1019 pub fn document_is_open_for_test(&self, file_path: &Path) -> bool {
1020 canonicalize_for_lsp(file_path).is_ok_and(|canonical_path| {
1021 self.documents
1022 .values()
1023 .any(|store| store.is_open(&canonical_path))
1024 })
1025 }
1026
1027 pub fn warm_error_warning_counts(&self) -> (usize, usize) {
1031 self.diagnostics.error_warning_counts()
1032 }
1033
1034 pub fn warm_error_warning_counts_with_provisional(&self) -> ((usize, usize), bool) {
1035 self.diagnostics.error_warning_counts_with_provisional()
1036 }
1037
1038 pub fn diagnostics_generation(&self) -> u64 {
1039 self.diagnostics.generation()
1040 }
1041
1042 pub fn filtered_error_warning_counts(
1047 &self,
1048 keep: impl FnMut(&std::path::Path) -> bool,
1049 ) -> (usize, usize) {
1050 self.diagnostics.filtered_error_warning_counts(keep)
1051 }
1052
1053 pub fn filtered_error_warning_counts_with_provisional(
1057 &self,
1058 keep: impl FnMut(&std::path::Path) -> bool,
1059 ) -> ((usize, usize), bool) {
1060 self.diagnostics
1061 .filtered_error_warning_counts_with_provisional(keep)
1062 }
1063
1064 pub fn provisional_server_keys(&self) -> Vec<ServerKey> {
1068 self.clients
1069 .iter()
1070 .filter(|(_, client)| client.diagnostics_are_provisional())
1071 .map(|(key, _)| key.clone())
1072 .collect()
1073 }
1074
1075 pub fn snapshot_diagnostic_epochs(&self, file_path: &Path) -> HashMap<ServerKey, u64> {
1080 let lookup_path = normalize_lookup_path(file_path);
1081 self.diagnostics
1082 .entries_for_file(&lookup_path)
1083 .into_iter()
1084 .map(|(key, entry)| (key.clone(), entry.epoch))
1085 .collect()
1086 }
1087
1088 pub fn snapshot_pre_edit_state(&self, file_path: &Path) -> HashMap<ServerKey, PreEditSnapshot> {
1091 let lookup_path = normalize_lookup_path(file_path);
1092 let mut snapshots: HashMap<ServerKey, PreEditSnapshot> = self
1093 .diagnostics
1094 .entries_for_file(&lookup_path)
1095 .into_iter()
1096 .map(|(key, entry)| {
1097 (
1098 key.clone(),
1099 PreEditSnapshot {
1100 epoch: entry.epoch,
1101 document_version_at_capture: None,
1102 },
1103 )
1104 })
1105 .collect();
1106
1107 for (key, store) in &self.documents {
1108 if let Some(version) = store.version(&lookup_path) {
1109 snapshots
1110 .entry(key.clone())
1111 .or_default()
1112 .document_version_at_capture = Some(version);
1113 }
1114 }
1115
1116 snapshots
1117 }
1118
1119 pub fn diagnostic_entry_is_fresh_for_document(
1127 &self,
1128 file_path: &Path,
1129 server_key: &ServerKey,
1130 pre: PreEditSnapshot,
1131 ) -> bool {
1132 let lookup_path = normalize_lookup_path(file_path);
1133 let Some(entry) = self
1134 .diagnostics
1135 .entries_for_file(&lookup_path)
1136 .into_iter()
1137 .find_map(|(key, entry)| if key == server_key { Some(entry) } else { None })
1138 else {
1139 return false;
1140 };
1141
1142 if entry.stale {
1143 return false;
1144 }
1145
1146 let target_version = self
1147 .documents
1148 .get(server_key)
1149 .and_then(|store| store.version(&lookup_path))
1150 .or(pre.document_version_at_capture)
1151 .unwrap_or(0);
1152
1153 matches!(entry.version, Some(version) if version >= target_version)
1154 }
1155
1156 pub fn wait_for_post_edit_diagnostics(
1179 &mut self,
1180 file_path: &Path,
1181 _config: &Config,
1185 expected_versions: &[(ServerKey, i32)],
1186 pre_snapshot: &HashMap<ServerKey, PreEditSnapshot>,
1187 timeout: std::time::Duration,
1188 ) -> PostEditWaitOutcome {
1189 let lookup_path = normalize_lookup_path(file_path);
1190 let deadline = std::time::Instant::now() + timeout;
1191
1192 let _ = self.drain_events_for_file(&lookup_path);
1197
1198 let mut fresh: HashMap<ServerKey, Vec<StoredDiagnostic>> = HashMap::new();
1199 let mut exited: Vec<ServerKey> = Vec::new();
1200
1201 loop {
1202 for (key, target_version) in expected_versions {
1210 if fresh.contains_key(key) || exited.contains(key) {
1211 continue;
1212 }
1213 if !self.clients.contains_key(key) {
1214 exited.push(key.clone());
1215 continue;
1216 }
1217 if let Some(entry) = self
1218 .diagnostics
1219 .entries_for_file(&lookup_path)
1220 .into_iter()
1221 .find_map(|(k, e)| if k == key { Some(e) } else { None })
1222 {
1223 let pre = pre_snapshot.get(key).copied().unwrap_or_default();
1224 let is_fresh = post_edit_entry_is_fresh(entry, *target_version, pre);
1225 if is_fresh {
1226 fresh.insert(key.clone(), entry.diagnostics.clone());
1227 }
1228 }
1229 }
1230
1231 if fresh.len() + exited.len() == expected_versions.len() {
1233 break;
1234 }
1235
1236 let now = std::time::Instant::now();
1237 if now >= deadline {
1238 break;
1239 }
1240
1241 let timeout = deadline.saturating_duration_since(now);
1242 match self.event_rx.recv_timeout(timeout) {
1243 Ok(event) => {
1244 self.handle_event(&event);
1245 }
1246 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
1247 }
1248 }
1249
1250 let pending: Vec<ServerKey> = expected_versions
1252 .iter()
1253 .filter(|(k, _)| !fresh.contains_key(k) && !exited.contains(k))
1254 .map(|(k, _)| k.clone())
1255 .collect();
1256
1257 let mut diagnostics: Vec<StoredDiagnostic> = fresh
1260 .into_iter()
1261 .flat_map(|(_, diags)| diags.into_iter())
1262 .collect();
1263 diagnostics.sort_by(|a, b| {
1264 a.file
1265 .cmp(&b.file)
1266 .then(a.line.cmp(&b.line))
1267 .then(a.column.cmp(&b.column))
1268 .then(a.message.cmp(&b.message))
1269 });
1270
1271 PostEditWaitOutcome {
1272 diagnostics,
1273 pending_servers: pending,
1274 exited_servers: exited,
1275 }
1276 }
1277
1278 pub fn wait_for_file_diagnostics(
1284 &mut self,
1285 file_path: &Path,
1286 config: &Config,
1287 deadline: std::time::Instant,
1288 ) -> Vec<StoredDiagnostic> {
1289 let lookup_path = normalize_lookup_path(file_path);
1290
1291 if self.server_key_for_file(&lookup_path, config).is_none() {
1292 return Vec::new();
1293 }
1294
1295 loop {
1296 if self.drain_events_for_file(&lookup_path) {
1297 break;
1298 }
1299
1300 let now = std::time::Instant::now();
1301 if now >= deadline {
1302 break;
1303 }
1304
1305 let timeout = deadline.saturating_duration_since(now);
1306 match self.event_rx.recv_timeout(timeout) {
1307 Ok(event) => {
1308 if matches!(
1309 self.handle_event(&event),
1310 Some(ref published_file) if published_file.as_path() == lookup_path.as_path()
1311 ) {
1312 break;
1313 }
1314 }
1315 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
1316 }
1317 }
1318
1319 self.get_diagnostics_for_file(&lookup_path)
1320 .into_iter()
1321 .cloned()
1322 .collect()
1323 }
1324
1325 pub const PULL_FILE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
1331
1332 pub fn pull_file_timeout() -> std::time::Duration {
1334 Self::PULL_FILE_TIMEOUT
1335 }
1336
1337 const PULL_WORKSPACE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
1341
1342 pub fn pull_file_diagnostics(
1353 &mut self,
1354 file_path: &Path,
1355 config: &Config,
1356 ) -> Result<Vec<PullFileResult>, LspError> {
1357 self.pull_file_diagnostics_tracked(file_path, config)
1358 .map(|tracked| tracked.results)
1359 }
1360
1361 pub(crate) fn pull_file_diagnostics_tracked(
1362 &mut self,
1363 file_path: &Path,
1364 config: &Config,
1365 ) -> Result<TrackedPullFileResult, LspError> {
1366 let canonical_path = canonicalize_for_lsp(file_path)?;
1367 let opened = self.ensure_file_open(&canonical_path, config)?;
1370 if opened.server_keys.is_empty() {
1371 return Ok(TrackedPullFileResult {
1372 results: Vec::new(),
1373 newly_opened: opened.newly_opened,
1374 });
1375 }
1376
1377 let uri = uri_for_path(&canonical_path)?;
1378 let mut results = Vec::with_capacity(opened.server_keys.len());
1379
1380 for key in opened.server_keys {
1381 let supports_pull = self
1382 .clients
1383 .get(&key)
1384 .and_then(|c| c.diagnostic_capabilities())
1385 .is_some_and(|caps| caps.pull_diagnostics);
1386
1387 if !supports_pull {
1388 results.push(PullFileResult {
1389 server_key: key.clone(),
1390 outcome: PullFileOutcome::PullNotSupported,
1391 });
1392 continue;
1393 }
1394
1395 let previous_result_id = self
1397 .diagnostics
1398 .entries_for_file(&canonical_path)
1399 .into_iter()
1400 .find(|(k, _)| **k == key)
1401 .and_then(|(_, entry)| entry.result_id.clone());
1402
1403 let identifier = self
1404 .clients
1405 .get(&key)
1406 .and_then(|c| c.diagnostic_capabilities())
1407 .and_then(|caps| caps.identifier.clone());
1408
1409 let params = AftDocumentDiagnosticParams {
1410 text_document: lsp_types::TextDocumentIdentifier { uri: uri.clone() },
1411 identifier,
1412 previous_result_id,
1413 work_done_progress_params: Default::default(),
1414 partial_result_params: Default::default(),
1415 };
1416
1417 let outcome = match self.send_pull_request(&key, params) {
1418 Ok(report) => {
1419 if matches!(
1420 &report,
1421 lsp_types::DocumentDiagnosticReportResult::Report(
1422 lsp_types::DocumentDiagnosticReport::Full(_)
1423 )
1424 ) {
1425 self.drain_events();
1430 }
1431 self.ingest_document_report(&key, &canonical_path, report)
1432 }
1433 Err(err) => {
1434 if let Some(result) = self.cache_post_initialize_exit(&key, &err) {
1435 PullFileOutcome::RequestFailed {
1436 reason: server_attempt_result_reason(&result),
1437 }
1438 } else if recoverable_pull_rejection(&err)
1439 && self.clients.get(&key).is_some_and(|client| {
1440 matches!(
1441 client.state(),
1442 ServerState::Ready | ServerState::Initializing
1443 )
1444 })
1445 {
1446 PullFileOutcome::RequestFailed {
1447 reason: format!("pull_rejected_push_fallback: {err}"),
1448 }
1449 } else {
1450 PullFileOutcome::RequestFailed {
1451 reason: err.to_string(),
1452 }
1453 }
1454 }
1455 };
1456
1457 results.push(PullFileResult {
1458 server_key: key,
1459 outcome,
1460 });
1461 }
1462
1463 Ok(TrackedPullFileResult {
1464 results,
1465 newly_opened: opened.newly_opened,
1466 })
1467 }
1468
1469 pub fn pull_workspace_diagnostics(
1474 &mut self,
1475 server_key: &ServerKey,
1476 timeout: Option<std::time::Duration>,
1477 ) -> Result<PullWorkspaceResult, LspError> {
1478 let timeout = timeout.unwrap_or(Self::PULL_WORKSPACE_TIMEOUT);
1479
1480 let supports_workspace = self
1481 .clients
1482 .get(server_key)
1483 .and_then(|c| c.diagnostic_capabilities())
1484 .is_some_and(|caps| caps.workspace_diagnostics);
1485
1486 if !supports_workspace {
1487 return Ok(PullWorkspaceResult {
1488 server_key: server_key.clone(),
1489 files_reported: Vec::new(),
1490 complete: false,
1491 cancelled: false,
1492 supports_workspace: false,
1493 });
1494 }
1495
1496 let identifier = self
1497 .clients
1498 .get(server_key)
1499 .and_then(|c| c.diagnostic_capabilities())
1500 .and_then(|caps| caps.identifier.clone());
1501
1502 let params = AftWorkspaceDiagnosticParams {
1503 identifier,
1504 previous_result_ids: Vec::new(),
1505 work_done_progress_params: Default::default(),
1506 partial_result_params: Default::default(),
1507 };
1508
1509 let result = match self
1510 .clients
1511 .get_mut(server_key)
1512 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?
1513 .send_request_with_timeout::<AftWorkspaceDiagnosticRequest>(params, timeout)
1514 {
1515 Ok(result) => result,
1516 Err(LspError::Timeout(_)) => {
1517 return Ok(PullWorkspaceResult {
1518 server_key: server_key.clone(),
1519 files_reported: Vec::new(),
1520 complete: false,
1521 cancelled: true,
1522 supports_workspace: true,
1523 });
1524 }
1525 Err(err) => {
1526 if let Some(result) = self.cache_post_initialize_exit(server_key, &err) {
1527 return Err(LspError::ServerNotReady(server_attempt_result_reason(
1528 &result,
1529 )));
1530 }
1531 return Err(err);
1532 }
1533 };
1534
1535 let (items, complete) = match result {
1539 lsp_types::WorkspaceDiagnosticReportResult::Report(report) => (report.items, true),
1540 lsp_types::WorkspaceDiagnosticReportResult::Partial(partial) => (partial.items, false),
1541 };
1542
1543 let mut files_reported = Vec::with_capacity(items.len());
1545 for item in items {
1546 match item {
1547 lsp_types::WorkspaceDocumentDiagnosticReport::Full(full) => {
1548 if let Some(file) = uri_to_path(&full.uri) {
1549 let stored = from_lsp_diagnostics(
1550 file.clone(),
1551 full.full_document_diagnostic_report.items.clone(),
1552 );
1553 self.diagnostics.publish_with_result_id(
1554 server_key.clone(),
1555 file.clone(),
1556 stored,
1557 full.full_document_diagnostic_report.result_id.clone(),
1558 );
1559 files_reported.push(file);
1560 }
1561 }
1562 lsp_types::WorkspaceDocumentDiagnosticReport::Unchanged(_unchanged) => {
1563 }
1566 }
1567 }
1568
1569 Ok(PullWorkspaceResult {
1570 server_key: server_key.clone(),
1571 files_reported,
1572 complete,
1573 cancelled: false,
1574 supports_workspace: true,
1575 })
1576 }
1577
1578 fn cache_post_initialize_exit(
1579 &mut self,
1580 key: &ServerKey,
1581 err: &LspError,
1582 ) -> Option<ServerAttemptResult> {
1583 let binary = self
1584 .server_binaries
1585 .get(key)
1586 .cloned()
1587 .unwrap_or_else(|| key.kind.id_str().to_string());
1588 let (status, stderr_tail) = {
1589 let client = self.clients.get_mut(key)?;
1590 let mut status = client.child_exit_status();
1591 for _ in 0..10 {
1592 if status.is_some() {
1593 break;
1594 }
1595 std::thread::sleep(std::time::Duration::from_millis(10));
1596 status = client.child_exit_status();
1597 }
1598 let status = status?;
1599 wait_for_stderr_tail(client);
1600 (status, client.stderr_tail())
1601 };
1602 let reason = format_post_initialize_exit_reason(&binary, status, &stderr_tail, err);
1603 let result = ServerAttemptResult::SpawnFailed { binary, reason };
1604 self.clients.remove(key);
1605 self.server_binaries.remove(key);
1606 self.documents.remove(key);
1607 self.diagnostics.clear_for_server(key);
1608 self.failed_spawns.insert(key.clone(), result.clone());
1609 Some(result)
1610 }
1611
1612 fn send_pull_request(
1614 &mut self,
1615 key: &ServerKey,
1616 params: AftDocumentDiagnosticParams,
1617 ) -> Result<lsp_types::DocumentDiagnosticReportResult, LspError> {
1618 let client = self
1619 .clients
1620 .get_mut(key)
1621 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?;
1622 client.send_request_with_timeout::<AftDocumentDiagnosticRequest>(
1626 params,
1627 Self::PULL_FILE_TIMEOUT,
1628 )
1629 }
1630
1631 fn ingest_document_report(
1634 &mut self,
1635 key: &ServerKey,
1636 canonical_path: &Path,
1637 result: lsp_types::DocumentDiagnosticReportResult,
1638 ) -> PullFileOutcome {
1639 let report = match result {
1640 lsp_types::DocumentDiagnosticReportResult::Report(report) => report,
1641 lsp_types::DocumentDiagnosticReportResult::Partial(_) => {
1642 return PullFileOutcome::PartialNotSupported;
1646 }
1647 };
1648
1649 match report {
1650 lsp_types::DocumentDiagnosticReport::Full(full) => {
1651 let result_id = full.full_document_diagnostic_report.result_id.clone();
1652 let stored = from_lsp_diagnostics(
1653 canonical_path.to_path_buf(),
1654 full.full_document_diagnostic_report.items.clone(),
1655 );
1656 let count = stored.len();
1657 let provisional = self
1658 .clients
1659 .get(key)
1660 .is_some_and(|client| client.diagnostics_are_provisional());
1661 self.diagnostics.publish_full_with_provisional(
1662 key.clone(),
1663 canonical_path.to_path_buf(),
1664 stored,
1665 result_id,
1666 None,
1667 provisional,
1668 );
1669 PullFileOutcome::Full {
1670 diagnostic_count: count,
1671 }
1672 }
1673 lsp_types::DocumentDiagnosticReport::Unchanged(_unchanged) => {
1674 if self
1681 .diagnostics
1682 .has_report_for_server_file(key, canonical_path)
1683 {
1684 self.diagnostics
1685 .mark_fresh_for_server_file(key, canonical_path);
1686 let authoritative = self
1687 .clients
1688 .get(key)
1689 .map_or(true, |client| !client.diagnostics_are_provisional());
1690 if authoritative {
1691 self.diagnostics
1692 .clear_provisional_for_server_file(key, canonical_path);
1693 }
1694 PullFileOutcome::Unchanged
1695 } else {
1696 PullFileOutcome::RequestFailed {
1697 reason: "no_cache_for_unchanged".to_string(),
1698 }
1699 }
1700 }
1701 }
1702 }
1703
1704 pub fn shutdown_all(&mut self) {
1706 for (key, mut client) in self.clients.drain() {
1707 if let Err(err) = client.shutdown() {
1708 slog_error!("error shutting down {:?}: {}", key, err);
1709 }
1710 }
1711 self.server_binaries.clear();
1712 self.documents.clear();
1713 self.diagnostics = DiagnosticsStore::new();
1714 }
1715
1716 pub fn has_active_servers(&self) -> bool {
1718 self.clients
1719 .values()
1720 .any(|client| client.state() == ServerState::Ready)
1721 }
1722
1723 pub fn active_server_keys(&self) -> Vec<ServerKey> {
1726 self.clients.keys().cloned().collect()
1727 }
1728
1729 #[cfg(windows)]
1733 #[doc(hidden)]
1734 pub fn watched_file_notification_trace_for_test(&self) -> &str {
1735 &self.last_watched_file_notification_trace
1736 }
1737
1738 pub fn get_diagnostics_for_file(&self, file: &Path) -> Vec<&StoredDiagnostic> {
1739 let normalized = normalize_lookup_path(file);
1740 self.diagnostics.for_file(&normalized)
1741 }
1742
1743 pub fn get_diagnostics_for_file_with_provisional(
1744 &self,
1745 file: &Path,
1746 ) -> Vec<(&StoredDiagnostic, bool)> {
1747 let normalized = normalize_lookup_path(file);
1748 self.diagnostics.for_file_with_provisional(&normalized)
1749 }
1750
1751 pub fn clear_failed_spawns(&mut self) -> usize {
1772 let n = self.failed_spawns.len();
1773 self.failed_spawns.clear();
1774 n
1775 }
1776
1777 #[cfg(test)]
1778 pub(crate) fn insert_failed_spawn_for_test(&mut self) {
1779 let key = ServerKey {
1780 kind: crate::lsp::registry::ServerKind::Rust,
1781 root: std::path::PathBuf::from("/tmp/test-root"),
1782 };
1783 self.failed_spawns.insert(
1784 key,
1785 ServerAttemptResult::SpawnFailed {
1786 binary: "rust-analyzer".to_string(),
1787 reason: "test".to_string(),
1788 },
1789 );
1790 }
1791
1792 pub fn clear_diagnostics_for_file(&mut self, file: &Path) -> bool {
1793 diagnostic_path_candidates(file)
1794 .into_iter()
1795 .fold(false, |removed, candidate| {
1796 removed | self.diagnostics.clear_for_file(&candidate)
1797 })
1798 }
1799
1800 pub fn mark_diagnostics_stale_for_file(&mut self, file: &Path) -> StaleDiagnosticsMark {
1804 let mut result = StaleDiagnosticsMark::default();
1805 for candidate in diagnostic_path_candidates(file) {
1806 let (had_entries, changed) = self.diagnostics.mark_stale_for_file(&candidate);
1807 result.had_entries |= had_entries;
1808 result.changed |= changed;
1809 }
1810 result
1811 }
1812
1813 pub fn get_diagnostics_for_directory(&self, dir: &Path) -> Vec<&StoredDiagnostic> {
1814 let normalized = normalize_lookup_path(dir);
1815 self.diagnostics.for_directory(&normalized)
1816 }
1817
1818 pub fn get_diagnostics_for_directory_with_provisional(
1819 &self,
1820 dir: &Path,
1821 ) -> Vec<(&StoredDiagnostic, bool)> {
1822 let normalized = normalize_lookup_path(dir);
1823 self.diagnostics.for_directory_with_provisional(&normalized)
1824 }
1825
1826 pub fn get_all_diagnostics(&self) -> Vec<&StoredDiagnostic> {
1827 self.diagnostics.all()
1828 }
1829
1830 pub fn get_all_diagnostics_with_provisional(&self) -> Vec<(&StoredDiagnostic, bool)> {
1831 self.diagnostics.all_with_provisional()
1832 }
1833
1834 pub fn has_any_diagnostic_reports(&self) -> bool {
1839 self.diagnostics.has_any_fresh_report()
1840 }
1841
1842 pub fn has_diagnostic_report_for_file(&self, file: &Path) -> bool {
1846 let normalized = normalize_lookup_path(file);
1847 self.diagnostics.has_any_fresh_report_for_file(&normalized)
1848 }
1849
1850 pub fn has_diagnostic_report_for_server_file(&self, server: &ServerKey, file: &Path) -> bool {
1854 let normalized = normalize_lookup_path(file);
1855 self.diagnostics
1856 .has_fresh_report_for_server_file(server, &normalized)
1857 }
1858
1859 fn drain_events_for_file(&mut self, file_path: &Path) -> bool {
1860 let mut saw_file_diagnostics = false;
1861 while let Ok(event) = self.event_rx.try_recv() {
1862 if matches!(
1863 self.handle_event(&event),
1864 Some(ref published_file) if published_file.as_path() == file_path
1865 ) {
1866 saw_file_diagnostics = true;
1867 }
1868 }
1869 saw_file_diagnostics
1870 }
1871
1872 fn handle_event(&mut self, event: &LspEvent) -> Option<PathBuf> {
1873 match event {
1874 LspEvent::Notification {
1875 server_kind,
1876 root,
1877 method,
1878 params: Some(params),
1879 } if method == "textDocument/publishDiagnostics" => {
1880 self.handle_publish_diagnostics(server_kind.clone(), root.clone(), params)
1881 }
1882 LspEvent::Notification {
1883 server_kind,
1884 root,
1885 method,
1886 params: Some(params),
1887 } if method == "experimental/serverStatus" => {
1888 self.handle_server_status(server_kind.clone(), root.clone(), params);
1889 None
1890 }
1891 LspEvent::ServerExited { server_kind, root } => {
1892 let key = ServerKey {
1893 kind: server_kind.clone(),
1894 root: root.clone(),
1895 };
1896 self.clients.remove(&key);
1897 self.server_binaries.remove(&key);
1898 self.documents.remove(&key);
1899 self.diagnostics.clear_for_server(&key);
1900 None
1901 }
1902 _ => None,
1903 }
1904 }
1905
1906 fn handle_publish_diagnostics(
1907 &mut self,
1908 server: ServerKind,
1909 root: PathBuf,
1910 params: &serde_json::Value,
1911 ) -> Option<PathBuf> {
1912 if let Ok(publish_params) =
1913 serde_json::from_value::<lsp_types::PublishDiagnosticsParams>(params.clone())
1914 {
1915 let file = uri_to_path(&publish_params.uri)?;
1916 let stored = from_lsp_diagnostics(file.clone(), publish_params.diagnostics);
1917 let key = ServerKey { kind: server, root };
1923 let provisional = self
1924 .clients
1925 .get(&key)
1926 .is_some_and(|client| client.diagnostics_are_provisional());
1927 self.diagnostics.publish_full_with_provisional(
1928 key,
1929 file.clone(),
1930 stored,
1931 None,
1932 publish_params.version,
1933 provisional,
1934 );
1935 return Some(file);
1936 }
1937 None
1938 }
1939
1940 fn handle_server_status(
1941 &mut self,
1942 server: ServerKind,
1943 root: PathBuf,
1944 params: &serde_json::Value,
1945 ) {
1946 if !matches!(&server, ServerKind::Rust)
1947 || params.get("quiescent").and_then(serde_json::Value::as_bool) != Some(true)
1948 {
1949 return;
1950 }
1951
1952 let key = ServerKey { kind: server, root };
1953 let became_quiescent = self
1954 .clients
1955 .get_mut(&key)
1956 .is_some_and(|client| client.set_rust_analyzer_quiescent(true));
1957 if became_quiescent {
1958 self.diagnostics.promote_provisional_for_server(&key);
1959 }
1960 }
1961
1962 fn spawn_server(
1963 &self,
1964 def: &ServerDef,
1965 root: &Path,
1966 config: &Config,
1967 ) -> Result<LspClient, LspError> {
1968 let binary = self.resolve_binary(def, config)?;
1969
1970 let mut merged_env = def.env.clone();
1974 for (key, value) in &self.extra_env {
1975 merged_env.insert(key.clone(), value.clone());
1976 }
1977
1978 let mut client = LspClient::spawn(
1979 def.kind.clone(),
1980 root.to_path_buf(),
1981 &binary,
1982 &def.args,
1983 &merged_env,
1984 self.event_tx.clone(),
1985 self.child_registry.clone(),
1986 )?;
1987 if let Err(err) = client.initialize(root, def.initialization_options.clone()) {
1988 wait_for_stderr_tail(&mut client);
1989 let stderr_tail = client.stderr_tail();
1990 let reason = if client.child_exited() || !stderr_tail.is_empty() {
1991 format_initialize_failure_reason(&def.binary, &stderr_tail, &err)
1992 } else {
1993 format!("server failed during initialize: {err}")
1994 };
1995 return Err(LspError::ServerNotReady(reason));
1996 }
1997 Ok(client)
1998 }
1999
2000 fn resolve_binary(&self, def: &ServerDef, config: &Config) -> Result<PathBuf, LspError> {
2001 if let Some(path) = self.binary_overrides.get(&def.kind) {
2002 if path.exists() {
2003 return Ok(path.clone());
2004 }
2005 return Err(LspError::NotFound(format!(
2006 "override binary for {:?} not found: {}",
2007 def.kind,
2008 path.display()
2009 )));
2010 }
2011
2012 if let Some(path) = env_binary_override(&def.kind) {
2013 if path.exists() {
2014 return Ok(path);
2015 }
2016 return Err(LspError::NotFound(format!(
2017 "environment override binary for {:?} not found: {}",
2018 def.kind,
2019 path.display()
2020 )));
2021 }
2022
2023 resolve_lsp_binary(
2028 &def.binary,
2029 config.project_root.as_deref(),
2030 &config.lsp_paths_extra,
2031 )
2032 .ok_or_else(|| {
2033 LspError::NotFound(format!(
2034 "language server binary '{}' not found in node_modules/.bin, lsp_paths_extra, or PATH",
2035 def.binary
2036 ))
2037 })
2038 }
2039
2040 fn server_key_for_file(&self, file_path: &Path, config: &Config) -> Option<ServerKey> {
2041 for def in servers_for_file(file_path, config) {
2042 let key = server_key_for_definition(&def, file_path)?;
2043 if self.clients.contains_key(&key) {
2044 return Some(key);
2045 }
2046 }
2047 None
2048 }
2049}
2050
2051impl Default for LspManager {
2052 fn default() -> Self {
2053 Self::new()
2054 }
2055}
2056
2057fn wait_for_stderr_tail(client: &mut LspClient) {
2058 for _ in 0..10 {
2059 if !client.stderr_tail().is_empty() {
2060 break;
2061 }
2062 std::thread::sleep(std::time::Duration::from_millis(10));
2063 }
2064}
2065
2066fn recoverable_pull_rejection(err: &LspError) -> bool {
2067 matches!(
2068 err,
2069 LspError::ServerError {
2070 code: -32601 | -32602,
2071 ..
2072 }
2073 )
2074}
2075
2076fn server_attempt_result_reason(result: &ServerAttemptResult) -> String {
2077 match result {
2078 ServerAttemptResult::SpawnFailed { binary, reason } => {
2079 format!("spawn_failed: {binary} ({reason})")
2080 }
2081 ServerAttemptResult::BinaryNotInstalled { binary } => {
2082 format!("binary_not_installed: {binary}")
2083 }
2084 ServerAttemptResult::NoRootMarker { looked_for } => {
2085 format!("no_root_marker (looked for: {})", looked_for.join(", "))
2086 }
2087 ServerAttemptResult::Ok { .. } => "ok".to_string(),
2088 }
2089}
2090
2091fn format_stderr_tail_for_reason(stderr_tail: &str) -> String {
2092 truncate_stderr_tail_for_reason(stderr_tail)
2093 .lines()
2094 .map(|line| format!(" {line}"))
2095 .collect::<Vec<_>>()
2096 .join("\n")
2097}
2098
2099fn truncate_stderr_tail_for_reason(stderr_tail: &str) -> String {
2100 if stderr_tail.len() <= STDERR_REASON_BYTES {
2101 return stderr_tail.to_string();
2102 }
2103
2104 let ellipsis = "...";
2105 let target_len = STDERR_REASON_BYTES.saturating_sub(ellipsis.len());
2106 let mut start = stderr_tail.len() - target_len;
2107 while start < stderr_tail.len() && !stderr_tail.is_char_boundary(start) {
2108 start += 1;
2109 }
2110 format!("{ellipsis}{}", &stderr_tail[start..])
2111}
2112
2113fn format_initialize_failure_reason(binary: &str, stderr_tail: &str, err: &LspError) -> String {
2114 let mut reason = format!("server crashed during initialize: {err}");
2115 if !stderr_tail.is_empty() {
2116 reason.push_str("; stderr (last 64 lines):\n");
2117 reason.push_str(&format_stderr_tail_for_reason(stderr_tail));
2118 reason.push_str("\n\n");
2119 reason.push_str(&failure_hint(binary, stderr_tail));
2120 }
2121 reason
2122}
2123
2124fn format_post_initialize_exit_reason(
2125 binary: &str,
2126 status: std::process::ExitStatus,
2127 stderr_tail: &str,
2128 err: &LspError,
2129) -> String {
2130 let code = status
2131 .code()
2132 .map(|c| c.to_string())
2133 .unwrap_or_else(|| "signal/unknown".to_string());
2134 let mut reason = format!("server exited after initialize (code {code}): {err}");
2135 if !stderr_tail.is_empty() {
2136 reason.push_str("; stderr (last 64 lines):\n");
2137 reason.push_str(&format_stderr_tail_for_reason(stderr_tail));
2138 reason.push_str("\n\n");
2139 reason.push_str(&failure_hint(binary, stderr_tail));
2140 }
2141 reason
2142}
2143
2144fn failure_hint(binary: &str, stderr_tail: &str) -> String {
2145 if stderr_tail.contains("MODULE_NOT_FOUND") || stderr_tail.contains("Cannot find module") {
2146 let package_manager = infer_package_manager(stderr_tail);
2147 format!(
2148 "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."
2149 )
2150 } else if let Some(component) = rustup_missing_component(stderr_tail) {
2151 format!("'{component}' is a rustup proxy but the component is not installed. Install it: rustup component add {component}")
2156 } else {
2157 format!("Hint: see stderr above for '{binary}' failure details.")
2158 }
2159}
2160
2161fn rustup_missing_component(stderr_tail: &str) -> Option<String> {
2167 let marker = "Unknown binary '";
2168 let start = stderr_tail.find(marker)? + marker.len();
2169 let rest = &stderr_tail[start..];
2170 let end = rest.find('\'')?;
2171 let name = &rest[..end];
2172 if name.is_empty() || !stderr_tail.contains("toolchain") {
2175 return None;
2176 }
2177 Some(name.to_string())
2178}
2179
2180fn infer_package_manager(stderr_tail: &str) -> &'static str {
2181 let lower = stderr_tail.to_ascii_lowercase();
2182 if lower.contains(".pnpm/") || lower.contains(".pnpm\\") || lower.contains("/pnpm/") {
2183 "pnpm"
2184 } else if lower.contains(".yarn/")
2185 || lower.contains(".yarn\\")
2186 || lower.contains("/yarn/")
2187 || lower.contains("yarn")
2188 {
2189 "yarn"
2190 } else {
2191 "npm"
2192 }
2193}
2194
2195fn canonicalize_for_lsp(file_path: &Path) -> Result<PathBuf, LspError> {
2196 std::fs::canonicalize(file_path)
2203 .map(|canonical| crate::inspect::job::normalize_path(&canonical))
2204 .map_err(LspError::from)
2205}
2206
2207fn resolve_for_lsp_uri(file_path: &Path) -> PathBuf {
2208 if let Ok(path) = std::fs::canonicalize(file_path) {
2211 return crate::inspect::job::normalize_path(&path);
2212 }
2213
2214 let mut existing = file_path.to_path_buf();
2215 let mut missing = Vec::new();
2216 while !existing.exists() {
2217 let Some(name) = existing.file_name() else {
2218 break;
2219 };
2220 missing.push(name.to_owned());
2221 let Some(parent) = existing.parent() else {
2222 break;
2223 };
2224 existing = parent.to_path_buf();
2225 }
2226
2227 let mut resolved = std::fs::canonicalize(&existing)
2228 .map(|canonical| crate::inspect::job::normalize_path(&canonical))
2229 .unwrap_or(existing);
2230 for segment in missing.into_iter().rev() {
2231 resolved.push(segment);
2232 }
2233 resolved
2234}
2235
2236fn language_id_for_extension(ext: &str) -> &'static str {
2237 match ext {
2238 "ts" => "typescript",
2239 "tsx" => "typescriptreact",
2240 "js" | "mjs" | "cjs" => "javascript",
2241 "jsx" => "javascriptreact",
2242 "py" | "pyi" => "python",
2243 "rs" => "rust",
2244 "go" => "go",
2245 "html" | "htm" => "html",
2246 _ => "plaintext",
2247 }
2248}
2249
2250fn normalize_lookup_path(path: &Path) -> PathBuf {
2251 std::fs::canonicalize(path)
2255 .map(|canonical| crate::inspect::job::normalize_path(&canonical))
2256 .unwrap_or_else(|_| path.to_path_buf())
2257}
2258
2259fn diagnostic_path_candidates(file: &Path) -> Vec<PathBuf> {
2260 let mut candidates = Vec::with_capacity(4);
2261 let mut add = |candidate: PathBuf| {
2262 if !candidates.iter().any(|existing| existing == &candidate) {
2263 candidates.push(candidate);
2264 }
2265 };
2266
2267 add(file.to_path_buf());
2271 add(normalize_lookup_path(file));
2272
2273 if let (Some(parent), Some(name)) = (file.parent(), file.file_name()) {
2277 if let Ok(canonical_parent) = std::fs::canonicalize(parent) {
2278 let reconstructed = canonical_parent.join(name);
2279 add(reconstructed.clone());
2280 add(crate::inspect::job::normalize_path(&reconstructed));
2281 }
2282 }
2283
2284 candidates
2285}
2286
2287fn classify_spawn_error(binary: &str, err: &LspError) -> ServerAttemptResult {
2294 match err {
2295 LspError::NotFound(_) => ServerAttemptResult::BinaryNotInstalled {
2300 binary: binary.to_string(),
2301 },
2302 other => ServerAttemptResult::SpawnFailed {
2303 binary: binary.to_string(),
2304 reason: other.to_string(),
2305 },
2306 }
2307}
2308
2309fn env_binary_override(kind: &ServerKind) -> Option<PathBuf> {
2310 let id = kind.id_str();
2311 let suffix: String = id
2312 .chars()
2313 .map(|ch| {
2314 if ch.is_ascii_alphanumeric() {
2315 ch.to_ascii_uppercase()
2316 } else {
2317 '_'
2318 }
2319 })
2320 .collect();
2321 let key = format!("AFT_LSP_{suffix}_BINARY");
2322 std::env::var_os(key).map(PathBuf::from)
2323}
2324
2325#[cfg(all(test, windows))]
2326mod windows_server_key_tests {
2327 use std::fs;
2328 use std::os::windows::ffi::OsStrExt;
2329
2330 use super::{canonicalize_for_lsp, server_key_for_definition};
2331 use crate::config::{Config, UserServerDef};
2332 use crate::lsp::registry::servers_for_file;
2333
2334 #[test]
2335 fn normalized_and_verbatim_inputs_produce_identical_server_key_material() {
2336 let temp_dir = tempfile::tempdir().expect("tempdir");
2337 let root = temp_dir.path().join("workspace");
2338 let source = root.join("src").join("main.customts");
2339 fs::create_dir_all(source.parent().expect("source parent")).expect("create source dir");
2340 fs::write(root.join("custom-root.json"), "{}\n").expect("write root marker");
2341 fs::write(&source, "export const value = 1;\n").expect("write source");
2342
2343 let config = Config {
2344 project_root: Some(root),
2345 lsp_servers: vec![UserServerDef {
2346 id: "custom-ts".to_string(),
2347 extensions: vec!["customts".to_string()],
2348 binary: "custom-ts-lsp".to_string(),
2349 args: Vec::new(),
2350 root_markers: vec!["custom-root.json".to_string()],
2351 env: Default::default(),
2352 initialization_options: None,
2353 disabled: false,
2354 }],
2355 ..Config::default()
2356 };
2357
2358 let normalized_input = canonicalize_for_lsp(&source).expect("normalized source path");
2359 let bare_canonical_input = fs::canonicalize(&source).expect("canonical source path");
2360 let key_for = |path: &std::path::Path| {
2361 let def = servers_for_file(path, &config)
2362 .into_iter()
2363 .find(|def| def.kind.id_str() == "custom-ts")
2364 .expect("custom server definition");
2365 server_key_for_definition(&def, path).expect("custom server root")
2366 };
2367
2368 let key_material = |key: &crate::lsp::roots::ServerKey| {
2369 let root_bytes = key
2370 .root
2371 .as_os_str()
2372 .encode_wide()
2373 .flat_map(u16::to_le_bytes)
2374 .collect::<Vec<_>>();
2375 (key.kind.id_str().to_string(), root_bytes)
2376 };
2377 let ensure_key = key_for(&normalized_input);
2378 let running_lookup_key = key_for(&bare_canonical_input);
2379
2380 assert_eq!(key_material(&ensure_key), key_material(&running_lookup_key));
2381 }
2382}
2383
2384#[cfg(test)]
2385mod failure_hint_tests {
2386 use super::{failure_hint, rustup_missing_component};
2387
2388 #[test]
2389 fn detects_rustup_proxy_without_component() {
2390 let stderr = "error: Unknown binary 'rust-analyzer' in official toolchain 'stable-aarch64-apple-darwin'.";
2392 assert_eq!(
2393 rustup_missing_component(stderr).as_deref(),
2394 Some("rust-analyzer")
2395 );
2396 let hint = failure_hint("rust-analyzer", stderr);
2397 assert!(
2398 hint.contains("rustup component add rust-analyzer"),
2399 "expected actionable rustup hint, got: {hint}"
2400 );
2401 }
2402
2403 #[test]
2404 fn ignores_unknown_binary_without_toolchain_phrasing() {
2405 let stderr = "fatal: Unknown binary 'foo' was requested by the linker.";
2408 assert_eq!(rustup_missing_component(stderr), None);
2409 assert!(failure_hint("foo", stderr).starts_with("Hint: see stderr"));
2410 }
2411
2412 #[test]
2413 fn npm_module_not_found_still_wins() {
2414 let stderr = "Error: Cannot find module '/x/typescript-language-server/lib/cli.mjs'";
2416 let hint = failure_hint("typescript-language-server", stderr);
2417 assert!(hint.contains("install -g"), "got: {hint}");
2418 }
2419}
2420
2421#[cfg(test)]
2422mod diagnostic_capacity_tests {
2423 use std::fs;
2424
2425 use super::LspManager;
2426 use crate::config::Config;
2427
2428 #[test]
2433 fn set_diagnostic_capacity_propagates_to_store() {
2434 let mut manager = LspManager::new();
2435 manager.set_diagnostic_capacity(7);
2436 assert_eq!(manager.diagnostics_store_for_test().capacity_for_test(), 7);
2437 manager.set_diagnostic_capacity(0); assert_eq!(manager.diagnostics_store_for_test().capacity_for_test(), 0);
2439 }
2440
2441 #[test]
2444 fn clear_failed_spawns_empties_the_cache() {
2445 let mut manager = LspManager::new();
2446 assert_eq!(manager.clear_failed_spawns(), 0);
2447 manager.insert_failed_spawn_for_test();
2448 assert_eq!(manager.clear_failed_spawns(), 1);
2449 assert_eq!(manager.clear_failed_spawns(), 0);
2450 }
2451
2452 #[test]
2453 fn post_write_notification_does_not_start_a_cold_server() {
2454 let dir = tempfile::tempdir().unwrap();
2455 let file = dir.path().join("main.ts");
2456 fs::write(dir.path().join("package.json"), "{}").unwrap();
2457 fs::write(&file, "export const value = 1;\n").unwrap();
2458
2459 let mut manager = LspManager::new();
2460 manager
2461 .notify_file_changed_if_running(&file, "export const value = 1;\n", &Config::default())
2462 .unwrap();
2463 assert!(manager.clients.is_empty());
2464 }
2465}
2466
2467#[cfg(test)]
2468mod clear_diagnostics_tests {
2469 use std::path::PathBuf;
2470
2471 use super::LspManager;
2472 use crate::lsp::client::LspEvent;
2473 use crate::lsp::diagnostics::{DiagnosticSeverity, StoredDiagnostic};
2474 use crate::lsp::position::uri_for_path;
2475 use crate::lsp::registry::ServerKind;
2476 use crate::lsp::roots::ServerKey;
2477
2478 fn err_diag(file: &PathBuf) -> StoredDiagnostic {
2479 StoredDiagnostic {
2480 file: file.clone(),
2481 line: 1,
2482 column: 1,
2483 end_line: 1,
2484 end_column: 2,
2485 severity: DiagnosticSeverity::Error,
2486 message: "boom".into(),
2487 code: None,
2488 source: None,
2489 }
2490 }
2491
2492 #[test]
2497 fn clear_diagnostics_for_deleted_file_matches_canonical_key() {
2498 let dir = tempfile::tempdir().unwrap();
2499 let canonical_dir = std::fs::canonicalize(dir.path()).unwrap();
2501 let canonical_file = canonical_dir.join("gone.ts");
2502 std::fs::write(&canonical_file, "x").unwrap();
2505
2506 let mut manager = LspManager::new();
2507 let key = ServerKey {
2508 kind: ServerKind::TypeScript,
2509 root: canonical_dir.clone(),
2510 };
2511 manager.diagnostics_store_mut_for_test().publish(
2512 key,
2513 canonical_file.clone(),
2514 vec![err_diag(&canonical_file)],
2515 );
2516 assert_eq!(manager.warm_error_warning_counts(), (1, 0));
2517
2518 std::fs::remove_file(&canonical_file).unwrap();
2519
2520 let watcher_path = dir.path().join("gone.ts");
2523 let removed = manager.clear_diagnostics_for_file(&watcher_path);
2524
2525 assert!(removed, "expected the deleted file's diagnostic to clear");
2526 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
2527 }
2528
2529 #[cfg(windows)]
2530 #[test]
2531 fn clear_diagnostics_for_deleted_file_matches_normalized_publish_key() {
2532 let dir = tempfile::tempdir().unwrap();
2533 let file = dir.path().join("normalized-gone.ts");
2534 std::fs::write(&file, "x").unwrap();
2535 let normalized_file = crate::inspect::job::canonicalize_normalized(&file);
2536
2537 let mut manager = LspManager::new();
2538 let key = ServerKey {
2539 kind: ServerKind::TypeScript,
2540 root: normalized_file.parent().unwrap().to_path_buf(),
2541 };
2542 manager.diagnostics_store_mut_for_test().publish(
2543 key,
2544 normalized_file.clone(),
2545 vec![err_diag(&normalized_file)],
2546 );
2547 std::fs::remove_file(&file).unwrap();
2548
2549 assert!(manager.clear_diagnostics_for_file(&file));
2550 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
2551 }
2552
2553 #[cfg(windows)]
2554 #[test]
2555 fn stale_diagnostics_for_deleted_file_matches_normalized_publish_key() {
2556 let dir = tempfile::tempdir().unwrap();
2557 let file = dir.path().join("normalized-stale.ts");
2558 std::fs::write(&file, "x").unwrap();
2559 let normalized_file = crate::inspect::job::canonicalize_normalized(&file);
2560
2561 let mut manager = LspManager::new();
2562 let key = ServerKey {
2563 kind: ServerKind::TypeScript,
2564 root: normalized_file.parent().unwrap().to_path_buf(),
2565 };
2566 manager.diagnostics_store_mut_for_test().publish(
2567 key,
2568 normalized_file.clone(),
2569 vec![err_diag(&normalized_file)],
2570 );
2571 std::fs::remove_file(&file).unwrap();
2572
2573 let result = manager.mark_diagnostics_stale_for_file(&file);
2574 assert!(result.had_entries);
2575 assert!(result.changed);
2576 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
2577 }
2578
2579 #[test]
2580 fn clear_diagnostics_for_unknown_file_is_noop() {
2581 let mut manager = LspManager::new();
2582 assert!(!manager.clear_diagnostics_for_file(&PathBuf::from("/nope/missing.ts")));
2583 assert_eq!(manager.warm_error_warning_counts(), (0, 0));
2584 }
2585
2586 #[test]
2587 fn drain_events_reports_publish_diagnostics_updates() {
2588 let dir = tempfile::tempdir().unwrap();
2589 let root = std::fs::canonicalize(dir.path()).unwrap();
2590 let file = root.join("main.ts");
2591 std::fs::write(&file, "const x: number = 'nope';").unwrap();
2592
2593 let mut manager = LspManager::new();
2594 let diagnostic = lsp_types::Diagnostic {
2595 range: lsp_types::Range {
2596 start: lsp_types::Position {
2597 line: 0,
2598 character: 0,
2599 },
2600 end: lsp_types::Position {
2601 line: 0,
2602 character: 1,
2603 },
2604 },
2605 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
2606 code: None,
2607 code_description: None,
2608 source: Some("test".into()),
2609 message: "boom".into(),
2610 related_information: None,
2611 tags: None,
2612 data: None,
2613 };
2614 let params = serde_json::to_value(lsp_types::PublishDiagnosticsParams {
2615 uri: uri_for_path(&file).unwrap(),
2616 diagnostics: vec![diagnostic],
2617 version: Some(1),
2618 })
2619 .unwrap();
2620 manager
2621 .event_tx
2622 .send(LspEvent::Notification {
2623 server_kind: ServerKind::TypeScript,
2624 root,
2625 method: "textDocument/publishDiagnostics".into(),
2626 params: Some(params),
2627 })
2628 .unwrap();
2629
2630 let drained = manager.drain_events();
2631
2632 assert!(drained.diagnostics_changed);
2633 assert_eq!(drained.events.len(), 1);
2634 assert_eq!(manager.warm_error_warning_counts(), (1, 0));
2635 }
2636}