1use 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 _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 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 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 #[must_use]
256 pub const fn capabilities(&self) -> &ServerCapabilities {
257 &self.capabilities
258 }
259
260 #[must_use]
262 pub fn position_encoding(&self) -> PositionEncodingKind {
263 self.position_encoding.clone()
264 }
265
266 #[must_use]
268 pub const fn client(&self) -> &LspClient {
269 &self.client
270 }
271
272 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}