diff --git a/cmd/micro/micro.go b/cmd/micro/micro.go index 7a3b21717d..4445a34812 100644 --- a/cmd/micro/micro.go +++ b/cmd/micro/micro.go @@ -273,6 +273,10 @@ func main() { screen.TermMessage(err) continue } + if err = config.OptionIsValid(k, nativeValue); err != nil { + screen.TermMessage(err) + continue + } config.GlobalSettings[k] = nativeValue config.VolatileSettings[k] = true } @@ -352,9 +356,9 @@ func main() { log.Println(clipErr, " or change 'clipboard' option") } + config.StartAutoSave() if a := config.GetGlobalOption("autosave").(float64); a > 0 { - config.SetAutoTime(int(a)) - config.StartAutoSave() + config.SetAutoTime(a) } screen.Events = make(chan tcell.Event) diff --git a/internal/action/command.go b/internal/action/command.go index f88660daac..5f56fcdbf3 100644 --- a/internal/action/command.go +++ b/internal/action/command.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "path/filepath" + "reflect" "regexp" "strconv" "strings" @@ -357,10 +358,24 @@ func reloadRuntime(reloadPlugins bool) { err := config.ReadSettings() if err != nil { screen.TermMessage(err) - } - err = config.InitGlobalSettings() - if err != nil { - screen.TermMessage(err) + } else { + parsedSettings := config.ParsedSettings() + defaultSettings := config.DefaultAllSettings() + for k := range defaultSettings { + if _, ok := config.VolatileSettings[k]; ok { + // reload should not override volatile settings + continue + } + + if _, ok := parsedSettings[k]; ok { + err = doSetGlobalOptionNative(k, parsedSettings[k]) + } else { + err = doSetGlobalOptionNative(k, defaultSettings[k]) + } + if err != nil { + screen.TermMessage(err) + } + } } if reloadPlugins { @@ -393,7 +408,7 @@ func reloadRuntime(reloadPlugins bool) { screen.TermMessage(err) } for _, b := range buffer.OpenBuffers { - b.UpdateRules() + b.ReloadSettings(true) } } @@ -512,16 +527,11 @@ func (h *BufPane) NewTabCmd(args []string) { } } -func SetGlobalOptionNative(option string, nativeValue interface{}) error { - // check for local option first... - for _, s := range config.LocalSettings { - if s == option { - MainTab().CurPane().Buf.SetOptionNative(option, nativeValue) - return nil - } +func doSetGlobalOptionNative(option string, nativeValue interface{}) error { + if reflect.DeepEqual(config.GlobalSettings[option], nativeValue) { + return nil } - // ...if it's not local continue with the globals config.GlobalSettings[option] = nativeValue config.ModifiedSettings[option] = true delete(config.VolatileSettings, option) @@ -542,8 +552,7 @@ func SetGlobalOptionNative(option string, nativeValue interface{}) error { } } else if option == "autosave" { if nativeValue.(float64) > 0 { - config.SetAutoTime(int(nativeValue.(float64))) - config.StartAutoSave() + config.SetAutoTime(nativeValue.(float64)) } else { config.SetAutoTime(0) } @@ -574,8 +583,30 @@ func SetGlobalOptionNative(option string, nativeValue interface{}) error { } } + return nil +} + +func SetGlobalOptionNative(option string, nativeValue interface{}) error { + if err := config.OptionIsValid(option, nativeValue); err != nil { + return err + } + + // check for local option first... + for _, s := range config.LocalSettings { + if s == option { + return MainTab().CurPane().Buf.SetOptionNative(option, nativeValue) + } + } + + // ...if it's not local continue with the globals... + if err := doSetGlobalOptionNative(option, nativeValue); err != nil { + return err + } + + // ...at last check the buffer locals for _, b := range buffer.OpenBuffers { - b.SetOptionNative(option, nativeValue) + b.DoSetOptionNative(option, nativeValue) + delete(b.LocalSettings, option) } return config.WriteSettings(filepath.Join(config.ConfigDir, "settings.json")) @@ -602,16 +633,10 @@ func (h *BufPane) ResetCmd(args []string) { } option := args[0] + defaults := config.DefaultAllSettings() - defaultGlobals := config.DefaultGlobalSettings() - defaultLocals := config.DefaultCommonSettings() - - if _, ok := defaultGlobals[option]; ok { - SetGlobalOptionNative(option, defaultGlobals[option]) - return - } - if _, ok := defaultLocals[option]; ok { - h.Buf.SetOptionNative(option, defaultLocals[option]) + if _, ok := defaults[option]; ok { + SetGlobalOptionNative(option, defaults[option]) return } InfoBar.Error(config.ErrInvalidOption) diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index 15cfe33564..c623fd586b 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -86,6 +86,8 @@ type SharedBuffer struct { // Settings customized by the user Settings map[string]interface{} + // LocalSettings customized by the user for this buffer only + LocalSettings map[string]bool Suggestions []string Completions []string @@ -326,6 +328,7 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT // assigning the filetype. settings := config.DefaultCommonSettings() b.Settings = config.DefaultCommonSettings() + b.LocalSettings = make(map[string]bool) for k, v := range config.GlobalSettings { if _, ok := config.DefaultGlobalOnlySettings[k]; !ok { // make sure setting is not global-only @@ -364,6 +367,9 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT case "dos": ff = FFDos } + } else { + // in case of autodetection treat as locally set + b.LocalSettings["fileformat"] = true } b.LineArray = NewLineArray(uint64(size), ff, reader) diff --git a/internal/buffer/settings.go b/internal/buffer/settings.go index 9d671dcf22..838df4a568 100644 --- a/internal/buffer/settings.go +++ b/internal/buffer/settings.go @@ -2,12 +2,54 @@ package buffer import ( "crypto/md5" + "reflect" "github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/screen" ) -func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error { +func (b *Buffer) ReloadSettings(reloadFiletype bool) { + settings := config.ParsedSettings() + + if _, ok := b.LocalSettings["filetype"]; !ok && reloadFiletype { + // need to update filetype before updating other settings based on it + b.Settings["filetype"] = "unknown" + if v, ok := settings["filetype"]; ok { + b.Settings["filetype"] = v + } + } + + // update syntax rules, which will also update filetype if needed + b.UpdateRules() + settings["filetype"] = b.Settings["filetype"] + + config.InitLocalSettings(settings, b.Path) + for k, v := range config.DefaultCommonSettings() { + if k == "filetype" { + // prevent recursion + continue + } + if _, ok := config.VolatileSettings[k]; ok { + // reload should not override volatile settings + continue + } + if _, ok := b.LocalSettings[k]; ok { + // reload should not override local settings + continue + } + if _, ok := settings[k]; ok { + b.DoSetOptionNative(k, settings[k]) + } else { + b.DoSetOptionNative(k, v) + } + } +} + +func (b *Buffer) DoSetOptionNative(option string, nativeValue interface{}) { + if reflect.DeepEqual(b.Settings[option], nativeValue) { + return + } + b.Settings[option] = nativeValue if option == "fastdirty" { @@ -26,17 +68,7 @@ func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error { } else if option == "statusline" { screen.Redraw() } else if option == "filetype" { - config.InitRuntimeFiles(true) - err := config.ReadSettings() - if err != nil { - screen.TermMessage(err) - } - err = config.InitGlobalSettings() - if err != nil { - screen.TermMessage(err) - } - config.InitLocalSettings(b.Settings, b.Path) - b.UpdateRules() + b.ReloadSettings(false) } else if option == "fileformat" { switch b.Settings["fileformat"].(string) { case "unix": @@ -85,6 +117,15 @@ func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error { if b.OptionCallback != nil { b.OptionCallback(option, nativeValue) } +} + +func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error { + if err := config.OptionIsValid(option, nativeValue); err != nil { + return err + } + + b.DoSetOptionNative(option, nativeValue) + b.LocalSettings[option] = true return nil } diff --git a/internal/config/autosave.go b/internal/config/autosave.go index 2d0f8cec30..0dbb2989b0 100644 --- a/internal/config/autosave.go +++ b/internal/config/autosave.go @@ -1,44 +1,49 @@ package config import ( - "sync" "time" ) var Autosave chan bool -var autotime int - -// lock for autosave -var autolock sync.Mutex +var autotime chan float64 func init() { Autosave = make(chan bool) + autotime = make(chan float64) } -func SetAutoTime(a int) { - autolock.Lock() - autotime = a - autolock.Unlock() -} - -func GetAutoTime() int { - autolock.Lock() - a := autotime - autolock.Unlock() - return a +func SetAutoTime(a float64) { + autotime <- a } func StartAutoSave() { go func() { + var a float64 + var t *time.Timer + var elapsed <-chan time.Time for { - autolock.Lock() - a := autotime - autolock.Unlock() - if a < 1 { - break + select { + case a = <-autotime: + if t != nil { + t.Stop() + for len(elapsed) > 0 { + <-elapsed + } + } + if a > 0 { + if t != nil { + t.Reset(time.Duration(a * float64(time.Second))) + } else { + t = time.NewTimer(time.Duration(a * float64(time.Second))) + elapsed = t.C + } + } + case <-elapsed: + if a > 0 { + t.Reset(time.Duration(a * float64(time.Second))) + Autosave <- true + } } - time.Sleep(time.Duration(a) * time.Second) - Autosave <- true } }() } diff --git a/internal/config/settings.go b/internal/config/settings.go index d25f0d0794..b8cfcd5166 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -154,10 +154,67 @@ var ( func init() { ModifiedSettings = make(map[string]bool) VolatileSettings = make(map[string]bool) - parsedSettings = make(map[string]interface{}) +} + +func validateParsedSettings() error { + var err error + defaults := DefaultAllSettings() + for k, v := range parsedSettings { + if strings.HasPrefix(reflect.TypeOf(v).String(), "map") { + if strings.HasPrefix(k, "ft:") { + for k1, v1 := range v.(map[string]interface{}) { + if _, ok := defaults[k1]; ok { + if e := verifySetting(k1, v1, defaults[k1]); e != nil { + err = e + parsedSettings[k].(map[string]interface{})[k1] = defaults[k1] + continue + } + } + } + } else { + if _, e := glob.Compile(k); e != nil { + err = errors.New("Error with glob setting " + k + ": " + e.Error()) + delete(parsedSettings, k) + continue + } + for k1, v1 := range v.(map[string]interface{}) { + if _, ok := defaults[k1]; ok { + if e := verifySetting(k1, v1, defaults[k1]); e != nil { + err = e + parsedSettings[k].(map[string]interface{})[k1] = defaults[k1] + continue + } + } + } + } + continue + } + + if k == "autosave" { + // if autosave is a boolean convert it to float + s, ok := v.(bool) + if ok { + if s { + parsedSettings["autosave"] = 8.0 + } else { + parsedSettings["autosave"] = 0.0 + } + } + continue + } + if _, ok := defaults[k]; ok { + if e := verifySetting(k, v, defaults[k]); e != nil { + err = e + parsedSettings[k] = defaults[k] + continue + } + } + } + return err } func ReadSettings() error { + parsedSettings = make(map[string]interface{}) filename := filepath.Join(ConfigDir, "settings.json") if _, e := os.Stat(filename); e == nil { input, err := ioutil.ReadFile(filename) @@ -172,46 +229,54 @@ func ReadSettings() error { 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 - } - } + err = validateParsedSettings() + if err != nil { + return err } } } return nil } -func verifySetting(option string, value reflect.Type, def reflect.Type) bool { +func ParsedSettings() map[string]interface{} { + s := make(map[string]interface{}) + for k, v := range parsedSettings { + s[k] = v + } + return s +} + +func verifySetting(option string, value interface{}, def interface{}) error { var interfaceArr []interface{} + valType := reflect.TypeOf(value) + defType := reflect.TypeOf(def) + assignable := false + switch option { case "pluginrepos", "pluginchannels": - return value.AssignableTo(reflect.TypeOf(interfaceArr)) + assignable = valType.AssignableTo(reflect.TypeOf(interfaceArr)) default: - return def.AssignableTo(value) + assignable = defType.AssignableTo(valType) } + if !assignable { + return fmt.Errorf("Error: setting '%s' has incorrect type (%s), using default value: %v (%s)", option, valType, def, defType) + } + + if err := OptionIsValid(option, value); err != nil { + return err + } + + return nil } // InitGlobalSettings initializes the options map and sets all options to their default values // Must be called after ReadSettings func InitGlobalSettings() error { var err error - GlobalSettings = DefaultGlobalSettings() + GlobalSettings = DefaultAllSettings() 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 } } @@ -221,40 +286,25 @@ func InitGlobalSettings() error { // InitLocalSettings scans the json in settings.json and sets the options locally based // on whether the filetype or path matches ft or glob local settings // Must be called after ReadSettings -func InitLocalSettings(settings map[string]interface{}, path string) error { - var parseError error +func InitLocalSettings(settings map[string]interface{}, path string) { for k, v := range parsedSettings { if strings.HasPrefix(reflect.TypeOf(v).String(), "map") { 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 } } } else { - g, err := glob.Compile(k) - if err != nil { - parseError = errors.New("Error with glob setting " + k + ": " + err.Error()) - continue - } - + g, _ := glob.Compile(k) 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 } } } } } - return parseError } // WriteSettings writes the settings to the specified filename as JSON @@ -269,7 +319,7 @@ func WriteSettings(filename string) error { var err error if _, e := os.Stat(ConfigDir); e == nil { - defaults := DefaultGlobalSettings() + defaults := DefaultAllSettings() // remove any options froms parsedSettings that have since been marked as default for k, v := range parsedSettings { @@ -304,7 +354,7 @@ func OverwriteSettings(filename string) error { var err error if _, e := os.Stat(ConfigDir); e == nil { - defaults := DefaultGlobalSettings() + defaults := DefaultAllSettings() for k, v := range GlobalSettings { if def, ok := defaults[k]; !ok || !reflect.DeepEqual(v, def) { if _, wr := ModifiedSettings[k]; wr { @@ -370,8 +420,8 @@ func GetInfoBarOffset() int { return offset } -// DefaultCommonSettings returns the default global settings for micro -// Note that colorscheme is a global only option +// DefaultCommonSettings returns a map of all common buffer settings +// and their default values func DefaultCommonSettings() map[string]interface{} { commonsettings := make(map[string]interface{}) for k, v := range defaultCommonSettings { @@ -380,21 +430,8 @@ func DefaultCommonSettings() map[string]interface{} { return commonsettings } -// DefaultGlobalSettings returns the default global settings for micro -// Note that colorscheme is a global only option -func DefaultGlobalSettings() map[string]interface{} { - globalsettings := make(map[string]interface{}) - for k, v := range defaultCommonSettings { - globalsettings[k] = v - } - for k, v := range DefaultGlobalOnlySettings { - globalsettings[k] = v - } - return globalsettings -} - -// DefaultAllSettings returns a map of all settings and their -// default values (both common and global settings) +// DefaultAllSettings returns a map of all common buffer & global-only settings +// and their default values func DefaultAllSettings() map[string]interface{} { allsettings := make(map[string]interface{}) for k, v := range defaultCommonSettings { @@ -419,18 +456,15 @@ func GetNativeValue(option string, realValue interface{}, value string) (interfa } else if kind == reflect.String { native = value } else if kind == reflect.Float64 { - i, err := strconv.Atoi(value) + f, err := strconv.ParseFloat(value, 64) if err != nil { return nil, ErrInvalidValue } - native = float64(i) + native = f } else { return nil, ErrInvalidValue } - if err := OptionIsValid(option, native); err != nil { - return nil, err - } return native, nil } @@ -446,13 +480,13 @@ func OptionIsValid(option string, value interface{}) error { // Option validators func validatePositiveValue(option string, value interface{}) error { - tabsize, ok := value.(float64) + nativeValue, ok := value.(float64) if !ok { return errors.New("Expected numeric type for " + option) } - if tabsize < 1 { + if nativeValue < 1 { return errors.New(option + " must be greater than 0") } diff --git a/runtime/plugins/comment/comment.lua b/runtime/plugins/comment/comment.lua index 034c490497..f86da9452f 100644 --- a/runtime/plugins/comment/comment.lua +++ b/runtime/plugins/comment/comment.lua @@ -66,9 +66,9 @@ local last_ft function updateCommentType(buf) if buf.Settings["commenttype"] == nil or (last_ft ~= buf.Settings["filetype"] and last_ft ~= nil) then if ft[buf.Settings["filetype"]] ~= nil then - buf.Settings["commenttype"] = ft[buf.Settings["filetype"]] + buf:SetOptionNative("commenttype", ft[buf.Settings["filetype"]]) else - buf.Settings["commenttype"] = "# %s" + buf:SetOptionNative("commenttype", "# %s") end last_ft = buf.Settings["filetype"]