internal/lsp: search candidate type's package for completions

When our expected type is a named type from another package, we now always
search that other package for completion candidates, even if it is not currently
imported.

Consider the example:

-- foo.go --
import "context"
func doSomething(ctx context.Context) {}

-- bar.go--
doSomething(<>)

"bar.go" doesn't import "context" yet, so normally you need to first import
"context" through whatever means before you get completion items from "context".
Now we notice that the expected type's package hasn't been imported yet and give
deep completions from "context".

Another use case is with literal completions. Consider:

-- foo.go --
import "bytes"
func doSomething(buf *bytes.Buffer) {}

-- bar.go--
doSomething(<>)

Now you will get a literal completion for "&bytes.Buffer{}" in "bar.go" even
though it hasn't imported "bytes" yet.

I had to pipe the import info around a bunch of places so the import is added
automatically for deep completions and literal completions.

Change-Id: Ie86af2aa64ee235038957c1eecf042f7ec2b329b
Reviewed-on: https://go-review.googlesource.com/c/tools/+/201207
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
Muir Manders 2019-10-15 14:42:30 -07:00 committed by Rebecca Stambler
parent b2a7f28a18
commit 86caa796c7
8 changed files with 108 additions and 46 deletions

View File

@ -325,7 +325,7 @@ func (c *completer) found(obj types.Object, score float64, imp *imports.ImportIn
} else if isTypeName(obj) {
// If obj is a *types.TypeName that didn't otherwise match, check
// if a literal object of this type makes a good candidate.
c.literal(obj.Type())
c.literal(obj.Type(), imp)
}
// Favor shallow matches by lowering weight according to depth.
@ -350,7 +350,7 @@ func (c *completer) found(obj types.Object, score float64, imp *imports.ImportIn
}
}
c.deepSearch(obj)
c.deepSearch(obj, imp)
}
// candidate represents a completion candidate.
@ -570,7 +570,7 @@ func (c *completer) selector(sel *ast.SelectorExpr) error {
// Is sel a qualified identifier?
if id, ok := sel.X.(*ast.Ident); ok {
if pkgname, ok := c.pkg.GetTypesInfo().Uses[id].(*types.PkgName); ok {
c.packageMembers(pkgname)
c.packageMembers(pkgname.Imported(), nil)
return nil
}
}
@ -581,17 +581,17 @@ func (c *completer) selector(sel *ast.SelectorExpr) error {
return errors.Errorf("cannot resolve %s", sel.X)
}
return c.methodsAndFields(tv.Type, tv.Addressable())
return c.methodsAndFields(tv.Type, tv.Addressable(), nil)
}
func (c *completer) packageMembers(pkg *types.PkgName) {
scope := pkg.Imported().Scope()
func (c *completer) packageMembers(pkg *types.Package, imp *imports.ImportInfo) {
scope := pkg.Scope()
for _, name := range scope.Names() {
c.found(scope.Lookup(name), stdScore, nil)
c.found(scope.Lookup(name), stdScore, imp)
}
}
func (c *completer) methodsAndFields(typ types.Type, addressable bool) error {
func (c *completer) methodsAndFields(typ types.Type, addressable bool, imp *imports.ImportInfo) error {
mset := c.methodSetCache[methodSetKey{typ, addressable}]
if mset == nil {
if addressable && !types.IsInterface(typ) && !isPointer(typ) {
@ -605,12 +605,12 @@ func (c *completer) methodsAndFields(typ types.Type, addressable bool) error {
}
for i := 0; i < mset.Len(); i++ {
c.found(mset.At(i).Obj(), stdScore, nil)
c.found(mset.At(i).Obj(), stdScore, imp)
}
// Add fields of T.
for _, f := range fieldSelections(typ) {
c.found(f, stdScore, nil)
c.found(f, stdScore, imp)
}
return nil
}
@ -681,6 +681,28 @@ func (c *completer) lexical() error {
}
}
if c.expectedType.objType != nil {
if named, _ := deref(c.expectedType.objType).(*types.Named); named != nil {
// If we expected a named type, check the type's package for
// completion items. This is useful when the current file hasn't
// imported the type's package yet.
if named.Obj() != nil && named.Obj().Pkg() != nil {
pkg := named.Obj().Pkg()
// Make sure the package name isn't already in use by another
// object, and that this file doesn't import the package yet.
if _, ok := seen[pkg.Name()]; !ok && pkg != c.pkg.GetTypes() && !alreadyImports(c.file, pkg.Path()) {
seen[pkg.Name()] = struct{}{}
obj := types.NewPkgName(0, nil, pkg.Name(), pkg)
c.found(obj, stdScore, &imports.ImportInfo{
ImportPath: pkg.Path(),
})
}
}
}
}
if c.opts.Unimported {
// Suggest packages that have not been imported yet.
pkgs, err := CandidateImports(c.ctx, c.view, c.filename)
@ -703,12 +725,11 @@ func (c *completer) lexical() error {
if c.expectedType.objType != nil {
// If we have an expected type and it is _not_ a named type, see
// if an object literal makes a good candidate. Named types are
// handled during the normal lexical object search. For example,
// if our expected type is "[]int", this will add a candidate of
// if an object literal makes a good candidate. For example, if
// our expected type is "[]int", this will add a candidate of
// "[]int{}".
if _, named := deref(c.expectedType.objType).(*types.Named); !named {
c.literal(c.expectedType.objType)
c.literal(c.expectedType.objType, nil)
}
}

View File

@ -13,6 +13,7 @@ import (
"go/types"
"strings"
"golang.org/x/tools/internal/imports"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/snippet"
"golang.org/x/tools/internal/span"
@ -90,17 +91,11 @@ func (c *completer) item(cand candidate) (CompletionItem, error) {
// If this candidate needs an additional import statement,
// add the additional text edits needed.
if cand.imp != nil {
edit, err := addNamedImport(c.view.Session().Cache().FileSet(), c.file, cand.imp.Name, cand.imp.ImportPath)
if err != nil {
return CompletionItem{}, err
}
addlEdits, err := ToProtocolEdits(c.mapper, edit)
if err != nil {
return CompletionItem{}, err
}
protocolEdits = append(protocolEdits, addlEdits...)
addlEdits, err := c.importEdits(cand.imp)
if err != nil {
return CompletionItem{}, err
}
protocolEdits = append(protocolEdits, addlEdits...)
detail = strings.TrimPrefix(detail, "untyped ")
item := CompletionItem{
@ -151,6 +146,20 @@ func (c *completer) item(cand candidate) (CompletionItem, error) {
return item, nil
}
// importEdits produces the text eddits necessary to add the given import to the current file.
func (c *completer) importEdits(imp *imports.ImportInfo) ([]protocol.TextEdit, error) {
if imp == nil {
return nil, nil
}
edit, err := addNamedImport(c.view.Session().Cache().FileSet(), c.file, imp.Name, imp.ImportPath)
if err != nil {
return nil, err
}
return ToProtocolEdits(c.mapper, edit)
}
func (c *completer) formatBuiltin(cand candidate) CompletionItem {
obj := cand.obj
item := CompletionItem{

View File

@ -11,6 +11,7 @@ import (
"strings"
"unicode"
"golang.org/x/tools/internal/imports"
"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/snippet"
@ -20,7 +21,7 @@ import (
// literal generates composite literal, function literal, and make()
// completion items.
func (c *completer) literal(literalType types.Type) {
func (c *completer) literal(literalType types.Type, imp *imports.ImportInfo) {
if c.expectedType.objType == nil {
return
}
@ -93,10 +94,14 @@ func (c *completer) literal(literalType types.Type) {
matchName = types.TypeString(t.Elem(), qf)
}
addlEdits, err := c.importEdits(imp)
if err != nil {
log.Error(c.ctx, "error adding import for literal candidate", err)
return
}
// If prefix matches the type name, client may want a composite literal.
if score := c.matcher.Score(matchName); score >= 0 {
var protocolEdits []protocol.TextEdit
if isPointer {
if sel != nil {
// If we are in a selector we must place the "&" before the selector.
@ -107,7 +112,7 @@ func (c *completer) literal(literalType types.Type) {
log.Error(c.ctx, "error making edit for literal pointer completion", err)
return
}
protocolEdits = append(protocolEdits, edits...)
addlEdits = append(addlEdits, edits...)
} else {
// Otherwise we can stick the "&" directly before the type name.
typeName = "&" + typeName
@ -116,13 +121,13 @@ func (c *completer) literal(literalType types.Type) {
switch t := literalType.Underlying().(type) {
case *types.Struct, *types.Array, *types.Slice, *types.Map:
c.compositeLiteral(t, typeName, float64(score), protocolEdits)
c.compositeLiteral(t, typeName, float64(score), addlEdits)
case *types.Basic:
// Add a literal completion for basic types that implement our
// expected interface (e.g. named string type http.Dir
// implements http.FileSystem).
if isInterface(c.expectedType.objType) {
c.basicLiteral(t, typeName, float64(score), protocolEdits)
c.basicLiteral(t, typeName, float64(score), addlEdits)
}
}
}
@ -134,11 +139,11 @@ func (c *completer) literal(literalType types.Type) {
switch literalType.Underlying().(type) {
case *types.Slice:
// The second argument to "make()" for slices is required, so default to "0".
c.makeCall(typeName, "0", float64(score))
c.makeCall(typeName, "0", float64(score), addlEdits)
case *types.Map, *types.Chan:
// Maps and channels don't require the second argument, so omit
// to keep things simple for now.
c.makeCall(typeName, "", float64(score))
c.makeCall(typeName, "", float64(score), addlEdits)
}
}
@ -341,7 +346,7 @@ func (c *completer) basicLiteral(T types.Type, typeName string, matchScore float
}
// makeCall adds a completion item for a "make()" call given a specific type.
func (c *completer) makeCall(typeName string, secondArg string, matchScore float64) {
func (c *completer) makeCall(typeName string, secondArg string, matchScore float64, edits []protocol.TextEdit) {
// Keep it simple and don't add any placeholders for optional "make()" arguments.
snip := &snippet.Builder{}
@ -365,10 +370,11 @@ func (c *completer) makeCall(typeName string, secondArg string, matchScore float
nonSnippet.WriteByte(')')
c.items = append(c.items, CompletionItem{
Label: nonSnippet.String(),
InsertText: nonSnippet.String(),
Score: matchScore * literalCandidateScore,
Kind: protocol.FunctionCompletion,
snippet: snip,
Label: nonSnippet.String(),
InsertText: nonSnippet.String(),
Score: matchScore * literalCandidateScore,
Kind: protocol.FunctionCompletion,
AdditionalTextEdits: edits,
snippet: snip,
})
}

View File

@ -8,6 +8,8 @@ import (
"go/types"
"strings"
"time"
"golang.org/x/tools/internal/imports"
)
// Limit deep completion results because in most cases there are too many
@ -140,7 +142,7 @@ func (c *completer) shouldPrune() bool {
// deepSearch searches through obj's subordinate objects for more
// completion items.
func (c *completer) deepSearch(obj types.Object) {
func (c *completer) deepSearch(obj types.Object, imp *imports.ImportInfo) {
if c.deepState.maxDepth == 0 {
return
}
@ -170,7 +172,7 @@ func (c *completer) deepSearch(obj types.Object) {
// the deep chain.
c.deepState.push(obj, true)
// The result of a function call is not addressable.
c.methodsAndFields(sig.Results().At(0).Type(), false)
c.methodsAndFields(sig.Results().At(0).Type(), false, imp)
c.deepState.pop()
}
}
@ -180,11 +182,11 @@ func (c *completer) deepSearch(obj types.Object) {
switch obj := obj.(type) {
case *types.PkgName:
c.packageMembers(obj)
c.packageMembers(obj.Imported(), imp)
default:
// For now it is okay to assume obj is addressable since we don't search beyond
// function calls.
c.methodsAndFields(obj.Type(), true)
c.methodsAndFields(obj.Type(), true, imp)
}
// Pop the object off our search stack.

View File

@ -37,7 +37,7 @@ import (
//
// addNamedImport only returns edits that affect the import declarations.
func addNamedImport(fset *token.FileSet, f *ast.File, name, path string) (edits []diff.TextEdit, err error) {
if alreadyImports(f, name, path) {
if alreadyImportsNamed(f, name, path) {
return nil, nil
}
@ -198,8 +198,19 @@ func isThirdParty(importPath string) bool {
return strings.Contains(importPath, ".")
}
// alreadyImports reports whether f has an import with the specified name and path.
func alreadyImports(f *ast.File, name, path string) bool {
// alreadyImports reports whether f has an import with the specified path.
func alreadyImports(f *ast.File, path string) bool {
for _, s := range f.Imports {
if importPath(s) == path {
return true
}
}
return false
}
// alreadyImportsNamed reports whether f has an import with the specified name
// and path.
func alreadyImportsNamed(f *ast.File, name, path string) bool {
for _, s := range f.Imports {
if importName(s) == name && importPath(s) == path {
return true

View File

@ -8,6 +8,8 @@ import (
f "golang.org/x/tools/internal/lsp/foo"
)
var FooStruct f.StructFoo
func Baz() {
defer bar.Bar() //@complete("B", Bar)
// TODO(rstambler): Test completion here.

View File

@ -1,6 +1,6 @@
-- summary --
CompletionsCount = 213
CompletionSnippetCount = 39
CompletionSnippetCount = 40
UnimportedCompletionsCount = 1
DeepCompletionsCount = 5
FuzzyCompletionsCount = 7

View File

@ -0,0 +1,11 @@
package unimported
import "golang.org/x/tools/internal/lsp/baz"
func _() {
foo.StructFoo{} //@item(litFooStructFoo, "foo.StructFoo{}", "struct{...}", "struct")
// We get the literal completion for "foo.StructFoo{}" even though we haven't
// imported "foo" yet.
baz.FooStruct = f //@snippet(" //", litFooStructFoo, "foo.StructFoo{$0\\}", "foo.StructFoo{$0\\}")
}