OXYGENHUB
# a269bee11bb2 oxygen / crates / oxy-core / src / diff.rs
5.5 KiB · Rust · last changed Import the Oxygen specification and the first Rust implementation (root change) 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};
9
use similar::{capture_diff_slices, Algorithm, 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
    let ops = capture_diff_slices(Algorithm::Myers, &old_lines, &new_lines);
45
46
    let anchor = |i: usize| -> (AtomRef, AtomRef) {
47
        let after = if i == 0 { AtomRef::Start } else { AtomRef::Atom { change: old[i - 1].0 .0, index: old[i - 1].0 .1 } };
48
        let before = if i >= old.len() { AtomRef::End } else { AtomRef::Atom { change: old[i].0 .0, index: old[i].0 .1 } };
49
        (after, before)
50
    };
51
52
    let mut deleted: Vec<AtomKey> = Vec::new();
53
    let mut inserts: Vec<Edit> = Vec::new();
54
    for op in ops {
55
        match op {
56
            DiffOp::Equal { .. } => {}
57
            DiffOp::Delete { old_index, old_len, .. } => {
58
                deleted.extend(old[old_index..old_index + old_len].iter().map(|(k, _)| *k));
59
            }
60
            DiffOp::Insert { old_index, new_index, new_len } => {
61
                let (after, before) = anchor(old_index);
62
                inserts.push(Edit::Insert {
63
                    node,
64
                    after,
65
                    before,
66
                    lines: new_lines[new_index..new_index + new_len].iter().map(|l| l.to_vec()).collect(),
67
                });
68
            }
69
            DiffOp::Replace { old_index, old_len, new_index, new_len } => {
70
                deleted.extend(old[old_index..old_index + old_len].iter().map(|(k, _)| *k));
71
                let (after, before) = anchor(old_index);
72
                inserts.push(Edit::Insert {
73
                    node,
74
                    after,
75
                    before,
76
                    lines: new_lines[new_index..new_index + new_len].iter().map(|l| l.to_vec()).collect(),
77
                });
78
            }
79
        }
80
    }
81
    let mut edits = Vec::new();
82
    if !deleted.is_empty() {
83
        edits.push(Edit::Delete { node, atoms: ranges(&deleted) });
84
    }
85
    edits.extend(inserts);
86
    edits
87
}
88
89
#[cfg(test)]
90
mod tests {
91
    use super::*;
92
    use crate::hash::ObjectRef;
93
94
    fn h(n: u8) -> ObjectRef {
95
        ObjectRef([n; 32])
96
    }
97
98
    #[test]
99
    fn split_keeps_terminators() {
100
        assert_eq!(split_lines(b"a\nb\n"), vec![&b"a\n"[..], &b"b\n"[..]]);
101
        assert_eq!(split_lines(b"a\nb"), vec![&b"a\n"[..], &b"b"[..]]);
102
        assert_eq!(split_lines(b""), Vec::<&[u8]>::new());
103
        assert_eq!(split_lines(b"\n\n"), vec![&b"\n"[..], &b"\n"[..]]);
104
    }
105
106
    #[test]
107
    fn ranges_coalesce() {
108
        let r = ranges(&[(h(1), 0), (h(1), 1), (h(1), 3), (h(2), 0)]);
109
        assert_eq!(
110
            r,
111
            vec![
112
                AtomRange { change: h(1), from: 0, to: 2 },
113
                AtomRange { change: h(1), from: 3, to: 4 },
114
                AtomRange { change: h(2), from: 0, to: 1 }
115
            ]
116
        );
117
    }
118
119
    #[test]
120
    fn edits_for_typical_changes() {
121
        let node = NodeId::random();
122
        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();
123
        // No change.
124
        assert!(text_edits(node, &old, b"a\nb\nc\n").is_empty());
125
        // Replace middle line.
126
        let e = text_edits(node, &old, b"a\nB\nc\n");
127
        assert_eq!(
128
            e,
129
            vec![
130
                Edit::Delete { node, atoms: vec![AtomRange { change: h(1), from: 1, to: 2 }] },
131
                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()] },
132
            ]
133
        );
134
        // Prepend and append.
135
        let e = text_edits(node, &old, b"top\na\nb\nc\nend\n");
136
        assert_eq!(e.len(), 2);
137
        assert!(matches!(&e[0], Edit::Insert { after: AtomRef::Start, before: AtomRef::Atom { index: 0, .. }, .. }));
138
        assert!(matches!(&e[1], Edit::Insert { after: AtomRef::Atom { index: 2, .. }, before: AtomRef::End, .. }));
139
        // Delete everything.
140
        let e = text_edits(node, &old, b"");
141
        assert_eq!(e, vec![Edit::Delete { node, atoms: vec![AtomRange { change: h(1), from: 0, to: 3 }] }]);
142
        // From empty.
143
        let e = text_edits(node, &[], b"x\n");
144
        assert_eq!(e, vec![Edit::Insert { node, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"x\n".to_vec()] }]);
145
    }
146
}