add more suggestions

This commit is contained in:
Jesse Duffield 2021-10-23 11:25:37 +11:00
parent 629494144f
commit ef544e6ce9
14 changed files with 239 additions and 35 deletions

View file

@ -311,6 +311,9 @@ type AppState struct {
LastUpdateCheck int64
RecentRepos []string
StartupPopupVersion int
// these are for custom commands typed in directly, not for custom commands in the lazygit config
CustomCommandsHistory []string
}
func getDefaultAppState() *AppState {

View file

@ -218,7 +218,7 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions)
func (gui *Gui) handleCheckoutByName() error {
return gui.prompt(promptOpts{
title: gui.Tr.BranchName + ":",
findSuggestionsFunc: gui.getBranchNameSuggestionsFunc(),
findSuggestionsFunc: gui.getRefsSuggestionsFunc(),
handleConfirm: func(response string) error {
return gui.handleCheckoutRef(response, handleCheckoutRefOptions{
span: "Checkout branch",

View file

@ -3,6 +3,7 @@
package gui
import (
"fmt"
"strings"
"github.com/jesseduffield/gocui"
@ -33,7 +34,6 @@ type askOpts struct {
handleConfirm func() error
handleClose func() error
handlersManageFocus bool
findSuggestionsFunc func(string) []*types.Suggestion
}
type promptOpts struct {
@ -50,7 +50,6 @@ func (gui *Gui) ask(opts askOpts) error {
handleConfirm: opts.handleConfirm,
handleClose: opts.handleClose,
handlersManageFocus: opts.handlersManageFocus,
findSuggestionsFunc: opts.findSuggestionsFunc,
})
}
@ -103,13 +102,6 @@ func (gui *Gui) wrappedPromptConfirmationFunction(handlersManageFocus bool, func
}
}
func (gui *Gui) clearConfirmationViewKeyBindings() {
keybindingConfig := gui.Config.GetUserConfig().Keybinding
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Return), gocui.ModNone)
}
func (gui *Gui) closeConfirmationPrompt(handlersManageFocus bool) error {
// we've already closed it so we can just return
if !gui.Views.Confirmation.Visible {
@ -165,7 +157,13 @@ func (gui *Gui) getConfirmationPanelDimensions(wrap bool, prompt string) (int, i
height/2 + panelHeight/2
}
func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool, findSuggestionsFunc func(string) []*types.Suggestion, editable bool) error {
func (gui *Gui) prepareConfirmationPanel(
title,
prompt string,
hasLoader bool,
findSuggestionsFunc func(string) []*types.Suggestion,
editable bool,
) error {
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(true, prompt)
// calling SetView on an existing view returns the same view, so I'm not bothering
// to reassign to gui.Views.Confirmation
@ -185,14 +183,15 @@ func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool, f
gui.findSuggestions = findSuggestionsFunc
if findSuggestionsFunc != nil {
suggestionsViewHeight := 11
suggestionsView, err := gui.g.SetView("suggestions", x0, y1, x1, y1+suggestionsViewHeight, 0)
suggestionsView, err := gui.g.SetView("suggestions", x0, y1+1, x1, y1+suggestionsViewHeight, 0)
if err != nil {
return err
}
suggestionsView.Wrap = false
suggestionsView.FgColor = theme.GocuiDefaultTextColor
gui.setSuggestions([]*types.Suggestion{})
gui.setSuggestions(findSuggestionsFunc(""))
suggestionsView.Visible = true
suggestionsView.Title = fmt.Sprintf(gui.Tr.SuggestionsTitle, gui.Config.GetUserConfig().Keybinding.Universal.TogglePanel)
}
gui.g.Update(func(g *gocui.Gui) error {
@ -248,7 +247,7 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
gui.renderString(gui.Views.Options, actions)
var onConfirm func() error
if opts.handleConfirmPrompt != nil {
onConfirm = gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt, func() string { return gui.Views.Confirmation.Buffer() })
onConfirm = gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt, func() string { return gui.Views.Confirmation.TextArea.GetContent() })
} else {
onConfirm = gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleConfirm)
}
@ -260,7 +259,11 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
}
keybindingConfig := gui.Config.GetUserConfig().Keybinding
onSuggestionConfirm := gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt, func() string { return gui.getSelectedSuggestionValue() })
onSuggestionConfirm := gui.wrappedPromptConfirmationFunction(
opts.handlersManageFocus,
opts.handleConfirmPrompt,
gui.getSelectedSuggestionValue,
)
confirmationKeybindings := []confirmationKeybinding{
{
@ -319,6 +322,16 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
return nil
}
func (gui *Gui) clearConfirmationViewKeyBindings() {
keybindingConfig := gui.Config.GetUserConfig().Keybinding
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Return), gocui.ModNone)
_ = gui.g.DeleteKeybinding("suggestions", gui.getKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
_ = gui.g.DeleteKeybinding("suggestions", gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
_ = gui.g.DeleteKeybinding("suggestions", gui.getKey(keybindingConfig.Universal.Return), gocui.ModNone)
}
func (gui *Gui) wrappedHandler(f func() error) func(g *gocui.Gui, v *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
return f()

View file

@ -125,7 +125,8 @@ func (gui *Gui) handleCreateDiffingMenuPanel() error {
displayString: gui.Tr.LcEnterRefToDiff,
onPress: func() error {
return gui.prompt(promptOpts{
title: gui.Tr.LcEnteRefName,
title: gui.Tr.LcEnteRefName,
findSuggestionsFunc: gui.getRefsSuggestionsFunc(),
handleConfirm: func(response string) error {
gui.State.Modes.Diffing.Ref = strings.TrimSpace(response)
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})

View file

@ -659,8 +659,9 @@ func (gui *Gui) handlePullFiles() error {
}
return gui.prompt(promptOpts{
title: gui.Tr.EnterUpstream,
initialContent: "origin/" + currentBranch.Name,
title: gui.Tr.EnterUpstream,
initialContent: "origin/" + currentBranch.Name,
findSuggestionsFunc: gui.getRemoteBranchesSuggestionsFunc("/"),
handleConfirm: func(upstream string) error {
if err := gui.GitCommand.SetUpstreamBranch(upstream); err != nil {
errorMessage := err.Error()
@ -798,8 +799,9 @@ func (gui *Gui) pushFiles() error {
return gui.push(pushOpts{setUpstream: true})
} else {
return gui.prompt(promptOpts{
title: gui.Tr.EnterUpstream,
initialContent: "origin " + currentBranch.Name,
title: gui.Tr.EnterUpstream,
initialContent: "origin " + currentBranch.Name,
findSuggestionsFunc: gui.getRemoteBranchesSuggestionsFunc(" "),
handleConfirm: func(upstream string) error {
var upstreamBranch, upstreamRemote string
split := strings.Split(upstream, " ")
@ -884,8 +886,21 @@ func (gui *Gui) anyFilesWithMergeConflicts() bool {
func (gui *Gui) handleCustomCommand() error {
return gui.prompt(promptOpts{
title: gui.Tr.CustomCommand,
title: gui.Tr.CustomCommand,
findSuggestionsFunc: gui.getCustomCommandsHistorySuggestionsFunc(),
handleConfirm: func(command string) error {
gui.Config.GetAppState().CustomCommandsHistory = utils.Limit(
utils.Uniq(
append(gui.Config.GetAppState().CustomCommandsHistory, command),
),
1000,
)
err := gui.Config.SaveAppState()
if err != nil {
gui.Log.Error(err)
}
gui.OnRunCommand(oscommands.NewCmdLogEntry(command, gui.Tr.Spans.CustomCommand, true))
return gui.runSubprocessWithSuspenseAndRefresh(
gui.OSCommand.PrepareShellSubProcess(command),

View file

@ -36,7 +36,7 @@ func (gui *Gui) handleCreateFilteringMenuPanel() error {
onPress: func() error {
return gui.prompt(promptOpts{
findSuggestionsFunc: gui.getFilePathSuggestionsFunc(),
title: gui.Tr.LcEnterFileName,
title: gui.Tr.EnterFileName,
handleConfirm: func(response string) error {
return gui.setFiltering(strings.TrimSpace(response))
},

View file

@ -1,6 +1,7 @@
package gui
import (
"fmt"
"os"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
@ -42,11 +43,7 @@ func matchesToSuggestions(matches []string) []*types.Suggestion {
func (gui *Gui) getRemoteSuggestionsFunc() func(string) []*types.Suggestion {
remoteNames := gui.getRemoteNames()
return func(input string) []*types.Suggestion {
return matchesToSuggestions(
utils.FuzzySearch(input, remoteNames),
)
}
return fuzzySearchFunc(remoteNames)
}
func (gui *Gui) getBranchNames() []string {
@ -61,7 +58,12 @@ func (gui *Gui) getBranchNameSuggestionsFunc() func(string) []*types.Suggestion
branchNames := gui.getBranchNames()
return func(input string) []*types.Suggestion {
matchingBranchNames := utils.FuzzySearch(sanitizedBranchName(input), branchNames)
var matchingBranchNames []string
if input == "" {
matchingBranchNames = branchNames
} else {
matchingBranchNames = utils.FuzzySearch(input, branchNames)
}
suggestions := make([]*types.Suggestion, len(matchingBranchNames))
for i, branchName := range matchingBranchNames {
@ -79,6 +81,8 @@ func (gui *Gui) getBranchNameSuggestionsFunc() func(string) []*types.Suggestion
// gui.State.FilesTrie. On the main thread we'll be doing a fuzzy search via
// gui.State.FilesTrie. So if we've looked for a file previously, we'll start with
// the old trie and eventually it'll be swapped out for the new one.
// Notably, unlike other suggestion functions we're not showing all the options
// if nothing has been typed because there'll be too much to display efficiently
func (gui *Gui) getFilePathSuggestionsFunc() func(string) []*types.Suggestion {
_ = gui.WithWaitingStatus(gui.Tr.LcLoadingFileSuggestions, func() error {
trie := patricia.NewTrie()
@ -131,3 +135,56 @@ func (gui *Gui) getFilePathSuggestionsFunc() func(string) []*types.Suggestion {
return suggestions
}
}
func (gui *Gui) getRemoteBranchNames(separator string) []string {
result := []string{}
for _, remote := range gui.State.Remotes {
for _, branch := range remote.Branches {
result = append(result, fmt.Sprintf("%s%s%s", remote.Name, separator, branch.Name))
}
}
return result
}
func (gui *Gui) getRemoteBranchesSuggestionsFunc(separator string) func(string) []*types.Suggestion {
return fuzzySearchFunc(gui.getRemoteBranchNames(separator))
}
func (gui *Gui) getTagNames() []string {
result := make([]string, len(gui.State.Tags))
for i, tag := range gui.State.Tags {
result[i] = tag.Name
}
return result
}
func (gui *Gui) getRefsSuggestionsFunc() func(string) []*types.Suggestion {
remoteBranchNames := gui.getRemoteBranchNames("/")
localBranchNames := gui.getBranchNames()
tagNames := gui.getTagNames()
additionalRefNames := []string{"HEAD", "FETCH_HEAD", "MERGE_HEAD", "ORIG_HEAD"}
refNames := append(append(append(remoteBranchNames, localBranchNames...), tagNames...), additionalRefNames...)
return fuzzySearchFunc(refNames)
}
func (gui *Gui) getCustomCommandsHistorySuggestionsFunc() func(string) []*types.Suggestion {
// reversing so that we display the latest command first
history := utils.Reverse(gui.Config.GetAppState().CustomCommandsHistory)
return fuzzySearchFunc(history)
}
func fuzzySearchFunc(options []string) func(string) []*types.Suggestion {
return func(input string) []*types.Suggestion {
var matches []string
if input == "" {
matches = options
} else {
matches = utils.FuzzySearch(input, options)
}
return matchesToSuggestions(matches)
}
}

View file

@ -111,6 +111,7 @@ func (gui *Gui) createAllViews() error {
gui.Views.Credentials.Editable = true
gui.Views.Suggestions.Visible = false
gui.Views.Suggestions.ContainsList = true
gui.Views.Menu.Visible = false

View file

@ -358,7 +358,7 @@ func chineseTranslationSet() TranslationSet {
LcFilterBy: "过滤",
LcExitFilterMode: "停止按路径过滤",
LcFilterPathOption: "输入要过滤的路径",
LcEnterFileName: "输入路径:",
EnterFileName: "输入路径:",
FilteringMenuTitle: "正在过滤",
MustExitFilterModeTitle: "命令不可用",
MustExitFilterModePrompt: "命令在过滤模式下不可用。退出过滤模式?",
@ -419,7 +419,7 @@ func chineseTranslationSet() TranslationSet {
SubCommitsTitle: "子提交",
SubmodulesTitle: "子模块",
NavigationTitle: "列表面板导航",
SuggestionsTitle: "意见建议",
SuggestionsCheatsheetTitle: "意见建议",
PushingTagStatus: "推送标签",
PullRequestURLCopiedToClipboard: "抓取请求网址已复制到剪贴板",
CommitMessageCopiedToClipboard: "提交消息复制到剪贴板",

View file

@ -327,7 +327,7 @@ func dutchTranslationSet() TranslationSet {
LcFilterBy: "filter bij",
LcExitFilterMode: "stop met filteren bij pad",
LcFilterPathOption: "vulin pad om op te filteren",
LcEnterFileName: "vulin path:",
EnterFileName: "Vulin path:",
FilteringMenuTitle: "Filteren",
MustExitFilterModeTitle: "Command niet beschikbaar",
MustExitFilterModePrompt: "Command niet beschikbaar in filter modus. Sluit filter modus?",

View file

@ -342,7 +342,7 @@ type TranslationSet struct {
LcFilterBy string
LcExitFilterMode string
LcFilterPathOption string
LcEnterFileName string
EnterFileName string
FilteringMenuTitle string
MustExitFilterModeTitle string
MustExitFilterModePrompt string
@ -404,6 +404,8 @@ type TranslationSet struct {
SubCommitsTitle string
SubmodulesTitle string
NavigationTitle string
SuggestionsCheatsheetTitle string
// Unlike the cheatsheet title above, the real suggestions title has a little message saying press tab to focus
SuggestionsTitle string
ExtrasTitle string
PushingTagStatus string
@ -867,7 +869,7 @@ func englishTranslationSet() TranslationSet {
LcFilterBy: "filter by",
LcExitFilterMode: "stop filtering by path",
LcFilterPathOption: "enter path to filter by",
LcEnterFileName: "enter path:",
EnterFileName: "Enter path:",
FilteringMenuTitle: "Filtering",
MustExitFilterModeTitle: "Command not available",
MustExitFilterModePrompt: "Command not available in filtered mode. Exit filtered mode?",
@ -930,7 +932,8 @@ func englishTranslationSet() TranslationSet {
SubCommitsTitle: "Sub-commits",
SubmodulesTitle: "Submodules",
NavigationTitle: "List Panel Navigation",
SuggestionsTitle: "Suggestions",
SuggestionsCheatsheetTitle: "Suggestions",
SuggestionsTitle: "Suggestions (press %s to focus)",
ExtrasTitle: "Extras",
PushingTagStatus: "pushing tag",
PullRequestURLCopiedToClipboard: "Pull request URL copied to clipboard",

View file

@ -116,3 +116,31 @@ func StringArraysOverlap(strArrA []string, strArrB []string) bool {
return false
}
func Uniq(values []string) []string {
added := make(map[string]bool)
result := make([]string, 0, len(values))
for _, value := range values {
if added[value] {
continue
}
added[value] = true
result = append(result, value)
}
return result
}
func Limit(values []string, limit int) []string {
if len(values) > limit {
return values[:limit]
}
return values
}
func Reverse(values []string) []string {
result := make([]string, len(values))
for i, val := range values {
result[len(values)-i-1] = val
}
return result
}

View file

@ -165,3 +165,86 @@ func TestEscapeSpecialChars(t *testing.T) {
})
}
}
func TestUniq(t *testing.T) {
for _, test := range []struct {
values []string
want []string
}{
{
values: []string{"a", "b", "c"},
want: []string{"a", "b", "c"},
},
{
values: []string{"a", "b", "a", "b", "c"},
want: []string{"a", "b", "c"},
},
} {
if got := Uniq(test.values); !assert.EqualValues(t, got, test.want) {
t.Errorf("Uniq(%v) = %v; want %v", test.values, got, test.want)
}
}
}
func TestLimit(t *testing.T) {
for _, test := range []struct {
values []string
limit int
want []string
}{
{
values: []string{"a", "b", "c"},
limit: 3,
want: []string{"a", "b", "c"},
},
{
values: []string{"a", "b", "c"},
limit: 4,
want: []string{"a", "b", "c"},
},
{
values: []string{"a", "b", "c"},
limit: 2,
want: []string{"a", "b"},
},
{
values: []string{"a", "b", "c"},
limit: 1,
want: []string{"a"},
},
{
values: []string{"a", "b", "c"},
limit: 0,
want: []string{},
},
{
values: []string{},
limit: 0,
want: []string{},
},
} {
if got := Limit(test.values, test.limit); !assert.EqualValues(t, got, test.want) {
t.Errorf("Limit(%v, %d) = %v; want %v", test.values, test.limit, got, test.want)
}
}
}
func TestReverse(t *testing.T) {
for _, test := range []struct {
values []string
want []string
}{
{
values: []string{"a", "b", "c"},
want: []string{"c", "b", "a"},
},
{
values: []string{},
want: []string{},
},
} {
if got := Reverse(test.values); !assert.EqualValues(t, got, test.want) {
t.Errorf("Reverse(%v) = %v; want %v", test.values, got, test.want)
}
}
}

View file

@ -81,7 +81,7 @@ func localisedTitle(mApp *app.App, str string) string {
"search": tr.SearchTitle,
"secondary": tr.SecondaryTitle,
"stash": tr.StashTitle,
"suggestions": tr.SuggestionsTitle,
"suggestions": tr.SuggestionsCheatsheetTitle,
"extras": tr.ExtrasTitle,
}