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::{DocumentTracker, NotificationCache, WorkspaceRoots};
22use crate::config::{
23 FilePattern, IndexingReadyTimeoutSecs, LanguageId, LanguageMap, ServerId, ServerSettlement,
24 ToolKind, ToolRouter,
25};
26use crate::error::{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 selection_range;
58mod servers;
59mod support;
60mod symbols;
61#[cfg(test)]
62mod testing;
63mod type_hierarchy;
64
65pub use addressing::{
66 AddressableTool, Addressed, MAX_SYMBOL_NAME_BYTES, PositionSource, ResolvedSymbol,
67 ResolvedTarget, SymbolName, SymbolNameError, SymbolQuery, SymbolTarget,
68};
69pub use availability::{DiagnosticsAnswer, DiagnosticsAvailability, DiagnosticsOrigin};
70pub use dto::*;
71pub use enclosing::{
72 Contextual, ContextualDiagnostic, ContextualLocation, EnclosingSymbol, EnclosingSymbolOutcome,
73 EnrichmentSummary, NotComputedReason, ResultContext, SymbolFidelity, UnavailableReason,
74};
75pub use kind_filter::{
76 CodeActionKindFilter, KindFilter, KindFilterField, KindFilterInput, RejectedKindFilter,
77 Rejection, SymbolKindFilter,
78};
79pub use restart::{
80 DiagnosticsRole, MAX_RESTART_SERVER_IDS, MAX_SERVER_ID_BYTES, NotificationReceivers,
81 NotificationWiring, RestartFailure, RestartOutcome, RestartServerResult, RestartTarget,
82 ServerIds, ServerIdsError, ServerRestartEntry,
83};
84pub use routing::Capability;
85pub use support::{RouteSupport, ToolSupportSnapshot};
86
87#[derive(Debug)]
103pub struct Translator {
104 servers: StdMutex<Servers>,
108 phase: StdMutex<Phase>,
110 startup_redactions: Arc<Redactions>,
113 merged_redactions: StdMutex<Option<MergedRedactions>>,
115 document_tracker: Arc<DocumentTracker>,
117 resource_limits: ResourceLimits,
123 workspace_roots: WorkspaceRoots,
126 language_map: Arc<LanguageMap>,
129 file_patterns: Arc<[FilePattern]>,
132 router: Arc<StdMutex<Arc<ToolRouter>>>,
137 configured_router: Arc<ToolRouter>,
143 notification_cache: Option<Arc<Mutex<NotificationCache>>>,
150 wiring: OnceLock<Arc<dyn NotificationWiring>>,
154 clock: Arc<dyn Clock>,
159 indexing_ready_timeout: std::time::Duration,
164}
165
166impl Translator {
167 #[must_use]
177 pub fn new() -> Self {
178 Self {
179 servers: StdMutex::new(Servers::default()),
180 phase: StdMutex::new(Phase::default()),
181 startup_redactions: Arc::default(),
182 merged_redactions: StdMutex::new(None),
183 document_tracker: Arc::new(DocumentTracker::new(
184 ResourceLimits::default(),
185 HashMap::new(),
186 )),
187 resource_limits: ResourceLimits::default(),
188 workspace_roots: WorkspaceRoots::default(),
189 language_map: Arc::default(),
190 file_patterns: Arc::default(),
191 router: Arc::new(StdMutex::new(Arc::new(ToolRouter::default()))),
192 configured_router: Arc::new(ToolRouter::default()),
193 notification_cache: None,
194 wiring: OnceLock::new(),
195 clock: Arc::new(SystemClock),
196 indexing_ready_timeout: navigation::INDEXING_READY_TIMEOUT,
197 }
198 }
199
200 #[cfg(test)]
206 #[must_use]
207 fn with_clock(mut self, clock: Arc<dyn Clock>) -> Self {
208 self.clock = clock;
209 self
210 }
211
212 pub fn set_workspace_roots(&mut self, roots: WorkspaceRoots) {
222 self.workspace_roots = roots;
223 }
224
225 #[must_use]
232 pub fn with_notification_cache(mut self, cache: Arc<Mutex<NotificationCache>>) -> Self {
233 self.notification_cache = Some(cache);
234 self
235 }
236
237 #[must_use]
241 pub(crate) fn with_startup_redactions(mut self, redactions: Arc<Redactions>) -> Self {
242 self.startup_redactions = redactions;
243 self
244 }
245
246 #[must_use]
254 pub const fn with_indexing_ready_timeout(mut self, timeout: IndexingReadyTimeoutSecs) -> Self {
255 self.indexing_ready_timeout = timeout.as_duration();
256 self
257 }
258
259 #[allow(
262 clippy::needless_pass_by_value,
263 reason = "public API: callers hand over the set they built"
264 )]
265 pub fn set_expected_servers(&self, servers: HashSet<ServerId>) {
266 lock_std(&self.servers).set_expected(&servers);
267 }
268
269 pub fn clear_expected_servers(&self) {
271 lock_std(&self.servers).clear_expected();
272 }
273
274 #[must_use]
279 pub fn with_router(mut self, router: ToolRouter) -> Self {
280 let router = Arc::new(router);
281 self.configured_router = Arc::clone(&router);
282 self.router = Arc::new(StdMutex::new(router));
283 self
284 }
285
286 #[cfg(test)]
291 pub(crate) fn record_startup_failures(&self, failures: &[ServerSpawnFailure]) {
292 let mut servers = lock_std(&self.servers);
293 for failure in failures {
294 servers.record_failure(failure);
295 }
296 }
297
298 pub(crate) fn startup_failure(&self, id: &ServerId) -> Option<ServerSpawnFailure> {
300 lock_std(&self.servers).failure(id).cloned()
301 }
302
303 pub(crate) fn startup_failures(&self) -> Vec<ServerSpawnFailure> {
305 lock_std(&self.servers).failures()
306 }
307
308 pub(crate) fn record_refusals(&self, refused: &[ServerSpawnFailure]) {
315 if refused.is_empty() {
316 return;
317 }
318 {
319 let mut servers = lock_std(&self.servers);
320 for failure in refused {
321 servers.record_failure(failure);
322 }
323 }
324 self.rebind_router_to_settled();
325 for failure in refused {
326 self.warn_failed_routes(&failure.server_id, &RouteLoss::of(failure));
327 }
328 }
329
330 pub(crate) async fn settle_after_init_panic(&self, configs: &[ServerInitConfig]) {
340 lock_std(&self.phase).init_panicked();
341 let registered: HashSet<ServerId> = {
342 let mut servers = lock_std(&self.servers);
343 servers.fail_unsettled(configs.iter().map(|config| {
344 let server_config = &config.server_config();
345 ServerSpawnFailure {
346 server_id: server_config.id(),
347 language_id: server_config.language_id.clone(),
348 command: server_config.command.server_command().clone(),
349 reason: StartupFailure::InitTaskPanicked,
350 }
351 }));
352 servers
353 .ids()
354 .filter(|id| servers.client(id).is_some())
355 .cloned()
356 .collect()
357 };
358 self.rebind_router_to_settled();
359 self.clear_expected_servers();
360
361 if let Some(cache) = &self.notification_cache {
362 let mut cache = cache.lock().await;
363 for id in ®istered {
364 cache.mark_push_degraded(id);
365 cache.reset_indexing_state(id, IndexingReset::Forget);
366 }
367 }
368 }
369
370 pub fn rebind_router(&self, registered: &HashSet<ServerId>) {
374 self.install_router(|id| {
375 if registered.contains(id) {
376 ServerSettlement::Registered
377 } else {
378 ServerSettlement::Failed
379 }
380 });
381 }
382
383 pub(crate) fn rebind_router_to_settled(&self) {
389 let settlements = lock_std(&self.servers).settlements();
390 self.install_router(|id| {
391 settlements
392 .get(id)
393 .copied()
394 .unwrap_or(ServerSettlement::Pending)
395 });
396 }
397
398 fn install_router(&self, settlement: impl Fn(&ServerId) -> ServerSettlement) {
399 let mut router = (*self.configured_router).clone();
400 router.rebind(settlement);
401 *lock_std(&self.router) = Arc::new(router);
402 }
403
404 pub(super) fn router_snapshot(&self) -> Arc<ToolRouter> {
407 Arc::clone(&lock_std(&self.router))
408 }
409
410 pub(crate) fn settle_started(&self, server: LspServer) -> (ServerId, LanguageId) {
412 let id = server.init_config().server_config().id();
413 let language = server.client().language_id().clone();
414 let registered = lock_std(&self.servers).register(id.clone(), Backend::Process(server));
415 drop(registered.displaced);
416 if !registered.was_expected {
417 tracing::error!("LSP server '{id}' settled twice or was never expected");
418 }
419 self.rebind_router_to_settled();
420 (id, language)
421 }
422
423 pub(crate) fn settle_failed(&self, failure: &ServerSpawnFailure) {
425 let id = failure.server_id.clone();
426 lock_std(&self.servers).announce_failure(failure);
429 self.rebind_router_to_settled();
430 let was_expected = lock_std(&self.servers).record_failure(failure);
431 if !was_expected {
432 tracing::error!("LSP server '{id}' settled twice or was never expected");
433 }
434 self.warn_failed_routes(&id, &RouteLoss::FailedToStart);
435 }
436
437 fn warn_failed_routes(&self, id: &ServerId, loss: &RouteLoss<'_>) {
440 let languages = self.configured_router.languages_routed_to(id);
441 let catch_all_states: Vec<String> = languages
442 .iter()
443 .map(|language| {
444 let state = match self.configured_router.catch_all_for(language.as_str()) {
445 None => "no catch-all".to_string(),
446 Some(catch_all) if catch_all == id => "it was the catch-all".to_string(),
447 Some(catch_all) if lock_std(&self.servers).client(catch_all).is_some() => {
448 format!("catch-all '{catch_all}' registered")
449 }
450 Some(catch_all) if self.startup_failure(catch_all).is_some() => {
451 format!("catch-all '{catch_all}' failed")
452 }
453 Some(catch_all) => format!("catch-all '{catch_all}' pending"),
454 };
455 format!("{language}: {state}")
456 })
457 .collect();
458 tracing::warn!(
459 "server '{id}' {loss}; routes of [{}] are rebound or dropped",
460 catch_all_states.join(", ")
461 );
462 }
463
464 #[must_use]
471 pub fn is_diagnostics_route(&self, language_id: &LanguageId, id: &ServerId) -> bool {
472 lock_std(&self.router).resolve(language_id.as_str(), ToolKind::Diagnostics) == Some(id)
473 }
474
475 #[must_use]
484 pub(crate) fn position_encoding_for(&self, server_id: &ServerId) -> PositionEncoding {
485 lock_std(&self.servers)
486 .server(server_id)
487 .and_then(|server| PositionEncoding::from_lsp(server.position_encoding().as_str()))
488 .unwrap_or(PositionEncoding::Utf16)
489 }
490
491 fn encoding_ctx(&self, server_id: &ServerId) -> EncodingCtx {
494 EncodingCtx::new(
495 self.position_encoding_for(server_id),
496 self.document_tracker.clone(),
497 self.workspace_roots.clone(),
498 )
499 }
500
501 fn rebuild_document_tracker(&mut self) {
512 self.document_tracker = Arc::new(DocumentTracker::new(
513 self.resource_limits,
514 (*self.language_map).clone(),
515 ));
516 }
517
518 #[must_use]
522 pub fn with_file_patterns(mut self, patterns: impl IntoIterator<Item = FilePattern>) -> Self {
523 self.file_patterns = patterns.into_iter().collect();
524 self
525 }
526
527 #[must_use]
536 pub fn with_extensions(mut self, language_map: impl Into<LanguageMap>) -> Self {
537 self.language_map = Arc::new(language_map.into());
538 self.rebuild_document_tracker();
539 self
540 }
541
542 #[must_use]
554 pub fn with_resource_limits(mut self, limits: ResourceLimits) -> Self {
555 self.resource_limits = limits;
556 self.rebuild_document_tracker();
557 self
558 }
559
560 #[cfg(test)]
563 pub(crate) fn register_client(&self, id: ServerId, client: crate::lsp::LspClient) {
564 lock_std(&self.servers).register_test_client(id, client);
565 }
566
567 pub(crate) fn server_text_redactions(&self) -> Arc<Redactions> {
572 let sources: Vec<Arc<Redactions>> = std::iter::once(Arc::clone(&self.startup_redactions))
573 .chain(
574 lock_std(&self.servers)
575 .clients()
576 .map(|client| Arc::clone(client.redactions())),
577 )
578 .filter(|set| !set.is_empty())
579 .collect();
580 let mut cache = lock_std(&self.merged_redactions);
581 if let Some(cached) = cache
582 .as_ref()
583 .filter(|cached| cached.is_built_from(&sources))
584 {
585 return Arc::clone(&cached.merged);
586 }
587 let merged = Arc::new(Redactions::union(sources.iter().map(Arc::as_ref)));
588 *cache = Some(MergedRedactions {
589 sources,
590 merged: Arc::clone(&merged),
591 });
592 merged
593 }
594
595 #[cfg(test)]
598 pub(crate) fn register_server(&self, id: ServerId, server: LspServer) {
599 lock_std(&self.servers).register_test_server(id, server);
600 }
601
602 pub fn register_server_complete(&self, server: LspServer) {
619 let id = server.init_config().server_config().id();
620 let registered = lock_std(&self.servers).register(id, Backend::Process(server));
621 drop(registered.displaced);
622 }
623
624 #[cfg(test)]
632 pub(crate) fn registered_server_count(&self) -> usize {
633 lock_std(&self.servers).running_servers().count()
634 }
635
636 #[must_use]
638 pub fn open_document_paths(&self) -> Vec<PathBuf> {
639 self.document_tracker.open_paths()
640 }
641
642 #[must_use]
644 pub fn is_document_open(&self, path: &Path) -> bool {
645 self.document_tracker.is_open(path)
646 }
647
648 #[must_use]
652 pub(crate) const fn document_tracker(&self) -> &Arc<DocumentTracker> {
653 &self.document_tracker
654 }
655
656 pub(crate) async fn shutdown_servers(&self) {
689 self.begin_shutdown();
690 let servers: Vec<(ServerId, LspServer)> = lock_std(&self.servers).drain_servers();
691 if servers.is_empty() {
692 return;
693 }
694
695 let mut tasks = tokio::task::JoinSet::new();
696 let mut ids = HashMap::new();
697 for (id, server) in servers {
698 let task_id = id.clone();
699 let handle = tasks.spawn(async move {
700 match server.shutdown().await {
701 Ok(()) => tracing::debug!(%id, "LSP server shut down gracefully"),
702 Err(e) => tracing::warn!(
703 %id, error = %e,
704 "LSP server shutdown failed, killing process instead"
705 ),
706 }
707 });
708 ids.insert(handle.id(), task_id);
709 }
710 join_shutdown_tasks(tasks, &ids).await;
711 }
712}
713
714async fn join_shutdown_tasks(
717 mut tasks: tokio::task::JoinSet<()>,
718 ids: &HashMap<tokio::task::Id, ServerId>,
719) {
720 while let Some(joined) = tasks.join_next_with_id().await {
721 if let Err(e) = joined {
722 let id = ids.get(&e.id()).map_or("unknown", ServerId::as_str);
723 tracing::error!(%id, error = %e, "LSP server shutdown task failed");
724 }
725 }
726}
727
728impl Default for Translator {
729 fn default() -> Self {
733 Self::new()
734 }
735}
736
737#[derive(Debug)]
739struct MergedRedactions {
740 sources: Vec<Arc<Redactions>>,
741 merged: Arc<Redactions>,
742}
743
744impl MergedRedactions {
745 fn is_built_from(&self, sources: &[Arc<Redactions>]) -> bool {
747 self.sources.len() == sources.len()
748 && sources
749 .iter()
750 .all(|set| self.sources.iter().any(|known| Arc::ptr_eq(known, set)))
751 }
752}
753
754enum RouteLoss<'a> {
756 FailedToStart,
757 Refused(&'a crate::error::UntrustedRefusal),
758}
759
760impl<'a> RouteLoss<'a> {
761 const fn of(failure: &'a ServerSpawnFailure) -> Self {
762 match &failure.reason {
763 StartupFailure::RefusedUntrustedWorkspace(refusal) => Self::Refused(refusal),
764 StartupFailure::Spawn(_) | StartupFailure::InitTaskPanicked => Self::FailedToStart,
765 }
766 }
767}
768
769impl std::fmt::Display for RouteLoss<'_> {
770 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
771 match self {
772 Self::FailedToStart => f.write_str("failed to start"),
773 Self::Refused(refusal) => write!(f, "refused (untrusted workspace: {refusal})"),
774 }
775 }
776}
777
778#[cfg(test)]
779mod tests {
780 use std::assert_matches;
781 use std::collections::{HashMap, HashSet};
782 use std::path::PathBuf;
783
784 use tempfile::TempDir;
785 use tokio::time::Duration;
786
787 use super::*;
788 use crate::config::{
789 DocumentLimit, FileExtension, ServerCommand, ServerId, SizeLimit, ToolKind, ToolRouter,
790 ToolSet,
791 };
792 use crate::error::Error;
793 use crate::test_lsp::fake_lsp_client;
794
795 #[tokio::test]
796 async fn test_server_text_redactions_follow_live_clients_across_respawn() {
797 let translator = Translator::new();
798 assert!(translator.server_text_redactions().is_empty());
799
800 let (a, _fake_a, _lanes_a) = crate::test_lsp::fake_lsp_client_with_redactions(
801 Redactions::new([("A_TOKEN".to_owned(), "alpha-secret-111".to_owned())]),
802 );
803 let (b, _fake_b, _lanes_b) = crate::test_lsp::fake_lsp_client_with_redactions(
804 Redactions::new([("B_TOKEN".to_owned(), "bravo-secret-222".to_owned())]),
805 );
806 translator.register_client(ServerId::from_static("a"), a);
807 translator.register_client(ServerId::from_static("b"), b);
808 let both = translator.server_text_redactions();
809 assert_eq!(both.apply("alpha-secret-111"), "[redacted:A_TOKEN]");
810 assert_eq!(both.apply("bravo-secret-222"), "[redacted:B_TOKEN]");
811
812 let (respawned, _fake_c, _lanes_c) = crate::test_lsp::fake_lsp_client_with_redactions(
813 Redactions::new([("C_TOKEN".to_owned(), "charlie-secret-333".to_owned())]),
814 );
815 translator.register_client(ServerId::from_static("a"), respawned);
816 let after = translator.server_text_redactions();
817 assert_eq!(after.apply("alpha-secret-111"), "alpha-secret-111");
818 assert_eq!(after.apply("charlie-secret-333"), "[redacted:C_TOKEN]");
819 assert_eq!(after.apply("bravo-secret-222"), "[redacted:B_TOKEN]");
820 }
821
822 #[tokio::test]
823 async fn test_server_text_redactions_are_cached_and_do_not_relog() {
824 use tracing_subscriber::prelude::*;
825
826 let translator = Translator::new();
827 let (a, _fake_a, _lanes_a) = crate::test_lsp::fake_lsp_client_with_redactions(
828 Redactions::new([("A_TOKEN".to_owned(), "alpha-secret-111".to_owned())]),
829 );
830 let (b, _fake_b, _lanes_b) = crate::test_lsp::fake_lsp_client_with_redactions(
831 Redactions::new([("B_TOKEN".to_owned(), "A_TOKEN]".to_owned())]),
832 );
833 translator.register_client(ServerId::from_static("a"), a);
834 translator.register_client(ServerId::from_static("b"), b);
835
836 let logs = crate::test_lsp::CapturedLogs::default();
837 let subscriber = tracing_subscriber::registry()
838 .with(tracing_subscriber::filter::LevelFilter::WARN)
839 .with(logs.clone());
840 let first = tracing::subscriber::with_default(subscriber, || {
841 let first = translator.server_text_redactions();
842 let second = translator.server_text_redactions();
843 let third = translator.server_text_redactions();
844 assert!(Arc::ptr_eq(&first, &second) && Arc::ptr_eq(&second, &third));
845 first
846 });
847
848 assert_eq!(logs.messages().len(), 1, "{:?}", logs.messages());
849 assert_eq!(first.apply("alpha-secret-111"), "[redacted:A_TOKEN]");
850 }
851
852 #[tokio::test]
853 async fn test_register_server_complete_fills_all_maps_under_init_config_id() {
854 let translator = Translator::new();
855 let config = crate::config::LspServerConfig::rust_analyzer();
856 let id = config.id();
857 translator.register_server_complete(crate::lsp::fake_lsp_server_with_config(config));
858
859 let servers = lock_std(&translator.servers);
860 assert!(servers.client(&id).is_some());
861 assert!(servers.server(&id).is_some());
862 drop(servers);
863 }
864
865 fn named_config(
866 name: &str,
867 language: &str,
868 handles: Option<Vec<ToolKind>>,
869 ) -> crate::config::LspServerConfig {
870 let mut config = crate::config::LspServerConfig::rust_analyzer();
871 config.name = Some(ServerId::new(name).unwrap());
872 config.language_id = LanguageId::new(language).unwrap();
873 config.handles = handles.map(|tools| ToolSet::new(tools).unwrap());
874 config
875 }
876
877 fn spawn_failure(config: &crate::config::LspServerConfig) -> ServerSpawnFailure {
878 ServerSpawnFailure {
879 server_id: config.id(),
880 language_id: config.language_id.clone(),
881 command: config.command.server_command().clone(),
882 reason: StartupFailure::InitTaskPanicked,
883 }
884 }
885
886 fn settle_all(
887 translator: &Translator,
888 configs: &[crate::config::LspServerConfig],
889 up: &[bool],
890 ) {
891 for (config, up) in configs.iter().zip(up) {
892 if *up {
893 translator.settle_started(crate::lsp::fake_lsp_server_with_config(config.clone()));
894 } else {
895 translator.settle_failed(&spawn_failure(config));
896 }
897 }
898 }
899
900 #[tokio::test]
903 async fn settle_in_any_completion_order_yields_the_batch_router_and_failures() {
904 let configs = [
905 named_config("x", "rust", Some(vec![ToolKind::Hover])),
906 named_config("c", "rust", None),
907 named_config("y", "python", None),
908 ];
909 let orders = [
910 [0, 1, 2],
911 [0, 2, 1],
912 [1, 0, 2],
913 [1, 2, 0],
914 [2, 0, 1],
915 [2, 1, 0],
916 ];
917 for outcome in 0..8u8 {
918 let up: Vec<bool> = (0..3).map(|bit| outcome & (1 << bit) != 0).collect();
919 let registered: HashSet<ServerId> = configs
920 .iter()
921 .zip(&up)
922 .filter(|(_, up)| **up)
923 .map(|(config, _)| config.id())
924 .collect();
925 let mut batch = ToolRouter::from_configs(&configs).unwrap();
926 batch.rebind_to_registered(®istered);
927
928 for order in orders {
929 let translator =
930 Translator::new().with_router(ToolRouter::from_configs(&configs).unwrap());
931 translator.set_expected_servers(
932 configs
933 .iter()
934 .map(crate::config::LspServerConfig::id)
935 .collect(),
936 );
937 for index in order {
938 settle_all(
939 &translator,
940 std::slice::from_ref(&configs[index]),
941 &[up[index]],
942 );
943 }
944
945 let router = lock_std(&translator.router).clone();
946 for language in ["rust", "python"] {
947 for tool in ToolKind::ALL.iter().copied() {
948 assert_eq!(
949 router.resolve(language, tool),
950 batch.resolve(language, tool),
951 "{language}/{tool:?}, up {up:?}, order {order:?}"
952 );
953 }
954 }
955 for tool in ToolKind::ALL.iter().copied() {
956 assert_eq!(router.resolve_any(tool), batch.resolve_any(tool));
957 }
958 let failed: HashSet<ServerId> = translator
959 .startup_failures()
960 .into_iter()
961 .map(|failure| failure.server_id)
962 .collect();
963 let expected_failed: HashSet<ServerId> = configs
964 .iter()
965 .map(crate::config::LspServerConfig::id)
966 .filter(|id| !registered.contains(id))
967 .collect();
968 assert_eq!(failed, expected_failed);
969 assert!(!lock_std(&translator.servers).any_expected());
970 }
971 }
972 }
973
974 #[tokio::test]
980 async fn dead_explicit_route_is_retryable_while_the_catch_all_initializes() {
981 use tracing_subscriber::prelude::*;
982
983 use crate::test_lsp::CapturedLogs;
984
985 let x = named_config(
986 "x",
987 "rust",
988 Some(vec![ToolKind::Hover, ToolKind::Diagnostics]),
989 );
990 let c = named_config("c", "rust", None);
991 let translator = Translator::new()
992 .with_extensions(crate::test_lsp::test_extensions())
993 .with_router(ToolRouter::from_configs([&x, &c]).unwrap());
994 translator.set_expected_servers([x.id(), c.id()].into_iter().collect());
995 let path = PathBuf::from("/ws/main.rs");
996 let logs = CapturedLogs::default();
997 let subscriber = tracing_subscriber::registry().with(logs.clone());
998
999 tracing::subscriber::with_default(subscriber, || {
1000 translator.settle_failed(&spawn_failure(&x));
1001
1002 let hover = translator
1003 .client_for_file(&path, ToolKind::Hover)
1004 .unwrap_err();
1005 std::assert_matches!(
1006 hover,
1007 Error::ServerInitializing { server_id } if server_id == c.id()
1008 );
1009 std::assert_matches!(
1010 translator.diagnostics_route_for_path(&path),
1011 routing::DiagnosticsRoute::Initializing(id) if id == c.id()
1012 );
1013 assert_eq!(
1014 translator
1015 .tool_support_snapshot()
1016 .document_support(&LanguageId::from_static("rust"), ToolKind::Hover),
1017 RouteSupport::Initializing
1018 );
1019 assert!(
1020 logs.entries()
1021 .iter()
1022 .all(|(level, _)| *level != tracing::Level::ERROR),
1023 "{:?}",
1024 logs.messages()
1025 );
1026 });
1027
1028 translator.settle_started(crate::lsp::fake_lsp_server_with_config(c.clone()));
1029 let (served_by, _) = translator.client_for_file(&path, ToolKind::Hover).unwrap();
1030 assert_eq!(served_by, c.id());
1031 }
1032
1033 #[tokio::test]
1038 async fn test_settle_failed_window_reads_initializing_not_failed() {
1039 let configs = [
1040 named_config("a", "rust", Some(vec![ToolKind::Hover])),
1041 named_config("b", "rust", None),
1042 ];
1043 let translator = Translator::new()
1044 .with_extensions(crate::test_lsp::test_extensions())
1045 .with_router(ToolRouter::from_configs(&configs).unwrap());
1046 translator.set_expected_servers(
1047 configs
1048 .iter()
1049 .map(crate::config::LspServerConfig::id)
1050 .collect(),
1051 );
1052 translator.settle_started(crate::lsp::fake_lsp_server_with_config(configs[1].clone()));
1053 let path = PathBuf::from("/ws/main.rs");
1054
1055 lock_std(&translator.servers).announce_failure(&spawn_failure(&configs[0]));
1056
1057 let err = translator
1058 .client_for_file(&path, ToolKind::Hover)
1059 .unwrap_err();
1060 std::assert_matches!(&err, Error::ServerInitializing { server_id } if *server_id == configs[0].id());
1061 assert_eq!(
1062 translator
1063 .tool_support_snapshot()
1064 .document_support(&LanguageId::from_static("rust"), ToolKind::Hover),
1065 RouteSupport::Initializing
1066 );
1067
1068 translator.settle_failed(&spawn_failure(&configs[0]));
1069 let (served_by, _) = translator.client_for_file(&path, ToolKind::Hover).unwrap();
1070 assert_eq!(served_by, configs[1].id());
1071 }
1072
1073 #[tokio::test]
1076 async fn test_snapshot_between_rebind_and_failure_record_still_routes_to_the_catch_all() {
1077 let configs = [
1078 named_config("a", "rust", Some(vec![ToolKind::Hover])),
1079 named_config("b", "rust", None),
1080 ];
1081 let translator = Translator::new()
1082 .with_extensions(crate::test_lsp::test_extensions())
1083 .with_router(ToolRouter::from_configs(&configs).unwrap());
1084 translator.set_expected_servers(
1085 configs
1086 .iter()
1087 .map(crate::config::LspServerConfig::id)
1088 .collect(),
1089 );
1090 translator.settle_started(crate::lsp::fake_lsp_server_with_config(configs[1].clone()));
1091
1092 lock_std(&translator.servers).announce_failure(&spawn_failure(&configs[0]));
1093 translator.rebind_router_to_settled();
1094
1095 assert_matches!(
1096 translator
1097 .tool_support_snapshot()
1098 .document_support(&LanguageId::from_static("rust"), ToolKind::Hover),
1099 RouteSupport::Supported { server } | RouteSupport::CapabilityNotAdvertised { server, .. }
1100 if server == configs[1].id()
1101 );
1102 }
1103
1104 #[tokio::test]
1108 async fn test_init_panic_during_a_restart_does_not_fail_or_drop_the_restarting_server() {
1109 let config = crate::config::LspServerConfig::rust_analyzer();
1110 let id = config.id();
1111 let translator = Translator::new();
1112 translator
1113 .register_server_complete(crate::lsp::fake_lsp_server_with_config(config.clone()));
1114 let lock_before = translator.respawn_lock(&id);
1115 let held = lock_std(&translator.servers).take_for_restart(&id).unwrap();
1116
1117 translator
1118 .settle_after_init_panic(&[crate::test_lsp::init_config_for(config)])
1119 .await;
1120
1121 assert!(translator.startup_failure(&id).is_none());
1122 assert!(Arc::ptr_eq(&lock_before, &translator.respawn_lock(&id)));
1123 let refused = lock_std(&translator.servers).restore(&id, held);
1124 assert!(refused.is_none());
1125 assert_eq!(translator.registered_server_count(), 1);
1126 }
1127
1128 #[tokio::test]
1131 async fn settle_after_init_panic_after_partial_settlement_records_only_unsettled() {
1132 let configs = [
1133 named_config("a", "rust", Some(vec![ToolKind::Hover])),
1134 named_config("b", "python", None),
1135 named_config("c", "typescript", None),
1136 ];
1137 let translator = Translator::new().with_router(ToolRouter::from_configs(&configs).unwrap());
1138 translator.set_expected_servers(
1139 configs
1140 .iter()
1141 .map(crate::config::LspServerConfig::id)
1142 .collect(),
1143 );
1144 translator.settle_started(crate::lsp::fake_lsp_server_with_config(configs[0].clone()));
1145 translator.settle_failed(&ServerSpawnFailure {
1146 reason: StartupFailure::Spawn(std::sync::Arc::new(Error::ServerNotFound {
1147 command: crate::config::ServerCommand::from_static("b"),
1148 source: std::io::Error::from(std::io::ErrorKind::NotFound),
1149 })),
1150 ..spawn_failure(&configs[1])
1151 });
1152 let init_configs: Vec<_> = configs
1153 .iter()
1154 .cloned()
1155 .map(crate::test_lsp::init_config_for)
1156 .collect();
1157
1158 translator.settle_after_init_panic(&init_configs).await;
1159
1160 assert!(translator.startup_failure(&configs[0].id()).is_none());
1161 std::assert_matches!(
1162 translator.startup_failure(&configs[1].id()).unwrap().reason,
1163 StartupFailure::Spawn(_)
1164 );
1165 std::assert_matches!(
1166 translator.startup_failure(&configs[2].id()).unwrap().reason,
1167 StartupFailure::InitTaskPanicked
1168 );
1169 }
1170
1171 #[tokio::test]
1175 async fn test_settle_after_init_panic_turns_initializing_into_startup_failure() {
1176 let config = crate::config::LspServerConfig::rust_analyzer();
1177 let id = config.id();
1178 let translator = Translator::new()
1179 .with_extensions(crate::test_lsp::test_extensions())
1180 .with_router(ToolRouter::catch_all([(
1181 id.clone(),
1182 LanguageId::from_static("rust"),
1183 )]));
1184 translator.set_expected_servers(HashSet::from([id.clone()]));
1185 let path = PathBuf::from("/ws/main.rs");
1186 let before = translator
1187 .client_for_file(&path, ToolKind::Hover)
1188 .unwrap_err();
1189 assert_matches!(before, Error::ServerInitializing { .. }, "got {before:?}");
1190
1191 translator
1192 .settle_after_init_panic(&[crate::test_lsp::init_config_for(config)])
1193 .await;
1194
1195 let after = translator
1196 .client_for_file(&path, ToolKind::Hover)
1197 .unwrap_err();
1198 assert_matches!(
1199 &after,
1200 Error::ServerFailedToStart(f)
1201 if f.server_id == id && matches!(f.reason, StartupFailure::InitTaskPanicked),
1202 "got {after:?}"
1203 );
1204 }
1205
1206 #[tokio::test]
1208 async fn test_settle_after_init_panic_keeps_registered_server_routable() {
1209 let config = crate::config::LspServerConfig::rust_analyzer();
1210 let id = config.id();
1211 let translator = Translator::new()
1212 .with_extensions(crate::test_lsp::test_extensions())
1213 .with_router(ToolRouter::catch_all([(
1214 id.clone(),
1215 LanguageId::from_static("rust"),
1216 )]));
1217 translator.set_expected_servers(HashSet::from([id.clone()]));
1218 translator
1219 .register_server_complete(crate::lsp::fake_lsp_server_with_config(config.clone()));
1220
1221 translator
1222 .settle_after_init_panic(&[crate::test_lsp::init_config_for(config)])
1223 .await;
1224
1225 let (routed, _client) = translator
1226 .client_for_file(Path::new("/ws/main.rs"), ToolKind::Hover)
1227 .unwrap();
1228 assert_eq!(routed, id);
1229 assert!(translator.startup_failure(&id).is_none());
1230 }
1231
1232 #[test]
1233 fn test_record_startup_failures_orders_listing_by_server_id() {
1234 let translator = Translator::new();
1235 let failure = |id: &str| ServerSpawnFailure {
1236 server_id: ServerId::new(id).unwrap(),
1237 language_id: LanguageId::new(id).unwrap(),
1238 command: ServerCommand::new(id).unwrap(),
1239 reason: StartupFailure::InitTaskPanicked,
1240 };
1241 translator.record_startup_failures(&[failure("zls"), failure("clangd"), failure("gopls")]);
1242
1243 let ids: Vec<_> = translator
1244 .startup_failures()
1245 .into_iter()
1246 .map(|f| f.server_id.as_str().to_string())
1247 .collect();
1248 assert_eq!(ids, ["clangd", "gopls", "zls"]);
1249 }
1250
1251 #[tokio::test]
1253 async fn test_join_shutdown_tasks_survives_panicking_task() {
1254 let mut tasks = tokio::task::JoinSet::new();
1255 let handle = tasks.spawn(async { panic!("shutdown task boom") });
1256 let mut ids = HashMap::new();
1257 ids.insert(handle.id(), ServerId::from_static("rust"));
1258 tasks.spawn(async {});
1259
1260 join_shutdown_tasks(tasks, &ids).await;
1261 }
1262
1263 #[test]
1264 fn test_translator_new() {
1265 let translator = Translator::new();
1266 assert!(translator.workspace_roots.is_empty());
1267 assert_eq!(translator.registered_server_count(), 0);
1268 }
1269
1270 #[test]
1271 fn test_set_workspace_roots() {
1272 let mut translator = Translator::new();
1273 let roots = vec![PathBuf::from("/test/root1"), PathBuf::from("/test/root2")];
1274 translator.set_workspace_roots(WorkspaceRoots::for_test(roots.clone(), Vec::new()));
1275 assert_eq!(translator.workspace_roots.canonical(), roots);
1276 }
1277
1278 #[test]
1279 fn test_register_server() {
1280 let translator = Translator::new();
1281
1282 assert_eq!(translator.registered_server_count(), 0);
1284
1285 }
1294
1295 #[tokio::test]
1298 async fn test_shutdown_servers_empty_registry_returns_promptly() {
1299 let translator = Translator::new();
1300
1301 let result =
1302 tokio::time::timeout(Duration::from_secs(1), translator.shutdown_servers()).await;
1303
1304 assert!(
1305 result.is_ok(),
1306 "shutdown_servers must return promptly when no servers are registered"
1307 );
1308 }
1309
1310 #[tokio::test]
1318 async fn test_shutdown_servers_drains_registered_servers() {
1319 let translator = Translator::new();
1320 translator.register_server(
1321 ServerId::from_static("server-a"),
1322 crate::lsp::fake_lsp_server(),
1323 );
1324 translator.register_server(
1325 ServerId::from_static("server-b"),
1326 crate::lsp::fake_lsp_server(),
1327 );
1328 assert_eq!(translator.registered_server_count(), 2);
1329
1330 let result =
1334 tokio::time::timeout(Duration::from_secs(20), translator.shutdown_servers()).await;
1335
1336 assert!(
1337 result.is_ok(),
1338 "shutdown_servers must not hang against non-responsive mock servers"
1339 );
1340 assert_eq!(
1341 translator.registered_server_count(),
1342 0,
1343 "all registered servers must be drained"
1344 );
1345 }
1346
1347 #[test]
1348 fn test_clear_expected_servers_reverts_to_no_server_after_all_routes_dropped() {
1349 let path = PathBuf::from("/ws/Assets/Scripts/Player.cs");
1355 let lang = LanguageMap::default().detect(&path);
1356 let id = ServerId::from(lang.clone());
1357
1358 let translator = Translator::new().with_router(ToolRouter::catch_all([(
1359 id.clone(),
1360 LanguageId::new(lang).unwrap(),
1361 )]));
1362 let mut expected = HashSet::new();
1363 expected.insert(id);
1364 translator.set_expected_servers(expected);
1365
1366 translator.rebind_router(&HashSet::new());
1367 translator.clear_expected_servers();
1368
1369 let err = translator
1370 .client_for_file(&path, ToolKind::Hover)
1371 .unwrap_err();
1372 assert_matches!(err, Error::NoServerForLanguage { .. });
1373 }
1374
1375 #[test]
1376 fn test_translator_with_custom_extensions() {
1377 let mut extension_map = HashMap::new();
1378 extension_map.insert(
1379 FileExtension::from_static("nu"),
1380 LanguageId::from_static("nushell"),
1381 );
1382 extension_map.insert(
1383 FileExtension::from_static("customext"),
1384 LanguageId::from_static("customlang"),
1385 );
1386
1387 let translator = Translator::new().with_extensions(extension_map);
1388
1389 assert_eq!(translator.language_map.detect(Path::new("a.nu")), "nushell");
1390 assert_eq!(
1391 translator.language_map.detect(Path::new("a.customext")),
1392 "customlang"
1393 );
1394 assert_eq!(translator.language_map.languages().count(), 2);
1395 }
1396
1397 #[tokio::test]
1405 async fn test_with_resource_limits_applies_before_with_extensions() {
1406 let limits = ResourceLimits {
1407 max_documents: DocumentLimit::new(1),
1408 max_file_size: SizeLimit::UNLIMITED,
1409 };
1410 let translator = Translator::new()
1411 .with_resource_limits(limits)
1412 .with_extensions(LanguageMap::default());
1413
1414 let dir = TempDir::new().unwrap();
1415 let path_a = dir.path().join("a.rs");
1416 std::fs::write(&path_a, "a").unwrap();
1417 let path_b = dir.path().join("b.rs");
1418 std::fs::write(&path_b, "b").unwrap();
1419
1420 let (client, _server) = fake_lsp_client();
1421 let server_id = ServerId::from_static("rust");
1422
1423 translator
1424 .document_tracker
1425 .ensure_open(&path_a, &server_id, &client)
1426 .await
1427 .unwrap();
1428 translator
1429 .document_tracker
1430 .ensure_open(&path_b, &server_id, &client)
1431 .await
1432 .unwrap();
1433 assert_eq!(translator.document_tracker.len(), 1);
1434 assert!(!translator.document_tracker.is_open(&path_a));
1435 assert!(translator.document_tracker.is_open(&path_b));
1436 }
1437
1438 #[tokio::test]
1451 async fn test_with_resource_limits_applies_after_with_extensions() {
1452 let limits = ResourceLimits {
1453 max_documents: DocumentLimit::new(1),
1454 max_file_size: SizeLimit::UNLIMITED,
1455 };
1456 let translator = Translator::new()
1457 .with_extensions(HashMap::from([(
1458 FileExtension::from_static("rs"),
1459 LanguageId::from_static("rust"),
1460 )]))
1461 .with_resource_limits(limits);
1462
1463 let dir = TempDir::new().unwrap();
1464 let path_a = dir.path().join("a.rs");
1465 std::fs::write(&path_a, "a").unwrap();
1466 let path_b = dir.path().join("b.rs");
1467 std::fs::write(&path_b, "b").unwrap();
1468
1469 let (client, _server) = fake_lsp_client();
1470 let server_id = ServerId::from_static("rust");
1471
1472 translator
1473 .document_tracker
1474 .ensure_open(&path_a, &server_id, &client)
1475 .await
1476 .unwrap();
1477 translator
1478 .document_tracker
1479 .ensure_open(&path_b, &server_id, &client)
1480 .await
1481 .unwrap();
1482 assert_eq!(translator.document_tracker.len(), 1);
1483 assert!(!translator.document_tracker.is_open(&path_a));
1484
1485 let state = translator.document_tracker.close(&path_b).unwrap();
1486 assert_eq!(
1487 state.language_id(),
1488 "rust",
1489 "extension map must have survived with_resource_limits's rebuild"
1490 );
1491 }
1492
1493 fn refusal_of(config: &crate::config::LspServerConfig) -> ServerSpawnFailure {
1494 ServerSpawnFailure {
1495 reason: StartupFailure::RefusedUntrustedWorkspace(
1496 crate::error::UntrustedRefusal::NotAllowed {
1497 builtin: Some(crate::config::BuiltinServer::RustAnalyzer),
1498 },
1499 ),
1500 ..spawn_failure(config)
1501 }
1502 }
1503
1504 #[test]
1505 fn test_record_refusals_without_refusals_leaves_translator_untouched() {
1506 let config = crate::config::LspServerConfig::rust_analyzer();
1507 let translator =
1508 Translator::new().with_router(ToolRouter::from_configs([&config]).unwrap());
1509 let before = translator.router_snapshot();
1510
1511 translator.record_refusals(&[]);
1512
1513 assert!(translator.startup_failures().is_empty());
1514 assert!(Arc::ptr_eq(&before, &translator.router_snapshot()));
1515 }
1516
1517 #[test]
1518 fn test_refused_server_without_catch_all_reports_the_refusal() {
1519 let config = crate::config::LspServerConfig::rust_analyzer();
1520 let translator = Translator::new()
1521 .with_extensions(crate::test_lsp::test_extensions())
1522 .with_router(ToolRouter::from_configs([&config]).unwrap());
1523
1524 translator.record_refusals(&[refusal_of(&config)]);
1525
1526 let err = translator
1527 .client_for_file(Path::new("/ws/main.rs"), ToolKind::Hover)
1528 .unwrap_err();
1529 assert_matches!(&err, Error::ServerFailedToStart(f) if f.server_id == config.id());
1530 let text = err.to_string();
1531 assert!(text.contains("'rust'"), "{text}");
1532 assert!(text.contains("--allow-server rust"), "{text}");
1533 assert!(!text.contains("not retried"), "{text}");
1534 }
1535
1536 #[tokio::test]
1537 async fn test_refused_explicit_server_leaves_allowed_catch_all_serving() {
1538 let explicit = named_config("explicit", "rust", Some(vec![ToolKind::Hover]));
1539 let catch_all = named_config("allowed", "rust", None);
1540 let translator = Translator::new()
1541 .with_extensions(crate::test_lsp::test_extensions())
1542 .with_router(ToolRouter::from_configs([&explicit, &catch_all]).unwrap());
1543 translator.set_expected_servers(HashSet::from([catch_all.id()]));
1544
1545 translator.record_refusals(&[refusal_of(&explicit)]);
1546 translator.settle_started(crate::lsp::fake_lsp_server_with_config(catch_all.clone()));
1547
1548 let (routed, _client) = translator
1549 .client_for_file(Path::new("/ws/main.rs"), ToolKind::Hover)
1550 .unwrap();
1551 assert_eq!(routed, catch_all.id());
1552 }
1553}