Use Picker in FileFinder

Max Brunsfeld created

Change summary

Cargo.lock                            |   1 
crates/file_finder/Cargo.toml         |   1 
crates/file_finder/src/file_finder.rs | 359 +++++++++-------------------
crates/picker/src/picker.rs           |  16 
4 files changed, 130 insertions(+), 247 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1907,6 +1907,7 @@ dependencies = [
  "env_logger 0.8.3",
  "fuzzy",
  "gpui",
+ "picker",
  "postage",
  "project",
  "serde_json",

crates/file_finder/Cargo.toml 🔗

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

crates/file_finder/src/file_finder.rs 🔗

@@ -1,13 +1,12 @@
-use editor::Editor;
 use fuzzy::PathMatch;
 use gpui::{
-    actions, elements::*, impl_internal_actions, keymap, AppContext, Axis, Entity, ModelHandle,
-    MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+    actions, elements::*, impl_internal_actions, AppContext, Entity, ModelHandle,
+    MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle,
 };
+use picker::{Picker, PickerDelegate};
 use project::{Project, ProjectPath, WorktreeId};
 use settings::Settings;
 use std::{
-    cmp,
     path::Path,
     sync::{
         atomic::{self, AtomicBool},
@@ -15,15 +14,11 @@ use std::{
     },
 };
 use util::post_inc;
-use workspace::{
-    menu::{Confirm, SelectNext, SelectPrev},
-    Workspace,
-};
+use workspace::Workspace;
 
 pub struct FileFinder {
-    handle: WeakViewHandle<Self>,
     project: ModelHandle<Project>,
-    query_editor: ViewHandle<Editor>,
+    picker: ViewHandle<Picker<Self>>,
     search_count: usize,
     latest_search_id: usize,
     latest_search_did_cancel: bool,
@@ -31,7 +26,6 @@ pub struct FileFinder {
     matches: Vec<PathMatch>,
     selected: Option<(usize, Arc<Path>)>,
     cancel_flag: Arc<AtomicBool>,
-    list_state: UniformListState,
 }
 
 #[derive(Clone)]
@@ -42,10 +36,7 @@ impl_internal_actions!(file_finder, [Select]);
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(FileFinder::toggle);
-    cx.add_action(FileFinder::confirm);
-    cx.add_action(FileFinder::select);
-    cx.add_action(FileFinder::select_prev);
-    cx.add_action(FileFinder::select_next);
+    Picker::<FileFinder>::init(cx);
 }
 
 pub enum Event {
@@ -62,140 +53,16 @@ impl View for FileFinder {
         "FileFinder"
     }
 
-    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let settings = cx.global::<Settings>();
-        Align::new(
-            ConstrainedBox::new(
-                Container::new(
-                    Flex::new(Axis::Vertical)
-                        .with_child(
-                            ChildView::new(&self.query_editor)
-                                .contained()
-                                .with_style(settings.theme.selector.input_editor.container)
-                                .boxed(),
-                        )
-                        .with_child(
-                            FlexItem::new(self.render_matches(cx))
-                                .flex(1., false)
-                                .boxed(),
-                        )
-                        .boxed(),
-                )
-                .with_style(settings.theme.selector.container)
-                .boxed(),
-            )
-            .with_max_width(500.0)
-            .with_max_height(420.0)
-            .boxed(),
-        )
-        .top()
-        .named("file finder")
+    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);
-    }
-
-    fn keymap_context(&self, _: &AppContext) -> keymap::Context {
-        let mut cx = Self::default_keymap_context();
-        cx.set.insert("menu".into());
-        cx
+        cx.focus(&self.picker);
     }
 }
 
 impl FileFinder {
-    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 finder = handle.upgrade(cx).unwrap();
-                    let finder = finder.read(cx);
-                    let start = range.start;
-                    range.end = cmp::min(range.end, finder.matches.len());
-                    items.extend(finder.matches[range].iter().enumerate().map(
-                        move |(i, path_match)| finder.render_match(path_match, start + i, cx),
-                    ));
-                },
-            );
-
-        Container::new(list.boxed())
-            .with_margin_top(6.0)
-            .named("matches")
-    }
-
-    fn render_match(&self, path_match: &PathMatch, index: usize, cx: &AppContext) -> ElementBox {
-        let selected_index = self.selected_index();
-        let settings = cx.global::<Settings>();
-        let style = if index == selected_index {
-            &settings.theme.selector.active_item
-        } else {
-            &settings.theme.selector.item
-        };
-        let (file_name, file_name_positions, full_path, full_path_positions) =
-            self.labels_for_match(path_match);
-        let container = Container::new(
-            Flex::row()
-                // .with_child(
-                //     Container::new(
-                //         LineBox::new(
-                //             Svg::new("icons/file-16.svg")
-                //                 .with_color(style.label.text.color)
-                //                 .boxed(),
-                //             style.label.text.clone(),
-                //         )
-                //         .boxed(),
-                //     )
-                //     .with_padding_right(6.0)
-                //     .boxed(),
-                // )
-                .with_child(
-                    Flex::column()
-                        .with_child(
-                            Label::new(file_name.to_string(), style.label.clone())
-                                .with_highlights(file_name_positions)
-                                .boxed(),
-                        )
-                        .with_child(
-                            Label::new(full_path, style.label.clone())
-                                .with_highlights(full_path_positions)
-                                .boxed(),
-                        )
-                        .flex(1., false)
-                        .boxed(),
-                )
-                .boxed(),
-        )
-        .with_style(style.container);
-
-        let action = Select(ProjectPath {
-            worktree_id: WorktreeId::from_usize(path_match.worktree_id),
-            path: path_match.path.clone(),
-        });
-        EventHandler::new(container.boxed())
-            .on_mouse_down(move |cx| {
-                cx.dispatch_action(action.clone());
-                true
-            })
-            .named("match")
-    }
-
     fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec<usize>, String, Vec<usize>) {
         let path_string = path_match.path.to_string_lossy();
         let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
@@ -252,16 +119,12 @@ impl FileFinder {
     pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
         cx.observe(&project, Self::project_updated).detach();
 
-        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));
 
         Self {
-            handle: cx.weak_handle(),
             project,
-            query_editor,
+            picker,
             search_count: 0,
             latest_search_id: 0,
             latest_search_did_cancel: false,
@@ -269,36 +132,60 @@ impl FileFinder {
             matches: Vec::new(),
             selected: None,
             cancel_flag: Arc::new(AtomicBool::new(false)),
-            list_state: Default::default(),
         }
     }
 
     fn project_updated(&mut self, _: ModelHandle<Project>, cx: &mut ViewContext<Self>) {
-        let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
-        self.spawn_search(query, cx).detach();
+        self.spawn_search(self.latest_search_query.clone(), cx)
+            .detach();
+    }
+
+    fn spawn_search(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
+        let search_id = util::post_inc(&mut self.search_count);
+        self.cancel_flag.store(true, atomic::Ordering::Relaxed);
+        self.cancel_flag = Arc::new(AtomicBool::new(false));
+        let cancel_flag = self.cancel_flag.clone();
+        let project = self.project.clone();
+        cx.spawn(|this, mut cx| async move {
+            let matches = project
+                .read_with(&cx, |project, cx| {
+                    project.match_paths(&query, false, false, 100, cancel_flag.as_ref(), cx)
+                })
+                .await;
+            let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
+            this.update(&mut cx, |this, cx| {
+                this.set_matches(search_id, did_cancel, query, matches, cx)
+            });
+        })
     }
 
