Skip to content

Commit

Permalink
Delete and edit custom commands history items (#3534)
Browse files Browse the repository at this point in the history
- **PR Description**

Allow deleting and editing custom command history items. Deleting is
done by hitting `d` on a suggestion; editing is done by hitting `e`,
which fills the selected item into the command prompt for further
editing.

Closes #2528.
  • Loading branch information
stefanhaller committed May 19, 2024
2 parents 269d012 + 010b0ae commit 866e805
Show file tree
Hide file tree
Showing 12 changed files with 198 additions and 24 deletions.
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)
}
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

0 comments on commit 866e805

Please sign in to comment.