From c6e1543aba625eb8e00a24270b07e67eb90c4108 Mon Sep 17 00:00:00 2001 From: Muir Manders Date: Mon, 29 Apr 2019 03:19:54 +0000 Subject: [PATCH] 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 TryBot-Result: Gobot Gobot Reviewed-by: Rebecca Stambler --- internal/lsp/cmd/cmd_test.go | 2 +- internal/lsp/completion.go | 70 ++++----------- internal/lsp/lsp_test.go | 84 ++++++++++++----- internal/lsp/source/completion.go | 83 ++++++++++++----- internal/lsp/source/completion_format.go | 75 ++++++++++++---- internal/lsp/source/completion_snippet.go | 100 +++++++++++++++++++++ internal/lsp/testdata/snippets/snippets.go | 30 +++++++ internal/lsp/tests/tests.go | 80 +++++++++++------ 8 files changed, 377 insertions(+), 147 deletions(-) create mode 100644 internal/lsp/source/completion_snippet.go create mode 100644 internal/lsp/testdata/snippets/snippets.go 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, + } +}