Skip to main content

lean_ctx/tools/
ctx_delta.rs

1use similar::{ChangeTag, TextDiff};
2
3use crate::core::cache::SessionCache;
4use crate::core::protocol;
5use crate::core::tokens::count_tokens;
6
7pub fn handle(cache: &mut SessionCache, path: &str) -> String {
8    let content = match std::fs::read_to_string(path) {
9        Ok(c) => c,
10        Err(e) => return format!("Error: {e}"),
11    };
12
13    let short = protocol::shorten_path(path);
14    let new_lines = content.lines().count();
15    let new_tokens = count_tokens(&content);
16
17    let Some(cached_entry) = cache.get(path) else {
18        cache.store(path, content.clone());
19        return format!(
20            "{short} [first read, {new_lines}L, {new_tokens} tok] — cached for future deltas"
21        );
22    };
23    let old_content = cached_entry.content.clone();
24    let old_hash = cached_entry.hash.clone();
25
26    let new_hash = compute_hash(&content);
27    if old_hash == new_hash {
28        return format!("{short} cached (no changes)");
29    }
30
31    let diff = TextDiff::from_lines(&old_content, &content);
32    let mut hunks = Vec::new();
33    let mut additions = 0usize;
34    let mut deletions = 0usize;
35
36    for group in diff.grouped_ops(3) {
37        let mut hunk_lines = Vec::new();
38        for op in &group {
39            for change in diff.iter_changes(op) {
40                let line_no = change.new_index().or(change.old_index()).map(|i| i + 1);
41                let text = change.value().trim_end_matches('\n');
42                match change.tag() {
43                    ChangeTag::Insert => {
44                        additions += 1;
45                        if let Some(n) = line_no {
46                            hunk_lines.push(format!("+{n}: {text}"));
47                        }
48                    }
49                    ChangeTag::Delete => {
50                        deletions += 1;
51                        if let Some(n) = line_no {
52                            hunk_lines.push(format!("-{n}: {text}"));
53                        }
54                    }
55                    ChangeTag::Equal => {
56                        if let Some(n) = line_no {
57                            hunk_lines.push(format!(" {n}: {text}"));
58                        }
59                    }
60                }
61            }
62        }
63        if !hunk_lines.is_empty() {
64            hunks.push(hunk_lines.join("\n"));
65        }
66    }
67
68    cache.store(path, content);
69
70    let delta_output = hunks.join("\n---\n");
71    let delta_tokens = count_tokens(&delta_output);
72    let savings = if new_tokens > 0 {
73        ((new_tokens as f64 - delta_tokens as f64) / new_tokens as f64 * 100.0) as u32
74    } else {
75        0
76    };
77
78    format!(
79        "{short} [delta] +{additions}/-{deletions} lines ({delta_tokens} tok, {savings}% saved vs full)\n{delta_output}"
80    )
81}
82
83fn compute_hash(content: &str) -> String {
84    use md5::{Digest, Md5};
85    let mut hasher = Md5::new();
86    hasher.update(content.as_bytes());
87    format!("{:x}", hasher.finalize())
88}