OXYGENHUB
# 51d4862a16d8 oxygen / crates / oxy-core / src / diff.rs
11.2 KiB · Rust · last changed A Git import commit costs what it changes 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
use std::collections::HashMap;
11
12
/// Split into lines, each keeping its terminator; a final unterminated line
13
/// is its own atom.
14
pub fn split_lines(b: &[u8]) -> Vec<&[u8]> {
15
    let mut out = Vec::new();
16
    let mut start = 0;
17
    for (i, &c) in b.iter().enumerate() {
18
        if c == b'\n' {
19
            out.push(&b[start..=i]);
20
            start = i + 1;
21
        }
22
    }
23
    if start < b.len() {
24
        out.push(&b[start..]);
25
    }
26
    out
27
}
28
29
/// Coalesce atom keys into half-open ranges of consecutive indices per change.
30
pub fn ranges(keys: &[AtomKey]) -> Vec<AtomRange> {
31
    let mut out: Vec<AtomRange> = Vec::new();
32
    for &(c, i) in keys {
33
        match out.last_mut() {
34
            Some(r) if r.change == c && r.to == i => r.to = i + 1,
35
            _ => out.push(AtomRange {
36
                change: c,
37
                from: i,
38
                to: i + 1,
39
            }),
40
        }
41
    }
42
    out
43
}
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.
51
fn kept_lines<'a>(old: &[&'a [u8]], new: &[&'a [u8]]) -> Vec<(usize, usize)> {
52
    let mut ids: HashMap<&'a [u8], u32> = HashMap::new();
53
    let mut intern = |l: &'a [u8]| -> u32 {
54
        let next = ids.len() as u32;
55
        *ids.entry(l).or_insert(next)
56
    };
57
    let o: Vec<u32> = old.iter().map(|l| intern(l)).collect();
58
    let n: Vec<u32> = new.iter().map(|l| intern(l)).collect();
59
    let mut in_old = vec![false; ids.len()];
60
    let mut in_new = vec![false; ids.len()];
61
    for &x in &o {
62
        in_old[x as usize] = true;
63
    }
64
    for &x in &n {
65
        in_new[x as usize] = true;
66
    }
67
    let oi: Vec<usize> = (0..o.len()).filter(|&i| in_new[o[i] as usize]).collect();
68
    let ni: Vec<usize> = (0..n.len()).filter(|&j| in_old[n[j] as usize]).collect();
69
    let os: Vec<u32> = oi.iter().map(|&i| o[i]).collect();
70
    let ns: Vec<u32> = ni.iter().map(|&j| n[j]).collect();
71
    let mut capture = similar::algorithms::Capture::new();
72
    let _ = similar::algorithms::myers::diff(&mut capture, &os, 0..os.len(), &ns, 0..ns.len());
73
    let mut kept = Vec::new();
74
    for op in capture.into_ops() {
75
        if let DiffOp::Equal {
76
            old_index,
77
            new_index,
78
            len,
79
        } = op
80
        {
81
            kept.extend((0..len).map(|k| (oi[old_index + k], ni[new_index + k])));
82
        }
83
    }
84
    kept
