Use Picker in Outline view

Max Brunsfeld created

Change summary

Cargo.lock                                    |   1 
assets/keymaps/default.json                   |  12 -
crates/file_finder/src/file_finder.rs         |   7 
crates/outline/Cargo.toml                     |   1 
crates/outline/src/outline.rs                 | 224 +++++---------------
crates/picker/src/picker.rs                   | 112 +++++----
crates/project_symbols/src/project_symbols.rs |   9 
7 files changed, 130 insertions(+), 236 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3374,6 +3374,7 @@ dependencies = [
  "gpui",
  "language",
  "ordered-float",
+ "picker",
  "postage",
  "settings",
  "smol",

assets/keymaps/default.json 🔗

@@ -250,22 +250,10 @@
             "\n"
         ]
     },
-    "OutlineView": {
-        "escape": "outline::Toggle"
-    },
-    "ProjectSymbolsView": {
-        "escape": "project_symbols::Toggle"
-    },
-    "ThemeSelector": {
-        "escape": "theme_selector::Toggle"
-    },
     "GoToLine": {
         "escape": "go_to_line::Toggle",
         "enter": "go_to_line::Confirm"
     },
-    "FileFinder": {
-        "escape": "file_finder::Toggle"
-    },
     "ChatPanel": {
         "enter": "chat_panel::Send"
     },

crates/file_finder/src/file_finder.rs 🔗

@@ -113,14 +113,11 @@ impl FileFinder {
     }
 
     pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
-        cx.observe(&project, Self::project_updated).detach();
-
         let handle = cx.weak_handle();
-        let picker = cx.add_view(|cx| Picker::new(handle, cx));
-
+        cx.observe(&project, Self::project_updated).detach();
         Self {
             project,
-            picker,
+            picker: cx.add_view(|cx| Picker::new(handle, cx)),
             search_count: 0,
             latest_search_id: 0,
             latest_search_did_cancel: false,

crates/outline/Cargo.toml 🔗

@@ -12,6 +12,7 @@ editor = { path = "../editor" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
+picker = { path = "../picker" }
 settings = { path = "../settings" }
 text = { path = "../text" }
 workspace = { path = "../workspace" }

crates/outline/src/outline.rs 🔗

@@ -4,38 +4,31 @@ use editor::{
 };
 use fuzzy::StringMatch;
 use gpui::{
-    actions, elements::*, geometry::vector::Vector2F, keymap, AppContext, Axis, Entity,
-    MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle,
+    actions, elements::*, geometry::vector::Vector2F, AppContext, Entity, MutableAppContext,
+    RenderContext, Task, View, ViewContext, ViewHandle,
 };
 use language::Outline;
 use ordered_float::OrderedFloat;
+use picker::{Picker, PickerDelegate};
 use settings::Settings;
 use std::cmp::{self, Reverse};
-use workspace::{
-    menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev},
-    Workspace,
-};
+use workspace::Workspace;
 
 actions!(outline, [Toggle]);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(OutlineView::toggle);
-    cx.add_action(OutlineView::confirm);
-    cx.add_action(OutlineView::select_prev);
-    cx.add_action(OutlineView::select_next);
-    cx.add_action(OutlineView::select_first);
-    cx.add_action(OutlineView::select_last);
+    Picker::<OutlineView>::init(cx);
 }
 
 struct OutlineView {
-    handle: WeakViewHandle<Self>,
+    picker: ViewHandle<Picker<Self>>,
     active_editor: ViewHandle<Editor>,
     outline: Outline<Anchor>,
     selected_match_index: usize,
     prev_scroll_position: Option<Vector2F>,
     matches: Vec<StringMatch>,
-    query_editor: ViewHandle<Editor>,
-    list_state: UniformListState,
+    last_query: String,
 }
 
 pub enum Event {
@@ -55,38 +48,12 @@ impl View for OutlineView {
         "OutlineView"
     }
 
-    fn keymap_context(&self, _: &AppContext) -> keymap::Context {
-        let mut cx = Self::default_keymap_context();
-        cx.set.insert("menu".into());
-        cx
-    }
-
-    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let settings = cx.global::<Settings>();
-
-        Flex::new(Axis::Vertical)
-            .with_child(
-                Container::new(ChildView::new(&self.query_editor).boxed())
-                    .with_style(settings.theme.selector.input_editor.container)
-                    .boxed(),
-            )
-            .with_child(
-                FlexItem::new(self.render_matches(cx))
-                    .flex(1.0, false)
-                    .boxed(),
-            )
-            .contained()
-            .with_style(settings.theme.selector.container)
-            .constrained()
-            .with_max_width(800.0)
-            .with_max_height(1200.0)
-            .aligned()
-            .top()
-            .named("outline view")
+    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+        ChildView::new(self.picker.clone()).boxed()
     }
 
     fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