-    fn on_query_editor_event(
+    fn set_matches(
         &mut self,
-        _: ViewHandle<Editor>,
-        event: &editor::Event,
+        search_id: usize,
+        did_cancel: bool,
+        query: String,
+        matches: Vec<PathMatch>,
         cx: &mut ViewContext<Self>,
     ) {
-        match event {
-            editor::Event::BufferEdited { .. } => {
-                let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
-                if query.is_empty() {
-                    self.latest_search_id = post_inc(&mut self.search_count);
-                    self.matches.clear();
-                    cx.notify();
-                } else {
-                    self.spawn_search(query, cx).detach();
-                }
+        if search_id >= self.latest_search_id {
+            self.latest_search_id = search_id;
+            if self.latest_search_did_cancel && query == self.latest_search_query {
+                util::extend_sorted(&mut self.matches, matches.into_iter(), 100, |a, b| b.cmp(a));
+            } else {
+                self.matches = matches;
             }
-            editor::Event::Blurred => cx.emit(Event::Dismissed),
-            _ => {}
+            self.latest_search_query = query;
+            self.latest_search_did_cancel = did_cancel;
+            cx.notify();
+            self.picker.update(cx, |_, cx| cx.notify());
         }
     }
+}
+
+impl PickerDelegate for FileFinder {
+    fn match_count(&self) -> usize {
+        self.matches.len()
+    }
 
     fn selected_index(&self) -> usize {
         if let Some(selected) = self.selected.as_ref() {
@@ -313,31 +200,24 @@ impl FileFinder {
         0
     }
 
-    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
-        let mut selected_index = self.selected_index();
-        if selected_index > 0 {
-            selected_index -= 1;
-            let mat = &self.matches[selected_index];
-            self.selected = Some((mat.worktree_id, mat.path.clone()));
-        }
-        self.list_state
-            .scroll_to(ScrollTarget::Show(selected_index));
+    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
+        let mat = &self.matches[ix];
+        self.selected = Some((mat.worktree_id, mat.path.clone()));
         cx.notify();
     }
 
-    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
-        let mut selected_index = self.selected_index();
-        if selected_index + 1 < self.matches.len() {
-            selected_index += 1;
-            let mat = &self.matches[selected_index];
-            self.selected = Some((mat.worktree_id, mat.path.clone()));
+    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
+        if query.is_empty() {
+            self.latest_search_id = post_inc(&mut self.search_count);
+            self.matches.clear();
+            cx.notify();
+            Task::ready(())
+        } else {
+            self.spawn_search(query, cx)
         }
-        self.list_state
-            .scroll_to(ScrollTarget::Show(selected_index));
-        cx.notify();
     }
 
-    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+    fn confirm(&mut self, cx: &mut ViewContext<Self>) {
         if let Some(m) = self.matches.get(self.selected_index()) {
             cx.emit(Event::Selected(ProjectPath {
                 worktree_id: WorktreeId::from_usize(m.worktree_id),
@@ -346,56 +226,57 @@ impl FileFinder {
         }
     }
 
-    fn select(&mut self, Select(project_path): &Select, cx: &mut ViewContext<Self>) {
-        cx.emit(Event::Selected(project_path.clone()));
+    fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+        cx.emit(Event::Dismissed);
     }
 
-    fn spawn_search(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
-        let search_id = util::post_inc(&mut self.search_count);
-        self.cancel_flag.store(true, atomic::Ordering::Relaxed);
-        self.cancel_flag = Arc::new(AtomicBool::new(false));
-        let cancel_flag = self.cancel_flag.clone();
-        let project = self.project.clone();
-        cx.spawn(|this, mut cx| async move {
-            let matches = project
-                .read_with(&cx, |project, cx| {
-                    project.match_paths(&query, false, false, 100, cancel_flag.as_ref(), cx)
-                })
-                .await;
-            let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
-            this.update(&mut cx, |this, cx| {
-                this.update_matches((search_id, did_cancel, query, matches), cx)
-            });
-        })
-    }
+    fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> ElementBox {
+        let path_match = &self.matches[ix];
+        let settings = cx.global::<Settings>();
+        let style = if selected {
+            &settings.theme.selector.active_item
+        } else {
+            &settings.theme.selector.item
+        };
+        let (file_name, file_name_positions, full_path, full_path_positions) =
+            self.labels_for_match(path_match);
+        let action = Select(ProjectPath {
+            worktree_id: WorktreeId::from_usize(path_match.worktree_id),
+            path: path_match.path.clone(),
+        });
 
-    fn update_matches(
-        &mut self,
-        (search_id, did_cancel, query, matches): (usize, bool, String, Vec<PathMatch>),
-        cx: &mut ViewContext<Self>,
-    ) {
-        if search_id >= self.latest_search_id {
-            self.latest_search_id = search_id;
-            if self.latest_search_did_cancel && query == self.latest_search_query {
-                util::extend_sorted(&mut self.matches, matches.into_iter(), 100, |a, b| b.cmp(a));
-            } else {
-                self.matches = matches;
-            }
-            self.latest_search_query = query;
-            self.latest_search_did_cancel = did_cancel;
-            self.list_state
-                .scroll_to(ScrollTarget::Show(self.selected_index()));
-            cx.notify();
-        }
+        EventHandler::new(
+            Flex::column()
+                .with_child(
+                    Label::new(file_name.to_string(), style.label.clone())
+                        .with_highlights(file_name_positions)
+                        .boxed(),
+                )
+                .with_child(
+                    Label::new(full_path, style.label.clone())
+                        .with_highlights(full_path_positions)
+                        .boxed(),
+                )
+                .flex(1., false)
+                .contained()
+                .with_style(style.container)
+                .boxed(),
+        )
+        .on_mouse_down(move |cx| {
+            cx.dispatch_action(action.clone());
+            true
+        })
+        .named("match")
     }
 }
 
 #[cfg(test)]
 mod tests {
     use super::*;
-    use editor::Input;
+    use editor::{Editor, Input};
     use serde_json::json;
     use std::path::PathBuf;
+    use workspace::menu::{Confirm, SelectNext};
     use workspace::{Workspace, WorkspaceParams};
 
     #[ctor::ctor]
@@ -518,25 +399,21 @@ mod tests {
             // Simulate a search being cancelled after the time limit,
             // returning only a subset of the matches that would have been found.
             finder.spawn_search(query.clone(), cx).detach();
-            finder.update_matches(
-                (
-                    finder.latest_search_id,
-                    true, // did-cancel
-                    query.clone(),
-                    vec![matches[1].clone(), matches[3].clone()],
-                ),
+            finder.set_matches(
+                finder.latest_search_id,
+                true, // did-cancel
+                query.clone(),
+                vec![matches[1].clone(), matches[3].clone()],
                 cx,
             );
 
             // Simulate another cancellation.
             finder.spawn_search(query.clone(), cx).detach();
-            finder.update_matches(
-                (
-                    finder.latest_search_id,
-                    true, // did-cancel
-                    query.clone(),
-                    vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
-                ),
+            finder.set_matches(
+                finder.latest_search_id,
+                true, // did-cancel
+                query.clone(),
+                vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
                 cx,
             );
 
@@ -631,9 +508,9 @@ mod tests {
         finder.update(cx, |f, cx| {
             assert_eq!(f.matches.len(), 2);
             assert_eq!(f.selected_index(), 0);
-            f.select_next(&SelectNext, cx);
+            f.set_selected_index(1, cx);
             assert_eq!(f.selected_index(), 1);
-            f.select_prev(&SelectPrev, cx);
+            f.set_selected_index(0, cx);
             assert_eq!(f.selected_index(), 0);
         });
     }

crates/picker/src/picker.rs 🔗

@@ -161,14 +161,18 @@ impl<D: PickerDelegate> Picker<D> {
             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();
+                    if let Some(delegate) = this.delegate.upgrade(cx) {
+                        let index = delegate.read(cx).selected_index();
+                        this.list_state.scroll_to(ScrollTarget::Show(index));
+                        cx.notify();
+                        this.update_task.take();
+                    }
                 });
             }));
         }
     }
 
-    fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
+    pub fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
         if let Some(delegate) = self.delegate.upgrade(cx) {
             let index = 0;
             delegate.update(cx, |delegate, cx| delegate.set_selected_index(0, cx));
@@ -177,7 +181,7 @@ impl<D: PickerDelegate> Picker<D> {
         }
     }
 
-    fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
+    pub fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
         if let Some(delegate) = self.delegate.upgrade(cx) {
             let index = delegate.update(cx, |delegate, cx| {
                 let match_count = delegate.match_count();
@@ -190,7 +194,7 @@ impl<D: PickerDelegate> Picker<D> {
         }
     }
 
-    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
+    pub fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
         if let Some(delegate) = self.delegate.upgrade(cx) {
             let index = delegate.update(cx, |delegate, cx| {
                 let mut selected_index = delegate.selected_index();
@@ -205,7 +209,7 @@ impl<D: PickerDelegate> Picker<D> {
         }
     }
 
-    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
+    pub fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
         if let Some(delegate) = self.delegate.upgrade(cx) {
             let index = delegate.update(cx, |delegate, cx| {
                 let mut selected_index = delegate.selected_index();