Fix panic calling blocks_intersecting_buffer_range with an empty range (#28049)

Previously, when comparing a block with an empty range to an empty query
range in non-inclusive mode, our binary search logic could end up
computing an inverted range, causing a panic.

This commit adds special casing when comparing empty blocks with empty
ranges.

cc @as-cii: I'm realizing that the approach to searching for the
intersecting replacement blocks makes some invalid assumptions about the
ordering of replace decorations. They aren't ordered at all by their end
range. @maxbrunsfeld and I are wondering if long term, we should remove
replace decorations and find another solution for folding buffers in
multi buffers.

Release Notes:

- Fixed an occasional panic that would occur when navigating to the next
change hunk with a pending inline transformation present.

Co-authored-by: Peter Tripp <petertripp@gmail.com>
This commit is contained in:
Nathan Sobo 2025-04-03 16:50:49 -06:00 committed by GitHub
parent ec40e2d85c
commit 8b5ea05163
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1235,21 +1235,21 @@ impl BlockMapWriter<'_> {
) -> &[Arc<CustomBlock>] {
let wrap_snapshot = self.0.wrap_snapshot.borrow();
let buffer = wrap_snapshot.buffer_snapshot();
let start_block_ix = match self.0.custom_blocks.binary_search_by(|probe| {
probe
.end()
.to_offset(buffer)
.cmp(&range.start)
.then(if inclusive {
let start_block_ix = match self.0.custom_blocks.binary_search_by(|block| {
let block_end = block.end().to_offset(buffer);
block_end.cmp(&range.start).then_with(|| {
if inclusive || (range.is_empty() && block.start().to_offset(buffer) == block_end) {
Ordering::Greater
} else {
Ordering::Less
})
}
})
}) {
Ok(ix) | Err(ix) => ix,
};
let end_block_ix = match self.0.custom_blocks.binary_search_by(|probe| {
probe
let end_block_ix = match self.0.custom_blocks.binary_search_by(|block| {
block
.start()
.to_offset(buffer)
.cmp(&range.end)
@ -1261,6 +1261,7 @@ impl BlockMapWriter<'_> {
}) {
Ok(ix) | Err(ix) => ix,
};
&self.0.custom_blocks[start_block_ix..end_block_ix]
}
}
@ -3505,6 +3506,42 @@ mod tests {
}
}
#[gpui::test]
fn test_remove_intersecting_replace_blocks_edge_case(cx: &mut gpui::TestAppContext) {
cx.update(init_test);
let text = "abc\ndef\nghi\njkl\nmno";
let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
let (_inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (_wrap_map, wraps_snapshot) =
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
let _block_id = writer.insert(vec![BlockProperties {
style: BlockStyle::Fixed,
placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(1, 0))),
height: 1,
render: Arc::new(|_| div().into_any()),
priority: 0,
}])[0];
let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
assert_eq!(blocks_snapshot.text(), "abc\n\ndef\nghi\njkl\nmno");
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
writer.remove_intersecting_replace_blocks(
[buffer_snapshot.anchor_after(Point::new(1, 0))
..buffer_snapshot.anchor_after(Point::new(1, 0))],
false,
);
let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
assert_eq!(blocks_snapshot.text(), "abc\n\ndef\nghi\njkl\nmno");
}
fn init_test(cx: &mut gpui::App) {
let settings = SettingsStore::test(cx);
cx.set_global(settings);