Skip to content

Commit ee6d6af

Browse files
committed
internal/lsp: restructure user options (CL 278433 continued)
This CL copies Heschi's structural changes to the options from CL 278433 and makes the necessary adjustments in the JSON and documentation generation. Nested settings are grouped together and the "status" of a given setting is also listed. Currently the only possible statuses are "experimental" and "debug", but I will add "advanced" in a follow-up (to indicate that a setting is only for advanced users). The options "set" function still expects flattened settings to avoid fundamentally changing people's current configurations, so VS Code Go will just have to make sure to flatten the settings before sending them to gopls (which should be easy enough). No names of any settings are changed (Heschi's earlier CL adjusted the experimental prefixes). As discussed offline, we've decided to prefix any setting that we expect to delete with "experimental", and so we'll leave existing setting names as they are. Updates golang/go#43101 Change-Id: I55cf7ef09ce7b5b1f8af06fcadb4ba2a44ec9b17 Reviewed-on: https://go-review.googlesource.com/c/tools/+/280192 Trust: Rebecca Stambler <[email protected]> Run-TryBot: Rebecca Stambler <[email protected]> gopls-CI: kokoro <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Suzy Mueller <[email protected]>
1 parent 2152f4e commit ee6d6af

File tree

5 files changed

+793
-478
lines changed

5 files changed

+793
-478
lines changed

gopls/doc/generate.go

Lines changed: 185 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"go/format"
1515
"go/token"
1616
"go/types"
17+
"io"
1718
"io/ioutil"
1819
"os"
1920
"path/filepath"
@@ -22,6 +23,7 @@ import (
2223
"sort"
2324
"strings"
2425
"time"
26+
"unicode"
2527

2628
"github.com/sanity-io/litter"
2729
"golang.org/x/tools/go/ast/astutil"
@@ -75,16 +77,19 @@ func loadAPI() (*source.APIJSON, error) {
7577
Options: map[string][]*source.OptionJSON{},
7678
}
7779
defaults := source.DefaultOptions()
78-
for _, cat := range []reflect.Value{
79-
reflect.ValueOf(defaults.DebuggingOptions),
80+
for _, category := range []reflect.Value{
8081
reflect.ValueOf(defaults.UserOptions),
81-
reflect.ValueOf(defaults.ExperimentalOptions),
8282
} {
83-
opts, err := loadOptions(cat, pkg)
83+
// Find the type information and ast.File corresponding to the category.
84+
optsType := pkg.Types.Scope().Lookup(category.Type().Name())
85+
if optsType == nil {
86+
return nil, fmt.Errorf("could not find %v in scope %v", category.Type().Name(), pkg.Types.Scope())
87+
}
88+
opts, err := loadOptions(category, optsType, pkg, "")
8489
if err != nil {
8590
return nil, err
8691
}
87-
catName := strings.TrimSuffix(cat.Type().Name(), "Options")
92+
catName := strings.TrimSuffix(category.Type().Name(), "Options")
8893
api.Options[catName] = opts
8994
}
9095

@@ -109,13 +114,7 @@ func loadAPI() (*source.APIJSON, error) {
109114
return api, nil
110115
}
111116

112-
func loadOptions(category reflect.Value, pkg *packages.Package) ([]*source.OptionJSON, error) {
113-
// Find the type information and ast.File corresponding to the category.
114-
optsType := pkg.Types.Scope().Lookup(category.Type().Name())
115-
if optsType == nil {
116-
return nil, fmt.Errorf("could not find %v in scope %v", category.Type().Name(), pkg.Types.Scope())
117-
}
118-
117+
func loadOptions(category reflect.Value, optsType types.Object, pkg *packages.Package, hierarchy string) ([]*source.OptionJSON, error) {
119118
file, err := fileForPos(pkg, optsType.Pos())
120119
if err != nil {
121120
return nil, err
@@ -131,6 +130,21 @@ func loadOptions(category reflect.Value, pkg *packages.Package) ([]*source.Optio
131130
for i := 0; i < optsStruct.NumFields(); i++ {
132131
// The types field gives us the type.
133132
typesField := optsStruct.Field(i)
133+
134+
// If the field name ends with "Options", assume it is a struct with
135+
// additional options and process it recursively.
136+
if h := strings.TrimSuffix(typesField.Name(), "Options"); h != typesField.Name() {
137+
// Keep track of the parent structs.
138+
if hierarchy != "" {
139+
h = hierarchy + "." + h
140+
}
141+
options, err := loadOptions(category, typesField, pkg, strings.ToLower(h))
142+
if err != nil {
143+
return nil, err
144+
}
145+
opts = append(opts, options...)
146+
continue
147+
}
134148
path, _ := astutil.PathEnclosingInterval(file, typesField.Pos(), typesField.Pos())
135149
if len(path) < 2 {
136150
return nil, fmt.Errorf("could not find AST node for field %v", typesField)
@@ -183,13 +197,21 @@ func loadOptions(category reflect.Value, pkg *packages.Package) ([]*source.Optio
183197
typ = strings.Replace(typ, m.Key().String(), m.Key().Underlying().String(), 1)
184198
}
185199
}
200+
// Get the status of the field by checking its struct tags.
201+
reflectStructField, ok := category.Type().FieldByName(typesField.Name())
202+
if !ok {
203+
return nil, fmt.Errorf("no struct field for %s", typesField.Name())
204+
}
205+
status := reflectStructField.Tag.Get("status")
186206

187207
opts = append(opts, &source.OptionJSON{
188208
Name: lowerFirst(typesField.Name()),
189209
Type: typ,
190210
Doc: lowerFirst(astField.Doc.Text()),
191211
Default: string(defBytes),
192212
EnumValues: enumValues,
213+
Status: status,
214+
Hierarchy: hierarchy,
193215
})
194216
}
195217
return opts, nil
@@ -411,34 +433,39 @@ func rewriteAPI(input []byte, api *source.APIJSON) ([]byte, error) {
411433

412434
var parBreakRE = regexp.MustCompile("\n{2,}")
413435

436+
type optionsGroup struct {
437+
title string
438+
final string
439+
level int
440+
options []*source.OptionJSON
441+
}
442+
414443
func rewriteSettings(doc []byte, api *source.APIJSON) ([]byte, error) {
415444
result := doc
416445
for category, opts := range api.Options {
446+
groups := collectGroups(opts)
447+
448+
// First, print a table of contents.
417449
section := bytes.NewBuffer(nil)
418-
for _, opt := range opts {
419-
var enumValues strings.Builder
420-
if len(opt.EnumValues) > 0 {
421-
var msg string
422-
if opt.Type == "enum" {
423-
msg = "\nMust be one of:\n\n"
424-
} else {
425-
msg = "\nCan contain any of:\n\n"
426-
}
427-
enumValues.WriteString(msg)
428-
for i, val := range opt.EnumValues {
429-
if val.Doc != "" {
430-
// Don't break the list item by starting a new paragraph.
431-
unbroken := parBreakRE.ReplaceAllString(val.Doc, "\\\n")
432-
fmt.Fprintf(&enumValues, "* %s", unbroken)
433-
} else {
434-
fmt.Fprintf(&enumValues, "* `%s`", val.Value)
435-
}
436-
if i < len(opt.EnumValues)-1 {
437-
fmt.Fprint(&enumValues, "\n")
438-
}
439-
}
450+
fmt.Fprintln(section, "")
451+
for _, h := range groups {
452+
writeBullet(section, h.final, h.level)
453+
}
454+
fmt.Fprintln(section, "")
455+
456+
// Currently, the settings document has a title and a subtitle, so
457+
// start at level 3 for a header beginning with "###".
458+
baseLevel := 3
459+
for _, h := range groups {
460+
level := baseLevel + h.level
461+
writeTitle(section, h.final, level)
462+
for _, opt := range h.options {
463+
header := strMultiply("#", level+1)
464+
fmt.Fprintf(section, "%s **%v** *%v*\n\n", header, opt.Name, opt.Type)
465+
writeStatus(section, opt.Status)
466+
enumValues := collectEnumValues(opt)
467+
fmt.Fprintf(section, "%v%v\nDefault: `%v`.\n\n", opt.Doc, enumValues, opt.Default)
440468
}
441-
fmt.Fprintf(section, "### **%v** *%v*\n%v%v\n\nDefault: `%v`.\n", opt.Name, opt.Type, opt.Doc, enumValues.String(), opt.Default)
442469
}
443470
var err error
444471
result, err = replaceSection(result, category, section.Bytes())
@@ -449,11 +476,133 @@ func rewriteSettings(doc []byte, api *source.APIJSON) ([]byte, error) {
449476

450477
section := bytes.NewBuffer(nil)
451478
for _, lens := range api.Lenses {
452-
fmt.Fprintf(section, "### **%v**\nIdentifier: `%v`\n\n%v\n\n", lens.Title, lens.Lens, lens.Doc)
479+
fmt.Fprintf(section, "### **%v**\n\nIdentifier: `%v`\n\n%v\n", lens.Title, lens.Lens, lens.Doc)
453480
}
454481
return replaceSection(result, "Lenses", section.Bytes())
455482
}
456483

484+
func collectGroups(opts []*source.OptionJSON) []optionsGroup {
485+
optsByHierarchy := map[string][]*source.OptionJSON{}
486+
for _, opt := range opts {
487+
optsByHierarchy[opt.Hierarchy] = append(optsByHierarchy[opt.Hierarchy], opt)
488+
}
489+
490+
// As a hack, assume that uncategorized items are less important to
491+
// users and force the empty string to the end of the list.
492+
var containsEmpty bool
493+
var sorted []string
494+
for h := range optsByHierarchy {
495+
if h == "" {
496+
containsEmpty = true
497+
continue
498+
}
499+
sorted = append(sorted, h)
500+
}
501+
sort.Strings(sorted)
502+
if containsEmpty {
503+
sorted = append(sorted, "")
504+
}
505+
var groups []optionsGroup
506+
baseLevel := 0
507+
for _, h := range sorted {
508+
split := strings.SplitAfter(h, ".")
509+
last := split[len(split)-1]
510+
// Hack to capitalize all of UI.
511+
if last == "ui" {
512+
last = "UI"
513+
}
514+
// A hierarchy may look like "ui.formatting". If "ui" has no
515+
// options of its own, it may not be added to the map, but it
516+
// still needs a heading.
517+
components := strings.Split(h, ".")
518+
for i := 1; i < len(components); i++ {
519+
parent := strings.Join(components[0:i], ".")
520+
if _, ok := optsByHierarchy[parent]; !ok {
521+
groups = append(groups, optionsGroup{
522+
title: parent,
523+
final: last,
524+
level: baseLevel + i,
525+
})
526+
}
527+
}
528+
groups = append(groups, optionsGroup{
529+
title: h,
530+
final: last,
531+
level: baseLevel + strings.Count(h, "."),
532+
options: optsByHierarchy[h],
533+
})
534+
}
535+
return groups
536+
}
537+
538+
func collectEnumValues(opt *source.OptionJSON) string {
539+
var enumValues strings.Builder
540+
if len(opt.EnumValues) > 0 {
541+
var msg string
542+
if opt.Type == "enum" {
543+
msg = "\nMust be one of:\n\n"
544+
} else {
545+
msg = "\nCan contain any of:\n\n"
546+
}
547+
enumValues.WriteString(msg)
548+
for i, val := range opt.EnumValues {
549+
if val.Doc != "" {
550+
unbroken := parBreakRE.ReplaceAllString(val.Doc, "\\\n")
551+
fmt.Fprintf(&enumValues, "* %s", unbroken)
552+
} else {
553+
fmt.Fprintf(&enumValues, "* `%s`", val.Value)
554+
}
555+
if i < len(opt.EnumValues)-1 {
556+
fmt.Fprint(&enumValues, "\n")
557+
}
558+
}
559+
}
560+
return enumValues.String()
561+
}
562+
563+
func writeBullet(w io.Writer, title string, level int) {
564+
if title == "" {
565+
return
566+
}
567+
// Capitalize the first letter of each title.
568+
prefix := strMultiply(" ", level)
569+
fmt.Fprintf(w, "%s* [%s](#%s)\n", prefix, capitalize(title), strings.ToLower(title))
570+
}
571+
572+
func writeTitle(w io.Writer, title string, level int) {
573+
if title == "" {
574+
return
575+
}
576+
// Capitalize the first letter of each title.
577+
fmt.Fprintf(w, "%s %s\n\n", strMultiply("#", level), capitalize(title))
578+
}
579+
580+
func writeStatus(section io.Writer, status string) {
581+
switch status {
582+
case "":
583+
case "advanced":
584+
fmt.Fprint(section, "**This is an advanced setting and should not be configured by most `gopls` users.**\n\n")
585+
case "debug":
586+
fmt.Fprint(section, "**This setting is for debugging purposes only.**\n\n")
587+
case "experimental":
588+
fmt.Fprint(section, "**This setting is experimental and may be deleted.**\n\n")
589+
default:
590+
fmt.Fprintf(section, "**Status: %s.**\n\n", status)
591+
}
592+
}
593+
594+
func capitalize(s string) string {
595+
return string(unicode.ToUpper(rune(s[0]))) + s[1:]
596+
}
597+
598+
func strMultiply(str string, count int) string {
599+
var result string
600+
for i := 0; i < count; i++ {
601+
result += string(str)
602+
}
603+
return result
604+
}
605+
457606
func rewriteCommands(doc []byte, api *source.APIJSON) ([]byte, error) {
458607
section := bytes.NewBuffer(nil)
459608
for _, command := range api.Commands {

0 commit comments

Comments
 (0)