// 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 }