| // 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 -in ./package.json -out ./package.json |
| package main |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "flag" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "log" |
| "os" |
| "os/exec" |
| "sort" |
| "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 |
| } |
| |
| f, err := ioutil.TempFile(workDir, "gopls.settings") |
| if err != nil { |
| return nil, err |
| } |
| |
| writeAsVSCodeSettings(f, options) |
| |
| 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 { |
| return priority(options[i].section) < priority(options[j].section) |
| }) |
| |
| opts := []*OptionJSON{} |
| for _, v := range options { |
| if name := sectionName(v.section); name != "" { |
| v.OptionJSON.Doc = name + " " + v.OptionJSON.Doc |
| } |
| opts = append(opts, v.OptionJSON) |
| } |
| return opts, nil |
| } |
| |
| func priority(section string) int { |
| switch section { |
| case "User": |
| return 0 |
| case "Experimental": |
| return 10 |
| case "Debugging": |
| return 100 |
| } |
| return 1000 |
| } |
| |
| func sectionName(section string) string { |
| switch section { |
| case "Experimental": |
| return "(Experimental)" |
| case "Debugging": |
| return "(For Debugging)" |
| } |
| return "" |
| } |
| |
| // 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 |
| } |
| |
| // convertToVSCodeSettings converts the options to the vscode setting format. |
| func writeAsVSCodeSettings(f io.Writer, options []*OptionJSON) { |
| line := func(format string, args ...interface{}) { |
| fmt.Fprintf(f, format, args...) |
| fmt.Fprintln(f) |
| } |
| |
| line(`{`) |
| line(`"gopls": {`) |
| line(` "type": "object",`) |
| line(` "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.",`) |
| line(` "scope": "resource",`) |
| line(` "additionalProperties": false,`) |
| line(` "properties": {`) |
| for i, o := range options { |
| line(` "%v" : {`, o.Name) |
| |
| typ := propertyType(o.Type) |
| line(` "type": %q,`, typ) |
| // TODO: consider 'additionalProperties' if gopls api-json outputs acceptable peoperties. |
| |
| doc := o.Doc |
| if mappedTo, ok := associatedToExtensionProperties[o.Name]; ok { |
| doc = fmt.Sprintf("%v\nIf unspecified, values of `%v` will be propagated.\n", doc, strings.Join(mappedTo, ", ")) |
| } |
| line(` "markdownDescription": %q,`, doc) |
| |
| var enums, enumDocs []string |
| for _, v := range o.EnumValues { |
| enums = append(enums, v.Value) |
| enumDocs = append(enumDocs, fmt.Sprintf("%q", v.Doc)) |
| } |
| if len(enums) > 0 { |
| line(` "enum": [%v],`, strings.Join(enums, ",")) |
| line(` "markdownEnumDescriptions": [%v],`, strings.Join(enumDocs, ",")) |
| } |
| |
| if len(o.Default) > 0 { |
| line(` "default": %v,`, o.Default) |
| } |
| |
| // TODO: are all gopls settings in the resource scope? |
| line(` "scope": "resource"`) |
| // TODO: deprecation attribute |
| |
| // "%v" : { |
| if i == len(options)-1 { |
| line(` }`) |
| } else { |
| line(` },`) |
| } |
| } |
| line(` }`) // "properties": { |
| line(` }`) // "gopls": { |
| line(`}`) // { |
| } |
| |
| var associatedToExtensionProperties = map[string][]string{ |
| "buildFlags": []string{"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) |
| } |
| |
| // 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 |
| } |
| |
| type OptionJSON struct { |
| Name string |
| Type string |
| Doc string |
| EnumValues []EnumValue |
| 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 |
| } |