mirror of https://github.com/golang/go.git
461 lines
12 KiB
Go
461 lines
12 KiB
Go
// 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"
|
|
)
|
|
|
|
// 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}}`,
|
|
}, {
|
|
label: "split",
|
|
details: "split string",
|
|
body: `{{if (eq (.TypeName .Type) "string") -}}
|
|
{{.Import "strings"}}.Split({{.X}}, "{{.Cursor}}")
|
|
{{- end}}`,
|
|
}, {
|
|
label: "join",
|
|
details: "join string slice",
|
|
body: `{{if and (eq .Kind "slice") (eq (.TypeName .ElemType) "string") -}}
|
|
{{.Import "strings"}}.Join({{.X}}, "{{.Cursor}}")
|
|
{{- 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 "", fmt.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
|
|
}
|
|
|
|
tokFile := c.snapshot.FileSet().File(c.pos)
|
|
|
|
// 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 {
|
|
switch p := c.path[i+1].(type) {
|
|
case *ast.ExprStmt:
|
|
stmtOK = true
|
|
case *ast.AssignStmt:
|
|
// In cases like:
|
|
//
|
|
// foo.<>
|
|
// bar = 123
|
|
//
|
|
// detect that "foo." makes up the entire statement since the
|
|
// apparent selector spans lines.
|
|
stmtOK = tokFile.Line(c.pos) < tokFile.Line(p.TokPos)
|
|
}
|
|
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.
|
|
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
|
|
}
|