diff --git a/crates/content/src/lib.rs b/crates/content/src/lib.rs index 04bc1cfa95..986bab495e 100644 --- a/crates/content/src/lib.rs +++ b/crates/content/src/lib.rs @@ -504,9 +504,20 @@ impl Pieces { /// for byte. Returns every piece's new bytes (unchanged pieces come back equal). pub fn splice(&self, edits: impl IntoIterator, Vec)>) -> Vec> { let mut out: Vec> = (0..self.len()).map(|i| Vec::with_capacity(self.range(i).len())).collect(); + // Only the pieces from the one holding `from` up to `to`: one call per edit must not + // visit every piece. let copy = |out: &mut Vec>, from: usize, to: usize| { - for (i, piece) in out.iter_mut().enumerate() { + if from >= to { + return; + } + let first = self.piece_of(from); + for (i, piece) in out.iter_mut().enumerate().skip(first) { + #[cfg(test)] + tests::PIECE_VISITS.with(|c| c.set(c.get() + 1)); let r = self.range(i); + if r.start >= to { + break; + } let (a, b) = (from.max(r.start), to.min(r.end)); if a < b { piece.extend_from_slice(self.data.get(a..b).unwrap_or_default()); diff --git a/crates/content/src/tests.rs b/crates/content/src/tests.rs index f32ab3da50..acf494320b 100644 --- a/crates/content/src/tests.rs +++ b/crates/content/src/tests.rs @@ -5,6 +5,30 @@ thread_local! { pub(crate) static RESCANNED: std::cell::Cell = const { std::cell::Cell::new(0) }; } +thread_local! { + /// Pieces [`Pieces::splice`] looked at while copying, on this thread. + pub(crate) static PIECE_VISITS: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +#[test] +fn splicing_visits_each_piece_a_few_times() { + let n = 5_000; + let pieces: Vec = (0..n).map(|i| format!("({i}) Tj")).collect(); + let joined = Pieces::join(&pieces); + let parsed = joined.parse(); + let edits: Vec<_> = parsed.ops.iter().map(|op| (op.span.clone(), b"(x) Tj".to_vec())).collect(); + assert_eq!(edits.len(), n); + PIECE_VISITS.with(|c| c.set(0)); + let out = joined.splice(edits); + let visits = PIECE_VISITS.with(|c| c.get()); + assert!(visits <= 4 * n, "{visits} piece visits for {n} pieces"); + assert!(out.iter().all(|p| p == b"(x) Tj")); + // A replacement spanning two pieces stays whole in the first and leaves the rest of the second. + let two = Pieces::join(&["q (a)", "Tj Q"]); + let tj = two.parse().ops.into_iter().find(|o| o.is("Tj")).map(|o| o.span).unwrap_or_default(); + assert_eq!(two.splice([(tj, b"(b) Tj".to_vec())]), [b"q (b) Tj".to_vec(), b" Q".to_vec()]); +} + #[test] fn a_long_unreadable_number_is_read_once() { for junk in ["+", "5"] {