Let an emptied main pane forget what it was showing

A pane that has been emptied is showing nothing, but it kept the scroll
position it was left at and went on claiming the render it used to show,
so the next render into it — the same command's output, the file it was
showing being selected again — counted as content the view already had,
and was revealed partway down.

So say what the empty pane is: at the top, and showing nothing that a
render can be a re-render of.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller
2026-10-04 19:01:02 +02:00
co-authored by Claude Opus 5
parent 5f6e9b1ef3
commit 3c76e7c035
4 changed files with 65 additions and 4 deletions
+10 -1
View File
@@ -162,10 +162,19 @@ func (gui *Gui) clampDiffSelectionToContent(view *gocui.View) {
}
// clearMainView empties a pane that is being given nothing to show, selection and all.
//
// An emptied pane is showing nothing, so it also goes back to the top and stops
// claiming the render it was showing: whatever it is given next is content the user
// hasn't seen there, and is shown from the top like any other.
func (gui *Gui) clearMainView(mainContext types.Context) {
mainContext.GetView().Clear()
view := mainContext.GetView()
view.Clear()
view.SetOrigin(0, 0)
mainContext.SetHasSelectableContent(false)
gui.State.ContextMgr.UpdateSelectionHighlights()
if manager := gui.getViewBufferManagerForView(view); manager != nil {
manager.ForgetRenderedContent()
}
}
// updateDiffSelectionVisibility works out whether a main pane holds anything for a
@@ -56,9 +56,6 @@ var PaneShownAgainStartsAtTheTop = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Secondary().
IsVisible().
Content(Contains("+LINE40")).
/* EXPECTED:
OriginY(0)
ACTUAL: */
OriginYAtLeast(1)
},
})
+11
View File
@@ -251,6 +251,17 @@ func (self *ViewBufferManager) GetTaskKey() string {
return self.taskKey
}
// ForgetRenderedContent records that the view no longer shows the render whose key it
// is holding, because it has been emptied. The key says what the view is showing, and
// the next task is compared against it to tell whether that task renders something
// new. A view with nothing in it is showing nothing, so whatever comes next is new.
func (self *ViewBufferManager) ForgetRenderedContent() {
self.taskIDMutex.Lock()
defer self.taskIDMutex.Unlock()
self.taskKey = ""
}
func NewViewBufferManager(
log *logrus.Entry,
writer io.Writer,
+44
View File
@@ -610,6 +610,50 @@ func TestKeepScrollPositionForNextTask(t *testing.T) {
assert.EqualValues(t, 0, beforeStartCount.Load())
}
// A view that has been emptied is showing nothing, so the render it was showing is no
// longer the one to compare the next task against: running the same command again is
// putting content into the view that isn't there any more, and starts from the top.
func TestForgetRenderedContent(t *testing.T) {
resetOrigin, getResetOriginCallCount := getCounter()
manager := NewViewBufferManager(
utils.NewDummyLog(),
io.Discard,
func() {}, // beforeStart
func() {}, // refreshView
func() {}, // onEndOfInput
resetOrigin,
func() {}, // beginRender
func() {}, // swapInRender
func() gocui.Task { return gocui.NewFakeTask() },
// no UI thread in the test; run the view mutations inline
func(f func()) error { f(); return nil },
)
runTaskToCompletion := func(key string) {
start := func() (Cmd, io.Reader) {
// not actually starting this because it's not necessary
return ExecCmd{Cmd: exec.Command("blah")}, &BlankLineReader{totalLinesToYield: 3}
}
done := make(chan struct{})
_ = manager.NewTask(
manager.NewCmdTask(start, "", LinesToRead{100, 50, nil}, func() { close(done) }), key)
<-done
}
runTaskToCompletion("cmd1")
assert.Equal(t, 1, getResetOriginCallCount())
// Rendering the same command's output again leaves the view where it is, that being
// what it already shows.
runTaskToCompletion("cmd1")
assert.Equal(t, 1, getResetOriginCallCount())
manager.ForgetRenderedContent()
runTaskToCompletion("cmd1")
assert.Equal(t, 2, getResetOriginCallCount(), "an emptied view is shown its content afresh")
}
func TestNewCmdTaskRefresh(t *testing.T) {
type scenario struct {
name string