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