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

Lukas Wirth and Kirill Bulatov created

`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>

Change summary

crates/agent_ui/src/acp/message_editor.rs     | 27 ++-----
crates/agent_ui/src/inline_assistant.rs       |  7 -
crates/agent_ui/src/text_thread_editor.rs     | 55 +++++----------
crates/editor/src/editor.rs                   | 18 +---
crates/editor/src/element.rs                  |  4 
crates/editor/src/hover_links.rs              | 22 ++---
crates/editor/src/hover_popover.rs            |  9 -
crates/editor/src/items.rs                    |  3 
crates/editor/src/lsp_colors.rs               | 39 +++-------
crates/git_ui/src/conflict_view.rs            | 72 ++++++--------------
crates/language_tools/src/syntax_tree_view.rs |  7 -
crates/multi_buffer/src/multi_buffer.rs       | 63 ++++++++++++++---
crates/outline_panel/src/outline_panel.rs     | 14 ++--
13 files changed, 144 insertions(+), 196 deletions(-)

Detailed changes

crates/agent_ui/src/acp/message_editor.rs 🔗

@@ -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)
-                .unwrap();
-            let end = snapshot
-                .anchor_in_excerpt(excerpt_id, completion.replace_range.end)
+            let range = snapshot
+                .anchor_range_in_excerpt(excerpt_id, completion.replace_range)
                 .unwrap();
-            editor.edit([(start..end, completion.new_text)], cx);
+            editor.edit([(range, completion.new_text)], cx);
             (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
         });
 

crates/agent_ui/src/inline_assistant.rs 🔗

@@ -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")?;

crates/agent_ui/src/text_thread_editor.rs 🔗

@@ -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)
-                    .unwrap();
-                let end = buffer
-                    .anchor_in_excerpt(excerpt_id, section.range.end)
+                let range = buffer
+                    .anchor_range_in_excerpt(excerpt_id, section.range)
                     .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(),

crates/editor/src/editor.rs 🔗

@@ -5989,15 +5989,8 @@ impl Editor {
         let snapshot = self.buffer.read(cx).snapshot(cx);
         let newest_anchor = self.selections.newest_anchor();
         let replace_range_multibuffer = {
-            let excerpt = snapshot.excerpt_containing(newest_anchor.range()).unwrap();
-            let multibuffer_anchor = snapshot
-                .anchor_in_excerpt(excerpt.id(), buffer.anchor_before(replace_range.start))
-                .unwrap()
-                ..snapshot
-                    .anchor_in_excerpt(excerpt.id(), buffer.anchor_before(replace_range.end))
-                    .unwrap();
-            multibuffer_anchor.start.to_offset(&snapshot)
-                ..multibuffer_anchor.end.to_offset(&snapshot)
+            let mut excerpt = snapshot.excerpt_containing(newest_anchor.range()).unwrap();
+            excerpt.map_range_from_buffer(replace_range.clone())
         };
         if snapshot.buffer_id_for_anchor(newest_anchor.head()) != Some(buffer.remote_id()) {
             return None;
@@ -7981,9 +7974,10 @@ impl Editor {
         let edits = edits
             .into_iter()
             .flat_map(|(range, new_text)| {
-                let start = multibuffer.anchor_in_excerpt(excerpt_id, range.start)?;
-                let end = multibuffer.anchor_in_excerpt(excerpt_id, range.end)?;
-                Some((start..end, new_text))
+                Some((
+                    multibuffer.anchor_range_in_excerpt(excerpt_id, range)?,
+                    new_text,
+                ))
             })
             .collect::<Vec<_>>();
         if edits.is_empty() {

crates/editor/src/element.rs 🔗

@@ -7464,8 +7464,8 @@ impl EditorElement {
                         }
                         let clipped_start = range.start.max(&buffer_range.start, buffer);
                         let clipped_end = range.end.min(&buffer_range.end, buffer);
-                        let range = buffer_snapshot.anchor_in_excerpt(excerpt_id, clipped_start)?
-                            ..buffer_snapshot.anchor_in_excerpt(excerpt_id, clipped_end)?;
+                        let range = buffer_snapshot
+                            .anchor_range_in_excerpt(excerpt_id, clipped_start..clipped_end)?;
                         let start = range.start.to_display_point(display_snapshot);
                         let end = range.end.to_display_point(display_snapshot);
                         let selection_layout = SelectionLayout {

crates/editor/src/hover_links.rs 🔗

@@ -534,10 +534,9 @@ pub fn show_link_definition(
                     if let Some((url_range, url)) = find_url(&buffer, text_anchor, cx.clone()) {
                         this.read_with(cx, |_, _| {
                             let range = maybe!({
-                                let start =
-                                    snapshot.anchor_in_excerpt(excerpt_id, url_range.start)?;
-                                let end = snapshot.anchor_in_excerpt(excerpt_id, url_range.end)?;
-                                Some(RangeInEditor::Text(start..end))
+                                let range =
+                                    snapshot.anchor_range_in_excerpt(excerpt_id, url_range)?;
+                                Some(RangeInEditor::Text(range))
                             });
                             (range, vec![HoverLink::Url(url)])
                         })
@@ -546,10 +545,9 @@ pub fn show_link_definition(
                         find_file(&buffer, project.clone(), text_anchor, cx).await
                     {
                         let range = maybe!({
-                            let start =
-                                snapshot.anchor_in_excerpt(excerpt_id, filename_range.start)?;
-                            let end = snapshot.anchor_in_excerpt(excerpt_id, filename_range.end)?;
-                            Some(RangeInEditor::Text(start..end))
+                            let range =
+                                snapshot.anchor_range_in_excerpt(excerpt_id, filename_range)?;
+                            Some(RangeInEditor::Text(range))
                         });
 
                         Some((range, vec![HoverLink::File(filename)]))
@@ -562,13 +560,11 @@ pub fn show_link_definition(
                                 (
                                     definition_result.iter().find_map(|link| {
                                         link.origin.as_ref().and_then(|origin| {
-                                            let start = snapshot.anchor_in_excerpt(
+                                            let range = snapshot.anchor_range_in_excerpt(
                                                 excerpt_id,
-                                                origin.range.start,
+                                                origin.range.clone(),
                                             )?;
-                                            let end = snapshot
-                                                .anchor_in_excerpt(excerpt_id, origin.range.end)?;
-                                            Some(RangeInEditor::Text(start..end))
+                                            Some(RangeInEditor::Text(range))
                                         })
                                     }),
                                     definition_result.into_iter().map(HoverLink::Text).collect(),

crates/editor/src/hover_popover.rs 🔗

@@ -467,13 +467,10 @@ fn show_hover(
                 let range = hover_result
                     .range
                     .and_then(|range| {
-                        let start = snapshot
+                        let range = snapshot
                             .buffer_snapshot()
-                            .anchor_in_excerpt(excerpt_id, range.start)?;
-                        let end = snapshot
-                            .buffer_snapshot()
-                            .anchor_in_excerpt(excerpt_id, range.end)?;
-                        Some(start..end)
+                            .anchor_range_in_excerpt(excerpt_id, range)?;
+                        Some(range)
                     })
                     .or_else(|| {
                         let snapshot = &snapshot.buffer_snapshot();

crates/editor/src/items.rs 🔗

@@ -364,10 +364,9 @@ impl FollowableItem for Editor {
     ) {
         let buffer = self.buffer.read(cx);
         let buffer = buffer.read(cx);
-        let Some((excerpt_id, _, _)) = buffer.as_singleton() else {
+        let Some(position) = buffer.as_singleton_anchor(location) else {
             return;
         };
-        let position = buffer.anchor_in_excerpt(*excerpt_id, location).unwrap();
         let selection = Selection {
             id: 0,
             reversed: false,

crates/editor/src/lsp_colors.rs 🔗

@@ -251,25 +251,14 @@ impl Editor {
                                     {
                                         continue;
                                     }
-                                    let Some(color_start_anchor) = multi_buffer_snapshot
-                                        .anchor_in_excerpt(
-                                            *excerpt_id,
-                                            buffer_snapshot.anchor_before(
-                                                buffer_snapshot
-                                                    .clip_point_utf16(color_start, Bias::Left),
-                                            ),
-                                        )
-                                    else {
-                                        continue;
-                                    };
-                                    let Some(color_end_anchor) = multi_buffer_snapshot
-                                        .anchor_in_excerpt(
-                                            *excerpt_id,
-                                            buffer_snapshot.anchor_after(
-                                                buffer_snapshot
-                                                    .clip_point_utf16(color_end, Bias::Right),
-                                            ),
-                                        )
+                                    let start = buffer_snapshot.anchor_before(
+                                        buffer_snapshot.clip_point_utf16(color_start, Bias::Left),
+                                    );
+                                    let end = buffer_snapshot.anchor_after(
+                                        buffer_snapshot.clip_point_utf16(color_end, Bias::Right),
+                                    );
+                                    let Some(range) = multi_buffer_snapshot
+                                        .anchor_range_in_excerpt(*excerpt_id, start..end)
                                     else {
                                         continue;
                                     };
@@ -285,16 +274,14 @@ impl Editor {
                                         new_buffer_colors.binary_search_by(|(probe, _)| {
                                             probe
                                                 .start
-                                                .cmp(&color_start_anchor, &multi_buffer_snapshot)
+                                                .cmp(&range.start, &multi_buffer_snapshot)
                                                 .then_with(|| {
-                                                    probe.end.cmp(
-                                                        &color_end_anchor,
-                                                        &multi_buffer_snapshot,
-                                                    )
+                                                    probe
+                                                        .end
+                                                        .cmp(&range.end, &multi_buffer_snapshot)
                                                 })
                                         });
-                                    new_buffer_colors
-                                        .insert(i, (color_start_anchor..color_end_anchor, color));
+                                    new_buffer_colors.insert(i, (range, color));
                                     break;
                                 }
                             }

crates/git_ui/src/conflict_view.rs 🔗

@@ -234,11 +234,7 @@ fn conflicts_updated(
                 continue;
             };
             let excerpt_id = *excerpt_id;
-            let Some(range) = snapshot
-                .anchor_in_excerpt(excerpt_id, conflict_range.start)
-                .zip(snapshot.anchor_in_excerpt(excerpt_id, conflict_range.end))
-                .map(|(start, end)| start..end)
-            else {
+            let Some(range) = snapshot.anchor_range_in_excerpt(excerpt_id, conflict_range) else {
                 continue;
             };
             removed_highlighted_ranges.push(range.clone());
@@ -321,27 +317,12 @@ fn update_conflict_highlighting(
     buffer: &editor::MultiBufferSnapshot,
     excerpt_id: editor::ExcerptId,
     cx: &mut Context<Editor>,
-) {
+) -> Option<()> {
     log::debug!("update conflict highlighting for {conflict:?}");
 
-    let outer_start = buffer
-        .anchor_in_excerpt(excerpt_id, conflict.range.start)
-        .unwrap();
-    let outer_end = buffer
-        .anchor_in_excerpt(excerpt_id, conflict.range.end)
-        .unwrap();
-    let our_start = buffer
-        .anchor_in_excerpt(excerpt_id, conflict.ours.start)
-        .unwrap();
-    let our_end = buffer
-        .anchor_in_excerpt(excerpt_id, conflict.ours.end)
-        .unwrap();
-    let their_start = buffer
-        .anchor_in_excerpt(excerpt_id, conflict.theirs.start)
-        .unwrap();
-    let their_end = buffer
-        .anchor_in_excerpt(excerpt_id, conflict.theirs.end)
-        .unwrap();
+    let outer = buffer.anchor_range_in_excerpt(excerpt_id, conflict.range.clone())?;
+    let ours = buffer.anchor_range_in_excerpt(excerpt_id, conflict.ours.clone())?;
+    let theirs = buffer.anchor_range_in_excerpt(excerpt_id, conflict.theirs.clone())?;
 
     let ours_background = cx.theme().colors().version_control_conflict_marker_ours;
     let theirs_background = cx.theme().colors().version_control_conflict_marker_theirs;
@@ -352,32 +333,29 @@ fn update_conflict_highlighting(
     };
 
     editor.insert_gutter_highlight::<ConflictsOuter>(
-        outer_start..their_end,
+        outer.start..theirs.end,
         |cx| cx.theme().colors().editor_background,
         cx,
     );
 
     // Prevent diff hunk highlighting within the entire conflict region.
-    editor.highlight_rows::<ConflictsOuter>(outer_start..outer_end, theirs_background, options, cx);
-    editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
+    editor.highlight_rows::<ConflictsOuter>(outer.clone(), theirs_background, options, cx);
+    editor.highlight_rows::<ConflictsOurs>(ours.clone(), ours_background, options, cx);
     editor.highlight_rows::<ConflictsOursMarker>(
-        outer_start..our_start,
+        outer.start..ours.start,
         ours_background,
         options,
         cx,
     );
-    editor.highlight_rows::<ConflictsTheirs>(
-        their_start..their_end,
-        theirs_background,
-        options,
-        cx,
-    );
+    editor.highlight_rows::<ConflictsTheirs>(theirs.clone(), theirs_background, options, cx);
     editor.highlight_rows::<ConflictsTheirsMarker>(
-        their_end..outer_end,
+        theirs.end..outer.end,
         theirs_background,
         options,
         cx,
     );
+
+    Some(())
 }
 
 fn render_conflict_buttons(
@@ -488,20 +466,16 @@ pub(crate) fn resolve_conflict(
                     })
                     .ok()?;
                 let &(_, block_id) = &state.block_ids[ix];
-                let start = snapshot
-                    .anchor_in_excerpt(excerpt_id, resolved_conflict.range.start)
-                    .unwrap();
-                let end = snapshot
-                    .anchor_in_excerpt(excerpt_id, resolved_conflict.range.end)
-                    .unwrap();
-
-                editor.remove_gutter_highlights::<ConflictsOuter>(vec![start..end], cx);
-
-                editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx);
-                editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx);
-                editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx);
-                editor.remove_highlighted_rows::<ConflictsOursMarker>(vec![start..end], cx);
-                editor.remove_highlighted_rows::<ConflictsTheirsMarker>(vec![start..end], cx);
+                let range =
+                    snapshot.anchor_range_in_excerpt(excerpt_id, resolved_conflict.range)?;
+
+                editor.remove_gutter_highlights::<ConflictsOuter>(vec![range.clone()], cx);
+
+                editor.remove_highlighted_rows::<ConflictsOuter>(vec![range.clone()], cx);
+                editor.remove_highlighted_rows::<ConflictsOurs>(vec![range.clone()], cx);
+                editor.remove_highlighted_rows::<ConflictsTheirs>(vec![range.clone()], cx);
+                editor.remove_highlighted_rows::<ConflictsOursMarker>(vec![range.clone()], cx);
+                editor.remove_highlighted_rows::<ConflictsTheirsMarker>(vec![range], cx);
                 editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
                 Some((workspace, project, multibuffer, buffer))
             })

crates/language_tools/src/syntax_tree_view.rs 🔗

@@ -356,12 +356,7 @@ impl SyntaxTreeView {
         let multibuffer = editor_state.editor.read(cx).buffer();
         let multibuffer = multibuffer.read(cx).snapshot(cx);
         let excerpt_id = buffer_state.excerpt_id;
-        let range = multibuffer
-            .anchor_in_excerpt(excerpt_id, range.start)
-            .unwrap()
-            ..multibuffer
-                .anchor_in_excerpt(excerpt_id, range.end)
-                .unwrap();
+        let range = multibuffer.anchor_range_in_excerpt(excerpt_id, range)?;
 
         // Update the editor with the anchor range.
         editor_state.editor.update(cx, |editor, cx| {

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -5230,8 +5230,30 @@ impl MultiBufferSnapshot {
         }
     }
 
+    /// Wraps the [`text::Anchor`] in a [`multi_buffer::Anchor`] if this multi-buffer is a singleton.
+    pub fn as_singleton_anchor(&self, text_anchor: text::Anchor) -> Option<Anchor> {
+        let (excerpt, buffer, _) = self.as_singleton()?;
+        Some(Anchor::in_buffer(*excerpt, buffer, text_anchor))
+    }
+
     /// Returns an anchor for the given excerpt and text anchor,
-    /// returns None if the excerpt_id is no longer valid.
+    /// Returns [`None`] if the excerpt_id is no longer valid or the text anchor range is out of excerpt's bounds.
+    pub fn anchor_range_in_excerpt(
+        &self,
+        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)?;
+
+        Some(
+            self.anchor_in_excerpt_(excerpt, text_anchor.start)?
+                ..self.anchor_in_excerpt_(excerpt, text_anchor.end)?,
+        )
+    }
+
+    /// Returns an anchor for the given excerpt and text anchor,
+    /// Returns [`None`] if the excerpt_id is no longer valid or the text anchor range is out of excerpt's bounds.
     pub fn anchor_in_excerpt(
         &self,
         excerpt_id: ExcerptId,
@@ -5239,8 +5261,32 @@ impl MultiBufferSnapshot {
     ) -> Option<Anchor> {
         let excerpt_id = self.latest_excerpt_id(excerpt_id);
         let excerpt = self.excerpt(excerpt_id)?;
+        self.anchor_in_excerpt_(excerpt, text_anchor)
+    }
+
+    fn anchor_in_excerpt_(&self, excerpt: &Excerpt, text_anchor: text::Anchor) -> Option<Anchor> {
+        match text_anchor.buffer_id {
+            Some(buffer_id) if buffer_id == excerpt.buffer_id => (),
+            Some(_) => return None,
+            None if text_anchor == text::Anchor::MAX || text_anchor == text::Anchor::MIN => {
+                return Some(Anchor::in_buffer(
+                    excerpt.id,
+                    excerpt.buffer_id,
+                    text_anchor,
+                ));
+            }
+            None => return None,
+        }
+
+        let context = &excerpt.range.context;
+        if context.start.cmp(&text_anchor, &excerpt.buffer).is_gt()
+            || context.end.cmp(&text_anchor, &excerpt.buffer).is_lt()
+        {
+            return None;
+        }
+
         Some(Anchor::in_buffer(
-            excerpt_id,
+            excerpt.id,
             excerpt.buffer_id,
             text_anchor,
         ))
@@ -6075,22 +6121,15 @@ impl MultiBufferSnapshot {
                 .flat_map(|item| {
                     Some(OutlineItem {
                         depth: item.depth,
-                        range: self.anchor_in_excerpt(*excerpt_id, item.range.start)?
-                            ..self.anchor_in_excerpt(*excerpt_id, item.range.end)?,
+                        range: self.anchor_range_in_excerpt(*excerpt_id, item.range)?,
                         text: item.text,
                         highlight_ranges: item.highlight_ranges,
                         name_ranges: item.name_ranges,
                         body_range: item.body_range.and_then(|body_range| {
-                            Some(
-                                self.anchor_in_excerpt(*excerpt_id, body_range.start)?
-                                    ..self.anchor_in_excerpt(*excerpt_id, body_range.end)?,
-                            )
+                            self.anchor_range_in_excerpt(*excerpt_id, body_range)
                         }),
                         annotation_range: item.annotation_range.and_then(|annotation_range| {
-                            Some(
-                                self.anchor_in_excerpt(*excerpt_id, annotation_range.start)?
-                                    ..self.anchor_in_excerpt(*excerpt_id, annotation_range.end)?,
-                            )
+                            self.anchor_range_in_excerpt(*excerpt_id, annotation_range)
                         }),
                     })
                 })

crates/outline_panel/src/outline_panel.rs 🔗

@@ -3192,13 +3192,13 @@ impl OutlinePanel {
             .into_iter()
             .flat_map(|excerpt| excerpt.iter_outlines())
             .flat_map(|outline| {
-                let start = multi_buffer_snapshot
-                    .anchor_in_excerpt(excerpt_id, outline.range.start)?
-                    .to_display_point(&editor_snapshot);
-                let end = multi_buffer_snapshot
-                    .anchor_in_excerpt(excerpt_id, outline.range.end)?
-                    .to_display_point(&editor_snapshot);
-                Some((start..end, outline))
+                let range = multi_buffer_snapshot
+                    .anchor_range_in_excerpt(excerpt_id, outline.range.clone())?;
+                Some((
+                    range.start.to_display_point(&editor_snapshot)
+                        ..range.end.to_display_point(&editor_snapshot),
+                    outline,
+                ))
             })
             .collect::<Vec<_>>();