Skip to main content

scrive_core/
patch.rs

1//! The change currency: [`Edit`] / [`Patch`] and the one position-mapping
2//! function every derived position flows through.
3//!
4//! A [`Patch`] is the sole record of "what changed" produced by a committed
5//! transaction. Selections, diagnostics, snippet stops, and find matches all
6//! move through [`Patch::map_offset`] — one function, one contract — so a
7//! position is never maintained two ways and cannot drift out of sync.
8//!
9//! Mapping is **prefix-preserving**: an edit deletes `[s, e)` and inserts `L`
10//! bytes at `s`; a position inside the overwritten prefix keeps its absolute
11//! offset, only the deleted tail *beyond* the insert clamps, and a pure
12//! deletion collapses its interior to `s`. Bias matters at exactly one place —
13//! a position sitting on the insertion point.
14
15use std::cell::RefCell;
16use std::ops::Range;
17
18use crate::coords::Bias;
19
20/// One span's coordinate transform: the pre-edit byte range `old` became the
21/// post-edit byte range `new`. For a single edit `old.start == new.start`; in a
22/// multi-edit [`Patch`], `new` is already in final post-edit coordinates.
23#[derive(Clone, PartialEq, Eq, Debug)]
24pub struct Edit {
25    /// Pre-edit byte range that was replaced.
26    pub old: Range<u32>,
27    /// Post-edit byte range it became.
28    pub new: Range<u32>,
29}
30
31/// An ordered set of [`Edit`]s — the one change currency.
32///
33/// Invariant (debug-asserted on every [`Patch::push`]): edits are sorted
34/// ascending and disjoint in *both* coordinate spaces (touching is allowed).
35#[derive(Clone, Default, Debug)]
36pub struct Patch(Vec<Edit>);
37
38impl Patch {
39    /// An empty patch (maps every position to itself).
40    #[must_use]
41    pub fn new() -> Self {
42        Self(Vec::new())
43    }
44
45    /// A patch of a single edit.
46    #[must_use]
47    pub fn single(edit: Edit) -> Self {
48        Self(vec![edit])
49    }
50
51    /// The edits, in ascending order.
52    #[must_use]
53    pub fn edits(&self) -> &[Edit] {
54        &self.0
55    }
56
57    /// Whether the patch changes nothing.
58    #[must_use]
59    pub fn is_empty(&self) -> bool {
60        self.0.is_empty()
61    }
62
63    /// Append an edit. Debug-panics if it breaks the ascending-disjoint
64    /// invariant in either coordinate space.
65    pub fn push(&mut self, edit: Edit) {
66        debug_assert!(edit.old.start <= edit.old.end && edit.new.start <= edit.new.end);
67        if let Some(last) = self.0.last() {
68            debug_assert!(
69                edit.old.start >= last.old.end,
70                "old ranges must be ascending and disjoint"
71            );
72            debug_assert!(
73                edit.new.start >= last.new.end,
74                "new ranges must be ascending and disjoint"
75            );
76        }
77        self.0.push(edit);
78    }
79
80    /// Map a pre-edit byte offset to its post-edit offset (the transit table).
81    ///
82    /// `bias` only matters when `offset` sits exactly on an insertion point:
83    /// `Left` keeps it before the inserted text, `Right` moves it after.
84    #[must_use]
85    pub fn map_offset(&self, offset: u32, bias: Bias) -> u32 {
86        // Complexity gate: this single-offset map scans the edit list, so calling
87        // it once per derived position (folds/decorations) is O(positions·edits) —
88        // the meter charges the scan so that cost shows as superlinear growth,
89        // pushing callers with many positions toward `map_many`, whose batched
90        // index charges only O(edits + queries).
91        crate::perf::charge(self.0.len() as u64);
92        let p = offset as i64;
93        let mut shift: i64 = 0;
94        for e in &self.0 {
95            let s = e.old.start as i64;
96            let end = e.old.end as i64;
97            let l = (e.new.end - e.new.start) as i64;
98            if p < s {
99                // In the gap before this edit — only prior deltas apply.
100                return (p + shift) as u32;
101            }
102            if p <= end {
103                let sn = s + shift; // == e.new.start
104                return map_within(p, s, end, sn, l, bias) as u32;
105            }
106            // This edit is fully before `p`; accumulate its delta and continue.
107            shift += l - (end - s);
108        }
109        (p + shift) as u32
110    }
111
112    /// Map a pre-edit byte range, biasing each endpoint independently, and
113    /// never inverting: if the mapped start would exceed the mapped end, both
114    /// collapse to the mapped end.
115    #[must_use]
116    pub fn map_range(
117        &self,
118        range: Range<u32>,
119        start_bias: Bias,
120        end_bias: Bias,
121    ) -> Range<u32> {
122        let start = self.map_offset(range.start, start_bias);
123        let end = self.map_offset(range.end, end_bias);
124        start.min(end)..end
125    }
126
127    /// Map a batch of offsets — each with its own [`Bias`] — through the patch,
128    /// appending results to `out` in query order. Semantically each entry is
129    /// exactly `map_offset(offset, bias)`, but the batch builds a prefix-shift
130    /// index once (O(edits)) and binary-searches per query (O(log edits)), so
131    /// the whole call is **O(edits + queries·log edits)** instead of the
132    /// per-offset loop's O(queries·edits). That is the difference between
133    /// O(C²) and O(C) when a document-scale multi-cursor edit produces a
134    /// C-edit patch and every derived view (C selections, N decorations, F fold
135    /// openers) must rebase through it.
136    ///
137    /// No ordering precondition on `queries` — the binary search makes it
138    /// order-independent, so nested/overlapping ranges (decorations) are fine.
139    pub fn map_many(&self, queries: &[(u32, Bias)], out: &mut Vec<u32>) {
140        // Complexity gate: deliberately UNMETERED. The batched map is the
141        // accepted O(edits + queries) bandwidth-class rebase — shifting derived
142        // positions is plain offset arithmetic, not a semantic pass. The meter
143        // measures semantic work plus the per-query `map_offset` cost; charging
144        // the batched mover here would set a linear floor that masks an *added*
145        // semantic O(n) cost elsewhere. Reverting a caller from `map_many` back
146        // to a per-item `map_offset` loop reappears on the meter (that call IS
147        // charged).
148        out.clear();
149        out.reserve(queries.len());
150        let edits = &self.0;
151        if edits.is_empty() {
152            out.extend(queries.iter().map(|&(p, _)| p));
153            return;
154        }
155        // `pref[i]` = accumulated net length delta of `edits[..i]`. Since edits
156        // are sorted ascending and disjoint, their old ranges — and old.end —
157        // are ascending, so a `partition_point` on old.end locates the first
158        // edit with `old.end >= p` (map_offset's "first `p <= end`") and
159        // `pref[k]` is the shift from all strictly-earlier edits (all `end < p`).
160        // The prefix table is a thread-local reused across every `map_many` call —
161        // and every mover (selections, decorations, folds, offset sets) flows
162        // through here per edit — so the batch mapper allocates nothing.
163        thread_local! {
164            static PREF: RefCell<Vec<i64>> = const { RefCell::new(Vec::new()) };
165        }
166        PREF.with(|cell| {
167            let pref = &mut *cell.borrow_mut();
168            pref.clear();
169            pref.push(0);
170            for e in edits {
171                let delta =
172                    i64::from(e.new.end - e.new.start) - i64::from(e.old.end - e.old.start);
173                pref.push(pref[pref.len() - 1] + delta);
174            }
175            for &(offset, bias) in queries {
176                let p = i64::from(offset);
177                let k = edits.partition_point(|e| i64::from(e.old.end) < p);
178                let mapped = if k == edits.len() {
179                    p + pref[k]
180                } else {
181                    let e = &edits[k];
182                    let s = i64::from(e.old.start);
183                    let shift = pref[k];
184                    if p < s {
185                        p + shift // in the gap before edit k — only prior deltas apply
186                    } else {
187                        let end = i64::from(e.old.end);
188                        let l = i64::from(e.new.end - e.new.start);
189                        map_within(p, s, end, s + shift, l, bias)
190                    }
191                };
192                out.push(mapped as u32);
193            }
194        });
195    }
196}
197
198/// The per-edit transit table for a position `p` known to lie in `[s, end]`.
199/// `sn` is the edit's post-edit start (`s + accumulated shift`), `l` the
200/// inserted length.
201fn map_within(p: i64, s: i64, end: i64, sn: i64, l: i64, bias: Bias) -> i64 {
202    if p == s {
203        // Insertion point / replace start — the only bias-dependent case.
204        return if bias == Bias::Right { sn + l } else { sn };
205    }
206    if p < end {
207        // Strictly interior. Prefix (within the inserted length) keeps its
208        // relative offset; the deleted tail beyond it clamps to sn + l.
209        let rel = p - s;
210        return if rel <= l { sn + rel } else { sn + l };
211    }
212    // p == end: the trailing boundary, continuous with the post-edit shift.
213    sn + l
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use crate::coords::Bias::{Left, Right};
220
221    fn edit(os: u32, oe: u32, ns: u32, ne: u32) -> Edit {
222        Edit { old: os..oe, new: ns..ne }
223    }
224
225    #[test]
226    fn empty_patch_is_identity() {
227        let p = Patch::new();
228        for o in 0..10 {
229            assert_eq!(p.map_offset(o, Left), o);
230            assert_eq!(p.map_offset(o, Right), o);
231        }
232    }
233
234    #[test]
235    fn pure_insertion_biases_at_the_point() {
236        // Insert 3 bytes at offset 5: old 5..5, new 5..8.
237        let p = Patch::single(edit(5, 5, 5, 8));
238        assert_eq!(p.map_offset(4, Left), 4); // before: unchanged
239        assert_eq!(p.map_offset(5, Left), 5); // at point, Left: stays before
240        assert_eq!(p.map_offset(5, Right), 8); // at point, Right: after insert
241        assert_eq!(p.map_offset(6, Left), 9); // after: shifted +3
242    }
243
244    #[test]
245    fn pure_deletion_collapses_interior_to_start() {
246        // Delete 3..7 (L = 0): old 3..7, new 3..3.
247        let p = Patch::single(edit(3, 7, 3, 3));
248        assert_eq!(p.map_offset(2, Left), 2); // before
249        assert_eq!(p.map_offset(3, Left), 3); // start
250        assert_eq!(p.map_offset(5, Left), 3); // interior collapses to start
251        assert_eq!(p.map_offset(7, Left), 3); // end collapses to start
252        assert_eq!(p.map_offset(8, Left), 4); // after: shifted -4
253    }
254
255    #[test]
256    fn net_growth_replace_preserves_interior_prefix() {
257        // Replace 2..4 (len 2) with 6 bytes: old 2..4, new 2..8. L = 6.
258        let p = Patch::single(edit(2, 4, 2, 8));
259        // A marker at offset 3 (inside the old range, within the inserted
260        // prefix since 3-2=1 ≤ 6) keeps its absolute offset — NOT collapsed.
261        assert_eq!(p.map_offset(3, Left), 3);
262        assert_eq!(p.map_offset(2, Left), 2);
263        assert_eq!(p.map_offset(2, Right), 8);
264        assert_eq!(p.map_offset(4, Left), 8); // end → sn + L
265        assert_eq!(p.map_offset(5, Left), 9); // after: shifted +4
266    }
267
268    #[test]
269    fn net_shrink_replace_clamps_tail_beyond_insert() {
270        // Replace 2..8 (len 6) with 2 bytes: old 2..8, new 2..4. L = 2.
271        let p = Patch::single(edit(2, 8, 2, 4));
272        assert_eq!(p.map_offset(3, Left), 3); // prefix (3-2=1 ≤ 2): kept
273        assert_eq!(p.map_offset(4, Left), 4); // prefix (4-2=2 ≤ 2): kept
274        assert_eq!(p.map_offset(5, Left), 4); // tail (5-2=3 > 2): clamps to sn+L
275        assert_eq!(p.map_offset(8, Left), 4); // end: sn+L
276        assert_eq!(p.map_offset(9, Left), 5); // after: shifted -4
277    }
278
279    #[test]
280    fn multi_edit_accumulates_shift() {
281        // Two edits: insert 2 at offset 1 (old 1..1,new 1..3), delete 5..6
282        // (old 5..6,new 7..7 — post-edit coords include the +2 from edit 1).
283        let mut p = Patch::new();
284        p.push(edit(1, 1, 1, 3)); // +2
285        p.push(edit(5, 6, 7, 7)); // -1, in post-edit space
286        assert_eq!(p.map_offset(0, Left), 0); // before both
287        assert_eq!(p.map_offset(1, Right), 3); // at first insert, Right
288        assert_eq!(p.map_offset(4, Left), 6); // between edits: +2
289        assert_eq!(p.map_offset(5, Left), 7); // start of deletion
290        assert_eq!(p.map_offset(6, Left), 7); // end of deletion → collapses
291        assert_eq!(p.map_offset(7, Left), 8); // after both: +2 -1 = +1
292    }
293
294    #[test]
295    fn map_range_never_inverts() {
296        // Delete a range that fully contains a tracked range → it collapses,
297        // start never exceeds end.
298        let p = Patch::single(edit(0, 10, 0, 0)); // delete everything
299        let r = p.map_range(3..7, Right, Left);
300        assert!(r.start <= r.end, "range inverted: {r:?}");
301        assert_eq!(r, 0..0);
302    }
303
304    /// Build a valid (ascending, disjoint, post-edit-coordinate) patch from a
305    /// script of `(gap_before, old_len, new_len)` triples — exactly the shape a
306    /// committed transaction produces.
307    fn patch_from_script(script: &[(u32, u32, u32)]) -> Patch {
308        let mut p = Patch::new();
309        let mut old = 0u32;
310        let mut shift: i64 = 0;
311        for &(gap, old_len, new_len) in script {
312            old += gap;
313            let ns = (i64::from(old) + shift) as u32;
314            p.push(Edit { old: old..old + old_len, new: ns..ns + new_len });
315            shift += i64::from(new_len) - i64::from(old_len);
316            old += old_len;
317        }
318        p
319    }
320
321    #[test]
322    fn map_many_matches_map_offset_oracle() {
323        // Deterministic LCG — no rand dep, reproducible.
324        let mut state: u64 = 0x9E37_79B9_7F4A_7C15;
325        let mut next = |n: u32| {
326            state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
327            ((state >> 33) as u32) % n
328        };
329        for _ in 0..400 {
330            let n_edits = next(6); // 0..=5 edits, incl. the empty-patch path
331            let script: Vec<(u32, u32, u32)> = (0..n_edits)
332                .map(|_| (next(5), next(4), next(4))) // gaps + del/ins incl. 0 (pure insert/delete)
333                .collect();
334            let patch = patch_from_script(&script);
335            // Query every offset in a window spanning before/through/after all
336            // edits, both biases — unsorted order to exercise order-independence.
337            let hi = 40u32;
338            let mut queries: Vec<(u32, Bias)> = Vec::new();
339            for o in 0..hi {
340                queries.push((o, Left));
341                queries.push((o, Right));
342            }
343            // Shuffle-ish: reverse half so the batch is not ascending.
344            queries[..hi as usize].reverse();
345            let mut got = Vec::new();
346            patch.map_many(&queries, &mut got);
347            assert_eq!(got.len(), queries.len());
348            for (i, &(o, b)) in queries.iter().enumerate() {
349                assert_eq!(
350                    got[i],
351                    patch.map_offset(o, b),
352                    "map_many diverged at offset {o} bias {b:?}, script {script:?}"
353                );
354            }
355        }
356    }
357
358    #[test]
359    fn map_many_empty_patch_is_identity() {
360        let p = Patch::new();
361        let q = vec![(7, Left), (3, Right), (0, Left)];
362        let mut out = vec![999]; // pre-populated: map_many must clear it
363        p.map_many(&q, &mut out);
364        assert_eq!(out, vec![7, 3, 0]);
365    }
366}