| // Copyright 2020 The Go Authors. All rights reserved. |
| // Licensed under the MIT License. |
| // See LICENSE in the project root for license information. |
| |
| // This command updates the gopls.* configurations in vscode-go package.json. |
| // |
| // Usage: from the project root directory, |
| // $ go run tools/goplssetting/main.go -in ./package.json -out ./package.json |
| package main |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "flag" |
| "fmt" |
| "io/ioutil" |
| "log" |
| "os" |
| "os/exec" |
| "sort" |
| "strconv" |
| "strings" |
| ) |
| |
| var ( |
| inPkgJSON = flag.String("in", "", "input package.json location") |
| outPkgJSON = flag.String("out", "", "output package.json location. If empty, output to the standard output.") |
| |
| work = flag.Bool("w", false, "if true, do not delete intermediate files") |
| ) |
| |
| func main() { |
| flag.Parse() |
| |
| if *inPkgJSON == "" { |
| log.Fatalf("-in file must be specified %q %q", *inPkgJSON, *outPkgJSON) |
| } |
| if _, err := os.Stat(*inPkgJSON); err != nil { |
| log.Fatalf("failed to find input package.json (%q): %v", *inPkgJSON, err) |
| } |
| |
| out, err := run(*inPkgJSON) |
| if err != nil { |
| log.Fatal(err) |
| } |
| if *outPkgJSON != "" { |
| if err := ioutil.WriteFile(*outPkgJSON, out, 0644); err != nil { |
| log.Fatalf("writing jq output to %q failed: %v", out, err) |
| } |
| } else { |
| fmt.Printf("%s", out) |
| } |
| } |
| |
| // run |
| func run(orgPkgJSON string) ([]byte, error) { |
| workDir, err := ioutil.TempDir("", "goplssettings") |
| if err != nil { |
| return nil, err |
| } |
| log.Printf("WORK=%v", workDir) |
| |
| if !*work { |
| defer os.RemoveAll(workDir) |
| } |
| |
| api, err := readGoplsAPI() |
| if err != nil { |
| return nil, err |
| } |
| options, err := extractOptions(api) |
| if err != nil { |
| return nil, err |
| } |
| b, err := asVSCodeSettings(options) |
| if err != nil { |
| return nil, err |
| } |
| f, err := ioutil.TempFile(workDir, "gopls.settings") |
| if err != nil { |
| return nil, err |
| } |
| if _, err := f.Write(b); err != nil { |
| return nil, err |
| } |
| if err := f.Close(); err != nil { |
| return nil, err |
| } |
| |
| return rewritePackageJSON(f.Name(), orgPkgJSON) |
| } |
| |
| // readGoplsAPI returns the output of `gopls api-json`. |
| func readGoplsAPI() (*APIJSON, error) { |
| version, err := exec.Command("gopls", "-v", "version").Output() |
| if err != nil { |
| return nil, fmt.Errorf("failed to check gopls version: %v", err) |
| } |
| log.Printf("Reading settings of gopls....\nversion:\n%s\n", version) |
| |
| out, err := exec.Command("gopls", "api-json").Output() |
| if err != nil { |
| return nil, fmt.Errorf("failed to run gopls: %v", err) |
| } |
| |
| api := &APIJSON{} |
| if err := json.Unmarshal(out, api); err != nil { |
| return nil, fmt.Errorf("failed to unmarshal: %v", err) |
| } |
| return api, nil |
| } |
| |
| // extractOptions extracts the options from APIJSON. |
| // It may rearrange the ordering and documentation for better presentation. |
| func extractOptions(api *APIJSON) ([]*OptionJSON, error) { |
| type sortableOptionJSON struct { |
| *OptionJSON |
| section string |
| } |
| options := []sortableOptionJSON{} |
| for k, v := range api.Options { |
| for _, o := range v { |
| options = append(options, sortableOptionJSON{OptionJSON: o, section: k}) |
| } |
| } |
| sort.SliceStable(options, func(i, j int) bool { |
| pi := priority(options[i].OptionJSON) |
| pj := priority(options[j].OptionJSON) |
| if pi == pj { |
| return options[i].Name < options[j].Name |
| } |
| return pi < pj |
| }) |
| |
| opts := []*OptionJSON{} |
| for _, v := range options { |
| if name := statusName(v.OptionJSON); name != "" { |
| v.OptionJSON.Doc = name + " " + v.OptionJSON.Doc |
| } |
| opts = append(opts, v.OptionJSON) |
| } |
| return opts, nil |
| } |
| |
| func priority(opt *OptionJSON) int { |
| switch toStatus(opt.Status) { |
| case Experimental: |
| return 10 |
| case Debug: |
| return 100 |
| } |
| return 1000 |
| } |
| |
| func statusName(opt *OptionJSON) string { |
| switch toStatus(opt.Status) { |
| case Experimental: |
| return "(Experimental)" |
| case Advanced: |
| return "(Advanced)" |
| case Debug: |
| return "(For Debugging)" |
| } |
| return "" |
| } |
| |
| func toStatus(s string) Status { |
| switch s { |
| case "experimental": |
| return Experimental |
| case "debug": |
| return Debug |
| case "advanced": |
| return Advanced |
| case "": |
| return None |
| default: |
| panic(fmt.Sprintf("unexpected status: %s", s)) |
| } |
| } |
| |
| // rewritePackageJSON rewrites the input package.json by running `jq` |
| // to update all existing gopls settings with the ones from the newSettings |
| // file. |
| func rewritePackageJSON(newSettings, inFile string) ([]byte, error) { |
| prog := `walk(if type == "object" then with_entries(select(.key | test("^gopls.[a-z]") | not)) else . end) | .contributes.configuration.properties *= $GOPLS_SETTINGS[0]` |
| |
| cmd := exec.Command("jq", "--slurpfile", "GOPLS_SETTINGS", newSettings, prog, inFile) |
| var stdout, stderr bytes.Buffer |
| cmd.Stdout = &stdout |
| cmd.Stderr = &stderr |
| if err := cmd.Run(); err != nil { |
| return nil, fmt.Errorf("jq run failed (%v): %s", err, &stderr) |
| } |
| return stdout.Bytes(), nil |
| } |
| |
| // asVSCodeSettings converts the given options to match the VS Code settings |
| // format. |
| func asVSCodeSettings(options []*OptionJSON) ([]byte, error) { |
| seen := map[string][]*OptionJSON{} |
| for _, opt := range options { |
| seen[opt.Hierarchy] = append(seen[opt.Hierarchy], opt) |
| } |
| for _, v := range seen { |
| sort.Slice(v, func(i, j int) bool { |
| return v[i].Name < v[j].Name |
| }) |
| } |
| properties, err := collectProperties(seen) |
| if err != nil { |
| return nil, err |
| } |
| return json.Marshal(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", |
| AdditionalProperties: false, |
| Properties: properties, |
| }, |
| }) |
| } |
| |
| func collectProperties(m map[string][]*OptionJSON) (map[string]*Object, error) { |
| var sorted []string |
| var containsEmpty bool |
| for k := range m { |
| if k == "" { |
| containsEmpty = true |
| continue |
| } |
| sorted = append(sorted, k) |
| } |
| sort.Strings(sorted) |
| if containsEmpty { |
| sorted = append(sorted, "") |
| } |
| properties := map[string]*Object{} |
| for _, hierarchy := range sorted { |
| for _, opt := range m[hierarchy] { |
| doc := opt.Doc |
| if mappedTo, ok := associatedToExtensionProperties[opt.Name]; ok { |
| doc = fmt.Sprintf("%v\nIf unspecified, values of `%v` will be propagated.\n", doc, strings.Join(mappedTo, ", ")) |
| } |
| obj := &Object{ |
| MarkdownDescription: doc, |
| // TODO: are all gopls settings in the resource scope? |
| Scope: "resource", |
| // TODO: consider 'additionalProperties' if gopls api-json |
| // outputs acceptable properties. |
| // TODO: deprecation attribute |
| } |
| // Handle any enum types. |
| if opt.Type == "enum" { |
| for _, v := range opt.EnumValues { |
| unquotedName, err := strconv.Unquote(v.Value) |
| if err != nil { |
| return nil, err |
| } |
| obj.Enum = append(obj.Enum, unquotedName) |
| obj.MarkdownEnumDescriptions = append(obj.MarkdownEnumDescriptions, v.Doc) |
| } |
| } |
| // Handle any objects whose keys are enums. |
| if len(opt.EnumKeys.Keys) > 0 { |
| if obj.Properties == nil { |
| obj.Properties = map[string]*Object{} |
| } |
| for _, k := range opt.EnumKeys.Keys { |
| unquotedName, err := strconv.Unquote(k.Name) |
| if err != nil { |
| return nil, err |
| } |
| obj.Properties[unquotedName] = &Object{ |
| Type: propertyType(opt.EnumKeys.ValueType), |
| MarkdownDescription: k.Doc, |
| Default: formatDefault(k.Default), |
| } |
| } |
| } |
| obj.Type = propertyType(opt.Type) |
| obj.Default = formatOptionDefault(opt) |
| |
| key := opt.Name |
| if hierarchy != "" { |
| key = hierarchy + "." + key |
| } |
| properties[key] = obj |
| } |
| } |
| return properties, nil |
| } |
| |
| func formatOptionDefault(opt *OptionJSON) interface{} { |
| // Each key will have its own default value, instead of one large global |
| // one. (Alternatively, we can build the default from the keys.) |
| if len(opt.EnumKeys.Keys) > 0 { |
| return nil |
| } |
| def := opt.Default |
| switch opt.Type { |
| case "enum", "string", "time.Duration": |
| unquote, err := strconv.Unquote(def) |
| if err == nil { |
| def = unquote |
| } |
| } |
| return formatDefault(def) |
| } |
| |
| // formatDefault converts a string-based default value to an actual value that |
| // can be marshaled to JSON. Right now, gopls generates default values as |
| // strings, but perhaps that will change. |
| func formatDefault(def string) interface{} { |
| switch def { |
| case "{}", "[]": |
| return nil |
| case "true": |
| return true |
| case "false": |
| return false |
| default: |
| return def |
| } |
| } |
| |
| var associatedToExtensionProperties = map[string][]string{ |
| "buildFlags": {"go.buildFlags", "go.buildTags"}, |
| } |
| |
| func propertyType(t string) string { |
| switch t { |
| case "string": |
| return "string" |
| case "bool": |
| return "boolean" |
| case "enum": |
| return "string" |
| case "time.Duration": |
| return "string" |
| case "[]string": |
| return "array" |
| case "map[string]string", "map[string]bool": |
| return "object" |
| } |
| 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"` |
| MarkdownDescription string `json:"markdownDescription,omitempty"` |
| AdditionalProperties bool `json:"additionalProperties,omitempty"` |
| Enum []string `json:"enum,omitempty"` |
| MarkdownEnumDescriptions []string `json:"markdownEnumDescriptions,omitempty"` |
| Default interface{} `json:"default,omitempty"` |
| Scope string `json:"scope,omitempty"` |
| Properties map[string]*Object `json:"properties,omitempty"` |
| } |
| |
| type Status int |
| |
| const ( |
| Experimental = Status(iota) |
| Debug |
| Advanced |
| None |
| ) |
| |
| // APIJSON is the output json type of `gopls api-json`. |
| // Types copied from golang.org/x/tools/internal/lsp/source/options.go. |
| type APIJSON struct { |
| Options map[string][]*OptionJSON |
| Commands []*CommandJSON |
| Lenses []*LensJSON |
| Analyzers []*AnalyzerJSON |
| } |
| |
| type OptionJSON struct { |
| Name string |
| Type string |
| Doc string |
| EnumKeys EnumKeys |
| EnumValues []EnumValue |
| Default string |
| Status string |
| Hierarchy string |
| } |
| |
| type EnumKeys struct { |
| ValueType string |
| Keys []EnumKey |
| } |
| |
| type EnumKey struct { |
| Name string |
| Doc string |
| Default string |
| } |
| |
| type EnumValue struct { |
| Value string |
| Doc string |
| } |
| |
| type CommandJSON struct { |
| Command string |
| Title string |
| Doc string |
| } |
| |
| type LensJSON struct { |
| Lens string |
| Title string |
| Doc string |
| } |
| |
| type AnalyzerJSON struct { |
| Name string |
| Doc string |
| Default bool |
| } |