lean_ctx/tools/
ctx_outline.rs1use crate::core::cache::SessionCache;
2use crate::core::signatures::{extract_signatures, Signature};
3use crate::core::tokens::count_tokens;
4use crate::tools::CrpMode;
5
6pub fn handle(path: &str, kind_filter: Option<&str>) -> (String, usize) {
8 let content = match std::fs::read_to_string(path) {
9 Ok(c) => c,
10 Err(e) => return (format!("ERROR: Cannot read {path}: {e}"), 0),
11 };
12 let full_tokens = count_tokens(&content);
13 let ext = std::path::Path::new(path)
14 .extension()
15 .and_then(|e| e.to_str())
16 .unwrap_or("");
17
18 let sigs = extract_signatures(&content, ext);
19 if sigs.is_empty() {
20 return (format!("No symbols found in {path}"), 0);
21 }
22
23 let filtered = filter_by_kind(&sigs, kind_filter);
24 let crp = CrpMode::effective();
25 let outline: String = filtered
26 .iter()
27 .map(|s| {
28 if crp.is_tdd() {
29 s.to_tdd()
30 } else {
31 s.to_compact()
32 }
33 })
34 .collect::<Vec<_>>()
35 .join("\n");
36
37 let sent = count_tokens(&outline);
38 let savings = crate::core::protocol::format_savings(full_tokens, sent);
39 (format!("{outline}\n{savings}"), full_tokens)
40}
41
42pub fn handle_via_read(
44 cache: &mut SessionCache,
45 path: &str,
46 kind_filter: Option<&str>,
47 crp_mode: CrpMode,
48) -> String {
49 if kind_filter.is_none() || kind_filter == Some("all") {
50 return crate::tools::ctx_read::handle(cache, path, "signatures", crp_mode);
51 }
52 let (result, _) = handle(path, kind_filter);
53 result
54}
55
56pub fn filter_by_kind<'a>(sigs: &'a [Signature], kind: Option<&str>) -> Vec<&'a Signature> {
57 match kind {
58 None | Some("all") => sigs.iter().collect(),
59 Some(k) => {
60 let k_lower = k.to_lowercase();
61 sigs.iter()
62 .filter(|s| s.kind.to_lowercase() == k_lower)
63 .collect()
64 }
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use crate::core::signatures::Signature;
72
73 fn sample_sigs() -> Vec<Signature> {
74 vec![
75 Signature {
76 kind: "fn",
77 name: "main".to_string(),
78 params: String::new(),
79 return_type: String::new(),
80 is_async: false,
81 is_exported: false,
82 indent: 0,
83 ..Signature::no_span()
84 },
85 Signature {
86 kind: "struct",
87 name: "Config".to_string(),
88 params: String::new(),
89 return_type: String::new(),
90 is_async: false,
91 is_exported: true,
92 indent: 0,
93 ..Signature::no_span()
94 },
95 Signature {
96 kind: "fn",
97 name: "load".to_string(),
98 params: "path: &str".to_string(),
99 return_type: "Self".to_string(),
100 is_async: false,
101 is_exported: true,
102 indent: 2,
103 ..Signature::no_span()
104 },
105 ]
106 }
107
108 #[test]
109 fn filter_fn_only() {
110 let sigs = sample_sigs();
111 let filtered = filter_by_kind(&sigs, Some("fn"));
112 assert_eq!(filtered.len(), 2);
113 }
114
115 #[test]
116 fn filter_struct_only() {
117 let sigs = sample_sigs();
118 let filtered = filter_by_kind(&sigs, Some("struct"));
119 assert_eq!(filtered.len(), 1);
120 assert_eq!(filtered[0].name, "Config");
121 }
122
123 #[test]
124 fn filter_all_returns_everything() {
125 let sigs = sample_sigs();
126 let filtered = filter_by_kind(&sigs, Some("all"));
127 assert_eq!(filtered.len(), 3);
128 }
129
130 #[test]
131 fn filter_none_returns_everything() {
132 let sigs = sample_sigs();
133 let filtered = filter_by_kind(&sigs, None);
134 assert_eq!(filtered.len(), 3);
135 }
136}