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 all 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
5 changes: 3 additions & 2 deletions helix-term/src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{
compositor::Compositor,
config::Config,
job::Jobs,
ui,
ui::{self, overlay::overlayed},
};

use log::{error, warn};
Expand Down Expand Up @@ -138,7 +138,8 @@ impl Application {
if first.is_dir() {
std::env::set_current_dir(&first)?;
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 {
let nr_of_files = args.files.len();
editor.open(first.to_path_buf(), Action::VerticalSplit)?;
Expand Down
14 changes: 7 additions & 7 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ use movement::Movement;
use crate::{
args,
compositor::{self, Component, Compositor},
ui::{self, FilePicker, Popup, Prompt, PromptEvent},
ui::{self, overlay::overlayed, FilePicker, Popup, Prompt, PromptEvent},
};

use crate::job::{self, Job, Jobs};
Expand Down Expand Up @@ -1785,7 +1785,7 @@ fn global_search(cx: &mut Context) {
},
|_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)
};
Expand Down Expand Up @@ -3251,7 +3251,7 @@ fn file_picker(cx: &mut Context) {
// 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 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) {
Expand Down Expand Up @@ -3319,7 +3319,7 @@ fn buffer_picker(cx: &mut Context) {
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) {
Expand Down Expand Up @@ -3397,7 +3397,7 @@ fn symbol_picker(cx: &mut Context) {
},
);
picker.truncate_start = false;
compositor.push(Box::new(picker))
compositor.push(Box::new(overlayed(picker)))
}
},
)
Expand Down Expand Up @@ -3457,7 +3457,7 @@ fn workspace_symbol_picker(cx: &mut Context) {
},
);
picker.truncate_start = false;
compositor.push(Box::new(picker))
compositor.push(Box::new(overlayed(picker)))
}
},
)
Expand Down Expand Up @@ -4117,7 +4117,7 @@ fn goto_impl(
Some((path, line))
},
);
compositor.push(Box::new(picker));
compositor.push(Box::new(overlayed(picker)));
}
}
}
Expand Down
1 change: 1 addition & 0 deletions 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;
pub mod overlay;
mod picker;
mod popup;
mod prompt;
Expand Down
73 changes: 73 additions & 0 deletions helix-term/src/ui/overlay.rs
Original file line number Diff line number Diff line change
@@ -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> {
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>,
}

/// 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)
}
}
110 changes: 63 additions & 47 deletions helix-term/src/ui/picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ 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;
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 @@ -90,7 +90,7 @@ impl<T> FilePicker<T> {
preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
) -> Self {
Self {
picker: Picker::new(false, options, format_fn, callback_fn),
picker: Picker::new(options, format_fn, callback_fn),
truncate_start: true,
preview_cache: HashMap::new(),
read_buffer: Vec::with_capacity(1024),
Expand Down Expand Up @@ -160,8 +160,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 +259,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 +280,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 +295,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 +315,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 +355,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 +409,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 +429,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 +482,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 +556,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