Skip to content

Commit d7d075e

Browse files
committed
posmap
1 parent d894536 commit d7d075e

File tree

3 files changed

+122
-39
lines changed

3 files changed

+122
-39
lines changed

gopls/doc/release/v0.18.0.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ When invoked on a return statement, hover reports the types of
100100

101101
## Improvements to "DocumentHighlight"
102102

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.
103+
When your cursor is inside a printf-like function, gopls now highlights the relationship between
104+
formatting verbs and arguments as visual cues to differentiate how operands are used in the format string.
105105

106106
```go
107107
fmt.Printf("Hello %s, you scored %d", name, score)

gopls/internal/golang/highlight.go

+98-25
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import (
1010
"go/ast"
1111
"go/token"
1212
"go/types"
13+
"strconv"
1314
"strings"
15+
"unicode/utf8"
1416

1517
"golang.org/x/tools/go/ast/astutil"
1618
"golang.org/x/tools/gopls/internal/cache"
@@ -74,15 +76,16 @@ func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, po
7476
func highlightPath(info *types.Info, path []ast.Node, pos token.Pos) (map[posRange]protocol.DocumentHighlightKind, error) {
7577
result := make(map[posRange]protocol.DocumentHighlightKind)
7678

77-
// Inside a printf-style call, printf("...%v...", arg)?
79+
// Inside a call to a printf-like function (as identified
80+
// by a simple heuristic).
7881
// Treat each corresponding ("%v", arg) pair as a highlight class.
7982
for _, node := range path {
8083
if call, ok := node.(*ast.CallExpr); ok {
8184
idx := formatStringIndex(info, call)
8285
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+
// We only care about literal format strings, so fmt.Sprint("a"+"b%s", "bar") won't be highlighted.
87+
if lit, ok := call.Args[idx].(*ast.BasicLit); ok && lit.Kind == token.STRING {
88+
highlightPrintf(call, idx, pos, lit, result)
8689
}
8790
}
8891
}
@@ -175,14 +178,21 @@ func formatStringIndex(info *types.Info, call *ast.CallExpr) int {
175178
}
176179

177180
// highlightPrintf highlights operations in a format string and their corresponding
178-
// variadic arguments in a printf-style function call.
181+
// variadic arguments in a (possible) printf-style function call.
179182
// For example:
180183
//
181184
// fmt.Printf("Hello %s, you scored %d", name, score)
182185
//
183186
// If the cursor is on %s or name, it will highlight %s as a write operation,
184187
// and name as a read operation.
185-
func highlightPrintf(call *ast.CallExpr, idx int, cursorPos token.Pos, format string, result map[posRange]protocol.DocumentHighlightKind) {
188+
func highlightPrintf(call *ast.CallExpr, idx int, cursorPos token.Pos, lit *ast.BasicLit, result map[posRange]protocol.DocumentHighlightKind) {
189+
format, err := strconv.Unquote(lit.Value)
190+
if err != nil {
191+
return
192+
}
193+
if !strings.Contains(format, "%") {
194+
return
195+
}
186196
operations, err := fmtstr.Parse(format, idx)
187197
if err != nil {
188198
return
@@ -199,18 +209,19 @@ func highlightPrintf(call *ast.CallExpr, idx int, cursorPos token.Pos, format st
199209

200210
formatPos := call.Args[idx].Pos()
201211
// 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) {
212+
highlightPair := func(rang fmtstr.Range, argIndex int) {
203213
var (
204-
rangeStart = formatPos + token.Pos(start)
205-
rangeEnd = formatPos + token.Pos(end)
214+
rangeStart = formatPos + posInStringLiteral(lit.Value, format, rang.Start)
215+
rangeEnd = formatPos + posInStringLiteral(lit.Value, format, rang.End-1) + 1
206216
arg ast.Expr // may not exist
207217
)
208-
visited[posRange{start: rangeStart, end: rangeEnd}] = argIndex
209-
if len(call.Args) > argIndex {
218+
visited[posRange{rangeStart, rangeEnd}] = argIndex
219+
if argIndex < len(call.Args) {
210220
arg = call.Args[argIndex]
211221
}
212222

213-
if (cursorPos >= rangeStart && cursorPos < rangeEnd) || (arg != nil && cursorPos >= arg.Pos() && cursorPos < arg.End()) {
223+
if rangeStart <= cursorPos && cursorPos < rangeEnd ||
224+
arg != nil && arg.Pos() <= cursorPos && cursorPos < arg.End() {
214225
highlightRange(result, rangeStart, rangeEnd, protocol.Write)
215226
if arg != nil {
216227
succeededArg = argIndex
@@ -219,35 +230,34 @@ func highlightPrintf(call *ast.CallExpr, idx int, cursorPos token.Pos, format st
219230
}
220231
}
221232

222-
for _, operation := range operations {
233+
for _, op := range operations {
223234
// If width or prec has any *, we can not highlight the full range from % to verb,
224235
// because it will overlap with the sub-range of *, for example:
225236
//
226237
// fmt.Printf("%*[3]d", 4, 5, 6)
227238
// ^ ^ we can only highlight this range when cursor in 6. '*' as a one-rune range will
228239
// highlight for 4.
229-
anyAsterisk := false
240+
hasAsterisk := false
230241

231-
width, prec, verb := operation.Width, operation.Prec, operation.Verb
232242
// 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)
243+
if op.Width.Dynamic != -1 {
244+
hasAsterisk = true
245+
highlightPair(op.Width.Range, op.Width.Dynamic)
236246
}
237247

238248
// 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)
249+
if op.Prec.Dynamic != -1 {
250+
hasAsterisk = true
251+
highlightPair(op.Prec.Range, op.Prec.Dynamic)
242252
}
243253

244254
// Try highlight Verb.
245-
if verb.Verb != '%' {
255+
if op.Verb.Verb != '%' {
246256
// 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)
257+
if hasAsterisk {
258+
highlightPair(op.Verb.Range, op.Verb.ArgIndex)
249259
} else {
250-
highlightPair(token.Pos(operation.Range.Start), token.Pos(operation.Range.End), verb.ArgIndex)
260+
highlightPair(op.Range, op.Verb.ArgIndex)
251261
}
252262
}
253263
}
@@ -260,6 +270,69 @@ func highlightPrintf(call *ast.CallExpr, idx int, cursorPos token.Pos, format st
260270
}
261271
}
262272

273+
// posInStringLiteral maps an offset in the unquoted string to
274+
// a token.Pos in the source file. The ast.BasicLit.Value is the quoted literal
275+
// as it appears in code, e.g. \x25d. We compare it to the unquoted string
276+
// from strconv.Unquote, e.g. "%d".
277+
func posInStringLiteral(literal string, unquoted string, logicalOffset int) token.Pos {
278+
literalIdx := 1 // Skip the initial quote char.
279+
logIdx := 0
280+
281+
// Advance by one unquoted rune and the corresponding literal string.
282+
advanceRune := func() {
283+
r, size := utf8.DecodeRuneInString(unquoted[logIdx:])
284+
if r == utf8.RuneError && size <= 1 {
285+
// Malformed UTF-8 or end of string,
286+
// move one byte in both strings to avoid infinite loops.
287+
logIdx++
288+
literalIdx++
289+
return
290+
}
291+
logIdx += size
292+
293+
if literalIdx >= len(literal)-1 {
294+
return
295+
}
296+
297+
if literal[literalIdx] == '\\' {
298+
remain := literal[literalIdx:]
299+
escLen := 0
300+
if len(remain) < 2 {
301+
escLen = 1 // just the '\'
302+
}
303+
switch remain[1] {
304+
case 'x':
305+
escLen = 4
306+
case 'u':
307+
escLen = 6
308+
case 'U':
309+
escLen = 10
310+
case 'a', 'b', 'f', 'n', 'r', 't', 'v', '\\', '"':
311+
escLen = 2
312+
case '0', '1', '2', '3', '4', '5', '6', '7':
313+
escLen = 4
314+
default:
315+
return
316+
}
317+
literalIdx += escLen
318+
} else {
319+
// non-escaped character
320+
literalIdx++
321+
}
322+
}
323+
324+
for logIdx < len(unquoted) && (logIdx < logicalOffset) && literalIdx < len(literal)-1 {
325+
advanceRune()
326+
}
327+
328+
// Clamp it to ensure we don't exceed array bounds.
329+
if literalIdx >= len(literal)-1 {
330+
literalIdx = len(literal) - 1
331+
}
332+
333+
return token.Pos(literalIdx)
334+
}
335+
263336
type posRange struct {
264337
start, end token.Pos
265338
}
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,55 @@
1+
12
This test checks functionality of the printf-like directives and operands highlight.
23
-- flags --
34
-ignore_extra_diags
45
-- highlights.go --
56
package highlightprintf
67
import (
7-
"fmt"
8-
)
8+
"fmt"
9+
)
10+
911
func BasicPrintfHighlights() {
1012
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)
13+
fmt.Printf("Hello %s, you have %d new messages!", "Alice", 5) //@hiloc(normald, "%d", write),hiloc(normalargs1, "5", read),highlightall(normald, normalargs1)
1214
}
15+
1316
func ComplexPrintfHighlights() {
1417
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)
18+
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)
1619
}
20+
1721
func MissingDirectives() {
1822
fmt.Printf("Hello %s, you have 5 new messages!", "Alice", 5) //@hiloc(missings, "%s", write),hiloc(missingargs0, "\"Alice\"", read),highlightall(missings, missingargs0)
1923
}
24+
2025
func TooManyDirectives() {
2126
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)
27+
fmt.Printf("Hello %s, you have %d new %s %q messages!", "Alice", 5) //@hiloc(toomanyd, "%d", write),hiloc(toomanyargs1, "5", read),highlightall(toomanyd, toomanyargs1)
2328
}
29+
2430
func VerbIsPercentage() {
2531
fmt.Printf("%4.2% %d", 6) //@hiloc(z1, "%d", write),hiloc(z2, "6", read),highlightall(z1, z2)
2632
}
33+
2734
func SpecialChars() {
2835
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)
36+
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)
3037
}
38+
3139
func Escaped() {
3240
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)
41+
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)
42+
fmt.Printf("%d \nss \x25[2]d", 234, 123) //@hiloc(zz1, "%d", write),hiloc(zz2, "234", read),highlightall(zz1,zz2)
43+
fmt.Printf("%d \nss \x25[2]d", 234, 123) //@hiloc(zz3, "\\x25[2]d", write),hiloc(zz4, "123", read),highlightall(zz3,zz4)
3444
}
3545

36-
func IndexedAsterisk() {
46+
func Indexed() {
3747
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)
48+
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)
49+
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)
50+
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)
4151
}
4252

43-
func MultipleSameIndexed() {
53+
func MultipleIndexed() {
4454
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)
4555
}

0 commit comments

Comments
 (0)