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 heuristics: None,
584 },
585 workspace_roots: vec![PathBuf::from("/workspace")],
586 initialization_options: Some(init_opts),
587 };
588
589 assert!(config.initialization_options.is_some());
590 assert_eq!(config.workspace_roots.len(), 1);
591 }
592
593 #[test]
594 fn test_server_init_config_empty_workspace() {
595 let config = ServerInitConfig {
596 server_config: LspServerConfig::typescript(),
597 workspace_roots: vec![],
598 initialization_options: None,
599 };
600
601 assert!(config.workspace_roots.is_empty());
602 }
603
604 #[test]
605 fn test_server_init_config_multiple_workspaces() {
606 let config = ServerInitConfig {
607 server_config: LspServerConfig::rust_analyzer(),
608 workspace_roots: vec![
609 PathBuf::from("/workspace1"),
610 PathBuf::from("/workspace2"),
611 PathBuf::from("/workspace3"),
612 ],
613 initialization_options: None,
614 };
615
616 assert_eq!(config.workspace_roots.len(), 3);
617 }
618
619 #[tokio::test]
620 async fn test_lsp_server_getters() {
621 use lsp_types::ServerCapabilities;
622
623 let mock_child = tokio::process::Command::new("echo")
624 .stdin(Stdio::piped())
625 .stdout(Stdio::piped())
626 .kill_on_drop(true)
627 .spawn()
628 .unwrap();
629
630 let mock_stdin = tokio::process::Command::new("cat")
631 .stdin(Stdio::piped())
632 .spawn()
633 .unwrap()
634 .stdin
635 .take()
636 .unwrap();
637
638 let mock_stdout = tokio::process::Command::new("echo")
639 .stdout(Stdio::piped())
640 .spawn()
641 .unwrap()
642 .stdout
643 .take()
644 .unwrap();
645
646 let transport = LspTransport::new(mock_stdin, mock_stdout);
647 let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
648
649 let server = LspServer {
650 client,
651 capabilities: ServerCapabilities::default(),
652 position_encoding: PositionEncodingKind::UTF8,
653 _child: mock_child,
654 };
655
656 assert_eq!(server.position_encoding(), PositionEncodingKind::UTF8);
657 assert!(server.capabilities().text_document_sync.is_none());
658
659 let debug_str = format!("{server:?}");
660 assert!(debug_str.contains("LspServer"));
661 assert!(debug_str.contains("<process>"));
662 }
663
664 #[test]
665 fn test_server_init_result_new_empty() {
666 let result = ServerInitResult::new();
667 assert!(!result.has_servers());
668 assert!(!result.all_failed());
669 assert!(!result.partial_success());
670 assert_eq!(result.server_count(), 0);
671 assert_eq!(result.failure_count(), 0);
672 }
673
674 #[test]
675 fn test_server_init_result_default() {
676 let result = ServerInitResult::default();
677 assert!(!result.has_servers());
678 assert_eq!(result.server_count(), 0);
679 assert_eq!(result.failure_count(), 0);
680 }
681
682 #[test]
683 fn test_server_init_result_all_failures() {
684 let mut result = ServerInitResult::new();
685
686 result.add_failure(ServerSpawnFailure {
687 language_id: "rust".to_string(),
688 command: "rust-analyzer".to_string(),
689 message: "not found".to_string(),
690 });
691
692 result.add_failure(ServerSpawnFailure {
693 language_id: "python".to_string(),
694 command: "pyright".to_string(),
695 message: "permission denied".to_string(),
696 });
697
698 assert!(!result.has_servers());
699 assert!(result.all_failed());
700 assert!(!result.partial_success());
701 assert_eq!(result.server_count(), 0);
702 assert_eq!(result.failure_count(), 2);
703 }
704
705 #[tokio::test]
706 async fn test_server_init_result_all_success() {
707 let mut result = ServerInitResult::new();
708
709 let mock_child1 = tokio::process::Command::new("echo")
710 .stdin(Stdio::piped())
711 .stdout(Stdio::piped())
712 .kill_on_drop(true)
713 .spawn()
714 .unwrap();
715
716 let mock_stdin1 = tokio::process::Command::new("cat")
717 .stdin(Stdio::piped())
718 .spawn()
719 .unwrap()
720 .stdin
721 .take()
722 .unwrap();
723
724 let mock_stdout1 = tokio::process::Command::new("echo")
725 .stdout(Stdio::piped())
726 .spawn()
727 .unwrap()
728 .stdout
729 .take()
730 .unwrap();
731
732 let transport1 = LspTransport::new(mock_stdin1, mock_stdout1);
733 let client1 = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport1);
734
735 let server1 = LspServer {
736 client: client1,
737 capabilities: lsp_types::ServerCapabilities::default(),
738 position_encoding: PositionEncodingKind::UTF8,
739 _child: mock_child1,
740 };
741
742 result.add_server("rust".to_string(), server1);
743
744 assert!(result.has_servers());
745 assert!(!result.all_failed());
746 assert!(!result.partial_success());
747 assert_eq!(result.server_count(), 1);
748 assert_eq!(result.failure_count(), 0);
749 }
750
751 #[tokio::test]
752 async fn test_server_init_result_partial_success() {
753 let mut result = ServerInitResult::new();
754
755 let mock_child = tokio::process::Command::new("echo")
756 .stdin(Stdio::piped())
757 .stdout(Stdio::piped())
758 .kill_on_drop(true)
759 .spawn()
760 .unwrap();
761
762 let mock_stdin = tokio::process::Command::new("cat")
763 .stdin(Stdio::piped())
764 .spawn()
765 .unwrap()
766 .stdin
767 .take()
768 .unwrap();
769
770 let mock_stdout = tokio::process::Command::new("echo")
771 .stdout(Stdio::piped())
772 .spawn()
773 .unwrap()
774 .stdout
775 .take()
776 .unwrap();
777
778 let transport = LspTransport::new(mock_stdin, mock_stdout);
779 let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
780
781 let server = LspServer {
782 client,
783 capabilities: lsp_types::ServerCapabilities::default(),
784 position_encoding: PositionEncodingKind::UTF8,
785 _child: mock_child,
786 };
787
788 result.add_server("rust".to_string(), server);
789
790 result.add_failure(ServerSpawnFailure {
791 language_id: "python".to_string(),
792 command: "pyright".to_string(),
793 message: "not found".to_string(),
794 });
795
796 assert!(result.has_servers());
797 assert!(!result.all_failed());
798 assert!(result.partial_success());
799 assert_eq!(result.server_count(), 1);
800 assert_eq!(result.failure_count(), 1);
801 }
802
803 #[tokio::test]
804 async fn test_server_init_result_multiple_servers() {
805 let mut result = ServerInitResult::new();
806
807 for i in 0..3 {
808 let mock_child = tokio::process::Command::new("echo")
809 .stdin(Stdio::piped())
810 .stdout(Stdio::piped())
811 .kill_on_drop(true)
812 .spawn()
813 .unwrap();
814
815 let mock_stdin = tokio::process::Command::new("cat")
816 .stdin(Stdio::piped())
817 .spawn()
818 .unwrap()
819 .stdin
820 .take()
821 .unwrap();
822
823 let mock_stdout = tokio::process::Command::new("echo")
824 .stdout(Stdio::piped())
825 .spawn()
826 .unwrap()
827 .stdout
828 .take()
829 .unwrap();
830
831 let transport = LspTransport::new(mock_stdin, mock_stdout);
832 let config = if i == 0 {
833 LspServerConfig::rust_analyzer()
834 } else if i == 1 {
835 LspServerConfig::pyright()
836 } else {
837 LspServerConfig::typescript()
838 };
839 let client = LspClient::from_transport(config.clone(), transport);
840
841 let server = LspServer {
842 client,
843 capabilities: lsp_types::ServerCapabilities::default(),
844 position_encoding: PositionEncodingKind::UTF8,
845 _child: mock_child,
846 };
847
848 result.add_server(config.language_id, server);
849 }
850
851 assert!(result.has_servers());
852 assert!(!result.all_failed());
853 assert!(!result.partial_success());
854 assert_eq!(result.server_count(), 3);
855 assert_eq!(result.failure_count(), 0);
856 }
857
858 #[tokio::test]
859 async fn test_server_init_result_replace_server() {
860 let mut result = ServerInitResult::new();
861
862 let mock_child1 = tokio::process::Command::new("echo")
863 .stdin(Stdio::piped())
864 .stdout(Stdio::piped())
865 .kill_on_drop(true)
866 .spawn()
867 .unwrap();
868
869 let mock_stdin1 = tokio::process::Command::new("cat")
870 .stdin(Stdio::piped())
871 .spawn()
872 .unwrap()
873 .stdin
874 .take()
875 .unwrap();
876
877 let mock_stdout1 = tokio::process::Command::new("echo")
878 .stdout(Stdio::piped())
879 .spawn()
880 .unwrap()
881 .stdout
882 .take()
883 .unwrap();
884
885 let transport1 = LspTransport::new(mock_stdin1, mock_stdout1);
886 let client1 = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport1);
887
888 let server1 = LspServer {
889 client: client1,
890 capabilities: lsp_types::ServerCapabilities::default(),
891 position_encoding: PositionEncodingKind::UTF8,
892 _child: mock_child1,
893 };
894
895 result.add_server("rust".to_string(), server1);
896 assert_eq!(result.server_count(), 1);
897
898 let mock_child2 = tokio::process::Command::new("echo")
899 .stdin(Stdio::piped())
900 .stdout(Stdio::piped())
901 .kill_on_drop(true)
902 .spawn()
903 .unwrap();
904
905 let mock_stdin2 = tokio::process::Command::new("cat")
906 .stdin(Stdio::piped())
907 .spawn()
908 .unwrap()
909 .stdin
910 .take()
911 .unwrap();
912
913 let mock_stdout2 = tokio::process::Command::new("echo")
914 .stdout(Stdio::piped())
915 .spawn()
916 .unwrap()
917 .stdout
918 .take()
919 .unwrap();
920
921 let transport2 = LspTransport::new(mock_stdin2, mock_stdout2);
922 let client2 = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport2);
923
924 let server2 = LspServer {
925 client: client2,
926 capabilities: lsp_types::ServerCapabilities::default(),
927 position_encoding: PositionEncodingKind::UTF16,
928 _child: mock_child2,
929 };
930
931 result.add_server("rust".to_string(), server2);
932 assert_eq!(result.server_count(), 1);
933 }
934
935 #[test]
936 fn test_server_init_result_debug() {
937 let mut result = ServerInitResult::new();
938
939 result.add_failure(ServerSpawnFailure {
940 language_id: "rust".to_string(),
941 command: "rust-analyzer".to_string(),
942 message: "not found".to_string(),
943 });
944
945 let debug_str = format!("{result:?}");
946 assert!(debug_str.contains("ServerInitResult"));
947 }
948
949 #[test]
950 fn test_server_init_result_multiple_failures() {
951 let mut result = ServerInitResult::new();
952
953 result.add_failure(ServerSpawnFailure {
954 language_id: "python".to_string(),
955 command: "pyright".to_string(),
956 message: "not found".to_string(),
957 });
958
959 result.add_failure(ServerSpawnFailure {
960 language_id: "typescript".to_string(),
961 command: "tsserver".to_string(),
962 message: "command not found".to_string(),
963 });
964
965 assert_eq!(result.failure_count(), 2);
966 assert_eq!(result.server_count(), 0);
967 assert!(result.all_failed());
968 assert!(!result.partial_success());
969 }
970
971 #[tokio::test]
972 async fn test_spawn_batch_empty_configs() {
973 let configs: &[ServerInitConfig] = &[];
974 let result = LspServer::spawn_batch(configs).await;
975
976 assert!(!result.has_servers());
977 assert!(!result.all_failed());
978 assert!(!result.partial_success());
979 assert_eq!(result.server_count(), 0);
980 assert_eq!(result.failure_count(), 0);
981 }
982
983 #[tokio::test]
984 async fn test_spawn_batch_single_invalid_config() {
985 let configs = vec![ServerInitConfig {
986 server_config: LspServerConfig {
987 language_id: "rust".to_string(),
988 command: "nonexistent-command-12345".to_string(),
989 args: vec![],
990 env: std::collections::HashMap::new(),
991 file_patterns: vec!["**/*.rs".to_string()],
992 initialization_options: None,
993 timeout_seconds: 10,
994 heuristics: None,
995 },
996 workspace_roots: vec![],
997 initialization_options: None,
998 }];
999
1000 let result = LspServer::spawn_batch(&configs).await;
1001
1002 assert!(!result.has_servers());
1003 assert!(result.all_failed());
1004 assert!(!result.partial_success());
1005 assert_eq!(result.server_count(), 0);
1006 assert_eq!(result.failure_count(), 1);
1007
1008 let failure = &result.failures[0];
1009 assert_eq!(failure.language_id, "rust");
1010 assert_eq!(failure.command, "nonexistent-command-12345");
1011 assert!(failure.message.contains("spawn"));
1012 }
1013
1014 #[tokio::test]
1015 async fn test_spawn_batch_all_invalid_configs() {
1016 let configs = vec![
1017 ServerInitConfig {
1018 server_config: LspServerConfig {
1019 language_id: "rust".to_string(),
1020 command: "nonexistent-rust-analyzer".to_string(),
1021 args: vec![],
1022 env: std::collections::HashMap::new(),
1023 file_patterns: vec!["**/*.rs".to_string()],
1024 initialization_options: None,
1025 timeout_seconds: 10,
1026 heuristics: None,
1027 },
1028 workspace_roots: vec![],
1029 initialization_options: None,
1030 },
1031 ServerInitConfig {
1032 server_config: LspServerConfig {
1033 language_id: "python".to_string(),
1034 command: "nonexistent-pyright".to_string(),
1035 args: vec![],
1036 env: std::collections::HashMap::new(),
1037 file_patterns: vec!["**/*.py".to_string()],
1038 initialization_options: None,
1039 timeout_seconds: 10,
1040 heuristics: None,
1041 },
1042 workspace_roots: vec![],
1043 initialization_options: None,
1044 },
1045 ServerInitConfig {
1046 server_config: LspServerConfig {
1047 language_id: "typescript".to_string(),
1048 command: "nonexistent-tsserver".to_string(),
1049 args: vec![],
1050 env: std::collections::HashMap::new(),
1051 file_patterns: vec!["**/*.ts".to_string()],
1052 initialization_options: None,
1053 timeout_seconds: 10,
1054 heuristics: None,
1055 },
1056 workspace_roots: vec![],
1057 initialization_options: None,
1058 },
1059 ];
1060
1061 let result = LspServer::spawn_batch(&configs).await;
1062
1063 assert!(!result.has_servers());
1064 assert!(result.all_failed());
1065 assert!(!result.partial_success());
1066 assert_eq!(result.server_count(), 0);
1067 assert_eq!(result.failure_count(), 3);
1068
1069 let failure_languages: Vec<_> = result
1070 .failures
1071 .iter()
1072 .map(|f| f.language_id.as_str())
1073 .collect();
1074 assert!(failure_languages.contains(&"rust"));
1075 assert!(failure_languages.contains(&"python"));
1076 assert!(failure_languages.contains(&"typescript"));
1077 }
1078
1079 #[tokio::test]
1080 async fn test_spawn_batch_multiple_invalid_configs_ordering() {
1081 let configs = vec![
1082 ServerInitConfig {
1083 server_config: LspServerConfig {
1084 language_id: "lang1".to_string(),
1085 command: "cmd1-nonexistent".to_string(),
1086 args: vec![],
1087 env: std::collections::HashMap::new(),
1088 file_patterns: vec![],
1089 initialization_options: None,
1090 timeout_seconds: 10,
1091 heuristics: None,
1092 },
1093 workspace_roots: vec![],
1094 initialization_options: None,
1095 },
1096 ServerInitConfig {
1097 server_config: LspServerConfig {
1098 language_id: "lang2".to_string(),
1099 command: "cmd2-nonexistent".to_string(),
1100 args: vec![],
1101 env: std::collections::HashMap::new(),
1102 file_patterns: vec![],
1103 initialization_options: None,
1104 timeout_seconds: 10,
1105 heuristics: None,
1106 },
1107 workspace_roots: vec![],
1108 initialization_options: None,
1109 },
1110 ];
1111
1112 let result = LspServer::spawn_batch(&configs).await;
1113
1114 assert_eq!(result.failure_count(), 2);
1115
1116 assert_eq!(result.failures[0].language_id, "lang1");
1117 assert_eq!(result.failures[0].command, "cmd1-nonexistent");
1118
1119 assert_eq!(result.failures[1].language_id, "lang2");
1120 assert_eq!(result.failures[1].command, "cmd2-nonexistent");
1121 }
1122
1123 #[tokio::test]
1124 async fn test_spawn_batch_logs_each_failure() {
1125 let configs = vec![
1126 ServerInitConfig {
1127 server_config: LspServerConfig {
1128 language_id: "test1".to_string(),
1129 command: "nonexistent-test1".to_string(),
1130 args: vec![],
1131 env: std::collections::HashMap::new(),
1132 file_patterns: vec![],
1133 initialization_options: None,
1134 timeout_seconds: 10,
1135 heuristics: None,
1136 },
1137 workspace_roots: vec![],
1138 initialization_options: None,
1139 },
1140 ServerInitConfig {
1141 server_config: LspServerConfig {
1142 language_id: "test2".to_string(),
1143 command: "nonexistent-test2".to_string(),
1144 args: vec![],
1145 env: std::collections::HashMap::new(),
1146 file_patterns: vec![],
1147 initialization_options: None,
1148 timeout_seconds: 10,
1149 heuristics: None,
1150 },
1151 workspace_roots: vec![],
1152 initialization_options: None,
1153 },
1154 ];
1155
1156 let result = LspServer::spawn_batch(&configs).await;
1157
1158 assert_eq!(result.failure_count(), 2);
1159 assert_eq!(result.failures[0].language_id, "test1");
1160 assert_eq!(result.failures[1].language_id, "test2");
1161 }
1162}