diff --git a/src/app.rs b/src/app.rs index b34a02d731..10d9dafda1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -248,10 +248,14 @@ impl App { Some(menu) => menu.menu, }; - // The the character received in the KeyEvent changes as shift is pressed, - // e.g. '/' becomes '?' on a US keyboard. So just ignore SHIFT. - let mods_without_shift = key.modifiers.difference(KeyModifiers::SHIFT); - self.state.pending_keys.push((mods_without_shift, key.code)); + // The character received in the KeyEvent changes as shift is pressed, + // e.g. '/' becomes '?' on a US keyboard. So ignore SHIFT for char keys. + // For non-char keys (e.g. arrows), SHIFT is meaningful and should be preserved. + let modifiers = match key.code { + event::KeyCode::Char(_) => key.modifiers.difference(KeyModifiers::SHIFT), + _ => key.modifiers, + }; + self.state.pending_keys.push((modifiers, key.code)); let matching_bindings = self .state @@ -304,13 +308,13 @@ impl App { MouseEventKind::ScrollUp => { let scroll_lines = self.state.config.general.mouse_scroll_lines; if scroll_lines > 0 { - self.screen_mut().scroll_up(scroll_lines); + self.screen_mut().scroll_view_up(scroll_lines); } } MouseEventKind::ScrollDown => { let scroll_lines = self.state.config.general.mouse_scroll_lines; if scroll_lines > 0 { - self.screen_mut().scroll_down(scroll_lines); + self.screen_mut().scroll_view_down(scroll_lines); } } _ => return Ok(false), diff --git a/src/default_config.toml b/src/default_config.toml index 7e1c4a8d10..0529690b9f 100644 --- a/src/default_config.toml +++ b/src/default_config.toml @@ -108,6 +108,8 @@ root.move_next_section = ["alt+j", "alt+down"] root.move_parent_section = ["alt+h", "alt+left"] root.half_page_up = ["ctrl+u"] root.half_page_down = ["ctrl+d"] +root.scroll_view_up = ["ctrl+y"] +root.scroll_view_down = ["ctrl+e"] root.show_refs = ["Y"] root.show = ["enter"] root.discard = ["K"] diff --git a/src/ops/editor.rs b/src/ops/editor.rs index 1a7ec1e251..c89cc04832 100644 --- a/src/ops/editor.rs +++ b/src/ops/editor.rs @@ -291,13 +291,13 @@ impl OpTrait for HalfPageUp { fn get_action(&self, _target: &ItemData) -> Option { Some(Rc::new(|app, _term| { app.close_menu(); - app.screen_mut().scroll_half_page_up(); + app.screen_mut().scroll_view_half_page_up(); Ok(()) })) } fn display(&self, _state: &State) -> String { - "Half page up".into() + "Scroll half page up".into() } } @@ -306,12 +306,42 @@ impl OpTrait for HalfPageDown { fn get_action(&self, _target: &ItemData) -> Option { Some(Rc::new(|app, _term| { app.close_menu(); - app.screen_mut().scroll_half_page_down(); + app.screen_mut().scroll_view_half_page_down(); Ok(()) })) } fn display(&self, _state: &State) -> String { - "Half page down".into() + "Scroll half page down".into() + } +} + +pub(crate) struct ScrollViewUp; +impl OpTrait for ScrollViewUp { + fn get_action(&self, _target: &ItemData) -> Option { + Some(Rc::new(|app, _term| { + app.close_menu(); + app.screen_mut().scroll_view_up(1); + Ok(()) + })) + } + + fn display(&self, _state: &State) -> String { + "Scroll view up".into() + } +} + +pub(crate) struct ScrollViewDown; +impl OpTrait for ScrollViewDown { + fn get_action(&self, _target: &ItemData) -> Option { + Some(Rc::new(|app, _term| { + app.close_menu(); + app.screen_mut().scroll_view_down(1); + Ok(()) + })) + } + + fn display(&self, _state: &State) -> String { + "Scroll view down".into() } } diff --git a/src/ops/mod.rs b/src/ops/mod.rs index da19bc6773..56afcc980c 100644 --- a/src/ops/mod.rs +++ b/src/ops/mod.rs @@ -112,6 +112,8 @@ pub(crate) enum Op { MoveParentSection, HalfPageUp, HalfPageDown, + ScrollViewUp, + ScrollViewDown, Refresh, Quit, @@ -142,6 +144,8 @@ impl Op { Op::MoveParentSection => Box::new(editor::MoveParentSection), Op::HalfPageUp => Box::new(editor::HalfPageUp), Op::HalfPageDown => Box::new(editor::HalfPageDown), + Op::ScrollViewUp => Box::new(editor::ScrollViewUp), + Op::ScrollViewDown => Box::new(editor::ScrollViewDown), Op::Checkout => Box::new(branch::Checkout), Op::CheckoutNewBranch => Box::new(branch::CheckoutNewBranch), Op::Spinoff => Box::new(branch::Spinoff), diff --git a/src/screen/mod.rs b/src/screen/mod.rs index cdc3ed9579..9de5363d6e 100644 --- a/src/screen/mod.rs +++ b/src/screen/mod.rs @@ -168,44 +168,24 @@ impl Screen { .unwrap_or(self.cursor) } - pub(crate) fn scroll_half_page_up(&mut self) { + pub(crate) fn scroll_view_half_page_up(&mut self) { let half_screen = self.size.height as usize / 2; - self.scroll = self.scroll.saturating_sub(half_screen); - - let nav_mode = self.selected_item_nav_mode(); - self.update_cursor(nav_mode); + self.scroll_view_up(half_screen); } - pub(crate) fn scroll_half_page_down(&mut self) { + pub(crate) fn scroll_view_half_page_down(&mut self) { let half_screen = self.size.height as usize / 2; - self.scroll = (self.scroll + half_screen).min( - self.line_index - .iter() - .copied() - .enumerate() - .map(|(line, _)| (line + 1).saturating_sub(half_screen)) - .next_back() - .unwrap_or(0), - ); - - let nav_mode = self.selected_item_nav_mode(); - self.update_cursor(nav_mode); + self.scroll_view_down(half_screen); } - pub(crate) fn scroll_up(&mut self, lines: usize) { + pub(crate) fn scroll_view_up(&mut self, lines: usize) { self.scroll = self.scroll.saturating_sub(lines); - let nav_mode = self.selected_item_nav_mode(); - self.update_cursor(nav_mode); + self.clamp_scroll(); } - pub(crate) fn scroll_down(&mut self, lines: usize) { - let max_scroll = self - .line_index - .len() - .saturating_sub(self.size.height as usize); - self.scroll = (self.scroll + lines).min(max_scroll); - let nav_mode = self.selected_item_nav_mode(); - self.update_cursor(nav_mode); + pub(crate) fn scroll_view_down(&mut self, lines: usize) { + self.scroll = self.scroll.saturating_add(lines); + self.clamp_scroll(); } pub(crate) fn toggle_section(&mut self) { @@ -231,6 +211,7 @@ impl Screen { } fn update_cursor(&mut self, nav_mode: NavMode) { + self.clamp_scroll(); self.clamp_cursor(); if self.is_cursor_off_screen() { self.move_cursor_to_screen_center(); @@ -272,6 +253,7 @@ impl Screen { .flatten() .map(|(i, _item)| i) .collect(); + self.clamp_scroll(); } fn is_cursor_off_screen(&self) -> bool { @@ -289,6 +271,26 @@ impl Screen { .clamp(0, self.line_index.len().saturating_sub(1)); } + fn clamp_scroll(&mut self) { + if self.line_index.is_empty() { + self.scroll = 0; + return; + } + + self.scroll = self.scroll.min(self.max_scroll_with_context()); + } + + fn max_scroll_with_context(&self) -> usize { + let len = self.line_index.len(); + if len == 0 { + return 0; + } + + let max_scroll = len.saturating_sub(self.size.height as usize); + let max_scroll = max_scroll.saturating_add(BOTTOM_CONTEXT_LINES); + max_scroll.min(len.saturating_sub(1)) + } + fn move_from_unselectable(&mut self, nav_mode: NavMode) { if !self.nav_filter(self.cursor, nav_mode) { self.select_previous(nav_mode); diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 14ade27f08..d9a310997d 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -285,7 +285,7 @@ fn hide_untracked() { let mut app = ctx.init_app(); let mut config = app.state.repo.config().unwrap(); - config.set_str("status.showUntrackedFiles", "off").unwrap(); + config.set_str("status.showUntrackedFiles", "no").unwrap(); ctx.update(&mut app, keys("g")); insta::assert_snapshot!(ctx.redact_buffer()); diff --git a/src/tests/snapshots/gitu__tests__editor__scroll_past_selection.snap b/src/tests/snapshots/gitu__tests__editor__scroll_past_selection.snap index 220ba90af6..1241613be4 100644 --- a/src/tests/snapshots/gitu__tests__editor__scroll_past_selection.snap +++ b/src/tests/snapshots/gitu__tests__editor__scroll_past_selection.snap @@ -2,24 +2,24 @@ source: src/tests/editor.rs expression: ctx.redact_buffer() --- - +line 20 (file-1) | - modified file-2 | -▌@@ -0,0 +1,20 @@ | -▌+line 1 (file-2) | -▌+line 2 (file-2) | -▌+line 3 (file-2) | -▌+line 4 (file-2) | -▌+line 5 (file-2) | -▌+line 6 (file-2) | -▌+line 7 (file-2) | -▌+line 8 (file-2) | -▌+line 9 (file-2) | -▌+line 10 (file-2) | -▌+line 11 (file-2) | -▌+line 12 (file-2) | -▌+line 13 (file-2) | -▌+line 14 (file-2) | -▌+line 15 (file-2) | -▌+line 16 (file-2) | -▌+line 17 (file-2) | -styles_hash: ae0fb0ed74b80538 + +line 3 (file-2) | + +line 4 (file-2) | + +line 5 (file-2) | + +line 6 (file-2) | + +line 7 (file-2) | + +line 8 (file-2) | + +line 9 (file-2) | + +line 10 (file-2) | + +line 11 (file-2) | + +line 12 (file-2) | + +line 13 (file-2) | + +line 14 (file-2) | + +line 15 (file-2) | + +line 16 (file-2) | + +line 17 (file-2) | + +line 18 (file-2) | + +line 19 (file-2) | + +line 20 (file-2) | + modified file-3 | + @@ -0,0 +1,20 @@ | +styles_hash: ba6ffe68111a437 diff --git a/src/tests/snapshots/gitu__tests__help_menu.snap b/src/tests/snapshots/gitu__tests__help_menu.snap index 474e2a7073..24e101ca71 100644 --- a/src/tests/snapshots/gitu__tests__help_menu.snap +++ b/src/tests/snapshots/gitu__tests__help_menu.snap @@ -6,20 +6,20 @@ expression: ctx.redact_buffer() ▌Your branch is up to date with 'origin/main'. | | Recent commits | - b66a0bf main origin/main add initial-file | ────────────────────────────────────────────────────────────────────────────────| - Help Submenu On branch main | - Y Show Refs b Branch tab Fold | - k/up Up c Commit | - j/down Down f Fetch | - ctrl+k/ctrl+up Up line h/? Help | - ctrl+j/ctrl+down Down line l Log | - alt+k/alt+up Prev section m Merge | - alt+j/alt+down Next section M Remote | - alt+h/alt+left Parent section F Pull | - ctrl+u Half page up P Push | - ctrl+d Half page down r Rebase | - g Refresh X Reset | - q/esc Quit/Close V Revert | - z Stash | -styles_hash: 43322e31c74e471 + Help Submenu On branch main | + Y Show Refs b Branch tab Fold | + k/up Up c Commit | + j/down Down f Fetch | + ctrl+k/ctrl+up Up line h/? Help | + ctrl+j/ctrl+down Down line l Log | + alt+k/alt+up Prev section m Merge | + alt+j/alt+down Next section M Remote | + alt+h/alt+left Parent section F Pull | + ctrl+u Scroll half page up P Push | + ctrl+d Scroll half page down r Rebase | + ctrl+y Scroll view up X Reset | + ctrl+e Scroll view down V Revert | + g Refresh z Stash | + q/esc Quit/Close | +styles_hash: 899f9bd379a3c1d3 diff --git a/src/tests/snapshots/gitu__tests__mouse_wheel_scroll_down.snap b/src/tests/snapshots/gitu__tests__mouse_wheel_scroll_down.snap index b375c732a7..681817679d 100644 --- a/src/tests/snapshots/gitu__tests__mouse_wheel_scroll_down.snap +++ b/src/tests/snapshots/gitu__tests__mouse_wheel_scroll_down.snap @@ -12,7 +12,7 @@ expression: ctx.redact_buffer() modified file07… | modified file08… | modified file09… | -▌modified file10… | + modified file10… | modified file11… | modified file12… | modified file13… | @@ -22,4 +22,4 @@ expression: ctx.redact_buffer() modified file17… | modified file18… | modified file19… | -styles_hash: 581a108fde4c40fe +styles_hash: e2b83c90b357195b diff --git a/src/tests/snapshots/gitu__tests__mouse_wheel_scroll_up.snap b/src/tests/snapshots/gitu__tests__mouse_wheel_scroll_up.snap index af7fc1c057..f40ae9408f 100644 --- a/src/tests/snapshots/gitu__tests__mouse_wheel_scroll_up.snap +++ b/src/tests/snapshots/gitu__tests__mouse_wheel_scroll_up.snap @@ -5,7 +5,7 @@ expression: ctx.redact_buffer() modified file14… | modified file15… | modified file16… | -▌modified file17… | + modified file17… | modified file18… | modified file19… | modified file20… | @@ -22,4 +22,4 @@ expression: ctx.redact_buffer() | Recent commits | ae744cc main add file30 | -styles_hash: f0f5b789b0d8de8f +styles_hash: 9d0bedcc92bfcaa8