]> git.lizzy.rs Git - micro.git/blobdiff - cmd/micro/settings.go
Make settings capitalization consistent
[micro.git] / cmd / micro / settings.go
index 4df44237eaf1400a5664f696024f6a76482fd424..6a99d2cf14b6b0f7e4f27c45728b038926382ed5 100644 (file)
@@ -1,19 +1,85 @@
 package main
 
 import (
+       "crypto/md5"
        "encoding/json"
+       "errors"
        "io/ioutil"
        "os"
        "reflect"
        "strconv"
+       "strings"
+
+       "github.com/flynn/json5"
+       "github.com/zyedidia/glob"
 )
 
+type optionValidator func(string, interface{}) error
+
 // The options that the user can set
-var settings map[string]interface{}
+var globalSettings map[string]interface{}
+
+var invalidSettings bool
+
+// Options with validators
+var optionValidators = map[string]optionValidator{
+       "tabsize":      validatePositiveValue,
+       "scrollmargin": validateNonNegativeValue,
+       "scrollspeed":  validateNonNegativeValue,
+       "colorscheme":  validateColorscheme,
+       "colorcolumn":  validateNonNegativeValue,
+       "fileformat":   validateLineEnding,
+}
 
-// InitSettings initializes the options map and sets all options to their default values
-func InitSettings() {
-       defaults := DefaultSettings()
+// InitGlobalSettings initializes the options map and sets all options to their default values
+func InitGlobalSettings() {
+       invalidSettings = false
+       defaults := DefaultGlobalSettings()
+       var parsed map[string]interface{}
+
+       filename := configDir + "/settings.json"
+       writeSettings := false
+       if _, e := os.Stat(filename); e == nil {
+               input, err := ioutil.ReadFile(filename)
+               if !strings.HasPrefix(string(input), "null") {
+                       if err != nil {
+                               TermMessage("Error reading settings.json file: " + err.Error())
+                               invalidSettings = true
+                               return
+                       }
+
+                       err = json5.Unmarshal(input, &parsed)
+                       if err != nil {
+                               TermMessage("Error reading settings.json:", err.Error())
+                               invalidSettings = true
+                       }
+               } else {
+                       writeSettings = true
+               }
+       }
+
+       globalSettings = make(map[string]interface{})
+       for k, v := range defaults {
+               globalSettings[k] = v
+       }
+       for k, v := range parsed {
+               if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
+                       globalSettings[k] = v
+               }
+       }
+
+       if _, err := os.Stat(filename); os.IsNotExist(err) || writeSettings {
+               err := WriteSettings(filename)
+               if err != nil {
+                       TermMessage("Error writing settings.json file: " + err.Error())
+               }
+       }
+}
+
+// InitLocalSettings scans the json in settings.json and sets the options locally based
+// on whether the buffer matches the glob
+func InitLocalSettings(buf *Buffer) {
+       invalidSettings = false
        var parsed map[string]interface{}
 
        filename := configDir + "/settings.json"
@@ -21,126 +87,416 @@ func InitSettings() {
                input, err := ioutil.ReadFile(filename)
                if err != nil {
                        TermMessage("Error reading settings.json file: " + err.Error())
+                       invalidSettings = true
                        return
                }
 
-               err = json.Unmarshal(input, &parsed)
+               err = json5.Unmarshal(input, &parsed)
                if err != nil {
                        TermMessage("Error reading settings.json:", err.Error())
+                       invalidSettings = true
                }
        }
 
-       settings = make(map[string]interface{})
-       for k, v := range defaults {
-               settings[k] = v
-       }
        for k, v := range parsed {
-               settings[k] = v
-       }
+               if strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
+                       g, err := glob.Compile(k)
+                       if err != nil {
+                               TermMessage("Error with glob setting ", k, ": ", err)
+                               continue
+                       }
 
-       err := WriteSettings(filename)
-       if err != nil {
-               TermMessage("Error writing settings.json file: " + err.Error())
+                       if g.MatchString(buf.Path) {
+                               for k1, v1 := range v.(map[string]interface{}) {
+                                       buf.Settings[k1] = v1
+                               }
+                       }
+               }
        }
 }
 
 // WriteSettings writes the settings to the specified filename as JSON
 func WriteSettings(filename string) error {
+       if invalidSettings {
+               // Do not write the settings if there was an error when reading them
+               return nil
+       }
+
        var err error
        if _, e := os.Stat(configDir); e == nil {
-               txt, _ := json.MarshalIndent(settings, "", "    ")
-               err = ioutil.WriteFile(filename, txt, 0644)
+               parsed := make(map[string]interface{})
+
+               filename := configDir + "/settings.json"
+               for k, v := range globalSettings {
+                       parsed[k] = v
+               }
+               if _, e := os.Stat(filename); e == nil {
+                       input, err := ioutil.ReadFile(filename)
+                       if string(input) != "null" {
+                               if err != nil {
+                                       return err
+                               }
+
+                               err = json5.Unmarshal(input, &parsed)
+                               if err != nil {
+                                       TermMessage("Error reading settings.json:", err.Error())
+                                       invalidSettings = true
+                               }
+
+                               for k, v := range parsed {
+                                       if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
+                                               if _, ok := globalSettings[k]; ok {
+                                                       parsed[k] = globalSettings[k]
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               txt, _ := json.MarshalIndent(parsed, "", "    ")
+               err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
        }
        return err
 }
 
 // AddOption creates a new option. This is meant to be called by plugins to add options.
 func AddOption(name string, value interface{}) {
-       settings[name] = value
+       globalSettings[name] = value
        err := WriteSettings(configDir + "/settings.json")
        if err != nil {
                TermMessage("Error writing settings.json file: " + err.Error())
        }
 }
 
-// GetOption returns the specified option. This is meant to be called by plugins to add options.
+// GetGlobalOption returns the global value of the given option
+func GetGlobalOption(name string) interface{} {
+       return globalSettings[name]
+}
+
+// GetLocalOption returns the local value of the given option
+func GetLocalOption(name string, buf *Buffer) interface{} {
+       return buf.Settings[name]
+}
+
+// GetOption returns the value of the given option
+// If there is a local version of the option, it returns that
+// otherwise it will return the global version
 func GetOption(name string) interface{} {
-       return settings[name]
+       if GetLocalOption(name, CurView().Buf) != nil {
+               return GetLocalOption(name, CurView().Buf)
+       }
+       return GetGlobalOption(name)
 }
 
-// DefaultSettings returns the default settings for micro
-func DefaultSettings() map[string]interface{} {
+// DefaultGlobalSettings returns the default global settings for micro
+// Note that colorscheme is a global only option
+func DefaultGlobalSettings() map[string]interface{} {
        return map[string]interface{}{
-               "autoindent":   true,
-               "colorscheme":  "monokai",
-               "cursorline":   false,
-               "ignorecase":   false,
-               "indentchar":   " ",
-               "ruler":        true,
-               "savecursor":   false,
-               "saveundo":     false,
-               "scrollspeed":  float64(2),
-               "scrollmargin": float64(3),
-               "statusline":   true,
-               "syntax":       true,
-               "tabsize":      float64(4),
-               "tabstospaces": false,
-       }
-}
-
-// SetOption prompts the user to set an option and checks that the response is valid
-func SetOption(option, value string) {
-       filename := configDir + "/settings.json"
-       if _, ok := settings[option]; !ok {
-               messenger.Error(option + " is not a valid option")
-               return
+               "autoindent":     true,
+               "keepautoindent": false,
+               "autosave":       false,
+               "colorcolumn":    float64(0),
+               "colorscheme":    "default",
+               "cursorline":     true,
+               "eofnewline":     false,
+               "fastdirty":      true,
+               "fileformat":     "unix",
+               "ignorecase":     false,
+               "indentchar":     " ",
+               "infobar":        true,
+               "keymenu":        false,
+               "mouse":          true,
+               "rmtrailingws":   false,
+               "ruler":          true,
+               "savecursor":     false,
+               "saveundo":       false,
+               "scrollspeed":    float64(2),
+               "scrollmargin":   float64(3),
+               "softwrap":       false,
+               "splitright":     true,
+               "splitbottom":    true,
+               "statusline":     true,
+               "sucmd":          "sudo",
+               "syntax":         true,
+               "tabmovement":    false,
+               "tabsize":        float64(4),
+               "tabstospaces":   false,
+               "termtitle":      false,
+               "pluginchannels": []string{
+                       "https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json",
+               },
+               "pluginrepos": []string{},
+               "useprimary":  true,
+       }
+}
+
+// DefaultLocalSettings returns the default local settings
+// Note that filetype is a local only option
+func DefaultLocalSettings() map[string]interface{} {
+       return map[string]interface{}{
+               "autoindent":     true,
+               "keepautoindent": false,
+               "autosave":       false,
+               "colorcolumn":    float64(0),
+               "cursorline":     true,
+               "eofnewline":     false,
+               "fastdirty":      true,
+               "fileformat":     "unix",
+               "filetype":       "Unknown",
+               "ignorecase":     false,
+               "indentchar":     " ",
+               "rmtrailingws":   false,
+               "ruler":          true,
+               "savecursor":     false,
+               "saveundo":       false,
+               "scrollspeed":    float64(2),
+               "scrollmargin":   float64(3),
+               "softwrap":       false,
+               "splitright":     true,
+               "splitbottom":    true,
+               "statusline":     true,
+               "syntax":         true,
+               "tabmovement":    false,
+               "tabsize":        float64(4),
+               "tabstospaces":   false,
+               "useprimary":     true,
+       }
+}
+
+// SetOption attempts to set the given option to the value
+// By default it will set the option as global, but if the option
+// is local only it will set the local version
+// Use setlocal to force an option to be set locally
+func SetOption(option, value string) error {
+       if _, ok := globalSettings[option]; !ok {
+               if _, ok := CurView().Buf.Settings[option]; !ok {
+                       return errors.New("Invalid option")
+               }
+               SetLocalOption(option, value, CurView())
+               return nil
        }
 
-       kind := reflect.TypeOf(settings[option]).Kind()
+       var nativeValue interface{}
+
+       kind := reflect.TypeOf(globalSettings[option]).Kind()
        if kind == reflect.Bool {
                b, err := ParseBool(value)
                if err != nil {
-                       messenger.Error("Invalid value for " + option)
-                       return
+                       return errors.New("Invalid value")
                }
-               settings[option] = b
+               nativeValue = b
        } else if kind == reflect.String {
-               settings[option] = value
+               nativeValue = value
        } else if kind == reflect.Float64 {
                i, err := strconv.Atoi(value)
                if err != nil {
-                       messenger.Error("Invalid value for " + option)
-                       return
+                       return errors.New("Invalid value")
                }
-               settings[option] = float64(i)
+               nativeValue = float64(i)
+       } else {
+               return errors.New("Option has unsupported value type")
+       }
+
+       if err := optionIsValid(option, nativeValue); err != nil {
+               return err
        }
 
+       globalSettings[option] = nativeValue
+
        if option == "colorscheme" {
-               LoadSyntaxFiles()
+               // LoadSyntaxFiles()
+               InitColorscheme()
                for _, tab := range tabs {
                        for _, view := range tab.views {
                                view.Buf.UpdateRules()
-                               if settings["syntax"].(bool) {
-                                       view.matches = Match(view)
-                               }
                        }
                }
        }
 
-       if option == "statusline" {
+       if option == "infobar" || option == "keymenu" {
+               for _, tab := range tabs {
+                       tab.Resize()
+               }
+       }
+
+       if option == "mouse" {
+               if !nativeValue.(bool) {
+                       screen.DisableMouse()
+               } else {
+                       screen.EnableMouse()
+               }
+       }
+
+       if _, ok := CurView().Buf.Settings[option]; ok {
                for _, tab := range tabs {
                        for _, view := range tab.views {
-                               view.ToggleStatusLine()
-                               if settings["syntax"].(bool) {
-                                       view.matches = Match(view)
+                               SetLocalOption(option, value, view)
+                       }
+               }
+       }
+
+       return nil
+}
+
+// SetLocalOption sets the local version of this option
+func SetLocalOption(option, value string, view *View) error {
+       buf := view.Buf
+       if _, ok := buf.Settings[option]; !ok {
+               return errors.New("Invalid option")
+       }
+
+       var nativeValue interface{}
+
+       kind := reflect.TypeOf(buf.Settings[option]).Kind()
+       if kind == reflect.Bool {
+               b, err := ParseBool(value)
+               if err != nil {
+                       return errors.New("Invalid value")
+               }
+               nativeValue = b
+       } else if kind == reflect.String {
+               nativeValue = value
+       } else if kind == reflect.Float64 {
+               i, err := strconv.Atoi(value)
+               if err != nil {
+                       return errors.New("Invalid value")
+               }
+               nativeValue = float64(i)
+       } else {
+               return errors.New("Option has unsupported value type")
+       }
+
+       if err := optionIsValid(option, nativeValue); err != nil {
+               return err
+       }
+
+       if option == "fastdirty" {
+               // If it is being turned off, we have to hash every open buffer
+               var empty [16]byte
+               for _, tab := range tabs {
+                       for _, v := range tab.views {
+                               if !nativeValue.(bool) {
+                                       if v.Buf.origHash == empty {
+                                               data, err := ioutil.ReadFile(v.Buf.AbsPath)
+                                               if err != nil {
+                                                       data = []byte{}
+                                               }
+                                               v.Buf.origHash = md5.Sum(data)
+                                       }
+                               } else {
+                                       v.Buf.IsModified = v.Buf.Modified()
                                }
                        }
                }
        }
 
-       err := WriteSettings(filename)
+       buf.Settings[option] = nativeValue
+
+       if option == "statusline" {
+               view.ToggleStatusLine()
+       }
+
+       if option == "filetype" {
+               // LoadSyntaxFiles()
+               InitColorscheme()
+               buf.UpdateRules()
+       }
+
+       if option == "fileformat" {
+               buf.IsModified = true
+       }
+
+       if option == "syntax" {
+               if !nativeValue.(bool) {
+                       buf.ClearMatches()
+               } else {
+                       buf.highlighter.HighlightStates(buf)
+               }
+       }
+
+       return nil
+}
+
+// SetOptionAndSettings sets the given option and saves the option setting to the settings config file
+func SetOptionAndSettings(option, value string) {
+       filename := configDir + "/settings.json"
+
+       err := SetOption(option, value)
+
+       if err != nil {
+               messenger.Error(err.Error())
+               return
+       }
+
+       err = WriteSettings(filename)
        if err != nil {
                messenger.Error("Error writing to settings.json: " + err.Error())
                return
        }
 }
+
+func optionIsValid(option string, value interface{}) error {
+       if validator, ok := optionValidators[option]; ok {
+               return validator(option, value)
+       }
+
+       return nil
+}
+
+// Option validators
+
+func validatePositiveValue(option string, value interface{}) error {
+       tabsize, ok := value.(float64)
+
+       if !ok {
+               return errors.New("Expected numeric type for " + option)
+       }
+
+       if tabsize < 1 {
+               return errors.New(option + " must be greater than 0")
+       }
+
+       return nil
+}
+
+func validateNonNegativeValue(option string, value interface{}) error {
+       nativeValue, ok := value.(float64)
+
+       if !ok {
+               return errors.New("Expected numeric type for " + option)
+       }
+
+       if nativeValue < 0 {
+               return errors.New(option + " must be non-negative")
+       }
+
+       return nil
+}
+
+func validateColorscheme(option string, value interface{}) error {
+       colorscheme, ok := value.(string)
+
+       if !ok {
+               return errors.New("Expected string type for colorscheme")
+       }
+
+       if !ColorschemeExists(colorscheme) {
+               return errors.New(colorscheme + " is not a valid colorscheme")
+       }
+
+       return nil
+}
+
+func validateLineEnding(option string, value interface{}) error {
+       endingType, ok := value.(string)
+
+       if !ok {
+               return errors.New("Expected string type for file format")
+       }
+
+       if endingType != "unix" && endingType != "dos" {
+               return errors.New("File format must be either 'unix' or 'dos'")
+       }
+
+       return nil
+}