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 }