Skip to main content

rust_analyzer_cli/lsp/
client.rs

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        // Spawn rust-analyzer process
28        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        // Stdio Writer Task
44        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        // Stdio Reader Task
56        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, // EOF
63                    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                                // Read blank line
69                                let mut blank = String::new();
70                                let _ = reader.read_line(&mut blank).await;
71
72                                // Read payload
73                                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        // Initialize handshake
105        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        // Call Hierarchy
382        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