github.com/april1989/origin-go-tools@v0.0.32/internal/lsp/source/completion_package.go (about) 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 source 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 "github.com/april1989/origin-go-tools/internal/lsp/fuzzy" 19 "github.com/april1989/origin-go-tools/internal/lsp/protocol" 20 "github.com/april1989/origin-go-tools/internal/span" 21 errors "golang.org/x/xerrors" 22 ) 23 24 // packageClauseCompletions offers completions for a package declaration when 25 // one is not present in the given file. 26 func packageClauseCompletions(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) ([]CompletionItem, *Selection, error) { 27 // We know that the AST for this file will be empty due to the missing 28 // package declaration, but parse it anyway to get a mapper. 29 pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) 30 if err != nil { 31 return nil, nil, err 32 } 33 34 cursorSpan, err := pgf.Mapper.PointSpan(pos) 35 if err != nil { 36 return nil, nil, err 37 } 38 rng, err := cursorSpan.Range(pgf.Mapper.Converter) 39 if err != nil { 40 return nil, nil, err 41 } 42 43 surrounding, err := packageCompletionSurrounding(ctx, snapshot.FileSet(), fh, pgf, rng.Start) 44 if err != nil { 45 return nil, nil, fmt.Errorf("invalid position for package completion: %w", err) 46 } 47 48 packageSuggestions, err := packageSuggestions(ctx, snapshot, fh.URI(), "") 49 if err != nil { 50 return nil, nil, err 51 } 52 53 var items []CompletionItem 54 for _, pkg := range packageSuggestions { 55 insertText := fmt.Sprintf("package %s", pkg.name) 56 items = append(items, CompletionItem{ 57 Label: insertText, 58 Kind: protocol.ModuleCompletion, 59 InsertText: insertText, 60 Score: pkg.score, 61 }) 62 } 63 64 return items, surrounding, nil 65 } 66 67 // packageCompletionSurrounding returns surrounding for package completion if a 68 // package completions can be suggested at a given position. A valid location 69 // for package completion is above any declarations or import statements. 70 func packageCompletionSurrounding(ctx context.Context, fset *token.FileSet, fh FileHandle, pgf *ParsedGoFile, pos token.Pos) (*Selection, error) { 71 src, err := fh.Read() 72 if err != nil { 73 return nil, err 74 } 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: 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: 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: 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 prefix := name.Name[:cursor] 193 packageSuggestions, err := packageSuggestions(ctx, c.snapshot, fileURI, prefix) 194 if err != nil { 195 return err 196 } 197 198 for _, pkg := range packageSuggestions { 199 if item, err := c.item(ctx, pkg); err == nil { 200 c.items = append(c.items, item) 201 } 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 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 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, 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" { 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 }