diff --git a/pkg/gocui/search_test.go b/pkg/gocui/search_test.go index f2ecccec4..ba20da9de 100644 --- a/pkg/gocui/search_test.go +++ b/pkg/gocui/search_test.go @@ -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() + } +} diff --git a/pkg/gocui/view.go b/pkg/gocui/view.go index 294b34f25..cc5e6c0e4 100644 --- a/pkg/gocui/view.go +++ b/pkg/gocui/view.go @@ -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 , 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