use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct ModeDelta {
pub mode: String,
pub added_lines: Vec<String>,
pub removed_lines: Vec<String>,
pub changed_lines: Vec<(String, String)>, pub unchanged_count: usize,
}
impl ModeDelta {
pub fn format_compact(&self) -> String {
let mut out = String::new();
out.push_str(&format!("[delta:{}] ", self.mode));
out.push_str(&format!("unchanged:{} ", self.unchanged_count));
if !self.added_lines.is_empty() {
out.push_str(&format!("+{} ", self.added_lines.len()));
}
if !self.removed_lines.is_empty() {
out.push_str(&format!("-{} ", self.removed_lines.len()));
}
if !self.changed_lines.is_empty() {
out.push_str(&format!("~{} ", self.changed_lines.len()));
}
out.push('\n');
for line in &self.added_lines {
out.push_str(&format!("+ {line}\n"));
}
for line in &self.removed_lines {
out.push_str(&format!("- {line}\n"));
}
for (old, new) in &self.changed_lines {
out.push_str(&format!("~ {old}\n→ {new}\n"));
}
out
}
pub fn token_savings_estimate(&self, full_output_tokens: usize) -> f64 {
let delta_lines =
self.added_lines.len() + self.removed_lines.len() + self.changed_lines.len() * 2;
let delta_approx_tokens = delta_lines * 10; if full_output_tokens == 0 {
return 0.0;
}
1.0 - (delta_approx_tokens as f64 / full_output_tokens as f64).min(1.0)
}
}
pub fn compute_delta(mode: &str, previous: &str, current: &str) -> Option<ModeDelta> {
if previous == current {
return Some(ModeDelta {
mode: mode.to_string(),
added_lines: Vec::new(),
removed_lines: Vec::new(),
changed_lines: Vec::new(),
unchanged_count: current.lines().count(),
});
}
let prev_lines: Vec<&str> = previous.lines().collect();
let curr_lines: Vec<&str> = current.lines().collect();
let mut added = Vec::new();
let mut removed = Vec::new();
let mut changed = Vec::new();
let mut unchanged = 0;
let prev_set: HashMap<&str, usize> = prev_lines
.iter()
.enumerate()
.map(|(i, &l)| (l, i))
.collect();
for &line in &curr_lines {
if prev_set.contains_key(line) {
unchanged += 1;
} else {
added.push(line.to_string());
}
}
let curr_set: HashMap<&str, usize> = curr_lines
.iter()
.enumerate()
.map(|(i, &l)| (l, i))
.collect();
for &line in &prev_lines {
if !curr_set.contains_key(line) {
removed.push(line.to_string());
}
}
let min_len = prev_lines.len().min(curr_lines.len());
for i in 0..min_len {
if prev_lines[i] != curr_lines[i]
&& !added.contains(&curr_lines[i].to_string())
&& !removed.contains(&prev_lines[i].to_string())
{
changed.push((prev_lines[i].to_string(), curr_lines[i].to_string()));
}
}
Some(ModeDelta {
mode: mode.to_string(),
added_lines: added,
removed_lines: removed,
changed_lines: changed,
unchanged_count: unchanged,
})
}
pub fn should_use_delta(delta: &ModeDelta, full_output_tokens: usize) -> bool {
let savings = delta.token_savings_estimate(full_output_tokens);
savings > 0.30
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identical_outputs_produce_zero_delta() {
let content = "fn main() {}\nfn helper() {}";
let delta = compute_delta("map", content, content).unwrap();
assert!(delta.added_lines.is_empty());
assert!(delta.removed_lines.is_empty());
assert!(delta.changed_lines.is_empty());
assert_eq!(delta.unchanged_count, 2);
}
#[test]
fn added_lines_detected() {
let prev = "fn main() {}";
let curr = "fn main() {}\nfn new_fn() {}";
let delta = compute_delta("signatures", prev, curr).unwrap();
assert_eq!(delta.added_lines.len(), 1);
assert!(delta.added_lines[0].contains("new_fn"));
}
#[test]
fn removed_lines_detected() {
let prev = "fn main() {}\nfn old_fn() {}";
let curr = "fn main() {}";
let delta = compute_delta("map", prev, curr).unwrap();
assert_eq!(delta.removed_lines.len(), 1);
assert!(delta.removed_lines[0].contains("old_fn"));
}
#[test]
fn savings_estimate_makes_sense() {
let delta = ModeDelta {
mode: "map".into(),
added_lines: vec!["new".into()],
removed_lines: Vec::new(),
changed_lines: Vec::new(),
unchanged_count: 50,
};
let savings = delta.token_savings_estimate(500);
assert!(savings > 0.9); }
#[test]
fn compact_format_is_readable() {
let delta = ModeDelta {
mode: "signatures".into(),
added_lines: vec!["+ pub fn new_api()".into()],
removed_lines: vec!["- pub fn old_api()".into()],
changed_lines: Vec::new(),
unchanged_count: 10,
};
let formatted = delta.format_compact();
assert!(formatted.contains("[delta:signatures]"));
assert!(formatted.contains("unchanged:10"));
}
}