1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-07-31 14:24:25 +03:00

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.
This commit is contained in:
Stefan Haller
2023-08-09 18:34:43 +02:00
parent 8f164f7bc5
commit 341b9725d4
6 changed files with 262 additions and 0 deletions

View File

@ -35,6 +35,7 @@ gui:
windowSize: 'normal' # one of 'normal' | 'half' | 'full' default is 'normal' windowSize: 'normal' # one of 'normal' | 'half' | 'full' default is 'normal'
scrollHeight: 2 # how many lines you scroll by scrollHeight: 2 # how many lines you scroll by
scrollPastBottom: true # enable scrolling past the bottom scrollPastBottom: true # enable scrolling past the bottom
scrollOffMargin: 2 # how many lines to keep before/after the cursor when it reaches the top/bottom of the view
sidePanelWidth: 0.3333 # number from 0 to 1 sidePanelWidth: 0.3333 # number from 0 to 1
expandFocusedSidePanel: false expandFocusedSidePanel: false
mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical' mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical'

View File

@ -31,6 +31,7 @@ type GuiConfig struct {
BranchColors map[string]string `yaml:"branchColors"` BranchColors map[string]string `yaml:"branchColors"`
ScrollHeight int `yaml:"scrollHeight"` ScrollHeight int `yaml:"scrollHeight"`
ScrollPastBottom bool `yaml:"scrollPastBottom"` ScrollPastBottom bool `yaml:"scrollPastBottom"`
ScrollOffMargin int `yaml:"scrollOffMargin"`
MouseEvents bool `yaml:"mouseEvents"` MouseEvents bool `yaml:"mouseEvents"`
SkipDiscardChangeWarning bool `yaml:"skipDiscardChangeWarning"` SkipDiscardChangeWarning bool `yaml:"skipDiscardChangeWarning"`
SkipStashWarning bool `yaml:"skipStashWarning"` SkipStashWarning bool `yaml:"skipStashWarning"`
@ -418,6 +419,7 @@ func GetDefaultConfig() *UserConfig {
Gui: GuiConfig{ Gui: GuiConfig{
ScrollHeight: 2, ScrollHeight: 2,
ScrollPastBottom: true, ScrollPastBottom: true,
ScrollOffMargin: 2,
MouseEvents: true, MouseEvents: true,
SkipDiscardChangeWarning: false, SkipDiscardChangeWarning: false,
SkipStashWarning: false, SkipStashWarning: false,

View File

@ -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 // 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. // we're not constantly re-rendering the main view.
if before != after { 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{}) return self.context.HandleFocus(types.OnFocusOpts{})
} }

View File

@ -159,13 +159,25 @@ func (self *PatchExplorerController) GetMouseKeybindings(opts types.KeybindingsO
} }
func (self *PatchExplorerController) HandlePrevLine() error { func (self *PatchExplorerController) HandlePrevLine() error {
before := self.context.GetState().GetSelectedLineIdx()
self.context.GetState().CycleSelection(false) 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 return nil
} }
func (self *PatchExplorerController) HandleNextLine() error { func (self *PatchExplorerController) HandleNextLine() error {
before := self.context.GetState().GetSelectedLineIdx()
self.context.GetState().CycleSelection(true) 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 return nil
} }

View File

@ -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
}

View File

@ -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)
})
}
}