summaryrefslogtreecommitdiff
path: root/predict/files.go
blob: 4654ec4d5ff247b51204bfbfa5ba10a948d049e0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
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
}