diff --git a/args.go b/args.go index 6d4fea6..1fe090a 100644 --- a/args.go +++ b/args.go @@ -1,23 +1,43 @@ package complete +import ( + "os" + "path/filepath" +) + // Args describes command line arguments type Args struct { // All lists of all arguments in command line (not including the command itself) - All []string + All []string // Completed lists of all completed arguments in command line, // If the last one is still being typed - no space after it, // it won't appear in this list of arguments. - Completed []string + Completed []string // Last argument in command line, the one being typed, if the last // character in the command line is a space, this argument will be empty, // otherwise this would be the last word. - Last string + Last string // LastCompleted is the last argument that was fully typed. // If the last character in the command line is space, this would be the // last word, otherwise, it would be the word before that. LastCompleted string } +// Directory 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 (a Args) Directory() string { + if info, err := os.Stat(a.Last); err == nil && info.IsDir() { + return a.Last + } + dir := filepath.Dir(a.Last) + _, err := os.Stat(dir) + if err != nil { + return "./" + } + return dir +} + func newArgs(line []string) Args { completed := removeLast(line) return Args{ diff --git a/complete_test.go b/complete_test.go index c9d544b..5cc4be0 100644 --- a/complete_test.go +++ b/complete_test.go @@ -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/", "./outer/", "./readme.md", "-flag2", "-flag3", "-h", "-global1", "-o"}, }, { args: "sub2 ./", - want: []string{"./", "./readme.md", "./dir/"}, + want: []string{"./", "./readme.md", "./dir/", "./outer/"}, }, { 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/", "./outer/", "./readme.md", "-flag2", "-flag3", "-h", "-global1", "-o"}, }, { args: "sub1 -fl", @@ -129,7 +129,7 @@ func TestCompleter_Complete(t *testing.T) { }, { args: "-o ", - want: append(testTXTFiles, "./", "./dir/"), + want: append(testTXTFiles, "./", "./dir/", "./outer/"), }, { args: "-o ./no-su", @@ -137,7 +137,7 @@ func TestCompleter_Complete(t *testing.T) { }, { args: "-o ./", - want: append(testTXTFiles, "./", "./dir/"), + want: append(testTXTFiles, "./", "./dir/", "./outer/"), }, { args: "-o ./read", diff --git a/gocomplete/complete.go b/gocomplete/complete.go index 1575e1b..e8ecacd 100644 --- a/gocomplete/complete.go +++ b/gocomplete/complete.go @@ -4,19 +4,17 @@ package main import "github.com/posener/complete" var ( - predictEllipsis = complete.PredictSet("./...") - - goFilesOrPackages = complete.PredictOr( - complete.PredictFiles("*.go"), - complete.PredictDirs("*"), - predictEllipsis, - ) + ellipsis = complete.PredictSet("./...") + anyPackage = predictPackages("") + goFiles = complete.PredictFiles("*.go") + anyFile = complete.PredictFiles("*") + anyGo = complete.PredictOr(goFiles, anyPackage, ellipsis) ) func main() { build := complete.Command{ Flags: complete.Flags{ - "-o": complete.PredictFiles("*"), + "-o": anyFile, "-i": complete.PredictNothing, "-a": complete.PredictNothing, @@ -35,18 +33,18 @@ func main() { "-installsuffix": complete.PredictAnything, "-ldflags": complete.PredictAnything, "-linkshared": complete.PredictNothing, - "-pkgdir": complete.PredictDirs("*"), + "-pkgdir": anyPackage, "-tags": complete.PredictAnything, "-toolexec": complete.PredictAnything, }, - Args: goFilesOrPackages, + Args: anyGo, } run := complete.Command{ Flags: complete.Flags{ "-exec": complete.PredictAnything, }, - Args: complete.PredictFiles("*.go"), + Args: goFiles, } test := complete.Command{ @@ -78,7 +76,7 @@ func main() { "-outputdir": complete.PredictDirs("*"), "-trace": complete.PredictFiles("*.out"), }, - Args: goFilesOrPackages, + Args: anyGo, } fmt := complete.Command{ @@ -86,7 +84,7 @@ func main() { "-n": complete.PredictNothing, "-x": complete.PredictNothing, }, - Args: goFilesOrPackages, + Args: anyGo, } get := complete.Command{ @@ -98,7 +96,7 @@ func main() { "-t": complete.PredictNothing, "-u": complete.PredictNothing, }, - Args: goFilesOrPackages, + Args: anyGo, } generate := complete.Command{ @@ -108,7 +106,7 @@ func main() { "-v": complete.PredictNothing, "-run": complete.PredictAnything, }, - Args: goFilesOrPackages, + Args: anyGo, } vet := complete.Command{ @@ -116,7 +114,7 @@ func main() { "-n": complete.PredictNothing, "-x": complete.PredictNothing, }, - Args: complete.PredictDirs("*"), + Args: anyGo, } list := complete.Command{ @@ -125,7 +123,7 @@ func main() { "-f": complete.PredictAnything, "-json": complete.PredictNothing, }, - Args: complete.PredictDirs("*"), + Args: complete.PredictOr(anyPackage, ellipsis), } tool := complete.Command{ @@ -142,7 +140,7 @@ func main() { "-n": complete.PredictNothing, "-x": complete.PredictNothing, }, - Args: complete.PredictDirs("*"), + Args: complete.PredictOr(anyPackage, ellipsis), } env := complete.Command{ @@ -153,7 +151,7 @@ func main() { version := complete.Command{} fix := complete.Command{ - Args: complete.PredictDirs("*"), + Args: anyGo, } // commands that also accepts the build flags diff --git a/gocomplete/pkgs.go b/gocomplete/pkgs.go new file mode 100644 index 0000000..b223ea9 --- /dev/null +++ b/gocomplete/pkgs.go @@ -0,0 +1,50 @@ +package main + +import ( + "bytes" + "encoding/json" + "os/exec" + "strings" + + "github.com/posener/complete" +) + +const goListFormat = `'{"name": "{{.Name}}", "dir": "{{.Dir}}"}'` + +func predictPackages(packageName string) complete.Predictor { + return complete.PredictFunc(func(a complete.Args) (prediction []string) { + dir := a.Directory() + dir = strings.TrimRight(dir, "/.") + "/..." + + pkgs := listPackages(dir) + + files := make([]string, 0, len(pkgs)) + for _, p := range pkgs { + if packageName != "" && p.Name != packageName { + continue + } + files = append(files, p.Path) + } + return complete.PredictFilesSet(files).Predict(a) + }) +} + +type pack struct { + Name string + Path string +} + +func listPackages(dir string) (pkgs []pack) { + out, err := exec.Command("go", "list", "-f", goListFormat, dir).Output() + if err != nil { + return + } + lines := bytes.Split(out, []byte("\n")) + for _, line := range lines { + var p pack + if err := json.Unmarshal(line, &p); err == nil { + pkgs = append(pkgs, p) + } + } + return +} diff --git a/predict_files.go b/predict_files.go index 8ad5368..5f83e77 100644 --- a/predict_files.go +++ b/predict_files.go @@ -25,64 +25,93 @@ func PredictFiles(pattern string) Predictor { } func files(pattern string, allowFiles bool) PredictFunc { + + // search for files according to arguments, + // if only one directory has matched the result, search recursively into + // this directory to give more results. return func(a Args) (prediction []string) { - prediction = predictFiles(a.Last, pattern, allowFiles) - return + for { + + prediction = predictFiles(a, pattern, allowFiles) + + // if the number of prediction is not 1, we either have many results or + // have no results, so we return it. + if len(prediction) != 1 { + return + } + + // if the result is only one item, we might want to recursively check + // for more accurate results. + if prediction[0] == a.Last { // avoid loop forever + return + } + + // only try deeper, if the one item is a directory + if stat, err := os.Stat(prediction[0]); err != nil || !stat.IsDir() { + return + } + + a.Last = prediction[0] + } } } -func predictFiles(last string, pattern string, allowFiles bool) (prediction []string) { - if strings.HasSuffix(last, "/..") { - return +func predictFiles(a Args, pattern string, allowFiles bool) []string { + if strings.HasSuffix(a.Last, "/..") { + return nil } - dir := dirFromLast(last) - rel := !filepath.IsAbs(pattern) - files := listFiles(dir, pattern) - - // get wording directory for relative name - workDir, err := os.Getwd() - if err != nil { - workDir = "" - } + dir := a.Directory() + files := listFiles(dir, pattern, allowFiles) // add dir if match files = append(files, dir) - // add all matching files to prediction - for _, f := range files { - if stat, err := os.Stat(f); err != nil || (!stat.IsDir() && !allowFiles) { - continue - } - - // change file name to relative if necessary - if rel && workDir != "" { - f = toRel(workDir, f) - } - - // test matching of file to the argument - if match.File(f, last) { - prediction = append(prediction, f) - } - } - return - + return PredictFilesSet(files).Predict(a) } -func listFiles(dir, pattern string) []string { +// PredictFilesSet predict according to file rules to a given set of file names +func PredictFilesSet(files []string) PredictFunc { + return func(a Args) (prediction []string) { + rel := !filepath.IsAbs(a.Directory()) + // add all matching files to prediction + for _, f := range files { + // change file name to relative if necessary + if rel { + f = toRel(f) + } + + // test matching of file to the argument + if match.File(f, a.Last) { + prediction = append(prediction, f) + } + } + return + } +} + +func listFiles(dir, pattern string, allowFiles bool) []string { + // set of all file names m := map[string]bool{} + + // list files if files, err := filepath.Glob(filepath.Join(dir, pattern)); err == nil { for _, f := range files { - m[f] = true - } - } - if dirs, err := ioutil.ReadDir(dir); err == nil { - for _, d := range dirs { - if d.IsDir() { - m[d.Name()] = true + if stat, err := os.Stat(f); err != nil || stat.IsDir() || allowFiles { + m[f] = true } } } + + // list directories + if dirs, err := ioutil.ReadDir(dir); err == nil { + for _, d := range dirs { + if d.IsDir() { + m[filepath.Join(dir, d.Name())] = true + } + } + } + list := make([]string, 0, len(m)) for k := range m { list = append(list, k) @@ -91,12 +120,18 @@ func listFiles(dir, pattern string) []string { } // toRel changes a file name to a relative name -func toRel(wd, file string) string { +func toRel(file string) string { + // get wording directory for relative name + workDir, err := os.Getwd() + if err != nil { + return file + } + abs, err := filepath.Abs(file) if err != nil { return file } - rel, err := filepath.Rel(wd, abs) + rel, err := filepath.Rel(workDir, abs) if err != nil { return file } @@ -108,18 +143,3 @@ func toRel(wd, file string) string { } return rel } - -// 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 index ebe8aa1..b2840c0 100644 --- a/predict_test.go +++ b/predict_test.go @@ -11,10 +11,10 @@ func TestPredicate(t *testing.T) { initTests() tests := []struct { - name string - p Predictor - arg string - want []string + name string + p Predictor + argList []string + want []string }{ { name: "set", @@ -22,10 +22,10 @@ func TestPredicate(t *testing.T) { want: []string{"a", "b", "c"}, }, { - name: "set with does", - p: PredictSet("./..", "./x"), - arg: "./.", - want: []string{"./.."}, + name: "set with does", + p: PredictSet("./..", "./x"), + argList: []string{"./.", "./.."}, + want: []string{"./.."}, }, { name: "set/empty", @@ -60,70 +60,86 @@ func TestPredicate(t *testing.T) { { name: "files/txt", p: PredictFiles("*.txt"), - want: []string{"./", "./dir/", "./a.txt", "./b.txt", "./c.txt", "./.dot.txt"}, + want: []string{"./", "./dir/", "./outer/", "./a.txt", "./b.txt", "./c.txt", "./.dot.txt"}, }, { - name: "files/txt", - p: PredictFiles("*.txt"), - arg: "./dir/", - want: []string{"./dir/"}, + name: "files/txt", + p: PredictFiles("*.txt"), + argList: []string{"./dir/"}, + want: []string{"./dir/"}, }, { - name: "files/x", - p: PredictFiles("x"), - arg: "./dir/", - want: []string{"./dir/", "./dir/x"}, + name: "complete files inside dir if it is the only match", + p: PredictFiles("foo"), + argList: []string{"./dir/", "./d"}, + want: []string{"./dir/", "./dir/foo"}, }, { - name: "files/*", - p: PredictFiles("x*"), - arg: "./dir/", - want: []string{"./dir/", "./dir/x"}, + name: "complete files inside dir when argList includes file name", + p: PredictFiles("*"), + argList: []string{"./dir/f", "./dir/foo"}, + want: []string{"./dir/foo"}, }, { - name: "files/md", - p: PredictFiles("*.md"), - want: []string{"./", "./dir/", "./readme.md"}, + name: "files/md", + p: PredictFiles("*.md"), + argList: []string{"", ".", "./"}, + want: []string{"./", "./dir/", "./outer/", "./readme.md"}, }, { - name: "dirs", - p: PredictDirs("*"), - arg: "./dir/", - want: []string{"./dir/"}, + name: "dirs", + p: PredictDirs("*"), + argList: []string{"./dir/", "./di", "di", "dir", "dir/"}, + want: []string{"./dir/"}, }, { - name: "dirs and files", - p: PredictFiles("*"), - arg: "./dir", - want: []string{"./dir/", "./dir/x"}, + name: "predict anything in dir", + p: PredictFiles("*"), + argList: []string{"./dir", "dir", "./dir/", "./di"}, + want: []string{"./dir/", "./dir/foo", "./dir/bar"}, }, { - name: "dirs", - p: PredictDirs("*"), - want: []string{"./", "./dir/"}, + name: "root directories", + p: PredictDirs("*"), + argList: []string{"", ".", "./"}, + want: []string{"./", "./dir/", "./outer/"}, }, { - name: "subdir", - p: PredictFiles("*"), - arg: "./dir/", - want: []string{"./dir/", "./dir/x"}, + name: "nested directories", + p: PredictDirs("*.md"), + argList: []string{"ou", "./ou", "./outer", "./outer/"}, + want: []string{"./outer/", "./outer/inner/"}, + }, + { + name: "nested inner directory", + p: PredictFiles("*.md"), + argList: []string{"outer/i"}, + want: []string{"./outer/inner/", "./outer/inner/readme.md"}, }, } for _, tt := range tests { - t.Run(tt.name+"?arg='"+tt.arg+"'", func(t *testing.T) { - matches := tt.p.Predict(newArgs(strings.Split(tt.arg, " "))) + // no args in argList, means an empty argument + if len(tt.argList) == 0 { + tt.argList = append(tt.argList, "") + } - sort.Strings(matches) - sort.Strings(tt.want) + for _, arg := range tt.argList { + t.Run(tt.name+"/arg="+arg, func(t *testing.T) { - got := strings.Join(matches, ",") - want := strings.Join(tt.want, ",") + matches := tt.p.Predict(newArgs(strings.Split(arg, " "))) - if got != want { - t.Errorf("failed %s\ngot = %s\nwant: %s", t.Name(), got, want) - } - }) + 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) + } + }) + } } } diff --git a/tests/dir/x b/tests/dir/bar similarity index 100% rename from tests/dir/x rename to tests/dir/bar diff --git a/tests/dir/foo b/tests/dir/foo new file mode 100644 index 0000000..e69de29 diff --git a/tests/outer/inner/readme.md b/tests/outer/inner/readme.md new file mode 100644 index 0000000..e69de29