From 40916dff6355a741f91bc47ca0a3c6443ac2d5a3 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sun, 18 Jun 2023 11:34:13 -0500 Subject: [PATCH 01/11] Move FilePicker::render from Component impl to normal impl Merges the code for the Picker and FilePicker into a single Picker that can show a file preview if a preview callback is provided. This change was mainly made to facilitate refactoring out a simple skeleton of a picker that does not do any filtering to be reused in a normal Picker and a DynamicPicker (see #5714; in particular [mikes-comment] and [gokuls-comment]). The crux of the issue is that a picker maintains a list of predefined options (eg. list of files in the directory) and (re-)filters them every time the picker prompt changes, while a dynamic picker (eg. interactive global search, #4687) recalculates the full list of options on every prompt change. Using a filtering picker to drive a dynamic picker hence does duplicate work of filtering thousands of matches for no reason. It could also cause problems like interfering with the regex pattern in the global search. I tried to directly extract a PickerBase to be reused in Picker and FilePicker and DynamicPicker, but the problem is that DynamicPicker is actually a DynamicFilePicker (i.e. it can preview file contents) which means we would need PickerBase, Picker, FilePicker, DynamicPicker and DynamicFilePicker and then another way of sharing the previewing code between a FilePicker and a DynamicFilePicker. By merging Picker and FilePicker into Picker, we only need PickerBase, Picker and DynamicPicker. [gokuls-comment]: https://github.com/helix-editor/helix/issues/5714#issuecomment-1410949578 [mikes-comment]: https://github.com/helix-editor/helix/issues/5714#issuecomment-1407451963 --- helix-term/src/ui/picker.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index d161f786c..c357c6d63 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -269,10 +269,8 @@ impl FilePicker { EventResult::Consumed(callback) } -} -impl Component for FilePicker { - fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { + fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { // +---------+ +---------+ // |prompt | |preview | // +---------+ | | @@ -416,6 +414,12 @@ impl Component for FilePicker { } } +impl Component for FilePicker { + fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { + self.render_picker(area, surface, cx); + } +} + #[derive(PartialEq, Eq, Debug)] struct PickerMatch { score: i64, From 104036bd7feed66b6f5d2467d5dbf1fbaa5ff9c3 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sun, 18 Jun 2023 11:40:30 -0500 Subject: [PATCH 02/11] Copy struct fields and new() from Picker to FilePicker --- helix-term/src/ui/picker.rs | 167 ++++++++++++++++++++---------------- 1 file changed, 93 insertions(+), 74 deletions(-) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index c357c6d63..d1f6d45e7 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -78,6 +78,26 @@ type FileCallback = Box Option>; pub type FileLocation = (PathOrId, Option<(usize, usize)>); pub struct FilePicker { + options: Vec, + editor_data: T::Data, + // filter: String, + matcher: Box, + matches: Vec, + + /// Current height of the completions box + completion_height: u16, + + cursor: usize, + // pattern: String, + prompt: Prompt, + previous_pattern: (String, FuzzyQuery), + /// Whether to show the preview panel (default true) + show_preview: bool, + /// Constraints for tabular formatting + widths: Vec, + + callback_fn: PickerCallback, + picker: Picker, pub truncate_start: bool, /// Caches paths to documents @@ -131,17 +151,49 @@ impl FilePicker { callback_fn: impl Fn(&mut Context, &T, Action) + 'static, preview_fn: impl Fn(&Editor, &T) -> Option + 'static, ) -> Self { - let truncate_start = true; - let mut picker = Picker::new(options, editor_data, callback_fn); - picker.truncate_start = truncate_start; + let prompt = Prompt::new( + "".into(), + None, + ui::completers::none, + |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {}, + ); - Self { - picker, - truncate_start, + let mut picker = Self { + options, + editor_data, + matcher: Box::default(), + matches: Vec::new(), + cursor: 0, + prompt, + previous_pattern: (String::new(), FuzzyQuery::default()), + truncate_start: true, + show_preview: true, + callback_fn: Box::new(callback_fn), + completion_height: 0, + widths: Vec::new(), preview_cache: HashMap::new(), read_buffer: Vec::with_capacity(1024), file_fn: Box::new(preview_fn), - } + + picker: unimplemented!(), + }; + + picker.calculate_column_widths(); + + // scoring on empty input + // TODO: just reuse score() + picker + .matches + .extend(picker.options.iter().enumerate().map(|(index, option)| { + let text = option.filter_text(&picker.editor_data); + PickerMatch { + index, + score: 0, + len: text.chars().count(), + } + })); + + picker } pub fn truncate_start(mut self, truncate_start: bool) -> Self { @@ -150,6 +202,38 @@ impl FilePicker { self } + pub fn set_options(&mut self, new_options: Vec) { + self.options = new_options; + self.cursor = 0; + self.force_score(); + self.calculate_column_widths(); + } + + /// Calculate the width constraints using the maximum widths of each column + /// for the current options. + fn calculate_column_widths(&mut self) { + let n = self + .options + .first() + .map(|option| option.format(&self.editor_data).cells.len()) + .unwrap_or_default(); + let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| { + let row = option.format(&self.editor_data); + // maintain max for each column + for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) { + let width = cell.content.width(); + if width > *acc { + *acc = width; + } + } + acc + }); + self.widths = max_lens + .into_iter() + .map(|len| Constraint::Length(len as u16)) + .collect(); + } + fn current_file(&self, editor: &Editor) -> Option { self.picker .selection() @@ -477,76 +561,11 @@ impl Picker { editor_data: T::Data, callback_fn: impl Fn(&mut Context, &T, Action) + 'static, ) -> Self { - let prompt = Prompt::new( - "".into(), - None, - ui::completers::none, - |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {}, - ); - - let mut picker = Self { - options, - editor_data, - matcher: Box::default(), - matches: Vec::new(), - cursor: 0, - prompt, - previous_pattern: (String::new(), FuzzyQuery::default()), - truncate_start: true, - show_preview: true, - callback_fn: Box::new(callback_fn), - completion_height: 0, - widths: Vec::new(), - }; - - picker.calculate_column_widths(); - - // scoring on empty input - // TODO: just reuse score() - picker - .matches - .extend(picker.options.iter().enumerate().map(|(index, option)| { - let text = option.filter_text(&picker.editor_data); - PickerMatch { - index, - score: 0, - len: text.chars().count(), - } - })); - - picker + unimplemented!() } pub fn set_options(&mut self, new_options: Vec) { - self.options = new_options; - self.cursor = 0; - self.force_score(); - self.calculate_column_widths(); - } - - /// Calculate the width constraints using the maximum widths of each column - /// for the current options. - fn calculate_column_widths(&mut self) { - let n = self - .options - .first() - .map(|option| option.format(&self.editor_data).cells.len()) - .unwrap_or_default(); - let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| { - let row = option.format(&self.editor_data); - // maintain max for each column - for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) { - let width = cell.content.width(); - if width > *acc { - *acc = width; - } - } - acc - }); - self.widths = max_lens - .into_iter() - .map(|len| Constraint::Length(len as u16)) - .collect(); + unimplemented!() } pub fn score(&mut self) { From 7a058c73617bb827a57dd635bba798a972809503 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sun, 18 Jun 2023 12:05:17 -0500 Subject: [PATCH 03/11] Move scoring functions from Picker to FilePicker --- helix-term/src/ui/picker.rs | 152 +++++++++++++++++++----------------- 1 file changed, 80 insertions(+), 72 deletions(-) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index d1f6d45e7..b7f593a81 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -234,6 +234,84 @@ impl FilePicker { .collect(); } + pub fn score(&mut self) { + let pattern = self.prompt.line(); + + if pattern == &self.previous_pattern.0 { + return; + } + + let (query, is_refined) = self + .previous_pattern + .1 + .refine(pattern, &self.previous_pattern.0); + + if pattern.is_empty() { + // Fast path for no pattern. + self.matches.clear(); + self.matches + .extend(self.options.iter().enumerate().map(|(index, option)| { + let text = option.filter_text(&self.editor_data); + PickerMatch { + index, + score: 0, + len: text.chars().count(), + } + })); + } else if is_refined { + // optimization: if the pattern is a more specific version of the previous one + // then we can score the filtered set. + self.matches.retain_mut(|pmatch| { + let option = &self.options[pmatch.index]; + let text = option.sort_text(&self.editor_data); + + match query.fuzzy_match(&text, &self.matcher) { + Some(s) => { + // Update the score + pmatch.score = s; + true + } + None => false, + } + }); + + self.matches.sort_unstable(); + } else { + self.force_score(); + } + + // reset cursor position + self.cursor = 0; + let pattern = self.prompt.line(); + self.previous_pattern.0.clone_from(pattern); + self.previous_pattern.1 = query; + } + + pub fn force_score(&mut self) { + let pattern = self.prompt.line(); + + let query = FuzzyQuery::new(pattern); + self.matches.clear(); + self.matches.extend( + self.options + .iter() + .enumerate() + .filter_map(|(index, option)| { + let text = option.filter_text(&self.editor_data); + + query + .fuzzy_match(&text, &self.matcher) + .map(|score| PickerMatch { + index, + score, + len: text.chars().count(), + }) + }), + ); + + self.matches.sort_unstable(); + } + fn current_file(&self, editor: &Editor) -> Option { self.picker .selection() @@ -569,81 +647,11 @@ impl Picker { } pub fn score(&mut self) { - let pattern = self.prompt.line(); - - if pattern == &self.previous_pattern.0 { - return; - } - - let (query, is_refined) = self - .previous_pattern - .1 - .refine(pattern, &self.previous_pattern.0); - - if pattern.is_empty() { - // Fast path for no pattern. - self.matches.clear(); - self.matches - .extend(self.options.iter().enumerate().map(|(index, option)| { - let text = option.filter_text(&self.editor_data); - PickerMatch { - index, - score: 0, - len: text.chars().count(), - } - })); - } else if is_refined { - // optimization: if the pattern is a more specific version of the previous one - // then we can score the filtered set. - self.matches.retain_mut(|pmatch| { - let option = &self.options[pmatch.index]; - let text = option.sort_text(&self.editor_data); - - match query.fuzzy_match(&text, &self.matcher) { - Some(s) => { - // Update the score - pmatch.score = s; - true - } - None => false, - } - }); - - self.matches.sort_unstable(); - } else { - self.force_score(); - } - - // reset cursor position - self.cursor = 0; - let pattern = self.prompt.line(); - self.previous_pattern.0.clone_from(pattern); - self.previous_pattern.1 = query; + unimplemented!() } pub fn force_score(&mut self) { - let pattern = self.prompt.line(); - - let query = FuzzyQuery::new(pattern); - self.matches.clear(); - self.matches.extend( - self.options - .iter() - .enumerate() - .filter_map(|(index, option)| { - let text = option.filter_text(&self.editor_data); - - query - .fuzzy_match(&text, &self.matcher) - .map(|score| PickerMatch { - index, - score, - len: text.chars().count(), - }) - }), - ); - - self.matches.sort_unstable(); + unimplemented!() } /// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`) From 8516f43837005474439b7e3c8184ecc9f3df0efc Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sun, 18 Jun 2023 12:05:59 -0500 Subject: [PATCH 04/11] Move navigation methods from Picker to FilePicker --- helix-term/src/ui/picker.rs | 79 ++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 23 deletions(-) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index b7f593a81..b6d1fbb82 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -312,6 +312,55 @@ impl FilePicker { self.matches.sort_unstable(); } + /// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`) + pub fn move_by(&mut self, amount: usize, direction: Direction) { + let len = self.matches.len(); + + if len == 0 { + // No results, can't move. + return; + } + + match direction { + Direction::Forward => { + self.cursor = self.cursor.saturating_add(amount) % len; + } + Direction::Backward => { + self.cursor = self.cursor.saturating_add(len).saturating_sub(amount) % len; + } + } + } + + /// Move the cursor down by exactly one page. After the last page comes the first page. + pub fn page_up(&mut self) { + self.move_by(self.completion_height as usize, Direction::Backward); + } + + /// Move the cursor up by exactly one page. After the first page comes the last page. + pub fn page_down(&mut self) { + self.move_by(self.completion_height as usize, Direction::Forward); + } + + /// Move the cursor to the first entry + pub fn to_start(&mut self) { + self.cursor = 0; + } + + /// Move the cursor to the last entry + pub fn to_end(&mut self) { + self.cursor = self.matches.len().saturating_sub(1); + } + + pub fn selection(&self) -> Option<&T> { + self.matches + .get(self.cursor) + .map(|pmatch| &self.options[pmatch.index]) + } + + pub fn toggle_preview(&mut self) { + self.show_preview = !self.show_preview; + } + fn current_file(&self, editor: &Editor) -> Option { self.picker .selection() @@ -656,51 +705,35 @@ impl Picker { /// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`) pub fn move_by(&mut self, amount: usize, direction: Direction) { - let len = self.matches.len(); - - if len == 0 { - // No results, can't move. - return; - } - - match direction { - Direction::Forward => { - self.cursor = self.cursor.saturating_add(amount) % len; - } - Direction::Backward => { - self.cursor = self.cursor.saturating_add(len).saturating_sub(amount) % len; - } - } + unimplemented!() } /// Move the cursor down by exactly one page. After the last page comes the first page. pub fn page_up(&mut self) { - self.move_by(self.completion_height as usize, Direction::Backward); + unimplemented!() } /// Move the cursor up by exactly one page. After the first page comes the last page. pub fn page_down(&mut self) { - self.move_by(self.completion_height as usize, Direction::Forward); + unimplemented!() } /// Move the cursor to the first entry pub fn to_start(&mut self) { - self.cursor = 0; + unimplemented!() } /// Move the cursor to the last entry pub fn to_end(&mut self) { - self.cursor = self.matches.len().saturating_sub(1); + unimplemented!() } pub fn selection(&self) -> Option<&T> { - self.matches - .get(self.cursor) - .map(|pmatch| &self.options[pmatch.index]) + unimplemented!() } pub fn toggle_preview(&mut self) { - self.show_preview = !self.show_preview; + unimplemented!() } fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { From 1e66e9198c3e00afd531f155816617043e787bb0 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sun, 18 Jun 2023 12:07:38 -0500 Subject: [PATCH 05/11] Move handle_event methods from Picker to FilePicker --- helix-term/src/ui/picker.rs | 156 +++++++++++++++++++----------------- 1 file changed, 81 insertions(+), 75 deletions(-) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index b6d1fbb82..0f334548b 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -361,6 +361,14 @@ impl FilePicker { self.show_preview = !self.show_preview; } + fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { + if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) { + // TODO: recalculate only if pattern changed + self.score(); + } + EventResult::Consumed(None) + } + fn current_file(&self, editor: &Editor) -> Option { self.picker .selection() @@ -603,7 +611,77 @@ impl FilePicker { return self.handle_idle_timeout(ctx); } // TODO: keybinds for scrolling preview - self.picker.handle_event(event, ctx) + + let key_event = match event { + Event::Key(event) => *event, + Event::Paste(..) => return self.prompt_handle_event(event, ctx), + Event::Resize(..) => return EventResult::Consumed(None), + _ => return EventResult::Ignored(None), + }; + + let close_fn = + EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _ctx| { + // remove the layer + compositor.last_picker = compositor.pop(); + }))); + + // 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); + } + key!(Tab) | key!(Down) | ctrl!('n') => { + self.move_by(1, Direction::Forward); + } + key!(PageDown) | ctrl!('d') => { + self.page_down(); + } + key!(PageUp) | ctrl!('u') => { + self.page_up(); + } + key!(Home) => { + self.to_start(); + } + key!(End) => { + self.to_end(); + } + key!(Esc) | ctrl!('c') => { + return close_fn; + } + alt!(Enter) => { + if let Some(option) = self.selection() { + (self.callback_fn)(ctx, option, Action::Load); + } + } + key!(Enter) => { + if let Some(option) = self.selection() { + (self.callback_fn)(ctx, option, Action::Replace); + } + return close_fn; + } + ctrl!('s') => { + if let Some(option) = self.selection() { + (self.callback_fn)(ctx, option, Action::HorizontalSplit); + } + return close_fn; + } + ctrl!('v') => { + if let Some(option) = self.selection() { + (self.callback_fn)(ctx, option, Action::VerticalSplit); + } + return close_fn; + } + ctrl!('t') => { + self.toggle_preview(); + } + _ => { + self.prompt_handle_event(event, ctx); + } + } + + EventResult::Consumed(None) } fn cursor(&self, area: Rect, ctx: &Editor) -> (Option, CursorKind) { @@ -737,11 +815,7 @@ impl Picker { } fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { - if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) { - // TODO: recalculate only if pattern changed - self.score(); - } - EventResult::Consumed(None) + unimplemented!() } } @@ -757,75 +831,7 @@ impl Component for Picker { } fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { - let key_event = match event { - Event::Key(event) => *event, - Event::Paste(..) => return self.prompt_handle_event(event, cx), - Event::Resize(..) => return EventResult::Consumed(None), - _ => return EventResult::Ignored(None), - }; - - let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _cx| { - // remove the layer - compositor.last_picker = compositor.pop(); - }))); - - // So that idle timeout retriggers - cx.editor.reset_idle_timer(); - - match key_event { - shift!(Tab) | key!(Up) | ctrl!('p') => { - self.move_by(1, Direction::Backward); - } - key!(Tab) | key!(Down) | ctrl!('n') => { - self.move_by(1, Direction::Forward); - } - key!(PageDown) | ctrl!('d') => { - self.page_down(); - } - key!(PageUp) | ctrl!('u') => { - self.page_up(); - } - key!(Home) => { - self.to_start(); - } - key!(End) => { - self.to_end(); - } - key!(Esc) | ctrl!('c') => { - return close_fn; - } - alt!(Enter) => { - if let Some(option) = self.selection() { - (self.callback_fn)(cx, option, Action::Load); - } - } - key!(Enter) => { - if let Some(option) = self.selection() { - (self.callback_fn)(cx, option, Action::Replace); - } - return close_fn; - } - ctrl!('s') => { - if let Some(option) = self.selection() { - (self.callback_fn)(cx, option, Action::HorizontalSplit); - } - return close_fn; - } - ctrl!('v') => { - if let Some(option) = self.selection() { - (self.callback_fn)(cx, option, Action::VerticalSplit); - } - return close_fn; - } - ctrl!('t') => { - self.toggle_preview(); - } - _ => { - self.prompt_handle_event(event, cx); - } - } - - EventResult::Consumed(None) + unimplemented!() } fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { From 49fbf8df53195d521cf03d03e288c583bcba4c7c Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sun, 18 Jun 2023 12:10:00 -0500 Subject: [PATCH 06/11] Move Component methods except render() to FilePicker --- helix-term/src/ui/picker.rs | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 0f334548b..627f94724 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -684,17 +684,19 @@ impl FilePicker { EventResult::Consumed(None) } - fn cursor(&self, area: Rect, ctx: &Editor) -> (Option, CursorKind) { - self.picker.cursor(area, ctx) + fn cursor(&self, area: Rect, editor: &Editor) -> (Option, CursorKind) { + let block = Block::default().borders(Borders::ALL); + // calculate the inner area inside the box + let inner = block.inner(area); + + // prompt area + let area = inner.clip_left(1).with_height(1); + + self.prompt.cursor(area, editor) } fn required_size(&mut self, (width, height): (u16, u16)) -> Option<(u16, u16)> { - let picker_width = if width > MIN_AREA_WIDTH_FOR_PREVIEW { - width / 2 - } else { - width - }; - self.picker.required_size((picker_width, height))?; + self.completion_height = height.saturating_sub(4); Some((width, height)) } @@ -826,8 +828,7 @@ impl Picker { impl Component for Picker { fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { - self.completion_height = viewport.1.saturating_sub(4); - Some(viewport) + unimplemented!() } fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { @@ -1001,14 +1002,7 @@ impl Component for Picker { } fn cursor(&self, area: Rect, editor: &Editor) -> (Option, CursorKind) { - let block = Block::default().borders(Borders::ALL); - // calculate the inner area inside the box - let inner = block.inner(area); - - // prompt area - let area = inner.clip_left(1).with_height(1); - - self.prompt.cursor(area, editor) + unimplemented!() } } From 34c8f9ab73ad7706d84f15eefddc182fd5db8b4e Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sun, 18 Jun 2023 12:11:24 -0500 Subject: [PATCH 07/11] Move Picker::render into FilePicker::render --- helix-term/src/ui/picker.rs | 330 ++++++++++++++++++------------------ 1 file changed, 167 insertions(+), 163 deletions(-) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 627f94724..e11dd1b76 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -490,6 +490,172 @@ impl FilePicker { } fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { + let text_style = cx.editor.theme.get("ui.text"); + let selected = cx.editor.theme.get("ui.text.focus"); + let highlight_style = cx.editor.theme.get("special").add_modifier(Modifier::BOLD); + + // -- Render the frame: + // clear area + let background = cx.editor.theme.get("ui.background"); + surface.clear_with(area, background); + + // don't like this but the lifetime sucks + let block = Block::default().borders(Borders::ALL); + + // calculate the inner area inside the box + let inner = block.inner(area); + + block.render(area, surface); + + // -- Render the input bar: + + let area = inner.clip_left(1).with_height(1); + + let count = format!("{}/{}", self.matches.len(), self.options.len()); + surface.set_stringn( + (area.x + area.width).saturating_sub(count.len() as u16 + 1), + area.y, + &count, + (count.len()).min(area.width as usize), + text_style, + ); + + self.prompt.render(area, surface, cx); + + // -- Separator + let sep_style = cx.editor.theme.get("ui.background.separator"); + let borders = BorderType::line_symbols(BorderType::Plain); + for x in inner.left()..inner.right() { + if let Some(cell) = surface.get_mut(x, inner.y + 1) { + cell.set_symbol(borders.horizontal).set_style(sep_style); + } + } + + // -- Render the contents: + // subtract area of prompt from top + let inner = inner.clip_top(2); + + let rows = inner.height; + let offset = self.cursor - (self.cursor % std::cmp::max(1, rows as usize)); + let cursor = self.cursor.saturating_sub(offset); + + let options = self + .matches + .iter() + .skip(offset) + .take(rows as usize) + .map(|pmatch| &self.options[pmatch.index]) + .map(|option| option.format(&self.editor_data)) + .map(|mut row| { + const TEMP_CELL_SEP: &str = " "; + + let line = row.cell_text().fold(String::new(), |mut s, frag| { + s.push_str(&frag); + s.push_str(TEMP_CELL_SEP); + s + }); + + // Items are filtered by using the text returned by menu::Item::filter_text + // but we do highlighting here using the text in Row and therefore there + // might be inconsistencies. This is the best we can do since only the + // text in Row is displayed to the end user. + let (_score, highlights) = FuzzyQuery::new(self.prompt.line()) + .fuzzy_indices(&line, &self.matcher) + .unwrap_or_default(); + + let highlight_byte_ranges: Vec<_> = line + .char_indices() + .enumerate() + .filter_map(|(char_idx, (byte_offset, ch))| { + highlights + .contains(&char_idx) + .then(|| byte_offset..byte_offset + ch.len_utf8()) + }) + .collect(); + + // The starting byte index of the current (iterating) cell + let mut cell_start_byte_offset = 0; + for cell in row.cells.iter_mut() { + let spans = match cell.content.lines.get(0) { + Some(s) => s, + None => { + cell_start_byte_offset += TEMP_CELL_SEP.len(); + continue; + } + }; + + let mut cell_len = 0; + + let graphemes_with_style: Vec<_> = spans + .0 + .iter() + .flat_map(|span| { + span.content + .grapheme_indices(true) + .zip(std::iter::repeat(span.style)) + }) + .map(|((grapheme_byte_offset, grapheme), style)| { + cell_len += grapheme.len(); + let start = cell_start_byte_offset; + + let grapheme_byte_range = + grapheme_byte_offset..grapheme_byte_offset + grapheme.len(); + + if highlight_byte_ranges.iter().any(|hl_rng| { + hl_rng.start >= start + grapheme_byte_range.start + && hl_rng.end <= start + grapheme_byte_range.end + }) { + (grapheme, style.patch(highlight_style)) + } else { + (grapheme, style) + } + }) + .collect(); + + let mut span_list: Vec<(String, Style)> = Vec::new(); + for (grapheme, style) in graphemes_with_style { + if span_list.last().map(|(_, sty)| sty) == Some(&style) { + let (string, _) = span_list.last_mut().unwrap(); + string.push_str(grapheme); + } else { + span_list.push((String::from(grapheme), style)) + } + } + + let spans: Vec = span_list + .into_iter() + .map(|(string, style)| Span::styled(string, style)) + .collect(); + let spans: Spans = spans.into(); + *cell = Cell::from(spans); + + cell_start_byte_offset += cell_len + TEMP_CELL_SEP.len(); + } + + row + }); + + let table = Table::new(options) + .style(text_style) + .highlight_style(selected) + .highlight_symbol(" > ") + .column_spacing(1) + .widths(&self.widths); + + use tui::widgets::TableState; + + table.render_table( + inner, + surface, + &mut TableState { + offset: 0, + selected: Some(cursor), + }, + self.truncate_start, + ); + } + + fn render_preview(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { // +---------+ +---------+ // |prompt | |preview | // +---------+ | | @@ -836,169 +1002,7 @@ impl Component for Picker { } fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { - let text_style = cx.editor.theme.get("ui.text"); - let selected = cx.editor.theme.get("ui.text.focus"); - let highlight_style = cx.editor.theme.get("special").add_modifier(Modifier::BOLD); - - // -- Render the frame: - // clear area - let background = cx.editor.theme.get("ui.background"); - surface.clear_with(area, background); - - // don't like this but the lifetime sucks - let block = Block::default().borders(Borders::ALL); - - // calculate the inner area inside the box - let inner = block.inner(area); - - block.render(area, surface); - - // -- Render the input bar: - - let area = inner.clip_left(1).with_height(1); - - let count = format!("{}/{}", self.matches.len(), self.options.len()); - surface.set_stringn( - (area.x + area.width).saturating_sub(count.len() as u16 + 1), - area.y, - &count, - (count.len()).min(area.width as usize), - text_style, - ); - - self.prompt.render(area, surface, cx); - - // -- Separator - let sep_style = cx.editor.theme.get("ui.background.separator"); - let borders = BorderType::line_symbols(BorderType::Plain); - for x in inner.left()..inner.right() { - if let Some(cell) = surface.get_mut(x, inner.y + 1) { - cell.set_symbol(borders.horizontal).set_style(sep_style); - } - } - - // -- Render the contents: - // subtract area of prompt from top - let inner = inner.clip_top(2); - - let rows = inner.height; - let offset = self.cursor - (self.cursor % std::cmp::max(1, rows as usize)); - let cursor = self.cursor.saturating_sub(offset); - - let options = self - .matches - .iter() - .skip(offset) - .take(rows as usize) - .map(|pmatch| &self.options[pmatch.index]) - .map(|option| option.format(&self.editor_data)) - .map(|mut row| { - const TEMP_CELL_SEP: &str = " "; - - let line = row.cell_text().fold(String::new(), |mut s, frag| { - s.push_str(&frag); - s.push_str(TEMP_CELL_SEP); - s - }); - - // Items are filtered by using the text returned by menu::Item::filter_text - // but we do highlighting here using the text in Row and therefore there - // might be inconsistencies. This is the best we can do since only the - // text in Row is displayed to the end user. - let (_score, highlights) = FuzzyQuery::new(self.prompt.line()) - .fuzzy_indices(&line, &self.matcher) - .unwrap_or_default(); - - let highlight_byte_ranges: Vec<_> = line - .char_indices() - .enumerate() - .filter_map(|(char_idx, (byte_offset, ch))| { - highlights - .contains(&char_idx) - .then(|| byte_offset..byte_offset + ch.len_utf8()) - }) - .collect(); - - // The starting byte index of the current (iterating) cell - let mut cell_start_byte_offset = 0; - for cell in row.cells.iter_mut() { - let spans = match cell.content.lines.get(0) { - Some(s) => s, - None => { - cell_start_byte_offset += TEMP_CELL_SEP.len(); - continue; - } - }; - - let mut cell_len = 0; - - let graphemes_with_style: Vec<_> = spans - .0 - .iter() - .flat_map(|span| { - span.content - .grapheme_indices(true) - .zip(std::iter::repeat(span.style)) - }) - .map(|((grapheme_byte_offset, grapheme), style)| { - cell_len += grapheme.len(); - let start = cell_start_byte_offset; - - let grapheme_byte_range = - grapheme_byte_offset..grapheme_byte_offset + grapheme.len(); - - if highlight_byte_ranges.iter().any(|hl_rng| { - hl_rng.start >= start + grapheme_byte_range.start - && hl_rng.end <= start + grapheme_byte_range.end - }) { - (grapheme, style.patch(highlight_style)) - } else { - (grapheme, style) - } - }) - .collect(); - - let mut span_list: Vec<(String, Style)> = Vec::new(); - for (grapheme, style) in graphemes_with_style { - if span_list.last().map(|(_, sty)| sty) == Some(&style) { - let (string, _) = span_list.last_mut().unwrap(); - string.push_str(grapheme); - } else { - span_list.push((String::from(grapheme), style)) - } - } - - let spans: Vec = span_list - .into_iter() - .map(|(string, style)| Span::styled(string, style)) - .collect(); - let spans: Spans = spans.into(); - *cell = Cell::from(spans); - - cell_start_byte_offset += cell_len + TEMP_CELL_SEP.len(); - } - - row - }); - - let table = Table::new(options) - .style(text_style) - .highlight_style(selected) - .highlight_symbol(" > ") - .column_spacing(1) - .widths(&self.widths); - - use tui::widgets::TableState; - - table.render_table( - inner, - surface, - &mut TableState { - offset: 0, - selected: Some(cursor), - }, - self.truncate_start, - ); + unimplemented!() } fn cursor(&self, area: Rect, editor: &Editor) -> (Option, CursorKind) { From 15cc09fc81d971cb9dbc2d5132e43919601190c2 Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sun, 18 Jun 2023 12:14:09 -0500 Subject: [PATCH 08/11] Render the preview in FilePicker --- helix-term/src/ui/picker.rs | 50 ++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index e11dd1b76..902c16a0e 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -656,44 +656,21 @@ impl FilePicker { } fn render_preview(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { - // +---------+ +---------+ - // |prompt | |preview | - // +---------+ | | - // |picker | | | - // | | | | - // +---------+ +---------+ - - let render_preview = self.picker.show_preview && area.width > MIN_AREA_WIDTH_FOR_PREVIEW; // -- Render the frame: // clear area let background = cx.editor.theme.get("ui.background"); let text = cx.editor.theme.get("ui.text"); surface.clear_with(area, background); - let picker_width = if render_preview { - area.width / 2 - } else { - area.width - }; - - let picker_area = area.with_width(picker_width); - self.picker.render(picker_area, surface, cx); - - if !render_preview { - return; - } - - let preview_area = area.clip_left(picker_width); - // don't like this but the lifetime sucks let block = Block::default().borders(Borders::ALL); // calculate the inner area inside the box - let inner = block.inner(preview_area); + let inner = block.inner(area); // 1 column gap on either side let margin = Margin::horizontal(1); let inner = inner.inner(&margin); - block.render(preview_area, surface); + block.render(area, surface); if let Some((path, range)) = self.current_file(cx.editor) { let preview = self.get_preview(path, cx.editor); @@ -873,7 +850,28 @@ impl FilePicker { impl Component for FilePicker { fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { - self.render_picker(area, surface, cx); + // +---------+ +---------+ + // |prompt | |preview | + // +---------+ | | + // |picker | | | + // | | | | + // +---------+ +---------+ + + let render_preview = self.picker.show_preview && area.width > MIN_AREA_WIDTH_FOR_PREVIEW; + + let picker_width = if render_preview { + area.width / 2 + } else { + area.width + }; + + let picker_area = area.with_width(picker_width); + self.render_picker(picker_area, surface, cx); + + if render_preview { + let preview_area = area.clip_left(picker_width); + self.render_preview(preview_area, surface, cx); + } } } From fc111213b5b5a3130399cda8a1964fa89acf153a Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sun, 18 Jun 2023 12:14:41 -0500 Subject: [PATCH 09/11] Move FilePicker struct def closer to impl block --- helix-term/src/ui/picker.rs | 60 ++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 902c16a0e..c06918d4d 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -77,36 +77,6 @@ type FileCallback = Box Option>; /// File path and range of lines (used to align and highlight lines) pub type FileLocation = (PathOrId, Option<(usize, usize)>); -pub struct FilePicker { - options: Vec, - editor_data: T::Data, - // filter: String, - matcher: Box, - matches: Vec, - - /// Current height of the completions box - completion_height: u16, - - cursor: usize, - // pattern: String, - prompt: Prompt, - previous_pattern: (String, FuzzyQuery), - /// Whether to show the preview panel (default true) - show_preview: bool, - /// Constraints for tabular formatting - widths: Vec, - - callback_fn: PickerCallback, - - picker: Picker, - pub truncate_start: bool, - /// Caches paths to documents - preview_cache: HashMap, - read_buffer: Vec, - /// Given an item in the picker, return the file path and line number to display. - file_fn: FileCallback, -} - pub enum CachedPreview { Document(Box), Binary, @@ -144,6 +114,36 @@ impl Preview<'_, '_> { } } +pub struct FilePicker { + options: Vec, + editor_data: T::Data, + // filter: String, + matcher: Box, + matches: Vec, + + /// Current height of the completions box + completion_height: u16, + + cursor: usize, + // pattern: String, + prompt: Prompt, + previous_pattern: (String, FuzzyQuery), + /// Whether to show the preview panel (default true) + show_preview: bool, + /// Constraints for tabular formatting + widths: Vec, + + callback_fn: PickerCallback, + + picker: Picker, + pub truncate_start: bool, + /// Caches paths to documents + preview_cache: HashMap, + read_buffer: Vec, + /// Given an item in the picker, return the file path and line number to display. + file_fn: FileCallback, +} + impl FilePicker { pub fn new( options: Vec, From 545acfda8884c890b78e586c86e4f7c5f9a15477 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sun, 18 Jun 2023 12:23:15 -0500 Subject: [PATCH 10/11] Make file preview callback optional When Picker and FilePicker are merged, not all Pickers will be able to show a preview. Co-authored-by: Gokul Soumya --- helix-term/src/commands.rs | 46 ++++++++--------- helix-term/src/commands/dap.rs | 92 ++++++++++++++++------------------ helix-term/src/commands/lsp.rs | 80 +++++++++++++---------------- helix-term/src/ui/mod.rs | 26 ++++------ helix-term/src/ui/picker.rs | 15 ++++-- 5 files changed, 121 insertions(+), 138 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 5fb4a70eb..b760b6924 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2184,11 +2184,9 @@ fn global_search(cx: &mut Context) { doc.set_selection(view.id, Selection::single(start, end)); align_view(doc, view, Align::Center); - }, - |_editor, FileResult { path, line_num }| { + }).with_preview(|_editor, FileResult { path, line_num }| { Some((path.clone().into(), Some((*line_num, *line_num)))) - }, - ); + }); compositor.push(Box::new(overlaid(picker))); }, )); @@ -2579,22 +2577,18 @@ fn buffer_picker(cx: &mut Context) { // mru items.sort_unstable_by_key(|item| std::cmp::Reverse(item.focused_at)); - let picker = FilePicker::new( - items, - (), - |cx, meta, action| { - cx.editor.switch(meta.id, action); - }, - |editor, meta| { - let doc = &editor.documents.get(&meta.id)?; - let &view_id = doc.selections().keys().next()?; - let line = doc - .selection(view_id) - .primary() - .cursor_line(doc.text().slice(..)); - Some((meta.id.into(), Some((line, line)))) - }, - ); + let picker = FilePicker::new(items, (), |cx, meta, action| { + cx.editor.switch(meta.id, action); + }) + .with_preview(|editor, meta| { + let doc = &editor.documents.get(&meta.id)?; + let &view_id = doc.selections().keys().next()?; + let line = doc + .selection(view_id) + .primary() + .cursor_line(doc.text().slice(..)); + Some((meta.id.into(), Some((line, line)))) + }); cx.push_layer(Box::new(overlaid(picker))); } @@ -2678,12 +2672,12 @@ fn jumplist_picker(cx: &mut Context) { doc.set_selection(view.id, meta.selection.clone()); view.ensure_cursor_in_view_center(doc, config.scrolloff); }, - |editor, meta| { - let doc = &editor.documents.get(&meta.id)?; - let line = meta.selection.primary().cursor_line(doc.text().slice(..)); - Some((meta.id.into(), Some((line, line)))) - }, - ); + ) + .with_preview(|editor, meta| { + let doc = &editor.documents.get(&meta.id)?; + let line = meta.selection.primary().cursor_line(doc.text().slice(..)); + Some((meta.id.into(), Some((line, line)))) + }); cx.push_layer(Box::new(overlaid(picker))); } diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index 84794bedf..2684e9469 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -73,21 +73,19 @@ fn thread_picker( let debugger = debugger!(editor); let thread_states = debugger.thread_states.clone(); - let picker = FilePicker::new( - threads, - thread_states, - move |cx, thread, _action| callback_fn(cx.editor, thread), - move |editor, thread| { - let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?; - let frame = frames.get(0)?; - let path = frame.source.as_ref()?.path.clone()?; - let pos = Some(( - frame.line.saturating_sub(1), - frame.end_line.unwrap_or(frame.line).saturating_sub(1), - )); - Some((path.into(), pos)) - }, - ); + let picker = FilePicker::new(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)?; + let path = frame.source.as_ref()?.path.clone()?; + let pos = Some(( + frame.line.saturating_sub(1), + frame.end_line.unwrap_or(frame.line).saturating_sub(1), + )); + Some((path.into(), pos)) + }); compositor.push(Box::new(picker)); }, ); @@ -728,39 +726,35 @@ pub fn dap_switch_stack_frame(cx: &mut Context) { let frames = debugger.stack_frames[&thread_id].clone(); - let picker = FilePicker::new( - frames, - (), - move |cx, frame, _action| { - let debugger = debugger!(cx.editor); - // TODO: this should be simpler to find - let pos = debugger.stack_frames[&thread_id] - .iter() - .position(|f| f.id == frame.id); - debugger.active_frame = pos; - - let frame = debugger.stack_frames[&thread_id] - .get(pos.unwrap_or(0)) - .cloned(); - if let Some(frame) = &frame { - jump_to_stack_frame(cx.editor, frame); - } - }, - move |_editor, frame| { - frame - .source - .as_ref() - .and_then(|source| source.path.clone()) - .map(|path| { - ( - path.into(), - Some(( - frame.line.saturating_sub(1), - frame.end_line.unwrap_or(frame.line).saturating_sub(1), - )), - ) - }) - }, - ); + let picker = FilePicker::new(frames, (), move |cx, frame, _action| { + let debugger = debugger!(cx.editor); + // TODO: this should be simpler to find + let pos = debugger.stack_frames[&thread_id] + .iter() + .position(|f| f.id == frame.id); + debugger.active_frame = pos; + + let frame = debugger.stack_frames[&thread_id] + .get(pos.unwrap_or(0)) + .cloned(); + if let Some(frame) = &frame { + jump_to_stack_frame(cx.editor, frame); + } + }) + .with_preview(move |_editor, frame| { + frame + .source + .as_ref() + .and_then(|source| source.path.clone()) + .map(|path| { + ( + path.into(), + Some(( + frame.line.saturating_sub(1), + frame.end_line.unwrap_or(frame.line).saturating_sub(1), + )), + ) + }) + }); cx.push_layer(Box::new(picker)) } diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 8c3fd13b5..7cc2eaf84 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -240,44 +240,40 @@ type SymbolPicker = FilePicker; fn sym_picker(symbols: Vec, current_path: Option) -> SymbolPicker { // TODO: drop current_path comparison and instead use workspace: bool flag? - FilePicker::new( - symbols, - current_path.clone(), - move |cx, item, action| { - let (view, doc) = current!(cx.editor); - push_jump(view, doc); - - if current_path.as_ref() != Some(&item.symbol.location.uri) { - let uri = &item.symbol.location.uri; - let path = match uri.to_file_path() { - Ok(path) => path, - Err(_) => { - let err = format!("unable to convert URI to filepath: {}", uri); - cx.editor.set_error(err); - return; - } - }; - if let Err(err) = cx.editor.open(&path, action) { - let err = format!("failed to open document: {}: {}", uri, err); - log::error!("{}", err); + FilePicker::new(symbols, current_path.clone(), move |cx, item, action| { + let (view, doc) = current!(cx.editor); + push_jump(view, doc); + + if current_path.as_ref() != Some(&item.symbol.location.uri) { + let uri = &item.symbol.location.uri; + let path = match uri.to_file_path() { + Ok(path) => path, + Err(_) => { + let err = format!("unable to convert URI to filepath: {}", uri); cx.editor.set_error(err); return; } + }; + if let Err(err) = cx.editor.open(&path, action) { + let err = format!("failed to open document: {}: {}", uri, err); + log::error!("{}", err); + cx.editor.set_error(err); + return; } + } - let (view, doc) = current!(cx.editor); + let (view, doc) = current!(cx.editor); - if let Some(range) = - lsp_range_to_range(doc.text(), item.symbol.location.range, item.offset_encoding) - { - // we flip the range so that the cursor sits on the start of the symbol - // (for example start of the function). - doc.set_selection(view.id, Selection::single(range.head, range.anchor)); - align_view(doc, view, Align::Center); - } - }, - move |_editor, item| Some(location_to_file_location(&item.symbol.location)), - ) + if let Some(range) = + lsp_range_to_range(doc.text(), item.symbol.location.range, item.offset_encoding) + { + // we flip the range so that the cursor sits on the start of the symbol + // (for example start of the function). + doc.set_selection(view.id, Selection::single(range.head, range.anchor)); + align_view(doc, view, Align::Center); + } + }) + .with_preview(move |_editor, item| Some(location_to_file_location(&item.symbol.location))) .truncate_start(false) } @@ -345,11 +341,11 @@ fn diag_picker( align_view(doc, view, Align::Center); } }, - move |_editor, PickerDiagnostic { url, diag, .. }| { - let location = lsp::Location::new(url.clone(), diag.range); - Some(location_to_file_location(&location)) - }, ) + .with_preview(move |_editor, PickerDiagnostic { url, diag, .. }| { + let location = lsp::Location::new(url.clone(), diag.range); + Some(location_to_file_location(&location)) + }) .truncate_start(false) } @@ -1047,14 +1043,10 @@ fn goto_impl( editor.set_error("No definition found."); } _locations => { - let picker = FilePicker::new( - locations, - cwdir, - move |cx, location, action| { - jump_to_location(cx.editor, location, offset_encoding, action) - }, - move |_editor, location| Some(location_to_file_location(location)), - ); + let picker = FilePicker::new(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))); compositor.push(Box::new(overlaid(picker))); } } diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index ec328ec55..c5e66d865 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -217,21 +217,17 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi log::debug!("file_picker init {:?}", Instant::now().duration_since(now)); - FilePicker::new( - files, - 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); - } - }, - |_editor, path| Some((path.clone().into(), None)), - ) + FilePicker::new(files, 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))) } pub mod completers { diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index c06918d4d..001526c44 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -141,7 +141,7 @@ pub struct FilePicker { preview_cache: HashMap, read_buffer: Vec, /// Given an item in the picker, return the file path and line number to display. - file_fn: FileCallback, + file_fn: Option>, } impl FilePicker { @@ -149,7 +149,6 @@ impl FilePicker { options: Vec, editor_data: T::Data, callback_fn: impl Fn(&mut Context, &T, Action) + 'static, - preview_fn: impl Fn(&Editor, &T) -> Option + 'static, ) -> Self { let prompt = Prompt::new( "".into(), @@ -173,7 +172,7 @@ impl FilePicker { widths: Vec::new(), preview_cache: HashMap::new(), read_buffer: Vec::with_capacity(1024), - file_fn: Box::new(preview_fn), + file_fn: None, picker: unimplemented!(), }; @@ -202,6 +201,14 @@ impl FilePicker { self } + pub fn with_preview( + mut self, + preview_fn: impl Fn(&Editor, &T) -> Option + 'static, + ) -> Self { + self.file_fn = Some(Box::new(preview_fn)); + self + } + pub fn set_options(&mut self, new_options: Vec) { self.options = new_options; self.cursor = 0; @@ -372,7 +379,7 @@ impl FilePicker { fn current_file(&self, editor: &Editor) -> Option { self.picker .selection() - .and_then(|current| (self.file_fn)(editor, current)) + .and_then(|current| (self.file_fn.as_ref()?)(editor, current)) .and_then(|(path_or_id, line)| path_or_id.get_canonicalized().ok().zip(Some(line))) } From f18acadbd0d7b15bc314fc3ede99f4546b72d76d Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sun, 18 Jun 2023 12:27:11 -0500 Subject: [PATCH 11/11] Completely remove old Picker and rename FilePicker to Picker --- helix-term/src/commands.rs | 10 +-- helix-term/src/commands/dap.rs | 6 +- helix-term/src/commands/lsp.rs | 14 ++-- helix-term/src/ui/mod.rs | 6 +- helix-term/src/ui/picker.rs | 133 +++------------------------------ 5 files changed, 27 insertions(+), 142 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index b760b6924..2c9295f11 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -55,8 +55,8 @@ use crate::{ job::Callback, keymap::ReverseKeymap, ui::{ - self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, CompletionItem, - FilePicker, Picker, Popup, Prompt, PromptEvent, + self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, CompletionItem, Picker, + Popup, Prompt, PromptEvent, }, }; @@ -2156,7 +2156,7 @@ fn global_search(cx: &mut Context) { return; } - let picker = FilePicker::new( + let picker = Picker::new( all_matches, current_path, move |cx, FileResult { path, line_num }, action| { @@ -2577,7 +2577,7 @@ fn buffer_picker(cx: &mut Context) { // mru items.sort_unstable_by_key(|item| std::cmp::Reverse(item.focused_at)); - let picker = FilePicker::new(items, (), |cx, meta, action| { + let picker = Picker::new(items, (), |cx, meta, action| { cx.editor.switch(meta.id, action); }) .with_preview(|editor, meta| { @@ -2654,7 +2654,7 @@ fn jumplist_picker(cx: &mut Context) { } }; - let picker = FilePicker::new( + let picker = Picker::new( cx.editor .tree .views() diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index 2684e9469..70a5ec212 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -2,7 +2,7 @@ use super::{Context, Editor}; use crate::{ compositor::{self, Compositor}, job::{Callback, Jobs}, - ui::{self, overlay::overlaid, FilePicker, Picker, Popup, Prompt, PromptEvent, Text}, + ui::{self, overlay::overlaid, Picker, Popup, Prompt, PromptEvent, Text}, }; use dap::{StackFrame, Thread, ThreadStates}; use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate}; @@ -73,7 +73,7 @@ fn thread_picker( let debugger = debugger!(editor); let thread_states = debugger.thread_states.clone(); - let picker = FilePicker::new(threads, thread_states, move |cx, thread, _action| { + let picker = Picker::new(threads, thread_states, move |cx, thread, _action| { callback_fn(cx.editor, thread) }) .with_preview(move |editor, thread| { @@ -726,7 +726,7 @@ pub fn dap_switch_stack_frame(cx: &mut Context) { let frames = debugger.stack_frames[&thread_id].clone(); - let picker = FilePicker::new(frames, (), move |cx, frame, _action| { + let picker = Picker::new(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 7cc2eaf84..55153648a 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -31,8 +31,8 @@ use crate::{ compositor::{self, Compositor}, job::Callback, ui::{ - self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, FilePicker, - Popup, PromptEvent, + self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, Picker, Popup, + PromptEvent, }, }; @@ -236,11 +236,11 @@ fn jump_to_location( align_view(doc, view, Align::Center); } -type SymbolPicker = FilePicker; +type SymbolPicker = Picker; fn sym_picker(symbols: Vec, current_path: Option) -> SymbolPicker { // TODO: drop current_path comparison and instead use workspace: bool flag? - FilePicker::new(symbols, current_path.clone(), move |cx, item, action| { + Picker::new(symbols, current_path.clone(), move |cx, item, action| { let (view, doc) = current!(cx.editor); push_jump(view, doc); @@ -288,7 +288,7 @@ fn diag_picker( diagnostics: BTreeMap>, current_path: Option, format: DiagnosticsFormat, -) -> FilePicker { +) -> Picker { // TODO: drop current_path comparison and instead use workspace: bool flag? // flatten the map to a vec of (url, diag) pairs @@ -314,7 +314,7 @@ fn diag_picker( error: cx.editor.theme.get("error"), }; - FilePicker::new( + Picker::new( flat_diag, (styles, format), move |cx, @@ -1043,7 +1043,7 @@ fn goto_impl( editor.set_error("No definition found."); } _locations => { - let picker = FilePicker::new(locations, cwdir, move |cx, location, action| { + let picker = Picker::new(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/ui/mod.rs b/helix-term/src/ui/mod.rs index c5e66d865..155f24356 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -21,7 +21,7 @@ pub use completion::{Completion, CompletionItem}; pub use editor::EditorView; pub use markdown::Markdown; pub use menu::Menu; -pub use picker::{DynamicPicker, FileLocation, FilePicker, Picker}; +pub use picker::{DynamicPicker, FileLocation, Picker}; pub use popup::Popup; pub use prompt::{Prompt, PromptEvent}; pub use spinner::{ProgressSpinners, Spinner}; @@ -158,7 +158,7 @@ pub fn regex_prompt( cx.push_layer(Box::new(prompt)); } -pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker { +pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker { use ignore::{types::TypesBuilder, WalkBuilder}; use std::time::Instant; @@ -217,7 +217,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi log::debug!("file_picker init {:?}", Instant::now().duration_since(now)); - FilePicker::new(files, root, move |cx, path: &PathBuf, action| { + Picker::new(files, 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) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 001526c44..04ed940cd 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -114,7 +114,7 @@ impl Preview<'_, '_> { } } -pub struct FilePicker { +pub struct Picker { options: Vec, editor_data: T::Data, // filter: String, @@ -135,7 +135,6 @@ pub struct FilePicker { callback_fn: PickerCallback, - picker: Picker, pub truncate_start: bool, /// Caches paths to documents preview_cache: HashMap, @@ -144,7 +143,7 @@ pub struct FilePicker { file_fn: Option>, } -impl FilePicker { +impl Picker { pub fn new( options: Vec, editor_data: T::Data, @@ -173,8 +172,6 @@ impl FilePicker { preview_cache: HashMap::new(), read_buffer: Vec::with_capacity(1024), file_fn: None, - - picker: unimplemented!(), }; picker.calculate_column_widths(); @@ -197,7 +194,6 @@ impl FilePicker { pub fn truncate_start(mut self, truncate_start: bool) -> Self { self.truncate_start = truncate_start; - self.picker.truncate_start = truncate_start; self } @@ -377,8 +373,7 @@ impl FilePicker { } fn current_file(&self, editor: &Editor) -> Option { - self.picker - .selection() + self.selection() .and_then(|current| (self.file_fn.as_ref()?)(editor, current)) .and_then(|(path_or_id, line)| path_or_id.get_canonicalized().ok().zip(Some(line))) } @@ -849,13 +844,9 @@ impl FilePicker { self.completion_height = height.saturating_sub(4); Some((width, height)) } - - fn id(&self) -> Option<&'static str> { - Some("file-picker") - } } -impl Component for FilePicker { +impl Component for Picker { fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { // +---------+ +---------+ // |prompt | |preview | @@ -864,7 +855,7 @@ impl Component for FilePicker { // | | | | // +---------+ +---------+ - let render_preview = self.picker.show_preview && area.width > MIN_AREA_WIDTH_FOR_PREVIEW; + let render_preview = self.show_preview && area.width > MIN_AREA_WIDTH_FOR_PREVIEW; let picker_width = if render_preview { area.width / 2 @@ -909,112 +900,6 @@ impl Ord for PickerMatch { type PickerCallback = Box; -pub struct Picker { - options: Vec, - editor_data: T::Data, - // filter: String, - matcher: Box, - matches: Vec, - - /// Current height of the completions box - completion_height: u16, - - cursor: usize, - // pattern: String, - prompt: Prompt, - previous_pattern: (String, FuzzyQuery), - /// Whether to truncate the start (default true) - pub truncate_start: bool, - /// Whether to show the preview panel (default true) - show_preview: bool, - /// Constraints for tabular formatting - widths: Vec, - - callback_fn: PickerCallback, -} - -impl Picker { - pub fn new( - options: Vec, - editor_data: T::Data, - callback_fn: impl Fn(&mut Context, &T, Action) + 'static, - ) -> Self { - unimplemented!() - } - - pub fn set_options(&mut self, new_options: Vec) { - unimplemented!() - } - - pub fn score(&mut self) { - unimplemented!() - } - - pub fn force_score(&mut self) { - unimplemented!() - } - - /// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`) - pub fn move_by(&mut self, amount: usize, direction: Direction) { - unimplemented!() - } - - /// Move the cursor down by exactly one page. After the last page comes the first page. - pub fn page_up(&mut self) { - unimplemented!() - } - - /// Move the cursor up by exactly one page. After the first page comes the last page. - pub fn page_down(&mut self) { - unimplemented!() - } - - /// Move the cursor to the first entry - pub fn to_start(&mut self) { - unimplemented!() - } - - /// Move the cursor to the last entry - pub fn to_end(&mut self) { - unimplemented!() - } - - pub fn selection(&self) -> Option<&T> { - unimplemented!() - } - - pub fn toggle_preview(&mut self) { - unimplemented!() - } - - fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { - unimplemented!() - } -} - -// process: -// - read all the files into a list, maxed out at a large value -// - on input change: -// - score all the names in relation to input - -impl Component for Picker { - fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { - unimplemented!() - } - - fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { - unimplemented!() - } - - fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { - unimplemented!() - } - - fn cursor(&self, area: Rect, editor: &Editor) -> (Option, CursorKind) { - unimplemented!() - } -} - /// Returns a new list of options to replace the contents of the picker /// when called with the current picker query, pub type DynQueryCallback = @@ -1023,7 +908,7 @@ 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: FilePicker, + file_picker: Picker, query_callback: DynQueryCallback, query: String, } @@ -1031,7 +916,7 @@ pub struct DynamicPicker { impl DynamicPicker { pub const ID: &'static str = "dynamic-picker"; - pub fn new(file_picker: FilePicker, query_callback: DynQueryCallback) -> Self { + pub fn new(file_picker: Picker, query_callback: DynQueryCallback) -> Self { Self { file_picker, query_callback, @@ -1047,7 +932,7 @@ impl Component for DynamicPicker { fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { let event_result = self.file_picker.handle_event(event, cx); - let current_query = self.file_picker.picker.prompt.line(); + let current_query = self.file_picker.prompt.line(); if !matches!(event, Event::IdleTimeout) || self.query == *current_query { return event_result; @@ -1063,7 +948,7 @@ impl Component for DynamicPicker { // 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::>>(Self::ID) { - Some(overlay) => &mut overlay.content.file_picker.picker, + Some(overlay) => &mut overlay.content.file_picker, None => return, }; picker.set_options(new_options);