]> git.lizzy.rs Git - micro.git/blobdiff - internal/config/settings.go
scala.yaml: add support for .sc extension (#2452)
[micro.git] / internal / config / settings.go
index 2d0787a9fb10f72fe5a7cf1c5ff3b6d46fc8f814..2c23f39b433f970d50654557fa7ac1bea5cea77b 100644 (file)
@@ -3,15 +3,17 @@ package config
 import (
        "encoding/json"
        "errors"
+       "fmt"
        "io/ioutil"
        "os"
+       "path/filepath"
        "reflect"
        "strconv"
        "strings"
 
-       "github.com/flynn/json5"
        "github.com/zyedidia/glob"
-       "github.com/zyedidia/micro/internal/util"
+       "github.com/zyedidia/json5"
+       "github.com/zyedidia/micro/v2/internal/util"
        "golang.org/x/text/encoding/htmlindex"
 )
 
@@ -25,11 +27,23 @@ var (
        GlobalSettings map[string]interface{}
 
        // This is the raw parsed json
-       parsedSettings map[string]interface{}
+       parsedSettings     map[string]interface{}
+       settingsParseError bool
+
+       // ModifiedSettings is a map of settings which should be written to disk
+       // because they have been modified by the user in this session
+       ModifiedSettings map[string]bool
 )
 
+func init() {
+       ModifiedSettings = make(map[string]bool)
+       parsedSettings = make(map[string]interface{})
+}
+
 // Options with validators
 var optionValidators = map[string]optionValidator{
+       "autosave":     validateNonNegativeValue,
+       "clipboard":    validateClipboard,
        "tabsize":      validatePositiveValue,
        "scrollmargin": validateNonNegativeValue,
        "scrollspeed":  validateNonNegativeValue,
@@ -40,33 +54,64 @@ var optionValidators = map[string]optionValidator{
 }
 
 func ReadSettings() error {
-       filename := ConfigDir + "/settings.json"
+       filename := filepath.Join(ConfigDir, "settings.json")
        if _, e := os.Stat(filename); e == nil {
                input, err := ioutil.ReadFile(filename)
                if err != nil {
+                       settingsParseError = true
                        return errors.New("Error reading settings.json file: " + err.Error())
                }
                if !strings.HasPrefix(string(input), "null") {
                        // Unmarshal the input into the parsed map
                        err = json5.Unmarshal(input, &parsedSettings)
                        if err != nil {
+                               settingsParseError = true
                                return errors.New("Error reading settings.json: " + err.Error())
                        }
+
+                       // check if autosave is a boolean and convert it to float if so
+                       if v, ok := parsedSettings["autosave"]; ok {
+                               s, ok := v.(bool)
+                               if ok {
+                                       if s {
+                                               parsedSettings["autosave"] = 8.0
+                                       } else {
+                                               parsedSettings["autosave"] = 0.0
+                                       }
+                               }
+                       }
                }
        }
        return nil
 }
 
+func verifySetting(option string, value reflect.Type, def reflect.Type) bool {
+       var interfaceArr []interface{}
+       switch option {
+       case "pluginrepos", "pluginchannels":
+               return value.AssignableTo(reflect.TypeOf(interfaceArr))
+       default:
+               return def.AssignableTo(value)
+       }
+}
+
 // InitGlobalSettings initializes the options map and sets all options to their default values
 // Must be called after ReadSettings
-func InitGlobalSettings() {
+func InitGlobalSettings() error {
+       var err error
        GlobalSettings = DefaultGlobalSettings()
 
        for k, v := range parsedSettings {
                if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
+                       if _, ok := GlobalSettings[k]; ok && !verifySetting(k, reflect.TypeOf(v), reflect.TypeOf(GlobalSettings[k])) {
+                               err = fmt.Errorf("Global Error: setting '%s' has incorrect type (%s), using default value: %v (%s)", k, reflect.TypeOf(v), GlobalSettings[k], reflect.TypeOf(GlobalSettings[k]))
+                               continue
+                       }
+
                        GlobalSettings[k] = v
                }
        }
+       return err
 }
 
 // InitLocalSettings scans the json in settings.json and sets the options locally based
@@ -79,6 +124,10 @@ func InitLocalSettings(settings map[string]interface{}, path string) error {
                        if strings.HasPrefix(k, "ft:") {
                                if settings["filetype"].(string) == k[3:] {
                                        for k1, v1 := range v.(map[string]interface{}) {
+                                               if _, ok := settings[k1]; ok && !verifySetting(k1, reflect.TypeOf(v1), reflect.TypeOf(settings[k1])) {
+                                                       parseError = fmt.Errorf("Error: setting '%s' has incorrect type (%s), using default value: %v (%s)", k, reflect.TypeOf(v1), settings[k1], reflect.TypeOf(settings[k1]))
+                                                       continue
+                                               }
                                                settings[k1] = v1
                                        }
                                }
@@ -91,6 +140,10 @@ func InitLocalSettings(settings map[string]interface{}, path string) error {
 
                                if g.MatchString(path) {
                                        for k1, v1 := range v.(map[string]interface{}) {
+                                               if _, ok := settings[k1]; ok && !verifySetting(k1, reflect.TypeOf(v1), reflect.TypeOf(settings[k1])) {
+                                                       parseError = fmt.Errorf("Error: setting '%s' has incorrect type (%s), using default value: %v (%s)", k, reflect.TypeOf(v1), settings[k1], reflect.TypeOf(settings[k1]))
+                                                       continue
+                                               }
                                                settings[k1] = v1
                                        }
                                }
@@ -102,10 +155,35 @@ func InitLocalSettings(settings map[string]interface{}, path string) error {
 
 // WriteSettings writes the settings to the specified filename as JSON
 func WriteSettings(filename string) error {
+       if settingsParseError {
+               // Don't write settings if there was a parse error
+               // because this will delete the settings.json if it
+               // is invalid. Instead we should allow the user to fix
+               // it manually.
+               return nil
+       }
+
        var err error
        if _, e := os.Stat(ConfigDir); e == nil {
+               defaults := DefaultGlobalSettings()
+
+               // remove any options froms parsedSettings that have since been marked as default
+               for k, v := range parsedSettings {
+                       if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
+                               cur, okcur := GlobalSettings[k]
+                               if def, ok := defaults[k]; ok && okcur && reflect.DeepEqual(cur, def) {
+                                       delete(parsedSettings, k)
+                               }
+                       }
+               }
+
+               // add any options to parsedSettings that have since been marked as non-default
                for k, v := range GlobalSettings {
-                       parsedSettings[k] = v
+                       if def, ok := defaults[k]; !ok || !reflect.DeepEqual(v, def) {
+                               if _, wr := ModifiedSettings[k]; wr {
+                                       parsedSettings[k] = v
+                               }
+                       }
                }
 
                txt, _ := json.MarshalIndent(parsedSettings, "", "    ")
@@ -114,12 +192,60 @@ func WriteSettings(filename string) error {
        return err
 }
 
-// AddOption creates a new option. This is meant to be called by plugins to add options.
-func AddOption(name string, value interface{}) error {
-       GlobalSettings[name] = value
-       err := WriteSettings(ConfigDir + "/settings.json")
-       if err != nil {
-               return errors.New("Error writing settings.json file: " + err.Error())
+// OverwriteSettings writes the current settings to settings.json and
+// resets any user configuration of local settings present in settings.json
+func OverwriteSettings(filename string) error {
+       settings := make(map[string]interface{})
+
+       var err error
+       if _, e := os.Stat(ConfigDir); e == nil {
+               defaults := DefaultGlobalSettings()
+               for k, v := range GlobalSettings {
+                       if def, ok := defaults[k]; !ok || !reflect.DeepEqual(v, def) {
+                               if _, wr := ModifiedSettings[k]; wr {
+                                       settings[k] = v
+                               }
+                       }
+               }
+
+               txt, _ := json.MarshalIndent(settings, "", "    ")
+               err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
+       }
+       return err
+}
+
+// RegisterCommonOptionPlug creates a new option (called pl.name). This is meant to be called by plugins to add options.
+func RegisterCommonOptionPlug(pl string, name string, defaultvalue interface{}) error {
+       name = pl + "." + name
+       if _, ok := GlobalSettings[name]; !ok {
+               defaultCommonSettings[name] = defaultvalue
+               GlobalSettings[name] = defaultvalue
+               err := WriteSettings(filepath.Join(ConfigDir, "settings.json"))
+               if err != nil {
+                       return errors.New("Error writing settings.json file: " + err.Error())
+               }
+       } else {
+               defaultCommonSettings[name] = defaultvalue
+       }
+       return nil
+}
+
+// RegisterGlobalOptionPlug creates a new global-only option (named pl.name)
+func RegisterGlobalOptionPlug(pl string, name string, defaultvalue interface{}) error {
+       return RegisterGlobalOption(pl+"."+name, defaultvalue)
+}
+
+// RegisterGlobalOption creates a new global-only option
+func RegisterGlobalOption(name string, defaultvalue interface{}) error {
+       if v, ok := GlobalSettings[name]; !ok {
+               DefaultGlobalOnlySettings[name] = defaultvalue
+               GlobalSettings[name] = defaultvalue
+               err := WriteSettings(filepath.Join(ConfigDir, "settings.json"))
+               if err != nil {
+                       return errors.New("Error writing settings.json file: " + err.Error())
+               }
+       } else {
+               DefaultGlobalOnlySettings[name] = v
        }
        return nil
 }
@@ -129,41 +255,50 @@ func GetGlobalOption(name string) interface{} {
        return GlobalSettings[name]
 }
 
-func DefaultCommonSettings() map[string]interface{} {
-       return map[string]interface{}{
-               "autoindent":     true,
-               "autosave":       false,
-               "basename":       false,
-               "colorcolumn":    float64(0),
-               "cursorline":     true,
-               "encoding":       "utf-8",
-               "eofnewline":     false,
-               "fastdirty":      true,
-               "fileformat":     "unix",
-               "hidehelp":       false,
-               "ignorecase":     false,
-               "indentchar":     " ",
-               "keepautoindent": false,
-               "matchbrace":     false,
-               "matchbraceleft": false,
-               "rmtrailingws":   false,
-               "ruler":          true,
-               "savecursor":     false,
-               "saveundo":       false,
-               "scrollbar":      false,
-               "scrollmargin":   float64(3),
-               "scrollspeed":    float64(2),
-               "softwrap":       false,
-               "smartpaste":     true,
-               "splitbottom":    true,
-               "splitright":     true,
-               "statusline":     true,
-               "syntax":         true,
-               "tabmovement":    false,
-               "tabsize":        float64(4),
-               "tabstospaces":   false,
-               "useprimary":     true,
-       }
+var defaultCommonSettings = map[string]interface{}{
+       "autoindent":     true,
+       "autosu":         false,
+       "backup":         true,
+       "backupdir":      "",
+       "basename":       false,
+       "colorcolumn":    float64(0),
+       "cursorline":     true,
+       "diffgutter":     false,
+       "encoding":       "utf-8",
+       "eofnewline":     true,
+       "fastdirty":      false,
+       "fileformat":     "unix",
+       "filetype":       "unknown",
+       "hlsearch":       false,
+       "incsearch":      true,
+       "ignorecase":     true,
+       "indentchar":     " ",
+       "keepautoindent": false,
+       "matchbrace":     true,
+       "mkparents":      false,
+       "permbackup":     false,
+       "readonly":       false,
+       "rmtrailingws":   false,
+       "ruler":          true,
+       "relativeruler":  false,
+       "savecursor":     false,
+       "saveundo":       false,
+       "scrollbar":      false,
+       "scrollmargin":   float64(3),
+       "scrollspeed":    float64(2),
+       "smartpaste":     true,
+       "softwrap":       false,
+       "splitbottom":    true,
+       "splitright":     true,
+       "statusformatl":  "$(filename) $(modified)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)",
+       "statusformatr":  "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help",
+       "statusline":     true,
+       "syntax":         true,
+       "tabmovement":    false,
+       "tabsize":        float64(4),
+       "tabstospaces":   false,
+       "useprimary":     true,
+       "wordwrap":       false,
 }
 
 func GetInfoBarOffset() int {
@@ -177,30 +312,71 @@ func GetInfoBarOffset() int {
        return offset
 }
 
+// DefaultCommonSettings returns the default global settings for micro
+// Note that colorscheme is a global only option
+func DefaultCommonSettings() map[string]interface{} {
+       commonsettings := make(map[string]interface{})
+       for k, v := range defaultCommonSettings {
+               commonsettings[k] = v
+       }
+       return commonsettings
+}
+
+// a list of settings that should only be globally modified and their
+// default values
+var DefaultGlobalOnlySettings = map[string]interface{}{
+       "autosave":       float64(0),
+       "clipboard":      "external",
+       "colorscheme":    "default",
+       "divchars":       "|-",
+       "divreverse":     true,
+       "infobar":        true,
+       "keymenu":        false,
+       "mouse":          true,
+       "parsecursor":    false,
+       "paste":          false,
+       "pluginchannels": []string{"https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json"},
+       "pluginrepos":    []string{},
+       "savehistory":    true,
+       "sucmd":          "sudo",
+       "tabhighlight":   false,
+       "tabreverse":     true,
+       "xterm":          false,
+}
+
+// a list of settings that should never be globally modified
+var LocalSettings = []string{
+       "filetype",
+       "readonly",
+}
+
 // DefaultGlobalSettings returns the default global settings for micro
 // Note that colorscheme is a global only option
 func DefaultGlobalSettings() map[string]interface{} {
-       common := DefaultCommonSettings()
-       common["colorscheme"] = "default"
-       common["infobar"] = true
-       common["keymenu"] = false
-       common["mouse"] = true
-       common["pluginchannels"] = []string{"https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json"}
-       common["pluginrepos"] = []string{}
-       common["savehistory"] = true
-       common["sucmd"] = "sudo"
-       common["termtitle"] = false
-       return common
+       globalsettings := make(map[string]interface{})
+       for k, v := range defaultCommonSettings {
+               globalsettings[k] = v
+       }
+       for k, v := range DefaultGlobalOnlySettings {
+               globalsettings[k] = v
+       }
+       return globalsettings
 }
 
-// DefaultLocalSettings returns the default local settings
-// Note that filetype is a local only option
-func DefaultLocalSettings() map[string]interface{} {
-       common := DefaultCommonSettings()
-       common["filetype"] = "unknown"
-       return common
+// DefaultAllSettings returns a map of all settings and their
+// default values (both common and global settings)
+func DefaultAllSettings() map[string]interface{} {
+       allsettings := make(map[string]interface{})
+       for k, v := range defaultCommonSettings {
+               allsettings[k] = v
+       }
+       for k, v := range DefaultGlobalOnlySettings {
+               allsettings[k] = v
+       }
+       return allsettings
 }
 
+// GetNativeValue parses and validates a value for a given option
 func GetNativeValue(option string, realValue interface{}, value string) (interface{}, error) {
        var native interface{}
        kind := reflect.TypeOf(realValue).Kind()
@@ -281,6 +457,22 @@ func validateColorscheme(option string, value interface{}) error {
        return nil
 }
 
+func validateClipboard(option string, value interface{}) error {
+       val, ok := value.(string)
+
+       if !ok {
+               return errors.New("Expected string type for clipboard")
+       }
+
+       switch val {
+       case "internal", "external", "terminal":
+       default:
+               return errors.New(option + " must be 'internal', 'external', or 'terminal'")
+       }
+
+       return nil
+}
+
 func validateLineEnding(option string, value interface{}) error {
        endingType, ok := value.(string)