@@ -10,12 +10,16 @@ import (
10
10
"go/ast"
11
11
"go/token"
12
12
"go/types"
13
+ "strconv"
14
+ "strings"
15
+ "unicode/utf8"
13
16
14
17
"golang.org/x/tools/go/ast/astutil"
15
18
"golang.org/x/tools/gopls/internal/cache"
16
19
"golang.org/x/tools/gopls/internal/file"
17
20
"golang.org/x/tools/gopls/internal/protocol"
18
21
"golang.org/x/tools/internal/event"
22
+ "golang.org/x/tools/internal/fmtstr"
19
23
)
20
24
21
25
func Highlight (ctx context.Context , snapshot * cache.Snapshot , fh file.Handle , position protocol.Position ) ([]protocol.DocumentHighlight , error ) {
@@ -49,7 +53,7 @@ func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, po
49
53
}
50
54
}
51
55
}
52
- result , err := highlightPath (path , pgf . File , pkg .TypesInfo ())
56
+ result , err := highlightPath (pkg .TypesInfo (), path , pos )
53
57
if err != nil {
54
58
return nil , err
55
59
}
@@ -69,8 +73,22 @@ func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, po
69
73
70
74
// highlightPath returns ranges to highlight for the given enclosing path,
71
75
// 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 ) {
76
+ func highlightPath (info * types. Info , path []ast.Node , pos token. Pos ) (map [posRange ]protocol.DocumentHighlightKind , error ) {
73
77
result := make (map [posRange ]protocol.DocumentHighlightKind )
78
+
79
+ // Inside a call to a printf-like function (as identified
80
+ // by a simple heuristic).
81
+ // Treat each corresponding ("%v", arg) pair as a highlight class.
82
+ for _ , node := range path {
83
+ if call , ok := node .(* ast.CallExpr ); ok {
84
+ lit , idx := formatStringAndIndex (info , call )
85
+ if idx != - 1 {
86
+ highlightPrintf (call , idx , pos , lit , result )
87
+ }
88
+ }
89
+ }
90
+
91
+ file := path [len (path )- 1 ].(* ast.File )
74
92
switch node := path [0 ].(type ) {
75
93
case * ast.BasicLit :
76
94
// Import path string literal?
@@ -131,6 +149,166 @@ func highlightPath(path []ast.Node, file *ast.File, info *types.Info) (map[posRa
131
149
return result , nil
132
150
}
133
151
152
+ // formatStringAndIndex returns the BasicLit and index of the BasicLit (the last
153
+ // non-variadic parameter) within the given printf-like call
154
+ // expression, returns -1 as index if unknown.
155
+ func formatStringAndIndex (info * types.Info , call * ast.CallExpr ) (* ast.BasicLit , int ) {
156
+ typ := info .Types [call .Fun ].Type
157
+ if typ == nil {
158
+ return nil , - 1 // missing type
159
+ }
160
+ sig , ok := typ .(* types.Signature )
161
+ if ! ok {
162
+ return nil , - 1 // ill-typed
163
+ }
164
+ if ! sig .Variadic () {
165
+ // Skip checking non-variadic functions.
166
+ return nil , - 1
167
+ }
168
+ idx := sig .Params ().Len () - 2
169
+ if idx < 0 {
170
+ // Skip checking variadic functions without
171
+ // fixed arguments.
172
+ return nil , - 1
173
+ }
174
+ // We only care about literal format strings, so fmt.Sprint("a"+"b%s", "bar") won't be highlighted.
175
+ if lit , ok := call .Args [idx ].(* ast.BasicLit ); ok && lit .Kind == token .STRING {
176
+ return lit , idx
177
+ }
178
+ return nil , - 1
179
+ }
180
+
181
+ // highlightPrintf highlights operations in a format string and their corresponding
182
+ // variadic arguments in a (possible) printf-style function call.
183
+ // For example:
184
+ //
185
+ // fmt.Printf("Hello %s, you scored %d", name, score)
186
+ //
187
+ // If the cursor is on %s or name, it will highlight %s as a write operation,
188
+ // and name as a read operation.
189
+ func highlightPrintf (call * ast.CallExpr , idx int , cursorPos token.Pos , lit * ast.BasicLit , result map [posRange ]protocol.DocumentHighlightKind ) {
190
+ format , err := strconv .Unquote (lit .Value )
191
+ if err != nil {
192
+ return
193
+ }
194
+ if ! strings .Contains (format , "%" ) {
195
+ return
196
+ }
197
+ operations , err := fmtstr .Parse (format , idx )
198
+ if err != nil {
199
+ return
200
+ }
201
+
202
+ // fmt.Printf("%[1]d %[1].2d", 3)
203
+ //
204
+ // When cursor is in `%[1]d`, we record `3` being successfully highlighted.
205
+ // And because we will also record `%[1].2d`'s corresponding arguments index is `3`
206
+ // in `visited`, even though it will not highlight any item in the first pass,
207
+ // in the second pass we can correctly highlight it. So the three are the same class.
208
+ succeededArg := 0
209
+ visited := make (map [posRange ]int , 0 )
210
+
211
+ // highlightPair highlights the operation and its potential argument pair if the cursor is within either range.
212
+ highlightPair := func (rang fmtstr.Range , argIndex int ) {
213
+ rangeStart , err := posInStringLiteral (lit , rang .Start )
214
+ if err != nil {
215
+ return
216
+ }
217
+ rangeEnd , err := posInStringLiteral (lit , rang .End )
218
+ if err != nil {
219
+ return
220
+ }
221
+ visited [posRange {rangeStart , rangeEnd }] = argIndex
222
+
223
+ var arg ast.Expr
224
+ if argIndex < len (call .Args ) {
225
+ arg = call .Args [argIndex ]
226
+ }
227
+
228
+ // cursorPos can't equal to end position, otherwise the two
229
+ // neighborhood such as (%[2]*d) are both highlighted if cursor in "*" (ending of [2]*).
230
+ if rangeStart <= cursorPos && cursorPos < rangeEnd ||
231
+ arg != nil && arg .Pos () <= cursorPos && cursorPos < arg .End () {
232
+ highlightRange (result , rangeStart , rangeEnd , protocol .Write )
233
+ if arg != nil {
234
+ succeededArg = argIndex
235
+ highlightRange (result , arg .Pos (), arg .End (), protocol .Read )
236
+ }
237
+ }
238
+ }
239
+
240
+ for _ , op := range operations {
241
+ // If width or prec has any *, we can not highlight the full range from % to verb,
242
+ // because it will overlap with the sub-range of *, for example:
243
+ //
244
+ // fmt.Printf("%*[3]d", 4, 5, 6)
245
+ // ^ ^ we can only highlight this range when cursor in 6. '*' as a one-rune range will
246
+ // highlight for 4.
247
+ hasAsterisk := false
248
+
249
+ // Try highlight Width if there is a *.
250
+ if op .Width .Dynamic != - 1 {
251
+ hasAsterisk = true
252
+ highlightPair (op .Width .Range , op .Width .Dynamic )
253
+ }
254
+
255
+ // Try highlight Precision if there is a *.
256
+ if op .Prec .Dynamic != - 1 {
257
+ hasAsterisk = true
258
+ highlightPair (op .Prec .Range , op .Prec .Dynamic )
259
+ }
260
+
261
+ // Try highlight Verb.
262
+ if op .Verb .Verb != '%' {
263
+ // If any * is found inside operation, narrow the highlight range.
264
+ if hasAsterisk {
265
+ highlightPair (op .Verb .Range , op .Verb .ArgIndex )
266
+ } else {
267
+ highlightPair (op .Range , op .Verb .ArgIndex )
268
+ }
269
+ }
270
+ }
271
+
272
+ // Second pass, try to highlight those missed operations.
273
+ for rang , argIndex := range visited {
274
+ if succeededArg == argIndex {
275
+ highlightRange (result , rang .start , rang .end , protocol .Write )
276
+ }
277
+ }
278
+ }
279
+
280
+ // posInStringLiteral returns the position within a string literal
281
+ // corresponding to the specified byte offset within the logical
282
+ // string that it denotes.
283
+ func posInStringLiteral (lit * ast.BasicLit , offset int ) (token.Pos , error ) {
284
+ raw := lit .Value
285
+
286
+ value , err := strconv .Unquote (raw )
287
+ if err != nil {
288
+ return 0 , err
289
+ }
290
+ if ! (0 <= offset && offset <= len (value )) {
291
+ return 0 , fmt .Errorf ("invalid offset" )
292
+ }
293
+
294
+ // remove quotes
295
+ quote := raw [0 ] // '"' or '`'
296
+ raw = raw [1 : len (raw )- 1 ]
297
+
298
+ var (
299
+ i = 0 // byte index within logical value
300
+ pos = lit .ValuePos + 1 // position within literal
301
+ )
302
+ for raw != "" && i < offset {
303
+ r , _ , rest , _ := strconv .UnquoteChar (raw , quote ) // can't fail
304
+ sz := len (raw ) - len (rest ) // length of literal char in raw bytes
305
+ pos += token .Pos (sz )
306
+ raw = raw [sz :]
307
+ i += utf8 .RuneLen (r )
308
+ }
309
+ return pos , nil
310
+ }
311
+
134
312
type posRange struct {
135
313
start , end token.Pos
136
314
}
0 commit comments