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}