mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-05-27 23:08:02 +02:00
Show Toast instead of error panel when invoking a disabled command (#3180)
- **PR Description** Addresses #3116. I'm not 100% sure I like the behavior, but I put it out there so that others can test it and form an opinion. It not only affects keybindings, but also invoking menu items (either with enter or with their key binding): the menu now stays open in that case, which I think is actually better. There's a horrible hack for keeping the integration tests working, I don't have a good idea how to fix that for real. Suggestions welcome.
This commit is contained in:
commit
f79305090b
@ -23,6 +23,7 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/logs"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
)
|
||||
@ -42,7 +43,7 @@ func Run(
|
||||
common *common.Common,
|
||||
startArgs appTypes.StartArgs,
|
||||
) {
|
||||
app, err := NewApp(config, common)
|
||||
app, err := NewApp(config, startArgs.IntegrationTest, common)
|
||||
|
||||
if err == nil {
|
||||
err = app.Run(startArgs)
|
||||
@ -94,7 +95,7 @@ func newLogger(cfg config.AppConfigurer) *logrus.Entry {
|
||||
}
|
||||
|
||||
// NewApp bootstrap a new application
|
||||
func NewApp(config config.AppConfigurer, common *common.Common) (*App, error) {
|
||||
func NewApp(config config.AppConfigurer, test integrationTypes.IntegrationTest, common *common.Common) (*App, error) {
|
||||
app := &App{
|
||||
closers: []io.Closer{},
|
||||
Config: config,
|
||||
@ -128,7 +129,7 @@ func NewApp(config config.AppConfigurer, common *common.Common) (*App, error) {
|
||||
showRecentRepos = true
|
||||
}
|
||||
|
||||
app.Gui, err = gui.NewGui(common, config, gitVersion, updater, showRecentRepos, dirName)
|
||||
app.Gui, err = gui.NewGui(common, config, gitVersion, updater, showRecentRepos, dirName, test)
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ func generateAtDir(cheatsheetDir string) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
mApp, _ := app.NewApp(mConfig, common)
|
||||
mApp, _ := app.NewApp(mConfig, nil, common)
|
||||
path := cheatsheetDir + "/Keybindings_" + lang + ".md"
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
|
@ -90,7 +90,7 @@ func (self *MenuViewModel) GetDisplayStrings(_ int, _ int) [][]string {
|
||||
|
||||
return lo.Map(menuItems, func(item *types.MenuItem, _ int) []string {
|
||||
displayStrings := item.LabelColumns
|
||||
if item.DisabledReason != "" {
|
||||
if item.DisabledReason != nil {
|
||||
displayStrings[0] = style.FgDefault.SetStrikethrough().Sprint(displayStrings[0])
|
||||
}
|
||||
|
||||
@ -172,8 +172,13 @@ func (self *MenuContext) GetKeybindings(opts types.KeybindingsOpts) []*types.Bin
|
||||
}
|
||||
|
||||
func (self *MenuContext) OnMenuPress(selectedItem *types.MenuItem) error {
|
||||
if selectedItem != nil && selectedItem.DisabledReason != "" {
|
||||
return self.c.ErrorMsg(selectedItem.DisabledReason)
|
||||
if selectedItem != nil && selectedItem.DisabledReason != nil {
|
||||
if selectedItem.DisabledReason.ShowErrorInPanel {
|
||||
return self.c.ErrorMsg(selectedItem.DisabledReason.Text)
|
||||
}
|
||||
|
||||
self.c.ErrorToast(self.c.Tr.DisabledMenuItemPrefix + selectedItem.DisabledReason.Text)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := self.c.PopContext(); err != nil {
|
||||
|
@ -264,13 +264,13 @@ func (self *BranchesController) viewUpstreamOptions(selectedBranch *models.Branc
|
||||
}
|
||||
|
||||
if !selectedBranch.IsTrackingRemote() {
|
||||
unsetUpstreamItem.DisabledReason = self.c.Tr.UpstreamNotSetError
|
||||
unsetUpstreamItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.UpstreamNotSetError}
|
||||
}
|
||||
|
||||
if !selectedBranch.RemoteBranchStoredLocally() {
|
||||
viewDivergenceItem.DisabledReason = self.c.Tr.UpstreamNotSetError
|
||||
upstreamResetItem.DisabledReason = self.c.Tr.UpstreamNotSetError
|
||||
upstreamRebaseItem.DisabledReason = self.c.Tr.UpstreamNotSetError
|
||||
viewDivergenceItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.UpstreamNotSetError}
|
||||
upstreamResetItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.UpstreamNotSetError}
|
||||
upstreamRebaseItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.UpstreamNotSetError}
|
||||
}
|
||||
|
||||
options := []*types.MenuItem{
|
||||
@ -309,16 +309,16 @@ func (self *BranchesController) press(selectedBranch *models.Branch) error {
|
||||
return self.c.Helpers().Refs.CheckoutRef(selectedBranch.Name, types.CheckoutRefOptions{})
|
||||
}
|
||||
|
||||
func (self *BranchesController) getDisabledReasonForPress() string {
|
||||
func (self *BranchesController) getDisabledReasonForPress() *types.DisabledReason {
|
||||
currentBranch := self.c.Helpers().Refs.GetCheckedOutRef()
|
||||
if currentBranch != nil {
|
||||
op := self.c.State().GetItemOperation(currentBranch)
|
||||
if op == types.ItemOperationFastForwarding || op == types.ItemOperationPulling {
|
||||
return self.c.Tr.CantCheckoutBranchWhilePulling
|
||||
return &types.DisabledReason{Text: self.c.Tr.CantCheckoutBranchWhilePulling}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *BranchesController) worktreeForBranch(branch *models.Branch) (*models.Worktree, bool) {
|
||||
@ -525,7 +525,7 @@ func (self *BranchesController) delete(branch *models.Branch) error {
|
||||
},
|
||||
}
|
||||
if checkedOutBranch.Name == branch.Name {
|
||||
localDeleteItem.DisabledReason = self.c.Tr.CantDeleteCheckOutBranch
|
||||
localDeleteItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.CantDeleteCheckOutBranch}
|
||||
}
|
||||
|
||||
remoteDeleteItem := &types.MenuItem{
|
||||
@ -536,7 +536,7 @@ func (self *BranchesController) delete(branch *models.Branch) error {
|
||||
},
|
||||
}
|
||||
if !branch.IsTrackingRemote() || branch.UpstreamGone {
|
||||
remoteDeleteItem.DisabledReason = self.c.Tr.UpstreamNotSetError
|
||||
remoteDeleteItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.UpstreamNotSetError}
|
||||
}
|
||||
|
||||
menuTitle := utils.ResolvePlaceholderString(
|
||||
@ -562,14 +562,14 @@ func (self *BranchesController) rebase() error {
|
||||
return self.c.Helpers().MergeAndRebase.RebaseOntoRef(selectedBranchName)
|
||||
}
|
||||
|
||||
func (self *BranchesController) getDisabledReasonForRebase() string {
|
||||
func (self *BranchesController) getDisabledReasonForRebase() *types.DisabledReason {
|
||||
selectedBranchName := self.context().GetSelected().Name
|
||||
checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef().Name
|
||||
if selectedBranchName == checkedOutBranch {
|
||||
return self.c.Tr.CantRebaseOntoSelf
|
||||
return &types.DisabledReason{Text: self.c.Tr.CantRebaseOntoSelf}
|
||||
}
|
||||
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *BranchesController) fastForward(branch *models.Branch) error {
|
||||
|
@ -848,15 +848,15 @@ func (self *FilesController) openCopyMenu() error {
|
||||
}
|
||||
|
||||
if node == nil {
|
||||
copyNameItem.DisabledReason = self.c.Tr.NoContentToCopyError
|
||||
copyPathItem.DisabledReason = self.c.Tr.NoContentToCopyError
|
||||
copyFileDiffItem.DisabledReason = self.c.Tr.NoContentToCopyError
|
||||
copyNameItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.NoContentToCopyError}
|
||||
copyPathItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.NoContentToCopyError}
|
||||
copyFileDiffItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.NoContentToCopyError}
|
||||
}
|
||||
if node != nil && !node.GetHasStagedOrTrackedChanges() {
|
||||
copyFileDiffItem.DisabledReason = self.c.Tr.NoContentToCopyError
|
||||
copyFileDiffItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.NoContentToCopyError}
|
||||
}
|
||||
if !self.anyStagedOrTrackedFile() {
|
||||
copyAllDiff.DisabledReason = self.c.Tr.NoContentToCopyError
|
||||
copyAllDiff.DisabledReason = &types.DisabledReason{Text: self.c.Tr.NoContentToCopyError}
|
||||
}
|
||||
|
||||
return self.c.Menu(types.CreateMenuOptions{
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/status"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
@ -23,7 +24,7 @@ func NewAppStatusHelper(c *HelperCommon, statusMgr func() *status.StatusManager,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *AppStatusHelper) Toast(message string) {
|
||||
func (self *AppStatusHelper) Toast(message string, kind types.ToastKind) {
|
||||
if self.c.RunningIntegrationTest() {
|
||||
// Don't bother showing toasts in integration tests. You can't check for
|
||||
// them anyway, and they would only slow down the test unnecessarily by
|
||||
@ -31,7 +32,7 @@ func (self *AppStatusHelper) Toast(message string) {
|
||||
return
|
||||
}
|
||||
|
||||
self.statusMgr().AddToastStatus(message)
|
||||
self.statusMgr().AddToastStatus(message, kind)
|
||||
|
||||
self.renderAppStatus()
|
||||
}
|
||||
@ -87,7 +88,8 @@ func (self *AppStatusHelper) HasStatus() bool {
|
||||
}
|
||||
|
||||
func (self *AppStatusHelper) GetStatusString() string {
|
||||
return self.statusMgr().GetStatusString()
|
||||
appStatus, _ := self.statusMgr().GetStatusString()
|
||||
return appStatus
|
||||
}
|
||||
|
||||
func (self *AppStatusHelper) renderAppStatus() {
|
||||
@ -95,7 +97,8 @@ func (self *AppStatusHelper) renderAppStatus() {
|
||||
ticker := time.NewTicker(time.Millisecond * utils.LoaderAnimationInterval)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
appStatus := self.statusMgr().GetStatusString()
|
||||
appStatus, color := self.statusMgr().GetStatusString()
|
||||
self.c.Views().AppStatus.FgColor = color
|
||||
self.c.OnUIThread(func() error {
|
||||
self.c.SetViewContent(self.c.Views().AppStatus, appStatus)
|
||||
return nil
|
||||
@ -127,7 +130,8 @@ func (self *AppStatusHelper) renderAppStatusSync(stop chan struct{}) {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
appStatus := self.statusMgr().GetStatusString()
|
||||
appStatus, color := self.statusMgr().GetStatusString()
|
||||
self.c.Views().AppStatus.FgColor = color
|
||||
self.c.SetViewContent(self.c.Views().AppStatus, appStatus)
|
||||
// Redraw all views of the bottom line:
|
||||
bottomLineViews := []*gocui.View{
|
||||
|
@ -378,11 +378,11 @@ func (self *ConfirmationHelper) IsPopupPanelFocused() bool {
|
||||
|
||||
func (self *ConfirmationHelper) TooltipForMenuItem(menuItem *types.MenuItem) string {
|
||||
tooltip := menuItem.Tooltip
|
||||
if menuItem.DisabledReason != "" {
|
||||
if menuItem.DisabledReason != nil {
|
||||
if tooltip != "" {
|
||||
tooltip += "\n\n"
|
||||
}
|
||||
tooltip += style.FgRed.Sprintf(self.c.Tr.DisabledMenuItemPrefix) + menuItem.DisabledReason
|
||||
tooltip += style.FgRed.Sprintf(self.c.Tr.DisabledMenuItemPrefix) + menuItem.DisabledReason.Text
|
||||
}
|
||||
return tooltip
|
||||
}
|
||||
|
@ -261,9 +261,9 @@ func (self *LocalCommitsController) squashDown(commit *models.Commit) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) getDisabledReasonForSquashDown(commit *models.Commit) string {
|
||||
func (self *LocalCommitsController) getDisabledReasonForSquashDown(commit *models.Commit) *types.DisabledReason {
|
||||
if self.context().GetSelectedLineIdx() >= len(self.c.Model().Commits)-1 {
|
||||
return self.c.Tr.CannotSquashOrFixupFirstCommit
|
||||
return &types.DisabledReason{Text: self.c.Tr.CannotSquashOrFixupFirstCommit}
|
||||
}
|
||||
|
||||
return self.rebaseCommandEnabled(todo.Squash, commit)
|
||||
@ -290,9 +290,9 @@ func (self *LocalCommitsController) fixup(commit *models.Commit) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) getDisabledReasonForFixup(commit *models.Commit) string {
|
||||
func (self *LocalCommitsController) getDisabledReasonForFixup(commit *models.Commit) *types.DisabledReason {
|
||||
if self.context().GetSelectedLineIdx() >= len(self.c.Model().Commits)-1 {
|
||||
return self.c.Tr.CannotSquashOrFixupFirstCommit
|
||||
return &types.DisabledReason{Text: self.c.Tr.CannotSquashOrFixupFirstCommit}
|
||||
}
|
||||
|
||||
return self.rebaseCommandEnabled(todo.Squash, commit)
|
||||
@ -528,9 +528,9 @@ func (self *LocalCommitsController) handleMidRebaseCommand(action todo.TodoComma
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) rebaseCommandEnabled(action todo.TodoCommand, commit *models.Commit) string {
|
||||
func (self *LocalCommitsController) rebaseCommandEnabled(action todo.TodoCommand, commit *models.Commit) *types.DisabledReason {
|
||||
if commit.Action == models.ActionConflict {
|
||||
return self.c.Tr.ChangingThisActionIsNotAllowed
|
||||
return &types.DisabledReason{Text: self.c.Tr.ChangingThisActionIsNotAllowed}
|
||||
}
|
||||
|
||||
if !commit.IsTODO() {
|
||||
@ -538,11 +538,11 @@ func (self *LocalCommitsController) rebaseCommandEnabled(action todo.TodoCommand
|
||||
// If we are in a rebase, the only action that is allowed for
|
||||
// non-todo commits is rewording the current head commit
|
||||
if !(action == todo.Reword && self.isHeadCommit()) {
|
||||
return self.c.Tr.AlreadyRebasing
|
||||
return &types.DisabledReason{Text: self.c.Tr.AlreadyRebasing}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
// for now we do not support setting 'reword' because it requires an editor
|
||||
@ -550,14 +550,14 @@ func (self *LocalCommitsController) rebaseCommandEnabled(action todo.TodoCommand
|
||||
// our input or we set a lazygit client as the EDITOR env variable and have it
|
||||
// request us to edit the commit message when prompted.
|
||||
if action == todo.Reword {
|
||||
return self.c.Tr.RewordNotSupported
|
||||
return &types.DisabledReason{Text: self.c.Tr.RewordNotSupported}
|
||||
}
|
||||
|
||||
if allowed := isChangeOfRebaseTodoAllowed(action); !allowed {
|
||||
return self.c.Tr.ChangingThisActionIsNotAllowed
|
||||
return &types.DisabledReason{Text: self.c.Tr.ChangingThisActionIsNotAllowed}
|
||||
}
|
||||
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) moveDown(commit *models.Commit) error {
|
||||
@ -687,12 +687,12 @@ func (self *LocalCommitsController) amendTo(commit *models.Commit) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) getDisabledReasonForAmendTo(commit *models.Commit) string {
|
||||
func (self *LocalCommitsController) getDisabledReasonForAmendTo(commit *models.Commit) *types.DisabledReason {
|
||||
if !self.isHeadCommit() && self.c.Model().WorkingTreeStateAtLastCommitRefresh != enums.REBASE_MODE_NONE {
|
||||
return self.c.Tr.AlreadyRebasing
|
||||
return &types.DisabledReason{Text: self.c.Tr.AlreadyRebasing}
|
||||
}
|
||||
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) amendAttribute(commit *models.Commit) error {
|
||||
@ -870,30 +870,30 @@ func (self *LocalCommitsController) squashAllAboveFixupCommits(commit *models.Co
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) getDisabledReasonForSquashAllAboveFixupCommits(commit *models.Commit) string {
|
||||
func (self *LocalCommitsController) getDisabledReasonForSquashAllAboveFixupCommits(commit *models.Commit) *types.DisabledReason {
|
||||
if self.c.Model().WorkingTreeStateAtLastCommitRefresh != enums.REBASE_MODE_NONE {
|
||||
return self.c.Tr.AlreadyRebasing
|
||||
return &types.DisabledReason{Text: self.c.Tr.AlreadyRebasing}
|
||||
}
|
||||
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
// For getting disabled reason
|
||||
func (self *LocalCommitsController) notMidRebase() string {
|
||||
func (self *LocalCommitsController) notMidRebase() *types.DisabledReason {
|
||||
if self.c.Model().WorkingTreeStateAtLastCommitRefresh != enums.REBASE_MODE_NONE {
|
||||
return self.c.Tr.AlreadyRebasing
|
||||
return &types.DisabledReason{Text: self.c.Tr.AlreadyRebasing}
|
||||
}
|
||||
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
// For getting disabled reason
|
||||
func (self *LocalCommitsController) canFindCommitForQuickStart() string {
|
||||
func (self *LocalCommitsController) canFindCommitForQuickStart() *types.DisabledReason {
|
||||
if _, err := self.findCommitForQuickStartInteractiveRebase(); err != nil {
|
||||
return err.Error()
|
||||
return &types.DisabledReason{Text: err.Error(), ShowErrorInPanel: true}
|
||||
}
|
||||
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) createTag(commit *models.Commit) error {
|
||||
@ -1028,23 +1028,23 @@ func (self *LocalCommitsController) checkSelected(callback func(*models.Commit)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) callGetDisabledReasonFuncWithSelectedCommit(callback func(*models.Commit) string) func() string {
|
||||
return func() string {
|
||||
func (self *LocalCommitsController) callGetDisabledReasonFuncWithSelectedCommit(callback func(*models.Commit) *types.DisabledReason) func() *types.DisabledReason {
|
||||
return func() *types.DisabledReason {
|
||||
commit := self.context().GetSelected()
|
||||
if commit == nil {
|
||||
return self.c.Tr.NoCommitSelected
|
||||
return &types.DisabledReason{Text: self.c.Tr.NoCommitSelected}
|
||||
}
|
||||
|
||||
return callback(commit)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) disabledIfNoSelectedCommit() func() string {
|
||||
return self.callGetDisabledReasonFuncWithSelectedCommit(func(*models.Commit) string { return "" })
|
||||
func (self *LocalCommitsController) disabledIfNoSelectedCommit() func() *types.DisabledReason {
|
||||
return self.callGetDisabledReasonFuncWithSelectedCommit(func(*models.Commit) *types.DisabledReason { return nil })
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) getDisabledReasonForRebaseCommandWithSelectedCommit(action todo.TodoCommand) func() string {
|
||||
return self.callGetDisabledReasonFuncWithSelectedCommit(func(commit *models.Commit) string {
|
||||
func (self *LocalCommitsController) getDisabledReasonForRebaseCommandWithSelectedCommit(action todo.TodoCommand) func() *types.DisabledReason {
|
||||
return self.callGetDisabledReasonFuncWithSelectedCommit(func(commit *models.Commit) *types.DisabledReason {
|
||||
return self.rebaseCommandEnabled(action, commit)
|
||||
})
|
||||
}
|
||||
@ -1077,12 +1077,12 @@ func (self *LocalCommitsController) paste() error {
|
||||
return self.c.Helpers().CherryPick.Paste()
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) getDisabledReasonForPaste() string {
|
||||
func (self *LocalCommitsController) getDisabledReasonForPaste() *types.DisabledReason {
|
||||
if !self.c.Helpers().CherryPick.CanPaste() {
|
||||
return self.c.Tr.NoCopiedCommits
|
||||
return &types.DisabledReason{Text: self.c.Tr.NoCopiedCommits}
|
||||
}
|
||||
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) markAsBaseCommit(commit *models.Commit) error {
|
||||
@ -1100,15 +1100,15 @@ func (self *LocalCommitsController) isHeadCommit() bool {
|
||||
}
|
||||
|
||||
// Convenience function for composing multiple disabled reason functions
|
||||
func (self *LocalCommitsController) require(callbacks ...func() string) func() string {
|
||||
return func() string {
|
||||
func (self *LocalCommitsController) require(callbacks ...func() *types.DisabledReason) func() *types.DisabledReason {
|
||||
return func() *types.DisabledReason {
|
||||
for _, callback := range callbacks {
|
||||
if disabledReason := callback(); disabledReason != "" {
|
||||
if disabledReason := callback(); disabledReason != nil {
|
||||
return disabledReason
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ func (self *OptionsMenuAction) Call() error {
|
||||
appendBindings := func(bindings []*types.Binding, section *types.MenuSection) {
|
||||
menuItems = append(menuItems,
|
||||
lo.Map(bindings, func(binding *types.Binding, _ int) *types.MenuItem {
|
||||
disabledReason := ""
|
||||
var disabledReason *types.DisabledReason
|
||||
if binding.GetDisabledReason != nil {
|
||||
disabledReason = binding.GetDisabledReason()
|
||||
}
|
||||
|
@ -59,16 +59,16 @@ func (self *SyncController) HandlePull() error {
|
||||
return self.branchCheckedOut(self.pull)()
|
||||
}
|
||||
|
||||
func (self *SyncController) getDisabledReasonForPushOrPull() string {
|
||||
func (self *SyncController) getDisabledReasonForPushOrPull() *types.DisabledReason {
|
||||
currentBranch := self.c.Helpers().Refs.GetCheckedOutRef()
|
||||
if currentBranch != nil {
|
||||
op := self.c.State().GetItemOperation(currentBranch)
|
||||
if op != types.ItemOperationNone {
|
||||
return self.c.Tr.CantPullOrPushSameBranchTwice
|
||||
return &types.DisabledReason{Text: self.c.Tr.CantPullOrPushSameBranchTwice}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *SyncController) branchCheckedOut(f func(*models.Branch) error) func() error {
|
||||
|
@ -17,6 +17,6 @@ func NewDummyUpdater() *updates.Updater {
|
||||
|
||||
func NewDummyGui() *Gui {
|
||||
newAppConfig := config.NewDummyAppConfig()
|
||||
dummyGui, _ := NewGui(utils.NewDummyCommon(), newAppConfig, &git_commands.GitVersion{}, NewDummyUpdater(), false, "")
|
||||
dummyGui, _ := NewGui(utils.NewDummyCommon(), newAppConfig, &git_commands.GitVersion{}, NewDummyUpdater(), false, "", nil)
|
||||
return dummyGui
|
||||
}
|
||||
|
@ -467,6 +467,7 @@ func NewGui(
|
||||
updater *updates.Updater,
|
||||
showRecentRepos bool,
|
||||
initialDir string,
|
||||
test integrationTypes.IntegrationTest,
|
||||
) (*Gui, error) {
|
||||
gui := &Gui{
|
||||
Common: cmn,
|
||||
@ -516,7 +517,7 @@ func NewGui(
|
||||
func(message string, f func() error) {
|
||||
gui.helpers.AppStatus.WithWaitingStatusSync(message, f)
|
||||
},
|
||||
func(message string) { gui.helpers.AppStatus.Toast(message) },
|
||||
func(message string, kind types.ToastKind) { gui.helpers.AppStatus.Toast(message, kind) },
|
||||
func() string { return gui.Views.Confirmation.TextArea.GetContent() },
|
||||
func() bool { return gui.c.InDemo() },
|
||||
)
|
||||
|
@ -20,11 +20,14 @@ import (
|
||||
type GuiDriver struct {
|
||||
gui *Gui
|
||||
isIdleChan chan struct{}
|
||||
toastChan chan string
|
||||
}
|
||||
|
||||
var _ integrationTypes.GuiDriver = &GuiDriver{}
|
||||
|
||||
func (self *GuiDriver) PressKey(keyStr string) {
|
||||
self.CheckAllToastsAcknowledged()
|
||||
|
||||
key := keybindings.GetKey(keyStr)
|
||||
|
||||
var r rune
|
||||
@ -46,6 +49,8 @@ func (self *GuiDriver) PressKey(keyStr string) {
|
||||
}
|
||||
|
||||
func (self *GuiDriver) Click(x, y int) {
|
||||
self.CheckAllToastsAcknowledged()
|
||||
|
||||
self.gui.g.ReplayedEvents.MouseEvents <- gocui.NewTcellMouseEventWrapper(
|
||||
tcell.NewEventMouse(x, y, tcell.ButtonPrimary, 0),
|
||||
0,
|
||||
@ -58,6 +63,12 @@ func (self *GuiDriver) waitTillIdle() {
|
||||
<-self.isIdleChan
|
||||
}
|
||||
|
||||
func (self *GuiDriver) CheckAllToastsAcknowledged() {
|
||||
if t := self.NextToast(); t != nil {
|
||||
self.Fail("Toast not acknowledged: " + *t)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *GuiDriver) Keys() config.KeybindingConfig {
|
||||
return self.gui.Config.GetUserConfig().Keybinding
|
||||
}
|
||||
@ -133,3 +144,12 @@ func (self *GuiDriver) SetCaptionPrefix(prefix string) {
|
||||
self.gui.setCaptionPrefix(prefix)
|
||||
self.waitTillIdle()
|
||||
}
|
||||
|
||||
func (self *GuiDriver) NextToast() *string {
|
||||
select {
|
||||
case t := <-self.toastChan:
|
||||
return &t
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -411,12 +411,17 @@ func (gui *Gui) SetMouseKeybinding(binding *gocui.ViewMouseBinding) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) callKeybindingHandler(binding *types.Binding) error {
|
||||
disabledReason := ""
|
||||
var disabledReason *types.DisabledReason
|
||||
if binding.GetDisabledReason != nil {
|
||||
disabledReason = binding.GetDisabledReason()
|
||||
}
|
||||
if disabledReason != "" {
|
||||
return gui.c.ErrorMsg(disabledReason)
|
||||
if disabledReason != nil {
|
||||
if disabledReason.ShowErrorInPanel {
|
||||
return gui.c.ErrorMsg(disabledReason.Text)
|
||||
}
|
||||
|
||||
gui.c.ErrorToast(gui.Tr.DisabledMenuItemPrefix + disabledReason.Text)
|
||||
return nil
|
||||
}
|
||||
return binding.Handler()
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ type PopupHandler struct {
|
||||
createMenuFn func(types.CreateMenuOptions) error
|
||||
withWaitingStatusFn func(message string, f func(gocui.Task) error)
|
||||
withWaitingStatusSyncFn func(message string, f func() error)
|
||||
toastFn func(message string)
|
||||
toastFn func(message string, kind types.ToastKind)
|
||||
getPromptInputFn func() string
|
||||
inDemo func() bool
|
||||
}
|
||||
@ -38,7 +38,7 @@ func NewPopupHandler(
|
||||
createMenuFn func(types.CreateMenuOptions) error,
|
||||
withWaitingStatusFn func(message string, f func(gocui.Task) error),
|
||||
withWaitingStatusSyncFn func(message string, f func() error),
|
||||
toastFn func(message string),
|
||||
toastFn func(message string, kind types.ToastKind),
|
||||
getPromptInputFn func() string,
|
||||
inDemo func() bool,
|
||||
) *PopupHandler {
|
||||
@ -63,7 +63,15 @@ func (self *PopupHandler) Menu(opts types.CreateMenuOptions) error {
|
||||
}
|
||||
|
||||
func (self *PopupHandler) Toast(message string) {
|
||||
self.toastFn(message)
|
||||
self.toastFn(message, types.ToastKindStatus)
|
||||
}
|
||||
|
||||
func (self *PopupHandler) ErrorToast(message string) {
|
||||
self.toastFn(message, types.ToastKindError)
|
||||
}
|
||||
|
||||
func (self *PopupHandler) SetToastFunc(f func(string, types.ToastKind)) {
|
||||
self.toastFn = f
|
||||
}
|
||||
|
||||
func (self *PopupHandler) WithWaitingStatus(message string, f func(gocui.Task) error) error {
|
||||
|
@ -3,6 +3,8 @@ package status
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
@ -26,7 +28,7 @@ type WaitingStatusHandle struct {
|
||||
}
|
||||
|
||||
func (self *WaitingStatusHandle) Show() {
|
||||
self.id = self.statusManager.addStatus(self.message, "waiting")
|
||||
self.id = self.statusManager.addStatus(self.message, "waiting", types.ToastKindStatus)
|
||||
self.renderFunc()
|
||||
}
|
||||
|
||||
@ -37,6 +39,7 @@ func (self *WaitingStatusHandle) Hide() {
|
||||
type appStatus struct {
|
||||
message string
|
||||
statusType string
|
||||
color gocui.Attribute
|
||||
id int
|
||||
}
|
||||
|
||||
@ -53,11 +56,12 @@ func (self *StatusManager) WithWaitingStatus(message string, renderFunc func(),
|
||||
handle.Hide()
|
||||
}
|
||||
|
||||
func (self *StatusManager) AddToastStatus(message string) int {
|
||||
id := self.addStatus(message, "toast")
|
||||
func (self *StatusManager) AddToastStatus(message string, kind types.ToastKind) int {
|
||||
id := self.addStatus(message, "toast", kind)
|
||||
|
||||
go func() {
|
||||
time.Sleep(time.Second * 2)
|
||||
delay := lo.Ternary(kind == types.ToastKindError, time.Second*4, time.Second*2)
|
||||
time.Sleep(delay)
|
||||
|
||||
self.removeStatus(id)
|
||||
}()
|
||||
@ -65,31 +69,37 @@ func (self *StatusManager) AddToastStatus(message string) int {
|
||||
return id
|
||||
}
|
||||
|
||||
func (self *StatusManager) GetStatusString() string {
|
||||
func (self *StatusManager) GetStatusString() (string, gocui.Attribute) {
|
||||
if len(self.statuses) == 0 {
|
||||
return ""
|
||||
return "", gocui.ColorDefault
|
||||
}
|
||||
topStatus := self.statuses[0]
|
||||
if topStatus.statusType == "waiting" {
|
||||
return topStatus.message + " " + utils.Loader(time.Now())
|
||||
return topStatus.message + " " + utils.Loader(time.Now()), topStatus.color
|
||||
}
|
||||
return topStatus.message
|
||||
return topStatus.message, topStatus.color
|
||||
}
|
||||
|
||||
func (self *StatusManager) HasStatus() bool {
|
||||
return len(self.statuses) > 0
|
||||
}
|
||||
|
||||
func (self *StatusManager) addStatus(message string, statusType string) int {
|
||||
func (self *StatusManager) addStatus(message string, statusType string, kind types.ToastKind) int {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
|
||||
self.nextId++
|
||||
id := self.nextId
|
||||
|
||||
color := gocui.ColorCyan
|
||||
if kind == types.ToastKindError {
|
||||
color = gocui.ColorRed
|
||||
}
|
||||
|
||||
newStatus := appStatus{
|
||||
message: message,
|
||||
statusType: statusType,
|
||||
color: color,
|
||||
id: id,
|
||||
}
|
||||
self.statuses = append([]appStatus{newStatus}, self.statuses...)
|
||||
|
@ -6,6 +6,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/popup"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
@ -32,7 +34,11 @@ func (gui *Gui) handleTestMode() {
|
||||
go func() {
|
||||
waitUntilIdle()
|
||||
|
||||
test.Run(&GuiDriver{gui: gui, isIdleChan: isIdleChan})
|
||||
toastChan := make(chan string, 100)
|
||||
gui.PopupHandler.(*popup.PopupHandler).SetToastFunc(
|
||||
func(message string, kind types.ToastKind) { toastChan <- message })
|
||||
|
||||
test.Run(&GuiDriver{gui: gui, isIdleChan: isIdleChan, toastChan: toastChan})
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
return gocui.ErrQuit
|
||||
|
@ -144,9 +144,18 @@ type IPopupHandler interface {
|
||||
WithWaitingStatusSync(message string, f func() error) error
|
||||
Menu(opts CreateMenuOptions) error
|
||||
Toast(message string)
|
||||
ErrorToast(message string)
|
||||
SetToastFunc(func(string, ToastKind))
|
||||
GetPromptInput() string
|
||||
}
|
||||
|
||||
type ToastKind int
|
||||
|
||||
const (
|
||||
ToastKindStatus ToastKind = iota
|
||||
ToastKindError
|
||||
)
|
||||
|
||||
type CreateMenuOptions struct {
|
||||
Title string
|
||||
Items []*MenuItem
|
||||
@ -192,6 +201,16 @@ type MenuSection struct {
|
||||
Column int // The column that this section title should be aligned with
|
||||
}
|
||||
|
||||
type DisabledReason struct {
|
||||
Text string
|
||||
|
||||
// When trying to invoke a disabled key binding or menu item, we normally
|
||||
// show the disabled reason as a toast; setting this to true shows it as an
|
||||
// error panel instead. This is useful if the text is very long, or if it is
|
||||
// important enough to show it more prominently, or both.
|
||||
ShowErrorInPanel bool
|
||||
}
|
||||
|
||||
type MenuItem struct {
|
||||
Label string
|
||||
|
||||
@ -210,9 +229,9 @@ type MenuItem struct {
|
||||
// The tooltip will be displayed upon highlighting the menu item
|
||||
Tooltip string
|
||||
|
||||
// If non-empty, show this in a tooltip, style the menu item as disabled,
|
||||
// If non-nil, show this in a tooltip, style the menu item as disabled,
|
||||
// and refuse to invoke the command
|
||||
DisabledReason string
|
||||
DisabledReason *DisabledReason
|
||||
|
||||
// Can be used to group menu items into sections with headers. MenuItems
|
||||
// with the same Section should be contiguous, and will automatically get a
|
||||
|
@ -31,7 +31,7 @@ type Binding struct {
|
||||
// disabled and we show the given text in an error message when trying to
|
||||
// invoke it. When left nil, the command is always enabled. Note that this
|
||||
// function must not do expensive calls.
|
||||
GetDisabledReason func() string
|
||||
GetDisabledReason func() *DisabledReason
|
||||
}
|
||||
|
||||
// A guard is a decorator which checks something before executing a handler
|
||||
|
@ -18,10 +18,12 @@ func (self *MenuDriver) Title(expected *TextMatcher) *MenuDriver {
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *MenuDriver) Confirm() {
|
||||
func (self *MenuDriver) Confirm() *MenuDriver {
|
||||
self.checkNecessaryChecksCompleted()
|
||||
|
||||
self.getViewDriver().PressEnter()
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *MenuDriver) Cancel() {
|
||||
@ -72,6 +74,11 @@ func (self *MenuDriver) Tooltip(option *TextMatcher) *MenuDriver {
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *MenuDriver) Tap(f func()) *MenuDriver {
|
||||
self.getViewDriver().Tap(f)
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *MenuDriver) checkNecessaryChecksCompleted() {
|
||||
if !self.hasCheckedTitle {
|
||||
self.t.Fail("You must check the title of a menu popup by calling Title() before calling Confirm()/Cancel().")
|
||||
|
@ -194,6 +194,8 @@ func (self *IntegrationTest) Run(gui integrationTypes.GuiDriver) {
|
||||
|
||||
self.run(testDriver, keys)
|
||||
|
||||
gui.CheckAllToastsAcknowledged()
|
||||
|
||||
if InputDelay() > 0 {
|
||||
// Clear whatever caption there was so it doesn't linger
|
||||
testDriver.SetCaption("")
|
||||
|
@ -102,8 +102,19 @@ func (self *TestDriver) ExpectPopup() *Popup {
|
||||
return &Popup{t: self}
|
||||
}
|
||||
|
||||
func (self *TestDriver) ExpectToast(matcher *TextMatcher) {
|
||||
self.Views().AppStatus().Content(matcher)
|
||||
func (self *TestDriver) ExpectToast(matcher *TextMatcher) *TestDriver {
|
||||
t := self.gui.NextToast()
|
||||
if t == nil {
|
||||
self.gui.Fail("Expected toast, but didn't get one")
|
||||
} else {
|
||||
self.matchString(matcher, "Unexpected toast message",
|
||||
func() string {
|
||||
return *t
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *TestDriver) ExpectClipboard(matcher *TextMatcher) {
|
||||
|
@ -78,6 +78,12 @@ func (self *fakeGuiDriver) SetCaption(string) {
|
||||
func (self *fakeGuiDriver) SetCaptionPrefix(string) {
|
||||
}
|
||||
|
||||
func (self *fakeGuiDriver) NextToast() *string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *fakeGuiDriver) CheckAllToastsAcknowledged() {}
|
||||
|
||||
func TestManualFailure(t *testing.T) {
|
||||
test := NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: unitTestDescription,
|
||||
|
@ -37,12 +37,11 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Tooltip(Contains("You cannot delete the checked out branch!")).
|
||||
Title(Equals("Delete branch 'branch-three'?")).
|
||||
Select(Contains("Delete local branch")).
|
||||
Confirm()
|
||||
t.ExpectPopup().
|
||||
Alert().
|
||||
Title(Equals("Error")).
|
||||
Content(Contains("You cannot delete the checked out branch!")).
|
||||
Confirm()
|
||||
Confirm().
|
||||
Tap(func() {
|
||||
t.ExpectToast(Contains("You cannot delete the checked out branch!"))
|
||||
}).
|
||||
Cancel()
|
||||
}).
|
||||
SelectNextItem().
|
||||
Press(keys.Universal.Remove).
|
||||
|
@ -48,11 +48,11 @@ var RebaseToUpstream = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Title(Equals("Upstream options")).
|
||||
Select(Contains("Rebase checked-out branch onto upstream of selected branch")).
|
||||
Tooltip(Contains("Disabled: The selected branch has no upstream (or the upstream is not stored locally)")).
|
||||
Confirm()
|
||||
t.ExpectPopup().Alert().
|
||||
Title(Equals("Error")).
|
||||
Content(Equals("The selected branch has no upstream (or the upstream is not stored locally)")).
|
||||
Confirm()
|
||||
Confirm().
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Disabled: The selected branch has no upstream (or the upstream is not stored locally)"))
|
||||
}).
|
||||
Cancel()
|
||||
}).
|
||||
SelectNextItem().
|
||||
Lines(
|
||||
|
@ -42,11 +42,11 @@ var ResetToUpstream = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Title(Equals("Upstream options")).
|
||||
Select(Contains("Reset checked-out branch onto upstream of selected branch")).
|
||||
Tooltip(Contains("Disabled: The selected branch has no upstream (or the upstream is not stored locally)")).
|
||||
Confirm()
|
||||
t.ExpectPopup().Alert().
|
||||
Title(Equals("Error")).
|
||||
Content(Equals("The selected branch has no upstream (or the upstream is not stored locally)")).
|
||||
Confirm()
|
||||
Confirm().
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Disabled: The selected branch has no upstream (or the upstream is not stored locally)"))
|
||||
}).
|
||||
Cancel()
|
||||
}).
|
||||
SelectNextItem().
|
||||
Lines(
|
||||
|
@ -30,12 +30,11 @@ var CopyMenu = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Title(Equals("Copy to clipboard")).
|
||||
Select(Contains("File name")).
|
||||
Tooltip(Equals("Disabled: Nothing to copy")).
|
||||
Confirm()
|
||||
|
||||
t.ExpectPopup().Alert().
|
||||
Title(Equals("Error")).
|
||||
Content(Equals("Nothing to copy")).
|
||||
Confirm()
|
||||
Confirm().
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Disabled: Nothing to copy"))
|
||||
}).
|
||||
Cancel()
|
||||
})
|
||||
|
||||
t.Shell().
|
||||
@ -56,12 +55,11 @@ var CopyMenu = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Title(Equals("Copy to clipboard")).
|
||||
Select(Contains("Diff of selected file")).
|
||||
Tooltip(Contains("Disabled: Nothing to copy")).
|
||||
Confirm()
|
||||
|
||||
t.ExpectPopup().Alert().
|
||||
Title(Equals("Error")).
|
||||
Content(Equals("Nothing to copy")).
|
||||
Confirm()
|
||||
Confirm().
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Disabled: Nothing to copy"))
|
||||
}).
|
||||
Cancel()
|
||||
}).
|
||||
Press(keys.Files.CopyFileInfoToClipboard).
|
||||
Tap(func() {
|
||||
@ -69,12 +67,11 @@ var CopyMenu = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Title(Equals("Copy to clipboard")).
|
||||
Select(Contains("Diff of all files")).
|
||||
Tooltip(Contains("Disabled: Nothing to copy")).
|
||||
Confirm()
|
||||
|
||||
t.ExpectPopup().Alert().
|
||||
Title(Equals("Error")).
|
||||
Content(Equals("Nothing to copy")).
|
||||
Confirm()
|
||||
Confirm().
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Disabled: Nothing to copy"))
|
||||
}).
|
||||
Cancel()
|
||||
})
|
||||
|
||||
t.Shell().
|
||||
@ -101,6 +98,8 @@ var CopyMenu = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Select(Contains("File name")).
|
||||
Confirm()
|
||||
|
||||
t.ExpectToast(Equals("File name copied to clipboard"))
|
||||
|
||||
expectClipboard(t, Contains("unstaged_file"))
|
||||
})
|
||||
|
||||
@ -113,6 +112,8 @@ var CopyMenu = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Select(Contains("Path")).
|
||||
Confirm()
|
||||
|
||||
t.ExpectToast(Equals("File path copied to clipboard"))
|
||||
|
||||
expectClipboard(t, Contains("dir/1-unstaged_file"))
|
||||
})
|
||||
|
||||
@ -126,6 +127,8 @@ var CopyMenu = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Tooltip(Equals("If there are staged items, this command considers only them. Otherwise, it considers all the unstaged ones.")).
|
||||
Confirm()
|
||||
|
||||
t.ExpectToast(Equals("File diff copied to clipboard"))
|
||||
|
||||
expectClipboard(t, Contains("+unstaged content (new)"))
|
||||
})
|
||||
|
||||
@ -145,6 +148,8 @@ var CopyMenu = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Tooltip(Equals("If there are staged items, this command considers only them. Otherwise, it considers all the unstaged ones.")).
|
||||
Confirm()
|
||||
|
||||
t.ExpectToast(Equals("File diff copied to clipboard"))
|
||||
|
||||
expectClipboard(t, Contains("+staged content (new)"))
|
||||
})
|
||||
|
||||
@ -158,6 +163,8 @@ var CopyMenu = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Tooltip(Equals("If there are staged items, this command considers only them. Otherwise, it considers all the unstaged ones.")).
|
||||
Confirm()
|
||||
|
||||
t.ExpectToast(Equals("All files diff copied to clipboard"))
|
||||
|
||||
expectClipboard(t, Contains("+staged content (new)"))
|
||||
})
|
||||
|
||||
@ -179,6 +186,8 @@ var CopyMenu = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Tooltip(Equals("If there are staged items, this command considers only them. Otherwise, it considers all the unstaged ones.")).
|
||||
Confirm()
|
||||
|
||||
t.ExpectToast(Equals("All files diff copied to clipboard"))
|
||||
|
||||
expectClipboard(t, Contains("+staged content (new)").Contains("+unstaged content (new)"))
|
||||
})
|
||||
},
|
||||
|
@ -34,10 +34,7 @@ var AmendNonHeadCommitDuringRebase = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
NavigateToLine(Contains(commit)).
|
||||
Press(keys.Commits.AmendToCommit)
|
||||
|
||||
t.ExpectPopup().Alert().
|
||||
Title(Equals("Error")).
|
||||
Content(Contains("Can't perform this action during a rebase")).
|
||||
Confirm()
|
||||
t.ExpectToast(Contains("Can't perform this action during a rebase"))
|
||||
}
|
||||
},
|
||||
})
|
||||
|
@ -29,9 +29,6 @@ var EditNonTodoCommitDuringRebase = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
NavigateToLine(Contains("commit 01")).
|
||||
Press(keys.Universal.Edit)
|
||||
|
||||
t.ExpectPopup().Alert().
|
||||
Title(Equals("Error")).
|
||||
Content(Contains("Can't perform this action during a rebase")).
|
||||
Confirm()
|
||||
t.ExpectToast(Contains("Can't perform this action during a rebase"))
|
||||
},
|
||||
})
|
||||
|
@ -39,9 +39,6 @@ var EditTheConflCommit = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
NavigateToLine(Contains("<-- YOU ARE HERE --- commit three")).
|
||||
Press(keys.Commits.RenameCommit)
|
||||
|
||||
t.ExpectPopup().Alert().
|
||||
Title(Equals("Error")).
|
||||
Content(Contains("Changing this kind of rebase todo entry is not allowed")).
|
||||
Confirm()
|
||||
t.ExpectToast(Contains("Changing this kind of rebase todo entry is not allowed"))
|
||||
},
|
||||
})
|
||||
|
@ -24,10 +24,7 @@ var FixupFirstCommit = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
NavigateToLine(Contains("commit 01")).
|
||||
Press(keys.Commits.MarkCommitAsFixup).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Alert().
|
||||
Title(Equals("Error")).
|
||||
Content(Equals("There's no commit below to squash into")).
|
||||
Confirm()
|
||||
t.ExpectToast(Equals("Disabled: There's no commit below to squash into"))
|
||||
}).
|
||||
Lines(
|
||||
Contains("commit 02"),
|
||||
|
@ -50,13 +50,12 @@ var QuickStart = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Contains("initial commit"),
|
||||
).
|
||||
// Verify we can't quick start from main
|
||||
Press(keys.Commits.StartInteractiveRebase).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Alert().
|
||||
Title(Equals("Error")).
|
||||
Content(Contains("Cannot start interactive rebase: the HEAD commit is a merge commit or is present on the main branch, so there is no appropriate base commit to start the rebase from. You can start an interactive rebase from a specific commit by selecting the commit and pressing `e`.")).
|
||||
Confirm()
|
||||
})
|
||||
Press(keys.Commits.StartInteractiveRebase)
|
||||
|
||||
t.ExpectPopup().Alert().
|
||||
Title(Equals("Error")).
|
||||
Content(Equals("Cannot start interactive rebase: the HEAD commit is a merge commit or is present on the main branch, so there is no appropriate base commit to start the rebase from. You can start an interactive rebase from a specific commit by selecting the commit and pressing `e`.")).
|
||||
Confirm()
|
||||
|
||||
t.Views().Branches().
|
||||
Focus().
|
||||
@ -80,15 +79,10 @@ var QuickStart = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Contains("initial commit"),
|
||||
).
|
||||
// Try again, verify we fail because we're already rebasing
|
||||
Press(keys.Commits.StartInteractiveRebase).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Alert().
|
||||
Title(Equals("Error")).
|
||||
Content(Contains("Can't perform this action during a rebase")).
|
||||
Confirm()
|
||||
Press(keys.Commits.StartInteractiveRebase)
|
||||
|
||||
t.Common().AbortRebase()
|
||||
})
|
||||
t.ExpectToast(Equals("Disabled: Can't perform this action during a rebase"))
|
||||
t.Common().AbortRebase()
|
||||
|
||||
// Verify if a merge commit is present on the branch we start from there
|
||||
t.Views().Branches().
|
||||
|
@ -24,10 +24,7 @@ var SquashDownFirstCommit = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
NavigateToLine(Contains("commit 01")).
|
||||
Press(keys.Commits.SquashDown).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Alert().
|
||||
Title(Equals("Error")).
|
||||
Content(Equals("There's no commit below to squash into")).
|
||||
Confirm()
|
||||
t.ExpectToast(Equals("Disabled: There's no commit below to squash into"))
|
||||
}).
|
||||
Lines(
|
||||
Contains("commit 02"),
|
||||
|
@ -27,6 +27,8 @@ var CopyToClipboard = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
).
|
||||
Press(keys.Universal.CopyToClipboard)
|
||||
|
||||
t.ExpectToast(Equals("'branch-a' Copied to clipboard"))
|
||||
|
||||
t.Views().Files().
|
||||
Focus()
|
||||
|
||||
|
@ -62,6 +62,9 @@ var DiffContextChange = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Contains(` 6a`),
|
||||
).
|
||||
Press(keys.Universal.IncreaseContextInDiffView).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Changed diff context size to 4"))
|
||||
}).
|
||||
SelectedLines(
|
||||
Contains(`@@ -1,7 +1,7 @@`),
|
||||
Contains(` 1a`),
|
||||
@ -74,6 +77,9 @@ var DiffContextChange = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Contains(` 7a`),
|
||||
).
|
||||
Press(keys.Universal.DecreaseContextInDiffView).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Changed diff context size to 3"))
|
||||
}).
|
||||
SelectedLines(
|
||||
Contains(`@@ -1,6 +1,6 @@`),
|
||||
Contains(` 1a`),
|
||||
@ -85,6 +91,9 @@ var DiffContextChange = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Contains(` 6a`),
|
||||
).
|
||||
Press(keys.Universal.DecreaseContextInDiffView).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Changed diff context size to 2"))
|
||||
}).
|
||||
SelectedLines(
|
||||
Contains(`@@ -1,5 +1,5 @@`),
|
||||
Contains(` 1a`),
|
||||
@ -95,6 +104,9 @@ var DiffContextChange = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Contains(` 5a`),
|
||||
).
|
||||
Press(keys.Universal.DecreaseContextInDiffView).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Changed diff context size to 1"))
|
||||
}).
|
||||
SelectedLines(
|
||||
Contains(`@@ -2,3 +2,3 @@`),
|
||||
Contains(` 2a`),
|
||||
@ -116,6 +128,9 @@ var DiffContextChange = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Contains(` 4a`),
|
||||
).
|
||||
Press(keys.Universal.IncreaseContextInDiffView).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Changed diff context size to 2"))
|
||||
}).
|
||||
SelectedLines(
|
||||
Contains(`@@ -1,5 +1,5 @@`),
|
||||
Contains(` 1a`),
|
||||
|
@ -65,6 +65,7 @@ var CrudAnnotated = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Title(Equals("Delete tag 'new-tag'?")).
|
||||
Content(Equals("Are you sure you want to delete the remote tag 'new-tag' from 'origin'?")).
|
||||
Confirm()
|
||||
t.ExpectToast(Equals("Remote tag deleted"))
|
||||
}).
|
||||
Lines(
|
||||
MatchesRegexp(`new-tag.*message`).IsSelected(),
|
||||
|
@ -70,6 +70,7 @@ var CrudLightweight = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Title(Equals("Delete tag 'new-tag'?")).
|
||||
Content(Equals("Are you sure you want to delete the remote tag 'new-tag' from 'origin'?")).
|
||||
Confirm()
|
||||
t.ExpectToast(Equals("Remote tag deleted"))
|
||||
}).
|
||||
Lines(
|
||||
MatchesRegexp(`new-tag.*initial commit`).IsSelected(),
|
||||
|
@ -43,4 +43,7 @@ type GuiDriver interface {
|
||||
View(viewName string) *gocui.View
|
||||
SetCaption(caption string)
|
||||
SetCaptionPrefix(prefix string)
|
||||
// Pop the next toast that was displayed; returns nil if there was none
|
||||
NextToast() *string
|
||||
CheckAllToastsAcknowledged()
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user