Skip to content

Commit

Permalink
Use an AsyncHook for picker preview highlighting
Browse files Browse the repository at this point in the history
The picker previously used the IdleTimeout event as a trigger for
syntax-highlighting the currently selected document in the preview pane.
This is a bit ad-hoc now that the event system has landed and we can
refactor towards an AsyncHook (like those used for LSP completion and
signature-help). This should resolve some odd scenarios where the
preview did not highlight because of a race between the idle timeout
and items appearing in the picker.
  • Loading branch information
the-mikedavis committed Feb 15, 2024
1 parent 59369d9 commit b57472f
Showing 1 changed file with 116 additions and 87 deletions.
203 changes: 116 additions & 87 deletions helix-term/src/ui/picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ use crate::{
EditorView,
},
};
use futures_util::{future::BoxFuture, FutureExt};
use futures_util::future::BoxFuture;
use helix_event::AsyncHook;
use nucleo::pattern::CaseMatching;
use nucleo::{Config, Nucleo, Utf32String};
use tokio::time::Instant;
use tui::{
buffer::Buffer as Surface,
layout::Constraint,
Expand All @@ -30,6 +32,7 @@ use std::{
atomic::{self, AtomicBool},
Arc,
},
time::Duration,
};

use crate::ui::{Prompt, PromptEvent};
Expand Down Expand Up @@ -201,6 +204,8 @@ pub struct Picker<T: Item> {
read_buffer: Vec<u8>,
/// Given an item in the picker, return the file path and line number to display.
file_fn: Option<FileCallback<T>>,
/// An event handler for syntax highlighting the currently previewed file.
preview_highlight_handler: tokio::sync::mpsc::Sender<()>,
}

impl<T: Item + 'static> Picker<T> {
Expand Down Expand Up @@ -265,6 +270,9 @@ impl<T: Item + 'static> Picker<T> {
|_editor: &mut Context, _pattern: &str, _event: PromptEvent| {},
);

let preview_highlight_handler = PreviewHighlightHandler::<T>::default().spawn();
helix_event::send_blocking(&preview_highlight_handler, ());

Self {
matcher,
editor_data,
Expand All @@ -280,6 +288,7 @@ impl<T: Item + 'static> Picker<T> {
preview_cache: HashMap::new(),
read_buffer: Vec::with_capacity(1024),
file_fn: None,
preview_highlight_handler,
}
}

Expand Down Expand Up @@ -315,6 +324,7 @@ impl<T: Item + 'static> Picker<T> {
injector.push(item, |dst| dst[0] = matcher_text);
}
}
helix_event::send_blocking(&self.preview_highlight_handler, ());
}

/// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`)
Expand Down Expand Up @@ -443,84 +453,6 @@ impl<T: Item + 'static> Picker<T> {
}
}

fn handle_idle_timeout(&mut self, cx: &mut Context) -> EventResult {
let Some((current_file, _)) = self.current_file(cx.editor) else {
return EventResult::Consumed(None);
};

// Try to find a document in the cache
let doc = match &current_file {
PathOrId::Id(doc_id) => doc_mut!(cx.editor, doc_id),
PathOrId::Path(path) => match self.preview_cache.get_mut(path) {
Some(CachedPreview::Document(ref mut doc)) => doc,
_ => return EventResult::Consumed(None),
},
};

let mut callback: Option<compositor::Callback> = None;

// Then attempt to highlight it if it has no language set
if doc.language_config().is_none() {
if let Some(language_config) = doc.detect_language_config(&cx.editor.syn_loader.load())
{
doc.language = Some(language_config.clone());
let text = doc.text().clone();
let loader = cx.editor.syn_loader.clone();
let job = tokio::task::spawn_blocking(move || {
let syntax = language_config
.highlight_config(&loader.load().scopes())
.and_then(|highlight_config| {
Syntax::new(text.slice(..), highlight_config, loader)
});
let callback = move |editor: &mut Editor, compositor: &mut Compositor| {
let Some(syntax) = syntax else {
log::info!("highlighting picker item failed");
return;
};
let picker = match compositor.find::<Overlay<Self>>() {
Some(Overlay { content, .. }) => Some(content),
None => compositor
.find::<Overlay<DynamicPicker<T>>>()
.map(|overlay| &mut overlay.content.file_picker),
};
let Some(picker) = picker else {
log::info!("picker closed before syntax highlighting finished");
return;
};
// Try to find a document in the cache
let doc = match current_file {
PathOrId::Id(doc_id) => doc_mut!(editor, &doc_id),
PathOrId::Path(path) => match picker.preview_cache.get_mut(&path) {
Some(CachedPreview::Document(ref mut doc)) => {
let diagnostics = Editor::doc_diagnostics(
&editor.language_servers,
&editor.diagnostics,
doc,
);
doc.replace_diagnostics(diagnostics, &[], None);
doc
}
_ => return,
},
};
doc.syntax = Some(syntax);
};
Callback::EditorCompositor(Box::new(callback))
});
let tmp: compositor::Callback = Box::new(move |_, ctx| {
ctx.jobs
.callback(job.map(|res| res.map_err(anyhow::Error::from)))
});
callback = Some(Box::new(tmp))
}
}

// QUESTION: do we want to compute inlay hints in pickers too ? Probably not for now
// but it could be interesting in the future

EventResult::Consumed(callback)
}

fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let status = self.matcher.tick(10);
let snapshot = self.matcher.snapshot();
Expand Down Expand Up @@ -828,9 +760,6 @@ impl<T: Item + 'static + Send + Sync> Component for Picker<T> {
}

fn handle_event(&mut self, event: &Event, ctx: &mut Context) -> EventResult {
if let Event::IdleTimeout = event {
return self.handle_idle_timeout(ctx);
}
// TODO: keybinds for scrolling preview

let key_event = match event {
Expand Down Expand Up @@ -863,9 +792,6 @@ impl<T: Item + 'static + Send + Sync> Component for Picker<T> {
EventResult::Consumed(Some(callback))
};

// So that idle timeout retriggers
ctx.editor.reset_idle_timer();

match key_event {
shift!(Tab) | key!(Up) | ctrl!('p') => {
self.move_by(1, Direction::Backward);
Expand Down Expand Up @@ -917,6 +843,8 @@ impl<T: Item + 'static + Send + Sync> Component for Picker<T> {
}
}

helix_event::send_blocking(&self.preview_highlight_handler, ());

EventResult::Consumed(None)
}

Expand Down Expand Up @@ -949,6 +877,108 @@ impl<T: Item> Drop for Picker<T> {

type PickerCallback<T> = Box<dyn Fn(&mut Context, &T, Action)>;

struct PreviewHighlightHandler<T: ui::menu::Item> {
phantom_data: std::marker::PhantomData<T>,
}

impl<T: ui::menu::Item> Default for PreviewHighlightHandler<T> {
fn default() -> Self {
Self {
phantom_data: Default::default(),
}
}
}

impl<T: ui::menu::Item> AsyncHook for PreviewHighlightHandler<T> {
type Event = ();

fn handle_event(
&mut self,
_event: Self::Event,
_timeout: Option<tokio::time::Instant>,
) -> Option<tokio::time::Instant> {
Some(Instant::now() + Duration::from_millis(250))
}

fn finish_debounce(&mut self) {
crate::job::dispatch_blocking(|editor, compositor| {
let picker = match compositor.find::<Overlay<Picker<T>>>() {
Some(Overlay { content, .. }) => content,
None => match compositor.find::<Overlay<DynamicPicker<T>>>() {
Some(Overlay { content, .. }) => &mut content.file_picker,
None => return,
},
};

let Some((current_file, _)) = picker.current_file(editor) else { return };
// Try to find a document in the cache
let doc = match &current_file {
PathOrId::Id(doc_id) => doc_mut!(editor, doc_id),
PathOrId::Path(path) => match picker.preview_cache.get_mut(path) {
Some(CachedPreview::Document(ref mut doc)) => doc,
_ => 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::spawn(async move {
let Some(syntax) =
language_config
.highlight_config(&loader.load().scopes())
.and_then(|highlight_config| {
Syntax::new(
text.slice(..),
highlight_config,
loader,
)
}) else {
log::info!("highlighting picker item failed");
return;
};

crate::job::dispatch(move |editor, compositor| {
let picker = match compositor.find::<Overlay<Picker<T>>>() {
Some(Overlay { content, .. }) => Some(content),
None => compositor
.find::<Overlay<DynamicPicker<T>>>()
.map(|overlay| &mut overlay.content.file_picker),
};
let Some(picker) = picker else {
log::info!("picker closed before syntax highlighting finished");
return;
};
// Try to find a document in the cache
let doc = match current_file {
PathOrId::Id(doc_id) => doc_mut!(editor, &doc_id),
PathOrId::Path(path) => match picker.preview_cache.get_mut(&path) {
Some(CachedPreview::Document(ref mut doc)) => {
let diagnostics = Editor::doc_diagnostics(
&editor.language_servers,
&editor.diagnostics,
doc,
);
doc.replace_diagnostics(diagnostics, &[], None);
doc
}
_ => return,
},
};
doc.syntax = Some(syntax);
})
.await;
});
});
}
}

/// Returns a new list of options to replace the contents of the picker
/// when called with the current picker query,
pub type DynQueryCallback<T> =
Expand Down Expand Up @@ -991,15 +1021,14 @@ impl<T: Item + Send + Sync + 'static> Component for DynamicPicker<T> {

cx.jobs.callback(async move {
let new_options = new_options.await?;
let callback = Callback::EditorCompositor(Box::new(move |editor, compositor| {
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::<Overlay<DynamicPicker<T>>>(ID) {
Some(overlay) => &mut overlay.content.file_picker,
None => return,
};
picker.set_options(new_options);
editor.reset_idle_timer();
}));
anyhow::Ok(callback)
});
Expand Down

0 comments on commit b57472f

Please sign in to comment.