mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-02-25 18:55:28 -06:00
Reduce memory consumption when loading large number of commits (#3687)
(Github decided to auto-close #2533, and I don't see any way to reopen it, so opening a new one here. Please see there for discussion and review.) When pressing `>` in the commits panel to trigger loading all the remaining commits past the initial 300, memory consumption is a pretty big problem for larger repositories. The two main causes seem to be 1. the cell memory from rendering the entire list of commits into the gocui view 2. the pipe sets when git.log.showGraph is on This PR addresses only the first of these problems, by not rendering the entire view, but only the visible portion of it. Since we already re-render the visible portion of the view on every layout call, this was relatively easy to do. Below are some measurements for our repository at work (261.985 commits): | | master | this PR | | ------------- | ------ | ------- | | without Graph | 855 MB | 360 MB | | with Graph | 3.1 GB | 770 MB | And for the linux kernel repo (1.170.387 commits): | | master | this PR | | ------------- | ----------------------------------------- | ------- | | without Graph | 5.8 GB | 1.2G | | with Graph | Killed by the OS after it reached 86.9 GB | 39.9 GB | The measurements were taken after scrolling all the way down in the list of commits. They have to be taken with a grain of salt, as memory consumption fluctuates quite a bit in ways that I find hard to make sense of. As you can see, there's more work to do to reduce the memory usage for the graph, but for our repo at work this PR makes it usable already, which it wasn't really before.
This commit is contained in:
commit
a171ec4294
2
go.mod
2
go.mod
@ -16,7 +16,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.20240623092910-a42926c14fc9
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20240623095254-05e1204c2454
|
||||
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
4
go.sum
@ -188,8 +188,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.20240623092910-a42926c14fc9 h1:JJ0DrXgAUpGBGV5w8nzrQLMWTgcTvf745IKAk08qjcM=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20240623092910-a42926c14fc9/go.mod h1:XtEbqCbn45keRXEu+OMZkjN5gw6AEob59afsgHjokZ8=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20240623095254-05e1204c2454 h1:rTPA5WiPM1SPUA3r2kSb3RiILC93am6irMvOLjO7JNA=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20240623095254-05e1204c2454/go.mod h1:XtEbqCbn45keRXEu+OMZkjN5gw6AEob59afsgHjokZ8=
|
||||
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=
|
||||
|
@ -1,6 +1,8 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -46,6 +48,29 @@ func (self *BackgroundRoutineMgr) startBackgroundRoutines() {
|
||||
refreshInterval)
|
||||
}
|
||||
}
|
||||
|
||||
if self.gui.Config.GetDebug() {
|
||||
self.goEvery(time.Second*time.Duration(10), self.gui.stopChan, func() error {
|
||||
formatBytes := func(b uint64) string {
|
||||
const unit = 1000
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := uint64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB",
|
||||
float64(b)/float64(div), "kMGTPE"[exp])
|
||||
}
|
||||
|
||||
m := runtime.MemStats{}
|
||||
runtime.ReadMemStats(&m)
|
||||
self.gui.c.Log.Infof("Heap memory in use: %s", formatBytes(m.HeapAlloc))
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (self *BackgroundRoutineMgr) startBackgroundFetch() {
|
||||
|
@ -20,11 +20,12 @@ type BaseContext struct {
|
||||
onFocusFn onFocusFn
|
||||
onFocusLostFn onFocusLostFn
|
||||
|
||||
focusable bool
|
||||
transient bool
|
||||
hasControlledBounds bool
|
||||
needsRerenderOnWidthChange bool
|
||||
highlightOnFocus bool
|
||||
focusable bool
|
||||
transient bool
|
||||
hasControlledBounds bool
|
||||
needsRerenderOnWidthChange bool
|
||||
needsRerenderOnHeightChange bool
|
||||
highlightOnFocus bool
|
||||
|
||||
*ParentContextMgr
|
||||
}
|
||||
@ -37,15 +38,16 @@ type (
|
||||
var _ types.IBaseContext = &BaseContext{}
|
||||
|
||||
type NewBaseContextOpts struct {
|
||||
Kind types.ContextKind
|
||||
Key types.ContextKey
|
||||
View *gocui.View
|
||||
WindowName string
|
||||
Focusable bool
|
||||
Transient bool
|
||||
HasUncontrolledBounds bool // negating for the sake of making false the default
|
||||
HighlightOnFocus bool
|
||||
NeedsRerenderOnWidthChange bool
|
||||
Kind types.ContextKind
|
||||
Key types.ContextKey
|
||||
View *gocui.View
|
||||
WindowName string
|
||||
Focusable bool
|
||||
Transient bool
|
||||
HasUncontrolledBounds bool // negating for the sake of making false the default
|
||||
HighlightOnFocus bool
|
||||
NeedsRerenderOnWidthChange bool
|
||||
NeedsRerenderOnHeightChange bool
|
||||
|
||||
OnGetOptionsMap func() map[string]string
|
||||
}
|
||||
@ -56,18 +58,19 @@ func NewBaseContext(opts NewBaseContextOpts) *BaseContext {
|
||||
hasControlledBounds := !opts.HasUncontrolledBounds
|
||||
|
||||
return &BaseContext{
|
||||
kind: opts.Kind,
|
||||
key: opts.Key,
|
||||
view: opts.View,
|
||||
windowName: opts.WindowName,
|
||||
onGetOptionsMap: opts.OnGetOptionsMap,
|
||||
focusable: opts.Focusable,
|
||||
transient: opts.Transient,
|
||||
hasControlledBounds: hasControlledBounds,
|
||||
highlightOnFocus: opts.HighlightOnFocus,
|
||||
needsRerenderOnWidthChange: opts.NeedsRerenderOnWidthChange,
|
||||
ParentContextMgr: &ParentContextMgr{},
|
||||
viewTrait: viewTrait,
|
||||
kind: opts.Kind,
|
||||
key: opts.Key,
|
||||
view: opts.View,
|
||||
windowName: opts.WindowName,
|
||||
onGetOptionsMap: opts.OnGetOptionsMap,
|
||||
focusable: opts.Focusable,
|
||||
transient: opts.Transient,
|
||||
hasControlledBounds: hasControlledBounds,
|
||||
highlightOnFocus: opts.HighlightOnFocus,
|
||||
needsRerenderOnWidthChange: opts.NeedsRerenderOnWidthChange,
|
||||
needsRerenderOnHeightChange: opts.NeedsRerenderOnHeightChange,
|
||||
ParentContextMgr: &ParentContextMgr{},
|
||||
viewTrait: viewTrait,
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,6 +200,10 @@ func (self *BaseContext) NeedsRerenderOnWidthChange() bool {
|
||||
return self.needsRerenderOnWidthChange
|
||||
}
|
||||
|
||||
func (self *BaseContext) NeedsRerenderOnHeightChange() bool {
|
||||
return self.needsRerenderOnHeightChange
|
||||
}
|
||||
|
||||
func (self *BaseContext) Title() string {
|
||||
return ""
|
||||
}
|
||||
|
@ -18,6 +18,9 @@ type ListContextTrait struct {
|
||||
// we should find out exactly which lines are now part of the path and refresh those.
|
||||
// We should also keep track of the previous path and refresh those lines too.
|
||||
refreshViewportOnChange bool
|
||||
// If this is true, we only render the visible lines of the list. Useful for lists that can
|
||||
// get very long, because it can save a lot of memory
|
||||
renderOnlyVisibleLines bool
|
||||
}
|
||||
|
||||
func (self *ListContextTrait) IsListContext() {}
|
||||
@ -25,7 +28,8 @@ func (self *ListContextTrait) IsListContext() {}
|
||||
func (self *ListContextTrait) FocusLine() {
|
||||
// Doing this at the end of the layout function because we need the view to be
|
||||
// resized before we focus the line, otherwise if we're in accordion mode
|
||||
// the view could be squashed and won't how to adjust the cursor/origin
|
||||
// the view could be squashed and won't how to adjust the cursor/origin.
|
||||
// Also, refreshing the viewport needs to happen after the view has been resized.
|
||||
self.c.AfterLayout(func() error {
|
||||
oldOrigin, _ := self.GetViewTrait().ViewPortYBounds()
|
||||
|
||||
@ -40,22 +44,18 @@ func (self *ListContextTrait) FocusLine() {
|
||||
self.GetViewTrait().CancelRangeSelect()
|
||||
}
|
||||
|
||||
// If FocusPoint() caused the view to scroll (because the selected line
|
||||
// was out of view before), we need to rerender the view port again.
|
||||
// This can happen when pressing , or . to scroll by pages, or < or > to
|
||||
// jump to the top or bottom.
|
||||
newOrigin, _ := self.GetViewTrait().ViewPortYBounds()
|
||||
if self.refreshViewportOnChange && oldOrigin != newOrigin {
|
||||
if self.refreshViewportOnChange {
|
||||
self.refreshViewport()
|
||||
} else if self.renderOnlyVisibleLines {
|
||||
newOrigin, _ := self.GetViewTrait().ViewPortYBounds()
|
||||
if oldOrigin != newOrigin {
|
||||
return self.HandleRender()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
self.setFooter()
|
||||
|
||||
if self.refreshViewportOnChange {
|
||||
self.refreshViewport()
|
||||
}
|
||||
}
|
||||
|
||||
func (self *ListContextTrait) refreshViewport() {
|
||||
@ -93,8 +93,21 @@ func (self *ListContextTrait) HandleFocusLost(opts types.OnFocusLostOpts) error
|
||||
// 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
|
||||
func (self *ListContextTrait) HandleRender() error {
|
||||
self.list.ClampSelection()
|
||||
content := self.renderLines(-1, -1)
|
||||
self.GetViewTrait().SetContent(content)
|
||||
if self.renderOnlyVisibleLines {
|
||||
// Rendering only the visible area can save a lot of cell memory for
|
||||
// those views that support it.
|
||||
totalLength := self.list.Len()
|
||||
if self.getNonModelItems != nil {
|
||||
totalLength += len(self.getNonModelItems())
|
||||
}
|
||||
self.GetViewTrait().SetContentLineCount(totalLength)
|
||||
startIdx, length := self.GetViewTrait().ViewPortYBounds()
|
||||
content := self.renderLines(startIdx, startIdx+length)
|
||||
self.GetViewTrait().SetViewPortContentAndClearEverythingElse(content)
|
||||
} else {
|
||||
content := self.renderLines(-1, -1)
|
||||
self.GetViewTrait().SetContent(content)
|
||||
}
|
||||
self.c.Render()
|
||||
self.setFooter()
|
||||
|
||||
@ -123,3 +136,7 @@ func (self *ListContextTrait) IsItemVisible(item types.HasUrn) bool {
|
||||
func (self *ListContextTrait) RangeSelectEnabled() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (self *ListContextTrait) RenderOnlyVisibleLines() bool {
|
||||
return self.renderOnlyVisibleLines
|
||||
}
|
||||
|
@ -72,12 +72,13 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
|
||||
SearchTrait: NewSearchTrait(c),
|
||||
ListContextTrait: &ListContextTrait{
|
||||
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
||||
View: c.Views().Commits,
|
||||
WindowName: "commits",
|
||||
Key: LOCAL_COMMITS_CONTEXT_KEY,
|
||||
Kind: types.SIDE_CONTEXT,
|
||||
Focusable: true,
|
||||
NeedsRerenderOnWidthChange: true,
|
||||
View: c.Views().Commits,
|
||||
WindowName: "commits",
|
||||
Key: LOCAL_COMMITS_CONTEXT_KEY,
|
||||
Kind: types.SIDE_CONTEXT,
|
||||
Focusable: true,
|
||||
NeedsRerenderOnWidthChange: true,
|
||||
NeedsRerenderOnHeightChange: true,
|
||||
})),
|
||||
ListRenderer: ListRenderer{
|
||||
list: viewModel,
|
||||
@ -85,6 +86,7 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
|
||||
},
|
||||
c: c,
|
||||
refreshViewportOnChange: true,
|
||||
renderOnlyVisibleLines: true,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -37,13 +37,14 @@ func NewRemoteBranchesContext(
|
||||
DynamicTitleBuilder: NewDynamicTitleBuilder(c.Tr.RemoteBranchesDynamicTitle),
|
||||
ListContextTrait: &ListContextTrait{
|
||||
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
||||
View: c.Views().RemoteBranches,
|
||||
WindowName: "branches",
|
||||
Key: REMOTE_BRANCHES_CONTEXT_KEY,
|
||||
Kind: types.SIDE_CONTEXT,
|
||||
Focusable: true,
|
||||
Transient: true,
|
||||
NeedsRerenderOnWidthChange: true,
|
||||
View: c.Views().RemoteBranches,
|
||||
WindowName: "branches",
|
||||
Key: REMOTE_BRANCHES_CONTEXT_KEY,
|
||||
Kind: types.SIDE_CONTEXT,
|
||||
Focusable: true,
|
||||
Transient: true,
|
||||
NeedsRerenderOnWidthChange: true,
|
||||
NeedsRerenderOnHeightChange: true,
|
||||
})),
|
||||
ListRenderer: ListRenderer{
|
||||
list: viewModel,
|
||||
|
@ -115,13 +115,14 @@ func NewSubCommitsContext(
|
||||
DynamicTitleBuilder: NewDynamicTitleBuilder(c.Tr.SubCommitsDynamicTitle),
|
||||
ListContextTrait: &ListContextTrait{
|
||||
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
||||
View: c.Views().SubCommits,
|
||||
WindowName: "branches",
|
||||
Key: SUB_COMMITS_CONTEXT_KEY,
|
||||
Kind: types.SIDE_CONTEXT,
|
||||
Focusable: true,
|
||||
Transient: true,
|
||||
NeedsRerenderOnWidthChange: true,
|
||||
View: c.Views().SubCommits,
|
||||
WindowName: "branches",
|
||||
Key: SUB_COMMITS_CONTEXT_KEY,
|
||||
Kind: types.SIDE_CONTEXT,
|
||||
Focusable: true,
|
||||
Transient: true,
|
||||
NeedsRerenderOnWidthChange: true,
|
||||
NeedsRerenderOnHeightChange: true,
|
||||
})),
|
||||
ListRenderer: ListRenderer{
|
||||
list: viewModel,
|
||||
@ -130,6 +131,7 @@ func NewSubCommitsContext(
|
||||
},
|
||||
c: c,
|
||||
refreshViewportOnChange: true,
|
||||
renderOnlyVisibleLines: true,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -34,6 +34,15 @@ func (self *ViewTrait) SetViewPortContent(content string) {
|
||||
self.view.OverwriteLines(y, content)
|
||||
}
|
||||
|
||||
func (self *ViewTrait) SetViewPortContentAndClearEverythingElse(content string) {
|
||||
_, y := self.view.Origin()
|
||||
self.view.OverwriteLinesAndClearEverythingElse(y, content)
|
||||
}
|
||||
|
||||
func (self *ViewTrait) SetContentLineCount(lineCount int) {
|
||||
self.view.SetContentLineCount(lineCount)
|
||||
}
|
||||
|
||||
func (self *ViewTrait) SetContent(content string) {
|
||||
self.view.SetContent(content)
|
||||
}
|
||||
|
@ -53,6 +53,9 @@ func (self *ListController) HandleScrollRight() error {
|
||||
func (self *ListController) HandleScrollUp() error {
|
||||
scrollHeight := self.c.UserConfig.Gui.ScrollHeight
|
||||
self.context.GetViewTrait().ScrollUp(scrollHeight)
|
||||
if self.context.RenderOnlyVisibleLines() {
|
||||
return self.context.HandleRender()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -60,6 +63,9 @@ func (self *ListController) HandleScrollUp() error {
|
||||
func (self *ListController) HandleScrollDown() error {
|
||||
scrollHeight := self.c.UserConfig.Gui.ScrollHeight
|
||||
self.context.GetViewTrait().ScrollDown(scrollHeight)
|
||||
if self.context.RenderOnlyVisibleLines() {
|
||||
return self.context.HandleRender()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -72,14 +72,26 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
frameOffset = 0
|
||||
}
|
||||
|
||||
mustRerender := false
|
||||
if context.NeedsRerenderOnWidthChange() {
|
||||
// view.Width() returns the width -1 for some reason
|
||||
oldWidth := view.Width() + 1
|
||||
newWidth := dimensionsObj.X1 - dimensionsObj.X0 + 2*frameOffset
|
||||
if oldWidth != newWidth {
|
||||
contextsToRerender = append(contextsToRerender, context)
|
||||
mustRerender = true
|
||||
}
|
||||
}
|
||||
if context.NeedsRerenderOnHeightChange() {
|
||||
// view.Height() returns the height -1 for some reason
|
||||
oldHeight := view.Height() + 1
|
||||
newHeight := dimensionsObj.Y1 - dimensionsObj.Y0 + 2*frameOffset
|
||||
if oldHeight != newHeight {
|
||||
mustRerender = true
|
||||
}
|
||||
}
|
||||
if mustRerender {
|
||||
contextsToRerender = append(contextsToRerender, context)
|
||||
}
|
||||
|
||||
_, err = g.SetView(
|
||||
viewName,
|
||||
|
@ -63,6 +63,9 @@ type IBaseContext interface {
|
||||
// true if the view needs to be rerendered when its width changes
|
||||
NeedsRerenderOnWidthChange() bool
|
||||
|
||||
// true if the view needs to be rerendered when its height changes
|
||||
NeedsRerenderOnHeightChange() bool
|
||||
|
||||
// returns the desired title for the view upon activation. If there is no desired title (returns empty string), then
|
||||
// no title will be set
|
||||
Title() string
|
||||
@ -150,6 +153,7 @@ type IListContext interface {
|
||||
FocusLine()
|
||||
IsListContext() // used for type switch
|
||||
RangeSelectEnabled() bool
|
||||
RenderOnlyVisibleLines() bool
|
||||
}
|
||||
|
||||
type IPatchExplorerContext interface {
|
||||
@ -172,6 +176,8 @@ type IViewTrait interface {
|
||||
SetRangeSelectStart(yIdx int)
|
||||
CancelRangeSelect()
|
||||
SetViewPortContent(content string)
|
||||
SetViewPortContentAndClearEverythingElse(content string)
|
||||
SetContentLineCount(lineCount int)
|
||||
SetContent(content string)
|
||||
SetFooter(value string)
|
||||
SetOriginX(value int)
|
||||
|
@ -431,6 +431,11 @@ func (self *ViewDriver) SelectPreviousItem() *ViewDriver {
|
||||
return self.PressFast(self.t.keys.Universal.PrevItem)
|
||||
}
|
||||
|
||||
// i.e. pressing '<'
|
||||
func (self *ViewDriver) GotoTop() *ViewDriver {
|
||||
return self.PressFast(self.t.keys.Universal.GotoTop)
|
||||
}
|
||||
|
||||
// i.e. pressing space
|
||||
func (self *ViewDriver) PressPrimaryAction() *ViewDriver {
|
||||
return self.Press(self.t.keys.Universal.Select)
|
||||
@ -457,21 +462,15 @@ func (self *ViewDriver) PressEscape() *ViewDriver {
|
||||
// - the user is not in a list item
|
||||
// - no list item is found containing the given text
|
||||
// - multiple list items are found containing the given text in the initial page of items
|
||||
//
|
||||
// NOTE: this currently assumes that BufferLines returns all the lines that can be accessed.
|
||||
// If this changes in future, we'll need to update this code to first attempt to find the item
|
||||
// in the current page and failing that, jump to the top of the view and iterate through all of it,
|
||||
// looking for the item.
|
||||
func (self *ViewDriver) NavigateToLine(matcher *TextMatcher) *ViewDriver {
|
||||
self.IsFocused()
|
||||
|
||||
view := self.getView()
|
||||
lines := view.BufferLines()
|
||||
|
||||
var matchIndex int
|
||||
matchIndex := -1
|
||||
|
||||
self.t.assertWithRetries(func() (bool, string) {
|
||||
matchIndex = -1
|
||||
var matches []string
|
||||
// first we look for a duplicate on the current screen. We won't bother looking beyond that though.
|
||||
for i, line := range lines {
|
||||
@ -483,13 +482,19 @@ func (self *ViewDriver) NavigateToLine(matcher *TextMatcher) *ViewDriver {
|
||||
}
|
||||
if len(matches) > 1 {
|
||||
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"))
|
||||
} else if len(matches) == 0 {
|
||||
return false, fmt.Sprintf("Could not find item matching: %s. Lines:\n%s", matcher.name(), strings.Join(lines, "\n"))
|
||||
} else {
|
||||
return true, ""
|
||||
}
|
||||
return true, ""
|
||||
})
|
||||
|
||||
// If no match was found, it could be that this is a view that renders only
|
||||
// the visible lines. In that case, we jump to the top and then press
|
||||
// down-arrow until we found the match. We simply return the first match we
|
||||
// find, so we have no way to assert that there are no duplicates.
|
||||
if matchIndex == -1 {
|
||||
self.GotoTop()
|
||||
matchIndex = len(lines)
|
||||
}
|
||||
|
||||
selectedLineIdx := self.getSelectedLineIdx()
|
||||
if selectedLineIdx == matchIndex {
|
||||
return self.SelectedLine(matcher)
|
||||
@ -514,12 +519,14 @@ func (self *ViewDriver) NavigateToLine(matcher *TextMatcher) *ViewDriver {
|
||||
for i := 0; i < maxNumKeyPresses; i++ {
|
||||
keyPress()
|
||||
idx := self.getSelectedLineIdx()
|
||||
if ok, _ := matcher.test(lines[idx]); ok {
|
||||
// It is important to use view.BufferLines() here and not lines, because it
|
||||
// could change with every keypress.
|
||||
if ok, _ := matcher.test(view.BufferLines()[idx]); ok {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
self.t.fail(fmt.Sprintf("Could not navigate to item matching: %s. Lines:\n%s", matcher.name(), strings.Join(lines, "\n")))
|
||||
self.t.fail(fmt.Sprintf("Could not navigate to item matching: %s. Lines:\n%s", matcher.name(), strings.Join(view.BufferLines(), "\n")))
|
||||
return self
|
||||
}
|
||||
|
||||
|
66
vendor/github.com/jesseduffield/gocui/view.go
generated
vendored
66
vendor/github.com/jesseduffield/gocui/view.go
generated
vendored
@ -771,13 +771,14 @@ func (v *View) writeRunes(p []rune) {
|
||||
}
|
||||
v.wx = 0
|
||||
default:
|
||||
moveCursor, cells := v.parseInput(r, v.wx, v.wy)
|
||||
truncateLine, cells := v.parseInput(r, v.wx, v.wy)
|
||||
if cells == nil {
|
||||
continue
|
||||
}
|
||||
v.writeCells(v.wx, v.wy, cells)
|
||||
if moveCursor {
|
||||
v.wx += len(cells)
|
||||
v.wx += len(cells)
|
||||
if truncateLine {
|
||||
v.lines[v.wy] = v.lines[v.wy][:v.wx]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -800,7 +801,7 @@ func (v *View) writeString(s string) {
|
||||
// contains the processed data.
|
||||
func (v *View) parseInput(ch rune, x int, _ int) (bool, []cell) {
|
||||
cells := []cell{}
|
||||
moveCursor := true
|
||||
truncateLine := false
|
||||
|
||||
isEscape, err := v.ei.parseOne(ch)
|
||||
if err != nil {
|
||||
@ -816,18 +817,13 @@ func (v *View) parseInput(ch rune, x int, _ int) (bool, []cell) {
|
||||
} else {
|
||||
repeatCount := 1
|
||||
if _, ok := v.ei.instruction.(eraseInLineFromCursor); ok {
|
||||
// fill rest of line
|
||||
// truncate line
|
||||
v.ei.instructionRead()
|
||||
cx := 0
|
||||
for _, cell := range v.lines[v.wy] {
|
||||
cx += runewidth.RuneWidth(cell.chr)
|
||||
}
|
||||
repeatCount = v.InnerWidth() - cx
|
||||
ch = ' '
|
||||
moveCursor = false
|
||||
repeatCount = 0
|
||||
truncateLine = true
|
||||
} else if isEscape {
|
||||
// do not output anything
|
||||
return moveCursor, nil
|
||||
return truncateLine, nil
|
||||
} else if ch == '\t' {
|
||||
// fill tab-sized space
|
||||
const tabStop = 4
|
||||
@ -844,7 +840,7 @@ func (v *View) parseInput(ch rune, x int, _ int) (bool, []cell) {
|
||||
}
|
||||
}
|
||||
|
||||
return moveCursor, cells
|
||||
return truncateLine, cells
|
||||
}
|
||||
|
||||
// Read reads data into p from the current reading position set by SetReadPos.
|
||||
@ -1590,19 +1586,51 @@ func (v *View) ClearTextArea() {
|
||||
_ = v.SetCursor(0, 0)
|
||||
}
|
||||
|
||||
// only call this function if you don't care where v.wx and v.wy end up
|
||||
func (v *View) OverwriteLines(y int, content string) {
|
||||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
||||
func (v *View) overwriteLines(y int, content string) {
|
||||
// break by newline, then for each line, write it, then add that erase command
|
||||
v.wx = 0
|
||||
v.wy = y
|
||||
|
||||
lines := strings.Replace(content, "\n", "\x1b[K\n", -1)
|
||||
// If the last line doesn't end with a linefeed, add the erase command at
|
||||
// the end too
|
||||
if !strings.HasSuffix(lines, "\n") {
|
||||
lines += "\x1b[K"
|
||||
}
|
||||
v.writeString(lines)
|
||||
}
|
||||
|
||||
// only call this function if you don't care where v.wx and v.wy end up
|
||||
func (v *View) OverwriteLines(y int, content string) {
|
||||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
||||
v.overwriteLines(y, content)
|
||||
}
|
||||
|
||||
// only call this function if you don't care where v.wx and v.wy end up
|
||||
func (v *View) OverwriteLinesAndClearEverythingElse(y int, content string) {
|
||||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
||||
v.overwriteLines(y, content)
|
||||
|
||||
for i := 0; i < y; i += 1 {
|
||||
v.lines[i] = nil
|
||||
}
|
||||
|
||||
for i := v.wy + 1; i < len(v.lines); i += 1 {
|
||||
v.lines[i] = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (v *View) SetContentLineCount(lineCount int) {
|
||||
if lineCount > 0 {
|
||||
v.makeWriteable(0, lineCount-1)
|
||||
}
|
||||
v.lines = v.lines[:lineCount]
|
||||
}
|
||||
|
||||
func (v *View) ScrollUp(amount int) {
|
||||
if amount > v.oy {
|
||||
amount = v.oy
|
||||
|
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
@ -172,7 +172,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.20240623092910-a42926c14fc9
|
||||
# github.com/jesseduffield/gocui v0.3.1-0.20240623095254-05e1204c2454
|
||||
## explicit; go 1.12
|
||||
github.com/jesseduffield/gocui
|
||||
# github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
|
||||
|
Loading…
Reference in New Issue
Block a user