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