use anyhow::{Result, anyhow};
use clap::Parser;
use reqwest::Client;
use rust_analyzer_cli::{
cli::{Cli, Commands},
daemon,
lsp::client::{extract_body_snippet, format_body_with_line_numbers},
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,
body,
max_lines,
} => {
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 mut symbols: Vec<SymbolItem> = res.json().await?;
if body {
for s in &mut symbols {
let (snippet, _, _, _) = extract_body_snippet(
PathBuf::from(&s.file).as_path(),
s.line,
s.line,
max_lines,
);
if !snippet.is_empty() {
s.body = Some(snippet);
}
}
}
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
);
if let Some(ref b) = s.body {
println!("{}", format_body_with_line_numbers(b, s.line));
}
}
}
}
Commands::Outline {
file,
file_list,
output,
body,
max_lines,
} => {
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 mut items: Vec<OutlineItem> = res.json().await?;
if body {
for item in &mut items {
let (snippet, _, _, _) = extract_body_snippet(
PathBuf::from(f).as_path(),
item.line,
item.line,
max_lines,
);
if !snippet.is_empty() {
item.body = Some(snippet);
}
}
}
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, body);
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,
body,
max_lines,
no_line_numbers,
} => {
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 mut items: Vec<DefinitionItem> = res.json().await?;
if body {
for item in &mut items {
let (snippet, _, _, _) = extract_body_snippet(
PathBuf::from(&item.file).as_path(),
item.line,
item.end_line,
max_lines,
);
if !snippet.is_empty() {
item.body = Some(snippet.clone());
item.snippet = Some(snippet);
}
}
}
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
);
if let Some(ref b) = d.body {
if no_line_numbers {
println!("{}", b);
} else {
println!("{}", format_body_with_line_numbers(b, d.line));
}
}
}
}
}
Commands::Body {
file,
line,
col,
max_lines,
no_line_numbers,
} => {
let client = Client::new();
let req = BodyQueryRequest {
file: file.clone(),
line,
col,
max_lines,
};
let res = client
.post(format!("{}/api/body", daemon_url))
.json(&req)
.send()
.await
.map_err(|_| daemon_not_running_error(port))?;
let res = check_response(res).await?;
let body_item: BodyItem = res.json().await?;
if json_output {
println!("{}", serde_json::to_string_pretty(&body_item)?);
} else {
println!(
"Body: {} ({}:{}-{}:{})",
body_item.file,
body_item.line,
body_item.col,
body_item.end_line,
body_item.end_col
);
if no_line_numbers {
println!("{}", body_item.body);
} else {
println!(
"{}",
format_body_with_line_numbers(&body_item.body, body_item.line)
);
}
if body_item.is_truncated {
println!(
"... (truncated at {} lines, total {} lines)",
body_item.body.lines().count(),
body_item.total_lines
);
}
}
}
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,
show_body: bool,
) {
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
));
if show_body && let Some(ref b) = item.body {
buf.push_str(&format_body_with_line_numbers(b, item.line));
buf.push('\n');
}
format_outline_items(&item.children, indent_level + 1, buf, show_body);
}
}