Cherry-pick "git: Remove unnecessary block map recomputation when splitting `SplittableEditor` (#49075)" to v0.224.x (#49127)

Cole Miller , Jakub , and Cameron McLoughlin created

Release Notes:

- Improved performance when toggling from the unified diff to the split
diff.

Co-authored-by: Jakub <jakub@zed.dev>
Co-authored-by: Cameron McLoughlin <cameron.studdstreet@gmail.com>

Change summary

crates/editor/src/display_map.rs           | 27 ++++++
crates/editor/src/display_map/block_map.rs | 15 +++
crates/editor/src/split.rs                 | 94 +++++++++++++++++++++++
3 files changed, 133 insertions(+), 3 deletions(-)

Detailed changes

crates/editor/src/display_map.rs 🔗

@@ -503,14 +503,37 @@ impl DisplayMap {
         };
         assert_eq!(self.entity_id, companion.read(cx).rhs_display_map_id);
 
-        let snapshot = self.unfold_intersecting([Anchor::min()..Anchor::max()], true, cx);
+        // Note, throwing away the wrap edits because we're going to recompute the maximal range for the block map regardless
+        let old_max_row = self.block_map.wrap_snapshot.borrow().max_point().row();
+        let snapshot = {
+            let edits = self.buffer_subscription.consume();
+            let snapshot = self.buffer.read(cx).snapshot(cx);
+            let tab_size = Self::tab_size(&self.buffer, cx);
+            let (snapshot, edits) = self.inlay_map.sync(snapshot, edits.into_inner());
+            let (mut writer, snapshot, edits) = self.fold_map.write(snapshot, edits);
+            let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+            let (_snapshot, _edits) = self
+                .wrap_map
+                .update(cx, |wrap_map, cx| wrap_map.sync(snapshot, edits, cx));
+
+            let (snapshot, edits) =
+                writer.unfold_intersecting([Anchor::min()..Anchor::max()], true);
+            let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+            let (snapshot, _edits) = self
+                .wrap_map
+                .update(cx, |wrap_map, cx| wrap_map.sync(snapshot, edits, cx));
+
+            self.block_map
+                .retain_blocks_raw(|block| !matches!(block.placement, BlockPlacement::Replace(_)));
+            snapshot
+        };
 
         let (companion_wrap_snapshot, companion_wrap_edits) =
             companion_display_map.update(cx, |dm, cx| dm.sync_through_wrap(cx));
 
         let edits = Patch::new(
             [text::Edit {
-                old: WrapRow(0)..snapshot.max_point().row(),
+                old: WrapRow(0)..old_max_row,
                 new: WrapRow(0)..snapshot.max_point().row(),
             }]
             .into_iter()

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

@@ -707,6 +707,7 @@ impl BlockMap {
         }
     }
 
+    // Warning: doesn't sync the block map, use advisedly
     pub(crate) fn insert_block_raw(
         &mut self,
         block: BlockProperties<Anchor>,
@@ -732,6 +733,20 @@ impl BlockMap {
         id
     }
 
+    // Warning: doesn't sync the block map, use advisedly
+    pub(crate) fn retain_blocks_raw(&mut self, mut pred: impl FnMut(&Arc<CustomBlock>) -> bool) {
+        let mut ids_to_remove = HashSet::default();
+        self.custom_blocks.retain(|block| {
+            let keep = pred(block);
+            if !keep {
+                ids_to_remove.insert(block.id);
+            }
+            keep
+        });
+        self.custom_blocks_by_id
+            .retain(|id, _| !ids_to_remove.contains(id));
+    }
+
     #[ztracing::instrument(skip_all, fields(edits = ?edits))]
     fn sync(
         &self,

crates/editor/src/split.rs 🔗

@@ -2090,9 +2090,12 @@ mod tests {
     use workspace::Workspace;
 
     use crate::SplittableEditor;
-    use crate::display_map::{BlockPlacement, BlockProperties, BlockStyle};
+    use crate::display_map::{
+        BlockPlacement, BlockProperties, BlockStyle, Crease, FoldPlaceholder,
+    };
     use crate::inlays::Inlay;
     use crate::test::{editor_content_with_blocks_and_width, set_block_content_for_tests};
+    use multi_buffer::MultiBufferOffset;
 
     async fn init_test(
         cx: &mut gpui::TestAppContext,
@@ -5452,6 +5455,95 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_range_folds_removed_on_split(cx: &mut gpui::TestAppContext) {
+        use rope::Point;
+        use unindent::Unindent as _;
+
+        let (editor, mut cx) = init_test(cx, SoftWrap::None, DiffViewStyle::Unified).await;
+
+        let base_text = "
+            aaa
+            bbb
+            ccc
+            ddd
+            eee"
+        .unindent();
+        let current_text = "
+            aaa
+            bbb
+            ccc
+            ddd
+            eee"
+        .unindent();
+
+        let (buffer, diff) = buffer_with_diff(&base_text, &current_text, &mut cx);
+
+        editor.update(cx, |editor, cx| {
+            let path = PathKey::for_buffer(&buffer, cx);
+            editor.set_excerpts_for_path(
+                path,
+                buffer.clone(),
+                vec![Point::new(0, 0)..buffer.read(cx).max_point()],
+                0,
+                diff.clone(),
+                cx,
+            );
+        });
+
+        cx.run_until_parked();
+
+        editor.update_in(cx, |editor, window, cx| {
+            editor.rhs_editor.update(cx, |rhs_editor, cx| {
+                rhs_editor.fold_creases(
+                    vec![Crease::simple(
+                        Point::new(1, 0)..Point::new(3, 0),
+                        FoldPlaceholder::test(),
+                    )],
+                    false,
+                    window,
+                    cx,
+                );
+            });
+        });
+
+        cx.run_until_parked();
+
+        editor.update_in(cx, |editor, window, cx| {
+            editor.split(window, cx);
+        });
+
+        cx.run_until_parked();
+
+        let (rhs_editor, lhs_editor) = editor.read_with(cx, |editor, _cx| {
+            (
+                editor.rhs_editor.clone(),
+                editor.lhs.as_ref().unwrap().editor.clone(),
+            )
+        });
+
+        let rhs_has_folds_after_split = rhs_editor.update(cx, |editor, cx| {
+            let snapshot = editor.display_snapshot(cx);
+            snapshot
+                .folds_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
+                .next()
+                .is_some()
+        });
+        assert!(
+            !rhs_has_folds_after_split,
+            "rhs should not have range folds after split"
+        );
+
+        let lhs_has_folds = lhs_editor.update(cx, |editor, cx| {
+            let snapshot = editor.display_snapshot(cx);
+            snapshot
+                .folds_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
+                .next()
+                .is_some()
+        });
+        assert!(!lhs_has_folds, "lhs should not have any range folds");
+    }
+
     #[gpui::test]
     async fn test_multiline_inlays_create_spacers(cx: &mut gpui::TestAppContext) {
         use rope::Point;