//! Turn a working-copy file into edits against the live atoms of a node. //! Anchors follow the convention in spec ยง7.4: an insertion at old position //! `i` is anchored after `old[i-1]` and before `old[i]`, both of which are //! adjacent in the author's view. use crate::graph::AtomKey; use crate::ids::NodeId; use crate::objects::{AtomRange, AtomRef, Edit}; use similar::{capture_diff_slices, Algorithm, DiffOp}; /// Split into lines, each keeping its terminator; a final unterminated line /// is its own atom. pub fn split_lines(b: &[u8]) -> Vec<&[u8]> { let mut out = Vec::new(); let mut start = 0; for (i, &c) in b.iter().enumerate() { if c == b'\n' { out.push(&b[start..=i]); start = i + 1; } } if start < b.len() { out.push(&b[start..]); } out } /// Coalesce atom keys into half-open ranges of consecutive indices per change. pub fn ranges(keys: &[AtomKey]) -> Vec { let mut out: Vec = Vec::new(); for &(c, i) in keys { match out.last_mut() { Some(r) if r.change == c && r.to == i => r.to = i + 1, _ => out.push(AtomRange { change: c, from: i, to: i + 1, }), } } out } /// Edits that transform the live lines `old` of `node` into `new`. pub fn text_edits(node: NodeId, old: &[(AtomKey, Vec)], new: &[u8]) -> Vec { let old_lines: Vec<&[u8]> = old.iter().map(|(_, l)| l.as_slice()).collect(); let new_lines = split_lines(new); let ops = capture_diff_slices(Algorithm::Myers, &old_lines, &new_lines); let anchor = |i: usize| -> (AtomRef, AtomRef) { let after = if i == 0 { AtomRef::Start } else { AtomRef::Atom { change: old[i - 1].0 .0, index: old[i - 1].0 .1, } }; let before = if i >= old.len() { AtomRef::End } else { AtomRef::Atom { change: old[i].0 .0, index: old[i].0 .1, } }; (after, before) }; let mut deleted: Vec = Vec::new(); let mut inserts: Vec = Vec::new(); for op in ops { match op { DiffOp::Equal { .. } => {} DiffOp::Delete { old_index, old_len, .. } => { deleted.extend(old[old_index..old_index + old_len].iter().map(|(k, _)| *k)); } DiffOp::Insert { old_index, new_index, new_len, } => { let (after, before) = anchor(old_index); inserts.push(Edit::Insert { node, after, before, lines: new_lines[new_index..new_index + new_len] .iter() .map(|l| l.to_vec()) .collect(), }); } DiffOp::Replace { old_index, old_len, new_index, new_len, } => { deleted.extend(old[old_index..old_index + old_len].iter().map(|(k, _)| *k)); let (after, before) = anchor(old_index); inserts.push(Edit::Insert { node, after, before, lines: new_lines[new_index..new_index + new_len] .iter() .map(|l| l.to_vec()) .collect(), }); } } } let mut edits = Vec::new(); if !deleted.is_empty() { edits.push(Edit::Delete { node, atoms: ranges(&deleted), }); } edits.extend(inserts); edits } #[cfg(test)] mod tests { use super::*; use crate::hash::ObjectRef; fn h(n: u8) -> ObjectRef { ObjectRef([n; 32]) } #[test] fn split_keeps_terminators() { assert_eq!(split_lines(b"a\nb\n"), vec![&b"a\n"[..], &b"b\n"[..]]); assert_eq!(split_lines(b"a\nb"), vec![&b"a\n"[..], &b"b"[..]]); assert_eq!(split_lines(b""), Vec::<&[u8]>::new()); assert_eq!(split_lines(b"\n\n"), vec![&b"\n"[..], &b"\n"[..]]); } #[test] fn ranges_coalesce() { let r = ranges(&[(h(1), 0), (h(1), 1), (h(1), 3), (h(2), 0)]); assert_eq!( r, vec![ AtomRange { change: h(1), from: 0, to: 2 }, AtomRange { change: h(1), from: 3, to: 4 }, AtomRange { change: h(2), from: 0, to: 1 } ] ); } #[test] fn edits_for_typical_changes() { let node = NodeId::random(); let old: Vec<(AtomKey, Vec)> = ["a\n", "b\n", "c\n"] .iter() .enumerate() .map(|(i, l)| ((h(1), i as u32), l.as_bytes().to_vec())) .collect(); // No change. assert!(text_edits(node, &old, b"a\nb\nc\n").is_empty()); // Replace middle line. let e = text_edits(node, &old, b"a\nB\nc\n"); assert_eq!( e, vec![ Edit::Delete { node, atoms: vec![AtomRange { change: h(1), from: 1, to: 2 }] }, Edit::Insert { node, after: AtomRef::Atom { change: h(1), index: 0 }, before: AtomRef::Atom { change: h(1), index: 1 }, lines: vec![b"B\n".to_vec()] }, ] ); // Prepend and append. let e = text_edits(node, &old, b"top\na\nb\nc\nend\n"); assert_eq!(e.len(), 2); assert!(matches!( &e[0], Edit::Insert { after: AtomRef::Start, before: AtomRef::Atom { index: 0, .. }, .. } )); assert!(matches!( &e[1], Edit::Insert { after: AtomRef::Atom { index: 2, .. }, before: AtomRef::End, .. } )); // Delete everything. let e = text_edits(node, &old, b""); assert_eq!( e, vec![Edit::Delete { node, atoms: vec![AtomRange { change: h(1), from: 0, to: 3 }] }] ); // From empty. let e = text_edits(node, &[], b"x\n"); assert_eq!( e, vec![Edit::Insert { node, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"x\n".to_vec()] }] ); } }