mirror of
https://github.com/helix-editor/helix.git
synced 2025-01-19 05:27:07 +04:00
Refactor Picker in terms of columns
`menu::Item` is replaced with column configurations for each picker which control how a column is displayed and whether it is passed to nucleo for filtering. (This is used for dynamic pickers so that we can filter those items with the dynamic picker callback rather than nucleo.) The picker has a new lucene-like syntax that can be used to filter the picker only on certain criteria. If a filter is not specified, the text in the prompt applies to the picker's configured "primary" column. Adding column configurations for each picker is left for the child commit.
This commit is contained in:
parent
dae3841a75
commit
f40fca88e0
@ -297,6 +297,7 @@ #### Interface
|
|||||||
| `ui.bufferline.background` | Style for bufferline background |
|
| `ui.bufferline.background` | Style for bufferline background |
|
||||||
| `ui.popup` | Documentation popups (e.g. Space + k) |
|
| `ui.popup` | Documentation popups (e.g. Space + k) |
|
||||||
| `ui.popup.info` | Prompt for multiple key options |
|
| `ui.popup.info` | Prompt for multiple key options |
|
||||||
|
| `ui.picker.header` | Column names in pickers with multiple columns |
|
||||||
| `ui.window` | Borderlines separating splits |
|
| `ui.window` | Borderlines separating splits |
|
||||||
| `ui.help` | Description box for commands |
|
| `ui.help` | Description box for commands |
|
||||||
| `ui.text` | Default text style, command prompts, popup text, etc. |
|
| `ui.text` | Default text style, command prompts, popup text, etc. |
|
||||||
|
@ -2317,7 +2317,9 @@ fn format(&self, current_path: &Self::Data) -> Row {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (picker, injector) = Picker::stream(current_path);
|
// TODO
|
||||||
|
let columns = vec![];
|
||||||
|
let (picker, injector) = Picker::stream(columns, current_path);
|
||||||
|
|
||||||
let dedup_symlinks = file_picker_config.deduplicate_links;
|
let dedup_symlinks = file_picker_config.deduplicate_links;
|
||||||
let absolute_root = search_root
|
let absolute_root = search_root
|
||||||
@ -2420,6 +2422,7 @@ fn format(&self, current_path: &Self::Data) -> Row {
|
|||||||
let call = move |_: &mut Editor, compositor: &mut Compositor| {
|
let call = move |_: &mut Editor, compositor: &mut Compositor| {
|
||||||
let picker = Picker::with_stream(
|
let picker = Picker::with_stream(
|
||||||
picker,
|
picker,
|
||||||
|
0,
|
||||||
injector,
|
injector,
|
||||||
move |cx, FileResult { path, line_num }, action| {
|
move |cx, FileResult { path, line_num }, action| {
|
||||||
let doc = match cx.editor.open(path, action) {
|
let doc = match cx.editor.open(path, action) {
|
||||||
@ -2937,7 +2940,8 @@ fn format(&self, _data: &Self::Data) -> Row {
|
|||||||
// mru
|
// mru
|
||||||
items.sort_unstable_by_key(|item| std::cmp::Reverse(item.focused_at));
|
items.sort_unstable_by_key(|item| std::cmp::Reverse(item.focused_at));
|
||||||
|
|
||||||
let picker = Picker::new(items, (), |cx, meta, action| {
|
let columns = vec![];
|
||||||
|
let picker = Picker::new(columns, 0, items, (), |cx, meta, action| {
|
||||||
cx.editor.switch(meta.id, action);
|
cx.editor.switch(meta.id, action);
|
||||||
})
|
})
|
||||||
.with_preview(|editor, meta| {
|
.with_preview(|editor, meta| {
|
||||||
@ -3014,7 +3018,10 @@ fn format(&self, _data: &Self::Data) -> Row {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let columns = vec![];
|
||||||
let picker = Picker::new(
|
let picker = Picker::new(
|
||||||
|
columns,
|
||||||
|
0,
|
||||||
cx.editor
|
cx.editor
|
||||||
.tree
|
.tree
|
||||||
.views()
|
.views()
|
||||||
@ -3180,7 +3187,8 @@ pub fn command_palette(cx: &mut Context) {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let picker = Picker::new(commands, keymap, move |cx, command, _action| {
|
let columns = vec![];
|
||||||
|
let picker = Picker::new(columns, 0, commands, keymap, move |cx, command, _action| {
|
||||||
let mut ctx = Context {
|
let mut ctx = Context {
|
||||||
register,
|
register,
|
||||||
count,
|
count,
|
||||||
|
@ -73,9 +73,14 @@ fn thread_picker(
|
|||||||
let debugger = debugger!(editor);
|
let debugger = debugger!(editor);
|
||||||
|
|
||||||
let thread_states = debugger.thread_states.clone();
|
let thread_states = debugger.thread_states.clone();
|
||||||
let picker = Picker::new(threads, thread_states, move |cx, thread, _action| {
|
let columns = vec![];
|
||||||
callback_fn(cx.editor, thread)
|
let picker = Picker::new(
|
||||||
})
|
columns,
|
||||||
|
0,
|
||||||
|
threads,
|
||||||
|
thread_states,
|
||||||
|
move |cx, thread, _action| callback_fn(cx.editor, thread),
|
||||||
|
)
|
||||||
.with_preview(move |editor, thread| {
|
.with_preview(move |editor, thread| {
|
||||||
let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
|
let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
|
||||||
let frame = frames.first()?;
|
let frame = frames.first()?;
|
||||||
@ -268,7 +273,11 @@ pub fn dap_launch(cx: &mut Context) {
|
|||||||
|
|
||||||
let templates = config.templates.clone();
|
let templates = config.templates.clone();
|
||||||
|
|
||||||
|
let columns = vec![];
|
||||||
|
|
||||||
cx.push_layer(Box::new(overlaid(Picker::new(
|
cx.push_layer(Box::new(overlaid(Picker::new(
|
||||||
|
columns,
|
||||||
|
0,
|
||||||
templates,
|
templates,
|
||||||
(),
|
(),
|
||||||
|cx, template, _action| {
|
|cx, template, _action| {
|
||||||
@ -736,7 +745,8 @@ pub fn dap_switch_stack_frame(cx: &mut Context) {
|
|||||||
|
|
||||||
let frames = debugger.stack_frames[&thread_id].clone();
|
let frames = debugger.stack_frames[&thread_id].clone();
|
||||||
|
|
||||||
let picker = Picker::new(frames, (), move |cx, frame, _action| {
|
let columns = vec![];
|
||||||
|
let picker = Picker::new(columns, 0, frames, (), move |cx, frame, _action| {
|
||||||
let debugger = debugger!(cx.editor);
|
let debugger = debugger!(cx.editor);
|
||||||
// TODO: this should be simpler to find
|
// TODO: this should be simpler to find
|
||||||
let pos = debugger.stack_frames[&thread_id]
|
let pos = debugger.stack_frames[&thread_id]
|
||||||
|
@ -241,18 +241,25 @@ fn jump_to_position(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type SymbolPicker = Picker<SymbolInformationItem>;
|
type SymbolPicker = Picker<SymbolInformationItem, Option<lsp::Url>>;
|
||||||
|
|
||||||
fn sym_picker(symbols: Vec<SymbolInformationItem>, current_path: Option<lsp::Url>) -> SymbolPicker {
|
fn sym_picker(symbols: Vec<SymbolInformationItem>, current_path: Option<lsp::Url>) -> SymbolPicker {
|
||||||
// TODO: drop current_path comparison and instead use workspace: bool flag?
|
// TODO: drop current_path comparison and instead use workspace: bool flag?
|
||||||
Picker::new(symbols, current_path, move |cx, item, action| {
|
let columns = vec![];
|
||||||
jump_to_location(
|
Picker::new(
|
||||||
cx.editor,
|
columns,
|
||||||
&item.symbol.location,
|
0,
|
||||||
item.offset_encoding,
|
symbols,
|
||||||
action,
|
current_path,
|
||||||
);
|
move |cx, item, action| {
|
||||||
})
|
jump_to_location(
|
||||||
|
cx.editor,
|
||||||
|
&item.symbol.location,
|
||||||
|
item.offset_encoding,
|
||||||
|
action,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
.with_preview(move |_editor, item| Some(location_to_file_location(&item.symbol.location)))
|
.with_preview(move |_editor, item| Some(location_to_file_location(&item.symbol.location)))
|
||||||
.truncate_start(false)
|
.truncate_start(false)
|
||||||
}
|
}
|
||||||
@ -263,11 +270,13 @@ enum DiagnosticsFormat {
|
|||||||
HideSourcePath,
|
HideSourcePath,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DiagnosticsPicker = Picker<PickerDiagnostic, (DiagnosticStyles, DiagnosticsFormat)>;
|
||||||
|
|
||||||
fn diag_picker(
|
fn diag_picker(
|
||||||
cx: &Context,
|
cx: &Context,
|
||||||
diagnostics: BTreeMap<PathBuf, Vec<(lsp::Diagnostic, LanguageServerId)>>,
|
diagnostics: BTreeMap<PathBuf, Vec<(lsp::Diagnostic, LanguageServerId)>>,
|
||||||
format: DiagnosticsFormat,
|
format: DiagnosticsFormat,
|
||||||
) -> Picker<PickerDiagnostic> {
|
) -> DiagnosticsPicker {
|
||||||
// TODO: drop current_path comparison and instead use workspace: bool flag?
|
// TODO: drop current_path comparison and instead use workspace: bool flag?
|
||||||
|
|
||||||
// flatten the map to a vec of (url, diag) pairs
|
// flatten the map to a vec of (url, diag) pairs
|
||||||
@ -293,7 +302,10 @@ fn diag_picker(
|
|||||||
error: cx.editor.theme.get("error"),
|
error: cx.editor.theme.get("error"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let columns = vec![];
|
||||||
Picker::new(
|
Picker::new(
|
||||||
|
columns,
|
||||||
|
0,
|
||||||
flat_diag,
|
flat_diag,
|
||||||
(styles, format),
|
(styles, format),
|
||||||
move |cx,
|
move |cx,
|
||||||
@ -817,7 +829,8 @@ fn goto_impl(
|
|||||||
}
|
}
|
||||||
[] => unreachable!("`locations` should be non-empty for `goto_impl`"),
|
[] => unreachable!("`locations` should be non-empty for `goto_impl`"),
|
||||||
_locations => {
|
_locations => {
|
||||||
let picker = Picker::new(locations, cwdir, move |cx, location, action| {
|
let columns = vec![];
|
||||||
|
let picker = Picker::new(columns, 0, locations, cwdir, move |cx, location, action| {
|
||||||
jump_to_location(cx.editor, location, offset_encoding, action)
|
jump_to_location(cx.editor, location, offset_encoding, action)
|
||||||
})
|
})
|
||||||
.with_preview(move |_editor, location| Some(location_to_file_location(location)));
|
.with_preview(move |_editor, location| Some(location_to_file_location(location)));
|
||||||
|
@ -9,7 +9,6 @@
|
|||||||
use helix_core::fuzzy::fuzzy_match;
|
use helix_core::fuzzy::fuzzy_match;
|
||||||
use helix_core::indent::MAX_INDENT;
|
use helix_core::indent::MAX_INDENT;
|
||||||
use helix_core::{line_ending, shellwords::Shellwords};
|
use helix_core::{line_ending, shellwords::Shellwords};
|
||||||
use helix_lsp::LanguageServerId;
|
|
||||||
use helix_view::document::{read_to_string, DEFAULT_LANGUAGE_NAME};
|
use helix_view::document::{read_to_string, DEFAULT_LANGUAGE_NAME};
|
||||||
use helix_view::editor::{CloseError, ConfigEvent};
|
use helix_view::editor::{CloseError, ConfigEvent};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@ -1378,16 +1377,6 @@ fn lsp_workspace_command(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LsIdCommand(LanguageServerId, helix_lsp::lsp::Command);
|
|
||||||
|
|
||||||
impl ui::menu::Item for LsIdCommand {
|
|
||||||
type Data = ();
|
|
||||||
|
|
||||||
fn format(&self, _data: &Self::Data) -> Row {
|
|
||||||
self.1.title.as_str().into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let doc = doc!(cx.editor);
|
let doc = doc!(cx.editor);
|
||||||
let ls_id_commands = doc
|
let ls_id_commands = doc
|
||||||
.language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
|
.language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
|
||||||
@ -1402,7 +1391,7 @@ fn format(&self, _data: &Self::Data) -> Row {
|
|||||||
if args.is_empty() {
|
if args.is_empty() {
|
||||||
let commands = ls_id_commands
|
let commands = ls_id_commands
|
||||||
.map(|(ls_id, command)| {
|
.map(|(ls_id, command)| {
|
||||||
LsIdCommand(
|
(
|
||||||
ls_id,
|
ls_id,
|
||||||
helix_lsp::lsp::Command {
|
helix_lsp::lsp::Command {
|
||||||
title: command.clone(),
|
title: command.clone(),
|
||||||
@ -1415,10 +1404,13 @@ fn format(&self, _data: &Self::Data) -> Row {
|
|||||||
let callback = async move {
|
let callback = async move {
|
||||||
let call: job::Callback = Callback::EditorCompositor(Box::new(
|
let call: job::Callback = Callback::EditorCompositor(Box::new(
|
||||||
move |_editor: &mut Editor, compositor: &mut Compositor| {
|
move |_editor: &mut Editor, compositor: &mut Compositor| {
|
||||||
|
let columns = vec![];
|
||||||
let picker = ui::Picker::new(
|
let picker = ui::Picker::new(
|
||||||
|
columns,
|
||||||
|
0,
|
||||||
commands,
|
commands,
|
||||||
(),
|
(),
|
||||||
move |cx, LsIdCommand(ls_id, command), _action| {
|
move |cx, (ls_id, command), _action| {
|
||||||
execute_lsp_command(cx.editor, *ls_id, command.clone());
|
execute_lsp_command(cx.editor, *ls_id, command.clone());
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
use helix_stdx::rope;
|
use helix_stdx::rope;
|
||||||
pub use markdown::Markdown;
|
pub use markdown::Markdown;
|
||||||
pub use menu::Menu;
|
pub use menu::Menu;
|
||||||
pub use picker::{DynamicPicker, FileLocation, Picker};
|
pub use picker::{Column as PickerColumn, DynamicPicker, FileLocation, Picker};
|
||||||
pub use popup::Popup;
|
pub use popup::Popup;
|
||||||
pub use prompt::{Prompt, PromptEvent};
|
pub use prompt::{Prompt, PromptEvent};
|
||||||
pub use spinner::{ProgressSpinners, Spinner};
|
pub use spinner::{ProgressSpinners, Spinner};
|
||||||
@ -170,7 +170,9 @@ pub fn raw_regex_prompt(
|
|||||||
cx.push_layer(Box::new(prompt));
|
cx.push_layer(Box::new(prompt));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker<PathBuf> {
|
type FilePicker = Picker<PathBuf, PathBuf>;
|
||||||
|
|
||||||
|
pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker {
|
||||||
use ignore::{types::TypesBuilder, WalkBuilder};
|
use ignore::{types::TypesBuilder, WalkBuilder};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
@ -217,16 +219,23 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker
|
|||||||
});
|
});
|
||||||
log::debug!("file_picker init {:?}", Instant::now().duration_since(now));
|
log::debug!("file_picker init {:?}", Instant::now().duration_since(now));
|
||||||
|
|
||||||
let picker = Picker::new(Vec::new(), root, move |cx, path: &PathBuf, action| {
|
let columns = vec![];
|
||||||
if let Err(e) = cx.editor.open(path, action) {
|
let picker = Picker::new(
|
||||||
let err = if let Some(err) = e.source() {
|
columns,
|
||||||
format!("{}", err)
|
0,
|
||||||
} else {
|
Vec::new(),
|
||||||
format!("unable to open \"{}\"", path.display())
|
root,
|
||||||
};
|
move |cx, path: &PathBuf, action| {
|
||||||
cx.editor.set_error(err);
|
if let Err(e) = cx.editor.open(path, action) {
|
||||||
}
|
let err = if let Some(err) = e.source() {
|
||||||
})
|
format!("{}", err)
|
||||||
|
} else {
|
||||||
|
format!("unable to open \"{}\"", path.display())
|
||||||
|
};
|
||||||
|
cx.editor.set_error(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
.with_preview(|_editor, path| Some((path.clone().into(), None)));
|
.with_preview(|_editor, path| Some((path.clone().into(), None)));
|
||||||
let injector = picker.injector();
|
let injector = picker.injector();
|
||||||
let timeout = std::time::Instant::now() + std::time::Duration::from_millis(30);
|
let timeout = std::time::Instant::now() + std::time::Duration::from_millis(30);
|
||||||
|
@ -21,12 +21,13 @@
|
|||||||
buffer::Buffer as Surface,
|
buffer::Buffer as Surface,
|
||||||
layout::Constraint,
|
layout::Constraint,
|
||||||
text::{Span, Spans},
|
text::{Span, Spans},
|
||||||
widgets::{Block, BorderType, Cell, Table},
|
widgets::{Block, BorderType, Cell, Row, Table},
|
||||||
};
|
};
|
||||||
|
|
||||||
use tui::widgets::Widget;
|
use tui::widgets::Widget;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
io::Read,
|
io::Read,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
@ -49,9 +50,9 @@
|
|||||||
Document, DocumentId, Editor,
|
Document, DocumentId, Editor,
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::handlers::PreviewHighlightHandler;
|
use super::overlay::Overlay;
|
||||||
|
|
||||||
use super::{menu::Item, overlay::Overlay};
|
use self::handlers::PreviewHighlightHandler;
|
||||||
|
|
||||||
pub const ID: &str = "picker";
|
pub const ID: &str = "picker";
|
||||||
|
|
||||||
@ -129,38 +130,36 @@ fn placeholder(&self) -> &str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn item_to_nucleo<T: Item>(item: T, editor_data: &T::Data) -> Option<(T, Utf32String)> {
|
fn inject_nucleo_item<T, D>(
|
||||||
let row = item.format(editor_data);
|
injector: &nucleo::Injector<T>,
|
||||||
let mut cells = row.cells.iter();
|
columns: &[Column<T, D>],
|
||||||
let mut text = String::with_capacity(row.cell_text().map(|cell| cell.len()).sum());
|
item: T,
|
||||||
let cell = cells.next()?;
|
editor_data: &D,
|
||||||
if let Some(cell) = cell.content.lines.first() {
|
) {
|
||||||
for span in &cell.0 {
|
let column_texts: Vec<Utf32String> = columns
|
||||||
text.push_str(&span.content);
|
.iter()
|
||||||
|
.filter(|column| column.filter)
|
||||||
|
.map(|column| column.format_text(&item, editor_data).into())
|
||||||
|
.collect();
|
||||||
|
injector.push(item, |dst| {
|
||||||
|
for (i, text) in column_texts.into_iter().enumerate() {
|
||||||
|
dst[i] = text;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
for cell in cells {
|
|
||||||
text.push(' ');
|
|
||||||
if let Some(cell) = cell.content.lines.first() {
|
|
||||||
for span in &cell.0 {
|
|
||||||
text.push_str(&span.content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some((item, text.into()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Injector<T: Item> {
|
pub struct Injector<T, D> {
|
||||||
dst: nucleo::Injector<T>,
|
dst: nucleo::Injector<T>,
|
||||||
editor_data: Arc<T::Data>,
|
columns: Arc<[Column<T, D>]>,
|
||||||
|
editor_data: Arc<D>,
|
||||||
shutown: Arc<AtomicBool>,
|
shutown: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Item> Clone for Injector<T> {
|
impl<I, D> Clone for Injector<I, D> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Injector {
|
Injector {
|
||||||
dst: self.dst.clone(),
|
dst: self.dst.clone(),
|
||||||
|
columns: self.columns.clone(),
|
||||||
editor_data: self.editor_data.clone(),
|
editor_data: self.editor_data.clone(),
|
||||||
shutown: self.shutown.clone(),
|
shutown: self.shutown.clone(),
|
||||||
}
|
}
|
||||||
@ -169,21 +168,56 @@ fn clone(&self) -> Self {
|
|||||||
|
|
||||||
pub struct InjectorShutdown;
|
pub struct InjectorShutdown;
|
||||||
|
|
||||||
impl<T: Item> Injector<T> {
|
impl<T, D> Injector<T, D> {
|
||||||
pub fn push(&self, item: T) -> Result<(), InjectorShutdown> {
|
pub fn push(&self, item: T) -> Result<(), InjectorShutdown> {
|
||||||
if self.shutown.load(atomic::Ordering::Relaxed) {
|
if self.shutown.load(atomic::Ordering::Relaxed) {
|
||||||
return Err(InjectorShutdown);
|
return Err(InjectorShutdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((item, matcher_text)) = item_to_nucleo(item, &self.editor_data) {
|
inject_nucleo_item(&self.dst, &self.columns, item, &self.editor_data);
|
||||||
self.dst.push(item, |dst| dst[0] = matcher_text);
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Picker<T: Item> {
|
type ColumnFormatFn<T, D> = for<'a> fn(&'a T, &'a D) -> Cell<'a>;
|
||||||
editor_data: Arc<T::Data>,
|
|
||||||
|
pub struct Column<T, D> {
|
||||||
|
name: Arc<str>,
|
||||||
|
format: ColumnFormatFn<T, D>,
|
||||||
|
/// Whether the column should be passed to nucleo for matching and filtering.
|
||||||
|
/// `DynamicPicker` uses this so that the dynamic column (for example regex in
|
||||||
|
/// global search) is not used for filtering twice.
|
||||||
|
filter: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, D> Column<T, D> {
|
||||||
|
pub fn new(name: impl Into<Arc<str>>, format: ColumnFormatFn<T, D>) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.into(),
|
||||||
|
format,
|
||||||
|
filter: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn without_filtering(mut self) -> Self {
|
||||||
|
self.filter = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format<'a>(&self, item: &'a T, data: &'a D) -> Cell<'a> {
|
||||||
|
(self.format)(item, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_text<'a>(&self, item: &'a T, data: &'a D) -> Cow<'a, str> {
|
||||||
|
let text: String = self.format(item, data).content.into();
|
||||||
|
text.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Picker<T: 'static + Send + Sync, D: 'static> {
|
||||||
|
columns: Arc<[Column<T, D>]>,
|
||||||
|
primary_column: usize,
|
||||||
|
editor_data: Arc<D>,
|
||||||
shutdown: Arc<AtomicBool>,
|
shutdown: Arc<AtomicBool>,
|
||||||
matcher: Nucleo<T>,
|
matcher: Nucleo<T>,
|
||||||
|
|
||||||
@ -211,16 +245,19 @@ pub struct Picker<T: Item> {
|
|||||||
preview_highlight_handler: Sender<Arc<Path>>,
|
preview_highlight_handler: Sender<Arc<Path>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Item + 'static> Picker<T> {
|
impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
|
||||||
pub fn stream(editor_data: T::Data) -> (Nucleo<T>, Injector<T>) {
|
pub fn stream(columns: Vec<Column<T, D>>, editor_data: D) -> (Nucleo<T>, Injector<T, D>) {
|
||||||
|
let matcher_columns = columns.iter().filter(|col| col.filter).count() as u32;
|
||||||
|
assert!(matcher_columns > 0);
|
||||||
let matcher = Nucleo::new(
|
let matcher = Nucleo::new(
|
||||||
Config::DEFAULT,
|
Config::DEFAULT,
|
||||||
Arc::new(helix_event::request_redraw),
|
Arc::new(helix_event::request_redraw),
|
||||||
None,
|
None,
|
||||||
1,
|
matcher_columns,
|
||||||
);
|
);
|
||||||
let streamer = Injector {
|
let streamer = Injector {
|
||||||
dst: matcher.injector(),
|
dst: matcher.injector(),
|
||||||
|
columns: columns.into(),
|
||||||
editor_data: Arc::new(editor_data),
|
editor_data: Arc::new(editor_data),
|
||||||
shutown: Arc::new(AtomicBool::new(false)),
|
shutown: Arc::new(AtomicBool::new(false)),
|
||||||
};
|
};
|
||||||
@ -228,24 +265,28 @@ pub fn stream(editor_data: T::Data) -> (Nucleo<T>, Injector<T>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
columns: Vec<Column<T, D>>,
|
||||||
|
primary_column: usize,
|
||||||
options: Vec<T>,
|
options: Vec<T>,
|
||||||
editor_data: T::Data,
|
editor_data: D,
|
||||||
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
|
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let matcher_columns = columns.iter().filter(|col| col.filter).count() as u32;
|
||||||
|
assert!(matcher_columns > 0);
|
||||||
let matcher = Nucleo::new(
|
let matcher = Nucleo::new(
|
||||||
Config::DEFAULT,
|
Config::DEFAULT,
|
||||||
Arc::new(helix_event::request_redraw),
|
Arc::new(helix_event::request_redraw),
|
||||||
None,
|
None,
|
||||||
1,
|
matcher_columns,
|
||||||
);
|
);
|
||||||
let injector = matcher.injector();
|
let injector = matcher.injector();
|
||||||
for item in options {
|
for item in options {
|
||||||
if let Some((item, matcher_text)) = item_to_nucleo(item, &editor_data) {
|
inject_nucleo_item(&injector, &columns, item, &editor_data);
|
||||||
injector.push(item, |dst| dst[0] = matcher_text);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Self::with(
|
Self::with(
|
||||||
matcher,
|
matcher,
|
||||||
|
columns.into(),
|
||||||
|
primary_column,
|
||||||
Arc::new(editor_data),
|
Arc::new(editor_data),
|
||||||
Arc::new(AtomicBool::new(false)),
|
Arc::new(AtomicBool::new(false)),
|
||||||
callback_fn,
|
callback_fn,
|
||||||
@ -254,18 +295,30 @@ pub fn new(
|
|||||||
|
|
||||||
pub fn with_stream(
|
pub fn with_stream(
|
||||||
matcher: Nucleo<T>,
|
matcher: Nucleo<T>,
|
||||||
injector: Injector<T>,
|
primary_column: usize,
|
||||||
|
injector: Injector<T, D>,
|
||||||
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
|
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self::with(matcher, injector.editor_data, injector.shutown, callback_fn)
|
Self::with(
|
||||||
|
matcher,
|
||||||
|
injector.columns,
|
||||||
|
primary_column,
|
||||||
|
injector.editor_data,
|
||||||
|
injector.shutown,
|
||||||
|
callback_fn,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with(
|
fn with(
|
||||||
matcher: Nucleo<T>,
|
matcher: Nucleo<T>,
|
||||||
editor_data: Arc<T::Data>,
|
columns: Arc<[Column<T, D>]>,
|
||||||
|
default_column: usize,
|
||||||
|
editor_data: Arc<D>,
|
||||||
shutdown: Arc<AtomicBool>,
|
shutdown: Arc<AtomicBool>,
|
||||||
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
|
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
assert!(!columns.is_empty());
|
||||||
|
|
||||||
let prompt = Prompt::new(
|
let prompt = Prompt::new(
|
||||||
"".into(),
|
"".into(),
|
||||||
None,
|
None,
|
||||||
@ -273,7 +326,14 @@ fn with(
|
|||||||
|_editor: &mut Context, _pattern: &str, _event: PromptEvent| {},
|
|_editor: &mut Context, _pattern: &str, _event: PromptEvent| {},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let widths = columns
|
||||||
|
.iter()
|
||||||
|
.map(|column| Constraint::Length(column.name.chars().count() as u16))
|
||||||
|
.collect();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
columns,
|
||||||
|
primary_column: default_column,
|
||||||
matcher,
|
matcher,
|
||||||
editor_data,
|
editor_data,
|
||||||
shutdown,
|
shutdown,
|
||||||
@ -284,17 +344,18 @@ fn with(
|
|||||||
show_preview: true,
|
show_preview: true,
|
||||||
callback_fn: Box::new(callback_fn),
|
callback_fn: Box::new(callback_fn),
|
||||||
completion_height: 0,
|
completion_height: 0,
|
||||||
widths: Vec::new(),
|
widths,
|
||||||
preview_cache: HashMap::new(),
|
preview_cache: HashMap::new(),
|
||||||
read_buffer: Vec::with_capacity(1024),
|
read_buffer: Vec::with_capacity(1024),
|
||||||
file_fn: None,
|
file_fn: None,
|
||||||
preview_highlight_handler: PreviewHighlightHandler::<T>::default().spawn(),
|
preview_highlight_handler: PreviewHighlightHandler::<T, D>::default().spawn(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn injector(&self) -> Injector<T> {
|
pub fn injector(&self) -> Injector<T, D> {
|
||||||
Injector {
|
Injector {
|
||||||
dst: self.matcher.injector(),
|
dst: self.matcher.injector(),
|
||||||
|
columns: self.columns.clone(),
|
||||||
editor_data: self.editor_data.clone(),
|
editor_data: self.editor_data.clone(),
|
||||||
shutown: self.shutdown.clone(),
|
shutown: self.shutdown.clone(),
|
||||||
}
|
}
|
||||||
@ -316,13 +377,17 @@ pub fn with_preview(
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_line(mut self, line: String, editor: &Editor) -> Self {
|
||||||
|
self.prompt.set_line(line, editor);
|
||||||
|
self.handle_prompt_change();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_options(&mut self, new_options: Vec<T>) {
|
pub fn set_options(&mut self, new_options: Vec<T>) {
|
||||||
self.matcher.restart(false);
|
self.matcher.restart(false);
|
||||||
let injector = self.matcher.injector();
|
let injector = self.matcher.injector();
|
||||||
for item in new_options {
|
for item in new_options {
|
||||||
if let Some((item, matcher_text)) = item_to_nucleo(item, &self.editor_data) {
|
inject_nucleo_item(&injector, &self.columns, item, &self.editor_data);
|
||||||
injector.push(item, |dst| dst[0] = matcher_text);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -376,27 +441,39 @@ pub fn selection(&self) -> Option<&T> {
|
|||||||
.map(|item| item.data)
|
.map(|item| item.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn header_height(&self) -> u16 {
|
||||||
|
if self.columns.len() > 1 {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn toggle_preview(&mut self) {
|
pub fn toggle_preview(&mut self) {
|
||||||
self.show_preview = !self.show_preview;
|
self.show_preview = !self.show_preview;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
|
fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
|
||||||
if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) {
|
if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) {
|
||||||
let pattern = self.prompt.line();
|
self.handle_prompt_change();
|
||||||
// TODO: better track how the pattern has changed
|
|
||||||
if pattern != &self.previous_pattern {
|
|
||||||
self.matcher.pattern.reparse(
|
|
||||||
0,
|
|
||||||
pattern,
|
|
||||||
CaseMatching::Smart,
|
|
||||||
pattern.starts_with(&self.previous_pattern),
|
|
||||||
);
|
|
||||||
self.previous_pattern = pattern.clone();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
EventResult::Consumed(None)
|
EventResult::Consumed(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_prompt_change(&mut self) {
|
||||||
|
let pattern = self.prompt.line();
|
||||||
|
// TODO: better track how the pattern has changed
|
||||||
|
if pattern != &self.previous_pattern {
|
||||||
|
self.matcher.pattern.reparse(
|
||||||
|
0,
|
||||||
|
pattern,
|
||||||
|
CaseMatching::Smart,
|
||||||
|
pattern.starts_with(&self.previous_pattern),
|
||||||
|
);
|
||||||
|
self.previous_pattern = pattern.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn current_file(&self, editor: &Editor) -> Option<FileLocation> {
|
fn current_file(&self, editor: &Editor) -> Option<FileLocation> {
|
||||||
self.selection()
|
self.selection()
|
||||||
.and_then(|current| (self.file_fn.as_ref()?)(editor, current))
|
.and_then(|current| (self.file_fn.as_ref()?)(editor, current))
|
||||||
@ -526,7 +603,7 @@ fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context)
|
|||||||
// -- Render the contents:
|
// -- Render the contents:
|
||||||
// subtract area of prompt from top
|
// subtract area of prompt from top
|
||||||
let inner = inner.clip_top(2);
|
let inner = inner.clip_top(2);
|
||||||
let rows = inner.height as u32;
|
let rows = inner.height.saturating_sub(self.header_height()) as u32;
|
||||||
let offset = self.cursor - (self.cursor % std::cmp::max(1, rows));
|
let offset = self.cursor - (self.cursor % std::cmp::max(1, rows));
|
||||||
let cursor = self.cursor.saturating_sub(offset);
|
let cursor = self.cursor.saturating_sub(offset);
|
||||||
let end = offset
|
let end = offset
|
||||||
@ -540,83 +617,94 @@ fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
let options = snapshot.matched_items(offset..end).map(|item| {
|
let options = snapshot.matched_items(offset..end).map(|item| {
|
||||||
snapshot.pattern().column_pattern(0).indices(
|
|
||||||
item.matcher_columns[0].slice(..),
|
|
||||||
&mut matcher,
|
|
||||||
&mut indices,
|
|
||||||
);
|
|
||||||
indices.sort_unstable();
|
|
||||||
indices.dedup();
|
|
||||||
let mut row = item.data.format(&self.editor_data);
|
|
||||||
|
|
||||||
let mut grapheme_idx = 0u32;
|
|
||||||
let mut indices = indices.drain(..);
|
|
||||||
let mut next_highlight_idx = indices.next().unwrap_or(u32::MAX);
|
|
||||||
if self.widths.len() < row.cells.len() {
|
|
||||||
self.widths.resize(row.cells.len(), Constraint::Length(0));
|
|
||||||
}
|
|
||||||
let mut widths = self.widths.iter_mut();
|
let mut widths = self.widths.iter_mut();
|
||||||
for cell in &mut row.cells {
|
let mut matcher_index = 0;
|
||||||
|
|
||||||
|
Row::new(self.columns.iter().map(|column| {
|
||||||
let Some(Constraint::Length(max_width)) = widths.next() else {
|
let Some(Constraint::Length(max_width)) = widths.next() else {
|
||||||
unreachable!();
|
unreachable!();
|
||||||
};
|
};
|
||||||
|
let mut cell = column.format(item.data, &self.editor_data);
|
||||||
|
let width = if column.filter {
|
||||||
|
snapshot.pattern().column_pattern(matcher_index).indices(
|
||||||
|
item.matcher_columns[matcher_index].slice(..),
|
||||||
|
&mut matcher,
|
||||||
|
&mut indices,
|
||||||
|
);
|
||||||
|
indices.sort_unstable();
|
||||||
|
indices.dedup();
|
||||||
|
let mut indices = indices.drain(..);
|
||||||
|
let mut next_highlight_idx = indices.next().unwrap_or(u32::MAX);
|
||||||
|
let mut span_list = Vec::new();
|
||||||
|
let mut current_span = String::new();
|
||||||
|
let mut current_style = Style::default();
|
||||||
|
let mut grapheme_idx = 0u32;
|
||||||
|
let mut width = 0;
|
||||||
|
|
||||||
// merge index highlights on top of existing hightlights
|
let spans: &[Span] =
|
||||||
let mut span_list = Vec::new();
|
cell.content.lines.first().map_or(&[], |it| it.0.as_slice());
|
||||||
let mut current_span = String::new();
|
for span in spans {
|
||||||
let mut current_style = Style::default();
|
// this looks like a bug on first glance, we are iterating
|
||||||
let mut width = 0;
|
// graphemes but treating them as char indices. The reason that
|
||||||
|
// this is correct is that nucleo will only ever consider the first char
|
||||||
let spans: &[Span] = cell.content.lines.first().map_or(&[], |it| it.0.as_slice());
|
// of a grapheme (and discard the rest of the grapheme) so the indices
|
||||||
for span in spans {
|
// returned by nucleo are essentially grapheme indecies
|
||||||
// this looks like a bug on first glance, we are iterating
|
for grapheme in span.content.graphemes(true) {
|
||||||
// graphemes but treating them as char indices. The reason that
|
let style = if grapheme_idx == next_highlight_idx {
|
||||||
// this is correct is that nucleo will only ever consider the first char
|
next_highlight_idx = indices.next().unwrap_or(u32::MAX);
|
||||||
// of a grapheme (and discard the rest of the grapheme) so the indices
|
span.style.patch(highlight_style)
|
||||||
// returned by nucleo are essentially grapheme indecies
|
} else {
|
||||||
for grapheme in span.content.graphemes(true) {
|
span.style
|
||||||
let style = if grapheme_idx == next_highlight_idx {
|
};
|
||||||
next_highlight_idx = indices.next().unwrap_or(u32::MAX);
|
if style != current_style {
|
||||||
span.style.patch(highlight_style)
|
if !current_span.is_empty() {
|
||||||
} else {
|
span_list.push(Span::styled(current_span, current_style))
|
||||||
span.style
|
}
|
||||||
};
|
current_span = String::new();
|
||||||
if style != current_style {
|
current_style = style;
|
||||||
if !current_span.is_empty() {
|
|
||||||
span_list.push(Span::styled(current_span, current_style))
|
|
||||||
}
|
}
|
||||||
current_span = String::new();
|
current_span.push_str(grapheme);
|
||||||
current_style = style;
|
grapheme_idx += 1;
|
||||||
}
|
}
|
||||||
current_span.push_str(grapheme);
|
width += span.width();
|
||||||
grapheme_idx += 1;
|
|
||||||
}
|
}
|
||||||
width += span.width();
|
|
||||||
}
|
|
||||||
|
|
||||||
span_list.push(Span::styled(current_span, current_style));
|
span_list.push(Span::styled(current_span, current_style));
|
||||||
|
cell = Cell::from(Spans::from(span_list));
|
||||||
|
matcher_index += 1;
|
||||||
|
width
|
||||||
|
} else {
|
||||||
|
cell.content
|
||||||
|
.lines
|
||||||
|
.first()
|
||||||
|
.map(|line| line.width())
|
||||||
|
.unwrap_or_default()
|
||||||
|
};
|
||||||
|
|
||||||
if width as u16 > *max_width {
|
if width as u16 > *max_width {
|
||||||
*max_width = width as u16;
|
*max_width = width as u16;
|
||||||
}
|
}
|
||||||
*cell = Cell::from(Spans::from(span_list));
|
|
||||||
|
|
||||||
// spacer
|
cell
|
||||||
if grapheme_idx == next_highlight_idx {
|
}))
|
||||||
next_highlight_idx = indices.next().unwrap_or(u32::MAX);
|
|
||||||
}
|
|
||||||
grapheme_idx += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
row
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let table = Table::new(options)
|
let mut table = Table::new(options)
|
||||||
.style(text_style)
|
.style(text_style)
|
||||||
.highlight_style(selected)
|
.highlight_style(selected)
|
||||||
.highlight_symbol(" > ")
|
.highlight_symbol(" > ")
|
||||||
.column_spacing(1)
|
.column_spacing(1)
|
||||||
.widths(&self.widths);
|
.widths(&self.widths);
|
||||||
|
|
||||||
|
// -- Header
|
||||||
|
if self.columns.len() > 1 {
|
||||||
|
let header_style = cx.editor.theme.get("ui.picker.header");
|
||||||
|
|
||||||
|
table = table.header(Row::new(self.columns.iter().map(|column| {
|
||||||
|
Cell::from(Span::styled(Cow::from(&*column.name), header_style))
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
use tui::widgets::TableState;
|
use tui::widgets::TableState;
|
||||||
|
|
||||||
table.render_table(
|
table.render_table(
|
||||||
@ -746,7 +834,7 @@ fn render_preview(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Item + 'static + Send + Sync> Component for Picker<T> {
|
impl<I: 'static + Send + Sync, D: 'static + Send + Sync> Component for Picker<I, D> {
|
||||||
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||||
// +---------+ +---------+
|
// +---------+ +---------+
|
||||||
// |prompt | |preview |
|
// |prompt | |preview |
|
||||||
@ -872,7 +960,7 @@ fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind)
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn required_size(&mut self, (width, height): (u16, u16)) -> Option<(u16, u16)> {
|
fn required_size(&mut self, (width, height): (u16, u16)) -> Option<(u16, u16)> {
|
||||||
self.completion_height = height.saturating_sub(4);
|
self.completion_height = height.saturating_sub(4 + self.header_height());
|
||||||
Some((width, height))
|
Some((width, height))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -880,7 +968,7 @@ fn id(&self) -> Option<&'static str> {
|
|||||||
Some(ID)
|
Some(ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<T: Item> Drop for Picker<T> {
|
impl<T: 'static + Send + Sync, D> Drop for Picker<T, D> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
// ensure we cancel any ongoing background threads streaming into the picker
|
// ensure we cancel any ongoing background threads streaming into the picker
|
||||||
self.shutdown.store(true, atomic::Ordering::Relaxed)
|
self.shutdown.store(true, atomic::Ordering::Relaxed)
|
||||||
@ -896,14 +984,14 @@ fn drop(&mut self) {
|
|||||||
|
|
||||||
/// A picker that updates its contents via a callback whenever the
|
/// A picker that updates its contents via a callback whenever the
|
||||||
/// query string changes. Useful for live grep, workspace symbols, etc.
|
/// query string changes. Useful for live grep, workspace symbols, etc.
|
||||||
pub struct DynamicPicker<T: ui::menu::Item + Send + Sync> {
|
pub struct DynamicPicker<T: 'static + Send + Sync, D: 'static + Send + Sync> {
|
||||||
file_picker: Picker<T>,
|
file_picker: Picker<T, D>,
|
||||||
query_callback: DynQueryCallback<T>,
|
query_callback: DynQueryCallback<T>,
|
||||||
query: String,
|
query: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: ui::menu::Item + Send + Sync> DynamicPicker<T> {
|
impl<T: Send + Sync, D: Send + Sync> DynamicPicker<T, D> {
|
||||||
pub fn new(file_picker: Picker<T>, query_callback: DynQueryCallback<T>) -> Self {
|
pub fn new(file_picker: Picker<T, D>, query_callback: DynQueryCallback<T>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
file_picker,
|
file_picker,
|
||||||
query_callback,
|
query_callback,
|
||||||
@ -912,20 +1000,22 @@ pub fn new(file_picker: Picker<T>, query_callback: DynQueryCallback<T>) -> Self
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Item + Send + Sync + 'static> Component for DynamicPicker<T> {
|
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) {
|
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||||
self.file_picker.render(area, surface, cx);
|
self.file_picker.render(area, surface, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
|
fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
|
||||||
let event_result = self.file_picker.handle_event(event, cx);
|
let event_result = self.file_picker.handle_event(event, cx);
|
||||||
let current_query = self.file_picker.prompt.line();
|
let Some(current_query) = self.file_picker.primary_query() else {
|
||||||
|
return event_result;
|
||||||
|
};
|
||||||
|
|
||||||
if !matches!(event, Event::IdleTimeout) || self.query == *current_query {
|
if !matches!(event, Event::IdleTimeout) || self.query == *current_query {
|
||||||
return event_result;
|
return event_result;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.query.clone_from(current_query);
|
self.query = current_query.to_string();
|
||||||
|
|
||||||
let new_options = (self.query_callback)(current_query.to_owned(), cx.editor);
|
let new_options = (self.query_callback)(current_query.to_owned(), cx.editor);
|
||||||
|
|
||||||
@ -934,7 +1024,7 @@ fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
|
|||||||
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,
|
// Wrapping of pickers in overlay is done outside the picker code,
|
||||||
// so this is fragile and will break if wrapped in some other widget.
|
// so this is fragile and will break if wrapped in some other widget.
|
||||||
let picker = match compositor.find_id::<Overlay<DynamicPicker<T>>>(ID) {
|
let picker = match compositor.find_id::<Overlay<Self>>(ID) {
|
||||||
Some(overlay) => &mut overlay.content.file_picker,
|
Some(overlay) => &mut overlay.content.file_picker,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
|
@ -3,19 +3,16 @@
|
|||||||
use helix_event::AsyncHook;
|
use helix_event::AsyncHook;
|
||||||
use tokio::time::Instant;
|
use tokio::time::Instant;
|
||||||
|
|
||||||
use crate::{
|
use crate::{job, ui::overlay::Overlay};
|
||||||
job,
|
|
||||||
ui::{menu::Item, overlay::Overlay},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{CachedPreview, DynamicPicker, Picker};
|
use super::{CachedPreview, DynamicPicker, Picker};
|
||||||
|
|
||||||
pub(super) struct PreviewHighlightHandler<T: Item> {
|
pub(super) struct PreviewHighlightHandler<T: 'static + Send + Sync, D: 'static + Send + Sync> {
|
||||||
trigger: Option<Arc<Path>>,
|
trigger: Option<Arc<Path>>,
|
||||||
phantom_data: std::marker::PhantomData<T>,
|
phantom_data: std::marker::PhantomData<(T, D)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Item> Default for PreviewHighlightHandler<T> {
|
impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Default for PreviewHighlightHandler<T, D> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
trigger: None,
|
trigger: None,
|
||||||
@ -24,7 +21,9 @@ fn default() -> Self {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Item> AsyncHook for PreviewHighlightHandler<T> {
|
impl<T: 'static + Send + Sync, D: 'static + Send + Sync> AsyncHook
|
||||||
|
for PreviewHighlightHandler<T, D>
|
||||||
|
{
|
||||||
type Event = Arc<Path>;
|
type Event = Arc<Path>;
|
||||||
|
|
||||||
fn handle_event(
|
fn handle_event(
|
||||||
@ -51,9 +50,9 @@ fn finish_debounce(&mut self) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
job::dispatch_blocking(move |editor, compositor| {
|
job::dispatch_blocking(move |editor, compositor| {
|
||||||
let picker = match compositor.find::<Overlay<Picker<T>>>() {
|
let picker = match compositor.find::<Overlay<Picker<T, D>>>() {
|
||||||
Some(Overlay { content, .. }) => content,
|
Some(Overlay { content, .. }) => content,
|
||||||
None => match compositor.find::<Overlay<DynamicPicker<T>>>() {
|
None => match compositor.find::<Overlay<DynamicPicker<T, D>>>() {
|
||||||
Some(Overlay { content, .. }) => &mut content.file_picker,
|
Some(Overlay { content, .. }) => &mut content.file_picker,
|
||||||
None => return,
|
None => return,
|
||||||
},
|
},
|
||||||
@ -88,10 +87,10 @@ fn finish_debounce(&mut self) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
job::dispatch_blocking(move |editor, compositor| {
|
job::dispatch_blocking(move |editor, compositor| {
|
||||||
let picker = match compositor.find::<Overlay<Picker<T>>>() {
|
let picker = match compositor.find::<Overlay<Picker<T, D>>>() {
|
||||||
Some(Overlay { content, .. }) => Some(content),
|
Some(Overlay { content, .. }) => Some(content),
|
||||||
None => compositor
|
None => compositor
|
||||||
.find::<Overlay<DynamicPicker<T>>>()
|
.find::<Overlay<DynamicPicker<T, D>>>()
|
||||||
.map(|overlay| &mut overlay.content.file_picker),
|
.map(|overlay| &mut overlay.content.file_picker),
|
||||||
};
|
};
|
||||||
let Some(picker) = picker else {
|
let Some(picker) = picker else {
|
||||||
|
@ -93,11 +93,15 @@ pub fn new(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_line(mut self, line: String, editor: &Editor) -> Self {
|
pub fn with_line(mut self, line: String, editor: &Editor) -> Self {
|
||||||
|
self.set_line(line, editor);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_line(&mut self, line: String, editor: &Editor) {
|
||||||
let cursor = line.len();
|
let cursor = line.len();
|
||||||
self.line = line;
|
self.line = line;
|
||||||
self.cursor = cursor;
|
self.cursor = cursor;
|
||||||
self.recalculate_completion(editor);
|
self.recalculate_completion(editor);
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_language(
|
pub fn with_language(
|
||||||
|
Loading…
Reference in New Issue
Block a user