Files
commandkit/completion_flags.go

218 lines
6.6 KiB
Go

package commandkit
import (
"strings"
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
)
type completionTargetKind uint8
const (
completionTargetNone completionTargetKind = iota
completionTargetCommands
completionTargetFlags
completionTargetValue
completionTargetOperand
)
type completionTarget struct {
kind completionTargetKind
choices []commandmeta.CompletionChoice
hint commandmeta.CompletionHint
prefix string
valuePrefix string
}
type completionFlagState struct {
supplied map[string]bool
operands []string
pending *commandmeta.Flag
ended bool
invalid bool
}
func completionFlags(context completionContext) []commandmeta.Flag {
flags := make([]commandmeta.Flag, 0, len(context.command.Flags)+len(context.command.CoreFlags)+1)
flags = append(flags, commandmeta.HelpFlag())
flags = append(flags, context.command.CoreFlags...)
flags = append(flags, context.command.Flags...)
return flags
}
func walkCompletionFlags(tokens []string, lookup commandmeta.FlagLookup) completionFlagState {
state := completionFlagState{supplied: make(map[string]bool)}
for index := 0; index < len(tokens); index++ {
token := tokens[index]
if state.ended {
state.operands = append(state.operands, token)
continue
}
switch commandmeta.ClassifyFlagToken(token) {
case commandmeta.TerminatorToken:
state.ended = true
case commandmeta.OperandToken:
state.operands = append(state.operands, token)
case commandmeta.LongFlagToken:
spelling, _, attached := commandmeta.SplitLongFlag(token)
flag, ok := lookup.Find(spelling)
if !ok {
state.invalid = true
return state
}
state.supplied[flag.Name] = true
if !flag.Boolean && !attached {
if index+1 == len(tokens) {
state.pending = &flag
return state
}
index++
}
case commandmeta.ShortFlagToken:
for offset := 1; offset < len(token); offset++ {
flag, ok := lookup.Find("-" + token[offset:offset+1])
if !ok {
state.invalid = true
return state
}
state.supplied[flag.Name] = true
suffix := token[offset+1:]
if flag.Boolean {
if strings.HasPrefix(suffix, "=") {
break
}
continue
}
if suffix == "" {
if index+1 == len(tokens) {
state.pending = &flag
return state
}
index++
}
break
}
}
}
return state
}
func completionTargetFor(snapshot *routingSnapshot, context completionContext) completionTarget {
if context.command == nil {
return completionTarget{}
}
flags := completionFlags(context)
lookup := commandmeta.NewFlagLookup(flags, context.aliases)
state := walkCompletionFlags(context.tail, lookup)
if state.invalid {
return completionTarget{}
}
if state.pending != nil {
return completionTarget{kind: completionTargetValue, hint: state.pending.Completion, valuePrefix: context.current}
}
if !state.ended && context.commandPosition {
if children := completionChildren(snapshot, context); len(children) > 0 {
return completionTarget{kind: completionTargetCommands, choices: children}
}
}
if !state.ended && strings.HasPrefix(context.current, "-") {
return completionOptionTarget(context.current, flags, lookup, state.supplied)
}
if hint, ok := completionOperandHint(context.command.Arguments, len(state.operands)); ok {
return completionTarget{kind: completionTargetOperand, hint: hint, valuePrefix: context.current}
}
if !state.ended && context.current == "" {
return completionTarget{kind: completionTargetFlags, choices: completionFlagNames(flags, state.supplied, "")}
}
return completionTarget{}
}
func completionOptionTarget(current string, flags []commandmeta.Flag, lookup commandmeta.FlagLookup, supplied map[string]bool) completionTarget {
if current == "-" {
return completionTarget{kind: completionTargetFlags, choices: completionFlagNames(flags, supplied, current)}
}
if strings.HasPrefix(current, "--") {
spelling, value, attached := commandmeta.SplitLongFlag(current)
if attached {
if flag, ok := lookup.Find(spelling); ok {
return completionTarget{kind: completionTargetValue, hint: flag.Completion, prefix: spelling + "=", valuePrefix: value}
}
return completionTarget{}
}
return completionTarget{kind: completionTargetFlags, choices: completionFlagNames(flags, supplied, current)}
}
used := make(map[string]bool, len(supplied))
for name, value := range supplied {
used[name] = value
}
for offset := 1; offset < len(current); offset++ {
flag, ok := lookup.Find("-" + current[offset:offset+1])
if !ok {
return completionTarget{}
}
used[flag.Name] = true
suffix := current[offset+1:]
if flag.Boolean {
if strings.HasPrefix(suffix, "=") {
return completionTarget{kind: completionTargetValue, hint: flag.Completion, prefix: current[:offset+2], valuePrefix: suffix[1:]}
}
continue
}
prefix := current[:offset+1]
if strings.HasPrefix(suffix, "=") {
prefix += "="
suffix = suffix[1:]
}
return completionTarget{kind: completionTargetValue, hint: flag.Completion, prefix: prefix, valuePrefix: suffix}
}
var choices []commandmeta.CompletionChoice
for _, flag := range flags {
if flag.Short != "" && completionFlagEligible(flag, used) {
choices = append(choices, commandmeta.CompletionChoice{Value: current + flag.Short, Description: flag.Usage})
}
}
return completionTarget{kind: completionTargetFlags, choices: choices}
}
func completionFlagNames(flags []commandmeta.Flag, supplied map[string]bool, prefix string) []commandmeta.CompletionChoice {
var choices []commandmeta.CompletionChoice
for _, flag := range flags {
if !completionFlagEligible(flag, supplied) {
continue
}
for _, spelling := range []string{"--" + flag.Name, shortCompletionSpelling(flag.Short)} {
if spelling != "" && strings.HasPrefix(spelling, prefix) {
choices = append(choices, commandmeta.CompletionChoice{Value: spelling, Description: flag.Usage})
}
}
}
return choices
}
func completionFlagEligible(flag commandmeta.Flag, supplied map[string]bool) bool {
return !supplied[flag.Name] || flag.Repeated || commandmeta.IsReportingFlag(flag.Name)
}
func shortCompletionSpelling(short string) string {
if short == "" {
return ""
}
return "-" + short
}
func completionOperandHint(arguments []commandmeta.Argument, completed int) (commandmeta.CompletionHint, bool) {
total := completed + 1
counts, err := allocateOperandCounts(total, arguments, false)
if err != nil {
return commandmeta.CompletionHint{}, false
}
position := 0
for index, count := range counts {
position += count
if position >= total {
return arguments[index].Completion, true
}
}
return commandmeta.CompletionHint{}, false
}