diff --git a/pkg/gui/controllers/helpers/diff_line_navigation.go b/pkg/gui/controllers/helpers/diff_line_navigation.go index a448ceaa1..d57fd325e 100644 --- a/pkg/gui/controllers/helpers/diff_line_navigation.go +++ b/pkg/gui/controllers/helpers/diff_line_navigation.go @@ -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 diff --git a/pkg/gui/controllers/helpers/staging_helper.go b/pkg/gui/controllers/helpers/staging_helper.go index a5a1fbb47..82cd4ce75 100644 --- a/pkg/gui/controllers/helpers/staging_helper.go +++ b/pkg/gui/controllers/helpers/staging_helper.go @@ -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) diff --git a/pkg/integration/tests/staging/select_next_line_after_staging_line_from_main_view.go b/pkg/integration/tests/staging/select_next_line_after_staging_line_from_main_view.go new file mode 100644 index 000000000..806807d16 --- /dev/null +++ b/pkg/integration/tests/staging/select_next_line_after_staging_line_from_main_view.go @@ -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"), + ) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 38ff367a2..27171812c 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -418,6 +418,7 @@ var tests = []*components.IntegrationTest{ staging.SelectNextHunkAfterUnstagingFromMainView, staging.SelectNextLineAfterStagingInTwoHunkDiff, staging.SelectNextLineAfterStagingIsolatedAddedLine, + staging.SelectNextLineAfterStagingLineFromMainView, staging.StageHunkFromMainView, staging.StageHunks, staging.StageHunksWithRapidKeypresses,