-        cx.focus(&self.query_editor);
+        cx.focus(&self.picker);
     }
 }
 
@@ -96,24 +63,16 @@ impl OutlineView {
         editor: ViewHandle<Editor>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        let query_editor = cx.add_view(|cx| {
-            Editor::single_line(Some(|theme| theme.selector.input_editor.clone()), cx)
-        });
-        cx.subscribe(&query_editor, Self::on_query_editor_event)
-            .detach();
-
-        let mut this = Self {
-            handle: cx.weak_handle(),
+        let handle = cx.weak_handle();
+        Self {
+            picker: cx.add_view(|cx| Picker::new(handle, cx).with_max_size(800., 1200.)),
+            last_query: Default::default(),
             matches: Default::default(),
             selected_match_index: 0,
             prev_scroll_position: Some(editor.update(cx, |editor, cx| editor.scroll_position(cx))),
             active_editor: editor,
             outline,
-            query_editor,
-            list_state: Default::default(),
-        };
-        this.update_matches(cx);
-        this
+        }
     }
 
     fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
@@ -137,34 +96,18 @@ impl OutlineView {
         }
     }
 
-    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
-        if self.selected_match_index > 0 {
-            self.select(self.selected_match_index - 1, true, false, cx);
-        }
-    }
-
-    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
-        if self.selected_match_index + 1 < self.matches.len() {
-            self.select(self.selected_match_index + 1, true, false, cx);
-        }
-    }
-
-    fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
-        self.select(0, true, false, cx);
-    }
-
-    fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
-        self.select(self.matches.len().saturating_sub(1), true, false, cx);
+    fn restore_active_editor(&mut self, cx: &mut MutableAppContext) {
+        self.active_editor.update(cx, |editor, cx| {
+            editor.highlight_rows(None);
+            if let Some(scroll_position) = self.prev_scroll_position {
+                editor.set_scroll_position(scroll_position, cx);
+            }
+        })
     }
 
