blob: c1582e6b379d3de433c9191483bdf1af1de87af7 [file] [log] [blame]
// 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/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/safetoken"
"golang.org/x/tools/gopls/internal/lsp/snippet"
"golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/imports"
)
// 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: "range",
details: "range over channel",
body: `{{if and (eq .Kind "chan") .StmtOK -}}
for {{.VarName .ElemType "e"}} := range {{.X}} {
{{.Cursor}}
}
{{- 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
// go/types predicates are undefined on types.Typ[types.Invalid].
if !types.Identical(t, types.Typ[types.Invalid]) && 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.pkg.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 = safetoken.Line(tokFile, c.pos) < safetoken.Line(tokFile, 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 := safetoken.Line(tokFile, sel.Pos()); startLine != safetoken.Line(tokFile, afterDot) {
if safetoken.Line(tokFile, 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.pkg.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 {
// TODO(adonovan): what if pkgPath has a vendor/ suffix?
// This may be the cause of go.dev/issue/56291.
if source.UnquoteImportPath(s) == source.ImportPath(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
}