Capture completion hints in command metadata
This commit is contained in:
28
command.go
28
command.go
@@ -98,6 +98,8 @@ type FlagMeta struct {
|
||||
Usage string
|
||||
// Required reports whether execution requires the flag to be supplied.
|
||||
Required bool
|
||||
// Completion describes optional value suggestions.
|
||||
Completion CompletionHint
|
||||
}
|
||||
|
||||
// ArgumentMeta describes a positional command argument. Its name and usage are
|
||||
@@ -107,6 +109,8 @@ type ArgumentMeta struct {
|
||||
Name string
|
||||
// Usage briefly describes the argument.
|
||||
Usage string
|
||||
// Completion describes optional value suggestions.
|
||||
Completion CompletionHint
|
||||
}
|
||||
|
||||
// Flag represents a typed binding for a command flag. Copies retain binding
|
||||
@@ -202,13 +206,14 @@ func (c typedCommand[I]) bridgeFlags() []commandmeta.Flag {
|
||||
for index, flag := range c.spec.Flags {
|
||||
binding := flag.binding
|
||||
flags[index] = commandmeta.Flag{
|
||||
Name: binding.meta.Name,
|
||||
Short: binding.meta.Short,
|
||||
Usage: binding.meta.Usage,
|
||||
Default: binding.defaultText,
|
||||
Boolean: binding.boolean,
|
||||
Repeated: binding.repeated,
|
||||
Required: binding.meta.Required,
|
||||
Name: binding.meta.Name,
|
||||
Short: binding.meta.Short,
|
||||
Usage: binding.meta.Usage,
|
||||
Default: binding.defaultText,
|
||||
Boolean: binding.boolean,
|
||||
Repeated: binding.repeated,
|
||||
Required: binding.meta.Required,
|
||||
Completion: bridgeCompletion(binding.meta.Completion),
|
||||
}
|
||||
}
|
||||
return flags
|
||||
@@ -219,10 +224,11 @@ func (c typedCommand[I]) bridgeArguments() []commandmeta.Argument {
|
||||
for index, argument := range c.spec.Arguments {
|
||||
binding := argument.binding
|
||||
arguments[index] = commandmeta.Argument{
|
||||
Name: binding.meta.Name,
|
||||
Usage: binding.meta.Usage,
|
||||
Min: binding.min,
|
||||
Max: binding.max,
|
||||
Name: binding.meta.Name,
|
||||
Usage: binding.meta.Usage,
|
||||
Min: binding.min,
|
||||
Max: binding.max,
|
||||
Completion: bridgeCompletion(binding.meta.Completion),
|
||||
}
|
||||
}
|
||||
return arguments
|
||||
|
||||
55
completion.go
Normal file
55
completion.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package commandkit
|
||||
|
||||
import commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
|
||||
|
||||
// CompletionChoice is a literal suggested value and its optional description.
|
||||
type CompletionChoice struct {
|
||||
Value string
|
||||
Description string
|
||||
}
|
||||
|
||||
// CompletionHint selects suggestions for a flag or positional value. Its zero
|
||||
// value requests no value suggestions; hints do not constrain accepted input.
|
||||
type CompletionHint struct {
|
||||
kind completionHintKind
|
||||
choices []CompletionChoice
|
||||
}
|
||||
|
||||
type completionHintKind uint8
|
||||
|
||||
const (
|
||||
completionNone completionHintKind = iota
|
||||
completionChoices
|
||||
completionFiles
|
||||
completionDirectories
|
||||
)
|
||||
|
||||
// CompleteChoices suggests literal values. The supplied slice is copied.
|
||||
func CompleteChoices(choices ...CompletionChoice) CompletionHint {
|
||||
return CompletionHint{kind: completionChoices, choices: append([]CompletionChoice(nil), choices...)}
|
||||
}
|
||||
|
||||
// CompleteFiles requests shell filename completion.
|
||||
func CompleteFiles() CompletionHint { return CompletionHint{kind: completionFiles} }
|
||||
|
||||
// CompleteDirectories requests shell directory completion.
|
||||
func CompleteDirectories() CompletionHint { return CompletionHint{kind: completionDirectories} }
|
||||
|
||||
// NoCompletion requests no value suggestions.
|
||||
func NoCompletion() CompletionHint { return CompletionHint{} }
|
||||
|
||||
func bridgeCompletion(hint CompletionHint) commandmeta.CompletionHint {
|
||||
result := commandmeta.CompletionHint{}
|
||||
switch hint.kind {
|
||||
case completionChoices:
|
||||
result.Kind = commandmeta.CompletionChoices
|
||||
for _, choice := range hint.choices {
|
||||
result.Choices = append(result.Choices, commandmeta.CompletionChoice{Value: choice.Value, Description: choice.Description})
|
||||
}
|
||||
case completionFiles:
|
||||
result.Kind = commandmeta.CompletionFiles
|
||||
case completionDirectories:
|
||||
result.Kind = commandmeta.CompletionDirectories
|
||||
}
|
||||
return result
|
||||
}
|
||||
73
completion_test.go
Normal file
73
completion_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package commandkit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
|
||||
)
|
||||
|
||||
func TestCompletionHintsFollowCapturedBindings(t *testing.T) {
|
||||
type inner struct{ values Supplied[[]string] }
|
||||
type outer struct{ flags FlagValues[inner] }
|
||||
choices := []CompletionChoice{{Value: "alpha", Description: "first"}}
|
||||
hint := CompleteChoices(choices...)
|
||||
choices[0] = CompletionChoice{Value: "changed"}
|
||||
parse := func(value string) (string, error) { return value, nil }
|
||||
repeated := RepeatFlag(FlagMeta{Name: "tag", Completion: hint}, nil, parse,
|
||||
func(input *inner, value Supplied[[]string]) { input.values = value })
|
||||
captured := CaptureFlags([]Flag[inner]{repeated}, func(input *outer) *FlagValues[inner] { return &input.flags })
|
||||
command := NewCommand(CommandSpec[outer]{
|
||||
Name: "run",
|
||||
Flags: append(captured,
|
||||
ValueFlag(FlagMeta{Name: "file", Completion: CompleteFiles()}, (*string)(nil), parse, func(*outer, Supplied[string]) {}),
|
||||
BoolFlag(FlagMeta{Name: "enabled"}, false, func(*outer, Supplied[bool]) {}),
|
||||
ValueFlag(FlagMeta{Name: "empty", Completion: CompleteChoices()}, (*string)(nil), parse, func(*outer, Supplied[string]) {})),
|
||||
Arguments: []Argument[outer]{
|
||||
ValueArgument(ArgumentMeta{Name: "directory", Completion: CompleteDirectories()}, false, parse, func(*outer, Supplied[string]) {}),
|
||||
RepeatArgument(ArgumentMeta{Name: "values", Completion: hint}, 0, -1, parse, func(*outer, Supplied[[]string]) {}),
|
||||
},
|
||||
})
|
||||
flags := command.record.payload.bridgeFlags()
|
||||
arguments := command.record.payload.bridgeArguments()
|
||||
want := commandmeta.CompletionHint{Kind: commandmeta.CompletionChoices, Choices: []commandmeta.CompletionChoice{{Value: "alpha", Description: "first"}}}
|
||||
if !flags[0].Repeated || !reflect.DeepEqual(flags[0].Completion, want) || !reflect.DeepEqual(arguments[1].Completion, want) {
|
||||
t.Fatalf("captured hints: flags=%#v arguments=%#v", flags, arguments)
|
||||
}
|
||||
if flags[1].Completion.Kind != commandmeta.CompletionFiles || flags[2].Completion.Kind != commandmeta.CompletionNone || flags[3].Completion.Kind != commandmeta.CompletionChoices || len(flags[3].Completion.Choices) != 0 || arguments[0].Completion.Kind != commandmeta.CompletionDirectories {
|
||||
t.Fatalf("hint strategies: flags=%#v arguments=%#v", flags, arguments)
|
||||
}
|
||||
flags[0].Completion.Choices[0].Value = "mutated"
|
||||
if got := command.record.payload.bridgeFlags()[0].Completion; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("bridged choices changed captured hint: %#v", got)
|
||||
}
|
||||
if got := bridgeCompletion(NoCompletion()); got.Kind != commandmeta.CompletionNone || !reflect.DeepEqual(got, bridgeCompletion(CompletionHint{})) {
|
||||
t.Fatalf("no completion: %#v", got)
|
||||
}
|
||||
for _, flag := range append(commandmeta.ReportingFlags(), commandmeta.HelpFlag()) {
|
||||
if !reflect.DeepEqual(flag.Completion.Choices, []commandmeta.CompletionChoice{{Value: "true"}, {Value: "false"}}) {
|
||||
t.Fatalf("reporting flag choices: %#v", flag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompletionChoicesDoNotRestrictExecution(t *testing.T) {
|
||||
type input struct{ mode Supplied[string] }
|
||||
var received input
|
||||
command := NewCommand(CommandSpec[input]{
|
||||
Name: "run",
|
||||
Flags: []Flag[input]{ValueFlag(FlagMeta{Name: "mode", Completion: CompleteChoices(CompletionChoice{Value: "suggested"})}, (*string)(nil),
|
||||
func(value string) (string, error) { return value, nil },
|
||||
func(i *input, value Supplied[string]) { i.mode = value })},
|
||||
Run: func(_ Invocation, value input) Outcome { received = value; return Success() },
|
||||
})
|
||||
app, err := New(Identity{Name: "example"}, command)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
outcome := app.Execute(context.Background(), []string{"run", "--mode=other"}, Streams{})
|
||||
if outcome.Status != 0 || !received.mode.Set || received.mode.Value != "other" {
|
||||
t.Fatalf("outcome=%#v input=%#v", outcome, received)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# Shell completion implementation plan
|
||||
|
||||
Status: Stage 1 complete. Stage 2 is next.
|
||||
Status: Stages 1–2 complete. Stage 3 is next.
|
||||
|
||||
Implement the [shell completion roadmap](completion.md) from baseline
|
||||
`2301872`. Follow the stages in order in one sprint. Intermediate stages may
|
||||
@@ -62,6 +62,8 @@ this run; the focused adapter suite and the full interactive matrix pass.
|
||||
|
||||
## Stage 2: Capture completion hints
|
||||
|
||||
Status: complete.
|
||||
|
||||
1. Add the following public API in the root package. Add
|
||||
`Completion CompletionHint` to both `FlagMeta` and `ArgumentMeta`;
|
||||
existing binding constructors and their signatures stay in place.
|
||||
@@ -97,6 +99,14 @@ this run; the focused adapter suite and the full interactive matrix pass.
|
||||
suggestions, repeated/captured bindings keep hints, and values absent from
|
||||
a suggestion list still reach the existing converter.
|
||||
|
||||
Completion: added the public choice and hint constructors, carried hints through
|
||||
flag and operand bindings into neutral metadata, and supplied explicit boolean
|
||||
choices for built-in help and reporting flags. Focused tests cover copied
|
||||
choices, repeated and captured bindings, the empty-choice strategy, independent
|
||||
bridge copies, and execution of values outside the suggested choices. Root,
|
||||
internal adapter, and neutral metadata package tests pass with `GOWORK=off` and
|
||||
`GOTOOLCHAIN=local`.
|
||||
|
||||
## Stage 3: Install completion as an ordinary declaration
|
||||
|
||||
1. Add `func Completion(executable string) Command`. The parameter is the
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
func interpretFlagTokens(argv []string, flags []commandmeta.Flag, aliases []commandmeta.FlagAlias) ([]string, map[string][]string, []commandmeta.FlagOccurrence, *commandmeta.Failure) {
|
||||
canonical := make(map[string]commandmeta.Flag, len(flags)*2+2)
|
||||
help := commandmeta.Flag{Name: "help", Short: "h", Boolean: true}
|
||||
help := commandmeta.HelpFlag()
|
||||
canonical["--help"] = help
|
||||
canonical["-h"] = help
|
||||
for _, flag := range flags {
|
||||
|
||||
@@ -26,21 +26,45 @@ type Command struct {
|
||||
|
||||
// Argument describes one positional input for usage rendering.
|
||||
type Argument struct {
|
||||
Name string
|
||||
Usage string
|
||||
Min int
|
||||
Max int
|
||||
Name string
|
||||
Usage string
|
||||
Min int
|
||||
Max int
|
||||
Completion CompletionHint
|
||||
}
|
||||
|
||||
// Flag describes one local option without exposing a backend value.
|
||||
type Flag struct {
|
||||
Name string
|
||||
Short string
|
||||
Usage string
|
||||
Default string
|
||||
Boolean bool
|
||||
Repeated bool
|
||||
Required bool
|
||||
Name string
|
||||
Short string
|
||||
Usage string
|
||||
Default string
|
||||
Boolean bool
|
||||
Repeated bool
|
||||
Required bool
|
||||
Completion CompletionHint
|
||||
}
|
||||
|
||||
// CompletionKind selects the neutral value suggestion strategy.
|
||||
type CompletionKind uint8
|
||||
|
||||
const (
|
||||
CompletionNone CompletionKind = iota
|
||||
CompletionChoices
|
||||
CompletionFiles
|
||||
CompletionDirectories
|
||||
)
|
||||
|
||||
// CompletionChoice carries a literal value and optional description.
|
||||
type CompletionChoice struct {
|
||||
Value string
|
||||
Description string
|
||||
}
|
||||
|
||||
// CompletionHint describes value suggestions without backend types.
|
||||
type CompletionHint struct {
|
||||
Kind CompletionKind
|
||||
Choices []CompletionChoice
|
||||
}
|
||||
|
||||
var reportingFlags = [...]Flag{
|
||||
@@ -50,8 +74,24 @@ var reportingFlags = [...]Flag{
|
||||
{Name: "progress", Usage: "show progress messages", Default: "false", Boolean: true},
|
||||
}
|
||||
|
||||
// BooleanCompletion returns the ordinary explicit boolean suggestions.
|
||||
func BooleanCompletion() CompletionHint {
|
||||
return CompletionHint{Kind: CompletionChoices, Choices: []CompletionChoice{{Value: "true"}, {Value: "false"}}}
|
||||
}
|
||||
|
||||
// HelpFlag describes the built-in help option for parser contexts.
|
||||
func HelpFlag() Flag {
|
||||
return Flag{Name: "help", Short: "h", Boolean: true, Completion: BooleanCompletion()}
|
||||
}
|
||||
|
||||
// ReportingFlags returns the flags shared by CommandKit's invocation reporter.
|
||||
func ReportingFlags() []Flag { return append([]Flag(nil), reportingFlags[:]...) }
|
||||
func ReportingFlags() []Flag {
|
||||
flags := append([]Flag(nil), reportingFlags[:]...)
|
||||
for index := range flags {
|
||||
flags[index].Completion = BooleanCompletion()
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
// IsReportingFlag reports whether name is reserved for a shared reporting flag.
|
||||
func IsReportingFlag(name string) bool {
|
||||
|
||||
Reference in New Issue
Block a user