85
}
86
87
/// Edits that transform the live lines `old` of `node` into `new`.
88
pub fn text_edits(node: NodeId, old: &[(AtomKey, Vec<u8>)], new: &[u8]) -> Vec<Edit> {
89
    let keys: Vec<AtomKey> = old.iter().map(|(k, _)| *k).collect();
90
    let old_lines: Vec<&[u8]> = old.iter().map(|(_, l)| l.as_slice()).collect();
91
    text_edits_of(node, &keys, &old_lines, new)
92
}
93
94
/// `text_edits` for live lines given as their atoms and, apart, their
95
/// text (`old[i]` is the text of `keys[i]`).
96
pub fn text_edits_of(node: NodeId, keys: &[AtomKey], old_lines: &[&[u8]], new: &[u8]) -> Vec<Edit> {
97
    let new_lines = split_lines(new);
98
99
    let anchor = |i: usize| -> (AtomRef, AtomRef) {
100
        let after = if i == 0 {
101
            AtomRef::Start
102
        } else {
103
            AtomRef::Atom {
104
                change: keys[i - 1].0,
105
                index: keys[i - 1].1,
106
            }
107
        };
108
        let before = if i >= keys.len() {
109
            AtomRef::End
110
        } else {
111
            AtomRef::Atom {
112
                change: keys[i].0,
113
                index: keys[i].1,
114
            }
115
        };
116
        (after, before)
117
    };
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.
121
    let mut deleted: Vec<AtomKey> = Vec::new();
122
    let mut inserts: Vec<Edit> = Vec::new();
123
    let (mut i, mut j) = (0, 0);
124
    let end = (old_lines.len(), new_lines.len());
125
    for (ki, kj) in kept_lines(old_lines, &new_lines).into_iter().chain([end]) {
126
        deleted.extend_from_slice(&keys[i..ki]);
127
        if kj > j {
128
            let (after, before) = anchor(i);
129
            inserts.push(Edit::Insert {
130
                node,
131
                after,
132
                before,
133
                lines: new_lines[j..kj].iter().copied().collect(),
134
            });
135
        }
136
        (i, j) = (ki + 1, kj + 1);
137
    }
138
    let mut edits = Vec::new();
139
    if !deleted.is_empty() {
140
        edits.push(Edit::Delete {
141
            node,
142
            atoms: ranges(&deleted),
143
        });
144
    }
145
    edits.extend(inserts);
146
    edits
147
}
148
149
#[cfg(test)]
150
mod tests {
151
    use super::*;
152
    use crate::hash::ObjectRef;
153
154
    fn h(n: u8) -> ObjectRef {
155
        ObjectRef([n; 32])
156
    }
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]
162
    fn edits_replay_to_the_new_text_on_repetitive_files() {
163
        use rand::{Rng, SeedableRng};
164
        let words: [&[u8]; 5] = [b"}\n", b"    }\n", b"\n", b"a\n", b"    b\n"];
165
        let mut rng = rand::rngs::StdRng::seed_from_u64(11);
166
        for _ in 0..2000 {
167
            let old: Vec<Vec<u8>> = (0..rng.gen_range(0..30))
168
                .map(|_| words[rng.gen_range(0..5)].to_vec())
169
                .collect();
170
            let new: Vec<u8> = (0..rng.gen_range(0..30))
171
                .flat_map(|_| words[rng.gen_range(0..5)].to_vec())
172
                .collect();
173
            let keyed: Vec<(AtomKey, Vec<u8>)> = old
174
                .iter()
175
                .enumerate()
176
                .map(|(i, l)| ((h(1), i as u32), l.clone()))
177
                .collect();
178
            let edits = text_edits(NodeId::ROOT, &keyed, &new);
179
            let mut dead = vec![false; old.len()];
180
            let mut after: std::collections::BTreeMap<i64, Vec<u8>> = Default::default();
181
            for e in &edits {
182
                match e {
183
                    Edit::Delete { atoms, .. } => atoms
184
                        .iter()
185
                        .for_each(|r| (r.from..r.to).for_each(|i| dead[i as usize] = true)),
186
                    Edit::Insert {
187
                        after: a,
188
                        before: b,
189
                        lines,
190
                        ..
191
                    } => {
192
                        let at = match a {
193
                            AtomRef::Start => -1,
194
                            AtomRef::Atom { index, .. } => *index as i64,
195
                            AtomRef::End => unreachable!(),
196
                        };
197
                        let next = match b {
198
                            AtomRef::End => old.len() as i64,
199
                            AtomRef::Atom { index, .. } => *index as i64,
200
                            AtomRef::Start => unreachable!(),
201
                        };
202
                        assert_eq!(next, at + 1, "anchors adjacent");
203
                        after.entry(at).or_default().extend(lines.iter().flatten());
204
                    }
205
                    _ => {}
206
                }
207
            }
208
            let mut out = after.get(&-1).cloned().unwrap_or_default();
209
            for (i, l) in old.iter().enumerate() {
210
                if !dead[i] {
211
                    out.extend(l);
212
                }
213
                out.extend(after.get(&(i as i64)).cloned().unwrap_or_default());
214
            }
215
            assert_eq!(
216
                String::from_utf8_lossy(&out),
217
                String::from_utf8_lossy(&new),
218
                "old {old:?}"
219
            );
220
        }
221
    }
