From 16fa22a36ef24e433e49426052e4156cb49064e0 Mon Sep 17 00:00:00 2001
From: Jesse Duffield <jessedduffield@gmail.com>
Date: Mon, 29 May 2023 14:24:49 +1000
Subject: [PATCH] Add suggestionsPreset to custom commands system

---
 docs/Custom_Command_Keybindings.md            |  1 +
 pkg/config/user_config.go                     |  3 +-
 pkg/gui/services/custom_commands/client.go    |  2 +-
 .../custom_commands/handler_creator.go        | 35 +++++++++-
 pkg/gui/services/custom_commands/resolver.go  |  5 ++
 .../custom_commands/suggestions_preset.go     | 64 +++++++++++++++++++
 pkg/integration/tests/test_list.go            |  1 +
 7 files changed, 107 insertions(+), 4 deletions(-)
 create mode 100644 pkg/integration/tests/custom_commands/suggestions_preset.go

diff --git a/docs/Custom_Command_Keybindings.md b/docs/Custom_Command_Keybindings.md
index 62a034ecf..af9431d07 100644
--- a/docs/Custom_Command_Keybindings.md
+++ b/docs/Custom_Command_Keybindings.md
@@ -107,6 +107,7 @@ The permitted prompt fields are:
 | type              | one of 'input', 'menu', or 'confirm'                                                           | yes        |
 | 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         |
