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  }