mod dir;
mod json;
#[cfg(test)]
mod tests;
use crate::core::signatures::{SigBackend, Signature, extract_signatures_with_backend};
use crate::core::tokens::count_tokens;
use crate::tools::CrpMode;
#[derive(Debug, Clone, Default)]
pub struct OutlineOpts<'a> {
pub kind: Option<&'a str>,
pub name_match: Option<&'a str>,
pub as_json: bool,
}
struct FileSymbols {
rel: String,
ext: String,
backend: SigBackend,
sigs: Vec<Signature>,
}
#[must_use]
pub fn run(path: &str, opts: &OutlineOpts) -> (String, usize) {
let p = std::path::Path::new(path);
match p.metadata() {
Ok(m) if m.is_dir() => dir::outline_dir(path, opts),
Ok(_) => outline_file(path, opts),
Err(e) => (format!("ERROR: Cannot read {path}: {e}"), 0),
}
}
fn outline_file(path: &str, opts: &OutlineOpts) -> (String, usize) {
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(e) => return (format!("ERROR: Cannot read {path}: {e}"), 0),
};
let full_tokens = count_tokens(&content);
let ext = ext_of(path);
let (sigs, backend) = extract_signatures_with_backend(&content, ext);
let filtered = filter_signatures(&sigs, opts);
if opts.as_json {
return (json::file_json(path, ext, backend, &filtered), full_tokens);
}
if filtered.is_empty() {
return (no_match_message(path, opts), 0);
}
let crp = CrpMode::effective();
let mut outline = filtered
.iter()
.map(|s| render_one(s, crp))
.collect::<Vec<_>>()
.join("\n");
if crp.is_tdd() {
let legend = crate::core::signatures::tdd_legend(&filtered);
if !legend.is_empty() {
outline = format!("{legend}\n{outline}");
}
}
outline.push('\n');
outline.push_str(crate::core::handle::USAGE_HINT);
let sent = count_tokens(&outline);
let savings = crate::core::protocol::format_savings(full_tokens, sent);
(format!("{outline}\n{savings}"), full_tokens)
}
fn render_one(s: &Signature, crp: CrpMode) -> String {
if crp.is_tdd() {
s.to_tdd_located()
} else {
s.to_compact_located()
}
}
fn filter_signatures<'a>(sigs: &'a [Signature], opts: &OutlineOpts) -> Vec<&'a Signature> {
let kind = opts.kind.map(str::to_lowercase);
let name = opts.name_match.map(str::to_lowercase);
sigs.iter()
.filter(|s| match &kind {
None => true,
Some(k) if k == "all" => true,
Some(k) => s.kind.eq_ignore_ascii_case(k),
})
.filter(|s| match &name {
None => true,
Some(n) => s.name.to_lowercase().contains(n.as_str()),
})
.collect()
}
fn no_match_message(path: &str, opts: &OutlineOpts) -> String {
match (opts.kind, opts.name_match) {
(_, Some(m)) => format!("No symbols matching '{m}' in {path}"),
(Some(k), None) if !k.eq_ignore_ascii_case("all") => format!("No '{k}' symbols in {path}"),
_ => format!("No symbols found in {path}"),
}
}
fn ext_of(path: &str) -> &str {
std::path::Path::new(path)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
}