Introduce `Buffer::edits_since_in_range`

Antonio Scandurra , Nathan Sobo , and Max Brunsfeld created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
Co-Authored-By: Max Brunsfeld <max@zed.dev>

Change summary

crates/language/Cargo.toml           |   2 
crates/language/src/fragment_list.rs |  29 ++++---
crates/text/src/tests.rs             |  26 +++++++
crates/text/src/text.rs              | 100 +++++++++++++++++++++++++++--
4 files changed, 135 insertions(+), 22 deletions(-)

Detailed changes

crates/language/Cargo.toml 🔗

@@ -1,7 +1,7 @@
 [package]
 name = "language"
 version = "0.1.0"
-edition = "2018"
+edition = "2021"
 
 [lib]
 path = "src/language.rs"

crates/language/src/fragment_list.rs 🔗

@@ -5,7 +5,7 @@ use parking_lot::Mutex;
 use smallvec::{smallvec, SmallVec};
 use std::{cmp, iter, mem, ops::Range};
 use sum_tree::{Bias, Cursor, SumTree};
-use text::TextSummary;
+use text::{Anchor, AnchorRangeExt, TextSummary};
 use theme::SyntaxTheme;
 
 const NEWLINES: &'static [u8] = &[b'\n'; u8::MAX as usize];
@@ -43,7 +43,7 @@ pub struct FragmentProperties<'a, T> {
 struct Entry {
     id: FragmentId,
     buffer: buffer::Snapshot,
-    buffer_range: Range<usize>,
+    buffer_range: Range<Anchor>,
     text_summary: TextSummary,
     header_height: u8,
 }
