From f1f5479d47ac1653e335c3e2fb0ea0a34eaf51b6 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 27 Sep 2026 11:37:33 +0200 Subject: [PATCH] Keep your place in the diff when switching diff renderers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cycling through the diff renderers is for comparing how they show the same change, which is hard to do from the top of the diff each time. The line you were on is the same line of the same file whichever renderer draws it, so the restore finds it again — by the records a renderer states, where it speaks the protocol, and by parsing its output as a diff otherwise. The restore sits in DiffHelper.RenderToMainAgain. A switch between a dark and a light terminal background renders the diff again through the same helper, so it keeps your place as well. Co-authored-by: Claude Opus 5 (1M context) --- pkg/gui/controllers.go | 2 +- pkg/gui/controllers/helpers/diff_helper.go | 13 +++- ...oth_panes_when_switching_diff_renderers.go | 78 +++++++++++++++++++ ..._position_when_switching_diff_renderers.go | 75 ++++++++++++++++++ pkg/integration/tests/test_list.go | 2 + 5 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 pkg/integration/tests/main_view/keep_position_in_both_panes_when_switching_diff_renderers.go create mode 100644 pkg/integration/tests/main_view/keep_position_when_switching_diff_renderers.go diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go index 8668277c6..b7afdf918 100644 --- a/pkg/gui/controllers.go +++ b/pkg/gui/controllers.go @@ -67,8 +67,8 @@ func (gui *Gui) resetHelpersAndControllers() { worktreeHelper, searchHelper, ) - diffHelper := helpers.NewDiffHelper(helperCommon) diffLineHelper := helpers.NewDiffLineHelper(helperCommon) + diffHelper := helpers.NewDiffHelper(helperCommon, diffLineHelper) cherryPickHelper := helpers.NewCherryPickHelper( helperCommon, rebaseHelper, diff --git a/pkg/gui/controllers/helpers/diff_helper.go b/pkg/gui/controllers/helpers/diff_helper.go index 02d8f005c..0547fafa3 100644 --- a/pkg/gui/controllers/helpers/diff_helper.go +++ b/pkg/gui/controllers/helpers/diff_helper.go @@ -14,12 +14,14 @@ import ( ) type DiffHelper struct { - c *HelperCommon + c *HelperCommon + diffLineHelper *DiffLineHelper } -func NewDiffHelper(c *HelperCommon) *DiffHelper { +func NewDiffHelper(c *HelperCommon, diffLineHelper *DiffLineHelper) *DiffHelper { return &DiffHelper{ - c: c, + c: c, + diffLineHelper: diffLineHelper, } } @@ -107,6 +109,11 @@ func (self *DiffHelper) RenderToMainAgain() { if currentSide.GetKey() == currentKey || currentKey == context.NORMAL_MAIN_CONTEXT_KEY || currentKey == context.NORMAL_SECONDARY_CONTEXT_KEY { + // Whatever changed can make the diff come out differently, such as a new + // renderer laying it out its own way, so the line you were looking at could + // end up anywhere in the view; keep it in front of you. + self.diffLineHelper.PreserveDiffPositionOnRerender(self.c.Contexts().Normal.GetView()) + self.diffLineHelper.PreserveDiffPositionOnRerender(self.c.Contexts().NormalSecondary.GetView()) currentSide.HandleRenderToMain() } } diff --git a/pkg/integration/tests/main_view/keep_position_in_both_panes_when_switching_diff_renderers.go b/pkg/integration/tests/main_view/keep_position_in_both_panes_when_switching_diff_renderers.go new file mode 100644 index 000000000..9f562f7db --- /dev/null +++ b/pkg/integration/tests/main_view/keep_position_in_both_panes_when_switching_diff_renderers.go @@ -0,0 +1,78 @@ +package main_view + +import ( + "fmt" + "strings" + + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var KeepPositionInBothPanesWhenSwitchingDiffRenderers = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Switching to another diff renderer keeps the place in the lower pane too, not only in the upper one", + ExtraCmdArgs: []string{}, + Skip: false, + Width: 120, + Height: 30, + SetupConfig: func(cfg *config.AppConfig) { + cfg.GetUserConfig().Gui.UseHunkModeInStagingView = false + // Renderers that speak the metadata protocol, so that focusing the main view + // keeps their rendering rather than falling back to git's own diff. + cfg.GetUserConfig().Git.DiffRenderers = []config.DiffRendererConfig{ + {Name: "plain", Command: `printf '\033]1717;1\007'; cat`}, + // The same diff, three lines further down the view. (Lines before the + // diff's own header aren't part of it, so it still reads the same.) + {Name: "banner", Command: `printf '\033]1717;1\007'; printf 'rendered for you\n\n\n'; cat`}, + } + }, + SetupRepo: func(shell *Shell) { + lines := make([]string, 40) + for i := range lines { + lines[i] = fmt.Sprintf("line%02d", i+1) + } + shell.CreateFileAndAdd("file1", strings.Join(lines, "\n")+"\n") + shell.Commit("one") + + // Four staged changes to have a diff worth scrolling in the lower pane, and one + // unstaged one to split the file's diff across both panes. + for _, i := range []int{5, 15, 25, 35} { + lines[i-1] = strings.ToUpper(lines[i-1]) + } + shell.UpdateFile("file1", strings.Join(lines, "\n")+"\n") + shell.GitAddAll() + + lines[39] = strings.ToUpper(lines[39]) + shell.UpdateFile("file1", strings.Join(lines, "\n")+"\n") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Press(keys.Universal.FocusMainView) + + t.Views().Main(). + IsFocused(). + PressTab() + + t.Views().Secondary(). + IsFocused(). + Press(keys.Main.NextHunk). + Press(keys.Main.NextHunk). + Press(keys.Main.NextHunk). + SelectedLines( + Contains("-line35"), + ). + SelectedLineIdx(35). + OriginY(14). + Press(keys.Universal.CycleDiffRenderers). + Tap(func() { + t.ExpectToast(Equals("Diff renderer: banner (2 of 2)")) + }). + // The banner pushed the whole diff three lines down, and the lower pane came + // along with it, just as the upper one would have. + SelectedLines( + Contains("-line35"), + ). + SelectedLineIdx(38). + OriginY(17) + }, +}) diff --git a/pkg/integration/tests/main_view/keep_position_when_switching_diff_renderers.go b/pkg/integration/tests/main_view/keep_position_when_switching_diff_renderers.go new file mode 100644 index 000000000..7241ac9bf --- /dev/null +++ b/pkg/integration/tests/main_view/keep_position_when_switching_diff_renderers.go @@ -0,0 +1,75 @@ +package main_view + +import ( + "fmt" + "strings" + + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var KeepPositionWhenSwitchingDiffRenderers = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Switching to another diff renderer keeps the line you were looking at where it was", + ExtraCmdArgs: []string{}, + Skip: false, + Width: 120, + Height: 30, + SetupConfig: func(cfg *config.AppConfig) { + cfg.GetUserConfig().Gui.UseHunkModeInStagingView = false + cfg.GetUserConfig().Git.DiffRenderers = []config.DiffRendererConfig{ + {Name: "plain", Command: "cat"}, + // The same diff, three lines further down the view. (Lines before the + // diff's own header aren't part of it, so it still reads the same.) + {Name: "banner", Command: `printf 'rendered for you\n\n\n'; cat`}, + } + }, + SetupRepo: func(shell *Shell) { + lines := make([]string, 40) + for i := range lines { + lines[i] = fmt.Sprintf("line%02d", i+1) + } + shell.CreateFileAndAdd("file1", strings.Join(lines, "\n")+"\n") + shell.Commit("one") + + for _, i := range []int{5, 15, 25, 35} { + lines[i-1] = strings.ToUpper(lines[i-1]) + } + shell.UpdateFile("file1", strings.Join(lines, "\n")+"\n") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Press(keys.Universal.FocusMainView) + + t.Views().Main(). + IsFocused(). + Press(keys.Main.NextHunk). + Press(keys.Main.NextHunk). + Press(keys.Main.NextHunk). + SelectedLines( + Contains("-line35"), + ). + SelectedLineIdx(35). + OriginY(14). + Press(keys.Universal.CycleDiffRenderers). + Tap(func() { + t.ExpectToast(Equals("Diff renderer: banner (2 of 2)")) + }). + // The banner pushed the whole diff three lines down, and the view came + // along with it: the same line on the same screen row (38 - 17 = 21). + SelectedLines( + Contains("-line35"), + ). + SelectedLineIdx(38). + OriginY(17). + Press(keys.Universal.CycleDiffRenderers). + Tap(func() { + t.ExpectToast(Equals("Diff renderer: plain (1 of 2)")) + }). + SelectedLines( + Contains("-line35"), + ). + SelectedLineIdx(35). + OriginY(14) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index ab8978354..c6d77a8b5 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -372,7 +372,9 @@ var tests = []*components.IntegrationTest{ main_view.FileNavigationScrollsToTheTop, main_view.HideSelectionWhenChangesVanish, main_view.KeepPositionInBothPanesWhenChangingContextSize, + main_view.KeepPositionInBothPanesWhenSwitchingDiffRenderers, main_view.KeepPositionWhenChangingContextSize, + main_view.KeepPositionWhenSwitchingDiffRenderers, main_view.KeepPositionWhenTheSelectionIsOffScreen, main_view.KeepScrollWhenTheDiffCantBeRead, main_view.KeepSelectionVisibleWhenDiffShrinks,