Implement basic fuzzy filtering in picker
This commit is contained in:
parent
bdec1c8202
commit
06960df287
4 changed files with 153 additions and 66 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
|
@ -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
|
||||||
}),
|
}),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue