Add `PageUp`, `PageDown`, `Ctrl-u`, `Ctrl-d`, `Home`, `End` keyboard shortcuts to file picker (#1612)

* Add `PageUp`, `PageDown`, `Ctrl-u`, `Ctrl-d`, `Home`, `End` keyboard shortcuts to file picker

* Refactor file picker paging logic

* change key mapping

* Add overlay component

* Use closure instead of margin to calculate size

* Don't wrap file picker in `Overlay` automatically
pull/1668/head
Ludwig Stecher 3 years ago committed by GitHub
parent 23907a063c
commit 4429993842
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -304,7 +304,11 @@ Keys to use within picker. Remapping currently not supported.
| Key | Description | | Key | Description |
| ----- | ------------- | | ----- | ------------- |
| `Up`, `Ctrl-k`, `Ctrl-p` | Previous entry | | `Up`, `Ctrl-k`, `Ctrl-p` | Previous entry |
| `PageUp`, `Ctrl-b` | Page up |
| `Down`, `Ctrl-j`, `Ctrl-n` | Next entry | | `Down`, `Ctrl-j`, `Ctrl-n` | Next entry |
| `PageDown`, `Ctrl-f` | Page down |
| `Home` | Go to first entry |
| `End` | Go to last entry |
| `Ctrl-space` | Filter options | | `Ctrl-space` | Filter options |
| `Enter` | Open selected | | `Enter` | Open selected |
| `Ctrl-s` | Open horizontally | | `Ctrl-s` | Open horizontally |

@ -13,7 +13,7 @@ use crate::{
compositor::Compositor, compositor::Compositor,
config::Config, config::Config,
job::Jobs, job::Jobs,
ui, ui::{self, overlay::overlayed},
}; };
use log::{error, warn}; use log::{error, warn};
@ -124,7 +124,8 @@ impl Application {
if first.is_dir() { if first.is_dir() {
std::env::set_current_dir(&first)?; std::env::set_current_dir(&first)?;
editor.new_file(Action::VerticalSplit); editor.new_file(Action::VerticalSplit);
compositor.push(Box::new(ui::file_picker(".".into(), &config.editor))); let picker = ui::file_picker(".".into(), &config.editor);
compositor.push(Box::new(overlayed(picker)));
} else { } else {
let nr_of_files = args.files.len(); let nr_of_files = args.files.len();
editor.open(first.to_path_buf(), Action::VerticalSplit)?; editor.open(first.to_path_buf(), Action::VerticalSplit)?;

@ -44,7 +44,7 @@ use movement::Movement;
use crate::{ use crate::{
args, args,
compositor::{self, Component, Compositor}, compositor::{self, Component, Compositor},
ui::{self, FilePicker, Popup, Prompt, PromptEvent}, ui::{self, overlay::overlayed, FilePicker, Popup, Prompt, PromptEvent},
}; };
use crate::job::{self, Job, Jobs}; use crate::job::{self, Job, Jobs};
@ -1824,7 +1824,7 @@ fn global_search(cx: &mut Context) {
}, },
|_editor, (line_num, path)| Some((path.clone(), Some((*line_num, *line_num)))), |_editor, (line_num, path)| Some((path.clone(), Some((*line_num, *line_num)))),
); );
compositor.push(Box::new(picker)); compositor.push(Box::new(overlayed(picker)));
}); });
Ok(call) Ok(call)
}; };
@ -3359,7 +3359,7 @@ fn file_picker(cx: &mut Context) {
// We don't specify language markers, root will be the root of the current git repo // We don't specify language markers, root will be the root of the current git repo
let root = find_root(None, &[]).unwrap_or_else(|| PathBuf::from("./")); let root = find_root(None, &[]).unwrap_or_else(|| PathBuf::from("./"));
let picker = ui::file_picker(root, &cx.editor.config); let picker = ui::file_picker(root, &cx.editor.config);
cx.push_layer(Box::new(picker)); cx.push_layer(Box::new(overlayed(picker)));
} }
fn buffer_picker(cx: &mut Context) { fn buffer_picker(cx: &mut Context) {
@ -3427,7 +3427,7 @@ fn buffer_picker(cx: &mut Context) {
Some((meta.path.clone()?, Some((line, line)))) Some((meta.path.clone()?, Some((line, line))))
}, },
); );
cx.push_layer(Box::new(picker)); cx.push_layer(Box::new(overlayed(picker)));
} }
fn symbol_picker(cx: &mut Context) { fn symbol_picker(cx: &mut Context) {
@ -3505,7 +3505,7 @@ fn symbol_picker(cx: &mut Context) {
}, },
); );
picker.truncate_start = false; picker.truncate_start = false;
compositor.push(Box::new(picker)) compositor.push(Box::new(overlayed(picker)))
} }
}, },
) )
@ -3564,7 +3564,7 @@ fn workspace_symbol_picker(cx: &mut Context) {
}, },
); );
picker.truncate_start = false; picker.truncate_start = false;
compositor.push(Box::new(picker)) compositor.push(Box::new(overlayed(picker)))
} }
}, },
) )
@ -4225,7 +4225,7 @@ fn goto_impl(
Some((path, line)) Some((path, line))
}, },
); );
compositor.push(Box::new(picker)); compositor.push(Box::new(overlayed(picker)));
} }
} }
} }

