diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 82fe9689164d..919ddc2260dc 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -273,6 +273,10 @@ pub struct Picker { /// An event handler for syntax highlighting the currently previewed file. preview_highlight_handler: Sender>, dynamic_query_handler: Option>, + + preview_scroll_offset: (Direction, usize), + preview_height: u16, + cursor_picker: u32, } impl Picker { @@ -394,6 +398,9 @@ impl Picker { file_fn: None, preview_highlight_handler: PreviewHighlightHandler::::default().spawn(), dynamic_query_handler: None, + preview_scroll_offset: (Direction::Forward, 0), + preview_height: 0, + cursor_picker: 0, } } @@ -445,6 +452,44 @@ impl Picker { self } + /// Moves the picker file preview by a number of lines, either down (`Forward`) or up (`Backward`) + fn move_preview_by(&mut self, amount: usize, move_direction: Direction) { + let (current_scroll_direction, current_scroll_offset) = self.preview_scroll_offset; + + match move_direction { + Direction::Backward => match current_scroll_direction { + Direction::Backward => { + self.preview_scroll_offset.1 = current_scroll_offset.saturating_add(amount); + } + Direction::Forward => { + if let Some(change) = current_scroll_offset.checked_sub(amount) { + self.preview_scroll_offset.1 = change; + } else { + self.preview_scroll_offset = ( + Direction::Backward, + amount.saturating_sub(current_scroll_offset), + ); + } + } + }, + Direction::Forward => match current_scroll_direction { + Direction::Backward => { + if let Some(change) = current_scroll_offset.checked_sub(amount) { + self.preview_scroll_offset.1 = change; + } else { + self.preview_scroll_offset = ( + Direction::Forward, + amount.saturating_sub(current_scroll_offset), + ); + } + } + Direction::Forward => { + self.preview_scroll_offset.1 = current_scroll_offset.saturating_add(amount); + } + }, + }; + } + /// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`) pub fn move_by(&mut self, amount: u32, direction: Direction) { let len = self.matcher.snapshot().matched_item_count(); @@ -849,6 +894,15 @@ impl Picker { let inner = inner.inner(margin); BLOCK.render(area, surface); + let mut preview_scroll_offset = self.preview_scroll_offset; + + // Reset preview scroll if cursor moved + let cursor_position = self.cursor_picker; + if self.cursor != cursor_position { + preview_scroll_offset = (Direction::Forward, 0); + self.cursor_picker = self.cursor; + } + if let Some((preview, range)) = self.get_preview(cx.editor) { let doc = match preview.document() { Some(doc) @@ -866,7 +920,8 @@ impl Picker { return; } }; - + let doc_height = doc.text().len_lines(); + let mut offset = ViewPosition::default(); if let Some((start_line, end_line)) = range { let height = end_line - start_line; @@ -894,6 +949,21 @@ impl Picker { } } + // limit scroll offset between [-offset.anchor, doc.text().len_lines() - offset.anchor - preview height] + preview_scroll_offset.1 = match preview_scroll_offset.0 { + Direction::Backward => preview_scroll_offset.1.min(offset.anchor), + Direction::Forward => preview_scroll_offset.1.min( + doc_height + .saturating_sub(offset.anchor) + .saturating_sub(inner.height as usize), + ), + }; + + offset.vertical_offset = match preview_scroll_offset.0 { + Direction::Backward => offset.anchor.saturating_sub(preview_scroll_offset.1), + Direction::Forward => offset.anchor.saturating_add(preview_scroll_offset.1), + }; + let syntax_highlights = EditorView::doc_syntax_highlights( doc, offset.anchor, @@ -943,6 +1013,39 @@ impl Picker { &cx.editor.theme, decorations, ); + + self.preview_scroll_offset = preview_scroll_offset; + + let win_height = inner.height as usize; + let len = doc_height; + let fits = len <= win_height; + let scroll = offset.vertical_offset; + let scroll_style = cx.editor.theme.get("ui.menu.scroll"); + + const fn div_ceil(a: usize, b: usize) -> usize { + (a + b - 1) / b + } + + if !fits { + let scroll_height = div_ceil(win_height.pow(2), len).min(win_height); + let scroll_line = (win_height - scroll_height) * scroll + / std::cmp::max(1, len.saturating_sub(win_height)); + + let mut cell; + for i in 0..win_height { + cell = &mut surface[(inner.right() - 1, inner.top() + i as u16)]; + + cell.set_symbol("▐"); // right half block + + if scroll_line <= i && i < scroll_line + scroll_height { + // Draw scroll thumb + cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset)); + } else { + // Draw scroll track + cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset)); + } + } + } } } } @@ -1074,6 +1177,36 @@ impl Component for Picker { self.toggle_preview(); } + alt!('k') | shift!(Up) if self.show_preview => { + self.move_preview_by( + ctx.editor.config().scroll_lines.unsigned_abs(), + Direction::Backward, + ); + } + alt!('j') | shift!(Down) if self.show_preview => { + self.move_preview_by( + ctx.editor.config().scroll_lines.unsigned_abs(), + Direction::Forward, + ); + } + alt!('u') if self.show_preview => { + self.move_preview_by( + self.preview_height.saturating_div(2) as usize, + Direction::Backward, + ); + } + alt!('d') if self.show_preview => { + self.move_preview_by( + self.preview_height.saturating_div(2) as usize, + Direction::Forward, + ); + } + alt!('b') if self.show_preview => { + self.move_preview_by(self.preview_height as usize, Direction::Backward); + } + alt!('f') if self.show_preview => { + self.move_preview_by(self.preview_height as usize, Direction::Forward); + } _ => { self.prompt_handle_event(event, ctx); } @@ -1095,6 +1228,8 @@ impl Component for Picker Option<(u16, u16)> { self.completion_height = height.saturating_sub(4 + self.header_height()); + self.preview_height = height.saturating_sub(2); + Some((width, height)) }