1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-04-27 12:32:37 +02:00

Delete and edit custom commands history items (#3534)

- **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.
This commit is contained in:
Stefan Haller 2024-05-19 07:09:13 +02:00 committed by GitHub
commit 866e80529b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 198 additions and 24 deletions

View File

@ -14,10 +14,13 @@ type SuggestionsContext struct {
} }
type SuggestionsContextState struct { type SuggestionsContextState struct {
Suggestions []*types.Suggestion Suggestions []*types.Suggestion
OnConfirm func() error OnConfirm func() error
OnClose func() error OnClose func() error
AsyncHandler *tasks.AsyncHandler OnDeleteSuggestion func() error
AsyncHandler *tasks.AsyncHandler
AllowEditSuggestion bool
// FindSuggestions will take a string that the user has typed into a prompt // FindSuggestions will take a string that the user has typed into a prompt
// and return a slice of suggestions which match that string. // and return a slice of suggestions which match that string.

View File

@ -1,6 +1,8 @@
package controllers package controllers
import ( import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types" "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), Key: opts.GetKey(opts.Config.Universal.TogglePanel),
Handler: func() error { Handler: func() error {
if len(self.c.Contexts().Suggestions.State.Suggestions) > 0 { 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 self.c.ReplaceContext(self.c.Contexts().Suggestions)
} }
return nil return nil

View File

@ -1,6 +1,7 @@
package controllers package controllers
import ( import (
"slices"
"strings" "strings"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
@ -17,6 +18,7 @@ func (self *CustomCommandAction) Call() error {
return self.c.Prompt(types.PromptOpts{ return self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.CustomCommand, Title: self.c.Tr.CustomCommand,
FindSuggestionsFunc: self.GetCustomCommandsHistorySuggestionsFunc(), FindSuggestionsFunc: self.GetCustomCommandsHistorySuggestionsFunc(),
AllowEditSuggestion: true,
HandleConfirm: func(command string) error { HandleConfirm: func(command string) error {
if self.shouldSaveCommand(command) { if self.shouldSaveCommand(command) {
self.c.GetAppState().CustomCommandsHistory = utils.Limit( self.c.GetAppState().CustomCommandsHistory = utils.Limit(
@ -32,13 +34,34 @@ func (self *CustomCommandAction) Call() error {
self.c.OS().Cmd.NewShell(command), 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 { 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` // this mimics the shell functionality `ignorespace`

View File

@ -159,6 +159,7 @@ func (self *ConfirmationHelper) prepareConfirmationPanel(
suggestionsContext.SetSuggestions(opts.FindSuggestionsFunc("")) suggestionsContext.SetSuggestions(opts.FindSuggestionsFunc(""))
suggestionsView.Visible = true suggestionsView.Visible = true
suggestionsView.Title = fmt.Sprintf(self.c.Tr.SuggestionsTitle, self.c.UserConfig.Keybinding.Universal.TogglePanel) suggestionsView.Title = fmt.Sprintf(self.c.Tr.SuggestionsTitle, self.c.UserConfig.Keybinding.Universal.TogglePanel)
suggestionsView.Subtitle = ""
} }
self.ResizeConfirmationPanel() self.ResizeConfirmationPanel()
@ -223,6 +224,8 @@ func (self *ConfirmationHelper) CreatePopupPanel(ctx goContext.Context, opts typ
return err return err
} }
self.c.Contexts().Suggestions.State.AllowEditSuggestion = opts.AllowEditSuggestion
self.c.State().GetRepoState().SetCurrentPopupOpts(&opts) self.c.State().GetRepoState().SetCurrentPopupOpts(&opts)
return self.c.PushContext(self.c.Contexts().Confirmation) 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) 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.OnConfirm = onConfirm
self.c.Contexts().Confirmation.State.OnClose = onClose self.c.Contexts().Confirmation.State.OnClose = onClose
self.c.Contexts().Suggestions.State.OnConfirm = onSuggestionConfirm self.c.Contexts().Suggestions.State.OnConfirm = onSuggestionConfirm
self.c.Contexts().Suggestions.State.OnClose = onClose self.c.Contexts().Suggestions.State.OnClose = onClose
self.c.Contexts().Suggestions.State.OnDeleteSuggestion = onDeleteSuggestion
return nil return nil
} }
@ -284,6 +297,7 @@ func (self *ConfirmationHelper) clearConfirmationViewKeyBindings() {
self.c.Contexts().Confirmation.State.OnClose = noop self.c.Contexts().Confirmation.State.OnClose = noop
self.c.Contexts().Suggestions.State.OnConfirm = noop self.c.Contexts().Suggestions.State.OnConfirm = noop
self.c.Contexts().Suggestions.State.OnClose = noop self.c.Contexts().Suggestions.State.OnClose = noop
self.c.Contexts().Suggestions.State.OnDeleteSuggestion = noop
} }
func (self *ConfirmationHelper) getSelectedSuggestionValue() string { func (self *ConfirmationHelper) getSelectedSuggestionValue() string {

View File

@ -40,8 +40,32 @@ func (self *SuggestionsController) GetKeybindings(opts types.KeybindingsOpts) []
Handler: func() error { return self.context().State.OnClose() }, Handler: func() error { return self.context().State.OnClose() },
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.TogglePanel), Key: opts.GetKey(opts.Config.Universal.TogglePanel),
Handler: func() error { return self.c.ReplaceContext(self.c.Contexts().Confirmation) }, 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
},
}, },
} }

View File

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

View File

@ -165,16 +165,18 @@ type CreateMenuOptions struct {
} }
type CreatePopupPanelOpts struct { type CreatePopupPanelOpts struct {
HasLoader bool HasLoader bool
Editable bool Editable bool
Title string Title string
Prompt string Prompt string
HandleConfirm func() error HandleConfirm func() error
HandleConfirmPrompt func(string) error HandleConfirmPrompt func(string) error
HandleClose func() error HandleClose func() error
HandleDeleteSuggestion func(int) error
FindSuggestionsFunc func(string) []*Suggestion FindSuggestionsFunc func(string) []*Suggestion
Mask bool Mask bool
AllowEditSuggestion bool
} }
type ConfirmOpts struct { type ConfirmOpts struct {
@ -192,9 +194,11 @@ type PromptOpts struct {
InitialContent string InitialContent string
FindSuggestionsFunc func(string) []*Suggestion FindSuggestionsFunc func(string) []*Suggestion
HandleConfirm func(string) error HandleConfirm func(string) error
AllowEditSuggestion bool
// CAPTURE THIS // CAPTURE THIS
HandleClose func() error HandleClose func() error
Mask bool HandleDeleteSuggestion func(int) error
Mask bool
} }
type MenuSection struct { type MenuSection struct {

View File

@ -631,6 +631,7 @@ type TranslationSet struct {
SuggestionsCheatsheetTitle string SuggestionsCheatsheetTitle string
// Unlike the cheatsheet title above, the real suggestions title has a little message saying press tab to focus // Unlike the cheatsheet title above, the real suggestions title has a little message saying press tab to focus
SuggestionsTitle string SuggestionsTitle string
SuggestionsSubtitle string
ExtrasTitle string ExtrasTitle string
PushingTagStatus string PushingTagStatus string
PullRequestURLCopiedToClipboard string PullRequestURLCopiedToClipboard string
@ -1593,6 +1594,7 @@ func EnglishTranslationSet() TranslationSet {
NavigationTitle: "List panel navigation", NavigationTitle: "List panel navigation",
SuggestionsCheatsheetTitle: "Suggestions", SuggestionsCheatsheetTitle: "Suggestions",
SuggestionsTitle: "Suggestions (press %s to focus)", SuggestionsTitle: "Suggestions (press %s to focus)",
SuggestionsSubtitle: "(press %s to delete, %s to edit)",
ExtrasTitle: "Command log", ExtrasTitle: "Command log",
PushingTagStatus: "Pushing tag", PushingTagStatus: "Pushing tag",
PullRequestURLCopiedToClipboard: "Pull request URL copied to clipboard", PullRequestURLCopiedToClipboard: "Pull request URL copied to clipboard",

View File

@ -82,3 +82,21 @@ func (self *PromptDriver) ConfirmSuggestion(matcher *TextMatcher) {
NavigateToLine(matcher). NavigateToLine(matcher).
PressEnter() 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
}

View File

@ -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"),
)
},
})

View File

@ -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"))
},
})

View File

@ -105,6 +105,8 @@ var tests = []*components.IntegrationTest{
custom_commands.BasicCmdFromConfig, custom_commands.BasicCmdFromConfig,
custom_commands.CheckForConflicts, custom_commands.CheckForConflicts,
custom_commands.ComplexCmdAtRuntime, custom_commands.ComplexCmdAtRuntime,
custom_commands.DeleteFromHistory,
custom_commands.EditHistory,
custom_commands.FormPrompts, custom_commands.FormPrompts,
custom_commands.History, custom_commands.History,
custom_commands.MenuFromCommand, custom_commands.MenuFromCommand,