on master help sign in
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

commit442999384256f89eddfa6625a0ffb0257df65ef7 authorLudwig Stecher <ludwig.stecher@gmx.de> dateTue 15 Feb 2022, 02:24 (5 years ago) parent23907a0
6 files changed+151−56
book/src/keymap.md+4 helix-term/src/application.rs+3−2 helix-term/src/commands.rs+7−7 helix-term/src/ui/mod.rs+1 helix-term/src/ui/overlay.rs+73 helix-term/src/ui/picker.rs+63−47
book/src/keymap.md+4
@@ -304,7 +304,11 @@ Keys to use within picker. Remapping currently not supported.
304304| Key | Description |
305305| ----- | ------------- |
306306| `Up`, `Ctrl-k`, `Ctrl-p` | Previous entry |
307+| `PageUp`, `Ctrl-b` | Page up |
307308| `Down`, `Ctrl-j`, `Ctrl-n` | Next entry |
309+| `PageDown`, `Ctrl-f` | Page down |
310+| `Home` | Go to first entry |
311+| `End` | Go to last entry |
308312| `Ctrl-space` | Filter options |
309313| `Enter` | Open selected |
310314| `Ctrl-s` | Open horizontally |
helix-term/src/application.rs+3−2
@@ -13,7 +13,7 @@ use crate::{
1313 compositor::Compositor,
1414 config::Config,
1515 job::Jobs,
16− ui,
16+ ui::{self, overlay::overlayed},
1717};
1818
1919use log::{error, warn};
@@ -124,7 +124,8 @@ impl Application {
124124 if first.is_dir() {
125125 std::env::set_current_dir(&first)?;
126126 editor.new_file(Action::VerticalSplit);
127− compositor.push(Box::new(ui::file_picker(".".into(), &config.editor)));
127+ let picker = ui::file_picker(".".into(), &config.editor);
128+ compositor.push(Box::new(overlayed(picker)));
128129 } else {
129130 let nr_of_files = args.files.len();
130131 editor.open(first.to_path_buf(), Action::VerticalSplit)?;
helix-term/src/commands.rs+7−7
@@ -44,7 +44,7 @@ use movement::Movement;
4444use crate::{
4545 args,
4646 compositor::{self, Component, Compositor},
47− ui::{self, FilePicker, Popup, Prompt, PromptEvent},
47+ ui::{self, overlay::overlayed, FilePicker, Popup, Prompt, PromptEvent},
4848};
4949
5050use crate::job::{self, Job, Jobs};
@@ -1824,7 +1824,7 @@ fn global_search(cx: &mut Context) {
18241824 },
18251825 |_editor, (line_num, path)| Some((path.clone(), Some((*line_num, *line_num)))),
18261826 );
1827− compositor.push(Box::new(picker));
1827+ compositor.push(Box::new(overlayed(picker)));
18281828 });
18291829 Ok(call)
18301830 };
@@ -3359,7 +3359,7 @@ fn file_picker(cx: &mut Context) {
33593359 // We don't specify language markers, root will be the root of the current git repo
33603360 let root = find_root(None, &[]).unwrap_or_else(|| PathBuf::from("./"));
33613361 let picker = ui::file_picker(root, &cx.editor.config);
3362− cx.push_layer(Box::new(picker));
3362+ cx.push_layer(Box::new(overlayed(picker)));
33633363}
33643364
33653365fn buffer_picker(cx: &mut Context) {
@@ -3427,7 +3427,7 @@ fn buffer_picker(cx: &mut Context) {
34273427 Some((meta.path.clone()?, Some((line, line))))
34283428 },
34293429 );
3430− cx.push_layer(Box::new(picker));
3430+ cx.push_layer(Box::new(overlayed(picker)));
34313431}
34323432
34333433fn symbol_picker(cx: &mut Context) {
@@ -3505,7 +3505,7 @@ fn symbol_picker(cx: &mut Context) {
35053505 },
35063506 );
35073507 picker.truncate_start = false;
3508− compositor.push(Box::new(picker))
3508+ compositor.push(Box::new(overlayed(picker)))
35093509 }
35103510 },
35113511 )
@@ -3564,7 +3564,7 @@ fn workspace_symbol_picker(cx: &mut Context) {
35643564 },
35653565 );
35663566 picker.truncate_start = false;
3567− compositor.push(Box::new(picker))
3567+ compositor.push(Box::new(overlayed(picker)))
35683568 }
35693569 },
35703570 )
@@ -4225,7 +4225,7 @@ fn goto_impl(
42254225 Some((path, line))
42264226 },
42274227 );
4228− compositor.push(Box::new(picker));
4228+ compositor.push(Box::new(overlayed(picker)));
42294229 }
42304230 }
42314231}
helix-term/src/ui/mod.rs+1
@@ -3,6 +3,7 @@ pub(crate) mod editor;
33mod info;
44mod markdown;
55pub mod menu;
6+pub mod overlay;
67mod picker;
78mod popup;
89mod prompt;
helix-term/src/ui/overlay.rsnew file+73
@@ -0,0 +1,73 @@
1+use crossterm::event::Event;
2+use helix_core::Position;
3+use helix_view::{
4+ graphics::{CursorKind, Rect},
5+ Editor,
6+};
7+use tui::buffer::Buffer;
8+
9+use crate::compositor::{Component, Context, EventResult};
10+
11+/// Contains a component placed in the center of the parent component
12+pub struct Overlay<T> {
13+ /// Child component
14+ pub content: T,
15+ /// Function to compute the size and position of the child component
16+ pub calc_child_size: Box<dyn Fn(Rect) -> Rect>,
17+}
18+
19+/// Surrounds the component with a margin of 5% on each side, and an additional 2 rows at the bottom
20+pub fn overlayed<T>(content: T) -> Overlay<T> {
21+ Overlay {
22+ content,
23+ calc_child_size: Box::new(|rect: Rect| clip_rect_relative(rect.clip_bottom(2), 90, 90)),
24+ }
25+}
26+
27+fn clip_rect_relative(rect: Rect, percent_horizontal: u8, percent_vertical: u8) -> Rect {
28+ fn mul_and_cast(size: u16, factor: u8) -> u16 {
29+ ((size as u32) * (factor as u32) / 100).try_into().unwrap()
30+ }
31+
32+ let inner_w = mul_and_cast(rect.width, percent_horizontal);
33+ let inner_h = mul_and_cast(rect.height, percent_vertical);
34+
35+ let offset_x = rect.width.saturating_sub(inner_w) / 2;
36+ let offset_y = rect.height.saturating_sub(inner_h) / 2;
37+
38+ Rect {
39+ x: rect.x + offset_x,
40+ y: rect.y + offset_y,
41+ width: inner_w,
42+ height: inner_h,
43+ }
44+}
45+
46+impl<T: Component + 'static> Component for Overlay<T> {
47+ fn render(&mut self, area: Rect, frame: &mut Buffer, ctx: &mut Context) {
48+ let dimensions = (self.calc_child_size)(area);
49+ self.content.render(dimensions, frame, ctx)
50+ }
51+
52+ fn required_size(&mut self, (width, height): (u16, u16)) -> Option<(u16, u16)> {
53+ let area = Rect {
54+ x: 0,
55+ y: 0,
56+ width,
57+ height,
58+ };
59+ let dimensions = (self.calc_child_size)(area);
60+ let viewport = (dimensions.width, dimensions.height);
61+ let _ = self.content.required_size(viewport)?;
62+ Some((width, height))
63+ }
64+
65+ fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult {
66+ self.content.handle_event(event, ctx)
67+ }
68+
69+ fn cursor(&self, area: Rect, ctx: &Editor) -> (Option<Position>, CursorKind) {
70+ let dimensions = (self.calc_child_size)(area);
71+ self.content.cursor(dimensions, ctx)
72+ }
73+}
helix-term/src/ui/picker.rs+63−47
@@ -21,14 +21,14 @@ use std::{
2121};
2222
2323use crate::ui::{Prompt, PromptEvent};
24−use helix_core::Position;
24+use helix_core::{movement::Direction, Position};
2525use helix_view::{
2626 editor::Action,
2727 graphics::{Color, CursorKind, Margin, Rect, Style},
2828 Document, Editor,
2929};
3030
31−pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80;
31+pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72;
3232/// Biggest file size to preview in bytes
3333pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024;
3434
@@ -90,7 +90,7 @@ impl<T> FilePicker<T> {
9090 preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
9191 ) -> Self {
9292 Self {
93− picker: Picker::new(false, options, format_fn, callback_fn),
93+ picker: Picker::new(options, format_fn, callback_fn),
9494 truncate_start: true,
9595 preview_cache: HashMap::new(),
9696 read_buffer: Vec::with_capacity(1024),
@@ -160,8 +160,7 @@ impl<T: 'static> Component for FilePicker<T> {
160160 // | | | |
161161 // +---------+ +---------+
162162
163− let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW;
164− let area = inner_rect(area);
163+ let render_preview = area.width > MIN_AREA_WIDTH_FOR_PREVIEW;
165164 // -- Render the frame:
166165 // clear area
167166 let background = cx.editor.theme.get("ui.background");
@@ -260,6 +259,16 @@ impl<T: 'static> Component for FilePicker<T> {
260259 fn cursor(&self, area: Rect, ctx: &Editor) -> (Option<Position>, CursorKind) {
261260 self.picker.cursor(area, ctx)
262261 }
262+
263+ fn required_size(&mut self, (width, height): (u16, u16)) -> Option<(u16, u16)> {
264+ let picker_width = if width > MIN_AREA_WIDTH_FOR_PREVIEW {
265+ width / 2
266+ } else {
267+ width
268+ };
269+ self.picker.required_size((picker_width, height))?;
270+ Some((width, height))
271+ }
263272}
264273
265274pub struct Picker<T> {
@@ -271,11 +280,12 @@ pub struct Picker<T> {
271280 /// Filter over original options.
272281 filters: Vec<usize>, // could be optimized into bit but not worth it now
273282
283+ /// Current height of the completions box
284+ completion_height: u16,
285+
274286 cursor: usize,
275287 // pattern: String,
276288 prompt: Prompt,
277− /// Whether to render in the middle of the area
278− render_centered: bool,
279289 /// Wheather to truncate the start (default true)
280290 pub truncate_start: bool,
281291
@@ -285,7 +295,6 @@ pub struct Picker<T> {
285295
286296impl<T> Picker<T> {
287297 pub fn new(
288− render_centered: bool,
289298 options: Vec<T>,
290299 format_fn: impl Fn(&T) -> Cow<str> + 'static,
291300 callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
@@ -306,10 +315,10 @@ impl<T> Picker<T> {
306315 filters: Vec::new(),
307316 cursor: 0,
308317 prompt,
309− render_centered,
310318 truncate_start: true,
311319 format_fn: Box::new(format_fn),
312320 callback_fn: Box::new(callback_fn),
321+ completion_height: 0,
313322 };
314323
315324 // TODO: scoring on empty input should just use a fastpath
@@ -346,22 +355,38 @@ impl<T> Picker<T> {
346355 self.cursor = 0;
347356 }
348357
349− pub fn move_up(&mut self) {
350− if self.matches.is_empty() {
351− return;
352− }
358+ /// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`)
359+ pub fn move_by(&mut self, amount: usize, direction: Direction) {
353360 let len = self.matches.len();
354− let pos = ((self.cursor + len.saturating_sub(1)) % len) % len;
355− self.cursor = pos;
356− }
357361
358− pub fn move_down(&mut self) {
359− if self.matches.is_empty() {
360− return;
362+ match direction {
363+ Direction::Forward => {
364+ self.cursor = self.cursor.saturating_add(amount) % len;
365+ }
366+ Direction::Backward => {
367+ self.cursor = self.cursor.saturating_add(len).saturating_sub(amount) % len;
368+ }
361369 }
362− let len = self.matches.len();
363− let pos = (self.cursor + 1) % len;
364− self.cursor = pos;
370+ }
371+
372+ /// Move the cursor down by exactly one page. After the last page comes the first page.
373+ pub fn page_up(&mut self) {
374+ self.move_by(self.completion_height as usize, Direction::Backward);
375+ }
376+
377+ /// Move the cursor up by exactly one page. After the first page comes the last page.
378+ pub fn page_down(&mut self) {
379+ self.move_by(self.completion_height as usize, Direction::Forward);
380+ }
381+
382+ /// Move the cursor to the first entry
383+ pub fn to_start(&mut self) {
384+ self.cursor = 0;
385+ }
386+
387+ /// Move the cursor to the last entry
388+ pub fn to_end(&mut self) {
389+ self.cursor = self.matches.len().saturating_sub(1);
365390 }
366391
367392 pub fn selection(&self) -> Option<&T> {
@@ -384,23 +409,10 @@ impl<T> Picker<T> {
384409// - on input change:
385410// - score all the names in relation to input
386411
387−fn inner_rect(area: Rect) -> Rect {
388− let margin = Margin {
389− vertical: area.height * 10 / 100,
390− horizontal: area.width * 10 / 100,
391− };
392− area.inner(&margin)
393−}
394−
395412impl<T: 'static> Component for Picker<T> {
396413 fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
397− let max_width = 50.min(viewport.0);
398− let max_height = 10.min(viewport.1.saturating_sub(2)); // add some spacing in the viewport
399−
400− let height = (self.options.len() as u16 + 4) // add some spacing for input + padding
401− .min(max_height);
402− let width = max_width;
403− Some((width, height))
414+ self.completion_height = viewport.1.saturating_sub(4);
415+ Some(viewport)
404416 }
405417
406418 fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
@@ -417,10 +429,22 @@ impl<T: 'static> Component for Picker<T> {
417429
418430 match key_event.into() {
419431 shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
420− self.move_up();
432+ self.move_by(1, Direction::Backward);
421433 }
422434 key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => {
423− self.move_down();
435+ self.move_by(1, Direction::Forward);
436+ }
437+ key!(PageDown) | ctrl!('f') => {
438+ self.page_down();
439+ }
440+ key!(PageUp) | ctrl!('b') => {
441+ self.page_up();
442+ }
443+ key!(Home) => {
444+ self.to_start();
445+ }
446+ key!(End) => {
447+ self.to_end();
424448 }
425449 key!(Esc) | ctrl!('c') => {
426450 return close_fn;
@@ -458,12 +482,6 @@ impl<T: 'static> Component for Picker<T> {
458482 }
459483
460484 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
461− let area = if self.render_centered {
462− inner_rect(area)
463− } else {
464− area
465− };
466−
467485 let text_style = cx.editor.theme.get("ui.text");
468486
469487 // -- Render the frame:
@@ -538,8 +556,6 @@ impl<T: 'static> Component for Picker<T> {
538556 }
539557
540558 fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
541− // TODO: this is mostly duplicate code
542− let area = inner_rect(area);
543559 let block = Block::default().borders(Borders::ALL);
544560 // calculate the inner area inside the box
545561 let inner = block.inner(area);