diff --git a/Makefile b/Makefile index 083ea1e922..6949882ee9 100644 --- a/Makefile +++ b/Makefile @@ -43,6 +43,7 @@ fetch-tags: generate: GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) go generate ./runtime + GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) go generate ./internal/config testgen: mkdir -p tools/vscode-tests diff --git a/internal/config/settings.go b/internal/config/settings.go index 45f6b3aad6..0d7fc276cc 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -17,6 +17,9 @@ import ( "golang.org/x/text/encoding/htmlindex" ) +// NOTE: '--' interprets rest of arguments as arguments for generate_settings.go +//go:generate $GOROOT/bin/go run ../../tools/generate_settings.go -- $GOFILE ../../runtime/help/options.md + type optionValidator func(string, any) error // a list of settings that need option validators @@ -41,97 +44,403 @@ var optionValidators = map[string]optionValidator{ // a list of settings with pre-defined choices var OptionChoices = map[string][]string{ - "clipboard": {"internal", "external", "terminal"}, - "fileformat": {"unix", "dos"}, - "helpsplit": {"hsplit", "vsplit"}, - "matchbracestyle": {"underline", "highlight"}, - "multiopen": {"tab", "hsplit", "vsplit"}, + "clipboard": { + // micro will use an internal clipboard. + "internal", + // accesses clipboard via an external tool, such as xclip/xsel + // or wl-clipboard on Linux, pbcopy/pbpaste on MacOS, and system calls on + // Windows. On Linux, if you do not have one of the tools installed, or if + // they are not working, micro will throw an error and use an internal + // clipboard. + "external", + // accesses the clipboard via your terminal emulator. Note that + // there is limited support among terminal emulators for this feature + // (called OSC 52). Terminals that are known to work are Kitty (enable + // reading with `clipboard_control` setting), iTerm2 (only copying), + // st, rxvt-unicode and xterm if enabled (see `> help copypaste` for + // details). Note that Gnome-terminal does not support this feature. With + // this setting, copy-paste **will** work over ssh. See `> help copypaste` + // for details. + "terminal", + }, + "fileformat": { + // default on Unix systems + "unix", + // default on Windows + "dos", + }, + "helpsplit": { + // open help in a horizontal split pane + "hsplit", + // open help in a vertical split pane + "vsplit", + }, + "matchbracestyle": { + // underline matching braces. + "underline", + // use `match-brace` style from the current theme. + "highlight", + }, + "multiopen": { + // open each file in a separate tab. + "tab", + // open files stacked top to bottom. + "hsplit", + // open files side-by-side. + "vsplit", + }, "reload": {"prompt", "auto", "disabled"}, - "truecolor": {"auto", "on", "off"}, + "truecolor": { + // enable usage of true color if micro detects that it is supported by + // the terminal, otherwise disable it. + "auto", + // force usage of true color even if micro does not detect its support + // by the terminal (of course this is not guaranteed to work well unless the + // terminal actually supports true color). + "on", + // disable true color usage. + "off", + }, } // a list of settings that can be globally and locally modified and their // default values var defaultCommonSettings = map[string]any{ + // when creating a new line, use the same indentation as the previous line. "autoindent": true, + // When a file is saved that the user doesn't have permission to + // modify, micro will ask if the user would like to use super user + // privileges to save the file. If this option is enabled, micro will + // automatically attempt to use super user privileges to save without + // asking the user. "autosu": false, + // micro will automatically keep backups of all open buffers. Backups + // are stored in `~/.config/micro/backups` and are removed when the buffer is + // closed cleanly. In the case of a system crash or a micro crash, the contents + // of the buffer can be recovered automatically by opening the file that was + // being edited before the crash, or manually by searching for the backup in + // the backup directory. Backups are made in the background for newly modified + // buffers every 8 seconds, or when micro detects a crash. "backup": true, + // the directory micro should place backups in. For the default + // value of `""` (empty string), the backup directory will be + // `ConfigDir/backups`, which is `~/.config/micro/backups` by default. The + // directory specified for backups will be created if it does not exist. "backupdir": "", + // in the infobar and tabbar, show only the basename of the file + // being edited rather than the full path. "basename": false, + // if this is not set to 0, it will display a column at the + // specified column. This is useful if you want column 80 to be highlighted + // special for example. "colorcolumn": float64(0), + // highlight the line that the cursor is on in a different color + // (the color is defined by the colorscheme you are using). "cursorline": true, + // if this is not set to 0, it will limit the amount of first + // lines in a file that are matched to determine the filetype. + // A higher limit means better accuracy of guessing the filetype, but also + // taking more time. "detectlimit": float64(100), + // display diff indicators before lines. "diffgutter": false, + // the encoding to open and save files with. Supported encodings + // are listed at https://www.w3.org/TR/encoding/. "encoding": "utf-8", + // micro will automatically add a newline to the end of the + // file if one does not exist. "eofnewline": true, + // this determines what kind of algorithm micro uses to determine + // if a buffer is modified or not. When `fastdirty` is on, micro just uses a + // boolean `modified` that is set to `true` as soon as the user makes an edit. + // This is fast, but can be inaccurate. If `fastdirty` is off, then micro will + // hash the current buffer against a hash of the original file (created when + // the buffer was loaded). This is more accurate but obviously more resource + // intensive. This option will be automatically enabled for the current buffer + // if the file size exceeds 50KB. "fastdirty": false, + // this determines what kind of line endings micro will use for + // the file. Unix line endings are just `\n` (linefeed) whereas dos line + // endings are `\r\n` (carriage return + linefeed). The two possible values for + // this option are `unix` and `dos`. The fileformat will be automatically + // detected (when you open an existing file) and displayed on the statusline, + // but this option is useful if you would like to change the line endings or if + // you are starting a new file. Changing this option while editing a file will + // change its line endings. Opening a file with this option set will only have + // an effect if the file is empty/newly created, because otherwise the fileformat + // will be automatically detected from the existing line endings. "fileformat": defaultFileFormat(), + // sets the filetype for the current buffer. Set this option to + // `off` to completely disable filetype detection. + // The default value will be automatically overridden depending on the file you open. "filetype": "unknown", + // highlight all instances of the searched text after a successful + // search. This highlighting can be temporarily turned off via the + // `UnhighlightSearch` action (triggered by the Esc key by default) or toggled + // on/off via the `ToggleHighlightSearch` action. Note that these actions don't + // change the `hlsearch` setting. As long as `hlsearch` is set to true, the next + // search will have the highlighting turned on again. "hlsearch": false, + // highlight tabs when spaces are expected, and spaces when tabs + // are expected. More precisely: if `tabstospaces` option is on, highlight + // all tab characters; if `tabstospaces` is off, highlight space characters + // in the initial indent part of the line. "hltaberrors": false, + // highlight trailing whitespaces at ends of lines. Note that + // it doesn't highlight newly added trailing whitespaces that naturally occur + // while typing text. It highlights only nasty forgotten trailing whitespaces. "hltrailingws": false, + // perform case-insensitive searches. "ignorecase": true, + // enable incremental search in "Find" prompt (matching as you type). "incsearch": true, + // sets the character to be shown to display tab characters. + // This option is **deprecated**, use the `tab` key in `showchars` option instead. "indentchar": " ", // Deprecated + // when using autoindent, whitespace is added for you. This + // option determines if when you move to the next line without any insertions + // the whitespace that was added should be deleted to remove trailing + // whitespace. By default, the autoindent whitespace is deleted if the line + // was left empty. "keepautoindent": false, + // show matching braces for '()', '{}', '[]' when the cursor + // is on a brace character or (if `matchbraceleft` is enabled) next to it. "matchbrace": true, + // simulate I-beam cursor behavior (cursor located not on a + // character but "between" characters): when showing matching braces, if there + // is no brace character directly under the cursor, match the brace character + // to the left of the cursor instead. Also when jumping to the matching brace, + // move the cursor either to the matching brace character or to the character + // next to it, depending on whether the initial cursor position was on the + // brace character or next to it (i.e. "inside" or "outside" the braces). + // With `matchbraceleft` disabled, micro will only match the brace directly + // under the cursor and will only jump to precisely to the matching brace. "matchbraceleft": true, + // whether to underline or highlight matching braces when + // `matchbrace` is enabled. The color of highlight is determined by the `match-brace` + // field in the current theme. "matchbracestyle": "underline", + // if a file is opened on a path that does not exist, the file + // cannot be saved because the parent directories don't exist. This option lets + // micro automatically create the parent directories in such a situation. "mkparents": false, + // the number of lines from the current view to keep in view + // when paging up or down. If this is set to 2, for instance, and you page + // down, the last two lines of the previous page will be the first two lines + // of the next page. "pageoverlap": float64(2), + // this option causes backups (see `backup` option) to be + // permanently saved. With permanent backups, micro will not remove backups when + // files are closed and will never apply them to existing files. Use this option + // if you are interested in manually managing your backup files. "permbackup": false, + // when enabled, disallows edits to the buffer. It is recommended + // to only ever set this option locally using `setlocal`. "readonly": false, + // make line numbers display relatively. If set to true, all + // lines except for the line that the cursor is located will display the distance + // from the cursor's line. "relativeruler": false, + // controls the reload behavior of the current buffer in case the file + // has changed. "reload": "prompt", + // micro will automatically trim trailing whitespaces at ends of + // lines. + // Note: This setting overrides `keepautoindent` and isn't used at timed `autosave` + // or forced `autosave` in case the buffer didn't change. A manual save will + // involve the action regardless if the buffer has been changed or not. "rmtrailingws": false, + // display line numbers. "ruler": true, + // remember where the cursor was last time the file was opened and + // put it there when you open the file again. Information is saved to + // `~/.config/micro/buffers/` "savecursor": false, + // when this option is on, undo is saved even after you close a file + // so if you close and reopen a file, you can keep undoing. Information is + // saved to `~/.config/micro/buffers/`. "saveundo": false, + // display a scroll bar "scrollbar": false, + // margin at which the view starts scrolling when the cursor + // approaches the edge of the view. "scrollmargin": float64(3), + // amount of lines to scroll for one scroll event. "scrollspeed": float64(2), + // sets what characters to be shown to display various invisible + // characters in the file. The characters shown will not be inserted into files. + // This option is specified in the form of `key1=value1,key2=value2,...`. + // + // Here are the list of keys: + // - `space`: space characters + // - `tab`: tab characters. If set, overrides the `indentchar` option. + // - `ispace`: space characters at indent position before the first visible + // character in a line. If this is not set, `space` will be shown + // instead. + // - `itab`: tab characters before the first visible character in a line. + // If this is not set, `tab` will be shown instead. + // + // Only `tab` and `itab` can display multiple characters (if possible), + // otherwise only the first character will be displayed. + // + // An example of this option value could be `tab=>,space=.,itab=|>,ispace=|` + // + // The color of the shown character is determined by the `indent-char` + // field in the current theme rather than the default text color. "showchars": "", + // add leading whitespace when pasting multiple lines. + // This will attempt to preserve the current indentation level when pasting an + // unindented block. "smartpaste": true, + // wrap lines that are too long to fit on the screen. "softwrap": false, + // when a horizontal split is created, create it below the + // current split. "splitbottom": true, + // when a vertical split is created, create it to the right of the + // current split. "splitright": true, + // format string definition for the left-justified part of the + // statusline. Special directives should be placed inside `$()`. Special + // directives include: `filename`, `modified`, `line`, `col`, `lines`, + // `percentage`, `opt`, `overwrite`, `bind`. + // The `opt` and `bind` directives take either an option or an action afterward + // and fill in the value of the option or the key bound to the action. "statusformatl": "$(filename) $(modified)$(overwrite)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)", + // format string definition for the right-justified part of the + // statusline. "statusformatr": "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help", + // display the status line at the bottom of the screen. "statusline": true, + // enables syntax highlighting. "syntax": true, + // navigate spaces at the beginning of lines as if they are tabs + // (e.g. move over 4 spaces at once). This option only does anything if + // `tabstospaces` is on. "tabmovement": false, + // the size in spaces that a tab character should be displayed with. "tabsize": float64(4), + // use spaces instead of tabs. Note: This option will be + // overridden by [the `ftoptions` plugin](https://github.com/micro-editor/micro/blob/master/runtime/plugins/ftoptions/ftoptions.lua) + // for certain filetypes. To disable this behavior, add `"ftoptions": false` to + // your config. See [issue #2213](https://github.com/micro-editor/micro/issues/2213) + // for more details. "tabstospaces": false, + // controls whether micro will use true colors (24-bit colors) when + // using a colorscheme with true colors, such as `solarized-tc` or `atom-dark`. + // Note: The change will take effect after the next start of `micro`. "truecolor": "auto", + // (only useful on unix) defines whether or not micro will use the + // primary clipboard to copy selections in the background. This does not affect + // the normal clipboard using `Ctrl-c` and `Ctrl-v`. "useprimary": true, + // wrap long lines by words, i.e. break at spaces. This option + // only does anything if `softwrap` is on. "wordwrap": false, } // a list of settings that should only be globally modified and their // default values var DefaultGlobalOnlySettings = map[string]any{ + // automatically save the buffer every n seconds, where n is the + // value of the autosave option. Also when quitting on a modified buffer, micro + // will automatically save and quit. Be warned, this option saves the buffer + // without prompting the user, so data may be overwritten. If this option is + // set to `0`, no autosaving is performed. "autosave": float64(0), + // specifies how micro should access the system clipboard. "clipboard": "external", + // use the given colorscheme. This setting is `global only`. + // The colorscheme can be either one of the colorschemes that micro comes with + // by default (such as `default`, `solarized` or `solarized-tc`) which are + // embedded in the micro binary, or a custom colorscheme stored in + // `~/.config/micro/colorschemes/$(option).micro` where `$(option)` is the + // option value. You can read more about micro's colorschemes and see the list + // of default colorschemes in `> help colors`. "colorscheme": "default", + // specifies the "divider" characters used for the dividing line + // between vertical/horizontal splits. The first character is for vertical + // dividers, and the second is for horizontal dividers. By default, for + // horizontal splits the statusline serves as a divider, but if the statusline + // is disabled the horizontal divider character will be used. "divchars": "|-", + // colorschemes provide the color (foreground and background) for + // the characters displayed in split dividers. With this option enabled, the + // colors specified by the colorscheme will be reversed (foreground and + // background colors swapped). "divreverse": true, + // forces micro to render the cursor using terminal colors rather + // than the actual terminal cursor. This is useful when the terminal's cursor is + // slow or otherwise unavailable/undesirable to use. + // Note: This option defaults to `true` in case `micro` is used in the legacy + // Windows Console. "fakecursor": defaultFakeCursor(), + // sets the split type to be used by the `help` command. "helpsplit": "hsplit", + // enables the line at the bottom of the editor where messages are + // printed. This option is `global only`. "infobar": true, + // display the nano-style key menu at the bottom of the screen. Note + // that ToggleKeyMenu is bound to `Alt-g` by default and this is displayed in + // the statusline. To disable the key binding, bind `Alt-g` to `None`. "keymenu": false, + // prevent plugins and lua scripts from binding any keys. + // Any custom actions must be binded manually either via commands like `bind` + // or by modifying the `bindings.json` file. "lockbindings": false, + // mouse support. When mouse support is disabled, + // usually the terminal will be able to access mouse events which can be useful + // if you want to copy from the terminal instead of from micro (if over ssh for + // example, because the terminal has access to the local clipboard and micro + // does not). "mouse": true, + // specifies how to layout multiple files opened at startup. + // Most useful as a command-line option, like `-multiopen vsplit`. Possible + // values correspond to commands (see `> help commands`) that open files: "multiopen": "tab", + // if enabled, this will cause micro to parse filenames such as + // `file.txt:10:5` as requesting to open `file.txt` with the cursor at line 10 + // and column 5. The column number can also be dropped to open the file at a + // given line and column 0. Note that with this option enabled it is not possible + // to open a file such as `file.txt:10:5`, where `:10:5` is part of the filename. + // It is also possible to open a file with a certain cursor location by using the + // `+LINE:COL` flag syntax. See `micro -help` for the command line options. "parsecursor": false, + // treat characters sent from the terminal in a single chunk as a paste + // event rather than a series of manual key presses. If you are pasting using + // the terminal keybinding (not `Ctrl-v`, which is micro's default paste + // keybinding) then it is a good idea to enable this option during the paste + // and disable once the paste is over. See `> help copypaste` for details about + // copying and pasting in a terminal environment. "paste": false, + // list of URLs pointing to plugin channels for downloading and + // installing plugins. A plugin channel consists of a json file with links to + // plugin repos, which store information about plugin versions and download URLs. + // By default, this option points to the official plugin channel hosted on GitHub + // at https://github.com/micro-editor/plugin-channel. "pluginchannels": []string{"https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json"}, + // a list of links to plugin repositories. "pluginrepos": []string{}, + // remember command history between closing and re-opening + // micro. Information is saved to `~/.config/micro/buffers/history`. "savehistory": true, + // specifies the character used for displaying the scrollbar "scrollbarchar": "|", + // specifies the super user command. On most systems this is "sudo" but + // on BSD it can be "doas." This option can be customized and is only used when + // saving with su. "sucmd": "sudo", + // always shows the tab bar, even when only one tab is open. "tabalways": false, + // inverts the tab characters' (filename, save indicator, etc) + // colors with respect to the tab bar. "tabhighlight": false, + // reverses the tab bar colors when active. "tabreverse": true, + // micro will assume that the terminal it is running in conforms to + // `xterm-256color` regardless of what the `$TERM` variable actually contains. + // Enabling this option may cause unwanted effects if your terminal in fact + // does not conform to the `xterm-256color` standard. "xterm": false, } diff --git a/tools/generate_settings.go b/tools/generate_settings.go new file mode 100644 index 0000000000..3a0b321338 --- /dev/null +++ b/tools/generate_settings.go @@ -0,0 +1,606 @@ +package main + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" + "sort" + "strings" +) + +type optionScope uint +const ( + scopeUnset optionScope = iota // uninitialized value + scopeCommon // Can use either set or setlocal + scopeOnlyGlobal // Only can use set (editor scope) + scopeOnlyLocal // Only can use setlocal (buffer scope) +) + +var scopeString = [4]string{ + scopeUnset: "ScopeUnset", + scopeCommon: "ScopeCommon", + scopeOnlyGlobal: "ScopeOnlyGlobal", + scopeOnlyLocal: "ScopeOnlyLocal", +} + +type choice struct { + name string + comment string +} + +type option struct { + name string + comment string + + typ string // (string, bool, float, int, array ...) + defaultValue any // (string, bool, float, int, array ...) + + scope optionScope + choices []choice + filled bool // found the option in `defaultCommonSettings` or `DefaultGlobalOnlySettings`. + // Extra measure to be sure. + // set to true @ collectOptionsFromMapString(). + // NOTE: `option.filled` should be done in the last top level node parsed +} + +// function for debugging purposes +func (o option) writeMarkdown(sb *strings.Builder) { + if sb == nil { panic("`sb` MUST not be nil") } + + sb.WriteString(fmt.Sprintf("* `%s`: %s\n", o.name, o.comment)) + + if len(o.choices) > 0 { + sb.WriteString(" Possible values are:\n\n") + for _, choice := range o.choices { + if choice.comment == "" { + sb.WriteString(fmt.Sprintf(" * `%s`\n", choice.name)) + } else { + sb.WriteString(fmt.Sprintf(" * `%s`: %s", choice.name, choice.comment)) + } + } + + sb.WriteString("\n") + } + + if o.typ == "bool" || o.typ == "float" || o.typ == "string" || o.typ == "[]string" { + sb.WriteString(fmt.Sprintf(" default value: `%s`\n\n", o.defaultValue)) + } else { + panic("This type was not checked if converts to string without issue") + } +} + +func (o option) String() string { + var sb strings.Builder + sb.WriteString(fmt.Sprintf("name: %s {\n", o.name)) + sb.WriteString(fmt.Sprintf("\ttype: \033[31m%s\033[0m,\n", o.typ)) + sb.WriteString(fmt.Sprintf("\tdefault: %v,\n", o.defaultValue)) + sb.WriteString(fmt.Sprintf("\tcomment: \033[33m%s\033[0m,\n", strings.TrimRight(o.comment, "\n"))) + sb.WriteString(fmt.Sprintf("\tscope: \033[34m%s\033[0m,\n", scopeString[o.scope])) + + if len(o.choices) > 0 { + sb.WriteString(fmt.Sprintf("\tchoices: ")) + for _, choice := range o.choices { + sb.WriteString(fmt.Sprintf("\"%s\", ", choice.name)) + } + sb.WriteString(fmt.Sprintf("\n")) + } + + sb.WriteString(fmt.Sprintf("\tfilled: %v,\n", o.filled)) + sb.WriteString("}") + return sb.String() +} + +func commentGroupToString(cgs []*ast.CommentGroup) string { + var sb strings.Builder + for _, cg := range cgs { + for i, comment := range cg.List { + linecomment := strings.TrimPrefix(comment.Text, "//") + linecomment = strings.TrimLeft(linecomment, " ") + if i > 0 { sb.WriteString(" ") } + sb.WriteString(linecomment) + sb.WriteString("\n") + } + } + return sb.String() +} + +// Returns a reference to the option and if it was created or not +func findOrCreateOption(optname string, opts *[]option) (*option, bool) { + if opts == nil { panic("`opts` MUST NOT be nil") } + + for i := 0; i < len(*opts); i++ { + if optname == (*opts)[i].name { return &(*opts)[i], false } + } + + *opts = append(*opts, option{name: optname, scope: scopeUnset, filled: false}) + return &(*opts)[len(*opts)-1], true +} + +func collectOptionsFromMapString(mapkv *ast.CompositeLit, fset *token.FileSet, comments ast.CommentMap, opts []option, scope optionScope) []option { + if scope != scopeCommon && scope != scopeOnlyGlobal { + panic("This functions should only be used for: `defaultCommonSettings` or `DefaultGlobalOnlySettings`") + } + + // ast.Print(fset, mapkv.Elts) + for _, elem := range mapkv.Elts { + kv, ok := elem.(*ast.KeyValueExpr) + if !ok { panic("Should be an `ast.KeyValueExpr`") } + // ast.Print(fset, kv) + + key, ok := kv.Key.(*ast.BasicLit) + if !ok { panic("Should be an `ast.BasicLit`") } + + optionComment := comments[kv] + // NOTE: enforce all options in `defaultCommonSettings` and + // `DefaultGlobalOnlySettings` have a comment. + if optionComment == nil { + fmt.Fprintf(os.Stderr, "\033[31mERROR: All options MUST have a description\n\033[0m") + fmt.Fprintf(os.Stderr, "\tMissing description for %s @ %v\n", + key.Value, fset.Position(key.ValuePos)) + os.Exit(1) + } + + optname := strings.Trim(key.Value, "\"") + opt, created := findOrCreateOption(optname, &opts) + + // NOTE: Option cannot exist from other scope or be already filled + // we set both in this function. + if !created && (opt.scope != scopeUnset || opt.filled) { + fmt.Fprintf(os.Stderr, "\033[31mERROR: option '%s' @ %v already present from a wrong scope (%s) or filled\n\033[0m", opt.name, fset.Position(key.ValuePos), scopeString[opt.scope]) + os.Exit(1) + } + + opt.comment = commentGroupToString(optionComment) + + switch val := kv.Value.(type) { + case *ast.Ident: + name := val.Name + if name == "true" || name == "false" { + opt.typ = "bool" + opt.defaultValue = name + } else { + ast.Print(fset, val) + panic("Unexpected value") + } + + case *ast.BasicLit: + if val.Kind == token.STRING { + opt.typ = "string" + opt.defaultValue = val.Value + } else { + ast.Print(fset, val) + panic("Unexpected value") + } + + case *ast.CallExpr: + // ast.Print(fset, val) + function, ok := val.Fun.(*ast.Ident) + if !ok { panic("Expected `*ast.Ident`") } + + if function.Name == "float64" { + if 1 != len(val.Args) { panic("Expects 1") } + uniqArg, ok := val.Args[0].(*ast.BasicLit) + if !ok { panic("Expected `*ast.BasicLit`") } + if uniqArg.Kind != token.INT { panic("Expected `token.INT`") } + opt.typ = "float" + opt.defaultValue = uniqArg.Value + + } else if function.Name == "defaultFileFormat" { // NOTE: Special runtime case + opt.typ = "string" + opt.defaultValue = "\"unix\"" + + } else if function.Name == "defaultFakeCursor" { // NOTE: Special runtime case + opt.typ = "bool" + opt.defaultValue = "false" + + } else { + ast.Print(fset, val) + panic("Unhandled CallExpr") + } + + case *ast.CompositeLit: + array, ok := val.Type.(*ast.ArrayType) + if !ok { panic("Expected only array types") } + arrayType, ok := array.Elt.(*ast.Ident) + if !ok { panic("Expected `*ast.Ident` as Name for `*ast.ArrayType`") } + opt.typ = fmt.Sprintf("[]%s", arrayType.Name) + + var strValue []string + for _, elem := range val.Elts { + switch e := elem.(type) { + case *ast.BasicLit: + strValue = append(strValue, e.Value) + default: + ast.Print(fset, elem) + panic("Unhandled element type in *ast.ArrayType") + } + } + opt.defaultValue = strValue + + default: + ast.Print(fset, val) + panic("Unhandled type for map value") + } + + opt.scope = scope + opt.filled = true + } + + return opts +} + +func collectOptionChoicesFromMapString(mapkv *ast.CompositeLit, fset *token.FileSet, comments ast.CommentMap, opts []option) []option { + // ast.Print(fset, mapkv.Elts) + for _, elem := range mapkv.Elts { + kv, ok := elem.(*ast.KeyValueExpr) + if !ok { panic("Should be an `ast.KeyValueExpr`") } + // ast.Print(fset, kv) + + key, ok := kv.Key.(*ast.BasicLit) + if !ok { panic("Should be an `ast.BasicLit`") } + + optname := strings.Trim(key.Value, "\"") + opt, _ := findOrCreateOption(optname, &opts) + + // Get the choices + switch val := kv.Value.(type) { + case *ast.CompositeLit: // OptionChoices is a map with value type []string + for _, elem := range val.Elts { + switch e := elem.(type) { + case *ast.BasicLit: + choiceName := strings.Trim(e.Value, "\"") + choiceComment := comments[e] + if choiceComment == nil { + fmt.Fprintf(os.Stderr, "\033[33mWARN: no description for choice %s @ %v\n\033[0m", + choiceName, fset.Position(e.ValuePos)) + } + opt.choices = append(opt.choices, choice{name: choiceName, comment: commentGroupToString(choiceComment)}) + + default: + ast.Print(fset, elem) + panic("Unreachable: expected only `string`") + } + } + + default: + ast.Print(fset, val) + panic("Unreachable: `OptionChoices` is a map with value type []string") + } + + } + + return opts +} + +func collectOptions(fset *token.FileSet, astFile *ast.File, comments ast.CommentMap) []option { + var options = make([]option, 0, 128) + + for _, decl := range astFile.Decls { + switch d := decl.(type) { + case *ast.FuncDecl: + continue + case *ast.GenDecl: + // ast.Print(fset, d) + + for _, spec := range d.Specs { + switch s := spec.(type) { + case *ast.ValueSpec: + if 1 < len(s.Names) { panic("only one name was expected") } + name := s.Names[0].Name // NOTE: only care for first one + + // All the variables with information are all maps (for now). + unwrapCompositeLitFromValueSpec := func(value *ast.ValueSpec) *ast.CompositeLit { + if 1 != len(value.Values) { panic("Should be 1 map") } + maplit, ok := value.Values[0].(*ast.CompositeLit) + if !ok { panic("Should be an `ast.CompositeLit`") } + return maplit + } + + if name == "OptionChoices" { + fmt.Fprintf(os.Stderr, "Processing: %s\n", name) + mapstring := unwrapCompositeLitFromValueSpec(s) + options = collectOptionChoicesFromMapString(mapstring, fset, comments, options) + + } else if name == "defaultCommonSettings" { + fmt.Fprintf(os.Stderr, "Processing: %s\n", name) + mapstring := unwrapCompositeLitFromValueSpec(s) + options = collectOptionsFromMapString(mapstring, fset, comments, options, scopeCommon) + } else if name == "DefaultGlobalOnlySettings" { + fmt.Fprintf(os.Stderr, "Processing: %s\n", name) + mapstring := unwrapCompositeLitFromValueSpec(s) + options = collectOptionsFromMapString(mapstring, fset, comments, options, scopeOnlyGlobal) + + } else if name == "LocalSettings" { + fmt.Fprintf(os.Stderr, "Processing: %s\n", name) + // NOTE: we patch the scope of the defaultCommonSettings + // of the options in the []string LocalSettings. + array := unwrapCompositeLitFromValueSpec(s) + for _, elem := range array.Elts { + _ = elem + value, ok := elem.(*ast.BasicLit) + if !ok { panic("Should be an `ast.BasicLit`") } + + optname := strings.Trim(value.Value, "\"") + opt, created := findOrCreateOption(optname, &options) + if created { + panic("Should be already present in `defaultCommonSettings`") + } + if opt.scope != scopeCommon { + panic("Should use `scopeCommon` because should be from `defaultCommonSettings`") + } + opt.scope = scopeOnlyLocal + } + } + default: + continue + } + } + + default: + ast.Print(fset, d) + panic("Unhandled top level type in `internal/config/settings.go`") + } + } + + return options +} + +func generateAndCheckPluginSection() string { + var sb strings.Builder + + var pluginsInfo = []struct { + name string + info string + }{ + {"autoclose", "automatically closes brackets, quotes, etc..."}, + {"comment", "provides automatic commenting for a number of languages"}, + {"ftoptions", "alters some default options depending on the filetype"}, + {"linter", "provides extensible linting for many languages"}, + {"literate", "provides advanced syntax highlighting for the Literate programming tool"}, + {"status", "provides some extensions to the status line (integration with Git and more)."}, + {"diff", `integrates the 'diffgutter' option with Git. If you are in a Git + directory, the diff gutter will show changes with respect to the most + recent Git commit rather than the diff since opening the file.`}, + } + + // Check all plugins are documented in `pluginsInfo` + // NOTE: this path is relative to this code location + const runtimePluginsDir = "../../runtime/plugins" + entries, err := os.ReadDir(runtimePluginsDir) + if err != nil { panic(err) } + + for i := 0; i < len(entries); i++ { + if !entries[i].IsDir() { continue } + var j int + for j = 0; j < len(pluginsInfo); j++ { + if entries[i].Name() == pluginsInfo[j].name { break } + } + if j == len(pluginsInfo) { + panic(fmt.Sprintf("%s is a built-in plugin not documented in `generate_settings.go`", entries[i].Name())) + } + } + + sb.WriteString(`--- + +Plugin options: all plugins come with a special option to enable or disable +them. The option is a boolean with the same name as the plugin itself. + +By default, the following plugins are provided, each with an option to enable +or disable them: + +`) + + for i := 0; i < len(pluginsInfo); i++ { + sb.WriteString(fmt.Sprintf("* `%s`: %s\n", pluginsInfo[i].name, pluginsInfo[i].info)) + } + + sb.WriteString(` +Any option you set in the editor will be saved to the file +'~/.config/micro/settings.json' so, in effect, your configuration file will be +created for you. If you'd like to take your configuration with you to another +machine, simply copy the 'settings.json' to the other machine. + +`) + + return sb.String() +} + +func generateMarkdownFile(options []option, path string) { + // NOTE: you can not use backticks inside multiline strings(``). + // Trade-off for readability in code(?) + const optionsSection = `# Options + +Micro stores all of the user configuration in its configuration directory. + +Micro uses '$MICRO_CONFIG_HOME' as the configuration directory. If this +environment variable is not set, it uses '$XDG_CONFIG_HOME/micro' instead. If +that environment variable is not set, it uses '~/.config/micro' as the +configuration directory. In the documentation, we use '~/.config/micro' to +refer to the configuration directory (even if it may in fact be somewhere else +if you have set either of the above environment variables). + +Here are the available options: + +` + + const settingsFileSection = `## Settings.json file + +The 'settings.json' file should go in your configuration directory (by default +at '~/.config/micro'), and should contain only options which have been modified +from their default setting. Here is the full list of options in json format, +so that you can see what the formatting should look like. + +` + + var globalAndLocalSection string = ` +## Global and local settings + +You can set these settings either globally or locally. Locally means that the +setting won't be saved to '~/.config/micro/settings.json' and that it will only +be set in the current buffer. Setting an option globally is the default, and +will set the option in all buffers. Use the 'setlocal' command to set an option +locally rather than globally. + +` + + +"The `colorscheme` option is global only, and the `filetype` option is local\n" + +"only. To set an option locally, use `setlocal` instead of `set`.\n\n" + + +"In the `settings.json` file you can also put set options locally by specifying\n" + +"either a glob or a filetype. Here is an example which has `tabstospaces` on for\n" + +"all files except Go files, and `tabsize` 4 for all files except Ruby files:\n\n" + +"```json\n" + +`{ + "ft:go": { + "tabstospaces": false + }, + "ft:ruby": { + "tabsize": 2 + }, + "tabstospaces": true, + "tabsize": 4 +} +` + +"```\n\n" + + +"Or similarly you can match with globs:\n\n" + + +"```json\n" + +`{ + "glob:*.go": { + "tabstospaces": false + }, + "glob:*.rb": { + "tabsize": 2 + }, + "tabstospaces": true, + "tabsize": 4 +} +` + +"```\n\n" + + +"You can also omit the `glob:` prefix before globs:\n\n" + + +"```json\n" + +`{ + "*.go": { + "tabstospaces": false + }, + "*.rb": { + "tabsize": 2 + }, + "tabstospaces": true, + "tabsize": 4 +} +` + +"```\n\n" + + +"But it is generally more recommended to use the `glob:` prefix, as it avoids\n" + +"potential conflicts with option names.\n" + + var sb strings.Builder + + sb.WriteString(optionsSection) + for _, opt := range options { + if opt.filled == false { + panic(fmt.Sprintf("Option '%s' was not filled!", opt.name)) + } + opt.writeMarkdown(&sb) + } + + sb.WriteString(generateAndCheckPluginSection()) + + sb.WriteString(settingsFileSection) + // write the default values in json format + sb.WriteString("```json\n{\n") + for i := 0; i < len(options); i++ { + opt := options[i] + sb.WriteString(fmt.Sprintf(" \"%s\": ", opt.name)) + if opt.typ == "bool" || opt.typ == "float" || opt.typ == "string" { + sb.WriteString(fmt.Sprintf("%s", opt.defaultValue)) + } else if opt.typ == "[]string" { + var arr []string = opt.defaultValue.([]string) + if len(arr) == 0 { + sb.WriteString("[]") + } else { + sb.WriteString("[\n") + for i := 0; i < len(arr); i++ { + if i == len(arr)-1 { + sb.WriteString(fmt.Sprintf(" %s\n", arr[i])) + } else { + sb.WriteString(fmt.Sprintf(" %s,\n", arr[i])) + } + } + sb.WriteString(" ]") + } + + } else { panic(fmt.Sprintf("unhandled '%s' for %s\n", opt.typ, opt.name)) } + + if i == len(options) - 1 { sb.WriteString("\n") } else { sb.WriteString(",\n") } + } + sb.WriteString("}\n```\n\n") + + sb.WriteString(globalAndLocalSection) + + err := os.WriteFile(path, []byte(sb.String()), 0644) + if err != nil { panic(err) } +} + +func main() { + settingsGoPath, markdownDestPath := "", "" + { // Arguments validation + var err string = "" + if len(os.Args) != 4 { + err = "unexpected amount of arguments" + } + // NOTE: '--' is needed in order to avoid picking the go file as part of the + // 'go run' command. + if err == "" && os.Args[1] != "--" { + err = "unexpected first argument should be '--'" + } + if err == "" && !strings.HasSuffix(os.Args[2], ".go") { + err = "second argument must be a *.go file" + } + if err == "" && strings.Contains(os.Args[2], "/") { + err = "second argument .go file must be a file in our relative path, no '/' allowed" + } + if err == "" && !strings.HasSuffix(os.Args[3], ".md") { + err = "third argument must be a markdown filepath" + } + + if err != "" { + fmt.Fprintf(os.Stderr, "\033[31mERROR: %s\n\033[0m", err) + fmt.Fprintf(os.Stderr, "Args: %d %v\n", len(os.Args), os.Args) + fmt.Fprintf(os.Stderr, "Usage: go run generate_settings.go -- \n") + os.Exit(1) + } + + settingsGoPath = os.Args[2] + markdownDestPath = os.Args[3] + } + + var fileset token.FileSet + var astFile *ast.File + var err error + var mode parser.Mode = parser.ParseComments | parser.SkipObjectResolution + astFile, err = parser.ParseFile(&fileset, settingsGoPath, nil, mode) + if err != nil { + fmt.Fprintf(os.Stderr, "\033[31mERROR: parsing %s: %v\n\033[0m", settingsGoPath, err) + os.Exit(1) + } + + allcomments := ast.NewCommentMap(&fileset, astFile, astFile.Comments) + options := collectOptions(&fileset, astFile, allcomments) + + sort.Slice(options, func(i, j int) bool { + return options[i].name < options[j].name + }) + + if false { // for debugging + for _, option := range options { fmt.Fprintf(os.Stderr, "%s\n", option) } + } + + generateMarkdownFile(options, markdownDestPath) +}