From a2e1254a9687d6a275f146792e23a1ebeb8f8b26 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Thu, 18 Jun 2026 15:24:45 +0200 Subject: [PATCH] Let the post-stage reveal target a different pane than the one acted on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- pkg/gui/controllers/helpers/staging_helper.go | 28 +++++++++++++------ pkg/gui/controllers/main_view_controller.go | 2 +- pkg/gui/gui_common.go | 4 +++ pkg/gui/types/common.go | 4 +++ 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/pkg/gui/controllers/helpers/staging_helper.go b/pkg/gui/controllers/helpers/staging_helper.go index b49eccbe8..f4aaedc6c 100644 --- a/pkg/gui/controllers/helpers/staging_helper.go +++ b/pkg/gui/controllers/helpers/staging_helper.go @@ -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 } diff --git a/pkg/gui/controllers/main_view_controller.go b/pkg/gui/controllers/main_view_controller.go index 4fdc4c954..3e85a86d2 100644 --- a/pkg/gui/controllers/main_view_controller.go +++ b/pkg/gui/controllers/main_view_controller.go @@ -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 { diff --git a/pkg/gui/gui_common.go b/pkg/gui/gui_common.go index e7b14ba04..a2e2b089d 100644 --- a/pkg/gui/gui_common.go +++ b/pkg/gui/gui_common.go @@ -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 } diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index 75cb53018..8883df996 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -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