summaryrefslogtreecommitdiff
path: root/predict/files.go
diff options
context:
space:
mode:
authorEyal Posener <[email protected]>2019-11-14 06:51:44 +0200
committerEyal Posener <[email protected]>2019-11-18 01:05:47 +0200
commit8724aaf18312e54750540a9578e00d61b1c545d8 (patch)
treed3e736b4fb279975bbcc017ae1bad53e454c5773 /predict/files.go
parent05b68ffc813dd10c420993cb1cf927b346c057b8 (diff)
V2
Diffstat (limited to 'predict/files.go')
-rw-r--r--predict/files.go175
1 files changed, 175 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
+}