github.com/v2fly/tools@v0.100.0/internal/lsp/source/code_lens.go (about) 1 // Copyright 2020 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package source 6 7 import ( 8 "context" 9 "go/ast" 10 "go/token" 11 "go/types" 12 "path/filepath" 13 "regexp" 14 "strings" 15 16 "github.com/v2fly/tools/internal/lsp/command" 17 "github.com/v2fly/tools/internal/lsp/protocol" 18 "github.com/v2fly/tools/internal/span" 19 ) 20 21 type LensFunc func(context.Context, Snapshot, FileHandle) ([]protocol.CodeLens, error) 22 23 // LensFuncs returns the supported lensFuncs for Go files. 24 func LensFuncs() map[command.Command]LensFunc { 25 return map[command.Command]LensFunc{ 26 command.Generate: goGenerateCodeLens, 27 command.Test: runTestCodeLens, 28 command.RegenerateCgo: regenerateCgoLens, 29 command.GCDetails: toggleDetailsCodeLens, 30 } 31 } 32 33 var ( 34 testRe = regexp.MustCompile("^Test[^a-z]") 35 benchmarkRe = regexp.MustCompile("^Benchmark[^a-z]") 36 ) 37 38 func runTestCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) { 39 codeLens := make([]protocol.CodeLens, 0) 40 41 fns, err := TestsAndBenchmarks(ctx, snapshot, fh) 42 if err != nil { 43 return nil, err 44 } 45 puri := protocol.URIFromSpanURI(fh.URI()) 46 for _, fn := range fns.Tests { 47 cmd, err := command.NewTestCommand("run test", puri, []string{fn.Name}, nil) 48 if err != nil { 49 return nil, err 50 } 51 rng := protocol.Range{Start: fn.Rng.Start, End: fn.Rng.Start} 52 codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: cmd}) 53 } 54 55 for _, fn := range fns.Benchmarks { 56 cmd, err := command.NewTestCommand("run benchmark", puri, nil, []string{fn.Name}) 57 if err != nil { 58 return nil, err 59 } 60 rng := protocol.Range{Start: fn.Rng.Start, End: fn.Rng.Start} 61 codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: cmd}) 62 } 63 64 if len(fns.Benchmarks) > 0 { 65 _, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage) 66 if err != nil { 67 return nil, err 68 } 69 // add a code lens to the top of the file which runs all benchmarks in the file 70 rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, pgf.File.Package, pgf.File.Package).Range() 71 if err != nil { 72 return nil, err 73 } 74 var benches []string 75 for _, fn := range fns.Benchmarks { 76 benches = append(benches, fn.Name) 77 } 78 cmd, err := command.NewTestCommand("run file benchmarks", puri, nil, benches) 79 if err != nil { 80 return nil, err 81 } 82 codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: cmd}) 83 } 84 return codeLens, nil 85 } 86 87 type testFn struct { 88 Name string 89 Rng protocol.Range 90 } 91 92 type testFns struct { 93 Tests []testFn 94 Benchmarks []testFn 95 } 96 97 func TestsAndBenchmarks(ctx context.Context, snapshot Snapshot, fh FileHandle) (testFns, error) { 98 var out testFns 99 100 if !strings.HasSuffix(fh.URI().Filename(), "_test.go") { 101 return out, nil 102 } 103 pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage) 104 if err != nil { 105 return out, err 106 } 107 108 for _, d := range pgf.File.Decls { 109 fn, ok := d.(*ast.FuncDecl) 110 if !ok { 111 continue 112 } 113 114 rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, d.Pos(), fn.End()).Range() 115 if err != nil { 116 return out, err 117 } 118 119 if matchTestFunc(fn, pkg, testRe, "T") { 120 out.Tests = append(out.Tests, testFn{fn.Name.Name, rng}) 121 } 122 123 if matchTestFunc(fn, pkg, benchmarkRe, "B") { 124 out.Benchmarks = append(out.Benchmarks, testFn{fn.Name.Name, rng}) 125 } 126 } 127 128 return out, nil 129 } 130 131 func matchTestFunc(fn *ast.FuncDecl, pkg Package, nameRe *regexp.Regexp, paramID string) bool { 132 // Make sure that the function name matches a test function. 133 if !nameRe.MatchString(fn.Name.Name) { 134 return false 135 } 136 info := pkg.GetTypesInfo() 137 if info == nil { 138 return false 139 } 140 obj := info.ObjectOf(fn.Name) 141 if obj == nil { 142 return false 143 } 144 sig, ok := obj.Type().(*types.Signature) 145 if !ok { 146 return false 147 } 148 // Test functions should have only one parameter. 149 if sig.Params().Len() != 1 { 150 return false 151 } 152 153 // Check the type of the only parameter 154 paramTyp, ok := sig.Params().At(0).Type().(*types.Pointer) 155 if !ok { 156 return false 157 } 158 named, ok := paramTyp.Elem().(*types.Named) 159 if !ok { 160 return false 161 } 162 namedObj := named.Obj() 163 if namedObj.Pkg().Path() != "testing" { 164 return false 165 } 166 return namedObj.Id() == paramID 167 } 168 169 func goGenerateCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) { 170 pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) 171 if err != nil { 172 return nil, err 173 } 174 const ggDirective = "//go:generate" 175 for _, c := range pgf.File.Comments { 176 for _, l := range c.List { 177 if !strings.HasPrefix(l.Text, ggDirective) { 178 continue 179 } 180 rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, l.Pos(), l.Pos()+token.Pos(len(ggDirective))).Range() 181 if err != nil { 182 return nil, err 183 } 184 dir := protocol.URIFromSpanURI(span.URIFromPath(filepath.Dir(fh.URI().Filename()))) 185 nonRecursiveCmd, err := command.NewGenerateCommand("run go generate", command.GenerateArgs{Dir: dir, Recursive: false}) 186 if err != nil { 187 return nil, err 188 } 189 recursiveCmd, err := command.NewGenerateCommand("run go generate ./...", command.GenerateArgs{Dir: dir, Recursive: true}) 190 if err != nil { 191 return nil, err 192 } 193 return []protocol.CodeLens{ 194 {Range: rng, Command: recursiveCmd}, 195 {Range: rng, Command: nonRecursiveCmd}, 196 }, nil 197 198 } 199 } 200 return nil, nil 201 } 202 203 func regenerateCgoLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) { 204 pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) 205 if err != nil { 206 return nil, err 207 } 208 var c *ast.ImportSpec 209 for _, imp := range pgf.File.Imports { 210 if imp.Path.Value == `"C"` { 211 c = imp 212 } 213 } 214 if c == nil { 215 return nil, nil 216 } 217 rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, c.Pos(), c.EndPos).Range() 218 if err != nil { 219 return nil, err 220 } 221 puri := protocol.URIFromSpanURI(fh.URI()) 222 cmd, err := command.NewRegenerateCgoCommand("regenerate cgo definitions", command.URIArg{URI: puri}) 223 if err != nil { 224 return nil, err 225 } 226 return []protocol.CodeLens{{Range: rng, Command: cmd}}, nil 227 } 228 229 func toggleDetailsCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) { 230 _, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage) 231 if err != nil { 232 return nil, err 233 } 234 rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, pgf.File.Package, pgf.File.Package).Range() 235 if err != nil { 236 return nil, err 237 } 238 puri := protocol.URIFromSpanURI(fh.URI()) 239 cmd, err := command.NewGCDetailsCommand("Toggle gc annotation details", puri) 240 if err != nil { 241 return nil, err 242 } 243 return []protocol.CodeLens{{Range: rng, Command: cmd}}, nil 244 }