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) -}