@@ -86,7 +86,8 @@ impl FragmentList {
         self.sync(cx);
 
         let buffer = props.buffer.read(cx);
-        let buffer_range = props.range.start.to_offset(buffer)..props.range.end.to_offset(buffer);
+        let buffer_range =
+            buffer.anchor_before(props.range.start)..buffer.anchor_after(props.range.end);
         let mut text_summary =
             buffer.text_summary_for_range::<TextSummary, _>(buffer_range.clone());
         if props.header_height > 0 {
@@ -148,12 +149,13 @@ impl FragmentList {
         let old_fragments = mem::take(&mut snapshot.entries);
         let mut cursor = old_fragments.cursor::<FragmentId>();
         for (buffer, fragment_id, patch_ix) in fragments_to_edit {
+            let buffer = buffer.read(cx);
             snapshot
                 .entries
                 .push_tree(cursor.slice(fragment_id, Bias::Left, &()), &());
 
             let fragment = cursor.item().unwrap();
-            let mut new_range = fragment.buffer_range.clone();
+            let mut new_range = fragment.buffer_range.to_offset(buffer);
             for edit in patches[patch_ix].edits() {
                 let edit_start = edit.new.start;
                 let edit_end = edit.new.start + edit.old_len();
@@ -177,7 +179,6 @@ impl FragmentList {
                 }
             }
 
-            let buffer = buffer.read(cx);
             let mut text_summary: TextSummary = buffer.text_summary_for_range(new_range.clone());
             if fragment.header_height > 0 {
                 text_summary.first_line_chars = 0;
@@ -189,7 +190,8 @@ impl FragmentList {
                 Entry {
                     id: fragment.id.clone(),
                     buffer: buffer.snapshot(),
-                    buffer_range: new_range,
+                    buffer_range: buffer.anchor_before(new_range.start)
+                        ..buffer.anchor_after(new_range.end),
                     text_summary,
                     header_height: fragment.header_height,
                 },
@@ -227,10 +229,11 @@ impl Snapshot {
         cursor.seek(&range.start, Bias::Right, &());
 
         let entry_chunks = cursor.item().map(|entry| {
-            let buffer_start = entry.buffer_range.start + (range.start - cursor.start());
+            let buffer_range = entry.buffer_range.to_offset(&entry.buffer);
+            let buffer_start = buffer_range.start + (range.start - cursor.start());
             let buffer_end = cmp::min(
-                entry.buffer_range.end,
-                entry.buffer_range.start + (range.end - cursor.start()),
+                buffer_range.end,
+                buffer_range.start + (range.end - cursor.start()),
             );
             entry.buffer.chunks(buffer_start..buffer_end, theme)
         });
@@ -305,17 +308,17 @@ impl<'a> Iterator for Chunks<'a> {
 
         self.cursor.next(&());
         let entry = self.cursor.item()?;
-
+        let buffer_range = entry.buffer_range.to_offset(&entry.buffer);
         let buffer_end = cmp::min(
-            entry.buffer_range.end,
-            entry.buffer_range.start + (self.range.end - self.cursor.start()),
+            buffer_range.end,
+            buffer_range.start + (self.range.end - self.cursor.start()),
         );
 
         self.header_height = entry.header_height;
         self.entry_chunks = Some(
             entry
                 .buffer
-                .chunks(entry.buffer_range.start..buffer_end, self.theme),
+                .chunks(buffer_range.start..buffer_end, self.theme),
         );
 
         Some(Chunk {

crates/text/src/tests.rs 🔗

@@ -102,6 +102,32 @@ fn test_random_edits(mut rng: StdRng) {
         }
         assert_eq!(text.to_string(), buffer.text());
 
+        for _ in 0..5 {
+            let end_ix = old_buffer.clip_offset(rng.gen_range(0..=old_buffer.len()), Bias::Right);
+            let start_ix = old_buffer.clip_offset(rng.gen_range(0..=end_ix), Bias::Left);
+            let range = old_buffer.anchor_before(start_ix)..old_buffer.anchor_after(end_ix);
+            let mut old_text = old_buffer.text_for_range(range.clone()).collect::<String>();
+            let edits = buffer
+                .edits_since_in_range::<usize>(&old_buffer.version, range.clone())
+                .collect::<Vec<_>>();
+            log::info!(
+                "applying edits since version {:?} to old text in range {:?}: {:?}: {:?}",
+                old_buffer.version(),
+                start_ix..end_ix,
+                old_text,
+                edits,
+            );
+
+            let new_text = buffer.text_for_range(range).collect::<String>();
+            for edit in edits {
+                old_text.replace_range(
+                    edit.new.start..edit.new.start + edit.old_len(),
+                    &new_text[edit.new],
+                );
+            }
+            assert_eq!(old_text, new_text);
+        }
+
         let subscription_edits = subscription.consume();
         log::info!(
             "applying subscription edits since version {:?} to old text: {:?}: {:?}",

crates/text/src/text.rs 🔗

@@ -302,6 +302,7 @@ struct Edits<'a, D: TextDimension<'a>, F: FnMut(&FragmentSummary) -> bool> {
     since: &'a clock::Global,
     old_end: D,
     new_end: D,
+    range: Range<FullOffset>,
 }
 
 #[derive(Clone, Debug, Default, Eq, PartialEq)]
@@ -402,6 +403,12 @@ struct FragmentTextSummary {
     deleted: usize,
 }
 
+impl FragmentTextSummary {
+    pub fn full_offset(&self) -> FullOffset {
+        FullOffset(self.visible + self.deleted)
+    }
+}
+
 impl<'a> sum_tree::Dimension<'a, FragmentSummary> for FragmentTextSummary {
     fn add_summary(&mut self, summary: &'a FragmentSummary, _: &Option<clock::Global>) {
         self.visible += summary.text.visible;
@@ -1873,6 +1880,17 @@ impl Snapshot {
         &'a self,
         since: &'a clock::Global,
     ) -> impl 'a + Iterator<Item = Edit<D>>
+    where
+        D: 'a + TextDimension<'a> + Ord,
+    {
+        self.edits_since_in_range(since, Anchor::min()..Anchor::max())
+    }
+
+    pub fn edits_since_in_range<'a, D>(
+        &'a self,
+        since: &'a clock::Global,
+        range: Range<Anchor>,
+    ) -> impl 'a + Iterator<Item = Edit<D>>
     where
         D: 'a + TextDimension<'a> + Ord,
     {
@@ -1885,14 +1903,36 @@ impl Snapshot {
             )
         };
 
+        let mut cursor = self
+            .fragments
+            .cursor::<(VersionedFullOffset, FragmentTextSummary)>();
+        cursor.seek(
+            &VersionedFullOffset::Offset(range.start.full_offset),
+            range.start.bias,
+            &Some(range.start.version),
+        );
+        let mut visible_start = cursor.start().1.visible;
+        let mut deleted_start = cursor.start().1.deleted;
+        if let Some(fragment) = cursor.item() {
+            let overshoot = range.start.full_offset.0 - cursor.start().0.full_offset().0;
+            if fragment.visible {
+                visible_start += overshoot;
+            } else {
+                deleted_start += overshoot;
+            }
+        }
+
+        let full_offset_start = FullOffset(visible_start + deleted_start);
+        let full_offset_end = range.end.to_full_offset(self, range.end.bias);
         Edits {
-            visible_cursor: self.visible_text.cursor(0),
-            deleted_cursor: self.deleted_text.cursor(0),
+            visible_cursor: self.visible_text.cursor(visible_start),
+            deleted_cursor: self.deleted_text.cursor(deleted_start),
             fragments_cursor,
             undos: &self.undo_map,
             since,
             old_end: Default::default(),
             new_end: Default::default(),
+            range: full_offset_start..full_offset_end,
         }
     }
 }
@@ -1960,9 +2000,19 @@ impl<'a, D: TextDimension<'a> + Ord, F: FnMut(&FragmentSummary) -> bool> Iterato
         let cursor = self.fragments_cursor.as_mut()?;
 
         while let Some(fragment) = cursor.item() {
-            let summary = self.visible_cursor.summary(cursor.start().visible);
-            self.old_end.add_assign(&summary);
-            self.new_end.add_assign(&summary);
+            if cursor.end(&None).full_offset() < self.range.start {
+                cursor.next(&None);
+                continue;
+            } else if cursor.start().full_offset() >= self.range.end {
+                break;
+            }
+
+            if cursor.start().visible > self.visible_cursor.offset() {
+                let summary = self.visible_cursor.summary(cursor.start().visible);
+                self.old_end.add_assign(&summary);
+                self.new_end.add_assign(&summary);
+            }
+
             if pending_edit
                 .as_ref()
                 .map_or(false, |change| change.new.end < self.new_end)
@@ -1971,7 +2021,12 @@ impl<'a, D: TextDimension<'a> + Ord, F: FnMut(&FragmentSummary) -> bool> Iterato
             }
 
             if !fragment.was_visible(&self.since, &self.undos) && fragment.visible {
-                let fragment_summary = self.visible_cursor.summary(cursor.end(&None).visible);
+                let visible_end = cmp::min(
+                    cursor.end(&None).visible,
+                    cursor.start().visible + (self.range.end - cursor.start().full_offset()),
+                );
+
+                let fragment_summary = self.visible_cursor.summary(visible_end);
                 let mut new_end = self.new_end.clone();
                 new_end.add_assign(&fragment_summary);
                 if let Some(pending_edit) = pending_edit.as_mut() {
@@ -1985,8 +2040,15 @@ impl<'a, D: TextDimension<'a> + Ord, F: FnMut(&FragmentSummary) -> bool> Iterato
 
                 self.new_end = new_end;
             } else if fragment.was_visible(&self.since, &self.undos) && !fragment.visible {
-                self.deleted_cursor.seek_forward(cursor.start().deleted);
-                let fragment_summary = self.deleted_cursor.summary(cursor.end(&None).deleted);
+                let deleted_end = cmp::min(
+                    cursor.end(&None).deleted,
+                    cursor.start().deleted + (self.range.end - cursor.start().full_offset()),
+                );
+
+                if cursor.start().deleted > self.deleted_cursor.offset() {
+                    self.deleted_cursor.seek_forward(cursor.start().deleted);
+                }
+                let fragment_summary = self.deleted_cursor.summary(deleted_end);
                 let mut old_end = self.old_end.clone();
                 old_end.add_assign(&fragment_summary);
                 if let Some(pending_edit) = pending_edit.as_mut() {
@@ -2251,6 +2313,28 @@ impl ToOffset for Anchor {
     fn to_offset<'a>(&self, content: &Snapshot) -> usize {
         content.summary_for_anchor(self)
     }
+
+    fn to_full_offset<'a>(&self, content: &Snapshot, bias: Bias) -> FullOffset {
+        if content.version == self.version {
+            self.full_offset
+        } else {
+            let mut cursor = content
+                .fragments
+                .cursor::<(VersionedFullOffset, FragmentTextSummary)>();
+            cursor.seek(
+                &VersionedFullOffset::Offset(self.full_offset),
+                bias,
+                &Some(self.version.clone()),
+            );
+
+            let mut full_offset = cursor.start().1.full_offset().0;
+            if cursor.item().is_some() {
+                full_offset += self.full_offset - cursor.start().0.full_offset();
+            }
+
+            FullOffset(full_offset)
+        }
+    }
 }
 
 impl<'a> ToOffset for &'a Anchor {