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'
|
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'
|
||||||
|
@ -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,
|
||||||
|
@ -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{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
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