internal/lsp/source: improve func literal completions
When a signature doesn't name its params, we make up param
names when generating a corresponding func literal:
var f func(myType) // no param name
f = fun<> // completes to "func(mt myType) {}"
Previously we would abbreviate named types and fall back to "_" for
builtins or repeated names. We would require placeholders to be
enabled when using "_" so the user could name the param easily. That
left users that don't use placeholders with no completion at all in
this case.
I made the following improvements:
- Generate a name for all params. For builtin types we use the first
letter, e.g. "i" for "int", "s" for "[]string". If a name is
repeated, we append incrementing numeric suffixes. For example,
"func(int, int32)" becomes "func(i1 int, i2 int32").
- No longer require placeholders to be enabled in any case.
- Fix handling of alias types so the param name and type name are
based on the alias, not the aliasee.
I also tweaked formatVarType to qualify packages using a
types.Qualifier. I needed it to respect a qualifier that doesn't
qualify anything so I could format e.g. "http.Response" as just
"Response" to come up with param names.
Fixes golang/go#38416.
Change-Id: I0ce8a0a4e2485dda41a0aa696d9fd48bea595869
Reviewed-on: https://go-review.googlesource.com/c/tools/+/246262
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/source/completion_literal.go b/internal/lsp/source/completion_literal.go
index b3a50ab..a115e95 100644
--- a/internal/lsp/source/completion_literal.go
+++ b/internal/lsp/source/completion_literal.go
@@ -6,6 +6,7 @@
import (
"context"
+ "fmt"
"go/ast"
"go/token"
"go/types"
@@ -161,7 +162,7 @@
if score := c.matcher.Score("func"); !cand.takeAddress && score >= 0 && !isInterface(expType) {
switch t := literalType.Underlying().(type) {
case *types.Signature:
- c.functionLiteral(t, float64(score))
+ c.functionLiteral(ctx, t, float64(score))
}
}
}
@@ -188,42 +189,69 @@
// functionLiteral adds a function literal completion item for the
// given signature.
-func (c *completer) functionLiteral(sig *types.Signature, matchScore float64) {
+func (c *completer) functionLiteral(ctx context.Context, sig *types.Signature, matchScore float64) {
snip := &snippet.Builder{}
snip.WriteText("func(")
- seenParamNames := make(map[string]bool)
+
+ // First we generate names for each param and keep a seen count so
+ // we know if we need to uniquify param names. For example,
+ // "func(int)" will become "func(i int)", but "func(int, int64)"
+ // will become "func(i1 int, i2 int64)".
+ var (
+ paramNames = make([]string, sig.Params().Len())
+ paramNameCount = make(map[string]int)
+ )
+ for i := 0; i < sig.Params().Len(); i++ {
+ var (
+ p = sig.Params().At(i)
+ name = p.Name()
+ )
+ if name == "" {
+ // If the param has no name in the signature, guess a name based
+ // on the type. Use an empty qualifier to ignore the package.
+ // For example, we want to name "http.Request" "r", not "hr".
+ name = formatVarType(ctx, c.snapshot, c.pkg, c.file, p, func(p *types.Package) string {
+ return ""
+ })
+ name = abbreviateTypeName(name)
+ }
+ paramNames[i] = name
+ if name != "_" {
+ paramNameCount[name]++
+ }
+ }
+
+ for n, c := range paramNameCount {
+ // Any names we saw more than once will need a unique suffix added
+ // on. Reset the count to 1 to act as the suffix for the first
+ // name.
+ if c >= 2 {
+ paramNameCount[n] = 1
+ } else {
+ delete(paramNameCount, n)
+ }
+ }
+
for i := 0; i < sig.Params().Len(); i++ {
if i > 0 {
snip.WriteText(", ")
}
- p := sig.Params().At(i)
- name := p.Name()
+ var (
+ p = sig.Params().At(i)
+ name = paramNames[i]
+ )
- // If the parameter has no name in the signature, we need to try
- // come up with a parameter name.
- if name == "" {
- // Our parameter names are guesses, so they must be placeholders
- // for easy correction. If placeholders are disabled, don't
- // offer the completion.
- if !c.opts.placeholders {
- return
- }
+ // Uniquify names by adding on an incrementing numeric suffix.
+ if idx, found := paramNameCount[name]; found {
+ paramNameCount[name]++
+ name = fmt.Sprintf("%s%d", name, idx)
+ }
- // Try abbreviating named types. If the type isn't named, or the
- // abbreviation duplicates a previous name, give up and use
- // "_". The user will have to provide a name for this parameter
- // in order to use it.
- if named, ok := deref(p.Type()).(*types.Named); ok {
- name = abbreviateCamel(named.Obj().Name())
- if seenParamNames[name] {
- name = "_"
- } else {
- seenParamNames[name] = true
- }
- } else {
- name = "_"
- }
+ if name != p.Name() && c.opts.placeholders {
+ // If we didn't use the signature's param name verbatim then we
+ // may have chosen a poor name. Give the user a placeholder so
+ // they can easily fix the name.
snip.WritePlaceholder(func(b *snippet.Builder) {
b.WriteText(name)
})
@@ -236,7 +264,7 @@
// of "i int, j int".
if i == sig.Params().Len()-1 || !types.Identical(p.Type(), sig.Params().At(i+1).Type()) {
snip.WriteText(" ")
- typeStr := types.TypeString(p.Type(), c.qf)
+ typeStr := formatVarType(ctx, c.snapshot, c.pkg, c.file, p, c.qf)
if sig.Variadic() && i == sig.Params().Len()-1 {
typeStr = strings.Replace(typeStr, "[]", "...", 1)
}
@@ -264,7 +292,7 @@
if name := r.Name(); name != "" {
snip.WriteText(name + " ")
}
- snip.WriteText(types.TypeString(r.Type(), c.qf))
+ snip.WriteText(formatVarType(ctx, c.snapshot, c.pkg, c.file, r, c.qf))
}
if resultsNeedParens {
snip.WriteText(")")
@@ -282,14 +310,38 @@
})
}
-// abbreviateCamel abbreviates camel case identifiers into
-// abbreviations. For example, "fooBar" is abbreviated "fb".
-func abbreviateCamel(s string) string {
+// abbreviateTypeName abbreviates type names into acronyms. For
+// example, "fooBar" is abbreviated "fb". Care is taken to ignore
+// non-identifier runes. For example, "[]int" becomes "i", and
+// "struct { i int }" becomes "s".
+func abbreviateTypeName(s string) string {
var (
b strings.Builder
useNextUpper bool
)
+
+ // Trim off leading non-letters. We trim everything between "[" and
+ // "]" to handle array types like "[someConst]int".
+ var inBracket bool
+ s = strings.TrimFunc(s, func(r rune) bool {
+ if inBracket {
+ inBracket = r != ']'
+ return true
+ }
+
+ if r == '[' {
+ inBracket = true
+ }
+
+ return !unicode.IsLetter(r)
+ })
+
for i, r := range s {
+ // Stop if we encounter a non-identifier rune.
+ if !unicode.IsLetter(r) && !unicode.IsNumber(r) {
+ break
+ }
+
if i == 0 {
b.WriteRune(unicode.ToLower(r))
}
diff --git a/internal/lsp/source/types_format.go b/internal/lsp/source/types_format.go
index 9fee430..2bf9f26 100644
--- a/internal/lsp/source/types_format.go
+++ b/internal/lsp/source/types_format.go
@@ -14,7 +14,6 @@
"go/types"
"strings"
- "golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/lsp/debug/tag"
"golang.org/x/tools/internal/lsp/protocol"
@@ -218,7 +217,7 @@
// If the request came from a different package than the one in which the
// types are defined, we may need to modify the qualifiers.
- qualified = qualifyExpr(snapshot.FileSet(), qualified, srcpkg, pkg, srcfile, clonedInfo)
+ qualified = qualifyExpr(snapshot.FileSet(), qualified, srcpkg, pkg, srcfile, clonedInfo, qf)
fmted := formatNode(snapshot.FileSet(), qualified)
return fmted
}
@@ -241,7 +240,7 @@
}
// qualifyExpr applies the "pkgName." prefix to any *ast.Ident in the expr.
-func qualifyExpr(fset *token.FileSet, expr ast.Expr, srcpkg, pkg Package, file *ast.File, clonedInfo map[token.Pos]*types.PkgName) ast.Expr {
+func qualifyExpr(fset *token.FileSet, expr ast.Expr, srcpkg, pkg Package, file *ast.File, clonedInfo map[token.Pos]*types.PkgName, qf types.Qualifier) ast.Expr {
ast.Inspect(expr, func(n ast.Node) bool {
switch n := n.(type) {
case *ast.ArrayType, *ast.ChanType, *ast.Ellipsis,
@@ -262,7 +261,7 @@
if !ok {
return false
}
- pkgName := importedPkgName(fset, srcpkg.GetTypes(), obj.Imported(), file)
+ pkgName := qf(obj.Imported())
if pkgName != "" {
x.Name = pkgName
}
@@ -273,7 +272,7 @@
}
// Only add the qualifier if the identifier is exported.
if ast.IsExported(n.Name) {
- pkgName := importedPkgName(fset, srcpkg.GetTypes(), pkg.GetTypes(), file)
+ pkgName := qf(pkg.GetTypes())
n.Name = pkgName + "." + n.Name
}
}
@@ -282,24 +281,6 @@
return expr
}
-// importedPkgName returns the package name used for pkg in srcpkg.
-func importedPkgName(fset *token.FileSet, srcpkg, pkg *types.Package, file *ast.File) string {
- if srcpkg == pkg {
- return ""
- }
- // If the file already imports the package under another name, use that.
- for _, group := range astutil.Imports(fset, file) {
- for _, cand := range group {
- if strings.Trim(cand.Path.Value, `"`) == pkg.Path() {
- if cand.Name != nil && cand.Name.Name != "" {
- return cand.Name.Name
- }
- }
- }
- }
- return pkg.Name()
-}
-
// cloneExpr only clones expressions that appear in the parameters or return
// values of a function declaration. The original expression may be returned
// to the caller in 2 cases:
diff --git a/internal/lsp/testdata/lsp/primarymod/snippets/literal_snippets.go.in b/internal/lsp/testdata/lsp/primarymod/snippets/literal_snippets.go.in
index 69b8cdf..d970bf1 100644
--- a/internal/lsp/testdata/lsp/primarymod/snippets/literal_snippets.go.in
+++ b/internal/lsp/testdata/lsp/primarymod/snippets/literal_snippets.go.in
@@ -131,14 +131,14 @@
sort.Slice(nil, fun) //@complete(")", litFunc),snippet(")", litFunc, "func(i, j int) bool {$0\\}", "func(i, j int) bool {$0\\}")
- http.HandleFunc("", f) //@snippet(")", litFunc, "", "func(${1:rw} http.ResponseWriter, ${2:r} *http.Request) {$0\\}")
+ http.HandleFunc("", f) //@snippet(")", litFunc, "func(rw http.ResponseWriter, r *http.Request) {$0\\}", "func(${1:rw} http.ResponseWriter, ${2:r} *http.Request) {$0\\}")
// no literal "func" completions
http.Handle("", fun) //@complete(")")
http.HandlerFunc() //@item(handlerFunc, "http.HandlerFunc()", "", "var")
http.Handle("", h) //@snippet(")", handlerFunc, "http.HandlerFunc($0)", "http.HandlerFunc($0)")
- http.Handle("", http.HandlerFunc()) //@snippet("))", litFunc, "", "func(${1:rw} http.ResponseWriter, ${2:r} *http.Request) {$0\\}")
+ http.Handle("", http.HandlerFunc()) //@snippet("))", litFunc, "func(rw http.ResponseWriter, r *http.Request) {$0\\}", "func(${1:rw} http.ResponseWriter, ${2:r} *http.Request) {$0\\}")
var namedReturn func(s string) (b bool)
namedReturn = f //@snippet(" //", litFunc, "func(s string) (b bool) {$0\\}", "func(s string) (b bool) {$0\\}")
@@ -150,7 +150,15 @@
multiNamedReturn = f //@snippet(" //", litFunc, "func() (b bool, i int) {$0\\}", "func() (b bool, i int) {$0\\}")
var duplicateParams func(myImpl, int, myImpl)
- duplicateParams = f //@snippet(" //", litFunc, "", "func(${1:mi} myImpl, ${2:_} int, ${3:_} myImpl) {$0\\}")
+ duplicateParams = f //@snippet(" //", litFunc, "func(mi1 myImpl, i int, mi2 myImpl) {$0\\}", "func(${1:mi1} myImpl, ${2:i} int, ${3:mi2} myImpl) {$0\\}")
+
+ type aliasImpl = myImpl
+ var aliasParams func(aliasImpl) aliasImpl
+ aliasParams = f //@snippet(" //", litFunc, "func(ai aliasImpl) aliasImpl {$0\\}", "func(${1:ai} aliasImpl) aliasImpl {$0\\}")
+
+ const two = 2
+ var builtinTypes func([]int, [two]bool, map[string]string, struct{ i int }, interface{ foo() }, <-chan int)
+ builtinTypes = f //@snippet(" //", litFunc, "func(i1 []int, b [two]bool, m map[string]string, s struct{ i int \\}, i2 interface{ foo() \\}, c <-chan int) {$0\\}", "func(${1:i1} []int, ${2:b} [two]bool, ${3:m} map[string]string, ${4:s} struct{ i int \\}, ${5:i2} interface{ foo() \\}, ${6:c} <-chan int) {$0\\}")
}
func _() {
diff --git a/internal/lsp/testdata/lsp/summary.txt.golden b/internal/lsp/testdata/lsp/summary.txt.golden
index 6a2ca71..2ea9450 100644
--- a/internal/lsp/testdata/lsp/summary.txt.golden
+++ b/internal/lsp/testdata/lsp/summary.txt.golden
@@ -2,7 +2,7 @@
CallHierarchyCount = 1
CodeLensCount = 5
CompletionsCount = 239
-CompletionSnippetCount = 81
+CompletionSnippetCount = 83
UnimportedCompletionsCount = 6
DeepCompletionsCount = 5
FuzzyCompletionsCount = 8