Simplify assertions in randomized patch test, fix some patch bugs

Max Brunsfeld created

Change summary

crates/editor/src/display_map/patch.rs | 160 ++++++++++++++++++++-------
1 file changed, 115 insertions(+), 45 deletions(-)

Detailed changes

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

@@ -15,6 +15,8 @@ impl Patch {
         let mut intermediate_end = 0;
 
         for mut new_edit in new.0.iter().cloned() {
+            eprintln!("edit {:?}", new_edit);
+
             let new_edit_delta = new_edit.new.len() as i32 - new_edit.old.len() as i32;
 
             if let Some(last_edit) = composed.last_mut() {
@@ -25,6 +27,8 @@ impl Patch {
                         intermediate_end = new_edit.old.end;
                     }
                     last_edit.new.end = (last_edit.new.end as i32 + new_edit_delta) as u32;
+                    new_delta += new_edit_delta;
+                    eprintln!("  merged {:?}", &composed);
                     continue;
                 }
             }
@@ -44,6 +48,7 @@ impl Patch {
                     new_edit.old.start = (new_edit.old.start as i32 - old_edit_delta) as u32;
                     new_edit.old.end = (new_edit.old.end as i32 - old_edit_delta) as u32;
                     composed.push(old_edit);
+                    eprintln!("  pushed preceding {:?}", &composed);
                 } else if old_edit.new.start <= intermediate_end {
                     if old_edit.new.start < intermediate_start {
                         new_edit.new.start -= intermediate_start - old_edit.new.start;
@@ -54,6 +59,7 @@ impl Patch {
                         new_edit.old.end += old_edit.new.end - intermediate_end;
                         intermediate_end = old_edit.new.end;
                     }
+                    eprintln!("  expanded w/ intersecting {:?} - {:?}", old_edit, new_edit);
                     new_edit.old.end = (new_edit.old.end as i32 - old_edit_delta) as u32;
                 } else {
                     break;
@@ -65,9 +71,25 @@ impl Patch {
 
             new_delta += new_edit_delta;
             composed.push(new_edit);
+            eprintln!("  pushing {:?}", &composed);
         }
 
         while let Some(mut old_edit) = old_edits.next() {
+            let old_edit_delta = old_edit.new.len() as i32 - old_edit.old.len() as i32;
+
+            if let Some(last_edit) = composed.last_mut() {
+                if intermediate_end >= old_edit.new.start {
+                    if old_edit.new.end > intermediate_end {
+                        last_edit.old.end += old_edit.new.end - intermediate_end;
+                        last_edit.new.end += old_edit.new.end - intermediate_end;
+                        intermediate_end = old_edit.new.end;
+                    }
+                    last_edit.old.end = (last_edit.old.end as i32 - old_edit_delta) as u32;
+                    eprintln!("  merged {:?}", &composed);
+                    continue;
+                }
+            }
+
             old_edit.new.start = (old_edit.new.start as i32 + new_delta) as u32;
             old_edit.new.end = (old_edit.new.end as i32 + new_delta) as u32;
             composed.push(old_edit);
@@ -255,71 +277,119 @@ mod tests {
         );
     }
 
-    #[gpui::test(iterations = 1000, seed = 131)]
+    #[gpui::test]
+    fn test_two_new_edits_touching_one_old_edit() {
+        assert_patch_composition(
+            Patch(vec![
+                Edit {
+                    old: 2..3,
+                    new: 2..4,
+                },
+                Edit {
+                    old: 7..7,
+                    new: 8..11,
+                },
+            ]),
+            Patch(vec![
+                Edit {
+                    old: 2..3,
+                    new: 2..2,
+                },
+                Edit {
+                    old: 4..4,
+                    new: 3..4,
+                },
+            ]),
+            Patch(vec![
+                Edit {
+                    old: 2..3,
+                    new: 2..4,
+                },
+                Edit {
+                    old: 7..7,
+                    new: 8..11,
+                },
+            ]),
+        );
+    }
+
+    #[gpui::test(iterations = 1000)]
     fn test_random(mut rng: StdRng) {
         let operations = env::var("OPERATIONS")
             .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
-            .unwrap_or(3);
+            .unwrap_or(2);
 
-        let initial_chars = (0..rng.gen_range(0..=5))
+        let initial_chars = (0..rng.gen_range(0..=10))
             .map(|_| rng.gen_range(b'a'..=b'z') as char)
             .collect::<Vec<_>>();
-        let mut final_chars = initial_chars.clone();
+        println!("initial chars: {:?}", initial_chars);
+
+        // Generate two sequential patches
         let mut patches = Vec::new();
+        let mut expected_chars = initial_chars.clone();
+        for i in 0..2 {
+            println!("patch {}:", i);
+
+            let mut delta = 0i32;
+            let mut last_edit_end = 0;
+            let mut edits = Vec::new();
+            for _ in 0..operations {
+                if last_edit_end >= expected_chars.len() {
+                    break;
+                }
 
-        println!("initial chars: {:?}", initial_chars);
-        for _ in 0..operations {
-            let end = rng.gen_range(0..=final_chars.len());
-            let start = rng.gen_range(0..=end);
-            let mut len = rng.gen_range(0..=3);
-            if start == end && len == 0 {
-                len += 1;
-            }
-            let new_chars = (0..len)
-                .map(|_| rng.gen_range(b'a'..=b'z') as char)
-                .collect::<Vec<_>>();
-            println!(
-                "editing {:?}: {:?}",
-                start..end,
-                new_chars.iter().collect::<String>()
-            );
+                let end = rng.gen_range(last_edit_end..=expected_chars.len());
+                let start = rng.gen_range(last_edit_end..=end);
+                let old_len = end - start;
 
-            let patch = Patch(vec![Edit {
-                old: start as u32..end as u32,
-                new: start as u32..start as u32 + new_chars.len() as u32,
-            }]);
-            if patches.is_empty() || rng.gen() {
-                println!("pushing singleton patch: {:?}", patch.0);
-                patches.push(patch);
-            } else {
-                let patch = patches.pop().unwrap().compose(&patch);
-                println!("composed patches: {:?}", patch.0);
-                patches.push(patch);
+                let mut new_len = rng.gen_range(0..=3);
+                if start == end && new_len == 0 {
+                    new_len += 1;
+                }
+
+                last_edit_end = start + new_len + 1;
+
+                let new_chars = (0..new_len)
+                    .map(|_| rng.gen_range(b'A'..=b'Z') as char)
+                    .collect::<Vec<_>>();
+                println!(
+                    "  editing {:?}: {:?}",
+                    start..end,
+                    new_chars.iter().collect::<String>()
+                );
+                edits.push(Edit {
+                    old: (start as i32 - delta) as u32..(end as i32 - delta) as u32,
+                    new: start as u32..(start + new_len) as u32,
+                });
+                expected_chars.splice(start..end, new_chars);
+
+                delta += new_len as i32 - old_len as i32;
             }
-            final_chars.splice(start..end, new_chars);
+
+            patches.push(Patch(edits));
         }
 
-        println!("final chars: {:?}", final_chars);
-        println!("final patches: {:?}", patches);
+        println!("old patch: {:?}", &patches[0]);
+        println!("new patch: {:?}", &patches[1]);
+        println!("initial chars: {:?}", initial_chars);
+        println!("final chars: {:?}", expected_chars);
 
-        let mut composed = Patch::default();
-        for patch in patches {
-            println!("composing patches {:?} and {:?}", composed, patch);
-            composed = composed.compose(&patch);
-            println!("composed {:?}", composed);
-        }
-        println!("composed edits: {:?}", composed);
-        let mut chars = initial_chars.clone();
+        // Compose the patches, and verify that it has the same effect as applying the
+        // two patches separately.
+        let composed = patches[0].compose(&patches[1]);
+        println!("composed patch: {:?}", &composed);
+
+        let mut actual_chars = initial_chars.clone();
         for edit in composed.0 {
-            chars.splice(
+            actual_chars.splice(
                 edit.new.start as usize..edit.new.start as usize + edit.old.len(),
-                final_chars[edit.new.start as usize..edit.new.end as usize]
+                expected_chars[edit.new.start as usize..edit.new.end as usize]
                     .iter()
                     .copied(),
             );
         }
 
-        assert_eq!(chars, final_chars);
+        assert_eq!(actual_chars, expected_chars);
     }
 
     #[track_caller]