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::path::PathBuf;
11use std::process::Stdio;
12use std::str::FromStr;
13
14use lsp_types::{
15    ClientCapabilities, ClientInfo, GeneralClientCapabilities, InitializeParams, InitializeResult,
16    InitializedParams, PositionEncodingKind, ServerCapabilities, Uri, WorkspaceFolder,
17};
18use tokio::process::Command;
19use tokio::time::Duration;
20use tracing::{debug, info};
21
22use crate::config::LspServerConfig;
23use crate::error::{Error, Result};
24use crate::lsp::client::LspClient;
25use crate::lsp::transport::LspTransport;
26
27/// State of an LSP server connection.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum ServerState {
30    /// Server has not been initialized.
31    Uninitialized,
32    /// Server is currently initializing.
33    Initializing,
34    /// Server is ready to handle requests.
35    Ready,
36    /// Server is shutting down.
37    ShuttingDown,
38    /// Server has been shut down.
39    Shutdown,
40}
41
42impl ServerState {
43    /// Check if the server is ready to handle requests.
44    #[must_use]
45    pub const fn is_ready(&self) -> bool {
46        matches!(self, Self::Ready)
47    }
48
49    /// Check if the server can accept new requests.
50    #[must_use]
51    pub const fn can_accept_requests(&self) -> bool {
52        matches!(self, Self::Ready)
53    }
54}
55
56/// Configuration for LSP server initialization.
57#[derive(Debug, Clone)]
58pub struct ServerInitConfig {
59    /// LSP server configuration.
60    pub server_config: LspServerConfig,
61    /// Workspace root paths.
62    pub workspace_roots: Vec<PathBuf>,
63    /// Initialization options (server-specific JSON).
64    pub initialization_options: Option<serde_json::Value>,
65}
66
67/// Managed LSP server instance with capabilities and encoding.
68pub struct LspServer {
69    client: LspClient,
70    capabilities: ServerCapabilities,
71    position_encoding: PositionEncodingKind,
72}
73
74impl LspServer {
75    /// Spawn and initialize LSP server.
76    ///
77    /// This performs the complete initialization sequence:
78    /// 1. Spawns the LSP server as a child process
79    /// 2. Sends initialize request with client capabilities
80    /// 3. Receives server capabilities from initialize response
81    /// 4. Sends initialized notification
82    ///
83    /// # Errors
84    ///
85    /// Returns an error if:
86    /// - Server process fails to spawn
87    /// - Initialize request fails or times out
88    /// - Server returns error during initialization
89    pub async fn spawn(config: ServerInitConfig) -> Result<Self> {
90        info!(
91            "Spawning LSP server: {} {:?}",
92            config.server_config.command, config.server_config.args
93        );
94
95        let mut child = Command::new(&config.server_config.command)
96            .args(&config.server_config.args)
97            .stdin(Stdio::piped())
98            .stdout(Stdio::piped())
99            .stderr(Stdio::null())
100            .kill_on_drop(true)
101            .spawn()
102            .map_err(|e| Error::ServerSpawnFailed {
103                command: config.server_config.command.clone(),
104                source: e,
105            })?;
106
107        let stdin = child
108            .stdin
109            .take()
110            .ok_or_else(|| Error::Transport("Failed to capture stdin".to_string()))?;
111        let stdout = child
112            .stdout
113            .take()
114            .ok_or_else(|| Error::Transport("Failed to capture stdout".to_string()))?;
115
116        let transport = LspTransport::new(stdin, stdout);
117        let client = LspClient::from_transport(config.server_config.clone(), transport);
118
119        let (capabilities, position_encoding) = Self::initialize(&client, &config).await?;
120
121        info!("LSP server initialized successfully");
122
123        Ok(Self {
124            client,
125            capabilities,
126            position_encoding,
127        })
128    }
129
130    /// Perform LSP initialization handshake.
131    ///
132    /// Sends initialize request and waits for response, then sends initialized notification.
133    async fn initialize(
134        client: &LspClient,
135        config: &ServerInitConfig,
136    ) -> Result<(ServerCapabilities, PositionEncodingKind)> {
137        debug!("Sending initialize request");
138
139        let workspace_folders: Vec<WorkspaceFolder> = config
140            .workspace_roots
141            .iter()
142            .map(|root| {
143                let path_str = root.to_str().ok_or_else(|| {
144                    let root_display = root.display();
145                    Error::InvalidUri(format!("Invalid UTF-8 in path: {root_display}"))
146                })?;
147                let uri_str = if cfg!(windows) {
148                    format!("file:///{}", path_str.replace('\\', "/"))
149                } else {
150                    format!("file://{path_str}")
151                };
152                let uri = Uri::from_str(&uri_str).map_err(|_| {
153                    let root_display = root.display();
154                    Error::InvalidUri(format!("Invalid workspace root: {root_display}"))
155                })?;
156                Ok(WorkspaceFolder {
157                    uri,
158                    name: root
159                        .file_name()
160                        .and_then(|n| n.to_str())
161                        .unwrap_or("workspace")
162                        .to_string(),
163                })
164            })
165            .collect::<Result<Vec<_>>>()?;
166
167        let params = InitializeParams {
168            process_id: Some(std::process::id()),
169            #[allow(deprecated)]
170            root_uri: None,
171            initialization_options: config.initialization_options.clone(),
172            capabilities: ClientCapabilities {
173                general: Some(GeneralClientCapabilities {
174                    position_encodings: Some(vec![
175                        PositionEncodingKind::UTF8,
176                        PositionEncodingKind::UTF16,
177                    ]),
178                    ..Default::default()
179                }),
180                text_document: Some(lsp_types::TextDocumentClientCapabilities {
181                    hover: Some(lsp_types::HoverClientCapabilities {
182                        dynamic_registration: Some(false),
183                        content_format: Some(vec![
184                            lsp_types::MarkupKind::Markdown,
185                            lsp_types::MarkupKind::PlainText,
186                        ]),
187                    }),
188                    definition: Some(lsp_types::GotoCapability {
189                        dynamic_registration: Some(false),
190                        link_support: Some(true),
191                    }),
192                    references: Some(lsp_types::ReferenceClientCapabilities {
193                        dynamic_registration: Some(false),
194                    }),
195                    ..Default::default()
196                }),
197                workspace: Some(lsp_types::WorkspaceClientCapabilities {
198                    workspace_folders: Some(true),
199                    ..Default::default()
200                }),
201                ..Default::default()
202            },
203            client_info: Some(ClientInfo {
204                name: "mcpls".to_string(),
205                version: Some(env!("CARGO_PKG_VERSION").to_string()),
206            }),
207            workspace_folders: Some(workspace_folders),
208            ..Default::default()
209        };
210
211        let result: InitializeResult = client
212            .request("initialize", params, Duration::from_secs(30))
213            .await
214            .map_err(|e| Error::LspInitFailed {
215                message: format!("Initialize request failed: {e}"),
216            })?;
217
218        let position_encoding = result
219            .capabilities
220            .position_encoding
221            .clone()
222            .unwrap_or(PositionEncodingKind::UTF16);
223
224        debug!(
225            "Server capabilities received, encoding: {:?}",
226            position_encoding
227        );
228
229        client
230            .notify("initialized", InitializedParams {})
231            .await
232            .map_err(|e| Error::LspInitFailed {
233                message: format!("Initialized notification failed: {e}"),
234            })?;
235
236        Ok((result.capabilities, position_encoding))
237    }
238
239    /// Get server capabilities.
240    #[must_use]
241    pub const fn capabilities(&self) -> &ServerCapabilities {
242        &self.capabilities
243    }
244
245    /// Get negotiated position encoding.
246    #[must_use]
247    pub fn position_encoding(&self) -> PositionEncodingKind {
248        self.position_encoding.clone()
249    }
250
251    /// Get client for making requests.
252    #[must_use]
253    pub const fn client(&self) -> &LspClient {
254        &self.client
255    }
256
257    /// Shutdown server gracefully.
258    ///
259    /// Sends shutdown request, waits for response, then sends exit notification.
260    ///
261    /// # Errors
262    ///
263    /// Returns an error if shutdown sequence fails.
264    pub async fn shutdown(self) -> Result<()> {
265        debug!("Shutting down LSP server");
266
267        let _: serde_json::Value = self
268            .client
269            .request("shutdown", serde_json::Value::Null, Duration::from_secs(5))
270            .await?;
271
272        self.client.notify("exit", serde_json::Value::Null).await?;
273
274        self.client.shutdown().await?;
275
276        info!("LSP server shut down successfully");
277        Ok(())
278    }
279}
280
281#[cfg(test)]
282#[allow(clippy::unwrap_used)]
283mod tests {
284    use super::*;
285
286    #[test]
287    fn test_server_state() {
288        assert!(ServerState::Ready.is_ready());
289        assert!(!ServerState::Uninitialized.is_ready());
290
291        assert!(ServerState::Ready.can_accept_requests());
292        assert!(!ServerState::Initializing.can_accept_requests());
293    }
294}