lean_ctx_server/tools/ctx_outline/
mod.rs1mod dir;
16mod json;
17#[cfg(test)]
18mod tests;
19
20use crate::tools::CrpMode;
21use lean_ctx::core::signatures::{SigBackend, Signature, extract_signatures_with_backend};
22use lean_ctx::core::tokens::count_tokens;
23
24#[derive(Debug, Clone, Default)]
28pub struct OutlineOpts<'a> {
29 pub kind: Option<&'a str>,
30 pub name_match: Option<&'a str>,
31 pub as_json: bool,
32}
33
34const TOOL: &str = "ctx_outline";
36
37struct FileSymbols {
41 rel: String,
42 ext: String,
43 backend: SigBackend,
44 sigs: Vec<Signature>,
45}
46
47#[cfg(test)]
54#[must_use]
55pub fn run(path: &str, opts: &OutlineOpts) -> (String, usize) {
56 let root = crate::tools::admitted_sources::fixture_root(path);
57 run_rooted(path, &root, opts)
58}
59
60#[must_use]
66pub fn run_rooted(path: &str, root: &str, opts: &OutlineOpts) -> (String, usize) {
67 let p = std::path::Path::new(path);
76 match p.metadata() {
77 Ok(m) if m.is_dir() => dir::outline_dir(path, root, opts),
78 Ok(_) => outline_file(path, root, opts),
79 Err(e) => (format!("ERROR: Cannot read {path}: {e}"), 0),
80 }
81}
82
83fn outline_file(path: &str, root: &str, opts: &OutlineOpts) -> (String, usize) {
84 use crate::tools::admitted_sources::SourceRead;
85 let mut remaining = lean_ctx::core::limits::max_read_bytes();
86 let content = match crate::tools::admitted_sources::read(path, root, TOOL, &mut remaining) {
87 SourceRead::Admitted(read) => read.content,
88 SourceRead::Withheld(reason) | SourceRead::Unavailable(reason) => {
89 return (format!("ERROR: Cannot read {path}: {reason}"), 0);
90 }
91 };
92 let full_tokens = count_tokens(&content);
93 let ext = ext_of(path);
94 let (sigs, backend) = extract_signatures_with_backend(&content, ext);
95 let filtered = filter_signatures(&sigs, opts);
96
97 if opts.as_json {
98 return (json::file_json(path, ext, backend, &filtered), full_tokens);
99 }
100
101 if filtered.is_empty() {
102 return (no_match_message(path, opts), 0);
103 }
104
105 let crp = CrpMode::effective();
106 let mut outline = filtered
107 .iter()
108 .map(|s| render_one(s, crp))
109 .collect::<Vec<_>>()
110 .join("\n");
111 if crp.is_tdd() {
112 let legend = lean_ctx::core::signatures::tdd_legend(&filtered);
113 if !legend.is_empty() {
114 outline = format!("{legend}\n{outline}");
115 }
116 }
117 outline.push('\n');
120 outline.push_str(lean_ctx::core::handle::USAGE_HINT);
121
122 let sent = count_tokens(&outline);
123 let savings = lean_ctx::core::protocol::format_savings(full_tokens, sent);
124 (format!("{outline}\n{savings}"), full_tokens)
125}
126
127fn render_one(s: &Signature, crp: CrpMode) -> String {
131 if crp.is_tdd() {
132 s.to_tdd_located()
133 } else {
134 s.to_compact_located()
135 }
136}
137
138fn filter_signatures<'a>(sigs: &'a [Signature], opts: &OutlineOpts) -> Vec<&'a Signature> {
140 let kind = opts.kind.map(str::to_lowercase);
141 let name = opts.name_match.map(str::to_lowercase);
142 sigs.iter()
143 .filter(|s| match &kind {
144 None => true,
145 Some(k) if k == "all" => true,
146 Some(k) => s.kind.eq_ignore_ascii_case(k),
147 })
148 .filter(|s| match &name {
149 None => true,
150 Some(n) => s.name.to_lowercase().contains(n.as_str()),
151 })
152 .collect()
153}
154
155fn no_match_message(path: &str, opts: &OutlineOpts) -> String {
156 match (opts.kind, opts.name_match) {
157 (_, Some(m)) => format!("No symbols matching '{m}' in {path}"),
158 (Some(k), None) if !k.eq_ignore_ascii_case("all") => format!("No '{k}' symbols in {path}"),
159 _ => format!("No symbols found in {path}"),
160 }
161}
162
163fn ext_of(path: &str) -> &str {
164 std::path::Path::new(path)
165 .extension()
166 .and_then(|e| e.to_str())
167 .unwrap_or("")
168}