Start on a randomized test for `BlockMap`

Antonio Scandurra and Max Brunsfeld created

This is currently passing and ensures we maintain the input coordinate
space correctly.

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

Change summary

crates/editor/src/display_map.rs           |  3 
crates/editor/src/display_map/block_map.rs | 83 ++++++++++++++++++++++-
crates/editor/src/display_map/patch.rs     | 15 +--
crates/editor/src/display_map/wrap_map.rs  | 63 +++++++++--------
4 files changed, 117 insertions(+), 47 deletions(-)

Detailed changes

crates/editor/src/display_map.rs 🔗

@@ -39,8 +39,7 @@ impl DisplayMap {
     ) -> Self {
         let (fold_map, snapshot) = FoldMap::new(buffer.clone(), cx);
         let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
-        let wrap_map =
-            cx.add_model(|cx| WrapMap::new(snapshot, font_id, font_size, wrap_width, cx));
+        let (wrap_map, _) = WrapMap::new(snapshot, font_id, font_size, wrap_width, cx);
         cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
         DisplayMap {
             buffer,

crates/editor/src/display_map/block_map.rs 🔗

@@ -11,7 +11,9 @@ struct BlockMap {
 
 struct BlockMapWriter<'a>(&'a mut BlockMap);
 
-struct BlockSnapshot {}
+struct BlockSnapshot {
+    transforms: SumTree<Transform>,
+}
 
 #[derive(Clone)]
 struct Transform {
@@ -42,7 +44,9 @@ impl BlockMap {
 
     fn read(&self, wrap_snapshot: WrapSnapshot, edits: Vec<WrapEdit>) -> BlockSnapshot {
         self.sync(wrap_snapshot, edits);
-        BlockSnapshot {}
+        BlockSnapshot {
+            transforms: self.transforms.lock().clone(),
+        }
     }
 
     fn write(&mut self, wrap_snapshot: WrapSnapshot, edits: Vec<WrapEdit>) -> BlockMapWriter {
@@ -51,7 +55,7 @@ impl BlockMap {
     }
 
     fn sync(&self, wrap_snapshot: WrapSnapshot, edits: Vec<WrapEdit>) {
-        let transforms = self.transforms.lock();
+        let mut transforms = self.transforms.lock();
         let mut new_transforms = SumTree::new();
         let mut cursor = transforms.cursor::<InputRow>();
         let mut edits = edits.into_iter().peekable();
@@ -76,8 +80,9 @@ impl BlockMap {
 
                 if let Some(next_edit) = edits.peek() {
                     if edit.old.end >= next_edit.old.start {
+                        let delta = next_edit.new.len() as i32 - next_edit.old.len() as i32;
                         edit.old.end = cmp::max(next_edit.old.end, edit.old.end);
-                        edit.new.end += (edit.new.len() as i32 - edit.old.len() as i32) as u32;
+                        edit.new.end = (edit.new.end as i32 + delta) as u32;
                         edits.next();
                     } else {
                         break;
@@ -98,6 +103,8 @@ impl BlockMap {
             }
         }
         new_transforms.push_tree(cursor.suffix(&()), &());
+        drop(cursor);
+        *transforms = new_transforms;
     }
 }
 
@@ -140,3 +147,71 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for OutputRow {
         self.0 += summary.output_rows;
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::BlockMap;
+    use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
+    use buffer::RandomCharIter;
+    use language::Buffer;
+    use rand::prelude::*;
+    use std::env;
+
+    #[gpui::test(iterations = 100)]
+    fn test_random(cx: &mut gpui::MutableAppContext, mut rng: StdRng) {
+        let operations = env::var("OPERATIONS")
+            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+            .unwrap_or(10);
+
+        let wrap_width = Some(rng.gen_range(0.0..=1000.0));
+        let tab_size = 1;
+        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
+        let font_id = cx
+            .font_cache()
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 14.0;
+
+        log::info!("Tab size: {}", tab_size);
+        log::info!("Wrap width: {:?}", wrap_width);
+
+        let buffer = cx.add_model(|cx| {
+            let len = rng.gen_range(0..10);
+            let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
+            Buffer::new(0, text, cx)
+        });
+        let (fold_map, folds_snapshot) = FoldMap::new(buffer.clone(), cx);
+        let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size);
+        let (wrap_map, wraps_snapshot) =
+            WrapMap::new(tabs_snapshot, font_id, font_size, wrap_width, cx);
+        let block_map = BlockMap::new(wraps_snapshot);
+
+        for _ in 0..operations {
+            match rng.gen_range(0..=100) {
+                0..=19 => {
+                    let wrap_width = if rng.gen_bool(0.2) {
+                        None
+                    } else {
+                        Some(rng.gen_range(0.0..=1000.0))
+                    };
+                    log::info!("Setting wrap width to {:?}", wrap_width);
+                    wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
+                }
+                _ => {
+                    buffer.update(cx, |buffer, _| buffer.randomly_edit(&mut rng, 5));
+                }
+            }
+
+            let (folds_snapshot, fold_edits) = fold_map.read(cx);
+            let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits);
+            let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
+                wrap_map.sync(tabs_snapshot, tab_edits, cx)
+            });
+            let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits);
+            assert_eq!(
+                blocks_snapshot.transforms.summary().input_rows,
+                wraps_snapshot.max_point().row() + 1
+            );
+        }
+    }
+}

crates/editor/src/display_map/patch.rs 🔗

@@ -1,4 +1,4 @@
-use std::{cmp, mem, slice};
+use std::{cmp, mem};
 
 type Edit = buffer::Edit<u32>;
 
@@ -10,6 +10,10 @@ impl Patch {
         Self(edits)
     }
 
+    pub fn into_inner(self) -> Vec<Edit> {
+        self.0
+    }
+
     pub fn compose(&self, other: &Self) -> Self {
         let mut old_edits_iter = self.0.iter().cloned().peekable();
         let mut new_edits_iter = other.0.iter().cloned().peekable();
@@ -167,15 +171,6 @@ impl Patch {
     }
 }
 
-impl<'a> IntoIterator for &'a Patch {
-    type Item = &'a Edit;
-    type IntoIter = slice::Iter<'a, Edit>;
-
-    fn into_iter(self) -> Self::IntoIter {
-        self.0.iter()
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;

crates/editor/src/display_map/wrap_map.rs 🔗

@@ -3,7 +3,10 @@ use super::{
     patch::Patch,
     tab_map::{self, Edit as TabEdit, Snapshot as TabSnapshot, TabPoint, TextSummary},
 };
-use gpui::{fonts::FontId, text_layout::LineWrapper, Entity, ModelContext, Task};
+use gpui::{
+    fonts::FontId, text_layout::LineWrapper, Entity, ModelContext, ModelHandle, MutableAppContext,
+    Task,
+};
 use language::{HighlightedChunk, Point};
 use lazy_static::lazy_static;
 use smol::future::yield_now;
@@ -78,20 +81,24 @@ impl WrapMap {
         font_id: FontId,
         font_size: f32,
         wrap_width: Option<f32>,
-        cx: &mut ModelContext<Self>,
-    ) -> Self {
-        let mut this = Self {
-            font: (font_id, font_size),
-            wrap_width: None,
-            pending_edits: Default::default(),
-            interpolated_edits: Default::default(),
-            edits_since_sync: Default::default(),
-            snapshot: Snapshot::new(tab_snapshot),
-            background_task: None,
-        };
-        this.set_wrap_width(wrap_width, cx);
-        mem::take(&mut this.edits_since_sync);
-        this
+        cx: &mut MutableAppContext,
+    ) -> (ModelHandle<Self>, Snapshot) {
+        let handle = cx.add_model(|cx| {
+            let mut this = Self {
+                font: (font_id, font_size),
+                wrap_width: None,
+                pending_edits: Default::default(),
+                interpolated_edits: Default::default(),
+                edits_since_sync: Default::default(),
+                snapshot: Snapshot::new(tab_snapshot),
+                background_task: None,
+            };
+            this.set_wrap_width(wrap_width, cx);
+            mem::take(&mut this.edits_since_sync);
+            this
+        });
+        let snapshot = handle.read(cx).snapshot.clone();
+        (handle, snapshot)
     }
 
     #[cfg(test)]
@@ -104,10 +111,13 @@ impl WrapMap {
         tab_snapshot: TabSnapshot,
         edits: Vec<TabEdit>,
         cx: &mut ModelContext<Self>,
-    ) -> (Snapshot, Patch) {
+    ) -> (Snapshot, Vec<Edit>) {
         self.pending_edits.push_back((tab_snapshot, edits));
         self.flush_edits(cx);
-        (self.snapshot.clone(), mem::take(&mut self.edits_since_sync))
+        (
+            self.snapshot.clone(),
+            mem::take(&mut self.edits_since_sync).into_inner(),
+        )
     }
 
     pub fn set_font(&mut self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) {
@@ -983,12 +993,6 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for WrapPoint {
     }
 }
 
-fn invert(edits: &mut Vec<Edit>) {
-    for edit in edits {
-        mem::swap(&mut edit.old, &mut edit.new);
-    }
-}
-
 fn consolidate_wrap_edits(edits: &mut Vec<Edit>) {
     let mut i = 1;
     while i < edits.len() {
@@ -1062,17 +1066,14 @@ mod tests {
         let unwrapped_text = tabs_snapshot.text();
         let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
 
-        let wrap_map = cx.add_model(|cx| {
-            WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx)
-        });
+        let (wrap_map, initial_snapshot) =
+            cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx));
         let (_observer, notifications) = Observer::new(&wrap_map, &mut cx);
 
         if wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) {
             notifications.recv().await.unwrap();
         }
 
-        let (initial_snapshot, _) =
-            wrap_map.update(&mut cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx));
         let actual_text = initial_snapshot.text();
         assert_eq!(
             actual_text, expected_text,
@@ -1155,9 +1156,9 @@ mod tests {
         }
 
         let mut initial_text = Rope::from(initial_snapshot.text().as_str());
-        for (snapshot, edits) in edits {
+        for (snapshot, patch) in edits {
             let snapshot_text = Rope::from(snapshot.text().as_str());
-            for edit in &edits {
+            for edit in &patch {
                 let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0));
                 let old_end = initial_text.point_to_offset(cmp::min(
                     Point::new(edit.new.start + edit.old.len() as u32, 0),
@@ -1206,7 +1207,7 @@ mod tests {
     }
 
     impl Snapshot {
-        fn text(&self) -> String {
+        pub fn text(&self) -> String {
             self.chunks_at(0).collect()
         }