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",
"clap 4.4.4",
"editor2",
"fuzzy2",
"gpui2",
"itertools 0.11.0",
"language2",

View file

@ -1,7 +1,7 @@
use editor::Editor;
use gpui::{
div, uniform_list, Component, Div, FocusEnabled, ParentElement, Render, StatefulInteractivity,
StatelessInteractive, Styled, UniformListScrollHandle, View, ViewContext, VisualContext,
StatelessInteractive, Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext,
WindowContext,
};
use std::cmp;
@ -10,6 +10,7 @@ pub struct Picker<D: PickerDelegate> {
pub delegate: D,
scroll_handle: UniformListScrollHandle,
editor: View<Editor>,
pending_update_matches: Option<Task<Option<()>>>,
}
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 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 dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
@ -35,10 +36,13 @@ pub trait PickerDelegate: Sized + 'static {
impl<D: PickerDelegate> Picker<D> {
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 {
delegate,
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>) {
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> {

View file

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

View file

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