1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-06-04 23:37:41 +02:00

Merge pull request #1416 from FoamScience/feature_menuOptions

This commit is contained in:
Mark Kopenga 2021-08-07 15:24:42 +02:00 committed by GitHub
commit 5d1a9639b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 87 additions and 41 deletions

View File

@ -47,7 +47,8 @@ customCommands:
title: 'Remote branch:' title: 'Remote branch:'
command: 'git branch -r --list {{index .PromptResponses 0}}/*' command: 'git branch -r --list {{index .PromptResponses 0}}/*'
filter: '.*{{index .PromptResponses 0}}/(?P<branch>.*)' filter: '.*{{index .PromptResponses 0}}/(?P<branch>.*)'
format: '{{ .branch }}' valueFormat: '{{ .branch }}'
labelFormat: ''
``` ```
Looking at the command assigned to the 'n' key, here's what the result looks like: Looking at the command assigned to the 'n' key, here's what the result looks like:
@ -92,19 +93,26 @@ The permitted contexts are:
The permitted prompt fields are: The permitted prompt fields are:
| _field_ | _description_ | _required_ | | _field_ | _description_ | _required_ |
| ------------ | -------------------------------------------------------------------------------- | ---------- | | ------------ | -------------------------------------------------------------------------------- | ---------- |
| type | one of 'input' or 'menu' | yes | | type | one of 'input' or 'menu' | yes |
| title | the title to display in the popup panel | no | | 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 | | 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 | | 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 | | command | (only applicable to 'menuFromCommand' prompts) the command to run to generate | yes |
| | menu options | | | | menu options | |
| filter | (only applicable to 'menuFromCommand' prompts) the regexp to run specifying | yes | | filter | (only applicable to 'menuFromCommand' prompts) the regexp to run specifying | yes |
| | groups which are going to be kept from the command's output | | | | 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 | | valueFormat | (only applicable to 'menuFromCommand' prompts) how to format matched groups from | yes |
| | the filter. You can use named groups, or `{{ .group_GROUPID }}`. | yes | | | the filter to construct a menu item's value (What gets appended to prompt | |
| | PS: named groups keep first match only | yes | | | responses when the item is selected). You can use named groups, | |
| | or `{{ .group_GROUPID }}`. | |
| | PS: named groups keep first match only | |
| labelFormat | (only applicable to 'menuFromCommand' prompts) how to format matched groups from | no |
| | the filter to construct the item's label (What's shown on screen). You can use | |
| | named groups, or `{{ .group_GROUPID }}`. If this is not specified, `valueFormat` | |
| | is shown instead. | |
| | PS: named groups keep first match only | |
The permitted option fields are: The permitted option fields are:
| _field_ | _description_ | _required_ | | _field_ | _description_ | _required_ |

View File

