internal/lsp/source/completion: add postfix snippet completions

Postfix snippets are artificial methods that allow the user to compose
common operations in an "argument oriented" fashion. For example,
instead of "sort.Slice(someSlice, ...)" a user can expand
"someSlice.sort!". The snippet labels end in "!" to make it clearer
they do something potentially unexpected. The postfix snippets have
low scores so they should not interfere with normal completions.

The snippets are represented (almost) entirely as Go text/template
templates. This way the user can create custom snippets to match their
general preferences or to capture common patterns in their codebase.
There is currently no way for the user to create snippets, but it
could be accomplished with a configuration file, custom LSP command,
or similar.

I started by implementing a variety of snippets to help flesh out the
various facilities needed by the templates. The most interesting
template capabilities are:
 - The ability to import packages as necessary (e.g. "sort" must be
   imported to call sort.Slice()).
 - The ability to generate unique variable names to avoid accidental
   shadowing issues.
 - The ability to weave LSP snippets into the template. Currently,
   only {{.Cursor}} is exposed, which corresponds to the snippet's
   final tab stop.

Briefly, these are the postfix snippets in this commit:
 - foo.sort => sort.Slice(foo, func(...){}) (slices)
 - foo.last => foo[len(foo)-1] (slices)
 - foo.reverse (slices)
 - foo.range => for i, v := range foo {} (slices/maps)
 - foo.append
     This snippet inserts a self-assignment append statement when
     appropriate, otherwise just an append expression.
 - foo.copy creates a copy of a slice
 - foo.clear empties out a map
 - foo.keys creates slice of keys
 - foo().var assigns result value(s) to variables
 - foo.print prints foo to stdout

Some of these are probably not very useful in practice, and I'm sure
there are lots of great ones I didn't think of.

Updates golang/go#39507.

Change-Id: I9ecc748aa79c0d47fa6ff72d4ea671e917a2d5d6
Reviewed-on: https://go-review.googlesource.com/c/tools/+/272586
Run-TryBot: Muir Manders <muir@mnd.rs>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
Muir Manders 2020-11-23 11:51:45 -08:00 коммит произвёл Rebecca Stambler
Родитель 2c039f7ffc
Коммит 09058ab085
13 изменённых файлов: 902 добавлений и 27 удалений

Просмотреть файл

