Skip to main content

rust_analyzer_cli/lsp/
client.rs

1use crate::lsp::types::*;
2use anyhow::{Context, Result, anyhow};
3use lsp_types::*;
4use serde::{Deserialize, Serialize};
5use serde_json::{Value, json};
6use std::collections::{HashMap, HashSet, VecDeque};
7use std::path::{Path, PathBuf};
8use std::process::Stdio;
9use std::sync::Arc;
10use std::sync::atomic::{AtomicI64, Ordering};
11use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
12use tokio::sync::{Mutex, mpsc, oneshot};
13use tokio::time::{Duration, timeout};
14use tracing::{debug, warn};
15use url::Url;
16
17pub struct LspClient {
18    tx_request: mpsc::Sender<Value>,
19    pending_requests: Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value>>>>>,
20    child: Arc<Mutex<tokio::process::Child>>,
21    open_documents: Arc<Mutex<HashMap<PathBuf, OpenDocument>>>,
22    workspace_root: PathBuf,
23    pub process_id: u32,
24}
25
26struct OpenDocument {
27    version: i32,
28    text: String,
29}
30
31#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
32pub struct BodySnippet {
33    pub body: String,
34    pub total_lines: usize,
35    pub is_truncated: bool,
36    pub end_line: u32,
37}
38
39impl BodySnippet {
40    fn empty(end_line: u32) -> Self {
41        Self {
42            body: String::new(),
43            total_lines: 0,
44            is_truncated: false,
45            end_line,
46        }
47    }
48}
49
50impl LspClient {
51    pub async fn start(workspace_root: &Path) -> Result<Self> {
52        let canonical_root = workspace_root
53            .canonicalize()
54            .unwrap_or_else(|_| workspace_root.to_path_buf());
55
56        // Spawn rust-analyzer process
57        let mut child = tokio::process::Command::new("rust-analyzer")
58            .stdin(Stdio::piped())
59            .stdout(Stdio::piped())
60            .stderr(Stdio::piped())
61            .current_dir(&canonical_root)
62            .spawn()
63            .context("Failed to spawn rust-analyzer. Please make sure `rust-analyzer` is installed and in PATH.")?;
64
65        let pid = child.id().unwrap_or(0);
66        let stdin = child
67            .stdin
68            .take()
69            .ok_or_else(|| anyhow!("Failed to open rust-analyzer stdin"))?;
70        let stdout = child
71            .stdout
72            .take()
73            .ok_or_else(|| anyhow!("Failed to open rust-analyzer stdout"))?;
74        let stderr = child.stderr.take();
75
76        let (tx_request, mut rx_request) = mpsc::channel::<Value>(100);
77        let pending_requests: Arc<Mutex<HashMap<i64, oneshot::Sender<Result<Value>>>>> =
78            Arc::new(Mutex::new(HashMap::new()));
79
80        // Stdio Writer Task
81        let mut writer_stdin = stdin;
82        tokio::spawn(async move {
83            while let Some(req) = rx_request.recv().await {
84                let body = serde_json::to_string(&req).unwrap_or_default();
85                let header = format!("Content-Length: {}\r\n\r\n", body.len());
86                let _ = writer_stdin.write_all(header.as_bytes()).await;
87                let _ = writer_stdin.write_all(body.as_bytes()).await;
88                let _ = writer_stdin.flush().await;
89            }
90        });
91
92        if let Some(stderr) = stderr {
93            tokio::spawn(async move {
94                let mut lines = BufReader::new(stderr).lines();
95                while let Ok(Some(line)) = lines.next_line().await {
96                    warn!(target: "rust_analyzer", "{}", line);
97                }
98            });
99        }
100
101        // Stdio Reader Task
102        let pending_map = pending_requests.clone();
103        let tx_server_response = tx_request.clone();
104        tokio::spawn(async move {
105            let mut reader = BufReader::new(stdout);
106            loop {
107                let mut line = String::new();
108                match reader.read_line(&mut line).await {
109                    Ok(0) => break, // EOF
110                    Ok(_) => {
111                        let line_trimmed = line.trim();
112                        if line_trimmed.starts_with("Content-Length:") {
113                            let len_str = line_trimmed.trim_start_matches("Content-Length:").trim();
114                            if let Ok(len) = len_str.parse::<usize>() {
115                                // Read blank line
116                                let mut blank = String::new();
117                                let _ = reader.read_line(&mut blank).await;
118
119                                // Read payload
120                                let mut buf = vec![0u8; len];
121                                if reader.read_exact(&mut buf).await.is_ok()
122                                    && let Ok(v) = serde_json::from_slice::<Value>(&buf)
123                                    && let Some(id) = v.get("id").and_then(|i| i.as_i64())
124                                {
125                                    let mut map = pending_map.lock().await;
126                                    if let Some(sender) = map.remove(&id) {
127                                        if let Some(err) = v.get("error") {
128                                            let _ = sender.send(Err(anyhow!("LSP error: {}", err)));
129                                        } else {
130                                            let result =
131                                                v.get("result").cloned().unwrap_or(Value::Null);
132                                            let _ = sender.send(Ok(result));
133                                        }
134                                    } else if let Some(method) =
135                                        v.get("method").and_then(Value::as_str)
136                                    {
137                                        let response = server_request_response(&v, method, id);
138                                        let _ = tx_server_response.send(response).await;
139                                    }
140                                }
141                            }
142                        }
143                    }
144                    Err(_) => break,
145                }
146            }
147        });
148
149        let client = Self {
150            tx_request,
151            pending_requests,
152            child: Arc::new(Mutex::new(child)),
153            open_documents: Arc::new(Mutex::new(HashMap::new())),
154            workspace_root: canonical_root,
155            process_id: pid,
156        };
157
158        // Initialize handshake
159        client.initialize().await?;
160
161        Ok(client)
162    }
163
164    pub async fn shutdown(&self) {
165        let open_files = self
166            .open_documents
167            .lock()
168            .await
169            .keys()
170            .cloned()
171            .collect::<Vec<_>>();
172        for file in open_files {
173            if let Ok(uri) = Url::from_file_path(file) {
174                let _ = self
175                    .send_notification(
176                        "textDocument/didClose",
177                        json!({ "textDocument": { "uri": uri.as_str() } }),
178                    )
179                    .await;
180            }
181        }
182
183        let _ = timeout(
184            Duration::from_secs(2),
185            self.send_request("shutdown", Value::Null),
186        )
187        .await;
188        let _ = self.send_notification("exit", Value::Null).await;
189
190        let mut child = self.child.lock().await;
191        match timeout(Duration::from_secs(2), child.wait()).await {
192            Ok(Ok(_)) => {}
193            Ok(Err(error)) => warn!(
194                "Failed to wait for rust-analyzer {}: {}",
195                self.process_id, error
196            ),
197            Err(_) => {
198                warn!(
199                    "Timed out waiting for rust-analyzer {}; killing it",
200                    self.process_id
201                );
202                if let Err(error) = child.kill().await {
203                    warn!(
204                        "Failed to kill rust-analyzer {}: {}",
205                        self.process_id, error
206                    );
207                }
208                let _ = child.wait().await;
209            }
210        }
211    }
212
213    async fn send_request(&self, method: &str, params: Value) -> Result<Value> {
214        static REQ_ID: AtomicI64 = AtomicI64::new(1);
215        let id = REQ_ID.fetch_add(1, Ordering::SeqCst);
216        let req = json!({
217            "jsonrpc": "2.0",
218            "id": id,
219            "method": method,
220            "params": params
221        });
222
223        let (tx, rx) = oneshot::channel();
224        {
225            let mut map = self.pending_requests.lock().await;
226            map.insert(id, tx);
227        }
228
229        self.tx_request
230            .send(req)
231            .await
232            .map_err(|_| anyhow!("LSP writer task channel closed"))?;
233
234        let result = match timeout(Duration::from_secs(30), rx).await {
235            Ok(result) => result.map_err(|_| anyhow!("LSP response channel dropped"))?,
236            Err(_) => {
237                self.pending_requests.lock().await.remove(&id);
238                return Err(anyhow!("LSP request '{method}' timed out after 30 seconds"));
239            }
240        };
241        result.map_err(|error| anyhow!("LSP request '{method}' failed: {error}"))
242    }
243
244    async fn send_notification(&self, method: &str, params: Value) -> Result<()> {
245        let req = json!({
246            "jsonrpc": "2.0",
247            "method": method,
248            "params": params
249        });
250        self.tx_request
251            .send(req)
252            .await
253            .map_err(|_| anyhow!("LSP writer task channel closed while sending {method}"))?;
254        Ok(())
255    }
256
257    fn validate_position(file: &Path, line: u32, col: u32) -> Result<()> {
258        if line == 0 || col == 0 {
259            return Err(anyhow!(
260                "Invalid position for '{}': line and column numbers are 1-based and must be at least 1.",
261                file.display()
262            ));
263        }
264        Ok(())
265    }
266
267    fn resolve_workspace_file(&self, file: &Path) -> Result<PathBuf> {
268        let candidate = if file.is_absolute() {
269            file.to_path_buf()
270        } else {
271            self.workspace_root.join(file)
272        };
273        let canonical = candidate.canonicalize().with_context(|| {
274            format!(
275                "Failed to resolve Rust source file '{}'.",
276                candidate.display()
277            )
278        })?;
279        if !canonical.starts_with(&self.workspace_root) {
280            return Err(anyhow!(
281                "Input file '{}' is outside workspace '{}'.",
282                file.display(),
283                self.workspace_root.display()
284            ));
285        }
286        Ok(canonical)
287    }
288
289    fn display_file(&self, file: &Path) -> String {
290        file.strip_prefix(&self.workspace_root)
291            .unwrap_or(file)
292            .to_string_lossy()
293            .to_string()
294    }
295
296    async fn initialize(&self) -> Result<()> {
297        let root_uri = Url::from_directory_path(&self.workspace_root)
298            .map_err(|_| anyhow!("Failed to convert workspace path to URI"))?;
299
300        let init_params = json!({
301            "processId": self.process_id,
302            "rootUri": root_uri.as_str(),
303            "workspaceFolders": [{
304                "uri": root_uri.as_str(),
305                "name": self.workspace_root.file_name().and_then(|name| name.to_str()).unwrap_or("workspace")
306            }],
307            "capabilities": {
308                "general": {
309                    "positionEncodings": ["utf-16"]
310                },
311                "textDocument": {
312                    "documentSymbol": { "hierarchicalDocumentSymbolSupport": true },
313                    "definition": { "dynamicRegistration": false },
314                    "hover": {
315                        "dynamicRegistration": false,
316                        "contentFormat": ["markdown", "plaintext"]
317                    },
318                    "references": { "dynamicRegistration": false },
319                    "callHierarchy": { "dynamicRegistration": false },
320                    "typeHierarchy": { "dynamicRegistration": false }
321                },
322                "workspace": {
323                    "symbol": { "dynamicRegistration": false }
324                }
325            }
326        });
327
328        let _res = self.send_request("initialize", init_params).await?;
329        self.send_notification("initialized", json!({})).await?;
330        Ok(())
331    }
332
333    pub async fn query_symbol(
334        &self,
335        name: &str,
336        kind_filter: &str,
337        exact: bool,
338        include_body: bool,
339        max_lines: usize,
340    ) -> Result<Vec<SymbolItem>> {
341        let params = json!({ "query": name });
342        let res = self.send_request("workspace/symbol", params).await?;
343        let symbols: Vec<SymbolInformation> =
344            match serde_json::from_value::<WorkspaceSymbolResponse>(res.clone()) {
345                Ok(WorkspaceSymbolResponse::Flat(syms)) => syms,
346                Ok(WorkspaceSymbolResponse::Nested(syms)) => syms
347                    .into_iter()
348                    .map(|s| SymbolInformation {
349                        name: s.name,
350                        kind: s.kind,
351                        tags: s.tags,
352                        #[allow(deprecated)]
353                        deprecated: None,
354
355                        location: match s.location {
356                            lsp_types::OneOf::Left(loc) => loc,
357                            lsp_types::OneOf::Right(w_loc) => Location {
358                                uri: w_loc.uri,
359                                range: Range::default(),
360                            },
361                        },
362                        container_name: s.container_name,
363                    })
364                    .collect(),
365                Err(_) => serde_json::from_value(res).unwrap_or_default(),
366            };
367
368        let mut result = Vec::new();
369        for sym in symbols {
370            if exact && sym.name != name {
371                continue;
372            }
373            let kind = canonical_symbol_kind(sym.kind, &sym.name, sym.container_name.as_deref());
374            if kind_filter != "any" {
375                let Some(expected_kind) = RustSymbolKind::parse_filter(kind_filter) else {
376                    return Err(anyhow!(
377                        "Invalid Rust symbol kind '{}'. Allowed values: {}.",
378                        kind_filter,
379                        RustSymbolKind::CLI_VALUES.join(", ")
380                    ));
381                };
382                if kind != expected_kind {
383                    continue;
384                }
385            }
386
387            let file_path = uri_to_file_path(&sym.location.uri);
388            let relative_file = self.display_file(&file_path);
389
390            let snippet = if include_body {
391                extract_body_snippet(
392                    &file_path,
393                    sym.location.range.start.line + 1,
394                    sym.location.range.end.line + 1,
395                    max_lines,
396                )
397            } else {
398                BodySnippet::empty(0)
399            };
400
401            let body_opt = if include_body && !snippet.body.is_empty() {
402                Some(snippet.body)
403            } else {
404                None
405            };
406
407            result.push(SymbolItem {
408                name: sym.name,
409                kind,
410                file: relative_file,
411                line: sym.location.range.start.line + 1,
412                col: sym.location.range.start.character + 1,
413                container_name: sym.container_name,
414                body: body_opt,
415            });
416        }
417
418        Ok(result)
419    }
420
421    async fn ensure_file_open(&self, canonical_file: &Path) -> Result<Url> {
422        let uri = Url::from_file_path(canonical_file)
423            .map_err(|_| anyhow!("Failed to convert file path to URI"))?;
424        let text = tokio::fs::read_to_string(canonical_file)
425            .await
426            .with_context(|| {
427                format!(
428                    "Failed to read Rust source file '{}'.",
429                    canonical_file.display()
430                )
431            })?;
432
433        let mut documents = self.open_documents.lock().await;
434        match documents.get_mut(canonical_file) {
435            None => {
436                self.send_notification(
437                    "textDocument/didOpen",
438                    json!({
439                        "textDocument": {
440                            "uri": uri.as_str(),
441                            "languageId": "rust",
442                            "version": 1,
443                            "text": text
444                        }
445                    }),
446                )
447                .await
448                .context("Failed to notify rust-analyzer that the source file was opened")?;
449                documents.insert(
450                    canonical_file.to_path_buf(),
451                    OpenDocument { version: 1, text },
452                );
453            }
454            Some(document) if document.text != text => {
455                document.version += 1;
456                self.send_notification(
457                    "textDocument/didChange",
458                    json!({
459                        "textDocument": {
460                            "uri": uri.as_str(),
461                            "version": document.version
462                        },
463                        "contentChanges": [{ "text": text }]
464                    }),
465                )
466                .await
467                .context("Failed to notify rust-analyzer that the source file changed")?;
468                document.text = text;
469            }
470            Some(_) => {}
471        }
472        Ok(uri)
473    }
474
475    pub async fn query_outline(
476        &self,
477        file: &Path,
478        include_body: bool,
479        max_lines: usize,
480    ) -> Result<Vec<OutlineItem>> {
481        let canonical_file = self.resolve_workspace_file(file)?;
482        let uri = self.ensure_file_open(&canonical_file).await?;
483
484        let params = json!({
485            "textDocument": { "uri": uri.as_str() }
486        });
487
488        let res = self
489            .send_request("textDocument/documentSymbol", params)
490            .await?;
491
492        fn convert_symbols(
493            symbols: Vec<DocumentSymbol>,
494            file_path: &Path,
495            include_body: bool,
496            max_lines: usize,
497        ) -> Vec<OutlineItem> {
498            symbols
499                .into_iter()
500                .map(|s| {
501                    let snippet = if include_body {
502                        extract_body_snippet(
503                            file_path,
504                            s.range.start.line + 1,
505                            s.range.end.line + 1,
506                            max_lines,
507                        )
508                    } else {
509                        BodySnippet::empty(0)
510                    };
511                    let body_opt = if include_body && !snippet.body.is_empty() {
512                        Some(snippet.body)
513                    } else {
514                        None
515                    };
516                    let kind = canonical_symbol_kind(s.kind, &s.name, s.detail.as_deref());
517
518                    OutlineItem {
519                        name: s.name,
520                        kind,
521                        detail: s.detail,
522                        line: s.range.start.line + 1,
523                        col: s.range.start.character + 1,
524                        end_line: s.range.end.line + 1,
525                        children: convert_symbols(
526                            s.children.unwrap_or_default(),
527                            file_path,
528                            include_body,
529                            max_lines,
530                        ),
531                        body: body_opt,
532                    }
533                })
534                .collect()
535        }
536
537        let items = match serde_json::from_value::<DocumentSymbolResponse>(res.clone()) {
538            Ok(DocumentSymbolResponse::Nested(symbols)) => {
539                convert_symbols(symbols, &canonical_file, include_body, max_lines)
540            }
541            Ok(DocumentSymbolResponse::Flat(syms)) => syms
542                .into_iter()
543                .map(|s| {
544                    let snippet = if include_body {
545                        extract_body_snippet(
546                            &canonical_file,
547                            s.location.range.start.line + 1,
548                            s.location.range.end.line + 1,
549                            max_lines,
550                        )
551                    } else {
552                        BodySnippet::empty(0)
553                    };
554                    let body_opt = if include_body && !snippet.body.is_empty() {
555                        Some(snippet.body)
556                    } else {
557                        None
558                    };
559                    let kind = canonical_symbol_kind(s.kind, &s.name, s.container_name.as_deref());
560
561                    OutlineItem {
562                        name: s.name,
563                        kind,
564                        detail: s.container_name,
565                        line: s.location.range.start.line + 1,
566                        col: s.location.range.start.character + 1,
567                        end_line: s.location.range.end.line + 1,
568                        children: vec![],
569                        body: body_opt,
570                    }
571                })
572                .collect(),
573            Err(_) => {
574                if let Ok(syms) = serde_json::from_value::<Vec<DocumentSymbol>>(res) {
575                    convert_symbols(syms, &canonical_file, include_body, max_lines)
576                } else {
577                    vec![]
578                }
579            }
580        };
581
582        Ok(items)
583    }
584
585    pub async fn query_definition(
586        &self,
587        file: &Path,
588        line: u32,
589        col: u32,
590        include_body: bool,
591        max_lines: usize,
592    ) -> Result<Vec<DefinitionItem>> {
593        Self::validate_position(file, line, col)?;
594        let canonical_file = self.resolve_workspace_file(file)?;
595        let uri = self.ensure_file_open(&canonical_file).await?;
596
597        // The public CLI/API uses 1-based coordinates; LSP text positions are 0-based.
598        let params = json!({
599            "textDocument": { "uri": uri.as_str() },
600            "position": { "line": line - 1, "character": col - 1 }
601        });
602
603        let res = self.send_request("textDocument/definition", params).await?;
604        let locations: Vec<Location> = match serde_json::from_value::<GotoDefinitionResponse>(res) {
605            Ok(GotoDefinitionResponse::Scalar(loc)) => vec![loc],
606            Ok(GotoDefinitionResponse::Array(locs)) => locs,
607            Ok(GotoDefinitionResponse::Link(links)) => links
608                .into_iter()
609                .map(|l| Location {
610                    uri: l.target_uri,
611                    range: l.target_range,
612                })
613                .collect(),
614            Err(_) => vec![],
615        };
616
617        let mut items = Vec::new();
618        for loc in locations {
619            let loc_file = uri_to_file_path(&loc.uri);
620            let rel_file = self.display_file(&loc_file);
621
622            let snippet = if include_body {
623                extract_body_snippet(
624                    &loc_file,
625                    loc.range.start.line + 1,
626                    loc.range.end.line + 1,
627                    max_lines,
628                )
629            } else {
630                BodySnippet::empty(0)
631            };
632
633            let body_opt = if include_body && !snippet.body.is_empty() {
634                Some(snippet.body.clone())
635            } else {
636                None
637            };
638
639            items.push(DefinitionItem {
640                file: rel_file,
641                line: loc.range.start.line + 1,
642                col: loc.range.start.character + 1,
643                end_line: loc.range.end.line + 1,
644                end_col: loc.range.end.character + 1,
645                snippet: body_opt.clone(),
646                body: body_opt,
647            });
648        }
649
650        Ok(items)
651    }
652
653    pub async fn query_references(
654        &self,
655        file: &Path,
656        line: u32,
657        col: u32,
658    ) -> Result<Vec<ReferenceItem>> {
659        Self::validate_position(file, line, col)?;
660        let canonical_file = self.resolve_workspace_file(file)?;
661        let uri = self.ensure_file_open(&canonical_file).await?;
662
663        let params = json!({
664            "textDocument": { "uri": uri.as_str() },
665            "position": { "line": line - 1, "character": col - 1 },
666            "context": { "includeDeclaration": true }
667        });
668        let res = self.send_request("textDocument/references", params).await?;
669        let locs: Vec<Location> = serde_json::from_value(res).unwrap_or_default();
670
671        Ok(locs
672            .into_iter()
673            .map(|loc| {
674                let loc_file = uri_to_file_path(&loc.uri);
675                let rel_file = self.display_file(&loc_file);
676                ReferenceItem {
677                    file: rel_file,
678                    line: loc.range.start.line + 1,
679                    col: loc.range.start.character + 1,
680                    end_line: loc.range.end.line + 1,
681                    end_col: loc.range.end.character + 1,
682                }
683            })
684            .collect())
685    }
686
687    pub async fn query_calls(
688        &self,
689        file: &Path,
690        line: u32,
691        col: u32,
692        direction: CallDirection,
693        depth: u32,
694    ) -> Result<Vec<CallItem>> {
695        Self::validate_position(file, line, col)?;
696        if depth == 0 {
697            return Err(anyhow!("Call depth must be at least 1."));
698        }
699        let canonical_file = self.resolve_workspace_file(file)?;
700        let uri = self.ensure_file_open(&canonical_file).await?;
701
702        // Call hierarchy uses a breadth-first walk so depth is a predictable maximum level.
703        let prep_params = json!({
704            "textDocument": { "uri": uri.as_str() },
705            "position": { "line": line - 1, "character": col - 1 }
706        });
707        let prep_res = self
708            .send_request("textDocument/prepareCallHierarchy", prep_params)
709            .await?;
710        let roots: Vec<CallHierarchyItem> = serde_json::from_value(prep_res).unwrap_or_default();
711        let Some(root) = roots.into_iter().next() else {
712            return Ok(Vec::new());
713        };
714
715        let mut visited = HashSet::from([call_hierarchy_key(&root)]);
716        let mut queue = VecDeque::from([(root, 0u32)]);
717        let mut results = Vec::new();
718        while let Some((item, level)) = queue.pop_front() {
719            if level >= depth {
720                continue;
721            }
722            let item_json = serde_json::to_value(&item)?;
723
724            if direction == CallDirection::Outgoing {
725                let out_res = self
726                    .send_request("callHierarchy/outgoingCalls", json!({ "item": item_json }))
727                    .await?;
728                let out_calls: Vec<CallHierarchyOutgoingCall> =
729                    serde_json::from_value(out_res).unwrap_or_default();
730                for call in out_calls {
731                    let target = call.to;
732                    if !visited.insert(call_hierarchy_key(&target)) {
733                        continue;
734                    }
735                    results.push(call_item_from_call(
736                        &target,
737                        &self.workspace_root,
738                        direction,
739                        level + 1,
740                    ));
741                    queue.push_back((target, level + 1));
742                }
743            } else {
744                let in_res = self
745                    .send_request("callHierarchy/incomingCalls", json!({ "item": item_json }))
746                    .await?;
747                let in_calls: Vec<CallHierarchyIncomingCall> =
748                    serde_json::from_value(in_res).unwrap_or_default();
749                for call in in_calls {
750                    let target = call.from;
751                    if !visited.insert(call_hierarchy_key(&target)) {
752                        continue;
753                    }
754                    results.push(call_item_from_call(
755                        &target,
756                        &self.workspace_root,
757                        direction,
758                        level + 1,
759                    ));
760                    queue.push_back((target, level + 1));
761                }
762            }
763        }
764
765        Ok(results)
766    }
767
768    pub async fn query_relations(
769        &self,
770        file: &Path,
771        line: u32,
772        col: u32,
773        mode: RelationMode,
774    ) -> Result<Vec<RelationItem>> {
775        Self::validate_position(file, line, col)?;
776        debug_assert_eq!(mode, RelationMode::Implementations);
777        let canonical_file = self.resolve_workspace_file(file)?;
778        let uri = self.ensure_file_open(&canonical_file).await?;
779        let params = json!({
780            "textDocument": { "uri": uri.as_str() },
781            "position": { "line": line - 1, "character": col - 1 }
782        });
783
784        let response = self
785            .send_request("textDocument/implementation", params)
786            .await?;
787        let locations = match serde_json::from_value::<GotoDefinitionResponse>(response) {
788            Ok(GotoDefinitionResponse::Scalar(location)) => vec![location],
789            Ok(GotoDefinitionResponse::Array(locations)) => locations,
790            Ok(GotoDefinitionResponse::Link(links)) => links
791                .into_iter()
792                .map(|link| Location {
793                    uri: link.target_uri,
794                    range: link.target_range,
795                })
796                .collect(),
797            Err(_) => Vec::new(),
798        };
799
800        Ok(locations
801            .into_iter()
802            .map(|location| {
803                let file = uri_to_file_path(&location.uri);
804                RelationItem {
805                    file: self.display_file(&file),
806                    line: location.range.start.line + 1,
807                    col: location.range.start.character + 1,
808                    end_line: location.range.end.line + 1,
809                    end_col: location.range.end.character + 1,
810                }
811            })
812            .collect())
813    }
814
815    pub async fn query_hover(&self, file: &Path, line: u32, col: u32) -> Result<Option<HoverItem>> {
816        Self::validate_position(file, line, col)?;
817        let canonical_file = self.resolve_workspace_file(file)?;
818        let uri = self.ensure_file_open(&canonical_file).await?;
819
820        let params = json!({
821            "textDocument": { "uri": uri.as_str() },
822            "position": { "line": line - 1, "character": col - 1 }
823        });
824
825        let res = self.send_request("textDocument/hover", params).await?;
826        // LSP uses null when the position has no documentation or type information.
827        let hover: Option<Hover> = serde_json::from_value(res)?;
828        let Some(hover) = hover else {
829            return Ok(None);
830        };
831
832        let loc_file = canonical_file;
833        let rel_file = self.display_file(&loc_file);
834        let range = hover.range.map(|range| HoverRange {
835            start_line: range.start.line + 1,
836            start_col: range.start.character + 1,
837            end_line: range.end.line + 1,
838            end_col: range.end.character + 1,
839        });
840
841        Ok(Some(HoverItem {
842            file: rel_file,
843            line,
844            col,
845            contents: hover_contents_to_markdown(hover.contents),
846            range,
847        }))
848    }
849
850    pub async fn query_body(
851        &self,
852        file: &Path,
853        line: u32,
854        col: u32,
855        max_lines: usize,
856    ) -> Result<BodyItem> {
857        Self::validate_position(file, line, col)?;
858        let canonical_file = self.resolve_workspace_file(file)?;
859        let uri = self.ensure_file_open(&canonical_file).await?;
860
861        let params = json!({
862            "textDocument": { "uri": uri.as_str() },
863            "position": { "line": line - 1, "character": col - 1 }
864        });
865
866        let res = self.send_request("textDocument/definition", params).await?;
867        let locations: Vec<Location> = match serde_json::from_value::<GotoDefinitionResponse>(res) {
868            Ok(GotoDefinitionResponse::Scalar(loc)) => vec![loc],
869            Ok(GotoDefinitionResponse::Array(locs)) => locs,
870            Ok(GotoDefinitionResponse::Link(links)) => links
871                .into_iter()
872                .map(|l| Location {
873                    uri: l.target_uri,
874                    range: l.target_range,
875                })
876                .collect(),
877            Err(_) => vec![],
878        };
879
880        if let Some(loc) = locations.first() {
881            let loc_file = uri_to_file_path(&loc.uri);
882            let rel_file = self.display_file(&loc_file);
883
884            let snippet = extract_body_snippet(
885                &loc_file,
886                loc.range.start.line + 1,
887                loc.range.end.line + 1,
888                max_lines,
889            );
890
891            Ok(BodyItem {
892                file: rel_file,
893                line: loc.range.start.line + 1,
894                col: loc.range.start.character + 1,
895                end_line: snippet.end_line,
896                end_col: loc.range.end.character + 1,
897                body: snippet.body,
898                total_lines: snippet.total_lines,
899                is_truncated: snippet.is_truncated,
900            })
901        } else {
902            Err(anyhow::anyhow!(
903                "No symbol/entity definition found at specified position"
904            ))
905        }
906    }
907}
908
909fn call_hierarchy_key(item: &CallHierarchyItem) -> String {
910    format!(
911        "{}:{}:{}:{}:{}",
912        item.uri.as_str(),
913        item.range.start.line,
914        item.range.start.character,
915        item.range.end.line,
916        item.range.end.character
917    )
918}
919
920fn call_item_from_call(
921    item: &CallHierarchyItem,
922    workspace_root: &Path,
923    direction: CallDirection,
924    depth: u32,
925) -> CallItem {
926    let file_path = uri_to_file_path(&item.uri);
927    let rel_file = file_path
928        .strip_prefix(workspace_root)
929        .unwrap_or(&file_path)
930        .to_string_lossy()
931        .to_string();
932    CallItem {
933        name: item.name.clone(),
934        kind: canonical_symbol_kind(item.kind, &item.name, item.detail.as_deref()),
935        file: rel_file,
936        line: item.range.start.line + 1,
937        col: item.range.start.character + 1,
938        end_line: item.range.end.line + 1,
939        end_col: item.range.end.character + 1,
940        direction,
941        depth,
942    }
943}
944
945fn canonical_symbol_kind(kind: SymbolKind, name: &str, detail: Option<&str>) -> RustSymbolKind {
946    if name.starts_with("impl ") || detail.is_some_and(|value| value.starts_with("impl ")) {
947        return RustSymbolKind::Impl;
948    }
949    if name.ends_with('!') {
950        return RustSymbolKind::Macro;
951    }
952    match kind {
953        SymbolKind::MODULE | SymbolKind::NAMESPACE | SymbolKind::PACKAGE => RustSymbolKind::Module,
954        SymbolKind::STRUCT | SymbolKind::CLASS => RustSymbolKind::Struct,
955        SymbolKind::ENUM => RustSymbolKind::Enum,
956        SymbolKind::ENUM_MEMBER => RustSymbolKind::EnumVariant,
957        SymbolKind::INTERFACE => RustSymbolKind::Trait,
958        SymbolKind::METHOD => RustSymbolKind::Method,
959        SymbolKind::CONSTRUCTOR => RustSymbolKind::AssociatedFunction,
960        SymbolKind::FIELD | SymbolKind::PROPERTY => RustSymbolKind::Field,
961        SymbolKind::FUNCTION => RustSymbolKind::Function,
962        SymbolKind::CONSTANT => RustSymbolKind::Const,
963        SymbolKind::VARIABLE => RustSymbolKind::Variable,
964        SymbolKind::TYPE_PARAMETER => RustSymbolKind::TypeParameter,
965        SymbolKind::OBJECT => RustSymbolKind::Impl,
966        _ => RustSymbolKind::Unknown,
967    }
968}
969
970fn server_request_response(message: &Value, method: &str, id: i64) -> Value {
971    let result = match method {
972        "workspace/configuration" => message
973            .get("params")
974            .and_then(|params| params.get("items"))
975            .and_then(Value::as_array)
976            .map(|items| vec![Value::Null; items.len()])
977            .map(Value::Array)
978            .unwrap_or_else(|| Value::Array(Vec::new())),
979        "client/registerCapability"
980        | "client/unregisterCapability"
981        | "window/workDoneProgress/create" => Value::Null,
982        _ => {
983            debug!(
984                "Ignoring unsupported rust-analyzer server request: {}",
985                method
986            );
987            return json!({
988                "jsonrpc": "2.0",
989                "id": id,
990                "error": {
991                    "code": -32601,
992                    "message": format!("Unsupported LSP server request: {method}")
993                }
994            });
995        }
996    };
997    json!({ "jsonrpc": "2.0", "id": id, "result": result })
998}
999
1000fn uri_to_file_path(uri: &lsp_types::Uri) -> PathBuf {
1001    Url::parse(uri.as_str())
1002        .ok()
1003        .and_then(|u| u.to_file_path().ok())
1004        .unwrap_or_default()
1005}
1006
1007fn marked_string_to_markdown(marked: MarkedString) -> String {
1008    match marked {
1009        MarkedString::LanguageString(language_string) => format!(
1010            "```{}\n{}\n```",
1011            language_string.language, language_string.value
1012        ),
1013        MarkedString::String(value) => value,
1014    }
1015}
1016
1017fn hover_contents_to_markdown(contents: HoverContents) -> String {
1018    match contents {
1019        HoverContents::Markup(markup) => markup.value,
1020        HoverContents::Scalar(marked) => marked_string_to_markdown(marked),
1021        HoverContents::Array(items) => items
1022            .into_iter()
1023            .map(marked_string_to_markdown)
1024            .collect::<Vec<_>>()
1025            .join("\n\n"),
1026    }
1027}
1028
1029pub fn extract_body_snippet(
1030    file: &Path,
1031    start_line: u32,
1032    mut end_line: u32,
1033    max_lines: usize,
1034) -> BodySnippet {
1035    let content = match std::fs::read_to_string(file) {
1036        Ok(c) => c,
1037        Err(_) => return BodySnippet::empty(end_line),
1038    };
1039    let lines: Vec<&str> = content.lines().collect();
1040    let start_idx = (start_line.saturating_sub(1)) as usize;
1041
1042    if start_idx >= lines.len() {
1043        return BodySnippet::empty(end_line);
1044    }
1045
1046    // Expand short LSP ranges to the complete Rust item before applying the output limit.
1047    if end_line <= start_line || end_line <= start_line + 1 {
1048        let mut depth = 0i32;
1049        let mut found_brace = false;
1050        for (i, line) in lines[start_idx..].iter().enumerate() {
1051            for ch in line.chars() {
1052                if ch == '{' {
1053                    depth += 1;
1054                    found_brace = true;
1055                } else if ch == '}' {
1056                    depth -= 1;
1057                }
1058            }
1059            if (found_brace && depth <= 0) || (!found_brace && line.trim().ends_with(';')) {
1060                end_line = start_line + i as u32 + 1;
1061                break;
1062            }
1063            if i >= 100 {
1064                end_line = start_line + i as u32 + 1;
1065                break;
1066            }
1067        }
1068    }
1069
1070    let end_idx = (end_line as usize).min(lines.len());
1071    let total = if end_idx > start_idx {
1072        end_idx - start_idx
1073    } else {
1074        1
1075    };
1076    // A zero limit is the documented opt-in for unlimited body output.
1077    let limit = if max_lines == 0 {
1078        total
1079    } else {
1080        max_lines.min(total)
1081    };
1082    let is_truncated = limit < total;
1083    let slice = &lines[start_idx..start_idx + limit];
1084    BodySnippet {
1085        body: slice.join("\n"),
1086        total_lines: total,
1087        is_truncated,
1088        end_line,
1089    }
1090}
1091
1092pub fn format_body_with_line_numbers(snippet: &str, start_line: u32) -> String {
1093    snippet
1094        .lines()
1095        .enumerate()
1096        .map(|(idx, line)| format!("{:4} | {}", start_line as usize + idx, line))
1097        .collect::<Vec<_>>()
1098        .join("\n")
1099}
1100
1101#[cfg(test)]
1102mod tests {
1103    use super::*;
1104    use lsp_types::{MarkupContent, MarkupKind};
1105
1106    #[test]
1107    fn test_hover_markup_is_preserved() {
1108        let contents = HoverContents::Markup(MarkupContent {
1109            kind: MarkupKind::Markdown,
1110            value: "/// Documentation\n\n```rust\nfn example() {}\n```".to_string(),
1111        });
1112
1113        assert_eq!(
1114            hover_contents_to_markdown(contents),
1115            "/// Documentation\n\n```rust\nfn example() {}\n```"
1116        );
1117    }
1118
1119    #[test]
1120    fn test_hover_language_string_becomes_markdown_code_block() {
1121        let contents = HoverContents::Scalar(MarkedString::LanguageString(LanguageString {
1122            language: "rust".to_string(),
1123            value: "fn example() {}".to_string(),
1124        }));
1125
1126        assert_eq!(
1127            hover_contents_to_markdown(contents),
1128            "```rust\nfn example() {}\n```"
1129        );
1130    }
1131
1132    #[test]
1133    fn test_hover_array_joins_marked_strings() {
1134        let contents = HoverContents::Array(vec![
1135            MarkedString::String("Summary".to_string()),
1136            MarkedString::LanguageString(LanguageString {
1137                language: "rust".to_string(),
1138                value: "fn example() {}".to_string(),
1139            }),
1140        ]);
1141
1142        assert_eq!(
1143            hover_contents_to_markdown(contents),
1144            "Summary\n\n```rust\nfn example() {}\n```"
1145        );
1146    }
1147
1148    #[test]
1149    fn test_body_snippet_json_roundtrip() {
1150        let snippet = BodySnippet {
1151            body: "fn example() {}".to_string(),
1152            total_lines: 1,
1153            is_truncated: false,
1154            end_line: 12,
1155        };
1156        let json = serde_json::to_string(&snippet).unwrap();
1157        let decoded: BodySnippet = serde_json::from_str(&json).unwrap();
1158        assert_eq!(decoded, snippet);
1159    }
1160
1161    #[test]
1162    fn test_body_snippet_reports_truncation() {
1163        let file = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/code_example.rs");
1164        let snippet = extract_body_snippet(&file, 39, 39, 2);
1165        assert_eq!(snippet.body.lines().count(), 2);
1166        assert!(snippet.total_lines > 100);
1167        assert!(snippet.is_truncated);
1168        assert!(snippet.end_line >= 39);
1169    }
1170}