Skip to content

Commit 63e5038

Browse files
committed
Consolidate DynamicPicker into Picker
DynamicPicker is a thin wrapper over Picker that holds some additional state, similar to the old FilePicker type. Like with FilePicker, we want to fold the two types together, having Picker optionally hold that extra state. The DynamicPicker is a little more complicated than FilePicker was though - it holds a query callback and current query string in state and provides some debounce for queries using the IdleTimeout event. We can move all of that state and debounce logic into an AsyncHook implementation, introduced here as `DynamicQueryHandler`. The hook receives updates to the primary query and debounces those events so that once a query has been idle for a short time (275ms) we re-run the query. A standard Picker created through `new` for example can be promoted into a Dynamic picker by chaining the new `with_dynamic_query` function, very similar to FilePicker's replacement `with_preview`. The workspace symbol picker has been migrated to the new way of writing dynamic pickers as an example. The child commit will promote global search into a dynamic Picker as well.
1 parent a0c52d5 commit 63e5038

File tree

4 files changed

+96
-100
lines changed

4 files changed

+96
-100
lines changed

helix-term/src/commands/lsp.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use helix_view::{
2727
use crate::{
2828
compositor::{self, Compositor},
2929
job::Callback,
30-
ui::{self, overlay::overlaid, DynamicPicker, FileLocation, Picker, Popup, PromptEvent},
30+
ui::{self, overlay::overlaid, FileLocation, Picker, Popup, PromptEvent},
3131
};
3232

3333
use std::{

helix-term/src/ui/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pub use completion::{Completion, CompletionItem};
2020
pub use editor::EditorView;
2121
pub use markdown::Markdown;
2222
pub use menu::Menu;
23-
pub use picker::{Column as PickerColumn, DynamicPicker, FileLocation, Picker};
23+
pub use picker::{Column as PickerColumn, FileLocation, Picker};
2424
pub use popup::Popup;
2525
pub use prompt::{Prompt, PromptEvent};
2626
pub use spinner::{ProgressSpinners, Spinner};

helix-term/src/ui/picker.rs

Lines changed: 23 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ mod query;
44
use crate::{
55
alt,
66
compositor::{self, Component, Compositor, Context, Event, EventResult},
7-
ctrl,
8-
job::Callback,
9-
key, shift,
7+
ctrl, key, shift,
108
ui::{
119
self,
1210
document::{render_document, LineDecoration, LinePos, TextRenderer},
@@ -52,8 +50,6 @@ use helix_view::{
5250

5351
pub const ID: &str = "picker";
5452

55-
use super::overlay::Overlay;
56-
5753
pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72;
5854
/// Biggest file size to preview in bytes
5955
pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024;
@@ -223,6 +219,11 @@ impl<T, D> Column<T, D> {
223219
}
224220
}
225221

222+
/// Returns a new list of options to replace the contents of the picker
223+
/// when called with the current picker query,
224+
type DynQueryCallback<T, D> =
225+
fn(String, &mut Editor, Arc<D>, &Injector<T, D>) -> BoxFuture<'static, anyhow::Result<()>>;
226+
226227
pub struct Picker<T: 'static + Send + Sync, D: 'static> {
227228
column_names: Vec<&'static str>,
228229
columns: Arc<Vec<Column<T, D>>>,
@@ -253,6 +254,8 @@ pub struct Picker<T: 'static + Send + Sync, D: 'static> {
253254
file_fn: Option<FileCallback<T>>,
254255
/// An event handler for syntax highlighting the currently previewed file.
255256
preview_highlight_handler: tokio::sync::mpsc::Sender<Arc<Path>>,
257+
dynamic_query_running: bool,
258+
dynamic_query_handler: Option<tokio::sync::mpsc::Sender<String>>,
256259
}
257260

258261
impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
@@ -362,6 +365,8 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
362365
read_buffer: Vec::with_capacity(1024),
363366
file_fn: None,
364367
preview_highlight_handler: handlers::PreviewHighlightHandler::<T, D>::default().spawn(),
368+
dynamic_query_running: false,
369+
dynamic_query_handler: None,
365370
}
366371
}
367372

@@ -396,12 +401,11 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
396401
self
397402
}
398403

399-
pub fn set_options(&mut self, new_options: Vec<T>) {
400-
self.matcher.restart(false);
401-
let injector = self.matcher.injector();
402-
for item in new_options {
403-
inject_nucleo_item(&injector, &self.columns, item, &self.editor_data);
404-
}
404+
pub fn with_dynamic_query(mut self, callback: DynQueryCallback<T, D>) -> Self {
405+
let handler = handlers::DynamicQueryHandler::new(callback).spawn();
406+
helix_event::send_blocking(&handler, self.primary_query().to_string());
407+
self.dynamic_query_handler = Some(handler);
408+
self
405409
}
406410

407411
/// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`)
@@ -504,6 +508,9 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
504508
.reparse(i, pattern, CaseMatching::Smart, append);
505509
}
506510
self.query = new_query;
511+
if let Some(handler) = &self.dynamic_query_handler {
512+
helix_event::send_blocking(handler, self.primary_query().to_string());
513+
}
507514
}
508515
}
509516
EventResult::Consumed(None)
@@ -615,7 +622,11 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
615622

