From 91714ef096fbe18916b6e3c9b63ac5785dcae4b6 Mon Sep 17 00:00:00 2001 From: ColonelBucket8 Date: Fri, 8 May 2026 10:39:17 +0800 Subject: [PATCH] feat: add keybinding to scroll top and bottom --- src/default_config.toml | 4 ++- src/ops/editor.rs | 28 +++++++++++++++++++ src/ops/mod.rs | 4 +++ src/screen/mod.rs | 24 ++++++++++++++++ src/tests/commit.rs | 4 +-- src/tests/mod.rs | 12 ++++---- .../snapshots/gitu__tests__help_menu.snap | 15 +++++----- src/ui/layout/mod.rs | 2 +- .../gitu__ui__layout__tests__gitu_mockup.snap | 2 +- 9 files changed, 77 insertions(+), 18 deletions(-) diff --git a/src/default_config.toml b/src/default_config.toml index c19e354e6d..b007413237 100644 --- a/src/default_config.toml +++ b/src/default_config.toml @@ -105,7 +105,7 @@ blame.code_line = { mods = "DIM" } [bindings] root.quit = ["q", "esc"] -root.refresh = ["g"] +root.refresh = ["g+r"] root.toggle_section = ["tab"] root.move_up = ["k", "up"] root.move_down = ["j", "down"] @@ -116,6 +116,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.move_top = ["g+g"] +root.move_bottom = ["G"] root.show_refs = ["Y"] root.show = ["enter"] root.discard = ["K"] diff --git a/src/ops/editor.rs b/src/ops/editor.rs index 62d20e64bf..4a48aea51f 100644 --- a/src/ops/editor.rs +++ b/src/ops/editor.rs @@ -300,3 +300,31 @@ impl OpTrait for HalfPageDown { "Half page down".into() } } + +pub(crate) struct MoveTop; +impl OpTrait for MoveTop { + fn get_action(&self, _target: &ItemData) -> Option { + Some(Rc::new(|app, _term| { + app.screen_mut().move_cursor_to_top(); + Ok(()) + })) + } + + fn display(&self, _state: &State) -> String { + "Top".into() + } +} + +pub(crate) struct MoveBottom; +impl OpTrait for MoveBottom { + fn get_action(&self, _target: &ItemData) -> Option { + Some(Rc::new(|app, _term| { + app.screen_mut().move_cursor_to_bottom(); + Ok(()) + })) + } + + fn display(&self, _state: &State) -> String { + "Bottom".into() + } +} diff --git a/src/ops/mod.rs b/src/ops/mod.rs index 7e87e8fa49..285e1a7d00 100644 --- a/src/ops/mod.rs +++ b/src/ops/mod.rs @@ -121,6 +121,8 @@ pub(crate) enum Op { MovePrevSection, MoveNextSection, MoveParentSection, + MoveTop, + MoveBottom, HalfPageUp, HalfPageDown, @@ -153,6 +155,8 @@ impl Op { Op::MoveParentSection => Box::new(editor::MoveParentSection), Op::HalfPageUp => Box::new(editor::HalfPageUp), Op::HalfPageDown => Box::new(editor::HalfPageDown), + Op::MoveTop => Box::new(editor::MoveTop), + Op::MoveBottom => Box::new(editor::MoveBottom), 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 1e6f947d42..41376296bc 100644 --- a/src/screen/mod.rs +++ b/src/screen/mod.rs @@ -326,6 +326,30 @@ impl Screen { } } + pub(crate) fn move_cursor_to_top(&mut self) { + if self.line_index.is_empty() { + return; + } + if let Some(first) = self.find_first_selectable() { + self.cursor = first; + self.scroll = 0; + } + } + + pub(crate) fn move_cursor_to_bottom(&mut self) { + if self.line_index.is_empty() { + return; + } + if let Some(last) = self.find_last_selectable() { + self.cursor = last; + self.scroll_fit_end(); + } + } + + fn find_last_selectable(&self) -> Option { + (0..self.line_index.len()).rfind(|&line_i| !self.at_line(line_i).unselectable) + } + pub(crate) fn is_collapsed(&self, item: &Item) -> bool { self.collapsed.contains(&item.id) } diff --git a/src/tests/commit.rs b/src/tests/commit.rs index bcf489b070..b6360050b3 100644 --- a/src/tests/commit.rs +++ b/src/tests/commit.rs @@ -19,7 +19,7 @@ fn commit_instant_fixup() { commit(&ctx.dir, "instant_fixup.txt", "mistake\n"); fs::write(ctx.dir.join("instant_fixup.txt"), "fixed\n").unwrap(); run(&ctx.dir, &["git", "add", "."]); - ctx.update(&mut state, keys("gjjjjjcF")); + ctx.update(&mut state, keys("grjjjjjcF")); insta::assert_snapshot!(ctx.redact_buffer()); } @@ -38,7 +38,7 @@ fn commit_instant_fixup_stashes_changes_and_keeps_empty() { fs::write(ctx.dir.join("instant_fixup.txt"), "fixed\n").unwrap(); run(&ctx.dir, &["git", "add", "."]); fs::write(ctx.dir.join("instant_fixup.txt"), "unstaged\n").unwrap(); - ctx.update(&mut state, keys("gjjjjjjjjjcF")); + ctx.update(&mut state, keys("grjjjjjjjjjcF")); insta::assert_snapshot!(ctx.redact_buffer()); } diff --git a/src/tests/mod.rs b/src/tests/mod.rs index d6b88fd156..66b972f37f 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -312,7 +312,7 @@ fn hide_untracked() { // Git expects "no|normal|all" here; "off" can error on some versions and break `git status`. config.set_str("status.showUntrackedFiles", "no").unwrap(); - ctx.update(&mut app, keys("g")); + ctx.update(&mut app, keys("gr")); insta::assert_snapshot!(ctx.redact_buffer()); } @@ -366,7 +366,7 @@ fn updated_externally() { fs::write(ctx.dir.join("a"), "test\n").unwrap(); - ctx.update(&mut app, keys("g")); + ctx.update(&mut app, keys("gr")); insta::assert_snapshot!(ctx.redact_buffer()); } @@ -443,7 +443,7 @@ fn crlf_diff() { commit(&ctx.dir, "crlf.txt", "unchanged\r\nunchanged\r\n"); fs::write(ctx.dir.join("crlf.txt"), "unchanged\r\nchanged\r\n").unwrap(); - ctx.update(&mut app, keys("g")); + ctx.update(&mut app, keys("gr")); insta::assert_snapshot!(ctx.redact_buffer()); } @@ -455,7 +455,7 @@ fn tab_diff() { commit(&ctx.dir, "tab.txt", "this has no tab prefixed\n"); fs::write(ctx.dir.join("tab.txt"), "\tthis has a tab prefixed\n").unwrap(); - ctx.update(&mut app, keys("g")); + ctx.update(&mut app, keys("gr")); insta::assert_snapshot!(ctx.redact_buffer()); } @@ -471,7 +471,7 @@ fn non_utf8_diff() { b"File with invalid UTF-8: \xff\xfe\n", ) .unwrap(); - ctx.update(&mut app, keys("g")); + ctx.update(&mut app, keys("gr")); insta::assert_snapshot!(ctx.redact_buffer()); } @@ -486,7 +486,7 @@ fn ext_diff() { run(&ctx.dir, &["git", "add", "-N", "unstaged.txt"]); run(&ctx.dir, &["git", "add", "staged.txt"]); run(&ctx.dir, &["git", "config", "diff.external", "/dev/null"]); - ctx.update(&mut app, keys("g")); + ctx.update(&mut app, keys("gr")); insta::assert_snapshot!(ctx.redact_buffer()); } diff --git a/src/tests/snapshots/gitu__tests__help_menu.snap b/src/tests/snapshots/gitu__tests__help_menu.snap index 0fc7b803aa..5c5364c37b 100644 --- a/src/tests/snapshots/gitu__tests__help_menu.snap +++ b/src/tests/snapshots/gitu__tests__help_menu.snap @@ -1,5 +1,6 @@ --- source: src/tests/mod.rs +assertion_line: 50 expression: ctx.redact_buffer() --- ▌On branch main | @@ -16,10 +17,10 @@ expression: ctx.redact_buffer() 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 | - A Cherry-pick | - z Stash | -styles_hash: f023ed50da817507 + g+g Top P Push | + G Bottom r Rebase | + ctrl+u Half page up X Reset | + ctrl+d Half page down V Revert | + g+r Refresh A Cherry-pick | + q/esc Quit/Close z Stash | +styles_hash: d3bd90efdd7b7678 diff --git a/src/ui/layout/mod.rs b/src/ui/layout/mod.rs index 5d47f61216..4438a9372f 100644 --- a/src/ui/layout/mod.rs +++ b/src/ui/layout/mod.rs @@ -665,7 +665,7 @@ mod tests { layout.text("/ Parent section"); layout.text(" Half page up"); layout.text(" Half page down"); - layout.text("g Refresh"); + layout.text("g+r Refresh"); layout.text("q/ Quit/Close"); }); layout.vertical(None, OPTS, |layout| { diff --git a/src/ui/layout/snapshots/gitu__ui__layout__tests__gitu_mockup.snap b/src/ui/layout/snapshots/gitu__ui__layout__tests__gitu_mockup.snap index 3fd6f3cf24..876b157cf8 100644 --- a/src/ui/layout/snapshots/gitu__ui__layout__tests__gitu_mockup.snap +++ b/src/ui/layout/snapshots/gitu__ui__layout__tests__gitu_mockup.snap @@ -25,5 +25,5 @@ j/ Down h/? Help K Discard / Parent section r Rebase Half page up X Reset Half page down V Revert -g Refresh z Stash +g+r Refresh z Stash q/ Quit/Close