OXYGENHUB
# 1c2bd9abdd4f oxygen / crates / oxy-core / src / diff.rs
10.6 KiB · Rust · last changed Diffs no longer anchor a block of new lines one line early 1 month ago source blame history raw
ff0af1ee unknown 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};
7891e21c Andy McCall ↶ 9
use similar::DiffOp;
ff0af1ee unknown 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,
75167672 Andy McCall 34
            _ => out.push(AtomRange {
35
                change: c,
36
                from: i,
37
                to: i + 1,
38
            }),
ff0af1ee unknown 39
        }
40
    }
41
    out
42
}
43
44
/// Edits that transform the live lines `old` of `node` into `new`.
45
pub fn text_edits(node: NodeId, old: &[(AtomKey, Vec<u8>)], new: &[u8]) -> Vec<Edit> {
46
    let old_lines: Vec<&[u8]> = old.iter().map(|(_, l)| l.as_slice()).collect();
47
    let new_lines = split_lines(new);
7891e21c Andy McCall ↶ 48
    // Myers ops in file order. `capture_diff_slices` also runs the `Compact`
49
    // hook, which slides deletions along runs of equal lines and can leave
50
    // an insertion with the old index of a line before an equal one: the
51
    // insertion was then anchored a line too early and landed on the wrong
52
    // side of it (found by verifying a real Git import commit by commit).
53
    let mut hook = similar::algorithms::Replace::new(similar::algorithms::Capture::new());
54
    let _ = similar::algorithms::diff_deadline(
55
        similar::Algorithm::Myers,
56
        &mut hook,
57
        &old_lines,
58
        0..old_lines.len(),
59
        &new_lines,
60
        0..new_lines.len(),
61
        None,
62
    );
63
    let ops = hook.into_inner().into_ops();
ff0af1ee unknown 64
65
    let anchor = |i: usize| -> (AtomRef, AtomRef) {
75167672 Andy McCall 66
        let after = if i == 0 {
67
            AtomRef::Start
68
        } else {
69
            AtomRef::Atom {
70
                change: old[i - 1].0 .0,
71
                index: old[i - 1].0 .1,
72
            }
73
        };
74
        let before = if i >= old.len() {
75
            AtomRef::End
76
        } else {
77
            AtomRef::Atom {
78
                change: old[i].0 .0,
79
                index: old[i].0 .1,
80
            }
81
        };
ff0af1ee unknown 82
        (after, before)
83
    };
84
85
    let mut deleted: Vec<AtomKey> = Vec::new();
86
    let mut inserts: Vec<Edit> = Vec::new();
87
    for op in ops {
88
        match op {
89
            DiffOp::Equal { .. } => {}
75167672 Andy McCall 90
            DiffOp::Delete {
91
                old_index, old_len, ..
92
            } => {
ff0af1ee unknown 93
                deleted.extend(old[old_index..old_index + old_len].iter().map(|(k, _)| *k));
94
            }
75167672 Andy McCall 95
            DiffOp::Insert {
96
                old_index,
97
                new_index,
98
                new_len,
99
            } => {
ff0af1ee unknown 100
                let (after, before) = anchor(old_index);
101
                inserts.push(Edit::Insert {
102
                    node,
103
                    after,
104
                    before,
75167672 Andy McCall 105
                    lines: new_lines[new_index..new_index + new_len]
106
                        .iter()
107
                        .map(|l| l.to_vec())
108
                        .collect(),
ff0af1ee unknown 109
                });
110
            }
75167672 Andy McCall 111
            DiffOp::Replace {
112
                old_index,
113
                old_len,
114
                new_index,
115
                new_len,
116
            } => {
ff0af1ee unknown 117
                deleted.extend(old[old_index..old_index + old_len].iter().map(|(k, _)| *k));
118
                let (after, before) = anchor(old_index);
119
                inserts.push(Edit::Insert {
120
                    node,
121
                    after,
122
                    before,
75167672 Andy McCall 123
                    lines: new_lines[new_index..new_index + new_len]
124
                        .iter()
125
                        .map(|l| l.to_vec())
126
                        .collect(),
ff0af1ee unknown 127
                });
128
            }
129
        }
130
    }
131
    let mut edits = Vec::new();
132
    if !deleted.is_empty() {
75167672 Andy McCall 133
        edits.push(Edit::Delete {
134
            node,
135
            atoms: ranges(&deleted),
136
        });
ff0af1ee unknown 137
    }
138
    edits.extend(inserts);
139
    edits
140
}
141
142
#[cfg(test)]
143
mod tests {
144
    use super::*;
145
    use crate::hash::ObjectRef;
146
147
    fn h(n: u8) -> ObjectRef {
148
        ObjectRef([n; 32])
7891e21c Andy McCall ↶ 149
    }
150
151
    /// Replaying `text_edits` by position must give the new text, on files
152
    /// full of repeated lines, where a diff has many equally good
153
    /// alignments to choose between.
154
    #[test]
155
    fn edits_replay_to_the_new_text_on_repetitive_files() {
156
        use rand::{Rng, SeedableRng};
157
        let words: [&[u8]; 5] = [b"}\n", b"    }\n", b"\n", b"a\n", b"    b\n"];
158
        let mut rng = rand::rngs::StdRng::seed_from_u64(11);
159
        for _ in 0..2000 {
160
            let old: Vec<Vec<u8>> = (0..rng.gen_range(0..30))
161
                .map(|_| words[rng.gen_range(0..5)].to_vec())
162
                .collect();
163
            let new: Vec<u8> = (0..rng.gen_range(0..30))
164
                .flat_map(|_| words[rng.gen_range(0..5)].to_vec())
165
                .collect();
166
            let keyed: Vec<(AtomKey, Vec<u8>)> = old
167
                .iter()
168
                .enumerate()
169
                .map(|(i, l)| ((h(1), i as u32), l.clone()))
170
                .collect();
171
            let edits = text_edits(NodeId::ROOT, &keyed, &new);
172
            let mut dead = vec![false; old.len()];
173
            let mut after: std::collections::BTreeMap<i64, Vec<u8>> = Default::default();
174
            for e in &edits {
175
                match e {
176
                    Edit::Delete { atoms, .. } => atoms
177
                        .iter()
178
                        .for_each(|r| (r.from..r.to).for_each(|i| dead[i as usize] = true)),
179
                    Edit::Insert {
180
                        after: a,
181
                        before: b,
182
                        lines,
183
                        ..
184
                    } => {
185
                        let at = match a {
186
                            AtomRef::Start => -1,
187
                            AtomRef::Atom { index, .. } => *index as i64,
188
                            AtomRef::End => unreachable!(),
189
                        };
190
                        let next = match b {
191
                            AtomRef::End => old.len() as i64,
192
                            AtomRef::Atom { index, .. } => *index as i64,
193
                            AtomRef::Start => unreachable!(),
194
                        };
195
                        assert_eq!(next, at + 1, "anchors adjacent");
196
                        after.entry(at).or_default().extend(lines.iter().flatten());
197
                    }
198
                    _ => {}
199
                }
200
            }
201
            let mut out = after.get(&-1).cloned().unwrap_or_default();
202
            for (i, l) in old.iter().enumerate() {
203
                if !dead[i] {
204
                    out.extend(l);
205
                }
206
                out.extend(after.get(&(i as i64)).cloned().unwrap_or_default());
207
            }
208
            assert_eq!(
209
                String::from_utf8_lossy(&out),
210
                String::from_utf8_lossy(&new),
211
                "old {old:?}"
212
            );
213
        }
ff0af1ee unknown 214
    }
215
216
    #[test]
217
    fn split_keeps_terminators() {
218
        assert_eq!(split_lines(b"a\nb\n"), vec![&b"a\n"[..], &b"b\n"[..]]);
219
        assert_eq!(split_lines(b"a\nb"), vec![&b"a\n"[..], &b"b"[..]]);
220
        assert_eq!(split_lines(b""), Vec::<&[u8]>::new());
221
        assert_eq!(split_lines(b"\n\n"), vec![&b"\n"[..], &b"\n"[..]]);
222
    }
223
224
    #[test]
225
    fn ranges_coalesce() {
226
        let r = ranges(&[(h(1), 0), (h(1), 1), (h(1), 3), (h(2), 0)]);
227
        assert_eq!(
228
            r,
229
            vec![
75167672 Andy McCall 230
                AtomRange {
231
                    change: h(1),
232
                    from: 0,
233
                    to: 2
234
                },
235
                AtomRange {
236
                    change: h(1),
237
                    from: 3,
238
                    to: 4
239
                },
240
                AtomRange {
241
                    change: h(2),
242
                    from: 0,
243
                    to: 1
244
                }
ff0af1ee unknown 245
            ]
246
        );
247
    }
248
249
    #[test]
250
    fn edits_for_typical_changes() {
251
        let node = NodeId::random();
75167672 Andy McCall 252
        let old: Vec<(AtomKey, Vec<u8>)> = ["a\n", "b\n", "c\n"]
253
            .iter()
254
            .enumerate()
255
            .map(|(i, l)| ((h(1), i as u32), l.as_bytes().to_vec()))
256
            .collect();
ff0af1ee unknown 257
        // No change.
258
        assert!(text_edits(node, &old, b"a\nb\nc\n").is_empty());
259
        // Replace middle line.
260
        let e = text_edits(node, &old, b"a\nB\nc\n");
261
        assert_eq!(
262
            e,
263
            vec![
75167672 Andy McCall 264
                Edit::Delete {
265
                    node,
266
                    atoms: vec![AtomRange {
267
                        change: h(1),
268
                        from: 1,
269
                        to: 2
270
                    }]
271
                },
272
                Edit::Insert {
273
                    node,
274
                    after: AtomRef::Atom {
275
                        change: h(1),
276
                        index: 0
277
                    },
278
                    before: AtomRef::Atom {
279
                        change: h(1),
280
                        index: 1
281
                    },
282
                    lines: vec![b"B\n".to_vec()]
283
                },
ff0af1ee unknown 284
            ]
285
        );
286
        // Prepend and append.
287
        let e = text_edits(node, &old, b"top\na\nb\nc\nend\n");
288
        assert_eq!(e.len(), 2);
75167672 Andy McCall 289
        assert!(matches!(
290
            &e[0],
291
            Edit::Insert {
292
                after: AtomRef::Start,
293
                before: AtomRef::Atom { index: 0, .. },
294
                ..
295
            }
296
        ));
297
        assert!(matches!(
298
            &e[1],
299
            Edit::Insert {
300
                after: AtomRef::Atom { index: 2, .. },
301
                before: AtomRef::End,
302
                ..
303
            }
304
        ));
ff0af1ee unknown 305
        // Delete everything.
306
        let e = text_edits(node, &old, b"");
75167672 Andy McCall 307
        assert_eq!(
308
            e,
309
            vec![Edit::Delete {
310
                node,
311
                atoms: vec![AtomRange {
312
                    change: h(1),
313
                    from: 0,
314
                    to: 3
315
                }]
316
            }]
317
        );
ff0af1ee unknown 318
        // From empty.
319
        let e = text_edits(node, &[], b"x\n");
75167672 Andy McCall 320
        assert_eq!(
321
            e,
322
            vec![Edit::Insert {
323
                node,
324
                after: AtomRef::Start,
325
                before: AtomRef::End,
326
                lines: vec![b"x\n".to_vec()]
327
            }]
328
        );
ff0af1ee unknown 329
    }
330
}