refactor(completion): name path arguments explicitly and share firstArg

The six commands that take a file or directory argument set their
completer directly instead of a Use-line parser wrapping every
command, the first-argument-only wrappers become one firstArg helper,
and only the bash completion help keeps a custom text.

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-12 10:50:37 +02:00
1 parent 1f4b5f868f
commit 9ac5b79e9e
22 files changed
+75 -212

No files matched your search

+36 -89
View File
@@ -519,9 +519,9 @@ func actionScope(scope func() services.ActionScope, t target) services.ActionSco
// pathFlags take local file paths, like every flag whose name ends in -file.
var pathFlags = []string{"asset", "attachment", "file", "input", "ssh-key"}
// completeDefaults completes paths only where a command takes them: path flags, flags marked with
// MarkFlagFilename, and positional arguments named file or directory in Use. Everything else without a
// completer completes nothing, by the root's default directive, rather than file names.
// completeDefaults completes paths only where a command takes them: path flags and flags marked
// with MarkFlagFilename. Everything else without a completer completes nothing, by the root's
// default directive, rather than file names.
func completeDefaults(cmd *cobra.Command) {
for _, flags := range []*pflag.FlagSet{cmd.Flags(), cmd.PersistentFlags()} {
flags.VisitAll(func(flag *pflag.Flag) {
@@ -536,23 +536,6 @@ func completeDefaults(cmd *cobra.Command) {
}
})
}
if slots := argSlots(cmd.Use); len(slots) > 0 {
complete := cmd.ValidArgsFunction
cmd.ValidArgsFunction = func(c *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
slot := slots[min(len(args), len(slots)-1)]
switch {
case len(args) >= len(slots) && !slot.variadic:
return nil, cobra.ShellCompDirectiveNoFileComp
case slot.kind == "file":
return completeFiles(c, args, toComplete)
case slot.kind == "directory":
return completeDirectories(c, args, toComplete)
case complete != nil:
return complete(c, args, toComplete)
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
}
for _, child := range cmd.Commands() {
completeDefaults(child)
}
@@ -568,89 +551,53 @@ func completeDirectories(*cobra.Command, []string, string) ([]cobra.Completion,
return nil, cobra.ShellCompDirectiveFilterDirs
}
// argSlot is one positional argument that a Use line declares, such as <number>, [directory] or
// <file>...; kind is "file" or "directory" for path arguments.
type argSlot struct {
kind string
variadic bool
}
// argSlots parses the positional arguments of a Use line.
func argSlots(use string) []argSlot {
_, rest, _ := strings.Cut(use, " ")
var slots []argSlot
depth, start := 0, 0
for i, r := range rest {
switch r {
case '<', '[', '{':
if depth == 0 {
start = i
}
depth++
case '>', ']', '}':
if depth--; depth != 0 {
continue
}
slot := argSlot{variadic: strings.HasPrefix(rest[i+1:], "...") || strings.HasSuffix(rest[start:i], "...")}
for name := range strings.FieldsFuncSeq(rest[start:i+1], func(r rune) bool { return strings.ContainsRune("<>[]|. ", r) }) {
switch {
case name == "file" || strings.HasSuffix(name, "-file"):
slot.kind = "file"
case name == "directory" || name == "dir":
slot.kind = "directory"
}
}
slots = append(slots, slot)
// firstArg completes only the first argument with complete.
func firstArg(complete cobra.CompletionFunc) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
if len(args) > 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return complete(cmd, args, toComplete)
}
return slots
}
// completionInstallHelp explains how to load the completion scripts, including with macOS's bash 3.2
// and in zsh without compinit.
// pathArgs completes the arguments before position n with complete, and paths with path from
// position n on: every later argument when variadic, else only the one at n.
func pathArgs(n int, variadic bool, path, complete cobra.CompletionFunc) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
switch {
case len(args) < n:
return complete(cmd, args, toComplete)
case len(args) == n || variadic:
return path(cmd, args, toComplete)
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
}
// completionInstallHelp explains how to load the bash script with macOS's bash 3.2, which cannot
// source <(tea completion bash); cobra's help covers the other shells.
func completionInstallHelp(root *cobra.Command) {
completion, _, err := root.Find([]string{"completion"})
if err != nil || completion == root {
return
}
completion.Short = "Generate shell completion scripts"
completion.Long = `Generate shell completion scripts for tea.
Completion offers commands, flags, flag values such as --state or --json fields, and values
from the selected instance, such as repositories, issue numbers and labels. See
tea completion <shell> --help for how to load the script in your shell.`
help := map[string][2]string{
"bash": {`Generate the completion script for bash.
The script needs the bash-completion package. On macOS, install bash-completion with
Homebrew for the system bash 3.2, or bash-completion@2 for a Homebrew bash, and load it
as its install notes say.
To load completions in every new shell, add the eval line below to ~/.bashrc (on macOS,
~/.bash_profile). Use eval: bash 3.2, the default on macOS, cannot source <(tea completion bash).`, `# Load completions in this shell, or in every shell from ~/.bashrc
$ eval "$(tea completion bash)"
# Or install the script for every user (Linux, then macOS)
$ tea completion bash > /etc/bash_completion.d/tea
$ tea completion bash > "$(brew --prefix)/etc/bash_completion.d/tea"`},
"zsh": {`Generate the completion script for zsh.
Completion must be initialized before the script loads: add these lines to ~/.zshrc,
keeping an existing compinit call instead of the first line. Without compinit, zsh
reports "command not found: compdef".`, `$ autoload -U compinit && compinit
$ source <(tea completion zsh)
# Or install the script into a directory of your fpath once
$ tea completion zsh > "${fpath[1]}/_tea"`},
"fish": {`Generate the completion script for fish.
To load completions in every new shell, install the script once.`, `$ tea completion fish > ~/.config/fish/completions/tea.fish`},
"powershell": {`Generate the completion script for PowerShell.
To load completions in every new shell, add this line to your PowerShell profile.`, `$ tea completion powershell | Out-String | Invoke-Expression`},
examples := map[string]string{
"bash": `$ eval "$(tea completion bash)"`,
"zsh": `$ source <(tea completion zsh)`,
"fish": `$ tea completion fish > ~/.config/fish/completions/tea.fish`,
"powershell": `$ tea completion powershell | Out-String | Invoke-Expression`,
}
for _, shell := range completion.Commands() {
shell.Long, shell.Example = help[shell.Name()][0], help[shell.Name()][1]
shell.Example = examples[shell.Name()]
if flag := shell.Flags().Lookup("no-descriptions"); flag != nil {
flag.Usage = "Disable completion descriptions"
}
}
bash, _, _ := completion.Find([]string{"bash"})
bash.Long = `Generate the completion script for bash. It needs the bash-completion package.
To load completions in every new shell, add the eval line below to ~/.bashrc (on macOS,
~/.bash_profile); bash 3.2, the default on macOS, cannot source <(tea completion bash).`
}