github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/internal/lsp/work/completion.go (about)

     1  // Copyright 2022 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 work
     6  
     7  import (
     8  	"context"
     9  	"go/token"
    10  	"os"
    11  	"path/filepath"
    12  	"sort"
    13  	"strings"
    14  
    15  	"github.com/powerman/golang-tools/internal/event"
    16  	"github.com/powerman/golang-tools/internal/lsp/protocol"
    17  	"github.com/powerman/golang-tools/internal/lsp/source"
    18  	errors "golang.org/x/xerrors"
    19  )
    20  
    21  func Completion(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle, position protocol.Position) (*protocol.CompletionList, error) {
    22  	ctx, done := event.Start(ctx, "work.Completion")
    23  	defer done()
    24  
    25  	// Get the position of the cursor.
    26  	pw, err := snapshot.ParseWork(ctx, fh)
    27  	if err != nil {
    28  		return nil, errors.Errorf("getting go.work file handle: %w", err)
    29  	}
    30  	spn, err := pw.Mapper.PointSpan(position)
    31  	if err != nil {
    32  		return nil, errors.Errorf("computing cursor position: %w", err)
    33  	}
    34  	rng, err := spn.Range(pw.Mapper.Converter)
    35  	if err != nil {
    36  		return nil, errors.Errorf("computing range: %w", err)
    37  	}
    38  
    39  	// Find the use statement the user is in.
    40  	cursor := rng.Start - 1
    41  	use, pathStart, _ := usePath(pw, cursor)
    42  	if use == nil {
    43  		return &protocol.CompletionList{}, nil
    44  	}
    45  	completingFrom := use.Path[:cursor-token.Pos(pathStart)]
    46  
    47  	// We're going to find the completions of the user input
    48  	// (completingFrom) by doing a walk on the innermost directory
    49  	// of the given path, and comparing the found paths to make sure
    50  	// that they match the component of the path after the
    51  	// innermost directory.
    52  	//
    53  	// We'll maintain two paths when doing this: pathPrefixSlash
    54  	// is essentially the path the user typed in, and pathPrefixAbs
    55  	// is the path made absolute from the go.work directory.
    56  
    57  	pathPrefixSlash := completingFrom
    58  	pathPrefixAbs := filepath.FromSlash(pathPrefixSlash)
    59  	if !filepath.IsAbs(pathPrefixAbs) {
    60  		pathPrefixAbs = filepath.Join(filepath.Dir(pw.URI.Filename()), pathPrefixAbs)
    61  	}
    62  
    63  	// pathPrefixDir is the directory that will be walked to find matches.
    64  	// If pathPrefixSlash is not explicitly a directory boundary (is either equivalent to "." or
    65  	// ends in a separator) we need to examine its parent directory to find sibling files that
    66  	// match.
    67  	depthBound := 5
    68  	pathPrefixDir, pathPrefixBase := pathPrefixAbs, ""
    69  	pathPrefixSlashDir := pathPrefixSlash
    70  	if filepath.Clean(pathPrefixSlash) != "." && !strings.HasSuffix(pathPrefixSlash, "/") {
    71  		depthBound++
    72  		pathPrefixDir, pathPrefixBase = filepath.Split(pathPrefixAbs)
    73  		pathPrefixSlashDir = dirNonClean(pathPrefixSlash)
    74  	}
    75  
    76  	var completions []string
    77  	// Stop traversing deeper once we've hit 10k files to try to stay generally under 100ms.
    78  	const numSeenBound = 10000
    79  	var numSeen int
    80  	stopWalking := errors.New("hit numSeenBound")
    81  	err = filepath.Walk(pathPrefixDir, func(wpath string, info os.FileInfo, err error) error {
    82  		if numSeen > numSeenBound {
    83  			// Stop traversing if we hit bound.
    84  			return stopWalking
    85  		}
    86  		numSeen++
    87  
    88  		// rel is the path relative to pathPrefixDir.
    89  		// Make sure that it has pathPrefixBase as a prefix
    90  		// otherwise it won't match the beginning of the
    91  		// base component of the path the user typed in.
    92  		rel := strings.TrimPrefix(wpath[len(pathPrefixDir):], string(filepath.Separator))
    93  		if info.IsDir() && wpath != pathPrefixDir && !strings.HasPrefix(rel, pathPrefixBase) {
    94  			return filepath.SkipDir
    95  		}
    96  
    97  		// Check for a match (a module directory).
    98  		if filepath.Base(rel) == "go.mod" {
    99  			relDir := strings.TrimSuffix(dirNonClean(rel), string(os.PathSeparator))
   100  			completionPath := join(pathPrefixSlashDir, filepath.ToSlash(relDir))
   101  
   102  			if !strings.HasPrefix(completionPath, completingFrom) {
   103  				return nil
   104  			}
   105  			if strings.HasSuffix(completionPath, "/") {
   106  				// Don't suggest paths that end in "/". This happens
   107  				// when the input is a path that ends in "/" and
   108  				// the completion is empty.
   109  				return nil
   110  			}
   111  			completion := completionPath[len(completingFrom):]
   112  			if completingFrom == "" && !strings.HasPrefix(completion, "./") {
   113  				// Bias towards "./" prefixes.
   114  				completion = join(".", completion)
   115  			}
   116  
   117  			completions = append(completions, completion)
   118  		}
   119  
   120  		if depth := strings.Count(rel, string(filepath.Separator)); depth >= depthBound {
   121  			return filepath.SkipDir
   122  		}
   123  		return nil
   124  	})
   125  	if err != nil && !errors.Is(err, stopWalking) {
   126  		return nil, errors.Errorf("walking to find completions: %w", err)
   127  	}
   128  
   129  	sort.Strings(completions)
   130  
   131  	var items []protocol.CompletionItem
   132  	for _, c := range completions {
   133  		items = append(items, protocol.CompletionItem{
   134  			Label:      c,
   135  			InsertText: c,
   136  		})
   137  	}
   138  	return &protocol.CompletionList{Items: items}, nil
   139  }
   140  
   141  // dirNonClean is filepath.Dir, without the Clean at the end.
   142  func dirNonClean(path string) string {
   143  	vol := filepath.VolumeName(path)
   144  	i := len(path) - 1
   145  	for i >= len(vol) && !os.IsPathSeparator(path[i]) {
   146  		i--
   147  	}
   148  	return path[len(vol) : i+1]
   149  }
   150  
   151  func join(a, b string) string {
   152  	if a == "" {
   153  		return b
   154  	}
   155  	if b == "" {
   156  		return a
   157  	}
   158  	return strings.TrimSuffix(a, "/") + "/" + b
   159  }