multi_buffer: Fix up some anchor checks (#43454)

Release Notes:

- N/A *or* Added/Fixed/Improved ...
This commit is contained in:
Lukas Wirth
2025-11-25 13:41:19 +00:00
committed by GitHub
parent a359a5a1f2
commit f8965317c3
15 changed files with 154 additions and 58 deletions
+36 -8
View File
@@ -9,14 +9,33 @@ use std::{
use sum_tree::Bias;
use text::BufferId;
#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)]
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
pub struct Anchor {
/// Invariant: If buffer id is `None`, excerpt id must be `ExcerptId::min()` or `ExcerptId::max()`.
pub buffer_id: Option<BufferId>,
pub excerpt_id: ExcerptId,
pub text_anchor: text::Anchor,
pub diff_base_anchor: Option<text::Anchor>,
}
impl std::fmt::Debug for Anchor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if *self == Self::min() {
return f.write_str("Anchor::MIN");
}
if *self == Self::max() {
return f.write_str("Anchor::MAX");
}
f.debug_struct("Anchor")
.field("buffer_id", &self.buffer_id)
.field("excerpt_id", &self.excerpt_id)
.field("text_anchor", &self.text_anchor)
.field("diff_base_anchor", &self.diff_base_anchor)
.finish()
}
}
impl Anchor {
pub fn with_diff_base_anchor(self, diff_base_anchor: text::Anchor) -> Self {
Self {
@@ -30,6 +49,10 @@ impl Anchor {
buffer_id: BufferId,
text_anchor: text::Anchor,
) -> Self {
debug_assert!(
text_anchor.buffer_id.is_none_or(|id| id == buffer_id),
"buffer id does not match the one in the text anchor: {buffer_id:?} {text_anchor:?}",
);
Self {
buffer_id: Some(buffer_id),
excerpt_id,
@@ -77,7 +100,12 @@ impl Anchor {
if excerpt_id_cmp.is_ne() {
return excerpt_id_cmp;
}
if self_excerpt_id == ExcerptId::min() || self_excerpt_id == ExcerptId::max() {
if self_excerpt_id == ExcerptId::max()
&& self.text_anchor == text::Anchor::MAX
&& self.text_anchor == text::Anchor::MAX
&& self.diff_base_anchor.is_none()
&& other.diff_base_anchor.is_none()
{
return Ordering::Equal;
}
if let Some(excerpt) = snapshot.excerpt(self_excerpt_id) {
@@ -119,8 +147,8 @@ impl Anchor {
&& let Some(excerpt) = snapshot.excerpt(self.excerpt_id)
{
return Self {
buffer_id: self.buffer_id,
excerpt_id: self.excerpt_id,
buffer_id: Some(excerpt.buffer_id),
excerpt_id: excerpt.id,
text_anchor: self.text_anchor.bias_left(&excerpt.buffer),
diff_base_anchor: self.diff_base_anchor.map(|a| {
if let Some(base_text) = snapshot
@@ -143,8 +171,8 @@ impl Anchor {
&& let Some(excerpt) = snapshot.excerpt(self.excerpt_id)
{
return Self {
buffer_id: self.buffer_id,
excerpt_id: self.excerpt_id,
buffer_id: Some(excerpt.buffer_id),
excerpt_id: excerpt.id,
text_anchor: self.text_anchor.bias_right(&excerpt.buffer),
diff_base_anchor: self.diff_base_anchor.map(|a| {
if let Some(base_text) = snapshot
@@ -174,8 +202,8 @@ impl Anchor {
}
pub fn is_valid(&self, snapshot: &MultiBufferSnapshot) -> bool {
if *self == Anchor::min() || *self == Anchor::max() {
true
if *self == Anchor::min() || self.excerpt_id == ExcerptId::max() {
!snapshot.is_empty()
} else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
(self.text_anchor == excerpt.range.context.start
|| self.text_anchor == excerpt.range.context.end
+31 -12
View File
@@ -5076,8 +5076,7 @@ impl MultiBufferSnapshot {
excerpt_id: ExcerptId,
text_anchor: Range<text::Anchor>,
) -> Option<Range<Anchor>> {
let excerpt_id = self.latest_excerpt_id(excerpt_id);
let excerpt = self.excerpt(excerpt_id)?;
let excerpt = self.excerpt(self.latest_excerpt_id(excerpt_id))?;
Some(
self.anchor_in_excerpt_(excerpt, text_anchor.start)?
@@ -5092,8 +5091,7 @@ impl MultiBufferSnapshot {
excerpt_id: ExcerptId,
text_anchor: text::Anchor,
) -> Option<Anchor> {
let excerpt_id = self.latest_excerpt_id(excerpt_id);
let excerpt = self.excerpt(excerpt_id)?;
let excerpt = self.excerpt(self.latest_excerpt_id(excerpt_id))?;
self.anchor_in_excerpt_(excerpt, text_anchor)
}
@@ -5130,7 +5128,8 @@ impl MultiBufferSnapshot {
}
pub fn can_resolve(&self, anchor: &Anchor) -> bool {
if anchor.excerpt_id == ExcerptId::min() || anchor.excerpt_id == ExcerptId::max() {
if *anchor == Anchor::min() || anchor.excerpt_id == ExcerptId::max() {
// todo(lw): should be `!self.is_empty()`
true
} else if let Some(excerpt) = self.excerpt(anchor.excerpt_id) {
excerpt.buffer.can_resolve(&anchor.text_anchor)
@@ -5791,8 +5790,8 @@ impl MultiBufferSnapshot {
.and_then(|(buffer, _)| buffer.file())
}
pub fn language_at<T: ToOffset>(&self, point: T) -> Option<&Arc<Language>> {
self.point_to_buffer_offset(point)
pub fn language_at<T: ToOffset>(&self, offset: T) -> Option<&Arc<Language>> {
self.point_to_buffer_offset(offset)
.and_then(|(buffer, offset)| buffer.language_at(offset))
}
@@ -5992,13 +5991,27 @@ impl MultiBufferSnapshot {
theme: Option<&SyntaxTheme>,
) -> Option<(BufferId, Vec<OutlineItem<Anchor>>)> {
let anchor = self.anchor_before(offset);
let excerpt_id = anchor.excerpt_id;
let excerpt = self.excerpt(excerpt_id)?;
let buffer_id = excerpt.buffer_id;
let excerpt @ &Excerpt {
id: excerpt_id,
buffer_id,
ref buffer,
..
} = self.excerpt(anchor.excerpt_id)?;
if cfg!(debug_assertions) {
match anchor.buffer_id {
// we clearly are hitting this according to sentry, but in what situations can this occur?
Some(anchor_buffer_id) => {
assert_eq!(
anchor_buffer_id, buffer_id,
"anchor {anchor:?} does not match with resolved excerpt {excerpt:?}"
)
}
None => assert_eq!(anchor, Anchor::max()),
}
};
Some((
buffer_id,
excerpt
.buffer
buffer
.symbols_containing(anchor.text_anchor, theme)
.into_iter()
.flat_map(|item| {
@@ -6114,6 +6127,12 @@ impl MultiBufferSnapshot {
}
}
/// Returns the excerpt for the given id. The returned excerpt is guaranteed
/// to have the same excerpt id as the one passed in, with the exception of
/// `ExcerptId::max()`.
///
/// Callers of this function should generally use the resulting excerpt's `id` field
/// afterwards.
fn excerpt(&self, excerpt_id: ExcerptId) -> Option<&Excerpt> {
let mut cursor = self.excerpts.cursor::<Option<&Locator>>(());
let locator = self.excerpt_locator_for_id(excerpt_id);
@@ -3050,7 +3050,9 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
for _ in 0..10 {
let end_ix = rng.random_range(0..=text_rope.len());
let end_ix = text_rope.floor_char_boundary(end_ix);
let start_ix = rng.random_range(0..=end_ix);
let start_ix = text_rope.floor_char_boundary(start_ix);
assert_eq!(
snapshot
.bytes_in_range(MultiBufferOffset(start_ix)..MultiBufferOffset(end_ix))
+9 -4
View File
@@ -57,7 +57,7 @@ impl MultiBuffer {
let snapshot = self.read(cx);
let excerpt = snapshot.excerpt(*excerpt_id)?;
Some(Anchor::in_buffer(
*excerpt_id,
excerpt.id,
excerpt.buffer_id,
excerpt.range.context.start,
))
@@ -182,11 +182,16 @@ impl MultiBuffer {
};
let ids_to_expand = HashSet::from_iter(ids);
let mut excerpt_id_ = None;
let expanded_ranges = excerpt_ids.iter().filter_map(|excerpt_id| {
let excerpt = snapshot.excerpt(*excerpt_id)?;
let excerpt_id = excerpt.id;
if excerpt_id_.is_none() {
excerpt_id_ = Some(excerpt_id);
}
let mut context = excerpt.range.context.to_point(&excerpt.buffer);
if ids_to_expand.contains(excerpt_id) {
if ids_to_expand.contains(&excerpt_id) {
match direction {
ExpandExcerptDirection::Up => {
context.start.row = context.start.row.saturating_sub(line_count);
@@ -222,10 +227,10 @@ impl MultiBuffer {
}
merged_ranges.push(range)
}
let Some(excerpt_id) = excerpt_ids.first() else {
let Some(excerpt_id) = excerpt_id_ else {
continue;
};
let Some(buffer_id) = &snapshot.buffer_id_for_excerpt(*excerpt_id) else {
let Some(buffer_id) = &snapshot.buffer_id_for_excerpt(excerpt_id) else {
continue;
};