github.com/jd-ly/tools@v0.5.7/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/jd-ly/tools/internal/lsp/protocol" 17 "github.com/jd-ly/tools/internal/span" 18 ) 19 20 type LensFunc func(context.Context, Snapshot, FileHandle) ([]protocol.CodeLens, error) 21 22 // LensFuncs returns the supported lensFuncs for Go files. 23 func LensFuncs() map[string]LensFunc { 24 return map[string]LensFunc{ 25 CommandGenerate.Name: goGenerateCodeLens, 26 CommandTest.Name: runTestCodeLens, 27 CommandRegenerateCgo.Name: regenerateCgoLens, 28 CommandToggleDetails.Name: toggleDetailsCodeLens, 29 } 30 } 31 32 var ( 33 testRe = regexp.MustCompile("^Test[^a-z]") 34 benchmarkRe = regexp.MustCompile("^Benchmark[^a-z]") 35 ) 36 37 func runTestCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) { 38 codeLens := make([]protocol.CodeLens, 0) 39 40 fns, err := TestsAndBenchmarks(ctx, snapshot, fh) 41 if err != nil { 42 return nil, err 43 } 44 for _, fn := range fns.Tests { 45 jsonArgs, err := MarshalArgs(fh.URI(), []string{fn.Name}, nil) 46 if err != nil { 47 return nil, err 48 } 49 codeLens = append(codeLens, protocol.CodeLens{ 50 Range: protocol.Range{Start: fn.Rng.Start, End: fn.Rng.Start}, 51 Command: protocol.Command{ 52 Title: "run test", 53 Command: CommandTest.ID(), 54 Arguments: jsonArgs, 55 }, 56 }) 57 } 58 59 for _, fn := range fns.Benchmarks { 60 jsonArgs, err := MarshalArgs(fh.URI(), nil, []string{fn.Name}) 61 if err != nil { 62 return nil, err 63 } 64 codeLens = append(codeLens, protocol.CodeLens{ 65 Range: protocol.Range{Start: fn.Rng.Start, End: fn.Rng.Start}, 66 Command: protocol.Command{ 67 Title: "run benchmark", 68 Command: CommandTest.ID(), 69 Arguments: jsonArgs, 70 }, 71 }) 72 } 73 74 _, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage) 75 if err != nil { 76 return nil, err 77 } 78 // add a code lens to the top of the file which runs all benchmarks in the file 79 rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, pgf.File.Package, pgf.File.Package).Range() 80 if err != nil { 81 return nil, err 82 } 83 args, err := MarshalArgs(fh.URI(), []string{}, fns.Benchmarks) 84 if err != nil { 85 return nil, err 86 } 87 codeLens = append(codeLens, protocol.CodeLens{ 88 Range: rng, 89 Command: protocol.Command{ 90 Title: "run file benchmarks", 91 Command: CommandTest.ID(), 92 Arguments: args, 93 }, 94 }) 95 return codeLens, nil 96 } 97 98 type testFn struct { 99 Name string 100 Rng protocol.Range 101 } 102 103 type testFns struct { 104 Tests []testFn 105 Benchmarks []testFn 106 } 107 108 func TestsAndBenchmarks(ctx context.Context, snapshot Snapshot, fh FileHandle) (testFns, error) { 109 var out testFns 110 111 if !strings.HasSuffix(fh.URI().Filename(), "_test.go") { 112 return out, nil 113 } 114 pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage) 115 if err != nil { 116 return out, err 117 } 118 119 for _, d := range pgf.File.Decls { 120 fn, ok := d.(*ast.FuncDecl) 121 if !ok { 122 continue 123 } 124 125 rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, d.Pos(), fn.End()).Range() 126 if err != nil { 127 return out, err 128 } 129 130 if matchTestFunc(fn, pkg, testRe, "T") { 131 out.Tests = append(out.Tests, testFn{fn.Name.Name, rng}) 132 } 133 134 if matchTestFunc(fn, pkg, benchmarkRe, "B") { 135 out.Benchmarks = append(out.Benchmarks, testFn{fn.Name.Name, rng}) 136 } 137 } 138 139 return out, nil 140 } 141 142 func matchTestFunc(fn *ast.FuncDecl, pkg Package, nameRe *regexp.Regexp, paramID string) bool { 143 // Make sure that the function name matches a test function. 144 if !nameRe.MatchString(fn.Name.Name) { 145 return false 146 } 147 info := pkg.GetTypesInfo() 148 if info == nil { 149 return false 150 } 151 obj := info.ObjectOf(fn.Name) 152 if obj == nil { 153 return false 154 } 155 sig, ok := obj.Type().(*types.Signature) 156 if !ok { 157 return false 158 } 159 // Test functions should have only one parameter. 160 if sig.Params().Len() != 1 { 161 return false 162 } 163 164 // Check the type of the only parameter 165 paramTyp, ok := sig.Params().At(0).Type().(*types.Pointer) 166 if !ok { 167 return false 168 } 169 named, ok := paramTyp.Elem().(*types.Named) 170 if !ok { 171 return false 172 } 173 namedObj := named.Obj() 174 if namedObj.Pkg().Path() != "testing" { 175 return false 176 } 177 return namedObj.Id() == paramID 178 } 179 180 func goGenerateCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) { 181 pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) 182 if err != nil { 183 return nil, err 184 } 185 const ggDirective = "//go:generate" 186 for _, c := range pgf.File.Comments { 187 for _, l := range c.List { 188 if !strings.HasPrefix(l.Text, ggDirective) { 189 continue 190 } 191 rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, l.Pos(), l.Pos()+token.Pos(len(ggDirective))).Range() 192 if err != nil { 193 return nil, err 194 } 195 dir := span.URIFromPath(filepath.Dir(fh.URI().Filename())) 196 nonRecursiveArgs, err := MarshalArgs(dir, false) 197 if err != nil { 198 return nil, err 199 } 200 recursiveArgs, err := MarshalArgs(dir, true) 201 if err != nil { 202 return nil, err 203 } 204 return []protocol.CodeLens{ 205 { 206 Range: rng, 207 Command: protocol.Command{ 208 Title: "run go generate", 209 Command: CommandGenerate.ID(), 210 Arguments: nonRecursiveArgs, 211 }, 212 }, 213 { 214 Range: rng, 215 Command: protocol.Command{ 216 Title: "run go generate ./...", 217 Command: CommandGenerate.ID(), 218 Arguments: recursiveArgs, 219 }, 220 }, 221 }, nil 222 223 } 224 } 225 return nil, nil 226 } 227 228 func regenerateCgoLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) { 229 pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) 230 if err != nil { 231 return nil, err 232 } 233 var c *ast.ImportSpec 234 for _, imp := range pgf.File.Imports { 235 if imp.Path.Value == `"C"` { 236 c = imp 237 } 238 } 239 if c == nil { 240 return nil, nil 241 } 242 rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, c.Pos(), c.EndPos).Range() 243 if err != nil { 244 return nil, err 245 } 246 jsonArgs, err := MarshalArgs(fh.URI()) 247 if err != nil { 248 return nil, err 249 } 250 return []protocol.CodeLens{ 251 { 252 Range: rng, 253 Command: protocol.Command{ 254 Title: "regenerate cgo definitions", 255 Command: CommandRegenerateCgo.ID(), 256 Arguments: jsonArgs, 257 }, 258 }, 259 }, nil 260 } 261 262 func toggleDetailsCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) { 263 _, pgf, err := GetParsedFile(ctx, snapshot, fh, WidestPackage) 264 if err != nil { 265 return nil, err 266 } 267 rng, err := NewMappedRange(snapshot.FileSet(), pgf.Mapper, pgf.File.Package, pgf.File.Package).Range() 268 if err != nil { 269 return nil, err 270 } 271 jsonArgs, err := MarshalArgs(fh.URI()) 272 if err != nil { 273 return nil, err 274 } 275 return []protocol.CodeLens{{ 276 Range: rng, 277 Command: protocol.Command{ 278 Title: "Toggle gc annotation details", 279 Command: CommandToggleDetails.ID(), 280 Arguments: jsonArgs, 281 }, 282 }}, nil 283 }