616623
let count = format!(
617624
"{}{}/{}",
618-
if status.running { "(running) " } else { "" },
625+
if status.running || self.dynamic_query_running {
626+
"(running) "
627+
} else {
628+
""
629+
},
619630
snapshot.matched_item_count(),
620631
snapshot.item_count(),
621632
);
@@ -1027,74 +1038,3 @@ impl<T: 'static + Send + Sync, D> Drop for Picker<T, D> {
10271038
}
10281039

10291040
type PickerCallback<T> = Box<dyn Fn(&mut Context, &T, Action)>;
1030-
1031-
/// Returns a new list of options to replace the contents of the picker
1032-
/// when called with the current picker query,
1033-
pub type DynQueryCallback<T> =
1034-
Box<dyn Fn(String, &mut Editor) -> BoxFuture<'static, anyhow::Result<Vec<T>>>>;
1035-
1036-
/// A picker that updates its contents via a callback whenever the
1037-
/// query string changes. Useful for live grep, workspace symbols, etc.
1038-
pub struct DynamicPicker<T: 'static + Send + Sync, D: 'static + Send + Sync> {
1039-
file_picker: Picker<T, D>,
1040-
query_callback: DynQueryCallback<T>,
1041-
query: String,
1042-
}
1043-
1044-
impl<T: Send + Sync, D: Send + Sync> DynamicPicker<T, D> {
1045-
pub fn new(file_picker: Picker<T, D>, query_callback: DynQueryCallback<T>) -> Self {
1046-
Self {
1047-
file_picker,
1048-
query_callback,
1049-
query: String::new(),
1050-
}
1051-
}
1052-
}
1053-
1054-
impl<T: Send + Sync + 'static, D: Send + Sync + 'static> Component for DynamicPicker<T, D> {
1055-
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
1056-
self.file_picker.render(area, surface, cx);
1057-
}
1058-
1059-
fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
1060-
let event_result = self.file_picker.handle_event(event, cx);
1061-
let Some(current_query) = self.file_picker.primary_query() else {
1062-
return event_result;
1063-
};
1064-
1065-
if !matches!(event, Event::IdleTimeout) || self.query == *current_query {
1066-
return event_result;
1067-
}
1068-
1069-
self.query = current_query.to_string();
1070-
1071-
let new_options = (self.query_callback)(current_query.to_owned(), cx.editor);
1072-
1073-
cx.jobs.callback(async move {
1074-
let new_options = new_options.await?;
1075-
let callback = Callback::EditorCompositor(Box::new(move |_editor, compositor| {
1076-
// Wrapping of pickers in overlay is done outside the picker code,
1077-
// so this is fragile and will break if wrapped in some other widget.
1078-
let picker = match compositor.find_id::<Overlay<Self>>(ID) {
1079-
Some(overlay) => &mut overlay.content.file_picker,
1080-
None => return,
1081-
};
1082-
picker.set_options(new_options);
1083-
}));
1084-
anyhow::Ok(callback)
1085-
});
1086-
EventResult::Consumed(None)
1087-
}
1088-
1089-
fn cursor(&self, area: Rect, ctx: &Editor) -> (Option<Position>, CursorKind) {
1090-
self.file_picker.cursor(area, ctx)
1091-
}
1092-
1093-
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
1094-
self.file_picker.required_size(viewport)
1095-
}
1096-
1097-
fn id(&self) -> Option<&'static str> {
1098-
Some(ID)
1099-
}
1100-
}

helix-term/src/ui/picker/handlers.rs

Lines changed: 71 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
use std::{path::Path, sync::Arc, time::Duration};
1+
use std::{
2+
path::Path,
3+
sync::{atomic, Arc},
4+
time::Duration,
5+
};
26

37
use helix_event::AsyncHook;
48
use tokio::time::Instant;
59

