1
0
mirror of https://github.com/golang/go synced 2024-11-18 12:14:42 -07:00

internal/lsp: add literal completion candidates

Add support for literal completion candidates such as "[]int{}" or
"make([]int, 0)". We support both named and unnamed types. I used the
existing type matching logic, so, for example, if the expected type is
an interface, we will suggest literal candidates that implement the
interface.

The literal candidates have a lower score than normal matching
candidates, so they shouldn't be disruptive in cases where you don't
want a literal candidate.

This commit adds support for slice, array, struct, map, and channel
literal candidates since they are pretty similar. Functions will be
supported in a subsequent commit.

I also added support for setting a snippet's final tab stop. This is
useful if you want the cursor to end up somewhere other than the
character after the snippet.

Change-Id: Id3b74260fff4d61703989b422267021b00cec005
Reviewed-on: https://go-review.googlesource.com/c/tools/+/193698
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
Muir Manders 2019-09-04 16:12:37 -07:00 committed by Rebecca Stambler
parent f45d5df211
commit 3d643c64ae
10 changed files with 323 additions and 8 deletions

View File

@ -73,6 +73,12 @@ func (s *Server) toProtocolCompletionItems(candidates []source.CompletionItem, r
insertText = candidate.Snippet()
}
// This can happen if the client has snippets disabled but the
// candidate only supports snippet insertion.
if insertText == "" {
continue
}
item := protocol.CompletionItem{
Label: candidate.Label,
Detail: candidate.Detail,

View File

@ -180,7 +180,7 @@ func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests
}
}
if got == nil {
t.Fatalf("%s: couldn't find completion matching %q", src.URI(), wantItem.Label)
t.Fatalf("%s:%d: couldn't find completion matching %q", src.URI(), src.Start().Line(), wantItem.Label)
}
var expected string
if usePlaceholders {

View File

@ -50,6 +50,13 @@ func (b *Builder) WritePlaceholder(fn func(*Builder)) {
b.sb.WriteByte('}')
}
// WriteFinalTabstop marks where cursor ends up after the user has
// cycled through all the normal tab stops. It defaults to the
// character after the snippet.
func (b *Builder) WriteFinalTabstop() {
fmt.Fprint(&b.sb, "$0")
}
// In addition to '\', '}', and '$', snippet choices also use '|' and ',' as
// meta characters, so they must be escaped within the choices.
var choiceReplacer = strings.NewReplacer(

View File

@ -48,4 +48,9 @@ func TestSnippetBuilder(t *testing.T) {
expect(`${1|one,{ \} \$ \| " \, / \\,three|}`, func(b *Builder) {
b.WriteChoice([]string{"one", `{ } $ | " , / \`, "three"})
})
expect("$0 hello", func(b *Builder) {
b.WriteFinalTabstop()
b.WriteText(" hello")
})
}

View File

@ -306,8 +306,12 @@ func (c *completer) found(obj types.Object, score float64, imp *imports.ImportIn
imp: imp,
}
if c.matchingType(&cand) {
if c.matchingCandidate(&cand) {
cand.score *= highScore
} 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())
}
// Favor shallow matches by lowering weight according to depth.
@ -651,6 +655,18 @@ 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
// "[]int{}".
if _, named := c.expectedType.objType.(*types.Named); !named {
c.literal(c.expectedType.objType)
}
}
return nil
}
@ -880,6 +896,10 @@ type typeInference struct {
// objType is the desired type of an object used at the query position.
objType types.Type
// variadic is true if objType is a slice type from an initial
// variadic param.
variadic bool
// wantTypeName is true if we expect the name of a type.
wantTypeName bool
@ -907,6 +927,7 @@ func expectedType(c *completer) typeInference {
var (
modifiers []typeModifier
variadic bool
typ types.Type
convertibleTo types.Type
)
@ -958,6 +979,7 @@ Nodes:
i = sig.Params().Len() - 1
}
typ = sig.Params().At(i).Type()
variadic = sig.Variadic() && i == sig.Params().Len()-1
break Nodes
}
}
@ -1033,6 +1055,7 @@ Nodes:
}
return typeInference{
variadic: variadic,
objType: typ,
modifiers: modifiers,
convertibleTo: convertibleTo,
@ -1176,9 +1199,15 @@ Nodes:
}
}
// matchingType reports whether an object is a good completion candidate
// in the context of the expected type.
func (c *completer) matchingType(cand *candidate) bool {
// matchingType reports whether a type matches the expected type.
func (c *completer) matchingType(T types.Type) bool {
fakeObj := types.NewVar(token.NoPos, c.pkg.GetTypes(), "", T)
return c.matchingCandidate(&candidate{obj: fakeObj})
}
// matchingCandidate reports whether a candidate matches our type
// inferences.
func (c *completer) matchingCandidate(cand *candidate) bool {
if isTypeName(cand.obj) {
return c.matchingTypeName(cand)
}

View File

@ -0,0 +1,164 @@
// 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 (
"go/types"
"strings"
"golang.org/x/tools/internal/lsp/snippet"
)
// literal generates composite literal and make() completion items.
func (c *completer) literal(literalType types.Type) {
if c.expectedType.objType == nil {
return
}
// Don't provide literal candidates for variadic function arguments.
// For example, don't provide "[]interface{}{}" in "fmt.Print(<>)".
if c.expectedType.variadic {
return
}
// Avoid literal candidates if the expected type is an empty
// interface. It isn't very useful to suggest a literal candidate of
// every possible type.
if isEmptyInterface(c.expectedType.objType) {
return
}
// We handle unnamed literal completions explicitly before searching
// for candidates. Avoid named-type literal completions for
// unnamed-type expected type since that results in duplicate
// candidates. For example, in
//
// type mySlice []int
// var []int = <>
//
// don't offer "mySlice{}" since we have already added a candidate
// of "[]int{}".
if _, named := literalType.(*types.Named); named {
if _, named := deref(c.expectedType.objType).(*types.Named); !named {
return
}
}
// Check if an object of type literalType or *literalType would
// match our expected type.
if !c.matchingType(literalType) {
literalType = types.NewPointer(literalType)
if !c.matchingType(literalType) {
return
}
}
ptr, isPointer := literalType.(*types.Pointer)
if isPointer {
literalType = ptr.Elem()
}
typeName := types.TypeString(literalType, c.qf)
// A type name of "[]int" doesn't work very will with the matcher
// since "[" isn't a valid identifier prefix. Here we strip off the
// slice (and array) prefix yielding just "int".
matchName := typeName
switch t := literalType.(type) {
case *types.Slice:
matchName = types.TypeString(t.Elem(), c.qf)
case *types.Array:
matchName = types.TypeString(t.Elem(), c.qf)
}
// If prefix matches the type name, client may want a composite literal.
if score := c.matcher.Score(matchName); score >= 0 {
if isPointer {
typeName = "&" + typeName
}
switch t := literalType.Underlying().(type) {
case *types.Struct, *types.Array, *types.Slice, *types.Map:
c.compositeLiteral(t, typeName, float64(score))
}
}
// If prefix matches "make", client may want a "make()"
// invocation. We also include the type name to allow for more
// flexible fuzzy matching.
if score := c.matcher.Score("make." + matchName); !isPointer && score >= 0 {
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))
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))
}
}
}
// literalCandidateScore is the base score for literal candidates.
// Literal candidates match the expected type so they should be high
// scoring, but we want them ranked below lexical objects of the
// correct type, so scale down highScore.
const literalCandidateScore = highScore / 2
// compositeLiteral adds a composite literal completion item for the given typeName.
func (c *completer) compositeLiteral(T types.Type, typeName string, matchScore float64) {
snip := &snippet.Builder{}
snip.WriteText(typeName + "{")
// Don't put the tab stop inside the composite literal curlies "{}"
// for structs that have no fields.
if strct, ok := T.(*types.Struct); !ok || strct.NumFields() > 0 {
snip.WriteFinalTabstop()
}
snip.WriteText("}")
nonSnippet := typeName + "{}"
c.items = append(c.items, CompletionItem{
Label: nonSnippet,
InsertText: nonSnippet,
Score: matchScore * literalCandidateScore,
Kind: VariableCompletionItem,
snippet: snip,
})
}
// makeCall adds a completion item for a "make()" call given a specific type.
func (c *completer) makeCall(typeName string, secondArg string, matchScore float64) {
// Keep it simple and don't add any placeholders for optional "make()" arguments.
snip := &snippet.Builder{}
snip.WriteText("make(" + typeName)
if secondArg != "" {
snip.WriteText(", ")
snip.WritePlaceholder(func(b *snippet.Builder) {
if c.opts.Placeholders {
b.WriteText(secondArg)
}
})
}
snip.WriteText(")")
var nonSnippet strings.Builder
nonSnippet.WriteString("make(" + typeName)
if secondArg != "" {
nonSnippet.WriteString(", ")
nonSnippet.WriteString(secondArg)
}
nonSnippet.WriteByte(')')
c.items = append(c.items, CompletionItem{
Label: nonSnippet.String(),
InsertText: nonSnippet.String(),
Score: matchScore * literalCandidateScore,
Kind: FunctionCompletionItem,
snippet: snip,
})
}

View File

@ -329,6 +329,11 @@ func isFunc(obj types.Object) bool {
return ok
}
func isEmptyInterface(T types.Type) bool {
intf, _ := T.(*types.Interface)
return intf != nil && intf.NumMethods() == 0
}
// typeConversion returns the type being converted to if call is a type
// conversion expression.
func typeConversion(call *ast.CallExpr, info *types.Info) types.Type {

View File

@ -7,7 +7,8 @@ type ncBar struct { //@item(structNCBar, "ncBar", "struct{...}", "struct")
}
func _() {
[]ncFoo{} //@item(litNCFoo, "[]ncFoo{}", "", "var")
_ := ncBar{
baz: [] //@complete(" //", structNCBar, structNCFoo)
baz: [] //@complete(" //", litNCFoo, structNCBar, structNCFoo)
}
}

View File

@ -0,0 +1,98 @@
package snippets
func _() {
[]int{} //@item(litIntSlice, "[]int{}", "", "var")
make([]int, 0) //@item(makeIntSlice, "make([]int, 0)", "", "func")
var slice []int
slice = i //@snippet(" //", litIntSlice, "[]int{$0\\}", "[]int{$0\\}")
slice = m //@snippet(" //", makeIntSlice, "make([]int, ${1:})", "make([]int, ${1:0})")
}
func _() {
type namedInt []int
namedInt{} //@item(litNamedSlice, "namedInt{}", "", "var")
make(namedInt, 0) //@item(makeNamedSlice, "make(namedInt, 0)", "", "func")
var namedSlice namedInt
namedSlice = n //@snippet(" //", litNamedSlice, "namedInt{$0\\}", "namedInt{$0\\}")
namedSlice = m //@snippet(" //", makeNamedSlice, "make(namedInt, ${1:})", "make(namedInt, ${1:0})")
}
func _() {
make(chan int) //@item(makeChan, "make(chan int)", "", "func")
var ch chan int
ch = m //@snippet(" //", makeChan, "make(chan int)", "make(chan int)")
}
func _() {
map[string]struct{}{} //@item(litMap, "map[string]struct{}{}", "", "var")
make(map[string]struct{}) //@item(makeMap, "make(map[string]struct{})", "", "func")
var m map[string]struct{}
m = m //@snippet(" //", litMap, "map[string]struct{\\}{$0\\}", "map[string]struct{\\}{$0\\}")
m = m //@snippet(" //", makeMap, "make(map[string]struct{\\})", "make(map[string]struct{\\})")
struct{}{} //@item(litEmptyStruct, "struct{}{}", "", "var")
m["hi"] = s //@snippet(" //", litEmptyStruct, "struct{\\}{\\}", "struct{\\}{\\}")
}
func _() {
type myStruct struct{ i int }
myStruct{} //@item(litStruct, "myStruct{}", "", "var")
&myStruct{} //@item(litStructPtr, "&myStruct{}", "", "var")
var ms myStruct
ms = m //@snippet(" //", litStruct, "myStruct{$0\\}", "myStruct{$0\\}")
var msPtr *myStruct
msPtr = m //@snippet(" //", litStructPtr, "&myStruct{$0\\}", "&myStruct{$0\\}")
msPtr = &m //@snippet(" //", litStruct, "myStruct{$0\\}", "myStruct{$0\\}")
}
type myImpl struct{}
func (myImpl) foo() {}
func (*myImpl) bar() {}
func _() {
type myIntf interface {
foo()
}
myImpl{} //@item(litImpl, "myImpl{}", "", "var")
var mi myIntf
mi = m //@snippet(" //", litImpl, "myImpl{\\}", "myImpl{\\}")
// only satisfied by pointer to myImpl
type myPtrIntf interface {
bar()
}
&myImpl{} //@item(litImplPtr, "&myImpl{}", "", "var")
var mpi myPtrIntf
mpi = m //@snippet(" //", litImplPtr, "&myImpl{\\}", "&myImpl{\\}")
}
func _() {
var s struct{ i []int } //@item(litSliceField, "i", "[]int", "field")
var foo []int
// no literal completions after selector
foo = s.i //@complete(" //", litSliceField)
}
func _() {
type myStruct struct{ i int } //@item(litStructType, "myStruct", "struct{...}", "struct")
foo := func(s string, args ...myStruct) {}
// Don't give literal slice candidate for variadic arg.
foo("", myStruct) //@complete(")", litStructType)
}

View File

@ -30,8 +30,8 @@ import (
// We hardcode the expected number of test cases to ensure that all tests
// are being executed. If a test is added, this number must be changed.
const (
ExpectedCompletionsCount = 162
ExpectedCompletionSnippetCount = 16
ExpectedCompletionsCount = 164
ExpectedCompletionSnippetCount = 29
ExpectedDiagnosticsCount = 21
ExpectedFormatCount = 6
ExpectedImportCount = 2