Skip to content

Commit cd7c003

Browse files
jhchabranstamblerre
authored andcommitted
internal/lsp: add support for hovering runes
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 <[email protected]> Trust: Rebecca Stambler <[email protected]> Trust: Robert Findley <[email protected]> Run-TryBot: Rebecca Stambler <[email protected]> gopls-CI: kokoro <[email protected]> TryBot-Result: Go Bot <[email protected]>
1 parent 258ee27 commit cd7c003

32 files changed

+491
-198
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ require (
88
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d
99
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
1010
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e
11+
golang.org/x/text v0.3.6
1112
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
1213
)

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
1818
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA=
1919
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
2020
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
21+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
22+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
2123
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
24+
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
2225
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
2326
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
2427
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=

internal/lsp/cmd/test/cmdtest.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ func (r *runner) AddImport(t *testing.T, uri span.URI, expectedImport string) {
108108
//TODO: import addition not supported on command line
109109
}
110110

111+
func (r *runner) Hover(t *testing.T, spn span.Span, info string) {
112+
//TODO: hovering not supported on command line
113+
}
114+
111115
func (r *runner) runGoplsCmd(t testing.TB, args ...string) (string, string) {
112116
rStdout, wStdout, err := os.Pipe()
113117
if err != nil {

internal/lsp/lsp_test.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -718,7 +718,7 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) {
718718
didSomething := false
719719
if hover != nil {
720720
didSomething = true
721-
tag := fmt.Sprintf("%s-hover", d.Name)
721+
tag := fmt.Sprintf("%s-hoverdef", d.Name)
722722
expectHover := string(r.data.Golden(tag, d.Src.URI().Filename(), func() ([]byte, error) {
723723
return []byte(hover.Contents.Value), nil
724724
}))
@@ -840,6 +840,43 @@ func (r *runner) Highlight(t *testing.T, src span.Span, locations []span.Span) {
840840
}
841841
}
842842

843+
func (r *runner) Hover(t *testing.T, src span.Span, text string) {
844+
m, err := r.data.Mapper(src.URI())
845+
if err != nil {
846+
t.Fatal(err)
847+
}
848+
loc, err := m.Location(src)
849+
if err != nil {
850+
t.Fatalf("failed for %v", err)
851+
}
852+
tdpp := protocol.TextDocumentPositionParams{
853+
TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI},
854+
Position: loc.Range.Start,
855+
}
856+
params := &protocol.HoverParams{
857+
TextDocumentPositionParams: tdpp,
858+
}
859+
hover, err := r.server.Hover(r.ctx, params)
860+
if err != nil {
861+
t.Fatal(err)
862+
}
863+
if text == "" {
864+
if hover != nil {
865+
t.Errorf("want nil, got %v\n", hover)
866+
}
867+
} else {
868+
if hover == nil {
869+
t.Fatalf("want hover result to include %s, but got nil", text)
870+
}
871+
if got := hover.Contents.Value; got != text {
872+
t.Errorf("want %v, got %v\n", text, got)
873+
}
874+
if want, got := loc.Range, hover.Range; want != got {
875+
t.Errorf("want range %v, got %v instead", want, got)
876+
}
877+
}
878+
}
879+
843880
func (r *runner) References(t *testing.T, src span.Span, itemList []span.Span) {
844881
sm, err := r.data.Mapper(src.URI())
845882
if err != nil {

internal/lsp/source/hover.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@ import (
1414
"go/format"
1515
"go/token"
1616
"go/types"
17+
"strconv"
1718
"strings"
1819
"time"
20+
"unicode/utf8"
1921

22+
"golang.org/x/text/unicode/runenames"
2023
"golang.org/x/tools/internal/event"
2124
"golang.org/x/tools/internal/lsp/protocol"
2225
"golang.org/x/tools/internal/typeparams"
@@ -66,6 +69,9 @@ type HoverInformation struct {
6669
func Hover(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (*protocol.Hover, error) {
6770
ident, err := Identifier(ctx, snapshot, fh, position)
6871
if err != nil {
72+
if hover, innerErr := hoverRune(ctx, snapshot, fh, position); innerErr == nil {
73+
return hover, nil
74+
}
6975
return nil, nil
7076
}
7177
h, err := HoverIdentifier(ctx, ident)
@@ -93,6 +99,155 @@ func Hover(ctx context.Context, snapshot Snapshot, fh FileHandle, position proto
9399
}, nil
94100
}
95101

102+
func hoverRune(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (*protocol.Hover, error) {
103+
ctx, done := event.Start(ctx, "source.hoverRune")
104+
defer done()
105+
106+
r, mrng, err := findRune(ctx, snapshot, fh, position)
107+
if err != nil {
108+
return nil, err
109+
}
110+
rng, err := mrng.Range()
111+
if err != nil {
112+
return nil, err
113+
}
114+
115+
var desc string
116+
runeName := runenames.Name(r)
117+
if len(runeName) > 0 && runeName[0] == '<' {
118+
// Check if the rune looks like an HTML tag. If so, trim the surrounding <>
119+
// characters to work around https://github.com/microsoft/vscode/issues/124042.
120+
runeName = strings.TrimRight(runeName[1:], ">")
121+
}
122+
if strconv.IsPrint(r) {
123+
desc = fmt.Sprintf("'%s', U+%04X, %s", string(r), uint32(r), runeName)
124+
} else {
125+
desc = fmt.Sprintf("U+%04X, %s", uint32(r), runeName)
126+
}
127+
return &protocol.Hover{
128+
Contents: protocol.MarkupContent{
129+
Kind: snapshot.View().Options().PreferredContentFormat,
130+
Value: desc,
131+
},
132+
Range: rng,
133+
}, nil
134+
}
135+
136+
// ErrNoRuneFound is the error returned when no rune is found at a particular position.
137+
var ErrNoRuneFound = errors.New("no rune found")
138+
139+
// findRune returns rune information for a position in a file.
140+
func findRune(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) (rune, MappedRange, error) {
141+
pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage)
142+
if err != nil {
143+
return 0, MappedRange{}, err
144+
}
145+
spn, err := pgf.Mapper.PointSpan(pos)
146+
if err != nil {
147+
return 0, MappedRange{}, err
148+
}
149+
rng, err := spn.Range(pgf.Mapper.Converter)
150+
if err != nil {
151+
return 0, MappedRange{}, err
152+
}
153+
154+
// Find the basic literal enclosing the given position, if there is one.
155+
var lit *ast.BasicLit
156+
var found bool
157+
ast.Inspect(pgf.File, func(n ast.Node) bool {
158+
if found {
159+
return false
160+
}
161+
if n, ok := n.(*ast.BasicLit); ok && rng.Start >= n.Pos() && rng.Start <= n.End() {
162+
lit = n
163+
found = true
164+
}
165+
return !found
166+
})
167+
if !found {
168+
return 0, MappedRange{}, ErrNoRuneFound
169+
}
170+
171+
var r rune
172+
var start, end token.Pos
173+
switch lit.Kind {
174+
case token.CHAR:
175+
s, err := strconv.Unquote(lit.Value)
176+
if err != nil {
177+
// If the conversion fails, it's because of an invalid syntax, therefore
178+
// there is no rune to be found.
179+
return 0, MappedRange{}, ErrNoRuneFound
180+
}
181+
r, _ = utf8.DecodeRuneInString(s)
182+
if r == utf8.RuneError {
183+
return 0, MappedRange{}, fmt.Errorf("rune error")
184+
}
185+
start, end = lit.Pos(), lit.End()
186+
case token.INT:
187+
// It's an integer, scan only if it is a hex litteral whose bitsize in
188+
// ranging from 8 to 32.
189+
if !(strings.HasPrefix(lit.Value, "0x") && len(lit.Value[2:]) >= 2 && len(lit.Value[2:]) <= 8) {
190+
return 0, MappedRange{}, ErrNoRuneFound
191+
}
192+
v, err := strconv.ParseUint(lit.Value[2:], 16, 32)
193+
if err != nil {
194+
return 0, MappedRange{}, err
195+
}
196+
r = rune(v)
197+
if r == utf8.RuneError {
198+
return 0, MappedRange{}, fmt.Errorf("rune error")
199+
}
200+
start, end = lit.Pos(), lit.End()
201+
case token.STRING:
202+
// It's a string, scan only if it contains a unicode escape sequence under or before the
203+
// current cursor position.
204+
var found bool
205+
strMappedRng, err := posToMappedRange(snapshot, pkg, lit.Pos(), lit.End())
206+
if err != nil {
207+
return 0, MappedRange{}, err
208+
}
209+
strRng, err := strMappedRng.Range()
210+
if err != nil {
211+
return 0, MappedRange{}, err
212+
}
213+
offset := strRng.Start.Character
214+
for i := pos.Character - offset; i > 0; i-- {
215+
// Start at the cursor position and search backward for the beginning of a rune escape sequence.
216+
rr, _ := utf8.DecodeRuneInString(lit.Value[i:])
217+
if rr == utf8.RuneError {
218+
return 0, MappedRange{}, fmt.Errorf("rune error")
219+
}
220+
if rr == '\\' {
221+
// Got the beginning, decode it.
222+
var tail string
223+
r, _, tail, err = strconv.UnquoteChar(lit.Value[i:], '"')
224+
if err != nil {
225+
// If the conversion fails, it's because of an invalid syntax, therefore is no rune to be found.
226+
return 0, MappedRange{}, ErrNoRuneFound
227+
}
228+
// Only the rune escape sequence part of the string has to be highlighted, recompute the range.
229+
runeLen := len(lit.Value) - (int(i) + len(tail))
230+
start = token.Pos(int(lit.Pos()) + int(i))
231+
end = token.Pos(int(start) + runeLen)
232+
found = true
233+
break
234+
}
235+
}
236+
if !found {
237+
// No escape sequence found
238+
return 0, MappedRange{}, ErrNoRuneFound
239+
}
240+
default:
241+
return 0, MappedRange{}, ErrNoRuneFound
242+
}
243+
244+
mappedRange, err := posToMappedRange(snapshot, pkg, start, end)
245+
if err != nil {
246+
return 0, MappedRange{}, err
247+
}
248+
return r, mappedRange, nil
249+
}
250+
96251
func HoverIdentifier(ctx context.Context, i *IdentifierInfo) (*HoverInformation, error) {
97252
ctx, done := event.Start(ctx, "source.Hover")
98253
defer done()

internal/lsp/source/source_test.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -576,12 +576,12 @@ func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) {
576576
didSomething := false
577577
if hover != "" {
578578
didSomething = true
579-
tag := fmt.Sprintf("%s-hover", d.Name)
579+
tag := fmt.Sprintf("%s-hoverdef", d.Name)
580580
expectHover := string(r.data.Golden(tag, d.Src.URI().Filename(), func() ([]byte, error) {
581581
return []byte(hover), nil
582582
}))
583583
if hover != expectHover {
584-
t.Errorf("hover for %s failed:\n%s", d.Src, tests.Diff(t, expectHover, hover))
584+
t.Errorf("hoverdef for %s failed:\n%s", d.Src, tests.Diff(t, expectHover, hover))
585585
}
586586
}
587587
if !d.OnlyHover {
@@ -682,6 +682,37 @@ func (r *runner) Highlight(t *testing.T, src span.Span, locations []span.Span) {
682682
}
683683
}
684684

685+
func (r *runner) Hover(t *testing.T, src span.Span, text string) {
686+
ctx := r.ctx
687+
_, srcRng, err := spanToRange(r.data, src)
688+
if err != nil {
689+
t.Fatal(err)
690+
}
691+
fh, err := r.snapshot.GetFile(r.ctx, src.URI())
692+
if err != nil {
693+
t.Fatal(err)
694+
}
695+
hover, err := source.Hover(ctx, r.snapshot, fh, srcRng.Start)
696+
if err != nil {
697+
t.Errorf("hover failed for %s: %v", src.URI(), err)
698+
}
699+
if text == "" {
700+
if hover != nil {
701+
t.Errorf("want nil, got %v\n", hover)
702+
}
703+
} else {
704+
if hover == nil {
705+
t.Fatalf("want hover result to not be nil")
706+
}
707+
if got := hover.Contents.Value; got != text {
708+
t.Errorf("want %v, got %v\n", got, text)
709+
}
710+
if want, got := srcRng, hover.Range; want != got {
711+
t.Errorf("want range %v, got %v instead", want, got)
712+
}
713+
}
714+
}
715+
685716
func (r *runner) References(t *testing.T, src span.Span, itemList []span.Span) {
686717
ctx := r.ctx
687718
_, srcRng, err := spanToRange(r.data, src)

internal/lsp/testdata/basiclit/basiclit.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,47 @@ func _() {
1010
_ = 1. //@complete(".")
1111

1212
_ = 'a' //@complete("' ")
13+
14+
_ = 'a' //@hover("'a'", "'a', U+0061, LATIN SMALL LETTER A")
15+
_ = 0x61 //@hover("0x61", "'a', U+0061, LATIN SMALL LETTER A")
16+
17+
_ = '\u2211' //@hover("'\\u2211'", "'∑', U+2211, N-ARY SUMMATION")
18+
_ = 0x2211 //@hover("0x2211", "'∑', U+2211, N-ARY SUMMATION")
19+
_ = "foo \u2211 bar" //@hover("\\u2211", "'∑', U+2211, N-ARY SUMMATION")
20+
21+
_ = '\a' //@hover("'\\a'", "U+0007, control")
22+
_ = "foo \a bar" //@hover("\\a", "U+0007, control")
23+
24+
_ = '\U0001F30A' //@hover("'\\U0001F30A'", "'🌊', U+1F30A, WATER WAVE")
25+
_ = 0x0001F30A //@hover("0x0001F30A", "'🌊', U+1F30A, WATER WAVE")
26+
_ = "foo \U0001F30A bar" //@hover("\\U0001F30A", "'🌊', U+1F30A, WATER WAVE")
27+
28+
_ = '\x7E' //@hover("'\\x7E'", "'~', U+007E, TILDE")
29+
_ = "foo \x7E bar" //@hover("\\x7E", "'~', U+007E, TILDE")
30+
_ = "foo \a bar" //@hover("\\a", "U+0007, control")
31+
32+
_ = '\173' //@hover("'\\173'", "'{', U+007B, LEFT CURLY BRACKET")
33+
_ = "foo \173 bar" //@hover("\\173", "'{', U+007B, LEFT CURLY BRACKET")
34+
_ = "foo \173 bar \u2211 baz" //@hover("\\173", "'{', U+007B, LEFT CURLY BRACKET")
35+
_ = "foo \173 bar \u2211 baz" //@hover("\\u2211", "'∑', U+2211, N-ARY SUMMATION")
36+
_ = "foo\173bar\u2211baz" //@hover("\\173", "'{', U+007B, LEFT CURLY BRACKET")
37+
_ = "foo\173bar\u2211baz" //@hover("\\u2211", "'∑', U+2211, N-ARY SUMMATION")
38+
39+
// search for runes in string only if there is an escaped sequence
40+
_ = "hello" //@hover("\"hello\"", "")
41+
42+
// incorrect escaped rune sequences
43+
_ = '\0' //@hover("'\\0'", "")
44+
_ = '\u22111' //@hover("'\\u22111'", "")
45+
_ = '\U00110000' //@hover("'\\U00110000'", "")
46+
_ = '\u12e45'//@hover("'\\u12e45'", "")
47+
_ = '\xa' //@hover("'\\xa'", "")
48+
_ = 'aa' //@hover("'aa'", "")
49+
50+
// other basic lits
51+
_ = 1 //@hover("1", "")
52+
_ = 1.2 //@hover("1.2", "")
53+
_ = 1.2i //@hover("1.2i", "")
54+
_ = 0123 //@hover("0123", "")
55+
_ = 0x1234567890 //@hover("0x1234567890", "")
1356
}

internal/lsp/testdata/cgo/declarecgo.go.golden

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func Example()
2222
"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)"
2323
}
2424

25-
-- funccgoexample-hover --
25+
-- funccgoexample-hoverdef --
2626
```go
2727
func Example()
2828
```

internal/lsp/testdata/cgoimport/usecgo.go.golden

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func cgo.Example()
2222
"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)"
2323
}
2424

25-
-- funccgoexample-hover --
25+
-- funccgoexample-hoverdef --
2626
```go
2727
func cgo.Example()
2828
```

0 commit comments

Comments
 (0)