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

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
This commit is contained in:
Lukas Wirth authored and GitHub committed 2026-03-09 08:45:46 +00:00
1 parent 3f2ddcbca3
commit 8d5689a7fa
2 files changed
+20 -10

No files matched your search

+16 -10
View File
@@ -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 { .. });
}
@@ -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 {