Skip to content

Commit

Permalink
internal/lsp: add support for hovering runes
Browse files Browse the repository at this point in the history
Enable to hover runes found in basic literals in various forms.
When a rune is found, the hover message provides a summary composed of a
printable version (if it exists) of the rune, its codepoint and its name.

Behaviour varies slightly depending on the basic literal: rune literals
always display the summary when hovered, string literals only display it
when an escaped rune sequence is found to avoid providing unnecessary
information, and finally number literals only when expressed as a
hexadecimal number whose size ranges from one to eight bytes.

Fixes golang/go#38239

Change-Id: I024fdd5c511a45c7c285e200ce1eda0669a45491
Reviewed-on: https://go-review.googlesource.com/c/tools/+/321810
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
Trust: Rebecca Stambler <rstambler@golang.org>
Trust: Robert Findley <rfindley@google.com>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
  • Loading branch information
jhchabran authored and stamblerre committed Sep 14, 2021
1 parent 258ee27 commit cd7c003
Show file tree
Hide file tree
Showing 32 changed files with 491 additions and 198 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ require (
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e
golang.org/x/text v0.3.6
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
)
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
Expand Down
4 changes: 4 additions & 0 deletions internal/lsp/cmd/test/cmdtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ func (r *runner) AddImport(t *testing.T, uri span.URI, expectedImport string) {
//TODO: import addition not supported on command line
}

func (r *runner) Hover(t *testing.T, spn span.Span, info string) {
//TODO: hovering not supported on command line
}

func (r *runner) runGoplsCmd(t testing.TB, args ...string) (string, string) {
rStdout, wStdout, err := os.Pipe()
if err != nil {
Expand Down
39 changes: 38 additions & 1 deletion internal/lsp/lsp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -718,7 +718,7 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) {
didSomething := false
if hover != nil {
didSomething = true
tag := fmt.Sprintf("%s-hover", d.Name)
tag := fmt.Sprintf("%s-hoverdef", d.Name)
expectHover := string(r.data.Golden(tag, d.Src.URI().Filename(), func() ([]byte, error) {
return []byte(hover.Contents.Value), nil
}))
Expand Down Expand Up @@ -840,6 +840,43 @@ func (r *runner) Highlight(t *testing.T, src span.Span, locations []span.Span) {
}
}

func (r *runner) Hover(t *testing.T, src span.Span, text string) {
m, err := r.data.Mapper(src.URI())
if err != nil {
t.Fatal(err)
}
loc, err := m.Location(src)
if err != nil {
t.Fatalf("failed for %v", err)
}
tdpp := protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI},
Position: loc.Range.Start,
}
params := &protocol.HoverParams{
TextDocumentPositionParams: tdpp,
}
hover, err := r.server.Hover(r.ctx, params)
if err != nil {
t.Fatal(err)
}
if text == "" {
if hover != nil {
t.Errorf("want nil, got %v\n", hover)
}
} else {
if hover == nil {
t.Fatalf("want hover result to include %s, but got nil", text)
}
if got := hover.Contents.Value; got != text {
t.Errorf("want %v, got %v\n", text, got)
}
if want, got := loc.Range, hover.Range; want != got {
t.Errorf("want range %v, got %v instead", want, got)
}
}
}

