OXYGENHUB
# 7891e21c762f oxygen / crates / oxy-core / src / rewrite.rs
21.1 KiB · Rust · last changed History editing: orphaned references on rewrite are reified by re-anchoring (nearest surviving line of the chain, else the chain's anchors; vanished nodes drop their edits) and reported as notes instead of refused (spec 0.6m) 1 month ago source blame history raw
1
//! Rewrite propagation (spec §8.4): when change `old` is replaced by `new`,
2
//! every change that depends on `old` is rewritten with its references
3
//! translated through a correspondence from `old`'s atoms and tree operations
4
//! to `new`'s. Rewritten changes keep their ChangeIds.
5
//!
6
//! Where a reference has no counterpart it is reified rather than refused:
7
//! an insert anchor moves to the nearest surviving atom of the same insert
8
//! chain in `old`, else to that chain's own anchors (an order conflict then
9
//! arises naturally if `new` placed other content there); deletions of
10
//! vanished atoms are dropped; operations and edits on nodes that vanished
11
//! are dropped. Every such adjustment is reported as a note.
12
13
use crate::hash::ChangeHash;
14
use crate::objects::{AtomRange, AtomRef, Change, Edit, OpRef, TreeOp};
15
use crate::state::State;
16
use std::collections::{BTreeSet, HashMap};
17
18
/// How `old`'s atoms and tree operations map onto `new`'s.
19
#[derive(Clone, Debug, Default, PartialEq, Eq)]
20
pub struct Correspondence {
21
    /// Old atom index → new atom index, per node the old change edited.
22
    /// Absent means the atom no longer exists.
23
    pub atoms: HashMap<u32, u32>,
24
    /// Old tree-op index → new tree-op index.
25
    pub ops: HashMap<u32, u32>,
26
}
27
28
impl Correspondence {
29
    /// Every atom and operation keeps its index: `new` has the same edits as `old`.
30
    pub fn identity(change: &Change) -> Correspondence {
31
        let mut atoms = HashMap::new();
32
        let mut per_node: HashMap<crate::ids::NodeId, u32> = HashMap::new();
33
        for e in &change.edits {
34
            if let Edit::Insert { node, lines, .. } = e {
35
                let idx = per_node.entry(*node).or_default();
36
                for _ in lines {
37
                    atoms.insert(*idx, *idx);
38
                    *idx += 1;
39
                }
40
            }
41
        }
42
        // Atom indices are per node, so the identity map is index → index for
43
        // the largest index any node uses.
44
        let max = per_node.values().copied().max().unwrap_or(0);
45
        for i in 0..max {
46
            atoms.insert(i, i);
47
        }
48
        let ops = (0..change.tree_ops.len() as u32).map(|i| (i, i)).collect();
49
        Correspondence { atoms, ops }
50
    }
51
}
52
53
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
54
pub enum RewriteError {
55
    #[error("change {0} is not in the state")]
56
    Missing(ChangeHash),
57
}
58
59
/// The result of propagating a rewrite.
60
#[derive(Clone, Debug, Default)]
61
pub struct Propagation {
62
    /// `(old hash, rewritten change)` in dependency order.
63
    pub changes: Vec<(ChangeHash, Change)>,
64
    /// Human-readable descriptions of reified orphaned references.
65
    pub notes: Vec<String>,
66
}
67
68
/// Where each atom of a change was inserted: the chain it belongs to.
69
struct Chain {
70
    first: u32,
71
    last: u32,
72
    after: AtomRef,
73
    before: AtomRef,
74
}
75
76
fn chains_of(c: &Change) -> Vec<Chain> {
77
    let mut per_node: HashMap<crate::ids::NodeId, u32> = HashMap::new();
78
    let mut out = Vec::new();
79
    for e in &c.edits {
80
        if let Edit::Insert { node, after, before, lines } = e {
81
            let idx = per_node.entry(*node).or_default();
82
            out.push(Chain { first: *idx, last: *idx + lines.len() as u32 - 1, after: *after, before: *before });
83
            *idx += lines.len() as u32;
84
        }
85
    }
86
    out
87
}
88
89
/// What a rewritten change was replaced by.
90
#[derive(Clone, Debug)]
91
enum Replacement {
92
    Change(ChangeHash),
93
    /// Removed: dependents inherit these dependencies instead.
94
    Removed(Vec<ChangeHash>),
95
}
96
97
/// Rewrite every change in `state` that depends on `old`, given `new` (or
98
/// `None` when `old` is removed) and the correspondence between them.
99
/// The result lists `(old hash, rewritten change)` pairs in dependency
100
/// order, not including `old` → `new` itself, plus notes about reified
101
/// orphaned references.
102
///
103
/// `load` fetches a change's full object (a state keeps only metadata).
104
pub fn propagate(state: &State, old: ChangeHash, new: Option<&Change>, corr: &Correspondence, load: &dyn Fn(&ChangeHash) -> Option<Change>) -> Result<Propagation, RewriteError> {
105
    let old_meta = state.get(&old).ok_or(RewriteError::Missing(old))?;
106
    let old_full = load(&old);
107
    let mut mapping: HashMap<ChangeHash, Replacement> = HashMap::new();
108
    mapping.insert(old, match new {
109
        Some(n) => Replacement::Change(n.hash()),
110
        None => Replacement::Removed(old_meta.deps.clone()),
111
    });
112
    let mut corrs: HashMap<ChangeHash, Correspondence> = HashMap::new();
113
    corrs.insert(old, corr.clone());
114
    // Chains of the rewritten change, for re-anchoring; vanished nodes.
115
    let chains: HashMap<ChangeHash, Vec<Chain>> = old_full.as_ref().map(|c| (old, chains_of(c))).into_iter().collect();
116
    let mut vanished_nodes: BTreeSet<crate::ids::NodeId> = BTreeSet::new();
117
    if let Some(c) = &old_full {
118
        for (i, op) in c.tree_ops.iter().enumerate() {
119
            if let TreeOp::AddNode { node, .. } = op {
120
                let survives = new.is_some() && corr.ops.contains_key(&(i as u32));
121
                if !survives {
122
                    vanished_nodes.insert(*node);
123
                }
124
            }
125
        }
126
    }
127
    let mut notes = Vec::new();
128
129
    // Dependents in topological order: every change whose closure has `old`,
130
    // sorted so that dependencies come first.
131
    let mut dependents: Vec<ChangeHash> = state.changes().filter(|(h, _)| **h != old && state.closure(h).contains(&old)).map(|(h, _)| *h).collect();
132
    dependents.sort_by_key(|h| (state.closure(h).len(), *h));
133
134
    let mut out = Vec::new();
135
    for h in dependents {
136
        let c = load(&h).ok_or(RewriteError::Missing(h))?;
137
        let rewritten = rewrite_one(&h, &c, &mapping, &corrs, &chains, &vanished_nodes, &mut notes);
138
        let rh = rewritten.hash();
139
        corrs.insert(h, Correspondence::identity(&rewritten));
140
        mapping.insert(h, Replacement::Change(rh));
141
        out.push((h, rewritten));
142
    }
143
    Ok(Propagation { changes: out, notes })
144
}
145
146
/// Re-anchor a reference to a vanished atom of `change`: the nearest
147
/// surviving atom of its chain (before it for `after` anchors, after it for
148
/// `before` anchors), else the chain's own anchor.
149
fn reanchor(change: &ChangeHash, index: u32, want_after: bool, mapping: &HashMap<ChangeHash, Replacement>, corrs: &HashMap<ChangeHash, Correspondence>, chains: &HashMap<ChangeHash, Vec<Chain>>) -> AtomRef {
150
    let corr = corrs.get(change);
151
    let chain = chains.get(change).and_then(|cs| cs.iter().find(|ch| ch.first <= index && index <= ch.last));
152
    if let (Some(Replacement::Change(nh)), Some(corr), Some(ch)) = (mapping.get(change), corr, chain) {
153
        let candidates: Box<dyn Iterator<Item = u32>> = if want_after { Box::new((ch.first..index).rev()) } else { Box::new(index + 1..=ch.last) };
154
        for j in candidates {
155
            if let Some(nj) = corr.atoms.get(&j) {
156
                return AtomRef::Atom { change: *nh, index: *nj };
157
            }
158
        }
159
    }
160
    match chain {
161
        Some(ch) => if want_after { ch.after } else { ch.before },
162
        None => if want_after { AtomRef::Start } else { AtomRef::End },
163
    }
164
}
165
166
fn rewrite_one(
167
    hash: &ChangeHash,
168
    c: &Change,
169
    mapping: &HashMap<ChangeHash, Replacement>,
170
    corrs: &HashMap<ChangeHash, Correspondence>,
171
    chains: &HashMap<ChangeHash, Vec<Chain>>,
172
    vanished_nodes: &BTreeSet<crate::ids::NodeId>,
173
    notes: &mut Vec<String>,
174
) -> Change {
175
    let short = hash.short(12);
176
    // An anchor: translate, or re-anchor when its counterpart is gone.
177
    let map_anchor = |r: AtomRef, want_after: bool, notes: &mut Vec<String>| -> AtomRef {
178
        match r {
179
            AtomRef::Atom { change, index } => match mapping.get(&change) {
180
                Some(Replacement::Change(nh)) => match corrs[&change].atoms.get(&index) {
181
                    Some(idx) => AtomRef::Atom { change: *nh, index: *idx },
182
                    None => {
183
                        notes.push(format!("change {short}: an anchor line of the rewritten change vanished; re-anchored to its nearest neighbour"));
184
                        reanchor(&change, index, want_after, mapping, corrs, chains)
185
                    }
186
                },
187
                Some(Replacement::Removed(_)) => {
188
                    notes.push(format!("change {short}: an anchor line of the removed change vanished; re-anchored to its neighbours"));
189
                    reanchor(&change, index, want_after, mapping, corrs, chains)
190
                }
191
                None => r,
192
            },
193
            other => other,
194
        }
195
    };
196
    // A tree-operation reference: translate, or `None` when gone.
197
    let map_op = |r: OpRef| -> Option<OpRef> {
198
        match mapping.get(&r.change) {
199
            Some(Replacement::Change(nh)) => corrs[&r.change].ops.get(&r.index).map(|idx| OpRef { change: *nh, index: *idx }),
200
            Some(Replacement::Removed(_)) => None,
201
            None => Some(r),
202
        }
203
    };
204
    let mut out = c.clone();
205
    let mut deps: BTreeSet<ChangeHash> = BTreeSet::new();
206
    for d in &c.deps {
207
        match mapping.get(d) {
208
            Some(Replacement::Change(nh)) => {
209
                deps.insert(*nh);
210
            }
211
            Some(Replacement::Removed(inherited)) => deps.extend(inherited.iter().copied()),
212
            None => {
213
                deps.insert(*d);
214
            }
215
        }
216
    }
217
    out.deps = deps.into_iter().collect();
218
    let mut ops = Vec::new();
219
    for op in &out.tree_ops {
220
        let node = op.node();
221
        if vanished_nodes.contains(&node) || matches!(op, TreeOp::AddNode { parent, .. } if vanished_nodes.contains(parent)) {
222
            notes.push(format!("change {short}: an operation on a file that vanished with the rewritten change was dropped"));
223
            continue;
224
        }
225
        match op {
226
            TreeOp::RemoveNode { node, placement } => match map_op(*placement) {
227
                Some(p) => ops.push(TreeOp::RemoveNode { node: *node, placement: p }),
228
                None => notes.push(format!("change {short}: removal of a placement that vanished was dropped")),
229
            },
230
            TreeOp::Move { node, old, parent, name } => match map_op(*old) {
231
                Some(o) => ops.push(TreeOp::Move { node: *node, old: o, parent: *parent, name: name.clone() }),
232
                None => notes.push(format!("change {short}: a move of {name} lost its origin and was dropped")),
233
            },
234
            TreeOp::SetMode { node, old, mode } => match map_op(*old) {
235
                Some(o) => ops.push(TreeOp::SetMode { node: *node, old: o, mode: *mode }),
236
                None => notes.push(format!("change {short}: a mode change lost its origin and was dropped")),
237
            },
238
            add @ TreeOp::AddNode { .. } => ops.push(add.clone()),
239
        }
240
    }
241
    out.tree_ops = ops;
242
    let mut edits = Vec::new();
243
    for e in &out.edits {
244
        if vanished_nodes.contains(&e.node()) {
245
            notes.push(format!("change {short}: an edit of a file that vanished with the rewritten change was dropped"));
246
            continue;
247
        }
248
        match e {
249
            Edit::Insert { node, after, before, lines } => edits.push(Edit::Insert {
250
                node: *node,
251
                after: map_anchor(*after, true, notes),
252
                before: map_anchor(*before, false, notes),
253
                lines: lines.clone(),
254
            }),
255
            Edit::Delete { node, atoms } => {
256
                let mut mapped: Vec<(ChangeHash, u32)> = Vec::new();
257
                let mut dropped = 0;
258
                for r in atoms.iter() {
259
                    for i in r.from..r.to {
260
                        match mapping.get(&r.change) {
261
                            Some(Replacement::Change(nh)) => match corrs[&r.change].atoms.get(&i) {
262
                                Some(idx) => mapped.push((*nh, *idx)),
263
                                None => dropped += 1,
264
                            },
265
                            Some(Replacement::Removed(_)) => dropped += 1,
266
                            None => mapped.push((r.change, i)),
267
                        }
268
                    }
269
                }
270
                if dropped > 0 {
271
                    notes.push(format!("change {short}: {dropped} deletion(s) of lines that vanished were dropped"));
272
                }
273
                let mut ranges: Vec<AtomRange> = Vec::new();
274
                for (ch, i) in mapped {
275
                    match ranges.last_mut() {
276
                        Some(r) if r.change == ch && r.to == i => r.to = i + 1,
277
                        _ => ranges.push(AtomRange { change: ch, from: i, to: i + 1 }),
278
                    }
279
                }
280
                if !ranges.is_empty() {
281
                    edits.push(Edit::Delete { node: *node, atoms: ranges });
282
                }
283
            }
284
            other => edits.push(other.clone()),
285
        }
286
    }
287
    out.edits = edits;
288
    out.normalize();
289
    out
290
}
291
292
#[cfg(test)]
293
mod tests {
294
    use super::*;
295
    use crate::hash::ObjectRef;
296
    use crate::ids::{ChangeId, NodeId};
297
    use crate::objects::{ChangeKind, Hlc, Mode, NodeKind};
298
    use crate::state::FileContent;
299
300
    fn change(deps: Vec<ChangeHash>, tree_ops: Vec<TreeOp>, edits: Vec<Edit>, msg: &str) -> Change {
301
        let mut c = Change { change_id: ChangeId::random(), deps, author: ObjectRef::ZERO, hlc: Hlc { wall_ms: 1, logical: 0 }, message: msg.into(), tree_ops, edits, kind: ChangeKind::Normal };
302
        c.normalize();
303
        c
304
    }
305
306
    #[test]
307
    fn describe_propagates_through_dependents() {
308
        let mut st = State::new();
309
        let f = NodeId::random();
310
        let c1 = change(vec![], vec![TreeOp::AddNode { node: f, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "f".into() }], vec![Edit::Insert { node: f, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"a\n".to_vec(), b"b\n".to_vec()] }], "one");
311
        let h1 = st.add(c1.clone()).unwrap();
312
        let c2 = change(vec![h1], vec![], vec![Edit::Delete { node: f, atoms: vec![AtomRange { change: h1, from: 0, to: 1 }] }, Edit::Insert { node: f, after: AtomRef::Atom { change: h1, index: 1 }, before: AtomRef::End, lines: vec![b"c\n".to_vec()] }], "two");
313
        let h2 = st.add(c2.clone()).unwrap();
314
        let c3 = change(vec![h2], vec![TreeOp::SetMode { node: f, old: OpRef { change: h1, index: 0 }, mode: Mode { exec: true, opaque: false } }], vec![Edit::Insert { node: f, after: AtomRef::Atom { change: h2, index: 0 }, before: AtomRef::End, lines: vec![b"d\n".to_vec()] }], "three");
315
        let h3 = st.add(c3.clone()).unwrap();
316
        let before = st.materialize();
317
318
        // Rewrite c1's message; everything downstream follows.
319
        let mut n1 = c1.clone();
320
        n1.message = "one, described".into();
321
        let all: HashMap<ChangeHash, Change> = [(h1, c1.clone()), (h2, c2.clone()), (h3, c3.clone())].into_iter().collect();
322
        let load = |h: &ChangeHash| all.get(h).cloned();
323
        let rewritten = propagate(&st, h1, Some(&n1), &Correspondence::identity(&c1), &load).unwrap().changes;
324
        assert_eq!(rewritten.iter().map(|(h, _)| *h).collect::<Vec<_>>(), vec![h2, h3]);
325
        let mut st2 = State::new();
326
        st2.add(n1.clone()).unwrap();
327
        for (_, c) in &rewritten {
328
            st2.add(c.clone()).unwrap();
329
        }
330
        assert_eq!(st2.len(), 3);
331
        let after = st2.materialize();
332
        assert_eq!(after.files["f"].mode, before.files["f"].mode);
333
        match (&after.files["f"].content, &before.files["f"].content) {
334
            (FileContent::Text { bytes: a, .. }, FileContent::Text { bytes: b, .. }) => assert_eq!(a, b),
335
            _ => panic!(),
336
        }
337
        assert!(matches!(&after.files["f"].content, FileContent::Text { bytes, .. } if bytes == b"b\nc\nd\n"));
338
        // ChangeIds survive; hashes change; deps point at the new hashes.
339
        assert_eq!(rewritten[0].1.change_id, c2.change_id);
340
        assert_eq!(rewritten[0].1.deps, vec![n1.hash()]);
341
        assert_ne!(rewritten[0].1.hash(), h2);
342
        assert_eq!(rewritten[1].1.deps, vec![rewritten[0].1.hash()]);
343
    }
344
345
    #[test]
346
    fn missing_counterpart_is_reanchored() {
347
        let mut st = State::new();
348
        let f = NodeId::random();
349
        let c1 = change(vec![], vec![TreeOp::AddNode { node: f, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "f".into() }], vec![Edit::Insert { node: f, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"a\n".to_vec(), b"b\n".to_vec()] }], "one");
350
        let h1 = st.add(c1.clone()).unwrap();
351
        let c2 = change(vec![h1], vec![], vec![Edit::Insert { node: f, after: AtomRef::Atom { change: h1, index: 1 }, before: AtomRef::End, lines: vec![b"c\n".to_vec()] }], "two");
352
        let h2 = st.add(c2.clone()).unwrap();
353
        let all: HashMap<ChangeHash, Change> = [(h1, c1.clone()), (h2, c2)].into_iter().collect();
354
        let load = |h: &ChangeHash| all.get(h).cloned();
355
        // The rewritten c1 drops line "b": atom 1 has no counterpart, so c2's
356
        // insert after "b" is re-anchored after "a", the nearest survivor.
357
        let mut n1 = c1.clone();
358
        n1.edits = vec![Edit::Insert { node: f, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"a\n".to_vec()] }];
