Use Picker in ProjectSymbolsView

Max Brunsfeld created

Change summary

Cargo.lock                                    |   1 
crates/picker/src/picker.rs                   |  46 +-
crates/project_symbols/Cargo.toml             |   1 
crates/project_symbols/src/project_symbols.rs | 331 +++++++-------------
4 files changed, 145 insertions(+), 234 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3794,6 +3794,7 @@ dependencies = [
  "fuzzy",
  "gpui",
  "ordered-float",
+ "picker",
  "postage",
  "project",
  "settings",

crates/picker/src/picker.rs 🔗

@@ -15,6 +15,7 @@ pub struct Picker<D: PickerDelegate> {
     delegate: WeakViewHandle<D>,
     query_editor: ViewHandle<Editor>,
     list_state: UniformListState,
+    update_task: Option<Task<()>>,
 }
 
 pub trait PickerDelegate: View {
@@ -87,12 +88,14 @@ impl<D: PickerDelegate> Picker<D> {
         });
         cx.subscribe(&query_editor, Self::on_query_editor_event)
             .detach();
-
-        Self {
-            delegate,
+        let mut this = Self {
             query_editor,
             list_state: Default::default(),
-        }
+            update_task: None,
+            delegate,
+        };
+        this.update_matches(cx);
+        this
     }
 
     fn render_matches(&self, cx: &AppContext) -> ElementBox {
@@ -137,22 +140,31 @@ impl<D: PickerDelegate> Picker<D> {
         event: &editor::Event,
         cx: &mut ViewContext<Self>,
     ) {
-        if let Some(delegate) = self.delegate.upgrade(cx) {
-            match event {
-                editor::Event::BufferEdited { .. } => {
-                    let query = self.query_editor.read(cx).text(cx);
-                    let update = delegate.update(cx, |d, cx| d.update_matches(query, cx));
-                    cx.spawn(|this, mut cx| async move {
-                        update.await;
-                        this.update(&mut cx, |_, cx| cx.notify());
+        match event {
+            editor::Event::BufferEdited { .. } => self.update_matches(cx),
+            editor::Event::Blurred => {
+                if let Some(delegate) = self.delegate.upgrade(cx) {
+                    delegate.update(cx, |delegate, cx| {
+                        delegate.dismiss(cx);
                     })
-                    .detach();
                 }
-                editor::Event::Blurred => delegate.update(cx, |delegate, cx| {
-                    delegate.dismiss(cx);
-                }),
-                _ => {}
             }
+            _ => {}
+        }
+    }
+
+    fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(delegate) = self.delegate.upgrade(cx) {
+            let query = self.query_editor.read(cx).text(cx);
+            let update = delegate.update(cx, |d, cx| d.update_matches(query, cx));
+            cx.notify();
+            self.update_task = Some(cx.spawn(|this, mut cx| async move {
+                update.await;
+                this.update(&mut cx, |this, cx| {
+                    cx.notify();
+                    this.update_task.take();
+                });
+            }));
         }
     }
 

crates/project_symbols/Cargo.toml 🔗

@@ -11,6 +11,7 @@ doctest = false
 editor = { path = "../editor" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
+picker = { path = "../picker" }
 project = { path = "../project" }
 text = { path = "../text" }
 settings = { path = "../settings" }

crates/project_symbols/src/project_symbols.rs 🔗

@@ -3,43 +3,32 @@ use editor::{
 };
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    actions, elements::*, keymap, AppContext, Axis, Entity, ModelHandle, MutableAppContext,
-    RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+    actions, elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Task,
+    View, ViewContext, ViewHandle,
 };
 use ordered_float::OrderedFloat;
+use picker::{Picker, PickerDelegate};
 use project::{Project, Symbol};
 use settings::Settings;
-use std::{
-    borrow::Cow,
-    cmp::{self, Reverse},
-};
+use std::{borrow::Cow, cmp::Reverse};
 use util::ResultExt;
-use workspace::{
-    menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev},
-    Workspace,
-};
+use workspace::Workspace;
 
 actions!(project_symbols, [Toggle]);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(ProjectSymbolsView::toggle);
-    cx.add_action(ProjectSymbolsView::confirm);
-    cx.add_action(ProjectSymbolsView::select_prev);
-    cx.add_action(ProjectSymbolsView::select_next);
-    cx.add_action(ProjectSymbolsView::select_first);
-    cx.add_action(ProjectSymbolsView::select_last);
+    Picker::<ProjectSymbolsView>::init(cx);
 }
 
 pub struct ProjectSymbolsView {
-    handle: WeakViewHandle<Self>,
+    picker: ViewHandle<Picker<Self>>,
     project: ModelHandle<Project>,
     selected_match_index: usize,
-    list_state: UniformListState,
     symbols: Vec<Symbol>,
     match_candidates: Vec<StringMatchCandidate>,
+    show_worktree_root_name: bool,
     matches: Vec<StringMatch>,
-    pending_symbols_task: Task<Option<()>>,
-    query_editor: ViewHandle<Editor>,
 }
 
 pub enum Event {
@@ -56,59 +45,29 @@ impl View for ProjectSymbolsView {
         "ProjectSymbolsView"
     }
 
-    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., false)
-                    .boxed(),
-            )
-            .contained()
-            .with_style(settings.theme.selector.container)
-            .constrained()
-            .with_max_width(500.0)
-            .with_max_height(420.0)
-            .aligned()
-            .top()
-            .named("project symbols 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);
     }
 }
 
 impl ProjectSymbolsView {
     fn new(project: ModelHandle<Project>, 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 handle = cx.weak_handle();
+        let picker = cx.add_view(|cx| Picker::new(handle, cx));
         let mut this = Self {
-            handle: cx.weak_handle(),
+            picker,
             project,
             selected_match_index: 0,
-            list_state: Default::default(),
             symbols: Default::default(),
             match_candidates: Default::default(),
             matches: Default::default(),
-            pending_symbols_task: Task::ready(None),
-            query_editor,
+            show_worktree_root_name: false,
         };
-        this.update_matches(cx);
+        this.update_matches(String::new(), cx).detach();
         this
     }
 
@@ -121,72 +80,7 @@ impl ProjectSymbolsView {
         });
     }
 
