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 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 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 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, 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 let mut blank = String::new();
118 let _ = reader.read_line(&mut blank).await;
119
120 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 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 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 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 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 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 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}