diff options
Diffstat (limited to 'helix-term/src/ui/picker.rs')
-rw-r--r-- | helix-term/src/ui/picker.rs | 191 |
1 files changed, 171 insertions, 20 deletions
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 0b67cd9c..9c6b328f 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -1,4 +1,7 @@ -use crate::compositor::{Component, Compositor, Context, EventResult}; +use crate::{ + compositor::{Component, Compositor, Context, EventResult}, + ui::EditorView, +}; use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use tui::{ buffer::Buffer as Surface, @@ -7,17 +10,153 @@ use tui::{ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::FuzzyMatcher; +use tui::widgets::Widget; -use std::borrow::Cow; +use std::{borrow::Cow, collections::HashMap, path::PathBuf}; use crate::ui::{Prompt, PromptEvent}; use helix_core::Position; use helix_view::{ + document::canonicalize_path, editor::Action, graphics::{Color, CursorKind, Rect, Style}, - Editor, + Document, Editor, }; +pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80; + +/// File path and line number (used to align and highlight a line) +type FileLocation = (PathBuf, Option<usize>); + +pub struct FilePicker<T> { + picker: Picker<T>, + /// Caches paths to documents + preview_cache: HashMap<PathBuf, Document>, + /// Given an item in the picker, return the file path and line number to display. + file_fn: Box<dyn Fn(&Editor, &T) -> Option<FileLocation>>, +} + +impl<T> FilePicker<T> { + pub fn new( + options: Vec<T>, + format_fn: impl Fn(&T) -> Cow<str> + 'static, + callback_fn: impl Fn(&mut Editor, &T, Action) + 'static, + preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static, + ) -> Self { + Self { + picker: Picker::new(false, options, format_fn, callback_fn), + preview_cache: HashMap::new(), + file_fn: Box::new(preview_fn), + } + } + + fn current_file(&self, editor: &Editor) -> Option<FileLocation> { + self.picker + .selection() + .and_then(|current| (self.file_fn)(editor, current)) + .and_then(|(path, line)| canonicalize_path(&path).ok().zip(Some(line))) + } + + fn calculate_preview(&mut self, editor: &Editor) { + if let Some((path, _line)) = self.current_file(editor) { + if !self.preview_cache.contains_key(&path) && editor.document_by_path(&path).is_none() { + // TODO: enable syntax highlighting; blocked by async rendering + let doc = Document::open(&path, None, Some(&editor.theme), None).unwrap(); + self.preview_cache.insert(path, doc); + } + } + } +} + +impl<T: 'static> Component for FilePicker<T> { + fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { + // +---------+ +---------+ + // |prompt | |preview | + // +---------+ | | + // |picker | | | + // | | | | + // +---------+ +---------+ + self.calculate_preview(cx.editor); + let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW; + let area = inner_rect(area); + // -- Render the frame: + // clear area + let background = cx.editor.theme.get("ui.background"); + surface.clear_with(area, background); + + let picker_width = if render_preview { + area.width / 2 + } else { + area.width + }; + + let picker_area = Rect::new(area.x, area.y, picker_width, area.height); + self.picker.render(picker_area, surface, cx); + + if !render_preview { + return; + } + + let preview_area = Rect::new(area.x + picker_width, area.y, area.width / 2, area.height); + + // don't like this but the lifetime sucks + let block = Block::default().borders(Borders::ALL); + + // calculate the inner area inside the box + let mut inner = block.inner(preview_area); + // 1 column gap on either side + inner.x += 1; + inner.width = inner.width.saturating_sub(2); + + block.render(preview_area, surface); + + if let Some((doc, line)) = self.current_file(cx.editor).and_then(|(path, line)| { + cx.editor + .document_by_path(&path) + .or_else(|| self.preview_cache.get(&path)) + .zip(Some(line)) + }) { + // align to middle + let first_line = line.unwrap_or(0).saturating_sub(inner.height as usize / 2); + let offset = Position::new(first_line, 0); + + let highlights = EditorView::doc_syntax_highlights( + doc, + offset, + area.height, + &cx.editor.theme, + &cx.editor.syn_loader, + ); + EditorView::render_text_highlights( + doc, + offset, + inner, + surface, + &cx.editor.theme, + highlights, + ); + + // highlight the line + if let Some(line) = line { + for x in inner.left()..inner.right() { + surface + .get_mut(x, inner.y + line.saturating_sub(first_line) as u16) + .set_style(cx.editor.theme.get("ui.selection.primary")); + } + } + } + } + + fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult { + // TODO: keybinds for scrolling preview + self.picker.handle_event(event, ctx) + } + + fn cursor(&self, area: Rect, ctx: &Editor) -> (Option<Position>, CursorKind) { + self.picker.cursor(area, ctx) + } +} + pub struct Picker<T> { options: Vec<T>, // filter: String, @@ -30,6 +169,8 @@ pub struct Picker<T> { cursor: usize, // pattern: String, prompt: Prompt, + /// Whether to render in the middle of the area + render_centered: bool, format_fn: Box<dyn Fn(&T) -> Cow<str>>, callback_fn: Box<dyn Fn(&mut Editor, &T, Action)>, @@ -37,6 +178,7 @@ pub struct Picker<T> { impl<T> Picker<T> { pub fn new( + render_centered: bool, options: Vec<T>, format_fn: impl Fn(&T) -> Cow<str> + 'static, callback_fn: impl Fn(&mut Editor, &T, Action) + 'static, @@ -57,6 +199,7 @@ impl<T> Picker<T> { filters: Vec::new(), cursor: 0, prompt, + render_centered, format_fn: Box::new(format_fn), callback_fn: Box::new(callback_fn), }; @@ -139,8 +282,8 @@ impl<T> Picker<T> { // - score all the names in relation to input fn inner_rect(area: Rect) -> Rect { - let padding_vertical = area.height * 20 / 100; - let padding_horizontal = area.width * 20 / 100; + let padding_vertical = area.height * 10 / 100; + let padding_horizontal = area.width * 10 / 100; Rect::new( area.x + padding_horizontal, @@ -174,7 +317,9 @@ impl<T: 'static> Component for Picker<T> { | KeyEvent { code: KeyCode::Char('p'), modifiers: KeyModifiers::CONTROL, - } => self.move_up(), + } => { + self.move_up(); + } KeyEvent { code: KeyCode::Down, .. @@ -185,7 +330,9 @@ impl<T: 'static> Component for Picker<T> { | KeyEvent { code: KeyCode::Char('n'), modifiers: KeyModifiers::CONTROL, - } => self.move_down(), + } => { + self.move_down(); + } KeyEvent { code: KeyCode::Esc, .. } @@ -239,16 +386,18 @@ impl<T: 'static> Component for Picker<T> { EventResult::Consumed(None) } - fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { - let area = inner_rect(area); + fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { + let area = if self.render_centered { + inner_rect(area) + } else { + area + }; // -- Render the frame: - // clear area let background = cx.editor.theme.get("ui.background"); surface.clear_with(area, background); - use tui::widgets::Widget; // don't like this but the lifetime sucks let block = Block::default().borders(Borders::ALL); @@ -263,21 +412,23 @@ impl<T: 'static> Component for Picker<T> { self.prompt.render(area, surface, cx); // -- Separator - let style = Style::default().fg(Color::Rgb(90, 89, 119)); - let symbols = BorderType::line_symbols(BorderType::Plain); + let sep_style = Style::default().fg(Color::Rgb(90, 89, 119)); + let borders = BorderType::line_symbols(BorderType::Plain); for x in inner.left()..inner.right() { surface .get_mut(x, inner.y + 1) - .set_symbol(symbols.horizontal) - .set_style(style); + .set_symbol(borders.horizontal) + .set_style(sep_style); } // -- Render the contents: + // subtract the area of the prompt (-2) and current item marker " > " (-3) + let inner = Rect::new(inner.x + 3, inner.y + 2, inner.width - 3, inner.height - 2); let style = cx.editor.theme.get("ui.text"); let selected = Style::default().fg(Color::Rgb(255, 255, 255)); - let rows = inner.height - 2; // -1 for search bar + let rows = inner.height; let offset = self.cursor / (rows as usize) * (rows as usize); let files = self.matches.iter().skip(offset).map(|(index, _score)| { @@ -286,14 +437,14 @@ impl<T: 'static> Component for Picker<T> { for (i, (_index, option)) in files.take(rows as usize).enumerate() { if i == (self.cursor - offset) { - surface.set_string(inner.x + 1, inner.y + 2 + i as u16, ">", selected); + surface.set_string(inner.x - 2, inner.y + i as u16, ">", selected); } surface.set_string_truncated( - inner.x + 3, - inner.y + 2 + i as u16, + inner.x, + inner.y + i as u16, (self.format_fn)(option), - (inner.width as usize).saturating_sub(3), // account for the " > " + inner.width as usize, if i == (self.cursor - offset) { selected } else { |