359
        let corr = Correspondence { atoms: [(0, 0)].into_iter().collect(), ops: [(0, 0)].into_iter().collect() };
360
        let p = propagate(&st, h1, Some(&n1), &corr, &load).unwrap();
361
        assert_eq!(p.notes.len(), 1, "{:?}", p.notes);
362
        let n1h = n1.hash();
363
        assert!(matches!(p.changes[0].1.edits[0], Edit::Insert { after: AtomRef::Atom { change, index: 0 }, before: AtomRef::End, .. } if change == n1h));
364
        let mut st2 = State::new();
365
        st2.add(n1.clone()).unwrap();
366
        st2.add(p.changes[0].1.clone()).unwrap();
367
        let m = st2.materialize();
368
        assert!(!m.is_conflicted());
369
        assert!(matches!(&m.files["f"].content, FileContent::Text { bytes, .. } if bytes == b"a\nc\n"));
370
        assert_eq!(propagate(&st, ObjectRef([9; 32]), Some(&n1), &corr, &load).unwrap_err(), RewriteError::Missing(ObjectRef([9; 32])));
371
372
        // Removing c1 altogether: f vanishes with it, so c2's edit is dropped.
373
        let p = propagate(&st, h1, None, &Correspondence::default(), &load).unwrap();
374
        assert_eq!(p.notes.len(), 1, "{:?}", p.notes);
