Keep your place in the diff when switching diff renderers

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) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller
2026-10-05 10:30:55 +02:00
co-authored by Claude Opus 5
parent 0eeb8adcbf
commit f1f5479d47
5 changed files with 166 additions and 4 deletions
+1 -1
View File
@@ -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,
+10 -3
View File
@@ -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()
}
}
@@ -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)
},
})
@@ -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)
},
})
+2
View File
@@ -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,