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. Touching edits can stack several
14//! insertions on one point (a formatter's `TextEdit` list, a snippet
15//! expansion); bias then treats the stack as one insertion — `Right` lands
16//! after all of it, `Left` before it.
17
18use std::cell::RefCell;
19use std::ops::Range;
20
21use crate::coords::Bias;
22
23/// One span's coordinate transform: the pre-edit byte range `old` became the
24/// post-edit byte range `new`. For a single edit `old.start == new.start`; in a
25/// multi-edit [`Patch`], `new` is already in final post-edit coordinates.
26#[derive(Clone, PartialEq, Eq, Debug)]
27pub struct Edit {
28    /// Pre-edit byte range that was replaced.
29    pub old: Range<u32>,
30    /// Post-edit byte range it became.
31    pub new: Range<u32>,
32}
33
34/// An ordered set of [`Edit`]s — the one change currency.
35///
36/// Invariant (debug-asserted on every [`Patch::push`]): edits are sorted
37/// ascending and disjoint in *both* coordinate spaces (touching is allowed).
38#[derive(Clone, Default, Debug)]
39pub struct Patch(Vec<Edit>);
40
41impl Patch {
42    /// An empty patch (maps every position to itself).
43    #[must_use]
44    pub fn new() -> Self {
45        Self(Vec::new())
46    }
47
48    /// A patch of a single edit.
49    #[must_use]
50    pub fn single(edit: Edit) -> Self {
51        Self(vec![edit])
52    }
53
54    /// The edits, in ascending order.
55    #[must_use]
56    pub fn edits(&self) -> &[Edit] {
57        &self.0
58    }
59
60    /// Whether the patch changes nothing.
61    #[must_use]
62    pub fn is_empty(&self) -> bool {
63        self.0.is_empty()
64    }
65
66    /// Append an edit. Debug-panics if it breaks the ascending-disjoint
67    /// invariant in either coordinate space.
68    pub fn push(&mut self, edit: Edit) {
69        debug_assert!(edit.old.start <= edit.old.end && edit.new.start <= edit.new.end);
70        if let Some(last) = self.0.last() {
71            debug_assert!(
72                edit.old.start >= last.old.end,
73                "old ranges must be ascending and disjoint"
74            );
75            debug_assert!(
76                edit.new.start >= last.new.end,
77                "new ranges must be ascending and disjoint"
78            );
79        }
80        self.0.push(edit);
81    }
82
83    /// Map a pre-edit byte offset to its post-edit offset (the transit table).
84    ///
85    /// `bias` only matters when `offset` sits exactly on an insertion point:
86    /// `Left` keeps it before the inserted text, `Right` moves it after.
87    ///
88    /// Several edits may share that point (touching is allowed): one replace
89    /// ending at it, any number of pure inserts at it, one replace starting at
90    /// it. `Right` moves past **all** of their text; `Left` stays before
91    /// everything inserted *at* the point — though after a replace that *ends*
92    /// there, since the point is that replace's trailing boundary.
93    #[must_use]
94    pub fn map_offset(&self, offset: u32, bias: Bias) -> u32 {
95        // Complexity gate: this single-offset map scans the edit list, so calling
96        // it once per derived position (folds/decorations) is O(positions·edits) —
97        // the meter charges the scan so that cost shows as superlinear growth,
98        // pushing callers with many positions toward `map_many`, whose batched
99        // index charges only O(edits + queries).
100        crate::perf::charge(self.0.len() as u64);
101        let p = offset as i64;
102        let mut shift: i64 = 0;
103        for (i, e) in self.0.iter().enumerate() {
104            let s = e.old.start as i64;
105            let end = e.old.end as i64;
106            if p < s {
107                // In the gap before this edit — only prior deltas apply.
108                return (p + shift) as u32;
109            }
110            if p <= end {
111                // `e` is the FIRST edit at `p` — the one `Left` maps through.
112                // `Right` maps through the LAST (see `last_at_point`); the edits
113                // between them sit at `p` too, so their deltas join the shift.
114                let k = if bias == Bias::Right { last_at_point(&self.0, i, p) } else { i };
115                shift += self.0[i..k].iter().map(net_delta).sum::<i64>();
116                let e = &self.0[k];
117                let s = i64::from(e.old.start);
118                let sn = s + shift; // == e.new.start
119                return map_within(p, s, i64::from(e.old.end), sn, inserted_len(e), bias) as u32;
120            }
121            // This edit is fully before `p`; accumulate its delta and continue.
122            shift += net_delta(e);
123        }
124        (p + shift) as u32
125    }
126
127    /// Map a pre-edit byte range, biasing each endpoint independently, and
128    /// never inverting: if the mapped start would exceed the mapped end, both
129    /// collapse to the mapped end.
130    #[must_use]
131    pub fn map_range(
132        &self,
133        range: Range<u32>,
134        start_bias: Bias,
135        end_bias: Bias,
136    ) -> Range<u32> {
137        let start = self.map_offset(range.start, start_bias);
138        let end = self.map_offset(range.end, end_bias);
139        start.min(end)..end
140    }
141
142    /// Map a batch of offsets — each with its own [`Bias`] — through the patch,
143    /// appending results to `out` in query order. Semantically each entry is
144    /// exactly `map_offset(offset, bias)`, but the batch builds a prefix-shift
145    /// index once (O(edits)) and binary-searches per query (O(log edits)), so
146    /// the whole call is **O(edits + queries·log edits)** instead of the
147    /// per-offset loop's O(queries·edits). That is the difference between
148    /// O(C²) and O(C) when a document-scale multi-cursor edit produces a
149    /// C-edit patch and every derived view (C selections, N decorations, F fold
150    /// openers) must rebase through it.
151    ///
152    /// No ordering precondition on `queries` — the binary search makes it
153    /// order-independent, so nested/overlapping ranges (decorations) are fine.
154    pub fn map_many(&self, queries: &[(u32, Bias)], out: &mut Vec<u32>) {
155        // Complexity gate: deliberately UNMETERED. The batched map is the
156        // accepted O(edits + queries) bandwidth-class rebase — shifting derived
157        // positions is plain offset arithmetic, not a semantic pass. The meter
158        // measures semantic work plus the per-query `map_offset` cost; charging
159        // the batched mover here would set a linear floor that masks an *added*
160        // semantic O(n) cost elsewhere. Reverting a caller from `map_many` back
161        // to a per-item `map_offset` loop reappears on the meter (that call IS
162        // charged).
163        out.clear();
164        out.reserve(queries.len());
165        let edits = &self.0;
166        if edits.is_empty() {
167            out.extend(queries.iter().map(|&(p, _)| p));
168            return;
169        }
170        // `pref[i]` = accumulated net length delta of `edits[..i]`. Since edits
171        // are sorted ascending and disjoint, their old ranges — and old.end —
172        // are ascending, so a `partition_point` on old.end locates the first
173        // edit with `old.end >= p` (map_offset's "first `p <= end`") and
174        // `pref[k]` is the shift from all strictly-earlier edits (all `end < p`).
175        // `Right` then moves `k` on to the last edit at `p` exactly as the scan
176        // does (the same `last_at_point`), and `pref` at the moved index already
177        // holds the skipped edits' deltas.
178        // The prefix table is a thread-local reused across every `map_many` call —
179        // and every mover (selections, decorations, folds, offset sets) flows
180        // through here per edit — so the batch mapper allocates nothing.
181        thread_local! {
182            static PREF: RefCell<Vec<i64>> = const { RefCell::new(Vec::new()) };
183        }
184        PREF.with(|cell| {
185            let pref = &mut *cell.borrow_mut();
186            pref.clear();
187            pref.push(0);
188            for e in edits {
189                pref.push(pref[pref.len() - 1] + net_delta(e));
190            }
191            for &(offset, bias) in queries {
192                let p = i64::from(offset);
193                let first = edits.partition_point(|e| i64::from(e.old.end) < p);
194                let mapped = if first == edits.len() || p < i64::from(edits[first].old.start) {
195                    // Past every edit, or in the gap before edit `first` — only
196                    // prior deltas apply.
197                    p + pref[first]
198                } else {
199                    let k = if bias == Bias::Right { last_at_point(edits, first, p) } else { first };
200                    let e = &edits[k];
201                    let s = i64::from(e.old.start);
202                    map_within(p, s, i64::from(e.old.end), s + pref[k], inserted_len(e), bias)
203                };
204                out.push(mapped as u32);
205            }
206        });
207    }
208}
209
210/// An edit's net length change (inserted minus deleted bytes).
211fn net_delta(e: &Edit) -> i64 {
212    inserted_len(e) - i64::from(e.old.end - e.old.start)
213}
214
215/// An edit's inserted length (its post-edit span).
216fn inserted_len(e: &Edit) -> i64 {
217    i64::from(e.new.end - e.new.start)
218}
219
220/// The LAST edit sitting at point `p`, given `first` — the first edit with
221/// `old.end >= p`, which must also have `old.start <= p` (`p` is in or on it).
222///
223/// Ascending-disjoint-with-touching admits exactly one shape of edits sharing a
224/// point, contiguous and in this order: at most one replace *ending* at `p`, any
225/// number of pure inserts *at* `p`, at most one replace *starting* at `p`. They
226/// are `edits[first..=last]`, where `last` is the final edit with
227/// `old.start <= p` — old starts are non-decreasing, so a partition point finds
228/// it in O(log edits). When `p` is strictly inside `first`, nothing later can
229/// start at or before `p`, so `last == first` and interior mapping is unchanged.
230///
231/// `Bias::Right` maps through `last` (after ALL text at `p`); `Left` through
232/// `first`. Both mappers share this one choice, so they cannot drift apart.
233fn last_at_point(edits: &[Edit], first: usize, p: i64) -> usize {
234    debug_assert!(i64::from(edits[first].old.start) <= p && p <= i64::from(edits[first].old.end));
235    first + edits[first..].partition_point(|e| i64::from(e.old.start) <= p) - 1
236}
237
238/// The per-edit transit table for a position `p` known to lie in `[s, end]`.
239/// `sn` is the edit's post-edit start (`s + accumulated shift`), `l` the
240/// inserted length.
241fn map_within(p: i64, s: i64, end: i64, sn: i64, l: i64, bias: Bias) -> i64 {
242    if p == s {
243        // Insertion point / replace start — the only bias-dependent case.
244        return if bias == Bias::Right { sn + l } else { sn };
245    }
246    if p < end {
247        // Strictly interior. Prefix (within the inserted length) keeps its
248        // relative offset; the deleted tail beyond it clamps to sn + l.
249        let rel = p - s;
250        return if rel <= l { sn + rel } else { sn + l };
251    }
252    // p == end: the trailing boundary, continuous with the post-edit shift.
253    sn + l
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259    use crate::coords::Bias::{Left, Right};
260
261    fn edit(os: u32, oe: u32, ns: u32, ne: u32) -> Edit {
262        Edit { old: os..oe, new: ns..ne }
263    }
264
265    #[test]
266    fn empty_patch_is_identity() {
267        let p = Patch::new();
268        for o in 0..10 {
269            assert_eq!(p.map_offset(o, Left), o);
270            assert_eq!(p.map_offset(o, Right), o);
271        }
272    }
273
274    #[test]
275    fn pure_insertion_biases_at_the_point() {
276        // Insert 3 bytes at offset 5: old 5..5, new 5..8.
277        let p = Patch::single(edit(5, 5, 5, 8));
278        assert_eq!(p.map_offset(4, Left), 4); // before: unchanged
279        assert_eq!(p.map_offset(5, Left), 5); // at point, Left: stays before
280        assert_eq!(p.map_offset(5, Right), 8); // at point, Right: after insert
281        assert_eq!(p.map_offset(6, Left), 9); // after: shifted +3
282    }
283
284    #[test]
285    fn pure_deletion_collapses_interior_to_start() {
286        // Delete 3..7 (L = 0): old 3..7, new 3..3.
287        let p = Patch::single(edit(3, 7, 3, 3));
288        assert_eq!(p.map_offset(2, Left), 2); // before
289        assert_eq!(p.map_offset(3, Left), 3); // start
290        assert_eq!(p.map_offset(5, Left), 3); // interior collapses to start
291        assert_eq!(p.map_offset(7, Left), 3); // end collapses to start
292        assert_eq!(p.map_offset(8, Left), 4); // after: shifted -4
293    }
294
295    #[test]
296    fn net_growth_replace_preserves_interior_prefix() {
297        // Replace 2..4 (len 2) with 6 bytes: old 2..4, new 2..8. L = 6.
298        let p = Patch::single(edit(2, 4, 2, 8));
299        // A marker at offset 3 (inside the old range, within the inserted
300        // prefix since 3-2=1 ≤ 6) keeps its absolute offset — NOT collapsed.
301        assert_eq!(p.map_offset(3, Left), 3);
302        assert_eq!(p.map_offset(2, Left), 2);
303        assert_eq!(p.map_offset(2, Right), 8);
304        assert_eq!(p.map_offset(4, Left), 8); // end → sn + L
305        assert_eq!(p.map_offset(5, Left), 9); // after: shifted +4
306    }
307
308    #[test]
309    fn net_shrink_replace_clamps_tail_beyond_insert() {
310        // Replace 2..8 (len 6) with 2 bytes: old 2..8, new 2..4. L = 2.
311        let p = Patch::single(edit(2, 8, 2, 4));
312        assert_eq!(p.map_offset(3, Left), 3); // prefix (3-2=1 ≤ 2): kept
313        assert_eq!(p.map_offset(4, Left), 4); // prefix (4-2=2 ≤ 2): kept
314        assert_eq!(p.map_offset(5, Left), 4); // tail (5-2=3 > 2): clamps to sn+L
315        assert_eq!(p.map_offset(8, Left), 4); // end: sn+L
316        assert_eq!(p.map_offset(9, Left), 5); // after: shifted -4
317    }
318
319    #[test]
320    fn multi_edit_accumulates_shift() {
321        // Two edits: insert 2 at offset 1 (old 1..1,new 1..3), delete 5..6
322        // (old 5..6,new 7..7 — post-edit coords include the +2 from edit 1).
323        let mut p = Patch::new();
324        p.push(edit(1, 1, 1, 3)); // +2
325        p.push(edit(5, 6, 7, 7)); // -1, in post-edit space
326        assert_eq!(p.map_offset(0, Left), 0); // before both
327        assert_eq!(p.map_offset(1, Right), 3); // at first insert, Right
328        assert_eq!(p.map_offset(4, Left), 6); // between edits: +2
329        assert_eq!(p.map_offset(5, Left), 7); // start of deletion
330        assert_eq!(p.map_offset(6, Left), 7); // end of deletion → collapses
331        assert_eq!(p.map_offset(7, Left), 8); // after both: +2 -1 = +1
332    }
333
334    /// Both mappers, asserted to agree, for one query — so every pinned
335    /// boundary case below checks `map_many` as well as `map_offset`.
336    fn mapped(p: &Patch, offset: u32, bias: Bias) -> u32 {
337        let single = p.map_offset(offset, bias);
338        let mut batch = Vec::new();
339        p.map_many(&[(offset, bias)], &mut batch);
340        assert_eq!(batch, vec![single], "map_many diverged from map_offset at {offset} {bias:?}");
341        single
342    }
343
344    #[test]
345    fn right_bias_passes_a_touching_replace_and_the_insert_after_it() {
346        // Regression (fails-first): "abcd" with [replace(1..2, "X"), insert(2,
347        // "Y")] → "aXYcd". A Right-biased caret at 2 (between `b` and `c`) must
348        // land after ALL text at 2 — offset 3, between `Y` and `c` — not stop
349        // after the touching replace's `X` (offset 2, splitting X|Y).
350        let mut p = Patch::new();
351        p.push(edit(1, 2, 1, 2)); // "b" → "X"
352        p.push(edit(2, 2, 2, 3)); // insert "Y" at 2, touching the replace's end
353        assert_eq!(mapped(&p, 2, Right), 3, "Right passes X and Y");
354        // Left keeps before the text inserted AT 2; the replace's end is its
355        // trailing boundary (continuous with its post-edit shift), so after X.
356        assert_eq!(mapped(&p, 2, Left), 2, "Left stays before Y");
357    }
358
359    #[test]
360    fn right_bias_passes_every_same_offset_insert() {
361        // Regression (fails-first): two pure inserts at 2 ("Y" then "Z", the
362        // stable-sorted transaction order). Right must pass both (4), not stop
363        // between them (3); Left stays before both (2).
364        let mut p = Patch::new();
365        p.push(edit(2, 2, 2, 3));
366        p.push(edit(2, 2, 3, 4));
367        assert_eq!(mapped(&p, 2, Right), 4, "Right passes both inserts");
368        assert_eq!(mapped(&p, 2, Left), 2, "Left stays before both inserts");
369        // The neighbours are untouched by the fix: plain shift on either side.
370        assert_eq!(mapped(&p, 1, Right), 1);
371        assert_eq!(mapped(&p, 3, Left), 5);
372    }
373
374    #[test]
375    fn map_range_never_inverts() {
376        // Delete a range that fully contains a tracked range → it collapses,
377        // start never exceeds end.
378        let p = Patch::single(edit(0, 10, 0, 0)); // delete everything
379        let r = p.map_range(3..7, Right, Left);
380        assert!(r.start <= r.end, "range inverted: {r:?}");
381        assert_eq!(r, 0..0);
382    }
383
384    /// Build a valid (ascending, disjoint, post-edit-coordinate) patch from a
385    /// script of `(gap_before, old_len, new_len)` triples — exactly the shape a
386    /// committed transaction produces.
387    fn patch_from_script(script: &[(u32, u32, u32)]) -> Patch {
388        let mut p = Patch::new();
389        let mut old = 0u32;
390        let mut shift: i64 = 0;
391        for &(gap, old_len, new_len) in script {
392            old += gap;
393            let ns = (i64::from(old) + shift) as u32;
394            p.push(Edit { old: old..old + old_len, new: ns..ns + new_len });
395            shift += i64::from(new_len) - i64::from(old_len);
396            old += old_len;
397        }
398        p
399    }
400
401    /// Deterministic LCG — no rand dep, reproducible. `next(n)` draws from `0..n`.
402    fn lcg(seed: u64) -> impl FnMut(u32) -> u32 {
403        let mut state = seed;
404        move |n| {
405            state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
406            ((state >> 33) as u32) % n
407        }
408    }
409
410    /// A script dense in the points several edits SHARE — the shapes a uniform
411    /// draw rarely stacks: a zero gap half the time (the next edit touches the
412    /// previous one's end) and a pure insert half the time (so a run of zero-gap
413    /// inserts is several edits at ONE offset, optionally capped by a replace
414    /// starting there). 0..=5 edits; the extent stays under 40 bytes.
415    fn shared_point_script(next: &mut impl FnMut(u32) -> u32) -> Vec<(u32, u32, u32)> {
416        (0..next(6))
417            .map(|_| {
418                let gap = if next(2) == 0 { 0 } else { 1 + next(4) };
419                let old_len = if next(2) == 0 { 0 } else { 1 + next(3) };
420                (gap, old_len, next(4))
421            })
422            .collect()
423    }
424
425    /// Whether old offset `p` is a point two edits share, as `(touching,
426    /// same_offset)`: `touching` — a replace ENDS at `p` and another edit starts
427    /// there; `same_offset` — two or more edits START at `p`.
428    fn shared_point_kind(patch: &Patch, p: u32) -> (bool, bool) {
429        let starts = patch.edits().iter().filter(|e| e.old.start == p).count();
430        let ends_here = patch.edits().iter().any(|e| e.old.start < p && e.old.end == p);
431        (ends_here && starts > 0, starts > 1)
432    }
433
434    /// Where a zero-width marker at old offset `p` REALLY lands — an oracle
435    /// independent of the shift arithmetic. It rebuilds the post-edit text as a
436    /// token stream (each surviving old byte is `Some(i)`, each inserted byte
437    /// `None`), then places the marker by what it is attached to:
438    ///
439    /// - `Left` binds to the byte before it: just after old byte `p - 1` if that
440    ///   survived (so before everything inserted at `p`); if an edit ENDING at
441    ///   `p` replaced it, just after that replacement (`p` is its trailing
442    ///   boundary). At `p == 0`, the very start.
443    /// - `Right` binds to the byte after it: just before old byte `p` if that
444    ///   survived (so after everything inserted at `p`); if an edit STARTING at
445    ///   `p` replaced it, just after that replacement (a replace is an insert at
446    ///   its start). At `p == old_len`, the very end.
447    ///
448    /// `None` for a strictly interior position (byte on the needed side replaced
449    /// by an edit that reaches past `p`) — that is the prefix-preserving clamp,
450    /// pinned by the single-edit tests, not a bias question.
451    fn marker_oracle(old_len: u32, patch: &Patch, p: u32, bias: Bias) -> Option<u32> {
452        let edits = patch.edits();
453        let mut toks: Vec<Option<u32>> = Vec::new();
454        let mut owner: Vec<Option<usize>> = vec![None; old_len as usize];
455        let mut replacement_end: Vec<usize> = Vec::new(); // token index after edit k's text
456        let mut cursor = 0;
457        for (k, e) in edits.iter().enumerate() {
458            toks.extend((cursor..e.old.start).map(Some));
459            for i in e.old.clone() {
460                owner[i as usize] = Some(k);
461            }
462            toks.extend((e.new.start..e.new.end).map(|_| None));
463            replacement_end.push(toks.len());
464            cursor = e.old.end;
465        }
466        toks.extend((cursor..old_len).map(Some));
467        let survivor = |i: u32| toks.iter().position(|&t| t == Some(i)).expect("surviving byte");
468        let at = match bias {
469            Left if p == 0 => 0,
470            Left => match owner[p as usize - 1] {
471                None => survivor(p - 1) + 1,
472                Some(k) if edits[k].old.end == p => replacement_end[k],
473                Some(_) => return None,
474            },
475            Right if p == old_len => toks.len(),
476            Right => match owner[p as usize] {
477                None => survivor(p),
478                Some(k) if edits[k].old.start == p => replacement_end[k],
479                Some(_) => return None,
480            },
481        };
482        Some(at as u32)
483    }
484
485    #[test]
486    fn bias_tracks_a_real_marker_through_touching_and_same_offset_edits() {
487        // Property: at every non-interior offset, both biases, `map_offset` (and
488        // `map_many`, via `mapped`) puts a marker exactly where the token-stream
489        // rebuild says it really is — over random patches dense in touching and
490        // same-offset edits, the shapes formatter `TextEdit` lists and snippet
491        // expansion produce.
492        let mut next = lcg(0xD1B5_4A32_D192_ED03);
493        let (mut touching, mut same_offset) = (0u32, 0u32);
494        for _ in 0..2000 {
495            let script = shared_point_script(&mut next);
496            let patch = patch_from_script(&script);
497            let old_len = patch.edits().last().map_or(0, |e| e.old.end) + next(3);
498            for p in 0..=old_len {
499                let (t, s) = shared_point_kind(&patch, p);
500                for bias in [Left, Right] {
501                    let Some(want) = marker_oracle(old_len, &patch, p, bias) else {
502                        continue;
503                    };
504                    assert_eq!(
505                        mapped(&patch, p, bias),
506                        want,
507                        "marker at {p} {bias:?} misplaced, script {script:?}"
508                    );
509                    touching += u32::from(t);
510                    same_offset += u32::from(s);
511                }
512            }
513        }
514        // The generator must actually reach the shapes this test exists for.
515        assert!(touching > 500, "only {touching} touching-point checks");
516        assert!(same_offset > 500, "only {same_offset} same-offset checks");
517    }
518
519    #[test]
520    fn map_many_matches_map_offset_oracle() {
521        let mut next = lcg(0x9E37_79B9_7F4A_7C15);
522        let (mut touching, mut same_offset) = (0u32, 0u32);
523        for round in 0..800 {
524            // Even rounds: uniform scripts (0..=5 edits; gaps + del/ins incl. 0,
525            // so pure inserts/deletes). Odd rounds: scripts dense in the points
526            // several edits share, where Right must pass every edit at the point
527            // — the batch's partition-point search must land on the same edit
528            // as the scan.
529            let script: Vec<(u32, u32, u32)> = if round % 2 == 0 {
530                (0..next(6)).map(|_| (next(5), next(4), next(4))).collect()
531            } else {
532                shared_point_script(&mut next)
533            };
534            let patch = patch_from_script(&script);
535            // Query every offset in a window spanning before/through/after all
536            // edits, both biases — unsorted order to exercise order-independence.
537            let hi = 40u32;
538            let mut queries: Vec<(u32, Bias)> = Vec::new();
539            for o in 0..hi {
540                queries.push((o, Left));
541                queries.push((o, Right));
542            }
543            // Shuffle-ish: reverse half so the batch is not ascending.
544            queries[..hi as usize].reverse();
545            let mut got = Vec::new();
546            patch.map_many(&queries, &mut got);
547            assert_eq!(got.len(), queries.len());
548            for (i, &(o, b)) in queries.iter().enumerate() {
549                assert_eq!(
550                    got[i],
551                    patch.map_offset(o, b),
552                    "map_many diverged at offset {o} bias {b:?}, script {script:?}"
553                );
554                let (t, s) = shared_point_kind(&patch, o);
555                touching += u32::from(t);
556                same_offset += u32::from(s);
557            }
558        }
559        // Coverage, not luck: the shared-point shapes were actually queried.
560        assert!(touching > 200, "only {touching} touching-point queries");
561        assert!(same_offset > 200, "only {same_offset} same-offset queries");
562    }
563
564    #[test]
565    fn map_many_empty_patch_is_identity() {
566        let p = Patch::new();
567        let q = vec![(7, Left), (3, Right), (0, Left)];
568        let mut out = vec![999]; // pre-populated: map_many must clear it
569        p.map_many(&q, &mut out);
570        assert_eq!(out, vec![7, 3, 0]);
571    }
572}