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