1use 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
15pub 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#[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 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 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 client.notify("initialized", serde_json::json!({})).await?;
96
97 Ok(client)
98 }
99
100 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 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 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 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 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 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
201
202 Ok(Vec::new())
205 }
206
207 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
216async 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 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
249fn 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}