@ -0,0 +1,398 @@
// Copyright 2021 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 completion
import (
"strings"
"testing"
. "golang.org/x/tools/gopls/internal/regtest"
)
func TestPostfixSnippetCompletion(t *testing.T) {
const mod = `
-- go.mod --
module mod.com
go 1.12
`
cases := []struct {
name string
before, after string
}{
{
name: "sort",
before: `
package foo
func _() {
var foo []int
foo.sort
}
`,
after: `
package foo
import "sort"
func _() {
var foo []int
sort.Slice(foo, func(i, j int) bool {
$0
})
}
`,
},
{
name: "sort_renamed_sort_package",
before: `
package foo
import blahsort "sort"
var j int
func _() {
var foo []int
foo.sort
}
`,
after: `
package foo
import blahsort "sort"
var j int
func _() {
var foo []int
blahsort.Slice(foo, func(i, j2 int) bool {
$0
})
}
`,
},
{
name: "last",
before: `
package foo
func _() {
var s struct { i []int }
s.i.last
}
`,
after: `
package foo
func _() {
var s struct { i []int }
s.i[len(s.i)-1]
}
`,
},
{
name: "reverse",
before: `
package foo
func _() {
var foo []int
foo.reverse
}
`,
after: `
package foo
func _() {
var foo []int
for i, j := 0, len(foo)-1; i < j; i, j = i+1, j-1 {
foo[i], foo[j] = foo[j], foo[i]
}
}
`,
},
{
name: "slice_range",
before: `
package foo
func _() {
type myThing struct{}
var foo []myThing
foo.range
}
`,
after: `
package foo
func _() {
type myThing struct{}
var foo []myThing
for i, mt := range foo {
$0
}
}
`,
},
{
name: "append_stmt",
before: `
package foo
func _() {
var foo []int
foo.append
}
`,
after: `
package foo
func _() {
var foo []int
foo = append(foo, $0)
}
`,
},
{
name: "append_expr",
before: `
package foo
func _() {
var foo []int
var _ []int = foo.append
}
`,
after: `
package foo
func _() {
var foo []int
var _ []int = append(foo, $0)
}
`,
},
{
name: "slice_copy",
before: `
package foo
func _() {
var foo []int
foo.copy
}
`,
after: `
package foo
func _() {
var foo []int
fooCopy := make([]int, len(foo))
copy(fooCopy, foo)
}
`,
},
{
name: "map_range",
before: `
package foo
func _() {
var foo map[string]int
foo.range
}
`,
after: `
package foo
func _() {
var foo map[string]int
for k, v := range foo {
$0
}
}
`,
},
{
name: "map_clear",
before: `
package foo
func _() {
var foo map[string]int
foo.clear
}
`,
after: `
package foo
func _() {
var foo map[string]int
for k := range foo {
delete(foo, k)
}
}
`,
},
{
name: "map_keys",
before: `
package foo
func _() {
var foo map[string]int
foo.keys
}
`,
after: `
package foo
func _() {
var foo map[string]int
keys := make([]string, 0, len(foo))
for k := range foo {
keys = append(keys, k)
}
}
`,
},
{
name: "var",
before: `
package foo
func foo() (int, error) { return 0, nil }
func _() {
foo().var
}
`,
after: `
package foo
func foo() (int, error) { return 0, nil }
func _() {
i, err := foo()
}
`,
},
{
name: "var_single_value",
before: `
package foo
func foo() error { return nil }
func _() {
foo().var
}
`,
after: `
package foo
func foo() error { return nil }
func _() {
err := foo()
}
`,
},
{
name: "var_same_type",
before: `
package foo
func foo() (int, int) { return 0, 0 }
func _() {
foo().var
}
`,
after: `
package foo
func foo() (int, int) { return 0, 0 }
func _() {
i, i2 := foo()
}
`,
},
{
name: "print_scalar",
before: `
package foo
func _() {
var foo int
foo.print
}
`,
after: `
package foo
import "fmt"
func _() {
var foo int
fmt.Printf("foo: %v\n", foo)
}
`,
},
{
name: "print_multi",
before: `
package foo
func foo() (int, error) { return 0, nil }
func _() {
foo().print
}
`,
after: `
package foo
import "fmt"
func foo() (int, error) { return 0, nil }
func _() {
fmt.Println(foo())
}
`,
},
}
Run(t, mod, func(t *testing.T, env *Env) {
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
c.before = strings.Trim(c.before, "\n")
c.after = strings.Trim(c.after, "\n")
env.CreateBuffer("foo.go", c.before)
pos := env.RegexpSearch("foo.go", "\n}")
completions := env.Completion("foo.go", pos)
if len(completions.Items) != 1 {
t.Fatalf("expected one completion, got %v", completions.Items)
}
env.AcceptCompletion("foo.go", pos, completions.Items[0])
if buf := env.Editor.BufferText("foo.go"); buf != c.after {
t.Errorf("\nGOT:\n%s\nEXPECTED:\n%s", buf, c.after)
}
})
}
})
}

Просмотреть файл