@ -3,6 +3,7 @@ pub(crate) mod editor;
mod info; mod info;
mod markdown; mod markdown;
pub mod menu; pub mod menu;
pub mod overlay;
mod picker; mod picker;
mod popup; mod popup;
mod prompt; mod prompt;

@ -0,0 +1,73 @@
use crossterm::event::Event;
use helix_core::Position;
use helix_view::{
graphics::{CursorKind, Rect},
Editor,
};
use tui::buffer::Buffer;
use crate::compositor::{Component, Context, EventResult};
/// Contains a component placed in the center of the parent component
pub struct Overlay<T> {
/// Child component
pub content: T,
/// Function to compute the size and position of the child component
pub calc_child_size: Box<dyn Fn(Rect) -> Rect>,
}
/// Surrounds the component with a margin of 5% on each side, and an additional 2 rows at the bottom
pub fn overlayed<T>(content: T) -> Overlay<T> {
Overlay {
content,
calc_child_size: Box::new(|rect: Rect| clip_rect_relative(rect.clip_bottom(2), 90, 90)),
}
}
fn clip_rect_relative(rect: Rect, percent_horizontal: u8, percent_vertical: u8) -> Rect {
fn mul_and_cast(size: u16, factor: u8) -> u16 {
((size as u32) * (factor as u32) / 100).try_into().unwrap()
}
let inner_w = mul_and_cast(rect.width, percent_horizontal);
let inner_h = mul_and_cast(rect.height, percent_vertical);
let offset_x = rect.width.saturating_sub(inner_w) / 2;
let offset_y = rect.height.saturating_sub(inner_h) / 2;
Rect {
x: rect.x + offset_x,
y: rect.y + offset_y,
width: inner_w,
height: inner_h,
}
}
impl<T: Component + 'static> Component for Overlay<T> {
fn render(&mut self, area: Rect, frame: &mut Buffer, ctx: &mut Context) {
let dimensions = (self.calc_child_size)(area);
self.content.render(dimensions, frame, ctx)
}
fn required_size(&mut self, (width, height): (u16, u16)) -> Option<(u16, u16)> {
let area = Rect {
x: 0,
y: 0,
width,
height,
};
let dimensions = (self.calc_child_size)(area);
let viewport = (dimensions.width, dimensions.height);
let _ = self.content.required_size(viewport)?;
Some((width, height))
}
fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult {
self.content.handle_event(event, ctx)
}
fn cursor(&self, area: Rect, ctx: &Editor) -> (Option<Position>, CursorKind) {
let dimensions = (self.calc_child_size)(area);
self.content.cursor(dimensions, ctx)
}
}

