From debe1653250f9baa6a5bc667cdca18e18862f39b Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sun, 3 Sep 2023 16:09:50 -0500 Subject: [PATCH] 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. --- helix-term/src/commands.rs | 14 +- helix-term/src/commands/dap.rs | 18 +- helix-term/src/commands/lsp.rs | 35 ++- helix-term/src/commands/typed.rs | 8 +- helix-term/src/ui/mod.rs | 33 ++- helix-term/src/ui/picker.rs | 398 +++++++++++++++++++++---------- helix-term/src/ui/prompt.rs | 6 +- 7 files changed, 356 insertions(+), 156 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 6e3fb93974bd7..9c12e6e0ae979 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2172,7 +2172,9 @@ fn global_search(cx: &mut Context) { 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 absolute_root = search_root @@ -2268,6 +2270,7 @@ fn global_search(cx: &mut Context) { let call = move |_: &mut Editor, compositor: &mut Compositor| { let picker = Picker::with_stream( picker, + 0, injector, move |cx, FileResult { path, line_num }, action| { let doc = match cx.editor.open(path, action) { @@ -2735,7 +2738,8 @@ fn buffer_picker(cx: &mut Context) { // mru 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); }) .with_preview(|editor, meta| { @@ -2812,7 +2816,10 @@ fn jumplist_picker(cx: &mut Context) { } }; + let columns = vec![]; let picker = Picker::new( + columns, + 0, cx.editor .tree .views() @@ -2889,7 +2896,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 { register, count, diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index cc013d1e6b0cd..4147d72ce0925 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -73,9 +73,14 @@ fn thread_picker( let debugger = debugger!(editor); let thread_states = debugger.thread_states.clone(); - let picker = Picker::new(threads, thread_states, move |cx, thread, _action| { - callback_fn(cx.editor, thread) - }) + let columns = vec![]; + let picker = Picker::new( + columns, + 0, + threads, + thread_states, + move |cx, thread, _action| callback_fn(cx.editor, thread), + ) .with_preview(move |editor, thread| { let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?; let frame = frames.get(0)?; @@ -268,7 +273,11 @@ pub fn dap_launch(cx: &mut Context) { let templates = config.templates.clone(); + let columns = vec![]; + cx.push_layer(Box::new(overlaid(Picker::new( + columns, + 0, templates, (), |cx, template, _action| { @@ -730,7 +739,8 @@ pub fn dap_switch_stack_frame(cx: &mut Context) { 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); // TODO: this should be simpler to find let pos = debugger.stack_frames[&thread_id] diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 57be1267ca52a..89a307864cf17 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -238,18 +238,25 @@ fn jump_to_location( } } -type SymbolPicker = Picker; +type SymbolPicker = Picker>; fn sym_picker(symbols: Vec, current_path: Option) -> SymbolPicker { // TODO: drop current_path comparison and instead use workspace: bool flag? - Picker::new(symbols, current_path, move |cx, item, action| { - jump_to_location( - cx.editor, - &item.symbol.location, - item.offset_encoding, - action, - ); - }) + let columns = vec![]; + Picker::new( + columns, + 0, + symbols, + 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))) .truncate_start(false) } @@ -260,12 +267,14 @@ enum DiagnosticsFormat { HideSourcePath, } +type DiagnosticsPicker = Picker; + fn diag_picker( cx: &Context, diagnostics: BTreeMap>, _current_path: Option, format: DiagnosticsFormat, -) -> Picker { +) -> DiagnosticsPicker { // TODO: drop current_path comparison and instead use workspace: bool flag? // flatten the map to a vec of (url, diag) pairs @@ -291,7 +300,10 @@ fn diag_picker( error: cx.editor.theme.get("error"), }; + let columns = vec![]; Picker::new( + columns, + 0, flat_diag, (styles, format), move |cx, @@ -1029,7 +1041,8 @@ fn goto_impl( editor.set_error("No definition found."); } _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) }) .with_preview(move |_editor, location| Some(location_to_file_location(location))); diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 0e1d943177084..03b1bf5559a6e 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1388,9 +1388,11 @@ fn lsp_workspace_command( let callback = async move { let call: job::Callback = Callback::EditorCompositor(Box::new( move |_editor: &mut Editor, compositor: &mut Compositor| { - let picker = ui::Picker::new(commands, (), move |cx, command, _action| { - execute_lsp_command(cx.editor, language_server_id, command.clone()); - }); + let columns = vec![]; + let picker = + ui::Picker::new(columns, 0, commands, (), move |cx, command, _action| { + execute_lsp_command(cx.editor, language_server_id, command.clone()); + }); compositor.push(Box::new(overlaid(picker))) }, )); diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index b21601086127b..4c21013477e9d 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -20,7 +20,7 @@ pub use completion::{Completion, CompletionItem}; pub use editor::EditorView; pub use markdown::Markdown; 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 prompt::{Prompt, PromptEvent}; pub use spinner::{ProgressSpinners, Spinner}; @@ -155,7 +155,9 @@ pub fn regex_prompt( cx.push_layer(Box::new(prompt)); } -pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker { +type FilePicker = Picker; + +pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker { use ignore::{types::TypesBuilder, WalkBuilder}; use std::time::Instant; @@ -199,16 +201,23 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker }); log::debug!("file_picker init {:?}", Instant::now().duration_since(now)); - let picker = Picker::new(Vec::new(), root, move |cx, path: &PathBuf, action| { - 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); - } - }) + let columns = vec![]; + let picker = Picker::new( + columns, + 0, + Vec::new(), + root, + move |cx, path: &PathBuf, action| { + 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))); let injector = picker.injector(); std::thread::spawn(move || { diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 1f94a72cfae3a..632337382aa79 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -17,12 +17,13 @@ use tui::{ buffer::Buffer as Surface, layout::Constraint, text::{Span, Spans}, - widgets::{Block, BorderType, Borders, Cell, Table}, + widgets::{Block, BorderType, Borders, Cell, Row, Table}, }; use tui::widgets::Widget; use std::{ + borrow::Cow, collections::HashMap, io::Read, path::PathBuf, @@ -47,7 +48,7 @@ use helix_view::{ }; pub const ID: &str = "picker"; -use super::{menu::Item, overlay::Overlay}; +use super::overlay::Overlay; pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72; /// Biggest file size to preview in bytes @@ -123,38 +124,36 @@ impl Preview<'_, '_> { } } -fn item_to_nucleo(item: T, editor_data: &T::Data) -> Option<(T, Utf32String)> { - let row = item.format(editor_data); - let mut cells = row.cells.iter(); - let mut text = String::with_capacity(row.cell_text().map(|cell| cell.len()).sum()); - let cell = cells.next()?; - if let Some(cell) = cell.content.lines.first() { - for span in &cell.0 { - text.push_str(&span.content); +fn inject_nucleo_item( + injector: &nucleo::Injector, + columns: &[Column], + item: T, + editor_data: &D, +) { + let column_texts: Vec = columns + .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 { +pub struct Injector { dst: nucleo::Injector, - editor_data: Arc, + columns: Arc>>, + editor_data: Arc, shutown: Arc, } -impl Clone for Injector { +impl Clone for Injector { fn clone(&self) -> Self { Injector { dst: self.dst.clone(), + columns: self.columns.clone(), editor_data: self.editor_data.clone(), shutown: Arc::new(AtomicBool::new(false)), } @@ -163,21 +162,57 @@ impl Clone for Injector { pub struct InjectorShutdown; -impl Injector { +impl Injector { pub fn push(&self, item: T) -> Result<(), InjectorShutdown> { if self.shutown.load(atomic::Ordering::Relaxed) { return Err(InjectorShutdown); } - if let Some((item, matcher_text)) = item_to_nucleo(item, &self.editor_data) { - self.dst.push(item, |dst| dst[0] = matcher_text); - } + inject_nucleo_item(&self.dst, &self.columns, item, &self.editor_data); Ok(()) } } -pub struct Picker { - editor_data: Arc, +type ColumnFormatFn = for<'a> fn(&'a T, &'a D) -> Cell<'a>; + +pub struct Column { + name: &'static str, + format: ColumnFormatFn, + /// 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 Column { + pub fn new(name: &'static str, format: ColumnFormatFn) -> Self { + Self { + name, + 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 { + column_names: Vec<&'static str>, + columns: Arc>>, + primary_column: usize, + editor_data: Arc, shutdown: Arc, matcher: Nucleo, @@ -186,7 +221,7 @@ pub struct Picker { cursor: u32, prompt: Prompt, - previous_pattern: String, + query: HashMap<&'static str, String>, /// Whether to show the preview panel (default true) show_preview: bool, @@ -203,16 +238,19 @@ pub struct Picker { file_fn: Option>, } -impl Picker { - pub fn stream(editor_data: T::Data) -> (Nucleo, Injector) { +impl Picker { + pub fn stream(columns: Vec>, editor_data: D) -> (Nucleo, Injector) { + let matcher_columns = columns.iter().filter(|col| col.filter).count() as u32; + assert!(matcher_columns > 0); let matcher = Nucleo::new( Config::DEFAULT, Arc::new(helix_event::request_redraw), None, - 1, + matcher_columns, ); let streamer = Injector { dst: matcher.injector(), + columns: Arc::new(columns), editor_data: Arc::new(editor_data), shutown: Arc::new(AtomicBool::new(false)), }; @@ -220,24 +258,28 @@ impl Picker { } pub fn new( + columns: Vec>, + primary_column: usize, options: Vec, - editor_data: T::Data, + editor_data: D, callback_fn: impl Fn(&mut Context, &T, Action) + 'static, ) -> Self { + let matcher_columns = columns.iter().filter(|col| col.filter).count() as u32; + assert!(matcher_columns > 0); let matcher = Nucleo::new( Config::DEFAULT, Arc::new(helix_event::request_redraw), None, - 1, + matcher_columns, ); let injector = matcher.injector(); for item in options { - if let Some((item, matcher_text)) = item_to_nucleo(item, &editor_data) { - injector.push(item, |dst| dst[0] = matcher_text); - } + inject_nucleo_item(&injector, &columns, item, &editor_data); } Self::with( matcher, + Arc::new(columns), + primary_column, Arc::new(editor_data), Arc::new(AtomicBool::new(false)), callback_fn, @@ -246,18 +288,30 @@ impl Picker { pub fn with_stream( matcher: Nucleo, - injector: Injector, + primary_column: usize, + injector: Injector, callback_fn: impl Fn(&mut Context, &T, Action) + 'static, ) -> 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( matcher: Nucleo, - editor_data: Arc, + columns: Arc>>, + default_column: usize, + editor_data: Arc, shutdown: Arc, callback_fn: impl Fn(&mut Context, &T, Action) + 'static, ) -> Self { + assert!(!columns.is_empty()); + let prompt = Prompt::new( "".into(), None, @@ -265,27 +319,37 @@ impl Picker { |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {}, ); + let column_names: Vec<_> = columns.iter().map(|column| column.name).collect(); + let widths = columns + .iter() + .map(|column| Constraint::Length(column.name.chars().count() as u16)) + .collect(); + Self { + column_names, + columns, + primary_column: default_column, matcher, editor_data, shutdown, cursor: 0, prompt, - previous_pattern: String::new(), + query: HashMap::default(), truncate_start: true, show_preview: true, callback_fn: Box::new(callback_fn), completion_height: 0, - widths: Vec::new(), + widths, preview_cache: HashMap::new(), read_buffer: Vec::with_capacity(1024), file_fn: None, } } - pub fn injector(&self) -> Injector { + pub fn injector(&self) -> Injector { Injector { dst: self.matcher.injector(), + columns: self.columns.clone(), editor_data: self.editor_data.clone(), shutown: self.shutdown.clone(), } @@ -307,13 +371,16 @@ impl Picker { self } + pub fn with_line(mut self, line: String, editor: &Editor) -> Self { + self.prompt.set_line(line, editor); + self + } + pub fn set_options(&mut self, new_options: Vec) { self.matcher.restart(false); let injector = self.matcher.injector(); for item in new_options { - if let Some((item, matcher_text)) = item_to_nucleo(item, &self.editor_data) { - injector.push(item, |dst| dst[0] = matcher_text); - } + inject_nucleo_item(&injector, &self.columns, item, &self.editor_data); } } @@ -373,21 +440,82 @@ impl Picker { fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) { - 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(); + let line = self.prompt.line(); + let new_query = self.parse_query(line); + if new_query != self.query { + for (i, column) in self + .columns + .iter() + .filter(|column| column.filter) + .enumerate() + { + let pattern = new_query + .get(column.name) + .map(|pattern| pattern.as_str()) + .unwrap_or_default(); + let append = self + .query + .get(column.name) + .map(|old_pattern| pattern.starts_with(old_pattern)) + .unwrap_or(false); + + self.matcher + .pattern + .reparse(i, pattern, CaseMatching::Smart, append); + } + self.query = new_query; } } EventResult::Consumed(None) } + fn parse_query(&self, input: &str) -> HashMap<&'static str, String> { + let mut fields: HashMap<&'static str, String> = HashMap::new(); + + // TODO: this is as basic as possible for a proof-of-concept. + // We should also support strings like `path:"a b c.txt"` and + // maybe some other lucene-like features. + + for token in input.trim().split_ascii_whitespace() { + match token.split_once(':') { + Some((key, value)) if !key.is_empty() => { + if let Some(existing_value) = fields.get_mut(key) { + // Concatenate multiple mentions of the same field. + existing_value.push(' '); + existing_value.push_str(value); + } else if let Some(idx) = self.column_names.iter().position(|name| name == &key) + { + // Only insert valid fields. + // TODO: case-insensitive field names? + fields.insert(self.column_names[idx], value.to_string()); + } else { + // Apply the token to the primary field. + if let Some(primary) = + fields.get_mut(&self.column_names[self.primary_column]) + { + primary.push(' '); + primary.push_str(token); + } else { + fields + .insert(self.column_names[self.primary_column], token.to_string()); + } + } + } + _ => { + if let Some(primary) = fields.get_mut(&self.column_names[self.primary_column]) { + primary.push(' '); + primary.push_str(token); + } else { + fields.insert(self.column_names[self.primary_column], token.to_string()); + } + } + } + } + + fields + } + fn current_file(&self, editor: &Editor) -> Option { self.selection() .and_then(|current| (self.file_fn.as_ref()?)(editor, current)) @@ -579,83 +707,109 @@ impl Picker { } 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(); - 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 { unreachable!(); }; - - // merge index highlights on top of existing hightlights - let mut span_list = Vec::new(); - let mut current_span = String::new(); - let mut current_style = Style::default(); - let mut width = 0; - - let spans: &[Span] = cell.content.lines.first().map_or(&[], |it| it.0.as_slice()); - for span in spans { - // this looks like a bug on first glance, we are iterating - // graphemes but treating them as char indices. The reason that - // this is correct is that nucleo will only ever consider the first char - // of a grapheme (and discard the rest of the grapheme) so the indices - // returned by nucleo are essentially grapheme indecies - for grapheme in span.content.graphemes(true) { - let style = if grapheme_idx == next_highlight_idx { - next_highlight_idx = indices.next().unwrap_or(u32::MAX); - span.style.patch(highlight_style) - } else { - span.style - }; - if style != current_style { - if !current_span.is_empty() { - span_list.push(Span::styled(current_span, current_style)) + 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; + + let spans: &[Span] = + cell.content.lines.first().map_or(&[], |it| it.0.as_slice()); + for span in spans { + // this looks like a bug on first glance, we are iterating + // graphemes but treating them as char indices. The reason that + // this is correct is that nucleo will only ever consider the first char + // of a grapheme (and discard the rest of the grapheme) so the indices + // returned by nucleo are essentially grapheme indecies + for grapheme in span.content.graphemes(true) { + let style = if grapheme_idx == next_highlight_idx { + next_highlight_idx = indices.next().unwrap_or(u32::MAX); + span.style.patch(highlight_style) + } else { + span.style + }; + if style != current_style { + if !current_span.is_empty() { + span_list.push(Span::styled(current_span, current_style)) + } + current_span = String::new(); + current_style = style; } - current_span = String::new(); - current_style = style; + current_span.push_str(grapheme); + grapheme_idx += 1; } - current_span.push_str(grapheme); - grapheme_idx += 1; + width += span.width(); } - 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 { *max_width = width as u16; } - *cell = Cell::from(Spans::from(span_list)); - - // spacer - if grapheme_idx == next_highlight_idx { - next_highlight_idx = indices.next().unwrap_or(u32::MAX); - } - grapheme_idx += 1; - } - row + cell + })) }); - let table = Table::new(options) + let mut table = Table::new(options) .style(text_style) .highlight_style(selected) .highlight_symbol(" > ") .column_spacing(1) .widths(&self.widths); + // -- Header + // TODO: theme keys ui.picker.header.text, ui.picker.header.separator + if self.columns.len() > 1 { + let header_text_style = cx.editor.theme.get("ui.picker.header.text"); + let header_separator_style = cx.editor.theme.get("ui.picker.header.separator"); + + table = table.header( + Row::new(self.columns.iter().zip(self.widths.iter()).map( + |(column, constraint)| { + let separator_len = constraint.apply(inner.width); + let separator = borders.horizontal.repeat(separator_len as usize); + + Cell::from(tui::text::Text { + lines: vec![ + Span::styled(column.name, header_text_style).into(), + Span::styled(separator, header_separator_style).into(), + ], + }) + }, + )) + .height(2), + ); + } + use tui::widgets::TableState; table.render_table( @@ -764,7 +918,7 @@ impl Picker { } } -impl Component for Picker { +impl Component for Picker { fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { // +---------+ +---------+ // |prompt | |preview | @@ -903,7 +1057,7 @@ impl Component for Picker { Some(ID) } } -impl Drop for Picker { +impl Drop for Picker { fn drop(&mut self) { // ensure we cancel any ongoing background threads streaming into the picker self.shutdown.store(true, atomic::Ordering::Relaxed) @@ -919,14 +1073,14 @@ pub type DynQueryCallback = /// A picker that updates its contents via a callback whenever the /// query string changes. Useful for live grep, workspace symbols, etc. -pub struct DynamicPicker { - file_picker: Picker, +pub struct DynamicPicker { + file_picker: Picker, query_callback: DynQueryCallback, query: String, } -impl DynamicPicker { - pub fn new(file_picker: Picker, query_callback: DynQueryCallback) -> Self { +impl DynamicPicker { + pub fn new(file_picker: Picker, query_callback: DynQueryCallback) -> Self { Self { file_picker, query_callback, @@ -935,7 +1089,7 @@ impl DynamicPicker { } } -impl Component for DynamicPicker { +impl Component for DynamicPicker { fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { self.file_picker.render(area, surface, cx); } @@ -957,7 +1111,7 @@ impl Component for DynamicPicker { 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::>>(ID) { + let picker = match compositor.find_id::>(ID) { Some(overlay) => &mut overlay.content.file_picker, None => return, }; diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 702a6e6714add..1b8a3d398c478 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -91,11 +91,15 @@ impl Prompt { } 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(); self.line = line; self.cursor = cursor; self.recalculate_completion(editor); - self } pub fn with_language(mut self, language: &'static str, loader: Arc) -> Self {