OXYGENHUB

Coverage / crates/oxy-core/src/diff.rs 98.8%

250 of 253 lines run, 39 of 41 functions · run #427 on main · run missed

1
//! Turn a working-copy file into edits against the live atoms of a node.
2
//! Anchors follow the convention in spec §7.4: an insertion at old position
3
//! `i` is anchored after `old[i-1]` and before `old[i]`, both of which are
4
//! adjacent in the author's view.
5
6
use crate::graph::AtomKey;
7
use crate::ids::NodeId;
8
use crate::objects::{AtomRange, AtomRef, Edit};
9
use similar::DiffOp;
10
use std::collections::HashMap;
11
12
/// Split into lines, each keeping its terminator; a final unterminated line
13
/// is its own atom.
144087×
pub fn split_lines(b: &[u8]) -> Vec<&[u8]> {
154087×
    let mut out = Vec::new();
164087×
    let mut start = 0;
17633073×
    for (i, &c) in b.iter().enumerate() {
18633073×
        if c == b'\n' {
1974499×
            out.push(&b[start..=i]);
2074499×
            start = i + 1;
21558574×
        }
22
    }
234087×
    if start < b.len() {
2418×
        out.push(&b[start..]);
254069×
    }
264087×
    out
274087×
}
28
29
/// Coalesce atom keys into half-open ranges of consecutive indices per change.
302245×
pub fn ranges(keys: &[AtomKey]) -> Vec<AtomRange> {
312245×
    let mut out: Vec<AtomRange> = Vec::new();
3217941×
    for &(c, i) in keys {
3317941×
        match out.last_mut() {
3415696×
            Some(r) if r.change == c && r.to == i => r.to = i + 1,
357533×
            _ => out.push(AtomRange {
367533×
                change: c,
377533×
                from: i,
387533×
                to: i + 1,
397533×
            }),
40
        }
41
    }
422245×
    out
432245×
}
44
45
/// The lines `old` and `new` keep in common, as (old index, new index) in
46
/// order: a longest common subsequence, found by Myers' algorithm. A line
47
/// found on one side only cannot be in it, so such lines are set aside
48
/// before the diff, as Git's xdiff does: a file rewritten whole (every
49
/// line ending changed, say) then costs time linear in its length rather
50
/// than quadratic. Lines are compared as small numbers, not bytes.
512946×
fn kept_lines<'a>(old: &[&'a [u8]], new: &[&'a [u8]]) -> Vec<(usize, usize)> {
522946×
    let mut ids: HashMap<&'a [u8], u32> = HashMap::new();
53102852×
    let mut intern = |l: &'a [u8]| -> u32 {
54102852×
        let next = ids.len() as u32;
55102852×
        *ids.entry(l).or_insert(next)
56102852×
    };
5750971×
    let o: Vec<u32> = old.iter().map(|l| intern(l)).collect();
5851881×
    let n: Vec<u32> = new.iter().map(|l| intern(l)).collect();
592946×
    let mut in_old = vec![false; ids.len()];
602946×
    let mut in_new = vec![false; ids.len()];
6150971×
    for &x in &o {
6250971×
        in_old[x as usize] = true;
6350971×
    }
6451881×
    for &x in &n {
6551881×
        in_new[x as usize] = true;
6651881×
    }
6750971×
    let oi: Vec<usize> = (0..o.len()).filter(|&i| in_new[o[i] as usize]).collect();
6851881×
    let ni: Vec<usize> = (0..n.len()).filter(|&j| in_old[n[j] as usize]).collect();
6945376×
    let os: Vec<u32> = oi.iter().map(|&i| o[i]).collect();
7045353×
    let ns: Vec<u32> = ni.iter().map(|&j| n[j]).collect();
712946×
    let mut capture = similar::algorithms::Capture::new();
722946×
    let _ = similar::algorithms::myers::diff(&mut capture, &os, 0..os.len(), &ns, 0..ns.len());
732946×
    let mut kept = Vec::new();
7425448×
    for op in capture.into_ops() {
75
        if let DiffOp::Equal {
769765×
            old_index,
779765×
            new_index,
789765×
            len,
7925448×
        } = op
80
        {
8133128×
            kept.extend((0..len).map(|k| (oi[old_index + k], ni[new_index + k])));
8215683×
        }
83
    }
842946×
    kept
