diff options
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/config/user_config.go | 27 | ||||
-rw-r--r-- | pkg/jsonschema/generate_config_docs.go | 269 | ||||
-rw-r--r-- | pkg/jsonschema/generator.go | 1 |
3 files changed, 284 insertions, 13 deletions
diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index c22594461..2df6e60b8 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -19,8 +19,6 @@ type UserConfig struct { ConfirmOnQuit bool `yaml:"confirmOnQuit"` // If true, exit Lazygit when the user presses escape in a context where there is nothing to cancel/close QuitOnTopLevelReturn bool `yaml:"quitOnTopLevelReturn"` - // Keybindings - Keybinding KeybindingConfig `yaml:"keybinding"` // Config relating to things outside of Lazygit like how files are opened, copying to clipboard, etc OS OSConfig `yaml:"os,omitempty"` // If true, don't display introductory popups upon opening Lazygit. @@ -38,6 +36,8 @@ type UserConfig struct { NotARepository string `yaml:"notARepository" jsonschema:"enum=prompt,enum=create,enum=skip,enum=quit"` // If true, display a confirmation when subprocess terminates. This allows you to view the output of the subprocess before returning to Lazygit. PromptToReturnFromSubprocess bool `yaml:"promptToReturnFromSubprocess"` + // Keybindings + Keybinding KeybindingConfig `yaml:"keybinding"` } type RefresherConfig struct { @@ -252,7 +252,7 @@ type PagingConfig struct { // diff-so-fancy // delta --dark --paging=never // ydiff -p cat -s --wrap --width={{columnWidth}} - Pager PagerType `yaml:"pager" jsonschema:"minLength=1"` + Pager PagerType `yaml:"pager"` // If true, Lazygit will use whatever pager is specified in `$GIT_PAGER`, `$PAGER`, or your *git config*. If the pager ends with something like ` | less` we will strip that part out, because less doesn't play nice with our rendering approach. If the custom pager uses less under the hood, that will also break rendering (hence the `--paging=never` flag for the `delta` pager). UseConfig bool `yaml:"useConfig"` // e.g. 'difft --color=always' @@ -294,9 +294,9 @@ type LogConfig struct { type CommitPrefixConfig struct { // pattern to match on. E.g. for 'feature/AB-123' to match on the AB-123 use "^\\w+\\/(\\w+-\\w+).*" - Pattern string `yaml:"pattern" jsonschema:"example=^\\w+\\/(\\w+-\\w+).*,minLength=1"` + Pattern string `yaml:"pattern" jsonschema:"example=^\\w+\\/(\\w+-\\w+).*"` // Replace directive. E.g. for 'feature/AB-123' to start the commit message with 'AB-123 ' use "[$1] " - Replace string `yaml:"replace" jsonschema:"example=[$1] ,minLength=1"` + Replace string `yaml:"replace" jsonschema:"example=[$1]"` } type UpdateConfig struct { @@ -684,6 +684,7 @@ func GetDefaultConfig() *UserConfig { CommandLogSize: 8, SplitDiff: "auto", SkipRewordInEditorWarning: false, + WindowSize: "normal", Border: "rounded", AnimateExplosion: true, PortraitMode: "auto", @@ -735,8 +736,14 @@ func GetDefaultConfig() *UserConfig { Method: "prompt", Days: 14, }, - ConfirmOnQuit: false, - QuitOnTopLevelReturn: false, + ConfirmOnQuit: false, + QuitOnTopLevelReturn: false, + OS: OSConfig{}, + DisableStartupPopups: false, + CustomCommands: []CustomCommand(nil), + Services: map[string]string(nil), + NotARepository: "prompt", + PromptToReturnFromSubprocess: true, Keybinding: KeybindingConfig{ Universal: KeybindingUniversalConfig{ Quit: "q", @@ -903,11 +910,5 @@ func GetDefaultConfig() *UserConfig { CommitMenu: "<c-o>", }, }, - OS: OSConfig{}, - DisableStartupPopups: false, - CustomCommands: []CustomCommand(nil), - Services: map[string]string(nil), - NotARepository: "prompt", - PromptToReturnFromSubprocess: true, } } diff --git a/pkg/jsonschema/generate_config_docs.go b/pkg/jsonschema/generate_config_docs.go new file mode 100644 index 000000000..441cc55e7 --- /dev/null +++ b/pkg/jsonschema/generate_config_docs.go @@ -0,0 +1,269 @@ +package jsonschema + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "os" + "strings" + + "github.com/iancoleman/orderedmap" + "github.com/jesseduffield/lazycore/pkg/utils" + "github.com/samber/lo" + + "gopkg.in/yaml.v3" +) + +type Node struct { + Name string + Description string + Default any + Children []*Node +} + +const ( + IndentLevel = 2 + DocumentationCommentStart = "<!-- START CONFIG YAML: AUTOMATICALLY GENERATED with `go generate ./..., DO NOT UPDATE MANUALLY -->\n" + DocumentationCommentEnd = "<!-- END CONFIG YAML -->" + DocumentationCommentStartLen = len(DocumentationCommentStart) +) + +func insertBlankLines(buffer bytes.Buffer) bytes.Buffer { + lines := strings.Split(strings.TrimRight(buffer.String(), "\n"), "\n") + + var newBuffer bytes.Buffer + + previousIndent := -1 + wasComment := false + + for _, line := range lines { + trimmedLine := strings.TrimLeft(line, " ") + indent := len(line) - len(trimmedLine) + isComment := strings.HasPrefix(trimmedLine, "#") + if isComment && !wasComment && indent <= previousIndent { + newBuffer.WriteString("\n") + } + newBuffer.WriteString(line) + newBuffer.WriteString("\n") + previousIndent = indent + wasComment = isComment + } + + return newBuffer +} + +func prepareMarshalledConfig(buffer bytes.Buffer) []byte { + buffer = insertBlankLines(buffer) + + // Remove all `---` lines + lines := strings.Split(strings.TrimRight(buffer.String(), "\n"), "\n") + + var newBuffer bytes.Buffer + + for _, line := range lines { + if strings.TrimSpace(line) != "---" { + newBuffer.WriteString(line) + newBuffer.WriteString("\n") + } + } + + config := newBuffer.Bytes() + + // Add markdown yaml block tag + config = append([]byte("```yaml\n"), config...) + config = append(config, []byte("```\n")...) + + return config +} + +func setComment(yamlNode *yaml.Node, description string) { + // Workaround for the way yaml formats the HeadComment if it contains + // blank lines: it renders these without a leading "#", but we want a + // leading "#" even on blank lines. However, yaml respects it if the + // HeadComment already contains a leading "#", so we prefix all lines + // (including blank ones) with "#". + yamlNode.HeadComment = strings.Join( + lo.Map(strings.Split(description, "\n"), func(s string, _ int) string { + if s == "" { + return "#" // avoid trailing space on blank lines + } + return "# " + s + }), + "\n") +} + +func (n *Node) MarshalYAML() (interface{}, error) { + node := yaml.Node{ + Kind: yaml.MappingNode, + } + + keyNode := yaml.Node{ + Kind: yaml.ScalarNode, + Value: n.Name, + } + if n.Description != "" { + setComment(&keyNode, n.Description) + } + + if n.Default != nil { + valueNode := yaml.Node{ + Kind: yaml.ScalarNode, + } + err := valueNode.Encode(n.Default) + if err != nil { + return nil, err + } + node.Content = append(node.Content, &keyNode, &valueNode) + } else if len(n.Children) > 0 { + childrenNode := yaml.Node{ + Kind: yaml.MappingNode, + } + for _, child := range n.Children { + childYaml, err := child.MarshalYAML() + if err != nil { + return nil, err + } + + childKey := yaml.Node{ + Kind: yaml.ScalarNode, + Value: child.Name, + } + if child.Description != "" { + setComment(&childKey, child.Description) + } + childYaml = childYaml.(*yaml.Node) + childrenNode.Content = append(childrenNode.Content, childYaml.(*yaml.Node).Content...) + } + node.Content = append(node.Content, &keyNode, &childrenNode) + } + + return &node, nil +} + +func getDescription(v *orderedmap.OrderedMap) string { + description, ok := v.Get("description") + if !ok { + description = "" + } + return description.(string) +} + +func getDefault(v *orderedmap.OrderedMap) (error, any) { + defaultValue, ok := v.Get("default") + if ok { + return nil, defaultValue + } + + dataType, ok := v.Get("type") + if ok { + dataTypeString := dataType.(string) + if dataTypeString == "string" { + return nil, "" + } + } + + return errors.New("Failed to get default value"), nil +} + +func parseNode(parent *Node, name string, value *orderedmap.OrderedMap) { + description := getDescription(value) + err, defaultValue := getDefault(value) + if err == nil { + leaf := &Node{Name: name, Description: description, Default: defaultValue} + parent.Children = append(parent.Children, leaf) + } + + properties, ok := value.Get("properties") + if !ok { + return + } + + orderedProperties := properties.(orderedmap.OrderedMap) + + node := &Node{Name: name, Description: description} + parent.Children = append(parent.Children, node) + + keys := orderedProperties.Keys() + for _, name := range keys { + value, _ := orderedProperties.Get(name) + typedValue := value.(orderedmap.OrderedMap) + parseNode(node, name, &typedValue) + } +} + +func writeToConfigDocs(config []byte) error { + configPath := utils.GetLazyRootDirectory() + "/docs/Config.md" + markdown, err := os.ReadFile(configPath) + if err != nil { + return fmt.Errorf("Error reading Config.md file %w", err) + } + + startConfigSectionIndex := bytes.Index(markdown, []byte(DocumentationCommentStart)) + if startConfigSectionIndex == -1 { + return errors.New("Default config starting comment not found") + } + + endConfigSectionIndex := bytes.Index(markdown[startConfigSectionIndex+DocumentationCommentStartLen:], []byte(DocumentationCommentEnd)) + if endConfigSectionIndex == -1 { + return errors.New("Default config closing comment not found") + } + + endConfigSectionIndex = endConfigSectionIndex + startConfigSectionIndex + DocumentationCommentStartLen + + newMarkdown := make([]byte, 0, len(markdown)-endConfigSectionIndex+startConfigSectionIndex+len(config)) + newMarkdown = append(newMarkdown, markdown[:startConfigSectionIndex+DocumentationCommentStartLen]...) + newMarkdown = append(newMarkdown, config...) + newMarkdown = append(newMarkdown, markdown[endConfigSectionIndex:]...) + + if err := os.WriteFile(configPath, newMarkdown, 0o644); err != nil { + return fmt.Errorf("Error writing to file %w", err) + } + return nil +} + +func GenerateConfigDocs() { + content, err := os.ReadFile(GetSchemaDir() + "/config.json") + if err != nil { + panic("Error reading config.json") + } + + schema := orderedmap.New() + + err = json.Unmarshal(content, &schema) + if err != nil { + panic("Failed to unmarshal config.json") + } + + root, ok := schema.Get("properties") + if !ok { + panic("properties key not found in schema") + } + orderedRoot := root.(orderedmap.OrderedMap) + + rootNode := Node{} + for _, name := range orderedRoot.Keys() { + value, _ := orderedRoot.Get(name) + typedValue := value.(orderedmap.OrderedMap) + parseNode(&rootNode, name, &typedValue) + } + + var buffer bytes.Buffer + encoder := yaml.NewEncoder(&buffer) + encoder.SetIndent(IndentLevel) + + for _, child := range rootNode.Children { + err := encoder.Encode(child) + if err != nil { + panic("Failed to Marshal document") + } + } + encoder.Close() + + config := prepareMarshalledConfig(buffer) + + err = writeToConfigDocs(config) + if err != nil { + panic(err) + } +} diff --git a/pkg/jsonschema/generator.go b/pkg/jsonschema/generator.go index 263f4784b..df53dee56 100644 --- a/pkg/jsonschema/generator.go +++ b/pkg/jsonschema/generator.go @@ -11,4 +11,5 @@ import ( func main() { fmt.Printf("Generating jsonschema in %s...\n", jsonschema.GetSchemaDir()) jsonschema.GenerateSchema() + jsonschema.GenerateConfigDocs() } |