1use std::collections::HashMap;
11use std::path::PathBuf;
12use std::process::Stdio;
13use std::str::FromStr;
14
15use lsp_types::{
16 ClientCapabilities, ClientInfo, GeneralClientCapabilities, InitializeParams, InitializeResult,
17 InitializedParams, PositionEncodingKind, ServerCapabilities, Uri, WorkspaceFolder,
18};
19use tokio::process::Command;
20use tokio::time::Duration;
21use tracing::{debug, info};
22
23use crate::config::LspServerConfig;
24use crate::error::{Error, Result, ServerSpawnFailure};
25use crate::lsp::client::LspClient;
26use crate::lsp::transport::LspTransport;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum ServerState {
31 Uninitialized,
33 Initializing,
35 Ready,
37 ShuttingDown,
39 Shutdown,
41}
42
43impl ServerState {
44 #[must_use]
46 pub const fn is_ready(&self) -> bool {
47 matches!(self, Self::Ready)
48 }
49
50 #[must_use]
52 pub const fn can_accept_requests(&self) -> bool {
53 matches!(self, Self::Ready)
54 }
55}
56
57#[derive(Debug, Clone)]
59pub struct ServerInitConfig {
60 pub server_config: LspServerConfig,
62 pub workspace_roots: Vec<PathBuf>,
64 pub initialization_options: Option<serde_json::Value>,
66}
67
68#[derive(Debug)]
92pub struct ServerInitResult {
93 pub servers: HashMap<String, LspServer>,
95 pub failures: Vec<ServerSpawnFailure>,
97}
98
99impl ServerInitResult {
100 #[must_use]
102 pub fn new() -> Self {
103 Self {
104 servers: HashMap::new(),
105 failures: Vec::new(),
106 }
107 }
108
109 #[must_use]
113 pub fn has_servers(&self) -> bool {
114 !self.servers.is_empty()
115 }
116
117 #[must_use]
122 pub fn all_failed(&self) -> bool {
123 self.servers.is_empty() && !self.failures.is_empty()
124 }
125
126 #[must_use]
130 pub fn partial_success(&self) -> bool {
131 !self.servers.is_empty() && !self.failures.is_empty()
132 }
133
134 #[must_use]
136 pub fn server_count(&self) -> usize {
137 self.servers.len()
138 }
139
140 #[must_use]
142 pub fn failure_count(&self) -> usize {
143 self.failures.len()
144 }
145
146 pub fn add_server(&mut self, language_id: String, server: LspServer) {
150 self.servers.insert(language_id, server);
151 }
152
153 pub fn add_failure(&mut self, failure: ServerSpawnFailure) {
155 self.failures.push(failure);
156 }
157}
158
159impl Default for ServerInitResult {
160 fn default() -> Self {
161 Self::new()
162 }
163}
164
165pub struct LspServer {
167 client: LspClient,
168 capabilities: ServerCapabilities,
169 position_encoding: PositionEncodingKind,
170 _child: tokio::process::Child,
173}
174
175impl std::fmt::Debug for LspServer {
176 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177 f.debug_struct("LspServer")
178 .field("client", &self.client)
179 .field("capabilities", &self.capabilities)
180 .field("position_encoding", &self.position_encoding)
181 .field("_child", &"<process>")
182 .finish()
183 }
184}
185
186impl LspServer {
187 pub async fn spawn(config: ServerInitConfig) -> Result<Self> {
202 info!(
203 "Spawning LSP server: {} {:?}",
204 config.server_config.command, config.server_config.args
205 );
206
207 let mut child = Command::new(&config.server_config.command)
208 .args(&config.server_config.args)
209 .stdin(Stdio::piped())
210 .stdout(Stdio::piped())
211 .stderr(Stdio::null())
212 .kill_on_drop(true)
213 .spawn()
214 .map_err(|e| Error::ServerSpawnFailed {
215 command: config.server_config.command.clone(),
216 source: e,
217 })?;
218
219 let stdin = child
220 .stdin
221 .take()
222 .ok_or_else(|| Error::Transport("Failed to capture stdin".to_string()))?;
223 let stdout = child
224 .stdout
225 .take()
226 .ok_or_else(|| Error::Transport("Failed to capture stdout".to_string()))?;
227
228 let transport = LspTransport::new(stdin, stdout);
229 let client = LspClient::from_transport(config.server_config.clone(), transport);
230
231 let (capabilities, position_encoding) = Self::initialize(&client, &config).await?;
232
233 info!("LSP server initialized successfully");
234
235 Ok(Self {
236 client,
237 capabilities,
238 position_encoding,
239 _child: child,
240 })
241 }
242
243 async fn initialize(
247 client: &LspClient,
248 config: &ServerInitConfig,
249 ) -> Result<(ServerCapabilities, PositionEncodingKind)> {
250 debug!("Sending initialize request");
251
252 let workspace_folders: Vec<WorkspaceFolder> = config
253 .workspace_roots
254 .iter()
255 .map(|root| {
256 let path_str = root.to_str().ok_or_else(|| {
257 let root_display = root.display();
258 Error::InvalidUri(format!("Invalid UTF-8 in path: {root_display}"))
259 })?;
260 let uri_str = if cfg!(windows) {
261 format!("file:///{}", path_str.replace('\\', "/"))
262 } else {
263 format!("file://{path_str}")
264 };
265 let uri = Uri::from_str(&uri_str).map_err(|_| {
266 let root_display = root.display();
267 Error::InvalidUri(format!("Invalid workspace root: {root_display}"))
268 })?;
269 Ok(WorkspaceFolder {
270 uri,
271 name: root
272 .file_name()
273 .and_then(|n| n.to_str())
274 .unwrap_or("workspace")
275 .to_string(),
276 })
277 })
278 .collect::<Result<Vec<_>>>()?;
279
280 let params = InitializeParams {
281 process_id: Some(std::process::id()),
282 #[allow(deprecated)]
283 root_uri: None,
284 initialization_options: config.initialization_options.clone(),
285 capabilities: ClientCapabilities {
286 general: Some(GeneralClientCapabilities {
287 position_encodings: Some(vec![
288 PositionEncodingKind::UTF8,
289 PositionEncodingKind::UTF16,
290 ]),
291 ..Default::default()
292 }),
293 text_document: Some(lsp_types::TextDocumentClientCapabilities {
294 hover: Some(lsp_types::HoverClientCapabilities {
295 dynamic_registration: Some(false),
296 content_format: Some(vec![
297 lsp_types::MarkupKind::Markdown,
298 lsp_types::MarkupKind::PlainText,
299 ]),
300 }),
301 definition: Some(lsp_types::GotoCapability {
302 dynamic_registration: Some(false),
303 link_support: Some(true),
304 }),
305 references: Some(lsp_types::ReferenceClientCapabilities {
306 dynamic_registration: Some(false),
307 }),
308 ..Default::default()
309 }),
310 workspace: Some(lsp_types::WorkspaceClientCapabilities {
311 workspace_folders: Some(true),
312 ..Default::default()
313 }),
314 ..Default::default()
315 },
316 client_info: Some(ClientInfo {
317 name: "mcpls".to_string(),
318 version: Some(env!("CARGO_PKG_VERSION").to_string()),
319 }),
320 workspace_folders: Some(workspace_folders),
321 ..Default::default()
322 };
323
324 let result: InitializeResult = client
325 .request("initialize", params, Duration::from_secs(30))
326 .await
327 .map_err(|e| Error::LspInitFailed {
328 message: format!("Initialize request failed: {e}"),
329 })?;
330
331 let position_encoding = result
332 .capabilities
333 .position_encoding
334 .clone()
335 .unwrap_or(PositionEncodingKind::UTF16);
336
337 debug!(
338 "Server capabilities received, encoding: {:?}",
339 position_encoding
340 );
341
342 client
343 .notify("initialized", InitializedParams {})
344 .await
345 .map_err(|e| Error::LspInitFailed {
346 message: format!("Initialized notification failed: {e}"),
347 })?;
348
349 Ok((result.capabilities, position_encoding))
350 }
351
352 #[must_use]
354 pub const fn capabilities(&self) -> &ServerCapabilities {
355 &self.capabilities
356 }
357
358 #[must_use]
360 pub fn position_encoding(&self) -> PositionEncodingKind {
361 self.position_encoding.clone()
362 }
363
364 #[must_use]
366 pub const fn client(&self) -> &LspClient {
367 &self.client
368 }
369
370 pub async fn shutdown(self) -> Result<()> {
378 debug!("Shutting down LSP server");
379
380 let _: serde_json::Value = self
381 .client
382 .request("shutdown", serde_json::Value::Null, Duration::from_secs(5))
383 .await?;
384
385 self.client.notify("exit", serde_json::Value::Null).await?;
386
387 self.client.shutdown().await?;
388
389 info!("LSP server shut down successfully");
390 Ok(())
391 }
392
393 pub async fn spawn_batch(configs: &[ServerInitConfig]) -> ServerInitResult {
440 let mut result = ServerInitResult::new();
441
442 for config in configs {
443 let language_id = config.server_config.language_id.clone();
444 let command = config.server_config.command.clone();
445
446 match Self::spawn(config.clone()).await {
447 Ok(server) => {
448 info!(
449 "Successfully spawned LSP server: {} ({})",
450 language_id, command
451 );
452 result.add_server(language_id, server);
453 }
454 Err(e) => {
455 tracing::error!(
456 "Failed to spawn LSP server: {} ({}): {}",
457 language_id,
458 command,
459 e
460 );
461 result.add_failure(ServerSpawnFailure {
462 language_id,
463 command,
464 message: e.to_string(),
465 });
466 }
467 }
468 }
469
470 result
471 }
472}
473
474#[cfg(test)]
475#[allow(clippy::unwrap_used)]
476mod tests {
477 use super::*;
478
479 #[test]
480 fn test_server_state_ready() {
481 assert!(ServerState::Ready.is_ready());
482 assert!(ServerState::Ready.can_accept_requests());
483 }
484
485 #[test]
486 fn test_server_state_uninitialized() {
487 assert!(!ServerState::Uninitialized.is_ready());
488 assert!(!ServerState::Uninitialized.can_accept_requests());
489 }
490
491 #[test]
492 fn test_server_state_initializing() {
493 assert!(!ServerState::Initializing.is_ready());
494 assert!(!ServerState::Initializing.can_accept_requests());
495 }
496
497 #[test]
498 fn test_server_state_shutting_down() {
499 assert!(!ServerState::ShuttingDown.is_ready());
500 assert!(!ServerState::ShuttingDown.can_accept_requests());
501 }
502
503 #[test]
504 fn test_server_state_shutdown() {
505 assert!(!ServerState::Shutdown.is_ready());
506 assert!(!ServerState::Shutdown.can_accept_requests());
507 }
508
509 #[test]
510 fn test_server_state_equality() {
511 assert_eq!(ServerState::Ready, ServerState::Ready);
512 assert_ne!(ServerState::Ready, ServerState::Uninitialized);
513 assert_eq!(ServerState::Shutdown, ServerState::Shutdown);
514 }
515
516 #[test]
517 fn test_server_state_clone() {
518 let state = ServerState::Ready;
519 let cloned = state;
520 assert_eq!(state, cloned);
521 }
522
523 #[test]
524 fn test_server_state_debug() {
525 let state = ServerState::Ready;
526 let debug_str = format!("{state:?}");
527 assert!(debug_str.contains("Ready"));
528 }
529
530 #[test]
531 fn test_server_init_config_clone() {
532 let config = ServerInitConfig {
533 server_config: LspServerConfig::rust_analyzer(),
534 workspace_roots: vec![PathBuf::from("/tmp/workspace")],
535 initialization_options: Some(serde_json::json!({"key": "value"})),
536 };
537
538 #[allow(clippy::redundant_clone)]
539 let cloned = config.clone();
540 assert_eq!(cloned.server_config.language_id, "rust");
541 assert_eq!(cloned.workspace_roots.len(), 1);
542 }
543
544 #[test]
545 fn test_server_init_config_debug() {
546 let config = ServerInitConfig {
547 server_config: LspServerConfig::pyright(),
548 workspace_roots: vec![],
549 initialization_options: None,
550 };
551
552 let debug_str = format!("{config:?}");
553 assert!(debug_str.contains("python"));
554 assert!(debug_str.contains("pyright"));
555 }
556
557 #[test]
558 fn test_server_init_config_with_options() {
559 use std::collections::HashMap;
560
561 let init_opts = serde_json::json!({
562 "settings": {
563 "python": {
564 "analysis": {
565 "typeCheckingMode": "strict"
566 }
567 }
568 }
569 });
570
571 let mut env = HashMap::new();
572 env.insert("PYTHONPATH".to_string(), "/usr/lib".to_string());
573
574 let config = ServerInitConfig {
575 server_config: LspServerConfig {
576 language_id: "python".to_string(),
577 command: "pyright-langserver".to_string(),
578 args: vec!["--stdio".to_string()],
579 env,
580 file_patterns: vec!["**/*.py".to_string()],
581 initialization_options: Some(init_opts.clone()),
582 timeout_seconds: 10,
583 },
584 workspace_roots: vec![PathBuf::from("/workspace")],
585 initialization_options: Some(init_opts),
586 };
587
588 assert!(config.initialization_options.is_some());
589 assert_eq!(config.workspace_roots.len(), 1);
590 }
591
592 #[test]
593 fn test_server_init_config_empty_workspace() {
594 let config = ServerInitConfig {
595 server_config: LspServerConfig::typescript(),
596 workspace_roots: vec![],
597 initialization_options: None,
598 };
599
600 assert!(config.workspace_roots.is_empty());
601 }
602
603 #[test]
604 fn test_server_init_config_multiple_workspaces() {
605 let config = ServerInitConfig {
606 server_config: LspServerConfig::rust_analyzer(),
607 workspace_roots: vec![
608 PathBuf::from("/workspace1"),
609 PathBuf::from("/workspace2"),
610 PathBuf::from("/workspace3"),
611 ],
612 initialization_options: None,
613 };
614
615 assert_eq!(config.workspace_roots.len(), 3);
616 }
617
618 #[tokio::test]
619 async fn test_lsp_server_getters() {
620 use lsp_types::ServerCapabilities;
621
622 let mock_child = tokio::process::Command::new("echo")
623 .stdin(Stdio::piped())
624 .stdout(Stdio::piped())
625 .kill_on_drop(true)
626 .spawn()
627 .unwrap();
628
629 let mock_stdin = tokio::process::Command::new("cat")
630 .stdin(Stdio::piped())
631 .spawn()
632 .unwrap()
633 .stdin
634 .take()
635 .unwrap();
636
637 let mock_stdout = tokio::process::Command::new("echo")
638 .stdout(Stdio::piped())
639 .spawn()
640 .unwrap()
641 .stdout
642 .take()
643 .unwrap();
644
645 let transport = LspTransport::new(mock_stdin, mock_stdout);
646 let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
647
648 let server = LspServer {
649 client,
650 capabilities: ServerCapabilities::default(),
651 position_encoding: PositionEncodingKind::UTF8,
652 _child: mock_child,
653 };
654
655 assert_eq!(server.position_encoding(), PositionEncodingKind::UTF8);
656 assert!(server.capabilities().text_document_sync.is_none());
657
658 let debug_str = format!("{server:?}");
659 assert!(debug_str.contains("LspServer"));
660 assert!(debug_str.contains("<process>"));
661 }
662
663 #[test]
664 fn test_server_init_result_new_empty() {
665 let result = ServerInitResult::new();
666 assert!(!result.has_servers());
667 assert!(!result.all_failed());
668 assert!(!result.partial_success());
669 assert_eq!(result.server_count(), 0);
670 assert_eq!(result.failure_count(), 0);
671 }
672
673 #[test]
674 fn test_server_init_result_default() {
675 let result = ServerInitResult::default();
676 assert!(!result.has_servers());
677 assert_eq!(result.server_count(), 0);
678 assert_eq!(result.failure_count(), 0);
679 }
680
681 #[test]
682 fn test_server_init_result_all_failures() {
683 let mut result = ServerInitResult::new();
684
685 result.add_failure(ServerSpawnFailure {
686 language_id: "rust".to_string(),
687 command: "rust-analyzer".to_string(),
688 message: "not found".to_string(),
689 });
690
691 result.add_failure(ServerSpawnFailure {
692 language_id: "python".to_string(),
693 command: "pyright".to_string(),
694 message: "permission denied".to_string(),
695 });
696
697 assert!(!result.has_servers());
698 assert!(result.all_failed());
699 assert!(!result.partial_success());
700 assert_eq!(result.server_count(), 0);
701 assert_eq!(result.failure_count(), 2);
702 }
703
704 #[tokio::test]
705 async fn test_server_init_result_all_success() {
706 let mut result = ServerInitResult::new();
707
708 let mock_child1 = tokio::process::Command::new("echo")
709 .stdin(Stdio::piped())
710 .stdout(Stdio::piped())
711 .kill_on_drop(true)
712 .spawn()
713 .unwrap();
714
715 let mock_stdin1 = tokio::process::Command::new("cat")
716 .stdin(Stdio::piped())
717 .spawn()
718 .unwrap()
719 .stdin
720 .take()
721 .unwrap();
722
723 let mock_stdout1 = tokio::process::Command::new("echo")
724 .stdout(Stdio::piped())
725 .spawn()
726 .unwrap()
727 .stdout
728 .take()
729 .unwrap();
730
731 let transport1 = LspTransport::new(mock_stdin1, mock_stdout1);
732 let client1 = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport1);
733
734 let server1 = LspServer {
735 client: client1,
736 capabilities: lsp_types::ServerCapabilities::default(),
737 position_encoding: PositionEncodingKind::UTF8,
738 _child: mock_child1,
739 };
740
741 result.add_server("rust".to_string(), server1);
742
743 assert!(result.has_servers());
744 assert!(!result.all_failed());
745 assert!(!result.partial_success());
746 assert_eq!(result.server_count(), 1);
747 assert_eq!(result.failure_count(), 0);
748 }
749
750 #[tokio::test]
751 async fn test_server_init_result_partial_success() {
752 let mut result = ServerInitResult::new();
753
754 let mock_child = tokio::process::Command::new("echo")
755 .stdin(Stdio::piped())
756 .stdout(Stdio::piped())
757 .kill_on_drop(true)
758 .spawn()
759 .unwrap();
760
761 let mock_stdin = tokio::process::Command::new("cat")
762 .stdin(Stdio::piped())
763 .spawn()
764 .unwrap()
765 .stdin
766 .take()
767 .unwrap();
768
769 let mock_stdout = tokio::process::Command::new("echo")
770 .stdout(Stdio::piped())
771 .spawn()
772 .unwrap()
773 .stdout
774 .take()
775 .unwrap();
776
777 let transport = LspTransport::new(mock_stdin, mock_stdout);
778 let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
779
780 let server = LspServer {
781 client,
782 capabilities: lsp_types::ServerCapabilities::default(),
783 position_encoding: PositionEncodingKind::UTF8,
784 _child: mock_child,
785 };
786
787 result.add_server("rust".to_string(), server);
788
789 result.add_failure(ServerSpawnFailure {
790 language_id: "python".to_string(),
791 command: "pyright".to_string(),
792 message: "not found".to_string(),
793 });
794
795 assert!(result.has_servers());
796 assert!(!result.all_failed());
797 assert!(result.partial_success());
798 assert_eq!(result.server_count(), 1);
799 assert_eq!(result.failure_count(), 1);
800 }
801
802 #[tokio::test]
803 async fn test_server_init_result_multiple_servers() {
804 let mut result = ServerInitResult::new();
805
806 for i in 0..3 {
807 let mock_child = tokio::process::Command::new("echo")
808 .stdin(Stdio::piped())
809 .stdout(Stdio::piped())
810 .kill_on_drop(true)
811 .spawn()
812 .unwrap();
813
814 let mock_stdin = tokio::process::Command::new("cat")
815 .stdin(Stdio::piped())
816 .spawn()
817 .unwrap()
818 .stdin
819 .take()
820 .unwrap();
821
822 let mock_stdout = tokio::process::Command::new("echo")
823 .stdout(Stdio::piped())
824 .spawn()
825 .unwrap()
826 .stdout
827 .take()
828 .unwrap();
829
830 let transport = LspTransport::new(mock_stdin, mock_stdout);
831 let config = if i == 0 {
832 LspServerConfig::rust_analyzer()
833 } else if i == 1 {
834 LspServerConfig::pyright()
835 } else {
836 LspServerConfig::typescript()
837 };
838 let client = LspClient::from_transport(config.clone(), transport);
839
840 let server = LspServer {
841 client,
842 capabilities: lsp_types::ServerCapabilities::default(),
843 position_encoding: PositionEncodingKind::UTF8,
844 _child: mock_child,
845 };
846
847 result.add_server(config.language_id, server);
848 }
849
850 assert!(result.has_servers());
851 assert!(!result.all_failed());
852 assert!(!result.partial_success());
853 assert_eq!(result.server_count(), 3);
854 assert_eq!(result.failure_count(), 0);
855 }
856
857 #[tokio::test]
858 async fn test_server_init_result_replace_server() {
859 let mut result = ServerInitResult::new();
860
861 let mock_child1 = tokio::process::Command::new("echo")
862 .stdin(Stdio::piped())
863 .stdout(Stdio::piped())
864 .kill_on_drop(true)
865 .spawn()
866 .unwrap();
867
868 let mock_stdin1 = tokio::process::Command::new("cat")
869 .stdin(Stdio::piped())
870 .spawn()
871 .unwrap()
872 .stdin
873 .take()
874 .unwrap();
875
876 let mock_stdout1 = tokio::process::Command::new("echo")
877 .stdout(Stdio::piped())
878 .spawn()
879 .unwrap()
880 .stdout
881 .take()
882 .unwrap();
883
884 let transport1 = LspTransport::new(mock_stdin1, mock_stdout1);
885 let client1 = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport1);
886
887 let server1 = LspServer {
888 client: client1,
889 capabilities: lsp_types::ServerCapabilities::default(),
890 position_encoding: PositionEncodingKind::UTF8,
891 _child: mock_child1,
892 };
893
894 result.add_server("rust".to_string(), server1);
895 assert_eq!(result.server_count(), 1);
896
897 let mock_child2 = tokio::process::Command::new("echo")
898 .stdin(Stdio::piped())
899 .stdout(Stdio::piped())
900 .kill_on_drop(true)
901 .spawn()
902 .unwrap();
903
904 let mock_stdin2 = tokio::process::Command::new("cat")
905 .stdin(Stdio::piped())
906 .spawn()
907 .unwrap()
908 .stdin
909 .take()
910 .unwrap();
911
912 let mock_stdout2 = tokio::process::Command::new("echo")
913 .stdout(Stdio::piped())
914 .spawn()
915 .unwrap()
916 .stdout
917 .take()
918 .unwrap();
919
920 let transport2 = LspTransport::new(mock_stdin2, mock_stdout2);
921 let client2 = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport2);
922
923 let server2 = LspServer {
924 client: client2,
925 capabilities: lsp_types::ServerCapabilities::default(),
926 position_encoding: PositionEncodingKind::UTF16,
927 _child: mock_child2,
928 };
929
930 result.add_server("rust".to_string(), server2);
931 assert_eq!(result.server_count(), 1);
932 }
933
934 #[test]
935 fn test_server_init_result_debug() {
936 let mut result = ServerInitResult::new();
937
938 result.add_failure(ServerSpawnFailure {
939 language_id: "rust".to_string(),
940 command: "rust-analyzer".to_string(),
941 message: "not found".to_string(),
942 });
943
944 let debug_str = format!("{result:?}");
945 assert!(debug_str.contains("ServerInitResult"));
946 }
947
948 #[test]
949 fn test_server_init_result_multiple_failures() {
950 let mut result = ServerInitResult::new();
951
952 result.add_failure(ServerSpawnFailure {
953 language_id: "python".to_string(),
954 command: "pyright".to_string(),
955 message: "not found".to_string(),
956 });
957
958 result.add_failure(ServerSpawnFailure {
959 language_id: "typescript".to_string(),
960 command: "tsserver".to_string(),
961 message: "command not found".to_string(),
962 });
963
964 assert_eq!(result.failure_count(), 2);
965 assert_eq!(result.server_count(), 0);
966 assert!(result.all_failed());
967 assert!(!result.partial_success());
968 }
969
970 #[tokio::test]
971 async fn test_spawn_batch_empty_configs() {
972 let configs: &[ServerInitConfig] = &[];
973 let result = LspServer::spawn_batch(configs).await;
974
975 assert!(!result.has_servers());
976 assert!(!result.all_failed());
977 assert!(!result.partial_success());
978 assert_eq!(result.server_count(), 0);
979 assert_eq!(result.failure_count(), 0);
980 }
981
982 #[tokio::test]
983 async fn test_spawn_batch_single_invalid_config() {
984 let configs = vec![ServerInitConfig {
985 server_config: LspServerConfig {
986 language_id: "rust".to_string(),
987 command: "nonexistent-command-12345".to_string(),
988 args: vec![],
989 env: std::collections::HashMap::new(),
990 file_patterns: vec!["**/*.rs".to_string()],
991 initialization_options: None,
992 timeout_seconds: 10,
993 },
994 workspace_roots: vec![],
995 initialization_options: None,
996 }];
997
998 let result = LspServer::spawn_batch(&configs).await;
999
1000 assert!(!result.has_servers());
1001 assert!(result.all_failed());
1002 assert!(!result.partial_success());
1003 assert_eq!(result.server_count(), 0);
1004 assert_eq!(result.failure_count(), 1);
1005
1006 let failure = &result.failures[0];
1007 assert_eq!(failure.language_id, "rust");
1008 assert_eq!(failure.command, "nonexistent-command-12345");
1009 assert!(failure.message.contains("spawn"));
1010 }
1011
1012 #[tokio::test]
1013 async fn test_spawn_batch_all_invalid_configs() {
1014 let configs = vec![
1015 ServerInitConfig {
1016 server_config: LspServerConfig {
1017 language_id: "rust".to_string(),
1018 command: "nonexistent-rust-analyzer".to_string(),
1019 args: vec![],
1020 env: std::collections::HashMap::new(),
1021 file_patterns: vec!["**/*.rs".to_string()],
1022 initialization_options: None,
1023 timeout_seconds: 10,
1024 },
1025 workspace_roots: vec![],
1026 initialization_options: None,
1027 },
1028 ServerInitConfig {
1029 server_config: LspServerConfig {
1030 language_id: "python".to_string(),
1031 command: "nonexistent-pyright".to_string(),
1032 args: vec![],
1033 env: std::collections::HashMap::new(),
1034 file_patterns: vec!["**/*.py".to_string()],
1035 initialization_options: None,
1036 timeout_seconds: 10,
1037 },
1038 workspace_roots: vec![],
1039 initialization_options: None,
1040 },
1041 ServerInitConfig {
1042 server_config: LspServerConfig {
1043 language_id: "typescript".to_string(),
1044 command: "nonexistent-tsserver".to_string(),
1045 args: vec![],
1046 env: std::collections::HashMap::new(),
1047 file_patterns: vec!["**/*.ts".to_string()],
1048 initialization_options: None,
1049 timeout_seconds: 10,
1050 },
1051 workspace_roots: vec![],
1052 initialization_options: None,
1053 },
1054 ];
1055
1056 let result = LspServer::spawn_batch(&configs).await;
1057
1058 assert!(!result.has_servers());
1059 assert!(result.all_failed());
1060 assert!(!result.partial_success());
1061 assert_eq!(result.server_count(), 0);
1062 assert_eq!(result.failure_count(), 3);
1063
1064 let failure_languages: Vec<_> = result
1065 .failures
1066 .iter()
1067 .map(|f| f.language_id.as_str())
1068 .collect();
1069 assert!(failure_languages.contains(&"rust"));
1070 assert!(failure_languages.contains(&"python"));
1071 assert!(failure_languages.contains(&"typescript"));
1072 }
1073
1074 #[tokio::test]
1075 async fn test_spawn_batch_multiple_invalid_configs_ordering() {
1076 let configs = vec![
1077 ServerInitConfig {
1078 server_config: LspServerConfig {
1079 language_id: "lang1".to_string(),
1080 command: "cmd1-nonexistent".to_string(),
1081 args: vec![],
1082 env: std::collections::HashMap::new(),
1083 file_patterns: vec![],
1084 initialization_options: None,
1085 timeout_seconds: 10,
1086 },
1087 workspace_roots: vec![],
1088 initialization_options: None,
1089 },
1090 ServerInitConfig {
1091 server_config: LspServerConfig {
1092 language_id: "lang2".to_string(),
1093 command: "cmd2-nonexistent".to_string(),
1094 args: vec![],
1095 env: std::collections::HashMap::new(),
1096 file_patterns: vec![],
1097 initialization_options: None,
1098 timeout_seconds: 10,
1099 },
1100 workspace_roots: vec![],
1101 initialization_options: None,
1102 },
1103 ];
1104
1105 let result = LspServer::spawn_batch(&configs).await;
1106
1107 assert_eq!(result.failure_count(), 2);
1108
1109 assert_eq!(result.failures[0].language_id, "lang1");
1110 assert_eq!(result.failures[0].command, "cmd1-nonexistent");
1111
1112 assert_eq!(result.failures[1].language_id, "lang2");
1113 assert_eq!(result.failures[1].command, "cmd2-nonexistent");
1114 }
1115
1116 #[tokio::test]
1117 async fn test_spawn_batch_logs_each_failure() {
1118 let configs = vec![
1119 ServerInitConfig {
1120 server_config: LspServerConfig {
1121 language_id: "test1".to_string(),
1122 command: "nonexistent-test1".to_string(),
1123 args: vec![],
1124 env: std::collections::HashMap::new(),
1125 file_patterns: vec![],
1126 initialization_options: None,
1127 timeout_seconds: 10,
1128 },
1129 workspace_roots: vec![],
1130 initialization_options: None,
1131 },
1132 ServerInitConfig {
1133 server_config: LspServerConfig {
1134 language_id: "test2".to_string(),
1135 command: "nonexistent-test2".to_string(),
1136 args: vec![],
1137 env: std::collections::HashMap::new(),
1138 file_patterns: vec![],
1139 initialization_options: None,
1140 timeout_seconds: 10,
1141 },
1142 workspace_roots: vec![],
1143 initialization_options: None,
1144 },
1145 ];
1146
1147 let result = LspServer::spawn_batch(&configs).await;
1148
1149 assert_eq!(result.failure_count(), 2);
1150 assert_eq!(result.failures[0].language_id, "test1");
1151 assert_eq!(result.failures[1].language_id, "test2");
1152 }
1153}