1use std::collections::{HashMap, HashSet};
10use std::path::{Path, PathBuf};
11use std::sync::{Arc, Mutex as StdMutex, OnceLock};
12
13use tokio::sync::Mutex;
14
15use self::clock::{Clock, SystemClock};
16use self::encoding_ctx::EncodingCtx;
17use self::servers::{Backend, Phase, Servers};
18use crate::bridge::encoding::PositionEncoding;
19use crate::bridge::indexing::IndexingReset;
20use crate::bridge::state::ResourceLimits;
21use crate::bridge::{DocumentOpenState, DocumentTracker, NotificationCache, WorkspaceRoots};
22use crate::config::{
23 FilePattern, IndexingReadyTimeoutSecs, LanguageId, LanguageMap, ServerId, ServerSettlement,
24 ToolKind, ToolRouter, WorkspaceTrust,
25};
26use crate::error::{ErrorChain, ServerSpawnFailure, StartupFailure};
27use crate::lsp::{LspServer, ServerInitConfig};
28use crate::redaction::Redactions;
29use crate::util::lock_std;
30
31mod addressing;
32mod assist;
33mod availability;
34mod call_hierarchy;
35#[cfg(test)]
36mod characterization;
37mod clock;
38mod diagnostics;
39mod dto;
40mod edits;
41mod enclosing;
42#[cfg(test)]
43mod enclosing_tests;
44mod encoding_ctx;
45mod folding_range;
46mod hierarchy;
47mod highlights;
48mod kind_filter;
49mod navigation;
50mod positioned;
51#[cfg(test)]
52mod prepare_range_tests;
53mod pull_support;
54mod respawn;
55mod restart;
56mod routing;
57mod save_support;
58mod selection_range;
59mod servers;
60mod support;
61mod symbols;
62#[cfg(test)]
63mod testing;
64mod type_hierarchy;
65
66pub use addressing::{
67 AddressableTool, Addressed, MAX_SYMBOL_NAME_BYTES, PositionSource, ResolvedSymbol,
68 ResolvedTarget, SymbolName, SymbolQuery, SymbolTarget,
69};
70pub use availability::{DiagnosticsAnswer, DiagnosticsAvailability, DiagnosticsOrigin};
71#[cfg(test)]
72pub use diagnostics::MAX_FAILED_PULLS_PER_STATE;
73pub use diagnostics::{
74 Interest, MAX_COMPANION_FILES, MAX_SWEPT_FILES_PER_TICK, SWEEP_INTERVAL, SweepContext,
75 SweepMemo,
76};
77pub use dto::*;
78pub use enclosing::{
79 Contextual, ContextualDiagnostic, ContextualLocation, EnclosingSymbol, EnclosingSymbolOutcome,
80 EnrichmentSummary, NotComputedReason, ResultContext, SymbolFidelity, UnavailableReason,
81};
82pub use kind_filter::{
83 CodeActionKindFilter, KindFilter, KindFilterField, KindFilterInput, RejectedKindFilter,
84 Rejection, SymbolKindFilter,
85};
86pub use restart::{
87 DiagnosticsRole, MAX_RESTART_SERVER_IDS, MAX_SERVER_ID_BYTES, NotificationReceivers,
88 NotificationWiring, RestartFailure, RestartOutcome, RestartServerResult, RestartTarget,
89 ServerIds, ServerIdsError, ServerRestartEntry,
90};
91pub use routing::Capability;
92pub use support::{RouteSupport, ToolSupportSnapshot};
93
94#[derive(Debug)]
110pub struct Translator {
111 servers: StdMutex<Servers>,
115 phase: StdMutex<Phase>,
117 startup_redactions: Arc<Redactions>,
120 merged_redactions: StdMutex<Option<MergedRedactions>>,
122 document_tracker: Arc<DocumentTracker>,
124 resource_limits: ResourceLimits,
130 workspace_roots: WorkspaceRoots,
133 language_map: Arc<LanguageMap>,
136 file_patterns: Arc<[FilePattern]>,
139 router: Arc<StdMutex<Arc<ToolRouter>>>,
144 configured_router: Arc<ToolRouter>,
150 notification_cache: Option<Arc<Mutex<NotificationCache>>>,
157 wiring: OnceLock<Arc<dyn NotificationWiring>>,
161 clock: Arc<dyn Clock>,
166 indexing_ready_timeout: std::time::Duration,
171 workspace_trust: WorkspaceTrust,
174}
175
176impl Translator {
177 #[must_use]
187 pub fn new() -> Self {
188 Self {
189 servers: StdMutex::new(Servers::default()),
190 phase: StdMutex::new(Phase::default()),
191 startup_redactions: Arc::default(),
192 merged_redactions: StdMutex::new(None),
193 document_tracker: Arc::new(DocumentTracker::new(
194 ResourceLimits::default(),
195 HashMap::new(),
196 )),
197 resource_limits: ResourceLimits::default(),
198 workspace_roots: WorkspaceRoots::default(),
199 language_map: Arc::default(),
200 file_patterns: Arc::default(),
201 router: Arc::new(StdMutex::new(Arc::new(ToolRouter::default()))),
202 configured_router: Arc::new(ToolRouter::default()),
203 notification_cache: None,
204 wiring: OnceLock::new(),
205 clock: Arc::new(SystemClock),
206 indexing_ready_timeout: navigation::INDEXING_READY_TIMEOUT,
207 workspace_trust: WorkspaceTrust::default(),
208 }
209 }
210
211 #[cfg(test)]
217 #[must_use]
218 fn with_clock(mut self, clock: Arc<dyn Clock>) -> Self {
219 self.clock = clock;
220 self
221 }
222
223 pub fn set_workspace_roots(&mut self, roots: WorkspaceRoots) {
233 self.workspace_roots = roots;
234 }
235
236 #[must_use]
243 pub fn with_notification_cache(mut self, cache: Arc<Mutex<NotificationCache>>) -> Self {
244 self.notification_cache = Some(cache);
245 self
246 }
247
248 #[must_use]
252 pub(crate) fn with_startup_redactions(mut self, redactions: Arc<Redactions>) -> Self {
253 self.startup_redactions = redactions;
254 self
255 }
256
257 #[must_use]
265 pub const fn with_indexing_ready_timeout(mut self, timeout: IndexingReadyTimeoutSecs) -> Self {
266 self.indexing_ready_timeout = timeout.as_duration();
267 self
268 }
269
270 #[must_use]
274 pub(crate) fn with_workspace_trust(mut self, trust: WorkspaceTrust) -> Self {
275 self.workspace_trust = trust;
276 self
277 }
278
279 #[allow(
282 clippy::needless_pass_by_value,
283 reason = "public API: callers hand over the set they built"
284 )]
285 pub fn set_expected_servers(&self, servers: HashSet<ServerId>) {
286 lock_std(&self.servers).set_expected(&servers);
287 }
288
289 pub fn clear_expected_servers(&self) {
291 lock_std(&self.servers).clear_expected();
292 }
293
294 #[must_use]
299 pub fn with_router(mut self, router: ToolRouter) -> Self {
300 let router = Arc::new(router);
301 self.configured_router = Arc::clone(&router);
302 self.router = Arc::new(StdMutex::new(router));
303 self
304 }
305
306 #[cfg(test)]
311 pub(crate) fn record_startup_failures(&self, failures: &[ServerSpawnFailure]) {
312 let mut servers = lock_std(&self.servers);
313 for failure in failures {
314 servers.record_failure(failure);
315 }
316 }
317
318 pub(crate) fn startup_failure(&self, id: &ServerId) -> Option<ServerSpawnFailure> {
320 lock_std(&self.servers).failure(id).cloned()
321 }
322
323 pub(crate) fn startup_failures(&self) -> Vec<ServerSpawnFailure> {
325 lock_std(&self.servers).failures()
326 }
327
328 pub(crate) fn record_refusals(&self, refused: &[ServerSpawnFailure]) {
335 if refused.is_empty() {
336 return;
337 }
338 {
339 let mut servers = lock_std(&self.servers);
340 for failure in refused {
341 servers.record_failure(failure);
342 }
343 }
344 self.rebind_router_to_settled();
345 for failure in refused {
346 self.warn_failed_routes(&failure.server_id, &RouteLoss::of(failure));
347 }
348 }
349
350 pub(crate) async fn settle_after_init_panic(&self, configs: &[ServerInitConfig]) {
360 lock_std(&self.phase).init_panicked();
361 let registered: HashSet<ServerId> = {
362 let mut servers = lock_std(&self.servers);
363 servers.fail_unsettled(configs.iter().map(|config| {
364 let server_config = &config.server_config();
365 ServerSpawnFailure {
366 server_id: server_config.id(),
367 language_id: server_config.language_id.clone(),
368 command: server_config.command.server_command().clone(),
369 reason: StartupFailure::InitTaskPanicked,
370 }
371 }));
372 servers
373 .ids()
374 .filter(|id| servers.client(id).is_some())
375 .cloned()
376 .collect()
377 };
378 self.rebind_router_to_settled();
379 self.clear_expected_servers();
380
381 if let Some(cache) = &self.notification_cache {
382 let mut cache = cache.lock().await;
383 for id in ®istered {
384 cache.mark_push_degraded(id);
385 cache.reset_indexing_state(id, IndexingReset::Forget);
386 }
387 }
388 }
389
390 pub fn rebind_router(&self, registered: &HashSet<ServerId>) {
394 self.install_router(|id| {
395 if registered.contains(id) {
396 ServerSettlement::Registered
397 } else {
398 ServerSettlement::Failed
399 }
400 });
401 }
402
403 pub(crate) fn rebind_router_to_settled(&self) {
409 let settlements = lock_std(&self.servers).settlements();
410 self.install_router(|id| {
411 settlements
412 .get(id)
413 .copied()
414 .unwrap_or(ServerSettlement::Pending)
415 });
416 }
417
418 fn install_router(&self, settlement: impl Fn(&ServerId) -> ServerSettlement) {
419 let mut router = (*self.configured_router).clone();
420 router.rebind(settlement);
421 *lock_std(&self.router) = Arc::new(router);
422 }
423
424 pub(super) fn router_snapshot(&self) -> Arc<ToolRouter> {
427 Arc::clone(&lock_std(&self.router))
428 }
429
430 pub(crate) fn settle_started(&self, server: LspServer) -> (ServerId, LanguageId) {
432 let id = server.init_config().server_config().id();
433 let language = server.client().language_id().clone();
434 let registered = lock_std(&self.servers).register(id.clone(), Backend::Process(server));
435 drop(registered.displaced);
436 if !registered.was_expected {
437 tracing::error!("LSP server '{id}' settled twice or was never expected");
438 }
439 self.rebind_router_to_settled();
440 (id, language)
441 }
442
443 pub(crate) fn settle_failed(&self, failure: &ServerSpawnFailure) {
445 let id = failure.server_id.clone();
446 lock_std(&self.servers).announce_failure(failure);
449 self.rebind_router_to_settled();
450 let was_expected = lock_std(&self.servers).record_failure(failure);
451 if !was_expected {
452 tracing::error!("LSP server '{id}' settled twice or was never expected");
453 }
454 self.warn_failed_routes(&id, &RouteLoss::FailedToStart);
455 }
456
457 fn warn_failed_routes(&self, id: &ServerId, loss: &RouteLoss<'_>) {
460 let languages = self.configured_router.languages_routed_to(id);
461 let catch_all_states: Vec<String> = languages
462 .iter()
463 .map(|language| {
464 let state = match self.configured_router.catch_all_for(language.as_str()) {
465 None => "no catch-all".to_string(),
466 Some(catch_all) if catch_all == id => "it was the catch-all".to_string(),
467 Some(catch_all) if lock_std(&self.servers).client(catch_all).is_some() => {
468 format!("catch-all '{catch_all}' registered")
469 }
470 Some(catch_all) if self.startup_failure(catch_all).is_some() => {
471 format!("catch-all '{catch_all}' failed")
472 }
473 Some(catch_all) => format!("catch-all '{catch_all}' pending"),
474 };
475 format!("{language}: {state}")
476 })
477 .collect();
478 tracing::warn!(
479 "server '{id}' {loss}; routes of [{}] are rebound or dropped",
480 catch_all_states.join(", ")
481 );
482 }
483
484 #[must_use]
491 pub fn is_diagnostics_route(&self, language_id: &LanguageId, id: &ServerId) -> bool {
492 lock_std(&self.router).resolve(language_id.as_str(), ToolKind::Diagnostics) == Some(id)
493 }
494
495 #[must_use]
504 pub(crate) fn position_encoding_for(&self, server_id: &ServerId) -> PositionEncoding {
505 lock_std(&self.servers)
506 .server(server_id)
507 .and_then(|server| PositionEncoding::from_lsp(server.position_encoding().as_str()))
508 .unwrap_or(PositionEncoding::Utf16)
509 }
510
511 fn encoding_ctx(&self, server_id: &ServerId) -> EncodingCtx {
514 EncodingCtx::new(
515 self.position_encoding_for(server_id),
516 self.document_tracker.clone(),
517 self.workspace_roots.clone(),
518 )
519 }
520
521 fn rebuild_document_tracker(&mut self) {
532 self.document_tracker = Arc::new(DocumentTracker::new(
533 self.resource_limits,
534 (*self.language_map).clone(),
535 ));
536 }
537
538 #[must_use]
542 pub fn with_file_patterns(mut self, patterns: impl IntoIterator<Item = FilePattern>) -> Self {
543 self.file_patterns = patterns.into_iter().collect();
544 self
545 }
546
547 #[must_use]
556 pub fn with_extensions(mut self, language_map: impl Into<LanguageMap>) -> Self {
557 self.language_map = Arc::new(language_map.into());
558 self.rebuild_document_tracker();
559 self
560 }
561
562 #[must_use]
574 pub fn with_resource_limits(mut self, limits: ResourceLimits) -> Self {
575 self.resource_limits = limits;
576 self.rebuild_document_tracker();
577 self
578 }
579
580 #[cfg(test)]
583 pub(crate) fn register_client(&self, id: ServerId, client: crate::lsp::LspClient) {
584 lock_std(&self.servers).register_test_client(id, client);
585 }
586
587 pub(crate) fn server_text_redactions(&self) -> Arc<Redactions> {
592 let sources: Vec<Arc<Redactions>> = std::iter::once(Arc::clone(&self.startup_redactions))
593 .chain(
594 lock_std(&self.servers)
595 .clients()
596 .map(|client| Arc::clone(client.redactions())),
597 )
598 .filter(|set| !set.is_empty())
599 .collect();
600 let mut cache = lock_std(&self.merged_redactions);
601 if let Some(cached) = cache
602 .as_ref()
603 .filter(|cached| cached.is_built_from(&sources))
604 {
605 return Arc::clone(&cached.merged);
606 }
607 let merged = Arc::new(Redactions::union(sources.iter().map(Arc::as_ref)));
608 *cache = Some(MergedRedactions {
609 sources,
610 merged: Arc::clone(&merged),
611 });
612 merged
613 }
614
615 #[cfg(test)]
618 pub(crate) fn register_server(&self, id: ServerId, server: LspServer) {
619 lock_std(&self.servers).register_test_server(id, server);
620 }
621
622 pub fn register_server_complete(&self, server: LspServer) {
639 let id = server.init_config().server_config().id();
640 let registered = lock_std(&self.servers).register(id, Backend::Process(server));
641 drop(registered.displaced);
642 }
643
644 #[cfg(test)]
652 pub(crate) fn registered_server_count(&self) -> usize {
653 lock_std(&self.servers).running_servers().count()
654 }
655
656 #[must_use]
658 pub fn open_document_paths(&self) -> Vec<PathBuf> {
659 self.document_tracker.open_paths()
660 }
661
662 #[must_use]
664 pub fn open_state(&self, path: &Path) -> DocumentOpenState {
665 self.document_tracker.open_state(path)
666 }
667
668 #[must_use]
672 pub(crate) const fn document_tracker(&self) -> &Arc<DocumentTracker> {
673 &self.document_tracker
674 }
675
676 pub(crate) async fn shutdown_servers(&self) {
709 self.begin_shutdown();
710 let servers: Vec<(ServerId, LspServer)> = lock_std(&self.servers).drain_servers();
711 if servers.is_empty() {
712 return;
713 }
714
715 let mut tasks = tokio::task::JoinSet::new();
716 let mut ids = HashMap::new();
717 for (id, server) in servers {
718 let task_id = id.clone();
719 let handle = tasks.spawn(async move {
720 match server.shutdown().await {
721 Ok(()) => tracing::debug!(%id, "LSP server shut down gracefully"),
722 Err(e) => tracing::warn!(
723 %id, error = %ErrorChain(&e),
724 "LSP server shutdown failed, killing process instead"
725 ),
726 }
727 });
728 ids.insert(handle.id(), task_id);
729 }
730 join_shutdown_tasks(tasks, &ids).await;
731 }
732}
733
734async fn join_shutdown_tasks(
737 mut tasks: tokio::task::JoinSet<()>,
738 ids: &HashMap<tokio::task::Id, ServerId>,
739) {
740 while let Some(joined) = tasks.join_next_with_id().await {
741 if let Err(e) = joined {
742 let id = ids.get(&e.id()).map_or("unknown", ServerId::as_str);
743 tracing::error!(%id, error = %e, "LSP server shutdown task failed");
744 }
745 }
746}
747
748impl Default for Translator {
749 fn default() -> Self {
753 Self::new()
754 }
755}
756
757#[derive(Debug)]
759struct MergedRedactions {
760 sources: Vec<Arc<Redactions>>,
761 merged: Arc<Redactions>,
762}
763
764impl MergedRedactions {
765 fn is_built_from(&self, sources: &[Arc<Redactions>]) -> bool {
767 self.sources.len() == sources.len()
768 && sources
769 .iter()
770 .all(|set| self.sources.iter().any(|known| Arc::ptr_eq(known, set)))
771 }
772}
773
774enum RouteLoss<'a> {
776 FailedToStart,
777 Refused(&'a crate::error::UntrustedRefusal),
778}
779
780impl<'a> RouteLoss<'a> {
781 const fn of(failure: &'a ServerSpawnFailure) -> Self {
782 match &failure.reason {
783 StartupFailure::RefusedUntrustedWorkspace(refusal) => Self::Refused(refusal),
784 StartupFailure::Spawn(_) | StartupFailure::InitTaskPanicked => Self::FailedToStart,
785 }
786 }
787}
788
789impl std::fmt::Display for RouteLoss<'_> {
790 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
791 match self {
792 Self::FailedToStart => f.write_str("failed to start"),
793 Self::Refused(refusal) => write!(f, "refused (untrusted workspace: {refusal})"),
794 }
795 }
796}
797
798#[cfg(test)]
799mod tests {
800 use std::assert_matches;
801 use std::collections::{HashMap, HashSet};
802 use std::path::PathBuf;
803
804 use tempfile::TempDir;
805 use tokio::time::Duration;
806
807 use super::*;
808 use crate::config::{
809 DocumentLimit, FileExtension, ServerCommand, ServerId, SizeLimit, ToolKind, ToolRouter,
810 ToolSet,
811 };
812 use crate::error::Error;
813 use crate::test_lsp::fake_lsp_client;
814
815 #[tokio::test]
816 async fn test_server_text_redactions_follow_live_clients_across_respawn() {
817 let translator = Translator::new();
818 assert!(translator.server_text_redactions().is_empty());
819
820 let (a, _fake_a, _lanes_a) = crate::test_lsp::fake_lsp_client_with_redactions(
821 Redactions::new([("A_TOKEN".to_owned(), "alpha-secret-111".to_owned())]),
822 );
823 let (b, _fake_b, _lanes_b) = crate::test_lsp::fake_lsp_client_with_redactions(
824 Redactions::new([("B_TOKEN".to_owned(), "bravo-secret-222".to_owned())]),
825 );
826 translator.register_client(ServerId::from_static("a"), a);
827 translator.register_client(ServerId::from_static("b"), b);
828 let both = translator.server_text_redactions();
829 assert_eq!(both.apply("alpha-secret-111"), "[redacted:A_TOKEN]");
830 assert_eq!(both.apply("bravo-secret-222"), "[redacted:B_TOKEN]");
831
832 let (respawned, _fake_c, _lanes_c) = crate::test_lsp::fake_lsp_client_with_redactions(
833 Redactions::new([("C_TOKEN".to_owned(), "charlie-secret-333".to_owned())]),
834 );
835 translator.register_client(ServerId::from_static("a"), respawned);
836 let after = translator.server_text_redactions();
837 assert_eq!(after.apply("alpha-secret-111"), "alpha-secret-111");
838 assert_eq!(after.apply("charlie-secret-333"), "[redacted:C_TOKEN]");
839 assert_eq!(after.apply("bravo-secret-222"), "[redacted:B_TOKEN]");
840 }
841
842 #[tokio::test]
843 async fn test_server_text_redactions_are_cached_and_do_not_relog() {
844 use tracing_subscriber::prelude::*;
845
846 let translator = Translator::new();
847 let (a, _fake_a, _lanes_a) = crate::test_lsp::fake_lsp_client_with_redactions(
848 Redactions::new([("A_TOKEN".to_owned(), "alpha-secret-111".to_owned())]),
849 );
850 let (b, _fake_b, _lanes_b) = crate::test_lsp::fake_lsp_client_with_redactions(
851 Redactions::new([("B_TOKEN".to_owned(), "A_TOKEN]".to_owned())]),
852 );
853 translator.register_client(ServerId::from_static("a"), a);
854 translator.register_client(ServerId::from_static("b"), b);
855
856 let logs = crate::test_lsp::CapturedLogs::default();
857 let subscriber = tracing_subscriber::registry()
858 .with(tracing_subscriber::filter::LevelFilter::WARN)
859 .with(logs.clone());
860 let first = tracing::subscriber::with_default(subscriber, || {
861 let first = translator.server_text_redactions();
862 let second = translator.server_text_redactions();
863 let third = translator.server_text_redactions();
864 assert!(Arc::ptr_eq(&first, &second) && Arc::ptr_eq(&second, &third));
865 first
866 });
867
868 assert_eq!(logs.messages().len(), 1, "{:?}", logs.messages());
869 assert_eq!(first.apply("alpha-secret-111"), "[redacted:A_TOKEN]");
870 }
871
872 #[tokio::test]
873 async fn test_register_server_complete_fills_all_maps_under_init_config_id() {
874 let translator = Translator::new();
875 let config = crate::config::LspServerConfig::rust_analyzer();
876 let id = config.id();
877 translator.register_server_complete(crate::lsp::fake_lsp_server_with_config(config));
878
879 let servers = lock_std(&translator.servers);
880 assert!(servers.client(&id).is_some());
881 assert!(servers.server(&id).is_some());
882 drop(servers);
883 }
884
885 fn named_config(
886 name: &str,
887 language: &str,
888 handles: Option<Vec<ToolKind>>,
889 ) -> crate::config::LspServerConfig {
890 let mut config = crate::config::LspServerConfig::rust_analyzer();
891 config.name = Some(ServerId::new(name).unwrap());
892 config.language_id = LanguageId::new(language).unwrap();
893 config.handles = handles.map(|tools| ToolSet::new(tools).unwrap());
894 config
895 }
896
897 fn spawn_failure(config: &crate::config::LspServerConfig) -> ServerSpawnFailure {
898 ServerSpawnFailure {
899 server_id: config.id(),
900 language_id: config.language_id.clone(),
901 command: config.command.server_command().clone(),
902 reason: StartupFailure::InitTaskPanicked,
903 }
904 }
905
906 fn settle_all(
907 translator: &Translator,
908 configs: &[crate::config::LspServerConfig],
909 up: &[bool],
910 ) {
911 for (config, up) in configs.iter().zip(up) {
912 if *up {
913 translator.settle_started(crate::lsp::fake_lsp_server_with_config(config.clone()));
914 } else {
915 translator.settle_failed(&spawn_failure(config));
916 }
917 }
918 }
919
920 #[tokio::test]
923 async fn settle_in_any_completion_order_yields_the_batch_router_and_failures() {
924 let configs = [
925 named_config("x", "rust", Some(vec![ToolKind::Hover])),
926 named_config("c", "rust", None),
927 named_config("y", "python", None),
928 ];
929 let orders = [
930 [0, 1, 2],
931 [0, 2, 1],
932 [1, 0, 2],
933 [1, 2, 0],
934 [2, 0, 1],
935 [2, 1, 0],
936 ];
937 for outcome in 0..8u8 {
938 let up: Vec<bool> = (0..3).map(|bit| outcome & (1 << bit) != 0).collect();
939 let registered: HashSet<ServerId> = configs
940 .iter()
941 .zip(&up)
942 .filter(|(_, up)| **up)
943 .map(|(config, _)| config.id())
944 .collect();
945 let mut batch = ToolRouter::from_configs(&configs).unwrap();
946 batch.rebind_to_registered(®istered);
947
948 for order in orders {
949 let translator =
950 Translator::new().with_router(ToolRouter::from_configs(&configs).unwrap());
951 translator.set_expected_servers(
952 configs
953 .iter()
954 .map(crate::config::LspServerConfig::id)
955 .collect(),
956 );
957 for index in order {
958 settle_all(
959 &translator,
960 std::slice::from_ref(&configs[index]),
961 &[up[index]],
962 );
963 }
964
965 let router = lock_std(&translator.router).clone();
966 for language in ["rust", "python"] {
967 for tool in ToolKind::ALL.iter().copied() {
968 assert_eq!(
969 router.resolve(language, tool),
970 batch.resolve(language, tool),
971 "{language}/{tool:?}, up {up:?}, order {order:?}"
972 );
973 }
974 }
975 for tool in ToolKind::ALL.iter().copied() {
976 assert_eq!(router.resolve_any(tool), batch.resolve_any(tool));
977 }
978 let failed: HashSet<ServerId> = translator
979 .startup_failures()
980 .into_iter()
981 .map(|failure| failure.server_id)
982 .collect();
983 let expected_failed: HashSet<ServerId> = configs
984 .iter()
985 .map(crate::config::LspServerConfig::id)
986 .filter(|id| !registered.contains(id))
987 .collect();
988 assert_eq!(failed, expected_failed);
989 assert!(!lock_std(&translator.servers).any_expected());
990 }
991 }
992 }
993
994 #[tokio::test]
1000 async fn dead_explicit_route_is_retryable_while_the_catch_all_initializes() {
1001 use tracing_subscriber::prelude::*;
1002
1003 use crate::test_lsp::CapturedLogs;
1004
1005 let x = named_config(
1006 "x",
1007 "rust",
1008 Some(vec![ToolKind::Hover, ToolKind::Diagnostics]),
1009 );
1010 let c = named_config("c", "rust", None);
1011 let translator = Translator::new()
1012 .with_extensions(crate::test_lsp::test_extensions())
1013 .with_router(ToolRouter::from_configs([&x, &c]).unwrap());
1014 translator.set_expected_servers([x.id(), c.id()].into_iter().collect());
1015 let path = PathBuf::from("/ws/main.rs");
1016 let logs = CapturedLogs::default();
1017 let subscriber = tracing_subscriber::registry().with(logs.clone());
1018
1019 tracing::subscriber::with_default(subscriber, || {
1020 translator.settle_failed(&spawn_failure(&x));
1021
1022 let hover = translator
1023 .client_for_file(&path, ToolKind::Hover)
1024 .unwrap_err();
1025 std::assert_matches!(
1026 hover,
1027 Error::ServerInitializing { server_id } if server_id == c.id()
1028 );
1029 std::assert_matches!(
1030 translator.diagnostics_route_for_path(&path),
1031 routing::DiagnosticsRoute::Initializing(id) if id == c.id()
1032 );
1033 assert_eq!(
1034 translator
1035 .tool_support_snapshot()
1036 .document_support(&LanguageId::from_static("rust"), ToolKind::Hover),
1037 RouteSupport::Initializing
1038 );
1039 assert!(
1040 logs.entries()
1041 .iter()
1042 .all(|(level, _)| *level != tracing::Level::ERROR),
1043 "{:?}",
1044 logs.messages()
1045 );
1046 });
1047
1048 translator.settle_started(crate::lsp::fake_lsp_server_with_config(c.clone()));
1049 let (served_by, _) = translator.client_for_file(&path, ToolKind::Hover).unwrap();
1050 assert_eq!(served_by, c.id());
1051 }
1052
1053 #[tokio::test]
1058 async fn test_settle_failed_window_reads_initializing_not_failed() {
1059 let configs = [
1060 named_config("a", "rust", Some(vec![ToolKind::Hover])),
1061 named_config("b", "rust", None),
1062 ];
1063 let translator = Translator::new()
1064 .with_extensions(crate::test_lsp::test_extensions())
1065 .with_router(ToolRouter::from_configs(&configs).unwrap());
1066 translator.set_expected_servers(
1067 configs
1068 .iter()
1069 .map(crate::config::LspServerConfig::id)
1070 .collect(),
1071 );
1072 translator.settle_started(crate::lsp::fake_lsp_server_with_config(configs[1].clone()));
1073 let path = PathBuf::from("/ws/main.rs");
1074
1075 lock_std(&translator.servers).announce_failure(&spawn_failure(&configs[0]));
1076
1077 let err = translator
1078 .client_for_file(&path, ToolKind::Hover)
1079 .unwrap_err();
1080 std::assert_matches!(&err, Error::ServerInitializing { server_id } if *server_id == configs[0].id());
1081 assert_eq!(
1082 translator
1083 .tool_support_snapshot()
1084 .document_support(&LanguageId::from_static("rust"), ToolKind::Hover),
1085 RouteSupport::Initializing
1086 );
1087
1088 translator.settle_failed(&spawn_failure(&configs[0]));
1089 let (served_by, _) = translator.client_for_file(&path, ToolKind::Hover).unwrap();
1090 assert_eq!(served_by, configs[1].id());
1091 }
1092
1093 #[tokio::test]
1096 async fn test_snapshot_between_rebind_and_failure_record_still_routes_to_the_catch_all() {
1097 let configs = [
1098 named_config("a", "rust", Some(vec![ToolKind::Hover])),
1099 named_config("b", "rust", None),
1100 ];
1101 let translator = Translator::new()
1102 .with_extensions(crate::test_lsp::test_extensions())
1103 .with_router(ToolRouter::from_configs(&configs).unwrap());
1104 translator.set_expected_servers(
1105 configs
1106 .iter()
1107 .map(crate::config::LspServerConfig::id)
1108 .collect(),
1109 );
1110 translator.settle_started(crate::lsp::fake_lsp_server_with_config(configs[1].clone()));
1111
1112 lock_std(&translator.servers).announce_failure(&spawn_failure(&configs[0]));
1113 translator.rebind_router_to_settled();
1114
1115 assert_matches!(
1116 translator
1117 .tool_support_snapshot()
1118 .document_support(&LanguageId::from_static("rust"), ToolKind::Hover),
1119 RouteSupport::Supported { server } | RouteSupport::CapabilityNotAdvertised { server, .. }
1120 if server == configs[1].id()
1121 );
1122 }
1123
1124 #[tokio::test]
1128 async fn test_init_panic_during_a_restart_does_not_fail_or_drop_the_restarting_server() {
1129 let config = crate::config::LspServerConfig::rust_analyzer();
1130 let id = config.id();
1131 let translator = Translator::new();
1132 translator
1133 .register_server_complete(crate::lsp::fake_lsp_server_with_config(config.clone()));
1134 let lock_before = translator.respawn_lock(&id);
1135 let held = lock_std(&translator.servers).take_for_restart(&id).unwrap();
1136
1137 translator
1138 .settle_after_init_panic(&[crate::test_lsp::init_config_for(config)])
1139 .await;
1140
1141 assert!(translator.startup_failure(&id).is_none());
1142 assert!(Arc::ptr_eq(&lock_before, &translator.respawn_lock(&id)));
1143 let refused = lock_std(&translator.servers).restore(&id, held);
1144 assert!(refused.is_none());
1145 assert_eq!(translator.registered_server_count(), 1);
1146 }
1147
1148 #[tokio::test]
1151 async fn settle_after_init_panic_after_partial_settlement_records_only_unsettled() {
1152 let configs = [
1153 named_config("a", "rust", Some(vec![ToolKind::Hover])),
1154 named_config("b", "python", None),
1155 named_config("c", "typescript", None),
1156 ];
1157 let translator = Translator::new().with_router(ToolRouter::from_configs(&configs).unwrap());
1158 translator.set_expected_servers(
1159 configs
1160 .iter()
1161 .map(crate::config::LspServerConfig::id)
1162 .collect(),
1163 );
1164 translator.settle_started(crate::lsp::fake_lsp_server_with_config(configs[0].clone()));
1165 translator.settle_failed(&ServerSpawnFailure {
1166 reason: StartupFailure::Spawn(std::sync::Arc::new(Error::ServerNotFound {
1167 command: crate::config::ServerCommand::from_static("b"),
1168 io: std::io::Error::from(std::io::ErrorKind::NotFound),
1169 })),
1170 ..spawn_failure(&configs[1])
1171 });
1172 let init_configs: Vec<_> = configs
1173 .iter()
1174 .cloned()
1175 .map(crate::test_lsp::init_config_for)
1176 .collect();
1177
1178 translator.settle_after_init_panic(&init_configs).await;
1179
1180 assert!(translator.startup_failure(&configs[0].id()).is_none());
1181 std::assert_matches!(
1182 translator.startup_failure(&configs[1].id()).unwrap().reason,
1183 StartupFailure::Spawn(_)
1184 );
1185 std::assert_matches!(
1186 translator.startup_failure(&configs[2].id()).unwrap().reason,
1187 StartupFailure::InitTaskPanicked
1188 );
1189 }
1190
1191 #[tokio::test]
1195 async fn test_settle_after_init_panic_turns_initializing_into_startup_failure() {
1196 let config = crate::config::LspServerConfig::rust_analyzer();
1197 let id = config.id();
1198 let translator = Translator::new()
1199 .with_extensions(crate::test_lsp::test_extensions())
1200 .with_router(ToolRouter::catch_all([(
1201 id.clone(),
1202 LanguageId::from_static("rust"),
1203 )]));
1204 translator.set_expected_servers(HashSet::from([id.clone()]));
1205 let path = PathBuf::from("/ws/main.rs");
1206 let before = translator
1207 .client_for_file(&path, ToolKind::Hover)
1208 .unwrap_err();
1209 assert_matches!(before, Error::ServerInitializing { .. }, "got {before:?}");
1210
1211 translator
1212 .settle_after_init_panic(&[crate::test_lsp::init_config_for(config)])
1213 .await;
1214
1215 let after = translator
1216 .client_for_file(&path, ToolKind::Hover)
1217 .unwrap_err();
1218 assert_matches!(
1219 &after,
1220 Error::ServerFailedToStart(f)
1221 if f.server_id == id && matches!(f.reason, StartupFailure::InitTaskPanicked),
1222 "got {after:?}"
1223 );
1224 }
1225
1226 #[tokio::test]
1228 async fn test_settle_after_init_panic_keeps_registered_server_routable() {
1229 let config = crate::config::LspServerConfig::rust_analyzer();
1230 let id = config.id();
1231 let translator = Translator::new()
1232 .with_extensions(crate::test_lsp::test_extensions())
1233 .with_router(ToolRouter::catch_all([(
1234 id.clone(),
1235 LanguageId::from_static("rust"),
1236 )]));
1237 translator.set_expected_servers(HashSet::from([id.clone()]));
1238 translator
1239 .register_server_complete(crate::lsp::fake_lsp_server_with_config(config.clone()));
1240
1241 translator
1242 .settle_after_init_panic(&[crate::test_lsp::init_config_for(config)])
1243 .await;
1244
1245 let (routed, _client) = translator
1246 .client_for_file(Path::new("/ws/main.rs"), ToolKind::Hover)
1247 .unwrap();
1248 assert_eq!(routed, id);
1249 assert!(translator.startup_failure(&id).is_none());
1250 }
1251
1252 #[test]
1253 fn test_record_startup_failures_orders_listing_by_server_id() {
1254 let translator = Translator::new();
1255 let failure = |id: &str| ServerSpawnFailure {
1256 server_id: ServerId::new(id).unwrap(),
1257 language_id: LanguageId::new(id).unwrap(),
1258 command: ServerCommand::new(id).unwrap(),
1259 reason: StartupFailure::InitTaskPanicked,
1260 };
1261 translator.record_startup_failures(&[failure("zls"), failure("clangd"), failure("gopls")]);
1262
1263 let ids: Vec<_> = translator
1264 .startup_failures()
1265 .into_iter()
1266 .map(|f| f.server_id.as_str().to_string())
1267 .collect();
1268 assert_eq!(ids, ["clangd", "gopls", "zls"]);
1269 }
1270
1271 #[tokio::test]
1273 async fn test_join_shutdown_tasks_survives_panicking_task() {
1274 let mut tasks = tokio::task::JoinSet::new();
1275 let handle = tasks.spawn(async { panic!("shutdown task boom") });
1276 let mut ids = HashMap::new();
1277 ids.insert(handle.id(), ServerId::from_static("rust"));
1278 tasks.spawn(async {});
1279
1280 join_shutdown_tasks(tasks, &ids).await;
1281 }
1282
1283 #[test]
1284 fn test_translator_new() {
1285 let translator = Translator::new();
1286 assert!(translator.workspace_roots.is_empty());
1287 assert_eq!(translator.registered_server_count(), 0);
1288 }
1289
1290 #[test]
1291 fn test_set_workspace_roots() {
1292 let mut translator = Translator::new();
1293 let roots = vec![PathBuf::from("/test/root1"), PathBuf::from("/test/root2")];
1294 translator.set_workspace_roots(WorkspaceRoots::for_test(roots.clone(), Vec::new()));
1295 assert_eq!(translator.workspace_roots.canonical(), roots);
1296 }
1297
1298 #[test]
1299 fn test_register_server() {
1300 let translator = Translator::new();
1301
1302 assert_eq!(translator.registered_server_count(), 0);
1304
1305 }
1314
1315 #[tokio::test]
1318 async fn test_shutdown_servers_empty_registry_returns_promptly() {
1319 let translator = Translator::new();
1320
1321 let result =
1322 tokio::time::timeout(Duration::from_secs(1), translator.shutdown_servers()).await;
1323
1324 assert!(
1325 result.is_ok(),
1326 "shutdown_servers must return promptly when no servers are registered"
1327 );
1328 }
1329
1330 #[tokio::test]
1338 async fn test_shutdown_servers_drains_registered_servers() {
1339 let translator = Translator::new();
1340 translator.register_server(
1341 ServerId::from_static("server-a"),
1342 crate::lsp::fake_lsp_server(),
1343 );
1344 translator.register_server(
1345 ServerId::from_static("server-b"),
1346 crate::lsp::fake_lsp_server(),
1347 );
1348 assert_eq!(translator.registered_server_count(), 2);
1349
1350 let result =
1354 tokio::time::timeout(Duration::from_secs(20), translator.shutdown_servers()).await;
1355
1356 assert!(
1357 result.is_ok(),
1358 "shutdown_servers must not hang against non-responsive mock servers"
1359 );
1360 assert_eq!(
1361 translator.registered_server_count(),
1362 0,
1363 "all registered servers must be drained"
1364 );
1365 }
1366
1367 #[test]
1368 fn test_clear_expected_servers_reverts_to_no_server_after_all_routes_dropped() {
1369 let path = PathBuf::from("/ws/Assets/Scripts/Player.cs");
1375 let lang = LanguageMap::default().detect(&path);
1376 let id = ServerId::from(lang.clone());
1377
1378 let translator = Translator::new().with_router(ToolRouter::catch_all([(
1379 id.clone(),
1380 LanguageId::new(lang).unwrap(),
1381 )]));
1382 let mut expected = HashSet::new();
1383 expected.insert(id);
1384 translator.set_expected_servers(expected);
1385
1386 translator.rebind_router(&HashSet::new());
1387 translator.clear_expected_servers();
1388
1389 let err = translator
1390 .client_for_file(&path, ToolKind::Hover)
1391 .unwrap_err();
1392 assert_matches!(err, Error::NoServerForLanguage { .. });
1393 }
1394
1395 #[test]
1396 fn test_translator_with_custom_extensions() {
1397 let mut extension_map = HashMap::new();
1398 extension_map.insert(
1399 FileExtension::from_static("nu"),
1400 LanguageId::from_static("nushell"),
1401 );
1402 extension_map.insert(
1403 FileExtension::from_static("customext"),
1404 LanguageId::from_static("customlang"),
1405 );
1406
1407 let translator = Translator::new().with_extensions(extension_map);
1408
1409 assert_eq!(translator.language_map.detect(Path::new("a.nu")), "nushell");
1410 assert_eq!(
1411 translator.language_map.detect(Path::new("a.customext")),
1412 "customlang"
1413 );
1414 assert_eq!(translator.language_map.languages().count(), 2);
1415 }
1416
1417 #[tokio::test]
1425 async fn test_with_resource_limits_applies_before_with_extensions() {
1426 let limits = ResourceLimits {
1427 max_documents: DocumentLimit::new(1),
1428 max_file_size: SizeLimit::UNLIMITED,
1429 };
1430 let translator = Translator::new()
1431 .with_resource_limits(limits)
1432 .with_extensions(LanguageMap::default());
1433
1434 let dir = TempDir::new().unwrap();
1435 let path_a = dir.path().join("a.rs");
1436 std::fs::write(&path_a, "a").unwrap();
1437 let path_b = dir.path().join("b.rs");
1438 std::fs::write(&path_b, "b").unwrap();
1439
1440 let (client, _server) = fake_lsp_client();
1441 let server_id = ServerId::from_static("rust");
1442
1443 translator
1444 .document_tracker
1445 .ensure_open_for_test(&path_a, &server_id, &client)
1446 .await
1447 .unwrap();
1448 translator
1449 .document_tracker
1450 .ensure_open_for_test(&path_b, &server_id, &client)
1451 .await
1452 .unwrap();
1453 assert_eq!(translator.document_tracker.len(), 1);
1454 assert_eq!(
1455 translator.document_tracker.open_state(&path_a),
1456 DocumentOpenState::NotOpen
1457 );
1458 assert_eq!(
1459 translator.document_tracker.open_state(&path_b),
1460 DocumentOpenState::Open
1461 );
1462 }
1463
1464 #[tokio::test]
1477 async fn test_with_resource_limits_applies_after_with_extensions() {
1478 let limits = ResourceLimits {
1479 max_documents: DocumentLimit::new(1),
1480 max_file_size: SizeLimit::UNLIMITED,
1481 };
1482 let translator = Translator::new()
1483 .with_extensions(HashMap::from([(
1484 FileExtension::from_static("rs"),
1485 LanguageId::from_static("rust"),
1486 )]))
1487 .with_resource_limits(limits);
1488
1489 let dir = TempDir::new().unwrap();
1490 let path_a = dir.path().join("a.rs");
1491 std::fs::write(&path_a, "a").unwrap();
1492 let path_b = dir.path().join("b.rs");
1493 std::fs::write(&path_b, "b").unwrap();
1494
1495 let (client, _server) = fake_lsp_client();
1496 let server_id = ServerId::from_static("rust");
1497
1498 translator
1499 .document_tracker
1500 .ensure_open_for_test(&path_a, &server_id, &client)
1501 .await
1502 .unwrap();
1503 translator
1504 .document_tracker
1505 .ensure_open_for_test(&path_b, &server_id, &client)
1506 .await
1507 .unwrap();
1508 assert_eq!(translator.document_tracker.len(), 1);
1509 assert_eq!(
1510 translator.document_tracker.open_state(&path_a),
1511 DocumentOpenState::NotOpen
1512 );
1513
1514 let state = translator.document_tracker.close(&path_b).unwrap();
1515 assert_eq!(
1516 state.language_id(),
1517 "rust",
1518 "extension map must have survived with_resource_limits's rebuild"
1519 );
1520 }
1521
1522 fn refusal_of(config: &crate::config::LspServerConfig) -> ServerSpawnFailure {
1523 ServerSpawnFailure {
1524 reason: StartupFailure::RefusedUntrustedWorkspace(
1525 crate::error::UntrustedRefusal::NotAllowed {
1526 builtin: Some(crate::config::BuiltinServer::RustAnalyzer),
1527 },
1528 ),
1529 ..spawn_failure(config)
1530 }
1531 }
1532
1533 #[test]
1534 fn test_record_refusals_without_refusals_leaves_translator_untouched() {
1535 let config = crate::config::LspServerConfig::rust_analyzer();
1536 let translator =
1537 Translator::new().with_router(ToolRouter::from_configs([&config]).unwrap());
1538 let before = translator.router_snapshot();
1539
1540 translator.record_refusals(&[]);
1541
1542 assert!(translator.startup_failures().is_empty());
1543 assert!(Arc::ptr_eq(&before, &translator.router_snapshot()));
1544 }
1545
1546 #[test]
1547 fn test_refused_server_without_catch_all_reports_the_refusal() {
1548 let config = crate::config::LspServerConfig::rust_analyzer();
1549 let translator = Translator::new()
1550 .with_extensions(crate::test_lsp::test_extensions())
1551 .with_router(ToolRouter::from_configs([&config]).unwrap());
1552
1553 translator.record_refusals(&[refusal_of(&config)]);
1554
1555 let err = translator
1556 .client_for_file(Path::new("/ws/main.rs"), ToolKind::Hover)
1557 .unwrap_err();
1558 assert_matches!(&err, Error::ServerFailedToStart(f) if f.server_id == config.id());
1559 let text = err.to_string();
1560 assert!(text.contains("'rust'"), "{text}");
1561 assert!(text.contains("--allow-server rust"), "{text}");
1562 assert!(!text.contains("not retried"), "{text}");
1563 }
1564
1565 #[tokio::test]
1566 async fn test_refused_explicit_server_leaves_allowed_catch_all_serving() {
1567 let explicit = named_config("explicit", "rust", Some(vec![ToolKind::Hover]));
1568 let catch_all = named_config("allowed", "rust", None);
1569 let translator = Translator::new()
1570 .with_extensions(crate::test_lsp::test_extensions())
1571 .with_router(ToolRouter::from_configs([&explicit, &catch_all]).unwrap());
1572 translator.set_expected_servers(HashSet::from([catch_all.id()]));
1573
1574 translator.record_refusals(&[refusal_of(&explicit)]);
1575 translator.settle_started(crate::lsp::fake_lsp_server_with_config(catch_all.clone()));
1576
1577 let (routed, _client) = translator
1578 .client_for_file(Path::new("/ws/main.rs"), ToolKind::Hover)
1579 .unwrap();
1580 assert_eq!(routed, catch_all.id());
1581 }
1582}