use imara_diff::{Algorithm, Diff, InternedInput};
pub const CONTEXT: usize = 3;
pub const MERGE_GAP: usize = CONTEXT * 2 + 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LineKind {
Context,
Add,
Del,
}
impl LineKind {
pub fn as_str(self) -> &'static str {
match self {
LineKind::Context => "context",
LineKind::Add => "add",
LineKind::Del => "del",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Line {
pub kind: LineKind,
pub old_no: Option<usize>,
pub new_no: Option<usize>,
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Hunk {
pub old_start: usize,
pub old_lines: usize,
pub new_start: usize,
pub new_lines: usize,
pub lines: Vec<Line>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct FileDiff {
pub hunks: Vec<Hunk>,
pub insertions: usize,
pub deletions: usize,
}
pub fn hunks(before: &str, after: &str) -> FileDiff {
let before_lines: Vec<&str> = before.lines().collect();
let after_lines: Vec<&str> = after.lines().collect();
let input = InternedInput::new(before, after);
let mut diff = Diff::compute(Algorithm::Histogram, &input);
diff.postprocess_lines(&input);
let raw: Vec<(std::ops::Range<usize>, std::ops::Range<usize>)> = diff
.hunks()
.map(|h| {
(
h.before.start as usize..h.before.end as usize,
h.after.start as usize..h.after.end as usize,
)
})
.collect();
let insertions = raw.iter().map(|(_, a)| a.len()).sum();
let deletions = raw.iter().map(|(b, _)| b.len()).sum();
let mut groups: Vec<Vec<usize>> = Vec::new();
for (index, (before_range, _)) in raw.iter().enumerate() {
let joins = groups.last().is_some_and(|group| {
let last = &raw[*group.last().expect("a group is never empty")].0;
before_range.start.saturating_sub(last.end) <= MERGE_GAP
});
if joins {
groups.last_mut().expect("checked just above").push(index);
} else {
groups.push(vec![index]);
}
}
let mut out = Vec::new();
for group in groups {
let first = &raw[group[0]];
let last = &raw[*group.last().expect("a group is never empty")];
let old_from = first.0.start.saturating_sub(CONTEXT);
let old_to = (last.0.end + CONTEXT).min(before_lines.len());
let new_from = first.1.start.saturating_sub(CONTEXT);
let new_to = (last.1.end + CONTEXT).min(after_lines.len());
let mut lines = Vec::new();
let mut old_at = old_from;
let mut new_at = new_from;
for index in &group {
let (before_range, after_range) = &raw[*index];
while old_at < before_range.start {
lines.push(Line {
kind: LineKind::Context,
old_no: Some(old_at + 1),
new_no: Some(new_at + 1),
text: before_lines[old_at].to_owned(),
});
old_at += 1;
new_at += 1;
}
for line in before_range.clone() {
lines.push(Line {
kind: LineKind::Del,
old_no: Some(line + 1),
new_no: None,
text: before_lines[line].to_owned(),
});
}
for line in after_range.clone() {
lines.push(Line {
kind: LineKind::Add,
old_no: None,
new_no: Some(line + 1),
text: after_lines[line].to_owned(),
});
}
old_at = before_range.end;
new_at = after_range.end;
}
while old_at < old_to && new_at < new_to {
lines.push(Line {
kind: LineKind::Context,
old_no: Some(old_at + 1),
new_no: Some(new_at + 1),
text: before_lines[old_at].to_owned(),
});
old_at += 1;
new_at += 1;
}
let old_lines = old_at - old_from;
let new_lines = new_at - new_from;
out.push(Hunk {
old_start: if old_lines == 0 { 0 } else { old_from + 1 },
old_lines,
new_start: if new_lines == 0 { 0 } else { new_from + 1 },
new_lines,
lines,
});
}
FileDiff {
hunks: out,
insertions,
deletions,
}
}
pub fn unified(diff: &FileDiff) -> String {
let mut s = String::new();
for h in &diff.hunks {
s.push_str(&format!(
"@@ -{},{} +{},{} @@\n",
h.old_start, h.old_lines, h.new_start, h.new_lines
));
for l in &h.lines {
s.push(match l.kind {
LineKind::Context => ' ',
LineKind::Add => '+',
LineKind::Del => '-',
});
s.push_str(&l.text);
s.push('\n');
}
}
s
}