Select the closest outline item when the outline view's query is empty

Antonio Scandurra and Max Brunsfeld created

Co-Authored-By: Max Brunsfeld <max@zed.dev>

Change summary

crates/editor/src/editor.rs   |  6 +++-
crates/language/src/buffer.rs |  6 ++++
crates/outline/src/outline.rs | 38 +++++++++++++++++++++++++++++++++---
3 files changed, 43 insertions(+), 7 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -28,8 +28,10 @@ use language::{
     BracketPair, Buffer, Diagnostic, DiagnosticSeverity, Language, Point, Selection, SelectionGoal,
     TransactionId,
 };
-pub use multi_buffer::{Anchor, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset, ToPoint};
-use multi_buffer::{AnchorRangeExt, MultiBufferChunks, MultiBufferSnapshot};
+pub use multi_buffer::{
+    Anchor, AnchorRangeExt, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset, ToPoint,
+};
+use multi_buffer::{MultiBufferChunks, MultiBufferSnapshot};
 use postage::watch;
 use serde::{Deserialize, Serialize};
 use smallvec::SmallVec;

crates/language/src/buffer.rs 🔗

@@ -1917,7 +1917,11 @@ impl BufferSnapshot {
             })
             .collect::<Vec<_>>();
 
-        Some(Outline::new(items))
+        if items.is_empty() {
+            None
+        } else {
+            Some(Outline::new(items))
+        }
     }
 
     pub fn enclosing_bracket_ranges<T: ToOffset>(

crates/outline/src/outline.rs 🔗

@@ -1,4 +1,4 @@
-use editor::{Anchor, Editor, EditorSettings};
+use editor::{Anchor, AnchorRangeExt, Editor, EditorSettings};
 use fuzzy::StringMatch;
 use gpui::{
     action,
@@ -14,7 +14,10 @@ use gpui::{
 use language::Outline;
 use ordered_float::OrderedFloat;
 use postage::watch;
-use std::{cmp, sync::Arc};
+use std::{
+    cmp::{self, Reverse},
+    sync::Arc,
+};
 use workspace::{Settings, Workspace};
 
 action!(Toggle);
@@ -34,6 +37,7 @@ pub fn init(cx: &mut MutableAppContext) {
 
 struct OutlineView {
     handle: WeakViewHandle<Self>,
+    editor: ViewHandle<Editor>,
     outline: Outline<Anchor>,
     selected_match_index: usize,
     matches: Vec<StringMatch>,
@@ -91,6 +95,7 @@ impl View for OutlineView {
 impl OutlineView {
     fn new(
         outline: Outline<Anchor>,
+        editor: ViewHandle<Editor>,
         settings: watch::Receiver<Settings>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
@@ -114,6 +119,7 @@ impl OutlineView {
             .detach();
         let mut this = Self {
             handle: cx.weak_handle(),
+            editor,
             matches: Default::default(),
             selected_match_index: 0,
             outline,
@@ -135,7 +141,7 @@ impl OutlineView {
         let buffer = editor.read(cx).buffer().read(cx).read(cx).outline();
         if let Some(outline) = buffer {
             workspace.toggle_modal(cx, |cx, workspace| {
-                cx.add_view(|cx| OutlineView::new(outline, workspace.settings(), cx))
+                cx.add_view(|cx| OutlineView::new(outline, editor, workspace.settings(), cx))
             })
         }
     }
@@ -185,7 +191,31 @@ impl OutlineView {
                     string: Default::default(),
                 })
                 .collect();
-            self.selected_match_index = 0;
+
+            let editor = self.editor.read(cx);
+            let buffer = editor.buffer().read(cx).read(cx);
+            let cursor_offset = editor.newest_selection::<usize>(&buffer).head();
+            self.selected_match_index = self
+                .outline
+                .items
+                .iter()
+                .enumerate()
+                .map(|(ix, item)| {
+                    let range = item.range.to_offset(&buffer);
+                    let distance_to_closest_endpoint = cmp::min(
+                        (range.start as isize - cursor_offset as isize).abs() as usize,
+                        (range.end as isize - cursor_offset as isize).abs() as usize,
+                    );
+                    let depth = if range.contains(&cursor_offset) {
+                        Some(item.depth)
+                    } else {
+                        None
+                    };
+                    (ix, depth, distance_to_closest_endpoint)
+                })
+                .max_by_key(|(_, depth, distance)| (*depth, Reverse(*distance)))
+                .unwrap()
+                .0;
         } else {
             self.matches = self.outline.search(&query, cx);
             self.selected_match_index = self