| // Copyright 2019 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 source | |
| import ( | |
| "context" | |
| "go/types" | |
| "strings" | |
| "time" | |
| ) | |
| // MaxDeepCompletions limits deep completion results because in most cases | |
| // there are too many to be useful. | |
| const MaxDeepCompletions = 3 | |
| // deepCompletionState stores our state as we search for deep completions. | |
| // "deep completion" refers to searching into objects' fields and methods to | |
| // find more completion candidates. | |
| type deepCompletionState struct { | |
| // maxDepth limits the deep completion search depth. 0 means | |
| // disabled and -1 means unlimited. | |
| maxDepth int | |
| // chain holds the traversal path as we do a depth-first search through | |
| // objects' members looking for exact type matches. | |
| chain []types.Object | |
| // chainNames holds the names of the chain objects. This allows us to | |
| // save allocations as we build many deep completion items. | |
| chainNames []string | |
| // highScores tracks the highest deep candidate scores we have found | |
| // so far. This is used to avoid work for low scoring deep candidates. | |
| highScores [MaxDeepCompletions]float64 | |
| // candidateCount is the count of unique deep candidates encountered | |
| // so far. | |
| candidateCount int | |
| } | |
| // push pushes obj onto our search stack. If invoke is true then | |
| // invocation parens "()" will be appended to the object name. | |
| func (s *deepCompletionState) push(obj types.Object, invoke bool) { | |
| s.chain = append(s.chain, obj) | |
| name := obj.Name() | |
| if invoke { | |
| name += "()" | |
| } | |
| s.chainNames = append(s.chainNames, name) | |
| } | |
| // pop pops the last object off our search stack. | |
| func (s *deepCompletionState) pop() { | |
| s.chain = s.chain[:len(s.chain)-1] | |
| s.chainNames = s.chainNames[:len(s.chainNames)-1] | |
| } | |
| // chainString joins the chain of objects' names together on ".". | |
| func (s *deepCompletionState) chainString(finalName string) string { | |
| s.chainNames = append(s.chainNames, finalName) | |
| chainStr := strings.Join(s.chainNames, ".") | |
| s.chainNames = s.chainNames[:len(s.chainNames)-1] | |
| return chainStr | |
| } | |
| // isHighScore returns whether score is among the top MaxDeepCompletions | |
| // deep candidate scores encountered so far. If so, it adds score to | |
| // highScores, possibly displacing an existing high score. | |
| func (s *deepCompletionState) isHighScore(score float64) bool { | |
| // Invariant: s.highScores is sorted with highest score first. Unclaimed | |
| // positions are trailing zeros. | |
| // If we beat an existing score then take its spot. | |
| for i, deepScore := range s.highScores { | |
| if score <= deepScore { | |
| continue | |
| } | |
| if deepScore != 0 && i != len(s.highScores)-1 { | |
| // If this wasn't an empty slot then we need to scooch everyone | |
| // down one spot. | |
| copy(s.highScores[i+1:], s.highScores[i:]) | |
| } | |
| s.highScores[i] = score | |
| return true | |
| } | |
| return false | |
| } | |
| // scorePenalty computes a deep candidate score penalty. A candidate | |
| // is penalized based on depth to favor shallower candidates. We also | |
| // give a slight bonus to unexported objects and a slight additional | |
| // penalty to function objects. | |
| func (s *deepCompletionState) scorePenalty() float64 { | |
| var deepPenalty float64 | |
| for _, dc := range s.chain { | |
| deepPenalty += 1 | |
| if !dc.Exported() { | |
| deepPenalty -= 0.1 | |
| } | |
| if _, isSig := dc.Type().Underlying().(*types.Signature); isSig { | |
| deepPenalty += 0.1 | |
| } | |
| } | |
| // Normalize penalty to a max depth of 10. | |
| return deepPenalty / 10 | |
| } | |
| func (c *completer) inDeepCompletion() bool { | |
| return len(c.deepState.chain) > 0 | |
| } | |
| // shouldPrune returns whether we should prune the current deep | |
| // candidate search to reduce the overall search scope. The | |
| // maximum search depth is reduced gradually as we use up our | |
| // completionBudget. | |
| func (c *completer) shouldPrune() bool { | |
| if !c.inDeepCompletion() { | |
| return false | |
| } | |
| // Check our remaining budget every 100 candidates. | |
| if c.opts.budget > 0 && c.deepState.candidateCount%100 == 0 { | |
| spent := float64(time.Since(c.startTime)) / float64(c.opts.budget) | |
| switch { | |
| case spent >= 0.90: | |
| // We are close to exhausting our budget. Disable deep completions. | |
| c.deepState.maxDepth = 0 | |
| case spent >= 0.75: | |
| // We are running out of budget, reduce max depth again. | |
| c.deepState.maxDepth = 2 | |
| case spent >= 0.5: | |
| // We have used half our budget, reduce max depth again. | |
| c.deepState.maxDepth = 3 | |
| case spent >= 0.25: | |
| // We have used a good chunk of our budget, so start limiting our search. | |
| // By default the search depth is unlimited, so this limit, while still | |
| // generous, is normally a huge reduction in search scope that will result | |
| // in our search completing very soon. | |
| c.deepState.maxDepth = 4 | |
| } | |
| } | |
| c.deepState.candidateCount++ | |
| if c.deepState.maxDepth >= 0 { | |
| return len(c.deepState.chain) >= c.deepState.maxDepth | |
| } | |
| return false | |
| } | |
| // deepSearch searches through cand's subordinate objects for more | |
| // completion items. | |
| func (c *completer) deepSearch(ctx context.Context, cand candidate) { | |
| if c.deepState.maxDepth == 0 { | |
| return | |
| } | |
| obj := cand.obj | |
| // If we are definitely completing a struct field name, deep completions | |
| // don't make sense. | |
| if c.wantStructFieldCompletions() && c.enclosingCompositeLiteral.inKey { | |
| return | |
| } | |
| // Don't search into type names. | |
| if isTypeName(obj) { | |
| return | |
| } | |
| if obj.Type() == nil { | |
| return | |
| } | |
| // Don't search embedded fields because they were already included in their | |
| // parent's fields. | |
| if v, ok := obj.(*types.Var); ok && v.Embedded() { | |
| return | |
| } | |
| if sig, ok := obj.Type().Underlying().(*types.Signature); ok { | |
| // If obj is a function that takes no arguments and returns one | |
| // value, keep searching across the function call. | |
| if sig.Params().Len() == 0 && sig.Results().Len() == 1 { | |
| // Pass invoke=true since the function needs to be invoked in | |
| // the deep chain. | |
| c.deepState.push(obj, true) | |
| // The result of a function call is not addressable. | |
| c.methodsAndFields(ctx, sig.Results().At(0).Type(), false, cand.imp) | |
| c.deepState.pop() | |
| } | |
| } | |
| // Push this object onto our search stack. | |
| c.deepState.push(obj, false) | |
| switch obj := obj.(type) { | |
| case *types.PkgName: | |
| c.packageMembers(ctx, obj.Imported(), stdScore, cand.imp) | |
| default: | |
| c.methodsAndFields(ctx, obj.Type(), cand.addressable, cand.imp) | |
| } | |
| // Pop the object off our search stack. | |
| c.deepState.pop() | |
| } |