mirror of
https://github.com/helix-editor/helix.git
synced 2025-01-19 13:37:06 +04:00
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
This commit is contained in:
parent
23907a063c
commit
4429993842
@ -304,7 +304,11 @@ # Picker
|
|||||||
| 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 @@
|
|||||||
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 @@ pub fn new(args: Args, mut config: Config) -> Result<Self, Error> {
|
|||||||
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 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 format(&self) -> Cow<str> {
|
|||||||
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 nested_to_flat(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
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 jump_to(
|
|||||||
Some((path, line))
|
Some((path, line))
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
compositor.push(Box::new(picker));
|
compositor.push(Box::new(overlayed(picker)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
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;
|
||||||
|
73
helix-term/src/ui/overlay.rs
Normal file
73
helix-term/src/ui/overlay.rs
Normal file
@ -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 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 @@ pub fn new(
|
|||||||
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 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
|||||||
// | | | |
|
// | | | |
|
||||||
// +---------+ +---------+
|
// +---------+ +---------+
|
||||||
|
|
||||||
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 @@ fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult {
|
|||||||
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 @@ pub fn new(
|
|||||||
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 @@ pub fn score(&mut self) {
|
|||||||
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 @@ pub fn save_filter(&mut self) {
|
|||||||
// - 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 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
|||||||
|
|
||||||
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 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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…
Reference in New Issue
Block a user