Skip to content

Commit d894536

Browse files
committed
highlight
1 parent b624d88 commit d894536

File tree

3 files changed

+188
-2
lines changed

3 files changed

+188
-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

+131-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
}
@@ -69,8 +71,24 @@ func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, po
6971

7072
// highlightPath returns ranges to highlight for the given enclosing path,
7173
// 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) {
74+
func highlightPath(info *types.Info, path []ast.Node, pos token.Pos) (map[posRange]protocol.DocumentHighlightKind, error) {
7375
result := make(map[posRange]protocol.DocumentHighlightKind)
76+
77+
// Inside a printf-style call, printf("...%v...", arg)?
78+
// Treat each corresponding ("%v", arg) pair as a highlight class.
79+
for _, node := range path {
80+
if call, ok := node.(*ast.CallExpr); ok {
81+
idx := formatStringIndex(info, call)
82+
if idx >= 0 && idx < len(call.Args) {
83+
// We only care string literal, so fmt.Sprint("a"+"b%s", "bar") won't highlight.
84+
if lit, ok := call.Args[idx].(*ast.BasicLit); ok && strings.Contains(lit.Value, "%") {
85+
highlightPrintf(call, idx, pos, lit.Value, result)
86+
}
87+
}
88+
}
89+
}
90+
91+
file := path[len(path)-1].(*ast.File)
7492
switch node := path[0].(type) {
7593
case *ast.BasicLit:
7694
// Import path string literal?
@@ -131,6 +149,117 @@ func highlightPath(path []ast.Node, file *ast.File, info *types.Info) (map[posRa
131149
return result, nil
132150
}
133151

152+
// formatStringIndex returns the index of the format string (the last
153+
// non-variadic parameter) within the given printf-like call
154+
// expression, or -1 if unknown.
155+
func formatStringIndex(info *types.Info, call *ast.CallExpr) int {
156+
typ := info.Types[call.Fun].Type
157+
if typ == nil {
158+
return -1 // missing type
159+
}
160+
sig, ok := typ.(*types.Signature)
161+
if !ok {
162+
return -1 // ill-typed
163+
}
164+
if !sig.Variadic() {
165+
// Skip checking non-variadic functions.
166+
return -1
167+
}
168+
idx := sig.Params().Len() - 2
169+
if idx < 0 {
170+
// Skip checking variadic functions without
171+
// fixed arguments.
172+
return -1
173+
}
174+
return idx
175+
}
176+
177+
// highlightPrintf highlights operations 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+
// fmt.Printf("%[1]d %[1].2d", 3)
192+
//
193+
// When cursor is in `%[1]d`, we record `3` being successfully highlighted.
194+
// And because we will also record `%[1].2d`'s corresponding arguments index is `3`
195+
// in `visited`, even though it will not highlight any item in the first pass,
196+
// in the second pass we can correctly highlight it. So the three are the same class.
197+
succeededArg := 0
198+
visited := make(map[posRange]int, 0)
199+
200+
formatPos := call.Args[idx].Pos()
201+
// highlightPair highlights the operation and its potential argument pair if the cursor is within either range.
202+
highlightPair := func(start, end token.Pos, argIndex int) {
203+
var (
204+
rangeStart = formatPos + token.Pos(start)
205+
rangeEnd = formatPos + token.Pos(end)
206+
arg ast.Expr // may not exist
207+
)
208+
visited[posRange{start: rangeStart, end: rangeEnd}] = argIndex
209+
if len(call.Args) > argIndex {
210+
arg = call.Args[argIndex]
211+
}
212+
213+
if (cursorPos >= rangeStart && cursorPos < rangeEnd) || (arg != nil && cursorPos >= arg.Pos() && cursorPos < arg.End()) {
214+
highlightRange(result, rangeStart, rangeEnd, protocol.Write)
215+
if arg != nil {
216+
succeededArg = argIndex
217+
highlightRange(result, arg.Pos(), arg.End(), protocol.Read)
218+
}
219+
}
220+
}
221+
222+
for _, operation := range operations {
223+
// If width or prec has any *, we can not highlight the full range from % to verb,
224+
// because it will overlap with the sub-range of *, for example:
225+
//
226+
// fmt.Printf("%*[3]d", 4, 5, 6)
227+
// ^ ^ we can only highlight this range when cursor in 6. '*' as a one-rune range will
228+
// highlight for 4.
229+
anyAsterisk := false
230+
231+
width, prec, verb := operation.Width, operation.Prec, operation.Verb
232+
// Try highlight Width if there is a *.
233+
if width.Dynamic != -1 {
234+
anyAsterisk = true
235+
highlightPair(token.Pos(width.Range.Start), token.Pos(width.Range.End), width.Dynamic)
236+
}
237+
238+
// Try highlight Precision if there is a *.
239+
if prec.Dynamic != -1 {
240+
anyAsterisk = true
241+
highlightPair(token.Pos(prec.Range.Start), token.Pos(prec.Range.End), prec.Dynamic)
242+
}
243+
244+
// Try highlight Verb.
245+
if verb.Verb != '%' {
246+
// If any * is found inside operation, narrow the highlight range.
247+
if anyAsterisk {
248+
highlightPair(token.Pos(verb.Range.Start), token.Pos(verb.Range.End), verb.ArgIndex)
249+
} else {
250+
highlightPair(token.Pos(operation.Range.Start), token.Pos(operation.Range.End), verb.ArgIndex)
251+
}
252+
}
253+
}
254+
255+
// Second pass, try to highlight those missed operations.
256+
for rang, argIndex := range visited {
257+
if succeededArg == argIndex {
258+
highlightRange(result, rang.start, rang.end, protocol.Write)
259+
}
260+
}
261+
}
262+
134263
type posRange struct {
135264
start, end token.Pos
136265
}
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)