1use crate::lsp::types::*;
2use anyhow::{anyhow, Context, Result};
3use lsp_types::*;
4use serde_json::{json, Value};
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7use std::process::Stdio;
8use std::sync::atomic::{AtomicI64, Ordering};
9use std::sync::Arc;
10use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
11use tokio::sync::{mpsc, oneshot, Mutex};
12use url::Url;
13
14pub struct LspClient {
15 tx_request: mpsc::Sender<Value>,
16 pending_requests: Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value>>>>>,
17 workspace_root: PathBuf,
18 pub process_id: u32,
19}
20
21impl LspClient {
22 pub async fn start(workspace_root: &Path) -> Result<Self> {
23 let canonical_root = workspace_root
24 .canonicalize()
25 .unwrap_or_else(|_| workspace_root.to_path_buf());
26
27 let mut child = tokio::process::Command::new("rust-analyzer")
29 .stdin(Stdio::piped())
30 .stdout(Stdio::piped())
31 .stderr(Stdio::null())
32 .spawn()
33 .context("Failed to spawn rust-analyzer. Please make sure `rust-analyzer` is installed and in PATH.")?;
34
35 let pid = child.id().unwrap_or(0);
36 let stdin = child.stdin.take().ok_or_else(|| anyhow!("Failed to open rust-analyzer stdin"))?;
37 let stdout = child.stdout.take().ok_or_else(|| anyhow!("Failed to open rust-analyzer stdout"))?;
38
39 let (tx_request, mut rx_request) = mpsc::channel::<Value>(100);
40 let pending_requests: Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value>>>>> =
41 Arc::new(Mutex::new(HashMap::new()));
42
43 let mut writer_stdin = stdin;
45 tokio::spawn(async move {
46 while let Some(req) = rx_request.recv().await {
47 let body = serde_json::to_string(&req).unwrap_or_default();
48 let header = format!("Content-Length: {}\r\n\r\n", body.len());
49 let _ = writer_stdin.write_all(header.as_bytes()).await;
50 let _ = writer_stdin.write_all(body.as_bytes()).await;
51 let _ = writer_stdin.flush().await;
52 }
53 });
54
55 let pending_map = pending_requests.clone();
57 tokio::spawn(async move {
58 let mut reader = BufReader::new(stdout);
59 loop {
60 let mut line = String::new();
61 match reader.read_line(&mut line).await {
62 Ok(0) => break, Ok(_) => {
64 let line_trimmed = line.trim();
65 if line_trimmed.starts_with("Content-Length:") {
66 let len_str = line_trimmed.trim_start_matches("Content-Length:").trim();
67 if let Ok(len) = len_str.parse::<usize>() {
68 let mut blank = String::new();
70 let _ = reader.read_line(&mut blank).await;
71
72 let mut buf = vec![0u8; len];
74 if reader.read_exact(&mut buf).await.is_ok() {
75 if let Ok(v) = serde_json::from_slice::<Value>(&buf) {
76 if let Some(id) = v.get("id").and_then(|i| i.as_i64()) {
77 let mut map = pending_map.lock().await;
78 if let Some(sender) = map.remove(&id) {
79 if let Some(err) = v.get("error") {
80 let _ = sender.send(Err(anyhow!("LSP error: {}", err)));
81 } else {
82 let result = v.get("result").cloned().unwrap_or(Value::Null);
83 let _ = sender.send(Ok(result));
84 }
85 }
86 }
87 }
88 }
89 }
90 }
91 }
92 Err(_) => break,
93 }
94 }
95 });
96
97 let client = Self {
98 tx_request,
99 pending_requests,
100 workspace_root: canonical_root,
101 process_id: pid,
102 };
103
104 client.initialize().await?;
106
107 Ok(client)
108 }
109
110 async fn send_request(&self, method: &str, params: Value) -> Result<Value> {
111 static REQ_ID: AtomicI64 = AtomicI64::new(1);
112 let id = REQ_ID.fetch_add(1, Ordering::SeqCst);
113 let req = json!({
114 "jsonrpc": "2.0",
115 "id": id,
116 "method": method,
117 "params": params
118 });
119
120 let (tx, rx) = oneshot::channel();
121 {
122 let mut map = self.pending_requests.lock().await;
123 map.insert(id, tx);
124 }
125
126 self.tx_request
127 .send(req)
128 .await
129 .map_err(|_| anyhow!("LSP writer task channel closed"))?;
130
131 rx.await.map_err(|_| anyhow!("LSP response channel dropped"))?
132 }
133
134 async fn send_notification(&self, method: &str, params: Value) -> Result<()> {
135 let req = json!({
136 "jsonrpc": "2.0",
137 "method": method,
138 "params": params
139 });
140 let _ = self.tx_request.send(req).await;
141 Ok(())
142 }
143
144
145 async fn initialize(&self) -> Result<()> {
146 let root_uri = Url::from_directory_path(&self.workspace_root)
147 .map_err(|_| anyhow!("Failed to convert workspace path to URI"))?;
148
149 let init_params = json!({
150 "processId": self.process_id,
151 "rootUri": root_uri.as_str(),
152 "capabilities": {
153 "textDocument": {
154 "documentSymbol": { "hierarchicalDocumentSymbolSupport": true },
155 "definition": { "dynamicRegistration": false },
156 "references": { "dynamicRegistration": false },
157 "callHierarchy": { "dynamicRegistration": false },
158 "typeHierarchy": { "dynamicRegistration": false }
159 },
160 "workspace": {
161 "symbol": { "dynamicRegistration": false }
162 }
163 }
164 });
165
166 let _res = self.send_request("initialize", init_params).await?;
167 self.send_notification("initialized", json!({})).await?;
168 Ok(())
169 }
170
171 pub async fn query_symbol(&self, name: &str, kind_filter: &str, exact: bool) -> Result<Vec<SymbolItem>> {
172 let params = json!({ "query": name });
173 let res = self.send_request("workspace/symbol", params).await?;
174 let symbols: Vec<SymbolInformation> = match serde_json::from_value::<WorkspaceSymbolResponse>(res.clone()) {
175 Ok(WorkspaceSymbolResponse::Flat(syms)) => syms,
176 Ok(WorkspaceSymbolResponse::Nested(syms)) => syms
177 .into_iter()
178 .map(|s| SymbolInformation {
179 name: s.name,
180 kind: s.kind,
181 tags: s.tags,
182 #[allow(deprecated)]
183 deprecated: None,
184
185 location: match s.location {
186 lsp_types::OneOf::Left(loc) => loc,
187 lsp_types::OneOf::Right(w_loc) => Location {
188 uri: w_loc.uri,
189 range: Range::default(),
190 },
191 },
192 container_name: s.container_name,
193 })
194 .collect(),
195 Err(_) => serde_json::from_value(res).unwrap_or_default(),
196 };
197
198
199 let mut result = Vec::new();
200 for sym in symbols {
201 if exact && sym.name != name {
202 continue;
203 }
204 let kind_str = format!("{:?}", sym.kind).to_lowercase();
205 if kind_filter != "any" && !kind_str.contains(&kind_filter.to_lowercase()) {
206 continue;
207 }
208
209 let file_path = uri_to_file_path(&sym.location.uri);
210 let relative_file = file_path
211 .strip_prefix(&self.workspace_root)
212 .unwrap_or(&file_path)
213 .to_string_lossy()
214 .to_string();
215
216 result.push(SymbolItem {
217 name: sym.name,
218 kind: kind_str,
219 file: relative_file,
220 line: sym.location.range.start.line + 1,
221 col: sym.location.range.start.character + 1,
222 container_name: sym.container_name,
223 });
224 }
225
226 Ok(result)
227 }
228
229 async fn ensure_file_open(&self, canonical_file: &Path) -> Result<Url> {
230 let uri = Url::from_file_path(canonical_file)
231 .map_err(|_| anyhow!("Failed to convert file path to URI"))?;
232 if let Ok(text) = tokio::fs::read_to_string(canonical_file).await {
233 let _ = self
234 .send_notification(
235 "textDocument/didOpen",
236 json!({
237 "textDocument": {
238 "uri": uri.as_str(),
239 "languageId": "rust",
240 "version": 1,
241 "text": text
242 }
243 }),
244 )
245 .await;
246 }
247 Ok(uri)
248 }
249
250 pub async fn query_outline(&self, file: &Path) -> Result<Vec<OutlineItem>> {
251 let canonical_file = file.canonicalize().unwrap_or_else(|_| file.to_path_buf());
252 let uri = self.ensure_file_open(&canonical_file).await?;
253
254 let params = json!({
255 "textDocument": { "uri": uri.as_str() }
256 });
257
258 let res = self.send_request("textDocument/documentSymbol", params).await?;
259
260
261 fn convert_symbols(symbols: Vec<DocumentSymbol>) -> Vec<OutlineItem> {
262 symbols
263 .into_iter()
264 .map(|s| OutlineItem {
265 name: s.name,
266 kind: format!("{:?}", s.kind).to_lowercase(),
267 detail: s.detail,
268 line: s.range.start.line + 1,
269 col: s.range.start.character + 1,
270 end_line: s.range.end.line + 1,
271 children: convert_symbols(s.children.unwrap_or_default()),
272 })
273 .collect()
274 }
275
276 let items = match serde_json::from_value::<DocumentSymbolResponse>(res.clone()) {
277 Ok(DocumentSymbolResponse::Nested(symbols)) => convert_symbols(symbols),
278 Ok(DocumentSymbolResponse::Flat(syms)) => syms
279 .into_iter()
280 .map(|s| OutlineItem {
281 name: s.name,
282 kind: format!("{:?}", s.kind).to_lowercase(),
283 detail: s.container_name,
284 line: s.location.range.start.line + 1,
285 col: s.location.range.start.character + 1,
286 end_line: s.location.range.end.line + 1,
287 children: vec![],
288 })
289 .collect(),
290 Err(_) => {
291 if let Ok(syms) = serde_json::from_value::<Vec<DocumentSymbol>>(res) {
292 convert_symbols(syms)
293 } else {
294 vec![]
295 }
296 }
297 };
298
299 Ok(items)
300 }
301
302
303 pub async fn query_definition(&self, file: &Path, line: u32, col: u32) -> Result<Vec<DefinitionItem>> {
304 let canonical_file = file.canonicalize().unwrap_or_else(|_| file.to_path_buf());
305 let uri = self.ensure_file_open(&canonical_file).await?;
306
307 let params = json!({
308 "textDocument": { "uri": uri.as_str() },
309 "position": { "line": line - 1, "character": col - 1 }
310 });
311
312 let res = self.send_request("textDocument/definition", params).await?;
313 let locations: Vec<Location> = match serde_json::from_value::<GotoDefinitionResponse>(res) {
314 Ok(GotoDefinitionResponse::Scalar(loc)) => vec![loc],
315 Ok(GotoDefinitionResponse::Array(locs)) => locs,
316 Ok(GotoDefinitionResponse::Link(links)) => links
317 .into_iter()
318 .map(|l| Location {
319 uri: l.target_uri,
320 range: l.target_selection_range,
321 })
322 .collect(),
323 Err(_) => vec![],
324 };
325
326 let mut items = Vec::new();
327 for loc in locations {
328 let loc_file = uri_to_file_path(&loc.uri);
329 let rel_file = loc_file
330 .strip_prefix(&self.workspace_root)
331 .unwrap_or(&loc_file)
332 .to_string_lossy()
333 .to_string();
334
335 items.push(DefinitionItem {
336 file: rel_file,
337 line: loc.range.start.line + 1,
338 col: loc.range.start.character + 1,
339 end_line: loc.range.end.line + 1,
340 end_col: loc.range.end.character + 1,
341 snippet: None,
342 });
343 }
344
345 Ok(items)
346 }
347
348 pub async fn query_cursor(&self, file: &Path, line: u32, col: u32, mode: &str, _depth: u32) -> Result<Vec<CursorItem>> {
349 let canonical_file = file.canonicalize().unwrap_or_else(|_| file.to_path_buf());
350 let uri = self.ensure_file_open(&canonical_file).await?;
351
352 if mode == "references" {
353 let params = json!({
354 "textDocument": { "uri": uri.as_str() },
355 "position": { "line": line - 1, "character": col - 1 },
356 "context": { "includeDeclaration": true }
357 });
358 let res = self.send_request("textDocument/references", params).await?;
359 let locs: Vec<Location> = serde_json::from_value(res).unwrap_or_default();
360
361 let mut items = Vec::new();
362 for loc in locs {
363 let loc_file = uri_to_file_path(&loc.uri);
364 let rel_file = loc_file
365 .strip_prefix(&self.workspace_root)
366 .unwrap_or(&loc_file)
367 .to_string_lossy()
368 .to_string();
369 items.push(CursorItem {
370 name: "reference".to_string(),
371 kind: "reference".to_string(),
372 file: rel_file,
373 line: loc.range.start.line + 1,
374 col: loc.range.start.character + 1,
375 caller_or_callee: None,
376 });
377 }
378 return Ok(items);
379 }
380
381 let prep_params = json!({
383 "textDocument": { "uri": uri.as_str() },
384 "position": { "line": line - 1, "character": col - 1 }
385 });
386 let prep_res = self.send_request("textDocument/prepareCallHierarchy", prep_params).await?;
387 let items: Vec<CallHierarchyItem> = serde_json::from_value(prep_res).unwrap_or_default();
388
389 let mut results = Vec::new();
390 if let Some(item) = items.first() {
391 let item_json = serde_json::to_value(item)?;
392 if mode == "outgoing" {
393 let out_res = self.send_request("callHierarchy/outgoingCalls", json!({ "item": item_json })).await?;
394 let out_calls: Vec<CallHierarchyOutgoingCall> = serde_json::from_value(out_res).unwrap_or_default();
395 for call in out_calls {
396 let loc_file = uri_to_file_path(&call.to.uri);
397 let rel_file = loc_file.strip_prefix(&self.workspace_root).unwrap_or(&loc_file).to_string_lossy().to_string();
398 results.push(CursorItem {
399 name: call.to.name,
400 kind: format!("{:?}", call.to.kind).to_lowercase(),
401 file: rel_file,
402 line: call.to.range.start.line + 1,
403 col: call.to.range.start.character + 1,
404 caller_or_callee: Some("callee".to_string()),
405 });
406 }
407 } else {
408 let in_res = self.send_request("callHierarchy/incomingCalls", json!({ "item": item_json })).await?;
409 let in_calls: Vec<CallHierarchyIncomingCall> = serde_json::from_value(in_res).unwrap_or_default();
410 for call in in_calls {
411 let loc_file = uri_to_file_path(&call.from.uri);
412 let rel_file = loc_file.strip_prefix(&self.workspace_root).unwrap_or(&loc_file).to_string_lossy().to_string();
413 results.push(CursorItem {
414 name: call.from.name,
415 kind: format!("{:?}", call.from.kind).to_lowercase(),
416 file: rel_file,
417 line: call.from.range.start.line + 1,
418 col: call.from.range.start.character + 1,
419 caller_or_callee: Some("caller".to_string()),
420 });
421 }
422 }
423 }
424
425 Ok(results)
426 }
427
428 pub async fn query_type_hierarchy(&self, file: &Path, line: u32, col: u32, mode: &str, _depth: u32) -> Result<Vec<TypeHierarchyItemResult>> {
429 let canonical_file = file.canonicalize().unwrap_or_else(|_| file.to_path_buf());
430 let uri = self.ensure_file_open(&canonical_file).await?;
431
432
433 let prep_params = json!({
434 "textDocument": { "uri": uri.as_str() },
435 "position": { "line": line - 1, "character": col - 1 }
436 });
437 let prep_res = self.send_request("textDocument/prepareTypeHierarchy", prep_params).await?;
438 let items: Vec<lsp_types::TypeHierarchyItem> = serde_json::from_value(prep_res).unwrap_or_default();
439 let mut results = Vec::new();
440
441 if let Some(item) = items.first() {
442 let item_json = serde_json::to_value(item)?;
443 let endpoint = if mode == "subtypes" {
444 "typeHierarchy/subtypes"
445 } else {
446 "typeHierarchy/supertypes"
447 };
448 let type_res = self.send_request(endpoint, json!({ "item": item_json })).await?;
449 let type_items: Vec<lsp_types::TypeHierarchyItem> = serde_json::from_value(type_res).unwrap_or_default();
450 for ti in type_items {
451 let file_path = uri_to_file_path(&ti.uri);
452 let rel_file = file_path.strip_prefix(&self.workspace_root).unwrap_or(&file_path).to_string_lossy().to_string();
453 results.push(TypeHierarchyItemResult {
454 name: ti.name,
455 kind: format!("{:?}", ti.kind).to_lowercase(),
456 file: rel_file,
457 line: ti.range.start.line + 1,
458 col: ti.range.start.character + 1,
459 detail: ti.detail,
460 });
461 }
462 }
463
464 Ok(results)
465 }
466
467}
468
469fn uri_to_file_path(uri: &lsp_types::Uri) -> PathBuf {
470 Url::parse(uri.as_str())
471 .ok()
472 .and_then(|u| u.to_file_path().ok())
473 .unwrap_or_default()
474}
475