From 34566357f41468b370ecfde5fbe804d2507a01de Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 9 Jun 2026 11:22:55 +0200 Subject: [PATCH] Revert "Stop mapping stale-tail view lines to the wrong buffer line" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts the freshViewLineCount stale-tail guard (§8), restoring the demonstrate-the-bug test state. The upcoming off-screen render rebuilds the displayed buffer wholesale on swap and lets refreshViewLinesIfNeeded truncate, so the stale tail never forms — a cleaner fix than tracking a fresh-count. Removing the guard on its own keeps that change focused; the bug it guarded against is re-fixed by the truncation in the next commit. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/gocui/view.go | 28 ++++++++++------------------ pkg/gocui/view_test.go | 3 +++ 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/pkg/gocui/view.go b/pkg/gocui/view.go index 2a7febb7d..d61a60eac 100644 --- a/pkg/gocui/view.go +++ b/pkg/gocui/view.go @@ -100,16 +100,6 @@ type View struct { // true and viewLines to nil viewLines []viewLine - // Number of leading entries in viewLines that correspond to the current - // buffer (v.buf.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 - // writeMutex protects locks the write process writeMutex sync.Mutex @@ -240,7 +230,6 @@ type pos struct { func (v *View) clearViewLines() { v.tainted = true v.viewLines = nil - v.freshViewLineCount = 0 v.clearHover() } @@ -1513,7 +1502,6 @@ func (v *View) refreshViewLinesIfNeeded() { } v.firstDirtyLine = len(lines) - v.freshViewLineCount = lineIdx v.tainted = false } @@ -1754,15 +1742,19 @@ func (v *View) BufferLineForViewLine(y int) (int, bool) { func (v *View) bufferLineForViewLine(y int) (int, bool) { v.refreshViewLinesIfNeeded() - // 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.buf.lines, so it is guaranteed in range. - if y < 0 || y >= v.freshViewLineCount { + if y < 0 || y >= len(v.viewLines) { return 0, false } - return v.viewLines[y].linesY, true + // 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.buf.lines. Guard against that. + linesY := v.viewLines[y].linesY + if linesY >= len(v.buf.lines) { + return 0, false + } + + return 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 89eaa7207..bfc4fe99f 100644 --- a/pkg/gocui/view_test.go +++ b/pkg/gocui/view_test.go @@ -230,7 +230,10 @@ 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) {