Advance to the next change line, not the next block, in the reveal

The post-stage reveal picked its primary candidate with AdjacentChangeBlock,
which skips the rest of the acted-on line's block. That's right in hunk mode,
where the whole block is staged, but wrong in line mode: staging the first
line of a multi-line block left the rest of the block in place, yet the
selection jumped past it to the next block instead of landing on the next
(now first) line of the same block.

Use AdjacentChangeLine — the first change line strictly after/before the
selection — for the next/previous candidates. It unifies both modes: after a
whole block (whose last line is followed by context) the next change line is
the next block's, so hunk mode is unchanged; after a single line the next
change line is the next line of the same block. place stays mode-aware
(hunk-expand vs single line), so nothing else changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller 2026-06-18 21:31:09 +02:00
parent df09df1940
commit f461378598
4 changed files with 78 additions and 5 deletions

View file

@ -32,6 +32,32 @@ func (self *StagingHelper) AdjacentChangeBlock(view *gocui.View, anchorViewLine
return view.ViewLineForBufferLine(target)
}
// AdjacentChangeLine returns the view line of the first change line strictly
// after (forward) or before anchorViewLine in view's displayed diff, or ok=false
// when there's none. Unlike AdjacentChangeBlock it doesn't skip the rest of the
// anchor's own block: the next change line after a line in the middle of a block is
// the very next line of that block. The post-stage reveal uses it so that staging a
// single line advances to the next line of the same block, while staging a whole
// block (whose last line is followed by context) still advances to the next block.
func (self *StagingHelper) AdjacentChangeLine(view *gocui.View, anchorViewLine int, forward bool) (int, bool) {
anchor, ok := view.BufferLineForViewLine(anchorViewLine)
if !ok {
return 0, false
}
resolved := self.resolveDiffLines(view.DiffLineContents())
step := 1
if !forward {
step = -1
}
for i := anchor + step; i >= 0 && i < len(resolved); i += step {
if resolved[i].ok && resolved[i].info.IsChange() {
return view.ViewLineForBufferLine(i)
}
}
return 0, false
}
// AdjacentFile returns the view line to move to for next/previous file navigation in
// view's (possibly multi-file) displayed diff, starting from anchorViewLine: the
// first row belonging to the next/previous file, found where the per-row metadata's

View file

@ -308,11 +308,15 @@ func (self *StagingHelper) RestoreFocusedMainViewOnEscape(explorerView, mainView
// 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 candidates, in priority order, are the change line 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.
// staged and the view flips to show the other (staged) side. Adjacent change *line*,
// not block: staging a single line should land on the next line of the same block,
// while staging a whole block lands on the next block (its last line is followed by
// context, so the next change line is the next block's). place — line- or hunk-mode —
// then selects from there. If none survives, place isn't called and the view
// re-renders without moving the selection.
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) {
@ -321,10 +325,10 @@ func (self *StagingHelper) RevealSelectionAfterStaging(sourceView *gocui.View, t
}
}
if next, ok := self.AdjacentChangeBlock(sourceView, lastLine, true); ok {
if next, ok := self.AdjacentChangeLine(sourceView, lastLine, true); ok {
addByViewLine(next)
}
if prev, ok := self.AdjacentChangeBlock(sourceView, firstLine, false); ok {
if prev, ok := self.AdjacentChangeLine(sourceView, firstLine, false); ok {
addByViewLine(prev)
}
addByViewLine(firstLine)

View file

@ -0,0 +1,42 @@
package staging
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var SelectNextLineAfterStagingLineFromMainView = NewIntegrationTest(NewIntegrationTestArgs{
Description: "In line mode, staging the first line of a block advances the selection to the next line of the same block, not to the next block",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.GetUserConfig().Gui.UseHunkModeInStagingView = false
},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("file1", "1\n2\n3\n4\n5\n6\n7\n8\n9\n")
shell.Commit("one")
// A three-line added block near the top, and a separate change block further
// down (so "next block" is a distinct, wrong target for line mode).
shell.UpdateFile("file1", "1\n2\nA\nB\nC\n3\n4\n5\n6\nX\n8\n9\n")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
IsFocused().
Press(keys.Universal.FocusMainView)
// Default (line) mode: the first added line is selected.
t.Views().Main().
IsFocused().
SelectedLines(
Contains("+A"),
).
// Stage just that line.
PressPrimaryAction().
// The selection advances to the next line of the same block, not to the
// distant change block below.
SelectedLines(
Contains("+B"),
)
},
})

View file

@ -418,6 +418,7 @@ var tests = []*components.IntegrationTest{
staging.SelectNextHunkAfterUnstagingFromMainView,
staging.SelectNextLineAfterStagingInTwoHunkDiff,
staging.SelectNextLineAfterStagingIsolatedAddedLine,
staging.SelectNextLineAfterStagingLineFromMainView,
staging.StageHunkFromMainView,
staging.StageHunks,
staging.StageHunksWithRapidKeypresses,