@ -21,14 +21,14 @@ use std::{
}; };
use crate::ui::{Prompt, PromptEvent}; use crate::ui::{Prompt, PromptEvent};
use helix_core::Position; use helix_core::{movement::Direction, Position};
use helix_view::{ use helix_view::{
editor::Action, editor::Action,
graphics::{Color, CursorKind, Margin, Rect, Style}, graphics::{Color, CursorKind, Margin, Rect, Style},
Document, Editor, Document, Editor,
}; };
pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80; pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72;
/// Biggest file size to preview in bytes /// Biggest file size to preview in bytes
pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024; pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024;
@ -90,7 +90,7 @@ impl<T> FilePicker<T> {
preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static, preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
) -> Self { ) -> Self {
Self { Self {
picker: Picker::new(false, options, format_fn, callback_fn), picker: Picker::new(options, format_fn, callback_fn),
truncate_start: true, truncate_start: true,
preview_cache: HashMap::new(), preview_cache: HashMap::new(),
read_buffer: Vec::with_capacity(1024), read_buffer: Vec::with_capacity(1024),
@ -160,8 +160,7 @@ impl<T: 'static> Component for FilePicker<T> {
// | | | | // | | | |
// +---------+ +---------+ // +---------+ +---------+
let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW; let render_preview = area.width > MIN_AREA_WIDTH_FOR_PREVIEW;
let area = inner_rect(area);
// -- Render the frame: // -- Render the frame:
// clear area // clear area
let background = cx.editor.theme.get("ui.background"); let background = cx.editor.theme.get("ui.background");
@ -260,6 +259,16 @@ impl<T: 'static> Component for FilePicker<T> {
fn cursor(&self, area: Rect, ctx: &Editor) -> (Option<Position>, CursorKind) { fn cursor(&self, area: Rect, ctx: &Editor) -> (Option<Position>, CursorKind) {
self.picker.cursor(area, ctx) self.picker.cursor(area, ctx)
} }
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))?;
Some((width, height))
}
} }
pub struct Picker<T> { pub struct Picker<T> {
@ -271,11 +280,12 @@ pub struct Picker<T> {
/// Filter over original options. /// Filter over original options.
filters: Vec<usize>, // could be optimized into bit but not worth it now filters: Vec<usize>, // could be optimized into bit but not worth it now
/// Current height of the completions box
completion_height: u16,
cursor: usize, cursor: usize,
// pattern: String, // pattern: String,
prompt: Prompt, prompt: Prompt,
/// Whether to render in the middle of the area
render_centered: bool,
/// Wheather to truncate the start (default true) /// Wheather to truncate the start (default true)
pub truncate_start: bool, pub truncate_start: bool,
@ -285,7 +295,6 @@ pub struct Picker<T> {
impl<T> Picker<T> { impl<T> Picker<T> {
pub fn new( pub fn new(
render_centered: bool,
options: Vec<T>, options: Vec<T>,
format_fn: impl Fn(&T) -> Cow<str> + 'static, format_fn: impl Fn(&T) -> Cow<str> + 'static,
callback_fn: impl Fn(&mut Context, &T, Action) + 'static, callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
@ -306,10 +315,10 @@ impl<T> Picker<T> {
filters: Vec::new(), filters: Vec::new(),
cursor: 0, cursor: 0,
prompt, prompt,
render_centered,
truncate_start: true, truncate_start: true,
format_fn: Box::new(format_fn), format_fn: Box::new(format_fn),
callback_fn: Box::new(callback_fn), callback_fn: Box::new(callback_fn),
completion_height: 0,
}; };
// TODO: scoring on empty input should just use a fastpath // TODO: scoring on empty input should just use a fastpath
@ -346,22 +355,38 @@ impl<T> Picker<T> {
self.cursor = 0; self.cursor = 0;
} }
pub fn move_up(&mut self) { /// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`)
if self.matches.is_empty() { pub fn move_by(&mut self, amount: usize, direction: Direction) {
return;
}
let len = self.matches.len(); let len = self.matches.len();
let pos = ((self.cursor + len.saturating_sub(1)) % len) % len;
self.cursor = pos; 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;
}
}
} }
pub fn move_down(&mut self) { /// Move the cursor down by exactly one page. After the last page comes the first page.
if self.matches.is_empty() { pub fn page_up(&mut self) {
return; self.move_by(self.completion_height as usize, Direction::Backward);
} }
let len = self.matches.len();
let pos = (self.cursor + 1) % len; /// Move the cursor up by exactly one page. After the first page comes the last page.
self.cursor = pos; 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> { pub fn selection(&self) -> Option<&T> {
@ -384,23 +409,10 @@ impl<T> Picker<T> {
// - on input change: // - on input change:
// - score all the names in relation to input // - score all the names in relation to input
fn inner_rect(area: Rect) -> Rect {
let margin = Margin {
vertical: area.height * 10 / 100,
horizontal: area.width * 10 / 100,
};
area.inner(&margin)
}
impl<T: 'static> Component for Picker<T> { impl<T: 'static> Component for Picker<T> {
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
let max_width = 50.min(viewport.0); self.completion_height = viewport.1.saturating_sub(4);
let max_height = 10.min(viewport.1.saturating_sub(2)); // add some spacing in the viewport Some(viewport)
let height = (self.options.len() as u16 + 4) // add some spacing for input + padding
.min(max_height);
let width = max_width;
Some((width, height))
} }
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
@ -417,10 +429,22 @@ impl<T: 'static> Component for Picker<T> {
match key_event.into() { match key_event.into() {
shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => { shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
self.move_up(); self.move_by(1, Direction::Backward);
} }
key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => { key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => {
self.move_down(); self.move_by(1, Direction::Forward);
}
key!(PageDown) | ctrl!('f') => {
self.page_down();
}
key!(PageUp) | ctrl!('b') => {
self.page_up();
}
key!(Home) => {
self.to_start();
}
key!(End) => {
self.to_end();
} }
key!(Esc) | ctrl!('c') => { key!(Esc) | ctrl!('c') => {
return close_fn; return close_fn;
@ -458,12 +482,6 @@ impl<T: 'static> Component for Picker<T> {
} }
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let area = if self.render_centered {
inner_rect(area)
} else {
area
};
let text_style = cx.editor.theme.get("ui.text"); let text_style = cx.editor.theme.get("ui.text");
// -- Render the frame: // -- Render the frame:
@ -538,8 +556,6 @@ impl<T: 'static> Component for Picker<T> {
} }
fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) { fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
// TODO: this is mostly duplicate code
let area = inner_rect(area);
let block = Block::default().borders(Borders::ALL); let block = Block::default().borders(Borders::ALL);
// calculate the inner area inside the box // calculate the inner area inside the box
let inner = block.inner(area); let inner = block.inner(area);

Loading…
Cancel
Save