1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-02-03 13:21:56 +02:00

Jump to middle of the view when selection leaves the visible area (#2915)

This commit is contained in:
Stefan Haller 2023-08-15 11:49:20 +02:00 committed by GitHub
commit 7402be98b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 349 additions and 40 deletions

View File

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

2
go.mod
View File

@ -15,7 +15,7 @@ require (
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d
github.com/jesseduffield/gocui v0.3.1-0.20230807090044-83a7161c8727
github.com/jesseduffield/gocui v0.3.1-0.20230815093813-9f3df4a6da3b
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e

4
go.sum
View File

@ -179,8 +179,8 @@ github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8T
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d h1:bO+OmbreIv91rCe8NmscRwhFSqkDJtzWCPV4Y+SQuXE=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
github.com/jesseduffield/gocui v0.3.1-0.20230807090044-83a7161c8727 h1:cLq698s96uDMm0n5379doAjIKoip3/8ioWIM8pySRLY=
github.com/jesseduffield/gocui v0.3.1-0.20230807090044-83a7161c8727/go.mod h1:trXE7RRGL2hTsv+Ntk+SHLtRobg9JE138n3Ug/X2Cf4=
github.com/jesseduffield/gocui v0.3.1-0.20230815093813-9f3df4a6da3b h1:D2Qgpvo+i7bIIBbi/UtzrpyTuUj020lJeGxMa0J1jGs=
github.com/jesseduffield/gocui v0.3.1-0.20230815093813-9f3df4a6da3b/go.mod h1:trXE7RRGL2hTsv+Ntk+SHLtRobg9JE138n3Ug/X2Cf4=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo=
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=

View File

@ -149,3 +149,8 @@ func (self *Patch) LineCount() int {
}
return count
}
// Returns the number of hunks of the patch
func (self *Patch) HunkCount() int {
return len(self.hunks)
}

View File

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

View File

@ -109,8 +109,9 @@ func (self *PatchExplorerContext) FocusSelection() {
_, viewHeight := view.Size()
bufferHeight := viewHeight - 1
_, origin := view.Origin()
numLines := view.LinesHeight()
newOriginY := state.CalculateOrigin(origin, bufferHeight)
newOriginY := state.CalculateOrigin(origin, bufferHeight, numLines)
_ = view.SetOriginY(newOriginY)

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
// 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{})
}

View File

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

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

View File

@ -2,21 +2,19 @@ package patch_exploring
import "github.com/jesseduffield/lazygit/pkg/utils"
func calculateOrigin(currentOrigin int, bufferHeight int, firstLineIdx int, lastLineIdx int, selectedLineIdx int, mode selectMode) int {
func calculateOrigin(currentOrigin int, bufferHeight int, numLines int, firstLineIdx int, lastLineIdx int, selectedLineIdx int, mode selectMode) int {
needToSeeIdx, wantToSeeIdx := getNeedAndWantLineIdx(firstLineIdx, lastLineIdx, selectedLineIdx, mode)
return calculateNewOriginWithNeededAndWantedIdx(currentOrigin, bufferHeight, needToSeeIdx, wantToSeeIdx)
return calculateNewOriginWithNeededAndWantedIdx(currentOrigin, bufferHeight, numLines, needToSeeIdx, wantToSeeIdx)
}
// we want to scroll our origin so that the index we need to see is in view
// and the other index we want to see (e.g. the other side of a line range)
// is in as close to being in view as possible.
func calculateNewOriginWithNeededAndWantedIdx(currentOrigin int, bufferHeight int, needToSeeIdx int, wantToSeeIdx int) int {
// is as close to being in view as possible.
func calculateNewOriginWithNeededAndWantedIdx(currentOrigin int, bufferHeight int, numLines int, needToSeeIdx int, wantToSeeIdx int) int {
origin := currentOrigin
if needToSeeIdx < currentOrigin {
origin = needToSeeIdx
} else if needToSeeIdx > currentOrigin+bufferHeight {
origin = needToSeeIdx - bufferHeight
if needToSeeIdx < currentOrigin || needToSeeIdx > currentOrigin+bufferHeight {
origin = utils.Max(utils.Min(needToSeeIdx-bufferHeight/2, numLines-bufferHeight-1), 0)
}
bottom := origin + bufferHeight

View File

@ -11,6 +11,7 @@ func TestNewOrigin(t *testing.T) {
name string
origin int
bufferHeight int
numLines int
firstLineIdx int
lastLineIdx int
selectedLineIdx int
@ -20,29 +21,54 @@ func TestNewOrigin(t *testing.T) {
scenarios := []scenario{
{
name: "selection above scroll window",
name: "selection above scroll window, enough room to put it in the middle",
origin: 250,
bufferHeight: 100,
numLines: 500,
firstLineIdx: 210,
lastLineIdx: 210,
selectedLineIdx: 210,
selectMode: LINE,
expected: 160,
},
{
name: "selection above scroll window, not enough room to put it in the middle",
origin: 50,
bufferHeight: 100,
numLines: 500,
firstLineIdx: 10,
lastLineIdx: 10,
selectedLineIdx: 10,
selectMode: LINE,
expected: 10,
expected: 0,
},
{
name: "selection below scroll window",
name: "selection below scroll window, enough room to put it in the middle",
origin: 0,
bufferHeight: 100,
numLines: 500,
firstLineIdx: 150,
lastLineIdx: 150,
selectedLineIdx: 150,
selectMode: LINE,
expected: 50,
expected: 100,
},
{
name: "selection below scroll window, not enough room to put it in the middle",
origin: 0,
bufferHeight: 100,
numLines: 200,
firstLineIdx: 199,
lastLineIdx: 199,
selectedLineIdx: 199,
selectMode: LINE,
expected: 99,
},
{
name: "selection within scroll window",
origin: 0,
bufferHeight: 100,
numLines: 500,
firstLineIdx: 50,
lastLineIdx: 50,
selectedLineIdx: 50,
@ -53,6 +79,7 @@ func TestNewOrigin(t *testing.T) {
name: "range ending below scroll window with selection at end of range",
origin: 0,
bufferHeight: 100,
numLines: 500,
firstLineIdx: 40,
lastLineIdx: 150,
selectedLineIdx: 150,
@ -63,6 +90,7 @@ func TestNewOrigin(t *testing.T) {
name: "range ending below scroll window with selection at beginning of range",
origin: 0,
bufferHeight: 100,
numLines: 500,
firstLineIdx: 40,
lastLineIdx: 150,
selectedLineIdx: 40,
@ -73,6 +101,7 @@ func TestNewOrigin(t *testing.T) {
name: "range starting above scroll window with selection at beginning of range",
origin: 50,
bufferHeight: 100,
numLines: 500,
firstLineIdx: 40,
lastLineIdx: 150,
selectedLineIdx: 40,
@ -83,6 +112,7 @@ func TestNewOrigin(t *testing.T) {
name: "hunk extending beyond both bounds of scroll window",
origin: 50,
bufferHeight: 100,
numLines: 500,
firstLineIdx: 40,
lastLineIdx: 200,
selectedLineIdx: 70,
@ -94,7 +124,7 @@ func TestNewOrigin(t *testing.T) {
for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
assert.EqualValues(t, s.expected, calculateOrigin(s.origin, s.bufferHeight, s.firstLineIdx, s.lastLineIdx, s.selectedLineIdx, s.selectMode))
assert.EqualValues(t, s.expected, calculateOrigin(s.origin, s.bufferHeight, s.numLines, s.firstLineIdx, s.lastLineIdx, s.selectedLineIdx, s.selectMode))
})
}
}

View File

@ -143,8 +143,13 @@ func (s *State) CycleHunk(forward bool) {
}
hunkIdx := s.patch.HunkContainingLine(s.selectedLineIdx)
start := s.patch.HunkStartIdx(hunkIdx + change)
s.selectedLineIdx = s.patch.GetNextChangeIdx(start)
if hunkIdx != -1 {
newHunkIdx := hunkIdx + change
if newHunkIdx >= 0 && newHunkIdx < s.patch.HunkCount() {
start := s.patch.HunkStartIdx(newHunkIdx)
s.selectedLineIdx = s.patch.GetNextChangeIdx(start)
}
}
}
func (s *State) CycleLine(forward bool) {
@ -216,8 +221,8 @@ func (s *State) SelectTop() {
s.SelectLine(0)
}
func (s *State) CalculateOrigin(currentOrigin int, bufferHeight int) int {
func (s *State) CalculateOrigin(currentOrigin int, bufferHeight int, numLines int) int {
firstLineIdx, lastLineIdx := s.SelectedRange()
return calculateOrigin(currentOrigin, bufferHeight, firstLineIdx, lastLineIdx, s.GetSelectedLineIdx(), s.selectMode)
return calculateOrigin(currentOrigin, bufferHeight, numLines, firstLineIdx, lastLineIdx, s.GetSelectedLineIdx(), s.selectMode)
}

View File

@ -273,25 +273,33 @@ func (v *View) FocusPoint(cx int, cy int) {
ly = 0
}
// if line is above origin, move origin and set cursor to zero
// if line is below origin + height, move origin and set cursor to max
// otherwise set cursor to value - origin
if ly > lineCount {
v.cx = cx
v.cy = cy
v.oy = 0
} else if cy < v.oy {
v.cx = cx
v.cy = 0
v.oy = cy
} else if cy > v.oy+ly {
v.cx = cx
v.cy = ly
v.oy = cy - ly
} else {
v.cx = cx
v.cy = cy - v.oy
v.oy = calculateNewOrigin(cy, v.oy, lineCount, ly)
v.cx = cx
v.cy = cy - v.oy
}
func calculateNewOrigin(selectedLine int, oldOrigin int, lineCount int, viewHeight int) int {
if viewHeight > lineCount {
return 0
} else if selectedLine < oldOrigin || selectedLine > oldOrigin+viewHeight {
// If the selected line is outside the visible area, scroll the view so
// that the selected line is in the middle.
newOrigin := selectedLine - viewHeight/2
// However, take care not to overflow if the total line count is less
// than the view height.
maxOrigin := lineCount - viewHeight - 1
if newOrigin > maxOrigin {
newOrigin = maxOrigin
}
if newOrigin < 0 {
newOrigin = 0
}
return newOrigin
}
return oldOrigin
}
func (s *searcher) search(str string) {

2
vendor/modules.txt vendored
View File

@ -124,7 +124,7 @@ github.com/jesseduffield/go-git/v5/utils/merkletrie/filesystem
github.com/jesseduffield/go-git/v5/utils/merkletrie/index
github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame
github.com/jesseduffield/go-git/v5/utils/merkletrie/noder
# github.com/jesseduffield/gocui v0.3.1-0.20230807090044-83a7161c8727
# github.com/jesseduffield/gocui v0.3.1-0.20230815093813-9f3df4a6da3b
## explicit; go 1.12
github.com/jesseduffield/gocui
# github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10