Skip to content

Commit 44160ef

Browse files
committed
Only render visible portion of the screen for commits view
1 parent dd2bffc commit 44160ef

File tree

9 files changed

+130
-62
lines changed

9 files changed

+130
-62
lines changed

pkg/gui/context/base_context.go

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ type BaseContext struct {
2020
onFocusFn onFocusFn
2121
onFocusLostFn onFocusLostFn
2222

23-
focusable bool
24-
transient bool
25-
hasControlledBounds bool
26-
needsRerenderOnWidthChange bool
27-
highlightOnFocus bool
23+
focusable bool
24+
transient bool
25+
hasControlledBounds bool
26+
needsRerenderOnWidthChange bool
27+
needsRerenderOnHeightChange bool
28+
highlightOnFocus bool
2829

2930
*ParentContextMgr
3031
}
@@ -37,15 +38,16 @@ type (
3738
var _ types.IBaseContext = &BaseContext{}
3839

3940
type NewBaseContextOpts struct {
40-
Kind types.ContextKind
41-
Key types.ContextKey
42-
View *gocui.View
43-
WindowName string
44-
Focusable bool
45-
Transient bool
46-
HasUncontrolledBounds bool // negating for the sake of making false the default
47-
HighlightOnFocus bool
48-
NeedsRerenderOnWidthChange bool
41+
Kind types.ContextKind
42+
Key types.ContextKey
43+
View *gocui.View
44+
WindowName string
45+
Focusable bool
46+
Transient bool
47+
HasUncontrolledBounds bool // negating for the sake of making false the default
48+
HighlightOnFocus bool
49+
NeedsRerenderOnWidthChange bool
50+
NeedsRerenderOnHeightChange bool
4951

5052
OnGetOptionsMap func() map[string]string
5153
}
@@ -56,18 +58,19 @@ func NewBaseContext(opts NewBaseContextOpts) *BaseContext {
5658
hasControlledBounds := !opts.HasUncontrolledBounds
5759

5860
return &BaseContext{
59-
kind: opts.Kind,
60-
key: opts.Key,
61-
view: opts.View,
62-
windowName: opts.WindowName,
63-
onGetOptionsMap: opts.OnGetOptionsMap,
64-
focusable: opts.Focusable,
65-
transient: opts.Transient,
66-
hasControlledBounds: hasControlledBounds,
67-
highlightOnFocus: opts.HighlightOnFocus,
68-
needsRerenderOnWidthChange: opts.NeedsRerenderOnWidthChange,
69-
ParentContextMgr: &ParentContextMgr{},
70-
viewTrait: viewTrait,
61+
kind: opts.Kind,
62+
key: opts.Key,
63+
view: opts.View,
64+
windowName: opts.WindowName,
65+
onGetOptionsMap: opts.OnGetOptionsMap,
66+
focusable: opts.Focusable,
67+
transient: opts.Transient,
68+
hasControlledBounds: hasControlledBounds,
69+
highlightOnFocus: opts.HighlightOnFocus,
70+
needsRerenderOnWidthChange: opts.NeedsRerenderOnWidthChange,
71+
needsRerenderOnHeightChange: opts.NeedsRerenderOnHeightChange,
72+
ParentContextMgr: &ParentContextMgr{},
73+
viewTrait: viewTrait,
7174
}
7275
}
7376

@@ -197,6 +200,10 @@ func (self *BaseContext) NeedsRerenderOnWidthChange() bool {
197200
return self.needsRerenderOnWidthChange
198201
}
199202

203+
func (self *BaseContext) NeedsRerenderOnHeightChange() bool {
204+
return self.needsRerenderOnHeightChange
205+
}
206+
200207
func (self *BaseContext) Title() string {
201208
return ""
202209
}

pkg/gui/context/list_context_trait.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ type ListContextTrait struct {
1818
// we should find out exactly which lines are now part of the path and refresh those.
1919
// We should also keep track of the previous path and refresh those lines too.
2020
refreshViewportOnChange bool
21+
// If this is true, we only render the visible lines of the list. Useful for lists that can
22+
// get very long, because it can save a lot of memory
23+
renderOnlyVisibleLines bool
2124
}
2225

2326
func (self *ListContextTrait) IsListContext() {}
@@ -28,6 +31,8 @@ func (self *ListContextTrait) FocusLine() {
2831
// the view could be squashed and won't how to adjust the cursor/origin.
2932
// Also, refreshing the viewport needs to happen after the view has been resized.
3033
self.c.AfterLayout(func() error {
34+
oldOrigin, _ := self.GetViewTrait().ViewPortYBounds()
35+
3136
self.GetViewTrait().FocusPoint(
3237
self.ModelIndexToViewIndex(self.list.GetSelectedLineIdx()))
3338

@@ -41,6 +46,11 @@ func (self *ListContextTrait) FocusLine() {
4146

4247
if self.refreshViewportOnChange {
4348
self.refreshViewport()
49+
} else if self.renderOnlyVisibleLines {
50+
newOrigin, _ := self.GetViewTrait().ViewPortYBounds()
51+
if oldOrigin != newOrigin {
52+
return self.HandleRender()
53+
}
4454
}
4555
return nil
4656
})
@@ -83,8 +93,21 @@ func (self *ListContextTrait) HandleFocusLost(opts types.OnFocusLostOpts) error
8393
// OnFocus assumes that the content of the context has already been rendered to the view. OnRender is the function which actually renders the content to the view
8494
func (self *ListContextTrait) HandleRender() error {
8595
self.list.ClampSelection()
86-
content := self.renderLines(-1, -1)
87-
self.GetViewTrait().SetContent(content)
96+
if self.renderOnlyVisibleLines {
97+
// Rendering only the visible area can save a lot of cell memory for
98+
// those views that support it.
99+
totalLength := self.list.Len()
100+
if self.getNonModelItems != nil {
101+
totalLength += len(self.getNonModelItems())
102+
}
103+
self.GetViewTrait().SetContentLineCount(totalLength)
104+
startIdx, length := self.GetViewTrait().ViewPortYBounds()
105+
content := self.renderLines(startIdx, startIdx+length)
106+
self.GetViewTrait().SetViewPortContentAndClearEverythingElse(content)
107+
} else {
108+
content := self.renderLines(-1, -1)
109+
self.GetViewTrait().SetContent(content)
110+
}
88111
self.c.Render()
89112
self.setFooter()
90113

pkg/gui/context/local_commits_context.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,19 +72,21 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
7272
SearchTrait: NewSearchTrait(c),
7373
ListContextTrait: &ListContextTrait{
7474
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
75-
View: c.Views().Commits,
76-
WindowName: "commits",
77-
Key: LOCAL_COMMITS_CONTEXT_KEY,
78-
Kind: types.SIDE_CONTEXT,
79-
Focusable: true,
80-
NeedsRerenderOnWidthChange: true,
75+
View: c.Views().Commits,
76+
WindowName: "commits",
77+
Key: LOCAL_COMMITS_CONTEXT_KEY,
78+
Kind: types.SIDE_CONTEXT,
79+
Focusable: true,
80+
NeedsRerenderOnWidthChange: true,
81+
NeedsRerenderOnHeightChange: true,
8182
})),
8283
ListRenderer: ListRenderer{
8384
list: viewModel,
8485
getDisplayStrings: getDisplayStrings,
8586
},
8687
c: c,
8788
refreshViewportOnChange: true,
89+
renderOnlyVisibleLines: true,
8890
},
8991
}
9092

