aboutsummaryrefslogtreecommitdiff
path: root/gopls/internal/lsp/source/inlay_hint.go
diff options
context:
space:
mode:
Diffstat (limited to 'gopls/internal/lsp/source/inlay_hint.go')
-rw-r--r--gopls/internal/lsp/source/inlay_hint.go394
1 files changed, 394 insertions, 0 deletions
diff --git a/gopls/internal/lsp/source/inlay_hint.go b/gopls/internal/lsp/source/inlay_hint.go
new file mode 100644
index 000000000..671d405dc
--- /dev/null
+++ b/gopls/internal/lsp/source/inlay_hint.go
@@ -0,0 +1,394 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package source
+
+import (
+ "context"
+ "fmt"
+ "go/ast"
+ "go/constant"
+ "go/token"
+ "go/types"
+ "strings"
+
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/internal/event"
+ "golang.org/x/tools/internal/typeparams"
+)
+
+const (
+ maxLabelLength = 28
+)
+
+type InlayHintFunc func(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint
+
+type Hint struct {
+ Name string
+ Doc string
+ Run InlayHintFunc
+}
+
+const (
+ ParameterNames = "parameterNames"
+ AssignVariableTypes = "assignVariableTypes"
+ ConstantValues = "constantValues"
+ RangeVariableTypes = "rangeVariableTypes"
+ CompositeLiteralTypes = "compositeLiteralTypes"
+ CompositeLiteralFieldNames = "compositeLiteralFields"
+ FunctionTypeParameters = "functionTypeParameters"
+)
+
+var AllInlayHints = map[string]*Hint{
+ AssignVariableTypes: {
+ Name: AssignVariableTypes,
+ Doc: "Enable/disable inlay hints for variable types in assign statements:\n```go\n\ti/* int*/, j/* int*/ := 0, len(r)-1\n```",
+ Run: assignVariableTypes,
+ },
+ ParameterNames: {
+ Name: ParameterNames,
+ Doc: "Enable/disable inlay hints for parameter names:\n```go\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)\n```",
+ Run: parameterNames,
+ },
+ ConstantValues: {
+ Name: ConstantValues,
+ 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```",
+ Run: constantValues,
+ },
+ RangeVariableTypes: {
+ Name: RangeVariableTypes,
+ 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```",
+ Run: rangeVariableTypes,
+ },
+ CompositeLiteralTypes: {
+ Name: CompositeLiteralTypes,
+ 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```",
+ Run: compositeLiteralTypes,
+ },
+ CompositeLiteralFieldNames: {
+ Name: CompositeLiteralFieldNames,
+ Doc: "Enable/disable inlay hints for composite literal field names:\n```go\n\t{/*in: */\"Hello, world\", /*want: */\"dlrow ,olleH\"}\n```",
+ Run: compositeLiteralFields,
+ },
+ FunctionTypeParameters: {
+ Name: FunctionTypeParameters,
+ Doc: "Enable/disable inlay hints for implicit type parameters on generic functions:\n```go\n\tmyFoo/*[int, string]*/(1, \"hello\")\n```",
+ Run: funcTypeParams,
+ },
+}
+
+func InlayHint(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) ([]protocol.InlayHint, error) {
+ ctx, done := event.Start(ctx, "source.InlayHint")
+ defer done()
+
+ pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), NarrowestPackage)
+ if err != nil {
+ return nil, fmt.Errorf("getting file for InlayHint: %w", err)
+ }
+
+ // Collect a list of the inlay hints that are enabled.
+ inlayHintOptions := snapshot.View().Options().InlayHintOptions
+ var enabledHints []InlayHintFunc
+ for hint, enabled := range inlayHintOptions.Hints {
+ if !enabled {
+ continue
+ }
+ if h, ok := AllInlayHints[hint]; ok {
+ enabledHints = append(enabledHints, h.Run)
+ }
+ }
+ if len(enabledHints) == 0 {
+ return nil, nil
+ }
+
+ info := pkg.GetTypesInfo()
+ q := Qualifier(pgf.File, pkg.GetTypes(), info)
+
+ // Set the range to the full file if the range is not valid.
+ start, end := pgf.File.Pos(), pgf.File.End()
+ if pRng.Start.Line < pRng.End.Line || pRng.Start.Character < pRng.End.Character {
+ // Adjust start and end for the specified range.
+ var err error
+ start, end, err = pgf.RangePos(pRng)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ var hints []protocol.InlayHint
+ ast.Inspect(pgf.File, func(node ast.Node) bool {
+ // If not in range, we can stop looking.
+ if node == nil || node.End() < start || node.Pos() > end {
+ return false
+ }
+ for _, fn := range enabledHints {
+ hints = append(hints, fn(node, pgf.Mapper, pgf.Tok, info, &q)...)
+ }
+ return true
+ })
+ return hints, nil
+}
+
+func parameterNames(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint {
+ callExpr, ok := node.(*ast.CallExpr)
+ if !ok {
+ return nil
+ }
+ signature, ok := info.TypeOf(callExpr.Fun).(*types.Signature)
+ if !ok {
+ return nil
+ }
+
+ var hints []protocol.InlayHint
+ for i, v := range callExpr.Args {
+ start, err := m.PosPosition(tf, v.Pos())
+ if err != nil {
+ continue
+ }
+ params := signature.Params()
+ // When a function has variadic params, we skip args after
+ // params.Len().
+ if i > params.Len()-1 {
+ break
+ }
+ param := params.At(i)
+ // param.Name is empty for built-ins like append
+ if param.Name() == "" {
+ continue
+ }
+ // Skip the parameter name hint if the arg matches the
+ // the parameter name.
+ if i, ok := v.(*ast.Ident); ok && i.Name == param.Name() {
+ continue
+ }
+
+ label := param.Name()
+ if signature.Variadic() && i == params.Len()-1 {
+ label = label + "..."
+ }
+ hints = append(hints, protocol.InlayHint{
+ Position: start,
+ Label: buildLabel(label + ":"),
+ Kind: protocol.Parameter,
+ PaddingRight: true,
+ })
+ }
+ return hints
+}
+
+func funcTypeParams(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint {
+ ce, ok := node.(*ast.CallExpr)
+ if !ok {
+ return nil
+ }
+ id, ok := ce.Fun.(*ast.Ident)
+ if !ok {
+ return nil
+ }
+ inst := typeparams.GetInstances(info)[id]
+ if inst.TypeArgs == nil {
+ return nil
+ }
+ start, err := m.PosPosition(tf, id.End())
+ if err != nil {
+ return nil
+ }
+ var args []string
+ for i := 0; i < inst.TypeArgs.Len(); i++ {
+ args = append(args, inst.TypeArgs.At(i).String())
+ }
+ if len(args) == 0 {
+ return nil
+ }
+ return []protocol.InlayHint{{
+ Position: start,
+ Label: buildLabel("[" + strings.Join(args, ", ") + "]"),
+ Kind: protocol.Type,
+ }}
+}
+
+func assignVariableTypes(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint {
+ stmt, ok := node.(*ast.AssignStmt)
+ if !ok || stmt.Tok != token.DEFINE {
+ return nil
+ }
+
+ var hints []protocol.InlayHint
+ for _, v := range stmt.Lhs {
+ if h := variableType(v, m, tf, info, q); h != nil {
+ hints = append(hints, *h)
+ }
+ }
+ return hints
+}
+
+func rangeVariableTypes(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint {
+ rStmt, ok := node.(*ast.RangeStmt)
+ if !ok {
+ return nil
+ }
+ var hints []protocol.InlayHint
+ if h := variableType(rStmt.Key, m, tf, info, q); h != nil {
+ hints = append(hints, *h)
+ }
+ if h := variableType(rStmt.Value, m, tf, info, q); h != nil {
+ hints = append(hints, *h)
+ }
+ return hints
+}
+
+func variableType(e ast.Expr, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) *protocol.InlayHint {
+ typ := info.TypeOf(e)
+ if typ == nil {
+ return nil
+ }
+ end, err := m.PosPosition(tf, e.End())
+ if err != nil {
+ return nil
+ }
+ return &protocol.InlayHint{
+ Position: end,
+ Label: buildLabel(types.TypeString(typ, *q)),
+ Kind: protocol.Type,
+ PaddingLeft: true,
+ }
+}
+
+func constantValues(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint {
+ genDecl, ok := node.(*ast.GenDecl)
+ if !ok || genDecl.Tok != token.CONST {
+ return nil
+ }
+
+ var hints []protocol.InlayHint
+ for _, v := range genDecl.Specs {
+ spec, ok := v.(*ast.ValueSpec)
+ if !ok {
+ continue
+ }
+ end, err := m.PosPosition(tf, v.End())
+ if err != nil {
+ continue
+ }
+ // Show hints when values are missing or at least one value is not
+ // a basic literal.
+ showHints := len(spec.Values) == 0
+ checkValues := len(spec.Names) == len(spec.Values)
+ var values []string
+ for i, w := range spec.Names {
+ obj, ok := info.ObjectOf(w).(*types.Const)
+ if !ok || obj.Val().Kind() == constant.Unknown {
+ return nil
+ }
+ if checkValues {
+ switch spec.Values[i].(type) {
+ case *ast.BadExpr:
+ return nil
+ case *ast.BasicLit:
+ default:
+ if obj.Val().Kind() != constant.Bool {
+ showHints = true
+ }
+ }
+ }
+ values = append(values, fmt.Sprintf("%v", obj.Val()))
+ }
+ if !showHints || len(values) == 0 {
+ continue
+ }
+ hints = append(hints, protocol.InlayHint{
+ Position: end,
+ Label: buildLabel("= " + strings.Join(values, ", ")),
+ PaddingLeft: true,
+ })
+ }
+ return hints
+}
+
+func compositeLiteralFields(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint {
+ compLit, ok := node.(*ast.CompositeLit)
+ if !ok {
+ return nil
+ }
+ typ := info.TypeOf(compLit)
+ if typ == nil {
+ return nil
+ }
+ if t, ok := typ.(*types.Pointer); ok {
+ typ = t.Elem()
+ }
+ strct, ok := typ.Underlying().(*types.Struct)
+ if !ok {
+ return nil
+ }
+
+ var hints []protocol.InlayHint
+ var allEdits []protocol.TextEdit
+ for i, v := range compLit.Elts {
+ if _, ok := v.(*ast.KeyValueExpr); !ok {
+ start, err := m.PosPosition(tf, v.Pos())
+ if err != nil {
+ continue
+ }
+ if i > strct.NumFields()-1 {
+ break
+ }
+ hints = append(hints, protocol.InlayHint{
+ Position: start,
+ Label: buildLabel(strct.Field(i).Name() + ":"),
+ Kind: protocol.Parameter,
+ PaddingRight: true,
+ })
+ allEdits = append(allEdits, protocol.TextEdit{
+ Range: protocol.Range{Start: start, End: start},
+ NewText: strct.Field(i).Name() + ": ",
+ })
+ }
+ }
+ // It is not allowed to have a mix of keyed and unkeyed fields, so
+ // have the text edits add keys to all fields.
+ for i := range hints {
+ hints[i].TextEdits = allEdits
+ }
+ return hints
+}
+
+func compositeLiteralTypes(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint {
+ compLit, ok := node.(*ast.CompositeLit)
+ if !ok {
+ return nil
+ }
+ typ := info.TypeOf(compLit)
+ if typ == nil {
+ return nil
+ }
+ if compLit.Type != nil {
+ return nil
+ }
+ prefix := ""
+ if t, ok := typ.(*types.Pointer); ok {
+ typ = t.Elem()
+ prefix = "&"
+ }
+ // The type for this composite literal is implicit, add an inlay hint.
+ start, err := m.PosPosition(tf, compLit.Lbrace)
+ if err != nil {
+ return nil
+ }
+ return []protocol.InlayHint{{
+ Position: start,
+ Label: buildLabel(fmt.Sprintf("%s%s", prefix, types.TypeString(typ, *q))),
+ Kind: protocol.Type,
+ }}
+}
+
+func buildLabel(s string) []protocol.InlayHintLabelPart {
+ label := protocol.InlayHintLabelPart{
+ Value: s,
+ }
+ if len(s) > maxLabelLength+len("...") {
+ label.Value = s[:maxLabelLength] + "..."
+ }
+ return []protocol.InlayHintLabelPart{label}
+}