package controllers import ( "fmt" "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/samber/lo" ) type PatchBuildingController struct { baseController c *ControllerCommon } var _ types.IController = &PatchBuildingController{} func NewPatchBuildingController( c *ControllerCommon, ) *PatchBuildingController { return &PatchBuildingController{ baseController: baseController{}, c: c, } } func (self *PatchBuildingController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { return []*types.Binding{ { Key: opts.GetKey(opts.Config.Universal.OpenFile), Handler: self.OpenFile, Description: self.c.Tr.OpenFile, Tooltip: self.c.Tr.OpenFileTooltip, }, { Key: opts.GetKey(opts.Config.Universal.Edit), Handler: self.EditFile, Description: self.c.Tr.EditFile, Tooltip: self.c.Tr.EditFileTooltip, }, { Key: opts.GetKey(opts.Config.Universal.Select), Handler: self.ToggleSelectionAndRefresh, Description: self.c.Tr.ToggleSelectionForPatch, DisplayOnScreen: true, }, { Key: opts.GetKey(opts.Config.Universal.Remove), Handler: self.discardSelection, GetDisabledReason: self.getDisabledReasonForDiscard, Description: self.c.Tr.RemoveSelectionFromPatch, Tooltip: self.c.Tr.RemoveSelectionFromPatchTooltip, DisplayOnScreen: true, }, { Key: opts.GetKey(opts.Config.Universal.Return), Handler: self.Escape, Description: self.c.Tr.ExitCustomPatchBuilder, DescriptionFunc: self.EscapeDescription, DisplayOnScreen: true, }, } } func (self *PatchBuildingController) Context() types.Context { return self.c.Contexts().CustomPatchBuilder } func (self *PatchBuildingController) context() types.IPatchExplorerContext { return self.c.Contexts().CustomPatchBuilder } func (self *PatchBuildingController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding { return []*gocui.ViewMouseBinding{} } func (self *PatchBuildingController) GetOnFocus() func(types.OnFocusOpts) { return func(opts types.OnFocusOpts) { // no need to change wrap on the secondary view because it can't be interacted with self.c.Views().PatchBuilding.Wrap = self.c.UserConfig().Gui.WrapLinesInStagingView self.c.Helpers().PatchBuilding.RefreshPatchBuildingPanel(opts) } } func (self *PatchBuildingController) GetOnFocusLost() func(types.OnFocusLostOpts) { return func(opts types.OnFocusLostOpts) { self.context().SetState(nil) self.c.Views().PatchBuilding.Wrap = true if self.c.Git().Patch.PatchBuilder.IsEmpty() { self.c.Git().Patch.PatchBuilder.Reset() } } } func (self *PatchBuildingController) OpenFile() error { self.context().GetMutex().Lock() defer self.context().GetMutex().Unlock() path := self.c.Contexts().CommitFiles.GetSelectedPath() if path == "" { return nil } return self.c.Helpers().Files.OpenFile(path) } func (self *PatchBuildingController) EditFile() error { self.context().GetMutex().Lock() defer self.context().GetMutex().Unlock() path := self.c.Contexts().CommitFiles.GetSelectedPath() if path == "" { return nil } lineNumber := self.context().GetState().CurrentLineNumber() lineNumber = self.c.Helpers().Diff.AdjustLineNumber(path, lineNumber, self.context().GetViewName()) return self.c.Helpers().Files.EditFileAtLine(path, lineNumber) } func (self *PatchBuildingController) ToggleSelectionAndRefresh() error { if err := self.toggleSelection(); err != nil { return err } self.c.Refresh(types.RefreshOptions{ Scope: []types.RefreshableView{types.PATCH_BUILDING, types.COMMIT_FILES}, }) return nil } func (self *PatchBuildingController) toggleSelection() error { self.context().GetMutex().Lock() defer self.context().GetMutex().Unlock() filename := self.c.Contexts().CommitFiles.GetSelectedPath() if filename == "" { return nil } state := self.context().GetState() // Get added/deleted lines in the selected patch range lineIndicesToToggle := state.LineIndicesOfAddedOrDeletedLinesInSelectedPatchRange() if len(lineIndicesToToggle) == 0 { // Only context lines or header lines selected, so nothing to do return nil } includedLineIndices, err := self.c.Git().Patch.PatchBuilder.GetFileIncLineIndices(filename) if err != nil { return err } toggleFunc := self.c.Git().Patch.PatchBuilder.AddFileLineRange firstSelectedChangeLineIsStaged := lo.Contains(includedLineIndices, lineIndicesToToggle[0]) if firstSelectedChangeLineIsStaged { toggleFunc = self.c.Git().Patch.PatchBuilder.RemoveFileLineRange } // add range of lines to those set for the file if err := toggleFunc(filename, lineIndicesToToggle); err != nil { // might actually want to return an error here self.c.Log.Error(err) } if state.SelectingRange() { state.SetLineSelectMode() } state.SelectNextStageableLineOfSameIncludedState(self.context().GetIncludedLineIndices(), firstSelectedChangeLineIsStaged) return nil } func (self *PatchBuildingController) getDisabledReasonForDiscard() *types.DisabledReason { if !self.c.Git().Patch.PatchBuilder.CanRebase { return &types.DisabledReason{Text: self.c.Tr.CanOnlyDiscardFromLocalCommits, ShowErrorInPanel: true} } if self.c.Git().Status.WorkingTreeState().Any() { return &types.DisabledReason{Text: self.c.Tr.CantPatchWhileRebasingError, ShowErrorInPanel: true} } if self.c.UserConfig().Git.DiffContextSize == 0 { text := fmt.Sprintf(self.c.Tr.Actions.NotEnoughContextToRemoveLines, self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView) return &types.DisabledReason{Text: text, ShowErrorInPanel: true} } return nil } func (self *PatchBuildingController) discardSelection() error { prompt := lo.Ternary(self.c.Git().Patch.PatchBuilder.IsEmpty(), self.c.Tr.DiscardLinesFromCommitPrompt, self.c.Tr.DiscardLinesFromCommitPromptWithReset) self.c.Confirm(types.ConfirmOpts{ Title: self.c.Tr.DiscardLinesFromCommitTitle, Prompt: prompt, HandleConfirm: func() error { return self.discardSelectionFromCommit() }, }) return nil } func (self *PatchBuildingController) discardSelectionFromCommit() error { // Reset the current patch if there is one. if !self.c.Git().Patch.PatchBuilder.IsEmpty() { self.c.Git().Patch.PatchBuilder.Reset() } if err := self.toggleSelection(); err != nil { return err } if self.c.Git().Patch.PatchBuilder.IsEmpty() { return nil } return self.c.WithWaitingStatusSync(self.c.Tr.RebasingStatus, func() error { commitIndex := self.getPatchCommitIndex() self.c.LogAction(self.c.Tr.Actions.RemovePatchFromCommit) err := self.c.Git().Patch.DeletePatchesFromCommit(self.c.Model().Commits, commitIndex) self.c.Helpers().PatchBuilding.Escape() return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseWithRefreshOptions( err, types.RefreshOptions{Mode: types.SYNC}) }) } func (self *PatchBuildingController) getPatchCommitIndex() int { for index, commit := range self.c.Model().Commits { if commit.Hash() == self.c.Git().Patch.PatchBuilder.To { return index } } return -1 } func (self *PatchBuildingController) Escape() error { context := self.c.Contexts().CustomPatchBuilder state := context.GetState() if state.SelectingRange() || state.SelectingHunkEnabledByUser() { state.SetLineSelectMode() self.c.PostRefreshUpdate(context) return nil } self.c.Helpers().PatchBuilding.Escape() return nil } func (self *PatchBuildingController) EscapeDescription() string { context := self.c.Contexts().CustomPatchBuilder if state := context.GetState(); state != nil { if state.SelectingRange() { return self.c.Tr.DismissRangeSelect } if state.SelectingHunkEnabledByUser() { return self.c.Tr.SelectLineByLine } } return self.c.Tr.ExitCustomPatchBuilder }