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}