pkg/gui/context/remote_branches_context.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,14 @@ func NewRemoteBranchesContext(
3737
DynamicTitleBuilder: NewDynamicTitleBuilder(c.Tr.RemoteBranchesDynamicTitle),
3838
ListContextTrait: &ListContextTrait{
3939
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
40-
View: c.Views().RemoteBranches,
41-
WindowName: "branches",
42-
Key: REMOTE_BRANCHES_CONTEXT_KEY,
43-
Kind: types.SIDE_CONTEXT,
44-
Focusable: true,
45-
Transient: true,
46-
NeedsRerenderOnWidthChange: true,
40+
View: c.Views().RemoteBranches,
41+
WindowName: "branches",
42+
Key: REMOTE_BRANCHES_CONTEXT_KEY,
43+
Kind: types.SIDE_CONTEXT,
44+
Focusable: true,
45+
Transient: true,
46+
NeedsRerenderOnWidthChange: true,
47+
NeedsRerenderOnHeightChange: true,
4748
})),
4849
ListRenderer: ListRenderer{
4950
list: viewModel,

pkg/gui/context/sub_commits_context.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,14 @@ func NewSubCommitsContext(
115115
DynamicTitleBuilder: NewDynamicTitleBuilder(c.Tr.SubCommitsDynamicTitle),
116116
ListContextTrait: &ListContextTrait{
117117
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
118-
View: c.Views().SubCommits,
119-
WindowName: "branches",
120-
Key: SUB_COMMITS_CONTEXT_KEY,
121-
Kind: types.SIDE_CONTEXT,
122-
Focusable: true,
123-
Transient: true,
124-
NeedsRerenderOnWidthChange: true,
118+
View: c.Views().SubCommits,
119+
WindowName: "branches",
120+
Key: SUB_COMMITS_CONTEXT_KEY,
121+
Kind: types.SIDE_CONTEXT,
122+
Focusable: true,
123+
Transient: true,
124+
NeedsRerenderOnWidthChange: true,
125+
NeedsRerenderOnHeightChange: true,
125126
})),
126127
ListRenderer: ListRenderer{
127128
list: viewModel,
@@ -130,6 +131,7 @@ func NewSubCommitsContext(
130131
},
131132
c: c,
132133
refreshViewportOnChange: true,
134+
renderOnlyVisibleLines: true,
133135
},
134136
}
135137

