internal/lsp: add settings for inlay hints and enable

This change adds user settings for enabling inlay hints, modeled
roughly after analyzers. This will allow users to turn on specific
inlay hints that they like and leave others off.

With all of the inlay hints turned off by default, we can now enable
inlay hints.

Change-Id: Ie5dfcbbab1e0b7312eafcc4aa08cb4fe8a83fc31
Reviewed-on: https://go-review.googlesource.com/c/tools/+/411906
Run-TryBot: Suzy Mueller <suzmue@golang.org>
Reviewed-by: Jamal Carvalho <jamal@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go
index 0695efc..4188d9d 100755
--- a/internal/lsp/source/api_json.go
+++ b/internal/lsp/source/api_json.go
@@ -506,6 +506,51 @@
 				Hierarchy: "ui.diagnostic",
 			},
 			{
+				Name: "hints",
+				Type: "map[string]bool",
+				Doc:  "hints specify inlay hints that users want to see.\nA full list of hints that gopls uses can be found\n[here](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md).\n",
+				EnumKeys: EnumKeys{Keys: []EnumKey{
+					{
+						Name:    "\"assign_variable_types\"",
+						Doc:     "Enable/disable inlay hints for variable types in assign statements:\n\n\ti/* int/*, j/* int/* := 0, len(r)-1",
+						Default: "false",
+					},
+					{
+						Name:    "\"composite_literal_fields\"",
+						Doc:     "Enable/disable inlay hints for composite literal field names:\n\n\t{in: \"Hello, world\", want: \"dlrow ,olleH\"}",
+						Default: "false",
+					},
+					{
+						Name:    "\"composite_literal_types\"",
+						Doc:     "Enable/disable inlay hints for composite literal types:\n\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}",
+						Default: "false",
+					},
+					{
+						Name:    "\"constant_values\"",
+						Doc:     "Enable/disable inlay hints for constant values:\n\n\tconst (\n\t\tKindNone   Kind = iota/* = 0*/\n\t\tKindPrint/*  = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)",
+						Default: "false",
+					},
+					{
+						Name:    "\"function_type_parameters\"",
+						Doc:     "Enable/disable inlay hints for implicit type parameters on generic functions:\n\n\tmyFoo/*[int, string]*/(1, \"hello\")",
+						Default: "false",
+					},
+					{
+						Name:    "\"parameter_names\"",
+						Doc:     "Enable/disable inlay hints for parameter names:\n\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)",
+						Default: "false",
+					},
+					{
+						Name:    "\"range_variable_types\"",
+						Doc:     "Enable/disable inlay hints for variable types in range statements:\n\n\tfor k/* int*/, v/* string/* := range []string{} {\n\t\tfmt.Println(k, v)\n\t}",
+						Default: "false",
+					},
+				}},
+				Default:   "{}",
+				Status:    "experimental",
+				Hierarchy: "ui.inlayhint",
+			},
+			{
 				Name: "codelenses",
 				Type: "map[string]bool",
 				Doc:  "codelenses overrides the enabled/disabled state of code lenses. See the\n\"Code Lenses\" section of the\n[Settings page](https://github.com/golang/tools/blob/master/gopls/doc/settings.md#code-lenses)\nfor the list of supported lenses.\n\nExample Usage:\n\n```json5\n\"gopls\": {\n...\n  \"codelenses\": {\n    \"generate\": false,  // Don't show the `go generate` lens.\n    \"gc_details\": true  // Show a code lens toggling the display of gc's choices.\n  }\n...\n}\n```\n",
@@ -979,4 +1024,34 @@
 			Default: true,
 		},
 	},
