Fix panic when `editor::OpenSelectionsInMultibuffer` only has pending selection (#32842)

Michael Sloan created

On the panics dashboard, saw this panic of `There must be at least one
selection` in `open_locations_in_multibuffer`. Only seems to have
happened once in the past month.

Fix is to include the pending selection. Since `selections.all()` cannot
provide anchor selections, added `selections.all_anchors()` which only
really does any work if there is a pending selection.

Also fixes a corner case in jump-to-definitions where if the definition
is `HoverLink::InlayHint` and the `compute_target_location` fails for
all definitions it could potentially also trigger this case (and return
`Navigated::Yes` instead of `Navigated::No`

Release Notes:

- N/A

Change summary

crates/editor/src/editor.rs                | 24 ++++++--
crates/editor/src/selections_collection.rs | 61 ++++++++++++++++-------
2 files changed, 60 insertions(+), 25 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -15130,16 +15130,21 @@ impl Editor {
                     })
                     .context("location tasks preparation")?;
 
-                let locations = future::join_all(location_tasks)
+                let locations: Vec<Location> = future::join_all(location_tasks)
                     .await
                     .into_iter()
                     .filter_map(|location| location.transpose())
                     .collect::<Result<_>>()
                     .context("location tasks")?;
 
+                if locations.is_empty() {
+                    return Ok(Navigated::No);
+                }
+
                 let Some(workspace) = workspace else {
                     return Ok(Navigated::No);
                 };
+
                 let opened = workspace
                     .update_in(cx, |workspace, window, cx| {
                         Self::open_locations_in_multibuffer(
@@ -15303,6 +15308,11 @@ impl Editor {
         window: &mut Window,
         cx: &mut Context<Workspace>,
     ) {
+        if locations.is_empty() {
+            log::error!("bug: open_locations_in_multibuffer called with empty list of locations");
+            return;
+        }
+
         // If there are multiple definitions, open them in a multibuffer
         locations.sort_by_key(|location| location.buffer.read(cx).remote_id());
         let mut locations = locations.into_iter().peekable();
@@ -18289,18 +18299,18 @@ impl Editor {
             return;
         };
 
+        let title = multibuffer.title(cx).to_string();
+
         let locations = self
             .selections
-            .disjoint_anchors()
-            .iter()
-            .map(|range| Location {
+            .all_anchors(cx)
+            .into_iter()
+            .map(|selection| Location {
                 buffer: buffer.clone(),
-                range: range.start.text_anchor..range.end.text_anchor,
+                range: selection.start.text_anchor..selection.end.text_anchor,
             })
             .collect::<Vec<_>>();
 
-        let title = multibuffer.title(cx).to_string();
-
         cx.spawn_in(window, async move |_, cx| {
             workspace.update_in(cx, |workspace, window, cx| {
                 Self::open_locations_in_multibuffer(

crates/editor/src/selections_collection.rs 🔗

@@ -81,9 +81,9 @@ impl SelectionsCollection {
         count
     }
 
-    /// The non-pending, non-overlapping selections. There could still be a pending
-    /// selection that overlaps these if the mouse is being dragged, etc. Returned as
-    /// selections over Anchors.
+    /// The non-pending, non-overlapping selections. There could be a pending selection that
+    /// overlaps these if the mouse is being dragged, etc. This could also be empty if there is a
+    /// pending selection. Returned as selections over Anchors.
     pub fn disjoint_anchors(&self) -> Arc<[Selection<Anchor>]> {
         self.disjoint.clone()
     }
@@ -94,6 +94,20 @@ impl SelectionsCollection {
         (0..disjoint.len()).map(move |ix| disjoint[ix].range())
     }
 
+    /// Non-overlapping selections using anchors, including the pending selection.
+    pub fn all_anchors(&self, cx: &mut App) -> Arc<[Selection<Anchor>]> {
+        if self.pending.is_none() {
+            self.disjoint_anchors()
+        } else {
+            let all_offset_selections = self.all::<usize>(cx);
+            let buffer = self.buffer(cx);
+            all_offset_selections
+                .into_iter()
+                .map(|selection| selection_to_anchor_selection(selection, &buffer))
+                .collect()
+        }
+    }
+
     pub fn pending_anchor(&self) -> Option<Selection<Anchor>> {
         self.pending
             .as_ref()
@@ -534,21 +548,11 @@ impl<'a> MutableSelectionsCollection<'a> {
             }
         }
 
-        self.collection.disjoint = Arc::from_iter(selections.into_iter().map(|selection| {
-            let end_bias = if selection.end > selection.start {
-                Bias::Left
-            } else {
-                Bias::Right
-            };
-            Selection {
-                id: selection.id,
-                start: buffer.anchor_after(selection.start),
-                end: buffer.anchor_at(selection.end, end_bias),
-                reversed: selection.reversed,
-                goal: selection.goal,
-            }
-        }));
-
+        self.collection.disjoint = Arc::from_iter(
+            selections
+                .into_iter()
+                .map(|selection| selection_to_anchor_selection(selection, &buffer)),
+        );
         self.collection.pending = None;
         self.selections_changed = true;
     }
@@ -880,6 +884,27 @@ impl DerefMut for MutableSelectionsCollection<'_> {
     }
 }
 
+fn selection_to_anchor_selection<T>(
+    selection: Selection<T>,
+    buffer: &MultiBufferSnapshot,
+) -> Selection<Anchor>
+where
+    T: ToOffset + Ord,
+{
+    let end_bias = if selection.end > selection.start {
+        Bias::Left
+    } else {
+        Bias::Right
+    };
+    Selection {
+        id: selection.id,
+        start: buffer.anchor_after(selection.start),
+        end: buffer.anchor_at(selection.end, end_bias),
+        reversed: selection.reversed,
+        goal: selection.goal,
+    }
+}
+
 // Panics if passed selections are not in order
 fn resolve_selections_display<'a>(
     selections: impl 'a + IntoIterator<Item = &'a Selection<Anchor>>,