diff --git a/internal/lsp/cmd/cmd_test.go b/internal/lsp/cmd/cmd_test.go index 0738b11129..3a5b45bf31 100644 --- a/internal/lsp/cmd/cmd_test.go +++ b/internal/lsp/cmd/cmd_test.go @@ -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 } diff --git a/internal/lsp/completion.go b/internal/lsp/completion.go index 332b779158..7e7a378303 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 @@ 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 " = ". - // We should not insert the " = " 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 84b4e6eb7e..9fcf9fdf3e 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -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 { diff --git a/internal/lsp/source/completion.go b/internal/lsp/source/completion.go index 52b5944af0..58e6f42d00 100644 --- a/internal/lsp/source/completion.go +++ b/internal/lsp/source/completion.go @@ -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 diff --git a/internal/lsp/source/completion_format.go b/internal/lsp/source/completion_format.go index dd5fab99b7..150158431d 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 @@ 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 diff --git a/internal/lsp/source/completion_snippet.go b/internal/lsp/source/completion_snippet.go new file mode 100644 index 0000000000..5e430e59ad --- /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 0000000000..9df7b6389b --- /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 73c30e99a4..22d4a5d88e 100644 --- a/internal/lsp/tests/tests.go +++ b/internal/lsp/tests/tests.go @@ -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, + } +}