Skip to content

Commit 397ea86

Browse files
committed
gopls/internal/golang: implement Go to TestXxx CodeLens
DO NOT REVIEW DO NOT SUBMIT
1 parent 5397e65 commit 397ea86

File tree

7 files changed

+244
-1
lines changed

7 files changed

+244
-1
lines changed

gopls/internal/cache/snapshot.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -976,7 +976,7 @@ func (s *Snapshot) WorkspaceMetadata(ctx context.Context) ([]*metadata.Package,
976976
defer s.mu.Unlock()
977977

978978
meta := make([]*metadata.Package, 0, s.workspacePackages.Len())
979-
for id, _ := range s.workspacePackages.All() {
979+
for id := range s.workspacePackages.All() {
980980
meta = append(meta, s.meta.Packages[id])
981981
}
982982
return meta, nil

gopls/internal/golang/code_lens.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,25 @@
55
package golang
66

77
import (
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+
}

gopls/internal/protocol/command/command_gen.go

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gopls/internal/protocol/command/interface.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,9 @@ type Interface interface {
305305

306306
// ModifyTags: Add or remove struct tags on a given node.
307307
ModifyTags(context.Context, ModifyTagsArgs) error
308+
309+
// GoToTest: Go to test declaration.
310+
GoToTest(context.Context, protocol.Location) error
308311
}
309312

310313
type RunTestsArgs struct {

gopls/internal/server/command.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1788,6 +1788,15 @@ func optionsStringToMap(options string) (map[string][]string, error) {
17881788
return optionsMap, nil
17891789
}
17901790

1791+
func (c *commandHandler) GoToTest(ctx context.Context, loc protocol.Location) error {
1792+
return c.run(ctx, commandConfig{
1793+
forURI: loc.URI,
1794+
}, func(ctx context.Context, deps commandDeps) error {
1795+
showDocumentImpl(ctx, c.s.client, protocol.URI(loc.URI), &loc.Range, c.s.options)
1796+
return nil
1797+
})
1798+
}
1799+
17911800
func (c *commandHandler) ModifyTags(ctx context.Context, args command.ModifyTagsArgs) error {
17921801
return c.run(ctx, commandConfig{
17931802
progress: "Modifying tags",

gopls/internal/settings/settings.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,12 @@ const (
356356
// module root so that it contains an up-to-date copy of all
357357
// necessary package dependencies.
358358
CodeLensVendor CodeLensSource = "vendor"
359+
360+
// Go to the functions's Test, Example, Benchmark, or Fuzz declarations
361+
//
362+
// This codelens source annotates the function and method declarations
363+
// with their corresponding Test, Example, Benchmark, and Fuzz functions.
364+
CodeLensGoToTest CodeLensSource = "go_to_test"
359365
)
360366

361367
// Note: CompletionOptions must be comparable with reflect.DeepEqual.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
-- settings.json --
2+
{
3+
"codelenses": {
4+
"go_to_test": true
5+
}
6+
}
7+
8+
-- a.go --
9+
//@codelenses()
10+
11+
package codelenses
12+
13+
func Foo() int { return 0 } //@codelens(re"()func Foo", "Go to TestFoo")
14+
15+
func bar() {} //@codelens(re"()func bar", "Go to Test_bar"),codelens(re"()func bar", "Go to Benchmark_bar"),codelens(re"()func bar", "Go to Fuzz_bar")
16+
17+
func xyz() {} //@codelens(re"()func xyz", "Go to TestXyz")
18+
19+
-- a1_test.go --
20+
package codelenses
21+
22+
import "testing"
23+
24+
func TestFoo(*testing.T) {}
25+
26+
-- a2_test.go --
27+
package codelenses
28+
29+
import "testing"
30+
31+
func TestXyz(*testing.T) {}
32+
33+
-- a3_test.go --
34+
package codelenses_test
35+
36+
import "testing"
37+
38+
func Test_bar(*testing.T) {}
39+
40+
func Benchmark_bar(*testing.B) {}
41+
42+
func Fuzz_bar(*testing.F) {}
43+
44+
-- writer.go --
45+
//@codelenses()
46+
47+
package codelenses
48+
49+
type Writer struct{}
50+
51+
func (w *Writer) Write() error { return nil } //@codelens(re"()func", "Go to TestWriter_Write")
52+
53+
func (w *Writer) Flush() {} //@codelens(re"()func", "Go to TestWriter_Flush")
54+
55+
-- writer_test.go --
56+
package codelenses
57+
58+
import "testing"
59+
60+
func TestWriter_Write(*testing.T) {}
61+
62+
func TestWriter_Flush(*testing.T) {}

0 commit comments

Comments
 (0)