use crate::cli::init::ensure_initialized;
use crate::db::Database;
use anyhow::Result;
use colored::Colorize;
use serde::Serialize;
use std::collections::HashSet;
use std::env;
#[derive(Serialize)]
struct JsonDefinition {
file_path: String,
start_line: u32,
end_line: u32,
chunk_type: String,
symbol_name: Option<String>,
signature: Option<String>,
module_path: Option<String>,
}
#[derive(Serialize)]
struct JsonImportedBy {
source_file: String,
target_symbol: String,
edge_type: String,
}
#[derive(Serialize)]
struct JsonImport {
target_symbol: String,
edge_type: String,
target_file: Option<String>,
}
#[derive(Serialize)]
struct JsonRefsOutput {
query: String,
definitions: Vec<JsonDefinition>,
imported_by: Vec<JsonImportedBy>,
imports: Vec<JsonImport>,
}
pub fn run(symbol: &str, json: bool) -> Result<()> {
let project_root = env::current_dir()?;
ensure_initialized(&project_root, false)?;
let db = Database::open(&project_root)?;
let def_chunks = db.find_chunks_by_symbol_name(symbol)?;
let unique_def_files: HashSet<&str> = def_chunks.iter().map(|c| c.file_path.as_str()).collect();
let imported_by = db.get_edges_by_target_symbol(symbol)?;
let imports = if unique_def_files.len() == 1 {
let file = unique_def_files.iter().next().unwrap();
db.get_edges_by_source_file(file)?
} else {
Vec::new()
};
if json {
let output = JsonRefsOutput {
query: symbol.to_string(),
definitions: def_chunks
.iter()
.map(|c| JsonDefinition {
file_path: c.file_path.clone(),
start_line: c.start_line,
end_line: c.end_line,
chunk_type: c.chunk_type.clone(),
symbol_name: c.symbol_name.clone(),
signature: c.signature.clone(),
module_path: c.module_path.clone(),
})
.collect(),
imported_by: imported_by
.iter()
.map(|e| JsonImportedBy {
source_file: e.source_file.clone(),
target_symbol: e.target_symbol.clone(),
edge_type: e.edge_type.clone(),
})
.collect(),
imports: imports
.iter()
.map(|e| JsonImport {
target_symbol: e.target_symbol.clone(),
edge_type: e.edge_type.clone(),
target_file: e.target_file.clone(),
})
.collect(),
};
println!("{}", serde_json::to_string_pretty(&output)?);
return Ok(());
}
println!("{} References for: {}", "→".blue(), symbol.cyan());
println!();
println!("{}", "Definitions:".bold());
if def_chunks.is_empty() {
println!(" {}", "(none found)".dimmed());
} else {
for chunk in &def_chunks {
let sym_label = chunk.symbol_name.as_deref().unwrap_or("").to_string();
println!(
" {}:{}-{} ({}) {}",
chunk.file_path.blue(),
chunk.start_line,
chunk.end_line,
chunk.chunk_type.magenta(),
sym_label.bold()
);
if let Some(ref mp) = chunk.module_path {
if !mp.is_empty() {
println!(" module: {}", mp.dimmed());
}
}
if let Some(ref sig) = chunk.signature {
if !sig.is_empty() {
println!(" signature: {}", sig.dimmed());
}
}
}
}
println!();
println!("{}", "Imported by:".bold());
if imported_by.is_empty() {
println!(" {}", "(none found)".dimmed());
} else {
for edge in &imported_by {
println!(
" {} {} {} ({})",
edge.source_file.blue(),
"→".dimmed(),
edge.target_symbol.cyan(),
edge.edge_type.dimmed()
);
}
}
println!();
match unique_def_files.len() {
0 => {
println!("{}", "Imports:".bold());
println!(
" {}",
"(no definition found — cannot determine imports)".dimmed()
);
}
1 => {
let file = unique_def_files.iter().next().unwrap();
println!("{} (from {})", "Imports:".bold(), file.blue());
if imports.is_empty() {
println!(" {}", "(none found)".dimmed());
} else {
for edge in &imports {
let target_info = match &edge.target_file {
Some(f) => format!("{} → {}", edge.target_symbol.cyan(), f.blue()),
None => edge.target_symbol.cyan().to_string(),
};
println!(" {} ({})", target_info, edge.edge_type.dimmed());
}
}
}
n => {
println!("{}", "Imports:".bold());
println!(
" {} ({} definition files found — showing imports requires a single file)",
"!".yellow(),
n
);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::{CodeChunk, SymbolEdge};
use chrono::Utc;
fn make_chunk(file_path: &str, symbol_name: &str, chunk_type: &str) -> CodeChunk {
let ts = Utc::now().to_rfc3339();
CodeChunk {
id: uuid::Uuid::new_v4().to_string(),
file_path: file_path.to_string(),
content: String::new(),
start_line: 1,
end_line: 5,
chunk_type: chunk_type.to_string(),
language: "rust".to_string(),
symbol_name: Some(symbol_name.to_string()),
content_hash: "abc".to_string(),
indexed_at: ts,
parent_symbol: None,
signature: Some(format!("pub struct {}", symbol_name)),
doc_comment: None,
module_path: Some("db".to_string()),
}
}
fn make_edge(source_file: &str, target_symbol: &str) -> SymbolEdge {
SymbolEdge {
id: uuid::Uuid::new_v4().to_string(),
source_file: source_file.to_string(),
target_symbol: target_symbol.to_string(),
edge_type: "imports".to_string(),
target_file: None,
indexed_at: Utc::now().to_rfc3339(),
}
}
#[test]
fn test_json_refs_output_serializes_correctly() {
let output = JsonRefsOutput {
query: "Database".to_string(),
definitions: vec![JsonDefinition {
file_path: "src/db/mod.rs".to_string(),
start_line: 51_u32,
end_line: 54_u32,
chunk_type: "class".to_string(),
symbol_name: Some("Database".to_string()),
signature: Some("pub struct Database".to_string()),
module_path: Some("db".to_string()),
}],
imported_by: vec![JsonImportedBy {
source_file: "src/indexer/mod.rs".to_string(),
target_symbol: "crate::db::Database".to_string(),
edge_type: "imports".to_string(),
}],
imports: vec![JsonImport {
target_symbol: "anyhow::Result".to_string(),
edge_type: "imports".to_string(),
target_file: None,
}],
};
let json_str = serde_json::to_string_pretty(&output).unwrap();
assert!(json_str.contains("\"query\": \"Database\""));
assert!(json_str.contains("\"file_path\": \"src/db/mod.rs\""));
assert!(json_str.contains("\"source_file\": \"src/indexer/mod.rs\""));
assert!(json_str.contains("\"target_symbol\": \"anyhow::Result\""));
assert!(json_str.contains("\"target_file\": null"));
}
#[test]
fn test_json_definition_always_includes_optional_fields() {
let def = JsonDefinition {
file_path: "src/main.rs".to_string(),
start_line: 1,
end_line: 3,
chunk_type: "function".to_string(),
symbol_name: Some("main".to_string()),
signature: None,
module_path: None,
};
let json_str = serde_json::to_string(&def).unwrap();
assert!(
json_str.contains("\"signature\":null") || json_str.contains("\"signature\": null"),
"signature must be present as null: {}",
json_str
);
assert!(
json_str.contains("\"module_path\":null") || json_str.contains("\"module_path\": null"),
"module_path must be present as null: {}",
json_str
);
}
#[test]
fn test_unique_file_count_same_file_multiple_chunks() {
let chunks = vec![
make_chunk("src/db/mod.rs", "Database", "class"),
make_chunk("src/db/mod.rs", "Database", "function"),
];
let unique: HashSet<&str> = chunks.iter().map(|c| c.file_path.as_str()).collect();
assert_eq!(unique.len(), 1);
}
#[test]
fn test_unique_file_count_multiple_files() {
let chunks = vec![
make_chunk("src/db/mod.rs", "Error", "class"),
make_chunk("src/indexer/mod.rs", "Error", "class"),
];
let unique: HashSet<&str> = chunks.iter().map(|c| c.file_path.as_str()).collect();
assert_eq!(unique.len(), 2);
}
#[test]
fn test_make_chunk_helper_has_expected_fields() {
let chunk = make_chunk("src/db/mod.rs", "Database", "class");
assert_eq!(chunk.file_path, "src/db/mod.rs");
assert_eq!(chunk.symbol_name.as_deref(), Some("Database"));
assert_eq!(chunk.chunk_type, "class");
}
#[test]
fn test_make_edge_helper_has_expected_fields() {
let edge = make_edge("src/indexer/mod.rs", "crate::db::Database");
assert_eq!(edge.source_file, "src/indexer/mod.rs");
assert_eq!(edge.target_symbol, "crate::db::Database");
assert_eq!(edge.edge_type, "imports");
assert!(edge.target_file.is_none());
}
}