diff --git a/command.go b/command.go index f64e225..f52d175 100644 --- a/command.go +++ b/command.go @@ -33,11 +33,13 @@ func (c *Command) options(args []string) (options []match.Matcher, only bool) { // remove the first argument, which is the command name args = args[1:] - last := last(args) - // if prev has something that needs to follow it, + wordCurrent := last(args) + wordCompleted := last(removeLast(args)) + // if wordCompleted has something that needs to follow it, // it is the most relevant completion - if predicate, ok := c.Flags[last]; ok && predicate != nil { - return predicate.predict(last), true + if predicate, ok := c.Flags[wordCompleted]; ok && predicate != nil { + Log("Predicting according to flag %s", wordCurrent) + return predicate.predict(wordCurrent), true } sub, options, only := c.searchSub(args) @@ -57,7 +59,7 @@ func (c *Command) options(args []string) (options []match.Matcher, only bool) { } // add additional expected argument of the command - options = append(options, c.Args.predict(last)...) + options = append(options, c.Args.predict(wordCurrent)...) return } @@ -65,7 +67,12 @@ func (c *Command) options(args []string) (options []match.Matcher, only bool) { // searchSub searches recursively within sub commands if the sub command appear // in the on of the arguments. func (c *Command) searchSub(args []string) (sub string, all []match.Matcher, only bool) { - for i, arg := range args { + + // search for sub command in all arguments except the last one + // because that one might not be completed yet + searchArgs := removeLast(args) + + for i, arg := range searchArgs { if cmd, ok := c.Sub[arg]; ok { sub = arg all, only = cmd.options(args[i:]) @@ -83,3 +90,10 @@ func (c *Command) subCommands() []match.Matcher { } return subs } + +func removeLast(a []string) []string { + if len(a) > 0 { + return a[:len(a)-1] + } + return a +} diff --git a/complete.go b/complete.go index 925c9a2..c91bf5f 100644 --- a/complete.go +++ b/complete.go @@ -59,11 +59,12 @@ func (c *Complete) Run() bool { // complete get a command an command line arguments and returns // matching completion options func complete(c Command, args []string) (matching []string) { - options, _ := c.options(args[:len(args)-1]) + options, _ := c.options(args) // choose only matching options l := last(args) for _, option := range options { + Log("option %T, %s -> %t", option, option, option.Match(l)) if option.Match(l) { matching = append(matching, option.String()) } @@ -87,6 +88,7 @@ func last(args []string) (last string) { } func output(options []string) { + Log("") // stdout of program defines the complete options for _, option := range options { fmt.Println(option) diff --git a/complete_test.go b/complete_test.go index 147a361..282a2f6 100644 --- a/complete_test.go +++ b/complete_test.go @@ -23,7 +23,7 @@ func TestCompleter_Complete(t *testing.T) { "-flag2": PredictNothing, "-flag3": PredictSet("opt1", "opt2", "opt12"), }, - Args: Predicate(PredictDirs).Or(PredictFiles("*.md")), + Args: Predicate(PredictDirs("*")).Or(PredictFiles("*.md")), }, }, Flags: map[string]Predicate{ @@ -41,7 +41,7 @@ func TestCompleter_Complete(t *testing.T) { allGlobals = append(allGlobals, flag) } - testTXTFiles := []string{"./a.txt", "./b.txt", "./c.txt"} + testTXTFiles := []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt"} tests := []struct { args string @@ -81,11 +81,11 @@ func TestCompleter_Complete(t *testing.T) { }, { args: "sub2 ", - want: []string{"./", "./dir", "./readme.md", "-flag2", "-flag3", "-h", "-global1", "-o"}, + want: []string{"./", "./dir/", "./readme.md", "-flag2", "-flag3", "-h", "-global1", "-o"}, }, { args: "sub2 ./", - want: []string{"./", "./readme.md", "./dir"}, + want: []string{"./", "./readme.md", "./dir/"}, }, { args: "sub2 re", @@ -93,7 +93,7 @@ func TestCompleter_Complete(t *testing.T) { }, { args: "sub2 -flag2 ", - want: []string{"./", "./dir", "./readme.md", "-flag2", "-flag3", "-h", "-global1", "-o"}, + want: []string{"./", "./dir/", "./readme.md", "-flag2", "-flag3", "-h", "-global1", "-o"}, }, { args: "sub1 -fl", diff --git a/gocomplete/complete.go b/gocomplete/complete.go index ac5f5ed..75d3672 100644 --- a/gocomplete/complete.go +++ b/gocomplete/complete.go @@ -7,7 +7,7 @@ var ( predictEllipsis = complete.PredictSet("./...") goFilesOrPackages = complete.PredictFiles("*.go"). - Or(complete.PredictDirs). + Or(complete.PredictDirs("*")). Or(predictEllipsis) ) @@ -33,7 +33,7 @@ func main() { "-installsuffix": complete.PredictAnything, "-ldflags": complete.PredictAnything, "-linkshared": complete.PredictNothing, - "-pkgdir": complete.PredictDirs, + "-pkgdir": complete.PredictDirs("*"), "-tags": complete.PredictAnything, "-toolexec": complete.PredictAnything, }, @@ -58,7 +58,7 @@ func main() { "-count": complete.PredictAnything, "-cover": complete.PredictNothing, "-covermode": complete.PredictSet("set", "count", "atomic"), - "-coverpkg": complete.PredictDirs, + "-coverpkg": complete.PredictDirs("*"), "-cpu": complete.PredictAnything, "-run": predictTest("Test", "Example"), "-short": complete.PredictNothing, @@ -73,7 +73,7 @@ func main() { "-memprofilerate": complete.PredictAnything, "-mutexprofile": complete.PredictFiles("*.out"), "-mutexprofilefraction": complete.PredictAnything, - "-outputdir": complete.PredictDirs, + "-outputdir": complete.PredictDirs("*"), "-trace": complete.PredictFiles("*.out"), }, Args: goFilesOrPackages, @@ -114,7 +114,7 @@ func main() { "-n": complete.PredictNothing, "-x": complete.PredictNothing, }, - Args: complete.PredictDirs, + Args: complete.PredictDirs("*"), } list := complete.Command{ @@ -123,7 +123,7 @@ func main() { "-f": complete.PredictAnything, "-json": complete.PredictNothing, }, - Args: complete.PredictDirs, + Args: complete.PredictDirs("*"), } tool := complete.Command{ @@ -140,7 +140,7 @@ func main() { "-n": complete.PredictNothing, "-x": complete.PredictNothing, }, - Args: complete.PredictDirs, + Args: complete.PredictDirs("*"), } env := complete.Command{ @@ -151,7 +151,7 @@ func main() { version := complete.Command{} fix := complete.Command{ - Args: complete.PredictDirs, + Args: complete.PredictDirs("*"), } // commands that also accepts the build flags diff --git a/match/file.go b/match/file.go index c972ce0..0b554ce 100644 --- a/match/file.go +++ b/match/file.go @@ -1,7 +1,6 @@ package match import ( - "path/filepath" "strings" ) @@ -15,16 +14,13 @@ func (a File) String() string { // Match returns true if prefix's abs path prefixes a's abs path func (a File) Match(prefix string) bool { - full, err := filepath.Abs(string(a)) - if err != nil { - return false - } - prefixFull, err := filepath.Abs(prefix) - if err != nil { - return false + + // special case for current directory completion + if a == "./" && (prefix == "." || prefix == "") { + return true } - // if the file has the prefix as prefix, - // but we don't want to show too many files, so, if it is in a deeper directory - omit it. - return strings.HasPrefix(full, prefixFull) && (full == prefixFull || !strings.Contains(full[len(prefixFull)+1:], "/")) + cmp := strings.TrimPrefix(string(a), "./") + prefix = strings.TrimPrefix(prefix, "./") + return strings.HasPrefix(cmp, prefix) } diff --git a/match/match_test.go b/match/match_test.go index ae1ffea..d7a851a 100644 --- a/match/match_test.go +++ b/match/match_test.go @@ -45,6 +45,7 @@ func TestMatch(t *testing.T) { {prefix: "", want: true}, {prefix: "f", want: true}, {prefix: "./f", want: true}, + {prefix: "./.", want: false}, {prefix: "file.", want: true}, {prefix: "./file.", want: true}, {prefix: "file.txt", want: true}, @@ -54,6 +55,7 @@ func TestMatch(t *testing.T) { {prefix: "/file.txt", want: false}, {prefix: "/fil", want: false}, {prefix: "/file.txt2", want: false}, + {prefix: "/.", want: false}, }, }, { @@ -62,6 +64,7 @@ func TestMatch(t *testing.T) { {prefix: "", want: true}, {prefix: "f", want: true}, {prefix: "./f", want: true}, + {prefix: "./.", want: false}, {prefix: "file.", want: true}, {prefix: "./file.", want: true}, {prefix: "file.txt", want: true}, @@ -71,14 +74,16 @@ func TestMatch(t *testing.T) { {prefix: "/file.txt", want: false}, {prefix: "/fil", want: false}, {prefix: "/file.txt2", want: false}, + {prefix: "/.", want: false}, }, }, { m: File("/file.txt"), tests: []matcherTest{ - {prefix: "", want: false}, + {prefix: "", want: true}, {prefix: "f", want: false}, {prefix: "./f", want: false}, + {prefix: "./.", want: false}, {prefix: "file.", want: false}, {prefix: "./file.", want: false}, {prefix: "file.txt", want: false}, @@ -88,13 +93,23 @@ func TestMatch(t *testing.T) { {prefix: "/file.txt", want: true}, {prefix: "/fil", want: true}, {prefix: "/file.txt2", want: false}, + {prefix: "/.", want: false}, + }, + }, + { + m: File("./"), + tests: []matcherTest{ + {prefix: "", want: true}, + {prefix: ".", want: true}, + {prefix: "./", want: true}, + {prefix: "./.", want: false}, }, }, } for _, tt := range tests { for _, ttt := range tt.tests { - name := "matcher:" + tt.m.String() + "/prefix:" + ttt.prefix + name := "matcher='" + tt.m.String() + "'&prefix='" + ttt.prefix + "'" t.Run(name, func(t *testing.T) { got := tt.m.Match(ttt.prefix) if got != ttt.want { diff --git a/predicate.go b/predicate.go index 4f8588f..bb7e8cb 100644 --- a/predicate.go +++ b/predicate.go @@ -3,7 +3,6 @@ package complete import ( "os" "path/filepath" - "strings" "github.com/posener/complete/match" ) @@ -53,23 +52,8 @@ func PredictSet(options ...string) Predicate { // 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(last string) (options []match.Matcher) { - path := dirFromLast(last) - dirs := []string{} - filepath.Walk(path, func(path string, info os.FileInfo, err error) error { - if err != nil { - return nil - } - if info.IsDir() { - dirs = append(dirs, path) - } - return nil - }) - // if given path is not absolute, return relative paths - if !filepath.IsAbs(path) { - filesToRel(dirs) - } - return filesToMatchers(dirs) +func PredictDirs(pattern string) Predicate { + return files(pattern, true, false) } // PredictFiles will search for files matching the given pattern in the started to @@ -77,12 +61,26 @@ func PredictDirs(last string) (options []match.Matcher) { // 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) } @@ -90,6 +88,21 @@ func PredictFiles(pattern string) Predicate { } } +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) { @@ -106,12 +119,12 @@ func filesToRel(files []string) { if err != nil { continue } - if rel == "." { - rel = "" - } - if !strings.HasPrefix(rel, ".") { + if rel != "." { rel = "./" + rel } + if info, err := os.Stat(rel); err == nil && info.IsDir() { + rel += "/" + } files[i] = rel } return @@ -129,6 +142,9 @@ func filesToMatchers(files []string) []match.Matcher { // 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 { diff --git a/predicate_test.go b/predicate_test.go index 264e8d1..1a694a1 100644 --- a/predicate_test.go +++ b/predicate_test.go @@ -21,6 +21,12 @@ func TestPredicate(t *testing.T) { 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(), @@ -59,7 +65,7 @@ func TestPredicate(t *testing.T) { { name: "files/txt", p: PredictFiles("*.txt"), - want: []string{"./a.txt", "./b.txt", "./c.txt"}, + want: []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt"}, }, { name: "files/txt", @@ -86,23 +92,39 @@ func TestPredicate(t *testing.T) { }, { name: "dirs", - p: PredictDirs, + p: PredictDirs("*"), arg: "./dir/", - want: []string{"./dir"}, + want: []string{"./dir/"}, + }, + { + name: "dirs and files", + p: PredictFilesOrDirs("*"), + arg: "./dir", + want: []string{"./dir/", "./dir/x"}, }, { name: "dirs", - p: PredictDirs, - want: []string{"./", "./dir"}, + p: PredictDirs("*"), + want: []string{"./", "./dir/"}, + }, + { + name: "subdir", + p: PredictFiles("*"), + arg: "./dir/", + want: []string{"./dir/x"}, }, } for _, tt := range tests { - t.Run(tt.name+"/"+tt.arg, func(t *testing.T) { + t.Run(tt.name+"?arg='"+tt.arg+"'", func(t *testing.T) { + matchers := tt.p.predict(tt.arg) + matchersString := []string{} for _, m := range matchers { - matchersString = append(matchersString, m.String()) + if m.Match(tt.arg) { + matchersString = append(matchersString, m.String()) + } } sort.Strings(matchersString) sort.Strings(tt.want) @@ -111,7 +133,7 @@ func TestPredicate(t *testing.T) { want := strings.Join(tt.want, ",") if got != want { - t.Errorf("failed %s\ngot = %s\nwant: %s", tt.name, got, want) + t.Errorf("failed %s\ngot = %s\nwant: %s", t.Name(), got, want) } }) } diff --git a/tests/.dot.txt b/tests/.dot.txt new file mode 100644 index 0000000..e69de29