Skip to main content

lean_ctx/tools/
ctx_outline.rs

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