@ -283,9 +283,10 @@ type CustomCommandPrompt struct {
Options []CustomCommandMenuOption Options []CustomCommandMenuOption
// this only applies to menuFromCommand // this only applies to menuFromCommand
Command string `yaml:"command"` Command string `yaml:"command"`
Filter string `yaml:"filter"` Filter string `yaml:"filter"`
Format string `yaml:"format"` ValueFormat string `yaml:"valueFormat"`
LabelFormat string `yaml:"labelFormat"`
} }
type CustomCommandMenuOption struct { type CustomCommandMenuOption struct {

View File

@ -33,6 +33,11 @@ type CustomCommandObjects struct {
PromptResponses []string PromptResponses []string
} }
type commandMenuEntry struct {
label string
value string
}
func (gui *Gui) resolveTemplate(templateStr string, promptResponses []string) (string, error) { func (gui *Gui) resolveTemplate(templateStr string, promptResponses []string) (string, error) {
objects := CustomCommandObjects{ objects := CustomCommandObjects{
SelectedFile: gui.getSelectedFile(), SelectedFile: gui.getSelectedFile(),
@ -118,21 +123,30 @@ func (gui *Gui) menuPrompt(prompt config.CustomCommandPrompt, promptResponses []
return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true}) 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, filter, valueFormat, labelFormat string) ([]commandMenuEntry, error) {
candidates := []string{}
reg, err := regexp.Compile(filter) reg, err := regexp.Compile(filter)
if err != nil { if err != nil {
return candidates, gui.surfaceError(errors.New("unable to parse filter regex, error: " + err.Error())) return nil, gui.surfaceError(errors.New("unable to parse filter regex, error: " + err.Error()))
} }
buff := bytes.NewBuffer(nil) buff := bytes.NewBuffer(nil)
temp, err := template.New("format").Parse(format)
valueTemp, err := template.New("format").Parse(valueFormat)
if err != nil { if err != nil {
return candidates, gui.surfaceError(errors.New("unable to parse format, error: " + err.Error())) return nil, gui.surfaceError(errors.New("unable to parse value format, error: " + err.Error()))
} }
descTemp, err := template.New("format").Parse(labelFormat)
if err != nil {
return nil, gui.surfaceError(errors.New("unable to parse label format, error: " + err.Error()))
}
candidates := []commandMenuEntry{}
for _, str := range strings.Split(string(commandOutput), "\n") { for _, str := range strings.Split(string(commandOutput), "\n") {
if str == "" { if str == "" {
continue continue
} }
tmplData := map[string]string{} tmplData := map[string]string{}
out := reg.FindAllStringSubmatch(str, -1) out := reg.FindAllStringSubmatch(str, -1)
if len(out) > 0 { if len(out) > 0 {
@ -146,12 +160,28 @@ func (gui *Gui) GenerateMenuCandidates(commandOutput string, filter string, form
} }
} }
} }
err = temp.Execute(buff, tmplData)
err = valueTemp.Execute(buff, tmplData)
if err != nil { if err != nil {
return candidates, gui.surfaceError(err) return candidates, gui.surfaceError(err)
} }
entry := commandMenuEntry{
value: strings.TrimSpace(buff.String()),
}
if labelFormat != "" {
buff.Reset()
err = descTemp.Execute(buff, tmplData)
if err != nil {
return candidates, gui.surfaceError(err)
}
entry.label = strings.TrimSpace(buff.String())
} else {
entry.label = entry.value
}
candidates = append(candidates, entry)
candidates = append(candidates, strings.TrimSpace(buff.String()))
buff.Reset() buff.Reset()
} }
return candidates, err return candidates, err
@ -177,7 +207,7 @@ func (gui *Gui) menuPromptFromCommand(prompt config.CustomCommandPrompt, promptR
} }
// Need to make a menu out of what the cmd has displayed // 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.ValueFormat, prompt.LabelFormat)
if err != nil { if err != nil {
return gui.surfaceError(err) return gui.surfaceError(err)
} }
@ -185,9 +215,9 @@ func (gui *Gui) menuPromptFromCommand(prompt config.CustomCommandPrompt, promptR
menuItems := make([]*menuItem, len(candidates)) menuItems := make([]*menuItem, len(candidates))
for i := range candidates { for i := range candidates {
menuItems[i] = &menuItem{ menuItems[i] = &menuItem{
displayStrings: []string{candidates[i]}, displayStrings: []string{candidates[i].label},
onPress: func() error { onPress: func() error {
promptResponses[responseIdx] = candidates[i] promptResponses[responseIdx] = candidates[i].value
return wrappedF() return wrappedF()
}, },
} }

View File

@ -83,32 +83,37 @@ func runCmdHeadless(cmd *exec.Cmd) error {
func TestGuiGenerateMenuCandidates(t *testing.T) { func TestGuiGenerateMenuCandidates(t *testing.T) {
type scenario struct { type scenario struct {
testName string testName string
cmdOut string cmdOut string
filter string filter string
format string valueFormat string
test func([]string, error) labelFormat string
test func([]commandMenuEntry, error)
} }
scenarios := []scenario{ scenarios := []scenario{
{ {
"Extract remote branch name", "Extract remote branch name",
"upstream/pr-1", "upstream/pr-1",
"upstream/(?P<branch>.*)", "(?P<remote>[a-z_]+)/(?P<branch>.*)",
"{{ .branch }}", "{{ .branch }}",
func(actual []string, err error) { "Remote: {{ .remote }}",
func(actualEntry []commandMenuEntry, err error) {
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, "pr-1", actual[0]) assert.EqualValues(t, "pr-1", actualEntry[0].value)
assert.EqualValues(t, "Remote: upstream", actualEntry[0].label)
}, },
}, },
{ {
"Multiple named groups", "Multiple named groups with empty labelFormat",
"upstream/pr-1", "upstream/pr-1",
"(?P<remote>[a-z]*)/(?P<branch>.*)", "(?P<remote>[a-z]*)/(?P<branch>.*)",
"{{ .branch }}|{{ .remote }}", "{{ .branch }}|{{ .remote }}",
func(actual []string, err error) { "",
func(actualEntry []commandMenuEntry, err error) {
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, "pr-1|upstream", actual[0]) assert.EqualValues(t, "pr-1|upstream", actualEntry[0].value)
assert.EqualValues(t, "pr-1|upstream", actualEntry[0].label)
}, },
}, },
{ {
@ -116,16 +121,18 @@ func TestGuiGenerateMenuCandidates(t *testing.T) {
"upstream/pr-1", "upstream/pr-1",
"(?P<remote>[a-z]*)/(?P<branch>.*)", "(?P<remote>[a-z]*)/(?P<branch>.*)",
"{{ .group_2 }}|{{ .group_1 }}", "{{ .group_2 }}|{{ .group_1 }}",
func(actual []string, err error) { "Remote: {{ .group_1 }}",
func(actualEntry []commandMenuEntry, err error) {
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, "pr-1|upstream", actual[0]) assert.EqualValues(t, "pr-1|upstream", actualEntry[0].value)
assert.EqualValues(t, "Remote: upstream", actualEntry[0].label)
}, },
}, },
} }
for _, s := range scenarios { for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) { t.Run(s.testName, func(t *testing.T) {
s.test(NewDummyGui().GenerateMenuCandidates(s.cmdOut, s.filter, s.format)) s.test(NewDummyGui().GenerateMenuCandidates(s.cmdOut, s.filter, s.valueFormat, s.labelFormat))
}) })
} }
} }