-    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
-        if self.selected_match_index > 0 {
-            self.select(self.selected_match_index - 1, 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, cx);
-        }
-    }
-
-    fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
-        self.select(0, cx);
-    }
-
-    fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
-        self.select(self.matches.len().saturating_sub(1), cx);
-    }
-
-    fn select(&mut self, index: usize, cx: &mut ViewContext<Self>) {
-        self.selected_match_index = index;
-        self.list_state.scroll_to(ScrollTarget::Show(index));
-        cx.notify();
-    }
-
-    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
-        if let Some(symbol) = self
-            .matches
-            .get(self.selected_match_index)
-            .map(|mat| self.symbols[mat.candidate_id].clone())
-        {
-            cx.emit(Event::Selected(symbol));
-        }
-    }
-
-    fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
-        self.filter(cx);
-        let query = self.query_editor.read(cx).text(cx);
-        let symbols = self
-            .project
-            .update(cx, |project, cx| project.symbols(&query, cx));
-        self.pending_symbols_task = cx.spawn_weak(|this, mut cx| async move {
-            let symbols = symbols.await.log_err()?;
-            if let Some(this) = this.upgrade(&cx) {
-                this.update(&mut cx, |this, cx| {
-                    this.match_candidates = symbols
-                        .iter()
-                        .enumerate()
-                        .map(|(id, symbol)| {
-                            StringMatchCandidate::new(
-                                id,
-                                symbol.label.text[symbol.label.filter_range.clone()].to_string(),
-                            )
-                        })
-                        .collect();
-                    this.symbols = symbols;
-                    this.filter(cx);
-                });
-            }
-            None
-        });
-    }
-
-    fn filter(&mut self, cx: &mut ViewContext<Self>) {
-        let query = self.query_editor.read(cx).text(cx);
+    fn filter(&mut self, query: &str, cx: &mut ViewContext<Self>) {
         let mut matches = if query.is_empty() {
             self.match_candidates
                 .iter()
@@ -201,7 +95,7 @@ impl ProjectSymbolsView {
         } else {
             smol::block_on(fuzzy::match_strings(
                 &self.match_candidates,
-                &query,
+                query,
                 false,
                 100,
                 &Default::default(),
@@ -225,57 +119,111 @@ impl ProjectSymbolsView {
         }
 
         self.matches = matches;
-        self.select_first(&SelectFirst, cx);
+        self.set_selected_index(0, cx);
         cx.notify();
     }
 
-    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");
+    fn on_event(
+        workspace: &mut Workspace,
+        _: ViewHandle<Self>,
+        event: &Event,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        match event {
+            Event::Dismissed => workspace.dismiss_modal(cx),
+            Event::Selected(symbol) => {
+                let buffer = workspace
+                    .project()
+                    .update(cx, |project, cx| project.open_buffer_for_symbol(symbol, cx));
+
+                let symbol = symbol.clone();
+                cx.spawn(|workspace, mut cx| async move {
+                    let buffer = buffer.await?;
+                    workspace.update(&mut cx, |workspace, cx| {
+                        let position = buffer
+                            .read(cx)
+                            .clip_point_utf16(symbol.range.start, Bias::Left);
+
+                        let editor = workspace.open_project_item::<Editor>(buffer, cx);
+                        editor.update(cx, |editor, cx| {
+                            editor.select_ranges(
+                                [position..position],
+                                Some(Autoscroll::Center),
+                                cx,
+                            );
+                        });
+                    });
+                    Ok::<_, anyhow::Error>(())
+                })
+                .detach_and_log_err(cx);
+                workspace.dismiss_modal(cx);
+            }
         }
+    }
+}
 
-        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());
+impl PickerDelegate for ProjectSymbolsView {
+    fn confirm(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(symbol) = self
+            .matches
+            .get(self.selected_match_index)
+            .map(|mat| self.symbols[mat.candidate_id].clone())
+        {
+            cx.emit(Event::Selected(symbol));
+        }
+    }
+
+    fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+        cx.emit(Event::Dismissed);
+    }
 
-                let show_worktree_root_name =
-                    view.project.read(cx).visible_worktrees(cx).count() > 1;
-                items.extend(view.matches[range].iter().enumerate().map(move |(ix, m)| {
-                    view.render_match(m, start + ix, show_worktree_root_name, cx)
-                }));
-            },
-        );
+    fn match_count(&self) -> usize {
+        self.matches.len()
+    }
 
-        Container::new(list.boxed())
-            .with_margin_top(6.0)
-            .named("matches")
+    fn selected_index(&self) -> usize {
+        self.selected_match_index
     }
 
-    fn render_match(
-        &self,
-        string_match: &StringMatch,
-        index: usize,
-        show_worktree_root_name: bool,
-        cx: &AppContext,
-    ) -> ElementBox {
+    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
+        self.selected_match_index = ix;
+        cx.notify();
+    }
+
+    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
+        self.filter(&query, cx);
+        self.show_worktree_root_name = self.project.read(cx).visible_worktrees(cx).count() > 1;
+        let symbols = self
+            .project
+            .update(cx, |project, cx| project.symbols(&query, cx));
+        cx.spawn_weak(|this, mut cx| async move {
+            let symbols = symbols.await.log_err();
+            if let Some(this) = this.upgrade(&cx) {
+                if let Some(symbols) = symbols {
+                    this.update(&mut cx, |this, cx| {
+                        this.match_candidates = symbols
+                            .iter()
+                            .enumerate()
+                            .map(|(id, symbol)| {
+                                StringMatchCandidate::new(
+                                    id,
+                                    symbol.label.text[symbol.label.filter_range.clone()]
+                                        .to_string(),
+                                )
+                            })
+                            .collect();
+                        this.symbols = symbols;
+                        this.filter(&query, cx);
+                    });
+                }
+            }
+        })
+    }
+
+    fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox {
+        let string_match = &self.matches[ix];
         let settings = cx.global::<Settings>();
-        let style = if index == self.selected_match_index {
+        let style = if selected {
             &settings.theme.selector.active_item
         } else {
             &settings.theme.selector.item
@@ -284,7 +232,7 @@ impl ProjectSymbolsView {
         let syntax_runs = styled_runs_for_code_label(&symbol.label, &settings.theme.editor.syntax);
 
         let mut path = symbol.path.to_string_lossy();
-        if show_worktree_root_name {
+        if self.show_worktree_root_name {
             let project = self.project.read(cx);
             if let Some(worktree) = project.worktree_for_id(symbol.worktree_id, cx) {
                 path = Cow::Owned(format!(
@@ -317,55 +265,4 @@ impl ProjectSymbolsView {
             .with_style(style.container)
             .boxed()
     }
-
-    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),
-            _ => {}
-        }
-    }
-
-    fn on_event(
-        workspace: &mut Workspace,
-        _: ViewHandle<Self>,
-        event: &Event,
-        cx: &mut ViewContext<Workspace>,
-    ) {
-        match event {
-            Event::Dismissed => workspace.dismiss_modal(cx),
-            Event::Selected(symbol) => {
-                let buffer = workspace
-                    .project()
-                    .update(cx, |project, cx| project.open_buffer_for_symbol(symbol, cx));
-
-                let symbol = symbol.clone();
-                cx.spawn(|workspace, mut cx| async move {
-                    let buffer = buffer.await?;
-                    workspace.update(&mut cx, |workspace, cx| {
-                        let position = buffer
-                            .read(cx)
-                            .clip_point_utf16(symbol.range.start, Bias::Left);
-
-                        let editor = workspace.open_project_item::<Editor>(buffer, cx);
-                        editor.update(cx, |editor, cx| {
-                            editor.select_ranges(
-                                [position..position],
-                                Some(Autoscroll::Center),
-                                cx,
-                            );
-                        });
-                    });
-                    Ok::<_, anyhow::Error>(())
-                })
-                .detach_and_log_err(cx);
-                workspace.dismiss_modal(cx);
-            }
-        }
-    }
 }