mirror of
https://github.com/golang/go.git
synced 2025-05-05 15:43:04 +00: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()
|
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{
|
item := protocol.CompletionItem{
|
||||||
Label: candidate.Label,
|
Label: candidate.Label,
|
||||||
Detail: candidate.Detail,
|
Detail: candidate.Detail,
|
||||||
|
@ -180,7 +180,7 @@ func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if got == nil {
|
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
|
var expected string
|
||||||
if usePlaceholders {
|
if usePlaceholders {
|
||||||
|
@ -50,6 +50,13 @@ func (b *Builder) WritePlaceholder(fn func(*Builder)) {
|
|||||||
b.sb.WriteByte('}')
|
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
|
// In addition to '\', '}', and '$', snippet choices also use '|' and ',' as
|
||||||
// meta characters, so they must be escaped within the choices.
|
// meta characters, so they must be escaped within the choices.
|
||||||
var choiceReplacer = strings.NewReplacer(
|
var choiceReplacer = strings.NewReplacer(
|
||||||
|
@ -48,4 +48,9 @@ func TestSnippetBuilder(t *testing.T) {
|
|||||||
expect(`${1|one,{ \} \$ \| " \, / \\,three|}`, func(b *Builder) {
|
expect(`${1|one,{ \} \$ \| " \, / \\,three|}`, func(b *Builder) {
|
||||||
b.WriteChoice([]string{"one", `{ } $ | " , / \`, "three"})
|
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,
|
imp: imp,
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.matchingType(&cand) {
|
if c.matchingCandidate(&cand) {
|
||||||
cand.score *= highScore
|
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.
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -880,6 +896,10 @@ type typeInference struct {
|
|||||||
// objType is the desired type of an object used at the query position.
|
// objType is the desired type of an object used at the query position.
|
||||||
objType types.Type
|
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 is true if we expect the name of a type.
|
||||||
wantTypeName bool
|
wantTypeName bool
|
||||||
|
|
||||||
@ -907,6 +927,7 @@ func expectedType(c *completer) typeInference {
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
modifiers []typeModifier
|
modifiers []typeModifier
|
||||||
|
variadic bool
|
||||||
typ types.Type
|
typ types.Type
|
||||||
convertibleTo types.Type
|
convertibleTo types.Type
|
||||||
)
|
)
|
||||||
@ -958,6 +979,7 @@ Nodes:
|
|||||||
i = sig.Params().Len() - 1
|
i = sig.Params().Len() - 1
|
||||||
}
|
}
|
||||||
typ = sig.Params().At(i).Type()
|
typ = sig.Params().At(i).Type()
|
||||||
|
variadic = sig.Variadic() && i == sig.Params().Len()-1
|
||||||
break Nodes
|
break Nodes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1033,6 +1055,7 @@ Nodes:
|
|||||||
}
|
}
|
||||||
|
|
||||||
return typeInference{
|
return typeInference{
|
||||||
|
variadic: variadic,
|
||||||
objType: typ,
|
objType: typ,
|
||||||
modifiers: modifiers,
|
modifiers: modifiers,
|
||||||
convertibleTo: convertibleTo,
|
convertibleTo: convertibleTo,
|
||||||
@ -1176,9 +1199,15 @@ Nodes:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// matchingType reports whether an object is a good completion candidate
|
// matchingType reports whether a type matches the expected type.
|
||||||
// in the context of the expected type.
|
func (c *completer) matchingType(T types.Type) bool {
|
||||||
func (c *completer) matchingType(cand *candidate) 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) {
|
if isTypeName(cand.obj) {
|
||||||
return c.matchingTypeName(cand)
|
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
|
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
|
// typeConversion returns the type being converted to if call is a type
|
||||||
// conversion expression.
|
// conversion expression.
|
||||||
func typeConversion(call *ast.CallExpr, info *types.Info) types.Type {
|
func typeConversion(call *ast.CallExpr, info *types.Info) types.Type {
|
||||||
|
@ -7,7 +7,8 @@ type ncBar struct { //@item(structNCBar, "ncBar", "struct{...}", "struct")
|
|||||||
}
|
}
|
||||||
|
|
||||||
func _() {
|
func _() {
|
||||||
|
[]ncFoo{} //@item(litNCFoo, "[]ncFoo{}", "", "var")
|
||||||
_ := ncBar{
|
_ := 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
|
// 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.
|
// are being executed. If a test is added, this number must be changed.
|
||||||
const (
|
const (
|
||||||
ExpectedCompletionsCount = 162
|
ExpectedCompletionsCount = 164
|
||||||
ExpectedCompletionSnippetCount = 16
|
ExpectedCompletionSnippetCount = 29
|
||||||
ExpectedDiagnosticsCount = 21
|
ExpectedDiagnosticsCount = 21
|
||||||
ExpectedFormatCount = 6
|
ExpectedFormatCount = 6
|
||||||
ExpectedImportCount = 2
|
ExpectedImportCount = 2
|
||||||
|
Loading…
x
Reference in New Issue
Block a user