lean_ctx/tools/
ctx_smart_read.rs1use crate::core::cache::SessionCache;
2use crate::core::mode_predictor::{FileSignature, ModePredictor};
3use crate::core::tokens::count_tokens;
4use crate::tools::CrpMode;
5
6pub fn select_mode(cache: &SessionCache, path: &str) -> String {
7 select_mode_with_task(cache, path, None)
8}
9
10pub fn select_mode_with_task(cache: &SessionCache, path: &str, task: Option<&str>) -> String {
11 let content = match std::fs::read_to_string(path) {
12 Ok(c) => c,
13 Err(_) => return "full".to_string(),
14 };
15
16 let token_count = count_tokens(&content);
17 let ext = std::path::Path::new(path)
18 .extension()
19 .and_then(|e| e.to_str())
20 .unwrap_or("");
21
22 if cache.get(path).is_some() {
23 let cached = cache.get(path).unwrap();
24 if cached.hash == compute_hash(&content) {
25 return "full".to_string();
26 }
27 return "diff".to_string();
28 }
29
30 if token_count <= 200 {
31 return "full".to_string();
32 }
33
34 if is_config_or_data(ext, path) {
35 return "full".to_string();
36 }
37
38 if let Some(t) = task {
39 if !t.is_empty() && token_count > 1000 && is_code(ext) {
40 return "task".to_string();
41 }
42 }
43
44 let sig = FileSignature::from_path(path, token_count);
45 let predictor = ModePredictor::new();
46 if let Some(predicted) = predictor.predict_best_mode(&sig) {
47 return predicted;
48 }
49
50 heuristic_mode(ext, token_count)
51}
52
53fn heuristic_mode(ext: &str, token_count: usize) -> String {
54 if token_count > 5000 {
55 if is_code(ext) {
56 return "signatures".to_string();
57 }
58 return "aggressive".to_string();
59 }
60 if token_count > 2000 && is_code(ext) {
61 return "map".to_string();
62 }
63 "full".to_string()
64}
65
66pub fn handle(cache: &mut SessionCache, path: &str, crp_mode: CrpMode) -> String {
67 let mode = select_mode(cache, path);
68 let result = crate::tools::ctx_read::handle(cache, path, &mode, crp_mode);
69 format!("[auto:{mode}] {result}")
70}
71
72fn compute_hash(content: &str) -> String {
73 use md5::{Digest, Md5};
74 let mut hasher = Md5::new();
75 hasher.update(content.as_bytes());
76 format!("{:x}", hasher.finalize())
77}
78
79pub fn is_code_ext(ext: &str) -> bool {
80 is_code(ext)
81}
82
83fn is_code(ext: &str) -> bool {
84 matches!(
85 ext,
86 "rs" | "ts"
87 | "tsx"
88 | "js"
89 | "jsx"
90 | "py"
91 | "go"
92 | "java"
93 | "c"
94 | "cpp"
95 | "cc"
96 | "h"
97 | "hpp"
98 | "rb"
99 | "cs"
100 | "kt"
101 | "swift"
102 | "php"
103 | "zig"
104 | "ex"
105 | "exs"
106 | "scala"
107 | "sc"
108 | "dart"
109 | "sh"
110 | "bash"
111 | "svelte"
112 | "vue"
113 )
114}
115
116fn is_config_or_data(ext: &str, path: &str) -> bool {
117 if matches!(
118 ext,
119 "json" | "yaml" | "yml" | "toml" | "xml" | "ini" | "cfg" | "env" | "lock"
120 ) {
121 return true;
122 }
123 let name = std::path::Path::new(path)
124 .file_name()
125 .and_then(|n| n.to_str())
126 .unwrap_or("");
127 matches!(
128 name,
129 "Cargo.toml"
130 | "package.json"
131 | "tsconfig.json"
132 | "Makefile"
133 | "Dockerfile"
134 | "docker-compose.yml"
135 | ".gitignore"
136 | ".env"
137 | "pyproject.toml"
138 | "go.mod"
139 | "build.gradle"
140 | "pom.xml"
141 )
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn test_config_detection() {
150 assert!(is_config_or_data("json", "package.json"));
151 assert!(is_config_or_data("toml", "Cargo.toml"));
152 assert!(!is_config_or_data("rs", "main.rs"));
153 }
154
155 #[test]
156 fn test_code_detection() {
157 assert!(is_code("rs"));
158 assert!(is_code("py"));
159 assert!(is_code("tsx"));
160 assert!(!is_code("json"));
161 }
162}