Skip to content

Commit a6c7d79

Browse files
committed
posmap
1 parent d894536 commit a6c7d79

File tree

3 files changed

+119
-40
lines changed

3 files changed

+119
-40
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

+95-26
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,17 @@ 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 + token.Pos(offsetInStringLiteral(lit.Value, format, rang.Start))
215+
rangeEnd = formatPos + token.Pos(offsetInStringLiteral(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
}
212-
213-
if (cursorPos >= rangeStart && cursorPos < rangeEnd) || (arg != nil && cursorPos >= arg.Pos() && cursorPos < arg.End()) {
222+
if rangeStart <= cursorPos && cursorPos < rangeEnd || arg != nil && arg.Pos() <= cursorPos && cursorPos < arg.End() {
214223
highlightRange(result, rangeStart, rangeEnd, protocol.Write)
215224
if arg != nil {
216225
succeededArg = argIndex
@@ -219,35 +228,34 @@ func highlightPrintf(call *ast.CallExpr, idx int, cursorPos token.Pos, format st
219228
}
220229
}
221230

222-
for _, operation := range operations {
231+
for _, op := range operations {
223232
// If width or prec has any *, we can not highlight the full range from % to verb,
224233
// because it will overlap with the sub-range of *, for example:
225234
//
226235
// fmt.Printf("%*[3]d", 4, 5, 6)
227236
// ^ ^ we can only highlight this range when cursor in 6. '*' as a one-rune range will
228237
// highlight for 4.
229-
anyAsterisk := false
238+
hasAsterisk := false
230239

231-
width, prec, verb := operation.Width, operation.Prec, operation.Verb
232240
// 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)
241+
if op.Width.Dynamic != -1 {
242+
hasAsterisk = true
243+
highlightPair(op.Width.Range, op.Width.Dynamic)
236244
}
237245

238246
// 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)
247+
if op.Prec.Dynamic != -1 {
248+
hasAsterisk = true
249+
highlightPair(op.Prec.Range, op.Prec.Dynamic)
242250
}
243251

244252
// Try highlight Verb.
245-
if verb.Verb != '%' {
253+
if op.Verb.Verb != '%' {
246254
// 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)
255+
if hasAsterisk {
256+
highlightPair(op.Verb.Range, op.Verb.ArgIndex)
249257
} else {
250-
highlightPair(token.Pos(operation.Range.Start), token.Pos(operation.Range.End), verb.ArgIndex)
258+
highlightPair(op.Range, op.Verb.ArgIndex)
251259
}
252260
}
253261
}
@@ -260,6 +268,67 @@ func highlightPrintf(call *ast.CallExpr, idx int, cursorPos token.Pos, format st
260268
}
261269
}
262270

271+
// offsetInStringLiteral maps an offset in the unquoted string to
272+
// relative to the literal string.
273+
func offsetInStringLiteral(literal string, unquoted string, logicalOffset int) int {
274+
literalIdx := 1 // Skip the initial quote char.
275+
logIdx := 0
276+
277+
// Advance by one unquoted rune and the corresponding literal string.
278+
advanceRune := func() {
279+
r, size := utf8.DecodeRuneInString(unquoted[logIdx:])
280+
if r == utf8.RuneError && size <= 1 {
281+
// Malformed UTF-8 or end of string,
282+
// move one byte in both strings to avoid infinite loops.
283+
logIdx++
284+
literalIdx++
285+
return
286+
}
287+
logIdx += size
288+
289+
if literalIdx >= len(literal)-1 {
290+
return
291+
}
292+
293+
if literal[literalIdx] == '\\' {
294+
remain := literal[literalIdx:]
295+
escLen := 0
296+
if len(remain) < 2 {
297+
escLen = 1 // just the '\'
298+
}
299+
switch remain[1] {
300+
case 'x':
301+
escLen = 4
302+
case 'u':
303+
escLen = 6
304+
case 'U':
305+
escLen = 10
306+
case 'a', 'b', 'f', 'n', 'r', 't', 'v', '\\', '"':
307+
escLen = 2
308+
case '0', '1', '2', '3', '4', '5', '6', '7':
309+
escLen = 4
310+
default:
311+
return
312+
}
313+
literalIdx += escLen
314+
} else {
315+
// non-escaped character
316+
literalIdx++
317+
}
318+
}
319+
320+
for logIdx < len(unquoted) && (logIdx < logicalOffset) && literalIdx < len(literal)-1 {
321+
advanceRune()
322+
}
323+
324+
// Clamp it to ensure we don't exceed array bounds.
325+
if literalIdx >= len(literal)-1 {
326+
literalIdx = len(literal) - 1
327+
}
328+
329+
return literalIdx
330+
}
331+
263332
type posRange struct {
264333
start, end token.Pos
265334
}
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)