use anyhow::{anyhow, Result};
use clap::Parser;
use reqwest::Client;
use rust_analyzer_cli::{
cli::{Cli, Commands},
daemon,
lsp::types::*,
};
use std::path::PathBuf;
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let cli = Cli::parse();
let port = cli.port;
let json_output = cli.json;
let daemon_url = format!("http://127.0.0.1:{}", port);
match cli.command {
Commands::Daemon { workspace } => {
daemon::server::start_daemon_server(workspace, port).await?;
}
Commands::Status => {
let client = Client::new();
let res = client
.get(format!("{}/status", daemon_url))
.send()
.await
.map_err(|_| daemon_not_running_error(port))?;
let res = check_response(res).await?;
let status: DaemonStatusResponse = res.json().await?;
if json_output {
println!("{}", serde_json::to_string_pretty(&status)?);
} else {
println!("rust-analyzer-cli Daemon Status:");
println!(" Status: {}", if status.ready { "READY" } else { "INITIALIZING" });
println!(" Workspace: {}", status.workspace_root);
println!(" rust-analyzer PID: {}", status.process_id);
}
}
Commands::Refresh => {
let client = Client::new();
let res = client
.post(format!("{}/refresh", daemon_url))
.send()
.await
.map_err(|_| daemon_not_running_error(port))?;
let res = check_response(res).await?;
let body: serde_json::Value = res.json().await?;
if json_output {
println!("{}", serde_json::to_string_pretty(&body)?);
} else {
println!("rust-analyzer session refreshed successfully.");
}
}
Commands::Symbol { name, kind, exact } => {
let client = Client::new();
let req = SymbolQueryRequest { name, kind, exact };
let res = client
.post(format!("{}/api/symbol", daemon_url))
.json(&req)
.send()
.await
.map_err(|_| daemon_not_running_error(port))?;
let res = check_response(res).await?;
let symbols: Vec<SymbolItem> = res.json().await?;
if json_output {
println!("{}", serde_json::to_string_pretty(&symbols)?);
} else if symbols.is_empty() {
println!("No matching symbols found.");
} else {
println!("Found {} symbol(s):", symbols.len());
for s in symbols {
let container = s.container_name.map(|c| format!(" ({})", c)).unwrap_or_default();
println!(" [{}] {}{} -> {}:{}:{}", s.kind, s.name, container, s.file, s.line, s.col);
}
}
}
Commands::Outline { file, file_list, output } => {
let client = Client::new();
let mut target_files = Vec::new();
if let Some(f) = file {
target_files.push(f);
} else if let Some(fl) = file_list {
for item in fl.split(',') {
let trimmed = item.trim();
if !trimmed.is_empty() {
target_files.push(PathBuf::from(trimmed));
}
}
} else {
return Err(anyhow!("Please specify --file <path> or --file-list <paths>"));
}
let mut all_outlines = Vec::new();
for f in &target_files {
let req = OutlineQueryRequest { file: f.clone() };
let res = client
.post(format!("{}/api/outline", daemon_url))
.json(&req)
.send()
.await
.map_err(|_| daemon_not_running_error(port))?;
let res = check_response(res).await?;
let items: Vec<OutlineItem> = res.json().await?;
all_outlines.push((f.to_string_lossy().to_string(), items));
}
let output_str = if json_output {
serde_json::to_string_pretty(&all_outlines)?
} else {
let mut buf = String::new();
for (path_str, items) in all_outlines {
buf.push_str(&format!("File: {}\n", path_str));
format_outline_items(&items, 1, &mut buf);
buf.push('\n');
}
buf
};
if let Some(out_path) = output {
std::fs::write(&out_path, &output_str)?;
println!("Outline successfully written to {}", out_path.display());
} else {
print!("{}", output_str);
}
}
Commands::Definition { file, line, col } => {
let client = Client::new();
let req = DefinitionQueryRequest { file, line, col };
let res = client
.post(format!("{}/api/definition", daemon_url))
.json(&req)
.send()
.await
.map_err(|_| daemon_not_running_error(port))?;
let res = check_response(res).await?;
let items: Vec<DefinitionItem> = res.json().await?;
if json_output {
println!("{}", serde_json::to_string_pretty(&items)?);
} else if items.is_empty() {
println!("No definition found at location.");
} else {
println!("Definition location(s):");
for d in items {
println!(" {}:{}:{} (end {}:{})", d.file, d.line, d.col, d.end_line, d.end_col);
}
}
}
Commands::Cursor { file, line, col, mode, depth } => {
let client = Client::new();
let req = CursorQueryRequest { file, line, col, mode, depth };
let res = client
.post(format!("{}/api/cursor", daemon_url))
.json(&req)
.send()
.await
.map_err(|_| daemon_not_running_error(port))?;
let res = check_response(res).await?;
let items: Vec<CursorItem> = res.json().await?;
if json_output {
println!("{}", serde_json::to_string_pretty(&items)?);
} else if items.is_empty() {
println!("No cursor references or calls found.");
} else {
println!("Cursor query results ({} items):", items.len());
for c in items {
let extra = c.caller_or_callee.map(|x| format!(" [{}]", x)).unwrap_or_default();
println!(" [{}] {}{} -> {}:{}:{}", c.kind, c.name, extra, c.file, c.line, c.col);
}
}
}
Commands::TypeHierarchy { file, line, col, mode, depth } => {
let client = Client::new();
let req = TypeHierarchyQueryRequest { file, line, col, mode, depth };
let res = client
.post(format!("{}/api/type-hierarchy", daemon_url))
.json(&req)
.send()
.await
.map_err(|_| daemon_not_running_error(port))?;
let res = check_response(res).await?;
let items: Vec<TypeHierarchyItemResult> = res.json().await?;
if json_output {
println!("{}", serde_json::to_string_pretty(&items)?);
} else if items.is_empty() {
println!("No type hierarchy items found.");
} else {
println!("Type hierarchy results:");
for t in items {
println!(" [{}] {} -> {}:{}:{}", t.kind, t.name, t.file, t.line, t.col);
}
}
}
Commands::Check { target } => {
let client = Client::new();
let req = CheckQueryRequest { target };
let res = client
.post(format!("{}/api/check", daemon_url))
.json(&req)
.send()
.await
.map_err(|_| daemon_not_running_error(port))?;
let res = check_response(res).await?;
let check_res: CheckResponse = res.json().await?;
if json_output {
println!("{}", serde_json::to_string_pretty(&check_res)?);
} else {
println!("Cargo Check Result: {}", if check_res.success { "PASSED" } else { "FAILED" });
for diag in check_res.diagnostics {
let loc = match (diag.file, diag.line, diag.col) {
(Some(f), Some(l), Some(c)) => format!(" at {}:{}:{}", f, l, c),
(Some(f), _, _) => format!(" at {}", f),
_ => String::new(),
};
println!(" [{}] {}{}", diag.level.to_uppercase(), diag.message, loc);
}
}
}
Commands::InitSkill { workspace, dir } => {
let skill_rel = dir.unwrap_or_else(|| PathBuf::from(".agents/skills/rust-codebase-navigation/SKILL.md"));
let target_path = workspace.join(skill_rel);
if let Some(parent) = target_path.parent() {
std::fs::create_dir_all(parent)?;
}
let skill_content = include_str!("../.agents/skills/rust-codebase-navigation/SKILL.md");
std::fs::write(&target_path, skill_content)?;
if json_output {
println!("{}", serde_json::json!({
"success": true,
"path": target_path.to_string_lossy()
}));
} else {
println!("Successfully installed skill to: {}", target_path.display());
}
}
}
Ok(())
}
async fn check_response(res: reqwest::Response) -> Result<reqwest::Response> {
if !res.status().is_success() {
let status = res.status();
let err_text = res.text().await.unwrap_or_else(|_| "Unknown error".to_string());
return Err(anyhow!("Daemon error ({}): {}", status, err_text));
}
Ok(res)
}
fn daemon_not_running_error(port: u16) -> anyhow::Error {
anyhow!(
"rust-analyzer-cli daemon is not running on http://127.0.0.1:{}.\nStart the daemon first with:\n rust-analyzer-cli daemon --workspace .",
port
)
}
fn format_outline_items(items: &[OutlineItem], indent_level: usize, buf: &mut String) {
let indent = " ".repeat(indent_level);
for item in items {
let detail = item.detail.as_ref().map(|d| format!(" ({})", d)).unwrap_or_default();
buf.push_str(&format!("{}[{}] {}{} at line {}:{}\n", indent, item.kind, item.name, detail, item.line, item.col));
format_outline_items(&item.children, indent_level + 1, buf);
}
}