use clap::{Parser, Subcommand};
use ignore::WalkBuilder;
use std::path::{Path, PathBuf};
mod adapters;
mod core;
mod prompt;
use crate::adapters::base::LanguageAdapter;
use crate::adapters::csharp::CSharpAdapter;
use crate::adapters::go::GoAdapter;
use crate::adapters::java::JavaAdapter;
use crate::adapters::kotlin::KotlinAdapter;
use crate::adapters::python::PythonAdapter;
use crate::adapters::rust::RustAdapter;
use crate::adapters::scala::ScalaAdapter;
use crate::adapters::typescript::TypeScriptAdapter;
use crate::core::{DigestOptions, OutlineOptions, ParseResult};
use ast_grep_core::Language;
use ast_grep_language::{LanguageExt, SupportLang};
#[derive(Parser)]
#[command(name = "ast-outline")]
#[command(about = "Fast, AST-based structural outline for source files", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
#[arg(num_args = 1..)]
paths: Vec<PathBuf>,
#[arg(long)]
no_private: bool,
#[arg(long)]
no_fields: bool,
#[arg(long)]
no_docs: bool,
#[arg(long)]
no_attrs: bool,
#[arg(long)]
no_lines: bool,
#[arg(long)]
glob: Option<String>,
}
#[derive(Subcommand)]
enum Commands {
Show {
path: PathBuf,
symbol: String,
#[arg(num_args = 0..)]
others: Vec<String>,
},
Digest {
#[arg(num_args = 1..)]
paths: Vec<PathBuf>,
#[arg(long)]
include_private: bool,
#[arg(long)]
include_fields: bool,
#[arg(long, default_value_t = 50)]
max_members: usize,
},
Implements {
target: String,
#[arg(num_args = 1..)]
paths: Vec<PathBuf>,
#[arg(short, long)]
direct: bool,
},
Prompt,
}
fn parse_file(path: &Path) -> Option<ParseResult> {
let lang = SupportLang::from_path(path);
let source = std::fs::read_to_string(path).ok()?;
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
if ext == "md" || ext == "markdown" || ext == "mdx" {
return Some(crate::adapters::markdown::parse_markdown(
path,
source.as_bytes(),
));
}
let lang = lang?;
match lang {
SupportLang::Rust => {
let ast_grep = lang.ast_grep(source.clone());
Some(RustAdapter.parse(path, source.as_bytes(), ast_grep.root()))
}
SupportLang::Python => {
let ast_grep = lang.ast_grep(source.clone());
Some(PythonAdapter.parse(path, source.as_bytes(), ast_grep.root()))
}
SupportLang::TypeScript | SupportLang::Tsx | SupportLang::JavaScript => {
let ast_grep = lang.ast_grep(source.clone());
Some(TypeScriptAdapter.parse(path, source.as_bytes(), ast_grep.root()))
}
SupportLang::CSharp => {
let ast_grep = lang.ast_grep(source.clone());
Some(CSharpAdapter.parse(path, source.as_bytes(), ast_grep.root()))
}
SupportLang::Go => {
let ast_grep = lang.ast_grep(source.clone());
Some(GoAdapter.parse(path, source.as_bytes(), ast_grep.root()))
}
SupportLang::Java => {
let ast_grep = lang.ast_grep(source.clone());
Some(JavaAdapter.parse(path, source.as_bytes(), ast_grep.root()))
}
SupportLang::Kotlin => {
let ast_grep = lang.ast_grep(source.clone());
Some(KotlinAdapter.parse(path, source.as_bytes(), ast_grep.root()))
}
SupportLang::Scala => {
let ast_grep = lang.ast_grep(source.clone());
Some(ScalaAdapter.parse(path, source.as_bytes(), ast_grep.root()))
}
_ => None,
}
}
fn walk_and_parse(paths: &[PathBuf], glob_str: Option<&str>) -> Vec<ParseResult> {
let (tx, rx) = std::sync::mpsc::channel();
if paths.is_empty() {
return Vec::new();
}
let mut builder = WalkBuilder::new(&paths[0]);
for p in paths.iter().skip(1) {
builder.add(p);
}
builder.hidden(false);
if let Some(g) = glob_str {
if let Ok(override_builder) = ignore::overrides::OverrideBuilder::new("").add(g) {
if let Ok(over) = override_builder.build() {
builder.overrides(over);
}
}
}
let walker = builder.build_parallel();
walker.run(|| {
let tx = tx.clone();
Box::new(move |result| {
if let Ok(entry) = result {
if entry.file_type().map_or(false, |ft| ft.is_file()) {
if let Some(parsed) = parse_file(entry.path()) {
let _ = tx.send(parsed);
}
}
}
ignore::WalkState::Continue
})
});
drop(tx);
let mut results: Vec<_> = rx.into_iter().collect();
results.sort_by(|a, b| a.path.cmp(&b.path));
results
}
fn main() {
let cli = Cli::parse();
if let Some(cmd) = &cli.command {
match cmd {
Commands::Show {
path,
symbol,
others,
} => {
if let Some(res) = parse_file(path) {
let mut symbols = vec![symbol.as_str()];
symbols.extend(others.iter().map(|s| s.as_str()));
for sym in symbols {
let matches = crate::core::find_symbols(&res, sym);
for m in matches {
println!(
"# {}:{}-{} {} ({})",
res.path.display(),
m.start_line,
m.end_line,
m.qualified_name,
m.kind
);
if !m.ancestor_signatures.is_empty() {
println!("# in: {}", m.ancestor_signatures.join(" → "));
}
println!("{}", m.source);
}
}
}
}
Commands::Digest {
paths,
include_private,
include_fields,
max_members,
} => {
let results = walk_and_parse(paths, None);
let opts = DigestOptions {
include_private: *include_private,
include_fields: *include_fields,
max_members_per_type: *max_members,
max_heading_depth: 3,
};
let root = if paths.len() == 1 && paths[0].is_dir() {
Some(paths[0].as_path())
} else {
None
};
println!("{}", crate::core::render_digest(&results, &opts, root));
}
Commands::Implements {
target,
paths,
direct,
} => {
let results = walk_and_parse(paths, None);
let matches = crate::core::find_implementations(&results, target, !direct);
println!(
"# {} match(es) for '{}' (incl. transitive):",
matches.len(),
target
);
for m in matches {
let via = if m.via.is_empty() {
String::new()
} else {
format!(" [via {}]", m.via.last().unwrap())
};
println!("{}:{} {} {}{}", m.path, m.start_line, m.kind, m.name, via);
}
}
Commands::Prompt => {
println!("{}", crate::prompt::AGENT_PROMPT);
}
}
} else if !cli.paths.is_empty() {
let results = walk_and_parse(&cli.paths, cli.glob.as_deref());
let opts = OutlineOptions {
include_private: !cli.no_private,
include_fields: !cli.no_fields,
include_xml_doc: !cli.no_docs,
include_attributes: !cli.no_attrs,
include_line_numbers: !cli.no_lines,
max_doc_lines: 6,
};
for res in results {
println!("{}", crate::core::render_outline(&res, &opts));
println!("");
}
} else {
println!("Please provide a path or command.");
}
}