Implement basic fuzzy filtering in picker

This commit is contained in:
Max Brunsfeld 2023-11-07 17:24:04 -08:00
parent bdec1c8202
commit 06960df287
4 changed files with 153 additions and 66 deletions

1
Cargo.lock generated
View file

@ -8558,6 +8558,7 @@ dependencies = [
"chrono", "chrono",
"clap 4.4.4", "clap 4.4.4",
"editor2", "editor2",
"fuzzy2",
"gpui2", "gpui2",
"itertools 0.11.0", "itertools 0.11.0",
"language2", "language2",

View file

@ -1,7 +1,7 @@
use editor::Editor; use editor::Editor;
use gpui::{ use gpui::{
div, uniform_list, Component, Div, FocusEnabled, ParentElement, Render, StatefulInteractivity, div, uniform_list, Component, Div, FocusEnabled, ParentElement, Render, StatefulInteractivity,
StatelessInteractive, Styled, UniformListScrollHandle, View, ViewContext, VisualContext, StatelessInteractive, Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext,
WindowContext, WindowContext,
}; };
use std::cmp; use std::cmp;
@ -10,6 +10,7 @@ pub struct Picker<D: PickerDelegate> {
pub delegate: D, pub delegate: D,
scroll_handle: UniformListScrollHandle, scroll_handle: UniformListScrollHandle,
editor: View<Editor>, editor: View<Editor>,
pending_update_matches: Option<Task<Option<()>>>,
} }
pub trait PickerDelegate: Sized + 'static { pub trait PickerDelegate: Sized + 'static {
@ -20,7 +21,7 @@ pub trait PickerDelegate: Sized + 'static {
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>); fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>);
// fn placeholder_text(&self) -> Arc<str>; // fn placeholder_text(&self) -> Arc<str>;
// fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()>; fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()>;
fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>); fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>); fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
@ -35,10 +36,13 @@ pub trait PickerDelegate: Sized + 'static {
impl<D: PickerDelegate> Picker<D> { impl<D: PickerDelegate> Picker<D> {
pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self { pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
let editor = cx.build_view(|cx| Editor::single_line(cx));
cx.subscribe(&editor, Self::on_input_editor_event).detach();
Self { Self {
delegate, delegate,
scroll_handle: UniformListScrollHandle::new(), scroll_handle: UniformListScrollHandle::new(),
editor: cx.build_view(|cx| Editor::single_line(cx)), pending_update_matches: None,
editor,
} }
} }
@ -93,6 +97,37 @@ impl<D: PickerDelegate> Picker<D> {
fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) { fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
self.delegate.confirm(true, cx); self.delegate.confirm(true, cx);
} }
fn on_input_editor_event(
&mut self,
_: View<Editor>,
event: &editor::Event,
cx: &mut ViewContext<Self>,
) {
if let editor::Event::BufferEdited = event {
let query = self.editor.read(cx).text(cx);
self.update_matches(query, cx);
}
}
pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
let update = self.delegate.update_matches(query, cx);
self.matches_updated(cx);
self.pending_update_matches = Some(cx.spawn(|this, mut cx| async move {
update.await;
this.update(&mut cx, |this, cx| {
this.matches_updated(cx);
})
.ok()
}));
}
fn matches_updated(&mut self, cx: &mut ViewContext<Self>) {
let index = self.delegate.selected_index();
self.scroll_handle.scroll_to_item(index);
self.pending_update_matches = None;
cx.notify();
}
} }
impl<D: PickerDelegate> Render for Picker<D> { impl<D: PickerDelegate> Render for Picker<D> {

View file

@ -15,6 +15,7 @@ backtrace-on-stack-overflow = "0.3.0"
clap = { version = "4.4", features = ["derive", "string"] } clap = { version = "4.4", features = ["derive", "string"] }
editor = { package = "editor2", path = "../editor2" } editor = { package = "editor2", path = "../editor2" }
chrono = "0.4" chrono = "0.4"
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
itertools = "0.11.0" itertools = "0.11.0"
language = { package = "language2", path = "../language2" } language = { package = "language2", path = "../language2" }

View file

@ -1,6 +1,9 @@
use std::sync::Arc;
use fuzzy::StringMatchCandidate;
use gpui::{ use gpui::{
div, Component, Div, KeyBinding, ParentElement, Render, SharedString, StatelessInteractive, div, Component, Div, KeyBinding, ParentElement, Render, StatelessInteractive, Styled, Task,
Styled, View, VisualContext, WindowContext, View, VisualContext, WindowContext,
}; };
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use theme2::ActiveTheme; use theme2::ActiveTheme;
@ -10,10 +13,30 @@ pub struct PickerStory {
} }
struct Delegate { struct Delegate {
candidates: Vec<SharedString>, candidates: Arc<[StringMatchCandidate]>,
matches: Vec<usize>,
selected_ix: usize, selected_ix: usize,
} }
impl Delegate {
fn new(strings: &[&str]) -> Self {
Self {
candidates: strings
.iter()
.copied()
.enumerate()
.map(|(id, string)| StringMatchCandidate {
id,
char_bag: string.into(),
string: string.into(),
})
.collect(),
matches: vec![],
selected_ix: 0,
}
}
}
impl PickerDelegate for Delegate { impl PickerDelegate for Delegate {
type ListItem = Div<Picker<Self>>; type ListItem = Div<Picker<Self>>;
@ -28,6 +51,10 @@ impl PickerDelegate for Delegate {
cx: &mut gpui::ViewContext<Picker<Self>>, cx: &mut gpui::ViewContext<Picker<Self>>,
) -> Self::ListItem { ) -> Self::ListItem {
let colors = cx.theme().colors(); let colors = cx.theme().colors();
let Some(candidate_ix) = self.matches.get(ix) else {
return div();
};
let candidate = self.candidates[*candidate_ix].string.clone();
div() div()
.text_color(colors.text) .text_color(colors.text)
@ -39,7 +66,7 @@ impl PickerDelegate for Delegate {
.bg(colors.element_active) .bg(colors.element_active)
.text_color(colors.text_accent) .text_color(colors.text_accent)
}) })
.child(self.candidates[ix].clone()) .child(candidate)
} }
fn selected_index(&self) -> usize { fn selected_index(&self) -> usize {
@ -52,16 +79,42 @@ impl PickerDelegate for Delegate {
} }
fn confirm(&mut self, secondary: bool, cx: &mut gpui::ViewContext<Picker<Self>>) { fn confirm(&mut self, secondary: bool, cx: &mut gpui::ViewContext<Picker<Self>>) {
let candidate_ix = self.matches[self.selected_ix];
let candidate = self.candidates[candidate_ix].string.clone();
if secondary { if secondary {
eprintln!("Secondary confirmed {}", self.candidates[self.selected_ix]) eprintln!("Secondary confirmed {}", candidate)
} else { } else {
eprintln!("Confirmed {}", self.candidates[self.selected_ix]) eprintln!("Confirmed {}", candidate)
} }
} }
fn dismissed(&mut self, cx: &mut gpui::ViewContext<Picker<Self>>) { fn dismissed(&mut self, cx: &mut gpui::ViewContext<Picker<Self>>) {
cx.quit(); cx.quit();
} }
fn update_matches(
&mut self,
query: String,
cx: &mut gpui::ViewContext<Picker<Self>>,
) -> Task<()> {
let candidates = self.candidates.clone();
self.matches = cx
.background_executor()
.block(fuzzy::match_strings(
&candidates,
&query,
true,
100,
&Default::default(),
cx.background_executor().clone(),
))
.into_iter()
.map(|r| r.candidate_id)
.collect();
self.selected_ix = 0;
Task::ready(())
}
} }
impl PickerStory { impl PickerStory {
@ -87,63 +140,60 @@ impl PickerStory {
PickerStory { PickerStory {
picker: cx.build_view(|cx| { picker: cx.build_view(|cx| {
let picker = Picker::new( let mut delegate = Delegate::new(&[
Delegate { "Baguette (France)",
candidates: vec![ "Baklava (Turkey)",
"Baguette (France)".into(), "Beef Wellington (UK)",
"Baklava (Turkey)".into(), "Biryani (India)",
"Beef Wellington (UK)".into(), "Borscht (Ukraine)",
"Biryani (India)".into(), "Bratwurst (Germany)",
"Borscht (Ukraine)".into(), "Bulgogi (Korea)",
"Bratwurst (Germany)".into(), "Burrito (USA)",
"Bulgogi (Korea)".into(), "Ceviche (Peru)",
"Burrito (USA)".into(), "Chicken Tikka Masala (India)",
"Ceviche (Peru)".into(), "Churrasco (Brazil)",
"Chicken Tikka Masala (India)".into(), "Couscous (North Africa)",
"Churrasco (Brazil)".into(), "Croissant (France)",
"Couscous (North Africa)".into(), "Dim Sum (China)",
"Croissant (France)".into(), "Empanada (Argentina)",
"Dim Sum (China)".into(), "Fajitas (Mexico)",
"Empanada (Argentina)".into(), "Falafel (Middle East)",
"Fajitas (Mexico)".into(), "Feijoada (Brazil)",
"Falafel (Middle East)".into(), "Fish and Chips (UK)",
"Feijoada (Brazil)".into(), "Fondue (Switzerland)",
"Fish and Chips (UK)".into(), "Goulash (Hungary)",
"Fondue (Switzerland)".into(), "Haggis (Scotland)",
"Goulash (Hungary)".into(), "Kebab (Middle East)",
"Haggis (Scotland)".into(), "Kimchi (Korea)",
"Kebab (Middle East)".into(), "Lasagna (Italy)",
"Kimchi (Korea)".into(), "Maple Syrup Pancakes (Canada)",
"Lasagna (Italy)".into(), "Moussaka (Greece)",
"Maple Syrup Pancakes (Canada)".into(), "Pad Thai (Thailand)",
"Moussaka (Greece)".into(), "Paella (Spain)",
"Pad Thai (Thailand)".into(), "Pancakes (USA)",
"Paella (Spain)".into(), "Pasta Carbonara (Italy)",
"Pancakes (USA)".into(), "Pavlova (Australia)",
"Pasta Carbonara (Italy)".into(), "Peking Duck (China)",
"Pavlova (Australia)".into(), "Pho (Vietnam)",
"Peking Duck (China)".into(), "Pierogi (Poland)",
"Pho (Vietnam)".into(), "Pizza (Italy)",
"Pierogi (Poland)".into(), "Poutine (Canada)",
"Pizza (Italy)".into(), "Pretzel (Germany)",
"Poutine (Canada)".into(), "Ramen (Japan)",
"Pretzel (Germany)".into(), "Rendang (Indonesia)",
"Ramen (Japan)".into(), "Sashimi (Japan)",
"Rendang (Indonesia)".into(), "Satay (Indonesia)",
"Sashimi (Japan)".into(), "Shepherd's Pie (Ireland)",
"Satay (Indonesia)".into(), "Sushi (Japan)",
"Shepherd's Pie (Ireland)".into(), "Tacos (Mexico)",
"Sushi (Japan)".into(), "Tandoori Chicken (India)",
"Tacos (Mexico)".into(), "Tortilla (Spain)",
"Tandoori Chicken (India)".into(), "Tzatziki (Greece)",
"Tortilla (Spain)".into(), "Wiener Schnitzel (Austria)",
"Tzatziki (Greece)".into(), ]);
"Wiener Schnitzel (Austria)".into(), delegate.update_matches("".into(), cx).detach();
],
selected_ix: 0, let picker = Picker::new(delegate, cx);
},
cx,
);
picker.focus(cx); picker.focus(cx);
picker picker
}), }),