610
use crate::ui::overlay::Overlay;
711

8-
use super::{CachedPreview, DynamicPicker, Picker};
12+
use super::{CachedPreview, DynQueryCallback, Picker};
913

1014
pub(super) struct PreviewHighlightHandler<T: 'static + Send + Sync, D: 'static + Send + Sync> {
1115
trigger: Option<Arc<Path>>,
@@ -48,12 +52,8 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> AsyncHook
4852
let Some(path) = self.trigger.take() else { return };
4953

5054
crate::job::dispatch_blocking(move |editor, compositor| {
51-
let picker = match compositor.find::<Overlay<Picker<T, D>>>() {
52-
Some(Overlay { content, .. }) => content,
53-
None => match compositor.find::<Overlay<DynamicPicker<T, D>>>() {
54-
Some(Overlay { content, .. }) => &mut content.file_picker,
55-
None => return,
56-
},
55+
let Some(Overlay { content: picker, .. }) = compositor.find::<Overlay<Picker<T, D>>>() else {
56+
return;
5757
};
5858

5959
let Some(CachedPreview::Document(ref mut doc)) = picker.preview_cache.get_mut(&*path) else {
@@ -85,13 +85,7 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> AsyncHook
8585
};
8686

8787
crate::job::dispatch_blocking(move |editor, compositor| {
88-
let picker = match compositor.find::<Overlay<Picker<T, D>>>() {
89-
Some(Overlay { content, .. }) => Some(content),
90-
None => compositor
91-
.find::<Overlay<DynamicPicker<T, D>>>()
92-
.map(|overlay| &mut overlay.content.file_picker),
93-
};
94-
let Some(picker) = picker else {
88+
let Some(Overlay { content: picker, .. }) = compositor.find::<Overlay<Picker<T, D>>>() else {
9589
log::info!("picker closed before syntax highlighting finished");
9690
return;
9791
};
@@ -110,3 +104,65 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> AsyncHook
110104
});
111105
}
112106
}
107+
108+
pub(super) struct DynamicQueryHandler<T: 'static + Send + Sync, D: 'static + Send + Sync> {
109+
callback: Arc<DynQueryCallback<T, D>>,
110+
last_query: String,
111+
query: Option<String>,
112+
}
113+
114+
impl<T: 'static + Send + Sync, D: 'static + Send + Sync> DynamicQueryHandler<T, D> {
115+
pub(super) fn new(callback: DynQueryCallback<T, D>) -> Self {
116+
Self {
117+
callback: Arc::new(callback),
118+
last_query: Default::default(),
119+
query: None,
120+
}
121+
}
122+
}
123+
124+
impl<T: 'static + Send + Sync, D: 'static + Send + Sync> AsyncHook for DynamicQueryHandler<T, D> {
125+
type Event = String;
126+
127+
fn handle_event(&mut self, query: Self::Event, _timeout: Option<Instant>) -> Option<Instant> {
128+
if query == self.last_query {
129+
// If the search query reverts to the last one we requested, no need to
130+
// make a new request.
131+
self.query = None;
132+
None
133+
} else {
134+
self.query = Some(query);
135+
Some(Instant::now() + Duration::from_millis(275))
136+
}
137+
}
138+
139+
fn finish_debounce(&mut self) {
140+
let Some(query) = self.query.take() else { return };
141+
self.last_query = query.clone();
142+
let callback = self.callback.clone();
143+
144+
crate::job::dispatch_blocking(move |editor, compositor| {
145+
let Some(Overlay { content: picker, .. }) = compositor.find::<Overlay<Picker<T, D>>>() else {
146+
return;
147+
};
148+
// Increment the version number to cancel any ongoing requests.
149+
picker.version.fetch_add(1, atomic::Ordering::Relaxed);
150+
picker.matcher.restart(false);
151+
picker.dynamic_query_running = true;
152+
let injector = picker.injector();
153+
let get_options = (callback)(query, editor, picker.editor_data.clone(), &injector);
154+
tokio::spawn(async move {
155+
if let Err(err) = get_options.await {
156+
log::info!("Dynamic request failed: {err}");
157+
}
158+
159+
crate::job::dispatch(|_editor, compositor| {
160+
let Some(Overlay { content: picker, .. }) = compositor.find::<Overlay<Picker<T, D>>>() else {
161+
return;
162+
};
163+
picker.dynamic_query_running = false;
164+
}).await;
165+
});
166+
})
167+
}
168+
}

0 commit comments

Comments
 (0)