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 }