aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src/ui/picker.rs
diff options
context:
space:
mode:
authorGokul Soumya2021-08-12 07:00:42 +0000
committerGitHub2021-08-12 07:00:42 +0000
commitd84f8b5fdef71da87ee108db07ba1167fc6a769b (patch)
tree057f87cb1107aae3eb966ff3e228b15d3e36ff2e /helix-term/src/ui/picker.rs
parent7d51805e94a461834ce34e0829da5859d1f9db32 (diff)
Show file preview in split pane in fuzzy finder (#534)
* Add preview pane for fuzzy finder * Fix picker preview lag by caching * Add picker preview for document symbols * Cache picker preview per document instead of view * Use line instead of range for preview doc * Add picker preview for buffer picker * Fix render bug and refactor picker * Refactor picker preview rendering * Split picker and preview and compose The current selected item is cloned on every event, which is undesirable * Refactor out clones in previewed picker * Retrieve doc from editor if possible in filepicker * Disable syntax highlight for picker preview Files already loaded in memory have syntax highlighting enabled * Ignore directory symlinks in file picker * Cleanup unnecessary pubs and derives * Remove unnecessary highlight from file picker * Reorganize buffer rendering * Use normal picker for code actions * Remove unnecessary generics and trait impls * Remove prepare_for_render and make render mutable * Skip picker preview if screen small, less padding
Diffstat (limited to 'helix-term/src/ui/picker.rs')
-rw-r--r--helix-term/src/ui/picker.rs191
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 {