pkg/gui/context/view_trait.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ func (self *ViewTrait) SetViewPortContent(content string) {
3434
self.view.OverwriteLines(y, content)
3535
}
3636

37+
func (self *ViewTrait) SetViewPortContentAndClearEverythingElse(content string) {
38+
_, y := self.view.Origin()
39+
self.view.OverwriteLinesAndClearEverythingElse(y, content)
40+
}
41+
42+
func (self *ViewTrait) SetContentLineCount(lineCount int) {
43+
self.view.SetContentLineCount(lineCount)
44+
}
45+
3746
func (self *ViewTrait) SetContent(content string) {
3847
self.view.SetContent(content)
3948
}

pkg/gui/layout.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,26 @@ func (gui *Gui) layout(g *gocui.Gui) error {
7272
frameOffset = 0
7373
}
7474

75+
mustRerender := false
7576
if context.NeedsRerenderOnWidthChange() {
7677
// view.Width() returns the width -1 for some reason
7778
oldWidth := view.Width() + 1
7879
newWidth := dimensionsObj.X1 - dimensionsObj.X0 + 2*frameOffset
7980
if oldWidth != newWidth {
80-
contextsToRerender = append(contextsToRerender, context)
81+
mustRerender = true
8182
}
8283
}
84+
if context.NeedsRerenderOnHeightChange() {
85+
// view.Height() returns the height -1 for some reason
86+
oldHeight := view.Height() + 1
87+
newHeight := dimensionsObj.Y1 - dimensionsObj.Y0 + 2*frameOffset
88+
if oldHeight != newHeight {
89+
mustRerender = true
90+
}
91+
}
92+
if mustRerender {
93+
contextsToRerender = append(contextsToRerender, context)
94+
}
8395

