mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
Work the search positions out when they are read, not on each line written
A view's search positions were worked out again from every write, and each of those walks the whole view. Content arrives a line at a time, so rendering into a searched view costs a walk per line. Streaming 2000 lines takes 565ms, where the same render into an unsearched view takes about 10ms. Mark the positions stale on a write instead, and work them out where they are read: when the view is drawn, when a key steps through the matches, when the status is asked for. That is at most once a frame, and the same 2000 lines now take 8ms. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4c90bc334c
commit
0505e778b3
|
|
@ -34,3 +34,25 @@ func TestSearchStatusAfterTheMatchesChange(t *testing.T) {
|
|||
assert.Equal(t, 0, index)
|
||||
assert.Equal(t, 1, total)
|
||||
}
|
||||
|
||||
func TestSearchPositionsFollowStreamedContent(t *testing.T) {
|
||||
v := NewView("name", 0, 0, 40, 10, OutputNormal)
|
||||
v.Search("match", nil)
|
||||
|
||||
// A render arrives a line at a time, and the status describes all of it.
|
||||
writeLines(v, "other", "match", "other", "match")
|
||||
|
||||
_, total := v.GetSearchStatus()
|
||||
assert.Equal(t, 2, total)
|
||||
}
|
||||
|
||||
func BenchmarkWriteToSearchedView(b *testing.B) {
|
||||
for b.Loop() {
|
||||
v := NewView("name", 0, 0, 100, 40, OutputNormal)
|
||||
v.Search("match", nil)
|
||||
for i := range 2000 {
|
||||
fmt.Fprintf(v, "line %d of a diff, most of which does not match\n", i)
|
||||
}
|
||||
v.GetSearchStatus()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -271,6 +271,12 @@ type searcher struct {
|
|||
currentSearchIndex int
|
||||
onSelectItem func(*View, int)
|
||||
renderSearchStatus func(*View, int, int)
|
||||
|
||||
// Whether the content has changed since the positions were worked out, so that
|
||||
// they have to be worked out again before they are read. Working them out walks
|
||||
// the whole view, and content arrives a line at a time, so it happens once per
|
||||
// read rather than once per line written.
|
||||
positionsStale bool
|
||||
}
|
||||
|
||||
func (v *View) setRenderSearchStatus(renderSearchStatus func(*View, int, int)) {
|
||||
|
|
@ -287,7 +293,27 @@ func (v *View) renderSearchStatus(index int, itemCount int) {
|
|||
}
|
||||
}
|
||||
|
||||
// refreshSearchPositions works the search positions out again if the content has
|
||||
// changed since they were last worked out. Every read of the positions goes through
|
||||
// this, so that no caller has to know whether the view has been drawn since the
|
||||
// content it is asking about arrived.
|
||||
func (v *View) refreshSearchPositions() {
|
||||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
||||
v.refreshSearchPositionsIfNeeded()
|
||||
}
|
||||
|
||||
// refreshSearchPositions for a caller that already holds writeMutex.
|
||||
func (v *View) refreshSearchPositionsIfNeeded() {
|
||||
if v.searcher.positionsStale {
|
||||
v.updateSearchPositions()
|
||||
}
|
||||
}
|
||||
|
||||
func (v *View) gotoNextMatch() error {
|
||||
v.refreshSearchPositions()
|
||||
|
||||
if len(v.searcher.searchPositions) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -307,6 +333,8 @@ func (v *View) gotoNextMatch() error {
|
|||
}
|
||||
|
||||
func (v *View) gotoPreviousMatch() error {
|
||||
v.refreshSearchPositions()
|
||||
|
||||
if len(v.searcher.searchPositions) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -328,6 +356,8 @@ func (v *View) gotoPreviousMatch() error {
|
|||
}
|
||||
|
||||
func (v *View) SelectSearchResult(index int) {
|
||||
v.refreshSearchPositions()
|
||||
|
||||
itemCount := len(v.searcher.searchPositions)
|
||||
if itemCount == 0 {
|
||||
return
|
||||
|
|
@ -347,6 +377,8 @@ func (v *View) SelectSearchResult(index int) {
|
|||
|
||||
// Returns <current match index>, <total matches>
|
||||
func (v *View) GetSearchStatus() (int, int) {
|
||||
v.refreshSearchPositions()
|
||||
|
||||
return v.searcher.currentSearchIndex, len(v.searcher.searchPositions)
|
||||
}
|
||||
|
||||
|
|
@ -420,6 +452,8 @@ func (v *View) nearestSearchPosition() int {
|
|||
}
|
||||
|
||||
func (v *View) SetNearestSearchPosition() {
|
||||
v.refreshSearchPositions()
|
||||
|
||||
if len(v.searcher.searchPositions) > 0 {
|
||||
newPos := v.nearestSearchPosition()
|
||||
if newPos != v.searcher.currentSearchIndex {
|
||||
|
|
@ -902,7 +936,7 @@ func (v *View) write(p []byte) {
|
|||
|
||||
v.buf.write(v, p)
|
||||
|
||||
v.updateSearchPositions()
|
||||
v.searcher.positionsStale = true
|
||||
}
|
||||
|
||||
// write parses p into cells and appends them to the buffer at its write cursor.
|
||||
|
|
@ -1353,6 +1387,8 @@ func stringToGraphemes(s string) []string {
|
|||
}
|
||||
|
||||
func (v *View) updateSearchPositions() {
|
||||
v.searcher.positionsStale = false
|
||||
|
||||
if v.searcher.searchString != "" {
|
||||
var normalizeRune func(s string) string
|
||||
var normalizedSearchStr string
|
||||
|
|
@ -1466,6 +1502,7 @@ func (v *View) draw(isWindowFocused bool) {
|
|||
}
|
||||
|
||||
v.refreshViewLinesIfNeeded()
|
||||
v.refreshSearchPositionsIfNeeded()
|
||||
|
||||
visibleViewLinesHeight := v.viewLineLengthIgnoringTrailingBlankLines()
|
||||
if v.Autoscroll && visibleViewLinesHeight > maxY {
|
||||
|
|
@ -2075,6 +2112,8 @@ func (v *View) setContentLineCount(lineCount int) {
|
|||
// result that is visible in the view, if any, or the first one that is below the view if none is
|
||||
// visible.
|
||||
func (v *View) selectVisibleSearchResultAfterScrollUp() {
|
||||
v.refreshSearchPositions()
|
||||
|
||||
if !v.Highlight && len(v.searcher.searchPositions) != 0 {
|
||||
windowBottom := v.oy + v.InnerHeight()
|
||||
if v.searcher.searchPositions[v.searcher.currentSearchIndex].Y >= windowBottom {
|
||||
|
|
@ -2098,6 +2137,8 @@ func (v *View) selectVisibleSearchResultAfterScrollUp() {
|
|||
// result that is visible in the view, if any, or the last one that is above the view if none is
|
||||
// visible.
|
||||
func (v *View) selectVisibleSearchResultAfterScrollDown() {
|
||||
v.refreshSearchPositions()
|
||||
|
||||
if !v.Highlight && len(v.searcher.searchPositions) != 0 {
|
||||
if v.searcher.searchPositions[v.searcher.currentSearchIndex].Y < v.oy {
|
||||
newSearchIndex := v.searcher.currentSearchIndex
|
||||
|
|
|
|||
Loading…
Reference in a new issue