gopls/internal/golang: support hover over selected expression
Hover over constant expression will report value of selected expr,
hover over regular expression will report type of selected expr.
The exact form of hover result is not yet determined, further
investigation or feedback may be needed to know what information
is useful.
Replace hoverLit with hoverConstantExpr, when the seleted range is
pointing to an expression which can resolve to an constant value,
provide the information regarding the value.
Test data hover/nearby.txt deleted. When hover over "*" in context
of "* T", gopls will evaluate the expression and return the type
of "* T".
For golang/go#69058
Change-Id: I4487dea00462f2004b004c11d3a19f0d2f9453d9
Reviewed-on: https://go-review.googlesource.com/c/tools/+/714641
Reviewed-by: Alan Donovan <adonovan@google.com>
Auto-Submit: Hongxiang Jiang <hxjiang@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/gopls/internal/golang/hover.go b/gopls/internal/golang/hover.go
index 5a63244..b5a1620 100644
--- a/gopls/internal/golang/hover.go
+++ b/gopls/internal/golang/hover.go
@@ -27,6 +27,7 @@
"unicode/utf8"
"golang.org/x/text/unicode/runenames"
+ goastutil "golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/gopls/internal/cache"
@@ -310,19 +311,42 @@
}
}
- // Handle hovering over various special kinds of syntax node.
- switch node := cur.Node().(type) {
- case *ast.BasicLit:
- // (import paths were handled above)
- return hoverLit(pgf, node, posRange)
- case *ast.ReturnStmt:
- return hoverReturnStatement(pgf, cur)
- }
-
// By convention, we qualify hover information relative to the package
// from which the request originated.
qual := typesinternal.FileQualifier(pgf.File, pkg.Types())
+ // Handle hovering over various special kinds of syntax node.
+ switch node := cur.Node().(type) {
+ // (import paths were handled above)
+ case *ast.ReturnStmt:
+ return hoverReturnStatement(pgf, cur)
+ case *ast.Ident:
+ // fall through to rest of function
+ case ast.Expr:
+ tv, ok := pkg.TypesInfo().Types[node]
+ if !ok {
+ return protocol.Range{}, nil, nil
+ }
+ if tv.Value != nil {
+ // non-identifier constant expression
+ return hoverConstantExpr(pgf, node, tv, posRange)
+ }
+ // non-constant, non-identifier expression
+ // TODO(hxjiang): what info should we provide other than type of
+ // the selected expression.
+ // TODO(hxjiang): show method set of the given expression' type.
+ r := &hoverResult{
+ Synopsis: goastutil.NodeDescription(node),
+ FullDocumentation: types.TypeString(pkg.TypesInfo().TypeOf(node), qual),
+ }
+
+ highlight, err := pgf.NodeRange(node)
+ if err != nil {
+ return protocol.Range{}, nil, err
+ }
+ return highlight, r, nil
+ }
+
// Handle hover over identifier.
// The general case: compute hover information for the object referenced by
@@ -975,102 +999,122 @@
}, nil
}
-// hoverLit computes hover information when hovering over the basic literal lit
-// in the file pgf. The provided pos must be the exact position of the range as
-// it is used to extract the hovered rune in strings.
+// hoverConstantExpr computes information about the value of a constant expression.
//
-// For example, hovering over "\u2211" in "foo \u2211 bar" yields:
-//
-// '∑', U+2211, N-ARY SUMMATION
-func hoverLit(pgf *parsego.File, lit *ast.BasicLit, posRng astutil.Range) (protocol.Range, *hoverResult, error) {
+// The provided start and end positions must be precise. For example, when hovering
+// over a string literal, the exact cursor position is used to identify and display
+// the value of the rune under the cursor.
+func hoverConstantExpr(pgf *parsego.File, expr ast.Expr, tv types.TypeAndValue, posRange astutil.Range) (protocol.Range, *hoverResult, error) {
var (
value string // if non-empty, a constant value to format in hover
r rune // if non-zero, format a description of this rune in hover
start, end token.Pos // hover span
)
- // Extract a rune from the current position.
- // 'Ω', "...Ω...", or 0x03A9 => 'Ω', U+03A9, GREEK CAPITAL LETTER OMEGA
- switch lit.Kind {
- case token.CHAR:
- s, err := strconv.Unquote(lit.Value)
- if err != nil {
- // If the conversion fails, it's because of an invalid syntax, therefore
- // there is no rune to be found.
- return protocol.Range{}, nil, nil
- }
- r, _ = utf8.DecodeRuneInString(s)
- if r == utf8.RuneError {
- return protocol.Range{}, nil, fmt.Errorf("rune error")
- }
- start, end = lit.Pos(), lit.End()
-
- case token.INT:
- // Short literals (e.g. 99 decimal, 07 octal) are uninteresting.
- if len(lit.Value) < 3 {
- return protocol.Range{}, nil, nil
- }
-
- v := constant.MakeFromLiteral(lit.Value, lit.Kind, 0)
- if v.Kind() != constant.Int {
- return protocol.Range{}, nil, nil
- }
-
- switch lit.Value[:2] {
- case "0x", "0X":
- // As a special case, try to recognize hexadecimal literals as runes if
- // they are within the range of valid unicode values.
- if v, ok := constant.Int64Val(v); ok && v > 0 && v <= utf8.MaxRune && utf8.ValidRune(rune(v)) {
- r = rune(v)
- }
+ if lit, ok := expr.(*ast.BasicLit); ok {
+ switch tv.Value.Kind() {
+ case constant.Bool:
fallthrough
- case "0o", "0O", "0b", "0B":
- // Format the decimal value of non-decimal literals.
- value = v.ExactString()
- start, end = lit.Pos(), lit.End()
- default:
- return protocol.Range{}, nil, nil
- }
-
- case token.STRING:
- // It's a string, scan only if it contains a unicode escape sequence
- // at the current cursor position.
- litOffset, err := safetoken.Offset(pgf.Tok, lit.Pos())
- if err != nil {
- return protocol.Range{}, nil, err
- }
- offset, err := safetoken.Offset(pgf.Tok, posRng.Start)
- if err != nil {
- return protocol.Range{}, nil, err
- }
- for i := offset - litOffset; i > 0; i-- {
- // Start at the cursor position and search backward for the beginning of a rune escape sequence.
- rr, _ := utf8.DecodeRuneInString(lit.Value[i:])
- if rr == utf8.RuneError {
- return protocol.Range{}, nil, fmt.Errorf("rune error")
- }
- if rr == '\\' {
- // Got the beginning, decode it.
- rr, _, tail, err := strconv.UnquoteChar(lit.Value[i:], '"')
+ case constant.Float:
+ fallthrough
+ case constant.Complex:
+ fallthrough
+ case constant.Unknown:
+ // In most cases, basic literals are uninteresting.
+ break
+ case constant.Int:
+ switch lit.Kind {
+ case token.CHAR:
+ s, err := strconv.Unquote(lit.Value)
if err != nil {
- // If the conversion fails, it's because of an invalid syntax,
- // therefore is no rune to be found.
+ // If the conversion fails, it's because of an invalid syntax, therefore
+ // there is no rune to be found.
return protocol.Range{}, nil, nil
}
- // Only the rune escape sequence part of the string has to be
- // highlighted, recompute the range.
- runeLen := len(lit.Value) - (i + len(tail))
- rStart := token.Pos(int(lit.Pos()) + i)
- rEnd := token.Pos(int(rStart) + runeLen)
-
- // A rune is only valid if the range is inside it.
- if rStart <= posRng.Pos() && posRng.End() <= rEnd {
- r = rr
- start = rStart
- end = rEnd
+ r, _ = utf8.DecodeRuneInString(s)
+ if r == utf8.RuneError {
+ return protocol.Range{}, nil, fmt.Errorf("rune error")
}
- break
+ start, end = lit.Pos(), lit.End()
+ case token.INT:
+ // Short literals (e.g. 99 decimal, 07 octal) are uninteresting.
+ if len(lit.Value) < 3 {
+ return protocol.Range{}, nil, nil
+ }
+
+ v := constant.MakeFromLiteral(lit.Value, lit.Kind, 0)
+ if v.Kind() != constant.Int {
+ return protocol.Range{}, nil, nil
+ }
+
+ switch lit.Value[:2] {
+ case "0x", "0X":
+ // As a special case, try to recognize hexadecimal literals as runes if
+ // they are within the range of valid unicode values.
+ if v, ok := constant.Int64Val(v); ok && v > 0 && v <= utf8.MaxRune && utf8.ValidRune(rune(v)) {
+ r = rune(v)
+ }
+ fallthrough
+ case "0o", "0O", "0b", "0B":
+ // Format the decimal value of non-decimal literals.
+ value = v.ExactString()
+ start, end = lit.Pos(), lit.End()
+ default:
+ return protocol.Range{}, nil, nil
+ }
}
+ case constant.String:
+ // Locate the unicode escape sequence under the current cursor
+ // position.
+ litOffset, err := safetoken.Offset(pgf.Tok, lit.Pos())
+ if err != nil {
+ return protocol.Range{}, nil, err
+ }
+ startOffset, err := safetoken.Offset(pgf.Tok, posRange.Pos())
+ if err != nil {
+ return protocol.Range{}, nil, err
+ }
+ for i := startOffset - litOffset; i > 0; i-- {
+ // Start at the cursor position and search backward for the beginning of a rune escape sequence.
+ rr, _ := utf8.DecodeRuneInString(lit.Value[i:])
+ if rr == utf8.RuneError {
+ return protocol.Range{}, nil, fmt.Errorf("rune error")
+ }
+ if rr == '\\' {
+ // Got the beginning, decode it.
+ rr, _, tail, err := strconv.UnquoteChar(lit.Value[i:], '"')
+ if err != nil {
+ // If the conversion fails, it's because of an invalid
+ // syntax, therefore is no rune to be found.
+ return protocol.Range{}, nil, nil
+ }
+ // Only the rune escape sequence part of the string has to
+ // be highlighted, recompute the range.
+ runeLen := len(lit.Value) - (i + len(tail))
+ pStart := token.Pos(int(lit.Pos()) + i)
+ pEnd := token.Pos(int(pStart) + runeLen)
+
+ if pEnd >= posRange.End() {
+ start, end = pStart, pEnd
+ r = rr
+ }
+ break
+ }
+ }
+ default:
+ panic("unexpected constant.Kind")
}
+ } else {
+ // By default, provide value information and the expression's range.
+ // It is possible evaluated value will give us the wrong number because
+ // type checker will perform error recovery.
+ // E.g. expression like '\u22111' + 1 is invalid but type checker
+ // will yield the value of `\u2211` + 1.
+
+ // We could be more smart about whether we want to evaluate an
+ // [ast.Expr] as a number or as a rune by evaluating the syntax of
+ // the expression. e.g. rune + int => rune, rune - int => rune.
+ value = tv.Value.String()
+ start, end = expr.Pos(), expr.End()
}
if value == "" && r == 0 { // nothing to format
diff --git a/gopls/internal/test/marker/testdata/hover/expression.txt b/gopls/internal/test/marker/testdata/hover/expression.txt
index 6812ea2..237e097 100644
--- a/gopls/internal/test/marker/testdata/hover/expression.txt
+++ b/gopls/internal/test/marker/testdata/hover/expression.txt
@@ -5,24 +5,63 @@
import "time"
+const (
+ _ = 'a' + 2//@hover("'a' + 2", "'a' + 2", "99")
+
+ _ = "A" + "B"//@hover("\"A\" + \"B\"", "\"A\" + \"B\"", "AB")
+
+ _ = 2 * 3 * 4//@hover("3 * 4", "2 * 3 * 4", "24")
+
+ _ = (1 < 2)//@hover("1 < 2", "1 < 2", "true")
+
+ _ = (1 + 2i) * (3 + 4i)//@hover("(1 + 2i) * (3 + 4i)", "(1 + 2i) * (3 + 4i)", "(-5 + 10i)")
+)
+
+type Foo struct {
+ foo string
+}
+
func _() {
- _ = time.Now().UTC().Add(10 * time.Minute)//@hover("time.Now().UTC()", _, utc)
+ // type of slice expression.
+ ints := []int{}
+ _ = ints[1:2]//@hover("ints[1:2]", "ints[1:2]", "[]int")
- root := node{}
- _ = root.child(3).name//@hover("root.child(3)", _, child)
+ foos := []Foo{}
+ _ = foos[1:2]//@hover("foos[1:2]", "foos[1:2]", "[]Foo")
+
+ // type of map expression
+ m := map[string]int{"one": 1}
+ _ = m["one"]//@hover("m[\"one\"]", "m[\"one\"]", "int")
+
+ // type of channel expression
+ ch := make(chan bool)
+ _ = <-ch//@hover("<-ch", "<-ch", "bool")
+
+ // type of pointer expression
+ ptr := &ch//@hover("&ch", "&ch", "*chan bool")
+ _ = *ptr//@hover("*ptr", "*ptr", "chan bool")
+
+ // type of unary
+ _ = -ints[0]//@hover("-ints[0]", "-ints[0]", "int")
+
+ // type of type assertion
+ var x any
+
+ _ = x.(*Foo)//@hover("x.(*Foo)", "x.(*Foo)", "*Foo"),diag(re"x.()", re"nil dereference in type assertion")
+
+ // Expression inside of switch statement does not have a type.
+ switch x := x.(type) {//@hover("x.(type)",_, _)
+ default:
+ _ = x
+ }
+
+ // type of function/method return value.
+ _ = time.Now().UTC().Add(10 * time.Minute)//@hover("time.Now().UTC()", "time.Now().UTC()", "time.Time")
+
+ // type of function/method signature.
+ _ = time.Now().UTC().Add(10 * time.Minute)//@hover("time.Now().UTC", "time.Now().UTC", "func() time.Time")
+
+ // type of field
+ var t time.Timer
+ _ = <-t.C//@hover("t.C", "t.C", "<-chan time.Time"),hover("<-t.C", "<-t.C", "time.Time")
}
-
--- lib.go --
-package a
-
-type node struct {
- name string
- children []*node
-}
-
-func (n *node) child(i int) *node {
- return n.children[i]
-}
-
--- @utc --
--- @child --
diff --git a/gopls/internal/test/marker/testdata/hover/hover.txt b/gopls/internal/test/marker/testdata/hover/hover.txt
index 12e13dc..92ebfeb 100644
--- a/gopls/internal/test/marker/testdata/hover/hover.txt
+++ b/gopls/internal/test/marker/testdata/hover/hover.txt
@@ -32,7 +32,7 @@
var y any
switch x := y.(type) { //@hover("x", "x", x)
case int:
- println(x) //@hover("x", "x", xint),hover(re"x()", "x", xint)
+ println(x) //@hover("x", "x", xint)
}
}
-- cmd/main.go --
diff --git a/gopls/internal/test/marker/testdata/hover/nearby.txt b/gopls/internal/test/marker/testdata/hover/nearby.txt
deleted file mode 100644
index 99b2cff..0000000
--- a/gopls/internal/test/marker/testdata/hover/nearby.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-This test checks the "nearby" logic of golang.objectsAt
-as used by Hover. The query is on the "*" of "*T"
-yet resolves to T.
-
--- go.mod --
-module mod.com
-go 1.18
-
--- a/a.go --
-package a
-
-type T int //@ loc(T, "T")
-
-var _ * T //@ hover("*", "T", "type T int")