Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/ui/picker/handlers.rs')
| -rw-r--r-- | helix-term/src/ui/picker/handlers.rs | 182 |
1 files changed, 182 insertions, 0 deletions
diff --git a/helix-term/src/ui/picker/handlers.rs b/helix-term/src/ui/picker/handlers.rs new file mode 100644 index 00000000..4896ccbc --- /dev/null +++ b/helix-term/src/ui/picker/handlers.rs @@ -0,0 +1,182 @@ +use std::{ + path::Path, + sync::{atomic, Arc}, + time::Duration, +}; + +use helix_event::AsyncHook; +use tokio::time::Instant; + +use crate::{job, ui::overlay::Overlay}; + +use super::{CachedPreview, DynQueryCallback, Picker}; + +pub(super) struct PreviewHighlightHandler<T: 'static + Send + Sync, D: 'static + Send + Sync> { + trigger: Option<Arc<Path>>, + phantom_data: std::marker::PhantomData<(T, D)>, +} + +impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Default for PreviewHighlightHandler<T, D> { + fn default() -> Self { + Self { + trigger: None, + phantom_data: Default::default(), + } + } +} + +impl<T: 'static + Send + Sync, D: 'static + Send + Sync> AsyncHook + for PreviewHighlightHandler<T, D> +{ + type Event = Arc<Path>; + + fn handle_event( + &mut self, + path: Self::Event, + timeout: Option<tokio::time::Instant>, + ) -> Option<tokio::time::Instant> { + if self + .trigger + .as_ref() + .is_some_and(|trigger| trigger == &path) + { + // If the path hasn't changed, don't reset the debounce + timeout + } else { + self.trigger = Some(path); + Some(Instant::now() + Duration::from_millis(150)) + } + } + + fn finish_debounce(&mut self) { + let Some(path) = self.trigger.take() else { + return; + }; + + job::dispatch_blocking(move |editor, compositor| { + let Some(Overlay { + content: picker, .. + }) = compositor.find::<Overlay<Picker<T, D>>>() + else { + return; + }; + + let Some(CachedPreview::Document(ref mut doc)) = picker.preview_cache.get_mut(&path) + else { + return; + }; + + if doc.language_config().is_some() { + return; + } + + let Some(language_config) = doc.detect_language_config(&editor.syn_loader.load()) + else { + return; + }; + doc.language = Some(language_config.clone()); + let text = doc.text().clone(); + let loader = editor.syn_loader.clone(); + + tokio::task::spawn_blocking(move || { + let Some(syntax) = language_config + .highlight_config(&loader.load().scopes()) + .and_then(|highlight_config| { + helix_core::Syntax::new(text.slice(..), highlight_config, loader) + }) + else { + log::info!("highlighting picker item failed"); + return; + }; + + job::dispatch_blocking(move |editor, compositor| { + let Some(Overlay { + content: picker, .. + }) = compositor.find::<Overlay<Picker<T, D>>>() + else { + log::info!("picker closed before syntax highlighting finished"); + return; + }; + let Some(CachedPreview::Document(ref mut doc)) = + picker.preview_cache.get_mut(&path) + else { + return; + }; + let diagnostics = helix_view::Editor::doc_diagnostics( + &editor.language_servers, + &editor.diagnostics, + doc, + ); + doc.replace_diagnostics(diagnostics, &[], None); + doc.syntax = Some(syntax); + }); + }); + }); + } +} + +pub(super) struct DynamicQueryHandler<T: 'static + Send + Sync, D: 'static + Send + Sync> { + callback: Arc<DynQueryCallback<T, D>>, + // Duration used as a debounce. + // Defaults to 100ms if not provided via `Picker::with_dynamic_query`. Callers may want to set + // this higher if the dynamic query is expensive - for example global search. + debounce: Duration, + last_query: Arc<str>, + query: Option<Arc<str>>, +} + +impl<T: 'static + Send + Sync, D: 'static + Send + Sync> DynamicQueryHandler<T, D> { + pub(super) fn new(callback: DynQueryCallback<T, D>, duration_ms: Option<u64>) -> Self { + Self { + callback: Arc::new(callback), + debounce: Duration::from_millis(duration_ms.unwrap_or(100)), + last_query: "".into(), + query: None, + } + } +} + +impl<T: 'static + Send + Sync, D: 'static + Send + Sync> AsyncHook for DynamicQueryHandler<T, D> { + type Event = Arc<str>; + + fn handle_event(&mut self, query: Self::Event, _timeout: Option<Instant>) -> Option<Instant> { + 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() + self.debounce) + } + } + + fn finish_debounce(&mut self) { + let Some(query) = self.query.take() else { + return; + }; + self.last_query = query.clone(); + let callback = self.callback.clone(); + + job::dispatch_blocking(move |editor, compositor| { + let Some(Overlay { + content: picker, .. + }) = compositor.find::<Overlay<Picker<T, D>>>() + else { + return; + }; + // Increment the version number to cancel any ongoing requests. + picker.version.fetch_add(1, atomic::Ordering::Relaxed); + picker.matcher.restart(false); + 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}"); + } + // NOTE: the Drop implementation of Injector will request a redraw when the + // injector falls out of scope here, clearing the "running" indicator. + }); + }) + } +} |