+| suggestionsPreset | (only applicable to 'input prompts'. Shows suggestions as the value is typed. One of 'files', 'branches', 'remotes', 'remoteBranches', refs'.                                 | no         |
 | body              | (only applicable to 'confirm' prompts) the immutable body text 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        |
diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go
index 3b311c0de..27ae17953 100644
--- a/pkg/config/user_config.go
+++ b/pkg/config/user_config.go
@@ -366,7 +366,8 @@ type CustomCommandPrompt struct {
 	Title string `yaml:"title"`
 
 	// this only apply to input prompts
-	InitialValue string `yaml:"initialValue"`
+	InitialValue      string `yaml:"initialValue"`
+	SuggestionsPreset string `yaml:"suggestionsPreset"`
 
 	// this only applies to confirm prompts
 	Body string `yaml:"body"`
diff --git a/pkg/gui/services/custom_commands/client.go b/pkg/gui/services/custom_commands/client.go
index 4cacba385..5e456ff6e 100644
--- a/pkg/gui/services/custom_commands/client.go
+++ b/pkg/gui/services/custom_commands/client.go
@@ -19,7 +19,7 @@ func NewClient(
 	helpers *helpers.Helpers,
 ) *Client {
 	sessionStateLoader := NewSessionStateLoader(c, helpers.Refs)
-	handlerCreator := NewHandlerCreator(c, sessionStateLoader)
+	handlerCreator := NewHandlerCreator(c, sessionStateLoader, helpers.Suggestions)
 	keybindingCreator := NewKeybindingCreator(c)
 	customCommands := c.UserConfig.CustomCommands
 
diff --git a/pkg/gui/services/custom_commands/handler_creator.go b/pkg/gui/services/custom_commands/handler_creator.go
index da1c449fd..2d13a6f55 100644
--- a/pkg/gui/services/custom_commands/handler_creator.go
+++ b/pkg/gui/services/custom_commands/handler_creator.go
@@ -1,6 +1,7 @@
 package custom_commands
 
 import (
+	"fmt"
 	"strings"
 	"text/template"
 
@@ -18,11 +19,13 @@ type HandlerCreator struct {
 	sessionStateLoader *SessionStateLoader
 	resolver           *Resolver
 	menuGenerator      *MenuGenerator
+	suggestionsHelper  *helpers.SuggestionsHelper
 }
 
 func NewHandlerCreator(
 	c *helpers.HelperCommon,
 	sessionStateLoader *SessionStateLoader,
+	suggestionsHelper *helpers.SuggestionsHelper,
 ) *HandlerCreator {
 	resolver := NewResolver(c.Common)
 	menuGenerator := NewMenuGenerator(c.Common)
@@ -32,6 +35,7 @@ func NewHandlerCreator(
 		sessionStateLoader: sessionStateLoader,
 		resolver:           resolver,
 		menuGenerator:      menuGenerator,
+		suggestionsHelper:  suggestionsHelper,
 	}
 }
 
@@ -104,15 +108,42 @@ func (self *HandlerCreator) call(customCommand config.CustomCommand) func() erro
 }
 
 func (self *HandlerCreator) inputPrompt(prompt *config.CustomCommandPrompt, wrappedF func(string) error) error {
+	var findSuggestionsFn func(string) []*types.Suggestion
+	if prompt.SuggestionsPreset != "" {
+		var err error
+		findSuggestionsFn, err = self.getPresetSuggestionsFn(prompt.SuggestionsPreset)
+		if err != nil {
+			return err
+		}
+	}
+
 	return self.c.Prompt(types.PromptOpts{
-		Title:          prompt.Title,
-		InitialContent: prompt.InitialValue,
+		Title:               prompt.Title,
+		InitialContent:      prompt.InitialValue,
+		FindSuggestionsFunc: findSuggestionsFn,
 		HandleConfirm: func(str string) error {
 			return wrappedF(str)
 		},
 	})
 }
 
+func (self *HandlerCreator) getPresetSuggestionsFn(preset string) (func(string) []*types.Suggestion, error) {
+	switch preset {
+	case "files":
+		return self.suggestionsHelper.GetFilePathSuggestionsFunc(), nil
+	case "branches":
+		return self.suggestionsHelper.GetBranchNameSuggestionsFunc(), nil
+	case "remotes":
+		return self.suggestionsHelper.GetRemoteSuggestionsFunc(), nil
+	case "remoteBranches":
+		return self.suggestionsHelper.GetRemoteBranchesSuggestionsFunc("/"), nil
+	case "refs":
+		return self.suggestionsHelper.GetRefsSuggestionsFunc(), nil
+	default:
+		return nil, fmt.Errorf("Unknown value for suggestionsPreset in custom command: %s. Valid values: files, branches, remotes, remoteBranches, refs", preset)
+	}
+}
+
 func (self *HandlerCreator) menuPrompt(prompt *config.CustomCommandPrompt, wrappedF func(string) error) error {
 	menuItems := slices.Map(prompt.Options, func(option config.CustomCommandMenuOption) *types.MenuItem {
 		return &types.MenuItem{
diff --git a/pkg/gui/services/custom_commands/resolver.go b/pkg/gui/services/custom_commands/resolver.go
index 4c32a51ca..119581bfc 100644
--- a/pkg/gui/services/custom_commands/resolver.go
+++ b/pkg/gui/services/custom_commands/resolver.go
@@ -34,6 +34,11 @@ func (self *Resolver) resolvePrompt(
 		return nil, err
 	}
 
+	result.SuggestionsPreset, err = resolveTemplate(prompt.SuggestionsPreset)
+	if err != nil {
+		return nil, err
+	}
+
 	result.Body, err = resolveTemplate(prompt.Body)
 	if err != nil {
 		return nil, err
diff --git a/pkg/integration/tests/custom_commands/suggestions_preset.go b/pkg/integration/tests/custom_commands/suggestions_preset.go
new file mode 100644
index 000000000..894e3b1fe
--- /dev/null
+++ b/pkg/integration/tests/custom_commands/suggestions_preset.go
@@ -0,0 +1,64 @@
+package custom_commands
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var SuggestionsPreset = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "Using a custom command that uses a suggestions preset in a prompt step",
+	ExtraCmdArgs: []string{},
+	Skip:         false,
+	SetupRepo: func(shell *Shell) {
+		shell.NewBranch("branch-one")
+		shell.EmptyCommit("blah")
+		shell.NewBranch("branch-two")
+		shell.EmptyCommit("blah")
+		shell.NewBranch("branch-three")
+		shell.EmptyCommit("blah")
+		shell.NewBranch("branch-four")
+		shell.EmptyCommit("blah")
+	},
+	SetupConfig: func(cfg *config.AppConfig) {
+		cfg.UserConfig.CustomCommands = []config.CustomCommand{
+			{
+				Key:     "a",
+				Context: "localBranches",
+				Command: `git checkout {{.Form.Branch}}`,
+				Prompts: []config.CustomCommandPrompt{
+					{
+						Key:               "Branch",
+						Type:              "input",
+						Title:             "Enter a branch name",
+						SuggestionsPreset: "branches",
+					},
+				},
+			},
+		}
+	},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Branches().
+			Focus().
+			Lines(
+				Contains("branch-four").IsSelected(),
+				Contains("branch-three"),
+				Contains("branch-two"),
+				Contains("branch-one"),
+			).
+			Press("a")
+
+		t.ExpectPopup().Prompt().
+			Title(Equals("Enter a branch name")).
+			Type("three").
+			SuggestionLines(Contains("branch-three")).
+			ConfirmFirstSuggestion()
+
+		t.Views().Branches().
+			Lines(
+				Contains("branch-three").IsSelected(),
+				Contains("branch-four"),
+				Contains("branch-two"),
+				Contains("branch-one"),
+			)
+	},
+})
diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go
index 1dd1a48e7..e0800f7bd 100644
--- a/pkg/integration/tests/test_list.go
+++ b/pkg/integration/tests/test_list.go
@@ -77,6 +77,7 @@ var tests = []*components.IntegrationTest{
 	custom_commands.MenuFromCommandsOutput,
 	custom_commands.MultiplePrompts,
 	custom_commands.OmitFromHistory,
+	custom_commands.SuggestionsPreset,
 	diff.Diff,
 	diff.DiffAndApplyPatch,
 	diff.DiffCommits,