jesseduffield.lazygit/pkg/gui/controllers/helpers/mode_helper.go
Stefan Haller 717448f105 Make RefreshOptions.Then a func() error, queue it via OnUIThread
This is preparation for upcoming commits that will bounce refresh-scope
model updates (e.g. Model.Files) onto the UI thread by enqueuing the
write via OnUIThread instead of applying it directly on the worker
goroutine. Once that lands, a Then callback that reads the model must
run after that queued write has been processed, not synchronously at
wg.Wait() time — at that point the workers have returned, but a bounce
they queued may not have been processed yet.

Queuing Then via OnUIThread here, ahead of that change, guarantees the
right ordering once it lands: a bounce queued earlier in the same
refresh is already sitting in the channel by the time wg.Wait()
returns, so Then enqueued after it will always be processed after, and
see the post-refresh model.

The signature change to func() error lets Then propagate errors
through gocui's normal error handler (the same path key-handler errors
take).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 18:09:33 +02:00

225 lines
6 KiB
Go

package helpers
import (
"fmt"
"strings"
"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
suppressRebasingMode 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.suppressRebasingMode && 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) ClearFiltering() error {
selectedCommitHash := self.c.Contexts().LocalCommits.GetSelectedCommitHash()
self.c.Modes().Filtering.Reset()
if self.c.State().GetRepoState().GetScreenMode() == types.SCREEN_HALF {
self.c.State().GetRepoState().SetScreenMode(types.SCREEN_NORMAL)
}
self.c.Refresh(types.RefreshOptions{
Scope: ScopesToRefreshWhenFilteringModeChanges(),
Then: func() error {
// 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())
}
self.c.PostRefreshUpdate(self.c.Contexts().LocalCommits)
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) SetSuppressRebasingMode(value bool) {
self.suppressRebasingMode = value
}