tools/goplssetting: command to autogenerate gopls settings
The tool runs `gopls api-json`, converts the result to vscode setting
formats, and updates vscode-go package.json.
Note: `gopls api-json` requires gopls v0.5.2+.
For golang/vscode-go#197
Change-Id: Idfca4e899d658b1647d6fedde4b3cb0343eeae30
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/265741
Trust: Hyang-Ah Hana Kim <hyangah@gmail.com>
Reviewed-by: Suzy Mueller <suzmue@golang.org>
diff --git a/tools/goplssetting/main.go b/tools/goplssetting/main.go
new file mode 100644
index 0000000..5ce729b
--- /dev/null
+++ b/tools/goplssetting/main.go
@@ -0,0 +1,279 @@
+// 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 emoji := sectionEmoji(v.section); emoji != "" {
+ v.OptionJSON.Doc = emoji + " " + 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 sectionEmoji(section string) string {
+ switch section {
+ case "Experimental":
+ return "๐งช"
+ case "Debugging":
+ return "๐"
+ }
+ 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(`{`)
+ for i, o := range options {
+ line(` "gopls.%v" : {`, o.Name)
+
+ typ := propertyType(o.Type)
+ line(` "type": %q,`, typ)
+ // TODO: consider 'additionalProperties' if gopls api-json outputs acceptable peoperties.
+
+ line(` "markdownDescription": %q,`, o.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
+
+ if i == len(options)-1 {
+ line(` }`)
+ } else {
+ line(` },`)
+ }
+ }
+ line(`}`)
+}
+
+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
+}
diff --git a/tools/goplssetting/main_test.go b/tools/goplssetting/main_test.go
new file mode 100644
index 0000000..368a693
--- /dev/null
+++ b/tools/goplssetting/main_test.go
@@ -0,0 +1,135 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Licensed under the MIT License.
+// See LICENSE in the project root for license information.
+
+package main
+
+import (
+ "bytes"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+func TestRun(t *testing.T) {
+ if _, err := exec.LookPath("gopls"); err != nil {
+ t.Skipf("gopls is not found (%v), skipping...", err)
+ }
+ if _, err := exec.LookPath("jq"); err != nil {
+ t.Skipf("jq is not found (%v), skipping...", err)
+ }
+ testfile := filepath.Join("..", "..", "package.json")
+ got, err := run(testfile)
+ if err != nil {
+ t.Fatalf("run failed: %v", err)
+ }
+ t.Logf("%s", got)
+}
+
+func TestWriteAsVSCodeSettings(t *testing.T) {
+ if _, err := exec.LookPath("jq"); err != nil {
+ t.Skipf("jq is not found (%v), skipping...", err)
+ }
+ testCases := []struct {
+ name string
+ in *OptionJSON
+ out string
+ }{
+ {
+ name: "boolean",
+ in: &OptionJSON{
+ Name: "verboseOutput",
+ Type: "bool",
+ Doc: "verboseOutput enables additional debug logging.\n",
+ Default: "false",
+ },
+ out: `"gopls.verboseOutput": {
+ "type": "boolean",
+ "markdownDescription": "verboseOutput enables additional debug logging.\n",
+ "default": false,
+ "scope": "resource"
+ }`,
+ },
+ {
+ name: "time",
+ in: &OptionJSON{
+ Name: "completionBudget",
+ Type: "time.Duration",
+ Default: "\"100ms\"",
+ },
+ out: `"gopls.completionBudget": {
+ "type": "string",
+ "markdownDescription": "",
+ "default": "100ms",
+ "scope": "resource"
+ }`,
+ },
+ {
+ name: "map",
+ in: &OptionJSON{
+ Name: "analyses",
+ Type: "map[string]bool",
+ Default: "{}",
+ },
+ out: `"gopls.analyses":{
+ "type": "object",
+ "markdownDescription": "",
+ "default": {},
+ "scope": "resource"
+ }`,
+ },
+ {
+ name: "enum",
+ in: &OptionJSON{
+ Name: "matcher",
+ Type: "enum",
+ EnumValues: []EnumValue{
+ {
+ Value: "\"CaseInsensitive\"",
+ Doc: "",
+ },
+ {
+ Value: "\"CaseSensitive\"",
+ Doc: "",
+ },
+ {
+ Value: "\"Fuzzy\"",
+ Doc: "",
+ },
+ },
+ Default: "\"Fuzzy\"",
+ },
+ out: `"gopls.matcher": {
+ "type": "string",
+ "markdownDescription": "",
+ "enum": [ "CaseInsensitive", "CaseSensitive", "Fuzzy" ],
+ "markdownEnumDescriptions": [ "","","" ],
+ "default": "Fuzzy",
+ "scope": "resource"
+ }`,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ options := []*OptionJSON{tc.in}
+ buf := &bytes.Buffer{}
+ writeAsVSCodeSettings(buf, options)
+ if got, want := normalize(t, buf.String()), normalize(t, "{ "+tc.out+" }"); got != want {
+ t.Errorf("writeAsVSCodeSettings = %v, want %v", got, want)
+ }
+ })
+ }
+}
+
+func normalize(t *testing.T, in string) string {
+ t.Helper()
+ cmd := exec.Command("jq")
+ cmd.Stdin = strings.NewReader(in)
+ out, err := cmd.Output()
+ if err != nil {
+ t.Fatalf("failed to run jq: %v", err)
+ }
+ return string(out)
+}