mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-22 09:26:19 +04:00
Consolidate DynamicPicker into Picker
DynamicPicker is a thin wrapper over Picker that holds some additional state, similar to the old FilePicker type. Like with FilePicker, we want to fold the two types together, having Picker optionally hold that extra state. The DynamicPicker is a little more complicated than FilePicker was though - it holds a query callback and current query string in state and provides some debounce for queries using the IdleTimeout event. We can move all of that state and debounce logic into an AsyncHook implementation, introduced here as `DynamicQueryHandler`. The hook receives updates to the primary query and debounces those events so that once a query has been idle for a short time (275ms) we re-run the query. A standard Picker created through `new` for example can be promoted into a Dynamic picker by chaining the new `with_dynamic_query` function, very similar to FilePicker's replacement `with_preview`. The workspace symbol picker has been migrated to the new way of writing dynamic pickers as an example. The child commit will promote global search into a dynamic Picker as well.
This commit is contained in:
parent
11f809c177
commit
9e31ba5475
@ -26,7 +26,7 @@
|
||||
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::{
|
||||
@ -413,6 +413,8 @@ fn nested_to_flat(
|
||||
}
|
||||
|
||||
pub fn workspace_symbol_picker(cx: &mut Context) {
|
||||
use crate::ui::picker::Injector;
|
||||
|
||||
let doc = doc!(cx.editor);
|
||||
if doc
|
||||
.language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
|
||||
@ -424,19 +426,21 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
|
||||
return;
|
||||
}
|
||||
|
||||
let get_symbols = move |pattern: String, editor: &mut Editor| {
|
||||
let get_symbols = |pattern: &str, editor: &mut Editor, _data, injector: &Injector<_, _>| {
|
||||
let doc = doc!(editor);
|
||||
let mut seen_language_servers = HashSet::new();
|
||||
let mut futures: FuturesOrdered<_> = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
|
||||
.filter(|ls| seen_language_servers.insert(ls.id()))
|
||||
.map(|language_server| {
|
||||
let request = language_server.workspace_symbols(pattern.clone()).unwrap();
|
||||
let request = language_server
|
||||
.workspace_symbols(pattern.to_string())
|
||||
.unwrap();
|
||||
let offset_encoding = language_server.offset_encoding();
|
||||
async move {
|
||||
let json = request.await?;
|
||||
|
||||
let response =
|
||||
let response: Vec<_> =
|
||||
serde_json::from_value::<Option<Vec<lsp::SymbolInformation>>>(json)?
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
@ -455,29 +459,56 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
|
||||
editor.set_error("No configured language server supports workspace symbols");
|
||||
}
|
||||
|
||||
let injector = injector.clone();
|
||||
async move {
|
||||
let mut symbols = Vec::new();
|
||||
// TODO if one symbol request errors, all other requests are discarded (even if they're valid)
|
||||
while let Some(mut lsp_items) = futures.try_next().await? {
|
||||
symbols.append(&mut lsp_items);
|
||||
while let Some(lsp_items) = futures.try_next().await? {
|
||||
for item in lsp_items {
|
||||
injector.push(item)?;
|
||||
}
|
||||
anyhow::Ok(symbols)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
.boxed()
|
||||
};
|
||||
let columns = vec![
|
||||
ui::PickerColumn::new("kind", |item: &SymbolInformationItem, _| {
|
||||
display_symbol_kind(item.symbol.kind).into()
|
||||
}),
|
||||
ui::PickerColumn::new("name", |item: &SymbolInformationItem, _| {
|
||||
item.symbol.name.as_str().into()
|
||||
})
|
||||
.without_filtering(),
|
||||
ui::PickerColumn::new("path", |item: &SymbolInformationItem, _| {
|
||||
match item.symbol.location.uri.to_file_path() {
|
||||
Ok(path) => path::get_relative_path(path.as_path())
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
.into(),
|
||||
Err(_) => item.symbol.location.uri.to_string().into(),
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
let initial_symbols = get_symbols("".to_owned(), cx.editor);
|
||||
let picker = Picker::new(
|
||||
columns,
|
||||
1, // name column
|
||||
vec![],
|
||||
(),
|
||||
move |cx, item, action| {
|
||||
jump_to_location(
|
||||
cx.editor,
|
||||
&item.symbol.location,
|
||||
item.offset_encoding,
|
||||
action,
|
||||
);
|
||||
},
|
||||
)
|
||||
.with_preview(|_editor, item| Some(location_to_file_location(&item.symbol.location)))
|
||||
.with_dynamic_query(get_symbols, None)
|
||||
.truncate_start(false);
|
||||
|
||||
cx.jobs.callback(async move {
|
||||
let symbols = initial_symbols.await?;
|
||||
let call = move |_editor: &mut Editor, compositor: &mut Compositor| {
|
||||
let picker = sym_picker(symbols, true);
|
||||
let dyn_picker = DynamicPicker::new(picker, Box::new(get_symbols));
|
||||
compositor.push(Box::new(overlaid(dyn_picker)))
|
||||
};
|
||||
|
||||
Ok(Callback::EditorCompositor(Box::new(call)))
|
||||
});
|
||||
cx.push_layer(Box::new(overlaid(picker)));
|
||||
}
|
||||
|
||||
pub fn diagnostics_picker(cx: &mut Context) {
|
||||
|
@ -21,7 +21,7 @@
|
||||
use helix_stdx::rope;
|
||||
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};
|
||||
|
@ -4,9 +4,7 @@
|
||||
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},
|
||||
@ -53,9 +51,7 @@
|
||||
Document, DocumentId, Editor,
|
||||
};
|
||||
|
||||
use super::overlay::Overlay;
|
||||
|
||||
use self::handlers::PreviewHighlightHandler;
|
||||
use self::handlers::{DynamicQueryHandler, PreviewHighlightHandler};
|
||||
|
||||
pub const ID: &str = "picker";
|
||||
|
||||
@ -221,6 +217,11 @@ fn format_text<'a>(&self, item: &'a T, data: &'a D) -> Cow<'a, str> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a new list of options to replace the contents of the picker
|
||||
/// when called with the current picker query,
|
||||
type DynQueryCallback<T, D> =
|
||||
fn(&str, &mut Editor, Arc<D>, &Injector<T, D>) -> BoxFuture<'static, anyhow::Result<()>>;
|
||||
|
||||
pub struct Picker<T: 'static + Send + Sync, D: 'static> {
|
||||
columns: Arc<[Column<T, D>]>,
|
||||
primary_column: usize,
|
||||
@ -250,6 +251,7 @@ pub struct Picker<T: 'static + Send + Sync, D: 'static> {
|
||||
file_fn: Option<FileCallback<T>>,
|
||||
/// An event handler for syntax highlighting the currently previewed file.
|
||||
preview_highlight_handler: Sender<Arc<Path>>,
|
||||
dynamic_query_handler: Option<Sender<Arc<str>>>,
|
||||
}
|
||||
|
||||
impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
|
||||
@ -359,6 +361,7 @@ fn with(
|
||||
read_buffer: Vec::with_capacity(1024),
|
||||
file_fn: None,
|
||||
preview_highlight_handler: PreviewHighlightHandler::<T, D>::default().spawn(),
|
||||
dynamic_query_handler: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -394,12 +397,15 @@ pub fn with_line(mut self, line: String, editor: &Editor) -> Self {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_options(&mut self, new_options: Vec<T>) {
|
||||
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<T, D>,
|
||||
debounce_ms: Option<u64>,
|
||||
) -> Self {
|
||||
let handler = DynamicQueryHandler::new(callback, debounce_ms).spawn();
|
||||
helix_event::send_blocking(&handler, self.primary_query());
|
||||
self.dynamic_query_handler = Some(handler);
|
||||
self
|
||||
}
|
||||
|
||||
/// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`)
|
||||
@ -514,6 +520,11 @@ fn handle_prompt_change(&mut self) {
|
||||
is_append,
|
||||
);
|
||||
}
|
||||
// If this is a dynamic picker, notify the query hook that the primary
|
||||
// query might have been updated.
|
||||
if let Some(handler) = &self.dynamic_query_handler {
|
||||
helix_event::send_blocking(handler, self.primary_query());
|
||||
}
|
||||
}
|
||||
|
||||
fn current_file(&self, editor: &Editor) -> Option<FileLocation> {
|
||||
@ -621,7 +632,11 @@ fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context)
|
||||
|
||||
let count = format!(
|
||||
"{}{}/{}",
|
||||
if status.running { "(running) " } else { "" },
|
||||
if status.running || self.matcher.active_injectors() > 0 {
|
||||
"(running) "
|
||||
} else {
|
||||
""
|
||||
},
|
||||
snapshot.matched_item_count(),
|
||||
snapshot.item_count(),
|
||||
);
|
||||
@ -1018,74 +1033,3 @@ fn drop(&mut self) {
|
||||
}
|
||||
|
||||
type PickerCallback<T> = Box<dyn Fn(&mut Context, &T, Action)>;
|
||||
|
||||
/// Returns a new list of options to replace the contents of the picker
|
||||
/// when called with the current picker query,
|
||||
pub type DynQueryCallback<T> =
|
||||
Box<dyn Fn(String, &mut Editor) -> BoxFuture<'static, anyhow::Result<Vec<T>>>>;
|
||||
|
||||
/// A picker that updates its contents via a callback whenever the
|
||||
/// query string changes. Useful for live grep, workspace symbols, etc.
|
||||
pub struct DynamicPicker<T: 'static + Send + Sync, D: 'static + Send + Sync> {
|
||||
file_picker: Picker<T, D>,
|
||||
query_callback: DynQueryCallback<T>,
|
||||
query: String,
|
||||
}
|
||||
|
||||
impl<T: Send + Sync, D: Send + Sync> DynamicPicker<T, D> {
|
||||
pub fn new(file_picker: Picker<T, D>, query_callback: DynQueryCallback<T>) -> Self {
|
||||
Self {
|
||||
file_picker,
|
||||
query_callback,
|
||||
query: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send + Sync + 'static, D: Send + Sync + 'static> Component for DynamicPicker<T, D> {
|
||||
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::<Overlay<Self>>(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<Position>, 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)
|
||||
}
|
||||
}
|
||||
|
@ -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::{job, ui::overlay::Overlay};
|
||||
|
||||
use super::{CachedPreview, DynamicPicker, Picker};
|
||||
use super::{CachedPreview, DynQueryCallback, Picker};
|
||||
|
||||
pub(super) struct PreviewHighlightHandler<T: 'static + Send + Sync, D: 'static + Send + Sync> {
|
||||
trigger: Option<Arc<Path>>,
|
||||
@ -50,12 +54,11 @@ fn finish_debounce(&mut self) {
|
||||
};
|
||||
|
||||
job::dispatch_blocking(move |editor, compositor| {
|
||||
let picker = match compositor.find::<Overlay<Picker<T, D>>>() {
|
||||
Some(Overlay { content, .. }) => content,
|
||||
None => match compositor.find::<Overlay<DynamicPicker<T, D>>>() {
|
||||
Some(Overlay { content, .. }) => &mut content.file_picker,
|
||||
None => return,
|
||||
},
|
||||
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)
|
||||
@ -87,13 +90,10 @@ fn finish_debounce(&mut self) {
|
||||
};
|
||||
|
||||
job::dispatch_blocking(move |editor, compositor| {
|
||||
let picker = match compositor.find::<Overlay<Picker<T, D>>>() {
|
||||
Some(Overlay { content, .. }) => Some(content),
|
||||
None => compositor
|
||||
.find::<Overlay<DynamicPicker<T, D>>>()
|
||||
.map(|overlay| &mut overlay.content.file_picker),
|
||||
};
|
||||
let Some(picker) = picker else {
|
||||
let Some(Overlay {
|
||||
content: picker, ..
|
||||
}) = compositor.find::<Overlay<Picker<T, D>>>()
|
||||
else {
|
||||
log::info!("picker closed before syntax highlighting finished");
|
||||
return;
|
||||
};
|
||||
@ -114,3 +114,72 @@ fn finish_debounce(&mut self) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
// The picker's shows its running indicator when there are any active
|
||||
// injectors. When we're done injecting new options, drop the injector
|
||||
// and request a redraw to remove the running indicator.
|
||||
drop(injector);
|
||||
helix_event::request_redraw();
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user