From d18c8c8dc39fa028949470064dc9a596762f7f09 Mon Sep 17 00:00:00 2001 From: Elwardi Date: Sat, 17 Jul 2021 18:02:11 +0100 Subject: [PATCH 1/9] Add prompt type: menuFromCommand --- pkg/config/user_config.go | 4 +++ pkg/gui/custom_commands.go | 59 +++++++++++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 4855bf816..0f0e50fab 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -280,6 +280,10 @@ type CustomCommandPrompt struct { // this only applies to menus Options []CustomCommandMenuOption + + // this only applies to menuFromCommand + Command string `yaml:"command"` + Filter string `yaml:"filter"` } type CustomCommandMenuOption struct { diff --git a/pkg/gui/custom_commands.go b/pkg/gui/custom_commands.go index 06be97da0..a17d30dd2 100644 --- a/pkg/gui/custom_commands.go +++ b/pkg/gui/custom_commands.go @@ -1,8 +1,10 @@ package gui import ( + "fmt" "log" "strings" + "regexp" "github.com/fatih/color" "github.com/jesseduffield/gocui" @@ -151,10 +153,65 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand return gui.surfaceError(err) } + return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true}) + } + case "menuFromCommand": + f = func() error { + // Collect cmd to run from config + cmdStr, err := gui.resolveTemplate(prompt.Command, promptResponses) + if err != nil { + return gui.surfaceError(err) + } + + // Collect Filter regexp + filter, err := gui.resolveTemplate(prompt.Filter, promptResponses) + if err != nil { + return gui.surfaceError(err) + } + + // Run and save output + message,err := gui.GitCommand.RunCommandWithOutput(cmdStr) + if err != nil { + return gui.surfaceError(err) + } + + // Need to make a menu out of what the cmd has displayed + var candidates []string + reg := regexp.MustCompile(filter) + for _,str := range strings.Split(string(message), "\n"){ + cand := str + if str != "" { + for i := 1; i < (reg.NumSubexp()+1); i++ { + trim := reg.ReplaceAllString(str, "${"+fmt.Sprint(i)+"}") + cand = strings.Trim(cand, trim) + } + candidates = append(candidates, cand) + } + } + + menuItems := make([]*menuItem, len(candidates)) + for i, option := range candidates { + option := option + + menuItems[i] = &menuItem{ + displayStrings: []string{option}, + onPress: func() error { + promptResponses[idx] = option + + return wrappedF() + }, + } + } + + title, err := gui.resolveTemplate(prompt.Title, promptResponses) + if err != nil { + return gui.surfaceError(err) + } + return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true}) } default: - return gui.createErrorPanel("custom command prompt must have a type of 'input' or 'menu'") + return gui.createErrorPanel("custom command prompt must have a type of 'input', 'menu' or 'menuFromCommand'") } } From 9daa47fb2df8b58a3edc82ed48687205c2bd85e9 Mon Sep 17 00:00:00 2001 From: Elwardi Date: Sat, 17 Jul 2021 19:18:41 +0100 Subject: [PATCH 2/9] Add docs for menuFromCommand prompts --- docs/Custom_Command_Keybindings.md | 16 ++++++++++++++++ pkg/gui/custom_commands.go | 6 +++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/docs/Custom_Command_Keybindings.md b/docs/Custom_Command_Keybindings.md index 5a63e2241..7a9f1bc0d 100644 --- a/docs/Custom_Command_Keybindings.md +++ b/docs/Custom_Command_Keybindings.md @@ -35,6 +35,18 @@ customCommands: command: "git flow {{index .PromptResponses 0}} start {{index .PromptResponses 1}}" context: 'localBranches' loadingText: 'creating branch' + - key : 'r' + description: 'Checkout a remote branch as FETCH_HEAD' + command: "git fetch {{index .PromptResponses 0}} {{index .PromptResponses 1}} && git checkout FETCH_HEAD" + context: 'remotes' + prompts: + - type: 'input' + title: 'Remote:' + initialValue: "{{index .SelectedRemote.Name }}" + - type: 'menuFromCommand' + title: 'Remote branch:' + command: 'git branch -r --list {{index .PromptResponses 0}}/*' + filter: '.*{{index .PromptResponses 0}}/(.*)' ``` Looking at the command assigned to the 'n' key, here's what the result looks like: @@ -85,6 +97,10 @@ The permitted prompt fields are: | title | the title to display in the popup panel | no | | initialValue | (only applicable to 'input' prompts) the initial value to appear in the text box | no | | options | (only applicable to 'menu' prompts) the options to display in the menu | no | +| command | (only applicable to 'menuFromCommand' prompts) the command to run to generate | yes | +| | menu options | | +| filter | (only applicable to 'menuFromCommand' prompts) the regexp to run specify groups | yes | +| | which are going to be kept from the command's output | | The permitted option fields are: | _field_ | _description_ | _required_ | diff --git a/pkg/gui/custom_commands.go b/pkg/gui/custom_commands.go index a17d30dd2..a07bd396b 100644 --- a/pkg/gui/custom_commands.go +++ b/pkg/gui/custom_commands.go @@ -179,11 +179,11 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand var candidates []string reg := regexp.MustCompile(filter) for _,str := range strings.Split(string(message), "\n"){ - cand := str + cand := "" if str != "" { for i := 1; i < (reg.NumSubexp()+1); i++ { - trim := reg.ReplaceAllString(str, "${"+fmt.Sprint(i)+"}") - cand = strings.Trim(cand, trim) + keep := reg.ReplaceAllString(str, "${"+fmt.Sprint(i)+"}") + cand += keep } candidates = append(candidates, cand) } From 77e9ee64a45bbee3f0e2367a8670b2e7d6a59c6f Mon Sep 17 00:00:00 2001 From: Elwardi Date: Sun, 18 Jul 2021 18:38:06 +0100 Subject: [PATCH 3/9] Apply suggestions from @mjarkk for menyFromCommands --- pkg/config/user_config.go | 6 ++-- pkg/gui/custom_commands.go | 68 ++++++++++++++++++-------------------- 2 files changed, 36 insertions(+), 38 deletions(-) diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 0f0e50fab..f98bde28e 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -281,9 +281,9 @@ type CustomCommandPrompt struct { // this only applies to menus Options []CustomCommandMenuOption - // this only applies to menuFromCommand - Command string `yaml:"command"` - Filter string `yaml:"filter"` + // this only applies to menuFromCommand + Command string `yaml:"command"` + Filter string `yaml:"filter"` } type CustomCommandMenuOption struct { diff --git a/pkg/gui/custom_commands.go b/pkg/gui/custom_commands.go index a07bd396b..7a70eeb95 100644 --- a/pkg/gui/custom_commands.go +++ b/pkg/gui/custom_commands.go @@ -1,10 +1,10 @@ package gui import ( - "fmt" "log" + "regexp" + "strconv" "strings" - "regexp" "github.com/fatih/color" "github.com/jesseduffield/gocui" @@ -157,47 +157,45 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand } case "menuFromCommand": f = func() error { - // Collect cmd to run from config - cmdStr, err := gui.resolveTemplate(prompt.Command, promptResponses) - if err != nil { - return gui.surfaceError(err) - } + // Collect cmd to run from config + cmdStr, err := gui.resolveTemplate(prompt.Command, promptResponses) + if err != nil { + return gui.surfaceError(err) + } - // Collect Filter regexp - filter, err := gui.resolveTemplate(prompt.Filter, promptResponses) - if err != nil { - return gui.surfaceError(err) - } + // Collect Filter regexp + filter, err := gui.resolveTemplate(prompt.Filter, promptResponses) + if err != nil { + return gui.surfaceError(err) + } - // Run and save output - message,err := gui.GitCommand.RunCommandWithOutput(cmdStr) - if err != nil { - return gui.surfaceError(err) - } + // Run and save output + message, err := gui.GitCommand.RunCommandWithOutput(cmdStr) + if err != nil { + return gui.surfaceError(err) + } // Need to make a menu out of what the cmd has displayed - var candidates []string - reg := regexp.MustCompile(filter) - for _,str := range strings.Split(string(message), "\n"){ - cand := "" - if str != "" { - for i := 1; i < (reg.NumSubexp()+1); i++ { - keep := reg.ReplaceAllString(str, "${"+fmt.Sprint(i)+"}") - cand += keep - } - candidates = append(candidates, cand) - } - } + candidates := []string{} + reg := regexp.MustCompile(filter) + for _, str := range strings.Split(string(message), "\n") { + cand := "" + if str == "" { + continue + } + for i := 1; i < (reg.NumSubexp() + 1); i++ { + keep := reg.ReplaceAllString(str, "${"+strconv.Itoa(i)+"}") + cand += keep + } + candidates = append(candidates, cand) + } menuItems := make([]*menuItem, len(candidates)) - for i, option := range candidates { - option := option - + for i := range candidates { menuItems[i] = &menuItem{ - displayStrings: []string{option}, + displayStrings: []string{candidates[i]}, onPress: func() error { - promptResponses[idx] = option - + promptResponses[idx] = candidates[i] return wrappedF() }, } From f1ced5539a9a53ea5c8e501243377d3c74cef2d5 Mon Sep 17 00:00:00 2001 From: Elwardi Date: Mon, 19 Jul 2021 11:46:29 +0100 Subject: [PATCH 4/9] Add option to format filter matches to menuFromCommand prompts --- docs/Custom_Command_Keybindings.md | 10 ++++++--- pkg/config/user_config.go | 1 + pkg/gui/custom_commands.go | 33 ++++++++++++++++++++++++------ 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/docs/Custom_Command_Keybindings.md b/docs/Custom_Command_Keybindings.md index 7a9f1bc0d..77fb90be5 100644 --- a/docs/Custom_Command_Keybindings.md +++ b/docs/Custom_Command_Keybindings.md @@ -46,7 +46,8 @@ customCommands: - type: 'menuFromCommand' title: 'Remote branch:' command: 'git branch -r --list {{index .PromptResponses 0}}/*' - filter: '.*{{index .PromptResponses 0}}/(.*)' + filter: '.*{{index .PromptResponses 0}}/(?P.*)' + format: '{{ .branch }}' ``` Looking at the command assigned to the 'n' key, here's what the result looks like: @@ -99,8 +100,11 @@ The permitted prompt fields are: | options | (only applicable to 'menu' prompts) the options to display in the menu | no | | command | (only applicable to 'menuFromCommand' prompts) the command to run to generate | yes | | | menu options | | -| filter | (only applicable to 'menuFromCommand' prompts) the regexp to run specify groups | yes | -| | which are going to be kept from the command's output | | +| filter | (only applicable to 'menuFromCommand' prompts) the regexp to run specifying | yes | +| | groups which are going to be kept from the command's output | | +| format | (only applicable to 'menuFromCommand' prompts) how to format matched groups from | yes | +| | the filter. You can use named groups, or `{{ .group_GROUPID_MATCHID }}`. | yes | +| | PS: named groups keep last non-empty match | yes | The permitted option fields are: | _field_ | _description_ | _required_ | diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index f98bde28e..d373b74f1 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -284,6 +284,7 @@ type CustomCommandPrompt struct { // this only applies to menuFromCommand Command string `yaml:"command"` Filter string `yaml:"filter"` + Format string `yaml:"format"` } type CustomCommandMenuOption struct { diff --git a/pkg/gui/custom_commands.go b/pkg/gui/custom_commands.go index 7a70eeb95..fbe92a72f 100644 --- a/pkg/gui/custom_commands.go +++ b/pkg/gui/custom_commands.go @@ -1,10 +1,13 @@ package gui import ( + "bytes" + "errors" "log" "regexp" "strconv" "strings" + "text/template" "github.com/fatih/color" "github.com/jesseduffield/gocui" @@ -168,6 +171,10 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand if err != nil { return gui.surfaceError(err) } + reg, err := regexp.Compile(filter) + if err != nil { + return gui.surfaceError(errors.New("unable to parse filter regex, error: " + err.Error())) + } // Run and save output message, err := gui.GitCommand.RunCommandWithOutput(cmdStr) @@ -177,17 +184,31 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand // Need to make a menu out of what the cmd has displayed candidates := []string{} - reg := regexp.MustCompile(filter) + temp := template.Must(template.New("format").Parse(prompt.Format)) for _, str := range strings.Split(string(message), "\n") { - cand := "" if str == "" { continue } - for i := 1; i < (reg.NumSubexp() + 1); i++ { - keep := reg.ReplaceAllString(str, "${"+strconv.Itoa(i)+"}") - cand += keep + buff := bytes.NewBuffer(nil) + groupNames := reg.SubexpNames() + tmplData := map[string]string{} + for matchNum, match := range reg.FindAllStringSubmatch(str, -1) { + if len(match) > 0 { + for groupIdx, group := range match { + // Record matched group with group and match ids + matchName := "group_" + strconv.Itoa(groupIdx) + "_" + strconv.Itoa(matchNum) + tmplData[matchName] = group + // Record last named group non-empty matches as group matches + name := groupNames[groupIdx] + _, ok := tmplData[name] + if name != "" && group != "" && !ok { + tmplData[name] = group + } + } + } } - candidates = append(candidates, cand) + temp.Execute(buff, tmplData) + candidates = append(candidates, strings.TrimSpace(buff.String())) } menuItems := make([]*menuItem, len(candidates)) From b92ff3ee3fdde05ac84f2790e6b10a10db0c4d2a Mon Sep 17 00:00:00 2001 From: Elwardi Date: Mon, 19 Jul 2021 13:06:00 +0100 Subject: [PATCH 5/9] Consider first match only in menuFromCommand prompt --- docs/Custom_Command_Keybindings.md | 4 ++-- pkg/gui/custom_commands.go | 25 +++++++++++-------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/docs/Custom_Command_Keybindings.md b/docs/Custom_Command_Keybindings.md index 77fb90be5..9dc42a9da 100644 --- a/docs/Custom_Command_Keybindings.md +++ b/docs/Custom_Command_Keybindings.md @@ -103,8 +103,8 @@ The permitted prompt fields are: | filter | (only applicable to 'menuFromCommand' prompts) the regexp to run specifying | yes | | | groups which are going to be kept from the command's output | | | format | (only applicable to 'menuFromCommand' prompts) how to format matched groups from | yes | -| | the filter. You can use named groups, or `{{ .group_GROUPID_MATCHID }}`. | yes | -| | PS: named groups keep last non-empty match | yes | +| | the filter. You can use named groups, or `{{ .group_GROUPID }}`. | yes | +| | PS: named groups keep first match only | yes | The permitted option fields are: | _field_ | _description_ | _required_ | diff --git a/pkg/gui/custom_commands.go b/pkg/gui/custom_commands.go index fbe92a72f..1ac92f49c 100644 --- a/pkg/gui/custom_commands.go +++ b/pkg/gui/custom_commands.go @@ -184,31 +184,28 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand // Need to make a menu out of what the cmd has displayed candidates := []string{} + buff := bytes.NewBuffer(nil) temp := template.Must(template.New("format").Parse(prompt.Format)) for _, str := range strings.Split(string(message), "\n") { if str == "" { continue } - buff := bytes.NewBuffer(nil) - groupNames := reg.SubexpNames() tmplData := map[string]string{} - for matchNum, match := range reg.FindAllStringSubmatch(str, -1) { - if len(match) > 0 { - for groupIdx, group := range match { - // Record matched group with group and match ids - matchName := "group_" + strconv.Itoa(groupIdx) + "_" + strconv.Itoa(matchNum) - tmplData[matchName] = group - // Record last named group non-empty matches as group matches - name := groupNames[groupIdx] - _, ok := tmplData[name] - if name != "" && group != "" && !ok { - tmplData[name] = group - } + out := reg.FindAllStringSubmatch(str, -1) + if len(out) > 0 { + for groupIdx, group := range reg.SubexpNames() { + // Record matched group with group ids + matchName := "group_" + strconv.Itoa(groupIdx) + tmplData[matchName] = group + // Record last named group non-empty matches as group matches + if group != "" { + tmplData[group] = out[0][idx] } } } temp.Execute(buff, tmplData) candidates = append(candidates, strings.TrimSpace(buff.String())) + buff.Reset() } menuItems := make([]*menuItem, len(candidates)) From f70435a20fb9641513aff9c94ea368973c39d252 Mon Sep 17 00:00:00 2001 From: Elwardi Date: Mon, 19 Jul 2021 13:31:44 +0100 Subject: [PATCH 6/9] Better format error catching in menuFromCommand prompts --- pkg/gui/custom_commands.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/gui/custom_commands.go b/pkg/gui/custom_commands.go index 1ac92f49c..c258b600f 100644 --- a/pkg/gui/custom_commands.go +++ b/pkg/gui/custom_commands.go @@ -185,7 +185,10 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand // Need to make a menu out of what the cmd has displayed candidates := []string{} buff := bytes.NewBuffer(nil) - temp := template.Must(template.New("format").Parse(prompt.Format)) + temp, err := template.New("format").Parse(prompt.Format) + if err != nil { + return gui.surfaceError(errors.New("unable to parse format, error: " + err.Error())) + } for _, str := range strings.Split(string(message), "\n") { if str == "" { continue @@ -203,7 +206,11 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand } } } - temp.Execute(buff, tmplData) + err = temp.Execute(buff, tmplData) + if err != nil { + return gui.surfaceError(err) + } + candidates = append(candidates, strings.TrimSpace(buff.String())) buff.Reset() } From edfb0a26b2bebb67520061df843aa95f78a981c0 Mon Sep 17 00:00:00 2001 From: Elwardi Date: Tue, 20 Jul 2021 20:59:03 +0100 Subject: [PATCH 7/9] Refactor code around handleCustomCommandKeybinding --- pkg/gui/custom_commands.go | 278 +++++++++++++++++++------------------ 1 file changed, 146 insertions(+), 132 deletions(-) diff --git a/pkg/gui/custom_commands.go b/pkg/gui/custom_commands.go index c258b600f..e9aeedb4f 100644 --- a/pkg/gui/custom_commands.go +++ b/pkg/gui/custom_commands.go @@ -54,6 +54,149 @@ func (gui *Gui) resolveTemplate(templateStr string, promptResponses []string) (s return utils.ResolveTemplate(templateStr, objects) } +func (gui *Gui) inputPrompt(prompt config.CustomCommandPrompt, promptResponses []string, responseIdx int, wrappedF func() error) error { + title, err := gui.resolveTemplate(prompt.Title, promptResponses) + if err != nil { + return gui.surfaceError(err) + } + + initialValue, err := gui.resolveTemplate(prompt.InitialValue, promptResponses) + if err != nil { + return gui.surfaceError(err) + } + + return gui.prompt(promptOpts{ + title: title, + initialContent: initialValue, + handleConfirm: func(str string) error { + promptResponses[responseIdx] = str + return wrappedF() + }, + }) +} + +func (gui *Gui) menuPrompt(prompt config.CustomCommandPrompt, promptResponses []string, responseIdx int, wrappedF func() error) error { + // need to make a menu here some how + menuItems := make([]*menuItem, len(prompt.Options)) + for i, option := range prompt.Options { + option := option + + nameTemplate := option.Name + if nameTemplate == "" { + // this allows you to only pass values rather than bother with names/descriptions + nameTemplate = option.Value + } + name, err := gui.resolveTemplate(nameTemplate, promptResponses) + if err != nil { + return gui.surfaceError(err) + } + + description, err := gui.resolveTemplate(option.Description, promptResponses) + if err != nil { + return gui.surfaceError(err) + } + + value, err := gui.resolveTemplate(option.Value, promptResponses) + if err != nil { + return gui.surfaceError(err) + } + + menuItems[i] = &menuItem{ + displayStrings: []string{name, utils.ColoredString(description, color.FgYellow)}, + onPress: func() error { + promptResponses[responseIdx] = value + return wrappedF() + }, + } + } + + title, err := gui.resolveTemplate(prompt.Title, promptResponses) + if err != nil { + return gui.surfaceError(err) + } + + return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true}) +} +func (gui *Gui) generateMenuCandidates(commandOutput string, filter string, format string) ([]string, error) { + candidates := []string{} + reg, err := regexp.Compile(filter) + if err != nil { + return candidates, gui.surfaceError(errors.New("unable to parse filter regex, error: " + err.Error())) + } + buff := bytes.NewBuffer(nil) + temp, err := template.New("format").Parse(format) + if err != nil { + return candidates, gui.surfaceError(errors.New("unable to parse format, error: " + err.Error())) + } + for _, str := range strings.Split(string(commandOutput), "\n") { + if str == "" { + continue + } + tmplData := map[string]string{} + out := reg.FindAllStringSubmatch(str, -1) + if len(out) > 0 { + for groupIdx, group := range reg.SubexpNames() { + // Record matched group with group ids + matchName := "group_" + strconv.Itoa(groupIdx) + tmplData[matchName] = group + // Record last named group non-empty matches as group matches + if group != "" { + tmplData[group] = out[0][groupIdx] + } + } + } + err = temp.Execute(buff, tmplData) + if err != nil { + return candidates, gui.surfaceError(err) + } + + candidates = append(candidates, strings.TrimSpace(buff.String())) + buff.Reset() + } + return candidates, err +} + +func (gui *Gui) menuPromptFromCommand(prompt config.CustomCommandPrompt, promptResponses []string, responseIdx int, wrappedF func() error) error { + // Collect cmd to run from config + cmdStr, err := gui.resolveTemplate(prompt.Command, promptResponses) + if err != nil { + return gui.surfaceError(err) + } + + // Collect Filter regexp + filter, err := gui.resolveTemplate(prompt.Filter, promptResponses) + if err != nil { + return gui.surfaceError(err) + } + + // Run and save output + message, err := gui.GitCommand.RunCommandWithOutput(cmdStr) + if err != nil { + return gui.surfaceError(err) + } + + // Need to make a menu out of what the cmd has displayed + candidates, err := gui.generateMenuCandidates(message, filter, prompt.Format) + + menuItems := make([]*menuItem, len(candidates)) + for i := range candidates { + menuItems[i] = &menuItem{ + displayStrings: []string{candidates[i]}, + onPress: func() error { + promptResponses[responseIdx] = candidates[i] + return wrappedF() + }, + } + } + + title, err := gui.resolveTemplate(prompt.Title, promptResponses) + if err != nil { + return gui.surfaceError(err) + } + + return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true}) +} + func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand) func() error { return func() error { promptResponses := make([]string, len(customCommand.Prompts)) @@ -94,144 +237,15 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand switch prompt.Type { case "input": f = func() error { - title, err := gui.resolveTemplate(prompt.Title, promptResponses) - if err != nil { - return gui.surfaceError(err) - } - - initialValue, err := gui.resolveTemplate(prompt.InitialValue, promptResponses) - if err != nil { - return gui.surfaceError(err) - } - - return gui.prompt(promptOpts{ - title: title, - initialContent: initialValue, - handleConfirm: func(str string) error { - promptResponses[idx] = str - - return wrappedF() - }, - }) + return gui.inputPrompt(prompt, promptResponses, idx, wrappedF) } case "menu": f = func() error { - // need to make a menu here some how - menuItems := make([]*menuItem, len(prompt.Options)) - for i, option := range prompt.Options { - option := option - - nameTemplate := option.Name - if nameTemplate == "" { - // this allows you to only pass values rather than bother with names/descriptions - nameTemplate = option.Value - } - name, err := gui.resolveTemplate(nameTemplate, promptResponses) - if err != nil { - return gui.surfaceError(err) - } - - description, err := gui.resolveTemplate(option.Description, promptResponses) - if err != nil { - return gui.surfaceError(err) - } - - value, err := gui.resolveTemplate(option.Value, promptResponses) - if err != nil { - return gui.surfaceError(err) - } - - menuItems[i] = &menuItem{ - displayStrings: []string{name, utils.ColoredString(description, color.FgYellow)}, - onPress: func() error { - promptResponses[idx] = value - - return wrappedF() - }, - } - } - - title, err := gui.resolveTemplate(prompt.Title, promptResponses) - if err != nil { - return gui.surfaceError(err) - } - - return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true}) + return gui.menuPrompt(prompt, promptResponses, idx, wrappedF) } case "menuFromCommand": f = func() error { - // Collect cmd to run from config - cmdStr, err := gui.resolveTemplate(prompt.Command, promptResponses) - if err != nil { - return gui.surfaceError(err) - } - - // Collect Filter regexp - filter, err := gui.resolveTemplate(prompt.Filter, promptResponses) - if err != nil { - return gui.surfaceError(err) - } - reg, err := regexp.Compile(filter) - if err != nil { - return gui.surfaceError(errors.New("unable to parse filter regex, error: " + err.Error())) - } - - // Run and save output - message, err := gui.GitCommand.RunCommandWithOutput(cmdStr) - if err != nil { - return gui.surfaceError(err) - } - - // Need to make a menu out of what the cmd has displayed - candidates := []string{} - buff := bytes.NewBuffer(nil) - temp, err := template.New("format").Parse(prompt.Format) - if err != nil { - return gui.surfaceError(errors.New("unable to parse format, error: " + err.Error())) - } - for _, str := range strings.Split(string(message), "\n") { - if str == "" { - continue - } - tmplData := map[string]string{} - out := reg.FindAllStringSubmatch(str, -1) - if len(out) > 0 { - for groupIdx, group := range reg.SubexpNames() { - // Record matched group with group ids - matchName := "group_" + strconv.Itoa(groupIdx) - tmplData[matchName] = group - // Record last named group non-empty matches as group matches - if group != "" { - tmplData[group] = out[0][idx] - } - } - } - err = temp.Execute(buff, tmplData) - if err != nil { - return gui.surfaceError(err) - } - - candidates = append(candidates, strings.TrimSpace(buff.String())) - buff.Reset() - } - - menuItems := make([]*menuItem, len(candidates)) - for i := range candidates { - menuItems[i] = &menuItem{ - displayStrings: []string{candidates[i]}, - onPress: func() error { - promptResponses[idx] = candidates[i] - return wrappedF() - }, - } - } - - title, err := gui.resolveTemplate(prompt.Title, promptResponses) - if err != nil { - return gui.surfaceError(err) - } - - return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true}) + return gui.menuPromptFromCommand(prompt, promptResponses, idx, wrappedF) } default: return gui.createErrorPanel("custom command prompt must have a type of 'input', 'menu' or 'menuFromCommand'") From 148bf2c070ab6f9a7e559851ed4d05496e651552 Mon Sep 17 00:00:00 2001 From: Elwardi Date: Thu, 22 Jul 2021 15:44:16 +0100 Subject: [PATCH 8/9] Add test for GenerateMenuCandidates from Custom Commands --- pkg/gui/custom_commands.go | 10 +++++--- pkg/gui/dummies.go | 21 ++++++++++++++++ pkg/gui/gui_test.go | 49 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 pkg/gui/dummies.go diff --git a/pkg/gui/custom_commands.go b/pkg/gui/custom_commands.go index e9aeedb4f..7ae1da8c0 100644 --- a/pkg/gui/custom_commands.go +++ b/pkg/gui/custom_commands.go @@ -117,7 +117,8 @@ func (gui *Gui) menuPrompt(prompt config.CustomCommandPrompt, promptResponses [] return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true}) } -func (gui *Gui) generateMenuCandidates(commandOutput string, filter string, format string) ([]string, error) { + +func (gui *Gui) GenerateMenuCandidates(commandOutput string, filter string, format string) ([]string, error) { candidates := []string{} reg, err := regexp.Compile(filter) if err != nil { @@ -138,7 +139,7 @@ func (gui *Gui) generateMenuCandidates(commandOutput string, filter string, form for groupIdx, group := range reg.SubexpNames() { // Record matched group with group ids matchName := "group_" + strconv.Itoa(groupIdx) - tmplData[matchName] = group + tmplData[matchName] = out[0][groupIdx] // Record last named group non-empty matches as group matches if group != "" { tmplData[group] = out[0][groupIdx] @@ -176,7 +177,10 @@ func (gui *Gui) menuPromptFromCommand(prompt config.CustomCommandPrompt, promptR } // Need to make a menu out of what the cmd has displayed - candidates, err := gui.generateMenuCandidates(message, filter, prompt.Format) + candidates, err := gui.GenerateMenuCandidates(message, filter, prompt.Format) + if err != nil { + return gui.surfaceError(err) + } menuItems := make([]*menuItem, len(candidates)) for i := range candidates { diff --git a/pkg/gui/dummies.go b/pkg/gui/dummies.go new file mode 100644 index 000000000..97a03d784 --- /dev/null +++ b/pkg/gui/dummies.go @@ -0,0 +1,21 @@ +package gui + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/i18n" + "github.com/jesseduffield/lazygit/pkg/updates" +) + +// NewDummyGui creates a new dummy GUI for testing +func NewDummyUpdater() *updates.Updater { + DummyUpdater, _ := updates.NewUpdater(utils.NewDummyLog(), config.NewDummyAppConfig(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog())) + return DummyUpdater +} + +func NewDummyGui() *Gui { + DummyGui, _ := NewGui(utils.NewDummyLog(), commands.NewDummyGitCommand(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog()), config.NewDummyAppConfig(), NewDummyUpdater(), "", false) + return DummyGui +} diff --git a/pkg/gui/gui_test.go b/pkg/gui/gui_test.go index 8e37cecfe..8fcf0d4de 100644 --- a/pkg/gui/gui_test.go +++ b/pkg/gui/gui_test.go @@ -80,3 +80,52 @@ func runCmdHeadless(cmd *exec.Cmd) error { return f.Close() } + +func TestGuiGenerateMenuCandidates(t *testing.T) { + type scenario struct { + testName string + cmdOut string + filter string + format string + test func([]string, error) + } + + scenarios := []scenario{ + { + "Extract remote branch name", + "upstream/pr-1", + "upstream/(?P.*)", + "{{ .branch }}", + func(actual []string, err error) { + assert.NoError(t, err) + assert.EqualValues(t, "pr-1", actual[0]) + }, + }, + { + "Multiple named groups", + "upstream/pr-1", + "(?P[a-z]*)/(?P.*)", + "{{ .branch }}|{{ .remote }}", + func(actual []string, err error) { + assert.NoError(t, err) + assert.EqualValues(t, "pr-1|upstream", actual[0]) + }, + }, + { + "Multiple named groups with group ids", + "upstream/pr-1", + "(?P[a-z]*)/(?P.*)", + "{{ .group_2 }}|{{ .group_1 }}", + func(actual []string, err error) { + assert.NoError(t, err) + assert.EqualValues(t, "pr-1|upstream", actual[0]) + }, + }, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + s.test(NewDummyGui().GenerateMenuCandidates(s.cmdOut, s.filter, s.format)) + }) + } +} From 713fae3e32a0482314a9489d3169ca62af2417de Mon Sep 17 00:00:00 2001 From: mjarkk Date: Thu, 22 Jul 2021 19:45:43 +0200 Subject: [PATCH 9/9] format code --- pkg/gui/custom_commands.go | 2 +- pkg/gui/dummies.go | 12 ++++++------ pkg/gui/gui_test.go | 32 ++++++++++++++++---------------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/pkg/gui/custom_commands.go b/pkg/gui/custom_commands.go index 7ae1da8c0..752587e69 100644 --- a/pkg/gui/custom_commands.go +++ b/pkg/gui/custom_commands.go @@ -139,7 +139,7 @@ func (gui *Gui) GenerateMenuCandidates(commandOutput string, filter string, form for groupIdx, group := range reg.SubexpNames() { // Record matched group with group ids matchName := "group_" + strconv.Itoa(groupIdx) - tmplData[matchName] = out[0][groupIdx] + tmplData[matchName] = out[0][groupIdx] // Record last named group non-empty matches as group matches if group != "" { tmplData[group] = out[0][groupIdx] diff --git a/pkg/gui/dummies.go b/pkg/gui/dummies.go index 97a03d784..d8a7cba29 100644 --- a/pkg/gui/dummies.go +++ b/pkg/gui/dummies.go @@ -1,21 +1,21 @@ package gui import ( - "github.com/jesseduffield/lazygit/pkg/config" - "github.com/jesseduffield/lazygit/pkg/utils" - "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/updates" + "github.com/jesseduffield/lazygit/pkg/utils" ) // NewDummyGui creates a new dummy GUI for testing func NewDummyUpdater() *updates.Updater { - DummyUpdater, _ := updates.NewUpdater(utils.NewDummyLog(), config.NewDummyAppConfig(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog())) - return DummyUpdater + DummyUpdater, _ := updates.NewUpdater(utils.NewDummyLog(), config.NewDummyAppConfig(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog())) + return DummyUpdater } func NewDummyGui() *Gui { - DummyGui, _ := NewGui(utils.NewDummyLog(), commands.NewDummyGitCommand(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog()), config.NewDummyAppConfig(), NewDummyUpdater(), "", false) + DummyGui, _ := NewGui(utils.NewDummyLog(), commands.NewDummyGitCommand(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog()), config.NewDummyAppConfig(), NewDummyUpdater(), "", false) return DummyGui } diff --git a/pkg/gui/gui_test.go b/pkg/gui/gui_test.go index 8fcf0d4de..ec1279608 100644 --- a/pkg/gui/gui_test.go +++ b/pkg/gui/gui_test.go @@ -86,39 +86,39 @@ func TestGuiGenerateMenuCandidates(t *testing.T) { testName string cmdOut string filter string - format string + format string test func([]string, error) } scenarios := []scenario{ { "Extract remote branch name", - "upstream/pr-1", - "upstream/(?P.*)", - "{{ .branch }}", + "upstream/pr-1", + "upstream/(?P.*)", + "{{ .branch }}", func(actual []string, err error) { - assert.NoError(t, err) - assert.EqualValues(t, "pr-1", actual[0]) + assert.NoError(t, err) + assert.EqualValues(t, "pr-1", actual[0]) }, }, { "Multiple named groups", - "upstream/pr-1", - "(?P[a-z]*)/(?P.*)", - "{{ .branch }}|{{ .remote }}", + "upstream/pr-1", + "(?P[a-z]*)/(?P.*)", + "{{ .branch }}|{{ .remote }}", func(actual []string, err error) { - assert.NoError(t, err) - assert.EqualValues(t, "pr-1|upstream", actual[0]) + assert.NoError(t, err) + assert.EqualValues(t, "pr-1|upstream", actual[0]) }, }, { "Multiple named groups with group ids", - "upstream/pr-1", - "(?P[a-z]*)/(?P.*)", - "{{ .group_2 }}|{{ .group_1 }}", + "upstream/pr-1", + "(?P[a-z]*)/(?P.*)", + "{{ .group_2 }}|{{ .group_1 }}", func(actual []string, err error) { - assert.NoError(t, err) - assert.EqualValues(t, "pr-1|upstream", actual[0]) + assert.NoError(t, err) + assert.EqualValues(t, "pr-1|upstream", actual[0]) }, }, }