Skip to main content

lean_ctx/tools/
ctx_outline.rs

1use crate::core::cache::SessionCache;
2use crate::core::signatures::{extract_signatures, Signature};
3use crate::core::tokens::count_tokens;
4use crate::tools::CrpMode;
5
6/// Thin redirect: delegates to ctx_read mode=signatures with optional kind filter.
7pub 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
42/// Also available via ctx_read mode=signatures. This adapts to the SessionCache path.
43pub 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}