diff --git a/pkg/gui/context/suggestions_context.go b/pkg/gui/context/suggestions_context.go index 59908fe5e..c741cc769 100644 --- a/pkg/gui/context/suggestions_context.go +++ b/pkg/gui/context/suggestions_context.go @@ -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. diff --git a/pkg/gui/controllers/confirmation_controller.go b/pkg/gui/controllers/confirmation_controller.go index aa5617fa8..45bd16a45 100644 --- a/pkg/gui/controllers/confirmation_controller.go +++ b/pkg/gui/controllers/confirmation_controller.go @@ -1,6 +1,8 @@ package controllers import ( + "fmt" + "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -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) + } + self.c.Views().Suggestions.Subtitle = subtitle return self.c.ReplaceContext(self.c.Contexts().Suggestions) } return nil diff --git a/pkg/gui/controllers/custom_command_action.go b/pkg/gui/controllers/custom_command_action.go index f4de3218e..39777e70a 100644 --- a/pkg/gui/controllers/custom_command_action.go +++ b/pkg/gui/controllers/custom_command_action.go @@ -1,6 +1,7 @@ package controllers import ( + "slices" "strings" "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" @@ -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( @@ -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` diff --git a/pkg/gui/controllers/helpers/confirmation_helper.go b/pkg/gui/controllers/helpers/confirmation_helper.go index 8c1265c15..b5a337fbb 100644 --- a/pkg/gui/controllers/helpers/confirmation_helper.go +++ b/pkg/gui/controllers/helpers/confirmation_helper.go @@ -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() @@ -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) @@ -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 } @@ -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 { diff --git a/pkg/gui/controllers/suggestions_controller.go b/pkg/gui/controllers/suggestions_controller.go index d6e6151ff..857952d9b 100644 --- a/pkg/gui/controllers/suggestions_controller.go +++ b/pkg/gui/controllers/suggestions_controller.go @@ -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 + }, }, } diff --git a/pkg/gui/popup/popup_handler.go b/pkg/gui/popup/popup_handler.go index 1ca9cc3ee..3f71644d3 100644 --- a/pkg/gui/popup/popup_handler.go +++ b/pkg/gui/popup/popup_handler.go @@ -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, }) } diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index 44f07c1e3..d50173078 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -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 { @@ -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 { diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 27905cf9b..caa920de1 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -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 @@ -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", diff --git a/pkg/integration/components/prompt_driver.go b/pkg/integration/components/prompt_driver.go index 023c2f438..a19c29aa4 100644 --- a/pkg/integration/components/prompt_driver.go +++ b/pkg/integration/components/prompt_driver.go @@ -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 +} diff --git a/pkg/integration/tests/custom_commands/delete_from_history.go b/pkg/integration/tests/custom_commands/delete_from_history.go new file mode 100644 index 000000000..e90e6c3c6 --- /dev/null +++ b/pkg/integration/tests/custom_commands/delete_from_history.go @@ -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"), + ) + }, +}) diff --git a/pkg/integration/tests/custom_commands/edit_history.go b/pkg/integration/tests/custom_commands/edit_history.go new file mode 100644 index 000000000..c6999b1f2 --- /dev/null +++ b/pkg/integration/tests/custom_commands/edit_history.go @@ -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")) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 2548702e9..7084fd99b 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -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,