lean_ctx/tools/
ctx_delta.rs1use 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}