Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delete and edit custom commands history items #3534

Merged
merged 3 commits into from
May 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions pkg/gui/context/suggestions_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ type SuggestionsContext struct {
}

type SuggestionsContextState struct {
Suggestions []*types.Suggestion
OnConfirm func() error
OnClose func() error
AsyncHandler *tasks.AsyncHandler
Suggestions []*types.Suggestion
OnConfirm func() error
OnClose func() error
OnDeleteSuggestion func() error
AsyncHandler *tasks.AsyncHandler

AllowEditSuggestion bool

// FindSuggestions will take a string that the user has typed into a prompt
// and return a slice of suggestions which match that string.
Expand Down
10 changes: 10 additions & 0 deletions pkg/gui/controllers/confirmation_controller.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package controllers

import (
"fmt"

"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
Expand Down Expand Up @@ -39,6 +41,14 @@ func (self *ConfirmationController) GetKeybindings(opts types.KeybindingsOpts) [
Key: opts.GetKey(opts.Config.Universal.TogglePanel),
Handler: func() error {
if len(self.c.Contexts().Suggestions.State.Suggestions) > 0 {
subtitle := ""
if self.c.State().GetRepoState().GetCurrentPopupOpts().HandleDeleteSuggestion != nil {
// We assume that whenever things are deletable, they
// are also editable, so we show both keybindings
subtitle = fmt.Sprintf(self.c.Tr.SuggestionsSubtitle,
self.c.UserConfig.Keybinding.Universal.Remove, self.c.UserConfig.Keybinding.Universal.Edit)
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jesseduffield I'm showing the keybindings when the suggestions panel gets the focus (and remove it again in the opposite handler in SuggestionsController). I would have preferred not to do this here, but in GetOnFocus/GetOnFocusLost, so that it is reliably called no matter how we switch focus (maybe we want to make it possible some day to click on a suggestion to select it).

But that's not possible right now because ContextMgr.Replace only calls Activate on the new context, but not deactivate on the old one. I tried to "fix" that, but that's not possible because the GetOnFocusLost of both ConfirmationController and SuggestionsController destroys the panel.

I'm not sure how to solve this cleanly; let me know if you have any suggestions. For now we just put it here in the tab handler, which works fine.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I am not sure how to solve that cleanly. Happy to use your stopgap solution

self.c.Views().Suggestions.Subtitle = subtitle
return self.c.ReplaceContext(self.c.Contexts().Suggestions)
}
return nil
Expand Down
27 changes: 25 additions & 2 deletions pkg/gui/controllers/custom_command_action.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package controllers

import (
"slices"
"strings"

"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
Expand All @@ -17,6 +18,7 @@ func (self *CustomCommandAction) Call() error {
return self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.CustomCommand,
FindSuggestionsFunc: self.GetCustomCommandsHistorySuggestionsFunc(),
AllowEditSuggestion: true,
HandleConfirm: func(command string) error {
if self.shouldSaveCommand(command) {
self.c.GetAppState().CustomCommandsHistory = utils.Limit(
Expand All @@ -32,13 +34,34 @@ func (self *CustomCommandAction) Call() error {
self.c.OS().Cmd.NewShell(command),
)
},
HandleDeleteSuggestion: func(index int) error {
// index is the index in the _filtered_ list of suggestions, so we
// need to map it back to the full list. There's no really good way
// to do this, but fortunately we keep the items in the
// CustomCommandsHistory unique, which allows us to simply search
// for it by string.
item := self.c.Contexts().Suggestions.GetItems()[index].Value
fullIndex := lo.IndexOf(self.c.GetAppState().CustomCommandsHistory, item)
if fullIndex == -1 {
// Should never happen, but better be safe
return nil
}

self.c.GetAppState().CustomCommandsHistory = slices.Delete(
self.c.GetAppState().CustomCommandsHistory, fullIndex, fullIndex+1)
self.c.SaveAppStateAndLogError()
self.c.Contexts().Suggestions.RefreshSuggestions()
return nil
},
})
}

func (self *CustomCommandAction) GetCustomCommandsHistorySuggestionsFunc() func(string) []*types.Suggestion {
history := self.c.GetAppState().CustomCommandsHistory
return func(input string) []*types.Suggestion {
history := self.c.GetAppState().CustomCommandsHistory

return helpers.FilterFunc(history, self.c.UserConfig.Gui.UseFuzzySearch())
return helpers.FilterFunc(history, self.c.UserConfig.Gui.UseFuzzySearch())(input)
}
}

// this mimics the shell functionality `ignorespace`
Expand Down
14 changes: 14 additions & 0 deletions pkg/gui/controllers/helpers/confirmation_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ func (self *ConfirmationHelper) prepareConfirmationPanel(
suggestionsContext.SetSuggestions(opts.FindSuggestionsFunc(""))
suggestionsView.Visible = true
suggestionsView.Title = fmt.Sprintf(self.c.Tr.SuggestionsTitle, self.c.UserConfig.Keybinding.Universal.TogglePanel)
suggestionsView.Subtitle = ""
}

self.ResizeConfirmationPanel()
Expand Down Expand Up @@ -223,6 +224,8 @@ func (self *ConfirmationHelper) CreatePopupPanel(ctx goContext.Context, opts typ
return err
}

self.c.Contexts().Suggestions.State.AllowEditSuggestion = opts.AllowEditSuggestion

self.c.State().GetRepoState().SetCurrentPopupOpts(&opts)

return self.c.PushContext(self.c.Contexts().Confirmation)
Expand Down Expand Up @@ -270,10 +273,20 @@ func (self *ConfirmationHelper) setKeyBindings(cancel goContext.CancelFunc, opts

onClose := self.wrappedConfirmationFunction(cancel, opts.HandleClose)

onDeleteSuggestion := func() error {
if opts.HandleDeleteSuggestion == nil {
return nil
}

idx := self.c.Contexts().Suggestions.GetSelectedLineIdx()
return opts.HandleDeleteSuggestion(idx)
}

self.c.Contexts().Confirmation.State.OnConfirm = onConfirm
self.c.Contexts().Confirmation.State.OnClose = onClose
self.c.Contexts().Suggestions.State.OnConfirm = onSuggestionConfirm
self.c.Contexts().Suggestions.State.OnClose = onClose
self.c.Contexts().Suggestions.State.OnDeleteSuggestion = onDeleteSuggestion

return nil
}
Expand All @@ -284,6 +297,7 @@ func (self *ConfirmationHelper) clearConfirmationViewKeyBindings() {
self.c.Contexts().Confirmation.State.OnClose = noop
self.c.Contexts().Suggestions.State.OnConfirm = noop
self.c.Contexts().Suggestions.State.OnClose = noop
self.c.Contexts().Suggestions.State.OnDeleteSuggestion = noop
}

func (self *ConfirmationHelper) getSelectedSuggestionValue() string {
Expand Down
28 changes: 26 additions & 2 deletions pkg/gui/controllers/suggestions_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,32 @@ func (self *SuggestionsController) GetKeybindings(opts types.KeybindingsOpts) []
Handler: func() error { return self.context().State.OnClose() },
},
{
Key: opts.GetKey(opts.Config.Universal.TogglePanel),
Handler: func() error { return self.c.ReplaceContext(self.c.Contexts().Confirmation) },
Key: opts.GetKey(opts.Config.Universal.TogglePanel),
Handler: func() error {
self.c.Views().Suggestions.Subtitle = ""
return self.c.ReplaceContext(self.c.Contexts().Confirmation)
},
},
{
Key: opts.GetKey(opts.Config.Universal.Remove),
Handler: func() error {
return self.context().State.OnDeleteSuggestion()
},
},
{
Key: opts.GetKey(opts.Config.Universal.Edit),
Handler: func() error {
if self.context().State.AllowEditSuggestion {
if selectedItem := self.c.Contexts().Suggestions.GetSelected(); selectedItem != nil {
self.c.Contexts().Confirmation.GetView().TextArea.Clear()
self.c.Contexts().Confirmation.GetView().TextArea.TypeString(selectedItem.Value)
self.c.Contexts().Confirmation.GetView().RenderTextArea()
self.c.Contexts().Suggestions.RefreshSuggestions()
return self.c.ReplaceContext(self.c.Contexts().Confirmation)
}
}
return nil
},
},
}

Expand Down
16 changes: 9 additions & 7 deletions pkg/gui/popup/popup_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,15 @@ func (self *PopupHandler) Confirm(opts types.ConfirmOpts) error {

func (self *PopupHandler) Prompt(opts types.PromptOpts) error {
return self.createPopupPanelFn(context.Background(), types.CreatePopupPanelOpts{
Title: opts.Title,
Prompt: opts.InitialContent,
Editable: true,
HandleConfirmPrompt: opts.HandleConfirm,
HandleClose: opts.HandleClose,
FindSuggestionsFunc: opts.FindSuggestionsFunc,
Mask: opts.Mask,
Title: opts.Title,
Prompt: opts.InitialContent,
Editable: true,
HandleConfirmPrompt: opts.HandleConfirm,
HandleClose: opts.HandleClose,
HandleDeleteSuggestion: opts.HandleDeleteSuggestion,
FindSuggestionsFunc: opts.FindSuggestionsFunc,
AllowEditSuggestion: opts.AllowEditSuggestion,
Mask: opts.Mask,
})
}

Expand Down
22 changes: 13 additions & 9 deletions pkg/gui/types/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,16 +165,18 @@ type CreateMenuOptions struct {
}

type CreatePopupPanelOpts struct {
HasLoader bool
Editable bool
Title string
Prompt string
HandleConfirm func() error
HandleConfirmPrompt func(string) error
HandleClose func() error
HasLoader bool
Editable bool
Title string
Prompt string
HandleConfirm func() error
HandleConfirmPrompt func(string) error
HandleClose func() error
HandleDeleteSuggestion func(int) error

FindSuggestionsFunc func(string) []*Suggestion
Mask bool
AllowEditSuggestion bool
}

type ConfirmOpts struct {
Expand All @@ -192,9 +194,11 @@ type PromptOpts struct {
InitialContent string
FindSuggestionsFunc func(string) []*Suggestion
HandleConfirm func(string) error
AllowEditSuggestion bool
// CAPTURE THIS
HandleClose func() error
Mask bool
HandleClose func() error
HandleDeleteSuggestion func(int) error
Mask bool
}

type MenuSection struct {
Expand Down
2 changes: 2 additions & 0 deletions pkg/i18n/english.go
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,7 @@ type TranslationSet struct {
SuggestionsCheatsheetTitle string
// Unlike the cheatsheet title above, the real suggestions title has a little message saying press tab to focus
SuggestionsTitle string
SuggestionsSubtitle string
ExtrasTitle string
PushingTagStatus string
PullRequestURLCopiedToClipboard string
Expand Down Expand Up @@ -1593,6 +1594,7 @@ func EnglishTranslationSet() TranslationSet {
NavigationTitle: "List panel navigation",
SuggestionsCheatsheetTitle: "Suggestions",
SuggestionsTitle: "Suggestions (press %s to focus)",
SuggestionsSubtitle: "(press %s to delete, %s to edit)",
ExtrasTitle: "Command log",
PushingTagStatus: "Pushing tag",
PullRequestURLCopiedToClipboard: "Pull request URL copied to clipboard",
Expand Down
18 changes: 18 additions & 0 deletions pkg/integration/components/prompt_driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,21 @@ func (self *PromptDriver) ConfirmSuggestion(matcher *TextMatcher) {
NavigateToLine(matcher).
PressEnter()
}

func (self *PromptDriver) DeleteSuggestion(matcher *TextMatcher) *PromptDriver {
self.t.press(self.t.keys.Universal.TogglePanel)
self.t.Views().Suggestions().
IsFocused().
NavigateToLine(matcher)
self.t.press(self.t.keys.Universal.Remove)
return self
}

func (self *PromptDriver) EditSuggestion(matcher *TextMatcher) *PromptDriver {
self.t.press(self.t.keys.Universal.TogglePanel)
self.t.Views().Suggestions().
IsFocused().
NavigateToLine(matcher)
self.t.press(self.t.keys.Universal.Edit)
return self
}
41 changes: 41 additions & 0 deletions pkg/integration/tests/custom_commands/delete_from_history.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package custom_commands

import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)

var DeleteFromHistory = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Delete an entry from the custom commands history",
ExtraCmdArgs: []string{},
Skip: false,
SetupRepo: func(shell *Shell) {},
SetupConfig: func(cfg *config.AppConfig) {},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
createCustomCommand := func(command string) {
t.GlobalPress(keys.Universal.ExecuteCustomCommand)
t.ExpectPopup().Prompt().
Title(Equals("Custom command:")).
Type(command).
Confirm()
}

createCustomCommand("echo 1")
createCustomCommand("echo 2")
createCustomCommand("echo 3")

t.GlobalPress(keys.Universal.ExecuteCustomCommand)
t.ExpectPopup().Prompt().
Title(Equals("Custom command:")).
SuggestionLines(
Contains("3"),
Contains("2"),
Contains("1"),
).
DeleteSuggestion(Contains("2")).
SuggestionLines(
Contains("3"),
Contains("1"),
)
},
})
31 changes: 31 additions & 0 deletions pkg/integration/tests/custom_commands/edit_history.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package custom_commands

import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)

var EditHistory = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Edit an entry from the custom commands history",
ExtraCmdArgs: []string{},
Skip: false,
SetupRepo: func(shell *Shell) {},
SetupConfig: func(cfg *config.AppConfig) {},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.GlobalPress(keys.Universal.ExecuteCustomCommand)
t.ExpectPopup().Prompt().
Title(Equals("Custom command:")).
Type("echo x").
Confirm()

t.GlobalPress(keys.Universal.ExecuteCustomCommand)
t.ExpectPopup().Prompt().
Title(Equals("Custom command:")).
Type("ec").
SuggestionLines(
Equals("echo x"),
).
EditSuggestion(Equals("echo x")).
InitialText(Equals("echo x"))
},
})
2 changes: 2 additions & 0 deletions pkg/integration/tests/test_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ var tests = []*components.IntegrationTest{
custom_commands.BasicCmdFromConfig,
custom_commands.CheckForConflicts,
custom_commands.ComplexCmdAtRuntime,
custom_commands.DeleteFromHistory,
custom_commands.EditHistory,
custom_commands.FormPrompts,
custom_commands.History,
custom_commands.MenuFromCommand,
Expand Down
Loading