#![forbid(unsafe_code)]
mod assign;
mod linear;
mod myers;
mod patience;
mod post;
mod script;
mod stable;
use std::collections::HashMap;
use std::hash::Hash;
use std::ops::Range;
pub use script::{ScriptError, apply, edit_cost, validate};
pub const LINEAR_SPACE_THRESHOLD: usize = 10_000;
pub const STABLE_LIMIT: usize = 1 << 20;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Op {
Equal {
a: usize,
b: usize,
},
Delete {
a: usize,
},
Insert {
b: usize,
},
Move {
a: usize,
b: usize,
},
Replace {
a: Range<usize>,
b: Range<usize>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Algorithm {
#[default]
Myers,
MyersLinearSpace,
Patience,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TieBreak {
#[default]
Stable,
Myers,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Cleanup {
#[default]
None,
Semantic {
min_equal_run: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiffOptions {
pub algorithm: Algorithm,
pub tie_break: TieBreak,
pub detect_moves: bool,
pub cleanup: Cleanup,
pub pair_replacements: bool,
}
impl Default for DiffOptions {
fn default() -> Self {
Self {
algorithm: Algorithm::Myers,
tie_break: TieBreak::Stable,
detect_moves: true,
cleanup: Cleanup::None,
pair_replacements: true,
}
}
}
impl DiffOptions {
pub fn raw() -> Self {
Self {
detect_moves: false,
pair_replacements: false,
..Self::default()
}
}
}
pub fn diff<T, K: Eq + Hash>(
a: &[T],
b: &[T],
key: impl Fn(&T) -> K,
opts: &DiffOptions,
) -> Vec<Op> {
Differ::new(a, b, key).options(opts.clone()).run()
}
type CostFn<'c> = Box<dyn Fn(usize, usize) -> f64 + 'c>;
type Classes = (Vec<Option<u32>>, Vec<Option<u32>>);
pub struct Differ<'c, T> {
a: &'c [T],
b: &'c [T],
keys_a: Vec<u32>,
keys_b: Vec<u32>,
classes: Option<Classes>,
cost: Option<CostFn<'c>>,
opts: DiffOptions,
}
impl<'c, T> Differ<'c, T> {
pub fn new<K: Eq + Hash>(a: &'c [T], b: &'c [T], key: impl Fn(&T) -> K) -> Self {
let (keys_a, keys_b) = intern(a, b, key);
Self {
a,
b,
keys_a,
keys_b,
classes: None,
cost: None,
opts: DiffOptions::default(),
}
}
pub fn options(mut self, opts: DiffOptions) -> Self {
self.opts = opts;
self
}
pub fn cost(mut self, cost: impl Fn(usize, usize) -> f64 + 'c) -> Self {
self.cost = Some(Box::new(cost));
self
}
pub fn class<C: Eq + Hash>(mut self, class: impl Fn(&T) -> Option<C>) -> Self {
let mut ids = HashMap::new();
let mut id = |item: &T| {
class(item).map(|c| {
let next = ids.len() as u32;
*ids.entry(c).or_insert(next)
})
};
let ca = self.a.iter().map(&mut id).collect();
let cb = self.b.iter().map(&mut id).collect();
self.classes = Some((ca, cb));
self
}
pub fn run(&self) -> Vec<Op> {
let (a, b) = (&self.keys_a[..], &self.keys_b[..]);
let mut ops = core_diff(a, b, self.opts.algorithm, self.opts.tie_break);
post::normalize(&mut ops);
let index_cost = |i: usize, j: usize| i.abs_diff(j) as f64;
let cost: &dyn Fn(usize, usize) -> f64 = match &self.cost {
Some(c) => c,
None => &index_cost,
};
if self.opts.detect_moves {
ops = post::detect_moves(ops, a, b, cost);
}
if let Cleanup::Semantic { min_equal_run } = self.opts.cleanup {
post::semantic_cleanup(&mut ops, min_equal_run);
}
if self.opts.pair_replacements {
ops = post::pair_adjacent(ops);
if let Some((ca, cb)) = &self.classes {
ops = post::pair_cross_hunk(ops, ca, cb, cost);
}
}
ops
}
}
pub fn expand(
ops: &[Op],
a: &[Range<usize>],
b: &[Range<usize>],
mut inner: impl FnMut(Range<usize>, Range<usize>) -> Vec<Op>,
) -> Vec<Op> {
let items = |g: &[Range<usize>], r: Range<usize>| match (g.get(r.start), r.end.checked_sub(1)) {
(Some(first), Some(last)) if r.start < r.end => first.start..g[last].end,
_ => 0..0,
};
let mut out = Vec::new();
for op in ops {
match op {
Op::Equal { a: i, b: j } | Op::Move { a: i, b: j } => {
let (ra, rb) = (a[*i].clone(), b[*j].clone());
let n = ra.len().min(rb.len());
for k in 0..n {
let (a, b) = (ra.start + k, rb.start + k);
out.push(match op {
Op::Equal { .. } => Op::Equal { a, b },
_ => Op::Move { a, b },
});
}
out.extend((ra.start + n..ra.end).map(|a| Op::Delete { a }));
out.extend((rb.start + n..rb.end).map(|b| Op::Insert { b }));
}
Op::Delete { a: i } => out.extend(a[*i].clone().map(|a| Op::Delete { a })),
Op::Insert { b: j } => out.extend(b[*j].clone().map(|b| Op::Insert { b })),
Op::Replace { a: ga, b: gb } => {
let (ra, rb) = (items(a, ga.clone()), items(b, gb.clone()));
let (ao, bo) = (ra.start, rb.start);
out.extend(inner(ra, rb).into_iter().map(|op| match op {
Op::Equal { a, b } => Op::Equal {
a: a + ao,
b: b + bo,
},
Op::Move { a, b } => Op::Move {
a: a + ao,
b: b + bo,
},
Op::Delete { a } => Op::Delete { a: a + ao },
Op::Insert { b } => Op::Insert { b: b + bo },
Op::Replace { a, b } => Op::Replace {
a: a.start + ao..a.end + ao,
b: b.start + bo..b.end + bo,
},
}));
}
}
}
out
}
fn intern<T, K: Eq + Hash>(a: &[T], b: &[T], key: impl Fn(&T) -> K) -> (Vec<u32>, Vec<u32>) {
let mut ids = HashMap::new();
let mut id = |item: &T| {
let next = ids.len() as u32;
*ids.entry(key(item)).or_insert(next)
};
let ka = a.iter().map(&mut id).collect();
let kb = b.iter().map(&mut id).collect();
(ka, kb)
}
fn core_diff(a: &[u32], b: &[u32], algorithm: Algorithm, tie_break: TieBreak) -> Vec<Op> {
let mut out = Vec::with_capacity(a.len().max(b.len()));
let sink = &mut Sink::new(&mut out);
let myers = match algorithm {
Algorithm::Patience => {
patience::diff(a, b, 0, 0, sink);
return out;
}
Algorithm::Myers if a.len() + b.len() <= LINEAR_SPACE_THRESHOLD => myers::diff,
Algorithm::Myers | Algorithm::MyersLinearSpace => linear::diff,
};
let fits = |a: &[u32], b: &[u32]| a.len().saturating_mul(b.len()) <= STABLE_LIMIT;
match tie_break {
TieBreak::Myers => myers(a, b, 0, 0, sink),
TieBreak::Stable if fits(a, b) => trimmed_none(a, b, sink),
TieBreak::Stable => trimmed(a, b, 0, 0, sink, |a, b, ao, bo, sink| {
if fits(a, b) {
stable::diff(a, b, ao, bo, sink)
} else {
myers(a, b, ao, bo, sink)
}
}),
}
out
}
fn trimmed_none(a: &[u32], b: &[u32], sink: &mut Sink) {
match (a.is_empty(), b.is_empty()) {
(true, _) => (0..b.len()).for_each(|j| sink.insert(j)),
(false, true) => (0..a.len()).for_each(|i| sink.delete(i)),
(false, false) => stable::diff(a, b, 0, 0, sink),
}
}
struct Sink<'o> {
out: &'o mut Vec<Op>,
}
impl<'o> Sink<'o> {
fn new(out: &'o mut Vec<Op>) -> Self {
Self { out }
}
fn equal(&mut self, a: usize, b: usize) {
self.out.push(Op::Equal { a, b });
}
fn delete(&mut self, a: usize) {
self.out.push(Op::Delete { a });
}
fn insert(&mut self, b: usize) {
self.out.push(Op::Insert { b });
}
}
fn emit_prefix(a: &[u32], b: &[u32], ao: usize, bo: usize, sink: &mut Sink) -> usize {
let n = a.iter().zip(b).take_while(|(x, y)| x == y).count();
for i in 0..n {
sink.equal(ao + i, bo + i);
}
n
}
fn suffix_len(a: &[u32], b: &[u32]) -> usize {
a.iter()
.rev()
.zip(b.iter().rev())
.take_while(|(x, y)| x == y)
.count()
}
fn trimmed(
a: &[u32],
b: &[u32],
ao: usize,
bo: usize,
sink: &mut Sink,
middle: impl FnOnce(&[u32], &[u32], usize, usize, &mut Sink),
) {
let p = emit_prefix(a, b, ao, bo, sink);
let (a, b) = (&a[p..], &b[p..]);
let s = suffix_len(a, b);
let (am, bm) = (&a[..a.len() - s], &b[..b.len() - s]);
let (ao, bo) = (ao + p, bo + p);
match (am.is_empty(), bm.is_empty()) {
(true, true) => {}
(true, false) => (0..bm.len()).for_each(|j| sink.insert(bo + j)),
(false, true) => (0..am.len()).for_each(|i| sink.delete(ao + i)),
(false, false) => middle(am, bm, ao, bo, sink),
}
for i in 0..s {
sink.equal(ao + am.len() + i, bo + bm.len() + i);
}
}