From 967bae76f3132c210e6275653f9b603593973858 Mon Sep 17 00:00:00 2001 From: Eyal Posener Date: Thu, 11 May 2017 20:28:31 +0300 Subject: Add Predictor interface --- args.go | 29 +++++---- command.go | 52 ++++++++------- complete.go | 16 +---- complete_test.go | 10 +-- gocomplete/complete.go | 8 ++- gocomplete/tests.go | 15 +++-- predicate.go | 154 -------------------------------------------- predicate_test.go | 140 ---------------------------------------- predict.go | 170 +++++++++++++++++++++++++++++++++++++++++++++++++ predict_test.go | 129 +++++++++++++++++++++++++++++++++++++ 10 files changed, 363 insertions(+), 360 deletions(-) delete mode 100644 predicate.go delete mode 100644 predicate_test.go create mode 100644 predict.go create mode 100644 predict_test.go 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/predicate.go deleted file mode 100644 index bb7e8cb..0000000 --- a/predicate.go +++ /dev/null @@ -1,154 +0,0 @@ -package complete - -import ( - "os" - "path/filepath" - - "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)...) } -} - -func (p Predicate) predict(last string) []match.Matcher { - if p == nil { - return nil - } - return p(last) -} - -// PredictNothing does not expect anything after. -var PredictNothing Predicate - -// PredictAnything expects something, but nothing particular, such as a number -// or arbitrary name. -func PredictAnything(last string) []match.Matcher { 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 - } -} - -// 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 { - return files(pattern, true, false) -} - -// PredictFiles will search for files matching the given pattern in the started to -// 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 { - return files(pattern, false, true) -} - -// PredictFilesOrDirs predict any file or directory that matches the pattern -func PredictFilesOrDirs(pattern string) Predicate { - 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) - files, err := filepath.Glob(filepath.Join(dir, pattern)) - if err != nil { - Log("failed glob operation with pattern '%s': %s", pattern, err) - } - if allowDirs { - files = append(files, dir) - } - files = selectByType(files, allowDirs, allowFiles) - if !filepath.IsAbs(pattern) { - filesToRel(files) - } - return filesToMatchers(files) - } -} - -func selectByType(names []string, allowDirs bool, allowFiles bool) []string { - filtered := make([]string, 0, len(names)) - for _, name := range names { - stat, err := os.Stat(name) - if err != nil { - continue - } - if (stat.IsDir() && !allowDirs) || (!stat.IsDir() && !allowFiles) { - continue - } - filtered = append(filtered, name) - } - return filtered -} - -// filesToRel, change list of files to their names in the relative -// to current directory form. -func filesToRel(files []string) { - wd, err := os.Getwd() - if err != nil { - return - } - for i := range files { - abs, err := filepath.Abs(files[i]) - if err != nil { - continue - } - rel, err := filepath.Rel(wd, abs) - if err != nil { - continue - } - if rel != "." { - rel = "./" + rel - } - if info, err := os.Stat(rel); err == nil && info.IsDir() { - rel += "/" - } - files[i] = rel - } - 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. -func dirFromLast(last string) string { - if info, err := os.Stat(last); err == nil && info.IsDir() { - return last - } - dir := filepath.Dir(last) - _, err := os.Stat(dir) - if err != nil { - return "./" - } - return dir -} diff --git a/predicate_test.go b/predicate_test.go deleted file mode 100644 index 1a694a1..0000000 --- a/predicate_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package complete - -import ( - "sort" - "strings" - "testing" -) - -func TestPredicate(t *testing.T) { - t.Parallel() - initTests() - - tests := []struct { - name string - p Predicate - arg string - want []string - }{ - { - name: "set", - p: PredictSet("a", "b", "c"), - want: []string{"a", "b", "c"}, - }, - { - name: "set with does", - p: PredictSet("./..", "./x"), - arg: "./.", - want: []string{"./.."}, - }, - { - name: "set/empty", - p: PredictSet(), - want: []string{}, - }, - { - name: "anything", - p: PredictAnything, - want: []string{}, - }, - { - name: "nothing", - p: PredictNothing, - want: []string{}, - }, - { - name: "or: word with nil", - p: PredictSet("a").Or(PredictNothing), - want: []string{"a"}, - }, - { - name: "or: nil with word", - p: PredictNothing.Or(PredictSet("a")), - want: []string{"a"}, - }, - { - name: "or: nil with nil", - p: PredictNothing.Or(PredictNothing), - want: []string{}, - }, - { - name: "or: word with word with word", - p: PredictSet("a").Or(PredictSet("b")).Or(PredictSet("c")), - want: []string{"a", "b", "c"}, - }, - { - name: "files/txt", - p: PredictFiles("*.txt"), - want: []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt"}, - }, - { - name: "files/txt", - p: PredictFiles("*.txt"), - arg: "./dir/", - want: []string{}, - }, - { - name: "files/x", - p: PredictFiles("x"), - arg: "./dir/", - want: []string{"./dir/x"}, - }, - { - name: "files/*", - p: PredictFiles("x*"), - arg: "./dir/", - want: []string{"./dir/x"}, - }, - { - name: "files/md", - p: PredictFiles("*.md"), - want: []string{"./readme.md"}, - }, - { - name: "dirs", - p: PredictDirs("*"), - arg: "./dir/", - want: []string{"./dir/"}, - }, - { - name: "dirs and files", - p: PredictFilesOrDirs("*"), - arg: "./dir", - want: []string{"./dir/", "./dir/x"}, - }, - { - name: "dirs", - p: PredictDirs("*"), - want: []string{"./", "./dir/"}, - }, - { - name: "subdir", - p: PredictFiles("*"), - arg: "./dir/", - want: []string{"./dir/x"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name+"?arg='"+tt.arg+"'", func(t *testing.T) { - - matchers := tt.p.predict(tt.arg) - - matchersString := []string{} - for _, m := range matchers { - if m.Match(tt.arg) { - matchersString = append(matchersString, m.String()) - } - } - sort.Strings(matchersString) - sort.Strings(tt.want) - - got := strings.Join(matchersString, ",") - want := strings.Join(tt.want, ",") - - if got != want { - t.Errorf("failed %s\ngot = %s\nwant: %s", t.Name(), got, want) - } - }) - } -} diff --git a/predict.go b/predict.go new file mode 100644 index 0000000..d5287c9 --- /dev/null +++ b/predict.go @@ -0,0 +1,170 @@ +package complete + +import ( + "os" + "path/filepath" + + "github.com/posener/complete/match" +) + +// Predictor implements a predict method, in which given +// command line arguments returns a list of options it predicts. +type Predictor interface { + Predict(Args) []string +} + +// 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(a) +} + +// PredictNothing does not expect anything after. +var PredictNothing Predictor + +// PredictAnything expects something, but nothing particular, such as a number +// or arbitrary name. +var PredictAnything = PredictFunc(func(Args) []string { return nil }) + +// PredictSet expects specific set of terms, given in the options argument. +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) Predictor { + return files(pattern, true, false) +} + +// PredictFiles will search for files matching the given pattern in the started to +// 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) Predictor { + return files(pattern, false, true) +} + +// 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) 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) + } + if allowDirs { + files = append(files, dir) + } + files = selectByType(files, allowDirs, allowFiles) + if !filepath.IsAbs(pattern) { + filesToRel(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 + } +} + +func selectByType(names []string, allowDirs bool, allowFiles bool) []string { + filtered := make([]string, 0, len(names)) + for _, name := range names { + stat, err := os.Stat(name) + if err != nil { + continue + } + if (stat.IsDir() && !allowDirs) || (!stat.IsDir() && !allowFiles) { + continue + } + filtered = append(filtered, name) + } + return filtered +} + +// filesToRel, change list of files to their names in the relative +// to current directory form. +func filesToRel(files []string) { + wd, err := os.Getwd() + if err != nil { + return + } + for i := range files { + abs, err := filepath.Abs(files[i]) + if err != nil { + continue + } + rel, err := filepath.Rel(wd, abs) + if err != nil { + continue + } + if rel != "." { + rel = "./" + rel + } + if info, err := os.Stat(rel); err == nil && info.IsDir() { + rel += "/" + } + files[i] = rel + } + return +} + +// 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. +func dirFromLast(last string) string { + if info, err := os.Stat(last); err == nil && info.IsDir() { + return last + } + dir := filepath.Dir(last) + _, err := os.Stat(dir) + if err != nil { + return "./" + } + return dir +} diff --git a/predict_test.go b/predict_test.go new file mode 100644 index 0000000..6b77fbe --- /dev/null +++ b/predict_test.go @@ -0,0 +1,129 @@ +package complete + +import ( + "sort" + "strings" + "testing" +) + +func TestPredicate(t *testing.T) { + t.Parallel() + initTests() + + tests := []struct { + name string + p Predictor + arg string + want []string + }{ + { + name: "set", + p: PredictSet("a", "b", "c"), + want: []string{"a", "b", "c"}, + }, + { + name: "set with does", + p: PredictSet("./..", "./x"), + arg: "./.", + want: []string{"./.."}, + }, + { + name: "set/empty", + p: PredictSet(), + want: []string{}, + }, + { + name: "anything", + p: PredictAnything, + want: []string{}, + }, + { + name: "or: word with nil", + p: PredictOr(PredictSet("a"), nil), + want: []string{"a"}, + }, + { + name: "or: nil with word", + p: PredictOr(nil, PredictSet("a")), + want: []string{"a"}, + }, + { + name: "or: nil with nil", + p: PredictOr(PredictNothing, PredictNothing), + want: []string{}, + }, + { + name: "or: word with word with word", + p: PredictOr(PredictSet("a"), PredictSet("b"), PredictSet("c")), + want: []string{"a", "b", "c"}, + }, + { + name: "files/txt", + p: PredictFiles("*.txt"), + want: []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt"}, + }, + { + name: "files/txt", + p: PredictFiles("*.txt"), + arg: "./dir/", + want: []string{}, + }, + { + name: "files/x", + p: PredictFiles("x"), + arg: "./dir/", + want: []string{"./dir/x"}, + }, + { + name: "files/*", + p: PredictFiles("x*"), + arg: "./dir/", + want: []string{"./dir/x"}, + }, + { + name: "files/md", + p: PredictFiles("*.md"), + want: []string{"./readme.md"}, + }, + { + name: "dirs", + p: PredictDirs("*"), + arg: "./dir/", + want: []string{"./dir/"}, + }, + { + name: "dirs and files", + p: PredictFilesOrDirs("*"), + arg: "./dir", + want: []string{"./dir/", "./dir/x"}, + }, + { + name: "dirs", + p: PredictDirs("*"), + want: []string{"./", "./dir/"}, + }, + { + name: "subdir", + p: PredictFiles("*"), + arg: "./dir/", + want: []string{"./dir/x"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name+"?arg='"+tt.arg+"'", func(t *testing.T) { + + matches := tt.p.Predict(newArgs(strings.Split(tt.arg, " "))) + + sort.Strings(matches) + sort.Strings(tt.want) + + got := strings.Join(matches, ",") + want := strings.Join(tt.want, ",") + + if got != want { + t.Errorf("failed %s\ngot = %s\nwant: %s", t.Name(), got, want) + } + }) + } +} -- cgit v1.2.3