8496
_, err = g.SetView(
8597
viewName,

pkg/gui/types/context.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ type IBaseContext interface {
6363
// true if the view needs to be rerendered when its width changes
6464
NeedsRerenderOnWidthChange() bool
6565

66+
// true if the view needs to be rerendered when its height changes
67+
NeedsRerenderOnHeightChange() bool
68+
6669
// returns the desired title for the view upon activation. If there is no desired title (returns empty string), then
6770
// no title will be set
6871
Title() string
@@ -172,6 +175,8 @@ type IViewTrait interface {
172175
SetRangeSelectStart(yIdx int)
173176
CancelRangeSelect()
174177
SetViewPortContent(content string)
178+
SetViewPortContentAndClearEverythingElse(content string)
179+
SetContentLineCount(lineCount int)
175180
SetContent(content string)
176181
SetFooter(value string)
177182
SetOriginX(value int)

pkg/integration/components/view_driver.go

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,11 @@ func (self *ViewDriver) SelectPreviousItem() *ViewDriver {
431431
return self.PressFast(self.t.keys.Universal.PrevItem)
432432
}
433433

434+
// i.e. pressing '<'
435+
func (self *ViewDriver) GotoTop() *ViewDriver {
436+
return self.PressFast(self.t.keys.Universal.GotoTop)
437+
}
438+
434439
// i.e. pressing space
435440
func (self *ViewDriver) PressPrimaryAction() *ViewDriver {
436441
return self.Press(self.t.keys.Universal.Select)
@@ -457,21 +462,15 @@ func (self *ViewDriver) PressEscape() *ViewDriver {
457462
// - the user is not in a list item
458463
// - no list item is found containing the given text
459464
// - multiple list items are found containing the given text in the initial page of items
460-
//
461-
// NOTE: this currently assumes that BufferLines returns all the lines that can be accessed.
462-
// If this changes in future, we'll need to update this code to first attempt to find the item
463-
// in the current page and failing that, jump to the top of the view and iterate through all of it,
464-
// looking for the item.
465465
func (self *ViewDriver) NavigateToLine(matcher *TextMatcher) *ViewDriver {
466466
self.IsFocused()
467467

468468
view := self.getView()
469469
lines := view.BufferLines()
470470

471-
var matchIndex int
471+
matchIndex := -1
472472

473473
self.t.assertWithRetries(func() (bool, string) {
474-
matchIndex = -1
475474
var matches []string
476475
// first we look for a duplicate on the current screen. We won't bother looking beyond that though.
477476
for i, line := range lines {
@@ -483,13 +482,19 @@ func (self *ViewDriver) NavigateToLine(matcher *TextMatcher) *ViewDriver {
483482
}
484483
if len(matches) > 1 {
485484
return false, fmt.Sprintf("Found %d matches for `%s`, expected only a single match. Matching lines:\n%s", len(matches), matcher.name(), strings.Join(matches, "\n"))
486-
} else if len(matches) == 0 {
487-
return false, fmt.Sprintf("Could not find item matching: %s. Lines:\n%s", matcher.name(), strings.Join(lines, "\n"))
488-
} else {
489-
return true, ""
490485
}
486+
return true, ""
491487
})
492488

489+
// If no match was found, it could be that this is a view that renders only
490+
// the visible lines. In that case, we jump to the top and then press
491+
// down-arrow until we found the match. We simply return the first match we
492+
// find, so we have no way to assert that there are no duplicates.
493+
if matchIndex == -1 {
494+
self.GotoTop()
495+
matchIndex = len(lines)
496+
}
497+
493498
selectedLineIdx := self.getSelectedLineIdx()
494499
if selectedLineIdx == matchIndex {
495500
return self.SelectedLine(matcher)
@@ -514,12 +519,14 @@ func (self *ViewDriver) NavigateToLine(matcher *TextMatcher) *ViewDriver {
514519
for i := 0; i < maxNumKeyPresses; i++ {
515520
keyPress()
516521
idx := self.getSelectedLineIdx()
517-
if ok, _ := matcher.test(lines[idx]); ok {
522+
// It is important to use view.BufferLines() here and not lines, because it
523+
// could change with every keypress.
524+
if ok, _ := matcher.test(view.BufferLines()[idx]); ok {
518525
return self
519526
}
520527
}
521528

522-
self.t.fail(fmt.Sprintf("Could not navigate to item matching: %s. Lines:\n%s", matcher.name(), strings.Join(lines, "\n")))
529+
self.t.fail(fmt.Sprintf("Could not navigate to item matching: %s. Lines:\n%s", matcher.name(), strings.Join(view.BufferLines(), "\n")))
523530
return self
524531
}
525532

0 commit comments

Comments
 (0)