5
5
package golang
6
6
7
7
import (
8
+ "cmp"
8
9
"context"
10
+ "fmt"
9
11
"go/ast"
10
12
"go/token"
11
13
"go/types"
12
14
"regexp"
15
+ "slices"
13
16
"strings"
17
+ "unicode"
14
18
15
19
"golang.org/x/tools/gopls/internal/cache"
20
+ "golang.org/x/tools/gopls/internal/cache/metadata"
16
21
"golang.org/x/tools/gopls/internal/cache/parsego"
17
22
"golang.org/x/tools/gopls/internal/file"
18
23
"golang.org/x/tools/gopls/internal/protocol"
19
24
"golang.org/x/tools/gopls/internal/protocol/command"
20
25
"golang.org/x/tools/gopls/internal/settings"
26
+ "golang.org/x/tools/gopls/internal/util/astutil"
21
27
)
22
28
23
29
// CodeLensSources returns the supported sources of code lenses for Go files.
@@ -26,6 +32,7 @@ func CodeLensSources() map[settings.CodeLensSource]cache.CodeLensSourceFunc {
26
32
settings .CodeLensGenerate : goGenerateCodeLens , // commands: Generate
27
33
settings .CodeLensTest : runTestCodeLens , // commands: Test
28
34
settings .CodeLensRegenerateCgo : regenerateCgoLens , // commands: RegenerateCgo
35
+ settings .CodeLensGoToTest : goToTestCodeLens , // commands: GoToTest
29
36
}
30
37
}
31
38
@@ -204,3 +211,143 @@ func regenerateCgoLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Ha
204
211
cmd := command .NewRegenerateCgoCommand ("regenerate cgo definitions" , command.URIArg {URI : puri })
205
212
return []protocol.CodeLens {{Range : rng , Command : cmd }}, nil
206
213
}
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