use std::ops::Range;
const MAX_CELLS: usize = 1_000_000;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Row {
Same { left: usize, right: usize },
Changed { left: usize, right: usize },
Added { right: usize },
Removed { left: usize },
}
impl Row {
pub fn left(&self) -> Option<usize> {
match *self {
Row::Same { left, .. } | Row::Changed { left, .. } | Row::Removed { left } => Some(left),
Row::Added { .. } => None,
}
}
pub fn right(&self) -> Option<usize> {
match *self {
Row::Same { right, .. } | Row::Changed { right, .. } | Row::Added { right } => {
Some(right)
}
Row::Removed { .. } => None,
}
}
pub fn is_same(&self) -> bool {
matches!(self, Row::Same { .. })
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Alignment {
pub rows: Vec<Row>,
pub hunks: Vec<Range<usize>>,
pub coarse: bool,
}
impl Alignment {
pub fn is_identical(&self) -> bool {
self.hunks.is_empty()
}
}
pub fn align(left: &[String], right: &[String]) -> Alignment {
let prefix = left
.iter()
.zip(right.iter())
.take_while(|(a, b)| a == b)
.count();
let max_suffix = (left.len() - prefix).min(right.len() - prefix);
let suffix = (0..max_suffix)
.take_while(|k| left[left.len() - 1 - k] == right[right.len() - 1 - k])
.count();
let mut rows: Vec<Row> = Vec::with_capacity(left.len().max(right.len()));
for i in 0..prefix {
rows.push(Row::Same { left: i, right: i });
}
let l_mid = &left[prefix..left.len() - suffix];
let r_mid = &right[prefix..right.len() - suffix];
let coarse = l_mid.len().saturating_mul(r_mid.len()) > MAX_CELLS;
let ops = match coarse {
true => {
let mut v: Vec<Op> = (0..l_mid.len()).map(Op::Del).collect();
v.extend((0..r_mid.len()).map(Op::Add));
v
}
false => lcs_ops(l_mid, r_mid),
};
emit(&ops, prefix, prefix, &mut rows);
for k in (0..suffix).rev() {
rows.push(Row::Same {
left: left.len() - 1 - k,
right: right.len() - 1 - k,
});
}
let hunks = group(&rows);
Alignment { rows, hunks, coarse }
}
enum Op {
Keep(usize, usize),
Del(usize),
Add(usize),
}
fn lcs_ops(a: &[String], b: &[String]) -> Vec<Op> {
let (n, m) = (a.len(), b.len());
if n == 0 || m == 0 {
let mut v: Vec<Op> = (0..n).map(Op::Del).collect();
v.extend((0..m).map(Op::Add));
return v;
}
let stride = m + 1;
let mut dp = vec![0u32; (n + 1) * stride];
for i in (0..n).rev() {
for j in (0..m).rev() {
dp[i * stride + j] = if a[i] == b[j] {
dp[(i + 1) * stride + j + 1] + 1
} else {
dp[(i + 1) * stride + j].max(dp[i * stride + j + 1])
};
}
}
let mut ops = Vec::with_capacity(n + m);
let (mut i, mut j) = (0usize, 0usize);
while i < n && j < m {
if a[i] == b[j] {
ops.push(Op::Keep(i, j));
i += 1;
j += 1;
} else if dp[(i + 1) * stride + j] >= dp[i * stride + j + 1] {
ops.push(Op::Del(i));
i += 1;
} else {
ops.push(Op::Add(j));
j += 1;
}
}
ops.extend((i..n).map(Op::Del));
ops.extend((j..m).map(Op::Add));
ops
}
fn emit(ops: &[Op], l_base: usize, r_base: usize, rows: &mut Vec<Row>) {
let mut dels: Vec<usize> = Vec::new();
let mut adds: Vec<usize> = Vec::new();
let flush = |dels: &mut Vec<usize>, adds: &mut Vec<usize>, rows: &mut Vec<Row>| {
let paired = dels.len().min(adds.len());
for k in 0..paired {
rows.push(Row::Changed {
left: l_base + dels[k],
right: r_base + adds[k],
});
}
for &d in &dels[paired..] {
rows.push(Row::Removed { left: l_base + d });
}
for &a in &adds[paired..] {
rows.push(Row::Added { right: r_base + a });
}
dels.clear();
adds.clear();
};
for op in ops {
match *op {
Op::Keep(i, j) => {
flush(&mut dels, &mut adds, rows);
rows.push(Row::Same {
left: l_base + i,
right: r_base + j,
});
}
Op::Del(i) => dels.push(i),
Op::Add(j) => adds.push(j),
}
}
flush(&mut dels, &mut adds, rows);
}
fn group(rows: &[Row]) -> Vec<Range<usize>> {
let mut out: Vec<Range<usize>> = Vec::new();
let mut start: Option<usize> = None;
for (i, r) in rows.iter().enumerate() {
match (r.is_same(), start) {
(false, None) => start = Some(i),
(true, Some(s)) => {
out.push(s..i);
start = None;
}
_ => {}
}
}
if let Some(s) = start {
out.push(s..rows.len());
}
out
}
pub fn lines_of(text: &str) -> Vec<String> {
let body = text.strip_suffix('\n').unwrap_or(text);
if body.is_empty() && text.is_empty() {
return Vec::new();
}
body.split('\n').map(|l| l.strip_suffix('\r').unwrap_or(l).to_string()).collect()
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Side {
#[default]
Live,
File,
Both,
}
impl Side {
pub fn cycle(self) -> Side {
match self {
Side::Live => Side::File,
Side::File => Side::Both,
Side::Both => Side::Live,
}
}
pub fn glyph(self) -> &'static str {
match self {
Side::Live => "\u{25c0}",
Side::File => "\u{25b6}",
Side::Both => "\u{25c6}",
}
}
pub fn label(self) -> &'static str {
match self {
Side::Live => "live",
Side::File => "file",
Side::Both => "both",
}
}
}
pub struct DiffView {
pub doc: usize,
pub name: String,
pub mine: Vec<String>,
pub theirs: Vec<String>,
pub align: Alignment,
pub scroll: usize,
pub hunk: usize,
pub sides: Vec<Side>,
pub snapshot_mtime: Option<std::time::SystemTime>,
}
impl DiffView {
pub fn new(
doc: usize,
name: String,
mine: Vec<String>,
theirs: Vec<String>,
snapshot_mtime: Option<std::time::SystemTime>,
) -> DiffView {
let align = align(&mine, &theirs);
let sides = vec![Side::default(); align.hunks.len()];
DiffView {
doc,
name,
mine,
theirs,
align,
scroll: 0,
hunk: 0,
sides,
snapshot_mtime,
}
}
pub fn side_at(&self, row: usize) -> Option<Side> {
let h = self.align.hunks.iter().position(|h| h.contains(&row))?;
self.sides.get(h).copied()
}
pub fn cycle_current(&mut self) -> Option<Side> {
let side = self.sides.get_mut(self.hunk)?;
*side = side.cycle();
Some(*side)
}
pub fn set_all(&mut self, side: Side) {
self.sides.iter_mut().for_each(|s| *s = side);
}
pub fn merged(&self) -> String {
let mut out: Vec<&str> = Vec::with_capacity(self.align.rows.len());
let mut i = 0usize;
let mut h = 0usize;
while i < self.align.rows.len() {
let row = self.align.rows[i];
if row.is_same() {
if let Some(l) = row.left() {
out.push(self.mine[l].as_str());
}
i += 1;
continue;
}
let range = self.align.hunks[h].clone();
let side = self.sides.get(h).copied().unwrap_or_default();
let rows = &self.align.rows[range.clone()];
if matches!(side, Side::Live | Side::Both) {
out.extend(rows.iter().filter_map(Row::left).map(|l| self.mine[l].as_str()));
}
if matches!(side, Side::File | Side::Both) {
out.extend(rows.iter().filter_map(Row::right).map(|r| self.theirs[r].as_str()));
}
i = range.end;
h += 1;
}
match out.is_empty() {
true => String::new(),
false => format!("{}\n", out.join("\n")),
}
}
pub fn rows(&self) -> usize {
self.align.rows.len()
}
pub fn reveal(&mut self, row: usize, height: usize) {
let pad = 2.min(height / 4);
if row < self.scroll + pad {
self.scroll = row.saturating_sub(pad);
} else if row + pad >= self.scroll + height {
self.scroll = (row + pad + 1).saturating_sub(height);
}
let max = self.rows().saturating_sub(height);
self.scroll = self.scroll.min(max);
}
pub fn step_hunk(&mut self, forward: bool) -> Option<usize> {
let n = self.align.hunks.len();
if n == 0 {
return None;
}
self.hunk = match forward {
true => (self.hunk + 1) % n,
false => (self.hunk + n - 1) % n,
};
Some(self.align.hunks[self.hunk].start)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn v(s: &[&str]) -> Vec<String> {
s.iter().map(|x| x.to_string()).collect()
}
fn check_covers(a: &Alignment, left: &[String], right: &[String]) {
let ls: Vec<usize> = a.rows.iter().filter_map(Row::left).collect();
let rs: Vec<usize> = a.rows.iter().filter_map(Row::right).collect();
assert_eq!(ls, (0..left.len()).collect::<Vec<_>>(), "left coverage");
assert_eq!(rs, (0..right.len()).collect::<Vec<_>>(), "right coverage");
}
#[test]
fn identical_texts_have_no_hunks() {
let a = v(&["one", "two", "three"]);
let al = align(&a, &a);
assert!(al.is_identical());
assert_eq!(al.rows.len(), 3);
assert!(al.rows.iter().all(Row::is_same));
check_covers(&al, &a, &a);
}
#[test]
fn an_edited_line_is_a_single_changed_row() {
let l = v(&["keep", "mine", "tail"]);
let r = v(&["keep", "theirs", "tail"]);
let al = align(&l, &r);
assert_eq!(
al.rows,
vec![
Row::Same { left: 0, right: 0 },
Row::Changed { left: 1, right: 1 },
Row::Same { left: 2, right: 2 },
]
);
assert_eq!(al.hunks, vec![1..2]);
check_covers(&al, &l, &r);
}
#[test]
fn one_sided_runs_get_filler_opposite() {
let l = v(&["a", "b"]);
let r = v(&["a", "new", "b"]);
let al = align(&l, &r);
assert_eq!(al.rows[1], Row::Added { right: 1 });
assert!(al.rows[1].left().is_none(), "filler on the left");
check_covers(&al, &l, &r);
let al = align(&r, &l);
assert_eq!(al.rows[1], Row::Removed { left: 1 });
assert!(al.rows[1].right().is_none(), "filler on the right");
check_covers(&al, &r, &l);
}
#[test]
fn uneven_runs_pair_then_spill() {
let l = v(&["top", "x", "bottom"]);
let r = v(&["top", "1", "2", "3", "bottom"]);
let al = align(&l, &r);
assert_eq!(al.rows[1], Row::Changed { left: 1, right: 1 });
assert_eq!(al.rows[2], Row::Added { right: 2 });
assert_eq!(al.rows[3], Row::Added { right: 3 });
assert_eq!(al.hunks, vec![1..4], "one hunk, not three");
check_covers(&al, &l, &r);
}
#[test]
fn separate_edits_are_separate_hunks() {
let l = v(&["a", "b", "c", "d", "e"]);
let r = v(&["a", "B", "c", "D", "e"]);
let al = align(&l, &r);
assert_eq!(al.hunks, vec![1..2, 3..4], "two hunks, not one spanning c");
assert!(al.rows[2].is_same(), "the line between them is untouched");
}
#[test]
fn an_empty_side_is_all_one_sided() {
let l = v(&["a", "b"]);
let empty: Vec<String> = Vec::new();
let al = align(&l, &empty);
assert_eq!(al.rows, vec![Row::Removed { left: 0 }, Row::Removed { left: 1 }]);
check_covers(&al, &l, &empty);
let al = align(&empty, &l);
assert_eq!(al.rows, vec![Row::Added { right: 0 }, Row::Added { right: 1 }]);
check_covers(&al, &empty, &l);
let al = align(&empty, &empty);
assert!(al.is_identical());
assert!(al.rows.is_empty());
}
#[test]
fn overlapping_prefix_and_suffix_do_not_double_count() {
let l = v(&["a", "a", "b"]);
let r = v(&["a", "b"]);
let al = align(&l, &r);
check_covers(&al, &l, &r);
assert_eq!(al.rows.len(), 3);
let l = v(&["x", "x", "x", "x"]);
let r = v(&["x", "x"]);
let al = align(&l, &r);
check_covers(&al, &l, &r);
}
#[test]
fn a_trailing_newline_is_not_a_line() {
assert_eq!(lines_of("a\nb\n"), v(&["a", "b"]));
assert_eq!(lines_of("a\nb"), v(&["a", "b"]));
assert_eq!(lines_of(""), Vec::<String>::new());
assert_eq!(lines_of("\n"), v(&[""]));
assert_eq!(lines_of("a\r\nb\r\n"), v(&["a", "b"]));
assert!(align(&lines_of("a\nb\n"), &lines_of("a\nb")).is_identical());
}
fn view(mine: &[&str], theirs: &[&str]) -> DiffView {
DiffView::new(0, "t.md".into(), v(mine), v(theirs), None)
}
#[test]
fn the_side_ladder_cycles_live_file_both() {
assert_eq!(Side::default(), Side::Live, "the default keeps your work");
assert_eq!(Side::Live.cycle(), Side::File);
assert_eq!(Side::File.cycle(), Side::Both);
assert_eq!(Side::Both.cycle(), Side::Live);
}
#[test]
fn every_hunk_starts_chosen_and_defaults_to_live() {
let v = view(&["a", "mine", "c", "x", "e"], &["a", "theirs", "c", "y", "e"]);
assert_eq!(v.sides.len(), v.align.hunks.len());
assert_eq!(v.sides, vec![Side::Live, Side::Live]);
assert_eq!(v.side_at(1), Some(Side::Live), "a differing row has a side");
assert_eq!(v.side_at(0), None, "an identical row has none");
assert_eq!(v.merged(), "a\nmine\nc\nx\ne\n", "so w with no toggles keeps live");
}
#[test]
fn merged_follows_the_chosen_side() {
let mut v = view(&["keep", "mine", "tail"], &["keep", "theirs", "extra", "tail"]);
assert_eq!(v.merged(), "keep\nmine\ntail\n");
v.set_all(Side::File);
assert_eq!(v.merged(), "keep\ntheirs\nextra\ntail\n");
v.set_all(Side::Both);
assert_eq!(v.merged(), "keep\nmine\ntheirs\nextra\ntail\n");
}
#[test]
fn each_hunk_merges_on_its_own_choice() {
let mut v = view(&["a", "mine", "c", "x", "e"], &["a", "theirs", "c", "y", "e"]);
assert_eq!(v.align.hunks.len(), 2);
v.sides[0] = Side::File;
v.sides[1] = Side::Both;
assert_eq!(v.merged(), "a\ntheirs\nc\nx\ny\ne\n");
v.set_all(Side::Live);
v.hunk = 1;
assert_eq!(v.cycle_current(), Some(Side::File));
assert_eq!(v.sides, vec![Side::Live, Side::File], "hunk 0 untouched");
}
#[test]
fn both_on_a_one_sided_hunk_is_not_a_duplicate() {
let mut v = view(&["a", "b"], &["a", "new", "b"]);
v.set_all(Side::Both);
assert_eq!(v.merged(), "a\nnew\nb\n");
v.set_all(Side::File);
assert_eq!(v.merged(), "a\nnew\nb\n", "same answer, nothing doubled");
v.set_all(Side::Live);
assert_eq!(v.merged(), "a\nb\n", "and live drops the insertion");
}
#[test]
fn merging_an_identical_pair_changes_nothing() {
let mut v = view(&["one", "two"], &["one", "two"]);
assert!(v.sides.is_empty());
for s in [Side::Live, Side::File, Side::Both] {
v.set_all(s);
assert_eq!(v.merged(), "one\ntwo\n");
}
assert_eq!(v.cycle_current(), None, "nothing to cycle");
}
#[test]
fn an_oversized_middle_falls_back_but_still_covers() {
let n = 1200; let l: Vec<String> = (0..n).map(|i| format!("left {i}")).collect();
let r: Vec<String> = (0..n).map(|i| format!("right {i}")).collect();
let al = align(&l, &r);
assert!(al.coarse, "it should report the fallback");
check_covers(&al, &l, &r);
assert_eq!(al.hunks.len(), 1, "the whole middle is one block");
let mut r2 = l.clone();
r2[600] = "changed".into();
let al = align(&l, &r2);
assert!(!al.coarse, "trimming brought it back inside the cap");
assert_eq!(al.hunks, vec![600..601]);
}
}