summaryrefslogtreecommitdiff
path: root/gocomplete/pkgs.go
blob: 252bb26f2a6383818203a82f8b22f5fd291c7773 (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
176
177
package main

import (
	"go/build"
	"io/ioutil"
	"log"
	"os"
	"os/user"
	"path/filepath"
	"strings"

	"github.com/posener/complete/v2/predict"
)

// predictPackages completes packages in the directory pointed by a.Last
// and packages that are one level below that package.
func predictPackages(prefix string) (prediction []string) {
	prediction = []string{prefix}
	lastPrediction := ""
	for len(prediction) == 1 && (lastPrediction == "" || lastPrediction != prediction[0]) {
		// if only one prediction, predict files within this prediction,
		// for example, if the user entered 'pk' and we have a package named 'pkg',
		// which is the only package prefixed with 'pk', we will automatically go one
		// level deeper and give the user the 'pkg' and all the nested packages within
		// that package.
		lastPrediction = prediction[0]
		prefix = prediction[0]
		prediction = predictLocalAndSystem(prefix)
	}
	return
}

func predictLocalAndSystem(prefix string) []string {
	localDirs := predict.FilesSet(listPackages(directory(prefix))).Predict(prefix)
	// System directories are not actual file names, for example: 'github.com/posener/complete' could
	// be the argument, but the actual filename is in $GOPATH/src/github.com/posener/complete'. this
	// is the reason to use the PredictSet and not the PredictDirs in this case.
	s := systemDirs(prefix)
	sysDirs := predict.Set(s).Predict(prefix)
	return append(localDirs, sysDirs...)
}

// listPackages looks in current pointed dir and in all it's direct sub-packages
// and return a list of paths to go packages.
func listPackages(dir string) (directories []string) {
	// add subdirectories
	files, err := ioutil.ReadDir(dir)
	if err != nil {
		log.Printf("failed reading directory %s: %s", dir, err)
		return
	}

	// build paths array
	paths := make([]string, 0, len(files)+1)
	for _, f := range files {
		if f.IsDir() {
			paths = append(paths, filepath.Join(dir, f.Name()))
		}
	}
	paths = append(paths, dir)

	// import packages according to given paths
	for _, p := range paths {
		pkg, err := build.ImportDir(p, 0)
		if err != nil {
			log.Printf("failed importing directory %s: %s", p, err)
			continue
		}
		directories = append(directories, pkg.Dir)
	}
	return
}

func systemDirs(dir string) (directories []string) {
	// get all paths from GOPATH environment variable and use their src directory
	paths := findGopath()
	for i := range paths {
		paths[i] = filepath.Join(paths[i], "src")
	}

	// normalize the directory to be an actual directory since it could be with an additional
	// characters after the last '/'.
	if !strings.HasSuffix(dir, "/") {
		dir = filepath.Dir(dir)
	}

	for _, basePath := range paths {
		path := filepath.Join(basePath, dir)
		files, err := ioutil.ReadDir(path)
		if err != nil {
			// path does not exists
			continue
		}
		// add the base path as one of the completion options
		switch dir {
		case "", ".", "/", "./":
		default:
			directories = append(directories, dir)
		}
		// add all nested directories of the base path
		// go supports only packages and not go files within the GOPATH
		for _, f := range files {
			if !f.IsDir() {
				continue
			}
			directories = append(directories, filepath.Join(dir, f.Name())+"/")
		}
	}
	return
}

func findGopath() []string {
	gopath := os.Getenv("GOPATH")
	if gopath == "" {
		// By convention
		// See rationale at https://github.com/golang/go/issues/17262
		usr, err := user.Current()
		if err != nil {
			return nil
		}
		usrgo := filepath.Join(usr.HomeDir, "go")
		return []string{usrgo}
	}
	listsep := string([]byte{os.PathListSeparator})
	entries := strings.Split(gopath, listsep)
	return entries
}

func directory(prefix string) string {
	if info, err := os.Stat(prefix); err == nil && info.IsDir() {
		return fixPathForm(prefix, prefix)
	}
	dir := filepath.Dir(prefix)
	if info, err := os.Stat(dir); err != nil || !info.IsDir() {
		return "./"
	}
	return fixPathForm(prefix, dir)
}

// 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
}