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 { trigger: Option>, phantom_data: std::marker::PhantomData<(T, D)>, } impl Default for PreviewHighlightHandler { fn default() -> Self { Self { trigger: None, phantom_data: Default::default(), } } } impl AsyncHook for PreviewHighlightHandler { type Event = Arc; fn handle_event( &mut self, path: Self::Event, timeout: Option, ) -> Option { 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::>>() else { return; }; let Some(CachedPreview::Document(ref mut doc)) = picker.preview_cache.get_mut(&path) else { return; }; if doc.syntax().is_some() { return; } let Some(language) = doc.language_config().map(|config| config.language()) else { return; }; let loader = editor.syn_loader.load(); let text = doc.text().clone(); tokio::task::spawn_blocking(move || { let syntax = match helix_core::Syntax::new(text.slice(..), language, &loader) { Ok(syntax) => syntax, Err(err) => { log::info!("highlighting picker preview failed: {err}"); return; } }; job::dispatch_blocking(move |editor, compositor| { let Some(Overlay { content: picker, .. }) = compositor.find::>>() 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 DynamicQueryChange { pub query: Arc, pub is_paste: bool, } pub(super) struct DynamicQueryHandler { callback: Arc>, // 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, query: Option>, } impl DynamicQueryHandler { pub(super) fn new(callback: DynQueryCallback, duration_ms: Option) -> Self { Self { callback: Arc::new(callback), debounce: Duration::from_millis(duration_ms.unwrap_or(100)), last_query: "".into(), query: None, } } } impl AsyncHook for DynamicQueryHandler { type Event = DynamicQueryChange; fn handle_event(&mut self, change: Self::Event, _timeout: Option) -> Option { let DynamicQueryChange { query, is_paste } = change; 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); if is_paste { self.finish_debounce(); None } else { 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::>>() 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. }); }) } }