| // 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 completion |
| |
| 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() |
| } |