use anyhow::{Context, Result, anyhow};
use clap::Parser;
use reqwest::Client;
use rust_analyzer_cli::{
cli::{Cli, Commands},
daemon,
lsp::client::format_body_with_line_numbers,
lsp::types::*,
};
use std::path::{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_with_output(workspace, port, json_output).await?;
}
Commands::Status => {
let client = Client::new();
let res = client
.get(format!("{}/status", daemon_url))
.send()
.await
.map_err(|error| daemon_connection_error(port, "GET /status", error))?;
let res = check_response(res, "GET /status").await?;
let status: DaemonStatusResponse = res
.json()
.await
.context("Failed to decode daemon status from GET /status")?;
if json_output {
println!("{}", serde_json::to_string_pretty(&status)?);
} else {
println!("rust-analyzer-cli Daemon Status:");
println!(" Status: {:?}", status.state);
println!(" Workspace: {}", status.workspace_root);
println!(
" rust-analyzer PID: {}",
status
.process_id
.map(|pid| pid.to_string())
.unwrap_or_else(|| "not started".to_string())
);
if let Some(error) = status.error {
println!(" Error: {}", error);
}
}
}
Commands::Refresh => {
let client = Client::new();
let res = client
.post(format!("{}/refresh", daemon_url))
.send()
.await
.map_err(|error| daemon_connection_error(port, "POST /refresh", error))?;
let res = check_response(res, "POST /refresh").await?;
let body: serde_json::Value = res
.json()
.await
.context("Failed to decode daemon response from POST /refresh")?;
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,
include_body: body,
max_lines,
};
let res = client
.post(format!("{}/api/symbol", daemon_url))
.json(&req)
.send()
.await
.map_err(|error| daemon_connection_error(port, "POST /api/symbol", error))?;
let res = check_response(res, "POST /api/symbol").await?;
let symbols: Vec<SymbolItem> = res
.json()
.await
.context("Failed to decode symbol results from POST /api/symbol")?;
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(),
include_body: body,
max_lines,
};
let res = client
.post(format!("{}/api/outline", daemon_url))
.json(&req)
.send()
.await
.map_err(|error| daemon_connection_error(port, "POST /api/outline", error))?;
let res = check_response(res, "POST /api/outline").await?;
let items: Vec<OutlineItem> = res
.json()
.await
.context("Failed to decode outline results from POST /api/outline")?;
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).with_context(|| {
format!(
"Failed to write outline output to {}. Check that the parent directory exists and is writable.",
out_path.display()
)
})?;
if json_output {
println!("{}", outline_output_summary(&out_path));
} else {
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,
include_body: body,
max_lines,
};
let res = client
.post(format!("{}/api/definition", daemon_url))
.json(&req)
.send()
.await
.map_err(|error| daemon_connection_error(port, "POST /api/definition", error))?;
let res = check_response(res, "POST /api/definition").await?;
let items: Vec<DefinitionItem> = res
.json()
.await
.context("Failed to decode definition results from POST /api/definition")?;
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(|error| daemon_connection_error(port, "POST /api/body", error))?;
let res = check_response(res, "POST /api/body").await?;
let body_item: BodyItem = res
.json()
.await
.context("Failed to decode body results from POST /api/body")?;
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::Hover { file, line, col } => {
let client = Client::new();
let req = HoverQueryRequest { file, line, col };
let res = client
.post(format!("{}/api/hover", daemon_url))
.json(&req)
.send()
.await
.map_err(|error| daemon_connection_error(port, "POST /api/hover", error))?;
let res = check_response(res, "POST /api/hover").await?;
let hover: Option<HoverItem> = res
.json()
.await
.context("Failed to decode hover results from POST /api/hover")?;
if json_output {
println!("{}", serde_json::to_string_pretty(&hover)?);
} else if let Some(item) = hover {
println!("Hover: {}:{}:{}", item.file, item.line, item.col);
println!("{}", item.contents);
if let Some(range) = item.range {
println!(
"Range: {}:{}-{}:{}",
range.start_line, range.start_col, range.end_line, range.end_col
);
}
} else {
println!("No hover information found.");
}
}
Commands::References { file, line, col } => {
let client = Client::new();
let req = ReferenceQueryRequest { file, line, col };
let res = client
.post(format!("{}/api/references", daemon_url))
.json(&req)
.send()
.await
.map_err(|error| daemon_connection_error(port, "POST /api/references", error))?;
let res = check_response(res, "POST /api/references").await?;
let items: Vec<ReferenceItem> = res
.json()
.await
.context("Failed to decode reference results from POST /api/references")?;
if json_output {
println!("{}", serde_json::to_string_pretty(&items)?);
} else if items.is_empty() {
println!("No references found.");
} else {
println!("References ({} items):", items.len());
for reference in items {
println!(
" {}:{}:{} (end {}:{})",
reference.file,
reference.line,
reference.col,
reference.end_line,
reference.end_col
);
}
}
}
Commands::Calls {
file,
line,
col,
direction,
depth,
} => {
let client = Client::new();
let req = CallQueryRequest {
file,
line,
col,
direction,
depth,
};
let res = client
.post(format!("{}/api/calls", daemon_url))
.json(&req)
.send()
.await
.map_err(|error| daemon_connection_error(port, "POST /api/calls", error))?;
let res = check_response(res, "POST /api/calls").await?;
let items: Vec<CallItem> = res
.json()
.await
.context("Failed to decode call results from POST /api/calls")?;
if json_output {
println!("{}", serde_json::to_string_pretty(&items)?);
} else if items.is_empty() {
println!("No {} calls found.", direction);
} else {
println!("{} calls ({} items):", direction, items.len());
for c in items {
println!(
" [{}] {} depth {} -> {}:{}:{} (end {}:{})",
c.kind, c.name, c.depth, c.file, c.line, c.col, c.end_line, c.end_col
);
}
}
}
Commands::Relations {
file,
line,
col,
mode,
} => {
let client = Client::new();
let req = RelationQueryRequest {
file,
line,
col,
mode,
};
let res = client
.post(format!("{}/api/relations", daemon_url))
.json(&req)
.send()
.await
.map_err(|error| daemon_connection_error(port, "POST /api/relations", error))?;
let res = check_response(res, "POST /api/relations").await?;
let items: Vec<RelationItem> = res
.json()
.await
.context("Failed to decode relation results from POST /api/relations")?;
if json_output {
println!("{}", serde_json::to_string_pretty(&items)?);
} else if items.is_empty() {
println!("No Rust relation implementations found.");
} else {
println!("Rust relation implementations:");
for t in items {
println!(
" {}:{}:{} (end {}:{})",
t.file, t.line, t.col, t.end_line, t.end_col
);
}
}
}
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).with_context(|| {
format!(
"Failed to create skill directory {}. Check that the workspace is writable.",
parent.display()
)
})?;
}
let skill_content = include_str!("../.agents/skills/rust-codebase-navigation/SKILL.md");
std::fs::write(&target_path, skill_content).with_context(|| {
format!(
"Failed to write project skill to {}. Check that the workspace is writable.",
target_path.display()
)
})?;
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, operation: &str) -> Result<reqwest::Response> {
if !res.status().is_success() {
let status = res.status();
let url = res.url().to_string();
let err_text = res
.text()
.await
.unwrap_or_else(|error| format!("Unable to read daemon error body: {error}"));
return Err(anyhow!(
"Daemon request failed: {operation} {url} returned {status}. Response: {err_text}"
));
}
Ok(res)
}
fn daemon_connection_error(port: u16, operation: &str, source: reqwest::Error) -> anyhow::Error {
anyhow!(
"Failed to {operation} at http://127.0.0.1:{port}: {source}\nThe daemon may not be running or may be unavailable. Start it with:\n rust-analyzer-cli daemon --workspace .\nThen retry the command."
)
}
fn outline_output_summary(path: &Path) -> serde_json::Value {
serde_json::json!({
"success": true,
"path": path.to_string_lossy(),
"format": "json"
})
}
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);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_outline_output_summary_is_valid_json() {
let summary = outline_output_summary(&PathBuf::from("out/outline.json"));
let encoded = serde_json::to_string(&summary).unwrap();
let decoded: serde_json::Value = serde_json::from_str(&encoded).unwrap();
assert_eq!(decoded["success"], true);
assert_eq!(decoded["format"], "json");
assert_eq!(decoded["path"], "out/outline.json");
}
}