golang.org/x/tools/gopls@v0.15.3/internal/golang/inlay_hint.go (about) 1 // Copyright 2022 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 golang 6 7 import ( 8 "context" 9 "fmt" 10 "go/ast" 11 "go/constant" 12 "go/token" 13 "go/types" 14 "strings" 15 16 "golang.org/x/tools/gopls/internal/cache" 17 "golang.org/x/tools/gopls/internal/file" 18 "golang.org/x/tools/gopls/internal/protocol" 19 "golang.org/x/tools/gopls/internal/util/typesutil" 20 "golang.org/x/tools/internal/event" 21 ) 22 23 const ( 24 maxLabelLength = 28 25 ) 26 27 type InlayHintFunc func(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint 28 29 type Hint struct { 30 Name string 31 Doc string 32 Run InlayHintFunc 33 } 34 35 const ( 36 ParameterNames = "parameterNames" 37 AssignVariableTypes = "assignVariableTypes" 38 ConstantValues = "constantValues" 39 RangeVariableTypes = "rangeVariableTypes" 40 CompositeLiteralTypes = "compositeLiteralTypes" 41 CompositeLiteralFieldNames = "compositeLiteralFields" 42 FunctionTypeParameters = "functionTypeParameters" 43 ) 44 45 var AllInlayHints = map[string]*Hint{ 46 AssignVariableTypes: { 47 Name: AssignVariableTypes, 48 Doc: "Enable/disable inlay hints for variable types in assign statements:\n```go\n\ti/* int*/, j/* int*/ := 0, len(r)-1\n```", 49 Run: assignVariableTypes, 50 }, 51 ParameterNames: { 52 Name: ParameterNames, 53 Doc: "Enable/disable inlay hints for parameter names:\n```go\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)\n```", 54 Run: parameterNames, 55 }, 56 ConstantValues: { 57 Name: ConstantValues, 58 Doc: "Enable/disable inlay hints for constant values:\n```go\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)\n```", 59 Run: constantValues, 60 }, 61 RangeVariableTypes: { 62 Name: RangeVariableTypes, 63 Doc: "Enable/disable inlay hints for variable types in range statements:\n```go\n\tfor k/* int*/, v/* string*/ := range []string{} {\n\t\tfmt.Println(k, v)\n\t}\n```", 64 Run: rangeVariableTypes, 65 }, 66 CompositeLiteralTypes: { 67 Name: CompositeLiteralTypes, 68 Doc: "Enable/disable inlay hints for composite literal types:\n```go\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}\n```", 69 Run: compositeLiteralTypes, 70 }, 71 CompositeLiteralFieldNames: { 72 Name: CompositeLiteralFieldNames, 73 Doc: "Enable/disable inlay hints for composite literal field names:\n```go\n\t{/*in: */\"Hello, world\", /*want: */\"dlrow ,olleH\"}\n```", 74 Run: compositeLiteralFields, 75 }, 76 FunctionTypeParameters: { 77 Name: FunctionTypeParameters, 78 Doc: "Enable/disable inlay hints for implicit type parameters on generic functions:\n```go\n\tmyFoo/*[int, string]*/(1, \"hello\")\n```", 79 Run: funcTypeParams, 80 }, 81 } 82 83 func InlayHint(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pRng protocol.Range) ([]protocol.InlayHint, error) { 84 ctx, done := event.Start(ctx, "golang.InlayHint") 85 defer done() 86 87 pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) 88 if err != nil { 89 return nil, fmt.Errorf("getting file for InlayHint: %w", err) 90 } 91 92 // Collect a list of the inlay hints that are enabled. 93 inlayHintOptions := snapshot.Options().InlayHintOptions 94 var enabledHints []InlayHintFunc 95 for hint, enabled := range inlayHintOptions.Hints { 96 if !enabled { 97 continue 98 } 99 if h, ok := AllInlayHints[hint]; ok { 100 enabledHints = append(enabledHints, h.Run) 101 } 102 } 103 if len(enabledHints) == 0 { 104 return nil, nil 105 } 106 107 info := pkg.GetTypesInfo() 108 q := typesutil.FileQualifier(pgf.File, pkg.GetTypes(), info) 109 110 // Set the range to the full file if the range is not valid. 111 start, end := pgf.File.Pos(), pgf.File.End() 112 if pRng.Start.Line < pRng.End.Line || pRng.Start.Character < pRng.End.Character { 113 // Adjust start and end for the specified range. 114 var err error 115 start, end, err = pgf.RangePos(pRng) 116 if err != nil { 117 return nil, err 118 } 119 } 120 121 var hints []protocol.InlayHint 122 ast.Inspect(pgf.File, func(node ast.Node) bool { 123 // If not in range, we can stop looking. 124 if node == nil || node.End() < start || node.Pos() > end { 125 return false 126 } 127 for _, fn := range enabledHints { 128 hints = append(hints, fn(node, pgf.Mapper, pgf.Tok, info, &q)...) 129 } 130 return true 131 }) 132 return hints, nil 133 } 134 135 func parameterNames(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { 136 callExpr, ok := node.(*ast.CallExpr) 137 if !ok { 138 return nil 139 } 140 signature, ok := info.TypeOf(callExpr.Fun).(*types.Signature) 141 if !ok { 142 return nil 143 } 144 145 var hints []protocol.InlayHint 146 for i, v := range callExpr.Args { 147 start, err := m.PosPosition(tf, v.Pos()) 148 if err != nil { 149 continue 150 } 151 params := signature.Params() 152 // When a function has variadic params, we skip args after 153 // params.Len(). 154 if i > params.Len()-1 { 155 break 156 } 157 param := params.At(i) 158 // param.Name is empty for built-ins like append 159 if param.Name() == "" { 160 continue 161 } 162 // Skip the parameter name hint if the arg matches 163 // the parameter name. 164 if i, ok := v.(*ast.Ident); ok && i.Name == param.Name() { 165 continue 166 } 167 168 label := param.Name() 169 if signature.Variadic() && i == params.Len()-1 { 170 label = label + "..." 171 } 172 hints = append(hints, protocol.InlayHint{ 173 Position: start, 174 Label: buildLabel(label + ":"), 175 Kind: protocol.Parameter, 176 PaddingRight: true, 177 }) 178 } 179 return hints 180 } 181 182 func funcTypeParams(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { 183 ce, ok := node.(*ast.CallExpr) 184 if !ok { 185 return nil 186 } 187 id, ok := ce.Fun.(*ast.Ident) 188 if !ok { 189 return nil 190 } 191 inst := info.Instances[id] 192 if inst.TypeArgs == nil { 193 return nil 194 } 195 start, err := m.PosPosition(tf, id.End()) 196 if err != nil { 197 return nil 198 } 199 var args []string 200 for i := 0; i < inst.TypeArgs.Len(); i++ { 201 args = append(args, inst.TypeArgs.At(i).String()) 202 } 203 if len(args) == 0 { 204 return nil 205 } 206 return []protocol.InlayHint{{ 207 Position: start, 208 Label: buildLabel("[" + strings.Join(args, ", ") + "]"), 209 Kind: protocol.Type, 210 }} 211 } 212 213 func assignVariableTypes(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint { 214 stmt, ok := node.(*ast.AssignStmt) 215 if !ok || stmt.Tok != token.DEFINE { 216 return nil 217 } 218 219 var hints []protocol.InlayHint 220 for _, v := range stmt.Lhs { 221 if h := variableType(v, m, tf, info, q); h != nil { 222 hints = append(hints, *h) 223 } 224 } 225 return hints 226 } 227 228 func rangeVariableTypes(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint { 229 rStmt, ok := node.(*ast.RangeStmt) 230 if !ok { 231 return nil 232 } 233 var hints []protocol.InlayHint 234 if h := variableType(rStmt.Key, m, tf, info, q); h != nil { 235 hints = append(hints, *h) 236 } 237 if h := variableType(rStmt.Value, m, tf, info, q); h != nil { 238 hints = append(hints, *h) 239 } 240 return hints 241 } 242 243 func variableType(e ast.Expr, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) *protocol.InlayHint { 244 typ := info.TypeOf(e) 245 if typ == nil { 246 return nil 247 } 248 end, err := m.PosPosition(tf, e.End()) 249 if err != nil { 250 return nil 251 } 252 return &protocol.InlayHint{ 253 Position: end, 254 Label: buildLabel(types.TypeString(typ, *q)), 255 Kind: protocol.Type, 256 PaddingLeft: true, 257 } 258 } 259 260 func constantValues(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { 261 genDecl, ok := node.(*ast.GenDecl) 262 if !ok || genDecl.Tok != token.CONST { 263 return nil 264 } 265 266 var hints []protocol.InlayHint 267 for _, v := range genDecl.Specs { 268 spec, ok := v.(*ast.ValueSpec) 269 if !ok { 270 continue 271 } 272 end, err := m.PosPosition(tf, v.End()) 273 if err != nil { 274 continue 275 } 276 // Show hints when values are missing or at least one value is not 277 // a basic literal. 278 showHints := len(spec.Values) == 0 279 checkValues := len(spec.Names) == len(spec.Values) 280 var values []string 281 for i, w := range spec.Names { 282 obj, ok := info.ObjectOf(w).(*types.Const) 283 if !ok || obj.Val().Kind() == constant.Unknown { 284 return nil 285 } 286 if checkValues { 287 switch spec.Values[i].(type) { 288 case *ast.BadExpr: 289 return nil 290 case *ast.BasicLit: 291 default: 292 if obj.Val().Kind() != constant.Bool { 293 showHints = true 294 } 295 } 296 } 297 values = append(values, fmt.Sprintf("%v", obj.Val())) 298 } 299 if !showHints || len(values) == 0 { 300 continue 301 } 302 hints = append(hints, protocol.InlayHint{ 303 Position: end, 304 Label: buildLabel("= " + strings.Join(values, ", ")), 305 PaddingLeft: true, 306 }) 307 } 308 return hints 309 } 310 311 func compositeLiteralFields(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { 312 compLit, ok := node.(*ast.CompositeLit) 313 if !ok { 314 return nil 315 } 316 typ := info.TypeOf(compLit) 317 if typ == nil { 318 return nil 319 } 320 if t, ok := typ.(*types.Pointer); ok { 321 typ = t.Elem() 322 } 323 strct, ok := typ.Underlying().(*types.Struct) 324 if !ok { 325 return nil 326 } 327 328 var hints []protocol.InlayHint 329 var allEdits []protocol.TextEdit 330 for i, v := range compLit.Elts { 331 if _, ok := v.(*ast.KeyValueExpr); !ok { 332 start, err := m.PosPosition(tf, v.Pos()) 333 if err != nil { 334 continue 335 } 336 if i > strct.NumFields()-1 { 337 break 338 } 339 hints = append(hints, protocol.InlayHint{ 340 Position: start, 341 Label: buildLabel(strct.Field(i).Name() + ":"), 342 Kind: protocol.Parameter, 343 PaddingRight: true, 344 }) 345 allEdits = append(allEdits, protocol.TextEdit{ 346 Range: protocol.Range{Start: start, End: start}, 347 NewText: strct.Field(i).Name() + ": ", 348 }) 349 } 350 } 351 // It is not allowed to have a mix of keyed and unkeyed fields, so 352 // have the text edits add keys to all fields. 353 for i := range hints { 354 hints[i].TextEdits = allEdits 355 } 356 return hints 357 } 358 359 func compositeLiteralTypes(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint { 360 compLit, ok := node.(*ast.CompositeLit) 361 if !ok { 362 return nil 363 } 364 typ := info.TypeOf(compLit) 365 if typ == nil { 366 return nil 367 } 368 if compLit.Type != nil { 369 return nil 370 } 371 prefix := "" 372 if t, ok := typ.(*types.Pointer); ok { 373 typ = t.Elem() 374 prefix = "&" 375 } 376 // The type for this composite literal is implicit, add an inlay hint. 377 start, err := m.PosPosition(tf, compLit.Lbrace) 378 if err != nil { 379 return nil 380 } 381 return []protocol.InlayHint{{ 382 Position: start, 383 Label: buildLabel(fmt.Sprintf("%s%s", prefix, types.TypeString(typ, *q))), 384 Kind: protocol.Type, 385 }} 386 } 387 388 func buildLabel(s string) []protocol.InlayHintLabelPart { 389 label := protocol.InlayHintLabelPart{ 390 Value: s, 391 } 392 if len(s) > maxLabelLength+len("...") { 393 label.Value = s[:maxLabelLength] + "..." 394 } 395 return []protocol.InlayHintLabelPart{label} 396 }