multi_buffer: Fix editor::ExpandExcerpts failing when cursor is at excerpt start (#42324)

The bug is easily verified by:

1. open any multi-buffer
2. place the cursor at the beginning of an excerpt
3. run the editor::ExpandExcerpts / editor: expand excerpts action
4. The excerpt is not expanded

Since the `buffer_ids_for_range` function basically did the same and had
even been changed the same way earlier I DRYed these functions as well.

Note: I'm a rust novice, so keep an extra eye on rust technicalities
when reviewing :)

---

Release Notes:

- Fix editor: expand excerpts failing when cursor is at excerpt start

---------

Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
This commit is contained in:
Ole Jørgen Brønner
2025-11-25 08:21:18 +01:00
committed by David
parent 624dab2027
commit 58fd39ddcb
2 changed files with 132 additions and 20 deletions

View File

@@ -3616,27 +3616,10 @@ impl MultiBufferSnapshot {
})
}
pub fn excerpt_ids_for_range<T: ToOffset>(
fn excerpts_for_range<T: ToOffset>(
&self,
range: Range<T>,
) -> impl Iterator<Item = ExcerptId> + '_ {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut cursor = self.cursor::<MultiBufferOffset, BufferOffset>();
cursor.seek(&range.start);
std::iter::from_fn(move || {
let region = cursor.region()?;
if region.range.start >= range.end {
return None;
}
cursor.next_excerpt();
Some(region.excerpt.id)
})
}
pub fn buffer_ids_for_range<T: ToOffset>(
&self,
range: Range<T>,
) -> impl Iterator<Item = BufferId> + '_ {
) -> impl Iterator<Item = &Excerpt> + '_ {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut cursor = self.cursor::<MultiBufferOffset, BufferOffset>();
cursor.seek(&range.start);
@@ -3648,10 +3631,25 @@ impl MultiBufferSnapshot {
return None;
}
cursor.next_excerpt();
Some(region.excerpt.buffer_id)
Some(region.excerpt)
})
}
pub fn excerpt_ids_for_range<T: ToOffset>(
&self,
range: Range<T>,
) -> impl Iterator<Item = ExcerptId> + '_ {
self.excerpts_for_range(range).map(|excerpt| excerpt.id)
}
pub fn buffer_ids_for_range<T: ToOffset>(
&self,
range: Range<T>,
) -> impl Iterator<Item = BufferId> + '_ {
self.excerpts_for_range(range)
.map(|excerpt| excerpt.buffer_id)
}
pub fn ranges_to_buffer_ranges<T: ToOffset>(
&self,
ranges: impl Iterator<Item = Range<T>>,

View File

@@ -4095,3 +4095,117 @@ fn test_random_chunk_bitmaps_with_diffs(cx: &mut App, mut rng: StdRng) {
}
}
}
/// Tests `excerpt_containing` and `excerpts_for_range` (functions mapping multi-buffer text-coordinates to excerpts)
#[gpui::test]
fn test_excerpts_containment_functions(cx: &mut App) {
// Multibuffer content for these tests:
// 0123
// 0: aa0
// 1: aa1
// -----
// 2: bb0
// 3: bb1
// -----MultiBufferOffset(0)..
// 4: cc0
let buffer_1 = cx.new(|cx| Buffer::local("aa0\naa1", cx));
let buffer_2 = cx.new(|cx| Buffer::local("bb0\nbb1", cx));
let buffer_3 = cx.new(|cx| Buffer::local("cc0", cx));
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
let (excerpt_1_id, excerpt_2_id, excerpt_3_id) = multibuffer.update(cx, |multibuffer, cx| {
let excerpt_1_id = multibuffer.push_excerpts(
buffer_1.clone(),
[ExcerptRange::new(Point::new(0, 0)..Point::new(1, 3))],
cx,
)[0];
let excerpt_2_id = multibuffer.push_excerpts(
buffer_2.clone(),
[ExcerptRange::new(Point::new(0, 0)..Point::new(1, 3))],
cx,
)[0];
let excerpt_3_id = multibuffer.push_excerpts(
buffer_3.clone(),
[ExcerptRange::new(Point::new(0, 0)..Point::new(0, 3))],
cx,
)[0];
(excerpt_1_id, excerpt_2_id, excerpt_3_id)
});
let snapshot = multibuffer.read(cx).snapshot(cx);
assert_eq!(snapshot.text(), "aa0\naa1\nbb0\nbb1\ncc0");
//// Test `excerpts_for_range`
let p00 = snapshot.point_to_offset(Point::new(0, 0));
let p10 = snapshot.point_to_offset(Point::new(1, 0));
let p20 = snapshot.point_to_offset(Point::new(2, 0));
let p23 = snapshot.point_to_offset(Point::new(2, 3));
let p13 = snapshot.point_to_offset(Point::new(1, 3));
let p40 = snapshot.point_to_offset(Point::new(4, 0));
let p43 = snapshot.point_to_offset(Point::new(4, 3));
let excerpts: Vec<_> = snapshot.excerpts_for_range(p00..p00).collect();
assert_eq!(excerpts.len(), 1);
assert_eq!(excerpts[0].id, excerpt_1_id);
// Cursor at very end of excerpt 3
let excerpts: Vec<_> = snapshot.excerpts_for_range(p43..p43).collect();
assert_eq!(excerpts.len(), 1);
assert_eq!(excerpts[0].id, excerpt_3_id);
let excerpts: Vec<_> = snapshot.excerpts_for_range(p00..p23).collect();
assert_eq!(excerpts.len(), 2);
assert_eq!(excerpts[0].id, excerpt_1_id);
assert_eq!(excerpts[1].id, excerpt_2_id);
// This range represent an selection with end-point just inside excerpt_2
// Today we only expand the first excerpt, but another interpretation that
// we could consider is expanding both here
let excerpts: Vec<_> = snapshot.excerpts_for_range(p10..p20).collect();
assert_eq!(excerpts.len(), 1);
assert_eq!(excerpts[0].id, excerpt_1_id);
//// Test that `excerpts_for_range` and `excerpt_containing` agree for all single offsets (cursor positions)
for offset in 0..=snapshot.len().0 {
let offset = MultiBufferOffset(offset);
let excerpts_for_range: Vec<_> = snapshot.excerpts_for_range(offset..offset).collect();
assert_eq!(
excerpts_for_range.len(),
1,
"Expected exactly one excerpt for offset {offset}",
);
let excerpt_containing = snapshot.excerpt_containing(offset..offset);
assert!(
excerpt_containing.is_some(),
"Expected excerpt_containing to find excerpt for offset {offset}",
);
assert_eq!(
excerpts_for_range[0].id,
excerpt_containing.unwrap().id(),
"excerpts_for_range and excerpt_containing should agree for offset {offset}",
);
}
//// Test `excerpt_containing` behavior with ranges:
// Ranges intersecting a single-excerpt
let containing = snapshot.excerpt_containing(p00..p13);
assert!(containing.is_some());
assert_eq!(containing.unwrap().id(), excerpt_1_id);
// Ranges intersecting multiple excerpts (should return None)
let containing = snapshot.excerpt_containing(p20..p40);
assert!(
containing.is_none(),
"excerpt_containing should return None for ranges spanning multiple excerpts"
);
}