mcpls_core/lsp/
lifecycle.rs1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum ServerState {
30 Uninitialized,
32 Initializing,
34 Ready,
36 ShuttingDown,
38 Shutdown,
40}
41
42impl ServerState {
43 #[must_use]
45 pub const fn is_ready(&self) -> bool {
46 matches!(self, Self::Ready)
47 }
48
49 #[must_use]
51 pub const fn can_accept_requests(&self) -> bool {
52 matches!(self, Self::Ready)
53 }
54}
55
56#[derive(Debug, Clone)]
58pub struct ServerInitConfig {
59 pub server_config: LspServerConfig,
61 pub workspace_roots: Vec<PathBuf>,
63 pub initialization_options: Option<serde_json::Value>,
65}
66
67pub struct LspServer {
69 client: LspClient,
70 capabilities: ServerCapabilities,
71 position_encoding: PositionEncodingKind,
72}
73
74impl LspServer {
75 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 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 #[must_use]
241 pub const fn capabilities(&self) -> &ServerCapabilities {
242 &self.capabilities
243 }
244
245 #[must_use]
247 pub fn position_encoding(&self) -> PositionEncodingKind {
248 self.position_encoding.clone()
249 }
250
251 #[must_use]
253 pub const fn client(&self) -> &LspClient {
254 &self.client
255 }
256
257 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}