Interpret flags and operands for completion
This commit is contained in:
217
completion_flags.go
Normal file
217
completion_flags.go
Normal file
@@ -0,0 +1,217 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user