Skip to main content

agent_code_lib/services/
lsp.rs

1//! Language Server Protocol integration.
2//!
3//! Connects to LSP servers to provide diagnostics (errors, warnings),
4//! symbol information, and code intelligence. Communicates via JSON-RPC
5//! over stdio, similar to the MCP transport.
6
7use std::path::{Path, PathBuf};
8use std::process::Stdio;
9
10use serde::{Deserialize, Serialize};
11use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
12use tokio::sync::Mutex;
13use tracing::debug;
14
15/// An LSP client connection to a language server.
16pub struct LspClient {
17    name: String,
18    stdin: Mutex<tokio::process::ChildStdin>,
19    stdout: Mutex<BufReader<tokio::process::ChildStdout>>,
20    child: Mutex<tokio::process::Child>,
21    next_id: Mutex<u64>,
22    root_uri: String,
23}
24
25/// A diagnostic reported by the language server.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct Diagnostic {
28    pub file: String,
29    pub line: u32,
30    pub column: u32,
31    pub severity: DiagnosticSeverity,
32    pub message: String,
33    pub source: Option<String>,
34}
35
36#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
37pub enum DiagnosticSeverity {
38    Error,
39    Warning,
40    Information,
41    Hint,
42}
43
44impl LspClient {
45    /// Spawn and initialize an LSP server.
46    pub async fn start(
47        name: &str,
48        command: &str,
49        args: &[String],
50        root_path: &Path,
51    ) -> Result<Self, String> {
52        let mut child = tokio::process::Command::new(command)
53            .args(args)
54            .stdin(Stdio::piped())
55            .stdout(Stdio::piped())
56            .stderr(Stdio::null())
57            .spawn()
58            .map_err(|e| format!("Failed to start LSP server '{name}': {e}"))?;
59
60        let stdin = child.stdin.take().ok_or("No stdin")?;
61        let stdout = child.stdout.take().ok_or("No stdout")?;
62
63        let root_uri = format!("file://{}", root_path.display());
64
65        let client = Self {
66            name: name.to_string(),
67            stdin: Mutex::new(stdin),
68            stdout: Mutex::new(BufReader::new(stdout)),
69            child: Mutex::new(child),
70            next_id: Mutex::new(1),
71            root_uri: root_uri.clone(),
72        };
73
74        // Send initialize request.
75        let init_result = client
76            .request(
77                "initialize",
78                serde_json::json!({
79                    "processId": std::process::id(),
80                    "rootUri": root_uri,
81                    "capabilities": {
82                        "textDocument": {
83                            "publishDiagnostics": {
84                                "relatedInformation": true
85                            }
86                        }
87                    }
88                }),
89            )
90            .await?;
91
92        debug!("LSP '{name}' initialized: {:?}", init_result);
93
94        // Send initialized notification.
95        client.notify("initialized", serde_json::json!({})).await?;
96
97        Ok(client)
98    }
99
100    /// Send a JSON-RPC request and read the response.
101    async fn request(
102        &self,
103        method: &str,
104        params: serde_json::Value,
105    ) -> Result<serde_json::Value, String> {
106        let id = {
107            let mut next = self.next_id.lock().await;
108            let id = *next;
109            *next += 1;
110            id
111        };
112
113        let body = serde_json::json!({
114            "jsonrpc": "2.0",
115            "id": id,
116            "method": method,
117            "params": params,
118        });
119
120        let body_str = serde_json::to_string(&body).map_err(|e| format!("Serialize error: {e}"))?;
121
122        let message = format!("Content-Length: {}\r\n\r\n{}", body_str.len(), body_str);
123
124        {
125            let mut stdin = self.stdin.lock().await;
126            stdin
127                .write_all(message.as_bytes())
128                .await
129                .map_err(|e| format!("Write error: {e}"))?;
130            stdin
131                .flush()
132                .await
133                .map_err(|e| format!("Flush error: {e}"))?;
134        }
135
136        // Read response (Content-Length header + body).
137        let mut stdout = self.stdout.lock().await;
138        let content_length = read_content_length(&mut stdout).await?;
139
140        let mut buf = vec![0u8; content_length];
141        tokio::io::AsyncReadExt::read_exact(&mut *stdout, &mut buf)
142            .await
143            .map_err(|e| format!("Read error: {e}"))?;
144
145        let response: serde_json::Value =
146            serde_json::from_slice(&buf).map_err(|e| format!("Parse error: {e}"))?;
147
148        if let Some(error) = response.get("error") {
149            return Err(format!("LSP error: {error}"));
150        }
151
152        Ok(response.get("result").cloned().unwrap_or_default())
153    }
154
155    /// Send a notification (no response expected).
156    async fn notify(&self, method: &str, params: serde_json::Value) -> Result<(), String> {
157        let body = serde_json::json!({
158            "jsonrpc": "2.0",
159            "method": method,
160            "params": params,
161        });
162
163        let body_str = serde_json::to_string(&body).map_err(|e| format!("Serialize error: {e}"))?;
164
165        let message = format!("Content-Length: {}\r\n\r\n{}", body_str.len(), body_str);
166
167        let mut stdin = self.stdin.lock().await;
168        stdin
169            .write_all(message.as_bytes())
170            .await
171            .map_err(|e| format!("Write error: {e}"))?;
172        stdin
173            .flush()
174            .await
175            .map_err(|e| format!("Flush error: {e}"))?;
176
177        Ok(())
178    }
179
180    /// Get diagnostics for a file by opening it and waiting for the server.
181    pub async fn get_diagnostics(&self, file_path: &PathBuf) -> Result<Vec<Diagnostic>, String> {
182        let uri = format!("file://{}", file_path.display());
183        let content = std::fs::read_to_string(file_path).map_err(|e| format!("Read error: {e}"))?;
184
185        // Notify the server that we opened the file.
186        self.notify(
187            "textDocument/didOpen",
188            serde_json::json!({
189                "textDocument": {
190                    "uri": uri,
191                    "languageId": detect_language(file_path),
192                    "version": 1,
193                    "text": content,
194                }
195            }),
196        )
197        .await?;
198
199        // Give the server a moment to analyze.
200        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
201
202        // Diagnostics come via notifications; for now return empty.
203        // Full implementation would read from a notification stream.
204        Ok(Vec::new())
205    }
206
207    /// Shut down the LSP server.
208    pub async fn shutdown(&self) {
209        let _ = self.request("shutdown", serde_json::json!(null)).await;
210        let _ = self.notify("exit", serde_json::json!(null)).await;
211        let mut child = self.child.lock().await;
212        let _ = child.kill().await;
213    }
214}
215
216/// Read the Content-Length header from an LSP message.
217async fn read_content_length(
218    reader: &mut BufReader<tokio::process::ChildStdout>,
219) -> Result<usize, String> {
220    loop {
221        let mut line = String::new();
222        reader
223            .read_line(&mut line)
224            .await
225            .map_err(|e| format!("Header read error: {e}"))?;
226
227        let trimmed = line.trim();
228        if trimmed.is_empty() {
229            return Err("Empty header line without Content-Length".to_string());
230        }
231
232        if let Some(len_str) = trimmed.strip_prefix("Content-Length: ") {
233            let length: usize = len_str
234                .parse()
235                .map_err(|e| format!("Invalid Content-Length: {e}"))?;
236
237            // Read the empty line after headers.
238            let mut empty = String::new();
239            reader
240                .read_line(&mut empty)
241                .await
242                .map_err(|e| format!("Header separator read error: {e}"))?;
243
244            return Ok(length);
245        }
246    }
247}
248
249/// Detect language ID from file extension.
250fn detect_language(path: &Path) -> &str {
251    match path.extension().and_then(|e| e.to_str()) {
252        Some("rs") => "rust",
253        Some("py") => "python",
254        Some("js") => "javascript",
255        Some("ts") => "typescript",
256        Some("tsx") => "typescriptreact",
257        Some("jsx") => "javascriptreact",
258        Some("go") => "go",
259        Some("java") => "java",
260        Some("rb") => "ruby",
261        Some("c" | "h") => "c",
262        Some("cpp" | "cc" | "cxx" | "hpp") => "cpp",
263        Some("cs") => "csharp",
264        Some("swift") => "swift",
265        Some("kt") => "kotlin",
266        Some("json") => "json",
267        Some("yaml" | "yml") => "yaml",
268        Some("toml") => "toml",
269        Some("md") => "markdown",
270        Some("html") => "html",
271        Some("css") => "css",
272        Some("sh" | "bash") => "shellscript",
273        _ => "plaintext",
274    }
275}