@@ -81,12 +81,9 @@ func highlightPath(info *types.Info, path []ast.Node, pos token.Pos) (map[posRan
81
81
// Treat each corresponding ("%v", arg) pair as a highlight class.
82
82
for _ , node := range path {
83
83
if call , ok := node .(* ast.CallExpr ); ok {
84
- idx := formatStringIndex (info , call )
85
- if idx >= 0 && idx < len (call .Args ) {
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 )
89
- }
84
+ idx , lit := formatLitAndIndex (info , call )
85
+ if idx != - 1 {
86
+ highlightPrintf (call , idx , pos , lit , result )
90
87
}
91
88
}
92
89
}
@@ -152,29 +149,34 @@ func highlightPath(info *types.Info, path []ast.Node, pos token.Pos) (map[posRan
152
149
return result , nil
153
150
}
154
151
155
- // formatStringIndex returns the index of the format string (the last
152
+ // formatLitAndIndex returns the BasicLit and index of the BasicLit (the last
156
153
// non-variadic parameter) within the given printf-like call
157
- // expression, or -1 if unknown.
158
- func formatStringIndex (info * types.Info , call * ast.CallExpr ) int {
154
+ // expression, returns -1 as index if unknown.
155
+ func formatLitAndIndex (info * types.Info , call * ast.CallExpr ) ( int , * ast. BasicLit ) {
159
156
typ := info .Types [call .Fun ].Type
160
157
if typ == nil {
161
- return - 1 // missing type
158
+ return - 1 , nil // missing type
162
159
}
163
160
sig , ok := typ .(* types.Signature )
164
161
if ! ok {
165
- return - 1 // ill-typed
162
+ return - 1 , nil // ill-typed
166
163
}
167
164
if ! sig .Variadic () {
168
165
// Skip checking non-variadic functions.
169
- return - 1
166
+ return - 1 , nil
170
167
}
171
168
idx := sig .Params ().Len () - 2
172
169
if idx < 0 {
173
170
// Skip checking variadic functions without
174
171
// fixed arguments.
175
- return - 1
172
+ return - 1 , nil
173
+ }
174
+
175
+ // We only care about literal format strings, so fmt.Sprint("a"+"b%s", "bar") won't be highlighted.
176
+ if lit , ok := call .Args [idx ].(* ast.BasicLit ); ok && lit .Kind == token .STRING {
177
+ return idx , lit
176
178
}
177
- return idx
179
+ return - 1 , nil
178
180
}
179
181
180
182
// highlightPrintf highlights operations in a format string and their corresponding
@@ -207,19 +209,27 @@ func highlightPrintf(call *ast.CallExpr, idx int, cursorPos token.Pos, lit *ast.
207
209
succeededArg := 0
208
210
visited := make (map [posRange ]int , 0 )
209
211
210
- formatPos := call .Args [idx ].Pos ()
211
212
// highlightPair highlights the operation and its potential argument pair if the cursor is within either range.
212
213
highlightPair := func (rang fmtstr.Range , argIndex int ) {
213
- var (
214
- rangeStart = formatPos + token .Pos (offsetInStringLiteral (lit .Value , format , rang .Start ))
215
- rangeEnd = formatPos + token .Pos (offsetInStringLiteral (lit .Value , format , rang .End - 1 )+ 1 )
216
- arg ast.Expr // may not exist
217
- )
214
+ rangeStart , err := posInStringLiteral (lit , rang .Start )
215
+ if err != nil {
216
+ return
217
+ }
218
+ rangeEnd , err := posInStringLiteral (lit , rang .End )
219
+ if err != nil {
220
+ return
221
+ }
218
222
visited [posRange {rangeStart , rangeEnd }] = argIndex
223
+
224
+ var arg ast.Expr
219
225
if argIndex < len (call .Args ) {
220
226
arg = call .Args [argIndex ]
221
227
}
222
- if rangeStart <= cursorPos && cursorPos < rangeEnd || arg != nil && arg .Pos () <= cursorPos && cursorPos < arg .End () {
228
+
229
+ // cursorPos can't equal to end position, otherwise the two
230
+ // neighborhood such as (%[2]*d) are both highlighted if cursor in "*" (ending of [2]*).
231
+ if rangeStart <= cursorPos && cursorPos < rangeEnd ||
232
+ arg != nil && arg .Pos () <= cursorPos && cursorPos < arg .End () {
223
233
highlightRange (result , rangeStart , rangeEnd , protocol .Write )
224
234
if arg != nil {
225
235
succeededArg = argIndex
@@ -268,65 +278,36 @@ func highlightPrintf(call *ast.CallExpr, idx int, cursorPos token.Pos, lit *ast.
268
278
}
269
279
}
270
280
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
- }
281
+ // posInStringLiteral returns the position within a string literal
282
+ // corresponding to the specified byte offset within the logical
283
+ // string that it denotes.
284
+ func posInStringLiteral (lit * ast.BasicLit , offset int ) (token.Pos , error ) {
285
+ raw := lit .Value
292
286
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
- }
287
+ value , err := strconv .Unquote (raw )
288
+ if err != nil {
289
+ return 0 , err
318
290
}
319
-
320
- for logIdx < len (unquoted ) && (logIdx < logicalOffset ) && literalIdx < len (literal )- 1 {
321
- advanceRune ()
291
+ if ! (0 <= offset && offset <= len (value )) {
292
+ return 0 , fmt .Errorf ("invalid offset" )
322
293
}
323
294
324
- // Clamp it to ensure we don't exceed array bounds.
325
- if literalIdx >= len (literal )- 1 {
326
- literalIdx = len (literal ) - 1
327
- }
295
+ // remove quotes
296
+ quote := raw [0 ] // '"' or '`'
297
+ raw = raw [1 : len (raw )- 1 ]
328
298
329
- return literalIdx
299
+ var (
300
+ i = 0 // byte index within logical value
301
+ pos = lit .ValuePos + 1 // position within literal
302
+ )
303
+ for raw != "" && i < offset {
304
+ r , _ , rest , _ := strconv .UnquoteChar (raw , quote ) // can't fail
305
+ sz := len (raw ) - len (rest ) // length of literal char in raw bytes
306
+ pos += token .Pos (sz )
307
+ raw = raw [sz :]
308
+ i += utf8 .RuneLen (r )
309
+ }
310
+ return pos , nil
330
311
}
331
312
332
313
type posRange struct {
0 commit comments