Skip to content
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

Merged
merged 6 commits into from
Feb 15, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions book/src/keymap.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,11 @@ Keys to use within picker. Remapping currently not supported.
| Key | Description |
| ----- | ------------- |
| `Up`, `Ctrl-k`, `Ctrl-p` | Previous entry |
| `PageUp`, `Ctrl-b` | Page up |
| `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 |
| `Enter` | Open selected |
| `Ctrl-s` | Open horizontally |
Expand Down
4 changes: 2 additions & 2 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3396,7 +3396,7 @@ fn symbol_picker(cx: &mut Context) {
Some((path, line))
},
);
picker.truncate_start = false;
picker.content.truncate_start = false;
compositor.push(Box::new(picker))
}
},
Expand Down Expand Up @@ -3456,7 +3456,7 @@ fn workspace_symbol_picker(cx: &mut Context) {
Some((path, line))
},
);
picker.truncate_start = false;
picker.content.truncate_start = false;
compositor.push(Box::new(picker))
}
},
Expand Down
8 changes: 7 additions & 1 deletion helix-term/src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub(crate) mod editor;
mod info;
mod markdown;
pub mod menu;
mod overlay;
mod picker;
mod popup;
mod prompt;
Expand All @@ -25,6 +26,8 @@ use helix_view::{Document, Editor, View};

use std::path::PathBuf;

use self::overlay::Overlay;

pub fn regex_prompt(
cx: &mut crate::commands::Context,
prompt: std::borrow::Cow<'static, str>,
Expand Down Expand Up @@ -93,7 +96,10 @@ pub fn regex_prompt(
)
}

pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker<PathBuf> {
pub fn file_picker(
root: PathBuf,
config: &helix_view::editor::Config,
) -> Overlay<FilePicker<PathBuf>> {
use ignore::{types::TypesBuilder, WalkBuilder};
use std::time;

Expand Down
65 changes: 65 additions & 0 deletions helix-term/src/ui/overlay.rs
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> {
Copy link
Member

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.

Copy link
Member

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)

Copy link
Contributor

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.

Copy link
Contributor Author

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.

/// 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)
}
}
127 changes: 74 additions & 53 deletions helix-term/src/ui/picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)),
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's return FilePicker here and have it wrapped in an overlay inside the ui::file_picker method. That makes it reusable in cases where we don't want to render it centered but instead nest it in a Popup for example.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 Overlay. The FilePicker struct is also used for selecting a buffer, for global search, for symbol search, and for Go to implementation/references.


Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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> {
Expand All @@ -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,

Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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> {
Expand All @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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);
Expand Down