1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3use std::str::FromStr;
4
5use crossbeam_channel::{unbounded, Receiver, RecvTimeoutError, Sender};
6use lsp_types::notification::{
7 DidChangeTextDocument, DidChangeWatchedFiles, DidCloseTextDocument, DidOpenTextDocument,
8};
9use lsp_types::{
10 DidChangeTextDocumentParams, DidChangeWatchedFilesParams, DidCloseTextDocumentParams,
11 DidOpenTextDocumentParams, FileChangeType, FileEvent, TextDocumentContentChangeEvent,
12 TextDocumentIdentifier, TextDocumentItem, VersionedTextDocumentIdentifier,
13};
14
15use crate::config::Config;
16use crate::lsp::client::{LspClient, LspEvent, ServerState};
17use crate::lsp::diagnostics::{from_lsp_diagnostics, DiagnosticsStore, StoredDiagnostic};
18use crate::lsp::document::DocumentStore;
19use crate::lsp::registry::{resolve_lsp_binary, servers_for_file, ServerDef, ServerKind};
20use crate::lsp::roots::{find_workspace_root, ServerKey};
21use crate::lsp::LspError;
22
23#[derive(Debug, Clone)]
28pub enum ServerAttemptResult {
29 Ok { server_key: ServerKey },
31 NoRootMarker { looked_for: Vec<String> },
34 BinaryNotInstalled { binary: String },
37 SpawnFailed { binary: String, reason: String },
39}
40
41#[derive(Debug, Clone)]
43pub struct ServerAttempt {
44 pub server_id: String,
46 pub server_name: String,
48 pub result: ServerAttemptResult,
49}
50
51#[derive(Debug, Clone, Default)]
57pub struct EnsureServerOutcomes {
58 pub successful: Vec<ServerKey>,
60 pub attempts: Vec<ServerAttempt>,
63}
64
65impl EnsureServerOutcomes {
66 pub fn no_server_registered(&self) -> bool {
68 self.attempts.is_empty()
69 }
70}
71
72#[derive(Debug, Clone, Default)]
82pub struct PostEditWaitOutcome {
83 pub diagnostics: Vec<StoredDiagnostic>,
87 pub pending_servers: Vec<ServerKey>,
91 pub exited_servers: Vec<ServerKey>,
95}
96
97impl PostEditWaitOutcome {
98 pub fn complete(&self) -> bool {
101 self.pending_servers.is_empty() && self.exited_servers.is_empty()
102 }
103}
104
105#[derive(Debug, Clone)]
107pub enum PullFileOutcome {
108 Full { diagnostic_count: usize },
110 Unchanged,
112 PartialNotSupported,
115 PullNotSupported,
118 RequestFailed { reason: String },
120}
121
122#[derive(Debug, Clone)]
124pub struct PullFileResult {
125 pub server_key: ServerKey,
126 pub outcome: PullFileOutcome,
127}
128
129#[derive(Debug, Clone)]
131pub struct PullWorkspaceResult {
132 pub server_key: ServerKey,
133 pub files_reported: Vec<PathBuf>,
137 pub complete: bool,
139 pub cancelled: bool,
141 pub supports_workspace: bool,
145}
146
147pub struct LspManager {
148 clients: HashMap<ServerKey, LspClient>,
150 documents: HashMap<ServerKey, DocumentStore>,
152 diagnostics: DiagnosticsStore,
154 event_tx: Sender<LspEvent>,
156 event_rx: Receiver<LspEvent>,
157 binary_overrides: HashMap<ServerKind, PathBuf>,
159 extra_env: HashMap<String, String>,
163}
164
165impl LspManager {
166 pub fn new() -> Self {
167 let (event_tx, event_rx) = unbounded();
168 Self {
169 clients: HashMap::new(),
170 documents: HashMap::new(),
171 diagnostics: DiagnosticsStore::new(),
172 event_tx,
173 event_rx,
174 binary_overrides: HashMap::new(),
175 extra_env: HashMap::new(),
176 }
177 }
178
179 pub fn set_extra_env(&mut self, key: &str, value: &str) {
183 self.extra_env.insert(key.to_string(), value.to_string());
184 }
185
186 pub fn server_count(&self) -> usize {
188 self.clients.len()
189 }
190
191 pub fn override_binary(&mut self, kind: ServerKind, binary_path: PathBuf) {
193 self.binary_overrides.insert(kind, binary_path);
194 }
195
196 pub fn ensure_server_for_file(&mut self, file_path: &Path, config: &Config) -> Vec<ServerKey> {
203 self.ensure_server_for_file_detailed(file_path, config)
204 .successful
205 }
206
207 pub fn ensure_server_for_file_detailed(
215 &mut self,
216 file_path: &Path,
217 config: &Config,
218 ) -> EnsureServerOutcomes {
219 let defs = servers_for_file(file_path, config);
220 let mut outcomes = EnsureServerOutcomes::default();
221
222 for def in defs {
223 let server_id = def.kind.id_str().to_string();
224 let server_name = def.name.to_string();
225
226 let Some(root) = find_workspace_root(file_path, &def.root_markers) else {
227 outcomes.attempts.push(ServerAttempt {
228 server_id,
229 server_name,
230 result: ServerAttemptResult::NoRootMarker {
231 looked_for: def.root_markers.iter().map(|s| s.to_string()).collect(),
232 },
233 });
234 continue;
235 };
236
237 let key = ServerKey {
238 kind: def.kind.clone(),
239 root,
240 };
241
242 if !self.clients.contains_key(&key) {
243 match self.spawn_server(&def, &key.root, config) {
244 Ok(client) => {
245 self.clients.insert(key.clone(), client);
246 self.documents.entry(key.clone()).or_default();
247 }
248 Err(err) => {
249 log::error!("failed to spawn {}: {}", def.name, err);
250 let result = classify_spawn_error(&def.binary, &err);
251 outcomes.attempts.push(ServerAttempt {
252 server_id,
253 server_name,
254 result,
255 });
256 continue;
257 }
258 }
259 }
260
261 outcomes.attempts.push(ServerAttempt {
262 server_id,
263 server_name,
264 result: ServerAttemptResult::Ok {
265 server_key: key.clone(),
266 },
267 });
268 outcomes.successful.push(key);
269 }
270
271 outcomes
272 }
273
274 pub fn ensure_server_for_file_default(&mut self, file_path: &Path) -> Vec<ServerKey> {
277 self.ensure_server_for_file(file_path, &Config::default())
278 }
279 pub fn ensure_file_open(
283 &mut self,
284 file_path: &Path,
285 config: &Config,
286 ) -> Result<Vec<ServerKey>, LspError> {
287 let canonical_path = canonicalize_for_lsp(file_path)?;
288 let server_keys = self.ensure_server_for_file(&canonical_path, config);
289 if server_keys.is_empty() {
290 return Ok(server_keys);
291 }
292
293 let uri = uri_for_path(&canonical_path)?;
294 let language_id = language_id_for_extension(
295 canonical_path
296 .extension()
297 .and_then(|ext| ext.to_str())
298 .unwrap_or_default(),
299 )
300 .to_string();
301
302 for key in &server_keys {
303 let already_open = self
304 .documents
305 .get(key)
306 .is_some_and(|store| store.is_open(&canonical_path));
307
308 if !already_open {
309 let content = std::fs::read_to_string(&canonical_path).map_err(LspError::Io)?;
310 if let Some(client) = self.clients.get_mut(key) {
311 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
312 text_document: TextDocumentItem::new(
313 uri.clone(),
314 language_id.clone(),
315 0,
316 content,
317 ),
318 })?;
319 }
320 self.documents
321 .entry(key.clone())
322 .or_default()
323 .open(canonical_path.clone());
324 continue;
325 }
326
327 let drifted = self
337 .documents
338 .get(key)
339 .is_some_and(|store| store.is_stale_on_disk(&canonical_path));
340 if drifted {
341 let content = std::fs::read_to_string(&canonical_path).map_err(LspError::Io)?;
342 let next_version = self
343 .documents
344 .get(key)
345 .and_then(|store| store.version(&canonical_path))
346 .map(|v| v + 1)
347 .unwrap_or(1);
348 if let Some(client) = self.clients.get_mut(key) {
349 client.send_notification::<DidChangeTextDocument>(
350 DidChangeTextDocumentParams {
351 text_document: VersionedTextDocumentIdentifier::new(
352 uri.clone(),
353 next_version,
354 ),
355 content_changes: vec![TextDocumentContentChangeEvent {
356 range: None,
357 range_length: None,
358 text: content,
359 }],
360 },
361 )?;
362 }
363 if let Some(store) = self.documents.get_mut(key) {
364 store.bump_version(&canonical_path);
365 }
366 }
367 }
368
369 Ok(server_keys)
370 }
371
372 pub fn ensure_file_open_default(
373 &mut self,
374 file_path: &Path,
375 ) -> Result<Vec<ServerKey>, LspError> {
376 self.ensure_file_open(file_path, &Config::default())
377 }
378
379 pub fn notify_file_changed(
385 &mut self,
386 file_path: &Path,
387 content: &str,
388 config: &Config,
389 ) -> Result<(), LspError> {
390 self.notify_file_changed_versioned(file_path, content, config)
391 .map(|_| ())
392 }
393
394 pub fn notify_file_changed_versioned(
405 &mut self,
406 file_path: &Path,
407 content: &str,
408 config: &Config,
409 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
410 let canonical_path = canonicalize_for_lsp(file_path)?;
411 let server_keys = self.ensure_server_for_file(&canonical_path, config);
412 if server_keys.is_empty() {
413 return Ok(Vec::new());
414 }
415
416 let uri = uri_for_path(&canonical_path)?;
417 let language_id = language_id_for_extension(
418 canonical_path
419 .extension()
420 .and_then(|ext| ext.to_str())
421 .unwrap_or_default(),
422 )
423 .to_string();
424
425 let mut versions: Vec<(ServerKey, i32)> = Vec::with_capacity(server_keys.len());
426
427 for key in server_keys {
428 let current_version = self
429 .documents
430 .get(&key)
431 .and_then(|store| store.version(&canonical_path));
432
433 if let Some(version) = current_version {
434 let next_version = version + 1;
435 if let Some(client) = self.clients.get_mut(&key) {
436 client.send_notification::<DidChangeTextDocument>(
437 DidChangeTextDocumentParams {
438 text_document: VersionedTextDocumentIdentifier::new(
439 uri.clone(),
440 next_version,
441 ),
442 content_changes: vec![TextDocumentContentChangeEvent {
443 range: None,
444 range_length: None,
445 text: content.to_string(),
446 }],
447 },
448 )?;
449 }
450 if let Some(store) = self.documents.get_mut(&key) {
451 store.bump_version(&canonical_path);
452 }
453 versions.push((key, next_version));
454 continue;
455 }
456
457 if let Some(client) = self.clients.get_mut(&key) {
458 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
459 text_document: TextDocumentItem::new(
460 uri.clone(),
461 language_id.clone(),
462 0,
463 content.to_string(),
464 ),
465 })?;
466 }
467 self.documents
468 .entry(key.clone())
469 .or_default()
470 .open(canonical_path.clone());
471 versions.push((key, 0));
474 }
475
476 Ok(versions)
477 }
478
479 pub fn notify_file_changed_default(
480 &mut self,
481 file_path: &Path,
482 content: &str,
483 ) -> Result<(), LspError> {
484 self.notify_file_changed(file_path, content, &Config::default())
485 }
486
487 pub fn notify_files_watched_changed(
493 &mut self,
494 paths: &[(PathBuf, FileChangeType)],
495 _config: &Config,
496 ) -> Result<(), LspError> {
497 if paths.is_empty() {
498 return Ok(());
499 }
500
501 let mut canonical_events = Vec::with_capacity(paths.len());
502 for (path, typ) in paths {
503 let canonical_path = resolve_for_lsp_uri(path);
504 canonical_events.push((canonical_path, *typ));
505 }
506
507 let keys: Vec<ServerKey> = self.clients.keys().cloned().collect();
508 for key in keys {
509 let mut changes = Vec::new();
510 for (path, typ) in &canonical_events {
511 if !path.starts_with(&key.root) {
512 continue;
513 }
514 changes.push(FileEvent::new(uri_for_path(path)?, *typ));
515 }
516
517 if changes.is_empty() {
518 continue;
519 }
520
521 if let Some(client) = self.clients.get_mut(&key) {
522 if !client.supports_watched_files() {
527 log::debug!(
528 "[aft-lsp] skipping didChangeWatchedFiles for {:?} (capability not declared)",
529 key
530 );
531 continue;
532 }
533 client.send_notification::<DidChangeWatchedFiles>(DidChangeWatchedFilesParams {
534 changes,
535 })?;
536 }
537 }
538
539 Ok(())
540 }
541
542 pub fn notify_file_closed(&mut self, file_path: &Path) -> Result<(), LspError> {
544 let canonical_path = canonicalize_for_lsp(file_path)?;
545 let uri = uri_for_path(&canonical_path)?;
546 let keys: Vec<ServerKey> = self.documents.keys().cloned().collect();
547
548 for key in keys {
549 let was_open = self
550 .documents
551 .get(&key)
552 .map(|store| store.is_open(&canonical_path))
553 .unwrap_or(false);
554 if !was_open {
555 continue;
556 }
557
558 if let Some(client) = self.clients.get_mut(&key) {
559 client.send_notification::<DidCloseTextDocument>(DidCloseTextDocumentParams {
560 text_document: TextDocumentIdentifier::new(uri.clone()),
561 })?;
562 }
563
564 if let Some(store) = self.documents.get_mut(&key) {
565 store.close(&canonical_path);
566 }
567 }
568
569 Ok(())
570 }
571
572 pub fn client_for_file(&self, file_path: &Path, config: &Config) -> Option<&LspClient> {
574 let key = self.server_key_for_file(file_path, config)?;
575 self.clients.get(&key)
576 }
577
578 pub fn client_for_file_default(&self, file_path: &Path) -> Option<&LspClient> {
579 self.client_for_file(file_path, &Config::default())
580 }
581
582 pub fn client_for_file_mut(
584 &mut self,
585 file_path: &Path,
586 config: &Config,
587 ) -> Option<&mut LspClient> {
588 let key = self.server_key_for_file(file_path, config)?;
589 self.clients.get_mut(&key)
590 }
591
592 pub fn client_for_file_mut_default(&mut self, file_path: &Path) -> Option<&mut LspClient> {
593 self.client_for_file_mut(file_path, &Config::default())
594 }
595
596 pub fn active_client_count(&self) -> usize {
598 self.clients.len()
599 }
600
601 pub fn drain_events(&mut self) -> Vec<LspEvent> {
603 let mut events = Vec::new();
604 while let Ok(event) = self.event_rx.try_recv() {
605 self.handle_event(&event);
606 events.push(event);
607 }
608 events
609 }
610
611 pub fn wait_for_diagnostics(
613 &mut self,
614 file_path: &Path,
615 config: &Config,
616 timeout: std::time::Duration,
617 ) -> Vec<StoredDiagnostic> {
618 let deadline = std::time::Instant::now() + timeout;
619 self.wait_for_file_diagnostics(file_path, config, deadline)
620 }
621
622 pub fn wait_for_diagnostics_default(
623 &mut self,
624 file_path: &Path,
625 timeout: std::time::Duration,
626 ) -> Vec<StoredDiagnostic> {
627 self.wait_for_diagnostics(file_path, &Config::default(), timeout)
628 }
629
630 #[doc(hidden)]
635 pub fn diagnostics_store_for_test(&self) -> &DiagnosticsStore {
636 &self.diagnostics
637 }
638
639 pub fn snapshot_diagnostic_epochs(&self, file_path: &Path) -> HashMap<ServerKey, u64> {
644 let lookup_path = normalize_lookup_path(file_path);
645 self.diagnostics
646 .entries_for_file(&lookup_path)
647 .into_iter()
648 .map(|(key, entry)| (key.clone(), entry.epoch))
649 .collect()
650 }
651
652 pub fn wait_for_post_edit_diagnostics(
675 &mut self,
676 file_path: &Path,
677 _config: &Config,
681 expected_versions: &[(ServerKey, i32)],
682 pre_snapshot: &HashMap<ServerKey, u64>,
683 timeout: std::time::Duration,
684 ) -> PostEditWaitOutcome {
685 let lookup_path = normalize_lookup_path(file_path);
686 let deadline = std::time::Instant::now() + timeout;
687
688 let _ = self.drain_events_for_file(&lookup_path);
693
694 let mut fresh: HashMap<ServerKey, Vec<StoredDiagnostic>> = HashMap::new();
695 let mut exited: Vec<ServerKey> = Vec::new();
696
697 loop {
698 for (key, target_version) in expected_versions {
705 if fresh.contains_key(key) || exited.contains(key) {
706 continue;
707 }
708 if !self.clients.contains_key(key) {
709 exited.push(key.clone());
710 continue;
711 }
712 if let Some(entry) = self
713 .diagnostics
714 .entries_for_file(&lookup_path)
715 .into_iter()
716 .find_map(|(k, e)| if k == key { Some(e) } else { None })
717 {
718 let is_fresh = match entry.version {
719 Some(v) => v == *target_version,
720 None => {
721 let pre = pre_snapshot.get(key).copied().unwrap_or(0);
722 entry.epoch > pre
723 }
724 };
725 if is_fresh {
726 fresh.insert(key.clone(), entry.diagnostics.clone());
727 }
728 }
729 }
730
731 if fresh.len() + exited.len() == expected_versions.len() {
733 break;
734 }
735
736 let now = std::time::Instant::now();
737 if now >= deadline {
738 break;
739 }
740
741 let timeout = deadline.saturating_duration_since(now);
742 match self.event_rx.recv_timeout(timeout) {
743 Ok(event) => {
744 self.handle_event(&event);
745 }
746 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
747 }
748 }
749
750 let pending: Vec<ServerKey> = expected_versions
752 .iter()
753 .filter(|(k, _)| !fresh.contains_key(k) && !exited.contains(k))
754 .map(|(k, _)| k.clone())
755 .collect();
756
757 let mut diagnostics: Vec<StoredDiagnostic> = fresh
760 .into_iter()
761 .flat_map(|(_, diags)| diags.into_iter())
762 .collect();
763 diagnostics.sort_by(|a, b| {
764 a.file
765 .cmp(&b.file)
766 .then(a.line.cmp(&b.line))
767 .then(a.column.cmp(&b.column))
768 .then(a.message.cmp(&b.message))
769 });
770
771 PostEditWaitOutcome {
772 diagnostics,
773 pending_servers: pending,
774 exited_servers: exited,
775 }
776 }
777
778 pub fn wait_for_file_diagnostics(
784 &mut self,
785 file_path: &Path,
786 config: &Config,
787 deadline: std::time::Instant,
788 ) -> Vec<StoredDiagnostic> {
789 let lookup_path = normalize_lookup_path(file_path);
790
791 if self.server_key_for_file(&lookup_path, config).is_none() {
792 return Vec::new();
793 }
794
795 loop {
796 if self.drain_events_for_file(&lookup_path) {
797 break;
798 }
799
800 let now = std::time::Instant::now();
801 if now >= deadline {
802 break;
803 }
804
805 let timeout = deadline.saturating_duration_since(now);
806 match self.event_rx.recv_timeout(timeout) {
807 Ok(event) => {
808 if matches!(
809 self.handle_event(&event),
810 Some(ref published_file) if published_file.as_path() == lookup_path.as_path()
811 ) {
812 break;
813 }
814 }
815 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
816 }
817 }
818
819 self.get_diagnostics_for_file(&lookup_path)
820 .into_iter()
821 .cloned()
822 .collect()
823 }
824
825 pub const PULL_FILE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
831
832 pub fn pull_file_timeout() -> std::time::Duration {
834 Self::PULL_FILE_TIMEOUT
835 }
836
837 const PULL_WORKSPACE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
841
842 pub fn pull_file_diagnostics(
853 &mut self,
854 file_path: &Path,
855 config: &Config,
856 ) -> Result<Vec<PullFileResult>, LspError> {
857 let canonical_path = canonicalize_for_lsp(file_path)?;
858 self.ensure_file_open(&canonical_path, config)?;
861
862 let server_keys = self.ensure_server_for_file(&canonical_path, config);
863 if server_keys.is_empty() {
864 return Ok(Vec::new());
865 }
866
867 let uri = uri_for_path(&canonical_path)?;
868 let mut results = Vec::with_capacity(server_keys.len());
869
870 for key in server_keys {
871 let supports_pull = self
872 .clients
873 .get(&key)
874 .and_then(|c| c.diagnostic_capabilities())
875 .is_some_and(|caps| caps.pull_diagnostics);
876
877 if !supports_pull {
878 results.push(PullFileResult {
879 server_key: key.clone(),
880 outcome: PullFileOutcome::PullNotSupported,
881 });
882 continue;
883 }
884
885 let previous_result_id = self
887 .diagnostics
888 .entries_for_file(&canonical_path)
889 .into_iter()
890 .find(|(k, _)| **k == key)
891 .and_then(|(_, entry)| entry.result_id.clone());
892
893 let identifier = self
894 .clients
895 .get(&key)
896 .and_then(|c| c.diagnostic_capabilities())
897 .and_then(|caps| caps.identifier.clone());
898
899 let params = lsp_types::DocumentDiagnosticParams {
900 text_document: lsp_types::TextDocumentIdentifier { uri: uri.clone() },
901 identifier,
902 previous_result_id,
903 work_done_progress_params: Default::default(),
904 partial_result_params: Default::default(),
905 };
906
907 let outcome = match self.send_pull_request(&key, params) {
908 Ok(report) => self.ingest_document_report(&key, &canonical_path, report),
909 Err(err) => PullFileOutcome::RequestFailed {
910 reason: err.to_string(),
911 },
912 };
913
914 results.push(PullFileResult {
915 server_key: key,
916 outcome,
917 });
918 }
919
920 Ok(results)
921 }
922
923 pub fn pull_workspace_diagnostics(
928 &mut self,
929 server_key: &ServerKey,
930 timeout: Option<std::time::Duration>,
931 ) -> Result<PullWorkspaceResult, LspError> {
932 let _timeout = timeout.unwrap_or(Self::PULL_WORKSPACE_TIMEOUT);
933
934 let supports_workspace = self
935 .clients
936 .get(server_key)
937 .and_then(|c| c.diagnostic_capabilities())
938 .is_some_and(|caps| caps.workspace_diagnostics);
939
940 if !supports_workspace {
941 return Ok(PullWorkspaceResult {
942 server_key: server_key.clone(),
943 files_reported: Vec::new(),
944 complete: false,
945 cancelled: false,
946 supports_workspace: false,
947 });
948 }
949
950 let identifier = self
951 .clients
952 .get(server_key)
953 .and_then(|c| c.diagnostic_capabilities())
954 .and_then(|caps| caps.identifier.clone());
955
956 let params = lsp_types::WorkspaceDiagnosticParams {
957 identifier,
958 previous_result_ids: Vec::new(),
959 work_done_progress_params: Default::default(),
960 partial_result_params: Default::default(),
961 };
962
963 let result = match self
971 .clients
972 .get_mut(server_key)
973 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?
974 .send_request::<lsp_types::request::WorkspaceDiagnosticRequest>(params)
975 {
976 Ok(result) => result,
977 Err(LspError::Timeout(_)) => {
978 return Ok(PullWorkspaceResult {
979 server_key: server_key.clone(),
980 files_reported: Vec::new(),
981 complete: false,
982 cancelled: true,
983 supports_workspace: true,
984 });
985 }
986 Err(err) => return Err(err),
987 };
988
989 let items = match result {
994 lsp_types::WorkspaceDiagnosticReportResult::Report(report) => report.items,
995 lsp_types::WorkspaceDiagnosticReportResult::Partial(_) => Vec::new(),
996 };
997
998 let mut files_reported = Vec::with_capacity(items.len());
1000 for item in items {
1001 match item {
1002 lsp_types::WorkspaceDocumentDiagnosticReport::Full(full) => {
1003 if let Some(file) = uri_to_path(&full.uri) {
1004 let stored = from_lsp_diagnostics(
1005 file.clone(),
1006 full.full_document_diagnostic_report.items.clone(),
1007 );
1008 self.diagnostics.publish_with_result_id(
1009 server_key.clone(),
1010 file.clone(),
1011 stored,
1012 full.full_document_diagnostic_report.result_id.clone(),
1013 );
1014 files_reported.push(file);
1015 }
1016 }
1017 lsp_types::WorkspaceDocumentDiagnosticReport::Unchanged(_unchanged) => {
1018 }
1021 }
1022 }
1023
1024 Ok(PullWorkspaceResult {
1025 server_key: server_key.clone(),
1026 files_reported,
1027 complete: true,
1028 cancelled: false,
1029 supports_workspace: true,
1030 })
1031 }
1032
1033 fn send_pull_request(
1035 &mut self,
1036 key: &ServerKey,
1037 params: lsp_types::DocumentDiagnosticParams,
1038 ) -> Result<lsp_types::DocumentDiagnosticReportResult, LspError> {
1039 let client = self
1040 .clients
1041 .get_mut(key)
1042 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?;
1043 client.send_request::<lsp_types::request::DocumentDiagnosticRequest>(params)
1044 }
1045
1046 fn ingest_document_report(
1049 &mut self,
1050 key: &ServerKey,
1051 canonical_path: &Path,
1052 result: lsp_types::DocumentDiagnosticReportResult,
1053 ) -> PullFileOutcome {
1054 let report = match result {
1055 lsp_types::DocumentDiagnosticReportResult::Report(report) => report,
1056 lsp_types::DocumentDiagnosticReportResult::Partial(_) => {
1057 return PullFileOutcome::PartialNotSupported;
1061 }
1062 };
1063
1064 match report {
1065 lsp_types::DocumentDiagnosticReport::Full(full) => {
1066 let result_id = full.full_document_diagnostic_report.result_id.clone();
1067 let stored = from_lsp_diagnostics(
1068 canonical_path.to_path_buf(),
1069 full.full_document_diagnostic_report.items.clone(),
1070 );
1071 let count = stored.len();
1072 self.diagnostics.publish_with_result_id(
1073 key.clone(),
1074 canonical_path.to_path_buf(),
1075 stored,
1076 result_id,
1077 );
1078 PullFileOutcome::Full {
1079 diagnostic_count: count,
1080 }
1081 }
1082 lsp_types::DocumentDiagnosticReport::Unchanged(_unchanged) => {
1083 PullFileOutcome::Unchanged
1086 }
1087 }
1088 }
1089
1090 pub fn shutdown_all(&mut self) {
1092 for (key, mut client) in self.clients.drain() {
1093 if let Err(err) = client.shutdown() {
1094 log::error!("error shutting down {:?}: {}", key, err);
1095 }
1096 }
1097 self.documents.clear();
1098 self.diagnostics = DiagnosticsStore::new();
1099 }
1100
1101 pub fn has_active_servers(&self) -> bool {
1103 self.clients
1104 .values()
1105 .any(|client| client.state() == ServerState::Ready)
1106 }
1107
1108 pub fn active_server_keys(&self) -> Vec<ServerKey> {
1111 self.clients.keys().cloned().collect()
1112 }
1113
1114 pub fn get_diagnostics_for_file(&self, file: &Path) -> Vec<&StoredDiagnostic> {
1115 let normalized = normalize_lookup_path(file);
1116 self.diagnostics.for_file(&normalized)
1117 }
1118
1119 pub fn get_diagnostics_for_directory(&self, dir: &Path) -> Vec<&StoredDiagnostic> {
1120 let normalized = normalize_lookup_path(dir);
1121 self.diagnostics.for_directory(&normalized)
1122 }
1123
1124 pub fn get_all_diagnostics(&self) -> Vec<&StoredDiagnostic> {
1125 self.diagnostics.all()
1126 }
1127
1128 fn drain_events_for_file(&mut self, file_path: &Path) -> bool {
1129 let mut saw_file_diagnostics = false;
1130 while let Ok(event) = self.event_rx.try_recv() {
1131 if matches!(
1132 self.handle_event(&event),
1133 Some(ref published_file) if published_file.as_path() == file_path
1134 ) {
1135 saw_file_diagnostics = true;
1136 }
1137 }
1138 saw_file_diagnostics
1139 }
1140
1141 fn handle_event(&mut self, event: &LspEvent) -> Option<PathBuf> {
1142 match event {
1143 LspEvent::Notification {
1144 server_kind,
1145 root,
1146 method,
1147 params: Some(params),
1148 } if method == "textDocument/publishDiagnostics" => {
1149 self.handle_publish_diagnostics(server_kind.clone(), root.clone(), params)
1150 }
1151 LspEvent::ServerExited { server_kind, root } => {
1152 let key = ServerKey {
1153 kind: server_kind.clone(),
1154 root: root.clone(),
1155 };
1156 self.clients.remove(&key);
1157 self.documents.remove(&key);
1158 self.diagnostics.clear_server(server_kind.clone());
1159 None
1160 }
1161 _ => None,
1162 }
1163 }
1164
1165 fn handle_publish_diagnostics(
1166 &mut self,
1167 server: ServerKind,
1168 root: PathBuf,
1169 params: &serde_json::Value,
1170 ) -> Option<PathBuf> {
1171 if let Ok(publish_params) =
1172 serde_json::from_value::<lsp_types::PublishDiagnosticsParams>(params.clone())
1173 {
1174 let file = uri_to_path(&publish_params.uri)?;
1175 let stored = from_lsp_diagnostics(file.clone(), publish_params.diagnostics);
1176 let key = ServerKey { kind: server, root };
1182 self.diagnostics
1183 .publish_full(key, file.clone(), stored, None, publish_params.version);
1184 return Some(file);
1185 }
1186 None
1187 }
1188
1189 fn spawn_server(
1190 &self,
1191 def: &ServerDef,
1192 root: &Path,
1193 config: &Config,
1194 ) -> Result<LspClient, LspError> {
1195 let binary = self.resolve_binary(def, config)?;
1196
1197 let mut merged_env = def.env.clone();
1201 for (key, value) in &self.extra_env {
1202 merged_env.insert(key.clone(), value.clone());
1203 }
1204
1205 let mut client = LspClient::spawn(
1206 def.kind.clone(),
1207 root.to_path_buf(),
1208 &binary,
1209 &def.args,
1210 &merged_env,
1211 self.event_tx.clone(),
1212 )?;
1213 client.initialize(root, def.initialization_options.clone())?;
1214 Ok(client)
1215 }
1216
1217 fn resolve_binary(&self, def: &ServerDef, config: &Config) -> Result<PathBuf, LspError> {
1218 if let Some(path) = self.binary_overrides.get(&def.kind) {
1219 if path.exists() {
1220 return Ok(path.clone());
1221 }
1222 return Err(LspError::NotFound(format!(
1223 "override binary for {:?} not found: {}",
1224 def.kind,
1225 path.display()
1226 )));
1227 }
1228
1229 if let Some(path) = env_binary_override(&def.kind) {
1230 if path.exists() {
1231 return Ok(path);
1232 }
1233 return Err(LspError::NotFound(format!(
1234 "environment override binary for {:?} not found: {}",
1235 def.kind,
1236 path.display()
1237 )));
1238 }
1239
1240 resolve_lsp_binary(
1245 &def.binary,
1246 config.project_root.as_deref(),
1247 &config.lsp_paths_extra,
1248 )
1249 .ok_or_else(|| {
1250 LspError::NotFound(format!(
1251 "language server binary '{}' not found in node_modules/.bin, lsp_paths_extra, or PATH",
1252 def.binary
1253 ))
1254 })
1255 }
1256
1257 fn server_key_for_file(&self, file_path: &Path, config: &Config) -> Option<ServerKey> {
1258 for def in servers_for_file(file_path, config) {
1259 let root = find_workspace_root(file_path, &def.root_markers)?;
1260 let key = ServerKey {
1261 kind: def.kind.clone(),
1262 root,
1263 };
1264 if self.clients.contains_key(&key) {
1265 return Some(key);
1266 }
1267 }
1268 None
1269 }
1270}
1271
1272impl Default for LspManager {
1273 fn default() -> Self {
1274 Self::new()
1275 }
1276}
1277
1278fn canonicalize_for_lsp(file_path: &Path) -> Result<PathBuf, LspError> {
1279 std::fs::canonicalize(file_path).map_err(LspError::from)
1280}
1281
1282fn resolve_for_lsp_uri(file_path: &Path) -> PathBuf {
1283 if let Ok(path) = std::fs::canonicalize(file_path) {
1284 return path;
1285 }
1286
1287 let mut existing = file_path.to_path_buf();
1288 let mut missing = Vec::new();
1289 while !existing.exists() {
1290 let Some(name) = existing.file_name() else {
1291 break;
1292 };
1293 missing.push(name.to_owned());
1294 let Some(parent) = existing.parent() else {
1295 break;
1296 };
1297 existing = parent.to_path_buf();
1298 }
1299
1300 let mut resolved = std::fs::canonicalize(&existing).unwrap_or(existing);
1301 for segment in missing.into_iter().rev() {
1302 resolved.push(segment);
1303 }
1304 resolved
1305}
1306
1307fn uri_for_path(path: &Path) -> Result<lsp_types::Uri, LspError> {
1308 let url = url::Url::from_file_path(path).map_err(|_| {
1309 LspError::NotFound(format!(
1310 "failed to convert '{}' to file URI",
1311 path.display()
1312 ))
1313 })?;
1314 lsp_types::Uri::from_str(url.as_str()).map_err(|_| {
1315 LspError::NotFound(format!("failed to parse file URI for '{}'", path.display()))
1316 })
1317}
1318
1319fn language_id_for_extension(ext: &str) -> &'static str {
1320 match ext {
1321 "ts" => "typescript",
1322 "tsx" => "typescriptreact",
1323 "js" | "mjs" | "cjs" => "javascript",
1324 "jsx" => "javascriptreact",
1325 "py" | "pyi" => "python",
1326 "rs" => "rust",
1327 "go" => "go",
1328 "html" | "htm" => "html",
1329 _ => "plaintext",
1330 }
1331}
1332
1333fn normalize_lookup_path(path: &Path) -> PathBuf {
1334 std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
1335}
1336
1337fn uri_to_path(uri: &lsp_types::Uri) -> Option<PathBuf> {
1338 let url = url::Url::parse(uri.as_str()).ok()?;
1339 url.to_file_path()
1340 .ok()
1341 .map(|path| normalize_lookup_path(&path))
1342}
1343
1344fn classify_spawn_error(binary: &str, err: &LspError) -> ServerAttemptResult {
1351 match err {
1352 LspError::NotFound(_) => ServerAttemptResult::BinaryNotInstalled {
1357 binary: binary.to_string(),
1358 },
1359 other => ServerAttemptResult::SpawnFailed {
1360 binary: binary.to_string(),
1361 reason: other.to_string(),
1362 },
1363 }
1364}
1365
1366fn env_binary_override(kind: &ServerKind) -> Option<PathBuf> {
1367 let id = kind.id_str();
1368 let suffix: String = id
1369 .chars()
1370 .map(|ch| {
1371 if ch.is_ascii_alphanumeric() {
1372 ch.to_ascii_uppercase()
1373 } else {
1374 '_'
1375 }
1376 })
1377 .collect();
1378 let key = format!("AFT_LSP_{suffix}_BINARY");
1379 std::env::var_os(key).map(PathBuf::from)
1380}