use std::collections::BTreeMap;
use std::path::Path;
use std::time::Duration;
use crate::jsonrpc::RpcClient;
use crate::lang::Lang;
pub fn server_for(lang: Lang) -> Option<(&'static str, &'static [&'static str])> {
match lang {
Lang::Rust => Some(("rust-analyzer", &[])),
Lang::Python => Some(("pyright-langserver", &["--stdio"])),
_ => None,
}
}
#[derive(Debug, Default)]
pub struct Enrichment {
pub verified_refs: BTreeMap<String, u64>,
pub server: String,
}
fn uri(path: &Path) -> String {
let p = path.to_string_lossy().replace('\\', "/");
if p.starts_with('/') {
format!("file://{p}")
} else {
format!("file:///{p}")
}
}
pub fn enrich(
root: &Path,
lang: Lang,
symbols: &[(String, String, u32)],
budget: Duration,
) -> std::io::Result<Enrichment> {
let (program, args) = server_for(lang)
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Unsupported, "no adapter"))?;
let mut client = RpcClient::spawn(program, args, root)?;
client.timeout = budget;
let root_uri = uri(&root.canonicalize()?);
client.request(
"initialize",
serde_json::json!({
"processId": std::process::id(),
"rootUri": root_uri,
"capabilities": {},
}),
)?;
client.notify("initialized", serde_json::json!({}))?;
let mut out = Enrichment {
server: program.to_string(),
..Enrichment::default()
};
for (name, rel, line) in symbols {
let zero_based_line = line.saturating_sub(1);
let file = root.join(rel);
let Ok(text) = std::fs::read_to_string(&file) else {
continue;
};
let file_uri = uri(&file.canonicalize().unwrap_or(file.clone()));
client.notify(
"textDocument/didOpen",
serde_json::json!({
"textDocument": {
"uri": file_uri,
"languageId": lang.name(),
"version": 1,
"text": text,
}
}),
)?;
let col = text
.lines()
.nth(zero_based_line as usize)
.and_then(|l| l.find(name.as_str()))
.unwrap_or(0);
let refs = client.request(
"textDocument/references",
serde_json::json!({
"textDocument": { "uri": file_uri },
"position": { "line": zero_based_line, "character": col },
"context": { "includeDeclaration": false },
}),
)?;
if let Some(arr) = refs.as_array() {
out.verified_refs.insert(name.clone(), arr.len() as u64);
}
}
client.shutdown();
Ok(out)
}
pub fn available(lang: Lang) -> bool {
let Some((program, _)) = server_for(lang) else {
return false;
};
std::process::Command::new(program)
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|s| s.success())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unsupported_language_has_no_server() {
assert!(server_for(Lang::Bash).is_none());
assert!(!available(Lang::Bash));
}
}