From f80a6ab97a2ec30fecc60e91f48d40eacd1bd0e9 Mon Sep 17 00:00:00 2001
From: ccoVeille <3875889+ccoVeille@users.noreply.github.com>
Date: Sat, 8 Feb 2025 02:00:26 +0100
Subject: [PATCH] feat: add CompletionWithDesc helper

The code has also been refactored to use a type alias for completion and a completion helper

Using a type alias is a non-breaking change and it makes the code more readable and easier to understand.

Signed-off-by: ccoVeille <3875889+ccoVeille@users.noreply.github.com>
---
 command.go     |  4 ++--
 completions.go | 50 +++++++++++++++++++++++++++++++++-----------------
 2 files changed, 35 insertions(+), 19 deletions(-)

diff --git a/command.go b/command.go
index 60b9c455..d108f856 100644
--- a/command.go
+++ b/command.go
@@ -82,7 +82,7 @@ type Command struct {
 	Example string
 
 	// ValidArgs is list of all valid non-flag arguments that are accepted in shell completions
-	ValidArgs []string
+	ValidArgs []CompletionChoice
 	// ValidArgsFunction is an optional function that provides valid non-flag arguments for shell completion.
 	// It is a dynamic version of using ValidArgs.
 	// Only one of ValidArgs and ValidArgsFunction can be used for a command.
@@ -94,7 +94,7 @@ type Command struct {
 	// ArgAliases is List of aliases for ValidArgs.
 	// These are not suggested to the user in the shell completion,
 	// but accepted if entered manually.
-	ArgAliases []string
+	ArgAliases []CompletionChoice
 
 	// BashCompletionFunction is custom bash functions used by the legacy bash autocompletion generator.
 	// For portability with other shells, it is recommended to instead use ValidArgsFunction
diff --git a/completions.go b/completions.go
index cd899c73..044a83e7 100644
--- a/completions.go
+++ b/completions.go
@@ -117,15 +117,31 @@ type CompletionOptions struct {
 	HiddenDefaultCmd bool
 }
 
+// CompletionChoice is a string that can be used for completions
+//
+// two formats are supported:
+//   - the name of the flag
+//   - the name of the flag with its description (separated by a tab)
+//
+// [CompletionChoiceWithDescription] can be used to create a completion string with a description using the TAB separator.
+//
+// Note: Go type alias is used to provide a more descriptive name in the documentation, but any string can be used.
+type CompletionChoice = string
+
 // CompletionFunc is a function that provides completion results.
-type CompletionFunc func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective)
+type CompletionFunc func(cmd *Command, args []CompletionChoice, toComplete string) ([]string, ShellCompDirective)
+
+// CompletionChoiceWithDescription returns a CompletionChoice with a description
+func CompletionChoiceWithDescription(name, desc string) CompletionChoice {
+	return name + "\t" + desc
+}
 
 // NoFileCompletions can be used to disable file completion for commands that should
 // not trigger file completions.
 //
 // This method satisfies [CompletionFunc].
 // It can be used with [Command.RegisterFlagCompletionFunc] and for [Command.ValidArgsFunction].
-func NoFileCompletions(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
+func NoFileCompletions(cmd *Command, args []CompletionChoice, toComplete string) ([]string, ShellCompDirective) {
 	return nil, ShellCompDirectiveNoFileComp
 }
 
@@ -134,8 +150,8 @@ func NoFileCompletions(cmd *Command, args []string, toComplete string) ([]string
 //
 // This method returns a function that satisfies [CompletionFunc]
 // It can be used with [Command.RegisterFlagCompletionFunc] and for [Command.ValidArgsFunction].
-func FixedCompletions(choices []string, directive ShellCompDirective) CompletionFunc {
-	return func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
+func FixedCompletions(choices []CompletionChoice, directive ShellCompDirective) CompletionFunc {
+	return func(cmd *Command, args []CompletionChoice, toComplete string) ([]string, ShellCompDirective) {
 		return choices, directive
 	}
 }
@@ -290,7 +306,7 @@ type SliceValue interface {
 	GetSlice() []string
 }
 
-func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDirective, error) {
+func (c *Command) getCompletions(args []string) (*Command, []CompletionChoice, ShellCompDirective, error) {
 	// The last argument, which is not completely typed by the user,
 	// should not be part of the list of arguments
 	toComplete := args[len(args)-1]
@@ -318,7 +334,7 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
 	}
 	if err != nil {
 		// Unable to find the real command. E.g., <program> someInvalidCmd <TAB>
-		return c, []string{}, ShellCompDirectiveDefault, fmt.Errorf("unable to find a command for arguments: %v", trimmedArgs)
+		return c, []CompletionChoice{}, ShellCompDirectiveDefault, fmt.Errorf("unable to find a command for arguments: %v", trimmedArgs)
 	}
 	finalCmd.ctx = c.ctx
 
@@ -348,7 +364,7 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
 
 	// Parse the flags early so we can check if required flags are set
 	if err = finalCmd.ParseFlags(finalArgs); err != nil {
-		return finalCmd, []string{}, ShellCompDirectiveDefault, fmt.Errorf("Error while parsing flags from args %v: %s", finalArgs, err.Error())
+		return finalCmd, []CompletionChoice{}, ShellCompDirectiveDefault, fmt.Errorf("Error while parsing flags from args %v: %s", finalArgs, err.Error())
 	}
 
 	realArgCount := finalCmd.Flags().NArg()
@@ -360,14 +376,14 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
 	if flagErr != nil {
 		// If error type is flagCompError and we don't want flagCompletion we should ignore the error
 		if _, ok := flagErr.(*flagCompError); !(ok && !flagCompletion) {
-			return finalCmd, []string{}, ShellCompDirectiveDefault, flagErr
+			return finalCmd, []CompletionChoice{}, ShellCompDirectiveDefault, flagErr
 		}
 	}
 
 	// Look for the --help or --version flags.  If they are present,
 	// there should be no further completions.
 	if helpOrVersionFlagPresent(finalCmd) {
-		return finalCmd, []string{}, ShellCompDirectiveNoFileComp, nil
+		return finalCmd, []CompletionChoice{}, ShellCompDirectiveNoFileComp, nil
 	}
 
 	// We only remove the flags from the arguments if DisableFlagParsing is not set.
@@ -396,11 +412,11 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
 				return finalCmd, subDir, ShellCompDirectiveFilterDirs, nil
 			}
 			// Directory completion
-			return finalCmd, []string{}, ShellCompDirectiveFilterDirs, nil
+			return finalCmd, []CompletionChoice{}, ShellCompDirectiveFilterDirs, nil
 		}
 	}
 
-	var completions []string
+	var completions []CompletionChoice
 	var directive ShellCompDirective
 
 	// Enforce flag groups before doing flag completions
@@ -542,7 +558,7 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
 	if completionFn != nil {
 		// Go custom completion defined for this flag or command.
 		// Call the registered completion function to get the completions.
-		var comps []string
+		var comps []CompletionChoice
 		comps, directive = completionFn(finalCmd, finalArgs, toComplete)
 		completions = append(completions, comps...)
 	}
@@ -562,12 +578,12 @@ func helpOrVersionFlagPresent(cmd *Command) bool {
 	return false
 }
 
-func getFlagNameCompletions(flag *pflag.Flag, toComplete string) []string {
+func getFlagNameCompletions(flag *pflag.Flag, toComplete string) []CompletionChoice {
 	if nonCompletableFlag(flag) {
-		return []string{}
+		return []CompletionChoice{}
 	}
 
-	var completions []string
+	var completions []CompletionChoice
 	flagName := "--" + flag.Name
 	if strings.HasPrefix(flagName, toComplete) {
 		// Flag without the =
@@ -595,8 +611,8 @@ func getFlagNameCompletions(flag *pflag.Flag, toComplete string) []string {
 	return completions
 }
 
-func completeRequireFlags(finalCmd *Command, toComplete string) []string {
-	var completions []string
+func completeRequireFlags(finalCmd *Command, toComplete string) []CompletionChoice {
+	var completions []CompletionChoice
 
 	doCompleteRequiredFlags := func(flag *pflag.Flag) {
 		if _, present := flag.Annotations[BashCompOneRequiredFlag]; present {