lean_ctx/core/
delta_response.rs1#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct DeltaResponse {
10 pub path: String,
11 pub hunks: Vec<Hunk>,
12 pub lines_changed: usize,
13 pub lines_unchanged: usize,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Hunk {
19 pub old_start: usize,
20 pub new_start: usize,
21 pub context_before: Vec<String>,
22 pub removed: Vec<String>,
23 pub added: Vec<String>,
24 pub context_after: Vec<String>,
25}
26
27impl DeltaResponse {
28 pub fn format(&self) -> String {
30 if self.hunks.is_empty() {
31 return format!("[unchanged: {} — no edits detected]", self.path);
32 }
33
34 let mut out = format!(
35 "Δ {} ({} lines changed, {} unchanged)\n",
36 self.path, self.lines_changed, self.lines_unchanged
37 );
38
39 for hunk in &self.hunks {
40 out.push_str(&format!(
41 "@@ -{},{} +{} @@\n",
42 hunk.old_start,
43 hunk.removed.len(),
44 hunk.new_start
45 ));
46 for line in &hunk.context_before {
47 out.push_str(&format!(" {line}\n"));
48 }
49 for line in &hunk.removed {
50 out.push_str(&format!("-{line}\n"));
51 }
52 for line in &hunk.added {
53 out.push_str(&format!("+{line}\n"));
54 }
55 for line in &hunk.context_after {
56 out.push_str(&format!(" {line}\n"));
57 }
58 }
59
60 out
61 }
62
63 pub fn savings_ratio(&self) -> f64 {
65 let total = self.lines_changed + self.lines_unchanged;
66 if total == 0 {
67 return 0.0;
68 }
69 self.lines_unchanged as f64 / total as f64
70 }
71}
72
73fn find_sync_point(old: &[&str], new: &[&str], max_look: usize) -> Option<(usize, usize)> {
76 let limit_o = old.len().min(max_look);
77 let limit_n = new.len().min(max_look);
78
79 for dist in 1..=(limit_o + limit_n) {
80 for skip_o in 0..=dist.min(limit_o) {
81 let skip_n = dist - skip_o;
82 if skip_n > limit_n {
83 continue;
84 }
85 if skip_o < old.len() && skip_n < new.len() && old.get(skip_o) == new.get(skip_n) {
86 return Some((skip_o, skip_n));
87 }
88 }
89 }
90
91 None
92}
93
94pub fn compute_delta(
99 path: &str,
100 old_content: &str,
101 new_content: &str,
102 context_lines: usize,
103) -> DeltaResponse {
104 let old_lines: Vec<&str> = old_content.lines().collect();
105 let new_lines: Vec<&str> = new_content.lines().collect();
106
107 if old_lines == new_lines {
108 return DeltaResponse {
109 path: path.to_string(),
110 hunks: Vec::new(),
111 lines_changed: 0,
112 lines_unchanged: old_lines.len(),
113 };
114 }
115
116 let mut hunks = Vec::new();
117 let mut i = 0;
118 let mut j = 0;
119 let mut lines_changed = 0;
120
121 while i < old_lines.len() || j < new_lines.len() {
122 if i < old_lines.len() && j < new_lines.len() && old_lines[i] == new_lines[j] {
123 i += 1;
124 j += 1;
125 continue;
126 }
127
128 let old_start = i + 1;
129 let new_start = j + 1;
130
131 let ctx_start = i.saturating_sub(context_lines);
132 let context_before: Vec<String> = old_lines[ctx_start..i]
133 .iter()
134 .map(std::string::ToString::to_string)
135 .collect();
136
137 let mut removed = Vec::new();
138 let mut added = Vec::new();
139
140 let sync = find_sync_point(&old_lines[i..], &new_lines[j..], 50);
143 let (old_skip, new_skip) = sync.unwrap_or((old_lines.len() - i, new_lines.len() - j));
144
145 for line in &old_lines[i..i + old_skip] {
146 removed.push(line.to_string());
147 }
148 i += old_skip;
149
150 for line in &new_lines[j..j + new_skip] {
151 added.push(line.to_string());
152 }
153 j += new_skip;
154
155 let ctx_end = (i + context_lines).min(old_lines.len());
156 let context_after: Vec<String> = old_lines[i..ctx_end]
157 .iter()
158 .map(std::string::ToString::to_string)
159 .collect();
160
161 lines_changed += removed.len() + added.len();
162 hunks.push(Hunk {
163 old_start,
164 new_start,
165 context_before,
166 removed,
167 added,
168 context_after,
169 });
170 }
171
172 let lines_unchanged = old_lines.len().saturating_sub(lines_changed);
173
174 DeltaResponse {
175 path: path.to_string(),
176 hunks,
177 lines_changed,
178 lines_unchanged,
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn no_changes_returns_empty_delta() {
188 let delta = compute_delta("test.rs", "line1\nline2", "line1\nline2", 2);
189 assert!(delta.hunks.is_empty());
190 assert_eq!(delta.lines_unchanged, 2);
191 assert_eq!(delta.format(), "[unchanged: test.rs — no edits detected]");
192 }
193
194 #[test]
195 fn single_line_change() {
196 let delta = compute_delta("test.rs", "a\nb\nc", "a\nB\nc", 1);
197 assert_eq!(delta.hunks.len(), 1);
198 assert_eq!(delta.hunks[0].removed, vec!["b"]);
199 assert_eq!(delta.hunks[0].added, vec!["B"]);
200 }
201
202 #[test]
203 fn format_includes_delta_marker() {
204 let delta = compute_delta("src/main.rs", "old line", "new line", 0);
205 let formatted = delta.format();
206 assert!(formatted.contains("Δ src/main.rs"));
207 assert!(formatted.contains("-old line"));
208 assert!(formatted.contains("+new line"));
209 }
210
211 #[test]
212 fn savings_ratio_high_for_small_change() {
213 let old = (0..100)
214 .map(|i| format!("line {i}"))
215 .collect::<Vec<_>>()
216 .join("\n");
217 let mut new_lines: Vec<String> = (0..100).map(|i| format!("line {i}")).collect();
218 new_lines[50] = "CHANGED LINE".to_string();
219 let new = new_lines.join("\n");
220
221 let delta = compute_delta("big.rs", &old, &new, 2);
222 assert!(
223 delta.savings_ratio() > 0.90,
224 "savings should be >90% for 1/100 change"
225 );
226 }
227}