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