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    /// Child process handle. Kept alive for process lifetime management.
73    /// When dropped, the process is terminated via SIGKILL (`kill_on_drop`).
74    _child: tokio::process::Child,
75}
76
77impl std::fmt::Debug for LspServer {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        f.debug_struct("LspServer")
80            .field("client", &self.client)
81            .field("capabilities", &self.capabilities)
82            .field("position_encoding", &self.position_encoding)
83            .field("_child", &"<process>")
84            .finish()
85    }
86}
87
88impl LspServer {
89    /// Spawn and initialize LSP server.
90    ///
91    /// This performs the complete initialization sequence:
92    /// 1. Spawns the LSP server as a child process
93    /// 2. Sends initialize request with client capabilities
94    /// 3. Receives server capabilities from initialize response
95    /// 4. Sends initialized notification
96    ///
97    /// # Errors
98    ///
99    /// Returns an error if:
100    /// - Server process fails to spawn
101    /// - Initialize request fails or times out
102    /// - Server returns error during initialization
103    pub async fn spawn(config: ServerInitConfig) -> Result<Self> {
104        info!(
105            "Spawning LSP server: {} {:?}",
106            config.server_config.command, config.server_config.args
107        );
108
109        let mut child = Command::new(&config.server_config.command)
110            .args(&config.server_config.args)
111            .stdin(Stdio::piped())
112            .stdout(Stdio::piped())
113            .stderr(Stdio::null())
114            .kill_on_drop(true)
115            .spawn()
116            .map_err(|e| Error::ServerSpawnFailed {
117                command: config.server_config.command.clone(),
118                source: e,
119            })?;
120
121        let stdin = child
122            .stdin
123            .take()
124            .ok_or_else(|| Error::Transport("Failed to capture stdin".to_string()))?;
125        let stdout = child
126            .stdout
127            .take()
128            .ok_or_else(|| Error::Transport("Failed to capture stdout".to_string()))?;
129
130        let transport = LspTransport::new(stdin, stdout);
131        let client = LspClient::from_transport(config.server_config.clone(), transport);
132
133        let (capabilities, position_encoding) = Self::initialize(&client, &config).await?;
134
135        info!("LSP server initialized successfully");
136
137        Ok(Self {
138            client,
139            capabilities,
140            position_encoding,
141            _child: child,
142        })
143    }
144
145    /// Perform LSP initialization handshake.
146    ///
147    /// Sends initialize request and waits for response, then sends initialized notification.
148    async fn initialize(
149        client: &LspClient,
150        config: &ServerInitConfig,
151    ) -> Result<(ServerCapabilities, PositionEncodingKind)> {
152        debug!("Sending initialize request");
153
154        let workspace_folders: Vec<WorkspaceFolder> = config
155            .workspace_roots
156            .iter()
157            .map(|root| {
158                let path_str = root.to_str().ok_or_else(|| {
159                    let root_display = root.display();
160                    Error::InvalidUri(format!("Invalid UTF-8 in path: {root_display}"))
161                })?;
162                let uri_str = if cfg!(windows) {
163                    format!("file:///{}", path_str.replace('\\', "/"))
164                } else {
165                    format!("file://{path_str}")
166                };
167                let uri = Uri::from_str(&uri_str).map_err(|_| {
168                    let root_display = root.display();
169                    Error::InvalidUri(format!("Invalid workspace root: {root_display}"))
170                })?;
171                Ok(WorkspaceFolder {
172                    uri,
173                    name: root
174                        .file_name()
175                        .and_then(|n| n.to_str())
176                        .unwrap_or("workspace")
177                        .to_string(),
178                })
179            })
180            .collect::<Result<Vec<_>>>()?;
181
182        let params = InitializeParams {
183            process_id: Some(std::process::id()),
184            #[allow(deprecated)]
185            root_uri: None,
186            initialization_options: config.initialization_options.clone(),
187            capabilities: ClientCapabilities {
188                general: Some(GeneralClientCapabilities {
189                    position_encodings: Some(vec![
190                        PositionEncodingKind::UTF8,
191                        PositionEncodingKind::UTF16,
192                    ]),
193                    ..Default::default()
194                }),
195                text_document: Some(lsp_types::TextDocumentClientCapabilities {
196                    hover: Some(lsp_types::HoverClientCapabilities {
197                        dynamic_registration: Some(false),
198                        content_format: Some(vec![
199                            lsp_types::MarkupKind::Markdown,
200                            lsp_types::MarkupKind::PlainText,
201                        ]),
202                    }),
203                    definition: Some(lsp_types::GotoCapability {
204                        dynamic_registration: Some(false),
205                        link_support: Some(true),
206                    }),
207                    references: Some(lsp_types::ReferenceClientCapabilities {
208                        dynamic_registration: Some(false),
209                    }),
210                    ..Default::default()
211                }),
212                workspace: Some(lsp_types::WorkspaceClientCapabilities {
213                    workspace_folders: Some(true),
214                    ..Default::default()
215                }),
216                ..Default::default()
217            },
218            client_info: Some(ClientInfo {
219                name: "mcpls".to_string(),
220                version: Some(env!("CARGO_PKG_VERSION").to_string()),
221            }),
222            workspace_folders: Some(workspace_folders),
223            ..Default::default()
224        };
225
226        let result: InitializeResult = client
227            .request("initialize", params, Duration::from_secs(30))
228            .await
229            .map_err(|e| Error::LspInitFailed {
230                message: format!("Initialize request failed: {e}"),
231            })?;
232
233        let position_encoding = result
234            .capabilities
235            .position_encoding
236            .clone()
237            .unwrap_or(PositionEncodingKind::UTF16);
238
239        debug!(
240            "Server capabilities received, encoding: {:?}",
241            position_encoding
242        );
243
244        client
245            .notify("initialized", InitializedParams {})
246            .await
247            .map_err(|e| Error::LspInitFailed {
248                message: format!("Initialized notification failed: {e}"),
249            })?;
250
251        Ok((result.capabilities, position_encoding))
252    }
253
254    /// Get server capabilities.
255    #[must_use]
256    pub const fn capabilities(&self) -> &ServerCapabilities {
257        &self.capabilities
258    }
259
260    /// Get negotiated position encoding.
261    #[must_use]
262    pub fn position_encoding(&self) -> PositionEncodingKind {
263        self.position_encoding.clone()
264    }
265
266    /// Get client for making requests.
267    #[must_use]
268    pub const fn client(&self) -> &LspClient {
269        &self.client
270    }
271
272    /// Shutdown server gracefully.
273    ///
274    /// Sends shutdown request, waits for response, then sends exit notification.
275    ///
276    /// # Errors
277    ///
278    /// Returns an error if shutdown sequence fails.
279    pub async fn shutdown(self) -> Result<()> {
280        debug!("Shutting down LSP server");
281
282        let _: serde_json::Value = self
283            .client
284            .request("shutdown", serde_json::Value::Null, Duration::from_secs(5))
285            .await?;
286
287        self.client.notify("exit", serde_json::Value::Null).await?;
288
289        self.client.shutdown().await?;
290
291        info!("LSP server shut down successfully");
292        Ok(())
293    }
294}
295
296#[cfg(test)]
297#[allow(clippy::unwrap_used)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn test_server_state() {
303        assert!(ServerState::Ready.is_ready());
304        assert!(!ServerState::Uninitialized.is_ready());
305
306        assert!(ServerState::Ready.can_accept_requests());
307        assert!(!ServerState::Initializing.can_accept_requests());
308    }
309}