Advance to the next hunk after toggling one into a custom patch

Staging a hunk from the focused main view advances the selection to the next
hunk, because staging removes the acted-on lines from the diff so the preserved
change-line ordinal lands on the next change. Toggling a hunk into a custom
patch left the selection sitting on the just-toggled hunk instead — the toggle
doesn't change the diff (only the inclusion set), so the same ordinal lands back
where it was, and you had to navigate by hand to build a patch hunk by hunk.

Give the toggle the same feel by advancing the reveal ordinal past the toggled
change lines (RevealSelectionAfterStaging gains an advanceBy arg, fed the toggled
change-line count). Staging and removal still pass 0 (their lines are consumed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller 2026-06-24 17:30:54 +02:00
parent 49df8caf82
commit ece5f0fb66
8 changed files with 40 additions and 29 deletions

View file

@ -592,7 +592,7 @@ func (self *FilesController) applyDiffLineSelection(mainViewName string, firstLi
// The Refresh above queued the main-view re-render; re-establish the selection at its
// change-line ordinal once that render lands, and focus the pane now holding the
// acted-on side.
revealSelectionAfterPrimaryAction(self.c, mainViewName, focusViewName, firstLineIdx)
revealSelectionAfterPrimaryAction(self.c, mainViewName, focusViewName, firstLineIdx, 0)
if focusViewName != mainViewName {
self.c.Context().Push(mainContextForViewName(self.c, focusViewName), types.OnFocusOpts{})
}

View file

@ -354,12 +354,18 @@ func (self *StagingHelper) RestoreFocusedMainViewOnEscape(explorerView, mainView
// 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); the ordinal is
// preserved across that move since both panes show the same side.
func (self *StagingHelper) RevealSelectionAfterStaging(sourceView *gocui.View, targetView *gocui.View, firstLine int, place func(viewLine int)) {
//
// advanceBy shifts the landed ordinal forward by that many change lines. Staging and
// unstaging consume the acted-on lines, so the preserved ordinal (advanceBy 0) already
// lands on the next change. A custom-patch toggle leaves the diff unchanged, so preserving
// the ordinal would land back on the just-toggled lines; passing the toggled change-line
// count advances past them to the next stageable hunk/line, matching the staging feel.
func (self *StagingHelper) RevealSelectionAfterStaging(sourceView *gocui.View, targetView *gocui.View, firstLine int, advanceBy int, place func(viewLine int)) {
ordinal, ok := self.changeLineOrdinal(sourceView, firstLine)
if !ok {
return
}
self.revealChangeLineAtOrdinal(targetView, ordinal, place)
self.revealChangeLineAtOrdinal(targetView, ordinal+advanceBy, place)
}
// changeLineOrdinal returns how many change lines precede the (change) line at

View file

@ -488,8 +488,11 @@ func (self *MainViewController) usingExternalDiff() bool {
// sourceViewName and targetViewName are usually the same pane, but staging can move the
// acted-on side to the other pane (passing that pane as the target). The target inherits
// the (collapsed) select mode; a range collapses back to a single line, hunk mode stays
// on to land on the next hunk.
func revealSelectionAfterPrimaryAction(c *ControllerCommon, sourceViewName string, targetViewName string, firstLineIdx int) {
// on to land on the next hunk. advanceBy is forwarded to RevealSelectionAfterStaging: 0 for
// staging/removal (the acted-on lines are consumed), the toggled change-line count for a
// custom-patch toggle (which leaves the diff intact, so the selection must be advanced past
// the just-toggled lines to land on the next stageable hunk).
func revealSelectionAfterPrimaryAction(c *ControllerCommon, sourceViewName string, targetViewName string, firstLineIdx int, advanceBy int) {
sourceContext := mainContextForViewName(c, sourceViewName)
targetContext := mainContextForViewName(c, targetViewName)
@ -503,7 +506,7 @@ func revealSelectionAfterPrimaryAction(c *ControllerCommon, sourceViewName strin
sourceView := sourceContext.GetView()
targetView := targetContext.GetView()
c.Helpers().Staging.RevealSelectionAfterStaging(sourceView, targetView, firstLineIdx, func(viewLine int) {
c.Helpers().Staging.RevealSelectionAfterStaging(sourceView, targetView, firstLineIdx, advanceBy, func(viewLine int) {
if mode == context.DiffSelectModeHunk {
selectDiffHunk(c, targetContext, viewLine)
} else {
@ -558,7 +561,7 @@ func preserveFocusedMainViewSelectionAcrossContentChange(c *ControllerCommon, ma
// caller, so a range collapses to its top and the ordinal lands on the nearest surviving
// change there.
first, _ := view.SelectedLineRange()
revealSelectionAfterPrimaryAction(c, mainContext.GetViewName(), mainContext.GetViewName(), first)
revealSelectionAfterPrimaryAction(c, mainContext.GetViewName(), mainContext.GetViewName(), first, 0)
}
// diffTaskCommandKey returns the buffer-manager task key a diff render task will register

View file

@ -76,9 +76,10 @@ func removePatchLinesFromFocusedMainView(
}
refresh()
// A removal doesn't change the diff command, so source and target are the same pane;
// the re-render still moves the selection in view-line space, so re-establish it.
revealSelectionAfterPrimaryAction(c, mainViewName, mainViewName, firstLineIdx)
// A removal shrinks the patch shown in the secondary, so the removed lines are consumed
// from its diff; preserving the ordinal (advanceBy 0) lands on the next surviving line,
// like unstaging.
revealSelectionAfterPrimaryAction(c, mainViewName, mainViewName, firstLineIdx, 0)
return nil
}
@ -132,10 +133,12 @@ func togglePatchFromFocusedMainView(
}
refresh()
// A toggle doesn't change the diff, so source and target are the same pane;
// the re-render (and the layout re-wrap when the secondary view first appears)
// still moves the selection in view-line space, so re-establish it.
revealSelectionAfterPrimaryAction(c, mainViewName, mainViewName, firstLineIdx)
// A toggle doesn't change the diff, so source and target are the same pane; the
// re-render (and the layout re-wrap when the secondary view first appears) still
// moves the selection in view-line space, so re-establish it — advanced past the
// just-toggled change lines to the next stageable hunk/line, since they aren't
// consumed (the diff is unchanged), unlike staging.
revealSelectionAfterPrimaryAction(c, mainViewName, mainViewName, firstLineIdx, len(infos))
return nil
},
})

View file

@ -62,12 +62,13 @@ var BuildFromMainView = NewIntegrationTest(NewIntegrationTestArgs{
Contains("+THREE"),
).
PressPrimaryAction().
// The selection is re-established on the same hunk after the toggle's
// After toggling the block in, the selection advances to the next stageable
// hunk (the NINE block) — like staging advances to the next hunk — across the
// re-render (which, when the secondary view first appears, also re-wraps the
// narrower diff).
SelectedLines(
Contains("-three"),
Contains("+THREE"),
Contains("-nine"),
Contains("+NINE"),
)
t.Views().Information().Content(Contains("Building patch"))

View file

@ -61,12 +61,12 @@ var BuildFromWholeCommitMainView = NewIntegrationTest(NewIntegrationTestArgs{
Contains("+THREE"),
).
PressPrimaryAction().
// The selection is re-established on the same block after the toggle's
// re-render (which, when the secondary view first appears, re-wraps the
// narrower diff).
// After toggling the block in, the selection advances to the next stageable
// block — file2's BETA block — across the re-render (which, when the secondary
// view first appears, re-wraps the narrower diff).
SelectedLines(
Contains("-three"),
Contains("+THREE"),
Contains("-beta"),
Contains("+BETA"),
)
t.Views().Information().Content(Contains("Building patch"))

View file

@ -48,12 +48,8 @@ var BuildMultiFileFromWholeCommitMainView = NewIntegrationTest(NewIntegrationTes
Contains("+THREE"),
).
PressPrimaryAction().
SelectedLines(
Contains("-three"),
Contains("+THREE"),
).
// Move to the next change block, which is in file2, and toggle it in too.
Press(keys.Universal.NextItem).
// Toggling the block in advances the selection to the next stageable block,
// which crosses the file boundary into file2's BETA block; toggle it in too.
SelectedLines(
Contains("-beta"),
Contains("+BETA"),

View file

@ -37,8 +37,10 @@ var KeepSelectionAfterMovingPatchOutMainView = NewIntegrationTest(NewIntegration
// Toggle just the first line into a custom patch, then leave a multi-line
// range selected — the patch move below doesn't go through the focused-main-
// view action handlers, so without the preserve net this stale range would be
// left painted over the shrunk diff.
// left painted over the shrunk diff. (Toggling advances the selection to the
// next line, so go back to 'one' to anchor a range that spans the toggled line.)
PressPrimaryAction().
NavigateToLine(Contains("+one")).
Press(keys.Universal.ToggleRangeSelect).
NavigateToLine(Contains("+four")).
SelectedLines(