use hereticus::matching::DiffLine;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Side<'l> {
Empty,
Same(&'l str),
Removed(&'l str),
Added(&'l str),
Changed(&'l str),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Half<'l> {
pub side: Side<'l>,
pub address: Option<u64>,
}
impl Half<'_> {
const EMPTY: Self = Self { side: Side::Empty, address: None };
pub fn is_change(self) -> bool {
!matches!(self.side, Side::Same(_))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SideRow<'l> {
pub left: Half<'l>,
pub right: Half<'l>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Addresses {
pub before: Vec<u64>,
pub after: Vec<u64>,
}
fn locate<'l>(
lines: &'l [DiffLine],
addresses: &Addresses,
) -> impl Iterator<Item = (&'l DiffLine, Option<u64>, Option<u64>)> {
let (mut before, mut after) =
(addresses.before.iter().copied(), addresses.after.iter().copied());
lines.iter().map(move |line| match line {
DiffLine::Same(_) | DiffLine::Changed { .. } => (line, before.next(), after.next()),
DiffLine::Removed(_) => (line, before.next(), None),
DiffLine::Added(_) => (line, None, after.next()),
})
}
pub fn side_by_side<'l>(lines: &'l [DiffLine], addresses: &Addresses) -> Vec<SideRow<'l>> {
let mut rows = Vec::with_capacity(lines.len());
let mut removed: Vec<Half<'l>> = Vec::new();
let mut added: Vec<Half<'l>> = Vec::new();
for (line, before, after) in locate(lines, addresses) {
match line {
DiffLine::Removed(text) => {
removed.push(Half { side: Side::Removed(text), address: before })
}
DiffLine::Added(text) => added.push(Half { side: Side::Added(text), address: after }),
DiffLine::Same(text) => {
flush_runs(&mut rows, &mut removed, &mut added);
rows.push(SideRow {
left: Half { side: Side::Same(text), address: before },
right: Half { side: Side::Same(text), address: after },
});
}
DiffLine::Changed { before: old, after: new } => {
flush_runs(&mut rows, &mut removed, &mut added);
rows.push(SideRow {
left: Half { side: Side::Changed(old), address: before },
right: Half { side: Side::Changed(new), address: after },
});
}
}
}
flush_runs(&mut rows, &mut removed, &mut added);
rows
}
fn flush_runs<'l>(
rows: &mut Vec<SideRow<'l>>,
removed: &mut Vec<Half<'l>>,
added: &mut Vec<Half<'l>>,
) {
let longest = removed.len().max(added.len());
let left = removed.drain(..).chain(core::iter::repeat(Half::EMPTY));
let right = added.drain(..).chain(core::iter::repeat(Half::EMPTY));
rows.extend(left.zip(right).take(longest).map(|(left, right)| SideRow { left, right }));
}
pub fn unified<'l>(lines: &'l [DiffLine], addresses: &Addresses) -> Vec<Half<'l>> {
locate(lines, addresses)
.flat_map(|(line, before, after)| match line {
DiffLine::Same(text) => [Half { side: Side::Same(text), address: after }, Half::EMPTY],
DiffLine::Removed(text) => {
[Half { side: Side::Removed(text), address: before }, Half::EMPTY]
}
DiffLine::Added(text) => {
[Half { side: Side::Added(text), address: after }, Half::EMPTY]
}
DiffLine::Changed { before: old, after: new } => [
Half { side: Side::Removed(old), address: before },
Half { side: Side::Added(new), address: after },
],
})
.filter(|half| half.side != Side::Empty)
.collect()
}
pub fn change_starts(changes: impl IntoIterator<Item = bool>) -> Vec<usize> {
let mut before = false;
changes
.into_iter()
.enumerate()
.filter_map(|(index, change)| {
let starts = change && !before;
before = change;
starts.then_some(index)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn same(text: &str) -> DiffLine {
DiffLine::Same(text.into())
}
fn half(side: Side<'_>, address: Option<u64>) -> Half<'_> {
Half { side, address }
}
#[test]
fn a_removed_run_and_an_added_run_share_rows() {
let lines = [
same("a"),
DiffLine::Removed("b".into()),
DiffLine::Removed("c".into()),
DiffLine::Added("d".into()),
same("e"),
DiffLine::Changed { before: "f".into(), after: "g".into() },
DiffLine::Added("h".into()),
];
let addresses = Addresses { before: vec![0, 4, 8, 12, 16], after: vec![0, 4, 8, 12, 16] };
let row = |left, right| SideRow { left, right };
assert_eq!(
side_by_side(&lines, &addresses),
[
row(half(Side::Same("a"), Some(0)), half(Side::Same("a"), Some(0))),
row(half(Side::Removed("b"), Some(4)), half(Side::Added("d"), Some(4))),
row(half(Side::Removed("c"), Some(8)), Half::EMPTY),
row(half(Side::Same("e"), Some(12)), half(Side::Same("e"), Some(8))),
row(half(Side::Changed("f"), Some(16)), half(Side::Changed("g"), Some(12))),
row(Half::EMPTY, half(Side::Added("h"), Some(16))),
]
);
assert!(side_by_side(&[], &addresses).is_empty());
}
#[test]
fn a_line_past_the_addresses_has_none() {
let lines = [same("a"), same("b")];
let addresses = Addresses { before: vec![0x10], after: vec![] };
let rows = side_by_side(&lines, &addresses);
assert_eq!(rows[0].left.address, Some(0x10));
assert_eq!(rows[1].left.address, None);
assert_eq!(rows[0].right.address, None);
}
#[test]
fn a_unified_diff_puts_a_changed_line_on_two_rows() {
let lines = [same("a"), DiffLine::Changed { before: "b".into(), after: "c".into() }];
let addresses = Addresses { before: vec![0x100, 0x104], after: vec![0x200, 0x204] };
assert_eq!(
unified(&lines, &addresses),
[
half(Side::Same("a"), Some(0x200)),
half(Side::Removed("b"), Some(0x104)),
half(Side::Added("c"), Some(0x204)),
]
);
}
#[test]
fn a_change_starts_where_a_run_of_changes_does() {
assert_eq!(change_starts([true, true, false, true, false, false, true]), [0, 3, 6]);
assert_eq!(change_starts([false, false]), Vec::<usize>::new());
assert_eq!(change_starts([]), Vec::<usize>::new());
}
}