-    fn select(&mut self, index: usize, navigate: bool, center: bool, cx: &mut ViewContext<Self>) {
-        self.selected_match_index = index;
-        self.list_state.scroll_to(if center {
-            ScrollTarget::Center(index)
-        } else {
-            ScrollTarget::Show(index)
-        });
-        if navigate {
+    fn set_selected_index(&mut self, ix: usize, navigate: bool, cx: &mut ViewContext<Self>) {
+        self.selected_match_index = ix;
+        if navigate && !self.matches.is_empty() {
             let selected_match = &self.matches[self.selected_match_index];
             let outline_item = &self.outline.items[selected_match.candidate_id];
             self.active_editor.update(cx, |active_editor, cx| {
@@ -181,27 +124,6 @@ impl OutlineView {
         cx.notify();
     }
 
-    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
-        self.prev_scroll_position.take();
-        self.active_editor.update(cx, |active_editor, cx| {
-            if let Some(rows) = active_editor.highlighted_rows() {
-                let snapshot = active_editor.snapshot(cx).display_snapshot;
-                let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
-                active_editor.select_ranges([position..position], Some(Autoscroll::Center), cx);
-            }
-        });
-        cx.emit(Event::Dismissed);
-    }
-
-    fn restore_active_editor(&mut self, cx: &mut MutableAppContext) {
-        self.active_editor.update(cx, |editor, cx| {
-            editor.highlight_rows(None);
-            if let Some(scroll_position) = self.prev_scroll_position {
-                editor.set_scroll_position(scroll_position, cx);
-            }
-        })
-    }
-
     fn on_event(
         workspace: &mut Workspace,
         _: ViewHandle<Self>,
@@ -212,24 +134,27 @@ impl OutlineView {
             Event::Dismissed => workspace.dismiss_modal(cx),
         }
     }
+}
 
-    fn on_query_editor_event(
-        &mut self,
-        _: ViewHandle<Editor>,
-        event: &editor::Event,
-        cx: &mut ViewContext<Self>,
-    ) {
-        match event {
-            editor::Event::Blurred => cx.emit(Event::Dismissed),
-            editor::Event::BufferEdited { .. } => self.update_matches(cx),
-            _ => {}
-        }
+impl PickerDelegate for OutlineView {
+    fn match_count(&self) -> usize {
+        self.matches.len()
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_match_index
     }
 
-    fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
+    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
+        self.set_selected_index(ix, true, cx);
+    }
+
+    fn center_selection_after_match_updates(&self) -> bool {
+        true
+    }
+
+    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
         let selected_index;
-        let navigate_to_selected_index;
-        let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
         if query.is_empty() {
             self.restore_active_editor(cx);
             self.matches = self
@@ -271,7 +196,6 @@ impl OutlineView {
                 .max_by_key(|(_, depth, distance)| (*depth, Reverse(*distance)))
                 .map(|(ix, _, _)| ix)
                 .unwrap_or(0);
-            navigate_to_selected_index = false;
         } else {
             self.matches = smol::block_on(self.outline.search(&query, cx.background().clone()));
             selected_index = self
@@ -281,57 +205,33 @@ impl OutlineView {
                 .max_by_key(|(_, m)| OrderedFloat(m.score))
                 .map(|(ix, _)| ix)
                 .unwrap_or(0);
-            navigate_to_selected_index = !self.matches.is_empty();
         }
-        self.select(selected_index, navigate_to_selected_index, true, cx);
+        self.last_query = query;
+        self.set_selected_index(selected_index, false, cx);
+        Task::ready(())
     }
 
-    fn render_matches(&self, cx: &AppContext) -> ElementBox {
-        if self.matches.is_empty() {
-            let settings = cx.global::<Settings>();
-            return Container::new(
-                Label::new(
-                    "No matches".into(),
-                    settings.theme.selector.empty.label.clone(),
-                )
-                .boxed(),
-            )
-            .with_style(settings.theme.selector.empty.container)
-            .named("empty matches");
-        }
-
-        let handle = self.handle.clone();
-        let list = UniformList::new(
-            self.list_state.clone(),
-            self.matches.len(),
-            move |mut range, items, cx| {
-                let cx = cx.as_ref();
-                let view = handle.upgrade(cx).unwrap();
-                let view = view.read(cx);
-                let start = range.start;
-                range.end = cmp::min(range.end, view.matches.len());
-                items.extend(
-                    view.matches[range]
-                        .iter()
-                        .enumerate()
-                        .map(move |(ix, m)| view.render_match(m, start + ix, cx)),
-                );
-            },
-        );
+    fn confirm(&mut self, cx: &mut ViewContext<Self>) {
+        self.prev_scroll_position.take();
+        self.active_editor.update(cx, |active_editor, cx| {
+            if let Some(rows) = active_editor.highlighted_rows() {
+                let snapshot = active_editor.snapshot(cx).display_snapshot;
+                let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
+                active_editor.select_ranges([position..position], Some(Autoscroll::Center), cx);
+            }
+        });
+        cx.emit(Event::Dismissed);
+    }
 
-        Container::new(list.boxed())
-            .with_margin_top(6.0)
-            .named("matches")
+    fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+        self.restore_active_editor(cx);
+        cx.emit(Event::Dismissed);
     }
 
-    fn render_match(
-        &self,
-        string_match: &StringMatch,
-        index: usize,
-        cx: &AppContext,
-    ) -> ElementBox {
+    fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox {
         let settings = cx.global::<Settings>();
-        let style = if index == self.selected_match_index {
+        let string_match = &self.matches[ix];
+        let style = if selected {
             &settings.theme.selector.active_item
         } else {
             &settings.theme.selector.item

crates/picker/src/picker.rs 🔗

@@ -1,9 +1,10 @@
 use editor::Editor;
 use gpui::{
     elements::{
-        ChildView, EventHandler, Flex, FlexItem, Label, ParentElement, ScrollTarget, UniformList,
+        ChildView, EventHandler, Flex, Label, ParentElement, ScrollTarget, UniformList,
         UniformListState,
     },
+    geometry::vector::{vec2f, Vector2F},
     keymap, AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, Task,
     View, ViewContext, ViewHandle, WeakViewHandle,
 };
@@ -18,6 +19,7 @@ pub struct Picker<D: PickerDelegate> {
     query_editor: ViewHandle<Editor>,
     list_state: UniformListState,
     update_task: Option<Task<()>>,
+    max_size: Vector2F,
 }
 
 pub trait PickerDelegate: View {
@@ -28,6 +30,9 @@ pub trait PickerDelegate: View {
     fn confirm(&mut self, cx: &mut ViewContext<Self>);
     fn dismiss(&mut self, cx: &mut ViewContext<Self>);
     fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox;
+    fn center_selection_after_match_updates(&self) -> bool {
+        false
+    }
 }
 
 impl<D: PickerDelegate> Entity for Picker<D> {
@@ -41,6 +46,13 @@ impl<D: PickerDelegate> View for Picker<D> {
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
         let settings = cx.global::<Settings>();
+        let delegate = self.delegate.clone();
+        let match_count = if let Some(delegate) = delegate.upgrade(cx.app) {
+            delegate.read(cx).match_count()
+        } else {
+            0
+        };
+
         Flex::new(Axis::Vertical)
             .with_child(
                 ChildView::new(&self.query_editor)
@@ -49,15 +61,44 @@ impl<D: PickerDelegate> View for Picker<D> {
                     .boxed(),
             )
             .with_child(
-                FlexItem::new(self.render_matches(cx))
-                    .flex(1., false)
-                    .boxed(),
+                if match_count == 0 {
+                    Label::new(
+                        "No matches".into(),
+                        settings.theme.selector.empty.label.clone(),
+                    )
+                    .contained()
+                    .with_style(settings.theme.selector.empty.container)
+                } else {
+                    UniformList::new(
+                        self.list_state.clone(),
+                        match_count,
+                        move |mut range, items, cx| {
+                            let cx = cx.as_ref();
+                            let delegate = delegate.upgrade(cx).unwrap();
+                            let delegate = delegate.read(cx);
+                            let selected_ix = delegate.selected_index();
+                            range.end = cmp::min(range.end, delegate.match_count());
+                            items.extend(range.map(move |ix| {
+                                EventHandler::new(delegate.render_match(ix, ix == selected_ix, cx))
+                                    .on_mouse_down(move |cx| {
+                                        cx.dispatch_action(SelectIndex(ix));
+                                        true
+                                    })
+                                    .boxed()
+                            }));
+                        },
+                    )
+                    .contained()
+                    .with_margin_top(6.0)
+                }
+                .flex(1., false)
+                .boxed(),
             )
             .contained()
             .with_style(settings.theme.selector.container)
             .constrained()
-            .with_max_width(500.0)
-            .with_max_height(420.0)
+            .with_max_width(self.max_size.x())
+            .with_max_height(self.max_size.y())
             .aligned()
             .top()
             .named("picker")
@@ -91,57 +132,20 @@ impl<D: PickerDelegate> Picker<D> {
         });
         cx.subscribe(&query_editor, Self::on_query_editor_event)
             .detach();
-        let mut this = Self {
+        let this = Self {
             query_editor,
             list_state: Default::default(),
             update_task: None,
             delegate,
+            max_size: vec2f(500., 420.),
         };
-        this.update_matches(cx);
+        cx.defer(|this, cx| this.update_matches(cx));
         this
     }
 
-    fn render_matches(&self, cx: &AppContext) -> ElementBox {
-        let delegate = self.delegate.clone();
-        let match_count = if let Some(delegate) = delegate.upgrade(cx) {
-            delegate.read(cx).match_count()
-        } else {
-            0
-        };
-
-        if match_count == 0 {
-            let settings = cx.global::<Settings>();
-            return Label::new(
-                "No matches".into(),
-                settings.theme.selector.empty.label.clone(),
-            )
-            .contained()
-            .with_style(settings.theme.selector.empty.container)
-            .named("empty matches");
-        }
-
-        UniformList::new(
-            self.list_state.clone(),
-            match_count,
-            move |mut range, items, cx| {
-                let cx = cx.as_ref();
-                let delegate = delegate.upgrade(cx).unwrap();
-                let delegate = delegate.read(cx);
-                let selected_ix = delegate.selected_index();
-                range.end = cmp::min(range.end, delegate.match_count());
-                items.extend(range.map(move |ix| {
-                    EventHandler::new(delegate.render_match(ix, ix == selected_ix, cx))
-                        .on_mouse_down(move |cx| {
-                            cx.dispatch_action(SelectIndex(ix));
-                            true
-                        })
-                        .boxed()
-                }));
-            },
-        )
-        .contained()
-        .with_margin_top(6.0)
-        .named("matches")
+    pub fn with_max_size(mut self, width: f32, height: f32) -> Self {
+        self.max_size = vec2f(width, height);
+        self
     }
 
     fn on_query_editor_event(
@@ -172,8 +176,14 @@ impl<D: PickerDelegate> Picker<D> {
                 update.await;
                 this.update(&mut cx, |this, cx| {
                     if let Some(delegate) = this.delegate.upgrade(cx) {
-                        let index = delegate.read(cx).selected_index();
-                        this.list_state.scroll_to(ScrollTarget::Show(index));
+                        let delegate = delegate.read(cx);
+                        let index = delegate.selected_index();
+                        let target = if delegate.center_selection_after_match_updates() {
+                            ScrollTarget::Center(index)
+                        } else {
+                            ScrollTarget::Show(index)
+                        };
+                        this.list_state.scroll_to(target);
                         cx.notify();
                         this.update_task.take();
                     }

crates/project_symbols/src/project_symbols.rs 🔗

@@ -57,18 +57,15 @@ impl View for ProjectSymbolsView {
 impl ProjectSymbolsView {
     fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
         let handle = cx.weak_handle();
-        let picker = cx.add_view(|cx| Picker::new(handle, cx));
-        let mut this = Self {
-            picker,
+        Self {
             project,
+            picker: cx.add_view(|cx| Picker::new(handle, cx)),
             selected_match_index: 0,
             symbols: Default::default(),
             match_candidates: Default::default(),
             matches: Default::default(),
             show_worktree_root_name: false,
-        };
-        this.update_matches(String::new(), cx).detach();
-        this
+        }
     }
 
     fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {