summaryrefslogtreecommitdiffstats
path: root/pkg
diff options
context:
space:
mode:
authorStefan Haller <stefan@haller-berlin.de>2023-08-09 18:34:43 +0200
committerStefan Haller <stefan@haller-berlin.de>2023-08-15 11:40:40 +0200
commit341b9725d4a91471ea4e5e9c515572abd9e66821 (patch)
treef3de36d9b7acf9d3fb6657f3a2f57195f1870b98 /pkg
parent8f164f7bc536ca711e531d49f96348fbae54b860 (diff)
Add ScrollOffMargin user config
When set to a non-zero value, views will scroll when the selection gets this close to the top or bottom of the view.
Diffstat (limited to 'pkg')
-rw-r--r--pkg/config/user_config.go2
-rw-r--r--pkg/gui/controllers/list_controller.go6
-rw-r--r--pkg/gui/controllers/patch_explorer_controller.go12
-rw-r--r--pkg/gui/controllers/scroll_off_margin.go70
-rw-r--r--pkg/gui/controllers/scroll_off_margin_test.go171
5 files changed, 261 insertions, 0 deletions
diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go
index 9eb8e028a..a217d8099 100644
--- a/pkg/config/user_config.go
+++ b/pkg/config/user_config.go
@@ -31,6 +31,7 @@ type GuiConfig struct {
BranchColors map[string]string `yaml:"branchColors"`
ScrollHeight int `yaml:"scrollHeight"`
ScrollPastBottom bool `yaml:"scrollPastBottom"`
+ ScrollOffMargin int `yaml:"scrollOffMargin"`
MouseEvents bool `yaml:"mouseEvents"`
SkipDiscardChangeWarning bool `yaml:"skipDiscardChangeWarning"`
SkipStashWarning bool `yaml:"skipStashWarning"`
@@ -418,6 +419,7 @@ func GetDefaultConfig() *UserConfig {
Gui: GuiConfig{
ScrollHeight: 2,
ScrollPastBottom: true,
+ ScrollOffMargin: 2,
MouseEvents: true,
SkipDiscardChangeWarning: false,
SkipStashWarning: false,
diff --git a/pkg/gui/controllers/list_controller.go b/pkg/gui/controllers/list_controller.go
index fb6d8736a..cdaea413a 100644
--- a/pkg/gui/controllers/list_controller.go
+++ b/pkg/gui/controllers/list_controller.go
@@ -82,6 +82,12 @@ func (self *ListController) handleLineChange(change int) error {
// doing this check so that if we're holding the up key at the start of the list
// we're not constantly re-rendering the main view.
if before != after {
+ if change == -1 {
+ checkScrollUp(self.context.GetViewTrait(), self.c.UserConfig.Gui.ScrollOffMargin, before, after)
+ } else if change == 1 {
+ checkScrollDown(self.context.GetViewTrait(), self.c.UserConfig.Gui.ScrollOffMargin, before, after)
+ }
+
return self.context.HandleFocus(types.OnFocusOpts{})
}
diff --git a/pkg/gui/controllers/patch_explorer_controller.go b/pkg/gui/controllers/patch_explorer_controller.go
index dd19d08db..83c8633a7 100644
--- a/pkg/gui/controllers/patch_explorer_controller.go
+++ b/pkg/gui/controllers/patch_explorer_controller.go
@@ -159,13 +159,25 @@ func (self *PatchExplorerController) GetMouseKeybindings(opts types.KeybindingsO
}
func (self *PatchExplorerController) HandlePrevLine() error {
+ before := self.context.GetState().GetSelectedLineIdx()
self.context.GetState().CycleSelection(false)
+ after := self.context.GetState().GetSelectedLineIdx()
+
+ if self.context.GetState().SelectingLine() {
+ checkScrollUp(self.context.GetViewTrait(), self.c.UserConfig.Gui.ScrollOffMargin, before, after)
+ }
return nil
}
func (self *PatchExplorerController) HandleNextLine() error {
+ before := self.context.GetState().GetSelectedLineIdx()
self.context.GetState().CycleSelection(true)
+ after := self.context.GetState().GetSelectedLineIdx()
+
+ if self.context.GetState().SelectingLine() {
+ checkScrollDown(self.context.GetViewTrait(), self.c.UserConfig.Gui.ScrollOffMargin, before, after)
+ }
return nil
}
diff --git a/pkg/gui/controllers/scroll_off_margin.go b/pkg/gui/controllers/scroll_off_margin.go
new file mode 100644
index 000000000..119f30090
--- /dev/null
+++ b/pkg/gui/controllers/scroll_off_margin.go
@@ -0,0 +1,70 @@
+package controllers
+
+import (
+ "github.com/jesseduffield/lazygit/pkg/gui/types"
+ "github.com/jesseduffield/lazygit/pkg/utils"
+)
+
+// To be called after pressing up-arrow; checks whether the cursor entered the
+// top scroll-off margin, and so the view needs to be scrolled up one line
+func checkScrollUp(view types.IViewTrait, scrollOffMargin int, lineIdxBefore int, lineIdxAfter int) {
+ viewPortStart, viewPortHeight := view.ViewPortYBounds()
+
+ linesToScroll := calculateLinesToScrollUp(
+ viewPortStart, viewPortHeight, scrollOffMargin, lineIdxBefore, lineIdxAfter)
+ if linesToScroll != 0 {
+ view.ScrollUp(linesToScroll)
+ }
+}
+
+// To be called after pressing down-arrow; checks whether the cursor entered the
+// bottom scroll-off margin, and so the view needs to be scrolled down one line
+func checkScrollDown(view types.IViewTrait, scrollOffMargin int, lineIdxBefore int, lineIdxAfter int) {
+ viewPortStart, viewPortHeight := view.ViewPortYBounds()
+
+ linesToScroll := calculateLinesToScrollDown(
+ viewPortStart, viewPortHeight, scrollOffMargin, lineIdxBefore, lineIdxAfter)
+ if linesToScroll != 0 {
+ view.ScrollDown(linesToScroll)
+ }
+}
+
+func calculateLinesToScrollUp(viewPortStart int, viewPortHeight int, scrollOffMargin int, lineIdxBefore int, lineIdxAfter int) int {
+ // Cap the margin to half the view height. This allows setting the config to
+ // a very large value to keep the cursor always in the middle of the screen.
+ // Use +.5 so that if the height is even, the top margin is one line higher
+ // than the bottom margin.
+ scrollOffMargin = utils.Min(scrollOffMargin, int((float64(viewPortHeight)+.5)/2))
+
+ // Scroll only if the "before" position was visible (this could be false if
+ // the scroll wheel was used to scroll the selected line out of view) ...
+ if lineIdxBefore >= viewPortStart && lineIdxBefore < viewPortStart+viewPortHeight {
+ marginEnd := viewPortStart + scrollOffMargin
+ // ... and the "after" position is within the top margin (or before it)
+ if lineIdxAfter < marginEnd {
+ return marginEnd - lineIdxAfter
+ }
+ }
+
+ return 0
+}
+
+func calculateLinesToScrollDown(viewPortStart int, viewPortHeight int, scrollOffMargin int, lineIdxBefore int, lineIdxAfter int) int {
+ // Cap the margin to half the view height. This allows setting the config to
+ // a very large value to keep the cursor always in the middle of the screen.
+ // Use -.5 so that if the height is even, the bottom margin is one line lower
+ // than the top margin.
+ scrollOffMargin = utils.Min(scrollOffMargin, int((float64(viewPortHeight)-.5)/2))
+
+ // Scroll only if the "before" position was visible (this could be false if
+ // the scroll wheel was used to scroll the selected line out of view) ...
+ if lineIdxBefore >= viewPortStart && lineIdxBefore < viewPortStart+viewPortHeight {
+ marginStart := viewPortStart + viewPortHeight - scrollOffMargin - 1
+ // ... and the "after" position is within the bottom margin (or after it)
+ if lineIdxAfter > marginStart {
+ return lineIdxAfter - marginStart
+ }
+ }
+
+ return 0
+}
diff --git a/pkg/gui/controllers/scroll_off_margin_test.go b/pkg/gui/controllers/scroll_off_margin_test.go
new file mode 100644
index 000000000..099da299f
--- /dev/null
+++ b/pkg/gui/controllers/scroll_off_margin_test.go
@@ -0,0 +1,171 @@
+package controllers
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func Test_calculateLinesToScrollUp(t *testing.T) {
+ scenarios := []struct {
+ name string
+ viewPortStart int
+ viewPortHeight int
+ scrollOffMargin int
+ lineIdxBefore int
+ lineIdxAfter int
+ expectedLinesToScroll int
+ }{
+ {
+ name: "before position is above viewport - don't scroll",
+ viewPortStart: 10,
+ viewPortHeight: 10,
+ scrollOffMargin: 3,
+ lineIdxBefore: 9,
+ lineIdxAfter: 8,
+ expectedLinesToScroll: 0,
+ },
+ {
+ name: "before position is below viewport - don't scroll",
+ viewPortStart: 10,
+ viewPortHeight: 10,
+ scrollOffMargin: 3,
+ lineIdxBefore: 20,
+ lineIdxAfter: 19,
+ expectedLinesToScroll: 0,
+ },
+ {
+ name: "before and after positions are outside scroll-off margin - don't scroll",
+ viewPortStart: 10,
+ viewPortHeight: 10,
+ scrollOffMargin: 3,
+ lineIdxBefore: 14,
+ lineIdxAfter: 13,
+ expectedLinesToScroll: 0,
+ },
+ {
+ name: "before outside, after inside scroll-off margin - scroll by 1",
+ viewPortStart: 10,
+ viewPortHeight: 10,
+ scrollOffMargin: 3,
+ lineIdxBefore: 13,
+ lineIdxAfter: 12,
+ expectedLinesToScroll: 1,
+ },
+ {
+ name: "before inside scroll-off margin - scroll by more than 1",
+ viewPortStart: 10,
+ viewPortHeight: 10,
+ scrollOffMargin: 3,
+ lineIdxBefore: 11,
+ lineIdxAfter: 10,
+ expectedLinesToScroll: 3,
+ },
+ {
+ name: "very large scroll-off margin - keep view centered (even viewport height)",
+ viewPortStart: 10,
+ viewPortHeight: 10,
+ scrollOffMargin: 999,
+ lineIdxBefore: 15,
+ lineIdxAfter: 14,
+ expectedLinesToScroll: 1,
+ },
+ {
+ name: "very large scroll-off margin - keep view centered (odd viewport height)",
+ viewPortStart: 10,
+ viewPortHeight: 9,
+ scrollOffMargin: 999,
+ lineIdxBefore: 14,
+ lineIdxAfter: 13,
+ expectedLinesToScroll: 1,
+ },
+ }
+ for _, scenario := range scenarios {
+ t.Run(scenario.name, func(t *testing.T) {
+ linesToScroll := calculateLinesToScrollUp(scenario.viewPortStart, scenario.viewPortHeight, scenario.scrollOffMargin, scenario.lineIdxBefore, scenario.lineIdxAfter)
+ assert.Equal(t, scenario.expectedLinesToScroll, linesToScroll)
+ })
+ }
+}
+
+func Test_calculateLinesToScrollDown(t *testing.T) {
+ scenarios := []struct {
+ name string
+ viewPortStart int
+ viewPortHeight int
+ scrollOffMargin int
+ lineIdxBefore int
+ lineIdxAfter int
+ expectedLinesToScroll int
+ }{
+ {
+ name: "before position is above viewport - don't scroll",
+ viewPortStart: 10,
+ viewPortHeight: 10,
+ scrollOffMargin: 3,
+ lineIdxBefore: 9,
+ lineIdxAfter: 10,
+ expectedLinesToScroll: 0,
+ },
+ {
+ name: "before position is below viewport - don't scroll",
+ viewPortStart: 10,
+ viewPortHeight: 10,
+ scrollOffMargin: 3,
+ lineIdxBefore: 20,
+ lineIdxAfter: 21,
+ expectedLinesToScroll: 0,
+ },
+ {
+ name: "before and after positions are outside scroll-off margin - don't scroll",
+ viewPortStart: 10,
+ viewPortHeight: 10,
+ scrollOffMargin: 3,
+ lineIdxBefore: 15,
+ lineIdxAfter: 16,
+ expectedLinesToScroll: 0,
+ },
+ {
+ name: "before outside, after inside scroll-off margin - scroll by 1",
+ viewPortStart: 10,
+ viewPortHeight: 10,
+ scrollOffMargin: 3,
+ lineIdxBefore: 16,
+ lineIdxAfter: 17,
+ expectedLinesToScroll: 1,
+ },
+ {
+ name: "before inside scroll-off margin - scroll by more than 1",
+ viewPortStart: 10,
+ viewPortHeight: 10,
+ scrollOffMargin: 3,
+ lineIdxBefore: 18,
+ lineIdxAfter: 19,
+ expectedLinesToScroll: 3,
+ },
+ {
+ name: "very large scroll-off margin - keep view centered (even viewport height)",
+ viewPortStart: 10,
+ viewPortHeight: 10,
+ scrollOffMargin: 999,
+ lineIdxBefore: 15,
+ lineIdxAfter: 16,
+ expectedLinesToScroll: 1,
+ },
+ {
+ name: "very large scroll-off margin - keep view centered (odd viewport height)",
+ viewPortStart: 10,
+ viewPortHeight: 9,
+ scrollOffMargin: 999,
+ lineIdxBefore: 14,
+ lineIdxAfter: 15,
+ expectedLinesToScroll: 1,
+ },
+ }
+ for _, scenario := range scenarios {
+ t.Run(scenario.name, func(t *testing.T) {
+ linesToScroll := calculateLinesToScrollDown(scenario.viewPortStart, scenario.viewPortHeight, scenario.scrollOffMargin, scenario.lineIdxBefore, scenario.lineIdxAfter)
+ assert.Equal(t, scenario.expectedLinesToScroll, linesToScroll)
+ })
+ }
+}