mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-10-06 14:06:32 -04:00
Leave a pane that only the permanent split keeps around
Configured to always split the diff, a pane is shown whether or not its side of the file holds anything, so emptying the side the focus is on no longer takes the pane away — but it does take away everything there was to do there, which is the question the focus is really asking. So the render says which of the panes it is giving something to act on, rather than the focus reading that off which panes are shown. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6335caa495
commit
616a1fa247
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+26
-24
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user