Add Predictor interface
This commit is contained in:
parent
a28594d28e
commit
967bae76f3
29
args.go
29
args.go
|
@ -1,25 +1,26 @@
|
|||
package complete
|
||||
|
||||
type args struct {
|
||||
all []string
|
||||
completed []string
|
||||
beingTyped string
|
||||
lastCompleted string
|
||||
// Args describes command line arguments
|
||||
type Args struct {
|
||||
All []string
|
||||
Completed []string
|
||||
Last string
|
||||
LastCompleted string
|
||||
}
|
||||
|
||||
func newArgs(line []string) args {
|
||||
func newArgs(line []string) Args {
|
||||
completed := removeLast(line)
|
||||
return args{
|
||||
all: line[1:],
|
||||
completed: completed,
|
||||
beingTyped: last(line),
|
||||
lastCompleted: last(completed),
|
||||
return Args{
|
||||
All: line[1:],
|
||||
Completed: completed,
|
||||
Last: last(line),
|
||||
LastCompleted: last(completed),
|
||||
}
|
||||
}
|
||||
|
||||
func (a args) from(i int) args {
|
||||
a.all = a.all[i:]
|
||||
a.completed = a.completed[i:]
|
||||
func (a Args) from(i int) Args {
|
||||
a.All = a.All[i:]
|
||||
a.Completed = a.Completed[i:]
|
||||
return a
|
||||
}
|
||||
|
||||
|
|
52
command.go
52
command.go
|
@ -12,30 +12,33 @@ type Command struct {
|
|||
Sub Commands
|
||||
|
||||
// Flags is a map of flags that the command accepts.
|
||||
// The key is the flag name, and the value is it's prediction predict.
|
||||
// The key is the flag name, and the value is it's predictions.
|
||||
Flags Flags
|
||||
|
||||
// Args are extra arguments that the command accepts, those who are
|
||||
// given without any flag before.
|
||||
Args Predicate
|
||||
Args Predictor
|
||||
}
|
||||
|
||||
// Commands is the type of Sub member, it maps a command name to a command struct
|
||||
type Commands map[string]Command
|
||||
|
||||
// Flags is the type Flags of the Flags member, it maps a flag name to the flag
|
||||
// prediction predict.
|
||||
type Flags map[string]Predicate
|
||||
// Flags is the type Flags of the Flags member, it maps a flag name to the flag predictions.
|
||||
type Flags map[string]Predictor
|
||||
|
||||
// predict returns all available complete predict for the given command
|
||||
// all are all except the last command line arguments relevant to the command
|
||||
func (c *Command) predict(a args) (options []match.Matcher, only bool) {
|
||||
// Predict returns all possible predictions for args according to the command struct
|
||||
func (c *Command) Predict(a Args) (predictions []string) {
|
||||
predictions, _ = c.predict(a)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Command) predict(a Args) (options []string, only bool) {
|
||||
|
||||
// if wordCompleted has something that needs to follow it,
|
||||
// it is the most relevant completion
|
||||
if predicate, ok := c.Flags[a.lastCompleted]; ok && predicate != nil {
|
||||
Log("Predicting according to flag %s", a.beingTyped)
|
||||
return predicate.predict(a.beingTyped), true
|
||||
if predictor, ok := c.Flags[a.LastCompleted]; ok && predictor != nil {
|
||||
Log("Predicting according to flag %s", a.Last)
|
||||
return predictor.Predict(a), true
|
||||
}
|
||||
|
||||
sub, options, only := c.searchSub(a)
|
||||
|
@ -45,24 +48,28 @@ func (c *Command) predict(a args) (options []match.Matcher, only bool) {
|
|||
|
||||
// if no sub command was found, return a list of the sub commands
|
||||
if sub == "" {
|
||||
options = append(options, c.subCommands()...)
|
||||
options = append(options, c.subCommands(a.Last)...)
|
||||
}
|
||||
|
||||
// add global available complete predict
|
||||
// add global available complete Predict
|
||||
for flag := range c.Flags {
|
||||
options = append(options, match.Prefix(flag))
|
||||
if m := match.Prefix(flag); m.Match(a.Last) {
|
||||
options = append(options, m.String())
|
||||
}
|
||||
}
|
||||
|
||||
// add additional expected argument of the command
|
||||
options = append(options, c.Args.predict(a.beingTyped)...)
|
||||
if c.Args != nil {
|
||||
options = append(options, c.Args.Predict(a)...)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// searchSub searches recursively within sub commands if the sub command appear
|
||||
// in the on of the arguments.
|
||||
func (c *Command) searchSub(a args) (sub string, all []match.Matcher, only bool) {
|
||||
for i, arg := range a.completed {
|
||||
func (c *Command) searchSub(a Args) (sub string, all []string, only bool) {
|
||||
for i, arg := range a.Completed {
|
||||
if cmd, ok := c.Sub[arg]; ok {
|
||||
sub = arg
|
||||
all, only = cmd.predict(a.from(i))
|
||||
|
@ -72,11 +79,12 @@ func (c *Command) searchSub(a args) (sub string, all []match.Matcher, only bool)
|
|||
return
|
||||
}
|
||||
|
||||
// suvCommands returns a list of matchers according to the sub command names
|
||||
func (c *Command) subCommands() []match.Matcher {
|
||||
subs := make([]match.Matcher, 0, len(c.Sub))
|
||||
// subCommands returns a list of matching sub commands
|
||||
func (c *Command) subCommands(last string) (prediction []string) {
|
||||
for sub := range c.Sub {
|
||||
subs = append(subs, match.Prefix(sub))
|
||||
if m := match.Prefix(sub); m.Match(last) {
|
||||
prediction = append(prediction, m.String())
|
||||
}
|
||||
}
|
||||
return subs
|
||||
return
|
||||
}
|
||||
|
|
16
complete.go
16
complete.go
|
@ -51,27 +51,13 @@ func (c *Complete) Run() bool {
|
|||
|
||||
a := newArgs(line)
|
||||
|
||||
options := complete(c.Command, a)
|
||||
options := c.Command.Predict(a)
|
||||
|
||||
Log("Completion: %s", options)
|
||||
output(options)
|
||||
return true
|
||||
}
|
||||
|
||||
// complete get a command an command line arguments and returns
|
||||
// matching completion options
|
||||
func complete(c Command, a args) (matching []string) {
|
||||
options, _ := c.predict(a)
|
||||
|
||||
for _, option := range options {
|
||||
Log("option %T, %s -> %t", option, option, option.Match(a.beingTyped))
|
||||
if option.Match(a.beingTyped) {
|
||||
matching = append(matching, option.String())
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getLine() ([]string, bool) {
|
||||
line := os.Getenv(envComplete)
|
||||
if line == "" {
|
||||
|
|
|
@ -13,20 +13,20 @@ func TestCompleter_Complete(t *testing.T) {
|
|||
c := Command{
|
||||
Sub: map[string]Command{
|
||||
"sub1": {
|
||||
Flags: map[string]Predicate{
|
||||
Flags: map[string]Predictor{
|
||||
"-flag1": PredictAnything,
|
||||
"-flag2": PredictNothing,
|
||||
},
|
||||
},
|
||||
"sub2": {
|
||||
Flags: map[string]Predicate{
|
||||
Flags: map[string]Predictor{
|
||||
"-flag2": PredictNothing,
|
||||
"-flag3": PredictSet("opt1", "opt2", "opt12"),
|
||||
},
|
||||
Args: Predicate(PredictDirs("*")).Or(PredictFiles("*.md")),
|
||||
Args: PredictOr(PredictDirs("*"), PredictFiles("*.md")),
|
||||
},
|
||||
},
|
||||
Flags: map[string]Predicate{
|
||||
Flags: map[string]Predictor{
|
||||
"-h": PredictNothing,
|
||||
"-global1": PredictAnything,
|
||||
"-o": PredictFiles("*.txt"),
|
||||
|
@ -176,7 +176,7 @@ func TestCompleter_Complete(t *testing.T) {
|
|||
os.Setenv(envComplete, tt.args)
|
||||
line, _ := getLine()
|
||||
|
||||
got := complete(c, newArgs(line))
|
||||
got := c.Predict(newArgs(line))
|
||||
|
||||
sort.Strings(tt.want)
|
||||
sort.Strings(got)
|
||||
|
|
|
@ -6,9 +6,11 @@ import "github.com/posener/complete"
|
|||
var (
|
||||
predictEllipsis = complete.PredictSet("./...")
|
||||
|
||||
goFilesOrPackages = complete.PredictFiles("*.go").
|
||||
Or(complete.PredictDirs("*")).
|
||||
Or(predictEllipsis)
|
||||
goFilesOrPackages = complete.PredictOr(
|
||||
complete.PredictFiles("*.go"),
|
||||
complete.PredictDirs("*"),
|
||||
predictEllipsis,
|
||||
)
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
|
@ -17,15 +17,16 @@ import (
|
|||
// and then all the relevant function names.
|
||||
// for test names use prefix of 'Test' or 'Example', and for benchmark
|
||||
// test names use 'Benchmark'
|
||||
func predictTest(funcPrefix ...string) complete.Predicate {
|
||||
return func(last string) []match.Matcher {
|
||||
func predictTest(funcPrefix ...string) complete.Predictor {
|
||||
return complete.PredictFunc(func(a complete.Args) (prediction []string) {
|
||||
tests := testNames(funcPrefix)
|
||||
options := make([]match.Matcher, len(tests))
|
||||
for i := range tests {
|
||||
options[i] = match.Prefix(tests[i])
|
||||
for _, t := range tests {
|
||||
if m := match.Prefix(t); m.Match(a.Last) {
|
||||
prediction = append(prediction, m.String())
|
||||
}
|
||||
}
|
||||
return options
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// get all test names in current directory
|
||||
|
|
|
@ -7,52 +7,70 @@ import (
|
|||
"github.com/posener/complete/match"
|
||||
)
|
||||
|
||||
// Predicate determines what terms can follow a command or a flag
|
||||
// It is used for auto completion, given last - the last word in the already
|
||||
// in the command line, what words can complete it.
|
||||
type Predicate func(last string) []match.Matcher
|
||||
|
||||
// Or unions two predicate functions, so that the result predicate
|
||||
// returns the union of their predication
|
||||
func (p Predicate) Or(other Predicate) Predicate {
|
||||
if p == nil {
|
||||
return other
|
||||
}
|
||||
if other == nil {
|
||||
return p
|
||||
}
|
||||
return func(last string) []match.Matcher { return append(p.predict(last), other.predict(last)...) }
|
||||
// Predictor implements a predict method, in which given
|
||||
// command line arguments returns a list of options it predicts.
|
||||
type Predictor interface {
|
||||
Predict(Args) []string
|
||||
}
|
||||
|
||||
func (p Predicate) predict(last string) []match.Matcher {
|
||||
// PredictOr unions two predicate functions, so that the result predicate
|
||||
// returns the union of their predication
|
||||
func PredictOr(predictors ...Predictor) Predictor {
|
||||
return PredictFunc(func(a Args) (prediction []string) {
|
||||
for _, p := range predictors {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
prediction = append(prediction, p.Predict(a)...)
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// PredictFunc determines what terms can follow a command or a flag
|
||||
// It is used for auto completion, given last - the last word in the already
|
||||
// in the command line, what words can complete it.
|
||||
type PredictFunc func(Args) []string
|
||||
|
||||
// Predict invokes the predict function and implements the Predictor interface
|
||||
func (p PredictFunc) Predict(a Args) []string {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return p(last)
|
||||
return p(a)
|
||||
}
|
||||
|
||||
// PredictNothing does not expect anything after.
|
||||
var PredictNothing Predicate
|
||||
var PredictNothing Predictor
|
||||
|
||||
// PredictAnything expects something, but nothing particular, such as a number
|
||||
// or arbitrary name.
|
||||
func PredictAnything(last string) []match.Matcher { return nil }
|
||||
var PredictAnything = PredictFunc(func(Args) []string { return nil })
|
||||
|
||||
// PredictSet expects specific set of terms, given in the options argument.
|
||||
func PredictSet(options ...string) Predicate {
|
||||
return func(last string) []match.Matcher {
|
||||
ret := make([]match.Matcher, len(options))
|
||||
for i := range options {
|
||||
ret[i] = match.Prefix(options[i])
|
||||
}
|
||||
return ret
|
||||
func PredictSet(options ...string) Predictor {
|
||||
p := predictSet{}
|
||||
for _, o := range options {
|
||||
p = append(p, match.Prefix(o))
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
type predictSet []match.Prefix
|
||||
|
||||
func (p predictSet) Predict(a Args) (prediction []string) {
|
||||
for _, m := range p {
|
||||
if m.Match(a.Last) {
|
||||
prediction = append(prediction, m.String())
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// PredictDirs will search for directories in the given started to be typed
|
||||
// path, if no path was started to be typed, it will complete to directories
|
||||
// in the current working directory.
|
||||
func PredictDirs(pattern string) Predicate {
|
||||
func PredictDirs(pattern string) Predictor {
|
||||
return files(pattern, true, false)
|
||||
}
|
||||
|
||||
|
@ -60,19 +78,19 @@ func PredictDirs(pattern string) Predicate {
|
|||
// be typed path, if no path was started to be typed, it will complete to files that
|
||||
// match the pattern in the current working directory.
|
||||
// To match any file, use "*" as pattern. To match go files use "*.go", and so on.
|
||||
func PredictFiles(pattern string) Predicate {
|
||||
func PredictFiles(pattern string) Predictor {
|
||||
return files(pattern, false, true)
|
||||
}
|
||||
|
||||
// PredictFilesOrDirs predict any file or directory that matches the pattern
|
||||
func PredictFilesOrDirs(pattern string) Predicate {
|
||||
// PredictFilesOrDirs any file or directory that matches the pattern
|
||||
func PredictFilesOrDirs(pattern string) Predictor {
|
||||
return files(pattern, true, true)
|
||||
}
|
||||
|
||||
func files(pattern string, allowDirs, allowFiles bool) Predicate {
|
||||
return func(last string) []match.Matcher {
|
||||
dir := dirFromLast(last)
|
||||
Log("looking for files in %s (last=%s)", dir, last)
|
||||
func files(pattern string, allowDirs, allowFiles bool) PredictFunc {
|
||||
return func(a Args) (prediction []string) {
|
||||
dir := dirFromLast(a.Last)
|
||||
Log("looking for files in %s (last=%s)", dir, a.Last)
|
||||
files, err := filepath.Glob(filepath.Join(dir, pattern))
|
||||
if err != nil {
|
||||
Log("failed glob operation with pattern '%s': %s", pattern, err)
|
||||
|
@ -84,7 +102,13 @@ func files(pattern string, allowDirs, allowFiles bool) Predicate {
|
|||
if !filepath.IsAbs(pattern) {
|
||||
filesToRel(files)
|
||||
}
|
||||
return filesToMatchers(files)
|
||||
// add all matching files to prediction
|
||||
for _, f := range files {
|
||||
if m := match.File(f); m.Match(a.Last) {
|
||||
prediction = append(prediction, m.String())
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -130,14 +154,6 @@ func filesToRel(files []string) {
|
|||
return
|
||||
}
|
||||
|
||||
func filesToMatchers(files []string) []match.Matcher {
|
||||
options := make([]match.Matcher, len(files))
|
||||
for i, f := range files {
|
||||
options[i] = match.File(f)
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
// dirFromLast gives the directory of the current written
|
||||
// last argument if it represents a file name being written.
|
||||
// in case that it is not, we fall back to the current directory.
|
|
@ -12,7 +12,7 @@ func TestPredicate(t *testing.T) {
|
|||
|
||||
tests := []struct {
|
||||
name string
|
||||
p Predicate
|
||||
p Predictor
|
||||
arg string
|
||||
want []string
|
||||
}{
|
||||
|
@ -37,29 +37,24 @@ func TestPredicate(t *testing.T) {
|
|||
p: PredictAnything,
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
name: "nothing",
|
||||
p: PredictNothing,
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
name: "or: word with nil",
|
||||
p: PredictSet("a").Or(PredictNothing),
|
||||
p: PredictOr(PredictSet("a"), nil),
|
||||
want: []string{"a"},
|
||||
},
|
||||
{
|
||||
name: "or: nil with word",
|
||||
p: PredictNothing.Or(PredictSet("a")),
|
||||
p: PredictOr(nil, PredictSet("a")),
|
||||
want: []string{"a"},
|
||||
},
|
||||
{
|
||||
name: "or: nil with nil",
|
||||
p: PredictNothing.Or(PredictNothing),
|
||||
p: PredictOr(PredictNothing, PredictNothing),
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
name: "or: word with word with word",
|
||||
p: PredictSet("a").Or(PredictSet("b")).Or(PredictSet("c")),
|
||||
p: PredictOr(PredictSet("a"), PredictSet("b"), PredictSet("c")),
|
||||
want: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
|
@ -118,18 +113,12 @@ func TestPredicate(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name+"?arg='"+tt.arg+"'", func(t *testing.T) {
|
||||
|
||||
matchers := tt.p.predict(tt.arg)
|
||||
matches := tt.p.Predict(newArgs(strings.Split(tt.arg, " ")))
|
||||
|
||||
matchersString := []string{}
|
||||
for _, m := range matchers {
|
||||
if m.Match(tt.arg) {
|
||||
matchersString = append(matchersString, m.String())
|
||||
}
|
||||
}
|
||||
sort.Strings(matchersString)
|
||||
sort.Strings(matches)
|
||||
sort.Strings(tt.want)
|
||||
|
||||
got := strings.Join(matchersString, ",")
|
||||
got := strings.Join(matches, ",")
|
||||
want := strings.Join(tt.want, ",")
|
||||
|
||||
if got != want {
|
Loading…
Reference in New Issue