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::{find_workspace_root, ServerKey};
28use crate::lsp::LspError;
29use crate::slog_error;
30
31const STDERR_REASON_BYTES: usize = 2 * 1024;
32
33#[derive(Debug, Clone)]
38pub enum ServerAttemptResult {
39 Ok { server_key: ServerKey },
41 NoRootMarker { looked_for: Vec<String> },
44 BinaryNotInstalled { binary: String },
47 SpawnFailed { binary: String, reason: String },
49}
50
51#[derive(Debug, Clone)]
53pub struct ServerAttempt {
54 pub server_id: String,
56 pub server_name: String,
58 pub result: ServerAttemptResult,
59}
60
61#[derive(Debug, Clone, Default)]
67pub struct EnsureServerOutcomes {
68 pub successful: Vec<ServerKey>,
70 pub attempts: Vec<ServerAttempt>,
73}
74
75impl EnsureServerOutcomes {
76 pub fn no_server_registered(&self) -> bool {
78 self.attempts.is_empty()
79 }
80}
81
82#[derive(Debug, Clone, Default)]
92pub struct PostEditWaitOutcome {
93 pub diagnostics: Vec<StoredDiagnostic>,
97 pub pending_servers: Vec<ServerKey>,
101 pub exited_servers: Vec<ServerKey>,
105}
106
107#[derive(Debug, Clone, Copy, Default)]
109pub struct PreEditSnapshot {
110 pub epoch: u64,
111 pub document_version_at_capture: Option<i32>,
112}
113
114pub fn post_edit_entry_is_fresh(
115 entry: &DiagnosticEntry,
116 target_version: i32,
117 pre: PreEditSnapshot,
118) -> bool {
119 if entry.epoch <= pre.epoch {
120 return false;
121 }
122
123 match entry.version {
124 Some(version) => version >= target_version,
125 None => false,
130 }
131}
132
133impl PostEditWaitOutcome {
134 pub fn complete(&self) -> bool {
137 self.pending_servers.is_empty() && self.exited_servers.is_empty()
138 }
139}
140
141#[derive(Debug, Clone)]
143pub enum PullFileOutcome {
144 Full { diagnostic_count: usize },
146 Unchanged,
148 PartialNotSupported,
151 PullNotSupported,
154 RequestFailed { reason: String },
156}
157
158#[derive(Debug, Clone)]
160pub struct PullFileResult {
161 pub server_key: ServerKey,
162 pub outcome: PullFileOutcome,
163}
164
165#[derive(Debug, Clone)]
167pub struct PullWorkspaceResult {
168 pub server_key: ServerKey,
169 pub files_reported: Vec<PathBuf>,
173 pub complete: bool,
175 pub cancelled: bool,
177 pub supports_workspace: bool,
181}
182
183pub struct LspManager {
184 clients: HashMap<ServerKey, LspClient>,
186 server_binaries: HashMap<ServerKey, String>,
190 documents: HashMap<ServerKey, DocumentStore>,
192 diagnostics: DiagnosticsStore,
194 event_tx: Sender<LspEvent>,
196 event_rx: Receiver<LspEvent>,
197 binary_overrides: HashMap<ServerKind, PathBuf>,
199 extra_env: HashMap<String, String>,
203 failed_spawns: HashMap<ServerKey, ServerAttemptResult>,
218 watched_file_skip_logged: HashSet<ServerKey>,
221 child_registry: LspChildRegistry,
225}
226
227impl LspManager {
228 pub fn new() -> Self {
229 let (event_tx, event_rx) = unbounded();
230 Self {
231 clients: HashMap::new(),
232 server_binaries: HashMap::new(),
233 documents: HashMap::new(),
234 diagnostics: DiagnosticsStore::new(),
235 event_tx,
236 event_rx,
237 binary_overrides: HashMap::new(),
238 extra_env: HashMap::new(),
239 failed_spawns: HashMap::new(),
240 watched_file_skip_logged: HashSet::new(),
241 child_registry: LspChildRegistry::new(),
242 }
243 }
244
245 pub fn set_child_registry(&mut self, registry: LspChildRegistry) {
247 self.child_registry = registry;
248 }
249
250 pub fn set_extra_env(&mut self, key: &str, value: &str) {
254 self.extra_env.insert(key.to_string(), value.to_string());
255 }
256
257 pub fn server_count(&self) -> usize {
259 self.clients.len()
260 }
261
262 pub fn override_binary(&mut self, kind: ServerKind, binary_path: PathBuf) {
264 self.binary_overrides.insert(kind, binary_path);
265 }
266
267 pub fn ensure_server_for_file(&mut self, file_path: &Path, config: &Config) -> Vec<ServerKey> {
274 self.ensure_server_for_file_detailed(file_path, config)
275 .successful
276 }
277
278 pub fn ensure_server_for_file_detailed(
286 &mut self,
287 file_path: &Path,
288 config: &Config,
289 ) -> EnsureServerOutcomes {
290 let defs = servers_for_file(file_path, config);
291 let mut outcomes = EnsureServerOutcomes::default();
292
293 for def in defs {
294 let server_id = def.kind.id_str().to_string();
295 let server_name = def.name.to_string();
296
297 let Some(root) = find_workspace_root(file_path, &def.root_markers) else {
298 outcomes.attempts.push(ServerAttempt {
299 server_id,
300 server_name,
301 result: ServerAttemptResult::NoRootMarker {
302 looked_for: def.root_markers.iter().map(|s| s.to_string()).collect(),
303 },
304 });
305 continue;
306 };
307
308 let key = ServerKey {
309 kind: def.kind.clone(),
310 root,
311 };
312
313 if !self.clients.contains_key(&key) {
314 if let Some(cached) = self.failed_spawns.get(&key) {
321 outcomes.attempts.push(ServerAttempt {
322 server_id,
323 server_name,
324 result: cached.clone(),
325 });
326 continue;
327 }
328
329 match self.spawn_server(&def, &key.root, config) {
330 Ok(client) => {
331 self.clients.insert(key.clone(), client);
332 self.server_binaries.insert(key.clone(), def.binary.clone());
333 self.documents.entry(key.clone()).or_default();
334 }
335 Err(err) => {
336 slog_error!("failed to spawn {}: {}", def.name, err);
337 let result = classify_spawn_error(&def.binary, &err);
338 self.failed_spawns.insert(key.clone(), result.clone());
342 outcomes.attempts.push(ServerAttempt {
343 server_id,
344 server_name,
345 result,
346 });
347 continue;
348 }
349 }
350 }
351
352 outcomes.attempts.push(ServerAttempt {
353 server_id,
354 server_name,
355 result: ServerAttemptResult::Ok {
356 server_key: key.clone(),
357 },
358 });
359 outcomes.successful.push(key);
360 }
361
362 outcomes
363 }
364
365 pub fn ensure_server_for_file_default(&mut self, file_path: &Path) -> Vec<ServerKey> {
368 self.ensure_server_for_file(file_path, &Config::default())
369 }
370 pub fn ensure_file_open(
374 &mut self,
375 file_path: &Path,
376 config: &Config,
377 ) -> Result<Vec<ServerKey>, LspError> {
378 let canonical_path = canonicalize_for_lsp(file_path)?;
379 let server_keys = self.ensure_server_for_file(&canonical_path, config);
380 if server_keys.is_empty() {
381 return Ok(server_keys);
382 }
383
384 let uri = uri_for_path(&canonical_path)?;
385 let language_id = language_id_for_extension(
386 canonical_path
387 .extension()
388 .and_then(|ext| ext.to_str())
389 .unwrap_or_default(),
390 )
391 .to_string();
392
393 for key in &server_keys {
394 let already_open = self
395 .documents
396 .get(key)
397 .is_some_and(|store| store.is_open(&canonical_path));
398
399 if !already_open {
400 let content = std::fs::read_to_string(&canonical_path).map_err(LspError::Io)?;
401 if let Some(client) = self.clients.get_mut(key) {
402 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
403 text_document: TextDocumentItem::new(
404 uri.clone(),
405 language_id.clone(),
406 0,
407 content,
408 ),
409 })?;
410 }
411 self.documents
412 .entry(key.clone())
413 .or_default()
414 .open(canonical_path.clone());
415 continue;
416 }
417
418 let drifted = self
428 .documents
429 .get(key)
430 .is_some_and(|store| store.is_stale_on_disk(&canonical_path));
431 if drifted {
432 let content = std::fs::read_to_string(&canonical_path).map_err(LspError::Io)?;
433 let next_version = self
434 .documents
435 .get(key)
436 .and_then(|store| store.version(&canonical_path))
437 .map(|v| v + 1)
438 .unwrap_or(1);
439 if let Some(client) = self.clients.get_mut(key) {
440 client.send_notification::<DidChangeTextDocument>(
441 DidChangeTextDocumentParams {
442 text_document: VersionedTextDocumentIdentifier::new(
443 uri.clone(),
444 next_version,
445 ),
446 content_changes: vec![TextDocumentContentChangeEvent {
447 range: None,
448 range_length: None,
449 text: content,
450 }],
451 },
452 )?;
453 }
454 if let Some(store) = self.documents.get_mut(key) {
455 store.bump_version(&canonical_path);
456 }
457 }
458 }
459
460 Ok(server_keys)
461 }
462
463 pub fn ensure_file_open_default(
464 &mut self,
465 file_path: &Path,
466 ) -> Result<Vec<ServerKey>, LspError> {
467 self.ensure_file_open(file_path, &Config::default())
468 }
469
470 pub fn notify_file_changed(
476 &mut self,
477 file_path: &Path,
478 content: &str,
479 config: &Config,
480 ) -> Result<(), LspError> {
481 self.notify_file_changed_versioned(file_path, content, config)
482 .map(|_| ())
483 }
484
485 pub fn notify_file_changed_versioned(
496 &mut self,
497 file_path: &Path,
498 content: &str,
499 config: &Config,
500 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
501 let canonical_path = canonicalize_for_lsp(file_path)?;
502 let server_keys = self.ensure_server_for_file(&canonical_path, config);
503 if server_keys.is_empty() {
504 return Ok(Vec::new());
505 }
506
507 let uri = uri_for_path(&canonical_path)?;
508 let language_id = language_id_for_extension(
509 canonical_path
510 .extension()
511 .and_then(|ext| ext.to_str())
512 .unwrap_or_default(),
513 )
514 .to_string();
515
516 let mut versions: Vec<(ServerKey, i32)> = Vec::with_capacity(server_keys.len());
517
518 for key in server_keys {
519 let current_version = self
520 .documents
521 .get(&key)
522 .and_then(|store| store.version(&canonical_path));
523
524 if let Some(version) = current_version {
525 let next_version = version + 1;
526 if let Some(client) = self.clients.get_mut(&key) {
527 client.send_notification::<DidChangeTextDocument>(
528 DidChangeTextDocumentParams {
529 text_document: VersionedTextDocumentIdentifier::new(
530 uri.clone(),
531 next_version,
532 ),
533 content_changes: vec![TextDocumentContentChangeEvent {
534 range: None,
535 range_length: None,
536 text: content.to_string(),
537 }],
538 },
539 )?;
540 }
541 if let Some(store) = self.documents.get_mut(&key) {
542 store.bump_version(&canonical_path);
543 }
544 versions.push((key, next_version));
545 continue;
546 }
547
548 if let Some(client) = self.clients.get_mut(&key) {
549 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
550 text_document: TextDocumentItem::new(
551 uri.clone(),
552 language_id.clone(),
553 0,
554 content.to_string(),
555 ),
556 })?;
557 }
558 self.documents
559 .entry(key.clone())
560 .or_default()
561 .open(canonical_path.clone());
562 versions.push((key, 0));
565 }
566
567 Ok(versions)
568 }
569
570 pub fn notify_file_changed_default(
571 &mut self,
572 file_path: &Path,
573 content: &str,
574 ) -> Result<(), LspError> {
575 self.notify_file_changed(file_path, content, &Config::default())
576 }
577
578 pub fn notify_files_watched_changed(
584 &mut self,
585 paths: &[(PathBuf, FileChangeType)],
586 _config: &Config,
587 ) -> Result<(), LspError> {
588 if paths.is_empty() {
589 return Ok(());
590 }
591
592 let mut canonical_events = Vec::with_capacity(paths.len());
593 for (path, typ) in paths {
594 let canonical_path = resolve_for_lsp_uri(path);
595 canonical_events.push((canonical_path, *typ));
596 }
597
598 let keys: Vec<ServerKey> = self.clients.keys().cloned().collect();
599 for key in keys {
600 let mut changes = Vec::new();
601 for (path, typ) in &canonical_events {
602 if !path.starts_with(&key.root) {
603 continue;
604 }
605 changes.push(FileEvent::new(uri_for_path(path)?, *typ));
606 }
607
608 if changes.is_empty() {
609 continue;
610 }
611
612 if let Some(client) = self.clients.get_mut(&key) {
613 let supports_static_watched_files = client.supports_watched_files();
619 let has_dynamic_registration = client.has_watched_file_registration();
620 if !(supports_static_watched_files || has_dynamic_registration) {
621 if self.watched_file_skip_logged.insert(key.clone()) {
622 log::debug!(
623 "skipping didChangeWatchedFiles for {:?} (not supported or registered)",
624 key
625 );
626 }
627 continue;
628 }
629 client.send_notification::<DidChangeWatchedFiles>(DidChangeWatchedFilesParams {
630 changes,
631 })?;
632 }
633 }
634
635 Ok(())
636 }
637
638 pub fn notify_file_closed(&mut self, file_path: &Path) -> Result<(), LspError> {
640 let canonical_path = canonicalize_for_lsp(file_path)?;
641 let uri = uri_for_path(&canonical_path)?;
642 let keys: Vec<ServerKey> = self.documents.keys().cloned().collect();
643
644 for key in keys {
645 let was_open = self
646 .documents
647 .get(&key)
648 .map(|store| store.is_open(&canonical_path))
649 .unwrap_or(false);
650 if !was_open {
651 continue;
652 }
653
654 if let Some(client) = self.clients.get_mut(&key) {
655 client.send_notification::<DidCloseTextDocument>(DidCloseTextDocumentParams {
656 text_document: TextDocumentIdentifier::new(uri.clone()),
657 })?;
658 }
659
660 if let Some(store) = self.documents.get_mut(&key) {
661 store.close(&canonical_path);
662 }
663 self.diagnostics
664 .clear_for_server_file(&key, &canonical_path);
665 }
666
667 Ok(())
668 }
669
670 pub fn client_for_file(&self, file_path: &Path, config: &Config) -> Option<&LspClient> {
672 let key = self.server_key_for_file(file_path, config)?;
673 self.clients.get(&key)
674 }
675
676 pub fn client_for_file_default(&self, file_path: &Path) -> Option<&LspClient> {
677 self.client_for_file(file_path, &Config::default())
678 }
679
680 pub fn client_for_file_mut(
682 &mut self,
683 file_path: &Path,
684 config: &Config,
685 ) -> Option<&mut LspClient> {
686 let key = self.server_key_for_file(file_path, config)?;
687 self.clients.get_mut(&key)
688 }
689
690 pub fn client_for_file_mut_default(&mut self, file_path: &Path) -> Option<&mut LspClient> {
691 self.client_for_file_mut(file_path, &Config::default())
692 }
693
694 pub fn active_client_count(&self) -> usize {
696 self.clients.len()
697 }
698
699 pub fn drain_events(&mut self) -> Vec<LspEvent> {
701 let mut events = Vec::new();
702 while let Ok(event) = self.event_rx.try_recv() {
703 self.handle_event(&event);
704 events.push(event);
705 }
706 events
707 }
708
709 pub fn wait_for_diagnostics(
711 &mut self,
712 file_path: &Path,
713 config: &Config,
714 timeout: std::time::Duration,
715 ) -> Vec<StoredDiagnostic> {
716 let deadline = std::time::Instant::now() + timeout;
717 self.wait_for_file_diagnostics(file_path, config, deadline)
718 }
719
720 pub fn wait_for_diagnostics_default(
721 &mut self,
722 file_path: &Path,
723 timeout: std::time::Duration,
724 ) -> Vec<StoredDiagnostic> {
725 self.wait_for_diagnostics(file_path, &Config::default(), timeout)
726 }
727
728 #[doc(hidden)]
733 pub fn diagnostics_store_for_test(&self) -> &DiagnosticsStore {
734 &self.diagnostics
735 }
736
737 pub fn snapshot_diagnostic_epochs(&self, file_path: &Path) -> HashMap<ServerKey, u64> {
742 let lookup_path = normalize_lookup_path(file_path);
743 self.diagnostics
744 .entries_for_file(&lookup_path)
745 .into_iter()
746 .map(|(key, entry)| (key.clone(), entry.epoch))
747 .collect()
748 }
749
750 pub fn snapshot_pre_edit_state(&self, file_path: &Path) -> HashMap<ServerKey, PreEditSnapshot> {
753 let lookup_path = normalize_lookup_path(file_path);
754 let mut snapshots: HashMap<ServerKey, PreEditSnapshot> = self
755 .diagnostics
756 .entries_for_file(&lookup_path)
757 .into_iter()
758 .map(|(key, entry)| {
759 (
760 key.clone(),
761 PreEditSnapshot {
762 epoch: entry.epoch,
763 document_version_at_capture: None,
764 },
765 )
766 })
767 .collect();
768
769 for (key, store) in &self.documents {
770 if let Some(version) = store.version(&lookup_path) {
771 snapshots
772 .entry(key.clone())
773 .or_default()
774 .document_version_at_capture = Some(version);
775 }
776 }
777
778 snapshots
779 }
780
781 pub fn diagnostic_entry_is_fresh_for_document(
789 &self,
790 file_path: &Path,
791 server_key: &ServerKey,
792 pre: PreEditSnapshot,
793 ) -> bool {
794 let lookup_path = normalize_lookup_path(file_path);
795 let Some(entry) = self
796 .diagnostics
797 .entries_for_file(&lookup_path)
798 .into_iter()
799 .find_map(|(key, entry)| if key == server_key { Some(entry) } else { None })
800 else {
801 return false;
802 };
803
804 let target_version = self
805 .documents
806 .get(server_key)
807 .and_then(|store| store.version(&lookup_path))
808 .or(pre.document_version_at_capture)
809 .unwrap_or(0);
810
811 matches!(entry.version, Some(version) if version >= target_version)
812 }
813
814 pub fn wait_for_post_edit_diagnostics(
837 &mut self,
838 file_path: &Path,
839 _config: &Config,
843 expected_versions: &[(ServerKey, i32)],
844 pre_snapshot: &HashMap<ServerKey, PreEditSnapshot>,
845 timeout: std::time::Duration,
846 ) -> PostEditWaitOutcome {
847 let lookup_path = normalize_lookup_path(file_path);
848 let deadline = std::time::Instant::now() + timeout;
849
850 let _ = self.drain_events_for_file(&lookup_path);
855
856 let mut fresh: HashMap<ServerKey, Vec<StoredDiagnostic>> = HashMap::new();
857 let mut exited: Vec<ServerKey> = Vec::new();
858
859 loop {
860 for (key, target_version) in expected_versions {
868 if fresh.contains_key(key) || exited.contains(key) {
869 continue;
870 }
871 if !self.clients.contains_key(key) {
872 exited.push(key.clone());
873 continue;
874 }
875 if let Some(entry) = self
876 .diagnostics
877 .entries_for_file(&lookup_path)
878 .into_iter()
879 .find_map(|(k, e)| if k == key { Some(e) } else { None })
880 {
881 let pre = pre_snapshot.get(key).copied().unwrap_or_default();
882 let is_fresh = post_edit_entry_is_fresh(entry, *target_version, pre);
883 if is_fresh {
884 fresh.insert(key.clone(), entry.diagnostics.clone());
885 }
886 }
887 }
888
889 if fresh.len() + exited.len() == expected_versions.len() {
891 break;
892 }
893
894 let now = std::time::Instant::now();
895 if now >= deadline {
896 break;
897 }
898
899 let timeout = deadline.saturating_duration_since(now);
900 match self.event_rx.recv_timeout(timeout) {
901 Ok(event) => {
902 self.handle_event(&event);
903 }
904 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
905 }
906 }
907
908 let pending: Vec<ServerKey> = expected_versions
910 .iter()
911 .filter(|(k, _)| !fresh.contains_key(k) && !exited.contains(k))
912 .map(|(k, _)| k.clone())
913 .collect();
914
915 let mut diagnostics: Vec<StoredDiagnostic> = fresh
918 .into_iter()
919 .flat_map(|(_, diags)| diags.into_iter())
920 .collect();
921 diagnostics.sort_by(|a, b| {
922 a.file
923 .cmp(&b.file)
924 .then(a.line.cmp(&b.line))
925 .then(a.column.cmp(&b.column))
926 .then(a.message.cmp(&b.message))
927 });
928
929 PostEditWaitOutcome {
930 diagnostics,
931 pending_servers: pending,
932 exited_servers: exited,
933 }
934 }
935
936 pub fn wait_for_file_diagnostics(
942 &mut self,
943 file_path: &Path,
944 config: &Config,
945 deadline: std::time::Instant,
946 ) -> Vec<StoredDiagnostic> {
947 let lookup_path = normalize_lookup_path(file_path);
948
949 if self.server_key_for_file(&lookup_path, config).is_none() {
950 return Vec::new();
951 }
952
953 loop {
954 if self.drain_events_for_file(&lookup_path) {
955 break;
956 }
957
958 let now = std::time::Instant::now();
959 if now >= deadline {
960 break;
961 }
962
963 let timeout = deadline.saturating_duration_since(now);
964 match self.event_rx.recv_timeout(timeout) {
965 Ok(event) => {
966 if matches!(
967 self.handle_event(&event),
968 Some(ref published_file) if published_file.as_path() == lookup_path.as_path()
969 ) {
970 break;
971 }
972 }
973 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
974 }
975 }
976
977 self.get_diagnostics_for_file(&lookup_path)
978 .into_iter()
979 .cloned()
980 .collect()
981 }
982
983 pub const PULL_FILE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
989
990 pub fn pull_file_timeout() -> std::time::Duration {
992 Self::PULL_FILE_TIMEOUT
993 }
994
995 const PULL_WORKSPACE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
999
1000 pub fn pull_file_diagnostics(
1011 &mut self,
1012 file_path: &Path,
1013 config: &Config,
1014 ) -> Result<Vec<PullFileResult>, LspError> {
1015 let canonical_path = canonicalize_for_lsp(file_path)?;
1016 self.ensure_file_open(&canonical_path, config)?;
1019
1020 let server_keys = self.ensure_server_for_file(&canonical_path, config);
1021 if server_keys.is_empty() {
1022 return Ok(Vec::new());
1023 }
1024
1025 let uri = uri_for_path(&canonical_path)?;
1026 let mut results = Vec::with_capacity(server_keys.len());
1027
1028 for key in server_keys {
1029 let supports_pull = self
1030 .clients
1031 .get(&key)
1032 .and_then(|c| c.diagnostic_capabilities())
1033 .is_some_and(|caps| caps.pull_diagnostics);
1034
1035 if !supports_pull {
1036 results.push(PullFileResult {
1037 server_key: key.clone(),
1038 outcome: PullFileOutcome::PullNotSupported,
1039 });
1040 continue;
1041 }
1042
1043 let previous_result_id = self
1045 .diagnostics
1046 .entries_for_file(&canonical_path)
1047 .into_iter()
1048 .find(|(k, _)| **k == key)
1049 .and_then(|(_, entry)| entry.result_id.clone());
1050
1051 let identifier = self
1052 .clients
1053 .get(&key)
1054 .and_then(|c| c.diagnostic_capabilities())
1055 .and_then(|caps| caps.identifier.clone());
1056
1057 let params = AftDocumentDiagnosticParams {
1058 text_document: lsp_types::TextDocumentIdentifier { uri: uri.clone() },
1059 identifier,
1060 previous_result_id,
1061 work_done_progress_params: Default::default(),
1062 partial_result_params: Default::default(),
1063 };
1064
1065 let outcome = match self.send_pull_request(&key, params) {
1066 Ok(report) => self.ingest_document_report(&key, &canonical_path, report),
1067 Err(err) => {
1068 if let Some(result) = self.cache_post_initialize_exit(&key, &err) {
1069 PullFileOutcome::RequestFailed {
1070 reason: server_attempt_result_reason(&result),
1071 }
1072 } else if recoverable_pull_rejection(&err)
1073 && self.clients.get(&key).is_some_and(|client| {
1074 matches!(
1075 client.state(),
1076 ServerState::Ready | ServerState::Initializing
1077 )
1078 })
1079 {
1080 PullFileOutcome::RequestFailed {
1081 reason: format!("pull_rejected_push_fallback: {err}"),
1082 }
1083 } else {
1084 PullFileOutcome::RequestFailed {
1085 reason: err.to_string(),
1086 }
1087 }
1088 }
1089 };
1090
1091 results.push(PullFileResult {
1092 server_key: key,
1093 outcome,
1094 });
1095 }
1096
1097 Ok(results)
1098 }
1099
1100 pub fn pull_workspace_diagnostics(
1105 &mut self,
1106 server_key: &ServerKey,
1107 timeout: Option<std::time::Duration>,
1108 ) -> Result<PullWorkspaceResult, LspError> {
1109 let timeout = timeout.unwrap_or(Self::PULL_WORKSPACE_TIMEOUT);
1110
1111 let supports_workspace = self
1112 .clients
1113 .get(server_key)
1114 .and_then(|c| c.diagnostic_capabilities())
1115 .is_some_and(|caps| caps.workspace_diagnostics);
1116
1117 if !supports_workspace {
1118 return Ok(PullWorkspaceResult {
1119 server_key: server_key.clone(),
1120 files_reported: Vec::new(),
1121 complete: false,
1122 cancelled: false,
1123 supports_workspace: false,
1124 });
1125 }
1126
1127 let identifier = self
1128 .clients
1129 .get(server_key)
1130 .and_then(|c| c.diagnostic_capabilities())
1131 .and_then(|caps| caps.identifier.clone());
1132
1133 let params = AftWorkspaceDiagnosticParams {
1134 identifier,
1135 previous_result_ids: Vec::new(),
1136 work_done_progress_params: Default::default(),
1137 partial_result_params: Default::default(),
1138 };
1139
1140 let result = match self
1141 .clients
1142 .get_mut(server_key)
1143 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?
1144 .send_request_with_timeout::<AftWorkspaceDiagnosticRequest>(params, timeout)
1145 {
1146 Ok(result) => result,
1147 Err(LspError::Timeout(_)) => {
1148 return Ok(PullWorkspaceResult {
1149 server_key: server_key.clone(),
1150 files_reported: Vec::new(),
1151 complete: false,
1152 cancelled: true,
1153 supports_workspace: true,
1154 });
1155 }
1156 Err(err) => {
1157 if let Some(result) = self.cache_post_initialize_exit(server_key, &err) {
1158 return Err(LspError::ServerNotReady(server_attempt_result_reason(
1159 &result,
1160 )));
1161 }
1162 return Err(err);
1163 }
1164 };
1165
1166 let (items, complete) = match result {
1170 lsp_types::WorkspaceDiagnosticReportResult::Report(report) => (report.items, true),
1171 lsp_types::WorkspaceDiagnosticReportResult::Partial(partial) => (partial.items, false),
1172 };
1173
1174 let mut files_reported = Vec::with_capacity(items.len());
1176 for item in items {
1177 match item {
1178 lsp_types::WorkspaceDocumentDiagnosticReport::Full(full) => {
1179 if let Some(file) = uri_to_path(&full.uri) {
1180 let stored = from_lsp_diagnostics(
1181 file.clone(),
1182 full.full_document_diagnostic_report.items.clone(),
1183 );
1184 self.diagnostics.publish_with_result_id(
1185 server_key.clone(),
1186 file.clone(),
1187 stored,
1188 full.full_document_diagnostic_report.result_id.clone(),
1189 );
1190 files_reported.push(file);
1191 }
1192 }
1193 lsp_types::WorkspaceDocumentDiagnosticReport::Unchanged(_unchanged) => {
1194 }
1197 }
1198 }
1199
1200 Ok(PullWorkspaceResult {
1201 server_key: server_key.clone(),
1202 files_reported,
1203 complete,
1204 cancelled: false,
1205 supports_workspace: true,
1206 })
1207 }
1208
1209 fn cache_post_initialize_exit(
1210 &mut self,
1211 key: &ServerKey,
1212 err: &LspError,
1213 ) -> Option<ServerAttemptResult> {
1214 let binary = self
1215 .server_binaries
1216 .get(key)
1217 .cloned()
1218 .unwrap_or_else(|| key.kind.id_str().to_string());
1219 let (status, stderr_tail) = {
1220 let client = self.clients.get_mut(key)?;
1221 let mut status = client.child_exit_status();
1222 for _ in 0..10 {
1223 if status.is_some() {
1224 break;
1225 }
1226 std::thread::sleep(std::time::Duration::from_millis(10));
1227 status = client.child_exit_status();
1228 }
1229 let status = status?;
1230 wait_for_stderr_tail(client);
1231 (status, client.stderr_tail())
1232 };
1233 let reason = format_post_initialize_exit_reason(&binary, status, &stderr_tail, err);
1234 let result = ServerAttemptResult::SpawnFailed { binary, reason };
1235 self.clients.remove(key);
1236 self.server_binaries.remove(key);
1237 self.documents.remove(key);
1238 self.diagnostics.clear_for_server(key);
1239 self.failed_spawns.insert(key.clone(), result.clone());
1240 Some(result)
1241 }
1242
1243 fn send_pull_request(
1245 &mut self,
1246 key: &ServerKey,
1247 params: AftDocumentDiagnosticParams,
1248 ) -> Result<lsp_types::DocumentDiagnosticReportResult, LspError> {
1249 let client = self
1250 .clients
1251 .get_mut(key)
1252 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?;
1253 client.send_request::<AftDocumentDiagnosticRequest>(params)
1254 }
1255
1256 fn ingest_document_report(
1259 &mut self,
1260 key: &ServerKey,
1261 canonical_path: &Path,
1262 result: lsp_types::DocumentDiagnosticReportResult,
1263 ) -> PullFileOutcome {
1264 let report = match result {
1265 lsp_types::DocumentDiagnosticReportResult::Report(report) => report,
1266 lsp_types::DocumentDiagnosticReportResult::Partial(_) => {
1267 return PullFileOutcome::PartialNotSupported;
1271 }
1272 };
1273
1274 match report {
1275 lsp_types::DocumentDiagnosticReport::Full(full) => {
1276 let result_id = full.full_document_diagnostic_report.result_id.clone();
1277 let stored = from_lsp_diagnostics(
1278 canonical_path.to_path_buf(),
1279 full.full_document_diagnostic_report.items.clone(),
1280 );
1281 let count = stored.len();
1282 self.diagnostics.publish_with_result_id(
1283 key.clone(),
1284 canonical_path.to_path_buf(),
1285 stored,
1286 result_id,
1287 );
1288 PullFileOutcome::Full {
1289 diagnostic_count: count,
1290 }
1291 }
1292 lsp_types::DocumentDiagnosticReport::Unchanged(_unchanged) => {
1293 if self
1297 .diagnostics
1298 .has_report_for_server_file(key, canonical_path)
1299 {
1300 PullFileOutcome::Unchanged
1301 } else {
1302 PullFileOutcome::RequestFailed {
1303 reason: "no_cache_for_unchanged".to_string(),
1304 }
1305 }
1306 }
1307 }
1308 }
1309
1310 pub fn shutdown_all(&mut self) {
1312 for (key, mut client) in self.clients.drain() {
1313 if let Err(err) = client.shutdown() {
1314 slog_error!("error shutting down {:?}: {}", key, err);
1315 }
1316 }
1317 self.server_binaries.clear();
1318 self.documents.clear();
1319 self.diagnostics = DiagnosticsStore::new();
1320 }
1321
1322 pub fn has_active_servers(&self) -> bool {
1324 self.clients
1325 .values()
1326 .any(|client| client.state() == ServerState::Ready)
1327 }
1328
1329 pub fn active_server_keys(&self) -> Vec<ServerKey> {
1332 self.clients.keys().cloned().collect()
1333 }
1334
1335 pub fn get_diagnostics_for_file(&self, file: &Path) -> Vec<&StoredDiagnostic> {
1336 let normalized = normalize_lookup_path(file);
1337 self.diagnostics.for_file(&normalized)
1338 }
1339
1340 pub fn get_diagnostics_for_directory(&self, dir: &Path) -> Vec<&StoredDiagnostic> {
1341 let normalized = normalize_lookup_path(dir);
1342 self.diagnostics.for_directory(&normalized)
1343 }
1344
1345 pub fn get_all_diagnostics(&self) -> Vec<&StoredDiagnostic> {
1346 self.diagnostics.all()
1347 }
1348
1349 fn drain_events_for_file(&mut self, file_path: &Path) -> bool {
1350 let mut saw_file_diagnostics = false;
1351 while let Ok(event) = self.event_rx.try_recv() {
1352 if matches!(
1353 self.handle_event(&event),
1354 Some(ref published_file) if published_file.as_path() == file_path
1355 ) {
1356 saw_file_diagnostics = true;
1357 }
1358 }
1359 saw_file_diagnostics
1360 }
1361
1362 fn handle_event(&mut self, event: &LspEvent) -> Option<PathBuf> {
1363 match event {
1364 LspEvent::Notification {
1365 server_kind,
1366 root,
1367 method,
1368 params: Some(params),
1369 } if method == "textDocument/publishDiagnostics" => {
1370 self.handle_publish_diagnostics(server_kind.clone(), root.clone(), params)
1371 }
1372 LspEvent::ServerExited { server_kind, root } => {
1373 let key = ServerKey {
1374 kind: server_kind.clone(),
1375 root: root.clone(),
1376 };
1377 self.clients.remove(&key);
1378 self.server_binaries.remove(&key);
1379 self.documents.remove(&key);
1380 self.diagnostics.clear_for_server(&key);
1381 None
1382 }
1383 _ => None,
1384 }
1385 }
1386
1387 fn handle_publish_diagnostics(
1388 &mut self,
1389 server: ServerKind,
1390 root: PathBuf,
1391 params: &serde_json::Value,
1392 ) -> Option<PathBuf> {
1393 if let Ok(publish_params) =
1394 serde_json::from_value::<lsp_types::PublishDiagnosticsParams>(params.clone())
1395 {
1396 let file = uri_to_path(&publish_params.uri)?;
1397 let stored = from_lsp_diagnostics(file.clone(), publish_params.diagnostics);
1398 let key = ServerKey { kind: server, root };
1404 self.diagnostics
1405 .publish_full(key, file.clone(), stored, None, publish_params.version);
1406 return Some(file);
1407 }
1408 None
1409 }
1410
1411 fn spawn_server(
1412 &self,
1413 def: &ServerDef,
1414 root: &Path,
1415 config: &Config,
1416 ) -> Result<LspClient, LspError> {
1417 let binary = self.resolve_binary(def, config)?;
1418
1419 let mut merged_env = def.env.clone();
1423 for (key, value) in &self.extra_env {
1424 merged_env.insert(key.clone(), value.clone());
1425 }
1426
1427 let mut client = LspClient::spawn(
1428 def.kind.clone(),
1429 root.to_path_buf(),
1430 &binary,
1431 &def.args,
1432 &merged_env,
1433 self.event_tx.clone(),
1434 self.child_registry.clone(),
1435 )?;
1436 if let Err(err) = client.initialize(root, def.initialization_options.clone()) {
1437 wait_for_stderr_tail(&mut client);
1438 let stderr_tail = client.stderr_tail();
1439 let reason = if client.child_exited() || !stderr_tail.is_empty() {
1440 format_initialize_failure_reason(&def.binary, &stderr_tail, &err)
1441 } else {
1442 format!("server failed during initialize: {err}")
1443 };
1444 return Err(LspError::ServerNotReady(reason));
1445 }
1446 Ok(client)
1447 }
1448
1449 fn resolve_binary(&self, def: &ServerDef, config: &Config) -> Result<PathBuf, LspError> {
1450 if let Some(path) = self.binary_overrides.get(&def.kind) {
1451 if path.exists() {
1452 return Ok(path.clone());
1453 }
1454 return Err(LspError::NotFound(format!(
1455 "override binary for {:?} not found: {}",
1456 def.kind,
1457 path.display()
1458 )));
1459 }
1460
1461 if let Some(path) = env_binary_override(&def.kind) {
1462 if path.exists() {
1463 return Ok(path);
1464 }
1465 return Err(LspError::NotFound(format!(
1466 "environment override binary for {:?} not found: {}",
1467 def.kind,
1468 path.display()
1469 )));
1470 }
1471
1472 resolve_lsp_binary(
1477 &def.binary,
1478 config.project_root.as_deref(),
1479 &config.lsp_paths_extra,
1480 )
1481 .ok_or_else(|| {
1482 LspError::NotFound(format!(
1483 "language server binary '{}' not found in node_modules/.bin, lsp_paths_extra, or PATH",
1484 def.binary
1485 ))
1486 })
1487 }
1488
1489 fn server_key_for_file(&self, file_path: &Path, config: &Config) -> Option<ServerKey> {
1490 for def in servers_for_file(file_path, config) {
1491 let root = find_workspace_root(file_path, &def.root_markers)?;
1492 let key = ServerKey {
1493 kind: def.kind.clone(),
1494 root,
1495 };
1496 if self.clients.contains_key(&key) {
1497 return Some(key);
1498 }
1499 }
1500 None
1501 }
1502}
1503
1504impl Default for LspManager {
1505 fn default() -> Self {
1506 Self::new()
1507 }
1508}
1509
1510fn wait_for_stderr_tail(client: &mut LspClient) {
1511 for _ in 0..10 {
1512 if !client.stderr_tail().is_empty() {
1513 break;
1514 }
1515 std::thread::sleep(std::time::Duration::from_millis(10));
1516 }
1517}
1518
1519fn recoverable_pull_rejection(err: &LspError) -> bool {
1520 matches!(
1521 err,
1522 LspError::ServerError {
1523 code: -32601 | -32602,
1524 ..
1525 }
1526 )
1527}
1528
1529fn server_attempt_result_reason(result: &ServerAttemptResult) -> String {
1530 match result {
1531 ServerAttemptResult::SpawnFailed { binary, reason } => {
1532 format!("spawn_failed: {binary} ({reason})")
1533 }
1534 ServerAttemptResult::BinaryNotInstalled { binary } => {
1535 format!("binary_not_installed: {binary}")
1536 }
1537 ServerAttemptResult::NoRootMarker { looked_for } => {
1538 format!("no_root_marker (looked for: {})", looked_for.join(", "))
1539 }
1540 ServerAttemptResult::Ok { .. } => "ok".to_string(),
1541 }
1542}
1543
1544fn format_stderr_tail_for_reason(stderr_tail: &str) -> String {
1545 truncate_stderr_tail_for_reason(stderr_tail)
1546 .lines()
1547 .map(|line| format!(" {line}"))
1548 .collect::<Vec<_>>()
1549 .join("\n")
1550}
1551
1552fn truncate_stderr_tail_for_reason(stderr_tail: &str) -> String {
1553 if stderr_tail.len() <= STDERR_REASON_BYTES {
1554 return stderr_tail.to_string();
1555 }
1556
1557 let ellipsis = "...";
1558 let target_len = STDERR_REASON_BYTES.saturating_sub(ellipsis.len());
1559 let mut start = stderr_tail.len() - target_len;
1560 while start < stderr_tail.len() && !stderr_tail.is_char_boundary(start) {
1561 start += 1;
1562 }
1563 format!("{ellipsis}{}", &stderr_tail[start..])
1564}
1565
1566fn format_initialize_failure_reason(binary: &str, stderr_tail: &str, err: &LspError) -> String {
1567 let mut reason = format!("server crashed during initialize: {err}");
1568 if !stderr_tail.is_empty() {
1569 reason.push_str("; stderr (last 64 lines):\n");
1570 reason.push_str(&format_stderr_tail_for_reason(stderr_tail));
1571 reason.push_str("\n\n");
1572 reason.push_str(&failure_hint(binary, stderr_tail));
1573 }
1574 reason
1575}
1576
1577fn format_post_initialize_exit_reason(
1578 binary: &str,
1579 status: std::process::ExitStatus,
1580 stderr_tail: &str,
1581 err: &LspError,
1582) -> String {
1583 let code = status
1584 .code()
1585 .map(|c| c.to_string())
1586 .unwrap_or_else(|| "signal/unknown".to_string());
1587 let mut reason = format!("server exited after initialize (code {code}): {err}");
1588 if !stderr_tail.is_empty() {
1589 reason.push_str("; stderr (last 64 lines):\n");
1590 reason.push_str(&format_stderr_tail_for_reason(stderr_tail));
1591 reason.push_str("\n\n");
1592 reason.push_str(&failure_hint(binary, stderr_tail));
1593 }
1594 reason
1595}
1596
1597fn failure_hint(binary: &str, stderr_tail: &str) -> String {
1598 if stderr_tail.contains("MODULE_NOT_FOUND") || stderr_tail.contains("Cannot find module") {
1599 let package_manager = infer_package_manager(stderr_tail);
1600 format!(
1601 "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."
1602 )
1603 } else {
1604 format!("Hint: see stderr above for '{binary}' failure details.")
1605 }
1606}
1607
1608fn infer_package_manager(stderr_tail: &str) -> &'static str {
1609 let lower = stderr_tail.to_ascii_lowercase();
1610 if lower.contains(".pnpm/") || lower.contains(".pnpm\\") || lower.contains("/pnpm/") {
1611 "pnpm"
1612 } else if lower.contains(".yarn/")
1613 || lower.contains(".yarn\\")
1614 || lower.contains("/yarn/")
1615 || lower.contains("yarn")
1616 {
1617 "yarn"
1618 } else {
1619 "npm"
1620 }
1621}
1622
1623fn canonicalize_for_lsp(file_path: &Path) -> Result<PathBuf, LspError> {
1624 std::fs::canonicalize(file_path).map_err(LspError::from)
1625}
1626
1627fn resolve_for_lsp_uri(file_path: &Path) -> PathBuf {
1628 if let Ok(path) = std::fs::canonicalize(file_path) {
1629 return path;
1630 }
1631
1632 let mut existing = file_path.to_path_buf();
1633 let mut missing = Vec::new();
1634 while !existing.exists() {
1635 let Some(name) = existing.file_name() else {
1636 break;
1637 };
1638 missing.push(name.to_owned());
1639 let Some(parent) = existing.parent() else {
1640 break;
1641 };
1642 existing = parent.to_path_buf();
1643 }
1644
1645 let mut resolved = std::fs::canonicalize(&existing).unwrap_or(existing);
1646 for segment in missing.into_iter().rev() {
1647 resolved.push(segment);
1648 }
1649 resolved
1650}
1651
1652fn language_id_for_extension(ext: &str) -> &'static str {
1653 match ext {
1654 "ts" => "typescript",
1655 "tsx" => "typescriptreact",
1656 "js" | "mjs" | "cjs" => "javascript",
1657 "jsx" => "javascriptreact",
1658 "py" | "pyi" => "python",
1659 "rs" => "rust",
1660 "go" => "go",
1661 "html" | "htm" => "html",
1662 _ => "plaintext",
1663 }
1664}
1665
1666fn normalize_lookup_path(path: &Path) -> PathBuf {
1667 std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
1668}
1669
1670fn classify_spawn_error(binary: &str, err: &LspError) -> ServerAttemptResult {
1677 match err {
1678 LspError::NotFound(_) => ServerAttemptResult::BinaryNotInstalled {
1683 binary: binary.to_string(),
1684 },
1685 other => ServerAttemptResult::SpawnFailed {
1686 binary: binary.to_string(),
1687 reason: other.to_string(),
1688 },
1689 }
1690}
1691
1692fn env_binary_override(kind: &ServerKind) -> Option<PathBuf> {
1693 let id = kind.id_str();
1694 let suffix: String = id
1695 .chars()
1696 .map(|ch| {
1697 if ch.is_ascii_alphanumeric() {
1698 ch.to_ascii_uppercase()
1699 } else {
1700 '_'
1701 }
1702 })
1703 .collect();
1704 let key = format!("AFT_LSP_{suffix}_BINARY");
1705 std::env::var_os(key).map(PathBuf::from)
1706}