375
        assert!(p.changes[0].1.edits.is_empty() && p.changes[0].1.deps.is_empty());
376
377
        // Rewriting c1 to keep the file but none of its lines: c2's line goes
378
        // to c1's chain anchors (START, END).
379
        let mut n2 = c1.clone();
380
        n2.edits = vec![];
381
        let corr = Correspondence { atoms: HashMap::new(), ops: [(0, 0)].into_iter().collect() };
382
        let p = propagate(&st, h1, Some(&n2), &corr, &load).unwrap();
383
        assert!(matches!(p.changes[0].1.edits[0], Edit::Insert { after: AtomRef::Start, before: AtomRef::End, .. }));
384
        let mut st3 = State::new();
385
        st3.add(n2.clone()).unwrap();
386
        st3.add(p.changes[0].1.clone()).unwrap();
387
        let m = st3.materialize();
388
        assert!(!m.is_conflicted());
389
        assert!(matches!(&m.files["f"].content, FileContent::Text { bytes, .. } if bytes == b"c\n"));
390
    }
391
392
    #[test]
393
    fn removal_rewires_dependencies_or_fails_on_references() {
394
        let mut st = State::new();
395
        let f = NodeId::random();
396
        let g = NodeId::random();
397
        let c1 = change(vec![], vec![TreeOp::AddNode { node: f, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "f".into() }], vec![], "one");
398
        let h1 = st.add(c1.clone()).unwrap();
399
        // c2 merely depends on c1 explicitly (touches another file).
400
        let c2 = change(vec![h1], vec![TreeOp::AddNode { node: g, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "g".into() }], vec![], "two");
401
        let h2 = st.add(c2.clone()).unwrap();
402
        let c3 = change(vec![h2], vec![], vec![Edit::Insert { node: g, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"x\n".to_vec()] }], "three");