222
223
    #[test]
224
    fn split_keeps_terminators() {
225
        assert_eq!(split_lines(b"a\nb\n"), vec![&b"a\n"[..], &b"b\n"[..]]);
226
        assert_eq!(split_lines(b"a\nb"), vec![&b"a\n"[..], &b"b"[..]]);
227
        assert_eq!(split_lines(b""), Vec::<&[u8]>::new());
228
        assert_eq!(split_lines(b"\n\n"), vec![&b"\n"[..], &b"\n"[..]]);
229
    }
230
231
    #[test]
232
    fn ranges_coalesce() {
233
        let r = ranges(&[(h(1), 0), (h(1), 1), (h(1), 3), (h(2), 0)]);
234
        assert_eq!(
235
            r,
236
            vec![
237
                AtomRange {
238
                    change: h(1),
239
                    from: 0,
240
                    to: 2
241
                },
242
                AtomRange {
243
                    change: h(1),
244
                    from: 3,
245
                    to: 4
246
                },
247
                AtomRange {
248
                    change: h(2),
249
                    from: 0,
250
                    to: 1
251
                }
252
            ]
253
        );
254
    }
255
256
    #[test]
257
    fn edits_for_typical_changes() {
258
        let node = NodeId::random();
259
        let old: Vec<(AtomKey, Vec<u8>)> = ["a\n", "b\n", "c\n"]
260
            .iter()
261
            .enumerate()
262
            .map(|(i, l)| ((h(1), i as u32), l.as_bytes().to_vec()))
263
            .collect();
264
        // No change.
265
        assert!(text_edits(node, &old, b"a\nb\nc\n").is_empty());
266
        // Replace middle line.
267
        let e = text_edits(node, &old, b"a\nB\nc\n");
268
        assert_eq!(
269
            e,
270
            vec![
271
                Edit::Delete {
272
                    node,
273
                    atoms: vec![AtomRange {
274
                        change: h(1),
275
                        from: 1,
276
                        to: 2
277
                    }]
278
                },
279
                Edit::Insert {
280
                    node,
281
                    after: AtomRef::Atom {
282
                        change: h(1),
283
                        index: 0
284
                    },
285
                    before: AtomRef::Atom {
286
                        change: h(1),
287
                        index: 1
288
                    },
289
                    lines: vec![b"B\n".to_vec()].into()
290
                },
291
            ]
292
        );
293
        // Prepend and append.
294
        let e = text_edits(node, &old, b"top\na\nb\nc\nend\n");
295
        assert_eq!(e.len(), 2);
296
        assert!(matches!(
297
            &e[0],
298
            Edit::Insert {
299
                after: AtomRef::Start,
300
                before: AtomRef::Atom { index: 0, .. },
301
                ..
302
            }
303
        ));
304
        assert!(matches!(
305
            &e[1],
306
            Edit::Insert {
307
                after: AtomRef::Atom { index: 2, .. },
308
                before: AtomRef::End,
309
                ..
310
            }
311
        ));
312
        // Delete everything.
313
        let e = text_edits(node, &old, b"");
314
        assert_eq!(
315
            e,
316
            vec![Edit::Delete {
317
                node,
318
                atoms: vec![AtomRange {
319
                    change: h(1),
320
                    from: 0,
321
                    to: 3
322
                }]
323
            }]
324
        );
325
        // From empty.
326
        let e = text_edits(node, &[], b"x\n");
327
        assert_eq!(
328
            e,
329
            vec![Edit::Insert {
330
                node,
331
                after: AtomRef::Start,
332
                before: AtomRef::End,
333
                lines: vec![b"x\n".to_vec()].into()
334
            }]
335
        );
336
    }
337
}