From 73853be484ebfbed623f8817fc51f632fe4b5be0 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Fri, 13 Feb 2026 10:52:04 -0500 Subject: [PATCH] git: Remove unnecessary block map recomputation when splitting `SplittableEditor` (#49075) We had a call to `BlockMap::unfold_intersecting` that ended up recomputing the entire block map for the RHS _without_ spacers, only to throw it away in favor of the version with spacers a few lines down. Now we only sync each block map once in `set_companion`. Release Notes: - Improved performance when toggling from the unified diff to the split diff. Co-authored-by: Jakub Co-authored-by: Cameron McLoughlin --- 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(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index ef44f34d21ada27896e8dab99f11fd6eb1255175..6234466445aa7e2bf700d7b9a92ea90935601260 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -504,14 +504,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() diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index b1abe6b39b2a4701bdbd99d51ad208b184b57ca3..f0547332d00d60d3c5ccc4407add977a659c6ab6 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/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, @@ -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) -> 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, diff --git a/crates/editor/src/split.rs b/crates/editor/src/split.rs index 28d38a8a7cc908b00d4d0744dd00724a7c6af540..9f7a83c7b2e4d545cdf0f6f6d6f8f56ae1d7aae3 100644 --- a/crates/editor/src/split.rs +++ b/crates/editor/src/split.rs @@ -2090,9 +2090,12 @@ mod tests { use workspace::MultiWorkspace; 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, @@ -5453,6 +5456,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, ¤t_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;