mirror of
https://github.com/go-task/task.git
synced 2026-09-01 19:50:16 +02:00
`--temp-dir` was missing from the flag-to-directive map, so it fell back to plain file completion while `--dir` and `--remote-cache-dir` offered directories. The rest is dead weight: `listTasks` re-defaulted a sorter `NewExecutor` already sets and scanned descriptions for templates even with `--no-descriptions`; `detectTaskName` had a `--` branch `Complete` returns before reaching; the two flag-value branches built the same suggestion slice twice. `os.Args[2:]` is now sliced in one place, `complete.Words()`, instead of three, and the test helpers reuse `slicesext.Convert`.
209 lines
5.6 KiB
Go
209 lines
5.6 KiB
Go
package complete
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/spf13/pflag"
|
|
|
|
"github.com/go-task/task/v3"
|
|
"github.com/go-task/task/v3/internal/refs"
|
|
"github.com/go-task/task/v3/internal/slicesext"
|
|
"github.com/go-task/task/v3/taskfile/ast"
|
|
)
|
|
|
|
// e may be nil when the Taskfile failed to load; flag completion still works.
|
|
func Complete(e *task.Executor, fs *pflag.FlagSet, args []string, opts Options) ([]Suggestion, Directive) {
|
|
ctx := parseContext(args)
|
|
|
|
if ctx.afterDash {
|
|
return nil, DirectiveDefault
|
|
}
|
|
|
|
if flag := ctx.flagValue(fs); flag != nil {
|
|
return completeFlagValue(flag.Name, "")
|
|
}
|
|
|
|
if strings.HasPrefix(ctx.toComplete, "-") {
|
|
if flagWord, _, ok := strings.Cut(ctx.toComplete, "="); ok {
|
|
if f := matchFlagName(fs, flagWord); f != nil && flagTakesValue(f) {
|
|
// Shells match against the whole token, so a bare value never would.
|
|
return completeFlagValue(f.Name, flagWord+"=")
|
|
}
|
|
}
|
|
return listFlags(fs), DirectiveNoFileComp
|
|
}
|
|
|
|
// No prior arg means no task word, so `task <tab>` never builds the list.
|
|
if e != nil && e.Taskfile != nil && len(args) > 1 {
|
|
if taskName := detectTaskName(args, taskNames(e), fs); taskName != "" {
|
|
return completeTaskVars(e, taskName)
|
|
}
|
|
}
|
|
|
|
return completeTaskNames(e, opts)
|
|
}
|
|
|
|
func NeedsTaskfile(args []string, fs *pflag.FlagSet) bool {
|
|
if !parseContext(args).inTaskContext(fs) {
|
|
return false
|
|
}
|
|
// Reading the Taskfile from standard input would hang the shell on a keystroke.
|
|
f := fs.Lookup("taskfile")
|
|
return f == nil || f.Value.String() != "-"
|
|
}
|
|
|
|
func taskNames(e *task.Executor) []string {
|
|
if e == nil || e.Taskfile == nil {
|
|
return nil
|
|
}
|
|
var out []string
|
|
for t := range e.Taskfile.Tasks.Values(nil) {
|
|
if t.Internal {
|
|
continue
|
|
}
|
|
name, _ := suggestedName(t.Task)
|
|
out = append(out, name)
|
|
for _, alias := range t.Aliases {
|
|
name, _ := suggestedName(alias)
|
|
out = append(out, name)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func completeTaskNames(e *task.Executor, opts Options) ([]Suggestion, Directive) {
|
|
if e == nil || e.Taskfile == nil {
|
|
return nil, DirectiveNoFileComp
|
|
}
|
|
tasks := listTasks(e, opts)
|
|
desc := func(t *ast.Task) string {
|
|
if opts.NoDescriptions {
|
|
return ""
|
|
}
|
|
return t.Desc
|
|
}
|
|
|
|
out := make([]Suggestion, 0, len(tasks))
|
|
seen := make(map[string]bool, len(tasks))
|
|
anyPartial := false
|
|
add := func(name, desc string) {
|
|
value, partial := suggestedName(name)
|
|
// `*-wildcard-*` has no prefix, and `wildcard-*` / `wildcard-*-*` share one.
|
|
if value == "" || seen[value] {
|
|
return
|
|
}
|
|
seen[value] = true
|
|
if partial {
|
|
anyPartial = true
|
|
if desc == "" && !opts.NoDescriptions {
|
|
desc = name
|
|
}
|
|
}
|
|
out = append(out, Suggestion{Value: value, Description: desc})
|
|
}
|
|
|
|
for _, t := range tasks {
|
|
add(t.Task, desc(t))
|
|
if opts.NoAliases {
|
|
continue
|
|
}
|
|
for _, alias := range t.Aliases {
|
|
add(alias, desc(t))
|
|
}
|
|
}
|
|
|
|
// A truncated pattern is half a name: the cursor must stay against it.
|
|
if anyPartial {
|
|
return out, DirectiveNoSpace | DirectiveNoFileComp
|
|
}
|
|
return out, DirectiveNoFileComp
|
|
}
|
|
|
|
// GetTaskList compiles every task, on every keystroke, and a description is the
|
|
// only compiled field read: worth its cost only when one holds a template.
|
|
func listTasks(e *task.Executor, opts Options) []*ast.Task {
|
|
out := make([]*ast.Task, 0, e.Taskfile.Tasks.Len())
|
|
templated := false
|
|
for t := range e.Taskfile.Tasks.Values(e.TaskSorter) {
|
|
if t.Internal {
|
|
continue
|
|
}
|
|
templated = templated || (!opts.NoDescriptions && strings.Contains(t.Desc, "{{"))
|
|
out = append(out, t)
|
|
}
|
|
|
|
if templated {
|
|
// The uncompiled tasks keep one broken task from emptying the list.
|
|
if compiled, err := e.GetTaskList(task.FilterOutInternal); err == nil {
|
|
return compiled
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// A pattern is truncated at its `*`: it is not runnable, `.MATCH` would be empty.
|
|
func suggestedName(name string) (string, bool) {
|
|
if prefix, _, ok := strings.Cut(name, "*"); ok {
|
|
return prefix, true
|
|
}
|
|
return strings.TrimRight(name, ":"), false
|
|
}
|
|
|
|
// prefix is `<flag>=` for the inline form, so a candidate matches the whole token.
|
|
func completeFlagValue(flagName, prefix string) ([]Suggestion, Directive) {
|
|
// An absent key yields DirectiveDefault, falling through to the enums.
|
|
switch flagDirective[flagName] {
|
|
case DirectiveFilterFileExt:
|
|
return suggest("", taskfileExtensions), DirectiveFilterFileExt
|
|
case DirectiveFilterDirs:
|
|
return nil, DirectiveFilterDirs
|
|
}
|
|
|
|
if values, ok := flagEnums[flagName]; ok {
|
|
return suggest(prefix, values), DirectiveNoFileComp
|
|
}
|
|
|
|
return nil, DirectiveDefault
|
|
}
|
|
|
|
func suggest(prefix string, values []string) []Suggestion {
|
|
return slicesext.Convert(values, func(v string) Suggestion {
|
|
return Suggestion{Value: prefix + v}
|
|
})
|
|
}
|
|
|
|
func completeTaskVars(e *task.Executor, taskName string) ([]Suggestion, Directive) {
|
|
compiled, err := e.FastCompiledTask(&task.Call{Task: taskName})
|
|
if err != nil || compiled == nil || compiled.Requires == nil {
|
|
return nil, DirectiveNoFileComp
|
|
}
|
|
|
|
out := make([]Suggestion, 0, 8)
|
|
for _, v := range compiled.Requires.Vars {
|
|
if v == nil || v.Name == "" {
|
|
continue
|
|
}
|
|
values := enumValues(v, compiled.Vars)
|
|
if len(values) == 0 {
|
|
out = append(out, Suggestion{Value: v.Name + "="})
|
|
continue
|
|
}
|
|
for _, val := range values {
|
|
out = append(out, Suggestion{Value: v.Name + "=" + val})
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return nil, DirectiveNoFileComp
|
|
}
|
|
// KeepOrder preserves the declaration order of the `requires` block.
|
|
return out, DirectiveNoSpace | DirectiveNoFileComp | DirectiveKeepOrder
|
|
}
|
|
|
|
func enumValues(v *ast.VarsWithValidation, vars *ast.Vars) []string {
|
|
resolved := refs.ResolveEnum(v, vars)
|
|
if resolved.Enum == nil {
|
|
return nil
|
|
}
|
|
return resolved.Enum.Value
|
|
}
|