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.rs182
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.
+ });
+ })
+ }
+}