-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add PageUp
, PageDown
, Ctrl-u
, Ctrl-d
, Home
, End
keyboard shortcuts to file picker
#1612
Changes from 5 commits
dfd7dbf
ce8bc19
256cad6
915cfb1
a59dc11
66f7bc9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
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>, | ||
} | ||
|
||
pub(super) 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) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,14 +21,16 @@ use std::{ | |
}; | ||
|
||
use crate::ui::{Prompt, PromptEvent}; | ||
use helix_core::Position; | ||
use helix_core::{movement::Direction, Position}; | ||
use helix_view::{ | ||
editor::Action, | ||
graphics::{Color, CursorKind, Margin, Rect, Style}, | ||
Document, Editor, | ||
}; | ||
|
||
pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80; | ||
use super::overlay::{clip_rect_relative, Overlay}; | ||
|
||
pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72; | ||
/// Biggest file size to preview in bytes | ||
pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024; | ||
|
||
|
@@ -88,13 +90,16 @@ impl<T> FilePicker<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), | ||
truncate_start: true, | ||
preview_cache: HashMap::new(), | ||
read_buffer: Vec::with_capacity(1024), | ||
file_fn: Box::new(preview_fn), | ||
) -> Overlay<Self> { | ||
Overlay { | ||
content: Self { | ||
picker: Picker::new(options, format_fn, callback_fn), | ||
truncate_start: true, | ||
preview_cache: HashMap::new(), | ||
read_buffer: Vec::with_capacity(1024), | ||
file_fn: Box::new(preview_fn), | ||
}, | ||
calc_child_size: Box::new(|rect: Rect| clip_rect_relative(rect.clip_bottom(2), 90, 90)), | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's return There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @archseer done. There were a few other places though where it needed to be wrapped in an |
||
|
||
|
@@ -160,8 +165,7 @@ impl<T: 'static> Component for FilePicker<T> { | |
// | | | | | ||
// +---------+ +---------+ | ||
|
||
let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW; | ||
let area = inner_rect(area); | ||
let render_preview = area.width > MIN_AREA_WIDTH_FOR_PREVIEW; | ||
// -- Render the frame: | ||
// clear area | ||
let background = cx.editor.theme.get("ui.background"); | ||
|
@@ -260,6 +264,16 @@ impl<T: 'static> Component for FilePicker<T> { | |
fn cursor(&self, area: Rect, ctx: &Editor) -> (Option<Position>, CursorKind) { | ||
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)) | ||
} | ||
Aloso marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
pub struct Picker<T> { | ||
|
@@ -271,11 +285,12 @@ pub struct Picker<T> { | |
/// Filter over original options. | ||
filters: Vec<usize>, // could be optimized into bit but not worth it now | ||
|
||
/// Current height of the completions box | ||
completion_height: u16, | ||
|
||
cursor: usize, | ||
// pattern: String, | ||
prompt: Prompt, | ||
/// Whether to render in the middle of the area | ||
render_centered: bool, | ||
/// Wheather to truncate the start (default true) | ||
pub truncate_start: bool, | ||
|
||
|
@@ -285,7 +300,6 @@ 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, | ||
|
@@ -306,10 +320,10 @@ impl<T> Picker<T> { | |
filters: Vec::new(), | ||
cursor: 0, | ||
prompt, | ||
render_centered, | ||
truncate_start: true, | ||
format_fn: Box::new(format_fn), | ||
callback_fn: Box::new(callback_fn), | ||
completion_height: 0, | ||
}; | ||
|
||
// TODO: scoring on empty input should just use a fastpath | ||
|
@@ -346,22 +360,38 @@ impl<T> Picker<T> { | |
self.cursor = 0; | ||
} | ||
|
||
pub fn move_up(&mut self) { | ||
if self.matches.is_empty() { | ||
return; | ||
} | ||
/// 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(); | ||
let pos = ((self.cursor + len.saturating_sub(1)) % len) % len; | ||
self.cursor = pos; | ||
} | ||
|
||
pub fn move_down(&mut self) { | ||
if self.matches.is_empty() { | ||
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; | ||
} | ||
} | ||
let len = self.matches.len(); | ||
let pos = (self.cursor + 1) % len; | ||
self.cursor = pos; | ||
} | ||
|
||
/// 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> { | ||
|
@@ -384,23 +414,10 @@ impl<T> Picker<T> { | |
// - on input change: | ||
// - 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> { | ||
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { | ||
let max_width = 50.min(viewport.0); | ||
let max_height = 10.min(viewport.1.saturating_sub(2)); // add some spacing in the 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)) | ||
self.completion_height = viewport.1.saturating_sub(4); | ||
Some(viewport) | ||
} | ||
|
||
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { | ||
|
@@ -417,10 +434,22 @@ impl<T: 'static> Component for Picker<T> { | |
|
||
match key_event.into() { | ||
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') => { | ||
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') => { | ||
return close_fn; | ||
|
@@ -458,12 +487,6 @@ impl<T: 'static> Component for Picker<T> { | |
} | ||
|
||
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"); | ||
|
||
// -- Render the frame: | ||
|
@@ -538,8 +561,6 @@ impl<T: 'static> Component for Picker<T> { | |
} | ||
|
||
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); | ||
// calculate the inner area inside the box | ||
let inner = block.inner(area); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please separate this out,
Popup
with some calculation should be enough to replace this type altogether.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(definitely avoid floats and use a percentage value between 0 and 100)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be better if a type that can only guarantee the value range but I am not sure if we have something like that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@archseer I refactored the code again, it is now much simpler. Also, I'm not sure I understand what you mean with your first comment.