summaryrefslogtreecommitdiff
path: root/predict
diff options
context:
space:
mode:
Diffstat (limited to 'predict')
-rw-r--r--predict/files.go175
-rw-r--r--predict/files_test.go233
-rw-r--r--predict/predict.go34
-rw-r--r--predict/predict_test.go61
-rw-r--r--predict/testdata/.dot.txt0
-rw-r--r--predict/testdata/a.txt0
-rw-r--r--predict/testdata/b.txt0
-rw-r--r--predict/testdata/c.txt0
-rw-r--r--predict/testdata/dir/bar0
-rw-r--r--predict/testdata/dir/foo0
-rw-r--r--predict/testdata/outer/inner/readme.md0
-rw-r--r--predict/testdata/readme.md3
12 files changed, 506 insertions, 0 deletions
diff --git a/predict/files.go b/predict/files.go
new file mode 100644
index 0000000..4654ec4
--- /dev/null
+++ b/predict/files.go
@@ -0,0 +1,175 @@
+package predict
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+// Dirs returns a predictor that predict directory paths. If a non-empty pattern is given, the
+// predicted paths will match that pattern.
+func Dirs(pattern string) FilesPredictor {
+ return FilesPredictor{pattern: pattern, includeFiles: false}
+}
+
+// Dirs returns a predictor that predict file or directory paths. If a non-empty pattern is given,
+// the predicted paths will match that pattern.
+func Files(pattern string) FilesPredictor {
+ return FilesPredictor{pattern: pattern, includeFiles: true}
+}
+
+type FilesPredictor struct {
+ pattern string
+ includeFiles bool
+}
+
+// Predict searches for files according to the given prefix.
+// If the only predicted path is a single directory, the search will continue another recursive
+// layer into that directory.
+func (f FilesPredictor) Predict(prefix string) (options []string) {
+ options = f.predictFiles(prefix)
+
+ // If the number of prediction is not 1, we either have many results or have no results, so we
+ // return it.
+ if len(options) != 1 {
+ return
+ }
+
+ // Only try deeper, if the one item is a directory.
+ if stat, err := os.Stat(options[0]); err != nil || !stat.IsDir() {
+ return
+ }
+
+ return f.predictFiles(options[0])
+}
+
+func (f FilesPredictor) predictFiles(prefix string) []string {
+ if strings.HasSuffix(prefix, "/..") {
+ return nil
+ }
+
+ dir := directory(prefix)
+ files := f.listFiles(dir)
+
+ // Add dir if match.
+ files = append(files, dir)
+
+ return FilesSet(files).Predict(prefix)
+}
+
+func (f FilesPredictor) listFiles(dir string) []string {
+ // Set of all file names.
+ m := map[string]bool{}
+
+ // List files.
+ if files, err := filepath.Glob(filepath.Join(dir, f.pattern)); err == nil {
+ for _, file := range files {
+ if stat, err := os.Stat(file); err != nil || stat.IsDir() || f.includeFiles {
+ m[file] = 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)
+ }
+ return list
+}
+
+// directory gives the directory of the given partial path in case that it is not, we fall back to
+// the current directory.
+func directory(path string) string {
+ if info, err := os.Stat(path); err == nil && info.IsDir() {
+ return fixPathForm(path, path)
+ }
+ dir := filepath.Dir(path)
+ if info, err := os.Stat(dir); err == nil && info.IsDir() {
+ return fixPathForm(path, dir)
+ }
+ return "./"
+}
+
+// FilesSet predict according to file rules to a given fixed set of file names.
+type FilesSet []string
+
+func (s FilesSet) Predict(prefix string) (prediction []string) {
+ // add all matching files to prediction
+ for _, f := range s {
+ f = fixPathForm(prefix, f)
+
+ // test matching of file to the argument
+ if matchFile(f, prefix) {
+ prediction = append(prediction, f)
+ }
+ }
+ if len(prediction) == 0 {
+ return s
+ }
+ return
+}
+
+// MatchFile returns true if prefix can match the file
+func matchFile(file, prefix string) bool {
+ // special case for current directory completion
+ if file == "./" && (prefix == "." || prefix == "") {
+ return true
+ }
+ if prefix == "." && strings.HasPrefix(file, ".") {
+ return true
+ }
+
+ file = strings.TrimPrefix(file, "./")
+ prefix = strings.TrimPrefix(prefix, "./")
+
+ return strings.HasPrefix(file, prefix)
+}
+
+// fixPathForm changes a file name to a relative name
+func fixPathForm(last string, 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
+ }
+
+ // If last is absolute, return path as absolute.
+ if filepath.IsAbs(last) {
+ return fixDirPath(abs)
+ }
+
+ rel, err := filepath.Rel(workDir, abs)
+ if err != nil {
+ return file
+ }
+
+ // Fix ./ prefix of path.
+ if rel != "." && strings.HasPrefix(last, ".") {
+ rel = "./" + rel
+ }
+
+ return fixDirPath(rel)
+}
+
+func fixDirPath(path string) string {
+ info, err := os.Stat(path)
+ if err == nil && info.IsDir() && !strings.HasSuffix(path, "/") {
+ path += "/"
+ }
+ return path
+}
diff --git a/predict/files_test.go b/predict/files_test.go
new file mode 100644
index 0000000..6d6cba2
--- /dev/null
+++ b/predict/files_test.go
@@ -0,0 +1,233 @@
+package predict
+
+import (
+ "fmt"
+ "os"
+ "sort"
+ "strings"
+ "testing"
+)
+
+func TestFiles(t *testing.T) {
+ err := os.Chdir("testdata")
+ if err != nil {
+ panic(err)
+ }
+ defer os.Chdir("..")
+
+ tests := []struct {
+ name string
+ p FilesPredictor
+ prefixes []string
+ want []string
+ }{
+ {
+ name: "files/txt",
+ p: Files("*.txt"),
+ prefixes: []string{""},
+ want: []string{"./", "dir/", "outer/", "a.txt", "b.txt", "c.txt", ".dot.txt"},
+ },
+ {
+ name: "files/txt",
+ p: Files("*.txt"),
+ prefixes: []string{"./dir/"},
+ want: []string{"./dir/"},
+ },
+ {
+ name: "complete files inside dir if it is the only match",
+ p: Files("foo"),
+ prefixes: []string{"./dir/", "./d"},
+ want: []string{"./dir/", "./dir/foo"},
+ },
+ {
+ name: "complete files inside dir when argList includes file name",
+ p: Files("*"),
+ prefixes: []string{"./dir/f", "./dir/foo"},
+ want: []string{"./dir/foo"},
+ },
+ {
+ name: "files/md",
+ p: Files("*.md"),
+ prefixes: []string{""},
+ want: []string{"./", "dir/", "outer/", "readme.md"},
+ },
+ {
+ name: "files/md with ./ prefix",
+ p: Files("*.md"),
+ prefixes: []string{".", "./"},
+ want: []string{"./", "./dir/", "./outer/", "./readme.md"},
+ },
+ {
+ name: "dirs",
+ p: Dirs("*"),
+ prefixes: []string{"di", "dir", "dir/"},
+ want: []string{"dir/"},
+ },
+ {
+ name: "dirs with ./ prefix",
+ p: Dirs("*"),
+ prefixes: []string{"./di", "./dir", "./dir/"},
+ want: []string{"./dir/"},
+ },
+ {
+ name: "predict anything in dir",
+ p: Files("*"),
+ prefixes: []string{"dir", "dir/", "di"},
+ want: []string{"dir/", "dir/foo", "dir/bar"},
+ },
+ {
+ name: "predict anything in dir with ./ prefix",
+ p: Files("*"),
+ prefixes: []string{"./dir", "./dir/", "./di"},
+ want: []string{"./dir/", "./dir/foo", "./dir/bar"},
+ },
+ {
+ name: "root directories",
+ p: Dirs("*"),
+ prefixes: []string{""},
+ want: []string{"./", "dir/", "outer/"},
+ },
+ {
+ name: "root directories with ./ prefix",
+ p: Dirs("*"),
+ prefixes: []string{".", "./"},
+ want: []string{"./", "./dir/", "./outer/"},
+ },
+ {
+ name: "nested directories",
+ p: Dirs("*.md"),
+ prefixes: []string{"ou", "outer", "outer/"},
+ want: []string{"outer/", "outer/inner/"},
+ },
+ {
+ name: "nested directories with ./ prefix",
+ p: Dirs("*.md"),
+ prefixes: []string{"./ou", "./outer", "./outer/"},
+ want: []string{"./outer/", "./outer/inner/"},
+ },
+ {
+ name: "nested inner directory",
+ p: Files("*.md"),
+ prefixes: []string{"outer/i"},
+ want: []string{"outer/inner/", "outer/inner/readme.md"},
+ },
+ }
+
+ for _, tt := range tests {
+ for _, prefix := range tt.prefixes {
+ t.Run(tt.name+"/prefix="+prefix, func(t *testing.T) {
+
+ matches := tt.p.Predict(prefix)
+
+ 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)
+ }
+ })
+ }
+ }
+}
+
+func TestMatchFile(t *testing.T) {
+ // Change to tests directory for testing completion of
+ // files and directories
+ err := os.Chdir("testdata")
+ if err != nil {
+ panic(err)
+ }
+ defer os.Chdir("..")
+
+ type matcherTest struct {
+ prefix string
+ want bool
+ }
+
+ tests := []struct {
+ long string
+ tests []matcherTest
+ }{
+ {
+ long: "file.txt",
+ tests: []matcherTest{
+ {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},
+ {prefix: "./file.txt", want: true},
+ {prefix: "other.txt", want: false},
+ {prefix: "/other.txt", want: false},
+ {prefix: "/file.txt", want: false},
+ {prefix: "/fil", want: false},
+ {prefix: "/file.txt2", want: false},
+ {prefix: "/.", want: false},
+ },
+ },
+ {
+ long: "./file.txt",
+ tests: []matcherTest{
+ {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},
+ {prefix: "./file.txt", want: true},
+ {prefix: "other.txt", want: false},
+ {prefix: "/other.txt", want: false},
+ {prefix: "/file.txt", want: false},
+ {prefix: "/fil", want: false},
+ {prefix: "/file.txt2", want: false},
+ {prefix: "/.", want: false},
+ },
+ },
+ {
+ long: "/file.txt",
+ tests: []matcherTest{
+ {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},
+ {prefix: "./file.txt", want: false},
+ {prefix: "other.txt", want: false},
+ {prefix: "/other.txt", want: false},
+ {prefix: "/file.txt", want: true},
+ {prefix: "/fil", want: true},
+ {prefix: "/file.txt2", want: false},
+ {prefix: "/.", want: false},
+ },
+ },
+ {
+ long: "./",
+ tests: []matcherTest{
+ {prefix: "", want: true},
+ {prefix: ".", want: true},
+ {prefix: "./", want: true},
+ {prefix: "./.", want: false},
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ for _, ttt := range tt.tests {
+ name := fmt.Sprintf("long=%q&prefix=%q", tt.long, ttt.prefix)
+ t.Run(name, func(t *testing.T) {
+ got := matchFile(tt.long, ttt.prefix)
+ if got != ttt.want {
+ t.Errorf("Failed %s: got = %t, want: %t", name, got, ttt.want)
+ }
+ })
+ }
+ }
+}
diff --git a/predict/predict.go b/predict/predict.go
new file mode 100644
index 0000000..f4d5bb7
--- /dev/null
+++ b/predict/predict.go
@@ -0,0 +1,34 @@
+// Package predict provides helper functions for completion predictors.
+package predict
+
+import "github.com/posener/complete"
+
+// Set predicts a set of predefined values.
+type Set []string
+
+func (p Set) Predict(_ string) (options []string) {
+ return p
+}
+
+var (
+ // Something is used to indicate that does not completes somthing. Such that other prediction
+ // wont be applied.
+ Something = Set{""}
+
+ // Nothing is used to indicate that does not completes anything.
+ Nothing = Set{}
+)
+
+// Or unions prediction functions, so that the result predication is the union of their
+// predications.
+func Or(ps ...complete.Predictor) complete.Predictor {
+ return complete.PredictFunc(func(prefix string) (options []string) {
+ for _, p := range ps {
+ if p == nil {
+ continue
+ }
+ options = append(options, p.Predict(prefix)...)
+ }
+ return
+ })
+}
diff --git a/predict/predict_test.go b/predict/predict_test.go
new file mode 100644
index 0000000..af3bf69
--- /dev/null
+++ b/predict/predict_test.go
@@ -0,0 +1,61 @@
+package predict
+
+import (
+ "testing"
+
+ "github.com/posener/complete"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPredict(t *testing.T) {
+ tests := []struct {
+ name string
+ p complete.Predictor
+ prefix string
+ want []string
+ }{
+ {
+ name: "set",
+ p: Set{"a", "b", "c"},
+ want: []string{"a", "b", "c"},
+ },
+ {
+ name: "set/empty",
+ p: Set{},
+ want: []string{},
+ },
+ {
+ name: "or: word with nil",
+ p: Or(Set{"a"}, nil),
+ want: []string{"a"},
+ },
+ {
+ name: "or: nil with word",
+ p: Or(nil, Set{"a"}),
+ want: []string{"a"},
+ },
+ {
+ name: "or: word with word with word",
+ p: Or(Set{"a"}, Set{"b"}, Set{"c"}),
+ want: []string{"a", "b", "c"},
+ },
+ {
+ name: "something",
+ p: Something,
+ want: []string{""},
+ },
+ {
+ name: "nothing",
+ p: Nothing,
+ prefix: "a",
+ want: []string{},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := tt.p.Predict(tt.prefix)
+ assert.ElementsMatch(t, tt.want, got, "Got: %+v", got)
+ })
+ }
+}
diff --git a/predict/testdata/.dot.txt b/predict/testdata/.dot.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/predict/testdata/.dot.txt
diff --git a/predict/testdata/a.txt b/predict/testdata/a.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/predict/testdata/a.txt
diff --git a/predict/testdata/b.txt b/predict/testdata/b.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/predict/testdata/b.txt
diff --git a/predict/testdata/c.txt b/predict/testdata/c.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/predict/testdata/c.txt
diff --git a/predict/testdata/dir/bar b/predict/testdata/dir/bar
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/predict/testdata/dir/bar
diff --git a/predict/testdata/dir/foo b/predict/testdata/dir/foo
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/predict/testdata/dir/foo
diff --git a/predict/testdata/outer/inner/readme.md b/predict/testdata/outer/inner/readme.md
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/predict/testdata/outer/inner/readme.md
diff --git a/predict/testdata/readme.md b/predict/testdata/readme.md
new file mode 100644
index 0000000..25ea22c
--- /dev/null
+++ b/predict/testdata/readme.md
@@ -0,0 +1,3 @@
+# About this directory
+
+This directory is for testing file completion purposes \ No newline at end of file