/
githubmirror
/
lazygit
Обзор
Документация
Войти
/
githubmirror
/
lazygit
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
pkg/gui/controllers/helpers/mode_helper.go
298 строк
9 KB
Stefan Haller
Scroll the selection into view after a filtering mode change
05 авг 2026, 18:29
05 авг 2026, 18:29
1b901c7
Код
Авторство
О чём код?
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 }