852946×
}
86
87
/// Edits that transform the live lines `old` of `node` into `new`.
882050×
pub fn text_edits(node: NodeId, old: &[(AtomKey, Vec<u8>)], new: &[u8]) -> Vec<Edit> {
892050×
    let keys: Vec<AtomKey> = old.iter().map(|(k, _)| *k).collect();
9029199×
    let old_lines: Vec<&[u8]> = old.iter().map(|(_, l)| l.as_slice()).collect();
912050×
    text_edits_of(node, &keys, &old_lines, new)
922050×
}
93
94
/// `text_edits` for live lines given as their atoms and, apart, their
95
/// text (`old[i]` is the text of `keys[i]`).
962946×
pub fn text_edits_of(node: NodeId, keys: &[AtomKey], old_lines: &[&[u8]], new: &[u8]) -> Vec<Edit> {
972946×
    let new_lines = split_lines(new);
98
997890×
    let anchor = |i: usize| -> (AtomRef, AtomRef) {
1007890×
        let after = if i == 0 {
1011277×
            AtomRef::Start
102
        } else {
1036613×
            AtomRef::Atom {
1046613×
                change: keys[i - 1].0,
1056613×
                index: keys[i - 1].1,
1066613×
            }
107
        };
1087890×
        let before = if i >= keys.len() {
1091053×
            AtomRef::End
110
        } else {
1116837×
            AtomRef::Atom {
1126837×
                change: keys[i].0,
1136837×
                index: keys[i].1,
1146837×
            }
115
        };
1167890×
        (after, before)
1177890×
    };
118
119
    // Each run of lines between two kept ones is deleted, and the new
120
    // lines there are inserted where the first deleted line was.
1212946×
    let mut deleted: Vec<AtomKey> = Vec::new();
1222946×
    let mut inserts: Vec<Edit> = Vec::new();
1232946×
    let (mut i, mut j) = (0, 0);
1242946×
    let end = (old_lines.len(), new_lines.len());
12536074×
    for (ki, kj) in kept_lines(old_lines, &new_lines).into_iter().chain([end]) {
12636074×
        deleted.extend_from_slice(&keys[i..ki]);
12736074×
        if kj > j {
1287890×
            let (after, before) = anchor(i);
1297890×
            inserts.push(Edit::Insert {
1307890×
                node,
1317890×
                after,
1327890×
                before,
1337890×
                lines: new_lines[j..kj].iter().copied().collect(),
1347890×
            });
13528184×
        }
13636074×
        (i, j) = (ki + 1, kj + 1);
137
    }
1382946×
    let mut edits = Vec::new();
1392946×
    if !deleted.is_empty() {
1402197×
        edits.push(Edit::Delete {
1412197×
            node,
1422197×
            atoms: ranges(&deleted),
1432197×
        });
1442197×
    }
1452946×
    edits.extend(inserts);
1462946×
    edits
1472946×
}
148
149
#[cfg(test)]
150
mod tests {
151
    use super::*;
152
    use crate::hash::ObjectRef;
153
15429106×
    fn h(n: u8) -> ObjectRef {
15529106×
        ObjectRef([n; 32])
15629106×
    }
157
158
    /// Replaying `text_edits` by position must give the new text, on files
159
    /// full of repeated lines, where a diff has many equally good
160
    /// alignments to choose between.
161
    #[test]
1621×
    fn edits_replay_to_the_new_text_on_repetitive_files() {
163
        use rand::{Rng, SeedableRng};
1641×
        let words: [&[u8]; 5] = [b"}\n", b"    }\n", b"\n", b"a\n", b"    b\n"];
1651×
        let mut rng = rand::rngs::StdRng::seed_from_u64(11);
1661×
        for _ in 0..2000 {
1672000×
            let old: Vec<Vec<u8>> = (0..rng.gen_range(0..30))
16829092×
                .map(|_| words[rng.gen_range(0..5)].to_vec())
1692000×
                .collect();
1702000×
            let new: Vec<u8> = (0..rng.gen_range(0..30))
17129543×
                .flat_map(|_| words[rng.gen_range(0..5)].to_vec())
1722000×
                .collect();
1732000×
            let keyed: Vec<(AtomKey, Vec<u8>)> = old
1742000×
                .iter()
1752000×
                .enumerate()
17629092×
                .map(|(i, l)| ((h(1), i as u32), l.clone()))
1772000×
                .collect();
1782000×
            let edits = text_edits(NodeId::ROOT, &keyed, &new);
1792000×
            let mut dead = vec![false; old.len()];
1802000×
            let mut after: std::collections::BTreeMap<i64, Vec<u8>> = Default::default();
1818219×
            for e in &edits {
1828219×
                match e {
1831725×
                    Edit::Delete { atoms, .. } => atoms
1841725×
                        .iter()
18516820×
                        .for_each(|r| (r.from..r.to).for_each(|i| dead[i as usize] = true)),
186
                    Edit::Insert {
1876494×
                        after: a,
1886494×
                        before: b,
1896494×
                        lines,
190
                        ..
191
                    } => {
1926494×
                        let at = match a {
1931053×
                            AtomRef::Start => -1,
1945441×
                            AtomRef::Atom { index, .. } => *index as i64,
1950×
                            AtomRef::End => unreachable!(),
196
                        };
1976494×
                        let next = match b {
198640×
                            AtomRef::End => old.len() as i64,
1995854×
                            AtomRef::Atom { index, .. } => *index as i64,
2000×
                            AtomRef::Start => unreachable!(),
201
                        };
2026494×
                        assert_eq!(next, at + 1, "anchors adjacent");
2036494×
                        after.entry(at).or_default().extend(lines.iter().flatten());
204
                    }
2050×
                    _ => {}
206
                }
207
            }
2082000×
            let mut out = after.get(&-1).cloned().unwrap_or_default();
20929092×
            for (i, l) in old.iter().enumerate() {
21029092×
                if !dead[i] {
21112272×
                    out.extend(l);
21216820×
                }
21329092×
                out.extend(after.get(&(i as i64)).cloned().unwrap_or_default());
214
            }
2152000×
            assert_eq!(
2162000×
                String::from_utf8_lossy(&out),
2172000×
                String::from_utf8_lossy(&new),
218
                "old {old:?}"
219
            );
220
        }
2211×
    }
222
223
    #[test]
2241×
    fn split_keeps_terminators() {
2251×
        assert_eq!(split_lines(b"a\nb\n"), vec![&b"a\n"[..], &b"b\n"[..]]);
2261×
        assert_eq!(split_lines(b"a\nb"), vec![&b"a\n"[..], &b"b"[..]]);
2271×
        assert_eq!(split_lines(b""), Vec::<&[u8]>::new());
2281×
        assert_eq!(split_lines(b"\n\n"), vec![&b"\n"[..], &b"\n"[..]]);
2291×
    }
230
231
    #[test]
2321×
    fn ranges_coalesce() {
2331×
        let r = ranges(&[(h(1), 0), (h(1), 1), (h(1), 3), (h(2), 0)]);
2341×
        assert_eq!(
235
            r,
2361×
            vec![
2371×
                AtomRange {
2381×
                    change: h(1),
2391×
                    from: 0,
2401×
                    to: 2
2411×
                },
2421×
                AtomRange {
2431×
                    change: h(1),
2441×
                    from: 3,
2451×
                    to: 4
2461×
                },
2471×
                AtomRange {
2481×
                    change: h(2),
2491×
                    from: 0,
2501×
                    to: 1
2511×
                }
252
            ]
253
        );
2541×
    }
255
256
    #[test]
2571×
    fn edits_for_typical_changes() {
2581×
        let node = NodeId::random();
2591×
        let old: Vec<(AtomKey, Vec<u8>)> = ["a\n", "b\n", "c\n"]
2601×
            .iter()
2611×
            .enumerate()
2623×
            .map(|(i, l)| ((h(1), i as u32), l.as_bytes().to_vec()))
2631×
            .collect();
264
        // No change.
2651×
        assert!(text_edits(node, &old, b"a\nb\nc\n").is_empty());
266
        // Replace middle line.
2671×
        let e = text_edits(node, &old, b"a\nB\nc\n");
2681×
        assert_eq!(
269
            e,
2701×
            vec![
2711×
                Edit::Delete {
2721×
                    node,
2731×
                    atoms: vec![AtomRange {
2741×
                        change: h(1),
2751×
                        from: 1,
2761×
                        to: 2
2771×
                    }]
2781×
                },
2791×
                Edit::Insert {
2801×
                    node,
2811×
                    after: AtomRef::Atom {
2821×
                        change: h(1),
2831×
                        index: 0
2841×
                    },
2851×
                    before: AtomRef::Atom {
2861×
                        change: h(1),
2871×
                        index: 1
2881×
                    },
2891×
                    lines: vec![b"B\n".to_vec()].into()
2901×
                },
291
            ]
292
        );
293
        // Prepend and append.
2941×
        let e = text_edits(node, &old, b"top\na\nb\nc\nend\n");
2951×
        assert_eq!(e.len(), 2);
2961×
        assert!(matches!(
2971×
            &e[0],
298
            Edit::Insert {
299
                after: AtomRef::Start,
300
                before: AtomRef::Atom { index: 0, .. },
301
                ..
302
            }
303
        ));
3041×
        assert!(matches!(
3051×
            &e[1],
306
            Edit::Insert {
307
                after: AtomRef::Atom { index: 2, .. },
308
                before: AtomRef::End,
309
                ..
310
            }
311
        ));
312
        // Delete everything.
3131×
        let e = text_edits(node, &old, b"");
3141×
        assert_eq!(
315
            e,
3161×
            vec![Edit::Delete {
3171×
                node,
3181×
                atoms: vec![AtomRange {
3191×
                    change: h(1),
3201×
                    from: 0,
3211×
                    to: 3
3221×
                }]
3231×
            }]
324
        );
325
        // From empty.
3261×
        let e = text_edits(node, &[], b"x\n");
3271×
        assert_eq!(
328
            e,
3291×
            vec![Edit::Insert {
3301×
                node,
3311×
                after: AtomRef::Start,
3321×
                before: AtomRef::End,
3331×
                lines: vec![b"x\n".to_vec()].into()
3341×
            }]
335
        );
3361×
    }
337
}