use crate::ast::Language;
use crate::ast::symbols::SymbolDef;
use crate::cli::global::GlobalFlags;
use anyhow::Context;
use std::path::{Path, PathBuf};
pub(super) fn resolve_lang(lang_arg: Option<&str>, path: &Path) -> Language {
lang_arg
.map(lang_from_str)
.unwrap_or_else(|| Language::from_path(path))
}
pub(super) fn setup_single_file(
path_arg: &str,
lang_arg: Option<&str>,
global: &GlobalFlags,
) -> anyhow::Result<(PathBuf, PathBuf, Language, String)> {
let cwd = global.resolve_cwd()?;
global.check_paths_contained(&cwd, [path_arg])?;
let target = cwd.join(path_arg);
let lang = resolve_lang(lang_arg, &target);
let source = std::fs::read_to_string(&target).with_context(|| format!("reading {path_arg}"))?;
Ok((cwd, target, lang, source))
}
pub(super) fn setup_multi_file(
path_arg: &str,
global: &GlobalFlags,
) -> anyhow::Result<(PathBuf, Vec<PathBuf>)> {
let cwd = global.resolve_cwd()?;
global.check_paths_contained(&cwd, [path_arg])?;
let target = cwd.join(path_arg);
let paths = resolve_target_paths(&target, path_arg, global)?;
Ok((cwd, paths))
}
pub(crate) fn display_path(path: &Path, cwd: &Path) -> String {
crate::files::relative_display(path, cwd)
.display()
.to_string()
}
pub(crate) fn resolve_target_paths(
target: &std::path::Path,
path_arg: &str,
global: &GlobalFlags,
) -> anyhow::Result<Vec<std::path::PathBuf>> {
if target.is_file() {
Ok(vec![target.to_path_buf()])
} else if target.is_dir() {
collect_source_files(target, global)
} else {
anyhow::bail!("path not found: {}", path_arg)
}
}
pub(crate) fn get_git_file_content(
cwd: &std::path::Path,
file_path: &str,
git_ref: &str,
) -> anyhow::Result<String> {
if git_ref.starts_with('-') {
anyhow::bail!("invalid git ref: must not start with '-'");
}
let output = std::process::Command::new("git")
.args(["show", &format!("{git_ref}:{file_path}")])
.current_dir(cwd)
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("git show {git_ref}:{file_path} failed: {}", stderr.trim());
}
Ok(String::from_utf8(output.stdout)?)
}
pub(crate) fn lang_from_str(s: &str) -> Language {
Language::from_name_or_ext(s)
}
pub use crate::ast::symbols::{filter_symbols, parse_kind_filter};
pub(crate) fn collect_source_files(
dir: &std::path::Path,
global: &GlobalFlags,
) -> anyhow::Result<Vec<std::path::PathBuf>> {
let dir_str = dir.to_string_lossy().into_owned();
let mut all_paths = crate::collect_file_paths_opts(&[dir_str], global, true, None)?;
all_paths.retain(|p| Language::from_path(p).has_grammar());
all_paths.sort();
Ok(all_paths)
}
pub(super) fn print_symbols_human(path: &str, symbols: &[&SymbolDef]) {
println!("{path}");
for sym in symbols {
print_symbol_human(sym, 1);
}
println!();
}
fn print_symbol_human(sym: &SymbolDef, indent: usize) {
let pad = " ".repeat(indent);
println!(
"{pad}{} {} [{}:{}]",
sym.kind, sym.name, sym.start_line, sym.end_line
);
for child in &sym.children {
print_symbol_human(child, indent + 1);
}
}
pub(super) fn print_symbols_compact(path: &str, symbols: &[&SymbolDef]) {
println!("{path}");
println!("|----");
for sym in symbols {
print_symbol_compact(sym, 0);
}
println!("|----");
println!();
}
fn print_symbol_compact(sym: &SymbolDef, indent: usize) {
let pad = " ".repeat(indent);
println!("|{pad}{} {}", sym.kind, sym.name);
for child in &sym.children {
print_symbol_compact(child, indent + 1);
}
}
pub(super) fn print_symbols_json(
path: &str,
symbols: &[&SymbolDef],
global: &GlobalFlags,
) -> anyhow::Result<()> {
for sym in symbols {
let obj = symbol_to_json(sym, path);
global.emit_json(&obj)?;
}
Ok(())
}
pub(crate) fn symbol_to_json(sym: &SymbolDef, path: &str) -> serde_json::Value {
let children: Vec<serde_json::Value> = sym
.children
.iter()
.map(|c| symbol_to_json(c, path))
.collect();
serde_json::json!({
"file": path,
"name": sym.name,
"kind": sym.kind.to_string(),
"start_line": sym.start_line,
"end_line": sym.end_line,
"signature": sym.signature,
"children": children,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::symbols::SymbolKind;
#[test]
fn parse_kind_filter_works() {
let filter = parse_kind_filter(&Some("function,struct".to_string()));
assert_eq!(filter.len(), 2);
assert!(filter.contains(&SymbolKind::Function));
assert!(filter.contains(&SymbolKind::Struct));
}
#[test]
fn parse_kind_filter_empty() {
let filter = parse_kind_filter(&None);
assert!(filter.is_empty());
}
#[test]
fn lang_from_str_works() {
assert_eq!(lang_from_str("rs"), Language::Rust);
assert_eq!(lang_from_str("py"), Language::Python);
assert_eq!(lang_from_str("go"), Language::Go);
}
#[test]
fn lang_from_str_language_names() {
assert_eq!(lang_from_str("rust"), Language::Rust);
assert_eq!(lang_from_str("python"), Language::Python);
assert_eq!(lang_from_str("typescript"), Language::TypeScript);
assert_eq!(lang_from_str("javascript"), Language::JavaScript);
assert_eq!(lang_from_str("golang"), Language::Go);
assert_eq!(lang_from_str("csharp"), Language::CSharp);
assert_eq!(lang_from_str("ruby"), Language::Ruby);
assert_eq!(lang_from_str("kotlin"), Language::Kotlin);
assert_eq!(lang_from_str("terraform"), Language::Hcl);
assert_eq!(lang_from_str("markdown"), Language::Markdown);
assert_eq!(lang_from_str("Rust"), Language::Rust); }
#[test]
fn resolve_target_paths_file() {
let dir = tempfile::TempDir::new().unwrap();
let f = dir.path().join("hello.rs");
std::fs::write(&f, "fn main() {}\n").unwrap();
let global = GlobalFlags::default();
let result = resolve_target_paths(&f, "hello.rs", &global).unwrap();
assert_eq!(result, vec![f]);
}
#[test]
fn resolve_target_paths_directory() {
let dir = tempfile::TempDir::new().unwrap();
let f = dir.path().join("lib.rs");
std::fs::write(&f, "fn foo() {}\n").unwrap();
std::fs::write(dir.path().join("notes.txt"), "not source").unwrap();
let global = GlobalFlags::default();
let result = resolve_target_paths(dir.path(), ".", &global).unwrap();
assert_eq!(result.len(), 1);
assert!(result[0].ends_with("lib.rs"));
}
#[test]
fn resolve_target_paths_nonexistent() {
let dir = tempfile::TempDir::new().unwrap();
let missing = dir.path().join("no_such_file.rs");
let global = GlobalFlags::default();
let err = resolve_target_paths(&missing, "no_such_file.rs", &global).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("path not found") && msg.contains("no_such_file.rs"),
"error should mention path not found and the argument: {msg}"
);
}
#[test]
fn context_line_calculation_no_overflow() {
let start_line: usize = 5;
let end_line: usize = 10;
let context: usize = usize::MAX;
let total_lines: usize = 100;
let start = start_line.saturating_sub(context.saturating_add(1));
let end = end_line.saturating_add(context).min(total_lines);
assert_eq!(start, 0);
assert_eq!(end, total_lines);
}
}