multi_buffer: Make anchor_in_excerpt fallible for bad text anchors (#40496)

`MultiBuffer::anchor_in_excerpt` currently just wraps the given text
anchor in a multibuffer anchor. This allows one to get a multibuffer
anchor that points outside its excerpt which is basically never what one
wants. This PR now does a bounds check and returns `None` if the given
text anchor is not within the bounds of the excerpt.

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
Lukas Wirth
2025-10-17 15:40:37 +00:00
committed by GitHub
co-authored by Kirill Bulatov
parent 7f9898a90b
commit 83bfe2ff7b
13 changed files with 142 additions and 194 deletions
+8 -19
View File
@@ -292,15 +292,10 @@ impl MessageEditor {
let snapshot = self
.editor
.update(cx, |editor, cx| editor.snapshot(window, cx));
let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot().as_singleton() else {
return Task::ready(());
};
let Some(start_anchor) = snapshot
.buffer_snapshot()
.anchor_in_excerpt(*excerpt_id, start)
else {
let Some(start_anchor) = snapshot.buffer_snapshot().as_singleton_anchor(start) else {
return Task::ready(());
};
let excerpt_id = start_anchor.excerpt_id;
let end_anchor = snapshot
.buffer_snapshot()
.anchor_before(start_anchor.to_offset(&snapshot.buffer_snapshot()) + content_len + 1);
@@ -332,7 +327,7 @@ impl MessageEditor {
})
.shared();
insert_crease_for_mention(
*excerpt_id,
excerpt_id,
start,
content_len,
mention_uri.name().into(),
@@ -344,7 +339,7 @@ impl MessageEditor {
)
} else {
insert_crease_for_mention(
*excerpt_id,
excerpt_id,
start,
content_len,
crease_text,
@@ -546,10 +541,7 @@ impl MessageEditor {
cx: &mut Context<Self>,
) {
let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
let Some((&excerpt_id, _, _)) = snapshot.as_singleton() else {
return;
};
let Some(start) = snapshot.anchor_in_excerpt(excerpt_id, source_range.start) else {
let Some(start) = snapshot.as_singleton_anchor(source_range.start) else {
return;
};
@@ -1694,13 +1686,10 @@ mod tests {
editor.update_in(cx, |editor, window, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let start = snapshot
.anchor_in_excerpt(excerpt_id, completion.replace_range.start)
let range = snapshot
.anchor_range_in_excerpt(excerpt_id, completion.replace_range)
.unwrap();
let end = snapshot
.anchor_in_excerpt(excerpt_id, completion.replace_range.end)
.unwrap();
editor.edit([(start..end, completion.new_text)], cx);
editor.edit([(range, completion.new_text)], cx);
(completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
});
+1 -6
View File
@@ -1878,12 +1878,7 @@ impl CodeActionProvider for AssistantCodeActionProvider {
}
let multibuffer_snapshot = multibuffer.read(cx);
Some(
multibuffer_snapshot
.anchor_in_excerpt(excerpt_id, action.range.start)?
..multibuffer_snapshot
.anchor_in_excerpt(excerpt_id, action.range.end)?,
)
multibuffer_snapshot.anchor_range_in_excerpt(excerpt_id, action.range)
})
})?
.context("invalid range")?;
+19 -36
View File
@@ -602,10 +602,8 @@ impl TextThreadEditor {
if let Some((crease_id, start)) = self.pending_thought_process.take() {
self.editor.update(cx, |editor, cx| {
let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let (excerpt_id, _, _) = multi_buffer_snapshot.as_singleton().unwrap();
let start_anchor = multi_buffer_snapshot
.anchor_in_excerpt(*excerpt_id, start)
.unwrap();
let start_anchor =
multi_buffer_snapshot.as_singleton_anchor(start).unwrap();
editor.display_map.update(cx, |display_map, cx| {
display_map.unfold_intersecting(
@@ -696,13 +694,10 @@ impl TextThreadEditor {
}
};
let start = buffer
.anchor_in_excerpt(excerpt_id, command.source_range.start)
let range = buffer
.anchor_range_in_excerpt(excerpt_id, command.source_range.clone())
.unwrap();
let end = buffer
.anchor_in_excerpt(excerpt_id, command.source_range.end)
.unwrap();
Crease::inline(start..end, placeholder, render_toggle, render_trailer)
Crease::inline(range, placeholder, render_toggle, render_trailer)
}),
cx,
);
@@ -773,14 +768,11 @@ impl TextThreadEditor {
let (&excerpt_id, _buffer_id, _buffer_snapshot) =
buffer.as_singleton().unwrap();
let start = buffer
.anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start)
.unwrap();
let end = buffer
.anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end)
let range = buffer
.anchor_range_in_excerpt(excerpt_id, invoked_slash_command.range.clone())
.unwrap();
editor.remove_folds_with_type(
&[start..end],
&[range],
TypeId::of::<PendingSlashCommand>(),
false,
cx,
@@ -797,14 +789,11 @@ impl TextThreadEditor {
let (&excerpt_id, _buffer_id, _buffer_snapshot) =
buffer.as_singleton().unwrap();
let context = self.context.downgrade();
let crease_start = buffer
.anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start)
.unwrap();
let crease_end = buffer
.anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end)
let range = buffer
.anchor_range_in_excerpt(excerpt_id, invoked_slash_command.range.clone())
.unwrap();
let crease = Crease::inline(
crease_start..crease_end,
range,
invoked_slash_command_fold_placeholder(command_id, context),
fold_toggle("invoked-slash-command"),
|_row, _folded, _window, _cx| Empty.into_any(),
@@ -842,17 +831,14 @@ impl TextThreadEditor {
let mut buffer_rows_to_fold = BTreeSet::new();
let mut creases = Vec::new();
for (section, status) in sections {
let start = buffer
.anchor_in_excerpt(excerpt_id, section.range.start)
let range = buffer
.anchor_range_in_excerpt(excerpt_id, section.range)
.unwrap();
let end = buffer
.anchor_in_excerpt(excerpt_id, section.range.end)
.unwrap();
let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
let buffer_row = MultiBufferRow(range.start.to_point(&buffer).row);
buffer_rows_to_fold.insert(buffer_row);
creases.push(
Crease::inline(
start..end,
range,
FoldPlaceholder {
render: render_thought_process_fold_icon_button(
cx.entity().downgrade(),
@@ -894,17 +880,14 @@ impl TextThreadEditor {
let mut buffer_rows_to_fold = BTreeSet::new();
let mut creases = Vec::new();
for section in sections {
let start = buffer
.anchor_in_excerpt(excerpt_id, section.range.start)
let range = buffer
.anchor_range_in_excerpt(excerpt_id, section.range)
.unwrap();
let end = buffer
.anchor_in_excerpt(excerpt_id, section.range.end)
.unwrap();
let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
let buffer_row = MultiBufferRow(range.start.to_point(&buffer).row);
buffer_rows_to_fold.insert(buffer_row);
creases.push(
Crease::inline(
start..end,
range,
FoldPlaceholder {
render: render_fold_icon_button(
cx.entity().downgrade(),