use std::ops::Range;
use std::time::Duration;
use similar::{Algorithm, ChangeTag, DiffOp, TextDiff};
use crate::display::Padding;
use crate::text::Document;
pub const DIFF_TIMEOUT: Duration = Duration::from_secs(2);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RowKind {
Equal,
Added,
Removed,
Changed,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DiffRow {
pub kind: RowKind,
pub left: Option<usize>,
pub right: Option<usize>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct DiffStats {
pub added: usize,
pub removed: usize,
pub changed: usize,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct LineDiff {
pub rows: Vec<DiffRow>,
pub hunks: Vec<Range<usize>>,
pub stats: DiffStats,
}
fn lines(doc: &Document) -> Vec<String> {
(0..doc.line_count()).map(|l| doc.line_text(l)).collect()
}
pub fn diff_lines(left: &Document, right: &Document) -> LineDiff {
let a = lines(left);
let b = lines(right);
let a_refs: Vec<&str> = a.iter().map(String::as_str).collect();
let b_refs: Vec<&str> = b.iter().map(String::as_str).collect();
let diff = TextDiff::configure()
.algorithm(Algorithm::Myers)
.timeout(DIFF_TIMEOUT)
.diff_slices(&a_refs, &b_refs);
let mut out = LineDiff::default();
let push = |out: &mut LineDiff, kind, left, right| {
if kind != RowKind::Equal {
let row = out.rows.len();
match out.hunks.last_mut() {
Some(h) if h.end == row => h.end += 1,
_ => out.hunks.push(row..row + 1),
}
match kind {
RowKind::Added => out.stats.added += 1,
RowKind::Removed => out.stats.removed += 1,
RowKind::Changed => out.stats.changed += 1,
RowKind::Equal => {}
}
}
out.rows.push(DiffRow { kind, left, right });
};
for op in diff.ops() {
match *op {
DiffOp::Equal {
old_index,
new_index,
len,
} => {
for i in 0..len {
push(
&mut out,
RowKind::Equal,
Some(old_index + i),
Some(new_index + i),
);
}
}
DiffOp::Delete {
old_index, old_len, ..
} => {
for i in 0..old_len {
push(&mut out, RowKind::Removed, Some(old_index + i), None);
}
}
DiffOp::Insert {
new_index, new_len, ..
} => {
for i in 0..new_len {
push(&mut out, RowKind::Added, None, Some(new_index + i));
}
}
DiffOp::Replace {
old_index,
old_len,
new_index,
new_len,
} => {
for i in 0..old_len.max(new_len) {
let l = (i < old_len).then_some(old_index + i);
let r = (i < new_len).then_some(new_index + i);
let kind = match (l, r) {
(Some(_), Some(_)) => RowKind::Changed,
(Some(_), None) => RowKind::Removed,
_ => RowKind::Added,
};
push(&mut out, kind, l, r);
}
}
}
}
out
}
pub fn diff_words(left: &str, right: &str) -> (Vec<Range<usize>>, Vec<Range<usize>>) {
let diff = TextDiff::configure()
.timeout(DIFF_TIMEOUT)
.diff_words(left, right);
let (mut a, mut b) = (Vec::new(), Vec::new());
let (mut ia, mut ib) = (0, 0);
let add = |ranges: &mut Vec<Range<usize>>, start: usize, len: usize| match ranges.last_mut() {
Some(last) if last.end == start => last.end += len,
_ => ranges.push(start..start + len),
};
for change in diff.iter_all_changes() {
let len = change.value().len();
match change.tag() {
ChangeTag::Equal => {
ia += len;
ib += len;
}
ChangeTag::Delete => {
add(&mut a, ia, len);
ia += len;
}
ChangeTag::Insert => {
add(&mut b, ib, len);
ib += len;
}
}
}
(a, b)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Side {
Left,
Right,
}
impl DiffRow {
pub fn line(&self, side: Side) -> Option<usize> {
match side {
Side::Left => self.left,
Side::Right => self.right,
}
}
}
impl LineDiff {
pub fn line_rows(&self, side: Side) -> Vec<usize> {
self.rows
.iter()
.enumerate()
.filter(|(_, r)| r.line(side).is_some())
.map(|(i, _)| i)
.collect()
}
pub fn padding(&self, side: Side) -> Padding {
let mut p = Padding::default();
let mut blank = 0;
for r in &self.rows {
if r.line(side).is_some() {
p.before.push(blank);
blank = 0;
} else {
blank += 1;
}
}
p.after_last = blank;
p
}
}
#[cfg(test)]
mod tests {
use super::*;
fn kinds(d: &LineDiff) -> Vec<(RowKind, Option<usize>, Option<usize>)> {
d.rows.iter().map(|r| (r.kind, r.left, r.right)).collect()
}
#[test]
fn rows_align_with_fillers() {
let left = Document::new("a\nb\nc\nd");
let right = Document::new("a\nB\nc\nnew\nd");
let d = diff_lines(&left, &right);
use RowKind::*;
assert_eq!(
kinds(&d),
vec![
(Equal, Some(0), Some(0)),
(Changed, Some(1), Some(1)),
(Equal, Some(2), Some(2)),
(Added, None, Some(3)),
(Equal, Some(3), Some(4)),
]
);
assert_eq!(d.hunks, vec![1..2, 3..4]);
assert_eq!(
d.stats,
DiffStats {
added: 1,
removed: 0,
changed: 1
}
);
}
#[test]
fn uneven_replacements_pair_then_spill() {
let left = Document::new("x\none\ntwo\nthree\ny");
let right = Document::new("x\nuno\ny");
let d = diff_lines(&left, &right);
use RowKind::*;
assert_eq!(
kinds(&d),
vec![
(Equal, Some(0), Some(0)),
(Changed, Some(1), Some(1)),
(Removed, Some(2), None),
(Removed, Some(3), None),
(Equal, Some(4), Some(2)),
]
);
assert_eq!(d.hunks, vec![1..4]);
}
#[test]
fn identical_documents_have_no_hunks() {
let doc = Document::new("same\ntext\n");
let d = diff_lines(&doc, &doc.clone());
assert!(d.hunks.is_empty());
assert_eq!(d.rows.len(), 3);
}
#[test]
fn word_ranges() {
let (a, b) = diff_words("let total = price * qty;", "let total = cost * qty + tax;");
let left = "let total = price * qty;";
let right = "let total = cost * qty + tax;";
let words = |s: &str, r: &[Range<usize>]| -> Vec<String> {
r.iter().map(|r| s[r.clone()].to_owned()).collect()
};
assert_eq!(words(left, &a), vec!["price", "qty;"]);
assert_eq!(words(right, &b), vec!["cost", "qty + tax;"]);
}
#[test]
fn padding_lines_up_both_sides() {
let d = diff_lines(
&Document::new(
"a
b",
),
&Document::new(
"new
a
new
b
end",
),
);
let left = d.padding(Side::Left);
assert_eq!(left.before, vec![1, 1]);
assert_eq!(left.after_last, 1);
assert_eq!(
d.padding(Side::Right),
Padding {
before: vec![0; 5],
after_last: 0
}
);
assert_eq!(d.line_rows(Side::Left), vec![1, 3]);
assert_eq!(d.line_rows(Side::Right), vec![0, 1, 2, 3, 4]);
}
}