@ -20,9 +20,8 @@ func (r *runner) Completion(t *testing.T, src span.Span, test tests.Completion,
opts.Matcher = source.CaseInsensitive opts.Matcher = source.CaseInsensitive
opts.CompleteUnimported = false opts.CompleteUnimported = false
opts.InsertTextFormat = protocol.SnippetTextFormat opts.InsertTextFormat = protocol.SnippetTextFormat
if !strings.Contains(string(src.URI()), "literal") { opts.LiteralCompletions = strings.Contains(string(src.URI()), "literal")
opts.LiteralCompletions = false opts.PostfixCompletions = strings.Contains(string(src.URI()), "postfix")
}
}) })
got = tests.FilterBuiltins(src, got) got = tests.FilterBuiltins(src, got)
want := expected(t, test, items) want := expected(t, test, items)
@ -101,6 +100,7 @@ func (r *runner) RankCompletion(t *testing.T, src span.Span, test tests.Completi
opts.Matcher = source.Fuzzy opts.Matcher = source.Fuzzy
opts.CompleteUnimported = false opts.CompleteUnimported = false
opts.LiteralCompletions = true opts.LiteralCompletions = true
opts.PostfixCompletions = true
}) })
want := expected(t, test, items) want := expected(t, test, items)
if msg := tests.CheckCompletionOrder(want, got, true); msg != "" { if msg := tests.CheckCompletionOrder(want, got, true); msg != "" {

Просмотреть файл

@ -275,6 +275,8 @@ func (e *Editor) initialize(ctx context.Context, workspaceFolders []string) erro
params.ProcessID = int32(os.Getpid()) params.ProcessID = int32(os.Getpid())
} }
params.Capabilities.TextDocument.Completion.CompletionItem.SnippetSupport = true
// This is a bit of a hack, since the fake editor doesn't actually support // This is a bit of a hack, since the fake editor doesn't actually support
// watching changed files that match a specific glob pattern. However, the // watching changed files that match a specific glob pattern. However, the
// editor does send didChangeWatchedFiles notifications, so set this to // editor does send didChangeWatchedFiles notifications, so set this to

Просмотреть файл

@ -46,6 +46,10 @@ func (b *Builder) PrependText(s string) {
b.sb.WriteString(rawSnip) b.sb.WriteString(rawSnip)
} }
func (b *Builder) Write(data []byte) (int, error) {
return b.sb.Write(data)
}
// WritePlaceholder writes a tab stop and placeholder value to the Builder. // WritePlaceholder writes a tab stop and placeholder value to the Builder.
// The callback style allows for creating nested placeholders. To write an // The callback style allows for creating nested placeholders. To write an
// empty tab stop, provide a nil callback. // empty tab stop, provide a nil callback.

Просмотреть файл

@ -96,6 +96,7 @@ type completionOptions struct {
placeholders bool placeholders bool
literal bool literal bool
snippets bool snippets bool
postfix bool
matcher source.Matcher matcher source.Matcher
budget time.Duration budget time.Duration
} }
@ -521,6 +522,7 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan
literal: opts.LiteralCompletions && opts.InsertTextFormat == protocol.SnippetTextFormat, literal: opts.LiteralCompletions && opts.InsertTextFormat == protocol.SnippetTextFormat,
budget: opts.CompletionBudget, budget: opts.CompletionBudget,
snippets: opts.InsertTextFormat == protocol.SnippetTextFormat, snippets: opts.InsertTextFormat == protocol.SnippetTextFormat,
postfix: opts.PostfixCompletions,
}, },
// default to a matcher that always matches // default to a matcher that always matches
matcher: prefixMatcher(""), matcher: prefixMatcher(""),
@ -1104,6 +1106,9 @@ func (c *completer) selector(ctx context.Context, sel *ast.SelectorExpr) error {
for _, cand := range candidates { for _, cand := range candidates {
c.deepState.enqueue(cand) c.deepState.enqueue(cand)
} }
c.addPostfixSnippetCandidates(ctx, sel)
return nil return nil
} }

Просмотреть файл

