blob: 3c9f331b55a7a0a2cc5e0c74dbbca035dc2f4b66 [file] [log] [blame]
// Copyright 2020 The Go Authors. All rights reserved.
// Licensed under the MIT License.
// See LICENSE in the project root for license information.
package goplssetting
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"sort"
"strconv"
"strings"
)
// Generate reads package.json and updates the gopls settings section
// based on `gopls api-json` output. This function requires `jq` to
// manipulate package.json.
func Generate(inputFile string, skipCleanup bool) ([]byte, error) {
if _, err := os.Stat(inputFile); err != nil {
return nil, err
}
if _, err := exec.LookPath("jq"); err != nil {
return nil, fmt.Errorf("missing `jq`: %w", err)
}
workDir, err := ioutil.TempDir("", "goplssettings")
if err != nil {
return nil, err
}
log.Printf("WORK=%v", workDir)
if !skipCleanup {
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(), inputFile)
}
// 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 := `.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
}