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::sync::mpsc;
21use tokio::time::Duration;
22use tracing::{debug, info};
23
24use crate::config::LspServerConfig;
25use crate::error::{Error, Result, ServerSpawnFailure};
26use crate::lsp::client::LspClient;
27use crate::lsp::transport::LspTransport;
28use crate::lsp::types::LspNotification;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum ServerState {
33 Uninitialized,
35 Initializing,
37 Ready,
39 ShuttingDown,
41 Shutdown,
43}
44
45impl ServerState {
46 #[must_use]
48 pub const fn is_ready(&self) -> bool {
49 matches!(self, Self::Ready)
50 }
51
52 #[must_use]
54 pub const fn can_accept_requests(&self) -> bool {
55 matches!(self, Self::Ready)
56 }
57}
58
59#[derive(Debug, Clone)]
61pub struct ServerInitConfig {
62 pub server_config: LspServerConfig,
64 pub workspace_roots: Vec<PathBuf>,
66 pub initialization_options: Option<serde_json::Value>,
68 pub notification_tx: Option<mpsc::Sender<LspNotification>>,
75}
76
77#[derive(Debug)]
101pub struct ServerInitResult {
102 pub servers: HashMap<String, LspServer>,
104 pub failures: Vec<ServerSpawnFailure>,
106}
107
108impl ServerInitResult {
109 #[must_use]
111 pub fn new() -> Self {
112 Self {
113 servers: HashMap::new(),
114 failures: Vec::new(),
115 }
116 }
117
118 #[must_use]
122 pub fn has_servers(&self) -> bool {
123 !self.servers.is_empty()
124 }
125
126 #[must_use]
131 pub fn all_failed(&self) -> bool {
132 self.servers.is_empty() && !self.failures.is_empty()
133 }
134
135 #[must_use]
139 pub fn partial_success(&self) -> bool {
140 !self.servers.is_empty() && !self.failures.is_empty()
141 }
142
143 #[must_use]
145 pub fn server_count(&self) -> usize {
146 self.servers.len()
147 }
148
149 #[must_use]
151 pub const fn failure_count(&self) -> usize {
152 self.failures.len()
153 }
154
155 pub fn add_server(&mut self, language_id: String, server: LspServer) {
159 self.servers.insert(language_id, server);
160 }
161
162 pub fn add_failure(&mut self, failure: ServerSpawnFailure) {
164 self.failures.push(failure);
165 }
166}
167
168impl Default for ServerInitResult {
169 fn default() -> Self {
170 Self::new()
171 }
172}
173
174pub struct LspServer {
176 client: LspClient,
177 capabilities: ServerCapabilities,
178 position_encoding: PositionEncodingKind,
179 pub notification_rx: mpsc::Receiver<LspNotification>,
184 _child: tokio::process::Child,
187}
188
189impl std::fmt::Debug for LspServer {
190 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191 f.debug_struct("LspServer")
192 .field("client", &self.client)
193 .field("capabilities", &self.capabilities)
194 .field("position_encoding", &self.position_encoding)
195 .field("notification_rx", &"<channel>")
196 .field("_child", &"<process>")
197 .finish()
198 }
199}
200
201impl LspServer {
202 pub fn take_notification_rx(&mut self) -> tokio::sync::mpsc::Receiver<LspNotification> {
208 let (_, dummy) = tokio::sync::mpsc::channel(1);
209 std::mem::replace(&mut self.notification_rx, dummy)
210 }
211
212 pub async fn spawn(config: ServerInitConfig) -> Result<Self> {
227 info!(
228 "Spawning LSP server: {} {:?}",
229 config.server_config.command, config.server_config.args
230 );
231
232 let mut child = Command::new(&config.server_config.command)
233 .args(&config.server_config.args)
234 .stdin(Stdio::piped())
235 .stdout(Stdio::piped())
236 .stderr(Stdio::null())
237 .kill_on_drop(true)
238 .spawn()
239 .map_err(|e| Error::ServerSpawnFailed {
240 command: config.server_config.command.clone(),
241 source: e,
242 })?;
243
244 let stdin = child
245 .stdin
246 .take()
247 .ok_or_else(|| Error::Transport("Failed to capture stdin".to_string()))?;
248 let stdout = child
249 .stdout
250 .take()
251 .ok_or_else(|| Error::Transport("Failed to capture stdout".to_string()))?;
252
253 let transport = LspTransport::new(stdin, stdout);
254 let (notification_tx, notification_rx) = mpsc::channel(64);
255 let client = LspClient::from_transport_with_notifications(
256 config.server_config.clone(),
257 transport,
258 notification_tx,
259 );
260
261 let (capabilities, position_encoding) = Self::initialize(&client, &config).await?;
262
263 info!("LSP server initialized successfully");
264
265 Ok(Self {
266 client,
267 capabilities,
268 position_encoding,
269 notification_rx,
270 _child: child,
271 })
272 }
273
274 #[allow(clippy::too_many_lines)]
278 async fn initialize(
279 client: &LspClient,
280 config: &ServerInitConfig,
281 ) -> Result<(ServerCapabilities, PositionEncodingKind)> {
282 debug!("Sending initialize request");
283
284 let workspace_folders: Vec<WorkspaceFolder> = config
285 .workspace_roots
286 .iter()
287 .map(|root| {
288 let path_str = root.to_str().ok_or_else(|| {
289 let root_display = root.display();
290 Error::InvalidUri(format!("Invalid UTF-8 in path: {root_display}"))
291 })?;
292 let uri_str = if cfg!(windows) {
293 let stripped = path_str.strip_prefix(r"\\?\").unwrap_or(path_str);
295 format!("file:///{}", stripped.replace('\\', "/"))
296 } else {
297 format!("file://{path_str}")
298 };
299 let uri = Uri::from_str(&uri_str).map_err(|_| {
300 let root_display = root.display();
301 Error::InvalidUri(format!("Invalid workspace root: {root_display}"))
302 })?;
303 Ok(WorkspaceFolder {
304 uri,
305 name: root
306 .file_name()
307 .and_then(|n| n.to_str())
308 .unwrap_or("workspace")
309 .to_string(),
310 })
311 })
312 .collect::<Result<Vec<_>>>()?;
313
314 let params = InitializeParams {
315 process_id: Some(std::process::id()),
316 #[allow(deprecated)]
317 root_uri: None,
318 initialization_options: config.initialization_options.clone(),
319 capabilities: ClientCapabilities {
320 general: Some(GeneralClientCapabilities {
321 position_encodings: Some(vec![
322 PositionEncodingKind::UTF8,
323 PositionEncodingKind::UTF16,
324 ]),
325 ..Default::default()
326 }),
327 text_document: Some(lsp_types::TextDocumentClientCapabilities {
328 hover: Some(lsp_types::HoverClientCapabilities {
329 dynamic_registration: Some(false),
330 content_format: Some(vec![
331 lsp_types::MarkupKind::Markdown,
332 lsp_types::MarkupKind::PlainText,
333 ]),
334 }),
335 definition: Some(lsp_types::GotoCapability {
336 dynamic_registration: Some(false),
337 link_support: Some(true),
338 }),
339 references: Some(lsp_types::ReferenceClientCapabilities {
340 dynamic_registration: Some(false),
341 }),
342 code_action: Some(lsp_types::CodeActionClientCapabilities {
343 dynamic_registration: Some(false),
344 data_support: Some(true),
345 resolve_support: Some(lsp_types::CodeActionCapabilityResolveSupport {
346 properties: vec!["edit".to_string()],
347 }),
348 code_action_literal_support: Some(lsp_types::CodeActionLiteralSupport {
351 code_action_kind: lsp_types::CodeActionKindLiteralSupport {
352 value_set: [
353 lsp_types::CodeActionKind::EMPTY,
354 lsp_types::CodeActionKind::QUICKFIX,
355 lsp_types::CodeActionKind::REFACTOR,
356 lsp_types::CodeActionKind::REFACTOR_EXTRACT,
357 lsp_types::CodeActionKind::REFACTOR_INLINE,
358 lsp_types::CodeActionKind::REFACTOR_REWRITE,
359 lsp_types::CodeActionKind::SOURCE,
360 lsp_types::CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
361 ]
362 .iter()
363 .map(|k| k.as_str().to_string())
364 .collect(),
365 },
366 }),
367 ..Default::default()
368 }),
369 ..Default::default()
370 }),
371 workspace: Some(lsp_types::WorkspaceClientCapabilities {
372 workspace_folders: Some(true),
373 ..Default::default()
374 }),
375 ..Default::default()
376 },
377 client_info: Some(ClientInfo {
378 name: "mcpls".to_string(),
379 version: Some(env!("CARGO_PKG_VERSION").to_string()),
380 }),
381 workspace_folders: Some(workspace_folders),
382 ..Default::default()
383 };
384
385 let result: InitializeResult = client
389 .request(
390 "initialize",
391 params,
392 Duration::from_secs(config.server_config.timeout_seconds),
393 )
394 .await
395 .map_err(|e| Error::LspInitFailed {
396 message: format!("Initialize request failed: {e}"),
397 })?;
398
399 let position_encoding = result
400 .capabilities
401 .position_encoding
402 .clone()
403 .unwrap_or(PositionEncodingKind::UTF16);
404
405 debug!(
406 "Server capabilities received, encoding: {:?}",
407 position_encoding
408 );
409
410 client
411 .notify("initialized", InitializedParams {})
412 .await
413 .map_err(|e| Error::LspInitFailed {
414 message: format!("Initialized notification failed: {e}"),
415 })?;
416
417 Ok((result.capabilities, position_encoding))
418 }
419
420 #[must_use]
422 pub const fn capabilities(&self) -> &ServerCapabilities {
423 &self.capabilities
424 }
425
426 #[must_use]
428 pub fn position_encoding(&self) -> PositionEncodingKind {
429 self.position_encoding.clone()
430 }
431
432 #[must_use]
434 pub const fn client(&self) -> &LspClient {
435 &self.client
436 }
437
438 pub async fn shutdown(self) -> Result<()> {
446 debug!("Shutting down LSP server");
447
448 let _: serde_json::Value = self
449 .client
450 .request("shutdown", serde_json::Value::Null, Duration::from_secs(5))
451 .await?;
452
453 self.client.notify("exit", serde_json::Value::Null).await?;
454
455 self.client.shutdown().await?;
456
457 info!("LSP server shut down successfully");
458 Ok(())
459 }
460
461 pub async fn spawn_batch(configs: &[ServerInitConfig]) -> ServerInitResult {
510 let mut result = ServerInitResult::new();
511
512 for config in configs {
513 let language_id = config.server_config.language_id.clone();
514 let command = config.server_config.command.clone();
515
516 match Self::spawn(config.clone()).await {
517 Ok(server) => {
518 info!(
519 "Successfully spawned LSP server: {} ({})",
520 language_id, command
521 );
522 result.add_server(language_id, server);
523 }
524 Err(e) => {
525 tracing::error!(
526 "Failed to spawn LSP server: {} ({}): {}",
527 language_id,
528 command,
529 e
530 );
531 result.add_failure(ServerSpawnFailure {
532 language_id,
533 command,
534 message: e.to_string(),
535 });
536 }
537 }
538 }
539
540 result
541 }
542}
543
544#[cfg(test)]
545#[allow(clippy::unwrap_used)]
546mod tests {
547 use super::*;
548
549 #[test]
550 fn test_server_state_ready() {
551 assert!(ServerState::Ready.is_ready());
552 assert!(ServerState::Ready.can_accept_requests());
553 }
554
555 #[test]
556 fn test_server_state_uninitialized() {
557 assert!(!ServerState::Uninitialized.is_ready());
558 assert!(!ServerState::Uninitialized.can_accept_requests());
559 }
560
561 #[test]
562 fn test_server_state_initializing() {
563 assert!(!ServerState::Initializing.is_ready());
564 assert!(!ServerState::Initializing.can_accept_requests());
565 }
566
567 #[test]
568 fn test_server_state_shutting_down() {
569 assert!(!ServerState::ShuttingDown.is_ready());
570 assert!(!ServerState::ShuttingDown.can_accept_requests());
571 }
572
573 #[test]
574 fn test_server_state_shutdown() {
575 assert!(!ServerState::Shutdown.is_ready());
576 assert!(!ServerState::Shutdown.can_accept_requests());
577 }
578
579 #[test]
580 fn test_server_state_equality() {
581 assert_eq!(ServerState::Ready, ServerState::Ready);
582 assert_ne!(ServerState::Ready, ServerState::Uninitialized);
583 assert_eq!(ServerState::Shutdown, ServerState::Shutdown);
584 }
585
586 #[test]
587 fn test_server_state_clone() {
588 let state = ServerState::Ready;
589 let cloned = state;
590 assert_eq!(state, cloned);
591 }
592
593 #[test]
594 fn test_server_state_debug() {
595 let state = ServerState::Ready;
596 let debug_str = format!("{state:?}");
597 assert!(debug_str.contains("Ready"));
598 }
599
600 #[test]
601 fn test_server_init_config_clone() {
602 let config = ServerInitConfig {
603 server_config: LspServerConfig::rust_analyzer(),
604 workspace_roots: vec![PathBuf::from("/tmp/workspace")],
605 initialization_options: Some(serde_json::json!({"key": "value"})),
606 notification_tx: None,
607 };
608
609 #[allow(clippy::redundant_clone)]
610 let cloned = config.clone();
611 assert_eq!(cloned.server_config.language_id, "rust");
612 assert_eq!(cloned.workspace_roots.len(), 1);
613 }
614
615 #[test]
616 fn test_server_init_config_debug() {
617 let config = ServerInitConfig {
618 server_config: LspServerConfig::pyright(),
619 workspace_roots: vec![],
620 initialization_options: None,
621 notification_tx: None,
622 };
623
624 let debug_str = format!("{config:?}");
625 assert!(debug_str.contains("python"));
626 assert!(debug_str.contains("pyright"));
627 }
628
629 #[test]
630 fn test_server_init_config_with_options() {
631 use std::collections::HashMap;
632
633 let init_opts = serde_json::json!({
634 "settings": {
635 "python": {
636 "analysis": {
637 "typeCheckingMode": "strict"
638 }
639 }
640 }
641 });
642
643 let mut env = HashMap::new();
644 env.insert("PYTHONPATH".to_string(), "/usr/lib".to_string());
645
646 let config = ServerInitConfig {
647 server_config: LspServerConfig {
648 language_id: "python".to_string(),
649 command: "pyright-langserver".to_string(),
650 args: vec!["--stdio".to_string()],
651 env,
652 file_patterns: vec!["**/*.py".to_string()],
653 initialization_options: Some(init_opts.clone()),
654 timeout_seconds: 10,
655 heuristics: None,
656 },
657 workspace_roots: vec![PathBuf::from("/workspace")],
658 initialization_options: Some(init_opts),
659 notification_tx: None,
660 };
661
662 assert!(config.initialization_options.is_some());
663 assert_eq!(config.workspace_roots.len(), 1);
664 }
665
666 #[test]
667 fn test_server_init_config_empty_workspace() {
668 let config = ServerInitConfig {
669 server_config: LspServerConfig::typescript(),
670 workspace_roots: vec![],
671 initialization_options: None,
672 notification_tx: None,
673 };
674
675 assert!(config.workspace_roots.is_empty());
676 }
677
678 #[test]
679 fn test_server_init_config_multiple_workspaces() {
680 let config = ServerInitConfig {
681 server_config: LspServerConfig::rust_analyzer(),
682 workspace_roots: vec![
683 PathBuf::from("/workspace1"),
684 PathBuf::from("/workspace2"),
685 PathBuf::from("/workspace3"),
686 ],
687 initialization_options: None,
688 notification_tx: None,
689 };
690
691 assert_eq!(config.workspace_roots.len(), 3);
692 }
693
694 #[tokio::test]
695 async fn test_lsp_server_getters() {
696 use lsp_types::ServerCapabilities;
697
698 let mock_child = tokio::process::Command::new("echo")
699 .stdin(Stdio::piped())
700 .stdout(Stdio::piped())
701 .kill_on_drop(true)
702 .spawn()
703 .unwrap();
704
705 let mock_stdin = tokio::process::Command::new("cat")
706 .stdin(Stdio::piped())
707 .spawn()
708 .unwrap()
709 .stdin
710 .take()
711 .unwrap();
712
713 let mock_stdout = tokio::process::Command::new("echo")
714 .stdout(Stdio::piped())
715 .spawn()
716 .unwrap()
717 .stdout
718 .take()
719 .unwrap();
720
721 let transport = LspTransport::new(mock_stdin, mock_stdout);
722 let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
723 let (_, mock_notification_rx) = mpsc::channel(1);
724
725 let server = LspServer {
726 client,
727 capabilities: ServerCapabilities::default(),
728 position_encoding: PositionEncodingKind::UTF8,
729 notification_rx: mock_notification_rx,
730 _child: mock_child,
731 };
732
733 assert_eq!(server.position_encoding(), PositionEncodingKind::UTF8);
734 assert!(server.capabilities().text_document_sync.is_none());
735
736 let debug_str = format!("{server:?}");
737 assert!(debug_str.contains("LspServer"));
738 assert!(debug_str.contains("<process>"));
739 }
740
741 #[test]
742 fn test_server_init_result_new_empty() {
743 let result = ServerInitResult::new();
744 assert!(!result.has_servers());
745 assert!(!result.all_failed());
746 assert!(!result.partial_success());
747 assert_eq!(result.server_count(), 0);
748 assert_eq!(result.failure_count(), 0);
749 }
750
751 #[test]
752 fn test_server_init_result_default() {
753 let result = ServerInitResult::default();
754 assert!(!result.has_servers());
755 assert_eq!(result.server_count(), 0);
756 assert_eq!(result.failure_count(), 0);
757 }
758
759 #[test]
760 fn test_server_init_result_all_failures() {
761 let mut result = ServerInitResult::new();
762
763 result.add_failure(ServerSpawnFailure {
764 language_id: "rust".to_string(),
765 command: "rust-analyzer".to_string(),
766 message: "not found".to_string(),
767 });
768
769 result.add_failure(ServerSpawnFailure {
770 language_id: "python".to_string(),
771 command: "pyright".to_string(),
772 message: "permission denied".to_string(),
773 });
774
775 assert!(!result.has_servers());
776 assert!(result.all_failed());
777 assert!(!result.partial_success());
778 assert_eq!(result.server_count(), 0);
779 assert_eq!(result.failure_count(), 2);
780 }
781
782 #[tokio::test]
783 async fn test_server_init_result_all_success() {
784 let mut result = ServerInitResult::new();
785
786 let mock_child1 = tokio::process::Command::new("echo")
787 .stdin(Stdio::piped())
788 .stdout(Stdio::piped())
789 .kill_on_drop(true)
790 .spawn()
791 .unwrap();
792
793 let mock_stdin1 = tokio::process::Command::new("cat")
794 .stdin(Stdio::piped())
795 .spawn()
796 .unwrap()
797 .stdin
798 .take()
799 .unwrap();
800
801 let mock_stdout1 = tokio::process::Command::new("echo")
802 .stdout(Stdio::piped())
803 .spawn()
804 .unwrap()
805 .stdout
806 .take()
807 .unwrap();
808
809 let transport1 = LspTransport::new(mock_stdin1, mock_stdout1);
810 let client1 = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport1);
811 let (_, mock_notification_rx1) = mpsc::channel(1);
812
813 let server1 = LspServer {
814 client: client1,
815 capabilities: lsp_types::ServerCapabilities::default(),
816 position_encoding: PositionEncodingKind::UTF8,
817 notification_rx: mock_notification_rx1,
818 _child: mock_child1,
819 };
820
821 result.add_server("rust".to_string(), server1);
822
823 assert!(result.has_servers());
824 assert!(!result.all_failed());
825 assert!(!result.partial_success());
826 assert_eq!(result.server_count(), 1);
827 assert_eq!(result.failure_count(), 0);
828 }
829
830 #[tokio::test]
831 async fn test_server_init_result_partial_success() {
832 let mut result = ServerInitResult::new();
833
834 let mock_child = tokio::process::Command::new("echo")
835 .stdin(Stdio::piped())
836 .stdout(Stdio::piped())
837 .kill_on_drop(true)
838 .spawn()
839 .unwrap();
840
841 let mock_stdin = tokio::process::Command::new("cat")
842 .stdin(Stdio::piped())
843 .spawn()
844 .unwrap()
845 .stdin
846 .take()
847 .unwrap();
848
849 let mock_stdout = tokio::process::Command::new("echo")
850 .stdout(Stdio::piped())
851 .spawn()
852 .unwrap()
853 .stdout
854 .take()
855 .unwrap();
856
857 let transport = LspTransport::new(mock_stdin, mock_stdout);
858 let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
859 let (_, mock_notification_rx) = mpsc::channel(1);
860
861 let server = LspServer {
862 client,
863 capabilities: lsp_types::ServerCapabilities::default(),
864 position_encoding: PositionEncodingKind::UTF8,
865 notification_rx: mock_notification_rx,
866 _child: mock_child,
867 };
868
869 result.add_server("rust".to_string(), server);
870
871 result.add_failure(ServerSpawnFailure {
872 language_id: "python".to_string(),
873 command: "pyright".to_string(),
874 message: "not found".to_string(),
875 });
876
877 assert!(result.has_servers());
878 assert!(!result.all_failed());
879 assert!(result.partial_success());
880 assert_eq!(result.server_count(), 1);
881 assert_eq!(result.failure_count(), 1);
882 }
883
884 #[tokio::test]
885 async fn test_server_init_result_multiple_servers() {
886 let mut result = ServerInitResult::new();
887
888 for i in 0..3 {
889 let mock_child = tokio::process::Command::new("echo")
890 .stdin(Stdio::piped())
891 .stdout(Stdio::piped())
892 .kill_on_drop(true)
893 .spawn()
894 .unwrap();
895
896 let mock_stdin = tokio::process::Command::new("cat")
897 .stdin(Stdio::piped())
898 .spawn()
899 .unwrap()
900 .stdin
901 .take()
902 .unwrap();
903
904 let mock_stdout = tokio::process::Command::new("echo")
905 .stdout(Stdio::piped())
906 .spawn()
907 .unwrap()
908 .stdout
909 .take()
910 .unwrap();
911
912 let transport = LspTransport::new(mock_stdin, mock_stdout);
913 let config = if i == 0 {
914 LspServerConfig::rust_analyzer()
915 } else if i == 1 {
916 LspServerConfig::pyright()
917 } else {
918 LspServerConfig::typescript()
919 };
920 let client = LspClient::from_transport(config.clone(), transport);
921 let (_, mock_notification_rx) = mpsc::channel(1);
922
923 let server = LspServer {
924 client,
925 capabilities: lsp_types::ServerCapabilities::default(),
926 position_encoding: PositionEncodingKind::UTF8,
927 notification_rx: mock_notification_rx,
928 _child: mock_child,
929 };
930
931 result.add_server(config.language_id, server);
932 }
933
934 assert!(result.has_servers());
935 assert!(!result.all_failed());
936 assert!(!result.partial_success());
937 assert_eq!(result.server_count(), 3);
938 assert_eq!(result.failure_count(), 0);
939 }
940
941 #[tokio::test]
942 async fn test_server_init_result_replace_server() {
943 let mut result = ServerInitResult::new();
944
945 let mock_child1 = tokio::process::Command::new("echo")
946 .stdin(Stdio::piped())
947 .stdout(Stdio::piped())
948 .kill_on_drop(true)
949 .spawn()
950 .unwrap();
951
952 let mock_stdin1 = tokio::process::Command::new("cat")
953 .stdin(Stdio::piped())
954 .spawn()
955 .unwrap()
956 .stdin
957 .take()
958 .unwrap();
959
960 let mock_stdout1 = tokio::process::Command::new("echo")
961 .stdout(Stdio::piped())
962 .spawn()
963 .unwrap()
964 .stdout
965 .take()
966 .unwrap();
967
968 let transport1 = LspTransport::new(mock_stdin1, mock_stdout1);
969 let client1 = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport1);
970 let (_, mock_notification_rx1) = mpsc::channel(1);
971
972 let server1 = LspServer {
973 client: client1,
974 capabilities: lsp_types::ServerCapabilities::default(),
975 position_encoding: PositionEncodingKind::UTF8,
976 notification_rx: mock_notification_rx1,
977 _child: mock_child1,
978 };
979
980 result.add_server("rust".to_string(), server1);
981 assert_eq!(result.server_count(), 1);
982
983 let mock_child2 = tokio::process::Command::new("echo")
984 .stdin(Stdio::piped())
985 .stdout(Stdio::piped())
986 .kill_on_drop(true)
987 .spawn()
988 .unwrap();
989
990 let mock_stdin2 = tokio::process::Command::new("cat")
991 .stdin(Stdio::piped())
992 .spawn()
993 .unwrap()
994 .stdin
995 .take()
996 .unwrap();
997
998 let mock_stdout2 = tokio::process::Command::new("echo")
999 .stdout(Stdio::piped())
1000 .spawn()
1001 .unwrap()
1002 .stdout
1003 .take()
1004 .unwrap();
1005
1006 let transport2 = LspTransport::new(mock_stdin2, mock_stdout2);
1007 let client2 = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport2);
1008 let (_, mock_notification_rx2) = mpsc::channel(1);
1009
1010 let server2 = LspServer {
1011 client: client2,
1012 capabilities: lsp_types::ServerCapabilities::default(),
1013 position_encoding: PositionEncodingKind::UTF16,
1014 notification_rx: mock_notification_rx2,
1015 _child: mock_child2,
1016 };
1017
1018 result.add_server("rust".to_string(), server2);
1019 assert_eq!(result.server_count(), 1);
1020 }
1021
1022 #[test]
1023 fn test_server_init_result_debug() {
1024 let mut result = ServerInitResult::new();
1025
1026 result.add_failure(ServerSpawnFailure {
1027 language_id: "rust".to_string(),
1028 command: "rust-analyzer".to_string(),
1029 message: "not found".to_string(),
1030 });
1031
1032 let debug_str = format!("{result:?}");
1033 assert!(debug_str.contains("ServerInitResult"));
1034 }
1035
1036 #[test]
1037 fn test_server_init_result_multiple_failures() {
1038 let mut result = ServerInitResult::new();
1039
1040 result.add_failure(ServerSpawnFailure {
1041 language_id: "python".to_string(),
1042 command: "pyright".to_string(),
1043 message: "not found".to_string(),
1044 });
1045
1046 result.add_failure(ServerSpawnFailure {
1047 language_id: "typescript".to_string(),
1048 command: "tsserver".to_string(),
1049 message: "command not found".to_string(),
1050 });
1051
1052 assert_eq!(result.failure_count(), 2);
1053 assert_eq!(result.server_count(), 0);
1054 assert!(result.all_failed());
1055 assert!(!result.partial_success());
1056 }
1057
1058 #[tokio::test]
1059 async fn test_spawn_batch_empty_configs() {
1060 let configs: &[ServerInitConfig] = &[];
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(), 0);
1068 }
1069
1070 #[tokio::test]
1071 async fn test_spawn_batch_single_invalid_config() {
1072 let configs = vec![ServerInitConfig {
1073 server_config: LspServerConfig {
1074 language_id: "rust".to_string(),
1075 command: "nonexistent-command-12345".to_string(),
1076 args: vec![],
1077 env: std::collections::HashMap::new(),
1078 file_patterns: vec!["**/*.rs".to_string()],
1079 initialization_options: None,
1080 timeout_seconds: 10,
1081 heuristics: None,
1082 },
1083 workspace_roots: vec![],
1084 initialization_options: None,
1085 notification_tx: None,
1086 }];
1087
1088 let result = LspServer::spawn_batch(&configs).await;
1089
1090 assert!(!result.has_servers());
1091 assert!(result.all_failed());
1092 assert!(!result.partial_success());
1093 assert_eq!(result.server_count(), 0);
1094 assert_eq!(result.failure_count(), 1);
1095
1096 let failure = &result.failures[0];
1097 assert_eq!(failure.language_id, "rust");
1098 assert_eq!(failure.command, "nonexistent-command-12345");
1099 assert!(failure.message.contains("spawn"));
1100 }
1101
1102 #[tokio::test]
1103 async fn test_spawn_batch_all_invalid_configs() {
1104 let configs = vec![
1105 ServerInitConfig {
1106 server_config: LspServerConfig {
1107 language_id: "rust".to_string(),
1108 command: "nonexistent-rust-analyzer".to_string(),
1109 args: vec![],
1110 env: std::collections::HashMap::new(),
1111 file_patterns: vec!["**/*.rs".to_string()],
1112 initialization_options: None,
1113 timeout_seconds: 10,
1114 heuristics: None,
1115 },
1116 workspace_roots: vec![],
1117 initialization_options: None,
1118 notification_tx: None,
1119 },
1120 ServerInitConfig {
1121 server_config: LspServerConfig {
1122 language_id: "python".to_string(),
1123 command: "nonexistent-pyright".to_string(),
1124 args: vec![],
1125 env: std::collections::HashMap::new(),
1126 file_patterns: vec!["**/*.py".to_string()],
1127 initialization_options: None,
1128 timeout_seconds: 10,
1129 heuristics: None,
1130 },
1131 workspace_roots: vec![],
1132 initialization_options: None,
1133 notification_tx: None,
1134 },
1135 ServerInitConfig {
1136 server_config: LspServerConfig {
1137 language_id: "typescript".to_string(),
1138 command: "nonexistent-tsserver".to_string(),
1139 args: vec![],
1140 env: std::collections::HashMap::new(),
1141 file_patterns: vec!["**/*.ts".to_string()],
1142 initialization_options: None,
1143 timeout_seconds: 10,
1144 heuristics: None,
1145 },
1146 workspace_roots: vec![],
1147 initialization_options: None,
1148 notification_tx: None,
1149 },
1150 ];
1151
1152 let result = LspServer::spawn_batch(&configs).await;
1153
1154 assert!(!result.has_servers());
1155 assert!(result.all_failed());
1156 assert!(!result.partial_success());
1157 assert_eq!(result.server_count(), 0);
1158 assert_eq!(result.failure_count(), 3);
1159
1160 let failure_languages: Vec<_> = result
1161 .failures
1162 .iter()
1163 .map(|f| f.language_id.as_str())
1164 .collect();
1165 assert!(failure_languages.contains(&"rust"));
1166 assert!(failure_languages.contains(&"python"));
1167 assert!(failure_languages.contains(&"typescript"));
1168 }
1169
1170 #[tokio::test]
1171 async fn test_spawn_batch_multiple_invalid_configs_ordering() {
1172 let configs = vec![
1173 ServerInitConfig {
1174 server_config: LspServerConfig {
1175 language_id: "lang1".to_string(),
1176 command: "cmd1-nonexistent".to_string(),
1177 args: vec![],
1178 env: std::collections::HashMap::new(),
1179 file_patterns: vec![],
1180 initialization_options: None,
1181 timeout_seconds: 10,
1182 heuristics: None,
1183 },
1184 workspace_roots: vec![],
1185 initialization_options: None,
1186 notification_tx: None,
1187 },
1188 ServerInitConfig {
1189 server_config: LspServerConfig {
1190 language_id: "lang2".to_string(),
1191 command: "cmd2-nonexistent".to_string(),
1192 args: vec![],
1193 env: std::collections::HashMap::new(),
1194 file_patterns: vec![],
1195 initialization_options: None,
1196 timeout_seconds: 10,
1197 heuristics: None,
1198 },
1199 workspace_roots: vec![],
1200 initialization_options: None,
1201 notification_tx: None,
1202 },
1203 ];
1204
1205 let result = LspServer::spawn_batch(&configs).await;
1206
1207 assert_eq!(result.failure_count(), 2);
1208
1209 assert_eq!(result.failures[0].language_id, "lang1");
1210 assert_eq!(result.failures[0].command, "cmd1-nonexistent");
1211
1212 assert_eq!(result.failures[1].language_id, "lang2");
1213 assert_eq!(result.failures[1].command, "cmd2-nonexistent");
1214 }
1215
1216 #[tokio::test]
1217 async fn test_spawn_batch_logs_each_failure() {
1218 let configs = vec![
1219 ServerInitConfig {
1220 server_config: LspServerConfig {
1221 language_id: "test1".to_string(),
1222 command: "nonexistent-test1".to_string(),
1223 args: vec![],
1224 env: std::collections::HashMap::new(),
1225 file_patterns: vec![],
1226 initialization_options: None,
1227 timeout_seconds: 10,
1228 heuristics: None,
1229 },
1230 workspace_roots: vec![],
1231 initialization_options: None,
1232 notification_tx: None,
1233 },
1234 ServerInitConfig {
1235 server_config: LspServerConfig {
1236 language_id: "test2".to_string(),
1237 command: "nonexistent-test2".to_string(),
1238 args: vec![],
1239 env: std::collections::HashMap::new(),
1240 file_patterns: vec![],
1241 initialization_options: None,
1242 timeout_seconds: 10,
1243 heuristics: None,
1244 },
1245 workspace_roots: vec![],
1246 initialization_options: None,
1247 notification_tx: None,
1248 },
1249 ];
1250
1251 let result = LspServer::spawn_batch(&configs).await;
1252
1253 assert_eq!(result.failure_count(), 2);
1254 assert_eq!(result.failures[0].language_id, "test1");
1255 assert_eq!(result.failures[1].language_id, "test2");
1256 }
1257}