@ -158,7 +158,7 @@ func (c *completer) item(ctx context.Context, cand candidate) (CompletionItem, e
if prefix != "" { if prefix != "" {
// If we are in a selector, add an edit to place prefix before selector. // If we are in a selector, add an edit to place prefix before selector.
if sel := enclosingSelector(c.path, c.pos); sel != nil { if sel := enclosingSelector(c.path, c.pos); sel != nil {
edits, err := prependEdit(c.snapshot.FileSet(), c.mapper, sel, prefix) edits, err := c.editText(sel.Pos(), sel.Pos(), prefix)
if err != nil { if err != nil {
return CompletionItem{}, err return CompletionItem{}, err
} }

Просмотреть файл

@ -7,14 +7,11 @@ package completion
import ( import (
"context" "context"
"fmt" "fmt"
"go/ast"
"go/token"
"go/types" "go/types"
"strings" "strings"
"unicode" "unicode"
"golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/snippet" "golang.org/x/tools/internal/lsp/snippet"
"golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/source"
@ -111,7 +108,7 @@ func (c *completer) literal(ctx context.Context, literalType types.Type, imp *im
// If we are in a selector we must place the "&" before the selector. // If we are in a selector we must place the "&" before the selector.
// For example, "foo.B<>" must complete to "&foo.Bar{}", not // For example, "foo.B<>" must complete to "&foo.Bar{}", not
// "foo.&Bar{}". // "foo.&Bar{}".
edits, err := prependEdit(c.snapshot.FileSet(), c.mapper, sel, "&") edits, err := c.editText(sel.Pos(), sel.Pos(), "&")
if err != nil { if err != nil {
event.Error(ctx, "error making edit for literal pointer completion", err) event.Error(ctx, "error making edit for literal pointer completion", err)
return return
@ -168,20 +165,6 @@ func (c *completer) literal(ctx context.Context, literalType types.Type, imp *im
} }
} }
// prependEdit produces text edits that preprend the specified prefix
// to the specified node.
func prependEdit(fset *token.FileSet, m *protocol.ColumnMapper, node ast.Node, prefix string) ([]protocol.TextEdit, error) {
rng := source.NewMappedRange(fset, m, node.Pos(), node.Pos())
spn, err := rng.Span()
if err != nil {
return nil, err
}
return source.ToProtocolEdits(m, []diff.TextEdit{{
Span: spn,
NewText: prefix,
}})
}
// literalCandidateScore is the base score for literal candidates. // literalCandidateScore is the base score for literal candidates.
// Literal candidates match the expected type so they should be high // Literal candidates match the expected type so they should be high
// scoring, but we want them ranked below lexical objects of the // scoring, but we want them ranked below lexical objects of the

Просмотреть файл

@ -0,0 +1,436 @@
// Copyright 2020 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 completion
import (
"context"
"fmt"
"go/ast"
"go/token"
"go/types"
"log"
"reflect"
"strings"
"sync"
"text/template"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/imports"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/snippet"
"golang.org/x/tools/internal/lsp/source"
errors "golang.org/x/xerrors"
)
// Postfix snippets are artificial methods that allow the user to
// compose common operations in an "argument oriented" fashion. For
// example, instead of "sort.Slice(someSlice, ...)" a user can expand
// "someSlice.sort!".
// postfixTmpl represents a postfix snippet completion candidate.
type postfixTmpl struct {
// label is the completion candidate's label presented to the user.
label string
// details is passed along to the client as the candidate's details.
details string
// body is the template text. See postfixTmplArgs for details on the
// facilities available to the template.
body string
tmpl *template.Template
}
// postfixTmplArgs are the template execution arguments available to
// the postfix snippet templates.
type postfixTmplArgs struct {
// StmtOK is true if it is valid to replace the selector with a
// statement. For example:
//
// func foo() {
// bar.sort! // statement okay
//
// someMethod(bar.sort!) // statement not okay
// }
StmtOK bool
// X is the textual SelectorExpr.X. For example, when completing
// "foo.bar.print!", "X" is "foo.bar".
X string
// Obj is the types.Object of SelectorExpr.X, if any.
Obj types.Object
// Type is the type of "foo.bar" in "foo.bar.print!".
Type types.Type
scope *types.Scope
snip snippet.Builder
importIfNeeded func(pkgPath string, scope *types.Scope) (name string, edits []protocol.TextEdit, err error)
edits []protocol.TextEdit
qf types.Qualifier
varNames map[string]bool
}
var postfixTmpls = []postfixTmpl{{
label: "sort",
details: "sort.Slice()",
body: `{{if and (eq .Kind "slice") .StmtOK -}}
{{.Import "sort"}}.Slice({{.X}}, func({{.VarName nil "i"}}, {{.VarName nil "j"}} int) bool {
{{.Cursor}}
})
{{- end}}`,
}, {
label: "last",
details: "s[len(s)-1]",
body: `{{if and (eq .Kind "slice") .Obj -}}
{{.X}}[len({{.X}})-1]
{{- end}}`,
}, {
label: "reverse",
details: "reverse slice",
body: `{{if and (eq .Kind "slice") .StmtOK -}}
{{$i := .VarName nil "i"}}{{$j := .VarName nil "j" -}}
for {{$i}}, {{$j}} := 0, len({{.X}})-1; {{$i}} < {{$j}}; {{$i}}, {{$j}} = {{$i}}+1, {{$j}}-1 {
{{.X}}[{{$i}}], {{.X}}[{{$j}}] = {{.X}}[{{$j}}], {{.X}}[{{$i}}]
}
{{end}}`,
}, {
label: "range",
details: "range over slice",
body: `{{if and (eq .Kind "slice") .StmtOK -}}
for {{.VarName nil "i"}}, {{.VarName .ElemType "v"}} := range {{.X}} {
{{.Cursor}}
}
{{- end}}`,
}, {
label: "append",
details: "append and re-assign slice",
body: `{{if and (eq .Kind "slice") .StmtOK .Obj -}}
{{.X}} = append({{.X}}, {{.Cursor}})
{{- end}}`,
}, {
label: "append",
details: "append to slice",
body: `{{if and (eq .Kind "slice") (not .StmtOK) -}}
append({{.X}}, {{.Cursor}})
{{- end}}`,
}, {
label: "copy",
details: "duplicate slice",
body: `{{if and (eq .Kind "slice") .StmtOK .Obj -}}
{{$v := (.VarName nil (printf "%sCopy" .X))}}{{$v}} := make([]{{.TypeName .ElemType}}, len({{.X}}))
copy({{$v}}, {{.X}})
{{end}}`,
}, {
label: "range",
details: "range over map",
body: `{{if and (eq .Kind "map") .StmtOK -}}
for {{.VarName .KeyType "k"}}, {{.VarName .ElemType "v"}} := range {{.X}} {
{{.Cursor}}
}
{{- end}}`,
}, {
label: "clear",
details: "clear map contents",
body: `{{if and (eq .Kind "map") .StmtOK -}}
{{$k := (.VarName .KeyType "k")}}for {{$k}} := range {{.X}} {
delete({{.X}}, {{$k}})
}
{{end}}`,
}, {
label: "keys",
details: "create slice of keys",
body: `{{if and (eq .Kind "map") .StmtOK -}}
{{$keysVar := (.VarName nil "keys")}}{{$keysVar}} := make([]{{.TypeName .KeyType}}, 0, len({{.X}}))
{{$k := (.VarName .KeyType "k")}}for {{$k}} := range {{.X}} {
{{$keysVar}} = append({{$keysVar}}, {{$k}})
}
{{end}}`,
}, {
label: "var",
details: "assign to variables",
body: `{{if and (eq .Kind "tuple") .StmtOK -}}
{{$a := .}}{{range $i, $v := .Tuple}}{{if $i}}, {{end}}{{$a.VarName $v.Type $v.Name}}{{end}} := {{.X}}
{{- end}}`,
}, {
label: "var",
details: "assign to variable",
body: `{{if and (ne .Kind "tuple") .StmtOK -}}
{{.VarName .Type ""}} := {{.X}}
{{- end}}`,
}, {
label: "print",
details: "print to stdout",
body: `{{if and (ne .Kind "tuple") .StmtOK -}}
{{.Import "fmt"}}.Printf("{{.EscapeQuotes .X}}: %v\n", {{.X}})
{{- end}}`,
}, {
label: "print",
details: "print to stdout",
body: `{{if and (eq .Kind "tuple") .StmtOK -}}
{{.Import "fmt"}}.Println({{.X}})
{{- end}}`,
}}
// Cursor indicates where the client's cursor should end up after the
// snippet is done.
func (a *postfixTmplArgs) Cursor() string {
a.snip.WriteFinalTabstop()
return ""
}
// Import makes sure the package corresponding to path is imported,
// returning the identifier to use to refer to the package.
func (a *postfixTmplArgs) Import(path string) (string, error) {
name, edits, err := a.importIfNeeded(path, a.scope)
if err != nil {
return "", errors.Errorf("couldn't import %q: %w", path, err)
}
a.edits = append(a.edits, edits...)
return name, nil
}
func (a *postfixTmplArgs) EscapeQuotes(v string) string {
return strings.ReplaceAll(v, `"`, `\\"`)
}
// ElemType returns the Elem() type of xType, if applicable.
func (a *postfixTmplArgs) ElemType() types.Type {
if e, _ := a.Type.(interface{ Elem() types.Type }); e != nil {
return e.Elem()
}
return nil
}
// Kind returns the underlying kind of type, e.g. "slice", "struct",
// etc.
func (a *postfixTmplArgs) Kind() string {
t := reflect.TypeOf(a.Type.Underlying())
return strings.ToLower(strings.TrimPrefix(t.String(), "*types."))
}
// KeyType returns the type of X's key. KeyType panics if X is not a
// map.
func (a *postfixTmplArgs) KeyType() types.Type {
return a.Type.Underlying().(*types.Map).Key()
}
// Tuple returns the tuple result vars if X is a call expression.
func (a *postfixTmplArgs) Tuple() []*types.Var {
tuple, _ := a.Type.(*types.Tuple)
if tuple == nil {
return nil
}
typs := make([]*types.Var, 0, tuple.Len())
for i := 0; i < tuple.Len(); i++ {
typs = append(typs, tuple.At(i))
}
return typs
}
// TypeName returns the textual representation of type t.
func (a *postfixTmplArgs) TypeName(t types.Type) (string, error) {
if t == nil || t == types.Typ[types.Invalid] {
return "", fmt.Errorf("invalid type: %v", t)
}
return types.TypeString(t, a.qf), nil
}
// VarName returns a suitable variable name for the type t. If t
// implements the error interface, "err" is used. If t is not a named
// type then nonNamedDefault is used. Otherwise a name is made by
// abbreviating the type name. If the resultant name is already in
// scope, an integer is appended to make a unique name.
func (a *postfixTmplArgs) VarName(t types.Type, nonNamedDefault string) string {
if t == nil {
t = types.Typ[types.Invalid]
}
var name string
if types.Implements(t, errorIntf) {
name = "err"
} else if _, isNamed := source.Deref(t).(*types.Named); !isNamed {
name = nonNamedDefault
}
if name == "" {
name = types.TypeString(t, func(p *types.Package) string {
return ""
})
name = abbreviateTypeName(name)
}
if dot := strings.LastIndex(name, "."); dot > -1 {
name = name[dot+1:]
}
uniqueName := name
for i := 2; ; i++ {
if s, _ := a.scope.LookupParent(uniqueName, token.NoPos); s == nil && !a.varNames[uniqueName] {
break
}
uniqueName = fmt.Sprintf("%s%d", name, i)
}
a.varNames[uniqueName] = true
return uniqueName
}
func (c *completer) addPostfixSnippetCandidates(ctx context.Context, sel *ast.SelectorExpr) {
if !c.opts.postfix {
return
}
initPostfixRules()
if sel == nil || sel.Sel == nil {
return
}
selType := c.pkg.GetTypesInfo().TypeOf(sel.X)
if selType == nil {
return
}
// Skip empty tuples since there is no value to operate on.
if tuple, ok := selType.Underlying().(*types.Tuple); ok && tuple == nil {
return
}
// Only replace sel with a statement if sel is already a statement.
var stmtOK bool
for i, n := range c.path {
if n == sel && i < len(c.path)-1 {
_, stmtOK = c.path[i+1].(*ast.ExprStmt)
break
}
}
scope := c.pkg.GetTypes().Scope().Innermost(c.pos)
if scope == nil {
return
}
// afterDot is the position after selector dot, e.g. "|" in
// "foo.|print".
afterDot := sel.Sel.Pos()
// We must detect dangling selectors such as:
//
// foo.<>
// bar
//
// and adjust afterDot so that we don't mistakenly delete the
// newline thinking "bar" is part of our selector.
tokFile := c.snapshot.FileSet().File(c.pos)
if startLine := tokFile.Line(sel.Pos()); startLine != tokFile.Line(afterDot) {
if tokFile.Line(c.pos) != startLine {
return
}
afterDot = c.pos
}
for _, rule := range postfixTmpls {
// When completing foo.print<>, "print" is naturally overwritten,
// but we need to also remove "foo." so the snippet has a clean
// slate.
edits, err := c.editText(sel.Pos(), afterDot, "")
if err != nil {
event.Error(ctx, "error calculating postfix edits", err)
return
}
tmplArgs := postfixTmplArgs{
X: source.FormatNode(c.snapshot.FileSet(), sel.X),
StmtOK: stmtOK,
Obj: exprObj(c.pkg.GetTypesInfo(), sel.X),
Type: selType,
qf: c.qf,
importIfNeeded: c.importIfNeeded,
scope: scope,
varNames: make(map[string]bool),
}
// Feed the template straight into the snippet builder. This
// allows templates to build snippets as they are executed.
err = rule.tmpl.Execute(&tmplArgs.snip, &tmplArgs)
if err != nil {
event.Error(ctx, "error executing postfix template", err)
continue
}
if strings.TrimSpace(tmplArgs.snip.String()) == "" {
continue
}
score := c.matcher.Score(rule.label)
if score <= 0 {
continue
}
c.items = append(c.items, CompletionItem{
Label: rule.label + "!",
Detail: rule.details,
Score: float64(score) * 0.01,
Kind: protocol.SnippetCompletion,
snippet: &tmplArgs.snip,
AdditionalTextEdits: append(edits, tmplArgs.edits...),
})
}
}
var postfixRulesOnce sync.Once
func initPostfixRules() {
postfixRulesOnce.Do(func() {
var idx int
for _, rule := range postfixTmpls {
var err error
rule.tmpl, err = template.New("postfix_snippet").Parse(rule.body)
if err != nil {
log.Panicf("error parsing postfix snippet template: %v", err)
}
postfixTmpls[idx] = rule
idx++
}
postfixTmpls = postfixTmpls[:idx]
})
}
// importIfNeeded returns the package identifier and any necessary
// edits to import package pkgPath.
func (c *completer) importIfNeeded(pkgPath string, scope *types.Scope) (string, []protocol.TextEdit, error) {
defaultName := imports.ImportPathToAssumedName(pkgPath)
// Check if file already imports pkgPath.
for _, s := range c.file.Imports {
if source.ImportPath(s) == pkgPath {
if s.Name == nil {
return defaultName, nil, nil
}
if s.Name.Name != "_" {
return s.Name.Name, nil, nil
}
}
}
// Give up if the package's name is already in use by another object.
if _, obj := scope.LookupParent(defaultName, token.NoPos); obj != nil {
return "", nil, fmt.Errorf("import name %q of %q already in use", defaultName, pkgPath)
}
edits, err := c.importEdits(&importInfo{
importPath: pkgPath,
})
if err != nil {
return "", nil, err
}
return defaultName, edits, nil
}

Просмотреть файл

@ -9,6 +9,8 @@ import (
"go/token" "go/token"
"go/types" "go/types"
"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/source"
) )
@ -310,3 +312,15 @@ func isBasicKind(t types.Type, k types.BasicInfo) bool {
b, _ := t.Underlying().(*types.Basic) b, _ := t.Underlying().(*types.Basic)
return b != nil && b.Info()&k > 0 return b != nil && b.Info()&k > 0
} }
func (c *completer) editText(from, to token.Pos, newText string) ([]protocol.TextEdit, error) {
rng := source.NewMappedRange(c.snapshot.FileSet(), c.mapper, from, to)
spn, err := rng.Span()
if err != nil {
return nil, err
}
return source.ToProtocolEdits(c.mapper, []diff.TextEdit{{
Span: spn,
NewText: newText,
}})
}

Просмотреть файл

@ -144,6 +144,7 @@ func DefaultOptions() *Options {
}, },
InternalOptions: InternalOptions{ InternalOptions: InternalOptions{
LiteralCompletions: true, LiteralCompletions: true,
PostfixCompletions: true,
TempModfile: true, TempModfile: true,
CompleteUnimported: true, CompleteUnimported: true,
CompletionDocumentation: true, CompletionDocumentation: true,
@ -436,6 +437,11 @@ type InternalOptions struct {
// their expected values. // their expected values.
LiteralCompletions bool LiteralCompletions bool
// PostfixCompletions enables pseudo method snippets such as
// "someSlice.sort!". Tests disable this flag to simplify their
// expected values.
PostfixCompletions bool
// VerboseWorkDoneProgress controls whether the LSP server should send // VerboseWorkDoneProgress controls whether the LSP server should send
// progress reports for all work done outside the scope of an RPC. // progress reports for all work done outside the scope of an RPC.
// Used by the regression tests. // Used by the regression tests.

Просмотреть файл

@ -177,9 +177,8 @@ func (r *runner) Completion(t *testing.T, src span.Span, test tests.Completion,
opts.DeepCompletion = false opts.DeepCompletion = false
opts.CompleteUnimported = false opts.CompleteUnimported = false
opts.InsertTextFormat = protocol.SnippetTextFormat opts.InsertTextFormat = protocol.SnippetTextFormat
if !strings.Contains(string(src.URI()), "literal") { opts.LiteralCompletions = strings.Contains(string(src.URI()), "literal")
opts.LiteralCompletions = false opts.PostfixCompletions = strings.Contains(string(src.URI()), "postfix")
}
}) })
got = tests.FilterBuiltins(src, got) got = tests.FilterBuiltins(src, got)
if diff := tests.DiffCompletionItems(want, got); diff != "" { if diff := tests.DiffCompletionItems(want, got); diff != "" {
@ -278,6 +277,7 @@ func (r *runner) RankCompletion(t *testing.T, src span.Span, test tests.Completi
_, got := r.callCompletion(t, src, func(opts *source.Options) { _, got := r.callCompletion(t, src, func(opts *source.Options) {
opts.DeepCompletion = true opts.DeepCompletion = true
opts.Matcher = source.Fuzzy opts.Matcher = source.Fuzzy
opts.PostfixCompletions = true
}) })
if msg := tests.CheckCompletionOrder(want, got, true); msg != "" { if msg := tests.CheckCompletionOrder(want, got, true); msg != "" {
t.Errorf("%s: %s", src, msg) t.Errorf("%s: %s", src, msg)

27
internal/lsp/testdata/snippets/postfix.go поставляемый Normal file
Просмотреть файл

@ -0,0 +1,27 @@
package snippets
// These tests check that postfix completions do and do not show up in
// certain cases. Tests for the postfix completion contents are under
// regtest.
func _() {
/* append! */ //@item(postfixAppend, "append!", "append and re-assign slice", "snippet")
var foo []int
foo.append //@rank(" //", postfixAppend)
[]int{}.append //@complete(" //")
[]int{}.last //@complete(" //")
/* copy! */ //@item(postfixCopy, "copy!", "duplicate slice", "snippet")
foo.copy //@rank(" //", postfixCopy)
var s struct{ i []int }
s.i.copy //@rank(" //", postfixCopy)
var _ []int = s.i.copy //@complete(" //")
var blah func() []int
blah().append //@complete(" //")
}

4
internal/lsp/testdata/summary.txt.golden поставляемый
Просмотреть файл

@ -1,12 +1,12 @@
-- summary -- -- summary --
CallHierarchyCount = 2 CallHierarchyCount = 2
CodeLensCount = 5 CodeLensCount = 5
CompletionsCount = 258 CompletionsCount = 262
CompletionSnippetCount = 94 CompletionSnippetCount = 94
UnimportedCompletionsCount = 5 UnimportedCompletionsCount = 5
DeepCompletionsCount = 5 DeepCompletionsCount = 5
FuzzyCompletionsCount = 8 FuzzyCompletionsCount = 8
RankedCompletionsCount = 159 RankedCompletionsCount = 162
CaseSensitiveCompletionsCount = 4 CaseSensitiveCompletionsCount = 4
DiagnosticsCount = 37 DiagnosticsCount = 37
FoldingRangesCount = 2 FoldingRangesCount = 2