use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DiffLine<'a> {
pub content: &'a [u8],
pub has_newline: bool,
}
impl<'a> DiffLine<'a> {
pub fn bytes_without_newline(&self) -> &'a [u8] {
if self.has_newline {
self.content.strip_suffix(b"\n").unwrap_or(self.content)
} else {
self.content
}
}
}
pub fn split_lines(blob: &[u8]) -> Vec<DiffLine<'_>> {
let mut lines = Vec::new();
let mut start = 0usize;
let len = blob.len();
let mut idx = 0usize;
while idx < len {
if blob[idx] == b'\n' {
lines.push(DiffLine {
content: &blob[start..=idx],
has_newline: true,
});
idx += 1;
start = idx;
} else {
idx += 1;
}
}
if start < len {
lines.push(DiffLine {
content: &blob[start..len],
has_newline: false,
});
}
lines
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiffOp {
Equal(usize),
Delete(usize),
Insert(usize),
}
pub fn myers_diff_lines(old: &[DiffLine<'_>], new: &[DiffLine<'_>]) -> Vec<DiffOp> {
let n_total = old.len();
let m_total = new.len();
let mut prefix = 0usize;
while prefix < n_total && prefix < m_total && old[prefix] == new[prefix] {
prefix += 1;
}
let mut suffix = 0usize;
while suffix < n_total - prefix
&& suffix < m_total - prefix
&& old[n_total - 1 - suffix] == new[m_total - 1 - suffix]
{
suffix += 1;
}
let old_mid = &old[prefix..n_total - suffix];
let new_mid = &new[prefix..m_total - suffix];
let mut ops: Vec<DiffOp> = Vec::new();
if prefix > 0 {
ops.push(DiffOp::Equal(prefix));
}
myers_core(old_mid, new_mid, &mut ops);
if suffix > 0 {
ops.push(DiffOp::Equal(suffix));
}
coalesce_ops(ops)
}
fn myers_core(old: &[DiffLine<'_>], new: &[DiffLine<'_>], out: &mut Vec<DiffOp>) {
let n = old.len() as isize;
let m = new.len() as isize;
if n == 0 {
if m > 0 {
out.push(DiffOp::Insert(m as usize));
}
return;
}
if m == 0 {
out.push(DiffOp::Delete(n as usize));
return;
}
let max = (n + m) as usize;
let offset = max as isize; let width = 2 * max + 1;
let mut v = vec![0isize; width];
let mut trace: Vec<Vec<isize>> = Vec::new();
let mut found_d: Option<usize> = None;
'search: for d in 0..=(max as isize) {
trace.push(v.clone());
let mut k = -d;
while k <= d {
let kidx = (k + offset) as usize;
let mut x = if k == -d
|| (k != d && v[(k - 1 + offset) as usize] < v[(k + 1 + offset) as usize])
{
v[(k + 1 + offset) as usize]
} else {
v[(k - 1 + offset) as usize] + 1
};
let mut y = x - k;
while x < n && y < m && old[x as usize] == new[y as usize] {
x += 1;
y += 1;
}
v[kidx] = x;
if x >= n && y >= m {
found_d = Some(d as usize);
break 'search;
}
k += 2;
}
}
let Some(d_end) = found_d else {
out.push(DiffOp::Delete(n as usize));
out.push(DiffOp::Insert(m as usize));
return;
};
backtrack(n, m, &trace, d_end, offset, out);
}
fn backtrack(
n: isize,
m: isize,
trace: &[Vec<isize>],
d_end: usize,
offset: isize,
out: &mut Vec<DiffOp>,
) {
let mut x = n;
let mut y = m;
let mut rev: Vec<DiffOp> = Vec::new();
for d in (0..=d_end).rev() {
let v = &trace[d];
let k = x - y;
let prev_k = if k == -(d as isize)
|| (k != d as isize && v[(k - 1 + offset) as usize] < v[(k + 1 + offset) as usize])
{
k + 1 } else {
k - 1 };
let prev_x = v[(prev_k + offset) as usize];
let prev_y = prev_x - prev_k;
while x > prev_x && y > prev_y {
rev.push(DiffOp::Equal(1));
x -= 1;
y -= 1;
}
if d > 0 {
if x == prev_x {
rev.push(DiffOp::Insert(1));
} else {
rev.push(DiffOp::Delete(1));
}
x = prev_x;
y = prev_y;
}
}
rev.reverse();
out.extend(rev);
}
fn coalesce_ops(ops: Vec<DiffOp>) -> Vec<DiffOp> {
let mut out: Vec<DiffOp> = Vec::with_capacity(ops.len());
for op in ops {
match (out.last_mut(), op) {
(Some(DiffOp::Equal(prev)), DiffOp::Equal(n)) => *prev += n,
(Some(DiffOp::Delete(prev)), DiffOp::Delete(n)) => *prev += n,
(Some(DiffOp::Insert(prev)), DiffOp::Insert(n)) => *prev += n,
_ => out.push(op),
}
}
out
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct WsIgnore {
pub all_space: bool,
pub space_change: bool,
pub space_at_eol: bool,
pub cr_at_eol: bool,
}
impl WsIgnore {
pub const EMPTY: Self = Self {
all_space: false,
space_change: false,
space_at_eol: false,
cr_at_eol: false,
};
pub fn is_empty(&self) -> bool {
!(self.all_space || self.space_change || self.space_at_eol || self.cr_at_eol)
}
}
#[inline]
fn xdl_isspace(c: u8) -> bool {
matches!(c, b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c)
}
pub(crate) fn canonicalize_line_for_match(line: &[u8], ignore: WsIgnore) -> Vec<u8> {
canonicalize_line(line, ignore)
}
pub(crate) fn canonicalize_line(line: &[u8], ignore: WsIgnore) -> Vec<u8> {
if ignore.all_space {
return line.iter().copied().filter(|&c| !xdl_isspace(c)).collect();
}
if ignore.space_change {
let mut out = Vec::with_capacity(line.len());
let mut i = 0usize;
while i < line.len() {
if xdl_isspace(line[i]) {
while i < line.len() && xdl_isspace(line[i]) {
i += 1;
}
out.push(b' ');
} else {
out.push(line[i]);
i += 1;
}
}
if out.last() == Some(&b' ') {
out.pop();
}
return out;
}
if ignore.space_at_eol {
let mut end = line.len();
while end > 0 && xdl_isspace(line[end - 1]) {
end -= 1;
}
return line[..end].to_vec();
}
if ignore.cr_at_eol {
if let Some(stripped) = line.strip_suffix(b"\n") {
if let Some(without_cr) = stripped.strip_suffix(b"\r") {
let mut out = without_cr.to_vec();
out.push(b'\n');
return out;
}
} else if let Some(without_cr) = line.strip_suffix(b"\r") {
return without_cr.to_vec();
}
return line.to_vec();
}
line.to_vec()
}
pub(crate) fn line_is_blank(line: &[u8], ignore: WsIgnore) -> bool {
if ignore.is_empty() {
line.len() <= 1
} else {
line.iter().all(|&c| xdl_isspace(c))
}
}
pub fn myers_diff_lines_ws(
old: &[DiffLine<'_>],
new: &[DiffLine<'_>],
ignore: WsIgnore,
algorithm: DiffAlgorithm,
) -> Vec<DiffOp> {
if ignore.is_empty() {
return diff_lines_with_algorithm(old, new, algorithm);
}
let old_canon: Vec<Vec<u8>> = old
.iter()
.map(|l| canonicalize_line(l.content, ignore))
.collect();
let new_canon: Vec<Vec<u8>> = new
.iter()
.map(|l| canonicalize_line(l.content, ignore))
.collect();
let old_lines: Vec<DiffLine<'_>> = old_canon
.iter()
.map(|c| DiffLine {
content: c.as_slice(),
has_newline: true,
})
.collect();
let new_lines: Vec<DiffLine<'_>> = new_canon
.iter()
.map(|c| DiffLine {
content: c.as_slice(),
has_newline: true,
})
.collect();
diff_lines_with_algorithm(&old_lines, &new_lines, algorithm)
}
type LineKey<'a> = (&'a [u8], bool);
#[inline]
fn line_key<'a>(line: &DiffLine<'a>) -> LineKey<'a> {
(line.content, line.has_newline)
}
pub fn patience_diff_lines(old: &[DiffLine<'_>], new: &[DiffLine<'_>]) -> Vec<DiffOp> {
patience_diff_lines_anchored(old, new, &[])
}
pub fn patience_diff_lines_anchored(
old: &[DiffLine<'_>],
new: &[DiffLine<'_>],
anchors: &[Vec<u8>],
) -> Vec<DiffOp> {
let mut ops: Vec<DiffOp> = Vec::new();
patience_recurse(old, new, 0, old.len(), 0, new.len(), anchors, &mut ops);
coalesce_ops(ops)
}
pub fn histogram_diff_lines(old: &[DiffLine<'_>], new: &[DiffLine<'_>]) -> Vec<DiffOp> {
let mut ops: Vec<DiffOp> = Vec::new();
histogram_recurse(old, new, 0, old.len(), 0, new.len(), &mut ops);
coalesce_ops(ops)
}
pub fn diff_lines_with_algorithm(
old: &[DiffLine<'_>],
new: &[DiffLine<'_>],
algorithm: DiffAlgorithm,
) -> Vec<DiffOp> {
match algorithm {
DiffAlgorithm::Myers | DiffAlgorithm::Minimal => myers_diff_lines(old, new),
DiffAlgorithm::Patience => patience_diff_lines(old, new),
DiffAlgorithm::Histogram => histogram_diff_lines(old, new),
}
}
fn emit_trivial_range(a0: usize, a1: usize, b0: usize, b1: usize, out: &mut Vec<DiffOp>) -> bool {
let old_len = a1 - a0;
let new_len = b1 - b0;
if old_len == 0 && new_len == 0 {
return true;
}
if old_len == 0 {
out.push(DiffOp::Insert(new_len));
return true;
}
if new_len == 0 {
out.push(DiffOp::Delete(old_len));
return true;
}
false
}
fn trim_common(
old: &[DiffLine<'_>],
new: &[DiffLine<'_>],
mut a0: usize,
mut a1: usize,
mut b0: usize,
mut b1: usize,
out: &mut Vec<DiffOp>,
) -> (usize, usize, usize, usize, usize) {
let mut prefix = 0usize;
while a0 < a1 && b0 < b1 && old[a0] == new[b0] {
a0 += 1;
b0 += 1;
prefix += 1;
}
if prefix > 0 {
out.push(DiffOp::Equal(prefix));
}
let mut suffix = 0usize;
while a1 > a0 && b1 > b0 && old[a1 - 1] == new[b1 - 1] {
a1 -= 1;
b1 -= 1;
suffix += 1;
}
(a0, a1, b0, b1, suffix)
}
#[allow(clippy::too_many_arguments)]
fn patience_recurse(
old: &[DiffLine<'_>],
new: &[DiffLine<'_>],
a0: usize,
a1: usize,
b0: usize,
b1: usize,
anchors: &[Vec<u8>],
out: &mut Vec<DiffOp>,
) {
if emit_trivial_range(a0, a1, b0, b1, out) {
return;
}
let (a0, a1, b0, b1, suffix) = trim_common(old, new, a0, a1, b0, b1, out);
if !emit_trivial_range(a0, a1, b0, b1, out) {
match patience_anchors(old, new, a0, a1, b0, b1, anchors) {
Some(aligned) => {
let mut cur_a = a0;
let mut cur_b = b0;
for (ai, bi) in aligned {
patience_recurse(old, new, cur_a, ai, cur_b, bi, anchors, out);
out.push(DiffOp::Equal(1));
cur_a = ai + 1;
cur_b = bi + 1;
}
patience_recurse(old, new, cur_a, a1, cur_b, b1, anchors, out);
}
None => myers_core(&old[a0..a1], &new[b0..b1], out),
}
}
if suffix > 0 {
out.push(DiffOp::Equal(suffix));
}
}
fn patience_anchors(
old: &[DiffLine<'_>],
new: &[DiffLine<'_>],
a0: usize,
a1: usize,
b0: usize,
b1: usize,
anchors: &[Vec<u8>],
) -> Option<Vec<(usize, usize)>> {
struct Occ {
count: usize,
pos: usize,
}
let mut in_old: HashMap<LineKey<'_>, Occ> = HashMap::new();
for (i, line) in old.iter().enumerate().take(a1).skip(a0) {
in_old
.entry(line_key(line))
.and_modify(|o| o.count += 1)
.or_insert(Occ { count: 1, pos: i });
}
let mut in_new: HashMap<LineKey<'_>, Occ> = HashMap::new();
for (j, line) in new.iter().enumerate().take(b1).skip(b0) {
in_new
.entry(line_key(line))
.and_modify(|o| o.count += 1)
.or_insert(Occ { count: 1, pos: j });
}
let mut pairs: Vec<(usize, usize)> = Vec::new();
for (i, line) in old.iter().enumerate().take(a1).skip(a0) {
let key = line_key(line);
let Some(o) = in_old.get(&key) else { continue };
if o.count != 1 || o.pos != i {
continue;
}
if let Some(n) = in_new.get(&key)
&& n.count == 1
{
pairs.push((i, n.pos));
}
}
if pairs.is_empty() {
return None;
}
let lis = if anchors.is_empty() {
longest_increasing_by_new(&pairs)
} else {
let is_anchor: Vec<bool> = pairs
.iter()
.map(|&(_, nj)| line_matches_anchor(new[nj].content, anchors))
.collect();
longest_increasing_by_new_anchored(&pairs, &is_anchor)
};
if lis.is_empty() { None } else { Some(lis) }
}
fn line_matches_anchor(line: &[u8], anchors: &[Vec<u8>]) -> bool {
anchors.iter().any(|anchor| line.starts_with(anchor))
}
fn longest_increasing_by_new(pairs: &[(usize, usize)]) -> Vec<(usize, usize)> {
if pairs.is_empty() {
return Vec::new();
}
let mut tails: Vec<usize> = Vec::new();
let mut prev: Vec<Option<usize>> = vec![None; pairs.len()];
for i in 0..pairs.len() {
let val = pairs[i].1;
let mut lo = 0usize;
let mut hi = tails.len();
while lo < hi {
let mid = lo + (hi - lo) / 2;
if pairs[tails[mid]].1 < val {
lo = mid + 1;
} else {
hi = mid;
}
}
if lo > 0 {
prev[i] = Some(tails[lo - 1]);
}
if lo == tails.len() {
tails.push(i);
} else {
tails[lo] = i;
}
}
let mut result: Vec<(usize, usize)> = Vec::with_capacity(tails.len());
let mut cur = tails.last().copied();
while let Some(i) = cur {
result.push(pairs[i]);
cur = prev[i];
}
result.reverse();
result
}
fn longest_increasing_by_new_anchored(
pairs: &[(usize, usize)],
is_anchor: &[bool],
) -> Vec<(usize, usize)> {
if pairs.is_empty() {
return Vec::new();
}
let mut sequence: Vec<usize> = Vec::with_capacity(pairs.len());
let mut prev: Vec<Option<usize>> = vec![None; pairs.len()];
let mut longest: usize = 0;
let mut anchor_i: isize = -1;
for (e, &(_, val)) in pairs.iter().enumerate() {
let i: isize = if longest == 0 || val > pairs[sequence[longest - 1]].1 {
longest as isize - 1
} else {
let mut lo = 0usize;
let mut hi = longest;
while lo < hi {
let mid = lo + (hi - lo) / 2;
if pairs[sequence[mid]].1 < val {
lo = mid + 1;
} else {
hi = mid;
}
}
lo as isize - 1
};
prev[e] = if i < 0 {
None
} else {
Some(sequence[i as usize])
};
let pos = (i + 1) as usize;
if (pos as isize) <= anchor_i {
continue;
}
if pos == sequence.len() {
sequence.push(e);
} else {
sequence[pos] = e;
}
if is_anchor[e] {
anchor_i = pos as isize;
longest = pos + 1;
} else if pos == longest {
longest += 1;
}
}
if longest == 0 {
return Vec::new();
}
let mut result: Vec<(usize, usize)> = Vec::with_capacity(longest);
let mut cur = Some(sequence[longest - 1]);
while let Some(i) = cur {
result.push(pairs[i]);
cur = prev[i];
}
result.reverse();
result
}
fn histogram_recurse(
old: &[DiffLine<'_>],
new: &[DiffLine<'_>],
a0: usize,
a1: usize,
b0: usize,
b1: usize,
out: &mut Vec<DiffOp>,
) {
if emit_trivial_range(a0, a1, b0, b1, out) {
return;
}
let (a0, a1, b0, b1, suffix) = trim_common(old, new, a0, a1, b0, b1, out);
if !emit_trivial_range(a0, a1, b0, b1, out) {
match histogram_region(old, new, a0, a1, b0, b1) {
Some(region) => {
histogram_recurse(old, new, a0, region.old_start, b0, region.new_start, out);
out.push(DiffOp::Equal(region.len));
histogram_recurse(
old,
new,
region.old_start + region.len,
a1,
region.new_start + region.len,
b1,
out,
);
}
None => myers_core(&old[a0..a1], &new[b0..b1], out),
}
}
if suffix > 0 {
out.push(DiffOp::Equal(suffix));
}
}
struct HistogramRegion {
old_start: usize,
new_start: usize,
len: usize,
}
fn histogram_region(
old: &[DiffLine<'_>],
new: &[DiffLine<'_>],
a0: usize,
a1: usize,
b0: usize,
b1: usize,
) -> Option<HistogramRegion> {
let mut buckets: HashMap<LineKey<'_>, Vec<usize>> = HashMap::new();
for (i, line) in old.iter().enumerate().take(a1).skip(a0) {
buckets.entry(line_key(line)).or_default().push(i);
}
let mut best: Option<HistogramRegion> = None;
let mut best_count = usize::MAX;
let mut best_len = 0usize;
let mut bj = b0;
while bj < b1 {
let key = line_key(&new[bj]);
let Some(positions) = buckets.get(&key) else {
bj += 1;
continue;
};
let occ = positions.len();
let mut next_bj = bj + 1;
for &ai in positions {
let mut start_a = ai;
let mut start_b = bj;
while start_a > a0 && start_b > b0 && old[start_a - 1] == new[start_b - 1] {
start_a -= 1;
start_b -= 1;
}
let mut len = 0usize;
while start_a + len < a1
&& start_b + len < b1
&& old[start_a + len] == new[start_b + len]
{
len += 1;
}
let run_count = occ;
let better = run_count < best_count || (run_count == best_count && len > best_len);
if better && len > 0 {
best_count = run_count;
best_len = len;
best = Some(HistogramRegion {
old_start: start_a,
new_start: start_b,
len,
});
if start_b + len > next_bj {
next_bj = start_b + len;
}
}
}
bj = next_bj.max(bj + 1);
}
best
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiffAlgorithm {
Myers,
Minimal,
Patience,
Histogram,
}