@@ -10,11 +10,14 @@ import (
10
10
"go/ast"
11
11
"go/token"
12
12
"go/types"
13
+ "io"
14
+ "strings"
13
15
14
16
"golang.org/x/tools/go/ast/astutil"
15
17
"golang.org/x/tools/gopls/internal/cache"
16
18
"golang.org/x/tools/gopls/internal/file"
17
19
"golang.org/x/tools/gopls/internal/protocol"
20
+ gastutil "golang.org/x/tools/gopls/internal/util/astutil"
18
21
"golang.org/x/tools/internal/event"
19
22
)
20
23
@@ -49,7 +52,7 @@ func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, po
49
52
}
50
53
}
51
54
}
52
- result , err := highlightPath (path , pgf .File , pkg .TypesInfo ())
55
+ result , err := highlightPath (path , pgf .File , pkg .TypesInfo (), pos )
53
56
if err != nil {
54
57
return nil , err
55
58
}
@@ -69,8 +72,19 @@ func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, po
69
72
70
73
// highlightPath returns ranges to highlight for the given enclosing path,
71
74
// 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 ) {
75
+ func highlightPath (path []ast.Node , file * ast.File , info * types.Info , pos token. Pos ) (map [posRange ]protocol.DocumentHighlightKind , error ) {
73
76
result := make (map [posRange ]protocol.DocumentHighlightKind )
77
+ // Inside a printf-style call?
78
+ for _ , node := range path {
79
+ if call , ok := node .(* ast.CallExpr ); ok {
80
+ for _ , args := range call .Args {
81
+ // Only try when pos is in right side of the format String.
82
+ if basicList , ok := args .(* ast.BasicLit ); ok && basicList .Pos () < pos && basicList .Kind == token .STRING {
83
+ highlightPrintf (basicList , call , pos , result )
84
+ }
85
+ }
86
+ }
87
+ }
74
88
switch node := path [0 ].(type ) {
75
89
case * ast.BasicLit :
76
90
// Import path string literal?
@@ -131,6 +145,266 @@ func highlightPath(path []ast.Node, file *ast.File, info *types.Info) (map[posRa
131
145
return result , nil
132
146
}
133
147
148
+ // highlightPrintf identifies and highlights the relationships between placeholders
149
+ // in a format string and their corresponding variadic arguments in a printf-style
150
+ // function call.
151
+ //
152
+ // For example:
153
+ //
154
+ // fmt.Printf("Hello %s, you scored %d", name, score)
155
+ //
156
+ // If the cursor is on %s or name, highlightPrintf will highlight %s as a write operation,
157
+ // and name as a read operation.
158
+ func highlightPrintf (directive * ast.BasicLit , call * ast.CallExpr , pos token.Pos , result map [posRange ]protocol.DocumentHighlightKind ) {
159
+ format := directive .Value
160
+ // Give up when encounter '% %', '%%' for simplicity.
161
+ // For example:
162
+ //
163
+ // fmt.Printf("hello % %s, %-2.3d\n", "world", 123)
164
+ //
165
+ // The implementation of fmt.doPrintf will ignore first two '%'s,
166
+ // causing arguments count bigger than placeholders count (2 > 1), producing
167
+ // "%!(EXTRA" error string in formatFunc and incorrect highlight range.
168
+ //
169
+ // fmt.Printf("%% %s, %-2.3d\n", "world", 123)
170
+ //
171
+ // This case it will not emit errors, but the recording range of parsef is going to
172
+ // shift left because two % are interpreted as one %(escaped), so it becomes:
173
+ // fmt.Printf("%% %s, %-2.3d\n", "world", 123)
174
+ // | | the range will include a whitespace in left of %s
175
+ for i := range len (format ) {
176
+ if format [i ] == '%' {
177
+ for j := i + 1 ; j < len (format ); j ++ {
178
+ c := format [j ]
179
+ if (c >= 'a' && c <= 'z' ) || (c >= 'A' && c <= 'Z' ) {
180
+ break
181
+ }
182
+ if c == '%' {
183
+ return
184
+ }
185
+ }
186
+ }
187
+ }
188
+
189
+ // Computation is based on count of '%', when placeholders and variadic arguments missmatch,
190
+ // users are most likely completing arguments, so try to highlight any unfinished one.
191
+ // Make sure variadic arguments passed to parsef matches correct count of '%'.
192
+ expectedVariadicArgs := make ([]ast.Expr , strings .Count (format , "%" ))
193
+ firstVariadic := - 1
194
+ for i , arg := range call .Args {
195
+ if directive == arg {
196
+ firstVariadic = i + 1
197
+ argsLen := len (call .Args ) - i - 1
198
+ if argsLen > len (expectedVariadicArgs ) {
199
+ // Translate from Printf(a0,"%d %d",5, 6, 7) to [5, 6]
200
+ copy (expectedVariadicArgs , call .Args [firstVariadic :firstVariadic + len (expectedVariadicArgs )])
201
+ } else {
202
+ // Translate from Printf(a0,"%d %d %s",5, 6) to [5, 6, nil]
203
+ copy (expectedVariadicArgs [:argsLen ], call .Args [firstVariadic :])
204
+ }
205
+ break
206
+ }
207
+ }
208
+ var percent formatPercent
209
+ // Get a position-ordered slice describing each directive item.
210
+ parsedDirectives := parsef (format , directive .Pos (), expectedVariadicArgs ... )
211
+ // Cursor in argument.
212
+ if pos > directive .End () {
213
+ // Which variadic argument cursor sits inside.
214
+ for i := firstVariadic ; i < len (call .Args ); i ++ {
215
+ if gastutil .NodeContains (call .Args [i ], pos ) {
216
+ // Offset relative to parsedDirectives.
217
+ // (Printf(a0,"%d %d %s",5, 6), firstVariadic=2,i=3)
218
+ // ^ cursor here
219
+ // -> ([5, 6, nil], firstVariadic=1)
220
+ // ^
221
+ firstVariadic = i - firstVariadic
222
+ break
223
+ }
224
+ }
225
+ index := - 1
226
+ for _ , part := range parsedDirectives {
227
+ switch part := part .(type ) {
228
+ case formatPercent :
229
+ percent = part
230
+ index ++
231
+ case formatVerb :
232
+ if token .Pos (percent ).IsValid () {
233
+ if index == firstVariadic {
234
+ // Placeholders behave like writting values from arguments to themselves,
235
+ // so highlight them with Write semantic.
236
+ highlightRange (result , token .Pos (percent ), part .rang .end , protocol .Write )
237
+ highlightRange (result , part .operand .Pos (), part .operand .End (), protocol .Read )
238
+ return
239
+ }
240
+ percent = formatPercent (token .NoPos )
241
+ }
242
+ }
243
+ }
244
+ } else {
245
+ // Cursor in format string.
246
+ for _ , part := range parsedDirectives {
247
+ switch part := part .(type ) {
248
+ case formatPercent :
249
+ percent = part
250
+ case formatVerb :
251
+ if token .Pos (percent ).IsValid () {
252
+ if token .Pos (percent ) <= pos && pos <= part .rang .end {
253
+ highlightRange (result , token .Pos (percent ), part .rang .end , protocol .Write )
254
+ if part .operand != nil {
255
+ highlightRange (result , part .operand .Pos (), part .operand .End (), protocol .Read )
256
+ }
257
+ return
258
+ }
259
+ percent = formatPercent (token .NoPos )
260
+ }
261
+ }
262
+ }
263
+ }
264
+ }
265
+
266
+ // Below are formatting directives definitions.
267
+ type formatPercent token.Pos
268
+ type formatLiteral struct {
269
+ literal string
270
+ rang posRange
271
+ }
272
+ type formatFlags struct {
273
+ flag string
274
+ rang posRange
275
+ }
276
+ type formatWidth struct {
277
+ width int
278
+ rang posRange
279
+ }
280
+ type formatPrec struct {
281
+ prec int
282
+ rang posRange
283
+ }
284
+ type formatVerb struct {
285
+ verb rune
286
+ rang posRange
287
+ operand ast.Expr // verb's corresponding operand, may be nil
288
+ }
289
+
290
+ type formatFunc func (fmt.State , rune )
291
+
292
+ var _ fmt.Formatter = formatFunc (nil )
293
+
294
+ func (f formatFunc ) Format (st fmt.State , verb rune ) { f (st , verb ) }
295
+
296
+ // parsef parses a printf-style format string into its constituent components together with
297
+ // their position in the source code, including [formatLiteral], formatting directives
298
+ // [formatFlags], [formatPrecision], [formatWidth], [formatPrecision], [formatVerb], and its operand.
299
+ func parsef (format string , pos token.Pos , args ... ast.Expr ) []any {
300
+ const sep = "!!!GOPLS_SEP!!!"
301
+ // A Conversion represents a single % operation and its operand.
302
+ type conversion struct {
303
+ verb rune
304
+ width int // or -1
305
+ prec int // or -1
306
+ flag string // some of "-+# 0"
307
+ arg ast.Expr
308
+ }
309
+ var convs []conversion
310
+ wrappers := make ([]any , len (args ))
311
+ for i , arg := range args {
312
+ wrappers [i ] = formatFunc (func (st fmt.State , verb rune ) {
313
+ io .WriteString (st , sep )
314
+ width , ok := st .Width ()
315
+ if ! ok {
316
+ width = - 1
317
+ }
318
+ prec , ok := st .Precision ()
319
+ if ! ok {
320
+ prec = - 1
321
+ }
322
+ flag := ""
323
+ for _ , b := range "-+# 0" {
324
+ if st .Flag (int (b )) {
325
+ flag += string (b )
326
+ }
327
+ }
328
+ convs = append (convs , conversion {
329
+ verb : verb ,
330
+ width : width ,
331
+ prec : prec ,
332
+ flag : flag ,
333
+ arg : arg ,
334
+ })
335
+ })
336
+ }
337
+
338
+ // Interleave the literals and the conversions.
339
+ var directives []any
340
+ for i , word := range strings .Split (fmt .Sprintf (format , wrappers ... ), sep ) {
341
+ if word != "" {
342
+ directives = append (directives , formatLiteral {
343
+ literal : word ,
344
+ rang : posRange {
345
+ start : pos ,
346
+ end : pos + token .Pos (len (word )),
347
+ },
348
+ })
349
+ pos = pos + token .Pos (len (word ))
350
+ }
351
+ if i < len (convs ) {
352
+ conv := convs [i ]
353
+ // Collect %.
354
+ directives = append (directives , formatPercent (pos ))
355
+ pos += 1
356
+ // Collect flags.
357
+ if flag := conv .flag ; flag != "" {
358
+ length := token .Pos (len (conv .flag ))
359
+ directives = append (directives , formatFlags {
360
+ flag : flag ,
361
+ rang : posRange {
362
+ start : pos ,
363
+ end : pos + length ,
364
+ },
365
+ })
366
+ pos += length
367
+ }
368
+ // Collect width.
369
+ if width := conv .width ; conv .width != - 1 {
370
+ length := token .Pos (len (fmt .Sprintf ("%d" , conv .width )))
371
+ directives = append (directives , formatWidth {
372
+ width : width ,
373
+ rang : posRange {
374
+ start : pos ,
375
+ end : pos + length ,
376
+ },
377
+ })
378
+ pos += length
379
+ }
380
+ // Collect precision, which starts with a dot.
381
+ if prec := conv .prec ; conv .prec != - 1 {
382
+ length := token .Pos (len (fmt .Sprintf ("%d" , conv .prec ))) + 1
383
+ directives = append (directives , formatPrec {
384
+ prec : prec ,
385
+ rang : posRange {
386
+ start : pos ,
387
+ end : pos + length ,
388
+ },
389
+ })
390
+ pos += length
391
+ }
392
+ // Collect verb, which must be present.
393
+ length := token .Pos (len (string (conv .verb )))
394
+ directives = append (directives , formatVerb {
395
+ verb : conv .verb ,
396
+ rang : posRange {
397
+ start : pos ,
398
+ end : pos + length ,
399
+ },
400
+ operand : conv .arg ,
401
+ })
402
+ pos += length
403
+ }
404
+ }
405
+ return directives
406
+ }
407
+
134
408
type posRange struct {
135
409
start , end token.Pos
136
410
}
0 commit comments