403
        let h3 = st.add(c3.clone()).unwrap();
404
        let mut all: HashMap<ChangeHash, Change> = [(h1, c1), (h2, c2), (h3, c3)].into_iter().collect();
405
        let load_all = all.clone();
406
        let load = move |h: &ChangeHash| load_all.get(h).cloned();
407
        // Removing c2 (an explicit-only dependency of nothing that references it) works: c3 inherits h1.
408
        let out = propagate(&st, h2, None, &Correspondence::default(), &load).unwrap().changes;
409
        assert_eq!(out.len(), 1);
410
        assert_eq!(out[0].0, h3);
411
        assert_eq!(out[0].1.deps, vec![h1]);
412
        // Removing c1 fails: c2 does not reference c1 (only an explicit dep), but
413
        // nothing else does either, so it succeeds and c2 inherits no deps.
414
        let out = propagate(&st, h1, None, &Correspondence::default(), &load).unwrap().changes;
415
        assert!(out.iter().any(|(h, c)| *h == h2 && c.deps.is_empty()));
416
        // Removing c2, which created g: c3's edit of g and c4's removal of g's
417
        // placement are dropped with notes, since the file no longer exists.
418
        let c4 = change(vec![h2], vec![TreeOp::RemoveNode { node: g, placement: OpRef { change: h2, index: 0 } }], vec![], "four");
419
        let h4 = st.add(c4.clone()).unwrap();
420
        all.insert(h4, c4);
421
        let load2 = |h: &ChangeHash| all.get(h).cloned();
422
        let p = propagate(&st, h2, None, &Correspondence::default(), &load2).unwrap();
423
        assert_eq!(p.notes.len(), 2, "{:?}", p.notes);
424
        for (_, c) in &p.changes {
425
            assert!(c.edits.is_empty() && c.tree_ops.is_empty());
426
        }
427
    }
428
}