get randomized test passing for many iterations 🎉

Cole Miller and cameron created

Co-authored-by: cameron <cameron.studdstreet@gmail.com>

Change summary

crates/editor/src/display_map/filter_map.rs | 239 ++++++++++++++++++----
crates/multi_buffer/src/multi_buffer.rs     |   2 
2 files changed, 198 insertions(+), 43 deletions(-)

Detailed changes

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

@@ -3,6 +3,7 @@ use std::{cmp, ops::Range};
 use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
 use multi_buffer::{AnchorRangeExt as _, MultiBufferSnapshot};
 use rope::{Point, TextSummary};
+use schemars::transform;
 use sum_tree::{Dimensions, SumTree};
 use text::Bias;
 use util::debug_panic;
@@ -102,6 +103,15 @@ struct FilterSnapshot {
 #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
 struct FilterOffset(usize);
 
+impl FilterOffset {
+    /// Convert a range of offsets to a range of [`FilterOffset`]s, given that
+    /// there are no filtered lines in the buffer (for example, if no
+    /// [`FilterMode`] is set).
+    pub fn naive_range(Range { start, end }: Range<usize>) -> Range<Self> {
+        FilterOffset(start)..FilterOffset(end)
+    }
+}
+
 impl sum_tree::Dimension<'_, TransformSummary> for FilterOffset {
     fn zero(cx: <TransformSummary as sum_tree::Summary>::Context<'_>) -> Self {
         FilterOffset(0)
@@ -172,7 +182,10 @@ impl FilterMap {
     fn check_invariants(&self) {
         use itertools::Itertools;
 
-        assert_eq!(
+        #[cfg(any(test, feature = "test-support"))]
+        log::info!("filter map output text:\n{}", self.snapshot.text());
+
+        pretty_assertions::assert_eq!(
             self.snapshot.transforms.summary().input.0,
             self.snapshot.buffer_snapshot.text_summary(),
             "input summary does not match buffer snapshot"
@@ -194,12 +207,12 @@ impl FilterMap {
             });
 
         let Some(mode) = self.mode else {
-            assert_eq!(
+            pretty_assertions::assert_eq!(
                 self.snapshot.transforms.iter().count(),
                 1,
                 "more than one transform in a trivial map"
             );
-            assert_eq!(
+            pretty_assertions::assert_eq!(
                 self.snapshot.transforms.summary().output.0,
                 self.snapshot.buffer_snapshot.text_summary(),
                 "output summary for trivial map does not match buffer snapshot"
@@ -227,7 +240,7 @@ impl FilterMap {
             .buffer_snapshot
             .text_summary_for_range::<TextSummary, _>(anchor..multi_buffer::Anchor::max());
 
-        assert_eq!(
+        pretty_assertions::assert_eq!(
             self.snapshot.transforms.summary().output.0,
             expected_summary,
             "wrong output summary for nontrivial map"
@@ -242,6 +255,8 @@ impl FilterMap {
         buffer_edits: Vec<text::Edit<usize>>,
     ) -> (FilterSnapshot, Vec<FilterEdit>) {
         let Some(mode) = self.mode else {
+            // If we're not filtering out anything, edits can be passed through
+            // unchanged and we only need one isomorphic transform.
             self.snapshot.buffer_snapshot = buffer_snapshot.clone();
             self.snapshot.transforms = SumTree::from_item(
                 Transform::Isomorphic {
@@ -254,15 +269,17 @@ impl FilterMap {
                 buffer_edits
                     .into_iter()
                     .map(|edit| text::Edit {
-                        old: FilterOffset(edit.old.start)..FilterOffset(edit.old.end),
-                        new: FilterOffset(edit.new.start)..FilterOffset(edit.new.end),
+                        old: FilterOffset::naive_range(edit.old),
+                        new: FilterOffset::naive_range(edit.new),
                     })
                     .collect(),
             );
         };
 
-        let mut new_transforms = SumTree::new(());
-        let mut cursor = self
+        dbg!(&buffer_edits);
+
+        let mut new_transforms: SumTree<Transform> = SumTree::new(());
+        let mut transform_cursor = self
             .snapshot
             .transforms
             .cursor::<Dimensions<usize, FilterOffset>>(());
@@ -271,32 +288,104 @@ impl FilterMap {
         // TODO in what follows we repeatedly call text_summary_for_range,
         // could use a persistent usize cursor over buffer_snapshot instead.
 
-        for buffer_edit in buffer_edits {
+        transform_cursor.next();
+
+        // for e1 in &buffer_edits {
+        //     for e2 in &buffer_edits {
+        //         assert!(!e1.old.overlaps(e2.old));
+        //     }
+        // }
+        //
+        // Edit { old: 3..6, new: 10..20 }    -> Edit { old: 10..20, new: something_else }
+        // break at old = 4
+        // old = 3..4, 4..6
+        // new = 10..20, 10..20
+        // new = 10..20, 20..20
+        // new = ??????
+
+        // convert vec<edit> to vecdeque<edit>
+        // iterate through edits
+        // check if an edit crosses a transform boundary
+        // if it does, truncate, and push the other half to the front of the queue
+
+        // |--------------------------------------| len = 216           self.snapshot.transforms (cursor) SumTree<FilterTransform>
+        //    <-----> <--------------> <------> <-------> <---->
+        //    <-----> <--------------> <------> <><-----> <---->
+        //    <-->
+        //
+        //    |
+        //            |
+        //                             |
+        //
+        // |---------| |-----------| |------------|                      new_transforms: SumTree<FilterTransform>
+        //
+        // first iteration should give us new transforms like:
+        //
+        //
+
+        let mut buffer_edits = buffer_edits.into_iter().peekable();
+
+        while let Some(buffer_edit) = buffer_edits.next() {
+            debug_assert!(transform_cursor.start().0 <= buffer_edit.old.end);
+
+            dbg!(
+                transform_cursor.start(),
+                buffer_edit.old.end,
+                transform_cursor.end()
+            );
             // Reuse any old transforms that strictly precede the start of the edit.
-            new_transforms.append(cursor.slice(&buffer_edit.old.end, Bias::Right), ());
+            log::info!(
+                "input len before append is {}",
+                new_transforms.summary().input.0.len
+            );
+            new_transforms.append(transform_cursor.slice(&buffer_edit.old.end, Bias::Left), ());
+            log::info!(
+                "input len after append is {}",
+                new_transforms.summary().input.0.len
+            );
+            dbg!(
+                transform_cursor.start(),
+                buffer_edit.old.end,
+                transform_cursor.end()
+            );
 
-            let mut edit_old_start = cursor.start().1;
+            let mut edit_old_start = transform_cursor.start().1;
             let mut edit_new_start = FilterOffset(new_transforms.summary().output.0.len);
 
             // If the edit starts in the middle of a transform, split the transform and push the unaffected portion.
-            if buffer_edit.old.start > cursor.start().0 {
-                let summary = self
-                    .snapshot
-                    .buffer_snapshot
-                    .text_summary_for_range(cursor.start().0..buffer_edit.old.start);
-                match cursor.item() {
+            if buffer_edit.new.start > new_transforms.summary().input.0.len {
+                let summary = buffer_snapshot.text_summary_for_range(
+                    new_transforms.summary().input.0.len..buffer_edit.new.start,
+                );
+                match transform_cursor.item() {
                     Some(Transform::Isomorphic { .. }) => {
                         push_isomorphic(&mut new_transforms, summary);
                         edit_old_start.0 += summary.len;
                         edit_new_start.0 += summary.len;
-                        cursor.next();
                     }
                     Some(Transform::Filter { .. }) => {
                         push_filter(&mut new_transforms, summary);
-                        cursor.next();
                     }
                     None => {}
                 }
+                // let summary = self
+                //     .snapshot
+                //     .buffer_snapshot
+                //     .text_summary_for_range(transform_cursor.start().0..buffer_edit.old.start);
+                // match transform_cursor.item() {
+                //     Some(Transform::Isomorphic { .. }) => {
+                //         push_isomorphic(&mut new_transforms, summary);
+                //         edit_old_start.0 += summary.len;
+                //         edit_new_start.0 += summary.len;
+                //         // transform_cursor.next();
+                //         dbg!(transform_cursor.start(), transform_cursor.end());
+                //     }
+                //     Some(Transform::Filter { .. }) => {
+                //         push_filter(&mut new_transforms, summary);
+                //         // transform_cursor.next();
+                //     }
+                //     None => {}
+                // }
             }
 
             // For each hunk in the edit, push the non-hunk region preceding it, then
@@ -329,32 +418,50 @@ impl FilterMap {
                 );
             }
 
-            // Set up the cursor for the next iteration by seeking it to the end of the edit
-            // and pushing the second half of any transform that's split thereby (we already
-            // covered the first half just above).
-            cursor.seek(&buffer_edit.old.end, Bias::Right);
-            let mut edit_old_end = cursor.end().1;
-            let mut edit_new_end = FilterOffset(new_transforms.summary().output.0.len);
-            if buffer_edit.old.end > cursor.start().0 {
-                let summary = self
-                    .snapshot
-                    .buffer_snapshot
-                    .text_summary_for_range(buffer_edit.old.end..cursor.end().0);
-                match cursor.item() {
+            //           transforms_cursor.start()
+            //           v
+            //           |----------------------------| old transforms
+            // -------------->    <------------->       edits
+            //               ^ buffer_edit.old.end
+
+            transform_cursor.seek(&buffer_edit.old.end, Bias::Right);
+            let mut edit_old_end = transform_cursor.end().1;
+            let edit_new_end = FilterOffset(new_transforms.summary().output.0.len);
+            if buffer_edit.old.end > transform_cursor.start().0 {
+                // let summary = self
+                //     .snapshot
+                //     .buffer_snapshot
+                //     .text_summary_for_range(buffer_edit.old.end..transform_cursor.end().0);
+                match transform_cursor.item() {
                     Some(Transform::Isomorphic { .. }) => {
-                        push_isomorphic(&mut new_transforms, summary);
-                        edit_old_end.0 += buffer_edit.old.end - cursor.start().0;
-                        edit_new_end.0 += buffer_edit.old.end - cursor.start().0;
-                        cursor.next();
+                        // push_isomorphic(&mut new_transforms, summary);
+                        edit_old_end.0 += buffer_edit.old.end - transform_cursor.start().0;
+                        // edit_new_end.0 += buffer_edit.old.end - transform_cursor.start().0;
+                        // transform_cursor.next();
                     }
                     Some(Transform::Filter { .. }) => {
-                        push_filter(&mut new_transforms, summary);
-                        cursor.next();
+                        // push_filter(&mut new_transforms, summary);
+                        // transform_cursor.next();
                     }
                     None => {}
                 }
             }
 
+            // If this is the last edit that intersects the current transform, consume the remainder of the transform and advance.
+            if buffer_edits.peek().is_none_or(|next_buffer_edit| {
+                next_buffer_edit.old.start >= transform_cursor.end().0
+            }) {
+                let suffix_start = new_transforms.summary().input.0.len;
+                let suffix_len = transform_cursor.end().0 - buffer_edit.old.end;
+                dbg!(suffix_start, suffix_start + suffix_len);
+                dbg!(buffer_snapshot.text());
+                let summary = buffer_snapshot.text_summary_for_range(
+                    suffix_start..std::cmp::min(suffix_start + suffix_len, buffer_snapshot.len()),
+                );
+                push_isomorphic(&mut new_transforms, summary);
+                transform_cursor.next();
+            }
+
             output_edits.push(text::Edit {
                 old: edit_old_start..edit_old_end,
                 new: edit_new_start..edit_new_end,
@@ -362,9 +469,20 @@ impl FilterMap {
         }
 
         // Append old transforms after the last edit.
-        new_transforms.append(cursor.slice(&usize::MAX, Bias::Left), ());
 
-        drop(cursor);
+        log::info!(
+            "input len before suffix is {}",
+            new_transforms.summary().input.0.len
+        );
+        let suffix = transform_cursor.suffix();
+        log::info!("suffix summary is {:?}", suffix.summary());
+        new_transforms.append(suffix, ());
+        log::info!(
+            "input len after suffix is {}",
+            new_transforms.summary().input.0.len
+        );
+
+        drop(transform_cursor);
 
         self.snapshot.transforms = new_transforms;
         self.snapshot.buffer_snapshot = buffer_snapshot;
@@ -374,7 +492,41 @@ impl FilterMap {
     }
 }
 
+impl FilterSnapshot {
+    #[cfg(any(test, feature = "test-support"))]
+    fn text(&self) -> String {
+        let mut offset = 0;
+        let mut output = String::new();
+
+        for transform in self.transforms.iter() {
+            match transform {
+                Transform::Isomorphic { summary } => {
+                    let strs = self.buffer_snapshot.text_for_range(
+                        offset.min(self.buffer_snapshot.len())
+                            ..(offset + summary.0.len).min(self.buffer_snapshot.len()),
+                    );
+
+                    for s in strs {
+                        output.push_str(s);
+                    }
+
+                    offset += summary.0.len;
+                }
+                Transform::Filter { summary } => {
+                    offset += summary.0.len;
+                }
+            }
+        }
+
+        output
+    }
+}
+
 fn push_isomorphic(transforms: &mut SumTree<Transform>, summary_to_add: TextSummary) {
+    log::info!(
+        "push_isomorphic, input len after push is {}",
+        transforms.summary().input.0.len + summary_to_add.len
+    );
     let mut merged = false;
     transforms.update_last(
         |transform| {
@@ -396,6 +548,10 @@ fn push_isomorphic(transforms: &mut SumTree<Transform>, summary_to_add: TextSumm
 }
 
 fn push_filter(transforms: &mut SumTree<Transform>, summary_to_add: TextSummary) {
+    log::info!(
+        "push_filter, input len after push is {}",
+        transforms.summary().input.0.len + summary_to_add.len
+    );
     let mut merged = false;
     transforms.update_last(
         |transform| {
@@ -530,11 +686,10 @@ mod tests {
                 rng.clone(),
                 cx,
             );
-            cx.run_until_parked();
-            let buffer_edits = subscription.consume();
             let buffer_snapshot =
                 multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
-            filter_map.sync(buffer_snapshot, buffer_edits.edits().to_owned());
+            let buffer_edits = subscription.consume().into_inner();
+            filter_map.sync(buffer_snapshot, buffer_edits);
         }
     }
 }

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -7192,8 +7192,8 @@ pub fn randomly_mutate_multibuffer_with_diffs(
             let buffer = buffers.choose(&mut rng).unwrap();
             buffer.update(cx, |buf, cx| {
                 let edit_count = rng.random_range(1..5);
+                log::info!("editing buffer");
                 buf.randomly_edit(&mut rng, edit_count, cx);
-                log::info!("buffer text:\n{}", buf.text());
                 *needs_diff_calculation = true;
             });
         }