@@ -14,6 +14,7 @@ import (
14
14
"go/format"
15
15
"go/token"
16
16
"go/types"
17
+ "io"
17
18
"io/ioutil"
18
19
"os"
19
20
"path/filepath"
@@ -22,6 +23,7 @@ import (
22
23
"sort"
23
24
"strings"
24
25
"time"
26
+ "unicode"
25
27
26
28
"github.com/sanity-io/litter"
27
29
"golang.org/x/tools/go/ast/astutil"
@@ -75,16 +77,19 @@ func loadAPI() (*source.APIJSON, error) {
75
77
Options : map [string ][]* source.OptionJSON {},
76
78
}
77
79
defaults := source .DefaultOptions ()
78
- for _ , cat := range []reflect.Value {
79
- reflect .ValueOf (defaults .DebuggingOptions ),
80
+ for _ , category := range []reflect.Value {
80
81
reflect .ValueOf (defaults .UserOptions ),
81
- reflect .ValueOf (defaults .ExperimentalOptions ),
82
82
} {
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 , "" )
84
89
if err != nil {
85
90
return nil , err
86
91
}
87
- catName := strings .TrimSuffix (cat .Type ().Name (), "Options" )
92
+ catName := strings .TrimSuffix (category .Type ().Name (), "Options" )
88
93
api .Options [catName ] = opts
89
94
}
90
95
@@ -109,13 +114,7 @@ func loadAPI() (*source.APIJSON, error) {
109
114
return api , nil
110
115
}
111
116
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 ) {
119
118
file , err := fileForPos (pkg , optsType .Pos ())
120
119
if err != nil {
121
120
return nil , err
@@ -131,6 +130,21 @@ func loadOptions(category reflect.Value, pkg *packages.Package) ([]*source.Optio
131
130
for i := 0 ; i < optsStruct .NumFields (); i ++ {
132
131
// The types field gives us the type.
133
132
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
+ }
134
148
path , _ := astutil .PathEnclosingInterval (file , typesField .Pos (), typesField .Pos ())
135
149
if len (path ) < 2 {
136
150
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
183
197
typ = strings .Replace (typ , m .Key ().String (), m .Key ().Underlying ().String (), 1 )
184
198
}
185
199
}
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" )
186
206
187
207
opts = append (opts , & source.OptionJSON {
188
208
Name : lowerFirst (typesField .Name ()),
189
209
Type : typ ,
190
210
Doc : lowerFirst (astField .Doc .Text ()),
191
211
Default : string (defBytes ),
192
212
EnumValues : enumValues ,
213
+ Status : status ,
214
+ Hierarchy : hierarchy ,
193
215
})
194
216
}
195
217
return opts , nil
@@ -411,34 +433,39 @@ func rewriteAPI(input []byte, api *source.APIJSON) ([]byte, error) {
411
433
412
434
var parBreakRE = regexp .MustCompile ("\n {2,}" )
413
435
436
+ type optionsGroup struct {
437
+ title string
438
+ final string
439
+ level int
440
+ options []* source.OptionJSON
441
+ }
442
+
414
443
func rewriteSettings (doc []byte , api * source.APIJSON ) ([]byte , error ) {
415
444
result := doc
416
445
for category , opts := range api .Options {
446
+ groups := collectGroups (opts )
447
+
448
+ // First, print a table of contents.
417
449
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 )
440
468
}
441
- fmt .Fprintf (section , "### **%v** *%v*\n %v%v\n \n Default: `%v`.\n " , opt .Name , opt .Type , opt .Doc , enumValues .String (), opt .Default )
442
469
}
443
470
var err error
444
471
result , err = replaceSection (result , category , section .Bytes ())
@@ -449,11 +476,133 @@ func rewriteSettings(doc []byte, api *source.APIJSON) ([]byte, error) {
449
476
450
477
section := bytes .NewBuffer (nil )
451
478
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 )
453
480
}
454
481
return replaceSection (result , "Lenses" , section .Bytes ())
455
482
}
456
483
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
+
457
606
func rewriteCommands (doc []byte , api * source.APIJSON ) ([]byte , error ) {
458
607
section := bytes .NewBuffer (nil )
459
608
for _ , command := range api .Commands {
0 commit comments