Skip to main content

mcpls_core/lsp/
lifecycle.rs

1//! LSP server lifecycle management.
2//!
3//! This module handles the complete lifecycle of an LSP server:
4//! 1. Spawn server process
5//! 2. Initialize → initialized handshake
6//! 3. Capability negotiation
7//! 4. Active request handling
8//! 5. Graceful shutdown sequence
9
10use 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/// State of an LSP server connection.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum ServerState {
33    /// Server has not been initialized.
34    Uninitialized,
35    /// Server is currently initializing.
36    Initializing,
37    /// Server is ready to handle requests.
38    Ready,
39    /// Server is shutting down.
40    ShuttingDown,
41    /// Server has been shut down.
42    Shutdown,
43}
44
45impl ServerState {
46    /// Check if the server is ready to handle requests.
47    #[must_use]
48    pub const fn is_ready(&self) -> bool {
49        matches!(self, Self::Ready)
50    }
51
52    /// Check if the server can accept new requests.
53    #[must_use]
54    pub const fn can_accept_requests(&self) -> bool {
55        matches!(self, Self::Ready)
56    }
57}
58
59/// Configuration for LSP server initialization.
60#[derive(Debug, Clone)]
61pub struct ServerInitConfig {
62    /// LSP server configuration.
63    pub server_config: LspServerConfig,
64    /// Workspace root paths.
65    pub workspace_roots: Vec<PathBuf>,
66    /// Initialization options (server-specific JSON).
67    pub initialization_options: Option<serde_json::Value>,
68    /// Optional channel for forwarding LSP notifications to the notification cache.
69    ///
70    /// When `Some`, the spawned LSP client sends every notification it receives
71    /// (publishDiagnostics, logMessage, showMessage, …) through this sender.
72    /// The caller is responsible for draining the corresponding receiver and
73    /// storing entries in [`crate::bridge::NotificationCache`].
74    pub notification_tx: Option<mpsc::Sender<LspNotification>>,
75}
76
77/// Result of attempting to spawn multiple LSP servers.
78///
79/// This type enables graceful degradation by collecting both
80/// successful initializations and failures. Use the helper methods
81/// to inspect the outcome and make decisions about how to proceed.
82///
83/// # Examples
84///
85/// ```
86/// use mcpls_core::lsp::ServerInitResult;
87/// use mcpls_core::error::ServerSpawnFailure;
88///
89/// let mut result = ServerInitResult::new();
90///
91/// // Check for different scenarios
92/// if result.all_failed() {
93///     eprintln!("All servers failed to initialize");
94/// } else if result.partial_success() {
95///     println!("Some servers succeeded, some failed");
96/// } else if result.has_servers() {
97///     println!("All servers initialized successfully");
98/// }
99/// ```
100#[derive(Debug)]
101pub struct ServerInitResult {
102    /// Successfully initialized servers (`language_id` -> server).
103    pub servers: HashMap<String, LspServer>,
104    /// Failures that occurred during spawn attempts.
105    pub failures: Vec<ServerSpawnFailure>,
106}
107
108impl ServerInitResult {
109    /// Create a new empty result.
110    #[must_use]
111    pub fn new() -> Self {
112        Self {
113            servers: HashMap::new(),
114            failures: Vec::new(),
115        }
116    }
117
118    /// Check if any servers were successfully initialized.
119    ///
120    /// Returns `true` if at least one server is available for use.
121    #[must_use]
122    pub fn has_servers(&self) -> bool {
123        !self.servers.is_empty()
124    }
125
126    /// Check if all attempted servers failed.
127    ///
128    /// Returns `true` only if there were failures and no servers succeeded.
129    /// Returns `false` for empty results (no servers configured).
130    #[must_use]
131    pub fn all_failed(&self) -> bool {
132        self.servers.is_empty() && !self.failures.is_empty()
133    }
134
135    /// Check if some but not all servers failed.
136    ///
137    /// Returns `true` if there are both successful servers and failures.
138    #[must_use]
139    pub fn partial_success(&self) -> bool {
140        !self.servers.is_empty() && !self.failures.is_empty()
141    }
142
143    /// Get the number of successfully initialized servers.
144    #[must_use]
145    pub fn server_count(&self) -> usize {
146        self.servers.len()
147    }
148
149    /// Get the number of failures.
150    #[must_use]
151    pub const fn failure_count(&self) -> usize {
152        self.failures.len()
153    }
154
155    /// Add a successful server.
156    ///
157    /// If a server with the same `language_id` already exists, it will be replaced.
158    pub fn add_server(&mut self, language_id: String, server: LspServer) {
159        self.servers.insert(language_id, server);
160    }
161
162    /// Add a failure.
163    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
174/// Managed LSP server instance with capabilities and encoding.
175pub struct LspServer {
176    client: LspClient,
177    capabilities: ServerCapabilities,
178    position_encoding: PositionEncodingKind,
179    /// Receiver for push notifications from the LSP server.
180    ///
181    /// Extract this before registering the server to receive real-time
182    /// notifications (e.g., `textDocument/publishDiagnostics`, `$/progress`).
183    pub notification_rx: mpsc::Receiver<LspNotification>,
184    /// Child process handle. Kept alive for process lifetime management.
185    /// When dropped, the process is terminated via SIGKILL (`kill_on_drop`).
186    _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    /// Take the notification receiver out of this server, replacing it with a dummy channel.
203    ///
204    /// Use this to extract the receiver for a background pump task before registering
205    /// the server with the translator. After this call, the server's `notification_rx`
206    /// will never receive messages.
207    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    /// Spawn and initialize LSP server.
213    ///
214    /// This performs the complete initialization sequence:
215    /// 1. Spawns the LSP server as a child process
216    /// 2. Sends initialize request with client capabilities
217    /// 3. Receives server capabilities from initialize response
218    /// 4. Sends initialized notification
219    ///
220    /// # Errors
221    ///
222    /// Returns an error if:
223    /// - Server process fails to spawn
224    /// - Initialize request fails or times out
225    /// - Server returns error during initialization
226    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    /// Perform LSP initialization handshake.
275    ///
276    /// Sends initialize request and waits for response, then sends initialized notification.
277    #[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                    // Strip \\?\ extended-path prefix that canonicalize() adds on Windows.
294                    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                        // Declare supported action kinds so the server returns
349                        // CodeAction objects (not just legacy Command objects).
350                        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        // Use the server's configured timeout for the initialize handshake too,
386        // not a hardcoded 30s: large solutions (e.g. a 130-project Unity .sln via
387        // OmniSharp) take minutes to respond to `initialize`.
388        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    /// Get server capabilities.
421    #[must_use]
422    pub const fn capabilities(&self) -> &ServerCapabilities {
423        &self.capabilities
424    }
425
426    /// Get negotiated position encoding.
427    #[must_use]
428    pub fn position_encoding(&self) -> PositionEncodingKind {
429        self.position_encoding.clone()
430    }
431
432    /// Get client for making requests.
433    #[must_use]
434    pub const fn client(&self) -> &LspClient {
435        &self.client
436    }
437
438    /// Shutdown server gracefully.
439    ///
440    /// Sends shutdown request, waits for response, then sends exit notification.
441    ///
442    /// # Errors
443    ///
444    /// Returns an error if shutdown sequence fails.
445    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    /// Spawn multiple LSP servers in batch mode with graceful degradation.
462    ///
463    /// Attempts to spawn and initialize all configured servers. If some servers
464    /// fail to spawn, the successful servers are still returned. This enables
465    /// graceful degradation where the system can continue to operate with
466    /// partial functionality.
467    ///
468    /// # Behavior
469    ///
470    /// - Attempts to spawn each server sequentially
471    /// - Logs success (info) and failure (error) for each server
472    /// - Accumulates successful servers and failures
473    /// - Never panics or returns early - attempts all servers
474    ///
475    /// # Examples
476    ///
477    /// ```
478    /// use mcpls_core::lsp::{LspServer, ServerInitConfig};
479    /// use mcpls_core::config::LspServerConfig;
480    /// use std::path::PathBuf;
481    ///
482    /// # async fn example() {
483    /// let configs = vec![
484    ///     ServerInitConfig {
485    ///         server_config: LspServerConfig::rust_analyzer(),
486    ///         workspace_roots: vec![PathBuf::from("/workspace")],
487    ///         initialization_options: None,
488    ///         notification_tx: None,
489    ///     },
490    ///     ServerInitConfig {
491    ///         server_config: LspServerConfig::pyright(),
492    ///         workspace_roots: vec![PathBuf::from("/workspace")],
493    ///         initialization_options: None,
494    ///         notification_tx: None,
495    ///     },
496    /// ];
497    ///
498    /// let result = LspServer::spawn_batch(&configs).await;
499    ///
500    /// if result.has_servers() {
501    ///     println!("Successfully spawned {} servers", result.server_count());
502    /// }
503    ///
504    /// if result.partial_success() {
505    ///     eprintln!("Warning: {} servers failed", result.failure_count());
506    /// }
507    /// # }
508    /// ```
509    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}