lean_ctx/tools/
ctx_outline.rs1use 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}