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