@@ -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
412434var 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+
414443func 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 = "\n Must be one of:\n \n "
424- } else {
425- msg = "\n Can 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\n Default: `%v`.\n \n " , opt .Doc , enumValues , opt .Default )
440468 }
441- fmt .Fprintf (section , "### **%v** *%v*\n %v%v\n \n Default: `%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**\n Identifier: `%v`\n \n %v\n \n " , lens .Title , lens .Lens , lens .Doc )
479+ fmt .Fprintf (section , "### **%v**\n \ n Identifier: `%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 = "\n Must be one of:\n \n "
544+ } else {
545+ msg = "\n Can 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+
457606func rewriteCommands (doc []byte , api * source.APIJSON ) ([]byte , error ) {
458607 section := bytes .NewBuffer (nil )
459608 for _ , command := range api .Commands {
0 commit comments