mirror of
https://github.com/golang/go.git
synced 2025-05-05 23:53:05 +00:00
internal/lsp: add struct literal field snippets
Now when you accept a struct literal field name completion, you will get a snippet that includes the colon, a tab stop, and a comma if the literal is multi-line. If you have "gopls.usePlaceholders" enabled, you will get a placeholder with the field's type as well. I pushed snippet generation into the "source" package so ast and type info is available. This allows for smarter, more context aware snippet generation. For example, this let me fix an issue with the function snippets where "foo<>()" was completing to "foo(<>)()". Now we don't add the function call snippet if the position is already in a CallExpr. I also added a new "Insert" field to CompletionItem to store the plain object name. This way, we don't have to undo label decorations when generating the insert text for the completion response. I also changed "filterText" to use this "Insert" field since you don't want the filter text to include the extra label decorations. Fixes golang/go#31556 Change-Id: I75266b2a4c0fe4036c44b315582f51738e464a39 GitHub-Last-Rev: 1ec28b2395c7bbe748940befe8c38579f5d75f61 GitHub-Pull-Request: golang/tools#89 Reviewed-on: https://go-review.googlesource.com/c/tools/+/173577 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
550556f78a
commit
c6e1543aba
@ -38,7 +38,7 @@ func testCommandLine(t *testing.T, exporter packagestest.Exporter) {
|
||||
tests.Run(t, r, data)
|
||||
}
|
||||
|
||||
func (r *runner) Completion(t *testing.T, data tests.Completions, items tests.CompletionItems) {
|
||||
func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests.CompletionSnippets, items tests.CompletionItems) {
|
||||
//TODO: add command line completions tests when it works
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,6 @@
|
||||
package lsp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
@ -52,10 +51,25 @@ func toProtocolCompletionItems(candidates []source.CompletionItem, prefix string
|
||||
if !strings.HasPrefix(candidate.Label, prefix) {
|
||||
continue
|
||||
}
|
||||
insertText := labelToInsertText(candidate.Label, candidate.Kind, insertTextFormat, usePlaceholders)
|
||||
|
||||
insertText := candidate.Insert
|
||||
if insertTextFormat == protocol.SnippetTextFormat {
|
||||
if usePlaceholders && candidate.PlaceholderSnippet != nil {
|
||||
insertText = candidate.PlaceholderSnippet.String()
|
||||
} else if candidate.PlainSnippet != nil {
|
||||
insertText = candidate.PlainSnippet.String()
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(insertText, prefix) {
|
||||
insertText = insertText[len(prefix):]
|
||||
}
|
||||
|
||||
filterText := candidate.Insert
|
||||
if strings.HasPrefix(filterText, prefix) {
|
||||
filterText = filterText[len(prefix):]
|
||||
}
|
||||
|
||||
item := protocol.CompletionItem{
|
||||
Label: candidate.Label,
|
||||
Detail: candidate.Detail,
|
||||
@ -72,7 +86,7 @@ func toProtocolCompletionItems(candidates []source.CompletionItem, prefix string
|
||||
// according to their score. This can be removed upon the resolution of
|
||||
// https://github.com/Microsoft/language-server-protocol/issues/348.
|
||||
SortText: fmt.Sprintf("%05d", i),
|
||||
FilterText: insertText,
|
||||
FilterText: filterText,
|
||||
Preselect: i == 0,
|
||||
}
|
||||
// Trigger signature help for any function or method completion.
|
||||
@ -113,53 +127,3 @@ func toProtocolCompletionItemKind(kind source.CompletionItemKind) protocol.Compl
|
||||
return protocol.TextCompletion
|
||||
}
|
||||
}
|
||||
|
||||
func labelToInsertText(label string, kind source.CompletionItemKind, insertTextFormat protocol.InsertTextFormat, usePlaceholders bool) string {
|
||||
switch kind {
|
||||
case source.ConstantCompletionItem:
|
||||
// The label for constants is of the format "<identifier> = <value>".
|
||||
// We should not insert the " = <value>" part of the label.
|
||||
if i := strings.Index(label, " ="); i >= 0 {
|
||||
return label[:i]
|
||||
}
|
||||
case source.FunctionCompletionItem, source.MethodCompletionItem:
|
||||
var trimmed, params string
|
||||
if i := strings.Index(label, "("); i >= 0 {
|
||||
trimmed = label[:i]
|
||||
params = strings.Trim(label[i:], "()")
|
||||
}
|
||||
if params == "" || trimmed == "" {
|
||||
return label
|
||||
}
|
||||
// Don't add parameters or parens for the plaintext insert format.
|
||||
if insertTextFormat == protocol.PlainTextTextFormat {
|
||||
return trimmed
|
||||
}
|
||||
// If we don't want to use placeholders, just add 2 parentheses with
|
||||
// the cursor in the middle.
|
||||
if !usePlaceholders {
|
||||
return trimmed + "($1)"
|
||||
}
|
||||
// If signature help is not enabled, we should give the user parameters
|
||||
// that they can tab through. The insert text format follows the
|
||||
// specification defined by Microsoft for LSP. The "$", "}, and "\"
|
||||
// characters should be escaped.
|
||||
r := strings.NewReplacer(
|
||||
`\`, `\\`,
|
||||
`}`, `\}`,
|
||||
`$`, `\$`,
|
||||
)
|
||||
b := bytes.NewBufferString(trimmed)
|
||||
b.WriteByte('(')
|
||||
for i, p := range strings.Split(params, ",") {
|
||||
if i != 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
fmt.Fprintf(b, "${%v:%v}", i+1, r.Replace(strings.TrimSpace(p)))
|
||||
}
|
||||
b.WriteByte(')')
|
||||
return b.String()
|
||||
|
||||
}
|
||||
return label
|
||||
}
|
||||
|
@ -130,26 +130,15 @@ func summarizeDiagnostics(i int, want []source.Diagnostic, got []source.Diagnost
|
||||
return msg.String()
|
||||
}
|
||||
|
||||
func (r *runner) Completion(t *testing.T, data tests.Completions, items tests.CompletionItems) {
|
||||
func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests.CompletionSnippets, items tests.CompletionItems) {
|
||||
for src, itemList := range data {
|
||||
var want []source.CompletionItem
|
||||
for _, pos := range itemList {
|
||||
want = append(want, *items[pos])
|
||||
}
|
||||
list, err := r.server.Completion(context.Background(), &protocol.CompletionParams{
|
||||
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
|
||||
TextDocument: protocol.TextDocumentIdentifier{
|
||||
URI: protocol.NewURI(src.URI()),
|
||||
},
|
||||
Position: protocol.Position{
|
||||
Line: float64(src.Start().Line() - 1),
|
||||
Character: float64(src.Start().Column() - 1),
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
list := r.runCompletion(t, src)
|
||||
|
||||
wantBuiltins := strings.Contains(string(src.URI()), "builtins")
|
||||
var got []protocol.CompletionItem
|
||||
for _, item := range list.Items {
|
||||
@ -158,30 +147,79 @@ func (r *runner) Completion(t *testing.T, data tests.Completions, items tests.Co
|
||||
}
|
||||
got = append(got, item)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("completion failed for %v: %v", src, err)
|
||||
}
|
||||
if diff := diffCompletionItems(t, src, want, got); diff != "" {
|
||||
t.Errorf("%s: %s", src, diff)
|
||||
}
|
||||
}
|
||||
// Make sure we don't crash completing the first position in file set.
|
||||
firstFile := r.data.Config.Fset.Position(1).Filename
|
||||
firstPos, err := span.NewRange(r.data.Exported.ExpectFileSet, 1, 2).Span()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = r.runCompletion(t, firstPos)
|
||||
|
||||
_, err := r.server.Completion(context.Background(), &protocol.CompletionParams{
|
||||
r.checkCompletionSnippets(t, snippets, items)
|
||||
}
|
||||
|
||||
func (r *runner) checkCompletionSnippets(t *testing.T, data tests.CompletionSnippets, items tests.CompletionItems) {
|
||||
origPlaceHolders := r.server.usePlaceholders
|
||||
origTextFormat := r.server.insertTextFormat
|
||||
defer func() {
|
||||
r.server.usePlaceholders = origPlaceHolders
|
||||
r.server.insertTextFormat = origTextFormat
|
||||
}()
|
||||
|
||||
r.server.insertTextFormat = protocol.SnippetTextFormat
|
||||
for _, usePlaceholders := range []bool{true, false} {
|
||||
r.server.usePlaceholders = usePlaceholders
|
||||
|
||||
for src, want := range data {
|
||||
list := r.runCompletion(t, src)
|
||||
|
||||
wantCompletion := items[want.CompletionItem]
|
||||
var gotItem *protocol.CompletionItem
|
||||
for _, item := range list.Items {
|
||||
if item.Label == wantCompletion.Label {
|
||||
gotItem = &item
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if gotItem == nil {
|
||||
t.Fatalf("%s: couldn't find completion matching %q", src.URI(), wantCompletion.Label)
|
||||
}
|
||||
|
||||
var expected string
|
||||
if usePlaceholders {
|
||||
expected = want.PlaceholderSnippet
|
||||
} else {
|
||||
expected = want.PlainSnippet
|
||||
}
|
||||
|
||||
if expected != gotItem.TextEdit.NewText {
|
||||
t.Errorf("%s: expected snippet %q, got %q", src, expected, gotItem.TextEdit.NewText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *runner) runCompletion(t *testing.T, src span.Span) *protocol.CompletionList {
|
||||
t.Helper()
|
||||
list, err := r.server.Completion(context.Background(), &protocol.CompletionParams{
|
||||
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
|
||||
TextDocument: protocol.TextDocumentIdentifier{
|
||||
URI: protocol.NewURI(span.FileURI(firstFile)),
|
||||
URI: protocol.NewURI(src.URI()),
|
||||
},
|
||||
Position: protocol.Position{
|
||||
Line: 0,
|
||||
Character: 0,
|
||||
Line: float64(src.Start().Line() - 1),
|
||||
Character: float64(src.Start().Column() - 1),
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func isBuiltin(item protocol.CompletionItem) bool {
|
||||
|
@ -12,12 +12,35 @@ import (
|
||||
"go/types"
|
||||
|
||||
"golang.org/x/tools/go/ast/astutil"
|
||||
"golang.org/x/tools/internal/lsp/snippet"
|
||||
)
|
||||
|
||||
type CompletionItem struct {
|
||||
Label, Detail string
|
||||
Kind CompletionItemKind
|
||||
Score float64
|
||||
// Label is the primary text the user sees for this completion item.
|
||||
Label string
|
||||
|
||||
// Detail is supplemental information to present to the user. This
|
||||
// often contains the Go type of the completion item.
|
||||
Detail string
|
||||
|
||||
// Insert is the text to insert if this item is selected. Any already-typed
|
||||
// prefix has not been trimmed. Insert does not contain snippets.
|
||||
Insert string
|
||||
|
||||
Kind CompletionItemKind
|
||||
|
||||
// Score is the internal relevance score. Higher is more relevant.
|
||||
Score float64
|
||||
|
||||
// PlainSnippet is the LSP snippet to be inserted if not nil and snippets are
|
||||
// enabled and placeholders are not desired. This can contain tabs stops, but
|
||||
// should not contain placeholder text.
|
||||
PlainSnippet *snippet.Builder
|
||||
|
||||
// PlaceholderSnippet is the LSP snippet to be inserted if not nil and
|
||||
// snippets are enabled and placeholders are desired. This can contain
|
||||
// placeholder text.
|
||||
PlaceholderSnippet *snippet.Builder
|
||||
}
|
||||
|
||||
type CompletionItemKind int
|
||||
@ -54,6 +77,7 @@ type completer struct {
|
||||
types *types.Package
|
||||
info *types.Info
|
||||
qf types.Qualifier
|
||||
fset *token.FileSet
|
||||
|
||||
// pos is the position at which the request was triggered.
|
||||
pos token.Pos
|
||||
@ -80,6 +104,15 @@ type completer struct {
|
||||
// preferTypeNames is true if we are completing at a position that expects a type,
|
||||
// not a value.
|
||||
preferTypeNames bool
|
||||
|
||||
// enclosingCompositeLiteral is the composite literal enclosing the position.
|
||||
enclosingCompositeLiteral *ast.CompositeLit
|
||||
|
||||
// enclosingKeyValue is the key value expression enclosing the position.
|
||||
enclosingKeyValue *ast.KeyValueExpr
|
||||
|
||||
// inCompositeLiteralField is true if we are completing a composite literal field.
|
||||
inCompositeLiteralField bool
|
||||
}
|
||||
|
||||
// found adds a candidate completion.
|
||||
@ -132,25 +165,29 @@ func Completion(ctx context.Context, f File, pos token.Pos) ([]CompletionItem, s
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
cl, kv, clField := enclosingCompositeLiteral(path, pos)
|
||||
c := &completer{
|
||||
types: pkg.GetTypes(),
|
||||
info: pkg.GetTypesInfo(),
|
||||
qf: qualifier(file, pkg.GetTypes(), pkg.GetTypesInfo()),
|
||||
path: path,
|
||||
pos: pos,
|
||||
seen: make(map[types.Object]bool),
|
||||
expectedType: expectedType(path, pos, pkg.GetTypesInfo()),
|
||||
enclosingFunction: enclosingFunction(path, pos, pkg.GetTypesInfo()),
|
||||
preferTypeNames: preferTypeNames(path, pos),
|
||||
types: pkg.GetTypes(),
|
||||
info: pkg.GetTypesInfo(),
|
||||
qf: qualifier(file, pkg.GetTypes(), pkg.GetTypesInfo()),
|
||||
fset: f.GetFileSet(ctx),
|
||||
path: path,
|
||||
pos: pos,
|
||||
seen: make(map[types.Object]bool),
|
||||
expectedType: expectedType(path, pos, pkg.GetTypesInfo()),
|
||||
enclosingFunction: enclosingFunction(path, pos, pkg.GetTypesInfo()),
|
||||
preferTypeNames: preferTypeNames(path, pos),
|
||||
enclosingCompositeLiteral: cl,
|
||||
enclosingKeyValue: kv,
|
||||
inCompositeLiteralField: clField,
|
||||
}
|
||||
|
||||
// Composite literals are handled entirely separately.
|
||||
if lit, kv, ok := c.enclosingCompositeLiteral(); lit != nil {
|
||||
c.expectedType = c.expectedCompositeLiteralType(lit, kv)
|
||||
if c.enclosingCompositeLiteral != nil {
|
||||
c.expectedType = c.expectedCompositeLiteralType(c.enclosingCompositeLiteral, c.enclosingKeyValue)
|
||||
|
||||
// ok means that we should return composite literal completions for this position.
|
||||
if ok {
|
||||
if err := c.compositeLiteral(lit, kv); err != nil {
|
||||
if c.inCompositeLiteralField {
|
||||
if err := c.compositeLiteral(c.enclosingCompositeLiteral, c.enclosingKeyValue); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return c.items, c.prefix, nil
|
||||
@ -363,8 +400,8 @@ func (c *completer) compositeLiteral(lit *ast.CompositeLit, kv *ast.KeyValueExpr
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *completer) enclosingCompositeLiteral() (lit *ast.CompositeLit, kv *ast.KeyValueExpr, ok bool) {
|
||||
for _, n := range c.path {
|
||||
func enclosingCompositeLiteral(path []ast.Node, pos token.Pos) (lit *ast.CompositeLit, kv *ast.KeyValueExpr, ok bool) {
|
||||
for _, n := range path {
|
||||
switch n := n.(type) {
|
||||
case *ast.CompositeLit:
|
||||
// The enclosing node will be a composite literal if the user has just
|
||||
@ -373,19 +410,19 @@ func (c *completer) enclosingCompositeLiteral() (lit *ast.CompositeLit, kv *ast.
|
||||
//
|
||||
// The position is not part of the composite literal unless it falls within the
|
||||
// curly braces (e.g. "foo.Foo<>Struct{}").
|
||||
if n.Lbrace <= c.pos && c.pos <= n.Rbrace {
|
||||
if n.Lbrace <= pos && pos <= n.Rbrace {
|
||||
lit = n
|
||||
|
||||
// If the cursor position is within a key-value expression inside the composite
|
||||
// literal, we try to determine if it is before or after the colon. If it is before
|
||||
// the colon, we return field completions. If the cursor does not belong to any
|
||||
// expression within the composite literal, we show composite literal completions.
|
||||
if expr, isKeyValue := exprAtPos(c.pos, n.Elts).(*ast.KeyValueExpr); kv == nil && isKeyValue {
|
||||
if expr, isKeyValue := exprAtPos(pos, n.Elts).(*ast.KeyValueExpr); kv == nil && isKeyValue {
|
||||
kv = expr
|
||||
|
||||
// If the position belongs to a key-value expression and is after the colon,
|
||||
// don't show composite literal completions.
|
||||
ok = c.pos <= kv.Colon
|
||||
ok = pos <= kv.Colon
|
||||
} else if kv == nil {
|
||||
ok = true
|
||||
}
|
||||
@ -397,7 +434,7 @@ func (c *completer) enclosingCompositeLiteral() (lit *ast.CompositeLit, kv *ast.
|
||||
|
||||
// If the position belongs to a key-value expression and is after the colon,
|
||||
// don't show composite literal completions.
|
||||
ok = c.pos <= kv.Colon
|
||||
ok = pos <= kv.Colon
|
||||
}
|
||||
case *ast.FuncType, *ast.CallExpr, *ast.TypeAssertExpr:
|
||||
// These node types break the type link between the leaf node and
|
||||
|
@ -5,18 +5,24 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/types"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/internal/lsp/snippet"
|
||||
)
|
||||
|
||||
// formatCompletion creates a completion item for a given types.Object.
|
||||
func (c *completer) item(obj types.Object, score float64) CompletionItem {
|
||||
label := obj.Name()
|
||||
detail := types.TypeString(obj.Type(), c.qf)
|
||||
var kind CompletionItemKind
|
||||
var (
|
||||
label = obj.Name()
|
||||
detail = types.TypeString(obj.Type(), c.qf)
|
||||
insert = label
|
||||
kind CompletionItemKind
|
||||
plainSnippet *snippet.Builder
|
||||
placeholderSnippet *snippet.Builder
|
||||
)
|
||||
|
||||
switch o := obj.(type) {
|
||||
case *types.TypeName:
|
||||
@ -40,6 +46,7 @@ func (c *completer) item(obj types.Object, score float64) CompletionItem {
|
||||
}
|
||||
if o.IsField() {
|
||||
kind = FieldCompletionItem
|
||||
plainSnippet, placeholderSnippet = c.structFieldSnippet(label, detail)
|
||||
} else if c.isParameter(o) {
|
||||
kind = ParameterCompletionItem
|
||||
} else {
|
||||
@ -47,12 +54,15 @@ func (c *completer) item(obj types.Object, score float64) CompletionItem {
|
||||
}
|
||||
case *types.Func:
|
||||
if sig, ok := o.Type().(*types.Signature); ok {
|
||||
label += formatParams(sig, c.qf)
|
||||
params := formatEachParam(sig, c.qf)
|
||||
label += formatParamParts(params)
|
||||
detail = strings.Trim(types.TypeString(sig.Results(), c.qf), "()")
|
||||
kind = FunctionCompletionItem
|
||||
if sig.Recv() != nil {
|
||||
kind = MethodCompletionItem
|
||||
}
|
||||
|
||||
plainSnippet, placeholderSnippet = c.funcCallSnippet(obj.Name(), params)
|
||||
}
|
||||
case *types.Builtin:
|
||||
item, ok := builtinDetails[obj.Name()]
|
||||
@ -71,10 +81,13 @@ func (c *completer) item(obj types.Object, score float64) CompletionItem {
|
||||
detail = strings.TrimPrefix(detail, "untyped ")
|
||||
|
||||
return CompletionItem{
|
||||
Label: label,
|
||||
Detail: detail,
|
||||
Kind: kind,
|
||||
Score: score,
|
||||
Label: label,
|
||||
Insert: insert,
|
||||
Detail: detail,
|
||||
Kind: kind,
|
||||
Score: score,
|
||||
PlainSnippet: plainSnippet,
|
||||
PlaceholderSnippet: placeholderSnippet,
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,28 +122,54 @@ func formatType(typ types.Type, qf types.Qualifier) (detail string, kind Complet
|
||||
return detail, kind
|
||||
}
|
||||
|
||||
// formatParams correctly format the parameters of a function.
|
||||
func formatParams(sig *types.Signature, qf types.Qualifier) string {
|
||||
var b bytes.Buffer
|
||||
// formatParams correctly formats the parameters of a function.
|
||||
func formatParams(sig *types.Signature, qualifier types.Qualifier) string {
|
||||
return formatParamParts(formatEachParam(sig, qualifier))
|
||||
}
|
||||
|
||||
func formatParamParts(params []string) string {
|
||||
totalLen := 2 // parens
|
||||
|
||||
// length of each param itself
|
||||
for _, p := range params {
|
||||
totalLen += len(p)
|
||||
}
|
||||
// length of ", " separator
|
||||
if len(params) > 1 {
|
||||
totalLen += 2 * (len(params) - 1)
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.Grow(totalLen)
|
||||
|
||||
b.WriteByte('(')
|
||||
for i := 0; i < sig.Params().Len(); i++ {
|
||||
for i, p := range params {
|
||||
if i > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
b.WriteString(p)
|
||||
}
|
||||
b.WriteByte(')')
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func formatEachParam(sig *types.Signature, qualifier types.Qualifier) []string {
|
||||
params := make([]string, 0, sig.Params().Len())
|
||||
for i := 0; i < sig.Params().Len(); i++ {
|
||||
el := sig.Params().At(i)
|
||||
typ := types.TypeString(el.Type(), qf)
|
||||
typ := types.TypeString(el.Type(), qualifier)
|
||||
// Handle a variadic parameter (can only be the final parameter).
|
||||
if sig.Variadic() && i == sig.Params().Len()-1 {
|
||||
typ = strings.Replace(typ, "[]", "...", 1)
|
||||
}
|
||||
if el.Name() == "" {
|
||||
fmt.Fprintf(&b, "%v", typ)
|
||||
params = append(params, typ)
|
||||
} else {
|
||||
fmt.Fprintf(&b, "%v %v", el.Name(), typ)
|
||||
params = append(params, el.Name()+" "+typ)
|
||||
}
|
||||
}
|
||||
b.WriteByte(')')
|
||||
return b.String()
|
||||
return params
|
||||
}
|
||||
|
||||
// qualifier returns a function that appropriately formats a types.PkgName
|
||||
|
100
internal/lsp/source/completion_snippet.go
Normal file
100
internal/lsp/source/completion_snippet.go
Normal file
@ -0,0 +1,100 @@
|
||||
// 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/ast"
|
||||
|
||||
"golang.org/x/tools/internal/lsp/snippet"
|
||||
)
|
||||
|
||||
// structField calculates the plain and placeholder snippets for struct literal
|
||||
// field names as in "Foo{Ba<>".
|
||||
func (c *completer) structFieldSnippet(label, detail string) (*snippet.Builder, *snippet.Builder) {
|
||||
if !c.inCompositeLiteralField {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cl := c.enclosingCompositeLiteral
|
||||
kv := c.enclosingKeyValue
|
||||
|
||||
// If we aren't in a composite literal or are already in a key/value
|
||||
// expression, we don't want a snippet.
|
||||
if cl == nil || kv != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if len(cl.Elts) > 0 {
|
||||
i := indexExprAtPos(c.pos, cl.Elts)
|
||||
if i >= len(cl.Elts) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// If our expression is not an identifer, we know it isn't a
|
||||
// struct field name.
|
||||
if _, ok := cl.Elts[i].(*ast.Ident); !ok {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// It is a multi-line literal if pos is not on the same line as the literal's
|
||||
// opening brace.
|
||||
multiLine := c.fset.Position(c.pos).Line != c.fset.Position(cl.Lbrace).Line
|
||||
|
||||
// Plain snippet will turn "Foo{Ba<>" into "Foo{Bar: <>"
|
||||
plain := &snippet.Builder{}
|
||||
plain.WriteText(label + ": ")
|
||||
plain.WritePlaceholder(nil)
|
||||
if multiLine {
|
||||
plain.WriteText(",")
|
||||
}
|
||||
|
||||
// Placeholder snippet will turn "Foo{Ba<>" into "Foo{Bar: *int*"
|
||||
placeholder := &snippet.Builder{}
|
||||
placeholder.WriteText(label + ": ")
|
||||
placeholder.WritePlaceholder(func(b *snippet.Builder) {
|
||||
b.WriteText(detail)
|
||||
})
|
||||
if multiLine {
|
||||
placeholder.WriteText(",")
|
||||
}
|
||||
|
||||
return plain, placeholder
|
||||
}
|
||||
|
||||
// funcCall calculates the plain and placeholder snippets for function calls.
|
||||
func (c *completer) funcCallSnippet(funcName string, params []string) (*snippet.Builder, *snippet.Builder) {
|
||||
for i := 1; i <= 2 && i < len(c.path); i++ {
|
||||
call, ok := c.path[i].(*ast.CallExpr)
|
||||
// If we are the left side (i.e. "Fun") part of a call expression,
|
||||
// we don't want a snippet since there are already parens present.
|
||||
if ok && call.Fun == c.path[i-1] {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Plain snippet turns "someFun<>" into "someFunc(<>)"
|
||||
plain := &snippet.Builder{}
|
||||
plain.WriteText(funcName + "(")
|
||||
if len(params) > 0 {
|
||||
plain.WritePlaceholder(nil)
|
||||
}
|
||||
plain.WriteText(")")
|
||||
|
||||
// Placeholder snippet turns "someFun<>" into "someFunc(*i int*, s string)"
|
||||
placeholder := &snippet.Builder{}
|
||||
placeholder.WriteText(funcName + "(")
|
||||
for i, p := range params {
|
||||
if i > 0 {
|
||||
placeholder.WriteText(", ")
|
||||
}
|
||||
placeholder.WritePlaceholder(func(b *snippet.Builder) {
|
||||
b.WriteText(p)
|
||||
})
|
||||
}
|
||||
placeholder.WriteText(")")
|
||||
|
||||
return plain, placeholder
|
||||
}
|
30
internal/lsp/testdata/snippets/snippets.go
vendored
Normal file
30
internal/lsp/testdata/snippets/snippets.go
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
package snippets
|
||||
|
||||
func foo(i int, b bool) {} //@item(snipFoo, "foo(i int, b bool)", "", "func")
|
||||
func bar(fn func()) func() {} //@item(snipBar, "bar(fn func())", "", "func")
|
||||
|
||||
type Foo struct {
|
||||
Bar int //@item(snipFieldBar, "Bar", "int", "field")
|
||||
}
|
||||
|
||||
func (Foo) Baz() func() {} //@item(snipMethodBaz, "Baz()", "func()", "field")
|
||||
|
||||
func _() {
|
||||
f //@snippet(" //", snipFoo, "oo(${1})", "oo(${1:i int}, ${2:b bool})")
|
||||
|
||||
bar //@snippet(" //", snipBar, "(${1})", "(${1:fn func()})")
|
||||
|
||||
bar(nil) //@snippet("(", snipBar, "", "")
|
||||
bar(ba) //@snippet(")", snipBar, "r(${1})", "r(${1:fn func()})")
|
||||
var f Foo
|
||||
bar(f.Ba) //@snippet(")", snipMethodBaz, "z()", "z()")
|
||||
|
||||
Foo{
|
||||
B //@snippet(" //", snipFieldBar, "ar: ${1},", "ar: ${1:int},")
|
||||
}
|
||||
|
||||
Foo{B} //@snippet("}", snipFieldBar, "ar: ${1}", "ar: ${1:int}")
|
||||
Foo{} //@snippet("}", snipFieldBar, "Bar: ${1}", "Bar: ${1:int}")
|
||||
|
||||
Foo{Foo{}.B} //@snippet("} ", snipFieldBar, "ar", "ar")
|
||||
}
|
@ -27,14 +27,15 @@ 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 = 85
|
||||
ExpectedDiagnosticsCount = 17
|
||||
ExpectedFormatCount = 4
|
||||
ExpectedDefinitionsCount = 21
|
||||
ExpectedTypeDefinitionsCount = 2
|
||||
ExpectedHighlightsCount = 2
|
||||
ExpectedSymbolsCount = 1
|
||||
ExpectedSignaturesCount = 19
|
||||
ExpectedCompletionsCount = 85
|
||||
ExpectedDiagnosticsCount = 17
|
||||
ExpectedFormatCount = 4
|
||||
ExpectedDefinitionsCount = 21
|
||||
ExpectedTypeDefinitionsCount = 2
|
||||
ExpectedHighlightsCount = 2
|
||||
ExpectedSymbolsCount = 1
|
||||
ExpectedSignaturesCount = 19
|
||||
ExpectedCompletionSnippetCount = 9
|
||||
)
|
||||
|
||||
const (
|
||||
@ -49,6 +50,7 @@ var updateGolden = flag.Bool("golden", false, "Update golden files")
|
||||
type Diagnostics map[span.URI][]source.Diagnostic
|
||||
type CompletionItems map[token.Pos]*source.CompletionItem
|
||||
type Completions map[span.Span][]token.Pos
|
||||
type CompletionSnippets map[span.Span]CompletionSnippet
|
||||
type Formats []span.Span
|
||||
type Definitions map[span.Span]Definition
|
||||
type Highlights map[string][]span.Span
|
||||
@ -57,17 +59,18 @@ type SymbolsChildren map[string][]source.Symbol
|
||||
type Signatures map[span.Span]source.SignatureInformation
|
||||
|
||||
type Data struct {
|
||||
Config packages.Config
|
||||
Exported *packagestest.Exported
|
||||
Diagnostics Diagnostics
|
||||
CompletionItems CompletionItems
|
||||
Completions Completions
|
||||
Formats Formats
|
||||
Definitions Definitions
|
||||
Highlights Highlights
|
||||
Symbols Symbols
|
||||
symbolsChildren SymbolsChildren
|
||||
Signatures Signatures
|
||||
Config packages.Config
|
||||
Exported *packagestest.Exported
|
||||
Diagnostics Diagnostics
|
||||
CompletionItems CompletionItems
|
||||
Completions Completions
|
||||
CompletionSnippets CompletionSnippets
|
||||
Formats Formats
|
||||
Definitions Definitions
|
||||
Highlights Highlights
|
||||
Symbols Symbols
|
||||
symbolsChildren SymbolsChildren
|
||||
Signatures Signatures
|
||||
|
||||
t testing.TB
|
||||
fragments map[string]string
|
||||
@ -76,7 +79,7 @@ type Data struct {
|
||||
|
||||
type Tests interface {
|
||||
Diagnostics(*testing.T, Diagnostics)
|
||||
Completion(*testing.T, Completions, CompletionItems)
|
||||
Completion(*testing.T, Completions, CompletionSnippets, CompletionItems)
|
||||
Format(*testing.T, Formats)
|
||||
Definition(*testing.T, Definitions)
|
||||
Highlight(*testing.T, Highlights)
|
||||
@ -92,18 +95,25 @@ type Definition struct {
|
||||
Match string
|
||||
}
|
||||
|
||||
type CompletionSnippet struct {
|
||||
CompletionItem token.Pos
|
||||
PlainSnippet string
|
||||
PlaceholderSnippet string
|
||||
}
|
||||
|
||||
func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data {
|
||||
t.Helper()
|
||||
|
||||
data := &Data{
|
||||
Diagnostics: make(Diagnostics),
|
||||
CompletionItems: make(CompletionItems),
|
||||
Completions: make(Completions),
|
||||
Definitions: make(Definitions),
|
||||
Highlights: make(Highlights),
|
||||
Symbols: make(Symbols),
|
||||
symbolsChildren: make(SymbolsChildren),
|
||||
Signatures: make(Signatures),
|
||||
Diagnostics: make(Diagnostics),
|
||||
CompletionItems: make(CompletionItems),
|
||||
Completions: make(Completions),
|
||||
CompletionSnippets: make(CompletionSnippets),
|
||||
Definitions: make(Definitions),
|
||||
Highlights: make(Highlights),
|
||||
Symbols: make(Symbols),
|
||||
symbolsChildren: make(SymbolsChildren),
|
||||
Signatures: make(Signatures),
|
||||
|
||||
t: t,
|
||||
dir: dir,
|
||||
@ -169,6 +179,7 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data {
|
||||
"highlight": data.collectHighlights,
|
||||
"symbol": data.collectSymbols,
|
||||
"signature": data.collectSignatures,
|
||||
"snippet": data.collectCompletionSnippets,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -188,7 +199,10 @@ func Run(t *testing.T, tests Tests, data *Data) {
|
||||
if len(data.Completions) != ExpectedCompletionsCount {
|
||||
t.Errorf("got %v completions expected %v", len(data.Completions), ExpectedCompletionsCount)
|
||||
}
|
||||
tests.Completion(t, data.Completions, data.CompletionItems)
|
||||
if len(data.CompletionSnippets) != ExpectedCompletionSnippetCount {
|
||||
t.Errorf("got %v snippets expected %v", len(data.CompletionSnippets), ExpectedCompletionSnippetCount)
|
||||
}
|
||||
tests.Completion(t, data.Completions, data.CompletionSnippets, data.CompletionItems)
|
||||
})
|
||||
|
||||
t.Run("Diagnostics", func(t *testing.T) {
|
||||
@ -357,3 +371,11 @@ func (data *Data) collectSignatures(spn span.Span, signature string, activeParam
|
||||
ActiveParameter: int(activeParam),
|
||||
}
|
||||
}
|
||||
|
||||
func (data *Data) collectCompletionSnippets(spn span.Span, item token.Pos, plain, placeholder string) {
|
||||
data.CompletionSnippets[spn] = CompletionSnippet{
|
||||
CompletionItem: item,
|
||||
PlainSnippet: plain,
|
||||
PlaceholderSnippet: placeholder,
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user