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