func (r *runner) References(t *testing.T, src span.Span, itemList []span.Span) {
sm, err := r.data.Mapper(src.URI())
if err != nil {
Expand Down
155 changes: 155 additions & 0 deletions internal/lsp/source/hover.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ import (
"go/format"
"go/token"
"go/types"
"strconv"
"strings"
"time"
"unicode/utf8"

"golang.org/x/text/unicode/runenames"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/typeparams"
Expand Down Expand Up @@ -66,6 +69,9 @@ type HoverInformation struct {
func Hover(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (*protocol.Hover, error) {
ident, err := Identifier(ctx, snapshot, fh, position)
if err != nil {
if hover, innerErr := hoverRune(ctx, snapshot, fh, position); innerErr == nil {
return hover, nil
}
return nil, nil
}
h, err := HoverIdentifier(ctx, ident)
Expand Down Expand Up @@ -93,6 +99,155 @@ func Hover(ctx context.Context, snapshot Snapshot, fh FileHandle, position proto
}, nil
}

func hoverRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (*protocol.Hover, error) {
ctx, done := event.Start(ctx, "source.hoverRune")
defer done()

r, mrng, err := findRune(ctx, snapshot, fh, position)
if err != nil {
return nil, err
}
rng, err := mrng.Range()
if err != nil {
return nil, err
}

var desc string
runeName := runenames.Name(r)
if len(runeName) > 0 && runeName[0] == '<' {
// Check if the rune looks like an HTML tag. If so, trim the surrounding <>
// characters to work around https://github.com/microsoft/vscode/issues/124042.
runeName = strings.TrimRight(runeName[1:], ">")
}
if strconv.IsPrint(r) {
desc = fmt.Sprintf("'%s', U+%04X, %s", string(r), uint32(r), runeName)
} else {
desc = fmt.Sprintf("U+%04X, %s", uint32(r), runeName)
}
return &protocol.Hover{
Contents: protocol.MarkupContent{
Kind: snapshot.View().Options().PreferredContentFormat,
Value: desc,
},
Range: rng,
}, nil
}

// ErrNoRuneFound is the error returned when no rune is found at a particular position.
var ErrNoRuneFound = errors.New("no rune found")

// findRune returns rune information for a position in a file.
func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) (rune, MappedRange, error) {
pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage)
if err != nil {
return 0, MappedRange{}, err
}
spn, err := pgf.Mapper.PointSpan(pos)
if err != nil {
return 0, MappedRange{}, err
}
rng, err := spn.Range(pgf.Mapper.Converter)
if err != nil {
return 0, MappedRange{}, err
}

// Find the basic literal enclosing the given position, if there is one.
var lit *ast.BasicLit
var found bool
ast.Inspect(pgf.File, func(n ast.Node) bool {
if found {
return false
}
if n, ok := n.(*ast.BasicLit); ok && rng.Start >= n.Pos() && rng.Start <= n.End() {
lit = n
found = true
}
return !found
})
if !found {
return 0, MappedRange{}, ErrNoRuneFound
}

var r rune
var start, end token.Pos
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 0, MappedRange{}, ErrNoRuneFound
}
r, _ = utf8.DecodeRuneInString(s)
if r == utf8.RuneError {
return 0, MappedRange{}, fmt.Errorf("rune error")
}
start, end = lit.Pos(), lit.End()
case token.INT:
// It's an integer, scan only if it is a hex litteral whose bitsize in
// ranging from 8 to 32.
if !(strings.HasPrefix(lit.Value, "0x") && len(lit.Value[2:]) >= 2 && len(lit.Value[2:]) <= 8) {
return 0, MappedRange{}, ErrNoRuneFound
}
v, err := strconv.ParseUint(lit.Value[2:], 16, 32)
if err != nil {
return 0, MappedRange{}, err
}
r = rune(v)
if r == utf8.RuneError {
return 0, MappedRange{}, fmt.Errorf("rune error")
}
start, end = lit.Pos(), lit.End()
case token.STRING:
// It's a string, scan only if it contains a unicode escape sequence under or before the
// current cursor position.
var found bool
strMappedRng, err := posToMappedRange(snapshot, pkg, lit.Pos(), lit.End())
if err != nil {
return 0, MappedRange{}, err
}
strRng, err := strMappedRng.Range()
if err != nil {
return 0, MappedRange{}, err
}
offset := strRng.Start.Character
for i := pos.Character - offset; 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 0, MappedRange{}, fmt.Errorf("rune error")
}
if rr == '\\' {
// Got the beginning, decode it.
var tail string
r, _, 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 0, MappedRange{}, ErrNoRuneFound
}
// Only the rune escape sequence part of the string has to be highlighted, recompute the range.
runeLen := len(lit.Value) - (int(i) + len(tail))
start = token.Pos(int(lit.Pos()) + int(i))
end = token.Pos(int(start) + runeLen)
found = true
break
}
}
if !found {
// No escape sequence found
return 0, MappedRange{}, ErrNoRuneFound
}
default:
return 0, MappedRange{}, ErrNoRuneFound
}

mappedRange, err := posToMappedRange(snapshot, pkg, start, end)
if err != nil {
return 0, MappedRange{}, err
}
return r, mappedRange, nil
}

