lean_ctx/tools/ctx_outline/
mod.rs1mod dir;
15mod json;
16#[cfg(test)]
17mod tests;
18
19use crate::core::signatures::{SigBackend, Signature, extract_signatures_with_backend};
20use crate::core::tokens::count_tokens;
21use crate::tools::CrpMode;
22
23#[derive(Debug, Clone, Default)]
27pub struct OutlineOpts<'a> {
28 pub kind: Option<&'a str>,
29 pub name_match: Option<&'a str>,
30 pub as_json: bool,
31}
32
33struct FileSymbols {
37 rel: String,
38 ext: String,
39 backend: SigBackend,
40 sigs: Vec<Signature>,
41}
42
43#[must_use]
46pub fn run(path: &str, opts: &OutlineOpts) -> (String, usize) {
47 let p = std::path::Path::new(path);
56 match p.metadata() {
57 Ok(m) if m.is_dir() => dir::outline_dir(path, opts),
58 Ok(_) => outline_file(path, opts),
59 Err(e) => (format!("ERROR: Cannot read {path}: {e}"), 0),
60 }
61}
62
63fn outline_file(path: &str, opts: &OutlineOpts) -> (String, usize) {
64 let content = match std::fs::read_to_string(path) {
65 Ok(c) => c,
66 Err(e) => return (format!("ERROR: Cannot read {path}: {e}"), 0),
67 };
68 let full_tokens = count_tokens(&content);
69 let ext = ext_of(path);
70 let (sigs, backend) = extract_signatures_with_backend(&content, ext);
71 let filtered = filter_signatures(&sigs, opts);
72
73 if opts.as_json {
74 return (json::file_json(path, ext, backend, &filtered), full_tokens);
75 }
76
77 if filtered.is_empty() {
78 return (no_match_message(path, opts), 0);
79 }
80
81 let crp = CrpMode::effective();
82 let mut outline = filtered
83 .iter()
84 .map(|s| render_one(s, crp))
85 .collect::<Vec<_>>()
86 .join("\n");
87 if crp.is_tdd() {
88 let legend = crate::core::signatures::tdd_legend(&filtered);
89 if !legend.is_empty() {
90 outline = format!("{legend}\n{outline}");
91 }
92 }
93 outline.push('\n');
96 outline.push_str(crate::core::handle::USAGE_HINT);
97
98 let sent = count_tokens(&outline);
99 let savings = crate::core::protocol::format_savings(full_tokens, sent);
100 (format!("{outline}\n{savings}"), full_tokens)
101}
102
103fn render_one(s: &Signature, crp: CrpMode) -> String {
107 if crp.is_tdd() {
108 s.to_tdd_located()
109 } else {
110 s.to_compact_located()
111 }
112}
113
114fn filter_signatures<'a>(sigs: &'a [Signature], opts: &OutlineOpts) -> Vec<&'a Signature> {
116 let kind = opts.kind.map(str::to_lowercase);
117 let name = opts.name_match.map(str::to_lowercase);
118 sigs.iter()
119 .filter(|s| match &kind {
120 None => true,
121 Some(k) if k == "all" => true,
122 Some(k) => s.kind.eq_ignore_ascii_case(k),
123 })
124 .filter(|s| match &name {
125 None => true,
126 Some(n) => s.name.to_lowercase().contains(n.as_str()),
127 })
128 .collect()
129}
130
131fn no_match_message(path: &str, opts: &OutlineOpts) -> String {
132 match (opts.kind, opts.name_match) {
133 (_, Some(m)) => format!("No symbols matching '{m}' in {path}"),
134 (Some(k), None) if !k.eq_ignore_ascii_case("all") => format!("No '{k}' symbols in {path}"),
135 _ => format!("No symbols found in {path}"),
136 }
137}
138
139fn ext_of(path: &str) -> &str {
140 std::path::Path::new(path)
141 .extension()
142 .and_then(|e| e.to_str())
143 .unwrap_or("")
144}