From acfaada238f75627c66aefd93fd485609223cd2b Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 15 Aug 2026 21:16:53 +0200 Subject: [PATCH] Let a view be read while it is re-rendering A re-render builds into an off-screen buffer and swaps it in when it has read enough to paint. Deciding where the new content should be shown means reading it before that swap: afterwards it is on screen already, and whatever we then scroll to has been seen at the wrong position first. So expose the off-screen buffer's diff-line contents and line count, the latter for telling when a line found there has a screenful below it. The contents come in two forms, the whole buffer and everything from a given line on, so that a reader following the render as it loads can look at each line once instead of re-reading the buffer per line. Co-authored-by: Claude Opus 5 (1M context) --- pkg/gocui/view.go | 57 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/pkg/gocui/view.go b/pkg/gocui/view.go index 6f2ef869f..1dd1aabb2 100644 --- a/pkg/gocui/view.go +++ b/pkg/gocui/view.go @@ -1996,9 +1996,60 @@ func (v *View) DiffLineContents() []DiffLineContent { v.writeMutex.Lock() defer v.writeMutex.Unlock() - contents := make([]DiffLineContent, len(v.buf.lines)) - for i := range v.buf.lines { - line := &v.buf.lines[i] + return diffLineContentsFrom(v.buf, 0) +} + +// OffscreenDiffLineContents is DiffLineContents for the content of a re-render in +// progress (see BeginOffscreenRender). A reader deciding where the new content +// should be shown has to work from this: it has to answer before the swap, since +// after the swap the content is already on screen. Returns nil when no re-render +// is underway. +func (v *View) OffscreenDiffLineContents() []DiffLineContent { + v.writeMutex.Lock() + defer v.writeMutex.Unlock() + + if v.offscreen == nil { + return nil + } + return diffLineContentsFrom(v.offscreen, 0) +} + +// OffscreenDiffLineContentsFrom is OffscreenDiffLineContents restricted to the lines +// from index `from` on (so result[0] is buffer line `from`). It lets a reader that +// follows a re-render as it loads look at each line once, rather than snapshotting +// the whole buffer again on every line — the difference between an O(n) and an O(n²) +// scan of a large diff. Returns nil when no re-render is underway, or when `from` is +// past the lines read so far. +func (v *View) OffscreenDiffLineContentsFrom(from int) []DiffLineContent { + v.writeMutex.Lock() + defer v.writeMutex.Unlock() + + if v.offscreen == nil || from < 0 || from >= len(v.offscreen.lines) { + return nil + } + return diffLineContentsFrom(v.offscreen, from) +} + +// OffscreenLineCount returns the number of unwrapped lines a re-render in progress +// has read so far, or 0 when none is underway. It tells a reader waiting for a +// particular line, cheaply, when a screenful below it has arrived too — so that the +// swap shows that line with content under it rather than at the bottom edge of a +// half-filled view. +func (v *View) OffscreenLineCount() int { + v.writeMutex.Lock() + defer v.writeMutex.Unlock() + + if v.offscreen == nil { + return 0 + } + return len(v.offscreen.lines) +} + +func diffLineContentsFrom(buf *viewBuffer, from int) []DiffLineContent { + lines := buf.lines[from:] + contents := make([]DiffLineContent, len(lines)) + for i := range lines { + line := &lines[i] var metadata []string for _, c := range line.cells { if c.metadata != "" && !slices.Contains(metadata, c.metadata) {