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, ServerId};
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<ServerId, 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, id: impl Into<ServerId>, server: LspServer) {
159 self.servers.insert(id.into(), 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 server_id = config.server_config.id();
514 let language_id = config.server_config.language_id.clone();
515 let command = config.server_config.command.clone();
516
517 match Self::spawn(config.clone()).await {
518 Ok(server) => {
519 info!(
520 "Successfully spawned LSP server: {} ({})",
521 server_id, command
522 );
523 result.add_server(server_id, server);
524 }
525 Err(e) => {
526 tracing::error!(
527 "Failed to spawn LSP server: {} ({}): {}",
528 server_id,
529 command,
530 e
531 );
532 result.add_failure(ServerSpawnFailure {
533 server_id,
534 language_id,
535 command,
536 message: e.to_string(),
537 });
538 }
539 }
540 }
541
542 result
543 }
544}
545
546#[cfg(test)]
547#[allow(clippy::unwrap_used)]
548mod tests {
549 use super::*;
550
551 #[test]
552 fn test_server_state_ready() {
553 assert!(ServerState::Ready.is_ready());
554 assert!(ServerState::Ready.can_accept_requests());
555 }
556
557 #[test]
558 fn test_server_state_uninitialized() {
559 assert!(!ServerState::Uninitialized.is_ready());
560 assert!(!ServerState::Uninitialized.can_accept_requests());
561 }
562
563 #[test]
564 fn test_server_state_initializing() {
565 assert!(!ServerState::Initializing.is_ready());
566 assert!(!ServerState::Initializing.can_accept_requests());
567 }
568
569 #[test]
570 fn test_server_state_shutting_down() {
571 assert!(!ServerState::ShuttingDown.is_ready());
572 assert!(!ServerState::ShuttingDown.can_accept_requests());
573 }
574
575 #[test]
576 fn test_server_state_shutdown() {
577 assert!(!ServerState::Shutdown.is_ready());
578 assert!(!ServerState::Shutdown.can_accept_requests());
579 }
580
581 #[test]
582 fn test_server_state_equality() {
583 assert_eq!(ServerState::Ready, ServerState::Ready);
584 assert_ne!(ServerState::Ready, ServerState::Uninitialized);
585 assert_eq!(ServerState::Shutdown, ServerState::Shutdown);
586 }
587
588 #[test]
589 fn test_server_state_clone() {
590 let state = ServerState::Ready;
591 let cloned = state;
592 assert_eq!(state, cloned);
593 }
594
595 #[test]
596 fn test_server_state_debug() {
597 let state = ServerState::Ready;
598 let debug_str = format!("{state:?}");
599 assert!(debug_str.contains("Ready"));
600 }
601
602 #[test]
603 fn test_server_init_config_clone() {
604 let config = ServerInitConfig {
605 server_config: LspServerConfig::rust_analyzer(),
606 workspace_roots: vec![PathBuf::from("/tmp/workspace")],
607 initialization_options: Some(serde_json::json!({"key": "value"})),
608 notification_tx: None,
609 };
610
611 #[allow(clippy::redundant_clone)]
612 let cloned = config.clone();
613 assert_eq!(cloned.server_config.language_id, "rust");
614 assert_eq!(cloned.workspace_roots.len(), 1);
615 }
616
617 #[test]
618 fn test_server_init_config_debug() {
619 let config = ServerInitConfig {
620 server_config: LspServerConfig::pyright(),
621 workspace_roots: vec![],
622 initialization_options: None,
623 notification_tx: None,
624 };
625
626 let debug_str = format!("{config:?}");
627 assert!(debug_str.contains("python"));
628 assert!(debug_str.contains("pyright"));
629 }
630
631 #[test]
632 fn test_server_init_config_with_options() {
633 use std::collections::HashMap;
634
635 let init_opts = serde_json::json!({
636 "settings": {
637 "python": {
638 "analysis": {
639 "typeCheckingMode": "strict"
640 }
641 }
642 }
643 });
644
645 let mut env = HashMap::new();
646 env.insert("PYTHONPATH".to_string(), "/usr/lib".to_string());
647
648 let config = ServerInitConfig {
649 server_config: LspServerConfig {
650 language_id: "python".to_string(),
651 command: "pyright-langserver".to_string(),
652 args: vec!["--stdio".to_string()],
653 env,
654 file_patterns: vec!["**/*.py".to_string()],
655 initialization_options: Some(init_opts.clone()),
656 timeout_seconds: 10,
657 heuristics: None,
658 name: None,
659 handles: None,
660 },
661 workspace_roots: vec![PathBuf::from("/workspace")],
662 initialization_options: Some(init_opts),
663 notification_tx: None,
664 };
665
666 assert!(config.initialization_options.is_some());
667 assert_eq!(config.workspace_roots.len(), 1);
668 }
669
670 #[test]
671 fn test_server_init_config_empty_workspace() {
672 let config = ServerInitConfig {
673 server_config: LspServerConfig::typescript(),
674 workspace_roots: vec![],
675 initialization_options: None,
676 notification_tx: None,
677 };
678
679 assert!(config.workspace_roots.is_empty());
680 }
681
682 #[test]
683 fn test_server_init_config_multiple_workspaces() {
684 let config = ServerInitConfig {
685 server_config: LspServerConfig::rust_analyzer(),
686 workspace_roots: vec![
687 PathBuf::from("/workspace1"),
688 PathBuf::from("/workspace2"),
689 PathBuf::from("/workspace3"),
690 ],
691 initialization_options: None,
692 notification_tx: None,
693 };
694
695 assert_eq!(config.workspace_roots.len(), 3);
696 }
697
698 #[tokio::test]
699 async fn test_lsp_server_getters() {
700 use lsp_types::ServerCapabilities;
701
702 let mock_child = tokio::process::Command::new("echo")
703 .stdin(Stdio::piped())
704 .stdout(Stdio::piped())
705 .kill_on_drop(true)
706 .spawn()
707 .unwrap();
708
709 let mock_stdin = tokio::process::Command::new("cat")
710 .stdin(Stdio::piped())
711 .spawn()
712 .unwrap()
713 .stdin
714 .take()
715 .unwrap();
716
717 let mock_stdout = tokio::process::Command::new("echo")
718 .stdout(Stdio::piped())
719 .spawn()
720 .unwrap()
721 .stdout
722 .take()
723 .unwrap();
724
725 let transport = LspTransport::new(mock_stdin, mock_stdout);
726 let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
727 let (_, mock_notification_rx) = mpsc::channel(1);
728
729 let server = LspServer {
730 client,
731 capabilities: ServerCapabilities::default(),
732 position_encoding: PositionEncodingKind::UTF8,
733 notification_rx: mock_notification_rx,
734 _child: mock_child,
735 };
736
737 assert_eq!(server.position_encoding(), PositionEncodingKind::UTF8);
738 assert!(server.capabilities().text_document_sync.is_none());
739
740 let debug_str = format!("{server:?}");
741 assert!(debug_str.contains("LspServer"));
742 assert!(debug_str.contains("<process>"));
743 }
744
745 #[test]
746 fn test_server_init_result_new_empty() {
747 let result = ServerInitResult::new();
748 assert!(!result.has_servers());
749 assert!(!result.all_failed());
750 assert!(!result.partial_success());
751 assert_eq!(result.server_count(), 0);
752 assert_eq!(result.failure_count(), 0);
753 }
754
755 #[test]
756 fn test_server_init_result_default() {
757 let result = ServerInitResult::default();
758 assert!(!result.has_servers());
759 assert_eq!(result.server_count(), 0);
760 assert_eq!(result.failure_count(), 0);
761 }
762
763 #[test]
764 fn test_server_init_result_all_failures() {
765 let mut result = ServerInitResult::new();
766
767 result.add_failure(ServerSpawnFailure {
768 server_id: ServerId::from("rust"),
769 language_id: "rust".to_string(),
770 command: "rust-analyzer".to_string(),
771 message: "not found".to_string(),
772 });
773
774 result.add_failure(ServerSpawnFailure {
775 server_id: ServerId::from("python"),
776 language_id: "python".to_string(),
777 command: "pyright".to_string(),
778 message: "permission denied".to_string(),
779 });
780
781 assert!(!result.has_servers());
782 assert!(result.all_failed());
783 assert!(!result.partial_success());
784 assert_eq!(result.server_count(), 0);
785 assert_eq!(result.failure_count(), 2);
786 }
787
788 #[tokio::test]
789 async fn test_server_init_result_all_success() {
790 let mut result = ServerInitResult::new();
791
792 let mock_child1 = tokio::process::Command::new("echo")
793 .stdin(Stdio::piped())
794 .stdout(Stdio::piped())
795 .kill_on_drop(true)
796 .spawn()
797 .unwrap();
798
799 let mock_stdin1 = tokio::process::Command::new("cat")
800 .stdin(Stdio::piped())
801 .spawn()
802 .unwrap()
803 .stdin
804 .take()
805 .unwrap();
806
807 let mock_stdout1 = tokio::process::Command::new("echo")
808 .stdout(Stdio::piped())
809 .spawn()
810 .unwrap()
811 .stdout
812 .take()
813 .unwrap();
814
815 let transport1 = LspTransport::new(mock_stdin1, mock_stdout1);
816 let client1 = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport1);
817 let (_, mock_notification_rx1) = mpsc::channel(1);
818
819 let server1 = LspServer {
820 client: client1,
821 capabilities: lsp_types::ServerCapabilities::default(),
822 position_encoding: PositionEncodingKind::UTF8,
823 notification_rx: mock_notification_rx1,
824 _child: mock_child1,
825 };
826
827 result.add_server("rust".to_string(), server1);
828
829 assert!(result.has_servers());
830 assert!(!result.all_failed());
831 assert!(!result.partial_success());
832 assert_eq!(result.server_count(), 1);
833 assert_eq!(result.failure_count(), 0);
834 }
835
836 #[tokio::test]
837 async fn test_server_init_result_partial_success() {
838 let mut result = ServerInitResult::new();
839
840 let mock_child = tokio::process::Command::new("echo")
841 .stdin(Stdio::piped())
842 .stdout(Stdio::piped())
843 .kill_on_drop(true)
844 .spawn()
845 .unwrap();
846
847 let mock_stdin = tokio::process::Command::new("cat")
848 .stdin(Stdio::piped())
849 .spawn()
850 .unwrap()
851 .stdin
852 .take()
853 .unwrap();
854
855 let mock_stdout = tokio::process::Command::new("echo")
856 .stdout(Stdio::piped())
857 .spawn()
858 .unwrap()
859 .stdout
860 .take()
861 .unwrap();
862
863 let transport = LspTransport::new(mock_stdin, mock_stdout);
864 let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
865 let (_, mock_notification_rx) = mpsc::channel(1);
866
867 let server = LspServer {
868 client,
869 capabilities: lsp_types::ServerCapabilities::default(),
870 position_encoding: PositionEncodingKind::UTF8,
871 notification_rx: mock_notification_rx,
872 _child: mock_child,
873 };
874
875 result.add_server("rust".to_string(), server);
876
877 result.add_failure(ServerSpawnFailure {
878 server_id: ServerId::from("python"),
879 language_id: "python".to_string(),
880 command: "pyright".to_string(),
881 message: "not found".to_string(),
882 });
883
884 assert!(result.has_servers());
885 assert!(!result.all_failed());
886 assert!(result.partial_success());
887 assert_eq!(result.server_count(), 1);
888 assert_eq!(result.failure_count(), 1);
889 }
890
891 #[tokio::test]
892 async fn test_server_init_result_multiple_servers() {
893 let mut result = ServerInitResult::new();
894
895 for i in 0..3 {
896 let mock_child = tokio::process::Command::new("echo")
897 .stdin(Stdio::piped())
898 .stdout(Stdio::piped())
899 .kill_on_drop(true)
900 .spawn()
901 .unwrap();
902
903 let mock_stdin = tokio::process::Command::new("cat")
904 .stdin(Stdio::piped())
905 .spawn()
906 .unwrap()
907 .stdin
908 .take()
909 .unwrap();
910
911 let mock_stdout = tokio::process::Command::new("echo")
912 .stdout(Stdio::piped())
913 .spawn()
914 .unwrap()
915 .stdout
916 .take()
917 .unwrap();
918
919 let transport = LspTransport::new(mock_stdin, mock_stdout);
920 let config = if i == 0 {
921 LspServerConfig::rust_analyzer()
922 } else if i == 1 {
923 LspServerConfig::pyright()
924 } else {
925 LspServerConfig::typescript()
926 };
927 let client = LspClient::from_transport(config.clone(), transport);
928 let (_, mock_notification_rx) = mpsc::channel(1);
929
930 let server = LspServer {
931 client,
932 capabilities: lsp_types::ServerCapabilities::default(),
933 position_encoding: PositionEncodingKind::UTF8,
934 notification_rx: mock_notification_rx,
935 _child: mock_child,
936 };
937
938 result.add_server(config.language_id, server);
939 }
940
941 assert!(result.has_servers());
942 assert!(!result.all_failed());
943 assert!(!result.partial_success());
944 assert_eq!(result.server_count(), 3);
945 assert_eq!(result.failure_count(), 0);
946 }
947
948 #[tokio::test]
949 async fn test_server_init_result_replace_server() {
950 let mut result = ServerInitResult::new();
951
952 let mock_child1 = tokio::process::Command::new("echo")
953 .stdin(Stdio::piped())
954 .stdout(Stdio::piped())
955 .kill_on_drop(true)
956 .spawn()
957 .unwrap();
958
959 let mock_stdin1 = tokio::process::Command::new("cat")
960 .stdin(Stdio::piped())
961 .spawn()
962 .unwrap()
963 .stdin
964 .take()
965 .unwrap();
966
967 let mock_stdout1 = tokio::process::Command::new("echo")
968 .stdout(Stdio::piped())
969 .spawn()
970 .unwrap()
971 .stdout
972 .take()
973 .unwrap();
974
975 let transport1 = LspTransport::new(mock_stdin1, mock_stdout1);
976 let client1 = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport1);
977 let (_, mock_notification_rx1) = mpsc::channel(1);
978
979 let server1 = LspServer {
980 client: client1,
981 capabilities: lsp_types::ServerCapabilities::default(),
982 position_encoding: PositionEncodingKind::UTF8,
983 notification_rx: mock_notification_rx1,
984 _child: mock_child1,
985 };
986
987 result.add_server("rust".to_string(), server1);
988 assert_eq!(result.server_count(), 1);
989
990 let mock_child2 = tokio::process::Command::new("echo")
991 .stdin(Stdio::piped())
992 .stdout(Stdio::piped())
993 .kill_on_drop(true)
994 .spawn()
995 .unwrap();
996
997 let mock_stdin2 = tokio::process::Command::new("cat")
998 .stdin(Stdio::piped())
999 .spawn()
1000 .unwrap()
1001 .stdin
1002 .take()
1003 .unwrap();
1004
1005 let mock_stdout2 = tokio::process::Command::new("echo")
1006 .stdout(Stdio::piped())
1007 .spawn()
1008 .unwrap()
1009 .stdout
1010 .take()
1011 .unwrap();
1012
1013 let transport2 = LspTransport::new(mock_stdin2, mock_stdout2);
1014 let client2 = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport2);
1015 let (_, mock_notification_rx2) = mpsc::channel(1);
1016
1017 let server2 = LspServer {
1018 client: client2,
1019 capabilities: lsp_types::ServerCapabilities::default(),
1020 position_encoding: PositionEncodingKind::UTF16,
1021 notification_rx: mock_notification_rx2,
1022 _child: mock_child2,
1023 };
1024
1025 result.add_server("rust".to_string(), server2);
1026 assert_eq!(result.server_count(), 1);
1027 }
1028
1029 #[test]
1030 fn test_server_init_result_debug() {
1031 let mut result = ServerInitResult::new();
1032
1033 result.add_failure(ServerSpawnFailure {
1034 server_id: ServerId::from("rust"),
1035 language_id: "rust".to_string(),
1036 command: "rust-analyzer".to_string(),
1037 message: "not found".to_string(),
1038 });
1039
1040 let debug_str = format!("{result:?}");
1041 assert!(debug_str.contains("ServerInitResult"));
1042 }
1043
1044 #[test]
1045 fn test_server_init_result_multiple_failures() {
1046 let mut result = ServerInitResult::new();
1047
1048 result.add_failure(ServerSpawnFailure {
1049 server_id: ServerId::from("python"),
1050 language_id: "python".to_string(),
1051 command: "pyright".to_string(),
1052 message: "not found".to_string(),
1053 });
1054
1055 result.add_failure(ServerSpawnFailure {
1056 server_id: ServerId::from("typescript"),
1057 language_id: "typescript".to_string(),
1058 command: "tsserver".to_string(),
1059 message: "command not found".to_string(),
1060 });
1061
1062 assert_eq!(result.failure_count(), 2);
1063 assert_eq!(result.server_count(), 0);
1064 assert!(result.all_failed());
1065 assert!(!result.partial_success());
1066 }
1067
1068 #[tokio::test]
1069 async fn test_spawn_batch_empty_configs() {
1070 let configs: &[ServerInitConfig] = &[];
1071 let result = LspServer::spawn_batch(configs).await;
1072
1073 assert!(!result.has_servers());
1074 assert!(!result.all_failed());
1075 assert!(!result.partial_success());
1076 assert_eq!(result.server_count(), 0);
1077 assert_eq!(result.failure_count(), 0);
1078 }
1079
1080 #[tokio::test]
1081 async fn test_spawn_batch_single_invalid_config() {
1082 let configs = vec![ServerInitConfig {
1083 server_config: LspServerConfig {
1084 language_id: "rust".to_string(),
1085 command: "nonexistent-command-12345".to_string(),
1086 args: vec![],
1087 env: std::collections::HashMap::new(),
1088 file_patterns: vec!["**/*.rs".to_string()],
1089 initialization_options: None,
1090 timeout_seconds: 10,
1091 heuristics: None,
1092 name: None,
1093 handles: None,
1094 },
1095 workspace_roots: vec![],
1096 initialization_options: None,
1097 notification_tx: None,
1098 }];
1099
1100 let result = LspServer::spawn_batch(&configs).await;
1101
1102 assert!(!result.has_servers());
1103 assert!(result.all_failed());
1104 assert!(!result.partial_success());
1105 assert_eq!(result.server_count(), 0);
1106 assert_eq!(result.failure_count(), 1);
1107
1108 let failure = &result.failures[0];
1109 assert_eq!(failure.language_id, "rust");
1110 assert_eq!(failure.command, "nonexistent-command-12345");
1111 assert!(failure.message.contains("spawn"));
1112 }
1113
1114 #[tokio::test]
1115 async fn test_spawn_batch_all_invalid_configs() {
1116 let configs = vec![
1117 ServerInitConfig {
1118 server_config: LspServerConfig {
1119 language_id: "rust".to_string(),
1120 command: "nonexistent-rust-analyzer".to_string(),
1121 args: vec![],
1122 env: std::collections::HashMap::new(),
1123 file_patterns: vec!["**/*.rs".to_string()],
1124 initialization_options: None,
1125 timeout_seconds: 10,
1126 heuristics: None,
1127 name: None,
1128 handles: None,
1129 },
1130 workspace_roots: vec![],
1131 initialization_options: None,
1132 notification_tx: None,
1133 },
1134 ServerInitConfig {
1135 server_config: LspServerConfig {
1136 language_id: "python".to_string(),
1137 command: "nonexistent-pyright".to_string(),
1138 args: vec![],
1139 env: std::collections::HashMap::new(),
1140 file_patterns: vec!["**/*.py".to_string()],
1141 initialization_options: None,
1142 timeout_seconds: 10,
1143 heuristics: None,
1144 name: None,
1145 handles: None,
1146 },
1147 workspace_roots: vec![],
1148 initialization_options: None,
1149 notification_tx: None,
1150 },
1151 ServerInitConfig {
1152 server_config: LspServerConfig {
1153 language_id: "typescript".to_string(),
1154 command: "nonexistent-tsserver".to_string(),
1155 args: vec![],
1156 env: std::collections::HashMap::new(),
1157 file_patterns: vec!["**/*.ts".to_string()],
1158 initialization_options: None,
1159 timeout_seconds: 10,
1160 heuristics: None,
1161 name: None,
1162 handles: None,
1163 },
1164 workspace_roots: vec![],
1165 initialization_options: None,
1166 notification_tx: None,
1167 },
1168 ];
1169
1170 let result = LspServer::spawn_batch(&configs).await;
1171
1172 assert!(!result.has_servers());
1173 assert!(result.all_failed());
1174 assert!(!result.partial_success());
1175 assert_eq!(result.server_count(), 0);
1176 assert_eq!(result.failure_count(), 3);
1177
1178 let failure_languages: Vec<_> = result
1179 .failures
1180 .iter()
1181 .map(|f| f.language_id.as_str())
1182 .collect();
1183 assert!(failure_languages.contains(&"rust"));
1184 assert!(failure_languages.contains(&"python"));
1185 assert!(failure_languages.contains(&"typescript"));
1186 }
1187
1188 #[tokio::test]
1189 async fn test_spawn_batch_multiple_invalid_configs_ordering() {
1190 let configs = vec![
1191 ServerInitConfig {
1192 server_config: LspServerConfig {
1193 language_id: "lang1".to_string(),
1194 command: "cmd1-nonexistent".to_string(),
1195 args: vec![],
1196 env: std::collections::HashMap::new(),
1197 file_patterns: vec![],
1198 initialization_options: None,
1199 timeout_seconds: 10,
1200 heuristics: None,
1201 name: None,
1202 handles: None,
1203 },
1204 workspace_roots: vec![],
1205 initialization_options: None,
1206 notification_tx: None,
1207 },
1208 ServerInitConfig {
1209 server_config: LspServerConfig {
1210 language_id: "lang2".to_string(),
1211 command: "cmd2-nonexistent".to_string(),
1212 args: vec![],
1213 env: std::collections::HashMap::new(),
1214 file_patterns: vec![],
1215 initialization_options: None,
1216 timeout_seconds: 10,
1217 heuristics: None,
1218 name: None,
1219 handles: None,
1220 },
1221 workspace_roots: vec![],
1222 initialization_options: None,
1223 notification_tx: None,
1224 },
1225 ];
1226
1227 let result = LspServer::spawn_batch(&configs).await;
1228
1229 assert_eq!(result.failure_count(), 2);
1230
1231 assert_eq!(result.failures[0].language_id, "lang1");
1232 assert_eq!(result.failures[0].command, "cmd1-nonexistent");
1233
1234 assert_eq!(result.failures[1].language_id, "lang2");
1235 assert_eq!(result.failures[1].command, "cmd2-nonexistent");
1236 }
1237
1238 #[tokio::test]
1239 async fn test_spawn_batch_logs_each_failure() {
1240 let configs = vec![
1241 ServerInitConfig {
1242 server_config: LspServerConfig {
1243 language_id: "test1".to_string(),
1244 command: "nonexistent-test1".to_string(),
1245 args: vec![],
1246 env: std::collections::HashMap::new(),
1247 file_patterns: vec![],
1248 initialization_options: None,
1249 timeout_seconds: 10,
1250 heuristics: None,
1251 name: None,
1252 handles: None,
1253 },
1254 workspace_roots: vec![],
1255 initialization_options: None,
1256 notification_tx: None,
1257 },
1258 ServerInitConfig {
1259 server_config: LspServerConfig {
1260 language_id: "test2".to_string(),
1261 command: "nonexistent-test2".to_string(),
1262 args: vec![],
1263 env: std::collections::HashMap::new(),
1264 file_patterns: vec![],
1265 initialization_options: None,
1266 timeout_seconds: 10,
1267 heuristics: None,
1268 name: None,
1269 handles: None,
1270 },
1271 workspace_roots: vec![],
1272 initialization_options: None,
1273 notification_tx: None,
1274 },
1275 ];
1276
1277 let result = LspServer::spawn_batch(&configs).await;
1278
1279 assert_eq!(result.failure_count(), 2);
1280 assert_eq!(result.failures[0].language_id, "test1");
1281 assert_eq!(result.failures[1].language_id, "test2");
1282 }
1283
1284 fn fake_lsp_server() -> LspServer {
1289 let mock_child = tokio::process::Command::new("echo")
1290 .stdin(Stdio::piped())
1291 .stdout(Stdio::piped())
1292 .kill_on_drop(true)
1293 .spawn()
1294 .unwrap();
1295 let mock_stdin = tokio::process::Command::new("cat")
1296 .stdin(Stdio::piped())
1297 .spawn()
1298 .unwrap()
1299 .stdin
1300 .take()
1301 .unwrap();
1302 let mock_stdout = tokio::process::Command::new("echo")
1303 .stdout(Stdio::piped())
1304 .spawn()
1305 .unwrap()
1306 .stdout
1307 .take()
1308 .unwrap();
1309 let transport = LspTransport::new(mock_stdin, mock_stdout);
1310 let client = LspClient::from_transport(LspServerConfig::pyright(), transport);
1311 let (_, mock_notification_rx) = mpsc::channel(1);
1312 LspServer {
1313 client,
1314 capabilities: lsp_types::ServerCapabilities::default(),
1315 position_encoding: PositionEncodingKind::UTF8,
1316 notification_rx: mock_notification_rx,
1317 _child: mock_child,
1318 }
1319 }
1320
1321 #[tokio::test]
1331 async fn test_register_servers_computes_diagnostics_flags_from_rebound_router() {
1332 use crate::bridge::Translator;
1333 use crate::config::{ServerId, ToolKind, ToolRouter};
1334
1335 let pylsp_id = ServerId::from("pylsp");
1336 let configs = vec![
1337 LspServerConfig {
1338 language_id: "python".to_string(),
1339 command: "pyright-langserver".to_string(),
1340 args: vec![],
1341 env: std::collections::HashMap::new(),
1342 file_patterns: vec![],
1343 initialization_options: None,
1344 timeout_seconds: 30,
1345 heuristics: None,
1346 name: Some("pyright-diag".to_string()),
1347 handles: Some(vec![ToolKind::Diagnostics]),
1348 },
1349 LspServerConfig {
1350 language_id: "python".to_string(),
1351 command: "pylsp".to_string(),
1352 args: vec![],
1353 env: std::collections::HashMap::new(),
1354 file_patterns: vec![],
1355 initialization_options: None,
1356 timeout_seconds: 30,
1357 heuristics: None,
1358 name: Some("pylsp".to_string()),
1359 handles: None,
1360 },
1361 ];
1362 let router = ToolRouter::from_configs(&configs).unwrap();
1363 let translator = Translator::new().with_router(router);
1364
1365 let mut result = ServerInitResult::new();
1367 result.add_server(pylsp_id.clone(), fake_lsp_server());
1368
1369 let registered = crate::register_servers(result, &translator);
1370
1371 assert_eq!(
1372 registered.diagnostics_flags.get(&pylsp_id),
1373 Some(&true),
1374 "pylsp must inherit the diagnostics route once pyright-diag is \
1375 known dead, and the flag must reflect that post-rebind state"
1376 );
1377 }
1378}