diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go index c5283d62f2..bbfe73bf3d 100644 --- a/internal/lsp/completion.go +++ b/internal/lsp/completion.go @@ -52,10 +52,6 @@ func (s *Server) completion(ctx context.Context, params *protocol.CompletionPara }, nil } -// Limit deep completion results because in some cases there are too many -// to be useful. -const maxDeepCompletions = 3 - func (s *Server) toProtocolCompletionItems(ctx context.Context, view source.View, m *protocol.ColumnMapper, candidates []source.CompletionItem, pos protocol.Position, surrounding *source.Selection) []protocol.CompletionItem { // Sort the candidates by score, since that is not supported by LSP yet. sort.SliceStable(candidates, func(i, j int) bool { @@ -92,7 +88,7 @@ func (s *Server) toProtocolCompletionItems(ctx context.Context, view source.View if !s.useDeepCompletions { continue } - if numDeepCompletionsSeen >= maxDeepCompletions { + if numDeepCompletionsSeen >= source.MaxDeepCompletions { continue } numDeepCompletionsSeen++ diff --git a/internal/lsp/source/completion.go b/internal/lsp/source/completion.go index 4506ce0907..d6d6c288dc 100644 --- a/internal/lsp/source/completion.go +++ b/internal/lsp/source/completion.go @@ -17,6 +17,7 @@ import ( "golang.org/x/tools/internal/lsp/fuzzy" "golang.org/x/tools/internal/lsp/snippet" "golang.org/x/tools/internal/span" + "golang.org/x/tools/internal/telemetry/log" "golang.org/x/tools/internal/telemetry/trace" errors "golang.org/x/xerrors" ) @@ -193,6 +194,11 @@ type completer struct { // matcher matches the candidates against the surrounding prefix. matcher matcher + + // methodSetCache caches the types.NewMethodSet call, which is relatively + // expensive and can be called many times for the same type while searching + // for deep completions. + methodSetCache map[methodSetKey]*types.MethodSet } type compLitInfo struct { @@ -216,6 +222,11 @@ type compLitInfo struct { maybeInFieldName bool } +type methodSetKey struct { + typ types.Type + addressable bool +} + // A Selection represents the cursor position and surrounding identifier. type Selection struct { Content string @@ -243,9 +254,7 @@ func (c *completer) setSurrounding(ident *ast.Ident) { // Fuzzy matching shares the "useDeepCompletions" config flag, so if deep completions // are enabled then also enable fuzzy matching. if c.deepState.enabled { - if c.surrounding.Prefix() != "" { - c.matcher = fuzzy.NewMatcher(c.surrounding.Prefix(), fuzzy.Symbol) - } + c.matcher = fuzzy.NewMatcher(c.surrounding.Prefix(), fuzzy.Symbol) } else { c.matcher = prefixMatcher(strings.ToLower(c.surrounding.Prefix())) } @@ -253,9 +262,11 @@ func (c *completer) setSurrounding(ident *ast.Ident) { // found adds a candidate completion. We will also search through the object's // members for more candidates. -func (c *completer) found(obj types.Object, score float64, imp *imports.ImportInfo) error { +func (c *completer) found(obj types.Object, score float64, imp *imports.ImportInfo) { if obj.Pkg() != nil && obj.Pkg() != c.types && !obj.Exported() { - return errors.Errorf("%s is inaccessible from %s", obj.Name(), c.types.Path()) + // obj is not accessible because it lives in another package and is not + // exported. Don't treat it as a completion candidate. + return } if c.inDeepCompletion() { @@ -264,13 +275,13 @@ func (c *completer) found(obj types.Object, score float64, imp *imports.ImportIn // "bar.Baz" even though "Baz" is represented the same types.Object in both. for _, seenObj := range c.deepState.chain { if seenObj == obj { - return nil + return } } } else { // At the top level, dedupe by object. if c.seen[obj] { - return nil + return } c.seen[obj] = true } @@ -288,23 +299,23 @@ func (c *completer) found(obj types.Object, score float64, imp *imports.ImportIn // Favor shallow matches by lowering weight according to depth. cand.score -= stdScore * float64(len(c.deepState.chain)) - item, err := c.item(cand) - if err != nil { - return err - } - if c.matcher == nil { - c.items = append(c.items, item) - } else { - score := c.matcher.Score(item.Label) - if score > 0 { - item.Score *= float64(score) - c.items = append(c.items, item) + cand.name = c.deepState.chainString(obj.Name()) + matchScore := c.matcher.Score(cand.name) + if matchScore > 0 { + cand.score *= float64(matchScore) + + // Avoid calling c.item() for deep candidates that wouldn't be in the top + // MaxDeepCompletions anyway. + if !c.inDeepCompletion() || c.deepState.isHighScore(cand.score) { + if item, err := c.item(cand); err == nil { + c.items = append(c.items, item) + } else { + log.Error(c.ctx, "error generating completion item", err) + } } } c.deepSearch(obj) - - return nil } // candidate represents a completion candidate. @@ -315,6 +326,9 @@ type candidate struct { // score is used to rank candidates. score float64 + // name is the deep object name path, e.g. "foo.bar" + name string + // expandFuncCall is true if obj should be invoked in the completion. // For example, expandFuncCall=true yields "foo()", expandFuncCall=false yields "foo". expandFuncCall bool @@ -381,6 +395,9 @@ func Completion(ctx context.Context, view View, f GoFile, pos token.Pos, opts Co enclosingFunction: enclosingFunction(path, pos, pkg.GetTypesInfo()), enclosingCompositeLiteral: clInfo, opts: opts, + // default to a matcher that always matches + matcher: prefixMatcher(""), + methodSetCache: make(map[methodSetKey]*types.MethodSet), } c.deepState.enabled = opts.DeepComplete @@ -498,14 +515,16 @@ func (c *completer) packageMembers(pkg *types.PkgName) { } func (c *completer) methodsAndFields(typ types.Type, addressable bool) error { - var mset *types.MethodSet - - if addressable && !types.IsInterface(typ) && !isPointer(typ) { - // Add methods of *T, which includes methods with receiver T. - mset = types.NewMethodSet(types.NewPointer(typ)) - } else { - // Add methods of T. - mset = types.NewMethodSet(typ) + mset := c.methodSetCache[methodSetKey{typ, addressable}] + if mset == nil { + if addressable && !types.IsInterface(typ) && !isPointer(typ) { + // Add methods of *T, which includes methods with receiver T. + mset = types.NewMethodSet(types.NewPointer(typ)) + } else { + // Add methods of T. + mset = types.NewMethodSet(typ) + } + c.methodSetCache[methodSetKey{typ, addressable}] = mset } for i := 0; i < mset.Len(); i++ { diff --git a/internal/lsp/source/completion_format.go b/internal/lsp/source/completion_format.go index 115120d82c..55e8675cc6 100644 --- a/internal/lsp/source/completion_format.go +++ b/internal/lsp/source/completion_format.go @@ -26,11 +26,11 @@ func (c *completer) item(cand candidate) (CompletionItem, error) { // Handle builtin types separately. if obj.Parent() == types.Universe { - return c.formatBuiltin(cand) + return c.formatBuiltin(cand), nil } var ( - label = c.deepState.chainString(obj.Name()) + label = cand.name detail = types.TypeString(obj.Type(), c.qf) insert = label kind CompletionItemKind @@ -172,7 +172,7 @@ func (c *completer) isParameter(v *types.Var) bool { return false } -func (c *completer) formatBuiltin(cand candidate) (CompletionItem, error) { +func (c *completer) formatBuiltin(cand candidate) CompletionItem { obj := cand.obj item := CompletionItem{ Label: obj.Name(), @@ -202,7 +202,7 @@ func (c *completer) formatBuiltin(cand candidate) (CompletionItem, error) { case *types.Nil: item.Kind = VariableCompletionItem } - return item, nil + return item } var replacer = strings.NewReplacer( diff --git a/internal/lsp/source/deep_completion.go b/internal/lsp/source/deep_completion.go index 7d2f6ad97b..2ef01a8415 100644 --- a/internal/lsp/source/deep_completion.go +++ b/internal/lsp/source/deep_completion.go @@ -9,6 +9,10 @@ import ( "strings" ) +// Limit 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. @@ -23,6 +27,10 @@ type deepCompletionState struct { // 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 } // push pushes obj onto our search stack. @@ -45,6 +53,34 @@ func (s *deepCompletionState) chainString(finalName string) string { 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. + + // First check for an unclaimed spot and claim if available. + for i, deepScore := range s.highScores { + if deepScore == 0 { + s.highScores[i] = score + return true + } + } + + // Otherwise, if we beat an existing score then take its spot and scoot + // all lower scores down one position. + for i, deepScore := range s.highScores { + if score > deepScore { + copy(s.highScores[i+1:], s.highScores[i:]) + s.highScores[i] = score + return true + } + } + + return false +} + func (c *completer) inDeepCompletion() bool { return len(c.deepState.chain) > 0 }