diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index ab58de7c8..93bc6f379 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -386,18 +386,20 @@ func (self *FilesController) renderWorkingTreeDiff(node *filetree.FileNode) { if showUnstaged { cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, false, paths) refreshOpts.Main = &types.ViewUpdateOpts{ - Task: types.NewRunDiffRendererTask(cmdObj.GetCmd()), - SubTitle: self.c.Helpers().Diff.IgnoringWhitespaceSubTitle(), - Title: self.c.Tr.UnstagedChanges, + Task: types.NewRunDiffRendererTask(cmdObj.GetCmd()), + SubTitle: self.c.Helpers().Diff.IgnoringWhitespaceSubTitle(), + Title: self.c.Tr.UnstagedChanges, + NothingToActOn: !node.GetHasUnstagedChanges(), } } if showStaged { cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, true, paths) refreshOpts.Secondary = &types.ViewUpdateOpts{ - Task: types.NewRunDiffRendererTask(cmdObj.GetCmd()), - SubTitle: self.c.Helpers().Diff.IgnoringWhitespaceSubTitle(), - Title: self.c.Tr.StagedChanges, + Task: types.NewRunDiffRendererTask(cmdObj.GetCmd()), + SubTitle: self.c.Helpers().Diff.IgnoringWhitespaceSubTitle(), + Title: self.c.Tr.StagedChanges, + NothingToActOn: !node.GetHasStagedChanges(), } } diff --git a/pkg/gui/main_panels.go b/pkg/gui/main_panels.go index 637181f4a..ba8e7c200 100644 --- a/pkg/gui/main_panels.go +++ b/pkg/gui/main_panels.go @@ -113,7 +113,7 @@ func (gui *Gui) refreshMainViews(opts types.RefreshMainOpts) { // Before the render is triggered, so that the pane the focus moves into can be // told where to put its selection as it renders. - gui.followFocusIntoShownPane(opts.Pair, panes) + gui.followFocusIntoWorkablePane(opts) gui.moveMainContextPairToTop(opts.Pair) @@ -191,38 +191,38 @@ func mainPanesFor(opts types.RefreshMainOpts) types.MainPanes { } } -// followFocusIntoShownPane moves the focus out of a main pane that the render about to -// happen leaves nothing in, and into the one it does. +// followFocusIntoWorkablePane moves the focus out of a main pane that the render about +// to happen leaves nothing to work on, and into the one it does. // -// Each side of a file's diff has a pane of its own, and a pane is only shown while its -// side has something in it. So anything that empties the side the focus is on takes -// that pane away with it: staging the last unstaged change, committing what was +// Each side of a file's diff has a pane of its own, and a pane holds something only +// while its side of the file does. So anything that empties the side the focus is on +// leaves that pane with nothing: staging the last unstaged change, committing what was // staged, or either of those happening outside lazygit and arriving with a refresh. -// Left where it was, the focus would be on a pane that isn't there, and the next -// keypress would act on nothing. +// Usually the pane goes away with its content; configured to always split the diff it +// stays, empty. Either way the focus has nothing left to act on where it is. // // The pane moved into gets its selection once the render has finished and there is // something to put one on, and shows none until then, so that the selection it was // left with the last time it was used doesn't appear for a frame. A pane that has // already been told where to put its selection — by the action that caused all this — // keeps what it was told. -func (gui *Gui) followFocusIntoShownPane(pair types.MainContextPair, panes types.MainPanes) { +func (gui *Gui) followFocusIntoWorkablePane(opts types.RefreshMainOpts) { // The focused main view's two panes only: the staging and patch-building views // arrange theirs for themselves, and the merge-conflicts view has just the one. - if pair.Main.GetKey() != context.NORMAL_MAIN_CONTEXT_KEY { + if opts.Pair.Main.GetKey() != context.NORMAL_MAIN_CONTEXT_KEY { return } current := gui.State.ContextMgr.CurrentStatic().GetKey() - if current != pair.Main.GetKey() && current != pair.Secondary.GetKey() { + if current != opts.Pair.Main.GetKey() && current != opts.Pair.Secondary.GetKey() { return } - shown := onlyShownPane(pair, panes) - if shown == nil || shown.GetKey() == current { + pane := onlyWorkablePane(opts) + if pane == nil || pane.GetKey() == current { return } - target := gui.mainContextForView(shown.GetView()) + target := gui.mainContextForView(pane.GetView()) target.SetHasSelectableContent(false) gui.State.ContextMgr.UpdateSelectionHighlights() if manager := gui.getManager(target.GetView()); !manager.HasRestoreForNextTask() { @@ -240,18 +240,20 @@ func (gui *Gui) followFocusIntoShownPane(pair types.MainContextPair, panes types gui.State.ContextMgr.Push(target, types.OnFocusOpts{}) } -// onlyShownPane returns the main pane a render leaves showing on its own, or nil when -// it leaves both showing. -func onlyShownPane(pair types.MainContextPair, panes types.MainPanes) types.Context { - switch panes { - case types.MainPaneOnly: - return pair.Main - case types.SecondaryPaneOnly: - return pair.Secondary - case types.BothMainPanes: +// onlyWorkablePane returns the main pane a render leaves as the only one worth having +// the focus in, or nil when that is true of both of them or of neither. Being shown is +// not the same as being worth working in: a pane the layout keeps around for the sake +// of always splitting the diff shows an empty side of the file. +func onlyWorkablePane(opts types.RefreshMainOpts) types.Context { + main := opts.Main != nil && !opts.Main.NothingToActOn + secondary := opts.Secondary != nil && !opts.Secondary.NothingToActOn + if main == secondary { return nil } - return nil + if main { + return opts.Pair.Main + } + return opts.Pair.Secondary } // clampDiffSelectionToContent brings the focused main view's selection back onto the diff --git a/pkg/gui/types/rendering.go b/pkg/gui/types/rendering.go index f59a535ba..f88a207b6 100644 --- a/pkg/gui/types/rendering.go +++ b/pkg/gui/types/rendering.go @@ -38,6 +38,11 @@ type ViewUpdateOpts struct { SubTitle string Task UpdateTask + + // NothingToActOn marks a pane that is being shown only because the layout is + // configured to always split the diff: its side of the file holds nothing, so it + // is not a pane to leave the focus in. + NothingToActOn bool } type RefreshMainOpts struct { diff --git a/pkg/integration/tests/main_view/focus_leaves_an_always_split_empty_pane.go b/pkg/integration/tests/main_view/focus_leaves_an_always_split_empty_pane.go new file mode 100644 index 000000000..a49330e71 --- /dev/null +++ b/pkg/integration/tests/main_view/focus_leaves_an_always_split_empty_pane.go @@ -0,0 +1,53 @@ +package main_view + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var FocusLeavesAnAlwaysSplitEmptyPane = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Configured to always split the diff, the emptied pane stays but the focus still leaves it", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(cfg *config.AppConfig) { + cfg.GetUserConfig().Gui.UseHunkModeInStagingView = false + cfg.GetUserConfig().Gui.SplitDiff = "always" + }, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("file1", "one\ntwo\nthree\nfour\nfive\n") + shell.Commit("one") + + shell.UpdateFileAndAdd("file1", "one\nSTAGED\ntwo\nthree\nfour\nfive\n") + shell.UpdateFile("file1", "one\nSTAGED\ntwo\nthree\nUNSTAGED\nfour\nfive\n") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Press(keys.Universal.FocusMainView) + + t.Views().Main(). + IsFocused(). + Press(keys.Universal.TogglePanel) + + t.Views().Secondary(). + IsFocused(). + SelectedLines( + Contains("+STAGED"), + ) + + t.Shell().Commit("two") + t.GlobalPress(keys.Universal.Refresh) + + // The staged side is empty now, but its pane is still shown because the split + // is configured as permanent. There is nothing left to act on in it, so the + // focus goes where there is. + t.Views().Secondary(). + IsVisible(). + Content(DoesNotContain("STAGED")) + t.Views().Main(). + IsFocused(). + SelectedLines( + Contains("+UNSTAGED"), + ) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index fb58456b9..bcdf37e43 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -377,6 +377,7 @@ var tests = []*components.IntegrationTest{ main_view.FileNavigationScrollsToTheTop, main_view.FocusFollowsAPaneEmptiedFromOutside, main_view.FocusFollowsIntoAPaneTakingOver, + main_view.FocusLeavesAnAlwaysSplitEmptyPane, main_view.HideSelectionWhenChangesVanish, main_view.KeepAWrappedLineCoveredAcrossARerender, main_view.KeepBothHalvesOfAChangeSelected,