@@ -19,6 +19,7 @@ import (
19
19
"golang.org/x/tools/gopls/internal/cache"
20
20
"golang.org/x/tools/gopls/internal/cache/metadata"
21
21
"golang.org/x/tools/gopls/internal/cache/parsego"
22
+ "golang.org/x/tools/gopls/internal/cache/testfuncs"
22
23
"golang.org/x/tools/gopls/internal/file"
23
24
"golang.org/x/tools/gopls/internal/protocol"
24
25
"golang.org/x/tools/gopls/internal/protocol/command"
@@ -213,6 +214,29 @@ func regenerateCgoLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Ha
213
214
}
214
215
215
216
func goToTestCodeLens (ctx context.Context , snapshot * cache.Snapshot , fh file.Handle ) ([]protocol.CodeLens , error ) {
217
+ matches , err := matchFunctionsWithTests (ctx , snapshot , fh )
218
+ if err != nil {
219
+ return nil , err
220
+ }
221
+
222
+ lenses := make ([]protocol.CodeLens , 0 , len (matches ))
223
+ for _ , t := range matches {
224
+ lenses = append (lenses , protocol.CodeLens {
225
+ Range : protocol.Range {Start : t .FuncPos , End : t .FuncPos },
226
+ Command : command .NewGoToTestCommand ("Go to " + t .Name , t .Loc ),
227
+ })
228
+ }
229
+ return lenses , nil
230
+ }
231
+
232
+ type TestMatch struct {
233
+ FuncPos protocol.Position // function position
234
+ Name string // test name
235
+ Loc protocol.Location // test location
236
+ Type testfuncs.TestType // test type
237
+ }
238
+
239
+ func matchFunctionsWithTests (ctx context.Context , snapshot * cache.Snapshot , fh file.Handle ) (matches []TestMatch , err error ) {
216
240
if strings .HasSuffix (fh .URI ().Path (), "_test.go" ) {
217
241
// Ignore test files.
218
242
return nil , nil
@@ -238,7 +262,12 @@ func goToTestCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Han
238
262
if err != nil {
239
263
return nil , fmt .Errorf ("couldn't parse file: %w" , err )
240
264
}
241
- funcPos := make (map [string ]protocol.Position )
265
+
266
+ type Func struct {
267
+ Name string
268
+ Pos protocol.Position
269
+ }
270
+ var fileFuncs []Func
242
271
for _ , d := range pgf .File .Decls {
243
272
fn , ok := d .(* ast.FuncDecl )
244
273
if ! ok {
@@ -254,32 +283,11 @@ func goToTestCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Han
254
283
_ , rname , _ := astutil .UnpackRecv (fn .Recv .List [0 ].Type )
255
284
name = rname .Name + "_" + fn .Name .Name
256
285
}
257
- funcPos [name ] = rng .Start
258
- }
259
-
260
- type TestType int
261
-
262
- // Types are sorted by priority from high to low.
263
- const (
264
- T TestType = iota + 1
265
- E
266
- B
267
- F
268
- )
269
- testTypes := map [string ]TestType {
270
- "Test" : T ,
271
- "Example" : E ,
272
- "Benchmark" : B ,
273
- "Fuzz" : F ,
274
- }
275
-
276
- type Test struct {
277
- FuncPos protocol.Position
278
- Name string
279
- Loc protocol.Location
280
- Type TestType
286
+ fileFuncs = append (fileFuncs , Func {
287
+ Name : name ,
288
+ Pos : rng .Start ,
289
+ })
281
290
}
282
- var matchedTests []Test
283
291
284
292
pkgIDs := make ([]PackageID , 0 , len (testPackages ))
285
293
for _ , pkg := range testPackages {
@@ -291,48 +299,54 @@ func goToTestCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Han
291
299
}
292
300
for _ , tests := range allTests {
293
301
for _ , test := range tests .All () {
294
- var (
295
- name string
296
- testType TestType
297
- )
298
- for prefix , t := range testTypes {
299
- if strings .HasPrefix (test .Name , prefix ) {
300
- testType = t
301
- name = test .Name [len (prefix ):]
302
- break
303
- }
302
+ if test .Subtest {
303
+ continue
304
304
}
305
- if testType == 0 {
306
- continue // unknown type
305
+ potentialFuncNames := getPotentialFuncNames (test )
306
+ if len (potentialFuncNames ) == 0 {
307
+ continue
307
308
}
308
- name = strings .TrimPrefix (name , "_" )
309
-
310
- // Try to find 'Foo' for 'TestFoo' and 'foo' for 'Test_foo'.
311
- pos , ok := funcPos [name ]
312
- if ! ok && token .IsExported (name ) {
313
- // Try to find 'foo' for 'TestFoo'.
314
- runes := []rune (name )
315
- runes [0 ] = unicode .ToLower (runes [0 ])
316
- pos , ok = funcPos [string (runes )]
309
+
310
+ var matchedFunc Func
311
+ for _ , fn := range fileFuncs {
312
+ var matched bool
313
+ for _ , n := range potentialFuncNames {
314
+ // Check the prefix to be able to match 'TestDeletePanics' with 'Delete'.
315
+ if strings .HasPrefix (n , fn .Name ) {
316
+ matched = true
317
+ break
318
+ }
319
+ }
320
+ if ! matched {
321
+ continue
322
+ }
323
+
324
+ // Use the most specific function:
325
+ //
326
+ // - match 'TestDelete', 'TestDeletePanics' with 'Delete'
327
+ // - match 'TestDeleteFunc', 'TestDeleteFuncClearTail' with 'DeleteFunc', not 'Delete'
328
+ if len (matchedFunc .Name ) < len (fn .Name ) {
329
+ matchedFunc = fn
330
+ }
317
331
}
318
- if ok {
332
+ if matchedFunc . Name != "" {
319
333
loc := test .Location
320
334
loc .Range .End = loc .Range .Start // move cursor to the test's beginning
321
335
322
- matchedTests = append (matchedTests , Test {
323
- FuncPos : pos ,
336
+ matches = append (matches , TestMatch {
337
+ FuncPos : matchedFunc . Pos ,
324
338
Name : test .Name ,
325
339
Loc : loc ,
326
- Type : testType ,
340
+ Type : test . Type ,
327
341
})
328
342
}
329
343
}
330
344
}
331
- if len (matchedTests ) == 0 {
345
+ if len (matches ) == 0 {
332
346
return nil , nil
333
347
}
334
348
335
- slices .SortFunc (matchedTests , func (a , b Test ) int {
349
+ slices .SortFunc (matches , func (a , b TestMatch ) int {
336
350
if v := protocol .ComparePosition (a .FuncPos , b .FuncPos ); v != 0 {
337
351
return v
338
352
}
@@ -341,13 +355,31 @@ func goToTestCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Han
341
355
}
342
356
return cmp .Compare (a .Name , b .Name )
343
357
})
358
+ return matches , nil
359
+ }
344
360
345
- lenses := make ([]protocol.CodeLens , 0 , len (matchedTests ))
346
- for _ , t := range matchedTests {
347
- lenses = append (lenses , protocol.CodeLens {
348
- Range : protocol.Range {Start : t .FuncPos , End : t .FuncPos },
349
- Command : command .NewGoToTestCommand ("Go to " + t .Name , t .Loc ),
350
- })
361
+ func getPotentialFuncNames (test testfuncs.Result ) []string {
362
+ var name string
363
+ switch test .Type {
364
+ case testfuncs .TypeTest :
365
+ name = strings .TrimPrefix (test .Name , "Test" )
366
+ case testfuncs .TypeBenchmark :
367
+ name = strings .TrimPrefix (test .Name , "Benchmark" )
368
+ case testfuncs .TypeFuzz :
369
+ name = strings .TrimPrefix (test .Name , "Fuzz" )
370
+ case testfuncs .TypeExample :
371
+ name = strings .TrimPrefix (test .Name , "Example" )
372
+ }
373
+ if name == "" {
374
+ return nil
375
+ }
376
+ name = strings .TrimPrefix (name , "_" )
377
+
378
+ lowerCasedName := []rune (name )
379
+ lowerCasedName [0 ] = unicode .ToLower (lowerCasedName [0 ])
380
+
381
+ return []string {
382
+ name , // 'Foo' for 'TestFoo', 'foo' for 'Test_foo'
383
+ string (lowerCasedName ), // 'foo' for 'TestFoo'
351
384
}
352
- return lenses , nil
353
385
}
0 commit comments