зеркало из https://github.com/golang/tools.git
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:
Родитель
2c039f7ffc
Коммит
09058ab085
|
@ -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.CompleteUnimported = false
|
||||
opts.InsertTextFormat = protocol.SnippetTextFormat
|
||||
if !strings.Contains(string(src.URI()), "literal") {
|
||||
opts.LiteralCompletions = false
|
||||
}
|
||||
opts.LiteralCompletions = strings.Contains(string(src.URI()), "literal")
|
||||
opts.PostfixCompletions = strings.Contains(string(src.URI()), "postfix")
|
||||
})
|
||||
got = tests.FilterBuiltins(src, got)
|
||||
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.CompleteUnimported = false
|
||||
opts.LiteralCompletions = true
|
||||
opts.PostfixCompletions = true
|
||||
})
|
||||
want := expected(t, test, items)
|
||||
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.Capabilities.TextDocument.Completion.CompletionItem.SnippetSupport = true
|
||||
|
||||
// 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
|
||||
// editor does send didChangeWatchedFiles notifications, so set this to
|
||||
|
|
|
@ -46,6 +46,10 @@ func (b *Builder) PrependText(s string) {
|
|||
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.
|
||||
// The callback style allows for creating nested placeholders. To write an
|
||||
// empty tab stop, provide a nil callback.
|
||||
|
|
|
@ -96,6 +96,7 @@ type completionOptions struct {
|
|||
placeholders bool
|
||||
literal bool
|
||||
snippets bool
|
||||
postfix bool
|
||||
matcher source.Matcher
|
||||
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,
|
||||
budget: opts.CompletionBudget,
|
||||
snippets: opts.InsertTextFormat == protocol.SnippetTextFormat,
|
||||
postfix: opts.PostfixCompletions,
|
||||
},
|
||||
// default to a matcher that always matches
|
||||
matcher: prefixMatcher(""),
|
||||
|
@ -1104,6 +1106,9 @@ func (c *completer) selector(ctx context.Context, sel *ast.SelectorExpr) error {
|
|||
for _, cand := range candidates {
|
||||
c.deepState.enqueue(cand)
|
||||
}
|
||||
|
||||
c.addPostfixSnippetCandidates(ctx, sel)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -158,7 +158,7 @@ func (c *completer) item(ctx context.Context, cand candidate) (CompletionItem, e
|
|||
if prefix != "" {
|
||||
// If we are in a selector, add an edit to place prefix before selector.
|
||||
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 {
|
||||
return CompletionItem{}, err
|
||||
}
|
||||
|
|
|
@ -7,14 +7,11 @@ package completion
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"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/snippet"
|
||||
"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.
|
||||
// For example, "foo.B<>" must complete to "&foo.Bar{}", not
|
||||
// "foo.&Bar{}".
|
||||
edits, err := prependEdit(c.snapshot.FileSet(), c.mapper, sel, "&")
|
||||
edits, err := c.editText(sel.Pos(), sel.Pos(), "&")
|
||||
if err != nil {
|
||||
event.Error(ctx, "error making edit for literal pointer completion", err)
|
||||
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.
|
||||
// Literal candidates match the expected type so they should be high
|
||||
// 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/types"
|
||||
|
||||
"golang.org/x/tools/internal/lsp/diff"
|
||||
"golang.org/x/tools/internal/lsp/protocol"
|
||||
"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)
|
||||
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{
|
||||
LiteralCompletions: true,
|
||||
PostfixCompletions: true,
|
||||
TempModfile: true,
|
||||
CompleteUnimported: true,
|
||||
CompletionDocumentation: true,
|
||||
|
@ -436,6 +437,11 @@ type InternalOptions struct {
|
|||
// their expected values.
|
||||
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
|
||||
// progress reports for all work done outside the scope of an RPC.
|
||||
// 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.CompleteUnimported = false
|
||||
opts.InsertTextFormat = protocol.SnippetTextFormat
|
||||
if !strings.Contains(string(src.URI()), "literal") {
|
||||
opts.LiteralCompletions = false
|
||||
}
|
||||
opts.LiteralCompletions = strings.Contains(string(src.URI()), "literal")
|
||||
opts.PostfixCompletions = strings.Contains(string(src.URI()), "postfix")
|
||||
})
|
||||
got = tests.FilterBuiltins(src, got)
|
||||
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) {
|
||||
opts.DeepCompletion = true
|
||||
opts.Matcher = source.Fuzzy
|
||||
opts.PostfixCompletions = true
|
||||
})
|
||||
if msg := tests.CheckCompletionOrder(want, got, true); msg != "" {
|
||||
t.Errorf("%s: %s", src, msg)
|
||||
|
|
|
@ -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(" //")
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
-- summary --
|
||||
CallHierarchyCount = 2
|
||||
CodeLensCount = 5
|
||||
CompletionsCount = 258
|
||||
CompletionsCount = 262
|
||||
CompletionSnippetCount = 94
|
||||
UnimportedCompletionsCount = 5
|
||||
DeepCompletionsCount = 5
|
||||
FuzzyCompletionsCount = 8
|
||||
RankedCompletionsCount = 159
|
||||
RankedCompletionsCount = 162
|
||||
CaseSensitiveCompletionsCount = 4
|
||||
DiagnosticsCount = 37
|
||||
FoldingRangesCount = 2
|
||||
|
|
Загрузка…
Ссылка в новой задаче