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