func HoverIdentifier(ctx context.Context, i *IdentifierInfo) (*HoverInformation, error) {
ctx, done := event.Start(ctx, "source.Hover")
defer done()
Expand Down
35 changes: 33 additions & 2 deletions internal/lsp/source/source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -576,12 +576,12 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) {
didSomething := false
if hover != "" {
didSomething = true
tag := fmt.Sprintf("%s-hover", d.Name)
tag := fmt.Sprintf("%s-hoverdef", d.Name)
expectHover := string(r.data.Golden(tag, d.Src.URI().Filename(), func() ([]byte, error) {
return []byte(hover), nil
}))
if hover != expectHover {
t.Errorf("hover for %s failed:\n%s", d.Src, tests.Diff(t, expectHover, hover))
t.Errorf("hoverdef for %s failed:\n%s", d.Src, tests.Diff(t, expectHover, hover))
}
}
if !d.OnlyHover {
Expand Down Expand Up @@ -682,6 +682,37 @@ func (r *runner) Highlight(t *testing.T, src span.Span, locations []span.Span) {
}
}

func (r *runner) Hover(t *testing.T, src span.Span, text string) {
ctx := r.ctx
_, srcRng, err := spanToRange(r.data, src)
if err != nil {
t.Fatal(err)
}
fh, err := r.snapshot.GetFile(r.ctx, src.URI())
if err != nil {
t.Fatal(err)
}
hover, err := source.Hover(ctx, r.snapshot, fh, srcRng.Start)
if err != nil {
t.Errorf("hover failed for %s: %v", src.URI(), err)
}
if text == "" {
if hover != nil {
t.Errorf("want nil, got %v\n", hover)
}
} else {
if hover == nil {
t.Fatalf("want hover result to not be nil")
}
if got := hover.Contents.Value; got != text {
t.Errorf("want %v, got %v\n", got, text)
}
if want, got := srcRng, hover.Range; want != got {
t.Errorf("want range %v, got %v instead", want, got)
}
}
}

func (r *runner) References(t *testing.T, src span.Span, itemList []span.Span) {
ctx := r.ctx
_, srcRng, err := spanToRange(r.data, src)
Expand Down
43 changes: 43 additions & 0 deletions internal/lsp/testdata/basiclit/basiclit.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,47 @@ func _() {
_ = 1. //@complete(".")

_ = 'a' //@complete("' ")

_ = 'a' //@hover("'a'", "'a', U+0061, LATIN SMALL LETTER A")
_ = 0x61 //@hover("0x61", "'a', U+0061, LATIN SMALL LETTER A")

_ = '\u2211' //@hover("'\\u2211'", "'∑', U+2211, N-ARY SUMMATION")
_ = 0x2211 //@hover("0x2211", "'∑', U+2211, N-ARY SUMMATION")
_ = "foo \u2211 bar" //@hover("\\u2211", "'∑', U+2211, N-ARY SUMMATION")

_ = '\a' //@hover("'\\a'", "U+0007, control")
_ = "foo \a bar" //@hover("\\a", "U+0007, control")

_ = '\U0001F30A' //@hover("'\\U0001F30A'", "'🌊', U+1F30A, WATER WAVE")
_ = 0x0001F30A //@hover("0x0001F30A", "'🌊', U+1F30A, WATER WAVE")
_ = "foo \U0001F30A bar" //@hover("\\U0001F30A", "'🌊', U+1F30A, WATER WAVE")

_ = '\x7E' //@hover("'\\x7E'", "'~', U+007E, TILDE")
_ = "foo \x7E bar" //@hover("\\x7E", "'~', U+007E, TILDE")
_ = "foo \a bar" //@hover("\\a", "U+0007, control")

_ = '\173' //@hover("'\\173'", "'{', U+007B, LEFT CURLY BRACKET")
_ = "foo \173 bar" //@hover("\\173", "'{', U+007B, LEFT CURLY BRACKET")
_ = "foo \173 bar \u2211 baz" //@hover("\\173", "'{', U+007B, LEFT CURLY BRACKET")
_ = "foo \173 bar \u2211 baz" //@hover("\\u2211", "'∑', U+2211, N-ARY SUMMATION")
_ = "foo\173bar\u2211baz" //@hover("\\173", "'{', U+007B, LEFT CURLY BRACKET")
_ = "foo\173bar\u2211baz" //@hover("\\u2211", "'∑', U+2211, N-ARY SUMMATION")

// search for runes in string only if there is an escaped sequence
_ = "hello" //@hover("\"hello\"", "")

// incorrect escaped rune sequences
_ = '\0' //@hover("'\\0'", "")
_ = '\u22111' //@hover("'\\u22111'", "")
_ = '\U00110000' //@hover("'\\U00110000'", "")
_ = '\u12e45'//@hover("'\\u12e45'", "")
_ = '\xa' //@hover("'\\xa'", "")
_ = 'aa' //@hover("'aa'", "")

// other basic lits
_ = 1 //@hover("1", "")
_ = 1.2 //@hover("1.2", "")
_ = 1.2i //@hover("1.2i", "")
_ = 0123 //@hover("0123", "")
_ = 0x1234567890 //@hover("0x1234567890", "")
}
2 changes: 1 addition & 1 deletion internal/lsp/testdata/cgo/declarecgo.go.golden
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func Example()
"description": "```go\nfunc Example()\n```\n\n[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo?utm_source=gopls#Example)"
}

-- funccgoexample-hover --
-- funccgoexample-hoverdef --
```go
func Example()
```
Expand Down
2 changes: 1 addition & 1 deletion internal/lsp/testdata/cgoimport/usecgo.go.golden
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func cgo.Example()
"description": "```go\nfunc cgo.Example()\n```\n\n[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/cgo?utm_source=gopls#Example)"
}

-- funccgoexample-hover --
-- funccgoexample-hoverdef --
```go
func cgo.Example()
```
Expand Down
Loading

0 comments on commit cd7c003

Please sign in to comment.