//! CLI tool for code graph navigation. //! //! Provides go-to-definition and go-to-references functionality. //! //! # Usage //! //! ```bash //! # Build the index for a repository //! code-graph index /path/to/repo //! //! # Build the index with custom cache location //! code-graph index /path/to/repo --cache /path/to/cache.bin //! //! # Go to definition (by position) //! code-graph definition /path/to/repo --file src/main.rs --row 10 --col 15 //! //! # Go to definition (by symbol name) //! code-graph definition /path/to/repo --symbol MyStruct //! //! # Go to references (by position) //! code-graph references /path/to/repo --file src/main.rs --row 10 --col 15 //! //! # Go to references (by symbol name) //! code-graph references /path/to/repo --symbol MyStruct //! //! # Show index statistics //! code-graph stats /path/to/repo //! ``` use std::path::{Path, PathBuf}; use std::time::Instant; use clap::{Parser, Subcommand}; use kigi_codebase_graph::{ IndexBuilder, Navigator, ScopeGraphIndex, get_cache_path, load_index, save_index, }; // Use mimalloc for faster allocation #[global_allocator] static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; #[derive(Parser)] #[command(name = "code-graph")] #[command(author, version, about = "High-performance code navigation tool", long_about = None)] struct Cli { #[command(subcommand)] command: Commands, } #[derive(Subcommand)] enum Commands { /// Build or rebuild the index for a repository Index { /// Path to the repository path: PathBuf, /// Custom cache file path (default: /.goto_index.bin) #[arg(short, long)] cache: Option, /// Force rebuild even if cache exists #[arg(short, long)] force: bool, /// Number of threads to use #[arg(short, long)] threads: Option, }, /// Go to definition for a symbol Definition { /// Path to the repository path: PathBuf, /// Custom cache file path (default: /.goto_index.bin) #[arg(long)] cache: Option, /// File path (for position-based lookup) #[arg(short, long)] file: Option, /// Row number (1-indexed, for position-based lookup) #[arg(short, long)] row: Option, /// Column number (1-indexed, for position-based lookup) #[arg(short, long)] col: Option, /// Symbol name (for direct lookup) #[arg(short, long)] symbol: Option, /// Output as JSON #[arg(long)] json: bool, }, /// Go to references for a symbol References { /// Path to the repository path: PathBuf, /// Custom cache file path (default: /.goto_index.bin) #[arg(long)] cache: Option, /// File path (for position-based lookup) #[arg(short, long)] file: Option, /// Row number (1-indexed, for position-based lookup) #[arg(short, long)] row: Option, /// Column number (1-indexed, for position-based lookup) #[arg(short, long)] col: Option, /// Symbol name (for direct lookup) #[arg(short, long)] symbol: Option, /// Include definition in results #[arg(long)] include_definition: bool, /// Output as JSON #[arg(long)] json: bool, }, /// Show index statistics Stats { /// Path to the repository path: PathBuf, /// Custom cache file path (default: /.goto_index.bin) #[arg(long)] cache: Option, }, } fn main() { let cli = Cli::parse(); match cli.command { Commands::Index { path, cache, force, threads, } => { cmd_index(&path, cache.as_deref(), force, threads); } Commands::Definition { path, cache, file, row, col, symbol, json, } => { cmd_definition(&path, cache.as_deref(), file, row, col, symbol, json); } Commands::References { path, cache, file, row, col, symbol, include_definition, json, } => { cmd_references( &path, cache.as_deref(), file, row, col, symbol, include_definition, json, ); } Commands::Stats { path, cache } => { cmd_stats(&path, cache.as_deref()); } } } fn effective_cache_path(repo_path: &Path, custom_cache: Option<&Path>) -> PathBuf { custom_cache .map(|p| p.to_path_buf()) .unwrap_or_else(|| get_cache_path(repo_path)) } fn load_or_build_index(repo_path: &Path, cache_path: &Path) -> ScopeGraphIndex { if let Ok(index) = load_index(cache_path) { println!("Loaded index from cache: {}", cache_path.display()); return index; } println!("Building index for: {}", repo_path.display()); let start = Instant::now(); let index = IndexBuilder::new() .build(repo_path) .expect("Failed to build index"); let elapsed = start.elapsed(); let (files, defs, refs) = index.stats(); println!( "Built index: {} files, {} defs, {} refs in {:?}", files, defs, refs, elapsed ); if let Err(e) = save_index(cache_path, &index) { println!("Warning: Failed to save cache: {}", e); } else { println!("Saved cache to: {}", cache_path.display()); } index } fn cmd_index(path: &Path, custom_cache: Option<&Path>, _force: bool, threads: Option) { let cache_path = effective_cache_path(path, custom_cache); println!("Building index for: {}", path.display()); let start = Instant::now(); let mut builder = IndexBuilder::new(); if let Some(t) = threads { builder = builder.with_threads(t); } let index = builder.build(path).expect("Failed to build index"); let elapsed = start.elapsed(); let (files, defs, refs) = index.stats(); println!("Index built successfully!"); println!(" Files indexed: {}", files); println!(" Definitions: {}", defs); println!(" References: {}", refs); println!(" Aliases: {}", index.alias_count()); println!(" Time: {:?}", elapsed); // Always save when explicitly indexing if let Err(e) = save_index(&cache_path, &index) { println!("Error saving cache: {}", e); std::process::exit(1); } else { println!(" Cache saved: {}", cache_path.display()); } } fn cmd_definition( repo_path: &Path, custom_cache: Option<&Path>, file: Option, row: Option, col: Option, symbol: Option, json: bool, ) { let cache_path = effective_cache_path(repo_path, custom_cache); let index = load_or_build_index(repo_path, &cache_path); let navigator = Navigator::new(index); let result = match (file, row, col, symbol) { (Some(file_path), Some(r), Some(c), _) => { let abs_path = if file_path.is_absolute() { file_path } else { repo_path.join(&file_path) }; match navigator.goto_definition(&abs_path, r, c) { Ok(r) => r, Err(e) => { println!("Error: {}", e); std::process::exit(1); } } } (_, _, _, Some(sym)) => navigator.goto_definition_by_name(&sym, None), _ => { println!("Error: Must provide either --file, --row, --col OR --symbol"); std::process::exit(1); } }; if json { print_json(&result); } else { println!("Symbol: {}", result.symbol); println!("Definitions ({}):", result.locations.len()); for loc in &result.locations { println!(" {}:{}", loc.path, loc.line); } } } fn cmd_references( repo_path: &Path, custom_cache: Option<&Path>, file: Option, row: Option, col: Option, symbol: Option, include_definition: bool, json: bool, ) { let cache_path = effective_cache_path(repo_path, custom_cache); let index = load_or_build_index(repo_path, &cache_path); let navigator = Navigator::new(index); let result = match (file, row, col, symbol) { (Some(file_path), Some(r), Some(c), _) => { let abs_path = if file_path.is_absolute() { file_path } else { repo_path.join(&file_path) }; match navigator.goto_references(&abs_path, r, c, include_definition) { Ok(r) => r, Err(e) => { println!("Error: {}", e); std::process::exit(1); } } } (_, _, _, Some(sym)) => navigator.goto_references_by_name(&sym, None, include_definition), _ => { println!("Error: Must provide either --file, --row, --col OR --symbol"); std::process::exit(1); } }; if json { print_json(&result); } else { println!("Symbol: {}", result.symbol); println!("References ({}):", result.locations.len()); for loc in &result.locations { if let Some(sym) = &loc.symbol { println!(" {}:{} (as {})", loc.path, loc.line, sym); } else { println!(" {}:{}", loc.path, loc.line); } } } } fn cmd_stats(path: &Path, custom_cache: Option<&Path>) { let cache_path = effective_cache_path(path, custom_cache); let index = load_or_build_index(path, &cache_path); let (files, defs, refs) = index.stats(); println!("Index Statistics for: {}", path.display()); println!(" Cache location: {}", cache_path.display()); println!(" Files indexed: {}", files); println!(" Definitions: {}", defs); println!(" References: {}", refs); println!(" Aliases: {}", index.alias_count()); let ref_counts = index.top_referenced_symbols(10); println!("\nTop 10 most referenced symbols:"); for (name, count) in &ref_counts { println!(" {:6} {}", count, name); } } fn print_json(result: &kigi_codebase_graph::NavigationResult) { use serde_json::json; let locations: Vec<_> = result .locations .iter() .map(|loc| { json!({ "path": &loc.path, "line": loc.line, "symbol": loc.symbol, }) }) .collect(); let output = json!({ "symbol": result.symbol, "locations": locations, }); println!("{}", serde_json::to_string_pretty(&output).unwrap()); }