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