| // Copyright 2020 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package source |
| |
| import ( |
| "context" |
| "fmt" |
| "go/ast" |
| "go/types" |
| "path/filepath" |
| "strings" |
| |
| "golang.org/x/tools/internal/lsp/fuzzy" |
| "golang.org/x/tools/internal/lsp/protocol" |
| "golang.org/x/tools/internal/span" |
| errors "golang.org/x/xerrors" |
| ) |
| |
| // packageClauseCompletions offers completions for a package declaration when |
| // one is not present in the given file. |
| func packageClauseCompletions(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) ([]CompletionItem, *Selection, error) { |
| // We know that the AST for this file will be empty due to the missing |
| // package declaration, but parse it anyway to get a mapper. |
| pgf, err := snapshot.ParseGo(ctx, fh, ParseHeader) |
| if err != nil { |
| return nil, nil, err |
| } |
| |
| // Check that the file is completely empty, to avoid offering incorrect package |
| // clause completions. |
| // TODO: Support package clause completions in all files. |
| if pgf.Tok.Size() != 0 { |
| return nil, nil, errors.New("package clause completion is only offered for empty file") |
| } |
| |
| cursorSpan, err := pgf.Mapper.PointSpan(pos) |
| if err != nil { |
| return nil, nil, err |
| } |
| rng, err := cursorSpan.Range(pgf.Mapper.Converter) |
| if err != nil { |
| return nil, nil, err |
| } |
| |
| surrounding := &Selection{ |
| content: "", |
| cursor: rng.Start, |
| mappedRange: newMappedRange(snapshot.FileSet(), pgf.Mapper, rng.Start, rng.Start), |
| } |
| |
| packageSuggestions, err := packageSuggestions(ctx, snapshot, fh.URI(), "") |
| if err != nil { |
| return nil, nil, err |
| } |
| |
| var items []CompletionItem |
| for _, pkg := range packageSuggestions { |
| insertText := fmt.Sprintf("package %s", pkg.name) |
| items = append(items, CompletionItem{ |
| Label: insertText, |
| Kind: protocol.ModuleCompletion, |
| InsertText: insertText, |
| Score: pkg.score, |
| }) |
| } |
| |
| return items, surrounding, nil |
| } |
| |
| // packageNameCompletions returns name completions for a package clause using |
| // the current name as prefix. |
| func (c *completer) packageNameCompletions(ctx context.Context, fileURI span.URI, name *ast.Ident) error { |
| cursor := int(c.pos - name.NamePos) |
| if cursor < 0 || cursor > len(name.Name) { |
| return errors.New("cursor is not in package name identifier") |
| } |
| |
| prefix := name.Name[:cursor] |
| packageSuggestions, err := packageSuggestions(ctx, c.snapshot, fileURI, prefix) |
| if err != nil { |
| return err |
| } |
| |
| for _, pkg := range packageSuggestions { |
| if item, err := c.item(ctx, pkg); err == nil { |
| c.items = append(c.items, item) |
| } |
| } |
| return nil |
| } |
| |
| // packageSuggestions returns a list of packages from workspace packages that |
| // have the given prefix and are used in the the same directory as the given |
| // file. This also includes test packages for these packages (<pkg>_test) and |
| // the directory name itself. |
| func packageSuggestions(ctx context.Context, snapshot Snapshot, fileURI span.URI, prefix string) ([]candidate, error) { |
| workspacePackages, err := snapshot.WorkspacePackages(ctx) |
| if err != nil { |
| return nil, err |
| } |
| |
| dirPath := filepath.Dir(string(fileURI)) |
| dirName := filepath.Base(dirPath) |
| |
| seenPkgs := make(map[string]struct{}) |
| |
| toCandidate := func(name string, score float64) candidate { |
| obj := types.NewPkgName(0, nil, name, types.NewPackage("", name)) |
| return candidate{obj: obj, name: name, score: score} |
| } |
| |
| matcher := fuzzy.NewMatcher(prefix) |
| |
| // The `go` command by default only allows one package per directory but we |
| // support multiple package suggestions since gopls is build system agnostic. |
| var packages []candidate |
| for _, pkg := range workspacePackages { |
| if pkg.Name() == "main" { |
| continue |
| } |
| if _, ok := seenPkgs[pkg.Name()]; ok { |
| continue |
| } |
| |
| // Only add packages that are previously used in the current directory. |
| var relevantPkg bool |
| for _, pgf := range pkg.CompiledGoFiles() { |
| if filepath.Dir(string(pgf.URI)) == dirPath { |
| relevantPkg = true |
| break |
| } |
| } |
| if !relevantPkg { |
| continue |
| } |
| |
| // Add a found package used in current directory as a high relevance |
| // suggestion and the test package for it as a medium relevance |
| // suggestion. |
| if score := float64(matcher.Score(pkg.Name())); score > 0 { |
| packages = append(packages, toCandidate(pkg.Name(), score*highScore)) |
| } |
| seenPkgs[pkg.Name()] = struct{}{} |
| |
| testPkgName := pkg.Name() + "_test" |
| if _, ok := seenPkgs[testPkgName]; ok || strings.HasSuffix(pkg.Name(), "_test") { |
| continue |
| } |
| if score := float64(matcher.Score(testPkgName)); score > 0 { |
| packages = append(packages, toCandidate(testPkgName, score*stdScore)) |
| } |
| seenPkgs[testPkgName] = struct{}{} |
| } |
| |
| // Add current directory name as a low relevance suggestion. |
| if _, ok := seenPkgs[dirName]; !ok { |
| if score := float64(matcher.Score(dirName)); score > 0 { |
| packages = append(packages, toCandidate(dirName, score*lowScore)) |
| } |
| |
| testDirName := dirName + "_test" |
| if score := float64(matcher.Score(testDirName)); score > 0 { |
| packages = append(packages, toCandidate(testDirName, score*lowScore)) |
| } |
| } |
| |
| if score := float64(matcher.Score("main")); score > 0 { |
| packages = append(packages, toCandidate("main", score*lowScore)) |
| } |
| |
| return packages, nil |
| } |