diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 3f3aaba2b..33f9e1fcd 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -268,6 +268,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 { @@ -389,6 +393,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, } } @@ -440,6 +447,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(); @@ -878,6 +923,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) @@ -911,6 +965,7 @@ impl Picker { return; } }; + let doc_height = doc.text().len_lines(); let mut offset = ViewPosition::default(); if let Some((start_line, end_line)) = range { @@ -939,6 +994,28 @@ impl Picker { } } + let mut current_line = doc.text().slice(..).char_to_line(offset.anchor); + + preview_scroll_offset.1 = match preview_scroll_offset.0 { + Direction::Backward => preview_scroll_offset.1.min(current_line), + Direction::Forward => preview_scroll_offset.1.min( + doc_height + .saturating_sub(current_line) + .saturating_sub(inner.height as usize), + ), + }; + + offset.anchor = match preview_scroll_offset.0 { + Direction::Backward => doc + .text() + .slice(..) + .line_to_char(current_line.saturating_sub(preview_scroll_offset.1)), + Direction::Forward => doc + .text() + .slice(..) + .line_to_char(current_line.saturating_add(preview_scroll_offset.1)), + }; + let loader = cx.editor.syn_loader.load(); let syntax_highlighter = @@ -973,6 +1050,8 @@ impl Picker { decorations.add_decoration(draw_highlight); } + current_line = doc.text().slice(..).char_to_line(offset.anchor); + render_document( surface, inner, @@ -985,6 +1064,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 = current_line; + 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)); + } + } + } } } } @@ -1056,10 +1168,10 @@ impl Component for Picker { self.move_by(1, Direction::Forward); } - key!(PageDown) | ctrl!('d') => { + key!(PageDown) | ctrl!('d') if !self.show_preview => { self.page_down(); } - key!(PageUp) | ctrl!('u') => { + key!(PageUp) | ctrl!('u') if !self.show_preview => { self.page_up(); } key!(Home) => { @@ -1124,6 +1236,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, + ); + } + key!(PageUp) | alt!('b') if self.show_preview => { + self.move_preview_by(self.preview_height as usize, Direction::Backward); + } + key!(PageDown) | alt!('f') if self.show_preview => { + self.move_preview_by(self.preview_height as usize, Direction::Forward); + } _ => { self.prompt_handle_event(event, ctx); } @@ -1153,6 +1295,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)) }