From 6b38e8fcde1fd51970bcf08876a09c0a98c5645a Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Thu, 1 Oct 2026 16:42:10 +0200 Subject: [PATCH 1/2] Demonstrate that git's diffstat keeps the width from before a screen mode change Pressing + in the commits panel moves to half-screen mode and renders the commit's diff again in the narrower main view. With git's own diff, the diffstat graph comes out as wide as the view was before, and wraps onto rows of its own. ContainsViewLines asserts on the rows the view draws, after wrapping. The existing line assertions see the lines of the content and can't tell a wrapped line from one that fits. Co-Authored-By: Claude Opus 5.5 (1M context) --- pkg/integration/components/view_driver.go | 33 +++++++++++ .../diff/stat_follows_a_screen_mode_change.go | 58 +++++++++++++++++++ pkg/integration/tests/test_list.go | 1 + 3 files changed, 92 insertions(+) create mode 100644 pkg/integration/tests/diff/stat_follows_a_screen_mode_change.go diff --git a/pkg/integration/components/view_driver.go b/pkg/integration/components/view_driver.go index dfae58c4b..ca29eff69 100644 --- a/pkg/integration/components/view_driver.go +++ b/pkg/integration/components/view_driver.go @@ -141,6 +141,39 @@ func (self *ViewDriver) VisibleLines(matchers ...*TextMatcher) *ViewDriver { return self.assertLines(originY, matchers...) } +// ContainsViewLines asserts that somewhere in the view there are consecutive view lines +// matching the given matchers. View lines are the rows the view draws its content in, +// so a line too long for the view is as many view lines as the view wraps it into. +func (self *ViewDriver) ContainsViewLines(matchers ...*TextMatcher) *ViewDriver { + self.validateMatchersPassed(matchers) + + self.t.assertWithRetries(func() (bool, string) { + lines := self.getView().ViewBufferLines() + + for i := 0; i+len(matchers) <= len(lines); i++ { + matches := true + for j, matcher := range matchers { + if ok, _ := matcher.test(lines[i+j]); !ok { + matches = false + break + } + } + if matches { + return true, "" + } + } + + return false, fmt.Sprintf( + "%s: Expected the following view lines to be contained:\n-----\n%s\n-----\nBut got:\n-----\n%s\n-----", + self.context, + expectedContentFromMatchers(matchers), + strings.Join(lines, "\n"), + ) + }) + + return self +} + // asserts that somewhere in the view there are consecutive lines matching the given matchers. func (self *ViewDriver) ContainsLines(matchers ...*TextMatcher) *ViewDriver { self.validateMatchersPassed(matchers) diff --git a/pkg/integration/tests/diff/stat_follows_a_screen_mode_change.go b/pkg/integration/tests/diff/stat_follows_a_screen_mode_change.go new file mode 100644 index 000000000..62880c5e1 --- /dev/null +++ b/pkg/integration/tests/diff/stat_follows_a_screen_mode_change.go @@ -0,0 +1,58 @@ +package diff + +import ( + "strconv" + "strings" + + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var StatFollowsAScreenModeChange = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "A commit's diffstat is laid out again to the width the view has after a screen mode change narrows it", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(cfg *config.AppConfig) { + // git's own diff, so that the render runs as a plain command rather + // than through a renderer. + cfg.GetUserConfig().Git.DiffRenderers = []config.DiffRendererConfig{ + {Type: "rawGit"}, + } + }, + SetupRepo: func(shell *Shell) { + // Enough added lines that git scales the graph to the width it has, + // rather than drawing one mark per line. + lines := make([]string, 200) + for i := range lines { + lines[i] = "line " + strconv.Itoa(i) + } + shell.CreateFileAndAdd("file1", strings.Join(lines, "\n")+"\n") + shell.Commit("add file1") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + Lines( + Contains("add file1").IsSelected(), + ) + + t.Views().Main(). + ContainsViewLines( + Contains("file1 | 200"), + Contains("1 file changed"), + ) + + // The focused panel takes half the screen, which leaves the main view + // narrower than it was. + t.Views().Commits().Press(keys.Universal.NextScreenMode) + + t.Views().Main(). + ContainsViewLines( + Contains("file1 | 200"), + /* EXPECTED: + Contains("1 file changed"), + ACTUAL: */ + MatchesRegexp(`^\++$`), + ) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 372b82c8f..67caf3bbb 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -231,6 +231,7 @@ var tests = []*components.IntegrationTest{ diff.IgnoreWhitespace, diff.RenameSimilarityThresholdChange, diff.RenderThroughAPipe, + diff.StatFollowsAScreenModeChange, diff.StatUsesTheViewWidth, file.ClickArrowToCollapse, file.CollapseExpand, From 0afb94e97b42d7b639a5a0c282aac8ddd48dde13 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Thu, 1 Oct 2026 16:50:48 +0200 Subject: [PATCH 2/2] Lay git's own diff out to the width the layout gives the view Changing the screen mode renders the main view again, and so does anything else that changes its size along with what it shows. With git's own diff, that render laid a commit's diffstat out to the width the view had before. The graph then wrapped onto rows of its own in a view that got narrower, and stopped short in one that got wider. A diff renderer's render is created after the layout pass, since only the layout settles the view's size, and takes its width there. git's own diff took its width straight away instead, when the render was asked for. Create its task after the layout as well. Co-Authored-By: Claude Opus 5.5 (1M context) --- pkg/gui/tasks_adapter.go | 14 ++++++++++---- .../diff/stat_follows_a_screen_mode_change.go | 3 --- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pkg/gui/tasks_adapter.go b/pkg/gui/tasks_adapter.go index ed8203237..7a67f4447 100644 --- a/pkg/gui/tasks_adapter.go +++ b/pkg/gui/tasks_adapter.go @@ -27,11 +27,17 @@ func (gui *Gui) newCmdTask(view *gocui.View, cmd *exec.Cmd, prefix string) error // thumb to shrink and snap back as the rest loads. view.FreezeScrollbarHeight() - // Snapshot the view width here, on the UI thread, so the task goroutine - // doesn't read the view's live dimensions while it streams output. - spec := renderSpec{view: view, cmd: cmd, width: view.InnerWidth()} + // The command lays its output out to the width of the view (git's diffstat + // graph does), and only the layout settles that, so the task is created after + // it, as a diff renderer's is. Taking the width there, on the UI thread, also + // keeps the task goroutine from reading the view's live dimensions while it + // streams output. + gui.afterLayout(func() error { + spec := renderSpec{view: view, cmd: cmd, width: view.InnerWidth()} + return gui.newTaskForRender(spec, prefix, cmdStr, gui.plainRender) + }) - return gui.newTaskForRender(spec, prefix, cmdStr, gui.plainRender) + return nil } // plainRender runs the command as it is, with its output going straight into diff --git a/pkg/integration/tests/diff/stat_follows_a_screen_mode_change.go b/pkg/integration/tests/diff/stat_follows_a_screen_mode_change.go index 62880c5e1..1c44caa62 100644 --- a/pkg/integration/tests/diff/stat_follows_a_screen_mode_change.go +++ b/pkg/integration/tests/diff/stat_follows_a_screen_mode_change.go @@ -49,10 +49,7 @@ var StatFollowsAScreenModeChange = NewIntegrationTest(NewIntegrationTestArgs{ t.Views().Main(). ContainsViewLines( Contains("file1 | 200"), - /* EXPECTED: Contains("1 file changed"), - ACTUAL: */ - MatchesRegexp(`^\++$`), ) }, })