From 1efcfcc1484ee2d4bdb8abc0c04219f1fcfe2577 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 10 Jul 2026 10:50:13 +0200 Subject: [PATCH] Don't share a live view's buffer when copying its content moveMainContextToTop copies the current top view's content into the view it's promoting, to avoid a flicker. The source can be a main view with a live streaming task (e.g. resolving a conflict promotes the merge-conflicts view over a main view that's mid-diff), and CopyContent both read and published that source's buffer unsafely: - it read the source's lines/viewLines while locking only the destination, racing the task's concurrent Write; and - it aliased the source's row slices into the destination, so the source's ongoing appends (growslice reading the shared array) and refreshViewLinesIfNeeded's in-place wrapping-cache writes (&lines[i]) kept racing this view's rendering after the copy. Lock the source for the read, and shallow-clone the row slices so the destination gets its own arrays. The per-row cell data is immutable once written, so it stays shared -- the clone cost is proportional to the number of rows, not their contents. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/gocui/view.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/pkg/gocui/view.go b/pkg/gocui/view.go index 39f2d78a9..b106eb21f 100644 --- a/pkg/gocui/view.go +++ b/pkg/gocui/view.go @@ -7,6 +7,7 @@ package gocui import ( "fmt" "io" + "slices" "strings" "sync" "unicode" @@ -1146,10 +1147,25 @@ func (v *View) CopyContent(from *View) { v.writeMutex.Lock() defer v.writeMutex.Unlock() + // A background task may be streaming output into the source view's buffer + // via Write, so read it under its own lock. The source is always a + // different view than the destination (see the sole caller, + // moveMainContextToTop), and no other code holds two view write locks at + // once, so this can't deadlock. + from.writeMutex.Lock() + defer from.writeMutex.Unlock() + v.clear() - v.lines = from.lines - v.viewLines = from.viewLines + // Clone the row slices rather than sharing them: the source view stays + // live (its streaming task keeps appending rows, and refreshViewLinesIfNeeded + // fills each row's wrapping cache in place via &lines[i]), so sharing the + // backing arrays would race those writes against this view's own rendering. + // This is a shallow clone -- the per-row cell data is immutable once written + // and stays shared, so the cost is proportional to the number of rows, not + // their contents. + v.lines = slices.Clone(from.lines) + v.viewLines = slices.Clone(from.viewLines) v.ox = from.ox v.oy = from.oy v.cx = from.cx