mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-12 08:36:25 -04:00
The commit list a filtering mode change leaves behind has nothing to do with the one that was showing, so the scroll position it inherits says nothing about where the selection ended up, and the selection can land anywhere off screen. PostRefreshUpdate only moves the cursor within the existing scroll position, so ask for the scroll separately, the way the commits refresh does when it moves the selection itself. Exiting filtering mode looked like it worked, but only by accident: the commits refresh recognizes the commit that was selected before it ran, selects it again at its new index, and scrolls because the index moved. That does nothing for the case where the commit is gone from the list, or for entering filtering mode, where we select the first commit ourselves.
299 lines
9.1 KiB
Go
299 lines
9.1 KiB
Go
package helpers
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/gocui"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
type ModeHelper struct {
|
|
c *HelperCommon
|
|
|
|
diffHelper *DiffHelper
|
|
patchBuildingHelper *PatchBuildingHelper
|
|
cherryPickHelper *CherryPickHelper
|
|
mergeAndRebaseHelper *MergeAndRebaseHelper
|
|
bisectHelper *BisectHelper
|
|
suppressWorkingTreeStateMode bool
|
|
}
|
|
|
|
func NewModeHelper(
|
|
c *HelperCommon,
|
|
diffHelper *DiffHelper,
|
|
patchBuildingHelper *PatchBuildingHelper,
|
|
cherryPickHelper *CherryPickHelper,
|
|
mergeAndRebaseHelper *MergeAndRebaseHelper,
|
|
bisectHelper *BisectHelper,
|
|
) *ModeHelper {
|
|
return &ModeHelper{
|
|
c: c,
|
|
diffHelper: diffHelper,
|
|
patchBuildingHelper: patchBuildingHelper,
|
|
cherryPickHelper: cherryPickHelper,
|
|
mergeAndRebaseHelper: mergeAndRebaseHelper,
|
|
bisectHelper: bisectHelper,
|
|
}
|
|
}
|
|
|
|
type ModeStatus struct {
|
|
IsActive func() bool
|
|
InfoLabel func() string
|
|
CancelLabel func() string
|
|
Reset func() error
|
|
}
|
|
|
|
func (self *ModeHelper) Statuses() []ModeStatus {
|
|
return []ModeStatus{
|
|
{
|
|
IsActive: self.c.Modes().Diffing.Active,
|
|
InfoLabel: func() string {
|
|
return self.withResetButton(
|
|
fmt.Sprintf(
|
|
"%s %s",
|
|
self.c.Tr.ShowingGitDiff,
|
|
"git diff "+strings.Join(self.diffHelper.DiffArgs(), " "),
|
|
),
|
|
style.FgMagenta,
|
|
)
|
|
},
|
|
CancelLabel: func() string {
|
|
return self.c.Tr.CancelDiffingMode
|
|
},
|
|
Reset: self.diffHelper.ExitDiffMode,
|
|
},
|
|
{
|
|
IsActive: self.c.Git().Patch.PatchBuilder.Active,
|
|
InfoLabel: func() string {
|
|
return self.withResetButton(self.c.Tr.BuildingPatch, style.FgYellow.SetBold())
|
|
},
|
|
CancelLabel: func() string {
|
|
return self.c.Tr.ExitCustomPatchBuilder
|
|
},
|
|
Reset: self.patchBuildingHelper.Reset,
|
|
},
|
|
{
|
|
IsActive: self.c.Modes().Filtering.Active,
|
|
InfoLabel: func() string {
|
|
filterContent := lo.Ternary(self.c.Modes().Filtering.GetPath() != "", self.c.Modes().Filtering.GetPath(), self.c.Modes().Filtering.GetAuthor())
|
|
return self.withResetButton(
|
|
fmt.Sprintf(
|
|
"%s '%s'",
|
|
self.c.Tr.FilteringBy,
|
|
filterContent,
|
|
),
|
|
style.FgRed,
|
|
)
|
|
},
|
|
CancelLabel: func() string {
|
|
return self.c.Tr.ExitFilterMode
|
|
},
|
|
Reset: self.ExitFilterMode,
|
|
},
|
|
{
|
|
IsActive: self.c.Modes().MarkedBaseCommit.Active,
|
|
InfoLabel: func() string {
|
|
return self.withResetButton(
|
|
self.c.Tr.MarkedBaseCommitStatus,
|
|
style.FgCyan,
|
|
)
|
|
},
|
|
CancelLabel: func() string {
|
|
return self.c.Tr.CancelMarkedBaseCommit
|
|
},
|
|
Reset: self.mergeAndRebaseHelper.ResetMarkedBaseCommit,
|
|
},
|
|
{
|
|
IsActive: self.c.Modes().CherryPicking.Active,
|
|
InfoLabel: func() string {
|
|
copiedCount := len(self.c.Modes().CherryPicking.CherryPickedCommits)
|
|
text := self.c.Tr.CommitsCopied
|
|
if copiedCount == 1 {
|
|
text = self.c.Tr.CommitCopied
|
|
}
|
|
|
|
return self.withResetButton(
|
|
fmt.Sprintf(
|
|
"%d %s",
|
|
copiedCount,
|
|
text,
|
|
),
|
|
style.FgCyan,
|
|
)
|
|
},
|
|
CancelLabel: func() string {
|
|
return self.c.Tr.ResetCherryPickShort
|
|
},
|
|
Reset: self.cherryPickHelper.Reset,
|
|
},
|
|
{
|
|
IsActive: func() bool {
|
|
return !self.suppressWorkingTreeStateMode && self.c.Git().Status.WorkingTreeState().Any()
|
|
},
|
|
InfoLabel: func() string {
|
|
workingTreeState := self.c.Git().Status.WorkingTreeState()
|
|
return self.withResetButton(
|
|
workingTreeState.Title(self.c.Tr), style.FgYellow,
|
|
)
|
|
},
|
|
CancelLabel: func() string {
|
|
return fmt.Sprintf(self.c.Tr.AbortTitle, self.c.Git().Status.WorkingTreeState().CommandName())
|
|
},
|
|
Reset: self.mergeAndRebaseHelper.AbortMergeOrRebaseWithConfirm,
|
|
},
|
|
{
|
|
IsActive: func() bool {
|
|
return self.c.Model().BisectInfo.Started()
|
|
},
|
|
InfoLabel: func() string {
|
|
return self.withResetButton(self.c.Tr.Bisect.Bisecting, style.FgGreen)
|
|
},
|
|
CancelLabel: func() string {
|
|
return self.c.Tr.Actions.ResetBisect
|
|
},
|
|
Reset: self.bisectHelper.Reset,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (self *ModeHelper) withResetButton(content string, textStyle style.TextStyle) string {
|
|
return textStyle.Sprintf(
|
|
"%s %s",
|
|
content,
|
|
style.AttrUnderline.Sprint(self.c.Tr.ResetInParentheses),
|
|
)
|
|
}
|
|
|
|
func (self *ModeHelper) GetActiveMode() (ModeStatus, bool) {
|
|
return lo.Find(self.Statuses(), func(mode ModeStatus) bool {
|
|
return mode.IsActive()
|
|
})
|
|
}
|
|
|
|
func (self *ModeHelper) IsAnyModeActive() bool {
|
|
return lo.SomeBy(self.Statuses(), func(mode ModeStatus) bool {
|
|
return mode.IsActive()
|
|
})
|
|
}
|
|
|
|
func (self *ModeHelper) ExitFilterMode() error {
|
|
return self.ClearFiltering()
|
|
}
|
|
|
|
func (self *ModeHelper) SetFilteringPath(path string) error {
|
|
return self.setFiltering(func() {
|
|
self.c.Modes().Filtering.SetPath(path)
|
|
})
|
|
}
|
|
|
|
func (self *ModeHelper) SetFilteringAuthor(author string) error {
|
|
return self.setFiltering(func() {
|
|
self.c.Modes().Filtering.SetAuthor(author)
|
|
})
|
|
}
|
|
|
|
func (self *ModeHelper) setFiltering(setFilter func()) error {
|
|
return self.changeFiltering(
|
|
func() {
|
|
// Whatever we were filtering by before is replaced, not added to
|
|
self.c.Modes().Filtering.Reset()
|
|
setFilter()
|
|
self.c.Modes().Filtering.SetSelectedCommitHash(
|
|
self.c.Contexts().LocalCommits.GetSelectedCommitHash())
|
|
},
|
|
func() {
|
|
self.c.Contexts().LocalCommits.SetSelection(0)
|
|
},
|
|
)
|
|
}
|
|
|
|
func (self *ModeHelper) ClearFiltering() error {
|
|
selectedCommitHash := self.c.Contexts().LocalCommits.GetSelectedCommitHash()
|
|
|
|
return self.changeFiltering(
|
|
self.c.Modes().Filtering.Reset,
|
|
func() {
|
|
// Find the commit that was last selected in filtering mode, and select it again after refreshing
|
|
if !self.c.Contexts().LocalCommits.SelectCommitByHash(selectedCommitHash) {
|
|
// If we couldn't find it (either because no commit was selected
|
|
// in filtering mode, or because the commit is outside the
|
|
// initial 300 range), go back to the commit that was selected
|
|
// before we entered filtering
|
|
self.c.Contexts().LocalCommits.SelectCommitByHash(self.c.Modes().Filtering.GetSelectedCommitHash())
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
// changeFiltering applies a change to the filtering mode: setFilter mutates the
|
|
// mode, then the views whose contents depend on the filter are reloaded, and
|
|
// selectCommit puts the selection where it belongs in the reloaded commit list.
|
|
//
|
|
// Reloading the commit list can take seconds in a big repo, so it happens on a
|
|
// worker with a waiting status. Everything the user can see of the change waits
|
|
// for it: the screen mode, the focused panel and the reloaded lists all land in
|
|
// the same frame, from the refresh's Then, rather than framing an unfiltered
|
|
// list as if it were the filtered one. Until then the pre-change state stays on
|
|
// screen, and it stays consistent, because the only thing that has changed
|
|
// behind it is the filter that the reload is in the middle of applying. The one
|
|
// thing that can't wait is the mode indicator in the information panel: the
|
|
// filter has to be set before the reload can use it, so the indicator leads the
|
|
// lists by however long the reload takes.
|
|
//
|
|
// Input is blocked for the duration: the keys the user presses arrive after the
|
|
// change, which is where they meant them to go, and it keeps a second filter
|
|
// change from racing this one — they would both refresh with whichever filter
|
|
// happened to be set when their git commands ran.
|
|
func (self *ModeHelper) changeFiltering(setFilter func(), selectCommit func()) error {
|
|
setFilter()
|
|
|
|
filtering := self.c.Modes().Filtering.Active()
|
|
message := lo.Ternary(filtering, self.c.Tr.ApplyingFilterStatus, self.c.Tr.RemovingFilterStatus)
|
|
|
|
return self.c.WithWaitingStatusBlockingInput(types.WaitingStatusOpts{Message: message}, func(gocui.Task) error {
|
|
self.c.RefreshFromWorker(types.RefreshOptions{
|
|
Scope: ScopesToRefreshWhenFilteringModeChanges(),
|
|
BatchUIUpdates: true,
|
|
Then: func() error {
|
|
repoState := self.c.State().GetRepoState()
|
|
if filtering {
|
|
if repoState.GetScreenMode() == types.SCREEN_NORMAL {
|
|
repoState.SetScreenMode(types.SCREEN_HALF)
|
|
}
|
|
self.c.Context().Push(self.c.Contexts().LocalCommits, types.OnFocusOpts{})
|
|
} else if repoState.GetScreenMode() == types.SCREEN_HALF {
|
|
repoState.SetScreenMode(types.SCREEN_NORMAL)
|
|
}
|
|
|
|
selectCommit()
|
|
self.c.PostRefreshUpdate(self.c.Contexts().LocalCommits)
|
|
// The list we just selected in has nothing to do with the one
|
|
// that was showing, so wherever it was scrolled to says nothing
|
|
// about where the selection now is. PostRefreshUpdate leaves the
|
|
// scroll position alone, so ask for it separately.
|
|
self.c.Contexts().LocalCommits.FocusLine(true)
|
|
return nil
|
|
},
|
|
})
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// Stashes really only need to be refreshed when filtering by path, not by author, but it's too much
|
|
// work to distinguish this, and refreshing stashes is fast, so we don't bother
|
|
func ScopesToRefreshWhenFilteringModeChanges() []types.RefreshableView {
|
|
return []types.RefreshableView{
|
|
types.COMMITS,
|
|
types.SUB_COMMITS,
|
|
types.REFLOG,
|
|
types.STASH,
|
|
}
|
|
}
|
|
|
|
func (self *ModeHelper) SetSuppressWorkingTreeStateMode(value bool) {
|
|
self.suppressWorkingTreeStateMode = value
|
|
}
|