1use std::path::PathBuf;
11use std::process::{Command, Stdio};
12use std::sync::OnceLock;
13
14use serde_json::Value;
15
16use crate::lsp::LspTransport;
17use crate::probe::Probe;
18
19const KIND_METHOD: u64 = 2;
21
22static RA_PATH_CACHE: OnceLock<PathBuf> = OnceLock::new();
23
24#[derive(serde::Serialize)]
26pub struct Method {
27 pub name: String,
29 pub detail: Option<String>,
31 pub documentation: Option<String>,
33}
34
35fn rustup_rust_analyzer() -> Option<PathBuf> {
36 let out = Command::new("rustup")
37 .args(["which", "rust-analyzer"])
38 .output()
39 .ok()?;
40
41 if !out.status.success() {
42 return None;
43 }
44
45 let path = String::from_utf8_lossy(&out.stdout).trim().to_string();
46 (!path.is_empty()).then(|| path.into())
47}
48
49pub fn find_rust_analyzer() -> anyhow::Result<PathBuf> {
61 if let Some(path) = RA_PATH_CACHE.get() {
62 return Ok(path.clone());
63 }
64 let path = if let Ok(path) = which("rust-analyzer") {
65 path
66 } else if let Some(path) = rustup_rust_analyzer() {
67 path
68 } else {
69 anyhow::bail!(
70 "rust-analyzer not found.\n\
71 Install it with: rustup component add rust-analyzer\n\
72 or ensure it is on your PATH."
73 )
74 };
75 Ok(RA_PATH_CACHE.get_or_init(|| path).clone())
76}
77
78#[cfg(unix)]
79fn which(name: &str) -> anyhow::Result<std::path::PathBuf> {
80 let out = Command::new("which").arg(name).output()?;
81 anyhow::ensure!(out.status.success(), "not found");
82 let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
83 Ok(s.into())
84}
85
86pub fn query_methods(
104 type_name: &str,
105 ra_path: &std::path::Path,
106 deps: Option<&str>,
107) -> anyhow::Result<Vec<Method>> {
108 let probe = Probe::new_with_deps(type_name, deps)?;
109 let mut child = Command::new(ra_path)
110 .stdin(Stdio::piped())
111 .stdout(Stdio::piped())
112 .stderr(Stdio::null())
113 .spawn()?;
114
115 let mut lsp = LspTransport::new(&mut child);
116 let pid = std::process::id();
117
118 lsp.send(&LspTransport::initialize(pid, &probe.root_uri()))?;
120 lsp.recv_until(20, |msg| {
121 (msg["id"] == 1 && msg["result"].is_object()).then_some(())
122 })?;
123
124 lsp.send(&LspTransport::initialized())?;
126
127 lsp.send(&LspTransport::did_open(&probe.src_uri(), &probe.source()?))?;
129
130 wait_for_indexing(&mut lsp)?;
132
133 let completion_response = {
136 let mut response = Value::Null;
137 for attempt in 1..=10u64 {
138 let req_id = attempt + 2;
139 lsp.send(&LspTransport::completion(
140 req_id,
141 &probe.src_uri(),
142 probe.dot_line,
143 probe.dot_col,
144 ))?;
145
146 let msg = lsp.recv_until(50, |msg| (msg["id"] == req_id).then(|| msg.clone()))?;
147
148 let has_items = msg["result"]["items"]
149 .as_array()
150 .is_some_and(|a| !a.is_empty());
151
152 if has_items {
153 response = msg;
154 break;
155 }
156
157 if attempt < 10 {
158 let delay = match attempt {
159 1 => 50, 2 => 100, 3 => 200, _ => 300, };
164 std::thread::sleep(std::time::Duration::from_millis(delay));
165 }
166 }
170 response
171 };
172
173 lsp.send(&LspTransport::shutdown(13))?;
175 let _ = lsp.recv_until(10, |msg| (msg["id"] == 13).then_some(()));
176 lsp.send(&LspTransport::exit())?;
177 let _ = child.wait();
178
179 parse_methods(&completion_response)
181}
182
183fn wait_for_indexing(lsp: &mut LspTransport) -> anyhow::Result<()> {
192 let debug = std::env::var("RUST_METH_DEBUG").is_ok();
193 let start = std::time::Instant::now();
194 let timeout = std::time::Duration::from_secs(10); lsp.recv_until(200, |msg| {
197 if start.elapsed() > timeout {
199 return Some(()); }
201
202 let method = msg["method"].as_str().unwrap_or("");
203 if debug {
204 eprintln!("[debug] {method}");
205 }
206
207 match method {
208 "$/progress" => {
209 if msg["params"]["value"]["kind"] == "end" {
210 Some(())
211 } else {
212 None
213 }
214 }
215 "experimental/serverStatus" => {
216 if msg["params"]["quiescent"] == true {
217 Some(())
218 } else {
219 None
220 }
221 }
222 "textDocument/publishDiagnostics" | "workspace/diagnostic/refresh" => Some(()),
224 _ => None,
225 }
226 })
227 .or(Ok(()))
228}
229
230pub fn parse_methods(response: &Value) -> anyhow::Result<Vec<Method>> {
237 let result = &response["result"];
238 let items: &[Value] = match result {
239 Value::Array(arr) => arr.as_slice(),
240 obj if obj["items"].is_array() => obj["items"].as_array().map_or(&[], Vec::as_slice),
241 _ => anyhow::bail!("Unexpected completion response shape: {response}"),
242 };
243
244 let mut methods: Vec<Method> = Vec::with_capacity(items.len() / 2);
245
246 for item in items {
247 if item["kind"].as_u64() != Some(KIND_METHOD) {
248 continue;
249 }
250 let name = item["label"]
251 .as_str()
252 .unwrap_or("")
253 .split('(')
254 .next()
255 .unwrap_or("")
256 .trim()
257 .to_string();
258 if name.is_empty() {
259 continue;
260 }
261 methods.push(Method {
262 name,
263 detail: item["detail"].as_str().map(str::to_string),
264 documentation: item["documentation"]["value"].as_str().map(str::to_string),
265 });
266 }
267
268 methods.sort_unstable_by(|a, b| a.name.cmp(&b.name));
269 methods.dedup_by(|a, b| a.name == b.name);
270 Ok(methods)
271}
272
273#[must_use]
275pub struct Definition {
276 pub path: String,
278 pub full_path: String,
280 pub line: u32,
282}
283
284pub fn query_definition(
294 type_name: &str,
295 method_name: &str,
296 ra_path: &std::path::Path,
297 deps: Option<&str>,
298) -> anyhow::Result<Option<Definition>> {
299 let probe = Probe::for_definition_with_deps(type_name, method_name, deps)?;
300
301 let mut child = Command::new(ra_path)
302 .stdin(Stdio::piped())
303 .stdout(Stdio::piped())
304 .stderr(Stdio::null())
305 .spawn()?;
306
307 let mut lsp = LspTransport::new(&mut child);
308 let pid = std::process::id();
309
310 lsp.send(&LspTransport::initialize(pid, &probe.root_uri()))?;
312 lsp.recv_until(20, |msg| {
313 (msg["id"] == 1 && msg["result"].is_object()).then_some(())
314 })?;
315
316 lsp.send(&LspTransport::initialized())?;
318 lsp.send(&LspTransport::did_open(&probe.src_uri(), &probe.source()?))?;
319
320 wait_for_indexing(&mut lsp)?;
322
323 let response = {
326 let mut result = Value::Null;
327 for attempt in 1..=10u64 {
328 let req_id = attempt + 2;
329 lsp.send(&LspTransport::definition(
330 req_id,
331 &probe.src_uri(),
332 probe.dot_line,
333 probe.dot_col,
334 ))?;
335
336 let msg = lsp.recv_until(50, |msg| (msg["id"] == req_id).then(|| msg.clone()))?;
337
338 let is_error = msg["error"]["code"].as_i64().is_some();
340 let is_null = msg["result"].is_null();
341
342 if !is_error && !is_null {
343 result = msg;
344 break;
345 }
346
347 if attempt < 10 {
348 if std::env::var("RUST_METH_DEBUG").is_ok() {
349 eprintln!("(attempt {attempt}: not ready, retrying…)");
350 }
351 std::thread::sleep(std::time::Duration::from_millis(500));
352 }
353 }
354 result
355 };
356
357 lsp.send(&LspTransport::shutdown(13))?;
358 let _ = lsp.recv_until(10, |msg| (msg["id"] == 13).then_some(()));
359 lsp.send(&LspTransport::exit())?;
360 let _ = child.wait();
361
362 Ok(parse_definition(&response))
363}
364
365#[must_use]
371pub fn parse_definition(response: &Value) -> Option<Definition> {
372 let result = &response["result"];
373 let location: &Value = match result {
374 Value::Array(arr) if !arr.is_empty() => &arr[0],
375 single if single.is_object() => single,
376 _ => return None,
377 };
378
379 let uri = location["uri"].as_str().unwrap_or("");
380 if uri.is_empty() {
381 return None;
382 }
383
384 let line = u32::try_from(location["range"]["start"]["line"].as_u64().unwrap_or(0))
385 .expect("LSP definition line should fit in u32");
386
387 let full_path_str = uri.strip_prefix("file://").unwrap_or(uri);
388
389 let path = full_path_str
390 .find("/library/")
391 .or_else(|| full_path_str.find("/src/"))
392 .map_or_else(
393 || full_path_str.to_string(),
394 |idx| full_path_str[idx + 1..].to_string(),
395 );
396
397 let full_path = full_path_str.to_string();
398
399 Some(Definition {
400 path,
401 full_path,
402 line,
403 })
404}
405
406#[cfg(test)]
407#[allow(clippy::unwrap_used)]
408mod tests {
409 use super::*;
410 use serde_json::json;
411
412 #[test]
415 fn parse_methods_empty_items_returns_empty_vec() {
416 let resp = json!({ "result": { "items": [], "isIncomplete": false } });
417 let methods = parse_methods(&resp).unwrap();
418 assert!(methods.is_empty());
419 }
420
421 #[test]
422 fn parse_methods_filters_non_method_kinds() {
423 let resp = json!({
425 "result": {
426 "items": [
427 { "kind": 2, "label": "len(…)" },
428 { "kind": 5, "label": "capacity" },
429 { "kind": 9, "label": "Clone" }
430 ]
431 }
432 });
433 let methods = parse_methods(&resp).unwrap();
434 assert_eq!(methods.len(), 1);
435 assert_eq!(methods[0].name, "len");
436 }
437
438 #[test]
439 fn parse_methods_deduplicates_same_name() {
440 let resp = json!({
441 "result": {
442 "items": [
443 { "kind": 2, "label": "clone(…)" },
444 { "kind": 2, "label": "clone(…)" }
445 ]
446 }
447 });
448 let methods = parse_methods(&resp).unwrap();
449 assert_eq!(methods.len(), 1);
450 assert_eq!(methods[0].name, "clone");
451 }
452
453 #[test]
454 fn parse_methods_returns_sorted_names() {
455 let resp = json!({
456 "result": {
457 "items": [
458 { "kind": 2, "label": "zip(…)" },
459 { "kind": 2, "label": "map(…)" },
460 { "kind": 2, "label": "filter(…)" }
461 ]
462 }
463 });
464 let methods = parse_methods(&resp).unwrap();
465 let names: Vec<&str> = methods.iter().map(|m| m.name.as_str()).collect();
466 assert_eq!(names, ["filter", "map", "zip"]);
467 }
468
469 #[test]
470 fn parse_methods_preserves_detail_and_documentation() {
471 let resp = json!({
472 "result": {
473 "items": [{
474 "kind": 2,
475 "label": "len(…)",
476 "detail": "pub fn len(&self) -> usize",
477 "documentation": { "value": "Returns the number of elements." }
478 }]
479 }
480 });
481 let methods = parse_methods(&resp).unwrap();
482 assert_eq!(methods.len(), 1);
483 assert_eq!(
484 methods[0].detail.as_deref(),
485 Some("pub fn len(&self) -> usize")
486 );
487 assert_eq!(
488 methods[0].documentation.as_deref(),
489 Some("Returns the number of elements.")
490 );
491 }
492
493 #[test]
494 fn parse_methods_no_detail_or_docs_is_none() {
495 let resp = json!({
496 "result": { "items": [{ "kind": 2, "label": "len(…)" }] }
497 });
498 let methods = parse_methods(&resp).unwrap();
499 assert!(methods[0].detail.is_none());
500 assert!(methods[0].documentation.is_none());
501 }
502
503 #[test]
504 fn parse_methods_array_result_form() {
505 let resp = json!({
507 "result": [
508 { "kind": 2, "label": "len(…)" },
509 { "kind": 2, "label": "is_empty(…)" }
510 ]
511 });
512 let methods = parse_methods(&resp).unwrap();
513 assert_eq!(methods.len(), 2);
514 }
515
516 #[test]
517 fn parse_methods_skips_empty_label() {
518 let resp = json!({
519 "result": {
520 "items": [
521 { "kind": 2, "label": "" },
522 { "kind": 2, "label": "len(…)" }
523 ]
524 }
525 });
526 let methods = parse_methods(&resp).unwrap();
527 assert_eq!(methods.len(), 1);
528 assert_eq!(methods[0].name, "len");
529 }
530
531 #[test]
532 fn parse_methods_unexpected_shape_returns_error() {
533 let resp = json!({ "result": "this_is_not_valid" });
534 assert!(parse_methods(&resp).is_err());
535 }
536
537 #[test]
541 fn parse_methods_third_party_label_stripped_at_paren() {
542 let resp = json!({
543 "result": {
544 "items": [
545 { "kind": 2, "label": "as_str(…)", "detail": "pub fn as_str(&self) -> &str" },
546 { "kind": 2, "label": "as_object(…)" }
547 ]
548 }
549 });
550 let methods = parse_methods(&resp).unwrap();
551 let names: Vec<&str> = methods.iter().map(|m| m.name.as_str()).collect();
552 assert!(names.contains(&"as_str"));
553 assert!(names.contains(&"as_object"));
554 }
555
556 #[test]
559 fn parse_definition_array_form() {
560 let resp = json!({
561 "result": [{
562 "uri": "file:///home/user/.rustup/toolchains/stable/library/core/src/num/mod.rs",
563 "range": {
564 "start": { "line": 42, "character": 0 },
565 "end": { "line": 42, "character": 10 }
566 }
567 }]
568 });
569 let def = parse_definition(&resp).unwrap();
570 assert_eq!(def.line, 42);
571 assert!(def.path.starts_with("library/"));
572 assert!(!def.full_path.starts_with("file://"));
573 }
574
575 #[test]
576 fn parse_definition_object_form() {
577 let resp = json!({
578 "result": {
579 "uri": "file:///home/user/.rustup/toolchains/stable/library/core/src/str/mod.rs",
580 "range": {
581 "start": { "line": 99, "character": 4 },
582 "end": { "line": 99, "character": 20 }
583 }
584 }
585 });
586 let def = parse_definition(&resp).unwrap();
587 assert_eq!(def.line, 99);
588 assert!(def.path.starts_with("library/"));
589 }
590
591 #[test]
592 fn parse_definition_null_result_returns_none() {
593 let resp = json!({ "result": null });
594 assert!(parse_definition(&resp).is_none());
595 }
596
597 #[test]
598 fn parse_definition_empty_array_returns_none() {
599 let resp = json!({ "result": [] });
600 assert!(parse_definition(&resp).is_none());
601 }
602
603 #[test]
604 fn parse_definition_empty_uri_returns_none() {
605 let resp = json!({
606 "result": [{
607 "uri": "",
608 "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }
609 }]
610 });
611 assert!(parse_definition(&resp).is_none());
612 }
613
614 #[test]
615 fn parse_definition_strips_library_prefix_from_path() {
616 let resp = json!({
617 "result": [{
618 "uri": "file:///home/user/.rustup/toolchains/stable/library/core/src/num/mod.rs",
619 "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }
620 }]
621 });
622 let def = parse_definition(&resp).unwrap();
623 assert!(def.path.starts_with("library/"));
625 assert!(!def.path.starts_with('/'));
626 }
627
628 #[test]
629 fn parse_definition_src_path_fallback() {
630 let resp = json!({
632 "result": [{
633 "uri": "file:///home/user/myproject/src/main.rs",
634 "range": { "start": { "line": 5, "character": 0 }, "end": { "line": 5, "character": 0 } }
635 }]
636 });
637 let def = parse_definition(&resp).unwrap();
638 assert!(def.path.starts_with("src/"));
639 assert_eq!(def.line, 5);
640 }
641
642 #[test]
643 fn parse_definition_full_path_does_not_start_with_file_scheme() {
644 let resp = json!({
645 "result": [{
646 "uri": "file:///home/user/project/src/lib.rs",
647 "range": { "start": { "line": 1, "character": 0 }, "end": { "line": 1, "character": 0 } }
648 }]
649 });
650 let def = parse_definition(&resp).unwrap();
651 assert!(!def.full_path.starts_with("file://"));
652 assert!(def.full_path.starts_with('/'));
653 }
654}