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:
Muir Manders 2019-04-29 03:19:54 +00:00 committed by Rebecca Stambler
parent 550556f78a
commit c6e1543aba
8 changed files with 377 additions and 147 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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

View File

@ -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

View 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
}

View 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")
}

View File

@ -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,
}
}