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