diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 86ec2f8021e3f..d575c837d4866 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -27,7 +27,7 @@ use helix_view::{ use crate::{ compositor::{self, Compositor}, job::Callback, - ui::{self, overlay::overlaid, DynamicPicker, FileLocation, Picker, Popup, PromptEvent}, + ui::{self, overlay::overlaid, FileLocation, Picker, Popup, PromptEvent}, }; use std::{ diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index d16daff7d7f6e..e12c4e706dc8d 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -20,7 +20,7 @@ pub use completion::{Completion, CompletionItem}; pub use editor::EditorView; pub use markdown::Markdown; pub use menu::Menu; -pub use picker::{Column as PickerColumn, DynamicPicker, FileLocation, Picker}; +pub use picker::{Column as PickerColumn, FileLocation, Picker}; pub use popup::Popup; pub use prompt::{Prompt, PromptEvent}; pub use spinner::{ProgressSpinners, Spinner}; diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index f4055b8e10530..b7653b59efe88 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -4,9 +4,7 @@ mod query; use crate::{ alt, compositor::{self, Component, Compositor, Context, Event, EventResult}, - ctrl, - job::Callback, - key, shift, + ctrl, key, shift, ui::{ self, document::{render_document, LineDecoration, LinePos, TextRenderer}, @@ -52,8 +50,6 @@ use helix_view::{ pub const ID: &str = "picker"; -use super::overlay::Overlay; - pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72; /// Biggest file size to preview in bytes pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024; @@ -223,6 +219,11 @@ impl Column { } } +/// Returns a new list of options to replace the contents of the picker +/// when called with the current picker query, +type DynQueryCallback = + fn(String, &mut Editor, Arc, &Injector) -> BoxFuture<'static, anyhow::Result<()>>; + pub struct Picker { column_names: Vec<&'static str>, columns: Arc>>, @@ -253,6 +254,8 @@ pub struct Picker { file_fn: Option>, /// An event handler for syntax highlighting the currently previewed file. preview_highlight_handler: tokio::sync::mpsc::Sender>, + dynamic_query_running: bool, + dynamic_query_handler: Option>, } impl Picker { @@ -362,6 +365,8 @@ impl Picker { read_buffer: Vec::with_capacity(1024), file_fn: None, preview_highlight_handler: handlers::PreviewHighlightHandler::::default().spawn(), + dynamic_query_running: false, + dynamic_query_handler: None, } } @@ -396,12 +401,11 @@ impl Picker { self } - pub fn set_options(&mut self, new_options: Vec) { - self.matcher.restart(false); - let injector = self.matcher.injector(); - for item in new_options { - inject_nucleo_item(&injector, &self.columns, item, &self.editor_data); - } + pub fn with_dynamic_query(mut self, callback: DynQueryCallback) -> Self { + let handler = handlers::DynamicQueryHandler::new(callback).spawn(); + helix_event::send_blocking(&handler, self.primary_query().to_string()); + self.dynamic_query_handler = Some(handler); + self } /// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`) @@ -502,6 +506,9 @@ impl Picker { .reparse(i, pattern, CaseMatching::Smart, append); } self.query = new_query; + if let Some(handler) = &self.dynamic_query_handler { + helix_event::send_blocking(handler, self.primary_query().to_string()); + } } } EventResult::Consumed(None) @@ -613,7 +620,11 @@ impl Picker { let count = format!( "{}{}/{}", - if status.running { "(running) " } else { "" }, + if status.running || self.dynamic_query_running { + "(running) " + } else { + "" + }, snapshot.matched_item_count(), snapshot.item_count(), ); @@ -1013,74 +1024,3 @@ impl Drop for Picker { } type PickerCallback = Box; - -/// Returns a new list of options to replace the contents of the picker -/// when called with the current picker query, -pub type DynQueryCallback = - Box BoxFuture<'static, anyhow::Result>>>; - -/// A picker that updates its contents via a callback whenever the -/// query string changes. Useful for live grep, workspace symbols, etc. -pub struct DynamicPicker { - file_picker: Picker, - query_callback: DynQueryCallback, - query: String, -} - -impl DynamicPicker { - pub fn new(file_picker: Picker, query_callback: DynQueryCallback) -> Self { - Self { - file_picker, - query_callback, - query: String::new(), - } - } -} - -impl Component for DynamicPicker { - fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { - self.file_picker.render(area, surface, cx); - } - - fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { - let event_result = self.file_picker.handle_event(event, cx); - let Some(current_query) = self.file_picker.primary_query() else { - return event_result; - }; - - if !matches!(event, Event::IdleTimeout) || self.query == *current_query { - return event_result; - } - - self.query = current_query.to_string(); - - let new_options = (self.query_callback)(current_query.to_owned(), cx.editor); - - cx.jobs.callback(async move { - let new_options = new_options.await?; - let callback = Callback::EditorCompositor(Box::new(move |_editor, compositor| { - // Wrapping of pickers in overlay is done outside the picker code, - // so this is fragile and will break if wrapped in some other widget. - let picker = match compositor.find_id::>(ID) { - Some(overlay) => &mut overlay.content.file_picker, - None => return, - }; - picker.set_options(new_options); - })); - anyhow::Ok(callback) - }); - EventResult::Consumed(None) - } - - fn cursor(&self, area: Rect, ctx: &Editor) -> (Option, CursorKind) { - self.file_picker.cursor(area, ctx) - } - - fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { - self.file_picker.required_size(viewport) - } - - fn id(&self) -> Option<&'static str> { - Some(ID) - } -} diff --git a/helix-term/src/ui/picker/handlers.rs b/helix-term/src/ui/picker/handlers.rs index 214247c9776af..0176a99544348 100644 --- a/helix-term/src/ui/picker/handlers.rs +++ b/helix-term/src/ui/picker/handlers.rs @@ -1,11 +1,15 @@ -use std::{path::Path, sync::Arc, time::Duration}; +use std::{ + path::Path, + sync::{atomic, Arc}, + time::Duration, +}; use helix_event::AsyncHook; use tokio::time::Instant; use crate::ui::overlay::Overlay; -use super::{CachedPreview, DynamicPicker, Picker}; +use super::{CachedPreview, DynQueryCallback, Picker}; pub(super) struct PreviewHighlightHandler { trigger: Option>, @@ -48,12 +52,8 @@ impl AsyncHook let Some(path) = self.trigger.take() else { return }; crate::job::dispatch_blocking(move |editor, compositor| { - let picker = match compositor.find::>>() { - Some(Overlay { content, .. }) => content, - None => match compositor.find::>>() { - Some(Overlay { content, .. }) => &mut content.file_picker, - None => return, - }, + let Some(Overlay { content: picker, .. }) = compositor.find::>>() else { + return; }; let Some(CachedPreview::Document(ref mut doc)) = picker.preview_cache.get_mut(&path) else { @@ -85,13 +85,7 @@ impl AsyncHook }; crate::job::dispatch_blocking(move |editor, compositor| { - let picker = match compositor.find::>>() { - Some(Overlay { content, .. }) => Some(content), - None => compositor - .find::>>() - .map(|overlay| &mut overlay.content.file_picker), - }; - let Some(picker) = picker else { + let Some(Overlay { content: picker, .. }) = compositor.find::>>() else { log::info!("picker closed before syntax highlighting finished"); return; }; @@ -110,3 +104,65 @@ impl AsyncHook }); } } + +pub(super) struct DynamicQueryHandler { + callback: Arc>, + last_query: String, + query: Option, +} + +impl DynamicQueryHandler { + pub(super) fn new(callback: DynQueryCallback) -> Self { + Self { + callback: Arc::new(callback), + last_query: Default::default(), + query: None, + } + } +} + +impl AsyncHook for DynamicQueryHandler { + type Event = String; + + fn handle_event(&mut self, query: Self::Event, _timeout: Option) -> Option { + if query == self.last_query { + // If the search query reverts to the last one we requested, no need to + // make a new request. + self.query = None; + None + } else { + self.query = Some(query); + Some(Instant::now() + Duration::from_millis(275)) + } + } + + fn finish_debounce(&mut self) { + let Some(query) = self.query.take() else { return }; + self.last_query = query.clone(); + let callback = self.callback.clone(); + + crate::job::dispatch_blocking(move |editor, compositor| { + let Some(Overlay { content: picker, .. }) = compositor.find::>>() else { + return; + }; + // Increment the version number to cancel any ongoing requests. + picker.version.fetch_add(1, atomic::Ordering::Relaxed); + picker.matcher.restart(false); + picker.dynamic_query_running = true; + let injector = picker.injector(); + let get_options = (callback)(query, editor, picker.editor_data.clone(), &injector); + tokio::spawn(async move { + if let Err(err) = get_options.await { + log::info!("Dynamic request failed: {err}"); + } + + crate::job::dispatch(|_editor, compositor| { + let Some(Overlay { content: picker, .. }) = compositor.find::>>() else { + return; + }; + picker.dynamic_query_running = false; + }).await; + }); + }) + } +}