github.com/v2fly/tools@v0.100.0/internal/lsp/source/completion/deep_completion.go (about) 1 // Copyright 2019 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 completion 6 7 import ( 8 "context" 9 "go/types" 10 "strings" 11 "time" 12 ) 13 14 // MaxDeepCompletions limits deep completion results because in most cases 15 // there are too many to be useful. 16 const MaxDeepCompletions = 3 17 18 // deepCompletionState stores our state as we search for deep completions. 19 // "deep completion" refers to searching into objects' fields and methods to 20 // find more completion candidates. 21 type deepCompletionState struct { 22 // enabled indicates wether deep completion is permitted. 23 enabled bool 24 25 // queueClosed is used to disable adding new sub-fields to search queue 26 // once we're running out of our time budget. 27 queueClosed bool 28 29 // searchQueue holds the current breadth first search queue. 30 searchQueue []candidate 31 32 // highScores tracks the highest deep candidate scores we have found 33 // so far. This is used to avoid work for low scoring deep candidates. 34 highScores [MaxDeepCompletions]float64 35 36 // candidateCount is the count of unique deep candidates encountered 37 // so far. 38 candidateCount int 39 } 40 41 // enqueue adds a candidate to the search queue. 42 func (s *deepCompletionState) enqueue(cand candidate) { 43 s.searchQueue = append(s.searchQueue, cand) 44 } 45 46 // dequeue removes and returns the leftmost element from the search queue. 47 func (s *deepCompletionState) dequeue() *candidate { 48 var cand *candidate 49 cand, s.searchQueue = &s.searchQueue[0], s.searchQueue[1:] 50 return cand 51 } 52 53 // scorePenalty computes a deep candidate score penalty. A candidate is 54 // penalized based on depth to favor shallower candidates. We also give a 55 // slight bonus to unexported objects and a slight additional penalty to 56 // function objects. 57 func (s *deepCompletionState) scorePenalty(cand *candidate) float64 { 58 var deepPenalty float64 59 for _, dc := range cand.path { 60 deepPenalty++ 61 62 if !dc.Exported() { 63 deepPenalty -= 0.1 64 } 65 66 if _, isSig := dc.Type().Underlying().(*types.Signature); isSig { 67 deepPenalty += 0.1 68 } 69 } 70 71 // Normalize penalty to a max depth of 10. 72 return deepPenalty / 10 73 } 74 75 // isHighScore returns whether score is among the top MaxDeepCompletions deep 76 // candidate scores encountered so far. If so, it adds score to highScores, 77 // possibly displacing an existing high score. 78 func (s *deepCompletionState) isHighScore(score float64) bool { 79 // Invariant: s.highScores is sorted with highest score first. Unclaimed 80 // positions are trailing zeros. 81 82 // If we beat an existing score then take its spot. 83 for i, deepScore := range s.highScores { 84 if score <= deepScore { 85 continue 86 } 87 88 if deepScore != 0 && i != len(s.highScores)-1 { 89 // If this wasn't an empty slot then we need to scooch everyone 90 // down one spot. 91 copy(s.highScores[i+1:], s.highScores[i:]) 92 } 93 s.highScores[i] = score 94 return true 95 } 96 97 return false 98 } 99 100 // newPath returns path from search root for an object following a given 101 // candidate. 102 func (s *deepCompletionState) newPath(cand *candidate, obj types.Object, invoke bool) ([]types.Object, []string) { 103 name := obj.Name() 104 if invoke { 105 name += "()" 106 } 107 108 // copy the slice since we don't want to overwrite the original slice. 109 path := append([]types.Object{}, cand.path...) 110 names := append([]string{}, cand.names...) 111 112 return append(path, obj), append(names, name) 113 } 114 115 // deepSearch searches a candidate and its subordinate objects for completion 116 // items if deep completion is enabled and adds the valid candidates to 117 // completion items. 118 func (c *completer) deepSearch(ctx context.Context) { 119 outer: 120 for len(c.deepState.searchQueue) > 0 { 121 cand := c.deepState.dequeue() 122 obj := cand.obj 123 124 if obj == nil { 125 continue 126 } 127 128 // At the top level, dedupe by object. 129 if len(cand.path) == 0 { 130 if c.seen[obj] { 131 continue 132 } 133 c.seen[obj] = true 134 } 135 136 // If obj is not accessible because it lives in another package and is 137 // not exported, don't treat it as a completion candidate unless it's 138 // a package completion candidate. 139 if !c.completionContext.packageCompletion && 140 obj.Pkg() != nil && obj.Pkg() != c.pkg.GetTypes() && !obj.Exported() { 141 continue 142 } 143 144 // If we want a type name, don't offer non-type name candidates. 145 // However, do offer package names since they can contain type names, 146 // and do offer any candidate without a type since we aren't sure if it 147 // is a type name or not (i.e. unimported candidate). 148 if c.wantTypeName() && obj.Type() != nil && !isTypeName(obj) && !isPkgName(obj) { 149 continue 150 } 151 152 // When searching deep, make sure we don't have a cycle in our chain. 153 // We don't dedupe by object because we want to allow both "foo.Baz" 154 // and "bar.Baz" even though "Baz" is represented the same types.Object 155 // in both. 156 for _, seenObj := range cand.path { 157 if seenObj == obj { 158 continue outer 159 } 160 } 161 162 c.addCandidate(ctx, cand) 163 164 c.deepState.candidateCount++ 165 if c.opts.budget > 0 && c.deepState.candidateCount%100 == 0 { 166 spent := float64(time.Since(c.startTime)) / float64(c.opts.budget) 167 select { 168 case <-ctx.Done(): 169 return 170 default: 171 // If we are almost out of budgeted time, no further elements 172 // should be added to the queue. This ensures remaining time is 173 // used for processing current queue. 174 if !c.deepState.queueClosed && spent >= 0.85 { 175 c.deepState.queueClosed = true 176 } 177 } 178 } 179 180 // if deep search is disabled, don't add any more candidates. 181 if !c.deepState.enabled || c.deepState.queueClosed { 182 continue 183 } 184 185 // Searching members for a type name doesn't make sense. 186 if isTypeName(obj) { 187 continue 188 } 189 if obj.Type() == nil { 190 continue 191 } 192 193 // Don't search embedded fields because they were already included in their 194 // parent's fields. 195 if v, ok := obj.(*types.Var); ok && v.Embedded() { 196 continue 197 } 198 199 if sig, ok := obj.Type().Underlying().(*types.Signature); ok { 200 // If obj is a function that takes no arguments and returns one 201 // value, keep searching across the function call. 202 if sig.Params().Len() == 0 && sig.Results().Len() == 1 { 203 path, names := c.deepState.newPath(cand, obj, true) 204 // The result of a function call is not addressable. 205 candidates := c.methodsAndFields(sig.Results().At(0).Type(), false, cand.imp) 206 for _, newCand := range candidates { 207 newCand.path, newCand.names = path, names 208 c.deepState.enqueue(newCand) 209 } 210 } 211 } 212 213 path, names := c.deepState.newPath(cand, obj, false) 214 switch obj := obj.(type) { 215 case *types.PkgName: 216 candidates := c.packageMembers(obj.Imported(), stdScore, cand.imp) 217 for _, newCand := range candidates { 218 newCand.path, newCand.names = path, names 219 c.deepState.enqueue(newCand) 220 } 221 default: 222 candidates := c.methodsAndFields(obj.Type(), cand.addressable, cand.imp) 223 for _, newCand := range candidates { 224 newCand.path, newCand.names = path, names 225 c.deepState.enqueue(newCand) 226 } 227 } 228 } 229 } 230 231 // addCandidate adds a completion candidate to suggestions, without searching 232 // its members for more candidates. 233 func (c *completer) addCandidate(ctx context.Context, cand *candidate) { 234 obj := cand.obj 235 if c.matchingCandidate(cand) { 236 cand.score *= highScore 237 238 if p := c.penalty(cand); p > 0 { 239 cand.score *= (1 - p) 240 } 241 } else if isTypeName(obj) { 242 // If obj is a *types.TypeName that didn't otherwise match, check 243 // if a literal object of this type makes a good candidate. 244 245 // We only care about named types (i.e. don't want builtin types). 246 if _, isNamed := obj.Type().(*types.Named); isNamed { 247 c.literal(ctx, obj.Type(), cand.imp) 248 } 249 } 250 251 // Lower score of method calls so we prefer fields and vars over calls. 252 if cand.expandFuncCall { 253 if sig, ok := obj.Type().Underlying().(*types.Signature); ok && sig.Recv() != nil { 254 cand.score *= 0.9 255 } 256 } 257 258 // Prefer private objects over public ones. 259 if !obj.Exported() && obj.Parent() != types.Universe { 260 cand.score *= 1.1 261 } 262 263 // Favor shallow matches by lowering score according to depth. 264 cand.score -= cand.score * c.deepState.scorePenalty(cand) 265 266 if cand.score < 0 { 267 cand.score = 0 268 } 269 270 cand.name = strings.Join(append(cand.names, cand.obj.Name()), ".") 271 if item, err := c.item(ctx, *cand); err == nil { 272 c.items = append(c.items, item) 273 } 274 } 275 276 // penalty reports a score penalty for cand in the range (0, 1). 277 // For example, a candidate is penalized if it has already been used 278 // in another switch case statement. 279 func (c *completer) penalty(cand *candidate) float64 { 280 for _, p := range c.inference.penalized { 281 if c.objChainMatches(cand, p.objChain) { 282 return p.penalty 283 } 284 } 285 286 return 0 287 } 288 289 // objChainMatches reports whether cand combined with the surrounding 290 // object prefix matches chain. 291 func (c *completer) objChainMatches(cand *candidate, chain []types.Object) bool { 292 // For example, when completing: 293 // 294 // foo.ba<> 295 // 296 // If we are considering the deep candidate "bar.baz", cand is baz, 297 // objChain is [foo] and deepChain is [bar]. We would match the 298 // chain [foo, bar, baz]. 299 if len(chain) != len(c.inference.objChain)+len(cand.path)+1 { 300 return false 301 } 302 303 if chain[len(chain)-1] != cand.obj { 304 return false 305 } 306 307 for i, o := range c.inference.objChain { 308 if chain[i] != o { 309 return false 310 } 311 } 312 313 for i, o := range cand.path { 314 if chain[i+len(c.inference.objChain)] != o { 315 return false 316 } 317 } 318 319 return true 320 }