diff --git a/pkg/gocui/view.go b/pkg/gocui/view.go index cbfbfb8cb..02f22e980 100644 --- a/pkg/gocui/view.go +++ b/pkg/gocui/view.go @@ -74,6 +74,16 @@ type View struct { // true and viewLines to nil viewLines []viewLine + // Number of leading entries in viewLines that correspond to the current + // buffer (v.lines), as built by the most recent refreshViewLinesIfNeeded. + // refreshViewLinesIfNeeded overwrites viewLines in place without truncating, + // so when a re-render is shorter than the previous one the tail keeps stale + // entries from that previous render (drawn deliberately, to avoid flicker + // while the new content loads). Those entries don't correspond to the + // current buffer, so readers that map a view line to a buffer line must + // ignore them; this is the count of the entries they may trust. + freshViewLineCount int + // If the last character written was a newline, we don't write it but // instead set pendingNewline to true. If more text is written, we write the // newline then. This is to avoid having an extra blank at the end of the view. @@ -212,6 +222,7 @@ type pos struct { func (v *View) clearViewLines() { v.tainted = true v.viewLines = nil + v.freshViewLineCount = 0 v.clearHover() } @@ -1476,6 +1487,7 @@ func (v *View) refreshViewLinesIfNeeded() { } v.firstDirtyLine = len(lines) + v.freshViewLineCount = lineIdx v.tainted = false } @@ -1716,19 +1728,15 @@ func (v *View) BufferLineForViewLine(y int) (int, bool) { func (v *View) bufferLineForViewLine(y int) (int, bool) { v.refreshViewLinesIfNeeded() - if y < 0 || y >= len(v.viewLines) { + // Bound on freshViewLineCount rather than len(v.viewLines): the entries past + // it are a stale tail retained for flicker-avoidance (see freshViewLineCount) + // and don't correspond to the current buffer. Within the fresh range every + // entry's linesY was just built from v.lines, so it is guaranteed in range. + if y < 0 || y >= v.freshViewLineCount { return 0, false } - // refreshViewLinesIfNeeded overwrites viewLines in place without truncating, - // so while a shorter re-render is loading, the tail of viewLines can still - // hold stale entries pointing past the (shrunk) v.lines. Guard against that. - linesY := v.viewLines[y].linesY - if linesY >= len(v.lines) { - return 0, false - } - - return linesY, true + return v.viewLines[y].linesY, true } // indexFunc allows to split lines by words taking into account spaces diff --git a/pkg/gocui/view_test.go b/pkg/gocui/view_test.go index 171517aea..2ed7e861f 100644 --- a/pkg/gocui/view_test.go +++ b/pkg/gocui/view_test.go @@ -230,10 +230,7 @@ func TestBufferLineForViewLineStaleTail(t *testing.T) { // buffer, so the mapping must fail. (On the buggy code it instead maps to // buffer line 1, the stale entry's lingering index.) _, ok = v.BufferLineForViewLine(4) - /* EXPECTED: assert.False(t, ok) - ACTUAL: */ - assert.True(t, ok) } func TestContainsColoredText(t *testing.T) {