use super::*;
pub(crate) fn unified_diff(old: &[String], new: &[String], context: usize) -> String {
if old == new {
return String::new();
}
let ops = lcs_diff(old, new);
format_unified(&ops, old, new, context)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DiffOp {
Equal,
Delete,
Insert,
}
pub(crate) fn lcs_diff(old: &[String], new: &[String]) -> Vec<(DiffOp, usize, usize)> {
let n = old.len();
let m = new.len();
let mut dp = vec![vec![0usize; m + 1]; n + 1];
for i in (0..n).rev() {
for j in (0..m).rev() {
dp[i][j] = if old[i] == new[j] {
dp[i + 1][j + 1] + 1
} else {
dp[i + 1][j].max(dp[i][j + 1])
};
}
}
let mut ops = Vec::new();
let (mut i, mut j) = (0usize, 0usize);
while i < n && j < m {
if old[i] == new[j] {
ops.push((DiffOp::Equal, i, j));
i += 1;
j += 1;
} else if dp[i + 1][j] >= dp[i][j + 1] {
ops.push((DiffOp::Delete, i, j));
i += 1;
} else {
ops.push((DiffOp::Insert, i, j));
j += 1;
}
}
while i < n {
ops.push((DiffOp::Delete, i, j));
i += 1;
}
while j < m {
ops.push((DiffOp::Insert, i, j));
j += 1;
}
ops
}
pub(crate) fn format_unified(
ops: &[(DiffOp, usize, usize)],
old: &[String],
new: &[String],
context: usize,
) -> String {
let is_change: Vec<bool> = ops.iter().map(|(o, _, _)| *o != DiffOp::Equal).collect();
let mut out = String::new();
let mut idx = 0usize;
while idx < ops.len() {
if !is_change[idx] {
idx += 1;
continue;
}
let mut start = idx;
let mut ctx_back = 0;
while start > 0 && !is_change[start - 1] && ctx_back < context {
start -= 1;
ctx_back += 1;
}
let mut end = idx;
while end < ops.len() {
if is_change[end] {
end += 1;
continue;
}
let mut run = end;
while run < ops.len() && !is_change[run] {
run += 1;
}
let equal_len = run - end;
if run < ops.len() && equal_len <= context.saturating_mul(2) {
end = run; } else {
end += equal_len.min(context);
break;
}
}
let slice = &ops[start..end];
let (mut old_lo, mut new_lo) = (usize::MAX, usize::MAX);
let (mut old_count, mut new_count) = (0usize, 0usize);
let mut body = String::new();
for (op, oi, nj) in slice {
match op {
DiffOp::Equal => {
old_lo = old_lo.min(*oi);
new_lo = new_lo.min(*nj);
old_count += 1;
new_count += 1;
body.push_str(&format!(" {}\n", old[*oi]));
}
DiffOp::Delete => {
old_lo = old_lo.min(*oi);
old_count += 1;
body.push_str(&format!("-{}\n", old[*oi]));
}
DiffOp::Insert => {
new_lo = new_lo.min(*nj);
new_count += 1;
body.push_str(&format!("+{}\n", new[*nj]));
}
}
}
if old_lo == usize::MAX {
old_lo = 0;
}
if new_lo == usize::MAX {
new_lo = 0;
}
let old_start = if old_count == 0 { old_lo } else { old_lo + 1 };
let new_start = if new_count == 0 { new_lo } else { new_lo + 1 };
out.push_str(&format!(
"@@ -{old_start},{old_count} +{new_start},{new_count} @@\n"
));
out.push_str(&body);
idx = end;
}
out
}
pub(crate) fn split_lines(content: &str) -> Vec<String> {
if content.is_empty() {
return Vec::new();
}
let trimmed = content.strip_suffix('\n').unwrap_or(content);
trimmed.split('\n').map(str::to_string).collect()
}
pub(crate) fn line_count(content: &str) -> usize {
split_lines(content).len()
}
pub(crate) fn first_line(s: &str) -> String {
s.lines().next().unwrap_or("").trim().to_string()
}
pub(crate) fn strip_gutter(snippet: &str) -> Vec<(usize, String)> {
let mut out = Vec::new();
for raw in snippet.split('\n') {
let line = raw;
let trimmed = line.trim_start();
let digits: String = trimmed.chars().take_while(char::is_ascii_digit).collect();
if digits.is_empty() {
continue;
}
let rest = &trimmed[digits.len()..];
let text = if let Some(t) = rest.strip_prefix('\t') {
t
} else if let Some(t) = rest.strip_prefix('\u{2192}') {
t
} else {
continue;
};
if let Ok(n) = digits.parse::<usize>() {
out.push((n, text.to_string()));
}
}
out
}
pub(crate) fn parse_turn_range(s: &str) -> Result<crate::text::RangeSpec> {
crate::text::parse_range_spec(s, "--turn", false)
}
pub(crate) fn parse_line_range(s: &str) -> Result<crate::text::RangeSpec> {
crate::text::parse_range_spec(s, "--file-lines", true)
}
pub(crate) fn truncate_excerpt(s: &str) -> String {
crate::text::truncate_excerpt(s, EXCERPT_MAX)
}