From ebdfd8046add89c1a5c978cee55d4355b5db417b Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 15 Aug 2023 11:39:36 +0200 Subject: [PATCH 1/5] Bump gocui --- go.mod | 2 +- go.sum | 4 +- vendor/github.com/jesseduffield/gocui/view.go | 44 +++++++++++-------- vendor/modules.txt | 2 +- 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/go.mod b/go.mod index 249fe4d46..5ec732e59 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 643db264b..5664ea2a8 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/vendor/github.com/jesseduffield/gocui/view.go b/vendor/github.com/jesseduffield/gocui/view.go index 939ceaa29..5c6fed903 100644 --- a/vendor/github.com/jesseduffield/gocui/view.go +++ b/vendor/github.com/jesseduffield/gocui/view.go @@ -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) { diff --git a/vendor/modules.txt b/vendor/modules.txt index 9561f09eb..539b8a488 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -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 From 4a4afc463994658f719ba5a09fb41fae06207bdd Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 12 Mar 2023 17:16:54 +0100 Subject: [PATCH 2/5] Fix typo in comment --- pkg/gui/patch_exploring/focus.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/gui/patch_exploring/focus.go b/pkg/gui/patch_exploring/focus.go index cf0908999..c3846235b 100644 --- a/pkg/gui/patch_exploring/focus.go +++ b/pkg/gui/patch_exploring/focus.go @@ -10,7 +10,7 @@ func calculateOrigin(currentOrigin int, bufferHeight int, firstLineIdx int, last // 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. +// is as close to being in view as possible. func calculateNewOriginWithNeededAndWantedIdx(currentOrigin int, bufferHeight int, needToSeeIdx int, wantToSeeIdx int) int { origin := currentOrigin if needToSeeIdx < currentOrigin { From 79c11a045886426482e473cc860b7fa7e4dbd1e6 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 12 Mar 2023 17:17:31 +0100 Subject: [PATCH 3/5] If selected line is outside, move it to the middle of the view Previously, the current line was only moved as much as necessary so that it's in view again. This had the problem that when jumping downwards from hunk to hunk with the right-arrow key, only the first line of the new hunk was shown at the bottom of the window. I prefer to put the selected line in the middle of the view in this case, so that I can see more of the newly selected hunk. This has the consequence that when scrolling through the view line by line using down-arrow, the view jumps by half a screen whenever I reach the bottom. I can see how some users might be opposed to this change, but I happen to like it too, because it allows me to see more context of what's ahead. --- pkg/gui/context/patch_explorer_context.go | 3 +- pkg/gui/patch_exploring/focus.go | 12 +++---- pkg/gui/patch_exploring/focus_test.go | 40 ++++++++++++++++++++--- pkg/gui/patch_exploring/state.go | 4 +-- 4 files changed, 44 insertions(+), 15 deletions(-) diff --git a/pkg/gui/context/patch_explorer_context.go b/pkg/gui/context/patch_explorer_context.go index 17ecae4ae..54a9356ac 100644 --- a/pkg/gui/context/patch_explorer_context.go +++ b/pkg/gui/context/patch_explorer_context.go @@ -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) diff --git a/pkg/gui/patch_exploring/focus.go b/pkg/gui/patch_exploring/focus.go index c3846235b..02fed7b28 100644 --- a/pkg/gui/patch_exploring/focus.go +++ b/pkg/gui/patch_exploring/focus.go @@ -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 as close to being in view as possible. -func calculateNewOriginWithNeededAndWantedIdx(currentOrigin int, bufferHeight int, needToSeeIdx int, wantToSeeIdx int) int { +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 diff --git a/pkg/gui/patch_exploring/focus_test.go b/pkg/gui/patch_exploring/focus_test.go index eb3ed7c66..bbe4b2341 100644 --- a/pkg/gui/patch_exploring/focus_test.go +++ b/pkg/gui/patch_exploring/focus_test.go @@ -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)) }) } } diff --git a/pkg/gui/patch_exploring/state.go b/pkg/gui/patch_exploring/state.go index ad5426805..356bff4f4 100644 --- a/pkg/gui/patch_exploring/state.go +++ b/pkg/gui/patch_exploring/state.go @@ -216,8 +216,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) } From 8f164f7bc536ca711e531d49f96348fbae54b860 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 12 Mar 2023 18:31:45 +0100 Subject: [PATCH 4/5] Stop cycling hunks when reaching the end Previously, when pressing right-arrow when the cursor is already in the last hunk, it would jump back to the beginning of that hunk. This can be confusing if the hunk is long, maybe the start of the hunk is already scrolled off the top of the window, and then pressing right-arrow actually scrolls *backwards*, which is counter-intuitive. It's better to do nothing in this case. Same for left-arrow when the cursor is already in the first hunk, although here the problem is not so severe (unless diff context was increased by a huge amount, and the start of the first hunk is scrolled off the bottom of the window). --- pkg/commands/patch/patch.go | 5 +++++ pkg/gui/patch_exploring/state.go | 9 +++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pkg/commands/patch/patch.go b/pkg/commands/patch/patch.go index 55dd8f80d..049334727 100644 --- a/pkg/commands/patch/patch.go +++ b/pkg/commands/patch/patch.go @@ -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) +} diff --git a/pkg/gui/patch_exploring/state.go b/pkg/gui/patch_exploring/state.go index 356bff4f4..1c82d59cb 100644 --- a/pkg/gui/patch_exploring/state.go +++ b/pkg/gui/patch_exploring/state.go @@ -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) { From 341b9725d4a91471ea4e5e9c515572abd9e66821 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Wed, 9 Aug 2023 18:34:43 +0200 Subject: [PATCH 5/5] 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. --- docs/Config.md | 1 + pkg/config/user_config.go | 2 + pkg/gui/controllers/list_controller.go | 6 + .../controllers/patch_explorer_controller.go | 12 ++ pkg/gui/controllers/scroll_off_margin.go | 70 +++++++ pkg/gui/controllers/scroll_off_margin_test.go | 171 ++++++++++++++++++ 6 files changed, 262 insertions(+) create mode 100644 pkg/gui/controllers/scroll_off_margin.go create mode 100644 pkg/gui/controllers/scroll_off_margin_test.go diff --git a/docs/Config.md b/docs/Config.md index 650c3c969..e8e31e3d9 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -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' 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) + }) + } +}