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:
parent
f45d5df211
commit
3d643c64ae
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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(
|
||||
|
@ -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")
|
||||
})
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
164
internal/lsp/source/completion_literal.go
Normal file
164
internal/lsp/source/completion_literal.go
Normal 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,
|
||||
})
|
||||
}
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
98
internal/lsp/testdata/snippets/literal_snippets.go
vendored
Normal file
98
internal/lsp/testdata/snippets/literal_snippets.go
vendored
Normal 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)
|
||||
}
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user