use serde_json::{json, Value};
use crate::ast;
use crate::tools::output::OutputGuard;
use crate::tools::{require_str_param, Tool, ToolContext};
use crate::fs::{
classify_reference_path, get_lsp_client, path_in_excluded_dir, require_path_param,
resolve_library_roots, resolve_read_path, uri_to_path, LspTimer,
};
use crate::symbol::query::find_unique_symbol_by_name_path;
pub struct References;
#[async_trait::async_trait]
impl Tool for References {
fn name(&self) -> &str {
"references"
}
fn description(&self) -> &str {
"Find all usages of a symbol. Requires symbol and file."
}
fn input_schema(&self) -> Value {
json!({
"type": "object",
"required": ["symbol", "path"],
"properties": {
"symbol": { "type": "string", "description": "Symbol identifier (e.g. 'MyStruct/my_method')" },
"path": { "type": "string", "description": "File containing the symbol" },
"detail_level": { "type": "string", "description": "'full' for bodies (default: compact)" },
"offset": { "type": "integer", "description": "Pagination offset" },
"limit": { "type": "integer", "description": "Max results (default 50)" },
"scope": { "type": "string", "description": "'project' (default), 'libraries', 'all', or 'lib:<name>'", "default": "project" }
}
})
}
async fn call(&self, input: Value, ctx: &ToolContext) -> anyhow::Result<Value> {
let name_path = require_str_param(&input, "symbol")?;
let rel_path = require_path_param(&input)?;
let scope = crate::library::scope::Scope::parse(input["scope"].as_str());
let full_path = resolve_read_path(&ctx.agent, rel_path).await?;
let raw_lang = ast::detect_language(&full_path)
.ok_or_else(|| anyhow::anyhow!("unsupported language"))?;
let root = ctx.agent.require_project_root().await?;
let (client, lang) = get_lsp_client(&ctx.agent, &*ctx.lsp, &full_path).await?;
let timer = LspTimer::start();
let symbols = client.document_symbols(&full_path, &lang).await?;
timer.record(&*ctx.lsp, raw_lang, &root).await;
let sym = find_unique_symbol_by_name_path(&symbols, name_path)?;
let refs = client
.references(&full_path, sym.start_line, sym.start_col, &lang)
.await?;
let lib_roots =
resolve_library_roots(&crate::library::scope::Scope::All, &ctx.agent).await?;
let total_raw = refs.len();
let refs: Vec<_> = refs
.into_iter()
.filter(|loc| {
uri_to_path(loc.uri.as_str())
.map(|p| !path_in_excluded_dir(&p))
.unwrap_or(true)
})
.collect();
let excluded = total_raw - refs.len();
let refs: Vec<_> = refs
.into_iter()
.filter(|loc| {
let Some(path) = uri_to_path(loc.uri.as_str()) else {
return true; };
let (classification, _) = classify_reference_path(&path, &root, &lib_roots);
match &scope {
crate::library::scope::Scope::Project => classification == "project",
crate::library::scope::Scope::Libraries => classification.starts_with("lib:"),
crate::library::scope::Scope::All => true,
crate::library::scope::Scope::Library(name) => {
classification == format!("lib:{}", name)
}
}
})
.collect();
let locations: Vec<Value> = refs
.iter()
.map(|loc| {
let file = uri_to_path(loc.uri.as_str())
.map(|p| {
let (_, display) = classify_reference_path(&p, &root, &lib_roots);
display
})
.unwrap_or_else(|| loc.uri.as_str().to_string());
let context = uri_to_path(loc.uri.as_str())
.and_then(|p| std::fs::read_to_string(p).ok())
.map(|src| {
let lines: Vec<&str> = src.lines().collect();
let line = loc.range.start.line as usize;
lines.get(line).unwrap_or(&"").to_string()
})
.unwrap_or_default();
json!({
"file": file,
"line": loc.range.start.line + 1,
"column": loc.range.start.character,
"context": context,
})
})
.collect();
let guard = OutputGuard::from_input(&input);
let budget = guard.max_results;
use crate::tools::file_group::{cap_grouped, group_by_file, groups_to_json};
let (visible, total, files) = cap_grouped(locations, budget);
let truncated = total > visible.len();
let groups = group_by_file(&visible);
let file_groups = groups_to_json(&groups);
let mut result = json!({
"file_groups": file_groups,
"total": total,
"files": files,
});
if excluded > 0 {
result["excluded_from_build_dirs"] = json!(excluded);
}
if truncated {
let overflow = json!({
"shown": visible.len(),
"total": total,
"hint": "This symbol has many references. Use detail_level='full' with offset/limit to paginate",
});
result["overflow"] = overflow;
}
Ok(result)
}
fn format_compact(&self, result: &Value) -> Option<String> {
use crate::tools::file_group::{groups_from_json, render_grouped};
let file_groups_arr = result["file_groups"].as_array()?;
if file_groups_arr.is_empty() {
return Some("0 references".to_string());
}
let groups = groups_from_json(file_groups_arr);
let total = result["total"].as_u64().unwrap_or(0) as usize;
let files = result["files"].as_u64().unwrap_or(groups.len() as u64) as usize;
let noun = if total == 1 {
"reference"
} else {
"references"
};
let render_item = |item: &Value| -> String {
let line = item["line"].as_u64().unwrap_or(0);
let context = item["context"].as_str().unwrap_or("").trim();
format!(" {line:>5} {context}")
};
Some(render_grouped(&groups, total, files, noun, render_item))
}
fn availability(&self, _caps: &crate::tools::ToolCapabilities) -> crate::tools::Availability {
crate::tools::Availability::RequiresLsp
}
}