Skip to content

Commit da0e15e

Browse files
committed
highlight
1 parent b624d88 commit da0e15e

File tree

3 files changed

+192
-2
lines changed

3 files changed

+192
-2
lines changed

gopls/doc/release/v0.18.0.md

+12
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,15 @@ The Definition query now supports additional locations:
9797

9898
When invoked on a return statement, hover reports the types of
9999
the function's result variables.
100+
101+
## Improvements to "DocumentHighlight"
102+
103+
When your cursor is inside a printf-like function, gopls now provide
104+
DocumentHighlight as visual cues to differentiate how operands are used in the format string.
105+
106+
```go
107+
fmt.Printf("Hello %s, you scored %d", name, score)
108+
```
109+
110+
If the cursor is either on `%s` or `name`, gopls will highlight `%s` as a write operation,
111+
and `name` as a read operation.

gopls/internal/golang/highlight.go

+135-2
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ import (
1010
"go/ast"
1111
"go/token"
1212
"go/types"
13+
"strings"
1314

1415
"golang.org/x/tools/go/ast/astutil"
1516
"golang.org/x/tools/gopls/internal/cache"
1617
"golang.org/x/tools/gopls/internal/file"
1718
"golang.org/x/tools/gopls/internal/protocol"
1819
"golang.org/x/tools/internal/event"
20+
"golang.org/x/tools/internal/fmtstr"
1921
)
2022

2123
func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) ([]protocol.DocumentHighlight, error) {
@@ -49,7 +51,7 @@ func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, po
4951
}
5052
}
5153
}
52-
result, err := highlightPath(path, pgf.File, pkg.TypesInfo())
54+
result, err := highlightPath(pkg.TypesInfo(), path, pos)
5355
if err != nil {
5456
return nil, err
5557
}
@@ -67,10 +69,51 @@ func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, po
6769
return ranges, nil
6870
}
6971

