diff --git a/args.go b/args.go index 0d2b94d..1fe090a 100644 --- a/args.go +++ b/args.go @@ -1,5 +1,10 @@ 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) @@ -18,6 +23,21 @@ type Args struct { 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/gocomplete/complete.go b/gocomplete/complete.go index 1575e1b..4b4da67 100644 --- a/gocomplete/complete.go +++ b/gocomplete/complete.go @@ -4,19 +4,18 @@ package main import "github.com/posener/complete" var ( - predictEllipsis = complete.PredictSet("./...") - - goFilesOrPackages = complete.PredictOr( - complete.PredictFiles("*.go"), - complete.PredictDirs("*"), - predictEllipsis, - ) + ellipsis = complete.PredictSet("./...") + mainPackages = predictPackages("main") + 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 +34,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 +77,7 @@ func main() { "-outputdir": complete.PredictDirs("*"), "-trace": complete.PredictFiles("*.out"), }, - Args: goFilesOrPackages, + Args: anyGo, } fmt := complete.Command{ @@ -86,7 +85,7 @@ func main() { "-n": complete.PredictNothing, "-x": complete.PredictNothing, }, - Args: goFilesOrPackages, + Args: anyGo, } get := complete.Command{ @@ -98,7 +97,7 @@ func main() { "-t": complete.PredictNothing, "-u": complete.PredictNothing, }, - Args: goFilesOrPackages, + Args: anyGo, } generate := complete.Command{ @@ -108,7 +107,7 @@ func main() { "-v": complete.PredictNothing, "-run": complete.PredictAnything, }, - Args: goFilesOrPackages, + Args: anyGo, } vet := complete.Command{ @@ -116,7 +115,7 @@ func main() { "-n": complete.PredictNothing, "-x": complete.PredictNothing, }, - Args: complete.PredictDirs("*"), + Args: anyGo, } list := complete.Command{ @@ -125,7 +124,7 @@ func main() { "-f": complete.PredictAnything, "-json": complete.PredictNothing, }, - Args: complete.PredictDirs("*"), + Args: complete.PredictOr(anyPackage, ellipsis), } tool := complete.Command{ @@ -142,7 +141,7 @@ func main() { "-n": complete.PredictNothing, "-x": complete.PredictNothing, }, - Args: complete.PredictDirs("*"), + Args: complete.PredictOr(anyPackage, ellipsis), } env := complete.Command{ @@ -153,7 +152,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 fe70c97..0543442 100644 --- a/predict_files.go +++ b/predict_files.go @@ -30,10 +30,9 @@ func files(pattern string, allowFiles bool) PredictFunc { // if only one directory has matched the result, search recursively into // this directory to give more results. return func(a Args) (prediction []string) { - last := a.Last for { - prediction = predictFiles(last, pattern, allowFiles) + 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. @@ -43,7 +42,7 @@ func files(pattern string, allowFiles bool) PredictFunc { // if the result is only one item, we might want to recursively check // for more accurate results. - if prediction[0] == last { // avoid loop forever + if prediction[0] == a.Last { // avoid loop forever return } @@ -52,56 +51,59 @@ func files(pattern string, allowFiles bool) PredictFunc { return } - last = prediction[0] + 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 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() { @@ -109,6 +111,7 @@ func listFiles(dir, pattern string) []string { } } } + list := make([]string, 0, len(m)) for k := range m { list = append(list, k) @@ -117,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 } @@ -134,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 5e3e5d4..9fe22a9 100644 --- a/predict_test.go +++ b/predict_test.go @@ -114,7 +114,7 @@ func TestPredicate(t *testing.T) { } for _, arg := range tt.argList { - t.Run(tt.name+"?arg='"+arg+"'", func(t *testing.T) { + t.Run(tt.name+"/arg="+arg, func(t *testing.T) { matches := tt.p.Predict(newArgs(strings.Split(arg, " ")))