+	Hints: []*HintJSON{
+		{
+			Name: "assign_variable_types",
+			Doc:  "Enable/disable inlay hints for variable types in assign statements:\n\n\ti/* int/*, j/* int/* := 0, len(r)-1",
+		},
+		{
+			Name: "composite_literal_fields",
+			Doc:  "Enable/disable inlay hints for composite literal field names:\n\n\t{in: \"Hello, world\", want: \"dlrow ,olleH\"}",
+		},
+		{
+			Name: "composite_literal_types",
+			Doc:  "Enable/disable inlay hints for composite literal types:\n\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}",
+		},
+		{
+			Name: "constant_values",
+			Doc:  "Enable/disable inlay hints for constant values:\n\n\tconst (\n\t\tKindNone   Kind = iota/* = 0*/\n\t\tKindPrint/*  = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)",
+		},
+		{
+			Name: "function_type_parameters",
+			Doc:  "Enable/disable inlay hints for implicit type parameters on generic functions:\n\n\tmyFoo/*[int, string]*/(1, \"hello\")",
+		},
+		{
+			Name: "parameter_names",
+			Doc:  "Enable/disable inlay hints for parameter names:\n\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)",
+		},
+		{
+			Name: "range_variable_types",
+			Doc:  "Enable/disable inlay hints for variable types in range statements:\n\n\tfor k/* int*/, v/* string/* := range []string{} {\n\t\tfmt.Println(k, v)\n\t}",
+		},
+	},
 }
diff --git a/internal/lsp/source/inlay_hint.go b/internal/lsp/source/inlay_hint.go
index 8369681..99e1ad0 100644
--- a/internal/lsp/source/inlay_hint.go
+++ b/internal/lsp/source/inlay_hint.go
@@ -23,6 +23,87 @@
 	maxLabelLength = 28
 )
 
