SplittableEditor: Sync custom blocks between RHS and LHS editors (#48575)

Jakub Konka created

Release Notes:

- Added handling of custom blocks in the RHS editor by creating matching
dummy custom blocks rendered as spacer blocks in the LHS editor when in
split view.

Change summary

crates/editor/src/display_map.rs           | 583 +++++++++++++------
crates/editor/src/display_map/block_map.rs | 189 +++++
crates/editor/src/element.rs               |  50 +
crates/editor/src/split.rs                 | 712 +++++++++++++++++++++++
4 files changed, 1,316 insertions(+), 218 deletions(-)

Detailed changes

crates/editor/src/display_map.rs ๐Ÿ”—

@@ -81,8 +81,8 @@ mod wrap_map;
 pub use crate::display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap};
 pub use block_map::{
     Block, BlockChunks as DisplayChunks, BlockContext, BlockId, BlockMap, BlockPlacement,
-    BlockPoint, BlockProperties, BlockRows, BlockStyle, CompanionView, CustomBlockId,
-    EditorMargins, RenderBlock, StickyHeaderExcerpt,
+    BlockPoint, BlockProperties, BlockRows, BlockStyle, CompanionView, CompanionViewMut,
+    CustomBlockId, EditorMargins, RenderBlock, StickyHeaderExcerpt,
 };
 pub use crease_map::*;
 pub use fold_map::{
@@ -238,6 +238,8 @@ pub(crate) struct Companion {
     lhs_excerpt_to_rhs_excerpt: HashMap<ExcerptId, ExcerptId>,
     rhs_rows_to_lhs_rows: ConvertMultiBufferRows,
     lhs_rows_to_rhs_rows: ConvertMultiBufferRows,
+    rhs_custom_blocks_to_lhs_custom_blocks: HashMap<CustomBlockId, CustomBlockId>,
+    lhs_custom_blocks_to_rhs_custom_blocks: HashMap<CustomBlockId, CustomBlockId>,
 }
 
 impl Companion {
@@ -256,9 +258,46 @@ impl Companion {
             lhs_excerpt_to_rhs_excerpt: Default::default(),
             rhs_rows_to_lhs_rows,
             lhs_rows_to_rhs_rows,
+            rhs_custom_blocks_to_lhs_custom_blocks: Default::default(),
+            lhs_custom_blocks_to_rhs_custom_blocks: Default::default(),
         }
     }
 
+    pub(crate) fn is_rhs(&self, display_map_id: EntityId) -> bool {
+        self.rhs_display_map_id == display_map_id
+    }
+
+    pub(crate) fn companion_custom_block_to_custom_block(
+        &self,
+        display_map_id: EntityId,
+    ) -> &HashMap<CustomBlockId, CustomBlockId> {
+        if self.is_rhs(display_map_id) {
+            &self.lhs_custom_blocks_to_rhs_custom_blocks
+        } else {
+            &self.rhs_custom_blocks_to_lhs_custom_blocks
+        }
+    }
+
+    pub(crate) fn add_custom_block_mapping(
+        &mut self,
+        lhs_id: CustomBlockId,
+        rhs_id: CustomBlockId,
+    ) {
+        self.lhs_custom_blocks_to_rhs_custom_blocks
+            .insert(lhs_id, rhs_id);
+        self.rhs_custom_blocks_to_lhs_custom_blocks
+            .insert(rhs_id, lhs_id);
+    }
+
+    pub(crate) fn remove_custom_block_mapping(
+        &mut self,
+        lhs_id: &CustomBlockId,
+        rhs_id: &CustomBlockId,
+    ) {
+        self.lhs_custom_blocks_to_rhs_custom_blocks.remove(lhs_id);
+        self.rhs_custom_blocks_to_lhs_custom_blocks.remove(rhs_id);
+    }
+
     pub(crate) fn convert_rows_to_companion(
         &self,
         display_map_id: EntityId,
@@ -266,7 +305,7 @@ impl Companion {
         our_snapshot: &MultiBufferSnapshot,
         bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>),
     ) -> Vec<CompanionExcerptPatch> {
-        let (excerpt_map, convert_fn) = if display_map_id == self.rhs_display_map_id {
+        let (excerpt_map, convert_fn) = if self.is_rhs(display_map_id) {
             (&self.rhs_excerpt_to_lhs_excerpt, self.rhs_rows_to_lhs_rows)
         } else {
             (&self.lhs_excerpt_to_rhs_excerpt, self.lhs_rows_to_rhs_rows)
@@ -281,7 +320,7 @@ impl Companion {
         companion_snapshot: &MultiBufferSnapshot,
         point: MultiBufferPoint,
     ) -> Range<MultiBufferPoint> {
-        let (excerpt_map, convert_fn) = if display_map_id == self.rhs_display_map_id {
+        let (excerpt_map, convert_fn) = if self.is_rhs(display_map_id) {
             (&self.lhs_excerpt_to_rhs_excerpt, self.lhs_rows_to_rhs_rows)
         } else {
             (&self.rhs_excerpt_to_lhs_excerpt, self.rhs_rows_to_lhs_rows)
@@ -306,7 +345,7 @@ impl Companion {
         &self,
         display_map_id: EntityId,
     ) -> &HashMap<ExcerptId, ExcerptId> {
-        if display_map_id == self.rhs_display_map_id {
+        if self.is_rhs(display_map_id) {
             &self.lhs_excerpt_to_rhs_excerpt
         } else {
             &self.rhs_excerpt_to_lhs_excerpt
@@ -314,7 +353,7 @@ impl Companion {
     }
 
     fn buffer_to_companion_buffer(&self, display_map_id: EntityId) -> &HashMap<BufferId, BufferId> {
-        if display_map_id == self.rhs_display_map_id {
+        if self.is_rhs(display_map_id) {
             &self.rhs_buffer_to_lhs_buffer
         } else {
             &self.lhs_buffer_to_rhs_buffer
@@ -493,20 +532,56 @@ impl DisplayMap {
             .read(snapshot.clone(), edits.clone(), companion_view);
 
         if let Some((companion_dm, _)) = &self.companion {
-            let _ = companion_dm.update(cx, |dm, _cx| {
+            let _ = companion_dm.update(cx, |dm, cx| {
                 if let Some((companion_snapshot, companion_edits)) = companion_wrap_data {
-                    let their_companion_ref = dm.companion.as_ref().map(|(_, c)| c.read(_cx));
+                    let their_companion_ref = dm.companion.as_ref().map(|(_, c)| c);
+
                     dm.block_map.read(
                         companion_snapshot,
                         companion_edits,
-                        their_companion_ref
-                            .map(|c| CompanionView::new(dm.entity_id, &snapshot, &edits, c)),
+                        their_companion_ref.map(|c| {
+                            CompanionView::new(dm.entity_id, &snapshot, &edits, c.read(cx))
+                        }),
                     );
                 }
             });
         }
     }
 
+    pub(crate) fn sync_custom_blocks_into_companion(&mut self, cx: &mut Context<Self>) {
+        if self.companion.is_none() {
+            return;
+        }
+
+        let (self_wrap_snapshot, _) = self.sync_through_wrap(cx);
+        let (companion_dm, companion) = self
+            .companion
+            .as_ref()
+            .expect("companion must exist at this point");
+
+        companion
+            .update(cx, |companion, cx| {
+                companion_dm.update(cx, |dm, cx| {
+                    let (companion_snapshot, _) = dm.sync_through_wrap(cx);
+                    // Sync existing custom blocks to the companion
+                    for block in self
+                        .block_map
+                        .read(self_wrap_snapshot.clone(), Patch::default(), None)
+                        .blocks
+                    {
+                        dm.block_map.insert_custom_block_into_companion(
+                            self.entity_id,
+                            &companion_snapshot,
+                            block,
+                            self_wrap_snapshot.buffer_snapshot(),
+                            companion,
+                        )
+                    }
+                })
+            })
+            .ok();
+    }
+
     pub(crate) fn companion(&self) -> Option<&Entity<Companion>> {
         self.companion.as_ref().map(|(_, c)| c)
     }
@@ -693,52 +768,68 @@ impl DisplayMap {
         let (self_wrap_snapshot, self_wrap_edits) =
             (self_new_wrap_snapshot.clone(), self_new_wrap_edits.clone());
 
-        let mut block_map = {
-            let companion_ref = self.companion.as_ref().map(|(_, c)| c.read(cx));
-            let companion_view = companion_wrap_data.as_ref().zip(companion_ref).map(
-                |((snapshot, edits), companion)| {
-                    CompanionView::new(self.entity_id, snapshot, edits, companion)
-                },
-            );
-            self.block_map
-                .write(self_new_wrap_snapshot, self_new_wrap_edits, companion_view)
-        };
-        let blocks = creases.into_iter().filter_map(|crease| {
-            if let Crease::Block {
-                range,
-                block_height,
-                render_block,
-                block_style,
-                block_priority,
-                ..
-            } = crease
-            {
-                Some((
+        let blocks = creases
+            .into_iter()
+            .filter_map(|crease| {
+                if let Crease::Block {
                     range,
-                    render_block,
                     block_height,
+                    render_block,
                     block_style,
                     block_priority,
-                ))
-            } else {
-                None
-            }
-        });
-        block_map.insert(
-            blocks
-                .into_iter()
-                .map(|(range, render, height, style, priority)| {
-                    let start = buffer_snapshot.anchor_before(range.start);
-                    let end = buffer_snapshot.anchor_after(range.end);
-                    BlockProperties {
-                        placement: BlockPlacement::Replace(start..=end),
-                        render,
-                        height: Some(height),
-                        style,
-                        priority,
-                    }
-                }),
-        );
+                    ..
+                } = crease
+                {
+                    Some((
+                        range,
+                        render_block,
+                        block_height,
+                        block_style,
+                        block_priority,
+                    ))
+                } else {
+                    None
+                }
+            })
+            .map(|(range, render, height, style, priority)| {
+                let start = buffer_snapshot.anchor_before(range.start);
+                let end = buffer_snapshot.anchor_after(range.end);
+                BlockProperties {
+                    placement: BlockPlacement::Replace(start..=end),
+                    render,
+                    height: Some(height),
+                    style,
+                    priority,
+                }
+            });
+
+        if let Some((companion_dm, companion)) = self.companion.as_ref()
+            && let Some((snapshot, edits)) = companion_wrap_data.as_ref()
+        {
+            companion_dm
+                .update(cx, |dm, cx| {
+                    companion.update(cx, |companion, _| {
+                        self.block_map
+                            .write(
+                                self_new_wrap_snapshot,
+                                self_new_wrap_edits,
+                                Some(CompanionViewMut::new(
+                                    self.entity_id,
+                                    snapshot,
+                                    edits,
+                                    companion,
+                                    &mut dm.block_map,
+                                )),
+                            )
+                            .insert(blocks);
+                    })
+                })
+                .ok();
+        } else {
+            self.block_map
+                .write(self_new_wrap_snapshot, self_new_wrap_edits, None)
+                .insert(blocks);
+        };
 
         if let Some((companion_dm, _)) = &self.companion {
             let _ = companion_dm.update(cx, |dm, cx| {
@@ -805,15 +896,29 @@ impl DisplayMap {
         let (self_wrap_snapshot, self_wrap_edits) =
             (self_new_wrap_snapshot.clone(), self_new_wrap_edits.clone());
 
+        if let Some((companion_dm, companion)) = self.companion.as_ref()
+            && let Some((snapshot, edits)) = companion_wrap_data.as_ref()
         {
-            let companion_ref = self.companion.as_ref().map(|(_, c)| c.read(cx));
-            let companion_view = companion_wrap_data.as_ref().zip(companion_ref).map(
-                |((snapshot, edits), companion)| {
-                    CompanionView::new(self.entity_id, snapshot, edits, companion)
-                },
-            );
+            companion_dm
+                .update(cx, |dm, cx| {
+                    companion.update(cx, |companion, _| {
+                        self.block_map.write(
+                            self_new_wrap_snapshot,
+                            self_new_wrap_edits,
+                            Some(CompanionViewMut::new(
+                                self.entity_id,
+                                snapshot,
+                                edits,
+                                companion,
+                                &mut dm.block_map,
+                            )),
+                        );
+                    })
+                })
+                .ok();
+        } else {
             self.block_map
-                .write(self_new_wrap_snapshot, self_new_wrap_edits, companion_view);
+                .write(self_new_wrap_snapshot, self_new_wrap_edits, None);
         }
 
         if let Some((companion_dm, _)) = &self.companion {
@@ -886,20 +991,33 @@ impl DisplayMap {
         let (self_wrap_snapshot, self_wrap_edits) =
             (self_new_wrap_snapshot.clone(), self_new_wrap_edits.clone());
 
-        let mut block_map = {
-            let companion_ref = self.companion.as_ref().map(|(_, c)| c.read(cx));
-            let companion_view = companion_wrap_data.as_ref().zip(companion_ref).map(
-                |((snapshot, edits), companion)| {
-                    CompanionView::new(self.entity_id, snapshot, edits, companion)
-                },
-            );
-            self.block_map.write(
-                self_new_wrap_snapshot.clone(),
-                self_new_wrap_edits,
-                companion_view,
-            )
-        };
-        block_map.remove_intersecting_replace_blocks(offset_ranges, inclusive);
+        if let Some((companion_dm, companion)) = self.companion.as_ref()
+            && let Some((snapshot, edits)) = companion_wrap_data.as_ref()
+        {
+            companion_dm
+                .update(cx, |dm, cx| {
+                    companion.update(cx, |companion, _| {
+                        self.block_map
+                            .write(
+                                self_new_wrap_snapshot.clone(),
+                                self_new_wrap_edits,
+                                Some(CompanionViewMut::new(
+                                    self.entity_id,
+                                    snapshot,
+                                    edits,
+                                    companion,
+                                    &mut dm.block_map,
+                                )),
+                            )
+                            .remove_intersecting_replace_blocks(offset_ranges, inclusive);
+                    })
+                })
+                .ok();
+        } else {
+            self.block_map
+                .write(self_new_wrap_snapshot.clone(), self_new_wrap_edits, None)
+                .remove_intersecting_replace_blocks(offset_ranges, inclusive);
+        }
 
         if let Some((companion_dm, _)) = &self.companion {
             let _ = companion_dm.update(cx, |dm, cx| {
@@ -934,19 +1052,33 @@ impl DisplayMap {
                 .ok()
         });
 
-        let companion_ref = self.companion.as_ref().map(|(_, c)| c.read(cx));
-        let companion_view = companion_wrap_data.as_ref().zip(companion_ref).map(
-            |((snapshot, edits), companion)| {
-                CompanionView::new(self.entity_id, snapshot, edits, companion)
-            },
-        );
-
-        let mut block_map = self.block_map.write(
-            self_wrap_snapshot.clone(),
-            self_wrap_edits.clone(),
-            companion_view,
-        );
-        block_map.disable_header_for_buffer(buffer_id);
+        if let Some((companion_dm, companion)) = self.companion.as_ref()
+            && let Some((snapshot, edits)) = companion_wrap_data.as_ref()
+        {
+            companion_dm
+                .update(cx, |dm, cx| {
+                    companion.update(cx, |companion, _| {
+                        self.block_map
+                            .write(
+                                self_wrap_snapshot.clone(),
+                                self_wrap_edits.clone(),
+                                Some(CompanionViewMut::new(
+                                    self.entity_id,
+                                    snapshot,
+                                    edits,
+                                    companion,
+                                    &mut dm.block_map,
+                                )),
+                            )
+                            .disable_header_for_buffer(buffer_id);
+                    })
+                })
+                .ok();
+        } else {
+            self.block_map
+                .write(self_wrap_snapshot.clone(), self_wrap_edits.clone(), None)
+                .disable_header_for_buffer(buffer_id);
+        }
 
         if let Some((companion_dm, _)) = &self.companion {
             let _ = companion_dm.update(cx, |dm, cx| {
@@ -1000,19 +1132,33 @@ impl DisplayMap {
                 .ok()
         });
 
-        let companion_ref = self.companion.as_ref().map(|(_, c)| c.read(cx));
-        let companion_view = companion_wrap_data.as_ref().zip(companion_ref).map(
-            |((snapshot, edits), companion)| {
-                CompanionView::new(self.entity_id, snapshot, edits, companion)
-            },
-        );
-
-        let mut block_map = self.block_map.write(
-            self_wrap_snapshot.clone(),
-            self_wrap_edits.clone(),
-            companion_view,
-        );
-        block_map.fold_buffers(buffer_ids.iter().copied(), self.buffer.read(cx), cx);
+        if let Some((companion_dm, companion)) = self.companion.as_ref()
+            && let Some((snapshot, edits)) = companion_wrap_data.as_ref()
+        {
+            companion_dm
+                .update(cx, |dm, cx| {
+                    companion.update(cx, |companion, cx| {
+                        self.block_map
+                            .write(
+                                self_wrap_snapshot.clone(),
+                                self_wrap_edits.clone(),
+                                Some(CompanionViewMut::new(
+                                    self.entity_id,
+                                    snapshot,
+                                    edits,
+                                    companion,
+                                    &mut dm.block_map,
+                                )),
+                            )
+                            .fold_buffers(buffer_ids.iter().copied(), self.buffer.read(cx), cx);
+                    })
+                })
+                .ok();
+        } else {
+            self.block_map
+                .write(self_wrap_snapshot.clone(), self_wrap_edits.clone(), None)
+                .fold_buffers(buffer_ids.iter().copied(), self.buffer.read(cx), cx);
+        }
 
         if let Some((companion_dm, companion_entity)) = &self.companion {
             let buffer_mapping = companion_entity
@@ -1025,21 +1171,30 @@ impl DisplayMap {
 
             let _ = companion_dm.update(cx, |dm, cx| {
                 if let Some((companion_snapshot, companion_edits)) = companion_wrap_data {
-                    let their_companion_ref = dm.companion.as_ref().map(|(_, c)| c.read(cx));
-                    let mut block_map = dm.block_map.write(
-                        companion_snapshot,
-                        companion_edits,
-                        their_companion_ref.map(|c| {
-                            CompanionView::new(
-                                dm.entity_id,
-                                &self_wrap_snapshot,
-                                &self_wrap_edits,
-                                c,
-                            )
-                        }),
-                    );
-                    if !their_buffer_ids.is_empty() {
-                        block_map.fold_buffers(their_buffer_ids, dm.buffer.read(cx), cx);
+                    if let Some((_, their_companion)) = dm.companion.as_ref() {
+                        their_companion.update(cx, |their_companion, cx| {
+                            let mut block_map = dm.block_map.write(
+                                companion_snapshot,
+                                companion_edits,
+                                Some(CompanionViewMut::new(
+                                    dm.entity_id,
+                                    &self_wrap_snapshot,
+                                    &self_wrap_edits,
+                                    their_companion,
+                                    &mut self.block_map,
+                                )),
+                            );
+                            if !their_buffer_ids.is_empty() {
+                                block_map.fold_buffers(their_buffer_ids, dm.buffer.read(cx), cx);
+                            }
+                        })
+                    } else {
+                        let mut block_map =
+                            dm.block_map
+                                .write(companion_snapshot, companion_edits, None);
+                        if !their_buffer_ids.is_empty() {
+                            block_map.fold_buffers(their_buffer_ids, dm.buffer.read(cx), cx);
+                        }
                     }
                 }
             });
@@ -1078,19 +1233,33 @@ impl DisplayMap {
                 .ok()
         });
 
-        let companion_ref = self.companion.as_ref().map(|(_, c)| c.read(cx));
-        let companion_view = companion_wrap_data.as_ref().zip(companion_ref).map(
-            |((snapshot, edits), companion)| {
-                CompanionView::new(self.entity_id, snapshot, edits, companion)
-            },
-        );
-
-        let mut block_map = self.block_map.write(
-            self_wrap_snapshot.clone(),
-            self_wrap_edits.clone(),
-            companion_view,
-        );
-        block_map.unfold_buffers(buffer_ids.iter().copied(), self.buffer.read(cx), cx);
+        if let Some((companion_dm, companion)) = self.companion.as_ref()
+            && let Some((snapshot, edits)) = companion_wrap_data.as_ref()
+        {
+            companion_dm
+                .update(cx, |dm, cx| {
+                    companion.update(cx, |companion, cx| {
+                        self.block_map
+                            .write(
+                                self_wrap_snapshot.clone(),
+                                self_wrap_edits.clone(),
+                                Some(CompanionViewMut::new(
+                                    self.entity_id,
+                                    snapshot,
+                                    edits,
+                                    companion,
+                                    &mut dm.block_map,
+                                )),
+                            )
+                            .unfold_buffers(buffer_ids.iter().copied(), self.buffer.read(cx), cx);
+                    })
+                })
+                .ok();
+        } else {
+            self.block_map
+                .write(self_wrap_snapshot.clone(), self_wrap_edits.clone(), None)
+                .unfold_buffers(buffer_ids.iter().copied(), self.buffer.read(cx), cx);
+        }
 
         if let Some((companion_dm, companion_entity)) = &self.companion {
             let buffer_mapping = companion_entity
@@ -1103,21 +1272,30 @@ impl DisplayMap {
 
             let _ = companion_dm.update(cx, |dm, cx| {
                 if let Some((companion_snapshot, companion_edits)) = companion_wrap_data {
-                    let their_companion_ref = dm.companion.as_ref().map(|(_, c)| c.read(cx));
-                    let mut block_map = dm.block_map.write(
-                        companion_snapshot,
-                        companion_edits,
-                        their_companion_ref.map(|c| {
-                            CompanionView::new(
-                                dm.entity_id,
-                                &self_wrap_snapshot,
-                                &self_wrap_edits,
-                                c,
-                            )
-                        }),
-                    );
-                    if !their_buffer_ids.is_empty() {
-                        block_map.unfold_buffers(their_buffer_ids, dm.buffer.read(cx), cx);
+                    if let Some((_, their_companion)) = dm.companion.as_ref() {
+                        their_companion.update(cx, |their_companion, cx| {
+                            let mut block_map = dm.block_map.write(
+                                companion_snapshot,
+                                companion_edits,
+                                Some(CompanionViewMut::new(
+                                    dm.entity_id,
+                                    &self_wrap_snapshot,
+                                    &self_wrap_edits,
+                                    their_companion,
+                                    &mut self.block_map,
+                                )),
+                            );
+                            if !their_buffer_ids.is_empty() {
+                                block_map.unfold_buffers(their_buffer_ids, dm.buffer.read(cx), cx);
+                            }
+                        })
+                    } else {
+                        let mut block_map =
+                            dm.block_map
+                                .write(companion_snapshot, companion_edits, None);
+                        if !their_buffer_ids.is_empty() {
+                            block_map.unfold_buffers(their_buffer_ids, dm.buffer.read(cx), cx);
+                        }
                     }
                 }
             });
@@ -1168,19 +1346,34 @@ impl DisplayMap {
                 .ok()
         });
 
-        let companion_ref = self.companion.as_ref().map(|(_, c)| c.read(cx));
-        let companion_view = companion_wrap_data.as_ref().zip(companion_ref).map(
-            |((snapshot, edits), companion)| {
-                CompanionView::new(self.entity_id, snapshot, edits, companion)
-            },
-        );
-
-        let mut block_map = self.block_map.write(
-            self_wrap_snapshot.clone(),
-            self_wrap_edits.clone(),
-            companion_view,
-        );
-        let result = block_map.insert(blocks);
+        let result = if let Some((companion_dm, companion)) = self.companion.as_ref()
+            && let Some((snapshot, edits)) = companion_wrap_data.as_ref()
+        {
+            companion_dm
+                .update(cx, |dm, cx| {
+                    companion.update(cx, |companion, _| {
+                        self.block_map
+                            .write(
+                                self_wrap_snapshot.clone(),
+                                self_wrap_edits.clone(),
+                                Some(CompanionViewMut::new(
+                                    self.entity_id,
+                                    snapshot,
+                                    edits,
+                                    companion,
+                                    &mut dm.block_map,
+                                )),
+                            )
+                            .insert(blocks)
+                    })
+                })
+                .ok()
+                .expect("success inserting blocks with companion")
+        } else {
+            self.block_map
+                .write(self_wrap_snapshot.clone(), self_wrap_edits.clone(), None)
+                .insert(blocks)
+        };
 
         if let Some((companion_dm, _)) = &self.companion {
             let _ = companion_dm.update(cx, |dm, cx| {
@@ -1215,19 +1408,33 @@ impl DisplayMap {
                 .ok()
         });
 
-        let companion_ref = self.companion.as_ref().map(|(_, c)| c.read(cx));
-        let companion_view = companion_wrap_data.as_ref().zip(companion_ref).map(
-            |((snapshot, edits), companion)| {
-                CompanionView::new(self.entity_id, snapshot, edits, companion)
-            },
-        );
-
-        let mut block_map = self.block_map.write(
-            self_wrap_snapshot.clone(),
-            self_wrap_edits.clone(),
-            companion_view,
-        );
-        block_map.resize(heights);
+        if let Some((companion_dm, companion)) = self.companion.as_ref()
+            && let Some((snapshot, edits)) = companion_wrap_data.as_ref()
+        {
+            companion_dm
+                .update(cx, |dm, cx| {
+                    companion.update(cx, |companion, _| {
+                        self.block_map
+                            .write(
+                                self_wrap_snapshot.clone(),
+                                self_wrap_edits.clone(),
+                                Some(CompanionViewMut::new(
+                                    self.entity_id,
+                                    snapshot,
+                                    edits,
+                                    companion,
+                                    &mut dm.block_map,
+                                )),
+                            )
+                            .resize(heights);
+                    })
+                })
+                .ok();
+        } else {
+            self.block_map
+                .write(self_wrap_snapshot.clone(), self_wrap_edits.clone(), None)
+                .resize(heights);
+        }
 
         if let Some((companion_dm, _)) = &self.companion {
             let _ = companion_dm.update(cx, |dm, cx| {
@@ -1265,19 +1472,33 @@ impl DisplayMap {
                 .ok()
         });
 
-        let companion_ref = self.companion.as_ref().map(|(_, c)| c.read(cx));
-        let companion_view = companion_wrap_data.as_ref().zip(companion_ref).map(
-            |((snapshot, edits), companion)| {
-                CompanionView::new(self.entity_id, snapshot, edits, companion)
-            },
-        );
-
-        let mut block_map = self.block_map.write(
-            self_wrap_snapshot.clone(),
-            self_wrap_edits.clone(),
-            companion_view,
-        );
-        block_map.remove(ids);
+        if let Some((companion_dm, companion)) = self.companion.as_ref()
+            && let Some((snapshot, edits)) = companion_wrap_data.as_ref()
+        {
+            companion_dm
+                .update(cx, |dm, cx| {
+                    companion.update(cx, |companion, _| {
+                        self.block_map
+                            .write(
+                                self_wrap_snapshot.clone(),
+                                self_wrap_edits.clone(),
+                                Some(CompanionViewMut::new(
+                                    self.entity_id,
+                                    snapshot,
+                                    edits,
+                                    companion,
+                                    &mut dm.block_map,
+                                )),
+                            )
+                            .remove(ids);
+                    })
+                })
+                .ok();
+        } else {
+            self.block_map
+                .write(self_wrap_snapshot.clone(), self_wrap_edits.clone(), None)
+                .remove(ids);
+        }
 
         if let Some((companion_dm, _)) = &self.companion {
             let _ = companion_dm.update(cx, |dm, cx| {

crates/editor/src/display_map/block_map.rs ๐Ÿ”—

@@ -48,7 +48,7 @@ pub struct BlockMap {
 }
 
 pub struct BlockMapReader<'a> {
-    blocks: &'a Vec<Arc<CustomBlock>>,
+    pub blocks: &'a Vec<Arc<CustomBlock>>,
     pub snapshot: BlockSnapshot,
 }
 
@@ -57,16 +57,22 @@ pub struct BlockMapWriter<'a> {
     companion: Option<BlockMapWriterCompanion<'a>>,
 }
 
-struct BlockMapWriterCompanion<'a>(CompanionView<'a>);
+struct BlockMapWriterCompanion<'a>(CompanionViewMut<'a>);
 
 impl<'a> Deref for BlockMapWriterCompanion<'a> {
-    type Target = CompanionView<'a>;
+    type Target = CompanionViewMut<'a>;
 
     fn deref(&self) -> &Self::Target {
         &self.0
     }
 }
 
+impl<'a> DerefMut for BlockMapWriterCompanion<'a> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+
 #[derive(Clone)]
 pub struct BlockSnapshot {
     pub(super) wrap_snapshot: WrapSnapshot,
@@ -282,6 +288,7 @@ pub struct BlockContext<'a, 'b> {
     pub em_width: Pixels,
     pub line_height: Pixels,
     pub block_id: BlockId,
+    pub height: u32,
     pub selected: bool,
     pub editor_style: &'b EditorStyle,
 }
@@ -542,6 +549,54 @@ impl<'a> CompanionView<'a> {
     }
 }
 
+impl<'a> From<CompanionViewMut<'a>> for CompanionView<'a> {
+    fn from(view_mut: CompanionViewMut<'a>) -> Self {
+        Self {
+            entity_id: view_mut.entity_id,
+            wrap_snapshot: view_mut.wrap_snapshot,
+            wrap_edits: view_mut.wrap_edits,
+            companion: view_mut.companion,
+        }
+    }
+}
+
+impl<'a> From<&'a CompanionViewMut<'a>> for CompanionView<'a> {
+    fn from(view_mut: &'a CompanionViewMut<'a>) -> Self {
+        Self {
+            entity_id: view_mut.entity_id,
+            wrap_snapshot: view_mut.wrap_snapshot,
+            wrap_edits: view_mut.wrap_edits,
+            companion: view_mut.companion,
+        }
+    }
+}
+
+pub struct CompanionViewMut<'a> {
+    entity_id: EntityId,
+    wrap_snapshot: &'a WrapSnapshot,
+    wrap_edits: &'a WrapPatch,
+    companion: &'a mut Companion,
+    block_map: &'a mut BlockMap,
+}
+
+impl<'a> CompanionViewMut<'a> {
+    pub(crate) fn new(
+        entity_id: EntityId,
+        wrap_snapshot: &'a WrapSnapshot,
+        wrap_edits: &'a WrapPatch,
+        companion: &'a mut Companion,
+        block_map: &'a mut BlockMap,
+    ) -> Self {
+        Self {
+            entity_id,
+            wrap_snapshot,
+            wrap_edits,
+            companion,
+            block_map,
+        }
+    }
+}
+
 impl BlockMap {
     #[ztracing::instrument(skip_all)]
     pub fn new(
@@ -600,9 +655,13 @@ impl BlockMap {
         &'a mut self,
         wrap_snapshot: WrapSnapshot,
         edits: WrapPatch,
-        companion_view: Option<CompanionView<'a>>,
+        companion_view: Option<CompanionViewMut<'a>>,
     ) -> BlockMapWriter<'a> {
-        self.sync(&wrap_snapshot, edits, companion_view);
+        self.sync(
+            &wrap_snapshot,
+            edits,
+            companion_view.as_ref().map(CompanionView::from),
+        );
         *self.wrap_snapshot.borrow_mut() = wrap_snapshot;
         BlockMapWriter {
             block_map: self,
@@ -1402,6 +1461,55 @@ impl BlockMap {
             _ => false,
         });
     }
+
+    pub(crate) fn insert_custom_block_into_companion(
+        &mut self,
+        entity_id: EntityId,
+        snapshot: &WrapSnapshot,
+        block: &CustomBlock,
+        companion_snapshot: &MultiBufferSnapshot,
+        companion: &mut Companion,
+    ) {
+        let their_anchor = block.placement.start();
+        let their_point = their_anchor.to_point(companion_snapshot);
+        let my_patches = companion.convert_rows_to_companion(
+            entity_id,
+            snapshot.buffer_snapshot(),
+            companion_snapshot,
+            (Bound::Included(their_point), Bound::Included(their_point)),
+        );
+        let my_excerpt = my_patches
+            .first()
+            .expect("at least one companion excerpt exists");
+        let my_range = my_excerpt.patch.edit_for_old_position(their_point).new;
+        let my_point = my_range.start;
+        let anchor = snapshot.buffer_snapshot().anchor_before(my_point);
+        let height = block.height.unwrap_or(1);
+        let new_block = BlockProperties {
+            placement: BlockPlacement::Above(anchor),
+            height: Some(height),
+            style: BlockStyle::Sticky,
+            render: Arc::new(move |cx| {
+                crate::EditorElement::render_spacer_block(
+                    cx.block_id,
+                    cx.height,
+                    cx.line_height,
+                    cx.window,
+                    cx.app,
+                )
+            }),
+            priority: 0,
+        };
+        log::debug!("Inserting matching companion custom block: {block:#?} => {new_block:#?}");
+        let new_block_id = self
+            .write(snapshot.clone(), Patch::default(), None)
+            .insert([new_block])[0];
+        if companion.is_rhs(entity_id) {
+            companion.add_custom_block_mapping(block.id, new_block_id);
+        } else {
+            companion.add_custom_block_mapping(new_block_id, block.id);
+        }
+    }
 }
 
 #[ztracing::instrument(skip(tree, wrap_snapshot))]
@@ -1562,7 +1670,7 @@ impl BlockMapWriter<'_> {
             };
             let new_block = Arc::new(CustomBlock {
                 id,
-                placement: block.placement,
+                placement: block.placement.clone(),
                 height: block.height,
                 render: Arc::new(Mutex::new(block.render)),
                 style: block.style,
@@ -1571,7 +1679,27 @@ impl BlockMapWriter<'_> {
             self.block_map
                 .custom_blocks
                 .insert(block_ix, new_block.clone());
-            self.block_map.custom_blocks_by_id.insert(id, new_block);
+            self.block_map
+                .custom_blocks_by_id
+                .insert(id, new_block.clone());
+
+            // Insert a matching custom block in the companion (if any)
+            if let Some(CompanionViewMut {
+                entity_id: their_entity_id,
+                wrap_snapshot: their_snapshot,
+                block_map: their_block_map,
+                companion,
+                ..
+            }) = self.companion.as_deref_mut()
+            {
+                their_block_map.insert_custom_block_into_companion(
+                    *their_entity_id,
+                    their_snapshot,
+                    &new_block,
+                    buffer,
+                    companion,
+                );
+            }
 
             edits = edits.compose([Edit {
                 old: start_row..end_row,
@@ -1584,13 +1712,13 @@ impl BlockMapWriter<'_> {
             wrap_snapshot,
             edits,
             self.companion.as_deref().map(
-                |&CompanionView {
+                |CompanionViewMut {
                      entity_id,
                      wrap_snapshot,
                      companion,
                      ..
                  }| {
-                    CompanionView::new(entity_id, wrap_snapshot, &default_patch, companion)
+                    CompanionView::new(*entity_id, wrap_snapshot, &default_patch, companion)
                 },
             ),
         );
@@ -1654,13 +1782,13 @@ impl BlockMapWriter<'_> {
             wrap_snapshot,
             edits,
             self.companion.as_deref().map(
-                |&CompanionView {
+                |CompanionViewMut {
                      entity_id,
                      wrap_snapshot,
                      companion,
                      ..
                  }| {
-                    CompanionView::new(entity_id, wrap_snapshot, &default_patch, companion)
+                    CompanionView::new(*entity_id, wrap_snapshot, &default_patch, companion)
                 },
             ),
         );
@@ -1709,18 +1837,49 @@ impl BlockMapWriter<'_> {
         self.block_map
             .custom_blocks_by_id
             .retain(|id, _| !block_ids.contains(id));
+
+        if let Some(CompanionViewMut {
+            entity_id: their_entity_id,
+            wrap_snapshot: their_snapshot,
+            companion,
+            block_map: their_block_map,
+            ..
+        }) = self.companion.as_deref_mut()
+        {
+            let their_block_ids: HashSet<_> = block_ids
+                .iter()
+                .filter_map(|my_block_id| {
+                    let mapping = companion.companion_custom_block_to_custom_block(*their_entity_id);
+                    let their_block_id =
+                        mapping.get(my_block_id)?;
+                    log::debug!("Removing custom block in the companion with id {their_block_id:?} for mine {my_block_id:?}");
+                    Some(*their_block_id)
+                })
+                .collect();
+            for (lhs_id, rhs_id) in block_ids.iter().zip(their_block_ids.iter()) {
+                if !companion.is_rhs(*their_entity_id) {
+                    companion.remove_custom_block_mapping(lhs_id, rhs_id);
+                } else {
+                    companion.remove_custom_block_mapping(rhs_id, lhs_id);
+                }
+            }
+            their_block_map
+                .write(their_snapshot.clone(), Patch::default(), None)
+                .remove(their_block_ids);
+        }
+
         let default_patch = Patch::default();
         self.block_map.sync(
             wrap_snapshot,
             edits,
             self.companion.as_deref().map(
-                |&CompanionView {
+                |CompanionViewMut {
                      entity_id,
                      wrap_snapshot,
                      companion,
                      ..
                  }| {
-                    CompanionView::new(entity_id, wrap_snapshot, &default_patch, companion)
+                    CompanionView::new(*entity_id, wrap_snapshot, &default_patch, companion)
                 },
             ),
         );
@@ -1809,13 +1968,13 @@ impl BlockMapWriter<'_> {
             &wrap_snapshot,
             edits,
             self.companion.as_deref().map(
-                |&CompanionView {
+                |CompanionViewMut {
                      entity_id,
                      wrap_snapshot,
                      companion,
                      ..
                  }| {
-                    CompanionView::new(entity_id, wrap_snapshot, &default_patch, companion)
+                    CompanionView::new(*entity_id, wrap_snapshot, &default_patch, companion)
                 },
             ),
         );

crates/editor/src/element.rs ๐Ÿ”—

@@ -3914,6 +3914,7 @@ impl EditorElement {
                         line_height,
                         em_width,
                         block_id,
+                        height: custom.height.unwrap_or(1),
                         selected,
                         max_width: text_hitbox.size.width.max(*scroll_width),
                         editor_style: &self.style,
@@ -4004,26 +4005,9 @@ impl EditorElement {
                 result.into_any()
             }
 
-            Block::Spacer { height, .. } => div()
-                .id(block_id)
-                .w_full()
-                .h((*height as f32) * line_height)
-                // the checkerboard pattern is semi-transparent, so we render a
-                // solid background to prevent indent guides peeking through
-                .bg(cx.theme().colors().editor_background)
-                .child(
-                    div()
-                        .size_full()
-                        .bg(checkerboard(cx.theme().colors().panel_background, {
-                            let target_size = 16.0;
-                            let scale = window.scale_factor();
-                            Self::checkerboard_size(
-                                f32::from(line_height) * scale,
-                                target_size * scale,
-                            )
-                        })),
-                )
-                .into_any(),
+            Block::Spacer { height, .. } => {
+                Self::render_spacer_block(block_id, *height, line_height, window, cx)
+            }
         };
 
         // Discover the element's content height, then round up to the nearest multiple of line height.
@@ -4102,6 +4086,32 @@ impl EditorElement {
         }
     }
 
+    pub fn render_spacer_block(
+        block_id: BlockId,
+        block_height: u32,
+        line_height: Pixels,
+        window: &mut Window,
+        cx: &App,
+    ) -> AnyElement {
+        div()
+            .id(block_id)
+            .w_full()
+            .h((block_height as f32) * line_height)
+            // the checkerboard pattern is semi-transparent, so we render a
+            // solid background to prevent indent guides peeking through
+            .bg(cx.theme().colors().editor_background)
+            .child(
+                div()
+                    .size_full()
+                    .bg(checkerboard(cx.theme().colors().panel_background, {
+                        let target_size = 16.0;
+                        let scale = window.scale_factor();
+                        Self::checkerboard_size(f32::from(line_height) * scale, target_size * scale)
+                    })),
+            )
+            .into_any()
+    }
+
     fn render_buffer_header(
         &self,
         for_excerpt: &ExcerptInfo,

crates/editor/src/split.rs ๐Ÿ”—

@@ -574,6 +574,9 @@ impl SplittableEditor {
         lhs_display_map.update(cx, |dm, cx| {
             dm.set_companion(Some((rhs_display_map.downgrade(), companion)), cx);
         });
+        rhs_display_map.update(cx, |dm, cx| {
+            dm.sync_custom_blocks_into_companion(cx);
+        });
 
         let shared_scroll_anchor = self
             .rhs_editor
@@ -1922,7 +1925,9 @@ impl LhsEditor {
 #[cfg(test)]
 mod tests {
     use buffer_diff::BufferDiff;
+    use collections::HashSet;
     use fs::FakeFs;
+    use gpui::Element as _;
     use gpui::{AppContext as _, Entity, Pixels, VisualTestContext};
     use language::language_settings::SoftWrap;
     use language::{Buffer, Capability};
@@ -1931,11 +1936,14 @@ mod tests {
     use project::Project;
     use rand::rngs::StdRng;
     use settings::SettingsStore;
-    use ui::{VisualContext as _, px};
+    use std::sync::Arc;
+    use ui::{VisualContext as _, div, px};
     use workspace::Workspace;
 
     use crate::SplittableEditor;
-    use crate::test::editor_content_with_blocks_and_width;
+    use crate::display_map::{BlockPlacement, BlockProperties, BlockStyle};
+    use crate::split::{SplitDiff, UnsplitDiff};
+    use crate::test::{editor_content_with_blocks_and_width, set_block_content_for_tests};
 
     async fn init_test(
         cx: &mut gpui::TestAppContext,
@@ -3847,4 +3855,704 @@ mod tests {
             "LHS should have same horizontal scroll position as RHS after autoscroll"
         );
     }
+
+    #[gpui::test]
+    async fn test_custom_block_sync_between_split_views(cx: &mut gpui::TestAppContext) {
+        use rope::Point;
+        use unindent::Unindent as _;
+
+        let (editor, mut cx) = init_test(cx, SoftWrap::None).await;
+
+        let base_text = "
+            bbb
+            ccc
+        "
+        .unindent();
+        let current_text = "
+            aaa
+            bbb
+            ccc
+        "
+        .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();
+
+        assert_split_content(
+            &editor,
+            "
+            ยง <no file>
+            ยง -----
+            aaa
+            bbb
+            ccc"
+            .unindent(),
+            "
+            ยง <no file>
+            ยง -----
+            ยง spacer
+            bbb
+            ccc"
+            .unindent(),
+            &mut cx,
+        );
+
+        let block_ids = editor.update(cx, |splittable_editor, cx| {
+            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
+                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
+                let anchor = snapshot.anchor_before(Point::new(2, 0));
+                rhs_editor.insert_blocks(
+                    [BlockProperties {
+                        placement: BlockPlacement::Above(anchor),
+                        height: Some(1),
+                        style: BlockStyle::Fixed,
+                        render: Arc::new(|_| div().into_any()),
+                        priority: 0,
+                    }],
+                    None,
+                    cx,
+                )
+            })
+        });
+
+        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
+        let lhs_editor =
+            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
+
+        cx.update(|_, cx| {
+            set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
+                "custom block".to_string()
+            });
+        });
+
+        let lhs_block_id = lhs_editor.read_with(cx, |lhs_editor, cx| {
+            let display_map = lhs_editor.display_map.read(cx);
+            let companion = display_map.companion().unwrap().read(cx);
+            let mapping = companion.companion_custom_block_to_custom_block(
+                rhs_editor.read(cx).display_map.entity_id(),
+            );
+            *mapping.get(&block_ids[0]).unwrap()
+        });
+
+        cx.update(|_, cx| {
+            set_block_content_for_tests(&lhs_editor, lhs_block_id, cx, |_| {
+                "custom block".to_string()
+            });
+        });
+
+        cx.run_until_parked();
+
+        assert_split_content(
+            &editor,
+            "
+            ยง <no file>
+            ยง -----
+            aaa
+            bbb
+            ยง custom block
+            ccc"
+            .unindent(),
+            "
+            ยง <no file>
+            ยง -----
+            ยง spacer
+            bbb
+            ยง custom block
+            ccc"
+            .unindent(),
+            &mut cx,
+        );
+
+        editor.update(cx, |splittable_editor, cx| {
+            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
+                rhs_editor.remove_blocks(HashSet::from_iter(block_ids), None, cx);
+            });
+        });
+
+        cx.run_until_parked();
+
+        assert_split_content(
+            &editor,
+            "
+            ยง <no file>
+            ยง -----
+            aaa
+            bbb
+            ccc"
+            .unindent(),
+            "
+            ยง <no file>
+            ยง -----
+            ยง spacer
+            bbb
+            ccc"
+            .unindent(),
+            &mut cx,
+        );
+    }
+
+    #[gpui::test]
+    async fn test_custom_block_deletion_and_resplit_sync(cx: &mut gpui::TestAppContext) {
+        use rope::Point;
+        use unindent::Unindent as _;
+
+        let (editor, mut cx) = init_test(cx, SoftWrap::None).await;
+
+        let base_text = "
+            bbb
+            ccc
+        "
+        .unindent();
+        let current_text = "
+            aaa
+            bbb
+            ccc
+        "
+        .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();
+
+        assert_split_content(
+            &editor,
+            "
+            ยง <no file>
+            ยง -----
+            aaa
+            bbb
+            ccc"
+            .unindent(),
+            "
+            ยง <no file>
+            ยง -----
+            ยง spacer
+            bbb
+            ccc"
+            .unindent(),
+            &mut cx,
+        );
+
+        let block_ids = editor.update(cx, |splittable_editor, cx| {
+            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
+                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
+                let anchor1 = snapshot.anchor_before(Point::new(2, 0));
+                let anchor2 = snapshot.anchor_before(Point::new(3, 0));
+                rhs_editor.insert_blocks(
+                    [
+                        BlockProperties {
+                            placement: BlockPlacement::Above(anchor1),
+                            height: Some(1),
+                            style: BlockStyle::Fixed,
+                            render: Arc::new(|_| div().into_any()),
+                            priority: 0,
+                        },
+                        BlockProperties {
+                            placement: BlockPlacement::Above(anchor2),
+                            height: Some(1),
+                            style: BlockStyle::Fixed,
+                            render: Arc::new(|_| div().into_any()),
+                            priority: 0,
+                        },
+                    ],
+                    None,
+                    cx,
+                )
+            })
+        });
+
+        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
+        let lhs_editor =
+            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
+
+        cx.update(|_, cx| {
+            set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
+                "custom block 1".to_string()
+            });
+            set_block_content_for_tests(&rhs_editor, block_ids[1], cx, |_| {
+                "custom block 2".to_string()
+            });
+        });
+
+        let (lhs_block_id_1, lhs_block_id_2) = lhs_editor.read_with(cx, |lhs_editor, cx| {
+            let display_map = lhs_editor.display_map.read(cx);
+            let companion = display_map.companion().unwrap().read(cx);
+            let mapping = companion.companion_custom_block_to_custom_block(
+                rhs_editor.read(cx).display_map.entity_id(),
+            );
+            (
+                *mapping.get(&block_ids[0]).unwrap(),
+                *mapping.get(&block_ids[1]).unwrap(),
+            )
+        });
+
+        cx.update(|_, cx| {
+            set_block_content_for_tests(&lhs_editor, lhs_block_id_1, cx, |_| {
+                "custom block 1".to_string()
+            });
+            set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
+                "custom block 2".to_string()
+            });
+        });
+
+        cx.run_until_parked();
+
+        assert_split_content(
+            &editor,
+            "
+            ยง <no file>
+            ยง -----
+            aaa
+            bbb
+            ยง custom block 1
+            ccc
+            ยง custom block 2"
+                .unindent(),
+            "
+            ยง <no file>
+            ยง -----
+            ยง spacer
+            bbb
+            ยง custom block 1
+            ccc
+            ยง custom block 2"
+                .unindent(),
+            &mut cx,
+        );
+
+        editor.update(cx, |splittable_editor, cx| {
+            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
+                rhs_editor.remove_blocks(HashSet::from_iter([block_ids[0]]), None, cx);
+            });
+        });
+
+        cx.run_until_parked();
+
+        assert_split_content(
+            &editor,
+            "
+            ยง <no file>
+            ยง -----
+            aaa
+            bbb
+            ccc
+            ยง custom block 2"
+                .unindent(),
+            "
+            ยง <no file>
+            ยง -----
+            ยง spacer
+            bbb
+            ccc
+            ยง custom block 2"
+                .unindent(),
+            &mut cx,
+        );
+
+        editor.update_in(cx, |splittable_editor, window, cx| {
+            splittable_editor.unsplit(&UnsplitDiff, window, cx);
+        });
+
+        cx.run_until_parked();
+
+        editor.update_in(cx, |splittable_editor, window, cx| {
+            splittable_editor.split(&SplitDiff, window, cx);
+        });
+
+        cx.run_until_parked();
+
+        let lhs_editor =
+            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
+
+        let lhs_block_id_2 = lhs_editor.read_with(cx, |lhs_editor, cx| {
+            let display_map = lhs_editor.display_map.read(cx);
+            let companion = display_map.companion().unwrap().read(cx);
+            let mapping = companion.companion_custom_block_to_custom_block(
+                rhs_editor.read(cx).display_map.entity_id(),
+            );
+            *mapping.get(&block_ids[1]).unwrap()
+        });
+
+        cx.update(|_, cx| {
+            set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
+                "custom block 2".to_string()
+            });
+        });
+
+        cx.run_until_parked();
+
+        assert_split_content(
+            &editor,
+            "
+            ยง <no file>
+            ยง -----
+            aaa
+            bbb
+            ccc
+            ยง custom block 2"
+                .unindent(),
+            "
+            ยง <no file>
+            ยง -----
+            ยง spacer
+            bbb
+            ccc
+            ยง custom block 2"
+                .unindent(),
+            &mut cx,
+        );
+    }
+
+    #[gpui::test]
+    async fn test_custom_block_sync_with_unsplit_start(cx: &mut gpui::TestAppContext) {
+        use rope::Point;
+        use unindent::Unindent as _;
+
+        let (editor, mut cx) = init_test(cx, SoftWrap::None).await;
+
+        let base_text = "
+            bbb
+            ccc
+        "
+        .unindent();
+        let current_text = "
+            aaa
+            bbb
+            ccc
+        "
+        .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, |splittable_editor, window, cx| {
+            splittable_editor.unsplit(&UnsplitDiff, window, cx);
+        });
+
+        cx.run_until_parked();
+
+        let rhs_editor = editor.read_with(cx, |editor, _| editor.rhs_editor.clone());
+
+        let block_ids = editor.update(cx, |splittable_editor, cx| {
+            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
+                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
+                let anchor1 = snapshot.anchor_before(Point::new(2, 0));
+                let anchor2 = snapshot.anchor_before(Point::new(3, 0));
+                rhs_editor.insert_blocks(
+                    [
+                        BlockProperties {
+                            placement: BlockPlacement::Above(anchor1),
+                            height: Some(1),
+                            style: BlockStyle::Fixed,
+                            render: Arc::new(|_| div().into_any()),
+                            priority: 0,
+                        },
+                        BlockProperties {
+                            placement: BlockPlacement::Above(anchor2),
+                            height: Some(1),
+                            style: BlockStyle::Fixed,
+                            render: Arc::new(|_| div().into_any()),
+                            priority: 0,
+                        },
+                    ],
+                    None,
+                    cx,
+                )
+            })
+        });
+
+        cx.update(|_, cx| {
+            set_block_content_for_tests(&rhs_editor, block_ids[0], cx, |_| {
+                "custom block 1".to_string()
+            });
+            set_block_content_for_tests(&rhs_editor, block_ids[1], cx, |_| {
+                "custom block 2".to_string()
+            });
+        });
+
+        cx.run_until_parked();
+
+        let rhs_content = editor_content_with_blocks_and_width(&rhs_editor, px(3000.0), &mut cx);
+        assert_eq!(
+            rhs_content,
+            "
+            ยง <no file>
+            ยง -----
+            aaa
+            bbb
+            ยง custom block 1
+            ccc
+            ยง custom block 2"
+                .unindent(),
+            "rhs content before split"
+        );
+
+        editor.update_in(cx, |splittable_editor, window, cx| {
+            splittable_editor.split(&SplitDiff, window, cx);
+        });
+
+        cx.run_until_parked();
+
+        let lhs_editor =
+            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
+
+        let (lhs_block_id_1, lhs_block_id_2) = lhs_editor.read_with(cx, |lhs_editor, cx| {
+            let display_map = lhs_editor.display_map.read(cx);
+            let companion = display_map.companion().unwrap().read(cx);
+            let mapping = companion.companion_custom_block_to_custom_block(
+                rhs_editor.read(cx).display_map.entity_id(),
+            );
+            (
+                *mapping.get(&block_ids[0]).unwrap(),
+                *mapping.get(&block_ids[1]).unwrap(),
+            )
+        });
+
+        cx.update(|_, cx| {
+            set_block_content_for_tests(&lhs_editor, lhs_block_id_1, cx, |_| {
+                "custom block 1".to_string()
+            });
+            set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
+                "custom block 2".to_string()
+            });
+        });
+
+        cx.run_until_parked();
+
+        assert_split_content(
+            &editor,
+            "
+            ยง <no file>
+            ยง -----
+            aaa
+            bbb
+            ยง custom block 1
+            ccc
+            ยง custom block 2"
+                .unindent(),
+            "
+            ยง <no file>
+            ยง -----
+            ยง spacer
+            bbb
+            ยง custom block 1
+            ccc
+            ยง custom block 2"
+                .unindent(),
+            &mut cx,
+        );
+
+        editor.update(cx, |splittable_editor, cx| {
+            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
+                rhs_editor.remove_blocks(HashSet::from_iter([block_ids[0]]), None, cx);
+            });
+        });
+
+        cx.run_until_parked();
+
+        assert_split_content(
+            &editor,
+            "
+            ยง <no file>
+            ยง -----
+            aaa
+            bbb
+            ccc
+            ยง custom block 2"
+                .unindent(),
+            "
+            ยง <no file>
+            ยง -----
+            ยง spacer
+            bbb
+            ccc
+            ยง custom block 2"
+                .unindent(),
+            &mut cx,
+        );
+
+        editor.update_in(cx, |splittable_editor, window, cx| {
+            splittable_editor.unsplit(&UnsplitDiff, window, cx);
+        });
+
+        cx.run_until_parked();
+
+        editor.update_in(cx, |splittable_editor, window, cx| {
+            splittable_editor.split(&SplitDiff, window, cx);
+        });
+
+        cx.run_until_parked();
+
+        let lhs_editor =
+            editor.read_with(cx, |editor, _| editor.lhs.as_ref().unwrap().editor.clone());
+
+        let lhs_block_id_2 = lhs_editor.read_with(cx, |lhs_editor, cx| {
+            let display_map = lhs_editor.display_map.read(cx);
+            let companion = display_map.companion().unwrap().read(cx);
+            let mapping = companion.companion_custom_block_to_custom_block(
+                rhs_editor.read(cx).display_map.entity_id(),
+            );
+            *mapping.get(&block_ids[1]).unwrap()
+        });
+
+        cx.update(|_, cx| {
+            set_block_content_for_tests(&lhs_editor, lhs_block_id_2, cx, |_| {
+                "custom block 2".to_string()
+            });
+        });
+
+        cx.run_until_parked();
+
+        assert_split_content(
+            &editor,
+            "
+            ยง <no file>
+            ยง -----
+            aaa
+            bbb
+            ccc
+            ยง custom block 2"
+                .unindent(),
+            "
+            ยง <no file>
+            ยง -----
+            ยง spacer
+            bbb
+            ccc
+            ยง custom block 2"
+                .unindent(),
+            &mut cx,
+        );
+
+        let new_block_ids = editor.update(cx, |splittable_editor, cx| {
+            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
+                let snapshot = rhs_editor.buffer().read(cx).snapshot(cx);
+                let anchor = snapshot.anchor_before(Point::new(2, 0));
+                rhs_editor.insert_blocks(
+                    [BlockProperties {
+                        placement: BlockPlacement::Above(anchor),
+                        height: Some(1),
+                        style: BlockStyle::Fixed,
+                        render: Arc::new(|_| div().into_any()),
+                        priority: 0,
+                    }],
+                    None,
+                    cx,
+                )
+            })
+        });
+
+        cx.update(|_, cx| {
+            set_block_content_for_tests(&rhs_editor, new_block_ids[0], cx, |_| {
+                "custom block 3".to_string()
+            });
+        });
+
+        let lhs_block_id_3 = lhs_editor.read_with(cx, |lhs_editor, cx| {
+            let display_map = lhs_editor.display_map.read(cx);
+            let companion = display_map.companion().unwrap().read(cx);
+            let mapping = companion.companion_custom_block_to_custom_block(
+                rhs_editor.read(cx).display_map.entity_id(),
+            );
+            *mapping.get(&new_block_ids[0]).unwrap()
+        });
+
+        cx.update(|_, cx| {
+            set_block_content_for_tests(&lhs_editor, lhs_block_id_3, cx, |_| {
+                "custom block 3".to_string()
+            });
+        });
+
+        cx.run_until_parked();
+
+        assert_split_content(
+            &editor,
+            "
+            ยง <no file>
+            ยง -----
+            aaa
+            bbb
+            ยง custom block 3
+            ccc
+            ยง custom block 2"
+                .unindent(),
+            "
+            ยง <no file>
+            ยง -----
+            ยง spacer
+            bbb
+            ยง custom block 3
+            ccc
+            ยง custom block 2"
+                .unindent(),
+            &mut cx,
+        );
+
+        editor.update(cx, |splittable_editor, cx| {
+            splittable_editor.rhs_editor.update(cx, |rhs_editor, cx| {
+                rhs_editor.remove_blocks(HashSet::from_iter([new_block_ids[0]]), None, cx);
+            });
+        });
+
+        cx.run_until_parked();
+
+        assert_split_content(
+            &editor,
+            "
+            ยง <no file>
+            ยง -----
+            aaa
+            bbb
+            ccc
+            ยง custom block 2"
+                .unindent(),
+            "
+            ยง <no file>
+            ยง -----
+            ยง spacer
+            bbb
+            ccc
+            ยง custom block 2"
+                .unindent(),
+            &mut cx,
+        );
+    }
 }