72+
// formatStringIndex returns the index of the format string (the last
73+
// non-variadic parameter) within the given printf-like call
74+
// expression, or -1 if unknown.
75+
func formatStringIndex(info *types.Info, call *ast.CallExpr) int {
76+
typ := info.Types[call.Fun].Type
77+
if typ == nil {
78+
return -1 // missing type
79+
}
80+
sig, ok := typ.(*types.Signature)
81+
if !ok {
82+
return -1 // ill-typed
83+
}
84+
if !sig.Variadic() {
85+
// Skip checking non-variadic functions.
86+
return -1
87+
}
88+
idx := sig.Params().Len() - 2
89+
if idx < 0 {
90+
// Skip checking variadic functions without
91+
// fixed arguments.
92+
return -1
93+
}
94+
return idx
95+
}
96+
7097
// highlightPath returns ranges to highlight for the given enclosing path,
7198
// which should be the result of astutil.PathEnclosingInterval.
72-
func highlightPath(path []ast.Node, file *ast.File, info *types.Info) (map[posRange]protocol.DocumentHighlightKind, error) {
99+
func highlightPath(info *types.Info, path []ast.Node, pos token.Pos) (map[posRange]protocol.DocumentHighlightKind, error) {
73100
result := make(map[posRange]protocol.DocumentHighlightKind)
101+
102+
// Inside a printf-style call, printf("...%v...", arg)?
103+
// Treat each corresponding ("%v", arg) pair as a highlight class.
104+
for _, node := range path {
105+
if call, ok := node.(*ast.CallExpr); ok {
106+
idx := formatStringIndex(info, call)
107+
if idx >= 0 && idx < len(call.Args) {
108+
// We only care string literal, so fmt.Sprint("a"+"b%s", "bar") won't highlight.
109+
if lit, ok := call.Args[idx].(*ast.BasicLit); ok && strings.Contains(lit.Value, "%") {
110+
highlightPrintf(call, idx, pos, lit.Value, result)
111+
}
112+
}
113+
}
114+
}
115+
116+
file := path[len(path)-1].(*ast.File)
74117
switch node := path[0].(type) {
75118
case *ast.BasicLit:
76119
// Import path string literal?
@@ -131,6 +174,96 @@ func highlightPath(path []ast.Node, file *ast.File, info *types.Info) (map[posRa
131174
return result, nil
132175
}
133176

177+
// highlightPrintf highlights directives in a format string and their corresponding
178+
// variadic arguments in a printf-style function call.
179+
// For example:
180+
//
181+
// fmt.Printf("Hello %s, you scored %d", name, score)
182+
//
183+
// If the cursor is on %s or name, it will highlight %s as a write operation,
184+
// and name as a read operation.
185+
func highlightPrintf(call *ast.CallExpr, idx int, cursorPos token.Pos, format string, result map[posRange]protocol.DocumentHighlightKind) {
186+
operations, err := fmtstr.Parse(format, idx)
187+
if err != nil {
188+
return
189+
}
190+
191+
// We need to record those posRange of operations that we have tried to highlight
192+
// and which argument is successfully highlighted (there is at most one), to make sure
193+
// we can highlight all same-index operations in a second pass. For example:
194+
//
195+
// fmt.Printf("%[1]d %[1].2d", 3)
196+
//
197+
// When cursor is in `%[1]d`, we record `3` being successfully highlighted.
198+
// And because we will also record `%[1].2d`'s corresponding arguments index is `3`
199+
// in `visited`, even though it will not highlight any item in the first pass,
200+
// in the second pass we can correctly highlight it. So the three are the same class.
201+
succeededArg := 0
202+
visited := make(map[posRange]int, 0)
203+
204+
formatPos := call.Args[idx].Pos()
205+
// highlightPair highlights the directive and its potential argument pair if the cursor is within either range.
206+
highlightPair := func(start, end token.Pos, argIndex int) {
207+
var (
208+
rangeStart = formatPos + token.Pos(start)
209+
rangeEnd = formatPos + token.Pos(end)
210+
arg ast.Expr // may not exist
211+
)
212+
visited[posRange{start: rangeStart, end: rangeEnd}] = argIndex
213+
if len(call.Args) > argIndex {
214+
arg = call.Args[argIndex]
215+
}
216+
217+
if (cursorPos >= rangeStart && cursorPos < rangeEnd) || (arg != nil && cursorPos >= arg.Pos() && cursorPos < arg.End()) {
218+
highlightRange(result, rangeStart, rangeEnd, protocol.Write)
219+
if arg != nil {
220+
succeededArg = argIndex
221+
highlightRange(result, arg.Pos(), arg.End(), protocol.Read)
222+
}
223+
}
224+
}
225+
226+
for _, operation := range operations {
227+
// If width or prec has any *, we can not highlight the full range from % to verb,
228+
// because it will overlap with the sub-range of *, for example:
229+
//
230+
// fmt.Printf("%*[3]d", 4, 5, 6)
231+
// ^ ^ we can only highlight this range when cursor in 6. '*' as a one-rune range will
232+
// highlight for 4.
233+
anyAsterisk := false
234+
235+
width, prec, verb := operation.Width, operation.Prec, operation.Verb
236+
// Try highlight Width if there is a *.
237+
if width.Dynamic != -1 {
238+
anyAsterisk = true
239+
highlightPair(token.Pos(width.Range.Start), token.Pos(width.Range.End), width.Dynamic)
240+
}
241+
242+
// Try highlight Precision if there is a *.
243+
if prec.Dynamic != -1 {
244+
anyAsterisk = true
245+
highlightPair(token.Pos(prec.Range.Start), token.Pos(prec.Range.End), prec.Dynamic)
246+
}
247+
248+
// Try highlight Verb.
249+
if verb.Verb != '%' {
250+
// If any * is found inside directive, narrow the highlight range.
251+
if anyAsterisk {
252+
highlightPair(token.Pos(verb.Range.Start), token.Pos(verb.Range.End), verb.ArgIndex)
253+
} else {
254+
highlightPair(token.Pos(operation.Range.Start), token.Pos(operation.Range.End), verb.ArgIndex)
255+
}
256+
}
257+
}
258+
259+
// Second pass, try to highlight those missed operations.
260+
for rang, argIndex := range visited {
261+
if succeededArg == argIndex {
262+
highlightRange(result, rang.start, rang.end, protocol.Write)
263+
}
264+
}
265+
}
266+
134267
type posRange struct {
135268
start, end token.Pos
136269
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
This test checks functionality of the printf-like directives and operands highlight.
2+
-- flags --
3+
-ignore_extra_diags
4+
-- highlights.go --
5+
package highlightprintf
6+
import (
7+
"fmt"
8+
)
9+
func BasicPrintfHighlights() {
10+
fmt.Printf("Hello %s, you have %d new messages!", "Alice", 5) //@hiloc(normals, "%s", write),hiloc(normalarg0, "\"Alice\"", read),highlightall(normals, normalarg0)
11+
fmt.Printf("Hello %s, you have %d new messages!", "Alice", 5) //@hiloc(normald, "%d", write),hiloc(normalargs1, "5", read),highlightall(normald, normalargs1)
12+
}
13+
func ComplexPrintfHighlights() {
14+
fmt.Printf("Hello %#3.4s, you have %-2.3d new messages!", "Alice", 5) //@hiloc(complexs, "%#3.4s", write),hiloc(complexarg0, "\"Alice\"", read),highlightall(complexs, complexarg0)
15+
fmt.Printf("Hello %#3.4s, you have %-2.3d new messages!", "Alice", 5) //@hiloc(complexd, "%-2.3d", write),hiloc(complexarg1, "5", read),highlightall(complexd, complexarg1)
16+
}
17+
func MissingDirectives() {
18+
fmt.Printf("Hello %s, you have 5 new messages!", "Alice", 5) //@hiloc(missings, "%s", write),hiloc(missingargs0, "\"Alice\"", read),highlightall(missings, missingargs0)
19+
}
20+
func TooManyDirectives() {
21+
fmt.Printf("Hello %s, you have %d new %s %q messages!", "Alice", 5) //@hiloc(toomanys, "%s", write),hiloc(toomanyargs0, "\"Alice\"", read),highlightall(toomanys, toomanyargs0)
22+
fmt.Printf("Hello %s, you have %d new %s %q messages!", "Alice", 5) //@hiloc(toomanyd, "%d", write),hiloc(toomanyargs1, "5", read),highlightall(toomanyd, toomanyargs1)
23+
}
24+
func VerbIsPercentage() {
25+
fmt.Printf("%4.2% %d", 6) //@hiloc(z1, "%d", write),hiloc(z2, "6", read),highlightall(z1, z2)
26+
}
27+
func SpecialChars() {
28+
fmt.Printf("Hello \n %s, you \t \n have %d new messages!", "Alice", 5) //@hiloc(specials, "%s", write),hiloc(specialargs0, "\"Alice\"", read),highlightall(specials, specialargs0)
29+
fmt.Printf("Hello \n %s, you \t \n have %d new messages!", "Alice", 5) //@hiloc(speciald, "%d", write),hiloc(specialargs1, "5", read),highlightall(speciald, specialargs1)
30+
}
31+
func Escaped() {
32+
fmt.Printf("Hello %% \n %s, you \t%% \n have %d new m%%essages!", "Alice", 5) //@hiloc(escapeds, "%s", write),hiloc(escapedargs0, "\"Alice\"", read),highlightall(escapeds, escapedargs0)
33+
fmt.Printf("Hello %% \n %s, you \t%% \n have %d new m%%essages!", "Alice", 5) //@hiloc(escapedd, "%s", write),hiloc(escapedargs1, "\"Alice\"", read),highlightall(escapedd, escapedargs1)
34+
}
35+
36+
func IndexedAsterisk() {
37+
fmt.Printf("%[1]d", 3) //@hiloc(i1, "%[1]d", write),hiloc(i2, "3", read),highlightall(i1, i2)
38+
fmt.Printf("%[1]*d", 3, 6) //@hiloc(i3, "[1]*", write),hiloc(i4, "3", read),hiloc(i5, "d", write),hiloc(i6, "6", read),highlightall(i3, i4),highlightall(i5, i6)
39+
fmt.Printf("%[2]*[1]d", 3, 4) //@hiloc(i7, "[2]*", write),hiloc(i8, "4", read),hiloc(i9, "[1]d", write),hiloc(i10, "3", read),highlightall(i7, i8),highlightall(i9, i10)
40+
fmt.Printf("%[2]*.[1]*[3]d", 4, 5, 6) //@hiloc(i11, "[2]*", write),hiloc(i12, "5", read),hiloc(i13, ".[1]*", write),hiloc(i14, "4", read),hiloc(i15, "[3]d", write),hiloc(i16, "6", read),highlightall(i11, i12),highlightall(i13, i14),highlightall(i15, i16)
41+
}
42+
43+
func MultipleSameIndexed() {
44+
fmt.Printf("%[1]d %[1].2d", 3) //@hiloc(m1, "%[1]d", write),hiloc(m2, "3", read),hiloc(m3, "%[1].2d", write),highlightall(m1, m2, m3)
45+
}

0 commit comments

Comments
 (0)