Giant blob of minor changes
[dotfiles/.git] / .config / coc / extensions / coc-go-data / tools / pkg / mod / golang.org / x / tools@v0.0.0-20201105173854-bc9fc8d8c4bc / internal / lsp / source / completion / package.go
1 // Copyright 2020 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
4
5 package completion
6
7 import (
8         "context"
9         "fmt"
10         "go/ast"
11         "go/parser"
12         "go/scanner"
13         "go/token"
14         "go/types"
15         "path/filepath"
16         "strings"
17
18         "golang.org/x/tools/internal/lsp/fuzzy"
19         "golang.org/x/tools/internal/lsp/protocol"
20         "golang.org/x/tools/internal/lsp/source"
21         "golang.org/x/tools/internal/span"
22         errors "golang.org/x/xerrors"
23 )
24
25 // packageClauseCompletions offers completions for a package declaration when
26 // one is not present in the given file.
27 func packageClauseCompletions(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, pos protocol.Position) ([]CompletionItem, *Selection, error) {
28         // We know that the AST for this file will be empty due to the missing
29         // package declaration, but parse it anyway to get a mapper.
30         pgf, err := snapshot.ParseGo(ctx, fh, source.ParseFull)
31         if err != nil {
32                 return nil, nil, err
33         }
34
35         cursorSpan, err := pgf.Mapper.PointSpan(pos)
36         if err != nil {
37                 return nil, nil, err
38         }
39         rng, err := cursorSpan.Range(pgf.Mapper.Converter)
40         if err != nil {
41                 return nil, nil, err
42         }
43
44         surrounding, err := packageCompletionSurrounding(snapshot.FileSet(), fh, pgf, rng.Start)
45         if err != nil {
46                 return nil, nil, errors.Errorf("invalid position for package completion: %w", err)
47         }
48
49         packageSuggestions, err := packageSuggestions(ctx, snapshot, fh.URI(), "")
50         if err != nil {
51                 return nil, nil, err
52         }
53
54         var items []CompletionItem
55         for _, pkg := range packageSuggestions {
56                 insertText := fmt.Sprintf("package %s", pkg.name)
57                 items = append(items, CompletionItem{
58                         Label:      insertText,
59                         Kind:       protocol.ModuleCompletion,
60                         InsertText: insertText,
61                         Score:      pkg.score,
62                 })
63         }
64
65         return items, surrounding, nil
66 }
67
68 // packageCompletionSurrounding returns surrounding for package completion if a
69 // package completions can be suggested at a given position. A valid location
70 // for package completion is above any declarations or import statements.
71 func packageCompletionSurrounding(fset *token.FileSet, fh source.FileHandle, pgf *source.ParsedGoFile, pos token.Pos) (*Selection, error) {
72         src, err := fh.Read()
73         if err != nil {
74                 return nil, err
75         }
76         // If the file lacks a package declaration, the parser will return an empty
77         // AST. As a work-around, try to parse an expression from the file contents.
78         expr, _ := parser.ParseExprFrom(fset, fh.URI().Filename(), src, parser.Mode(0))
79         if expr == nil {
80                 return nil, fmt.Errorf("unparseable file (%s)", fh.URI())
81         }
82         tok := fset.File(expr.Pos())
83         cursor := tok.Pos(pgf.Tok.Offset(pos))
84         m := &protocol.ColumnMapper{
85                 URI:       pgf.URI,
86                 Content:   src,
87                 Converter: span.NewContentConverter(fh.URI().Filename(), src),
88         }
89
90         // If we were able to parse out an identifier as the first expression from
91         // the file, it may be the beginning of a package declaration ("pack ").
92         // We can offer package completions if the cursor is in the identifier.
93         if name, ok := expr.(*ast.Ident); ok {
94                 if cursor >= name.Pos() && cursor <= name.End() {
95                         if !strings.HasPrefix(PACKAGE, name.Name) {
96                                 return nil, fmt.Errorf("cursor in non-matching ident")
97                         }
98                         return &Selection{
99                                 content:     name.Name,
100                                 cursor:      cursor,
101                                 MappedRange: source.NewMappedRange(fset, m, name.Pos(), name.End()),
102                         }, nil
103                 }
104         }
105
106         // The file is invalid, but it contains an expression that we were able to
107         // parse. We will use this expression to construct the cursor's
108         // "surrounding".
109
110         // First, consider the possibility that we have a valid "package" keyword
111         // with an empty package name ("package "). "package" is parsed as an
112         // *ast.BadDecl since it is a keyword. This logic would allow "package" to
113         // appear on any line of the file as long as it's the first code expression
114         // in the file.
115         lines := strings.Split(string(src), "\n")
116         cursorLine := tok.Line(cursor)
117         if cursorLine <= 0 || cursorLine > len(lines) {
118                 return nil, fmt.Errorf("invalid line number")
119         }
120         if fset.Position(expr.Pos()).Line == cursorLine {
121                 words := strings.Fields(lines[cursorLine-1])
122                 if len(words) > 0 && words[0] == PACKAGE {
123                         content := PACKAGE
124                         // Account for spaces if there are any.
125                         if len(words) > 1 {
126                                 content += " "
127                         }
128
129                         start := expr.Pos()
130                         end := token.Pos(int(expr.Pos()) + len(content) + 1)
131                         // We have verified that we have a valid 'package' keyword as our
132                         // first expression. Ensure that cursor is in this keyword or
133                         // otherwise fallback to the general case.
134                         if cursor >= start && cursor <= end {
135                                 return &Selection{
136                                         content:     content,
137                                         cursor:      cursor,
138                                         MappedRange: source.NewMappedRange(fset, m, start, end),
139                                 }, nil
140                         }
141                 }
142         }
143
144         // If the cursor is after the start of the expression, no package
145         // declaration will be valid.
146         if cursor > expr.Pos() {
147                 return nil, fmt.Errorf("cursor after expression")
148         }
149
150         // If the cursor is in a comment, don't offer any completions.
151         if cursorInComment(fset, cursor, src) {
152                 return nil, fmt.Errorf("cursor in comment")
153         }
154
155         // The surrounding range in this case is the cursor except for empty file,
156         // in which case it's end of file - 1
157         start, end := cursor, cursor
158         if tok.Size() == 0 {
159                 start, end = tok.Pos(0)-1, tok.Pos(0)-1
160         }
161
162         return &Selection{
163                 content:     "",
164                 cursor:      cursor,
165                 MappedRange: source.NewMappedRange(fset, m, start, end),
166         }, nil
167 }
168
169 func cursorInComment(fset *token.FileSet, cursor token.Pos, src []byte) bool {
170         var s scanner.Scanner
171         s.Init(fset.File(cursor), src, func(_ token.Position, _ string) {}, scanner.ScanComments)
172         for {
173                 pos, tok, lit := s.Scan()
174                 if pos <= cursor && cursor <= token.Pos(int(pos)+len(lit)) {
175                         return tok == token.COMMENT
176                 }
177                 if tok == token.EOF {
178                         break
179                 }
180         }
181         return false
182 }
183
184 // packageNameCompletions returns name completions for a package clause using
185 // the current name as prefix.
186 func (c *completer) packageNameCompletions(ctx context.Context, fileURI span.URI, name *ast.Ident) error {
187         cursor := int(c.pos - name.NamePos)
188         if cursor < 0 || cursor > len(name.Name) {
189                 return errors.New("cursor is not in package name identifier")
190         }
191
192         c.completionContext.packageCompletion = true
193
194         prefix := name.Name[:cursor]
195         packageSuggestions, err := packageSuggestions(ctx, c.snapshot, fileURI, prefix)
196         if err != nil {
197                 return err
198         }
199
200         for _, pkg := range packageSuggestions {
201                 c.deepState.enqueue(pkg)
202         }
203         return nil
204 }
205
206 // packageSuggestions returns a list of packages from workspace packages that
207 // have the given prefix and are used in the same directory as the given
208 // file. This also includes test packages for these packages (<pkg>_test) and
209 // the directory name itself.
210 func packageSuggestions(ctx context.Context, snapshot source.Snapshot, fileURI span.URI, prefix string) ([]candidate, error) {
211         workspacePackages, err := snapshot.WorkspacePackages(ctx)
212         if err != nil {
213                 return nil, err
214         }
215
216         dirPath := filepath.Dir(string(fileURI))
217         dirName := filepath.Base(dirPath)
218
219         seenPkgs := make(map[string]struct{})
220
221         toCandidate := func(name string, score float64) candidate {
222                 obj := types.NewPkgName(0, nil, name, types.NewPackage("", name))
223                 return candidate{obj: obj, name: name, detail: name, score: score}
224         }
225
226         matcher := fuzzy.NewMatcher(prefix)
227
228         // The `go` command by default only allows one package per directory but we
229         // support multiple package suggestions since gopls is build system agnostic.
230         var packages []candidate
231         for _, pkg := range workspacePackages {
232                 if pkg.Name() == "main" || pkg.Name() == "" {
233                         continue
234                 }
235                 if _, ok := seenPkgs[pkg.Name()]; ok {
236                         continue
237                 }
238
239                 // Only add packages that are previously used in the current directory.
240                 var relevantPkg bool
241                 for _, pgf := range pkg.CompiledGoFiles() {
242                         if filepath.Dir(string(pgf.URI)) == dirPath {
243                                 relevantPkg = true
244                                 break
245                         }
246                 }
247                 if !relevantPkg {
248                         continue
249                 }
250
251                 // Add a found package used in current directory as a high relevance
252                 // suggestion and the test package for it as a medium relevance
253                 // suggestion.
254                 if score := float64(matcher.Score(pkg.Name())); score > 0 {
255                         packages = append(packages, toCandidate(pkg.Name(), score*highScore))
256                 }
257                 seenPkgs[pkg.Name()] = struct{}{}
258
259                 testPkgName := pkg.Name() + "_test"
260                 if _, ok := seenPkgs[testPkgName]; ok || strings.HasSuffix(pkg.Name(), "_test") {
261                         continue
262                 }
263                 if score := float64(matcher.Score(testPkgName)); score > 0 {
264                         packages = append(packages, toCandidate(testPkgName, score*stdScore))
265                 }
266                 seenPkgs[testPkgName] = struct{}{}
267         }
268
269         // Add current directory name as a low relevance suggestion.
270         if _, ok := seenPkgs[dirName]; !ok {
271                 if score := float64(matcher.Score(dirName)); score > 0 {
272                         packages = append(packages, toCandidate(dirName, score*lowScore))
273                 }
274
275                 testDirName := dirName + "_test"
276                 if score := float64(matcher.Score(testDirName)); score > 0 {
277                         packages = append(packages, toCandidate(testDirName, score*lowScore))
278                 }
279         }
280
281         if score := float64(matcher.Score("main")); score > 0 {
282                 packages = append(packages, toCandidate("main", score*lowScore))
283         }
284
285         return packages, nil
286 }