use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::OnceLock;
use serde_json::Value;
use crate::lsp::LspTransport;
use crate::probe::Probe;
const KIND_METHOD: u64 = 2;
static RA_PATH_CACHE: OnceLock<PathBuf> = OnceLock::new();
#[derive(serde::Serialize)]
pub struct Method {
pub name: String,
pub detail: Option<String>,
pub documentation: Option<String>,
}
fn rustup_rust_analyzer() -> Option<PathBuf> {
let out = Command::new("rustup")
.args(["which", "rust-analyzer"])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let path = String::from_utf8_lossy(&out.stdout).trim().to_string();
(!path.is_empty()).then(|| path.into())
}
pub fn find_rust_analyzer() -> anyhow::Result<PathBuf> {
if let Some(path) = RA_PATH_CACHE.get() {
return Ok(path.clone());
}
let path = if let Ok(path) = which("rust-analyzer") {
path
} else if let Some(path) = rustup_rust_analyzer() {
path
} else {
anyhow::bail!(
"rust-analyzer not found.\n\
Install it with: rustup component add rust-analyzer\n\
or ensure it is on your PATH."
)
};
Ok(RA_PATH_CACHE.get_or_init(|| path).clone())
}
#[cfg(unix)]
fn which(name: &str) -> anyhow::Result<std::path::PathBuf> {
let out = Command::new("which").arg(name).output()?;
anyhow::ensure!(out.status.success(), "not found");
let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
Ok(s.into())
}
pub fn query_methods(
type_name: &str,
ra_path: &std::path::Path,
deps: Option<&str>,
) -> anyhow::Result<Vec<Method>> {
let probe = Probe::new_with_deps(type_name, deps)?;
let mut child = Command::new(ra_path)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()?;
let mut lsp = LspTransport::new(&mut child);
let pid = std::process::id();
lsp.send(&LspTransport::initialize(pid, &probe.root_uri()))?;
lsp.recv_until(20, |msg| {
(msg["id"] == 1 && msg["result"].is_object()).then_some(())
})?;
lsp.send(&LspTransport::initialized())?;
lsp.send(&LspTransport::did_open(&probe.src_uri(), &probe.source()?))?;
wait_for_indexing(&mut lsp)?;
let completion_response = {
let mut response = Value::Null;
for attempt in 1..=10u64 {
let req_id = attempt + 2;
lsp.send(&LspTransport::completion(
req_id,
&probe.src_uri(),
probe.dot_line,
probe.dot_col,
))?;
let msg = lsp.recv_until(50, |msg| (msg["id"] == req_id).then(|| msg.clone()))?;
let has_items = msg["result"]["items"]
.as_array()
.is_some_and(|a| !a.is_empty());
if has_items {
response = msg;
break;
}
if attempt < 10 {
let delay = match attempt {
1 => 50, 2 => 100, 3 => 200, _ => 300, };
std::thread::sleep(std::time::Duration::from_millis(delay));
}
}
response
};
lsp.send(&LspTransport::shutdown(13))?;
let _ = lsp.recv_until(10, |msg| (msg["id"] == 13).then_some(()));
lsp.send(&LspTransport::exit())?;
let _ = child.wait();
parse_methods(&completion_response)
}
fn wait_for_indexing(lsp: &mut LspTransport) -> anyhow::Result<()> {
let debug = std::env::var("RUST_METH_DEBUG").is_ok();
let start = std::time::Instant::now();
let timeout = std::time::Duration::from_secs(10);
lsp.recv_until(200, |msg| {
if start.elapsed() > timeout {
return Some(()); }
let method = msg["method"].as_str().unwrap_or("");
if debug {
eprintln!("[debug] {method}");
}
match method {
"$/progress" => {
if msg["params"]["value"]["kind"] == "end" {
Some(())
} else {
None
}
}
"experimental/serverStatus" => {
if msg["params"]["quiescent"] == true {
Some(())
} else {
None
}
}
"textDocument/publishDiagnostics" | "workspace/diagnostic/refresh" => Some(()),
_ => None,
}
})
.or(Ok(()))
}
pub fn parse_methods(response: &Value) -> anyhow::Result<Vec<Method>> {
let result = &response["result"];
let items: &[Value] = match result {
Value::Array(arr) => arr.as_slice(),
obj if obj["items"].is_array() => obj["items"].as_array().map_or(&[], Vec::as_slice),
_ => anyhow::bail!("Unexpected completion response shape: {response}"),
};
let mut methods: Vec<Method> = Vec::with_capacity(items.len() / 2);
for item in items {
if item["kind"].as_u64() != Some(KIND_METHOD) {
continue;
}
let name = item["label"]
.as_str()
.unwrap_or("")
.split('(')
.next()
.unwrap_or("")
.trim()
.to_string();
if name.is_empty() {
continue;
}
methods.push(Method {
name,
detail: item["detail"].as_str().map(str::to_string),
documentation: item["documentation"]["value"].as_str().map(str::to_string),
});
}
methods.sort_unstable_by(|a, b| a.name.cmp(&b.name));
methods.dedup_by(|a, b| a.name == b.name);
Ok(methods)
}
#[must_use]
pub struct Definition {
pub path: String,
pub full_path: String,
pub line: u32,
}
pub fn query_definition(
type_name: &str,
method_name: &str,
ra_path: &std::path::Path,
deps: Option<&str>,
) -> anyhow::Result<Option<Definition>> {
let probe = Probe::for_definition_with_deps(type_name, method_name, deps)?;
let mut child = Command::new(ra_path)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()?;
let mut lsp = LspTransport::new(&mut child);
let pid = std::process::id();
lsp.send(&LspTransport::initialize(pid, &probe.root_uri()))?;
lsp.recv_until(20, |msg| {
(msg["id"] == 1 && msg["result"].is_object()).then_some(())
})?;
lsp.send(&LspTransport::initialized())?;
lsp.send(&LspTransport::did_open(&probe.src_uri(), &probe.source()?))?;
wait_for_indexing(&mut lsp)?;
let response = {
let mut result = Value::Null;
for attempt in 1..=10u64 {
let req_id = attempt + 2;
lsp.send(&LspTransport::definition(
req_id,
&probe.src_uri(),
probe.dot_line,
probe.dot_col,
))?;
let msg = lsp.recv_until(50, |msg| (msg["id"] == req_id).then(|| msg.clone()))?;
let is_error = msg["error"]["code"].as_i64().is_some();
let is_null = msg["result"].is_null();
if !is_error && !is_null {
result = msg;
break;
}
if attempt < 10 {
if std::env::var("RUST_METH_DEBUG").is_ok() {
eprintln!("(attempt {attempt}: not ready, retrying…)");
}
std::thread::sleep(std::time::Duration::from_millis(500));
}
}
result
};
lsp.send(&LspTransport::shutdown(13))?;
let _ = lsp.recv_until(10, |msg| (msg["id"] == 13).then_some(()));
lsp.send(&LspTransport::exit())?;
let _ = child.wait();
Ok(parse_definition(&response))
}
#[must_use]
pub fn parse_definition(response: &Value) -> Option<Definition> {
let result = &response["result"];
let location: &Value = match result {
Value::Array(arr) if !arr.is_empty() => &arr[0],
single if single.is_object() => single,
_ => return None,
};
let uri = location["uri"].as_str().unwrap_or("");
if uri.is_empty() {
return None;
}
let line = u32::try_from(location["range"]["start"]["line"].as_u64().unwrap_or(0))
.expect("LSP definition line should fit in u32");
let full_path_str = uri.strip_prefix("file://").unwrap_or(uri);
let path = full_path_str
.find("/library/")
.or_else(|| full_path_str.find("/src/"))
.map_or_else(
|| full_path_str.to_string(),
|idx| full_path_str[idx + 1..].to_string(),
);
let full_path = full_path_str.to_string();
Some(Definition {
path,
full_path,
line,
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn parse_methods_empty_items_returns_empty_vec() {
let resp = json!({ "result": { "items": [], "isIncomplete": false } });
let methods = parse_methods(&resp).unwrap();
assert!(methods.is_empty());
}
#[test]
fn parse_methods_filters_non_method_kinds() {
let resp = json!({
"result": {
"items": [
{ "kind": 2, "label": "len(…)" },
{ "kind": 5, "label": "capacity" },
{ "kind": 9, "label": "Clone" }
]
}
});
let methods = parse_methods(&resp).unwrap();
assert_eq!(methods.len(), 1);
assert_eq!(methods[0].name, "len");
}
#[test]
fn parse_methods_deduplicates_same_name() {
let resp = json!({
"result": {
"items": [
{ "kind": 2, "label": "clone(…)" },
{ "kind": 2, "label": "clone(…)" }
]
}
});
let methods = parse_methods(&resp).unwrap();
assert_eq!(methods.len(), 1);
assert_eq!(methods[0].name, "clone");
}
#[test]
fn parse_methods_returns_sorted_names() {
let resp = json!({
"result": {
"items": [
{ "kind": 2, "label": "zip(…)" },
{ "kind": 2, "label": "map(…)" },
{ "kind": 2, "label": "filter(…)" }
]
}
});
let methods = parse_methods(&resp).unwrap();
let names: Vec<&str> = methods.iter().map(|m| m.name.as_str()).collect();
assert_eq!(names, ["filter", "map", "zip"]);
}
#[test]
fn parse_methods_preserves_detail_and_documentation() {
let resp = json!({
"result": {
"items": [{
"kind": 2,
"label": "len(…)",
"detail": "pub fn len(&self) -> usize",
"documentation": { "value": "Returns the number of elements." }
}]
}
});
let methods = parse_methods(&resp).unwrap();
assert_eq!(methods.len(), 1);
assert_eq!(
methods[0].detail.as_deref(),
Some("pub fn len(&self) -> usize")
);
assert_eq!(
methods[0].documentation.as_deref(),
Some("Returns the number of elements.")
);
}
#[test]
fn parse_methods_no_detail_or_docs_is_none() {
let resp = json!({
"result": { "items": [{ "kind": 2, "label": "len(…)" }] }
});
let methods = parse_methods(&resp).unwrap();
assert!(methods[0].detail.is_none());
assert!(methods[0].documentation.is_none());
}
#[test]
fn parse_methods_array_result_form() {
let resp = json!({
"result": [
{ "kind": 2, "label": "len(…)" },
{ "kind": 2, "label": "is_empty(…)" }
]
});
let methods = parse_methods(&resp).unwrap();
assert_eq!(methods.len(), 2);
}
#[test]
fn parse_methods_skips_empty_label() {
let resp = json!({
"result": {
"items": [
{ "kind": 2, "label": "" },
{ "kind": 2, "label": "len(…)" }
]
}
});
let methods = parse_methods(&resp).unwrap();
assert_eq!(methods.len(), 1);
assert_eq!(methods[0].name, "len");
}
#[test]
fn parse_methods_unexpected_shape_returns_error() {
let resp = json!({ "result": "this_is_not_valid" });
assert!(parse_methods(&resp).is_err());
}
#[test]
fn parse_methods_third_party_label_stripped_at_paren() {
let resp = json!({
"result": {
"items": [
{ "kind": 2, "label": "as_str(…)", "detail": "pub fn as_str(&self) -> &str" },
{ "kind": 2, "label": "as_object(…)" }
]
}
});
let methods = parse_methods(&resp).unwrap();
let names: Vec<&str> = methods.iter().map(|m| m.name.as_str()).collect();
assert!(names.contains(&"as_str"));
assert!(names.contains(&"as_object"));
}
#[test]
fn parse_definition_array_form() {
let resp = json!({
"result": [{
"uri": "file:///home/user/.rustup/toolchains/stable/library/core/src/num/mod.rs",
"range": {
"start": { "line": 42, "character": 0 },
"end": { "line": 42, "character": 10 }
}
}]
});
let def = parse_definition(&resp).unwrap();
assert_eq!(def.line, 42);
assert!(def.path.starts_with("library/"));
assert!(!def.full_path.starts_with("file://"));
}
#[test]
fn parse_definition_object_form() {
let resp = json!({
"result": {
"uri": "file:///home/user/.rustup/toolchains/stable/library/core/src/str/mod.rs",
"range": {
"start": { "line": 99, "character": 4 },
"end": { "line": 99, "character": 20 }
}
}
});
let def = parse_definition(&resp).unwrap();
assert_eq!(def.line, 99);
assert!(def.path.starts_with("library/"));
}
#[test]
fn parse_definition_null_result_returns_none() {
let resp = json!({ "result": null });
assert!(parse_definition(&resp).is_none());
}
#[test]
fn parse_definition_empty_array_returns_none() {
let resp = json!({ "result": [] });
assert!(parse_definition(&resp).is_none());
}
#[test]
fn parse_definition_empty_uri_returns_none() {
let resp = json!({
"result": [{
"uri": "",
"range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }
}]
});
assert!(parse_definition(&resp).is_none());
}
#[test]
fn parse_definition_strips_library_prefix_from_path() {
let resp = json!({
"result": [{
"uri": "file:///home/user/.rustup/toolchains/stable/library/core/src/num/mod.rs",
"range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }
}]
});
let def = parse_definition(&resp).unwrap();
assert!(def.path.starts_with("library/"));
assert!(!def.path.starts_with('/'));
}
#[test]
fn parse_definition_src_path_fallback() {
let resp = json!({
"result": [{
"uri": "file:///home/user/myproject/src/main.rs",
"range": { "start": { "line": 5, "character": 0 }, "end": { "line": 5, "character": 0 } }
}]
});
let def = parse_definition(&resp).unwrap();
assert!(def.path.starts_with("src/"));
assert_eq!(def.line, 5);
}
#[test]
fn parse_definition_full_path_does_not_start_with_file_scheme() {
let resp = json!({
"result": [{
"uri": "file:///home/user/project/src/lib.rs",
"range": { "start": { "line": 1, "character": 0 }, "end": { "line": 1, "character": 0 } }
}]
});
let def = parse_definition(&resp).unwrap();
assert!(!def.full_path.starts_with("file://"));
assert!(def.full_path.starts_with('/'));
}
}