From da3e0f7147a3400374d643f6d746b0911fc5eaaa Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 30 Jan 2024 12:04:02 +0100 Subject: [PATCH 1/3] Support deleting items from the custom commands history In the custom commands panel you can now tab to the suggestions and hit 'd' to delete items from there. Useful if you mistyped a command and don't want it to appear in your history any more. --- pkg/gui/context/suggestions_context.go | 9 ++-- pkg/gui/controllers/custom_command_action.go | 26 +++++++++++- .../helpers/confirmation_helper.go | 11 +++++ pkg/gui/controllers/suggestions_controller.go | 6 +++ pkg/gui/popup/popup_handler.go | 15 +++---- pkg/gui/types/common.go | 20 +++++---- pkg/integration/components/prompt_driver.go | 9 ++++ .../custom_commands/delete_from_history.go | 41 +++++++++++++++++++ pkg/integration/tests/test_list.go | 1 + 9 files changed, 116 insertions(+), 22 deletions(-) create mode 100644 pkg/integration/tests/custom_commands/delete_from_history.go diff --git a/pkg/gui/context/suggestions_context.go b/pkg/gui/context/suggestions_context.go index 59908fe5eb7..7b657121a00 100644 --- a/pkg/gui/context/suggestions_context.go +++ b/pkg/gui/context/suggestions_context.go @@ -14,10 +14,11 @@ 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 // 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/custom_command_action.go b/pkg/gui/controllers/custom_command_action.go index f4de3218eb9..3225867d992 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" @@ -32,13 +33,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 8c1265c15cf..0e5e54109cc 100644 --- a/pkg/gui/controllers/helpers/confirmation_helper.go +++ b/pkg/gui/controllers/helpers/confirmation_helper.go @@ -270,10 +270,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 +294,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 d6e6151ff4e..4491cfe35ed 100644 --- a/pkg/gui/controllers/suggestions_controller.go +++ b/pkg/gui/controllers/suggestions_controller.go @@ -43,6 +43,12 @@ func (self *SuggestionsController) GetKeybindings(opts types.KeybindingsOpts) [] Key: opts.GetKey(opts.Config.Universal.TogglePanel), Handler: func() error { return self.c.ReplaceContext(self.c.Contexts().Confirmation) }, }, + { + Key: opts.GetKey(opts.Config.Universal.Remove), + Handler: func() error { + return self.context().State.OnDeleteSuggestion() + }, + }, } return bindings diff --git a/pkg/gui/popup/popup_handler.go b/pkg/gui/popup/popup_handler.go index 1ca9cc3eefb..ac003e94399 100644 --- a/pkg/gui/popup/popup_handler.go +++ b/pkg/gui/popup/popup_handler.go @@ -104,13 +104,14 @@ 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, + Mask: opts.Mask, }) } diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index 44f07c1e305..39378484b40 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -165,13 +165,14 @@ 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 @@ -193,8 +194,9 @@ type PromptOpts struct { FindSuggestionsFunc func(string) []*Suggestion HandleConfirm func(string) error // CAPTURE THIS - HandleClose func() error - Mask bool + HandleClose func() error + HandleDeleteSuggestion func(int) error + Mask bool } type MenuSection struct { diff --git a/pkg/integration/components/prompt_driver.go b/pkg/integration/components/prompt_driver.go index 023c2f4389c..d1cce878ca2 100644 --- a/pkg/integration/components/prompt_driver.go +++ b/pkg/integration/components/prompt_driver.go @@ -82,3 +82,12 @@ 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 +} 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 00000000000..e90e6c3c6ee --- /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/test_list.go b/pkg/integration/tests/test_list.go index 2548702e9a0..eae6a5f7b9e 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -105,6 +105,7 @@ var tests = []*components.IntegrationTest{ custom_commands.BasicCmdFromConfig, custom_commands.CheckForConflicts, custom_commands.ComplexCmdAtRuntime, + custom_commands.DeleteFromHistory, custom_commands.FormPrompts, custom_commands.History, custom_commands.MenuFromCommand, From a7041cf492cb24bfd2bbaed0df5d91a943b78ca0 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 27 Apr 2024 22:18:57 +0200 Subject: [PATCH 2/3] Allow editing a custom command from the suggestions list by pressing 'e' For custom commands it is useful to select an earlier command and have it copied to the prompt for further editing. This can be done by hitting 'e' now. For other types of suggestion panels we don't enable this behavior, as you can't create arbitrary new items there that don't already exist as a suggestion. --- pkg/gui/context/suggestions_context.go | 2 ++ pkg/gui/controllers/custom_command_action.go | 1 + .../helpers/confirmation_helper.go | 2 ++ pkg/gui/controllers/suggestions_controller.go | 15 +++++++++ pkg/gui/popup/popup_handler.go | 1 + pkg/gui/types/common.go | 2 ++ pkg/integration/components/prompt_driver.go | 9 ++++++ .../tests/custom_commands/edit_history.go | 31 +++++++++++++++++++ pkg/integration/tests/test_list.go | 1 + 9 files changed, 64 insertions(+) create mode 100644 pkg/integration/tests/custom_commands/edit_history.go diff --git a/pkg/gui/context/suggestions_context.go b/pkg/gui/context/suggestions_context.go index 7b657121a00..c741cc769a9 100644 --- a/pkg/gui/context/suggestions_context.go +++ b/pkg/gui/context/suggestions_context.go @@ -20,6 +20,8 @@ type SuggestionsContextState struct { 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. FindSuggestions func(string) []*types.Suggestion diff --git a/pkg/gui/controllers/custom_command_action.go b/pkg/gui/controllers/custom_command_action.go index 3225867d992..39777e70a0d 100644 --- a/pkg/gui/controllers/custom_command_action.go +++ b/pkg/gui/controllers/custom_command_action.go @@ -18,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( diff --git a/pkg/gui/controllers/helpers/confirmation_helper.go b/pkg/gui/controllers/helpers/confirmation_helper.go index 0e5e54109cc..e4b76ce4005 100644 --- a/pkg/gui/controllers/helpers/confirmation_helper.go +++ b/pkg/gui/controllers/helpers/confirmation_helper.go @@ -223,6 +223,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) diff --git a/pkg/gui/controllers/suggestions_controller.go b/pkg/gui/controllers/suggestions_controller.go index 4491cfe35ed..655c7465bb9 100644 --- a/pkg/gui/controllers/suggestions_controller.go +++ b/pkg/gui/controllers/suggestions_controller.go @@ -49,6 +49,21 @@ func (self *SuggestionsController) GetKeybindings(opts types.KeybindingsOpts) [] 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 + }, + }, } return bindings diff --git a/pkg/gui/popup/popup_handler.go b/pkg/gui/popup/popup_handler.go index ac003e94399..3f71644d301 100644 --- a/pkg/gui/popup/popup_handler.go +++ b/pkg/gui/popup/popup_handler.go @@ -111,6 +111,7 @@ func (self *PopupHandler) Prompt(opts types.PromptOpts) error { 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 39378484b40..d5017307820 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -176,6 +176,7 @@ type CreatePopupPanelOpts struct { FindSuggestionsFunc func(string) []*Suggestion Mask bool + AllowEditSuggestion bool } type ConfirmOpts struct { @@ -193,6 +194,7 @@ type PromptOpts struct { InitialContent string FindSuggestionsFunc func(string) []*Suggestion HandleConfirm func(string) error + AllowEditSuggestion bool // CAPTURE THIS HandleClose func() error HandleDeleteSuggestion func(int) error diff --git a/pkg/integration/components/prompt_driver.go b/pkg/integration/components/prompt_driver.go index d1cce878ca2..a19c29aa4c8 100644 --- a/pkg/integration/components/prompt_driver.go +++ b/pkg/integration/components/prompt_driver.go @@ -91,3 +91,12 @@ func (self *PromptDriver) DeleteSuggestion(matcher *TextMatcher) *PromptDriver { 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/edit_history.go b/pkg/integration/tests/custom_commands/edit_history.go new file mode 100644 index 00000000000..c6999b1f2bb --- /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 eae6a5f7b9e..7084fd99b3c 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -106,6 +106,7 @@ var tests = []*components.IntegrationTest{ custom_commands.CheckForConflicts, custom_commands.ComplexCmdAtRuntime, custom_commands.DeleteFromHistory, + custom_commands.EditHistory, custom_commands.FormPrompts, custom_commands.History, custom_commands.MenuFromCommand, From 010b0ae923360ccaaed34cce6301966bb7c9c9ed Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 17 May 2024 18:13:16 +0200 Subject: [PATCH 3/3] Show delete/edit keybindings in suggestions subtitle if available --- pkg/gui/controllers/confirmation_controller.go | 10 ++++++++++ pkg/gui/controllers/helpers/confirmation_helper.go | 1 + pkg/gui/controllers/suggestions_controller.go | 7 +++++-- pkg/i18n/english.go | 2 ++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/pkg/gui/controllers/confirmation_controller.go b/pkg/gui/controllers/confirmation_controller.go index aa5617fa81d..45bd16a456e 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/helpers/confirmation_helper.go b/pkg/gui/controllers/helpers/confirmation_helper.go index e4b76ce4005..b5a337fbb05 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() diff --git a/pkg/gui/controllers/suggestions_controller.go b/pkg/gui/controllers/suggestions_controller.go index 655c7465bb9..857952d9bf5 100644 --- a/pkg/gui/controllers/suggestions_controller.go +++ b/pkg/gui/controllers/suggestions_controller.go @@ -40,8 +40,11 @@ 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), diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 27905cf9bb1..caa920de1ff 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",