Take the write mutex when clearing view lines and reading the buffer

A view's line buffer, its viewLines/tainted flags, and its hover state
are all written from the command-task goroutine (under writeMutex) as it
renders. But three accessors reached that same state from the UI thread
without the lock: SetView and the GUI-resize path cleared a view's lines
directly, viewsToRedrawContentOnly read the tainted flag, and Buffer read
the line buffer. Each raced a rendering task.

Guard them with writeMutex, matching the view's other buffer accessors.
These are reads/clears of state writeMutex already protects, not new
callers of it -- the view's geometry stays outside the mutex.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller 2026-07-08 13:01:01 +02:00
parent f6eaed8cd4
commit 65cb439076
2 changed files with 18 additions and 3 deletions

View file

@ -385,7 +385,7 @@ func (g *Gui) SetView(name string, x0, y0, x1, y1 int, overlaps byte) (*View, er
v.y1 = y1
if sizeChanged {
v.clearViewLines()
v.ClearViewLines()
if v.Editable {
cursorX, cursorY := v.TextArea.GetCursorXY()
@ -1461,7 +1461,7 @@ func (g *Gui) flush() error {
// if GUI's size has changed, we need to redraw all views
if maxX != g.maxX || maxY != g.maxY {
for _, v := range g.views {
v.clearViewLines()
v.ClearViewLines()
}
}
g.maxX, g.maxY = maxX, maxY
@ -1500,7 +1500,7 @@ func viewsToRedrawContentOnly(views []*View) []*View {
redrawIndexes := set.New[int]()
for i, v := range views {
if !v.tainted && !redrawIndexes.Includes(i) {
if !v.IsTainted() && !redrawIndexes.Includes(i) {
continue
}

View file

@ -214,6 +214,16 @@ func (v *View) clearViewLines() {
v.clearHover()
}
// ClearViewLines is clearViewLines guarded by writeMutex. It's for callers on
// the UI thread (the layout pass) that touch a view whose content a task
// goroutine may be writing concurrently: viewLines/tainted/hover are all
// buffer state that writeMutex protects.
func (v *View) ClearViewLines() {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
v.clearViewLines()
}
type searcher struct {
searchString string
searchPositions []SearchPosition
@ -1287,6 +1297,8 @@ func (v *View) updateSearchPositions() {
// IsTainted tells us if the view is tainted
func (v *View) IsTainted() bool {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
return v.tainted
}
@ -1535,6 +1547,9 @@ func (v *View) BufferLines() []string {
// Buffer returns a string with the contents of the view's internal
// buffer.
func (v *View) Buffer() string {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
return linesToString(v.lines)
}