@@ -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 && gastutil .NodeContains (call , pos ) {
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,270 @@ 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
+ if firstVariadic == - 1 {
209
+ // No argument at right of directive.
210
+ return
211
+ }
212
+ var percent formatPercent
213
+ // Get a position-ordered slice describing each directive item.
214
+ parsedDirectives := parsef (format , directive .Pos (), expectedVariadicArgs ... )
215
+ // Cursor in argument.
216
+ if pos > directive .End () {
217
+ // Which variadic argument cursor sits inside.
218
+ for i := firstVariadic ; i < len (call .Args ); i ++ {
219
+ if gastutil .NodeContains (call .Args [i ], pos ) {
220
+ // Offset relative to parsedDirectives.
221
+ // (Printf(a0,"%d %d %s",5, 6), firstVariadic=2,i=3)
222
+ // ^ cursor here
223
+ // -> ([5, 6, nil], firstVariadic=1)
224
+ // ^
225
+ firstVariadic = i - firstVariadic
226
+ break
227
+ }
228
+ }
229
+ index := - 1
230
+ for _ , part := range parsedDirectives {
231
+ switch part := part .(type ) {
232
+ case formatPercent :
233
+ percent = part
234
+ index ++
235
+ case formatVerb :
236
+ if token .Pos (percent ).IsValid () {
237
+ if index == firstVariadic {
238
+ // Placeholders behave like writting values from arguments to themselves,
239
+ // so highlight them with Write semantic.
240
+ highlightRange (result , token .Pos (percent ), part .rang .end , protocol .Write )
241
+ highlightRange (result , part .arg .Pos (), part .arg .End (), protocol .Read )
242
+ return
243
+ }
244
+ percent = formatPercent (token .NoPos )
245
+ }
246
+ }
247
+ }
248
+ } else {
249
+ // Cursor in format string.
250
+ for _ , part := range parsedDirectives {
251
+ switch part := part .(type ) {
252
+ case formatPercent :
253
+ percent = part
254
+ case formatVerb :
255
+ if token .Pos (percent ).IsValid () {
256
+ if token .Pos (percent ) <= pos && pos <= part .rang .end {
257
+ highlightRange (result , token .Pos (percent ), part .rang .end , protocol .Write )
258
+ if part .arg != nil {
259
+ highlightRange (result , part .arg .Pos (), part .arg .End (), protocol .Read )
260
+ }
261
+ return
262
+ }
263
+ percent = formatPercent (token .NoPos )
264
+ }
265
+ }
266
+ }
267
+ }
268
+ }
269
+
270
+ // Below are formatting directives definitions.
271
+ type formatPercent token.Pos
272
+ type formatLiteral struct {
273
+ literal string
274
+ rang posRange
275
+ }
276
+ type formatFlags struct {
277
+ flag string
278
+ rang posRange
279
+ }
280
+ type formatWidth struct {
281
+ width int
282
+ rang posRange
283
+ }
284
+ type formatPrec struct {
285
+ prec int
286
+ rang posRange
287
+ }
288
+ type formatVerb struct {
289
+ verb rune
290
+ rang posRange
291
+ arg ast.Expr // may be nil
292
+ }
293
+
294
+ type formatFunc func (fmt.State , rune )
295
+
296
+ var _ fmt.Formatter = formatFunc (nil )
297
+
298
+ func (f formatFunc ) Format (st fmt.State , verb rune ) { f (st , verb ) }
299
+
300
+ // parsef parses a printf-style format string into its constituent components together with
301
+ // their position in the source code, including literals, formatting directives
302
+ // (flags, width, precision, verb), and its operand.
303
+ func parsef (format string , pos token.Pos , args ... ast.Expr ) []any {
304
+ const sep = "!!!GOPLS_SEP!!!"
305
+ // A Conversion represents a single % operation and its operand.
306
+ type conversion struct {
307
+ verb rune
308
+ width int // or -1
309
+ prec int // or -1
310
+ flag string // some of "-+# 0"
311
+ arg ast.Expr
312
+ }
313
+ var convs []conversion
314
+ wrappers := make ([]any , len (args ))
315
+ for i , arg := range args {
316
+ wrappers [i ] = formatFunc (func (st fmt.State , verb rune ) {
317
+ io .WriteString (st , sep )
318
+ width , ok := st .Width ()
319
+ if ! ok {
320
+ width = - 1
321
+ }
322
+ prec , ok := st .Precision ()
323
+ if ! ok {
324
+ prec = - 1
325
+ }
326
+ flag := ""
327
+ for _ , b := range "-+# 0" {
328
+ if st .Flag (int (b )) {
329
+ flag += string (b )
330
+ }
331
+ }
332
+ convs = append (convs , conversion {
333
+ verb : verb ,
334
+ width : width ,
335
+ prec : prec ,
336
+ flag : flag ,
337
+ arg : arg ,
338
+ })
339
+ })
340
+ }
341
+
342
+ // Interleave the literals and the conversions.
343
+ var directives []any
344
+ for i , word := range strings .Split (fmt .Sprintf (format , wrappers ... ), sep ) {
345
+ if word != "" {
346
+ directives = append (directives , formatLiteral {
347
+ literal : word ,
348
+ rang : posRange {
349
+ start : pos ,
350
+ end : pos + token .Pos (len (word )),
351
+ },
352
+ })
353
+ pos = pos + token .Pos (len (word ))
354
+ }
355
+ if i < len (convs ) {
356
+ conv := convs [i ]
357
+ // Collect %.
358
+ directives = append (directives , formatPercent (pos ))
359
+ pos += 1
360
+ // Collect flags.
361
+ if flag := conv .flag ; flag != "" {
362
+ length := token .Pos (len (conv .flag ))
363
+ directives = append (directives , formatFlags {
364
+ flag : flag ,
365
+ rang : posRange {
366
+ start : pos ,
367
+ end : pos + length ,
368
+ },
369
+ })
370
+ pos += length
371
+ }
372
+ // Collect width.
373
+ if width := conv .width ; conv .width != - 1 {
374
+ length := token .Pos (len (fmt .Sprintf ("%d" , conv .width )))
375
+ directives = append (directives , formatWidth {
376
+ width : width ,
377
+ rang : posRange {
378
+ start : pos ,
379
+ end : pos + length ,
380
+ },
381
+ })
382
+ pos += length
383
+ }
384
+ // Collect precision, which starts with a dot.
385
+ if prec := conv .prec ; conv .prec != - 1 {
386
+ length := token .Pos (len (fmt .Sprintf ("%d" , conv .prec ))) + 1
387
+ directives = append (directives , formatPrec {
388
+ prec : prec ,
389
+ rang : posRange {
390
+ start : pos ,
391
+ end : pos + length ,
392
+ },
393
+ })
394
+ pos += length
395
+ }
396
+ // Collect verb, which must be present.
397
+ length := token .Pos (len (string (conv .verb )))
398
+ directives = append (directives , formatVerb {
399
+ verb : conv .verb ,
400
+ rang : posRange {
401
+ start : pos ,
402
+ end : pos + length ,
403
+ },
404
+ arg : conv .arg ,
405
+ })
406
+ pos += length
407
+ }
408
+ }
409
+ return directives
410
+ }
411
+
134
412
type posRange struct {
135
413
start , end token.Pos
136
414
}
0 commit comments