Skip to content

Commit 2ceecad

Browse files
authored
Use refs in jsonschema (#4309)
- **PR Description** This turns the generated jsonschema into a flat-ter schema by using refs. This helps avoid the stack overflow described here: #4276 (comment) As a side effect: os.editInTerminal started appearing in the generated section of `Config.md`. I think this is the correct behavior, and I am not sure why it wasn't in there before. I feel like this still could use a bit of cleaning up. I might be able to get rid of the `OriginalPropertiesMapping` field that we added to [jsonschema](https://github.com/invopop/jsonschema), but I need experiment some more when I get time. - **Please check if the PR fulfills these requirements** * [x] Cheatsheets are up-to-date (run `go generate ./...`) * [x] Code has been formatted (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting)) * [ ] Tests have been added/updated (see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md) for the integration test guide) * [ ] Text is internationalised (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation)) * [ ] If a new UserConfig entry was added, make sure it can be hot-reloaded (see [here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig)) * [ ] Docs have been updated if necessary * [x] You've read through your own file changes for silly mistakes etc
2 parents 62c6ba7 + 3b85307 commit 2ceecad

File tree

12 files changed

+1578
-1826
lines changed

12 files changed

+1578
-1826
lines changed

Diff for: docs/Config.md

+3
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,9 @@ os:
411411
# window is closed.
412412
editAtLineAndWait: ""
413413

414+
# Whether lazygit suspends until an edit process returns
415+
editInTerminal: false
416+
414417
# For opening a directory in an editor
415418
openDirInEditor: ""
416419

Diff for: go.mod

-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ require (
1111
github.com/gdamore/tcell/v2 v2.8.1
1212
github.com/go-errors/errors v1.5.1
1313
github.com/gookit/color v1.4.2
14-
github.com/iancoleman/orderedmap v0.3.0
1514
github.com/imdario/mergo v0.3.11
1615
github.com/integrii/flaggy v1.4.0
1716
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68

Diff for: go.sum

-2
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,6 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
171171
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
172172
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
173173
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
174-
github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc=
175-
github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE=
176174
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
177175
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
178176
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=

Diff for: pkg/config/user_config.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -557,8 +557,8 @@ type OSConfig struct {
557557
EditAtLineAndWait string `yaml:"editAtLineAndWait,omitempty"`
558558

559559
// Whether lazygit suspends until an edit process returns
560-
// Pointer to bool so that we can distinguish unset (nil) from false.
561-
// We're naming this `editInTerminal` for backwards compatibility
560+
// [dev] Pointer to bool so that we can distinguish unset (nil) from false.
561+
// [dev] We're naming this `editInTerminal` for backwards compatibility
562562
SuspendOnEdit *bool `yaml:"editInTerminal,omitempty"`
563563

564564
// For opening a directory in an editor

Diff for: pkg/jsonschema/generate.go

+61-11
Original file line numberDiff line numberDiff line change
@@ -7,41 +7,76 @@ import (
77
"fmt"
88
"os"
99
"reflect"
10+
"strings"
1011

1112
"github.com/jesseduffield/lazycore/pkg/utils"
1213
"github.com/jesseduffield/lazygit/pkg/config"
1314
"github.com/karimkhaleel/jsonschema"
15+
"github.com/samber/lo"
1416
)
1517

1618
func GetSchemaDir() string {
1719
return utils.GetLazyRootDirectory() + "/schema"
1820
}
1921

20-
func GenerateSchema() {
22+
func GenerateSchema() *jsonschema.Schema {
2123
schema := customReflect(&config.UserConfig{})
2224
obj, _ := json.MarshalIndent(schema, "", " ")
2325
obj = append(obj, '\n')
2426

2527
if err := os.WriteFile(GetSchemaDir()+"/config.json", obj, 0o644); err != nil {
2628
fmt.Println("Error writing to file:", err)
27-
return
29+
return nil
30+
}
31+
return schema
32+
}
33+
34+
func getSubSchema(rootSchema, parentSchema *jsonschema.Schema, key string) *jsonschema.Schema {
35+
subSchema, found := parentSchema.Properties.Get(key)
36+
if !found {
37+
panic(fmt.Sprintf("Failed to find subSchema at %s on parent", key))
38+
}
39+
40+
// This means the schema is defined on the rootSchema's Definitions
41+
if subSchema.Ref != "" {
42+
key, _ = strings.CutPrefix(subSchema.Ref, "#/$defs/")
43+
refSchema, ok := rootSchema.Definitions[key]
44+
if !ok {
45+
panic(fmt.Sprintf("Failed to find #/$defs/%s", key))
46+
}
47+
refSchema.Description = subSchema.Description
48+
return refSchema
2849
}
50+
51+
return subSchema
2952
}
3053

3154
func customReflect(v *config.UserConfig) *jsonschema.Schema {
32-
defaultConfig := config.GetDefaultConfig()
33-
r := &jsonschema.Reflector{FieldNameTag: "yaml", RequiredFromJSONSchemaTags: true, DoNotReference: true}
55+
r := &jsonschema.Reflector{FieldNameTag: "yaml", RequiredFromJSONSchemaTags: true}
3456
if err := r.AddGoComments("github.com/jesseduffield/lazygit/pkg/config", "../config"); err != nil {
3557
panic(err)
3658
}
3759
schema := r.Reflect(v)
60+
defaultConfig := config.GetDefaultConfig()
61+
userConfigSchema := schema.Definitions["UserConfig"]
62+
63+
defaultValue := reflect.ValueOf(defaultConfig).Elem()
64+
65+
yamlToFieldNames := lo.Invert(userConfigSchema.OriginalPropertiesMapping)
3866

39-
setDefaultVals(defaultConfig, schema)
67+
for pair := userConfigSchema.Properties.Oldest(); pair != nil; pair = pair.Next() {
68+
yamlName := pair.Key
69+
fieldName := yamlToFieldNames[yamlName]
70+
71+
subSchema := getSubSchema(schema, userConfigSchema, yamlName)
72+
73+
setDefaultVals(schema, subSchema, defaultValue.FieldByName(fieldName).Interface())
74+
}
4075

4176
return schema
4277
}
4378

44-
func setDefaultVals(defaults any, schema *jsonschema.Schema) {
79+
func setDefaultVals(rootSchema, schema *jsonschema.Schema, defaults any) {
4580
t := reflect.TypeOf(defaults)
4681
v := reflect.ValueOf(defaults)
4782

@@ -50,6 +85,24 @@ func setDefaultVals(defaults any, schema *jsonschema.Schema) {
5085
v = v.Elem()
5186
}
5287

88+
k := t.Kind()
89+
_ = k
90+
91+
switch t.Kind() {
92+
case reflect.Bool:
93+
schema.Default = v.Bool()
94+
case reflect.Int:
95+
schema.Default = v.Int()
96+
case reflect.String:
97+
schema.Default = v.String()
98+
default:
99+
// Do nothing
100+
}
101+
102+
if t.Kind() != reflect.Struct {
103+
return
104+
}
105+
53106
for i := 0; i < t.NumField(); i++ {
54107
value := v.Field(i).Interface()
55108
parentKey := t.Field(i).Name
@@ -59,13 +112,10 @@ func setDefaultVals(defaults any, schema *jsonschema.Schema) {
59112
continue
60113
}
61114

62-
subSchema, ok := schema.Properties.Get(key)
63-
if !ok {
64-
continue
65-
}
115+
subSchema := getSubSchema(rootSchema, schema, key)
66116

67117
if isStruct(value) {
68-
setDefaultVals(value, subSchema)
118+
setDefaultVals(rootSchema, subSchema, value)
69119
} else if !isZeroValue(value) {
70120
subSchema.Default = value
71121
}

Diff for: pkg/jsonschema/generate_config_docs.go

+67-88
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@ package jsonschema
22

33
import (
44
"bytes"
5-
"encoding/json"
65
"errors"
76
"fmt"
87
"os"
98
"strings"
109

11-
"github.com/iancoleman/orderedmap"
1210
"github.com/jesseduffield/lazycore/pkg/utils"
11+
"github.com/karimkhaleel/jsonschema"
1312
"github.com/samber/lo"
1413

1514
"gopkg.in/yaml.v3"
@@ -78,13 +77,20 @@ func prepareMarshalledConfig(buffer bytes.Buffer) []byte {
7877
}
7978

8079
func setComment(yamlNode *yaml.Node, description string) {
80+
// Filter out lines containing "[dev]"; this allows us to add developer
81+
// documentation to properties that don't get included in the docs
82+
lines := strings.Split(description, "\n")
83+
lines = lo.Filter(lines, func(s string, _ int) bool {
84+
return !strings.Contains(s, "[dev]")
85+
})
86+
8187
// Workaround for the way yaml formats the HeadComment if it contains
8288
// blank lines: it renders these without a leading "#", but we want a
8389
// leading "#" even on blank lines. However, yaml respects it if the
8490
// HeadComment already contains a leading "#", so we prefix all lines
8591
// (including blank ones) with "#".
8692
yamlNode.HeadComment = strings.Join(
87-
lo.Map(strings.Split(description, "\n"), func(s string, _ int) string {
93+
lo.Map(lines, func(s string, _ int) string {
8894
if s == "" {
8995
return "#" // avoid trailing space on blank lines
9096
}
@@ -106,16 +112,7 @@ func (n *Node) MarshalYAML() (interface{}, error) {
106112
setComment(&keyNode, n.Description)
107113
}
108114

109-
if n.Default != nil {
110-
valueNode := yaml.Node{
111-
Kind: yaml.ScalarNode,
112-
}
113-
err := valueNode.Encode(n.Default)
114-
if err != nil {
115-
return nil, err
116-
}
117-
node.Content = append(node.Content, &keyNode, &valueNode)
118-
} else if len(n.Children) > 0 {
115+
if len(n.Children) > 0 {
119116
childrenNode := yaml.Node{
120117
Kind: yaml.MappingNode,
121118
}
@@ -136,60 +133,18 @@ func (n *Node) MarshalYAML() (interface{}, error) {
136133
childrenNode.Content = append(childrenNode.Content, childYaml.(*yaml.Node).Content...)
137134
}
138135
node.Content = append(node.Content, &keyNode, &childrenNode)
139-
}
140-
141-
return &node, nil
142-
}
143-
144-
func getDescription(v *orderedmap.OrderedMap) string {
145-
description, ok := v.Get("description")
146-
if !ok {
147-
description = ""
148-
}
149-
return description.(string)
150-
}
151-
152-
func getDefault(v *orderedmap.OrderedMap) (error, any) {
153-
defaultValue, ok := v.Get("default")
154-
if ok {
155-
return nil, defaultValue
156-
}
157-
158-
dataType, ok := v.Get("type")
159-
if ok {
160-
dataTypeString := dataType.(string)
161-
if dataTypeString == "string" {
162-
return nil, ""
136+
} else {
137+
valueNode := yaml.Node{
138+
Kind: yaml.ScalarNode,
163139
}
140+
err := valueNode.Encode(n.Default)
141+
if err != nil {
142+
return nil, err
143+
}
144+
node.Content = append(node.Content, &keyNode, &valueNode)
164145
}
165146

166-
return errors.New("Failed to get default value"), nil
167-
}
168-
169-
func parseNode(parent *Node, name string, value *orderedmap.OrderedMap) {
170-
description := getDescription(value)
171-
err, defaultValue := getDefault(value)
172-
if err == nil {
173-
leaf := &Node{Name: name, Description: description, Default: defaultValue}
174-
parent.Children = append(parent.Children, leaf)
175-
}
176-
177-
properties, ok := value.Get("properties")
178-
if !ok {
179-
return
180-
}
181-
182-
orderedProperties := properties.(orderedmap.OrderedMap)
183-
184-
node := &Node{Name: name, Description: description}
185-
parent.Children = append(parent.Children, node)
186-
187-
keys := orderedProperties.Keys()
188-
for _, name := range keys {
189-
value, _ := orderedProperties.Get(name)
190-
typedValue := value.(orderedmap.OrderedMap)
191-
parseNode(node, name, &typedValue)
192-
}
147+
return &node, nil
193148
}
194149

195150
func writeToConfigDocs(config []byte) error {
@@ -222,31 +177,12 @@ func writeToConfigDocs(config []byte) error {
222177
return nil
223178
}
224179

225-
func GenerateConfigDocs() {
226-
content, err := os.ReadFile(GetSchemaDir() + "/config.json")
227-
if err != nil {
228-
panic("Error reading config.json")
229-
}
230-
231-
schema := orderedmap.New()
232-
233-
err = json.Unmarshal(content, &schema)
234-
if err != nil {
235-
panic("Failed to unmarshal config.json")
236-
}
237-
238-
root, ok := schema.Get("properties")
239-
if !ok {
240-
panic("properties key not found in schema")
180+
func GenerateConfigDocs(schema *jsonschema.Schema) {
181+
rootNode := &Node{
182+
Children: make([]*Node, 0),
241183
}
242-
orderedRoot := root.(orderedmap.OrderedMap)
243184

244-
rootNode := Node{}
245-
for _, name := range orderedRoot.Keys() {
246-
value, _ := orderedRoot.Get(name)
247-
typedValue := value.(orderedmap.OrderedMap)
248-
parseNode(&rootNode, name, &typedValue)
249-
}
185+
recurseOverSchema(schema, schema.Definitions["UserConfig"], rootNode)
250186

251187
var buffer bytes.Buffer
252188
encoder := yaml.NewEncoder(&buffer)
@@ -262,8 +198,51 @@ func GenerateConfigDocs() {
262198

263199
config := prepareMarshalledConfig(buffer)
264200

265-
err = writeToConfigDocs(config)
201+
err := writeToConfigDocs(config)
266202
if err != nil {
267203
panic(err)
268204
}
269205
}
206+
207+
func recurseOverSchema(rootSchema, schema *jsonschema.Schema, parent *Node) {
208+
if schema == nil || schema.Properties == nil || schema.Properties.Len() == 0 {
209+
return
210+
}
211+
212+
for pair := schema.Properties.Oldest(); pair != nil; pair = pair.Next() {
213+
subSchema := getSubSchema(rootSchema, schema, pair.Key)
214+
215+
// Skip empty objects
216+
if subSchema.Type == "object" && subSchema.Properties == nil {
217+
continue
218+
}
219+
220+
// Skip empty arrays
221+
if isZeroValue(subSchema.Default) && subSchema.Type == "array" {
222+
continue
223+
}
224+
225+
node := Node{
226+
Name: pair.Key,
227+
Description: subSchema.Description,
228+
Default: getZeroValue(subSchema.Default, subSchema.Type),
229+
}
230+
parent.Children = append(parent.Children, &node)
231+
recurseOverSchema(rootSchema, subSchema, &node)
232+
}
233+
}
234+
235+
func getZeroValue(val any, t string) any {
236+
if !isZeroValue(val) {
237+
return val
238+
}
239+
240+
switch t {
241+
case "string":
242+
return ""
243+
case "boolean":
244+
return false
245+
default:
246+
return nil
247+
}
248+
}

Diff for: pkg/jsonschema/generator.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ import (
1010

1111
func main() {
1212
fmt.Printf("Generating jsonschema in %s...\n", jsonschema.GetSchemaDir())
13-
jsonschema.GenerateSchema()
14-
jsonschema.GenerateConfigDocs()
13+
schema := jsonschema.GenerateSchema()
14+
jsonschema.GenerateConfigDocs(schema)
1515
}

0 commit comments

Comments
 (0)