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:
@ -35,6 +35,7 @@ gui:
|
||||
windowSize: 'normal' # one of 'normal' | 'half' | 'full' default is 'normal'
|
||||
scrollHeight: 2 # how many lines you scroll by
|
||||
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
|
||||
expandFocusedSidePanel: false
|
||||
mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical'
|
||||
|
@ -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,
|
||||
|
@ -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{})
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
70
pkg/gui/controllers/scroll_off_margin.go
Normal file
70
pkg/gui/controllers/scroll_off_margin.go
Normal 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
|
||||
}
|
171
pkg/gui/controllers/scroll_off_margin_test.go
Normal file
171
pkg/gui/controllers/scroll_off_margin_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user