aboutsummaryrefslogtreecommitdiff
path: root/gopls/internal/lsp/source/completion/postfix_snippets.go
diff options
context:
space:
mode:
Diffstat (limited to 'gopls/internal/lsp/source/completion/postfix_snippets.go')
-rw-r--r--gopls/internal/lsp/source/completion/postfix_snippets.go471
1 files changed, 471 insertions, 0 deletions
diff --git a/gopls/internal/lsp/source/completion/postfix_snippets.go b/gopls/internal/lsp/source/completion/postfix_snippets.go
new file mode 100644
index 000000000..0737ec246
--- /dev/null
+++ b/gopls/internal/lsp/source/completion/postfix_snippets.go
@@ -0,0 +1,471 @@
+// 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/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 = 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.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
+}