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>
diff --git a/internal/lsp/cmd/cmd_test.go b/internal/lsp/cmd/cmd_test.go
index 0738b11..3a5b45b 100644
--- a/internal/lsp/cmd/cmd_test.go
+++ b/internal/lsp/cmd/cmd_test.go
@@ -38,7 +38,7 @@
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
}
diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go
index 332b779..7e7a378 100644
--- a/internal/lsp/completion.go
+++ b/internal/lsp/completion.go
@@ -5,7 +5,6 @@
package lsp
import (
- "bytes"
"context"
"fmt"
"sort"
@@ -52,10 +51,25 @@
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 @@
// 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 @@
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
-}
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index 84b4e6e..9fcf9fd 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -130,26 +130,15 @@
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 @@
}
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 {
diff --git a/internal/lsp/source/completion.go b/internal/lsp/source/completion.go
index 52b5944..58e6f42 100644
--- a/internal/lsp/source/completion.go
+++ b/internal/lsp/source/completion.go
@@ -12,12 +12,35 @@
"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 @@
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 @@
// 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 @@
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 @@
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 @@
//
// 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 @@
// 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
diff --git a/internal/lsp/source/completion_format.go b/internal/lsp/source/completion_format.go
index dd5fab9..1501584 100644
--- a/internal/lsp/source/completion_format.go
+++ b/internal/lsp/source/completion_format.go
@@ -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 @@
}
if o.IsField() {
kind = FieldCompletionItem
+ plainSnippet, placeholderSnippet = c.structFieldSnippet(label, detail)
} else if c.isParameter(o) {
kind = ParameterCompletionItem
} else {
@@ -47,12 +54,15 @@
}
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 @@
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 @@
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
diff --git a/internal/lsp/source/completion_snippet.go b/internal/lsp/source/completion_snippet.go
new file mode 100644
index 0000000..5e430e5
--- /dev/null
+++ b/internal/lsp/source/completion_snippet.go
@@ -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
+}
diff --git a/internal/lsp/testdata/snippets/snippets.go b/internal/lsp/testdata/snippets/snippets.go
new file mode 100644
index 0000000..9df7b63
--- /dev/null
+++ b/internal/lsp/testdata/snippets/snippets.go
@@ -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")
+}
diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go
index 73c30e9..22d4a5d 100644
--- a/internal/lsp/tests/tests.go
+++ b/internal/lsp/tests/tests.go
@@ -27,14 +27,15 @@
// 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 @@
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 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 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 @@
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 @@
"highlight": data.collectHighlights,
"symbol": data.collectSymbols,
"signature": data.collectSignatures,
+ "snippet": data.collectCompletionSnippets,
}); err != nil {
t.Fatal(err)
}
@@ -188,7 +199,10 @@
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 @@
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,
+ }
+}