diff --git a/args.go b/args.go index bb45d1c..86dd41a 100644 --- a/args.go +++ b/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 } diff --git a/command.go b/command.go index 6e4a773..4f2207a 100644 --- a/command.go +++ b/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 } diff --git a/complete.go b/complete.go index 2780e62..be0876e 100644 --- a/complete.go +++ b/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 == "" { diff --git a/complete_test.go b/complete_test.go index 0079c30..ee5a133 100644 --- a/complete_test.go +++ b/complete_test.go @@ -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) diff --git a/gocomplete/complete.go b/gocomplete/complete.go index 75d3672..1575e1b 100644 --- a/gocomplete/complete.go +++ b/gocomplete/complete.go @@ -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() { diff --git a/gocomplete/tests.go b/gocomplete/tests.go index 40210d4..4be3f0d 100644 --- a/gocomplete/tests.go +++ b/gocomplete/tests.go @@ -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 diff --git a/predicate.go b/predict.go similarity index 60% rename from predicate.go rename to predict.go index bb7e8cb..d5287c9 100644 --- a/predicate.go +++ b/predict.go @@ -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. diff --git a/predicate_test.go b/predict_test.go similarity index 78% rename from predicate_test.go rename to predict_test.go index 1a694a1..6b77fbe 100644 --- a/predicate_test.go +++ b/predict_test.go @@ -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 {