editor: Fix underflow panic in block map sync when blocks overlap (#51078)

Lukas Wirth created

In `BlockMap::sync`, blocks within an edited region are sorted and
processed sequentially. Each block placement computes
`rows_before_block` by subtracting `new_transforms.summary().input_rows`
from the block's target position. The `Near`/`Below` cases have a guard
that skips the block if the target is already behind the current
progress, but `Above` and `Replace` were missing this guard.

When a `Replace` block (tie_break 0) is processed before an `Above`
block (tie_break 1) at the same or overlapping position, the `Replace`
block consumes multiple input rows, advancing `input_rows` past the
`Above` block's position. The subsequent `position - input_rows`
subtraction underflows on `u32`, producing a huge `RowDelta` that wraps
`wrap_row_end` past `wrap_row_start`, creating an inverted range that
propagates through the display map layers and panics as `begin <= end
(47 <= 0)` in a rope chunk slice.

Add underflow guards to `Above` and `Replace`, matching the existing
pattern in `Near`/`Below`.

Release Notes:

- Fixed a source of underflowing subtractions causing spurious panics

Change summary

crates/editor/src/display_map/block_map.rs  | 26 ++++++++++++++--------
crates/editor/src/display_map/dimensions.rs |  4 +++
2 files changed, 20 insertions(+), 10 deletions(-)

Detailed changes

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

@@ -1091,23 +1091,29 @@ impl BlockMap {
                 };
 
                 let rows_before_block;
-                match block_placement {
-                    BlockPlacement::Above(position) => {
-                        rows_before_block = position - new_transforms.summary().input_rows;
+                let input_rows = new_transforms.summary().input_rows;
+                match &block_placement {
+                    &BlockPlacement::Above(position) => {
+                        let Some(delta) = position.checked_sub(input_rows) else {
+                            continue;
+                        };
+                        rows_before_block = delta;
                         just_processed_folded_buffer = false;
                     }
-                    BlockPlacement::Near(position) | BlockPlacement::Below(position) => {
+                    &BlockPlacement::Near(position) | &BlockPlacement::Below(position) => {
                         if just_processed_folded_buffer {
                             continue;
                         }
-                        if position + RowDelta(1) < new_transforms.summary().input_rows {
+                        let Some(delta) = (position + RowDelta(1)).checked_sub(input_rows) else {
                             continue;
-                        }
-                        rows_before_block =
-                            (position + RowDelta(1)) - new_transforms.summary().input_rows;
+                        };
+                        rows_before_block = delta;
                     }
-                    BlockPlacement::Replace(ref range) => {
-                        rows_before_block = *range.start() - new_transforms.summary().input_rows;
+                    BlockPlacement::Replace(range) => {
+                        let Some(delta) = range.start().checked_sub(input_rows) else {
+                            continue;
+                        };
+                        rows_before_block = delta;
                         summary.input_rows = WrapRow(1) + (*range.end() - *range.start());
                         just_processed_folded_buffer = matches!(block, Block::FoldedBuffer { .. });
                     }

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

@@ -41,6 +41,10 @@ macro_rules! impl_for_row_types {
             pub fn saturating_sub(self, other: $row_delta) -> $row {
                 $row(self.0.saturating_sub(other.0))
             }
+
+            pub fn checked_sub(self, other: $row) -> Option<$row_delta> {
+                self.0.checked_sub(other.0).map($row_delta)
+            }
         }
 
         impl ::std::ops::Add for $row {