diff --git a/extension/tools/generate.go b/extension/tools/generate.go
index 086babe6d5..7078d1605a 100644
--- a/extension/tools/generate.go
+++ b/extension/tools/generate.go
@@ -495,9 +495,18 @@ func enumDescriptionsSnippet(p *Property) string {
if hasDesc && len(desc) == len(p.Enum) {
b.WriteString("\n\n")
for i, e := range p.Enum {
- fmt.Fprintf(b, "* `%v`", e)
- if d := desc[i]; d != "" {
- fmt.Fprintf(b, ": %v", strings.TrimRight(strings.ReplaceAll(d, "\n\n", "
"), "\n"))
+ enumName := fmt.Sprintf("`%v`", e)
+ if d := desc[i]; d == "" {
+ fmt.Fprintf(b, "* %v", enumName)
+ } else {
+ enumDesc := strings.TrimRight(strings.ReplaceAll(d, "\n\n", "
"), "\n")
+ if strings.HasPrefix(d, enumName+":") {
+ // gopls's enum descriptions are sometimes already formatted
+ // like `name: description` format. Remove the duplicate prefix.
+ fmt.Fprintf(b, "* %v", enumDesc)
+ } else {
+ fmt.Fprintf(b, "* %v: %v", enumName, enumDesc)
+ }
}
b.WriteString("\n")
}
@@ -644,7 +653,7 @@ func describeDebugProperty(p *Property) string {
if p.MarkdownDescription != "" {
desc = p.MarkdownDescription
}
- if p == nil || strings.Contains(desc, "Not applicable when using `dlv-dap` mode.") {
+ if strings.Contains(desc, "Not applicable when using `dlv-dap` mode.") {
return ""
}
diff --git a/extension/tools/goplssetting/goplssetting.go b/extension/tools/goplssetting/goplssetting.go
index 6c13812213..0284bee24e 100644
--- a/extension/tools/goplssetting/goplssetting.go
+++ b/extension/tools/goplssetting/goplssetting.go
@@ -10,8 +10,10 @@ import (
"fmt"
"io/ioutil"
"log"
+ "maps"
"os"
"os/exec"
+ "slices"
"sort"
"strconv"
"strings"
@@ -47,7 +49,7 @@ func Generate(inputFile string, skipCleanup bool) ([]byte, error) {
if err != nil {
return nil, err
}
- b, err := asVSCodeSettings(options)
+ b, err := asVSCodeSettingsJSON(options)
if err != nil {
return nil, err
}
@@ -169,9 +171,17 @@ func rewritePackageJSON(newSettings, inFile string) ([]byte, error) {
return bytes.TrimSpace(stdout.Bytes()), nil
}
-// asVSCodeSettings converts the given options to match the VS Code settings
+// asVSCodeSettingsJSON converts the given options to match the VS Code settings
// format.
-func asVSCodeSettings(options []*Option) ([]byte, error) {
+func asVSCodeSettingsJSON(options []*Option) ([]byte, error) {
+ obj, err := asVSCodeSettings(options)
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(obj)
+}
+
+func asVSCodeSettings(options []*Option) (map[string]*Object, error) {
seen := map[string][]*Option{}
for _, opt := range options {
seen[opt.Hierarchy] = append(seen[opt.Hierarchy], opt)
@@ -192,7 +202,7 @@ func asVSCodeSettings(options []*Option) ([]byte, error) {
AdditionalProperties: false,
Properties: goplsProperties,
}
- return json.Marshal(goProperties)
+ return goProperties, nil
}
func collectProperties(m map[string][]*Option) (goplsProperties, goProperties map[string]*Object, err error) {
@@ -261,18 +271,38 @@ func toObject(opt *Option) (*Object, error) {
// outputs acceptable properties.
// TODO: deprecation attribute
}
- // Handle any enum types.
- if opt.Type == "enum" {
+ if opt.Type != "enum" {
+ obj.Type = propertyType(opt.Type)
+ } else { // Map enum type to a sum type.
+ // Assume value type is bool | string.
+ seenTypes := map[string]bool{}
for _, v := range opt.EnumValues {
- unquotedName, err := strconv.Unquote(v.Value)
- if err != nil {
- return nil, err
+ // EnumValue.Value: string in JSON syntax (quoted)
+ var x any
+ if err := json.Unmarshal([]byte(v.Value), &x); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal %q: %v", v.Value, err)
+ }
+
+ switch t := x.(type) {
+ case string:
+ obj.Enum = append(obj.Enum, t)
+ seenTypes["string"] = true
+ case bool:
+ obj.Enum = append(obj.Enum, t)
+ seenTypes["bool"] = true
+ default:
+ panic(fmt.Sprintf("type %T %+v as enum value type is not supported", t, t))
}
- obj.Enum = append(obj.Enum, unquotedName)
obj.MarkdownEnumDescriptions = append(obj.MarkdownEnumDescriptions, v.Doc)
}
- }
- // Handle any objects whose keys are enums.
+ obj.Type = propertyType(slices.Sorted(maps.Keys(seenTypes))...)
+ }
+ // Handle objects whose keys are listed in EnumKeys.
+ // Gopls uses either enum or string as key types, for example,
+ // map[string]bool: analyses
+ // map[enum]bool: codelenses, annotations
+ // Both cases where 'enum' is used as a key type actually use
+ // only string type enum. For simplicity, map all to string-keyed objects.
if len(opt.EnumKeys.Keys) > 0 {
if obj.Properties == nil {
obj.Properties = map[string]*Object{}
@@ -289,7 +319,6 @@ func toObject(opt *Option) (*Object, error) {
}
}
}
- obj.Type = propertyType(opt.Type)
obj.Default = formatOptionDefault(opt)
return obj, nil
@@ -337,14 +366,27 @@ var associatedToExtensionProperties = map[string][]string{
"buildFlags": {"go.buildFlags", "go.buildTags"},
}
-func propertyType(t string) string {
+func propertyType(typs ...string) any /* string or []string */ {
+ if len(typs) == 0 {
+ panic("unexpected: len(typs) == 0")
+ }
+ if len(typs) == 1 {
+ return mapType(typs[0])
+ }
+
+ var ret []string
+ for _, t := range typs {
+ ret = append(ret, mapType(t))
+ }
+ return ret
+}
+
+func mapType(t string) string {
switch t {
case "string":
return "string"
case "bool":
return "boolean"
- case "enum":
- return "string"
case "time.Duration":
return "string"
case "[]string":
@@ -352,27 +394,18 @@ func propertyType(t string) string {
case "map[string]string", "map[string]bool", "map[enum]string", "map[enum]bool":
return "object"
case "any":
- return "boolean" // TODO(hyangah): change to "" after https://go.dev/cl/593656 is released.
+ return "boolean"
}
log.Fatalf("unknown type %q", t)
return ""
}
-func check(err error) {
- if err == nil {
- return
- }
-
- log.Output(1, err.Error())
- os.Exit(1)
-}
-
// Object represents a VS Code settings object.
type Object struct {
- Type string `json:"type,omitempty"`
+ Type any `json:"type,omitempty"` // string | []string
MarkdownDescription string `json:"markdownDescription,omitempty"`
AdditionalProperties bool `json:"additionalProperties,omitempty"`
- Enum []string `json:"enum,omitempty"`
+ Enum []any `json:"enum,omitempty"`
MarkdownEnumDescriptions []string `json:"markdownEnumDescriptions,omitempty"`
Default interface{} `json:"default,omitempty"`
Scope string `json:"scope,omitempty"`
@@ -395,6 +428,7 @@ type API struct {
Options map[string][]*Option
Lenses []*Lens
Analyzers []*Analyzer
+ Hints []*Hint
}
type Option struct {
diff --git a/extension/tools/goplssetting/goplssetting_test.go b/extension/tools/goplssetting/goplssetting_test.go
index 0a35e4a5cc..5811d1a7d8 100644
--- a/extension/tools/goplssetting/goplssetting_test.go
+++ b/extension/tools/goplssetting/goplssetting_test.go
@@ -5,11 +5,11 @@
package goplssetting
import (
- "bytes"
"os/exec"
"path/filepath"
- "strings"
"testing"
+
+ "github.com/google/go-cmp/cmp"
)
func TestRun(t *testing.T) {
@@ -34,7 +34,7 @@ func TestWriteAsVSCodeSettings(t *testing.T) {
testCases := []struct {
name string
in *Option
- out string
+ want map[string]*Object
}{
{
name: "boolean",
@@ -44,12 +44,14 @@ func TestWriteAsVSCodeSettings(t *testing.T) {
Doc: "verboseOutput enables additional debug logging.\n",
Default: "false",
},
- out: `"verboseOutput": {
- "type": "boolean",
- "markdownDescription": "verboseOutput enables additional debug logging.\n",
- "default": false,
- "scope": "resource"
- }`,
+ want: map[string]*Object{
+ "verboseOutput": {
+ Type: "boolean",
+ MarkdownDescription: "verboseOutput enables additional debug logging.\n",
+ Default: false,
+ Scope: "resource",
+ },
+ },
},
{
name: "time",
@@ -58,11 +60,13 @@ func TestWriteAsVSCodeSettings(t *testing.T) {
Type: "time.Duration",
Default: "\"100ms\"",
},
- out: `"completionBudget": {
- "type": "string",
- "default": "100ms",
- "scope": "resource"
- }`,
+ want: map[string]*Object{
+ "completionBudget": {
+ Type: "string",
+ Default: "100ms",
+ Scope: "resource",
+ },
+ },
},
{
name: "map",
@@ -71,10 +75,12 @@ func TestWriteAsVSCodeSettings(t *testing.T) {
Type: "map[string]bool",
Default: "{}",
},
- out: `"analyses":{
- "type": "object",
- "scope": "resource"
- }`,
+ want: map[string]*Object{
+ "analyses": {
+ Type: "object",
+ Scope: "resource",
+ },
+ },
},
{
name: "enum",
@@ -97,13 +103,46 @@ func TestWriteAsVSCodeSettings(t *testing.T) {
},
Default: "\"Fuzzy\"",
},
- out: `"matcher": {
- "type": "string",
- "enum": [ "CaseInsensitive", "CaseSensitive", "Fuzzy" ],
- "markdownEnumDescriptions": [ "","","" ],
- "default": "Fuzzy",
- "scope": "resource"
- }`,
+ want: map[string]*Object{
+ "matcher": {
+ Type: "string",
+ Enum: []any{"CaseInsensitive", "CaseSensitive", "Fuzzy"},
+ MarkdownEnumDescriptions: []string{"", "", ""},
+ Default: "Fuzzy",
+ Scope: "resource",
+ },
+ },
+ },
+ {
+ name: "mixedEnum",
+ in: &Option{
+ Name: "linksInHover",
+ Type: "enum",
+ EnumValues: []EnumValue{
+ {
+ Value: "false",
+ Doc: "`false`: ...",
+ },
+ {
+ Value: "true",
+ Doc: "`true`: ...",
+ },
+ {
+ Value: "\"gopls\"",
+ Doc: "`\"gopls\"`: ...",
+ },
+ },
+ Default: "true",
+ },
+ want: map[string]*Object{
+ "linksInHover": {
+ Type: []string{"boolean", "string"},
+ Enum: []any{false, true, "gopls"},
+ MarkdownEnumDescriptions: []string{"`false`: ...", "`true`: ...", "`\"gopls\"`: ..."},
+ Scope: "resource",
+ Default: true,
+ },
+ },
},
{
name: "array",
@@ -112,48 +151,34 @@ func TestWriteAsVSCodeSettings(t *testing.T) {
Type: "[]string",
Default: "[\"-node_modules\", \"-vendor\"]",
},
- out: `"directoryFilters": {
- "type": "array",
- "default": ["-node_modules", "-vendor"],
- "scope": "resource"
- }`,
+ want: map[string]*Object{
+ "directoryFilters": {
+ Type: "array",
+ Default: []string{"-node_modules", "-vendor"},
+ Scope: "resource",
+ },
+ },
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
options := []*Option{tc.in}
- b, err := asVSCodeSettings(options)
+ got, err := asVSCodeSettings(options)
if err != nil {
t.Fatal(err)
}
- if got, want := normalize(t, string(b)), normalize(t, `
- {
+ want := map[string]*Object{
"gopls": {
- "type": "object",
- "markdownDescription": "Configure the default Go language server ('gopls'). In most cases, configuring this section is unnecessary. See [the documentation](https://github.com/golang/tools/blob/master/gopls/doc/settings.md) for all available settings.",
- "scope": "resource",
- "properties": {
- `+tc.out+`
- }
- }
- }`); got != want {
- t.Errorf("writeAsVSCodeSettings = %v, want %v", got, want)
+ Type: "object",
+ MarkdownDescription: "Configure the default Go language server ('gopls'). In most cases, configuring this section is unnecessary. See [the documentation](https://github.com/golang/tools/blob/master/gopls/doc/settings.md) for all available settings.",
+ Scope: "resource",
+ Properties: tc.want,
+ },
+ }
+ if diff := cmp.Diff(want, got); diff != "" {
+ t.Errorf("writeAsVSCodeSettings = %v; diff = %v", got, diff)
}
})
}
}
-
-func normalize(t *testing.T, in string) string {
- t.Helper()
- cmd := exec.Command("jq")
- cmd.Stdin = strings.NewReader(in)
- stderr := new(bytes.Buffer)
- cmd.Stderr = stderr
-
- out, err := cmd.Output()
- if err != nil {
- t.Fatalf("%s\n%s\nfailed to run jq: %v", in, stderr, err)
- }
- return string(out)
-}