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:
commit
866e80529b
@ -17,8 +17,11 @@ type SuggestionsContextState struct {
|
|||||||
Suggestions []*types.Suggestion
|
Suggestions []*types.Suggestion
|
||||||
OnConfirm func() error
|
OnConfirm func() error
|
||||||
OnClose func() error
|
OnClose func() error
|
||||||
|
OnDeleteSuggestion func() error
|
||||||
AsyncHandler *tasks.AsyncHandler
|
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.
|
||||||
FindSuggestions func(string) []*types.Suggestion
|
FindSuggestions func(string) []*types.Suggestion
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
return func(input string) []*types.Suggestion {
|
||||||
history := self.c.GetAppState().CustomCommandsHistory
|
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`
|
||||||
|
@ -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 {
|
||||||
|
@ -41,7 +41,31 @@ func (self *SuggestionsController) GetKeybindings(opts types.KeybindingsOpts) []
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
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
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,7 +109,9 @@ func (self *PopupHandler) Prompt(opts types.PromptOpts) error {
|
|||||||
Editable: true,
|
Editable: true,
|
||||||
HandleConfirmPrompt: opts.HandleConfirm,
|
HandleConfirmPrompt: opts.HandleConfirm,
|
||||||
HandleClose: opts.HandleClose,
|
HandleClose: opts.HandleClose,
|
||||||
|
HandleDeleteSuggestion: opts.HandleDeleteSuggestion,
|
||||||
FindSuggestionsFunc: opts.FindSuggestionsFunc,
|
FindSuggestionsFunc: opts.FindSuggestionsFunc,
|
||||||
|
AllowEditSuggestion: opts.AllowEditSuggestion,
|
||||||
Mask: opts.Mask,
|
Mask: opts.Mask,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -172,9 +172,11 @@ type CreatePopupPanelOpts struct {
|
|||||||
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,8 +194,10 @@ 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
|
||||||
|
HandleDeleteSuggestion func(int) error
|
||||||
Mask bool
|
Mask bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
41
pkg/integration/tests/custom_commands/delete_from_history.go
Normal file
41
pkg/integration/tests/custom_commands/delete_from_history.go
Normal 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"),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
31
pkg/integration/tests/custom_commands/edit_history.go
Normal file
31
pkg/integration/tests/custom_commands/edit_history.go
Normal 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"))
|
||||||
|
},
|
||||||
|
})
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user