1pub mod bridge;
37pub mod config;
38pub mod error;
39pub mod lsp;
40pub mod mcp;
41pub mod transport;
42
43use std::collections::{HashMap, HashSet};
44use std::path::PathBuf;
45use std::sync::Arc;
46
47use bridge::resources::make_uri;
48use bridge::{NotificationCache, ResourceSubscriptions, Translator};
49pub use config::{ProjectConfigTrust, ServerConfig};
50use config::{ServerId, ToolRouter};
51pub use error::Error;
52use lsp::{LspNotification, LspServer, ServerInitConfig};
53use rmcp::model::ResourceUpdatedNotificationParam;
54use tokio::sync::{Mutex, OnceCell};
55use tokio::task::JoinSet;
56use tracing::{error, info, warn};
57#[cfg(feature = "transport-http")]
58pub use transport::HttpConfig;
59pub use transport::Transport;
60#[cfg(feature = "transport-http")]
61use transport::run_http;
62use transport::run_stdio;
63
64pub(crate) async fn diagnostics_pump(
90 _server_id: String,
91 mut rx: tokio::sync::mpsc::Receiver<LspNotification>,
92 notification_cache: Arc<Mutex<NotificationCache>>,
93 subs: Arc<ResourceSubscriptions>,
94 peer_cell: Arc<OnceCell<rmcp::Peer<rmcp::RoleServer>>>,
95 mut cancel_rx: tokio::sync::watch::Receiver<bool>,
96 caches_diagnostics: bool,
97) {
98 loop {
99 tokio::select! {
100 result = cancel_rx.changed() => {
102 if result.is_err() || *cancel_rx.borrow() {
104 break;
105 }
106 }
107 msg = rx.recv() => {
108 let Some(notif) = msg else { break };
109 match notif {
110 LspNotification::PublishDiagnostics(p) => {
111 if !caches_diagnostics {
120 continue;
121 }
122 {
123 let mut cache = notification_cache.lock().await;
124 cache.store_diagnostics(&p.uri, p.version, p.diagnostics);
125 }
126
127 if subs.is_empty().await {
129 continue;
130 }
131
132 let Some(peer) = peer_cell.get() else { continue };
134 let Some(path) = bridge::uri_to_path(&p.uri) else { continue };
135 let Ok(mcp_uri) = make_uri(&path) else { continue };
136
137 if !subs.contains(&mcp_uri).await {
138 continue;
139 }
140
141 if peer
142 .notify_resource_updated(ResourceUpdatedNotificationParam::new(
143 mcp_uri,
144 ))
145 .await
146 .is_err()
147 {
148 break;
150 }
151 }
152 LspNotification::LogMessage(m) => {
153 let mut cache = notification_cache.lock().await;
154 cache.store_log(m.typ.into(), m.message);
155 }
156 LspNotification::ShowMessage(m) => {
157 let mut cache = notification_cache.lock().await;
158 cache.store_message(m.typ.into(), m.message);
159 }
160 LspNotification::Progress { .. } | LspNotification::Other { .. } => {}
161 }
162 }
163 }
164 }
165}
166
167pub(crate) struct RegisteredServers {
170 pub(crate) receivers: HashMap<ServerId, tokio::sync::mpsc::Receiver<lsp::LspNotification>>,
172 pub(crate) diagnostics_flags: HashMap<ServerId, bool>,
177}
178
179pub(crate) fn register_servers(
189 mut result: lsp::ServerInitResult,
190 translator: &bridge::Translator,
191) -> RegisteredServers {
192 let mut receivers = HashMap::new();
193 for (id, server) in &mut result.servers {
194 receivers.insert(id.clone(), server.take_notification_rx());
195 }
196
197 let registered: HashSet<ServerId> = result.servers.keys().cloned().collect();
198
199 let mut language_by_id = HashMap::new();
200 for (id, server) in result.servers {
201 let client = server.client().clone();
202 language_by_id.insert(id.clone(), client.language_id().to_string());
203 translator.register_client(id.clone(), client);
204 translator.register_server(id, server);
205 }
206
207 translator.rebind_router(®istered);
208
209 let diagnostics_flags = language_by_id
210 .into_iter()
211 .map(|(id, language)| {
212 let is_diagnostics_server = translator.is_diagnostics_route(&language, &id);
213 (id, is_diagnostics_server)
214 })
215 .collect();
216
217 RegisteredServers {
218 receivers,
219 diagnostics_flags,
220 }
221}
222
223fn resolve_workspace_roots(config_roots: &[PathBuf]) -> Vec<PathBuf> {
234 if config_roots.is_empty() {
235 match std::env::current_dir() {
236 Ok(cwd) => {
237 match cwd.canonicalize() {
239 Ok(canonical) => {
240 info!(
241 "Using current directory as workspace root: {}",
242 canonical.display()
243 );
244 vec![canonical]
245 }
246 Err(e) => {
247 warn!(
250 "Failed to canonicalize current directory: {e}, using non-canonical path"
251 );
252 vec![cwd]
253 }
254 }
255 }
256 Err(e) => {
257 warn!("Failed to get current directory: {e}, using fallback");
260 vec![PathBuf::from(".")]
261 }
262 }
263 } else {
264 config_roots.to_vec()
265 }
266}
267
268pub async fn serve(config: ServerConfig) -> Result<(), Error> {
286 serve_with(config, Transport::Stdio).await
287}
288
289pub async fn serve_with(config: ServerConfig, transport: Transport) -> Result<(), Error> {
329 info!("Starting MCPLS server...");
330
331 let workspace_roots = resolve_workspace_roots(&config.workspace.roots);
332 let extension_map = config.build_effective_extension_map();
333 let max_depth = Some(config.workspace.heuristics_max_depth);
334
335 let applicable_configs: Vec<ServerInitConfig> = config
336 .lsp_servers
337 .iter()
338 .filter_map(|lsp_config| {
339 let should_spawn = workspace_roots
340 .iter()
341 .any(|root| lsp_config.should_spawn(root, max_depth));
342
343 if !should_spawn {
344 info!(
345 "Skipping LSP server '{}' ({}): no project markers found",
346 lsp_config.language_id, lsp_config.command
347 );
348 return None;
349 }
350
351 Some(ServerInitConfig {
352 server_config: lsp_config.clone(),
353 workspace_roots: workspace_roots.clone(),
354 initialization_options: lsp_config.initialization_options.clone(),
355 notification_tx: None,
356 })
357 })
358 .collect();
359
360 info!(
361 "Attempting to spawn {} applicable LSP server(s)...",
362 applicable_configs.len()
363 );
364
365 let router = ToolRouter::from_configs(applicable_configs.iter().map(|c| &c.server_config))?;
370
371 let mut translator = Translator::new()
372 .with_extensions(extension_map)
373 .with_router(router);
374 translator.set_workspace_roots(workspace_roots.clone());
375
376 let expected_servers: HashSet<ServerId> = applicable_configs
380 .iter()
381 .map(|c| c.server_config.id())
382 .collect();
383 translator.set_expected_servers(expected_servers);
384
385 let workspace_roots_snapshot: Arc<[PathBuf]> = Arc::from(workspace_roots.clone());
395
396 let translator = Arc::new(translator);
397 let notification_cache = Arc::new(Mutex::new(NotificationCache::new()));
401 let subscriptions = Arc::new(ResourceSubscriptions::new());
402 let peer_cell = Arc::new(OnceCell::new());
404
405 let (cancel_tx, cancel_rx) = tokio::sync::watch::channel(false);
407
408 if applicable_configs.is_empty() {
409 warn!("No applicable LSP servers configured — starting in protocol-only mode");
410 } else {
411 info!(
412 "Spawning {} LSP server(s) in the background...",
413 applicable_configs.len()
414 );
415 spawn_lsp_servers_background(
416 applicable_configs,
417 Arc::clone(&translator),
418 Arc::clone(¬ification_cache),
419 Arc::clone(&subscriptions),
420 Arc::clone(&peer_cell),
421 cancel_rx.clone(),
422 );
423 }
424
425 info!("Starting MCP server with rmcp...");
426 let mcp_server = mcp::McplsServer::new(
427 Arc::clone(&translator),
428 Arc::clone(¬ification_cache),
429 Arc::clone(&workspace_roots_snapshot),
430 Arc::clone(&subscriptions),
431 );
432 info!("MCPLS server initialized successfully");
433
434 let result = match transport {
435 Transport::Stdio => {
436 info!("Listening for MCP requests on stdio...");
437 run_stdio(mcp_server, &peer_cell).await
438 }
439 #[cfg(feature = "transport-http")]
440 Transport::Http(cfg) => run_http(mcp_server, cfg).await,
441 };
442
443 let _ = cancel_tx.send(true);
445
446 info!("MCPLS server shutting down");
447 result
448}
449
450fn spawn_lsp_servers_background(
461 applicable_configs: Vec<ServerInitConfig>,
462 translator: Arc<Translator>,
463 notification_cache: Arc<Mutex<NotificationCache>>,
464 subscriptions: Arc<ResourceSubscriptions>,
465 peer_cell: Arc<OnceCell<rmcp::Peer<rmcp::RoleServer>>>,
466 cancel_rx: tokio::sync::watch::Receiver<bool>,
467) {
468 tokio::spawn(async move {
469 let result = LspServer::spawn_batch(&applicable_configs).await;
470
471 if result.all_failed() {
472 error!(
473 "All {} configured LSP server(s) failed to initialize",
474 result.failure_count()
475 );
476 for failure in &result.failures {
477 error!("Server initialization failed: {}", failure);
478 }
479 translator.rebind_router(&HashSet::new());
486 translator.clear_expected_servers();
487 return;
488 }
489
490 if result.partial_success() {
491 warn!(
492 "Partial server initialization: {} succeeded, {} failed",
493 result.server_count(),
494 result.failure_count()
495 );
496 for failure in &result.failures {
497 error!("Server initialization failed: {}", failure);
498 }
499 }
500
501 let server_count = result.server_count();
502 let registered = register_servers(result, &translator);
503 translator.clear_expected_servers();
508 info!("Proceeding with {} LSP server(s)", server_count);
509
510 let mut pumps: JoinSet<()> = JoinSet::new();
512 for (id, rx) in registered.receivers {
513 let caches_diagnostics = registered
514 .diagnostics_flags
515 .get(&id)
516 .copied()
517 .unwrap_or(false);
518 pumps.spawn(diagnostics_pump(
519 id.to_string(),
520 rx,
521 Arc::clone(¬ification_cache),
522 Arc::clone(&subscriptions),
523 Arc::clone(&peer_cell),
524 cancel_rx.clone(),
525 caches_diagnostics,
526 ));
527 }
528 while pumps.join_next().await.is_some() {}
529 });
530}
531
532#[cfg(test)]
533#[allow(clippy::unwrap_used)]
534mod tests {
535 use super::*;
536
537 #[test]
538 fn test_resolve_workspace_roots_empty_config() {
539 let roots = resolve_workspace_roots(&[]);
540 assert_eq!(roots.len(), 1);
541 assert!(
542 roots[0].is_absolute(),
543 "Workspace root should be absolute path"
544 );
545 }
546
547 #[test]
548 fn test_resolve_workspace_roots_with_config() {
549 let config_roots = vec![PathBuf::from("/test/root")];
550 let roots = resolve_workspace_roots(&config_roots);
551 assert_eq!(roots, config_roots);
552 }
553
554 #[test]
555 fn test_resolve_workspace_roots_multiple_paths() {
556 let config_roots = vec![PathBuf::from("/test/root1"), PathBuf::from("/test/root2")];
557 let roots = resolve_workspace_roots(&config_roots);
558 assert_eq!(roots, config_roots);
559 assert_eq!(roots.len(), 2);
560 }
561
562 #[test]
563 fn test_resolve_workspace_roots_preserves_order() {
564 let config_roots = vec![
565 PathBuf::from("/workspace/alpha"),
566 PathBuf::from("/workspace/beta"),
567 PathBuf::from("/workspace/gamma"),
568 ];
569 let roots = resolve_workspace_roots(&config_roots);
570 assert_eq!(roots[0], PathBuf::from("/workspace/alpha"));
571 assert_eq!(roots[1], PathBuf::from("/workspace/beta"));
572 assert_eq!(roots[2], PathBuf::from("/workspace/gamma"));
573 }
574
575 #[test]
576 fn test_resolve_workspace_roots_single_path() {
577 let config_roots = vec![PathBuf::from("/single/workspace")];
578 let roots = resolve_workspace_roots(&config_roots);
579 assert_eq!(roots.len(), 1);
580 assert_eq!(roots[0], PathBuf::from("/single/workspace"));
581 }
582
583 #[test]
584 fn test_resolve_workspace_roots_empty_returns_cwd() {
585 let roots = resolve_workspace_roots(&[]);
586 assert!(
587 !roots.is_empty(),
588 "Should return at least one workspace root"
589 );
590 }
591
592 #[test]
593 fn test_resolve_workspace_roots_relative_paths() {
594 let config_roots = vec![
595 PathBuf::from("relative/path1"),
596 PathBuf::from("relative/path2"),
597 ];
598 let roots = resolve_workspace_roots(&config_roots);
599 assert_eq!(roots.len(), 2);
600 assert_eq!(roots[0], PathBuf::from("relative/path1"));
601 assert_eq!(roots[1], PathBuf::from("relative/path2"));
602 }
603
604 #[test]
605 fn test_resolve_workspace_roots_mixed_paths() {
606 let config_roots = vec![
607 PathBuf::from("/absolute/path"),
608 PathBuf::from("relative/path"),
609 ];
610 let roots = resolve_workspace_roots(&config_roots);
611 assert_eq!(roots.len(), 2);
612 assert_eq!(roots[0], PathBuf::from("/absolute/path"));
613 assert_eq!(roots[1], PathBuf::from("relative/path"));
614 }
615
616 #[test]
617 fn test_resolve_workspace_roots_with_dot_path() {
618 let config_roots = vec![PathBuf::from(".")];
619 let roots = resolve_workspace_roots(&config_roots);
620 assert_eq!(roots, config_roots);
621 }
622
623 #[test]
624 fn test_resolve_workspace_roots_with_parent_path() {
625 let config_roots = vec![PathBuf::from("..")];
626 let roots = resolve_workspace_roots(&config_roots);
627 assert_eq!(roots.len(), 1);
628 assert_eq!(roots[0], PathBuf::from(".."));
629 }
630
631 #[test]
632 fn test_resolve_workspace_roots_unicode_paths() {
633 let config_roots = vec![
634 PathBuf::from("/workspace/テスト"),
635 PathBuf::from("/workspace/тест"),
636 ];
637 let roots = resolve_workspace_roots(&config_roots);
638 assert_eq!(roots.len(), 2);
639 assert_eq!(roots[0], PathBuf::from("/workspace/テスト"));
640 assert_eq!(roots[1], PathBuf::from("/workspace/тест"));
641 }
642
643 #[test]
644 fn test_resolve_workspace_roots_spaces_in_paths() {
645 let config_roots = vec![
646 PathBuf::from("/workspace/path with spaces"),
647 PathBuf::from("/another path/workspace"),
648 ];
649 let roots = resolve_workspace_roots(&config_roots);
650 assert_eq!(roots.len(), 2);
651 assert_eq!(roots[0], PathBuf::from("/workspace/path with spaces"));
652 }
653
654 mod graceful_degradation_tests {
656 use super::*;
657 use crate::error::ServerSpawnFailure;
658 use crate::lsp::ServerInitResult;
659
660 #[test]
661 fn test_all_servers_failed_error_handling() {
662 let mut result = ServerInitResult::new();
663 result.add_failure(ServerSpawnFailure {
664 server_id: ServerId::from("rust"),
665 language_id: "rust".to_string(),
666 command: "rust-analyzer".to_string(),
667 message: "not found".to_string(),
668 });
669 result.add_failure(ServerSpawnFailure {
670 server_id: ServerId::from("python"),
671 language_id: "python".to_string(),
672 command: "pyright".to_string(),
673 message: "not found".to_string(),
674 });
675
676 assert!(result.all_failed());
677 assert_eq!(result.failure_count(), 2);
678 assert_eq!(result.server_count(), 0);
679 }
680
681 #[test]
682 fn test_partial_success_detection() {
683 use std::collections::HashMap;
684
685 let mut result = ServerInitResult::new();
686 result.servers = HashMap::new(); result.add_failure(ServerSpawnFailure {
689 server_id: ServerId::from("python"),
690 language_id: "python".to_string(),
691 command: "pyright".to_string(),
692 message: "not found".to_string(),
693 });
694
695 assert_eq!(result.failure_count(), 1);
697 assert_eq!(result.server_count(), 0);
698 }
699
700 #[test]
701 fn test_all_servers_succeeded_detection() {
702 use std::collections::HashMap;
703
704 let mut result = ServerInitResult::new();
705 result.servers = HashMap::new(); assert_eq!(result.failure_count(), 0);
708 assert!(!result.all_failed());
709 assert!(!result.partial_success());
710 }
711
712 #[test]
713 fn test_all_servers_failed_to_init_error() {
714 let failures = vec![
715 ServerSpawnFailure {
716 server_id: ServerId::from("rust"),
717 language_id: "rust".to_string(),
718 command: "rust-analyzer".to_string(),
719 message: "command not found".to_string(),
720 },
721 ServerSpawnFailure {
722 server_id: ServerId::from("python"),
723 language_id: "python".to_string(),
724 command: "pyright".to_string(),
725 message: "permission denied".to_string(),
726 },
727 ];
728
729 let err = Error::AllServersFailedToInit { count: 2, failures };
730
731 assert!(err.to_string().contains("all LSP servers failed"));
732 assert!(err.to_string().contains("2 configured"));
733
734 if let Error::AllServersFailedToInit { count, failures: f } = err {
736 assert_eq!(count, 2);
737 assert_eq!(f.len(), 2);
738 assert_eq!(f[0].language_id, "rust");
739 assert_eq!(f[1].language_id, "python");
740 } else {
741 panic!("Expected AllServersFailedToInit error");
742 }
743 }
744
745 #[test]
746 fn test_graceful_degradation_with_empty_config() {
747 let result = ServerInitResult::new();
748
749 assert!(!result.all_failed());
751 assert!(!result.partial_success());
752 assert!(!result.has_servers());
753 assert_eq!(result.server_count(), 0);
754 assert_eq!(result.failure_count(), 0);
755 }
756
757 #[test]
758 fn test_server_spawn_failure_display() {
759 let failure = ServerSpawnFailure {
760 server_id: ServerId::from("typescript"),
761 language_id: "typescript".to_string(),
762 command: "tsserver".to_string(),
763 message: "executable not found in PATH".to_string(),
764 };
765
766 let display = failure.to_string();
767 assert!(display.contains("typescript"));
768 assert!(display.contains("tsserver"));
769 assert!(display.contains("executable not found"));
770 }
771
772 #[test]
773 fn test_result_helpers_consistency() {
774 let mut result = ServerInitResult::new();
775
776 assert!(!result.has_servers());
778 assert!(!result.all_failed());
779 assert!(!result.partial_success());
780
781 result.add_failure(ServerSpawnFailure {
783 server_id: ServerId::from("go"),
784 language_id: "go".to_string(),
785 command: "gopls".to_string(),
786 message: "error".to_string(),
787 });
788
789 assert!(result.all_failed());
790 assert!(!result.has_servers());
791 assert!(!result.partial_success());
792 }
793
794 #[tokio::test]
795 async fn test_serve_degrades_when_all_servers_fail_to_spawn() {
796 use crate::config::{LspServerConfig, WorkspaceConfig};
797
798 let config = ServerConfig {
808 workspace: WorkspaceConfig {
809 roots: vec![PathBuf::from("/tmp/test-workspace")],
810 position_encodings: vec!["utf-8".to_string(), "utf-16".to_string()],
811 language_extensions: vec![],
812 heuristics_max_depth: 10,
813 },
814 lsp_servers: vec![LspServerConfig {
815 language_id: "rust".to_string(),
816 command: "nonexistent-command-that-will-fail-12345".to_string(),
817 args: vec![],
818 env: std::collections::HashMap::new(),
819 file_patterns: vec!["**/*.rs".to_string()],
820 initialization_options: None,
821 timeout_seconds: 10,
822 heuristics: None,
823 name: None,
824 handles: None,
825 }],
826 };
827
828 let outcome =
833 tokio::time::timeout(std::time::Duration::from_secs(2), serve(config)).await;
834
835 match outcome {
836 Err(_elapsed) => {}
838 Ok(Ok(())) => {}
840 Ok(Err(err)) => assert!(
842 !matches!(err, Error::NoServersAvailable(_))
843 && !matches!(err, Error::AllServersFailedToInit { .. }),
844 "serve() must not fail fast now that LSP init is backgrounded; got: {err:?}"
845 ),
846 }
847 }
848
849 #[tokio::test]
850 async fn test_serve_starts_with_empty_config() {
851 use crate::config::WorkspaceConfig;
852
853 let config = ServerConfig {
857 workspace: WorkspaceConfig {
858 roots: vec![PathBuf::from("/tmp/test-workspace")],
859 position_encodings: vec!["utf-8".to_string(), "utf-16".to_string()],
860 language_extensions: vec![],
861 heuristics_max_depth: 10,
862 },
863 lsp_servers: vec![],
864 };
865
866 let result = serve(config).await;
867
868 if let Err(ref err) = result {
871 assert!(
872 !matches!(err, Error::NoServersAvailable(_)),
873 "serve() must not return NoServersAvailable for empty lsp_servers config"
874 );
875 }
876 }
877 }
878
879 #[allow(clippy::unwrap_used, clippy::expect_used)]
884 mod pump_tests {
885 use lsp_types::{PublishDiagnosticsParams, Uri};
886 use tokio::sync::{mpsc, watch};
887
888 use super::*;
889
890 fn make_cache() -> Arc<Mutex<NotificationCache>> {
891 Arc::new(Mutex::new(NotificationCache::new()))
892 }
893
894 fn make_subs() -> Arc<ResourceSubscriptions> {
895 Arc::new(ResourceSubscriptions::new())
896 }
897
898 type PeerCell = Arc<OnceCell<rmcp::Peer<rmcp::RoleServer>>>;
899
900 fn make_peer_cell() -> PeerCell {
901 Arc::new(OnceCell::new())
902 }
903
904 #[tokio::test]
906 async fn test_pump_caches_before_peer_set() {
907 let cache = make_cache();
908 let subs = make_subs();
909 let peer_cell = make_peer_cell();
910 let (tx, rx) = mpsc::channel(8);
911 let (_cancel_tx, cancel_rx) = watch::channel(false);
914
915 let c = Arc::clone(&cache);
916 tokio::spawn(diagnostics_pump(
917 "rust".to_string(),
918 rx,
919 c,
920 Arc::clone(&subs),
921 Arc::clone(&peer_cell),
922 cancel_rx,
923 true,
924 ));
925
926 let uri: Uri = "file:///test/main.rs".parse().unwrap();
927 tx.send(LspNotification::PublishDiagnostics(
928 PublishDiagnosticsParams {
929 uri: uri.clone(),
930 diagnostics: vec![],
931 version: None,
932 },
933 ))
934 .await
935 .unwrap();
936 drop(tx);
937
938 let cached = tokio::time::timeout(std::time::Duration::from_secs(5), async {
940 loop {
941 tokio::task::yield_now().await;
942 let found = {
943 let guard = cache.lock().await;
944 guard.get_diagnostics(uri.as_str()).is_some()
945 };
946 if found {
947 return true;
948 }
949 tokio::time::sleep(std::time::Duration::from_millis(5)).await;
950 }
951 })
952 .await
953 .expect("pump did not cache diagnostics within 5 s");
954 assert!(cached, "diagnostics should be cached before peer is set");
955 }
956
957 #[tokio::test]
959 async fn test_pump_exits_on_cancel() {
960 let cache = make_cache();
961 let subs = make_subs();
962 let peer_cell = make_peer_cell();
963 let (_tx, rx) = mpsc::channel::<LspNotification>(8);
964 let (cancel_tx, cancel_rx) = watch::channel(false);
965
966 let handle = tokio::spawn(diagnostics_pump(
967 "rust".to_string(),
968 rx,
969 cache,
970 subs,
971 peer_cell,
972 cancel_rx,
973 true,
974 ));
975
976 cancel_tx.send(true).unwrap();
977 tokio::time::timeout(std::time::Duration::from_millis(200), handle)
979 .await
980 .expect("pump did not exit within timeout")
981 .unwrap();
982 }
983
984 #[tokio::test]
986 async fn test_pump_exits_when_cancel_sender_dropped() {
987 let cache = make_cache();
988 let subs = make_subs();
989 let peer_cell = make_peer_cell();
990 let (_tx, rx) = mpsc::channel::<LspNotification>(8);
991 let (cancel_tx, cancel_rx) = watch::channel(false);
992
993 let handle = tokio::spawn(diagnostics_pump(
994 "rust".to_string(),
995 rx,
996 cache,
997 subs,
998 peer_cell,
999 cancel_rx,
1000 true,
1001 ));
1002
1003 drop(cancel_tx); tokio::time::timeout(std::time::Duration::from_millis(200), handle)
1005 .await
1006 .expect("pump did not exit within timeout")
1007 .unwrap();
1008 }
1009
1010 #[tokio::test]
1016 async fn test_pump_makes_progress_while_translator_lock_held() {
1017 let translator = Arc::new(Mutex::new(Translator::new()));
1018 let cache = make_cache();
1019 let subs = make_subs();
1020 let peer_cell = make_peer_cell();
1021 let (tx, rx) = mpsc::channel(8);
1022 let (_cancel_tx, cancel_rx) = watch::channel(false);
1023
1024 let lock_acquired = Arc::new(tokio::sync::Notify::new());
1027 let notify = Arc::clone(&lock_acquired);
1028 let holder = tokio::spawn(async move {
1029 let _guard = translator.lock().await;
1030 notify.notify_one();
1031 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
1032 });
1033 lock_acquired.notified().await;
1034
1035 tokio::spawn(diagnostics_pump(
1036 "rust".to_string(),
1037 rx,
1038 Arc::clone(&cache),
1039 subs,
1040 peer_cell,
1041 cancel_rx,
1042 true,
1043 ));
1044
1045 let uri: Uri = "file:///test/locked.rs".parse().unwrap();
1046 tx.send(LspNotification::PublishDiagnostics(
1047 PublishDiagnosticsParams {
1048 uri: uri.clone(),
1049 diagnostics: vec![],
1050 version: None,
1051 },
1052 ))
1053 .await
1054 .unwrap();
1055 drop(tx);
1056
1057 tokio::time::timeout(std::time::Duration::from_millis(500), async {
1060 loop {
1061 {
1062 let guard = cache.lock().await;
1063 if guard.get_diagnostics(uri.as_str()).is_some() {
1064 return;
1065 }
1066 }
1067 tokio::time::sleep(std::time::Duration::from_millis(5)).await;
1068 }
1069 })
1070 .await
1071 .expect("pump stalled behind translator lock");
1072
1073 holder.await.unwrap();
1074 }
1075 }
1076}