OXYGENHUB
# b4fa2bc1ca6a oxygen / crates / oxy-core / src / diff.rs
8.9 KiB · Rust · last changed Diffs no longer anchor a block of new lines one line early 1 month ago source blame history raw
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
11
/// Split into lines, each keeping its terminator; a final unterminated line
12
/// is its own atom.
13
pub fn split_lines(b: &[u8]) -> Vec<&[u8]> {
14
    let mut out = Vec::new();
15
    let mut start = 0;
16
    for (i, &c) in b.iter().enumerate() {
17
        if c == b'\n' {
18
            out.push(&b[start..=i]);
19
            start = i + 1;
20
        }
21
    }
22
    if start < b.len() {
23
        out.push(&b[start..]);
24
    }
25
    out
26
}
27
28
/// Coalesce atom keys into half-open ranges of consecutive indices per change.
29
pub fn ranges(keys: &[AtomKey]) -> Vec<AtomRange> {
30
    let mut out: Vec<AtomRange> = Vec::new();
31
    for &(c, i) in keys {
32
        match out.last_mut() {
33
            Some(r) if r.change == c && r.to == i => r.to = i + 1,
34
            _ => out.push(AtomRange { change: c, from: i, to: i + 1 }),
35
        }
36
    }
37
    out
38
}
39
40
/// Edits that transform the live lines `old` of `node` into `new`.
41
pub fn text_edits(node: NodeId, old: &[(AtomKey, Vec<u8>)], new: &[u8]) -> Vec<Edit> {
42
    let old_lines: Vec<&[u8]> = old.iter().map(|(_, l)| l.as_slice()).collect();
43
    let new_lines = split_lines(new);
44
    // Myers ops in file order. `capture_diff_slices` also runs the `Compact`
45
    // hook, which slides deletions along runs of equal lines and can leave
46
    // an insertion with the old index of a line before an equal one: the
47
    // insertion was then anchored a line too early and landed on the wrong
48
    // side of it (found by verifying a real Git import commit by commit).
49
    let mut hook = similar::algorithms::Replace::new(similar::algorithms::Capture::new());
50
    let _ = similar::algorithms::diff_deadline(
51
        similar::Algorithm::Myers,
52
        &mut hook,
53
        &old_lines,
54
        0..old_lines.len(),
55
        &new_lines,
56
        0..new_lines.len(),
57
        None,
58
    );
59
    let ops = hook.into_inner().into_ops();
60
61
    let anchor = |i: usize| -> (AtomRef, AtomRef) {
62
        let after = if i == 0 { AtomRef::Start } else { AtomRef::Atom { change: old[i - 1].0 .0, index: old[i - 1].0 .1 } };
63
        let before = if i >= old.len() { AtomRef::End } else { AtomRef::Atom { change: old[i].0 .0, index: old[i].0 .1 } };
64
        (after, before)
65
    };
66
67
    let mut deleted: Vec<AtomKey> = Vec::new();
68
    let mut inserts: Vec<Edit> = Vec::new();
69
    for op in ops {
70
        match op {
71
            DiffOp::Equal { .. } => {}
72
            DiffOp::Delete { old_index, old_len, .. } => {
73
                deleted.extend(old[old_index..old_index + old_len].iter().map(|(k, _)| *k));
74
            }
75
            DiffOp::Insert { old_index, new_index, new_len } => {
76
                let (after, before) = anchor(old_index);
77
                inserts.push(Edit::Insert {
78
                    node,
79
                    after,
80
                    before,
81
                    lines: new_lines[new_index..new_index + new_len].iter().map(|l| l.to_vec()).collect(),
82
                });
83
            }
84
            DiffOp::Replace { old_index, old_len, new_index, new_len } => {
85
                deleted.extend(old[old_index..old_index + old_len].iter().map(|(k, _)| *k));
86
                let (after, before) = anchor(old_index);
87
                inserts.push(Edit::Insert {
88
                    node,
89
                    after,
90
                    before,
91
                    lines: new_lines[new_index..new_index + new_len].iter().map(|l| l.to_vec()).collect(),
92
                });
93
            }
94
        }
95
    }
96
    let mut edits = Vec::new();
97
    if !deleted.is_empty() {
98
        edits.push(Edit::Delete { node, atoms: ranges(&deleted) });
99
    }
100
    edits.extend(inserts);
101
    edits
102
}
103
104
#[cfg(test)]
105
mod tests {
106
    use super::*;
107
    use crate::hash::ObjectRef;
108
109
    fn h(n: u8) -> ObjectRef {
110
        ObjectRef([n; 32])
111
    }
112
113
    /// Replaying `text_edits` by position must give the new text, on files
114
    /// full of repeated lines, where a diff has many equally good
115
    /// alignments to choose between.
116
    #[test]
117
    fn edits_replay_to_the_new_text_on_repetitive_files() {
118
        use rand::{Rng, SeedableRng};
119
        let words: [&[u8]; 5] = [b"}\n", b"    }\n", b"\n", b"a\n", b"    b\n"];
120
        let mut rng = rand::rngs::StdRng::seed_from_u64(11);
121
        for _ in 0..2000 {
122
            let old: Vec<Vec<u8>> = (0..rng.gen_range(0..30))
123
                .map(|_| words[rng.gen_range(0..5)].to_vec())
124
                .collect();
125
            let new: Vec<u8> = (0..rng.gen_range(0..30))
126
                .flat_map(|_| words[rng.gen_range(0..5)].to_vec())
127
                .collect();
128
            let keyed: Vec<(AtomKey, Vec<u8>)> = old
129
                .iter()
130
                .enumerate()
131
                .map(|(i, l)| ((h(1), i as u32), l.clone()))
132
                .collect();
133
            let edits = text_edits(NodeId::ROOT, &keyed, &new);
134
            let mut dead = vec![false; old.len()];
135
            let mut after: std::collections::BTreeMap<i64, Vec<u8>> = Default::default();
136
            for e in &edits {
137
                match e {
138
                    Edit::Delete { atoms, .. } => atoms
139
                        .iter()
140
                        .for_each(|r| (r.from..r.to).for_each(|i| dead[i as usize] = true)),
141
                    Edit::Insert {
142
                        after: a,
143
                        before: b,
144
                        lines,
145
                        ..
146
                    } => {
147
                        let at = match a {
148
                            AtomRef::Start => -1,
149
                            AtomRef::Atom { index, .. } => *index as i64,
150
                            AtomRef::End => unreachable!(),
151
                        };
152
                        let next = match b {
153
                            AtomRef::End => old.len() as i64,
154
                            AtomRef::Atom { index, .. } => *index as i64,
155
                            AtomRef::Start => unreachable!(),
156
                        };
157
                        assert_eq!(next, at + 1, "anchors adjacent");
158
                        after.entry(at).or_default().extend(lines.iter().flatten());
159
                    }
160
                    _ => {}
161
                }
162
            }
163
            let mut out = after.get(&-1).cloned().unwrap_or_default();
164
            for (i, l) in old.iter().enumerate() {
165
                if !dead[i] {
166
                    out.extend(l);
167
                }
168
                out.extend(after.get(&(i as i64)).cloned().unwrap_or_default());
169
            }
170
            assert_eq!(
171
                String::from_utf8_lossy(&out),
172
                String::from_utf8_lossy(&new),
173
                "old {old:?}"
174
            );
175
        }
176
    }
177
178
    #[test]
179
    fn split_keeps_terminators() {
180
        assert_eq!(split_lines(b"a\nb\n"), vec![&b"a\n"[..], &b"b\n"[..]]);
181
        assert_eq!(split_lines(b"a\nb"), vec![&b"a\n"[..], &b"b"[..]]);
182
        assert_eq!(split_lines(b""), Vec::<&[u8]>::new());
183
        assert_eq!(split_lines(b"\n\n"), vec![&b"\n"[..], &b"\n"[..]]);
184
    }
185
186
    #[test]
187
    fn ranges_coalesce() {
188
        let r = ranges(&[(h(1), 0), (h(1), 1), (h(1), 3), (h(2), 0)]);
189
        assert_eq!(
190
            r,
191
            vec![
192
                AtomRange { change: h(1), from: 0, to: 2 },
193
                AtomRange { change: h(1), from: 3, to: 4 },
194
                AtomRange { change: h(2), from: 0, to: 1 }
195
            ]
196
        );
197
    }
198
199
    #[test]
200
    fn edits_for_typical_changes() {
201
        let node = NodeId::random();
202
        let old: Vec<(AtomKey, Vec<u8>)> = ["a\n", "b\n", "c\n"].iter().enumerate().map(|(i, l)| ((h(1), i as u32), l.as_bytes().to_vec())).collect();
203
        // No change.
204
        assert!(text_edits(node, &old, b"a\nb\nc\n").is_empty());
205
        // Replace middle line.
206
        let e = text_edits(node, &old, b"a\nB\nc\n");
207
        assert_eq!(
208
            e,
209
            vec![
210
                Edit::Delete { node, atoms: vec![AtomRange { change: h(1), from: 1, to: 2 }] },
211
                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()] },
212
            ]
213
        );
214
        // Prepend and append.
215
        let e = text_edits(node, &old, b"top\na\nb\nc\nend\n");
216
        assert_eq!(e.len(), 2);
217
        assert!(matches!(&e[0], Edit::Insert { after: AtomRef::Start, before: AtomRef::Atom { index: 0, .. }, .. }));
218
        assert!(matches!(&e[1], Edit::Insert { after: AtomRef::Atom { index: 2, .. }, before: AtomRef::End, .. }));
219
        // Delete everything.
220
        let e = text_edits(node, &old, b"");
221
        assert_eq!(e, vec![Edit::Delete { node, atoms: vec![AtomRange { change: h(1), from: 0, to: 3 }] }]);
222
        // From empty.
223
        let e = text_edits(node, &[], b"x\n");
224
        assert_eq!(e, vec![Edit::Insert { node, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"x\n".to_vec()] }]);
225
    }
226
}