diff --git a/gopls/doc/codelenses.md b/gopls/doc/codelenses.md index dd1fd8eec83..55cbd31fc43 100644 --- a/gopls/doc/codelenses.md +++ b/gopls/doc/codelenses.md @@ -40,6 +40,17 @@ Default: on File type: Go +## `go_to_test`: Go to the functions's Test, Example, Benchmark, or Fuzz declarations + + +This codelens source annotates function and method declarations +with their corresponding Test, Example, Benchmark, and Fuzz functions. + + +Default: off + +File type: Go + ## `regenerate_cgo`: Re-generate cgo declarations diff --git a/gopls/internal/cache/snapshot.go b/gopls/internal/cache/snapshot.go index b97d93af116..2b3b047d5f4 100644 --- a/gopls/internal/cache/snapshot.go +++ b/gopls/internal/cache/snapshot.go @@ -978,7 +978,7 @@ func (s *Snapshot) WorkspaceMetadata(ctx context.Context) ([]*metadata.Package, defer s.mu.Unlock() meta := make([]*metadata.Package, 0, s.workspacePackages.Len()) - for id, _ := range s.workspacePackages.All() { + for id := range s.workspacePackages.All() { meta = append(meta, s.meta.Packages[id]) } return meta, nil diff --git a/gopls/internal/cache/testfuncs/tests.go b/gopls/internal/cache/testfuncs/tests.go index e0e3ce1beca..8e417a2a1e2 100644 --- a/gopls/internal/cache/testfuncs/tests.go +++ b/gopls/internal/cache/testfuncs/tests.go @@ -8,6 +8,7 @@ import ( "go/ast" "go/constant" "go/types" + "iter" "strings" "unicode" "unicode/utf8" @@ -35,22 +36,36 @@ func (index *Index) Encode() []byte { return packageCodec.Encode(index.pkg) } -func (index *Index) All() []Result { - var results []Result - for _, file := range index.pkg.Files { - for _, test := range file.Tests { - results = append(results, test.result()) +func (index *Index) All() iter.Seq[Result] { + return func(yield func(Result) bool) { + for _, file := range index.pkg.Files { + for _, test := range file.Tests { + if !yield(test.result()) { + return + } + } } } - return results } // A Result reports a test function type Result struct { Location protocol.Location // location of the test Name string // name of the test + Type TestType // type of the test + Subtest bool } +type TestType int + +const ( + TypeInvalid TestType = iota + TypeTest + TypeBenchmark + TypeFuzz + TypeExample +) + // NewIndex returns a new index of method-set information for all // package-level types in the specified package. func NewIndex(files []*parsego.File, info *types.Info) *Index { @@ -84,8 +99,8 @@ func (b *indexBuilder) build(files []*parsego.File, info *types.Info) *Index { continue } - isTest, isExample := isTestOrExample(obj) - if !isTest && !isExample { + testType := getTestType(obj) + if testType == TypeInvalid { continue } @@ -93,6 +108,7 @@ func (b *indexBuilder) build(files []*parsego.File, info *types.Info) *Index { t.Name = decl.Name.Name t.Location.URI = file.URI t.Location.Range, _ = file.NodeRange(decl) + t.Type = testType i, ok := b.fileIndex[t.Location.URI] if !ok { @@ -105,7 +121,8 @@ func (b *indexBuilder) build(files []*parsego.File, info *types.Info) *Index { b.visited[obj] = true // Check for subtests - if isTest { + switch testType { + case TypeTest, TypeBenchmark, TypeFuzz: b.Files[i].Tests = append(b.Files[i].Tests, b.findSubtests(t, decl.Type, decl.Body, file, files, info)...) } } @@ -168,6 +185,8 @@ func (b *indexBuilder) findSubtests(parent gobTest, typ *ast.FuncType, body *ast t.Name = b.uniqueName(parent.Name, rewrite(constant.StringVal(val))) t.Location.URI = file.URI t.Location.Range, _ = file.NodeRange(call) + t.Type = parent.Type + t.Subtest = true tests = append(tests, t) fn, typ, body := findFunc(files, info, body, call.Args[1]) @@ -182,7 +201,8 @@ func (b *indexBuilder) findSubtests(parent gobTest, typ *ast.FuncType, body *ast } // Never recurse if the second argument is a top-level test function - if isTest, _ := isTestOrExample(fn); isTest { + switch getTestType(fn) { + case TypeTest, TypeBenchmark, TypeFuzz: continue } @@ -258,30 +278,35 @@ func findFunc(files []*parsego.File, info *types.Info, body *ast.BlockStmt, expr return nil, nil, nil } -// isTestOrExample reports whether the given func is a testing func or an -// example func (or neither). isTestOrExample returns (true, false) for testing -// funcs, (false, true) for example funcs, and (false, false) otherwise. -func isTestOrExample(fn *types.Func) (isTest, isExample bool) { +// getTestType reports the test type of the given function. +func getTestType(fn *types.Func) TestType { sig := fn.Type().(*types.Signature) - if sig.Params().Len() == 0 && - sig.Results().Len() == 0 { - return false, isTestName(fn.Name(), "Example") + if sig.Params().Len() == 0 && sig.Results().Len() == 0 { + if isTestName(fn.Name(), "Example") { + return TypeExample + } + return TypeInvalid } kind, ok := testKind(sig) if !ok { - return false, false + return TypeInvalid } switch kind.Name() { case "T": - return isTestName(fn.Name(), "Test"), false + if isTestName(fn.Name(), "Test") { + return TypeTest + } case "B": - return isTestName(fn.Name(), "Benchmark"), false + if isTestName(fn.Name(), "Benchmark") { + return TypeBenchmark + } case "F": - return isTestName(fn.Name(), "Fuzz"), false - default: - return false, false // "can't happen" (see testKind) + if isTestName(fn.Name(), "Fuzz") { + return TypeFuzz + } } + return TypeInvalid } // isTestName reports whether name is a valid test name for the test kind @@ -352,6 +377,8 @@ type gobFile struct { type gobTest struct { Location protocol.Location // location of the test Name string // name of the test + Type TestType // type of the test + Subtest bool } func (t *gobTest) result() Result { diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json index 1cee2452e69..df4b29dcd1a 100644 --- a/gopls/internal/doc/api.json +++ b/gopls/internal/doc/api.json @@ -2013,6 +2013,12 @@ "Default": "true", "Status": "" }, + { + "Name": "\"go_to_test\"", + "Doc": "`\"go_to_test\"`: Go to the functions's Test, Example, Benchmark, or Fuzz declarations\n\nThis codelens source annotates function and method declarations\nwith their corresponding Test, Example, Benchmark, and Fuzz functions.\n", + "Default": "false", + "Status": "" + }, { "Name": "\"regenerate_cgo\"", "Doc": "`\"regenerate_cgo\"`: Re-generate cgo declarations\n\nThis codelens source annotates an `import \"C\"` declaration\nwith a command to re-run the [cgo\ncommand](https://pkg.go.dev/cmd/cgo) to regenerate the\ncorresponding Go declarations.\n\nUse this after editing the C code in comments attached to\nthe import, or in C header files included by it.\n", @@ -2200,6 +2206,14 @@ "Default": true, "Status": "" }, + { + "FileType": "Go", + "Lens": "go_to_test", + "Title": "Go to the functions's Test, Example, Benchmark, or Fuzz declarations", + "Doc": "\nThis codelens source annotates function and method declarations\nwith their corresponding Test, Example, Benchmark, and Fuzz functions.\n", + "Default": false, + "Status": "" + }, { "FileType": "Go", "Lens": "regenerate_cgo", diff --git a/gopls/internal/golang/code_lens.go b/gopls/internal/golang/code_lens.go index b04724e0cbc..ce866e108e7 100644 --- a/gopls/internal/golang/code_lens.go +++ b/gopls/internal/golang/code_lens.go @@ -5,19 +5,26 @@ package golang import ( + "cmp" "context" + "fmt" "go/ast" "go/token" "go/types" "regexp" + "slices" "strings" + "unicode" "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/cache/parsego" + "golang.org/x/tools/gopls/internal/cache/testfuncs" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/gopls/internal/settings" + "golang.org/x/tools/internal/astutil" ) // CodeLensSources returns the supported sources of code lenses for Go files. @@ -26,6 +33,7 @@ func CodeLensSources() map[settings.CodeLensSource]cache.CodeLensSourceFunc { settings.CodeLensGenerate: goGenerateCodeLens, // commands: Generate settings.CodeLensTest: runTestCodeLens, // commands: Test settings.CodeLensRegenerateCgo: regenerateCgoLens, // commands: RegenerateCgo + settings.CodeLensGoToTest: goToTestCodeLens, // commands: GoToTest } } @@ -204,3 +212,180 @@ func regenerateCgoLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Ha cmd := command.NewRegenerateCgoCommand("regenerate cgo definitions", command.URIArg{URI: puri}) return []protocol.CodeLens{{Range: rng, Command: cmd}}, nil } + +func goToTestCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) { + if !snapshot.Options().ClientOptions.ShowDocumentSupported { + // GoToTest command uses 'window/showDocument' request. Don't generate + // code lenses for clients that won't be able to use them. + return nil, nil + } + + matches, err := matchFunctionsWithTests(ctx, snapshot, fh) + if err != nil { + return nil, err + } + + lenses := make([]protocol.CodeLens, 0, len(matches)) + for _, t := range matches { + lenses = append(lenses, protocol.CodeLens{ + Range: protocol.Range{Start: t.FuncPos, End: t.FuncPos}, + Command: command.NewGoToTestCommand("Go to "+t.Name, t.Loc), + }) + } + return lenses, nil +} + +type TestMatch struct { + FuncPos protocol.Position // function position + Name string // test name + Loc protocol.Location // test location + Type testfuncs.TestType // test type +} + +func matchFunctionsWithTests(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) (matches []TestMatch, err error) { + if strings.HasSuffix(fh.URI().Path(), "_test.go") { + // Ignore test files. + return nil, nil + } + + // Inspect all packages to cover both "p [p.test]" and "p_test [p.test]". + allPackages, err := snapshot.WorkspaceMetadata(ctx) + if err != nil { + return nil, fmt.Errorf("couldn't request workspace metadata: %w", err) + } + dir := fh.URI().Dir() + testPackages := slices.DeleteFunc(allPackages, func(meta *metadata.Package) bool { + if meta.IsIntermediateTestVariant() || len(meta.CompiledGoFiles) == 0 || meta.ForTest == "" { + return true + } + return meta.CompiledGoFiles[0].Dir() != dir + }) + if len(testPackages) == 0 { + return nil, nil + } + + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) + if err != nil { + return nil, fmt.Errorf("couldn't parse file: %w", err) + } + + type Func struct { + Name string + Pos protocol.Position + } + var fileFuncs []Func + for _, d := range pgf.File.Decls { + fn, ok := d.(*ast.FuncDecl) + if !ok { + continue + } + rng, err := pgf.NodeRange(fn) + if err != nil { + return nil, fmt.Errorf("couldn't get node range: %w", err) + } + + name := fn.Name.Name + if fn.Recv != nil && len(fn.Recv.List) > 0 { + _, rname, _ := astutil.UnpackRecv(fn.Recv.List[0].Type) + name = rname.Name + "_" + fn.Name.Name + } + fileFuncs = append(fileFuncs, Func{ + Name: name, + Pos: rng.Start, + }) + } + + pkgIDs := make([]PackageID, 0, len(testPackages)) + for _, pkg := range testPackages { + pkgIDs = append(pkgIDs, pkg.ID) + } + allTests, err := snapshot.Tests(ctx, pkgIDs...) + if err != nil { + return nil, fmt.Errorf("couldn't request all tests for packages %v: %w", pkgIDs, err) + } + for _, tests := range allTests { + for test := range tests.All() { + if test.Subtest { + continue + } + potentialFuncNames := getPotentialFuncNames(test) + if len(potentialFuncNames) == 0 { + continue + } + + var matchedFunc Func + for _, fn := range fileFuncs { + var matched bool + for _, potentialName := range potentialFuncNames { + // Check the prefix to match 'TestDeletePanics' with 'Delete'. + if strings.HasPrefix(potentialName, fn.Name) { + matched = true + break + } + } + if !matched { + continue + } + + // Use the most specific function: + // + // - match 'TestDelete', 'TestDeletePanics' with 'Delete' + // - match 'TestDeleteFunc', 'TestDeleteFuncClearTail' with 'DeleteFunc', not 'Delete' + if len(matchedFunc.Name) < len(fn.Name) { + matchedFunc = fn + } + } + if matchedFunc.Name != "" { + loc := test.Location + loc.Range.End = loc.Range.Start // move cursor to the test's beginning + + matches = append(matches, TestMatch{ + FuncPos: matchedFunc.Pos, + Name: test.Name, + Loc: loc, + Type: test.Type, + }) + } + } + } + if len(matches) == 0 { + return nil, nil + } + + slices.SortFunc(matches, func(a, b TestMatch) int { + if v := protocol.ComparePosition(a.FuncPos, b.FuncPos); v != 0 { + return v + } + if v := cmp.Compare(a.Type, b.Type); v != 0 { + return v + } + return cmp.Compare(a.Name, b.Name) + }) + return matches, nil +} + +func getPotentialFuncNames(test testfuncs.Result) []string { + var name string + switch test.Type { + case testfuncs.TypeTest: + name = strings.TrimPrefix(test.Name, "Test") + case testfuncs.TypeBenchmark: + name = strings.TrimPrefix(test.Name, "Benchmark") + case testfuncs.TypeFuzz: + name = strings.TrimPrefix(test.Name, "Fuzz") + case testfuncs.TypeExample: + name = strings.TrimPrefix(test.Name, "Example") + } + if name == "" { + return nil + } + name = strings.TrimPrefix(name, "_") // 'Foo' for 'TestFoo', 'foo' for 'Test_foo' + + names := []string{name} + if token.IsExported(name) { + unexportedName := []rune(name) + unexportedName[0] = unicode.ToLower(unexportedName[0]) + names = append(names, string(unexportedName)) // 'foo' for 'TestFoo' + } + return names +} diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go index 38b1798b507..a2ab74b74aa 100644 --- a/gopls/internal/golang/codeaction.go +++ b/gopls/internal/golang/codeaction.go @@ -238,6 +238,7 @@ type codeActionProducer struct { var codeActionProducers = [...]codeActionProducer{ {kind: protocol.QuickFix, fn: quickFix, needPkg: true}, {kind: protocol.SourceOrganizeImports, fn: sourceOrganizeImports}, + {kind: settings.GoToTest, fn: goToTestCodeAction, needPkg: true}, {kind: settings.AddTest, fn: addTest, needPkg: true}, {kind: settings.GoAssembly, fn: goAssembly, needPkg: true}, {kind: settings.GoDoc, fn: goDoc, needPkg: true}, @@ -1129,3 +1130,38 @@ func toggleCompilerOptDetails(ctx context.Context, req *codeActionsRequest) erro } return nil } + +// goToTestCodeAction produces "Go to TestXxx" code action. +// See [server.commandHandler.GoToTest] for command implementation. +func goToTestCodeAction(ctx context.Context, req *codeActionsRequest) error { + if !req.snapshot.Options().ClientOptions.ShowDocumentSupported { + // GoToTest command uses 'window/showDocument' request. Don't generate + // code actions for clients that won't be able to use them. + return nil + } + + path, _ := astutil.PathEnclosingInterval(req.pgf.File, req.start, req.end) + if len(path) < 2 { + return nil + } + fn, ok := path[len(path)-2].(*ast.FuncDecl) + if !ok { + return nil + } + fnRng, err := req.pgf.NodeRange(fn) + if err != nil { + return fmt.Errorf("couldn't get node range: %w", err) + } + + matches, err := matchFunctionsWithTests(ctx, req.snapshot, req.fh) + if err != nil { + return err + } + for _, m := range matches { + if m.FuncPos == fnRng.Start { + cmd := command.NewGoToTestCommand("Go to "+m.Name, m.Loc) + req.addCommandAction(cmd, false) + } + } + return nil +} diff --git a/gopls/internal/protocol/command/command_gen.go b/gopls/internal/protocol/command/command_gen.go index b4dee88ab54..897518d271e 100644 --- a/gopls/internal/protocol/command/command_gen.go +++ b/gopls/internal/protocol/command/command_gen.go @@ -42,6 +42,7 @@ const ( GCDetails Command = "gopls.gc_details" Generate Command = "gopls.generate" GoGetPackage Command = "gopls.go_get_package" + GoToTest Command = "gopls.go_to_test" ListImports Command = "gopls.list_imports" ListKnownPackages Command = "gopls.list_known_packages" MaybePromptForTelemetry Command = "gopls.maybe_prompt_for_telemetry" @@ -89,6 +90,7 @@ var Commands = []Command{ GCDetails, Generate, GoGetPackage, + GoToTest, ListImports, ListKnownPackages, MaybePromptForTelemetry, @@ -230,6 +232,12 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte return nil, err } return nil, s.GoGetPackage(ctx, a0) + case GoToTest: + var a0 protocol.Location + if err := UnmarshalArgs(params.Arguments, &a0); err != nil { + return nil, err + } + return nil, s.GoToTest(ctx, a0) case ListImports: var a0 URIArg if err := UnmarshalArgs(params.Arguments, &a0); err != nil { @@ -515,6 +523,14 @@ func NewGoGetPackageCommand(title string, a0 GoGetPackageArgs) *protocol.Command } } +func NewGoToTestCommand(title string, a0 protocol.Location) *protocol.Command { + return &protocol.Command{ + Title: title, + Command: GoToTest.String(), + Arguments: MustMarshalArgs(a0), + } +} + func NewListImportsCommand(title string, a0 URIArg) *protocol.Command { return &protocol.Command{ Title: title, diff --git a/gopls/internal/protocol/command/interface.go b/gopls/internal/protocol/command/interface.go index 61977f68237..d4806fffd5c 100644 --- a/gopls/internal/protocol/command/interface.go +++ b/gopls/internal/protocol/command/interface.go @@ -305,6 +305,9 @@ type Interface interface { // ModifyTags: Add or remove struct tags on a given node. ModifyTags(context.Context, ModifyTagsArgs) error + + // GoToTest: Go to test declaration. + GoToTest(context.Context, protocol.Location) error } type RunTestsArgs struct { diff --git a/gopls/internal/server/command.go b/gopls/internal/server/command.go index bc986ce7816..c14e74d3f21 100644 --- a/gopls/internal/server/command.go +++ b/gopls/internal/server/command.go @@ -234,7 +234,7 @@ func (h *commandHandler) Packages(ctx context.Context, args command.PackagesArgs for i, tests := range allTests { pkg := &result.Packages[start+i] fileByPath := map[protocol.DocumentURI]*command.TestFile{} - for _, test := range tests.All() { + for test := range tests.All() { test := command.TestCase{ Name: test.Name, Loc: test.Location, @@ -1784,6 +1784,15 @@ func optionsStringToMap(options string) (map[string][]string, error) { return optionsMap, nil } +func (c *commandHandler) GoToTest(ctx context.Context, loc protocol.Location) error { + return c.run(ctx, commandConfig{ + forURI: loc.URI, + }, func(ctx context.Context, deps commandDeps) error { + showDocumentImpl(ctx, c.s.client, protocol.URI(loc.URI), &loc.Range, c.s.options) + return nil + }) +} + func (c *commandHandler) ModifyTags(ctx context.Context, args command.ModifyTagsArgs) error { return c.run(ctx, commandConfig{ progress: "Modifying tags", diff --git a/gopls/internal/settings/codeactionkind.go b/gopls/internal/settings/codeactionkind.go index e083cae0ed6..a770e8da4c5 100644 --- a/gopls/internal/settings/codeactionkind.go +++ b/gopls/internal/settings/codeactionkind.go @@ -83,6 +83,7 @@ const ( GoToggleCompilerOptDetails protocol.CodeActionKind = "source.toggleCompilerOptDetails" AddTest protocol.CodeActionKind = "source.addTest" OrganizeImports protocol.CodeActionKind = "source.organizeImports" + GoToTest protocol.CodeActionKind = "source.go_to_test" // gopls GoplsDocFeatures protocol.CodeActionKind = "gopls.doc.features" diff --git a/gopls/internal/settings/settings.go b/gopls/internal/settings/settings.go index 76820cb28ca..a1780908dbe 100644 --- a/gopls/internal/settings/settings.go +++ b/gopls/internal/settings/settings.go @@ -356,6 +356,12 @@ const ( // module root so that it contains an up-to-date copy of all // necessary package dependencies. CodeLensVendor CodeLensSource = "vendor" + + // Go to the functions's Test, Example, Benchmark, or Fuzz declarations + // + // This codelens source annotates function and method declarations + // with their corresponding Test, Example, Benchmark, and Fuzz functions. + CodeLensGoToTest CodeLensSource = "go_to_test" ) // Note: CompletionOptions must be comparable with reflect.DeepEqual. diff --git a/gopls/internal/test/integration/misc/codeactions_test.go b/gopls/internal/test/integration/misc/codeactions_test.go index 105e96af2a8..a77f17e73eb 100644 --- a/gopls/internal/test/integration/misc/codeactions_test.go +++ b/gopls/internal/test/integration/misc/codeactions_test.go @@ -12,6 +12,7 @@ import ( "github.com/google/go-cmp/cmp" "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/gopls/internal/settings" . "golang.org/x/tools/gopls/internal/test/integration" ) @@ -175,3 +176,117 @@ package a } }) } + +func TestGoToTestCodeAction(t *testing.T) { + const src = ` +-- a.go -- +package codelenses + +func bar() { + _ = "bar" +} + +func noTests() { + _ = "no_tests" +} + +-- a_test.go -- +package codelenses_test + +import "testing" + +func Test_bar(*testing.T) {} + +func Benchmark_bar(*testing.B) {} + +func Fuzz_bar(*testing.F) {} + +-- slices.go -- +package codelenses + +func Delete() { + _ = "delete" +} + +func DeleteFunc() {} + +-- slices_test.go -- +package codelenses + +import "testing" + +func TestDelete(*testing.T) {} + +func TestDeleteClearTail(*testing.T) {} + +func TestDeletePanics(*testing.T) {} + +func TestDeleteFunc(*testing.T) {} + +func TestDeleteFuncClearTail(*testing.T) {} + +` + + Run(t, src, func(t *testing.T, env *Env) { + getLoc := func(file, re string) protocol.Location { + t.Helper() + + env.OpenFile(file) + return env.RegexpSearch(file, re) + } + + type CodeAction struct { + Title string + Loc protocol.Location + } + for _, tt := range []struct { + file, re string + want []CodeAction + }{ + { + file: "a.go", re: `_ = "bar"`, + want: []CodeAction{ + {Title: "Go to Test_bar", Loc: getLoc("a_test.go", "()func Test_bar")}, + {Title: "Go to Benchmark_bar", Loc: getLoc("a_test.go", "()func Benchmark_bar")}, + {Title: "Go to Fuzz_bar", Loc: getLoc("a_test.go", "()func Fuzz_bar")}, + }, + }, + { + file: "a.go", re: `_ = "no_tests"`, + want: nil, + }, + { + file: "slices.go", re: `_ = "delete"`, + want: []CodeAction{ + {Title: "Go to TestDelete", Loc: getLoc("slices_test.go", "()func TestDelete")}, + {Title: "Go to TestDeleteClearTail", Loc: getLoc("slices_test.go", "()func TestDeleteClearTail")}, + {Title: "Go to TestDeletePanics", Loc: getLoc("slices_test.go", "()func TestDeletePanics")}, + }, + }, + } { + env.OpenFile(tt.file) + loc := env.RegexpSearch(tt.file, tt.re) + actions, err := env.Editor.CodeAction(env.Ctx, loc, nil, protocol.CodeActionUnknownTrigger) + if err != nil { + t.Fatal(err) + } + var got []CodeAction + for _, v := range actions { + if v.Kind == "source.go_to_test" { + var loc protocol.Location + err := command.UnmarshalArgs(v.Command.Arguments, &loc) + if err != nil { + t.Fatal(err) + } + got = append(got, CodeAction{ + Title: v.Title, + Loc: loc, + }) + } + } + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("CodeAction mismatch (-want +got):\n%s", diff) + } + } + }) +} diff --git a/gopls/internal/test/marker/testdata/codelens/go_to_test.txt b/gopls/internal/test/marker/testdata/codelens/go_to_test.txt new file mode 100644 index 00000000000..6326f98720d --- /dev/null +++ b/gopls/internal/test/marker/testdata/codelens/go_to_test.txt @@ -0,0 +1,110 @@ +-- settings.json -- +{ + "codelenses": { + "go_to_test": true + } +} + +-- a.go -- +//@codelenses() + +package codelenses + +func Foo() int { return 0 } //@codelens(re"()func Foo", "Go to TestFoo") + +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") + +func xyz() {} //@codelens(re"()func xyz", "Go to TestXyz") + +func Translate() { //@codelens(re"()func Translate", "Go to TestTranslate") + translate() +} + +func translate() {} + +-- a1_test.go -- +package codelenses + +import "testing" + +func TestFoo(*testing.T) {} + +func TestTranslate(*testing.T) { + translate() +} + +-- a2_test.go -- +package codelenses + +import "testing" + +func TestXyz(*testing.T) {} + +-- a3_test.go -- +package codelenses_test + +import "testing" + +func Test_bar(*testing.T) {} + +func Benchmark_bar(*testing.B) {} + +func Fuzz_bar(*testing.F) {} + +-- writer.go -- +//@codelenses() + +package codelenses + +type Writer struct{} + +func (w *Writer) Write() error { return nil } //@codelens(re"()func", "Go to TestWriter_Write") + +func (w *Writer) Flush() {} //@codelens(re"()func", "Go to TestWriter_Flush") + +-- writer_test.go -- +package codelenses + +import "testing" + +func TestWriter_Write(*testing.T) {} + +func TestWriter_Flush(*testing.T) {} + +-- slices.go -- +//@codelenses() + +package codelenses + +func Delete() {} //@codelens(re"()func", "Go to TestDelete"),codelens(re"()func", "Go to TestDeleteClearTail"),codelens(re"()func", "Go to TestDeletePanics") + +func DeleteFunc() {} //@codelens(re"()func", "Go to TestDeleteFunc"),codelens(re"()func", "Go to TestDeleteFuncClearTail") + +-- slices_test.go -- +package codelenses + +import "testing" + +func TestDelete(*testing.T) {} + +func TestDeleteClearTail(*testing.T) {} + +func TestDeletePanics(*testing.T) {} + +func TestDeleteFunc(*testing.T) {} + +func TestDeleteFuncClearTail(*testing.T) {} + +-- example.go -- +//@codelenses() + +package codelenses + +func Sort() {} //@codelens(re"()func", "Go to ExampleSort_long"),codelens(re"()func", "Go to ExampleSort_short") + +-- example_test.go -- +package codelenses + +func ExampleSort_short() {} + +func ExampleSort_long() {}