55package golang
66
77import (
8+ "cmp"
89 "context"
10+ "fmt"
911 "go/ast"
1012 "go/token"
1113 "go/types"
1214 "regexp"
15+ "slices"
1316 "strings"
17+ "unicode"
1418
1519 "golang.org/x/tools/gopls/internal/cache"
20+ "golang.org/x/tools/gopls/internal/cache/metadata"
1621 "golang.org/x/tools/gopls/internal/cache/parsego"
1722 "golang.org/x/tools/gopls/internal/file"
1823 "golang.org/x/tools/gopls/internal/protocol"
1924 "golang.org/x/tools/gopls/internal/protocol/command"
2025 "golang.org/x/tools/gopls/internal/settings"
26+ "golang.org/x/tools/gopls/internal/util/astutil"
2127)
2228
2329// CodeLensSources returns the supported sources of code lenses for Go files.
@@ -26,6 +32,7 @@ func CodeLensSources() map[settings.CodeLensSource]cache.CodeLensSourceFunc {
2632 settings .CodeLensGenerate : goGenerateCodeLens , // commands: Generate
2733 settings .CodeLensTest : runTestCodeLens , // commands: Test
2834 settings .CodeLensRegenerateCgo : regenerateCgoLens , // commands: RegenerateCgo
35+ settings .CodeLensGoToTest : goToTestCodeLens , // commands: GoToTest
2936 }
3037}
3138
@@ -204,3 +211,143 @@ func regenerateCgoLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Ha
204211 cmd := command .NewRegenerateCgoCommand ("regenerate cgo definitions" , command.URIArg {URI : puri })
205212 return []protocol.CodeLens {{Range : rng , Command : cmd }}, nil
206213}
214+
215+ func goToTestCodeLens (ctx context.Context , snapshot * cache.Snapshot , fh file.Handle ) ([]protocol.CodeLens , error ) {
216+ if strings .HasSuffix (fh .URI ().Path (), "_test.go" ) {
217+ // Ignore test files.
218+ return nil , nil
219+ }
220+
221+ // Inspect all packages to cover both "p [p.test]" and "p_test [p.test]".
222+ allPackages , err := snapshot .WorkspaceMetadata (ctx )
223+ if err != nil {
224+ return nil , fmt .Errorf ("couldn't request workspace metadata: %w" , err )
225+ }
226+ dir := fh .URI ().Dir ()
227+ testPackages := slices .DeleteFunc (allPackages , func (meta * metadata.Package ) bool {
228+ if meta .IsIntermediateTestVariant () || len (meta .CompiledGoFiles ) == 0 || meta .ForTest == "" {
229+ return true
230+ }
231+ return meta .CompiledGoFiles [0 ].Dir () != dir
232+ })
233+ if len (testPackages ) == 0 {
234+ return nil , nil
235+ }
236+
237+ pgf , err := snapshot .ParseGo (ctx , fh , parsego .Full )
238+ if err != nil {
239+ return nil , fmt .Errorf ("couldn't parse file: %w" , err )
240+ }
241+ funcPos := make (map [string ]protocol.Position )
242+ for _ , d := range pgf .File .Decls {
243+ fn , ok := d .(* ast.FuncDecl )
244+ if ! ok {
245+ continue
246+ }
247+ rng , err := pgf .NodeRange (fn )
248+ if err != nil {
249+ return nil , fmt .Errorf ("couldn't get node range: %w" , err )
250+ }
251+
252+ name := fn .Name .Name
253+ if fn .Recv != nil && len (fn .Recv .List ) > 0 {
254+ _ , rname , _ := astutil .UnpackRecv (fn .Recv .List [0 ].Type )
255+ name = rname .Name + "_" + fn .Name .Name
256+ }
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
281+ }
282+ var matchedTests []Test
283+
284+ pkgIDs := make ([]PackageID , 0 , len (testPackages ))
285+ for _ , pkg := range testPackages {
286+ pkgIDs = append (pkgIDs , pkg .ID )
287+ }
288+ allTests , err := snapshot .Tests (ctx , pkgIDs ... )
289+ if err != nil {
290+ return nil , fmt .Errorf ("couldn't request all tests for packages %v: %w" , pkgIDs , err )
291+ }
292+ for _ , tests := range allTests {
293+ 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+ }
304+ }
305+ if testType == 0 {
306+ continue // unknown type
307+ }
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 )]
317+ }
318+ if ok {
319+ loc := test .Location
320+ loc .Range .End = loc .Range .Start // move cursor to the test's beginning
321+
322+ matchedTests = append (matchedTests , Test {
323+ FuncPos : pos ,
324+ Name : test .Name ,
325+ Loc : loc ,
326+ Type : testType ,
327+ })
328+ }
329+ }
330+ }
331+ if len (matchedTests ) == 0 {
332+ return nil , nil
333+ }
334+
335+ slices .SortFunc (matchedTests , func (a , b Test ) int {
336+ if v := protocol .ComparePosition (a .FuncPos , b .FuncPos ); v != 0 {
337+ return v
338+ }
339+ if v := cmp .Compare (a .Type , b .Type ); v != 0 {
340+ return v
341+ }
342+ return cmp .Compare (a .Name , b .Name )
343+ })
344+
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+ })
351+ }
352+ return lenses , nil
353+ }
0 commit comments