1
0
mirror of https://github.com/golang/go synced 2024-10-01 01:18:32 -06:00
go/internal/lsp/source/completion_statements.go
Muir Manders cf2d1e09c8 internal/lsp/source: offer smart "append()" completions
Assigning a slice to the appendage of itself is common and tedious
enough to warrant a special case completion candidate. We now offer
smarter "append()" candidates:

    var foo []int
    foo = app<> // offer "append(foo, <>)"
    fo<> // offer "foo = append(foo, <>)"

The latter is only offered if the best completion candidate is a
slice. It is inserted as the second-best candidate because it seems
impossible to avoid annoying false positives if it is ranked first.

I added a new debug option to disable literal completions. This was to
clean up some test logic that was disabling snippets for all tests
just to defeat literal completions. My tests were failing mysteriously
due to having snippets disabled, and it was hard to figure out why.

Change-Id: I3e8313e00a1409840cb99d5d71c593435a7aeb71
Reviewed-on: https://go-review.googlesource.com/c/tools/+/221777
Run-TryBot: Muir Manders <muir@mnd.rs>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
2020-05-20 22:05:37 +00:00

305 lines
7.6 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 source
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/snippet"
)
// addStatementCandidates adds full statement completion candidates
// appropriate for the current context.
func (c *completer) addStatementCandidates() {
c.addErrCheckAndReturn()
c.addAssignAppend()
}
// addAssignAppend offers a completion candidate of the form:
//
// someSlice = append(someSlice, )
//
// It will offer the "append" completion in two situations:
//
// 1. Position is in RHS of assign, prefix matches "append", and
// corresponding LHS object is a slice. For example,
// "foo = ap<>" completes to "foo = append(foo, )".
//
// Or
//
// 2. Prefix is an ident or selector in an *ast.ExprStmt (i.e.
// beginning of statement), and our best matching candidate is a
// slice. For example: "foo.ba" completes to "foo.bar = append(foo.bar, )".
func (c *completer) addAssignAppend() {
if len(c.path) < 3 {
return
}
ident, _ := c.path[0].(*ast.Ident)
if ident == nil {
return
}
var (
// sliceText is the full name of our slice object, e.g. "s.abc" in
// "s.abc = app<>".
sliceText string
// needsLHS is true if we need to prepend the LHS slice name and
// "=" to our candidate.
needsLHS = false
fset = c.snapshot.View().Session().Cache().FileSet()
)
switch n := c.path[1].(type) {
case *ast.AssignStmt:
// We are already in an assignment. Make sure our prefix matches "append".
if c.matcher.Score("append") <= 0 {
return
}
exprIdx := exprAtPos(c.pos, n.Rhs)
if exprIdx == len(n.Rhs) || exprIdx > len(n.Lhs)-1 {
return
}
lhsType := c.pkg.GetTypesInfo().TypeOf(n.Lhs[exprIdx])
if lhsType == nil {
return
}
// Make sure our corresponding LHS object is a slice.
if _, isSlice := lhsType.Underlying().(*types.Slice); !isSlice {
return
}
// The name or our slice is whatever's in the LHS expression.
sliceText = formatNode(fset, n.Lhs[exprIdx])
case *ast.SelectorExpr:
// Make sure we are a selector at the beginning of a statement.
if _, parentIsExprtStmt := c.path[2].(*ast.ExprStmt); !parentIsExprtStmt {
return
}
// So far we only know the first part of our slice name. For
// example in "s.a<>" we only know our slice begins with "s."
// since the user could still be typing.
sliceText = formatNode(fset, n.X) + "."
needsLHS = true
case *ast.ExprStmt:
needsLHS = true
default:
return
}
var (
label string
snip snippet.Builder
score = highScore
)
if needsLHS {
// Offer the long form assign + append candidate if our best
// candidate is a slice.
bestItem := c.topCandidate()
if bestItem == nil || bestItem.obj == nil || bestItem.obj.Type() == nil {
return
}
if _, isSlice := bestItem.obj.Type().Underlying().(*types.Slice); !isSlice {
return
}
// Don't rank the full form assign + append candidate above the
// slice itself.
score = bestItem.Score - 0.01
// Fill in rest of sliceText now that we have the object name.
sliceText += bestItem.Label
// Fill in the candidate's LHS bits.
label = fmt.Sprintf("%s = ", bestItem.Label)
snip.WriteText(label)
}
snip.WriteText(fmt.Sprintf("append(%s, ", sliceText))
snip.WritePlaceholder(nil)
snip.WriteText(")")
c.items = append(c.items, CompletionItem{
Label: label + fmt.Sprintf("append(%s, )", sliceText),
Kind: protocol.FunctionCompletion,
Score: score,
snippet: &snip,
})
}
// topCandidate returns the strictly highest scoring candidate
// collected so far. If the top two candidates have the same score,
// nil is returned.
func (c *completer) topCandidate() *CompletionItem {
var bestItem, secondBestItem *CompletionItem
for i := range c.items {
if bestItem == nil || c.items[i].Score > bestItem.Score {
bestItem = &c.items[i]
} else if secondBestItem == nil || c.items[i].Score > secondBestItem.Score {
secondBestItem = &c.items[i]
}
}
// If secondBestItem has the same score, bestItem isn't
// the strict best.
if secondBestItem != nil && secondBestItem.Score == bestItem.Score {
return nil
}
return bestItem
}
// addErrCheckAndReturn offers a completion candidate of the form:
//
// if err != nil {
// return nil, err
// }
//
// The position must be in a function that returns an error, and the
// statement preceding the position must be an assignment where the
// final LHS object is an error. addErrCheckAndReturn will synthesize
// zero values as necessary to make the return statement valid.
func (c *completer) addErrCheckAndReturn() {
if len(c.path) < 2 || c.enclosingFunc == nil || !c.opts.placeholders {
return
}
var (
errorType = types.Universe.Lookup("error").Type()
result = c.enclosingFunc.sig.Results()
)
// Make sure our enclosing function returns an error.
if result.Len() == 0 || !types.Identical(result.At(result.Len()-1).Type(), errorType) {
return
}
prevLine := prevStmt(c.pos, c.path)
if prevLine == nil {
return
}
// Make sure our preceding statement was as assignment.
assign, _ := prevLine.(*ast.AssignStmt)
if assign == nil || len(assign.Lhs) == 0 {
return
}
lastAssignee := assign.Lhs[len(assign.Lhs)-1]
// Make sure the final assignee is an error.
if !types.Identical(c.pkg.GetTypesInfo().TypeOf(lastAssignee), errorType) {
return
}
var (
// errText is e.g. "err" in "foo, err := bar()".
errText = formatNode(c.snapshot.View().Session().Cache().FileSet(), lastAssignee)
// Whether we need to include the "if" keyword in our candidate.
needsIf = true
)
// "_" isn't a real object.
if errText == "_" {
return
}
// Below we try to detect if the user has already started typing "if
// err" so we can replace what they've typed with our complete
// statement.
switch n := c.path[0].(type) {
case *ast.Ident:
switch c.path[1].(type) {
case *ast.ExprStmt:
// This handles:
//
// f, err := os.Open("foo")
// i<>
// Make sure they are typing "if".
if c.matcher.Score("if") <= 0 {
return
}
case *ast.IfStmt:
// This handles:
//
// f, err := os.Open("foo")
// if er<>
// Make sure they are typing the error's name.
if c.matcher.Score(errText) <= 0 {
return
}
needsIf = false
default:
return
}
case *ast.IfStmt:
// This handles:
//
// f, err := os.Open("foo")
// if <>
// Avoid false positives by ensuring the if's cond is a bad
// expression. For example, don't offer the completion in cases
// like "if <> somethingElse".
if _, bad := n.Cond.(*ast.BadExpr); !bad {
return
}
// If "if" is our direct prefix, we need to include it in our
// candidate since the existing "if" will be overwritten.
needsIf = c.pos == n.Pos()+token.Pos(len("if"))
}
// Build up a snippet that looks like:
//
// if err != nil {
// return <zero value>, ..., ${1:err}
// }
//
// We make the error a placeholder so it is easy to alter the error.
var snip snippet.Builder
if needsIf {
snip.WriteText("if ")
}
snip.WriteText(fmt.Sprintf("%s != nil {\n\treturn ", errText))
for i := 0; i < result.Len()-1; i++ {
snip.WriteText(formatZeroValue(result.At(i).Type(), c.qf))
snip.WriteText(", ")
}
snip.WritePlaceholder(func(b *snippet.Builder) {
b.WriteText(errText)
})
snip.WriteText("\n}")
label := fmt.Sprintf("%[1]s != nil { return %[1]s }", errText)
if needsIf {
label = "if " + label
}
c.items = append(c.items, CompletionItem{
Label: label,
// There doesn't seem to be a more appropriate kind.
Kind: protocol.KeywordCompletion,
Score: highScore,
snippet: &snip,
})
}