+type InlayHintFunc func(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint
+
+type Hint struct {
+	Name string
+	Doc  string
+	Run  InlayHintFunc
+}
+
+const (
+	ParameterNames             = "parameter_names"
+	AssignVariableTypes        = "assign_variable_types"
+	ConstantValues             = "constant_values"
+	RangeVariableTypes         = "range_variable_types"
+	CompositeLiteralTypes      = "composite_literal_types"
+	CompositeLiteralFieldNames = "composite_literal_fields"
+	FunctionTypeParameters     = "function_type_parameters"
+)
+
+var AllInlayHints = map[string]*Hint{
+	AssignVariableTypes: {
+		Name: AssignVariableTypes,
+		Doc: `Enable/disable inlay hints for variable types in assign statements:
+
+	i/* int/*, j/* int/* := 0, len(r)-1`,
+		Run: assignVariableTypes,
+	},
+	ParameterNames: {
+		Name: ParameterNames,
+		Doc: `Enable/disable inlay hints for parameter names:
+
+	parseInt(/* str: */ "123", /* radix: */ 8)`,
+		Run: parameterNames,
+	},
+	ConstantValues: {
+		Name: ConstantValues,
+		Doc: `Enable/disable inlay hints for constant values:
+
+	const (
+		KindNone   Kind = iota/* = 0*/
+		KindPrint/*  = 1*/
+		KindPrintf/* = 2*/
+		KindErrorf/* = 3*/
+	)`,
+		Run: constantValues,
+	},
+	RangeVariableTypes: {
+		Name: RangeVariableTypes,
+		Doc: `Enable/disable inlay hints for variable types in range statements:
+
+	for k/* int*/, v/* string/* := range []string{} {
+		fmt.Println(k, v)
+	}`,
+		Run: rangeVariableTypes,
+	},
+	CompositeLiteralTypes: {
+		Name: CompositeLiteralTypes,
+		Doc: `Enable/disable inlay hints for composite literal types:
+
+	for _, c := range []struct {
+		in, want string
+	}{
+		/*struct{ in string; want string }*/{"Hello, world", "dlrow ,olleH"},
+	}`,
+		Run: compositeLiteralTypes,
+	},
+	CompositeLiteralFieldNames: {
+		Name: CompositeLiteralFieldNames,
+		Doc: `Enable/disable inlay hints for composite literal field names:
+
+	{in: "Hello, world", want: "dlrow ,olleH"}`,
+		Run: compositeLiteralFields,
+	},
+	FunctionTypeParameters: {
+		Name: FunctionTypeParameters,
+		Doc: `Enable/disable inlay hints for implicit type parameters on generic functions:
+
+	myFoo/*[int, string]*/(1, "hello")`,
+		Run: funcTypeParams,
+	},
+}
+
 func InlayHint(ctx context.Context, snapshot Snapshot, fh FileHandle, _ protocol.Range) ([]protocol.InlayHint, error) {
 	ctx, done := event.Start(ctx, "source.InlayHint")
 	defer done()
@@ -32,38 +113,47 @@
 		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
+	}
+
 	tmap := lsppos.NewTokenMapper(pgf.Src, pgf.Tok)
 	info := pkg.GetTypesInfo()
 	q := Qualifier(pgf.File, pkg.GetTypes(), info)
 
 	var hints []protocol.InlayHint
 	ast.Inspect(pgf.File, func(node ast.Node) bool {
-		switch n := node.(type) {
-		case *ast.CallExpr:
-			hints = append(hints, parameterNames(n, tmap, info)...)
-			hints = append(hints, funcTypeParams(n, tmap, info)...)
-		case *ast.AssignStmt:
-			hints = append(hints, assignVariableTypes(n, tmap, info, &q)...)
-		case *ast.RangeStmt:
-			hints = append(hints, rangeVariableTypes(n, tmap, info, &q)...)
-		case *ast.GenDecl:
-			hints = append(hints, constantValues(n, tmap, info)...)
-		case *ast.CompositeLit:
-			hints = append(hints, compositeLiterals(n, tmap, info, &q)...)
+		for _, fn := range enabledHints {
+			hints = append(hints, fn(node, tmap, info, &q)...)
 		}
 		return true
 	})
 	return hints, nil
 }
 
-func parameterNames(node *ast.CallExpr, tmap *lsppos.TokenMapper, info *types.Info) []protocol.InlayHint {
-	signature, ok := info.TypeOf(node.Fun).(*types.Signature)
+func parameterNames(node ast.Node, tmap *lsppos.TokenMapper, 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 node.Args {
+	for i, v := range callExpr.Args {
 		start, ok := tmap.Position(v.Pos())
 		if !ok {
 			continue
@@ -92,8 +182,12 @@
 	return hints
 }
 
-func funcTypeParams(node *ast.CallExpr, tmap *lsppos.TokenMapper, info *types.Info) []protocol.InlayHint {
-	id, ok := node.Fun.(*ast.Ident)
+func funcTypeParams(node ast.Node, tmap *lsppos.TokenMapper, 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
 	}
@@ -119,12 +213,14 @@
 	}}
 }
 
-func assignVariableTypes(node *ast.AssignStmt, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint {
-	if node.Tok != token.DEFINE {
+func assignVariableTypes(node ast.Node, tmap *lsppos.TokenMapper, 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 node.Lhs {
+	for _, v := range stmt.Lhs {
 		if h := variableType(v, tmap, info, q); h != nil {
 			hints = append(hints, *h)
 		}
@@ -132,12 +228,16 @@
 	return hints
 }
 
-func rangeVariableTypes(node *ast.RangeStmt, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint {
+func rangeVariableTypes(node ast.Node, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint {
+	rStmt, ok := node.(*ast.RangeStmt)
+	if !ok {
+		return nil
+	}
 	var hints []protocol.InlayHint
-	if h := variableType(node.Key, tmap, info, q); h != nil {
+	if h := variableType(rStmt.Key, tmap, info, q); h != nil {
 		hints = append(hints, *h)
 	}
-	if h := variableType(node.Value, tmap, info, q); h != nil {
+	if h := variableType(rStmt.Value, tmap, info, q); h != nil {
 		hints = append(hints, *h)
 	}
 	return hints
@@ -160,13 +260,14 @@
 	}
 }
 
-func constantValues(node *ast.GenDecl, tmap *lsppos.TokenMapper, info *types.Info) []protocol.InlayHint {
-	if node.Tok != token.CONST {
+func constantValues(node ast.Node, tmap *lsppos.TokenMapper, 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 node.Specs {
+	for _, v := range genDecl.Specs {
 		spec, ok := v.(*ast.ValueSpec)
 		if !ok {
 			continue
@@ -210,36 +311,26 @@
 	return hints
 }
 
-func compositeLiterals(node *ast.CompositeLit, tmap *lsppos.TokenMapper, info *types.Info, q *types.Qualifier) []protocol.InlayHint {
-	typ := info.TypeOf(node)
+func compositeLiteralFields(node ast.Node, tmap *lsppos.TokenMapper, 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
 	}
-
-	prefix := ""
 	if t, ok := typ.(*types.Pointer); ok {
 		typ = t.Elem()
-		prefix = "&"
 	}
-
 	strct, ok := typ.Underlying().(*types.Struct)
 	if !ok {
 		return nil
 	}
 
 	var hints []protocol.InlayHint
-	if node.Type == nil {
-		// The type for this struct is implicit, add an inlay hint.
-		if start, ok := tmap.Position(node.Lbrace); ok {
-			hints = append(hints, protocol.InlayHint{
-				Position: &start,
-				Label:    buildLabel(fmt.Sprintf("%s%s", prefix, types.TypeString(typ, *q))),
-				Kind:     protocol.Type,
-			})
-		}
-	}
 
-	for i, v := range node.Elts {
+	for i, v := range compLit.Elts {
 		if _, ok := v.(*ast.KeyValueExpr); !ok {
 			start, ok := tmap.Position(v.Pos())
 			if !ok {
@@ -259,6 +350,35 @@
 	return hints
 }
 
+func compositeLiteralTypes(node ast.Node, tmap *lsppos.TokenMapper, 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, ok := tmap.Position(compLit.Lbrace)
+	if !ok {
+		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,
diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go
index d1d34ef..5da14eb 100644
--- a/internal/lsp/source/options.go
+++ b/internal/lsp/source/options.go
@@ -130,6 +130,7 @@
 							Nil:    true,
 						},
 					},
+					InlayHintOptions: InlayHintOptions{},
 					DocumentationOptions: DocumentationOptions{
 						HoverKind:    FullDocumentation,
 						LinkTarget:   "pkg.go.dev",
@@ -289,6 +290,7 @@
 	CompletionOptions
 	NavigationOptions
 	DiagnosticOptions
+	InlayHintOptions
 
 	// Codelenses overrides the enabled/disabled state of code lenses. See the
 	// "Code Lenses" section of the
@@ -407,6 +409,13 @@
 	ExperimentalWatchedFileDelay time.Duration `status:"experimental"`
 }
 
+type InlayHintOptions struct {
+	// Hints specify inlay hints that users want to see.
+	// A full list of hints that gopls uses can be found
+	// [here](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md).
+	Hints map[string]bool `status:"experimental"`
+}
+
 type NavigationOptions struct {
 	// ImportShortcut specifies whether import statements should link to
 	// documentation or go to definitions.
@@ -915,6 +924,9 @@
 	case "analyses":
 		result.setBoolMap(&o.Analyses)
 
+	case "hints":
+		result.setBoolMap(&o.Hints)
+
 	case "annotations":
 		result.setAnnotationMap(&o.Annotations)
 
@@ -1351,6 +1363,7 @@
 	Commands  []*CommandJSON
 	Lenses    []*LensJSON
 	Analyzers []*AnalyzerJSON
+	Hints     []*HintJSON
 }
 
 type OptionJSON struct {
@@ -1416,12 +1429,8 @@
 }
 
 func shouldShowEnumKeysInSettings(name string) bool {
-	// Both of these fields have too many possible options to print.
-	return !hardcodedEnumKeys(name)
-}
-
-func hardcodedEnumKeys(name string) bool {
-	return name == "analyses" || name == "codelenses"
+	// These fields have too many possible options to print.
+	return !(name == "analyses" || name == "codelenses" || name == "hints")
 }
 
 type EnumKeys struct {
@@ -1489,3 +1498,17 @@
 func (a *AnalyzerJSON) Write(w io.Writer) {
 	fmt.Fprintf(w, "%s (%s): %v", a.Name, a.Doc, a.Default)
 }
+
+type HintJSON struct {
+	Name    string
+	Doc     string
+	Default bool
+}
+
+func (h *HintJSON) String() string {
+	return h.Name
+}
+
+func (h *HintJSON) Write(w io.Writer) {
+	fmt.Fprintf(w, "%s (%s): %v", h.Name, h.Doc, h.Default)
+}