Skip to main content

lean_ctx/tools/
ctx_smart_read.rs

1use 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    // task mode (IB-filter) is never auto-selected — it reorders lines and breaks edits.
37    // Users can still explicitly request mode: "task".
38
39    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}