Let the post-stage reveal target a different pane than the one acted on

Staging or unstaging can move the acted-on side to the other half of the
focused main view — unstaging the first hunk of an only-staged file splits
it, pushing the staged remainder to the secondary pane. The reveal that
re-selects the nearest surviving change after the op then has to ride the
re-render of that other pane, not the one the user acted in.

Split RevealSelectionAfterStaging's single view into a source (where the
candidate lines are read) and a target (where they're found again and
re-selected), and get-or-create the target's buffer manager so the restore
can be installed on a pane that hasn't rendered yet. Behavior-preserving:
the sole caller passes the same view for both, as before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller 2026-06-18 15:24:45 +02:00
parent e58f6d43ad
commit a2e1254a96
4 changed files with 28 additions and 10 deletions

View file

@ -270,32 +270,39 @@ func (self *StagingHelper) RestoreFocusedMainViewOnEscape(explorerView, mainView
// focused main view's diff, for the selection to land on the change nearest the one
// just acted on rather than at a stale (and possibly off-content) position — the same
// "advance to the next change" the staging view does. firstLine and lastLine bound
// the just-staged selection in the current (pre-staging) diff; place positions and
// re-selects the landed line in the current select mode. Install it before triggering
// the re-render: it rides the next render of view, like RestoreFocusedMainViewOnEscape.
// the just-staged selection in sourceView's current (pre-staging) diff; place
// positions and re-selects the landed line in the current select mode. Install it
// before triggering the re-render: it rides the next render of targetView, like
// RestoreFocusedMainViewOnEscape.
//
// sourceView is the pane the user acted in; targetView is the pane to re-select in.
// They are usually the same, but staging or unstaging can move the acted-on side to
// the other pane — e.g. unstaging the first hunk of an only-staged file splits it,
// pushing the staged remainder to the secondary half — in which case the candidate
// lines, captured from sourceView, are found again in targetView's re-render.
//
// The candidates, in priority order, are the change block after the selection, then
// the one before it (both survive staging — only the selection itself was staged),
// then the selection's own first line, which only turns up when the whole side was
// staged and the view flips to show the other (staged) side. If none survives, place
// isn't called and the view re-renders without moving the selection.
func (self *StagingHelper) RevealSelectionAfterStaging(view *gocui.View, firstLine int, lastLine int, place func(viewLine int)) {
func (self *StagingHelper) RevealSelectionAfterStaging(sourceView *gocui.View, targetView *gocui.View, firstLine int, lastLine int, place func(viewLine int)) {
var candidates []diffLineAnchor
addByViewLine := func(viewLine int) {
if info, ok := self.GetDiffLineInfoForView(view, viewLine); ok {
if info, ok := self.GetDiffLineInfoForView(sourceView, viewLine); ok {
candidates = append(candidates, diffLineAnchor{identity: info})
}
}
if next, ok := self.AdjacentChangeBlock(view, lastLine, true); ok {
if next, ok := self.AdjacentChangeBlock(sourceView, lastLine, true); ok {
addByViewLine(next)
}
if prev, ok := self.AdjacentChangeBlock(view, firstLine, false); ok {
if prev, ok := self.AdjacentChangeBlock(sourceView, firstLine, false); ok {
addByViewLine(prev)
}
addByViewLine(firstLine)
self.restoreDiffLinePositionOnRerender(view, candidates, func(_ diffLineAnchor, viewLine int) {
self.restoreDiffLinePositionOnRerender(targetView, candidates, func(_ diffLineAnchor, viewLine int) {
place(viewLine)
})
}
@ -325,7 +332,10 @@ func (self *StagingHelper) restoreDiffLinePositionOnRerender(view *gocui.View, c
if len(candidates) == 0 {
return
}
manager := self.c.GetViewBufferManagerForView(view)
// Get-or-create: the target pane may not have rendered yet (the secondary half
// when a stage/unstage first splits the diff), so the restore has to be set on a
// manager that the upcoming render will then reuse.
manager := self.c.GetOrCreateViewBufferManagerForView(view)
if manager == nil {
return
}

View file

@ -296,7 +296,7 @@ func (self *MainViewController) stageSelectedLine() error {
// The diff re-renders after staging; install a restore (before the handler
// triggers that re-render) so the selection lands on the next change rather than
// at a now-meaningless position.
self.c.Helpers().Staging.RevealSelectionAfterStaging(v, first, last, func(viewLine int) {
self.c.Helpers().Staging.RevealSelectionAfterStaging(v, v, first, last, func(viewLine int) {
if self.sel().Mode == context.DiffSelectModeHunk {
self.selectHunkAround(viewLine)
} else {

View file

@ -185,6 +185,10 @@ func (self *guiCommon) ReadLinesToFillView(view *gocui.View) {
self.gui.readLinesToFillView(view)
}
func (self *guiCommon) GetOrCreateViewBufferManagerForView(view *gocui.View) *tasks.ViewBufferManager {
return self.gui.getManager(view)
}
func (self *guiCommon) State() types.IStateAccessor {
return self.gui.stateAccessor
}

View file

@ -67,6 +67,10 @@ type IGuiCommon interface {
// return the view buffer manager for the given view, or nil if it doesn't have one
GetViewBufferManagerForView(view *gocui.View) *tasks.ViewBufferManager
// return the view buffer manager for the given view, creating it if the view
// hasn't rendered yet. Used to install a render restore on a pane that is about
// to appear (e.g. the secondary half when a stage/unstage splits the diff).
GetOrCreateViewBufferManagerForView(view *gocui.View) *tasks.ViewBufferManager
// read enough lines into the given view's buffer to fill it at its current
// scroll position, plus some read-ahead for smooth scrolling