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;
22use crate::slog_error;
23
24#[derive(Debug, Clone)]
29pub enum ServerAttemptResult {
30 Ok { server_key: ServerKey },
32 NoRootMarker { looked_for: Vec<String> },
35 BinaryNotInstalled { binary: String },
38 SpawnFailed { binary: String, reason: String },
40}
41
42#[derive(Debug, Clone)]
44pub struct ServerAttempt {
45 pub server_id: String,
47 pub server_name: String,
49 pub result: ServerAttemptResult,
50}
51
52#[derive(Debug, Clone, Default)]
58pub struct EnsureServerOutcomes {
59 pub successful: Vec<ServerKey>,
61 pub attempts: Vec<ServerAttempt>,
64}
65
66impl EnsureServerOutcomes {
67 pub fn no_server_registered(&self) -> bool {
69 self.attempts.is_empty()
70 }
71}
72
73#[derive(Debug, Clone, Default)]
83pub struct PostEditWaitOutcome {
84 pub diagnostics: Vec<StoredDiagnostic>,
88 pub pending_servers: Vec<ServerKey>,
92 pub exited_servers: Vec<ServerKey>,
96}
97
98impl PostEditWaitOutcome {
99 pub fn complete(&self) -> bool {
102 self.pending_servers.is_empty() && self.exited_servers.is_empty()
103 }
104}
105
106#[derive(Debug, Clone)]
108pub enum PullFileOutcome {
109 Full { diagnostic_count: usize },
111 Unchanged,
113 PartialNotSupported,
116 PullNotSupported,
119 RequestFailed { reason: String },
121}
122
123#[derive(Debug, Clone)]
125pub struct PullFileResult {
126 pub server_key: ServerKey,
127 pub outcome: PullFileOutcome,
128}
129
130#[derive(Debug, Clone)]
132pub struct PullWorkspaceResult {
133 pub server_key: ServerKey,
134 pub files_reported: Vec<PathBuf>,
138 pub complete: bool,
140 pub cancelled: bool,
142 pub supports_workspace: bool,
146}
147
148pub struct LspManager {
149 clients: HashMap<ServerKey, LspClient>,
151 documents: HashMap<ServerKey, DocumentStore>,
153 diagnostics: DiagnosticsStore,
155 event_tx: Sender<LspEvent>,
157 event_rx: Receiver<LspEvent>,
158 binary_overrides: HashMap<ServerKind, PathBuf>,
160 extra_env: HashMap<String, String>,
164 failed_spawns: HashMap<ServerKey, ServerAttemptResult>,
179}
180
181impl LspManager {
182 pub fn new() -> Self {
183 let (event_tx, event_rx) = unbounded();
184 Self {
185 clients: HashMap::new(),
186 documents: HashMap::new(),
187 diagnostics: DiagnosticsStore::new(),
188 event_tx,
189 event_rx,
190 binary_overrides: HashMap::new(),
191 extra_env: HashMap::new(),
192 failed_spawns: HashMap::new(),
193 }
194 }
195
196 pub fn set_extra_env(&mut self, key: &str, value: &str) {
200 self.extra_env.insert(key.to_string(), value.to_string());
201 }
202
203 pub fn server_count(&self) -> usize {
205 self.clients.len()
206 }
207
208 pub fn override_binary(&mut self, kind: ServerKind, binary_path: PathBuf) {
210 self.binary_overrides.insert(kind, binary_path);
211 }
212
213 pub fn ensure_server_for_file(&mut self, file_path: &Path, config: &Config) -> Vec<ServerKey> {
220 self.ensure_server_for_file_detailed(file_path, config)
221 .successful
222 }
223
224 pub fn ensure_server_for_file_detailed(
232 &mut self,
233 file_path: &Path,
234 config: &Config,
235 ) -> EnsureServerOutcomes {
236 let defs = servers_for_file(file_path, config);
237 let mut outcomes = EnsureServerOutcomes::default();
238
239 for def in defs {
240 let server_id = def.kind.id_str().to_string();
241 let server_name = def.name.to_string();
242
243 let Some(root) = find_workspace_root(file_path, &def.root_markers) else {
244 outcomes.attempts.push(ServerAttempt {
245 server_id,
246 server_name,
247 result: ServerAttemptResult::NoRootMarker {
248 looked_for: def.root_markers.iter().map(|s| s.to_string()).collect(),
249 },
250 });
251 continue;
252 };
253
254 let key = ServerKey {
255 kind: def.kind.clone(),
256 root,
257 };
258
259 if !self.clients.contains_key(&key) {
260 if let Some(cached) = self.failed_spawns.get(&key) {
267 outcomes.attempts.push(ServerAttempt {
268 server_id,
269 server_name,
270 result: cached.clone(),
271 });
272 continue;
273 }
274
275 match self.spawn_server(&def, &key.root, config) {
276 Ok(client) => {
277 self.clients.insert(key.clone(), client);
278 self.documents.entry(key.clone()).or_default();
279 }
280 Err(err) => {
281 slog_error!("failed to spawn {}: {}", def.name, err);
282 let result = classify_spawn_error(&def.binary, &err);
283 self.failed_spawns.insert(key.clone(), result.clone());
287 outcomes.attempts.push(ServerAttempt {
288 server_id,
289 server_name,
290 result,
291 });
292 continue;
293 }
294 }
295 }
296
297 outcomes.attempts.push(ServerAttempt {
298 server_id,
299 server_name,
300 result: ServerAttemptResult::Ok {
301 server_key: key.clone(),
302 },
303 });
304 outcomes.successful.push(key);
305 }
306
307 outcomes
308 }
309
310 pub fn ensure_server_for_file_default(&mut self, file_path: &Path) -> Vec<ServerKey> {
313 self.ensure_server_for_file(file_path, &Config::default())
314 }
315 pub fn ensure_file_open(
319 &mut self,
320 file_path: &Path,
321 config: &Config,
322 ) -> Result<Vec<ServerKey>, LspError> {
323 let canonical_path = canonicalize_for_lsp(file_path)?;
324 let server_keys = self.ensure_server_for_file(&canonical_path, config);
325 if server_keys.is_empty() {
326 return Ok(server_keys);
327 }
328
329 let uri = uri_for_path(&canonical_path)?;
330 let language_id = language_id_for_extension(
331 canonical_path
332 .extension()
333 .and_then(|ext| ext.to_str())
334 .unwrap_or_default(),
335 )
336 .to_string();
337
338 for key in &server_keys {
339 let already_open = self
340 .documents
341 .get(key)
342 .is_some_and(|store| store.is_open(&canonical_path));
343
344 if !already_open {
345 let content = std::fs::read_to_string(&canonical_path).map_err(LspError::Io)?;
346 if let Some(client) = self.clients.get_mut(key) {
347 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
348 text_document: TextDocumentItem::new(
349 uri.clone(),
350 language_id.clone(),
351 0,
352 content,
353 ),
354 })?;
355 }
356 self.documents
357 .entry(key.clone())
358 .or_default()
359 .open(canonical_path.clone());
360 continue;
361 }
362
363 let drifted = self
373 .documents
374 .get(key)
375 .is_some_and(|store| store.is_stale_on_disk(&canonical_path));
376 if drifted {
377 let content = std::fs::read_to_string(&canonical_path).map_err(LspError::Io)?;
378 let next_version = self
379 .documents
380 .get(key)
381 .and_then(|store| store.version(&canonical_path))
382 .map(|v| v + 1)
383 .unwrap_or(1);
384 if let Some(client) = self.clients.get_mut(key) {
385 client.send_notification::<DidChangeTextDocument>(
386 DidChangeTextDocumentParams {
387 text_document: VersionedTextDocumentIdentifier::new(
388 uri.clone(),
389 next_version,
390 ),
391 content_changes: vec![TextDocumentContentChangeEvent {
392 range: None,
393 range_length: None,
394 text: content,
395 }],
396 },
397 )?;
398 }
399 if let Some(store) = self.documents.get_mut(key) {
400 store.bump_version(&canonical_path);
401 }
402 }
403 }
404
405 Ok(server_keys)
406 }
407
408 pub fn ensure_file_open_default(
409 &mut self,
410 file_path: &Path,
411 ) -> Result<Vec<ServerKey>, LspError> {
412 self.ensure_file_open(file_path, &Config::default())
413 }
414
415 pub fn notify_file_changed(
421 &mut self,
422 file_path: &Path,
423 content: &str,
424 config: &Config,
425 ) -> Result<(), LspError> {
426 self.notify_file_changed_versioned(file_path, content, config)
427 .map(|_| ())
428 }
429
430 pub fn notify_file_changed_versioned(
441 &mut self,
442 file_path: &Path,
443 content: &str,
444 config: &Config,
445 ) -> Result<Vec<(ServerKey, i32)>, LspError> {
446 let canonical_path = canonicalize_for_lsp(file_path)?;
447 let server_keys = self.ensure_server_for_file(&canonical_path, config);
448 if server_keys.is_empty() {
449 return Ok(Vec::new());
450 }
451
452 let uri = uri_for_path(&canonical_path)?;
453 let language_id = language_id_for_extension(
454 canonical_path
455 .extension()
456 .and_then(|ext| ext.to_str())
457 .unwrap_or_default(),
458 )
459 .to_string();
460
461 let mut versions: Vec<(ServerKey, i32)> = Vec::with_capacity(server_keys.len());
462
463 for key in server_keys {
464 let current_version = self
465 .documents
466 .get(&key)
467 .and_then(|store| store.version(&canonical_path));
468
469 if let Some(version) = current_version {
470 let next_version = version + 1;
471 if let Some(client) = self.clients.get_mut(&key) {
472 client.send_notification::<DidChangeTextDocument>(
473 DidChangeTextDocumentParams {
474 text_document: VersionedTextDocumentIdentifier::new(
475 uri.clone(),
476 next_version,
477 ),
478 content_changes: vec![TextDocumentContentChangeEvent {
479 range: None,
480 range_length: None,
481 text: content.to_string(),
482 }],
483 },
484 )?;
485 }
486 if let Some(store) = self.documents.get_mut(&key) {
487 store.bump_version(&canonical_path);
488 }
489 versions.push((key, next_version));
490 continue;
491 }
492
493 if let Some(client) = self.clients.get_mut(&key) {
494 client.send_notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
495 text_document: TextDocumentItem::new(
496 uri.clone(),
497 language_id.clone(),
498 0,
499 content.to_string(),
500 ),
501 })?;
502 }
503 self.documents
504 .entry(key.clone())
505 .or_default()
506 .open(canonical_path.clone());
507 versions.push((key, 0));
510 }
511
512 Ok(versions)
513 }
514
515 pub fn notify_file_changed_default(
516 &mut self,
517 file_path: &Path,
518 content: &str,
519 ) -> Result<(), LspError> {
520 self.notify_file_changed(file_path, content, &Config::default())
521 }
522
523 pub fn notify_files_watched_changed(
529 &mut self,
530 paths: &[(PathBuf, FileChangeType)],
531 _config: &Config,
532 ) -> Result<(), LspError> {
533 if paths.is_empty() {
534 return Ok(());
535 }
536
537 let mut canonical_events = Vec::with_capacity(paths.len());
538 for (path, typ) in paths {
539 let canonical_path = resolve_for_lsp_uri(path);
540 canonical_events.push((canonical_path, *typ));
541 }
542
543 let keys: Vec<ServerKey> = self.clients.keys().cloned().collect();
544 for key in keys {
545 let mut changes = Vec::new();
546 for (path, typ) in &canonical_events {
547 if !path.starts_with(&key.root) {
548 continue;
549 }
550 changes.push(FileEvent::new(uri_for_path(path)?, *typ));
551 }
552
553 if changes.is_empty() {
554 continue;
555 }
556
557 if let Some(client) = self.clients.get_mut(&key) {
558 if !client.supports_watched_files() {
563 log::debug!(
564 "skipping didChangeWatchedFiles for {:?} (capability not declared)",
565 key
566 );
567 continue;
568 }
569 client.send_notification::<DidChangeWatchedFiles>(DidChangeWatchedFilesParams {
570 changes,
571 })?;
572 }
573 }
574
575 Ok(())
576 }
577
578 pub fn notify_file_closed(&mut self, file_path: &Path) -> Result<(), LspError> {
580 let canonical_path = canonicalize_for_lsp(file_path)?;
581 let uri = uri_for_path(&canonical_path)?;
582 let keys: Vec<ServerKey> = self.documents.keys().cloned().collect();
583
584 for key in keys {
585 let was_open = self
586 .documents
587 .get(&key)
588 .map(|store| store.is_open(&canonical_path))
589 .unwrap_or(false);
590 if !was_open {
591 continue;
592 }
593
594 if let Some(client) = self.clients.get_mut(&key) {
595 client.send_notification::<DidCloseTextDocument>(DidCloseTextDocumentParams {
596 text_document: TextDocumentIdentifier::new(uri.clone()),
597 })?;
598 }
599
600 if let Some(store) = self.documents.get_mut(&key) {
601 store.close(&canonical_path);
602 }
603 }
604
605 Ok(())
606 }
607
608 pub fn client_for_file(&self, file_path: &Path, config: &Config) -> Option<&LspClient> {
610 let key = self.server_key_for_file(file_path, config)?;
611 self.clients.get(&key)
612 }
613
614 pub fn client_for_file_default(&self, file_path: &Path) -> Option<&LspClient> {
615 self.client_for_file(file_path, &Config::default())
616 }
617
618 pub fn client_for_file_mut(
620 &mut self,
621 file_path: &Path,
622 config: &Config,
623 ) -> Option<&mut LspClient> {
624 let key = self.server_key_for_file(file_path, config)?;
625 self.clients.get_mut(&key)
626 }
627
628 pub fn client_for_file_mut_default(&mut self, file_path: &Path) -> Option<&mut LspClient> {
629 self.client_for_file_mut(file_path, &Config::default())
630 }
631
632 pub fn active_client_count(&self) -> usize {
634 self.clients.len()
635 }
636
637 pub fn drain_events(&mut self) -> Vec<LspEvent> {
639 let mut events = Vec::new();
640 while let Ok(event) = self.event_rx.try_recv() {
641 self.handle_event(&event);
642 events.push(event);
643 }
644 events
645 }
646
647 pub fn wait_for_diagnostics(
649 &mut self,
650 file_path: &Path,
651 config: &Config,
652 timeout: std::time::Duration,
653 ) -> Vec<StoredDiagnostic> {
654 let deadline = std::time::Instant::now() + timeout;
655 self.wait_for_file_diagnostics(file_path, config, deadline)
656 }
657
658 pub fn wait_for_diagnostics_default(
659 &mut self,
660 file_path: &Path,
661 timeout: std::time::Duration,
662 ) -> Vec<StoredDiagnostic> {
663 self.wait_for_diagnostics(file_path, &Config::default(), timeout)
664 }
665
666 #[doc(hidden)]
671 pub fn diagnostics_store_for_test(&self) -> &DiagnosticsStore {
672 &self.diagnostics
673 }
674
675 pub fn snapshot_diagnostic_epochs(&self, file_path: &Path) -> HashMap<ServerKey, u64> {
680 let lookup_path = normalize_lookup_path(file_path);
681 self.diagnostics
682 .entries_for_file(&lookup_path)
683 .into_iter()
684 .map(|(key, entry)| (key.clone(), entry.epoch))
685 .collect()
686 }
687
688 pub fn wait_for_post_edit_diagnostics(
711 &mut self,
712 file_path: &Path,
713 _config: &Config,
717 expected_versions: &[(ServerKey, i32)],
718 pre_snapshot: &HashMap<ServerKey, u64>,
719 timeout: std::time::Duration,
720 ) -> PostEditWaitOutcome {
721 let lookup_path = normalize_lookup_path(file_path);
722 let deadline = std::time::Instant::now() + timeout;
723
724 let _ = self.drain_events_for_file(&lookup_path);
729
730 let mut fresh: HashMap<ServerKey, Vec<StoredDiagnostic>> = HashMap::new();
731 let mut exited: Vec<ServerKey> = Vec::new();
732
733 loop {
734 for (key, target_version) in expected_versions {
741 if fresh.contains_key(key) || exited.contains(key) {
742 continue;
743 }
744 if !self.clients.contains_key(key) {
745 exited.push(key.clone());
746 continue;
747 }
748 if let Some(entry) = self
749 .diagnostics
750 .entries_for_file(&lookup_path)
751 .into_iter()
752 .find_map(|(k, e)| if k == key { Some(e) } else { None })
753 {
754 let is_fresh = match entry.version {
755 Some(v) => v == *target_version,
756 None => {
757 let pre = pre_snapshot.get(key).copied().unwrap_or(0);
758 entry.epoch > pre
759 }
760 };
761 if is_fresh {
762 fresh.insert(key.clone(), entry.diagnostics.clone());
763 }
764 }
765 }
766
767 if fresh.len() + exited.len() == expected_versions.len() {
769 break;
770 }
771
772 let now = std::time::Instant::now();
773 if now >= deadline {
774 break;
775 }
776
777 let timeout = deadline.saturating_duration_since(now);
778 match self.event_rx.recv_timeout(timeout) {
779 Ok(event) => {
780 self.handle_event(&event);
781 }
782 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
783 }
784 }
785
786 let pending: Vec<ServerKey> = expected_versions
788 .iter()
789 .filter(|(k, _)| !fresh.contains_key(k) && !exited.contains(k))
790 .map(|(k, _)| k.clone())
791 .collect();
792
793 let mut diagnostics: Vec<StoredDiagnostic> = fresh
796 .into_iter()
797 .flat_map(|(_, diags)| diags.into_iter())
798 .collect();
799 diagnostics.sort_by(|a, b| {
800 a.file
801 .cmp(&b.file)
802 .then(a.line.cmp(&b.line))
803 .then(a.column.cmp(&b.column))
804 .then(a.message.cmp(&b.message))
805 });
806
807 PostEditWaitOutcome {
808 diagnostics,
809 pending_servers: pending,
810 exited_servers: exited,
811 }
812 }
813
814 pub fn wait_for_file_diagnostics(
820 &mut self,
821 file_path: &Path,
822 config: &Config,
823 deadline: std::time::Instant,
824 ) -> Vec<StoredDiagnostic> {
825 let lookup_path = normalize_lookup_path(file_path);
826
827 if self.server_key_for_file(&lookup_path, config).is_none() {
828 return Vec::new();
829 }
830
831 loop {
832 if self.drain_events_for_file(&lookup_path) {
833 break;
834 }
835
836 let now = std::time::Instant::now();
837 if now >= deadline {
838 break;
839 }
840
841 let timeout = deadline.saturating_duration_since(now);
842 match self.event_rx.recv_timeout(timeout) {
843 Ok(event) => {
844 if matches!(
845 self.handle_event(&event),
846 Some(ref published_file) if published_file.as_path() == lookup_path.as_path()
847 ) {
848 break;
849 }
850 }
851 Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
852 }
853 }
854
855 self.get_diagnostics_for_file(&lookup_path)
856 .into_iter()
857 .cloned()
858 .collect()
859 }
860
861 pub const PULL_FILE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
867
868 pub fn pull_file_timeout() -> std::time::Duration {
870 Self::PULL_FILE_TIMEOUT
871 }
872
873 const PULL_WORKSPACE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
877
878 pub fn pull_file_diagnostics(
889 &mut self,
890 file_path: &Path,
891 config: &Config,
892 ) -> Result<Vec<PullFileResult>, LspError> {
893 let canonical_path = canonicalize_for_lsp(file_path)?;
894 self.ensure_file_open(&canonical_path, config)?;
897
898 let server_keys = self.ensure_server_for_file(&canonical_path, config);
899 if server_keys.is_empty() {
900 return Ok(Vec::new());
901 }
902
903 let uri = uri_for_path(&canonical_path)?;
904 let mut results = Vec::with_capacity(server_keys.len());
905
906 for key in server_keys {
907 let supports_pull = self
908 .clients
909 .get(&key)
910 .and_then(|c| c.diagnostic_capabilities())
911 .is_some_and(|caps| caps.pull_diagnostics);
912
913 if !supports_pull {
914 results.push(PullFileResult {
915 server_key: key.clone(),
916 outcome: PullFileOutcome::PullNotSupported,
917 });
918 continue;
919 }
920
921 let previous_result_id = self
923 .diagnostics
924 .entries_for_file(&canonical_path)
925 .into_iter()
926 .find(|(k, _)| **k == key)
927 .and_then(|(_, entry)| entry.result_id.clone());
928
929 let identifier = self
930 .clients
931 .get(&key)
932 .and_then(|c| c.diagnostic_capabilities())
933 .and_then(|caps| caps.identifier.clone());
934
935 let params = lsp_types::DocumentDiagnosticParams {
936 text_document: lsp_types::TextDocumentIdentifier { uri: uri.clone() },
937 identifier,
938 previous_result_id,
939 work_done_progress_params: Default::default(),
940 partial_result_params: Default::default(),
941 };
942
943 let outcome = match self.send_pull_request(&key, params) {
944 Ok(report) => self.ingest_document_report(&key, &canonical_path, report),
945 Err(err) => PullFileOutcome::RequestFailed {
946 reason: err.to_string(),
947 },
948 };
949
950 results.push(PullFileResult {
951 server_key: key,
952 outcome,
953 });
954 }
955
956 Ok(results)
957 }
958
959 pub fn pull_workspace_diagnostics(
964 &mut self,
965 server_key: &ServerKey,
966 timeout: Option<std::time::Duration>,
967 ) -> Result<PullWorkspaceResult, LspError> {
968 let _timeout = timeout.unwrap_or(Self::PULL_WORKSPACE_TIMEOUT);
969
970 let supports_workspace = self
971 .clients
972 .get(server_key)
973 .and_then(|c| c.diagnostic_capabilities())
974 .is_some_and(|caps| caps.workspace_diagnostics);
975
976 if !supports_workspace {
977 return Ok(PullWorkspaceResult {
978 server_key: server_key.clone(),
979 files_reported: Vec::new(),
980 complete: false,
981 cancelled: false,
982 supports_workspace: false,
983 });
984 }
985
986 let identifier = self
987 .clients
988 .get(server_key)
989 .and_then(|c| c.diagnostic_capabilities())
990 .and_then(|caps| caps.identifier.clone());
991
992 let params = lsp_types::WorkspaceDiagnosticParams {
993 identifier,
994 previous_result_ids: Vec::new(),
995 work_done_progress_params: Default::default(),
996 partial_result_params: Default::default(),
997 };
998
999 let result = match self
1007 .clients
1008 .get_mut(server_key)
1009 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?
1010 .send_request::<lsp_types::request::WorkspaceDiagnosticRequest>(params)
1011 {
1012 Ok(result) => result,
1013 Err(LspError::Timeout(_)) => {
1014 return Ok(PullWorkspaceResult {
1015 server_key: server_key.clone(),
1016 files_reported: Vec::new(),
1017 complete: false,
1018 cancelled: true,
1019 supports_workspace: true,
1020 });
1021 }
1022 Err(err) => return Err(err),
1023 };
1024
1025 let items = match result {
1030 lsp_types::WorkspaceDiagnosticReportResult::Report(report) => report.items,
1031 lsp_types::WorkspaceDiagnosticReportResult::Partial(_) => Vec::new(),
1032 };
1033
1034 let mut files_reported = Vec::with_capacity(items.len());
1036 for item in items {
1037 match item {
1038 lsp_types::WorkspaceDocumentDiagnosticReport::Full(full) => {
1039 if let Some(file) = uri_to_path(&full.uri) {
1040 let stored = from_lsp_diagnostics(
1041 file.clone(),
1042 full.full_document_diagnostic_report.items.clone(),
1043 );
1044 self.diagnostics.publish_with_result_id(
1045 server_key.clone(),
1046 file.clone(),
1047 stored,
1048 full.full_document_diagnostic_report.result_id.clone(),
1049 );
1050 files_reported.push(file);
1051 }
1052 }
1053 lsp_types::WorkspaceDocumentDiagnosticReport::Unchanged(_unchanged) => {
1054 }
1057 }
1058 }
1059
1060 Ok(PullWorkspaceResult {
1061 server_key: server_key.clone(),
1062 files_reported,
1063 complete: true,
1064 cancelled: false,
1065 supports_workspace: true,
1066 })
1067 }
1068
1069 fn send_pull_request(
1071 &mut self,
1072 key: &ServerKey,
1073 params: lsp_types::DocumentDiagnosticParams,
1074 ) -> Result<lsp_types::DocumentDiagnosticReportResult, LspError> {
1075 let client = self
1076 .clients
1077 .get_mut(key)
1078 .ok_or_else(|| LspError::ServerNotReady("server not found".into()))?;
1079 client.send_request::<lsp_types::request::DocumentDiagnosticRequest>(params)
1080 }
1081
1082 fn ingest_document_report(
1085 &mut self,
1086 key: &ServerKey,
1087 canonical_path: &Path,
1088 result: lsp_types::DocumentDiagnosticReportResult,
1089 ) -> PullFileOutcome {
1090 let report = match result {
1091 lsp_types::DocumentDiagnosticReportResult::Report(report) => report,
1092 lsp_types::DocumentDiagnosticReportResult::Partial(_) => {
1093 return PullFileOutcome::PartialNotSupported;
1097 }
1098 };
1099
1100 match report {
1101 lsp_types::DocumentDiagnosticReport::Full(full) => {
1102 let result_id = full.full_document_diagnostic_report.result_id.clone();
1103 let stored = from_lsp_diagnostics(
1104 canonical_path.to_path_buf(),
1105 full.full_document_diagnostic_report.items.clone(),
1106 );
1107 let count = stored.len();
1108 self.diagnostics.publish_with_result_id(
1109 key.clone(),
1110 canonical_path.to_path_buf(),
1111 stored,
1112 result_id,
1113 );
1114 PullFileOutcome::Full {
1115 diagnostic_count: count,
1116 }
1117 }
1118 lsp_types::DocumentDiagnosticReport::Unchanged(_unchanged) => {
1119 PullFileOutcome::Unchanged
1122 }
1123 }
1124 }
1125
1126 pub fn shutdown_all(&mut self) {
1128 for (key, mut client) in self.clients.drain() {
1129 if let Err(err) = client.shutdown() {
1130 slog_error!("error shutting down {:?}: {}", key, err);
1131 }
1132 }
1133 self.documents.clear();
1134 self.diagnostics = DiagnosticsStore::new();
1135 }
1136
1137 pub fn has_active_servers(&self) -> bool {
1139 self.clients
1140 .values()
1141 .any(|client| client.state() == ServerState::Ready)
1142 }
1143
1144 pub fn active_server_keys(&self) -> Vec<ServerKey> {
1147 self.clients.keys().cloned().collect()
1148 }
1149
1150 pub fn get_diagnostics_for_file(&self, file: &Path) -> Vec<&StoredDiagnostic> {
1151 let normalized = normalize_lookup_path(file);
1152 self.diagnostics.for_file(&normalized)
1153 }
1154
1155 pub fn get_diagnostics_for_directory(&self, dir: &Path) -> Vec<&StoredDiagnostic> {
1156 let normalized = normalize_lookup_path(dir);
1157 self.diagnostics.for_directory(&normalized)
1158 }
1159
1160 pub fn get_all_diagnostics(&self) -> Vec<&StoredDiagnostic> {
1161 self.diagnostics.all()
1162 }
1163
1164 fn drain_events_for_file(&mut self, file_path: &Path) -> bool {
1165 let mut saw_file_diagnostics = false;
1166 while let Ok(event) = self.event_rx.try_recv() {
1167 if matches!(
1168 self.handle_event(&event),
1169 Some(ref published_file) if published_file.as_path() == file_path
1170 ) {
1171 saw_file_diagnostics = true;
1172 }
1173 }
1174 saw_file_diagnostics
1175 }
1176
1177 fn handle_event(&mut self, event: &LspEvent) -> Option<PathBuf> {
1178 match event {
1179 LspEvent::Notification {
1180 server_kind,
1181 root,
1182 method,
1183 params: Some(params),
1184 } if method == "textDocument/publishDiagnostics" => {
1185 self.handle_publish_diagnostics(server_kind.clone(), root.clone(), params)
1186 }
1187 LspEvent::ServerExited { server_kind, root } => {
1188 let key = ServerKey {
1189 kind: server_kind.clone(),
1190 root: root.clone(),
1191 };
1192 self.clients.remove(&key);
1193 self.documents.remove(&key);
1194 self.diagnostics.clear_server(server_kind.clone());
1195 None
1196 }
1197 _ => None,
1198 }
1199 }
1200
1201 fn handle_publish_diagnostics(
1202 &mut self,
1203 server: ServerKind,
1204 root: PathBuf,
1205 params: &serde_json::Value,
1206 ) -> Option<PathBuf> {
1207 if let Ok(publish_params) =
1208 serde_json::from_value::<lsp_types::PublishDiagnosticsParams>(params.clone())
1209 {
1210 let file = uri_to_path(&publish_params.uri)?;
1211 let stored = from_lsp_diagnostics(file.clone(), publish_params.diagnostics);
1212 let key = ServerKey { kind: server, root };
1218 self.diagnostics
1219 .publish_full(key, file.clone(), stored, None, publish_params.version);
1220 return Some(file);
1221 }
1222 None
1223 }
1224
1225 fn spawn_server(
1226 &self,
1227 def: &ServerDef,
1228 root: &Path,
1229 config: &Config,
1230 ) -> Result<LspClient, LspError> {
1231 let binary = self.resolve_binary(def, config)?;
1232
1233 let mut merged_env = def.env.clone();
1237 for (key, value) in &self.extra_env {
1238 merged_env.insert(key.clone(), value.clone());
1239 }
1240
1241 let mut client = LspClient::spawn(
1242 def.kind.clone(),
1243 root.to_path_buf(),
1244 &binary,
1245 &def.args,
1246 &merged_env,
1247 self.event_tx.clone(),
1248 )?;
1249 client.initialize(root, def.initialization_options.clone())?;
1250 Ok(client)
1251 }
1252
1253 fn resolve_binary(&self, def: &ServerDef, config: &Config) -> Result<PathBuf, LspError> {
1254 if let Some(path) = self.binary_overrides.get(&def.kind) {
1255 if path.exists() {
1256 return Ok(path.clone());
1257 }
1258 return Err(LspError::NotFound(format!(
1259 "override binary for {:?} not found: {}",
1260 def.kind,
1261 path.display()
1262 )));
1263 }
1264
1265 if let Some(path) = env_binary_override(&def.kind) {
1266 if path.exists() {
1267 return Ok(path);
1268 }
1269 return Err(LspError::NotFound(format!(
1270 "environment override binary for {:?} not found: {}",
1271 def.kind,
1272 path.display()
1273 )));
1274 }
1275
1276 resolve_lsp_binary(
1281 &def.binary,
1282 config.project_root.as_deref(),
1283 &config.lsp_paths_extra,
1284 )
1285 .ok_or_else(|| {
1286 LspError::NotFound(format!(
1287 "language server binary '{}' not found in node_modules/.bin, lsp_paths_extra, or PATH",
1288 def.binary
1289 ))
1290 })
1291 }
1292
1293 fn server_key_for_file(&self, file_path: &Path, config: &Config) -> Option<ServerKey> {
1294 for def in servers_for_file(file_path, config) {
1295 let root = find_workspace_root(file_path, &def.root_markers)?;
1296 let key = ServerKey {
1297 kind: def.kind.clone(),
1298 root,
1299 };
1300 if self.clients.contains_key(&key) {
1301 return Some(key);
1302 }
1303 }
1304 None
1305 }
1306}
1307
1308impl Default for LspManager {
1309 fn default() -> Self {
1310 Self::new()
1311 }
1312}
1313
1314fn canonicalize_for_lsp(file_path: &Path) -> Result<PathBuf, LspError> {
1315 std::fs::canonicalize(file_path).map_err(LspError::from)
1316}
1317
1318fn resolve_for_lsp_uri(file_path: &Path) -> PathBuf {
1319 if let Ok(path) = std::fs::canonicalize(file_path) {
1320 return path;
1321 }
1322
1323 let mut existing = file_path.to_path_buf();
1324 let mut missing = Vec::new();
1325 while !existing.exists() {
1326 let Some(name) = existing.file_name() else {
1327 break;
1328 };
1329 missing.push(name.to_owned());
1330 let Some(parent) = existing.parent() else {
1331 break;
1332 };
1333 existing = parent.to_path_buf();
1334 }
1335
1336 let mut resolved = std::fs::canonicalize(&existing).unwrap_or(existing);
1337 for segment in missing.into_iter().rev() {
1338 resolved.push(segment);
1339 }
1340 resolved
1341}
1342
1343fn uri_for_path(path: &Path) -> Result<lsp_types::Uri, LspError> {
1344 let url = url::Url::from_file_path(path).map_err(|_| {
1345 LspError::NotFound(format!(
1346 "failed to convert '{}' to file URI",
1347 path.display()
1348 ))
1349 })?;
1350 lsp_types::Uri::from_str(url.as_str()).map_err(|_| {
1351 LspError::NotFound(format!("failed to parse file URI for '{}'", path.display()))
1352 })
1353}
1354
1355fn language_id_for_extension(ext: &str) -> &'static str {
1356 match ext {
1357 "ts" => "typescript",
1358 "tsx" => "typescriptreact",
1359 "js" | "mjs" | "cjs" => "javascript",
1360 "jsx" => "javascriptreact",
1361 "py" | "pyi" => "python",
1362 "rs" => "rust",
1363 "go" => "go",
1364 "html" | "htm" => "html",
1365 _ => "plaintext",
1366 }
1367}
1368
1369fn normalize_lookup_path(path: &Path) -> PathBuf {
1370 std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
1371}
1372
1373fn uri_to_path(uri: &lsp_types::Uri) -> Option<PathBuf> {
1374 let url = url::Url::parse(uri.as_str()).ok()?;
1375 url.to_file_path()
1376 .ok()
1377 .map(|path| normalize_lookup_path(&path))
1378}
1379
1380fn classify_spawn_error(binary: &str, err: &LspError) -> ServerAttemptResult {
1387 match err {
1388 LspError::NotFound(_) => ServerAttemptResult::BinaryNotInstalled {
1393 binary: binary.to_string(),
1394 },
1395 other => ServerAttemptResult::SpawnFailed {
1396 binary: binary.to_string(),
1397 reason: other.to_string(),
1398 },
1399 }
1400}
1401
1402fn env_binary_override(kind: &ServerKind) -> Option<PathBuf> {
1403 let id = kind.id_str();
1404 let suffix: String = id
1405 .chars()
1406 .map(|ch| {
1407 if ch.is_ascii_alphanumeric() {
1408 ch.to_ascii_uppercase()
1409 } else {
1410 '_'
1411 }
1412 })
1413 .collect();
1414 let key = format!("AFT_LSP_{suffix}_BINARY");
1415 std::env::var_os(key).map(PathBuf::from)
1416}