@@ -19,6 +19,7 @@ import (
1919 "golang.org/x/tools/gopls/internal/cache"
2020 "golang.org/x/tools/gopls/internal/cache/metadata"
2121 "golang.org/x/tools/gopls/internal/cache/parsego"
22+ "golang.org/x/tools/gopls/internal/cache/testfuncs"
2223 "golang.org/x/tools/gopls/internal/file"
2324 "golang.org/x/tools/gopls/internal/protocol"
2425 "golang.org/x/tools/gopls/internal/protocol/command"
@@ -213,6 +214,29 @@ func regenerateCgoLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Ha
213214}
214215
215216func 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 ) {
216240 if strings .HasSuffix (fh .URI ().Path (), "_test.go" ) {
217241 // Ignore test files.
218242 return nil , nil
@@ -238,7 +262,12 @@ func goToTestCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Han
238262 if err != nil {
239263 return nil , fmt .Errorf ("couldn't parse file: %w" , err )
240264 }
241- funcPos := make (map [string ]protocol.Position )
265+
266+ type Func struct {
267+ Name string
268+ Pos protocol.Position
269+ }
270+ var fileFuncs []Func
242271 for _ , d := range pgf .File .Decls {
243272 fn , ok := d .(* ast.FuncDecl )
244273 if ! ok {
@@ -254,32 +283,11 @@ func goToTestCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Han
254283 _ , rname , _ := astutil .UnpackRecv (fn .Recv .List [0 ].Type )
255284 name = rname .Name + "_" + fn .Name .Name
256285 }
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+ })
281290 }
282- var matchedTests []Test
283291
284292 pkgIDs := make ([]PackageID , 0 , len (testPackages ))
285293 for _ , pkg := range testPackages {
@@ -291,48 +299,54 @@ func goToTestCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Han
291299 }
292300 for _ , tests := range allTests {
293301 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
304304 }
305- if testType == 0 {
306- continue // unknown type
305+ potentialFuncNames := getPotentialFuncNames (test )
306+ if len (potentialFuncNames ) == 0 {
307+ continue
307308 }
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+ }
317331 }
318- if ok {
332+ if matchedFunc . Name != "" {
319333 loc := test .Location
320334 loc .Range .End = loc .Range .Start // move cursor to the test's beginning
321335
322- matchedTests = append (matchedTests , Test {
323- FuncPos : pos ,
336+ matches = append (matches , TestMatch {
337+ FuncPos : matchedFunc . Pos ,
324338 Name : test .Name ,
325339 Loc : loc ,
326- Type : testType ,
340+ Type : test . Type ,
327341 })
328342 }
329343 }
330344 }
331- if len (matchedTests ) == 0 {
345+ if len (matches ) == 0 {
332346 return nil , nil
333347 }
334348
335- slices .SortFunc (matchedTests , func (a , b Test ) int {
349+ slices .SortFunc (matches , func (a , b TestMatch ) int {
336350 if v := protocol .ComparePosition (a .FuncPos , b .FuncPos ); v != 0 {
337351 return v
338352 }
@@ -341,13 +355,31 @@ func goToTestCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Han
341355 }
342356 return cmp .Compare (a .Name , b .Name )
343357 })
358+ return matches , nil
359+ }
344360
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'
351384 }
352- return lenses , nil
353385}
0 commit comments