Add randomized test for `DisplayMap::buffer_rows` and fix logic errors

Antonio Scandurra created

Change summary

zed/src/editor/display_map.rs          | 66 +++++++++++++++++++++++++++
zed/src/editor/display_map/wrap_map.rs | 31 +++++++-----
zed/src/util.rs                        |  6 ++
3 files changed, 87 insertions(+), 16 deletions(-)

Detailed changes

zed/src/editor/display_map.rs 🔗

@@ -272,9 +272,73 @@ mod tests {
         language::{Language, LanguageConfig},
         settings::Theme,
         test::*,
+        util::RandomCharIter,
     };
     use buffer::History;
-    use std::sync::Arc;
+    use rand::prelude::*;
+    use std::{env, sync::Arc};
+
+    #[gpui::test]
+    async fn test_random(mut cx: gpui::TestAppContext) {
+        cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
+        let iterations = env::var("ITERATIONS")
+            .map(|i| i.parse().expect("invalid `ITERATIONS` variable"))
+            .unwrap_or(100);
+        let operations = env::var("OPERATIONS")
+            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+            .unwrap_or(10);
+        let seed_range = if let Ok(seed) = env::var("SEED") {
+            let seed = seed.parse().expect("invalid `SEED` variable");
+            seed..seed + 1
+        } else {
+            0..iterations
+        };
+        let font_cache = cx.font_cache();
+
+        for seed in seed_range {
+            dbg!(seed);
+            let mut rng = StdRng::seed_from_u64(seed);
+            let settings = Settings {
+                buffer_font_family: font_cache.load_family(&["Helvetica"]).unwrap(),
+                ui_font_family: font_cache.load_family(&["Helvetica"]).unwrap(),
+                buffer_font_size: 12.0,
+                ui_font_size: 12.0,
+                tab_size: rng.gen_range(1..=4),
+                theme: Arc::new(Theme::default()),
+            };
+
+            let buffer = cx.add_model(|cx| {
+                let len = rng.gen_range(0..10);
+                let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
+                log::info!("Initial buffer text: {:?}", text);
+                Buffer::new(0, text, cx)
+            });
+            let wrap_width = Some(rng.gen_range(20.0..=100.0));
+            let map = cx.read(|cx| DisplayMap::new(buffer.clone(), settings, wrap_width, cx));
+
+            for _op_ix in 0..operations {
+                buffer.update(&mut cx, |buffer, cx| buffer.randomly_mutate(&mut rng, cx));
+                let snapshot = cx.read(|cx| map.snapshot(cx));
+                let expected_buffer_rows = (0..=snapshot.max_point().row())
+                    .map(|display_row| {
+                        DisplayPoint::new(display_row, 0)
+                            .to_buffer_point(&snapshot, Bias::Left)
+                            .row
+                    })
+                    .collect::<Vec<_>>();
+                for start_display_row in 0..expected_buffer_rows.len() {
+                    assert_eq!(
+                        snapshot
+                            .buffer_rows(start_display_row as u32)
+                            .collect::<Vec<_>>(),
+                        &expected_buffer_rows[start_display_row..],
+                        "invalid buffer_rows({}..)",
+                        start_display_row
+                    );
+                }
+            }
+        }
+    }
 
     #[gpui::test]
     async fn test_soft_wraps(mut cx: gpui::TestAppContext) {

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

@@ -167,14 +167,15 @@ impl Snapshot {
     pub fn buffer_rows(&self, start_row: u32) -> BufferRows {
         let mut transforms = self.transforms.cursor::<OutputPoint, InputPoint>();
         transforms.seek(&OutputPoint::new(start_row, 0), Bias::Right, &());
-        let input_row = transforms.sum_start().row();
-        let mut input_buffer_rows = self.input.buffer_rows(start_row);
+        let input_row = transforms.sum_start().row() + (start_row - transforms.seek_start().row());
+        let mut input_buffer_rows = self.input.buffer_rows(input_row);
         let input_buffer_row = input_buffer_rows.next().unwrap();
         BufferRows {
             transforms,
-            input_row,
             input_buffer_row,
             input_buffer_rows,
+            output_row: start_row,
+            max_output_row: self.max_point().row(),
         }
     }
 
@@ -212,8 +213,9 @@ pub struct HighlightedChunks<'a> {
 
 pub struct BufferRows<'a> {
     input_buffer_rows: fold_map::BufferRows<'a>,
-    input_row: u32,
     input_buffer_row: u32,
+    output_row: u32,
+    max_output_row: u32,
     transforms: Cursor<'a, Transform, OutputPoint, InputPoint>,
 }
 
@@ -299,18 +301,19 @@ impl<'a> Iterator for BufferRows<'a> {
     type Item = u32;
 
     fn next(&mut self) -> Option<Self::Item> {
-        let result = self.input_buffer_row;
-        if self.input_row + 1 < self.transforms.sum_end(&()).row() {
-            self.input_row += 1;
+        if self.output_row > self.max_output_row {
+            return None;
+        }
+
+        let buffer_row = self.input_buffer_row;
+        self.output_row += 1;
+        self.transforms
+            .seek_forward(&OutputPoint::new(self.output_row, 0), Bias::Left, &());
+        if self.transforms.item().map_or(false, |t| t.is_isomorphic()) {
             self.input_buffer_row = self.input_buffer_rows.next().unwrap();
-        } else {
-            self.transforms.seek_forward(
-                &OutputPoint::new(self.transforms.seek_start().row() + 1, 0),
-                Bias::Right,
-                &(),
-            );
         }
-        Some(result)
+
+        Some(buffer_row)
     }
 }
 

zed/src/util.rs 🔗

@@ -67,7 +67,11 @@ impl<T: Rng> Iterator for RandomCharIter<T> {
 
     fn next(&mut self) -> Option<Self::Item> {
         if self.0.gen_bool(1.0 / 5.0) {
-            Some('\n')
+            match self.0.gen_range(0..=2) {
+                0 => Some('\t'),
+                1 => Some(' '),
+                _ => Some('\n'),
+            }
         }
         // two-byte greek letters
         else if self.0.gen_bool(1.0 / 8.0) {