]> git.donarmstrong.com Git - dactyl.git/blobdiff - common/modules/options.jsm
Import 1.0rc1 supporting Firefox up to 11.*
[dactyl.git] / common / modules / options.jsm
index a09fef257b3dc149ab1b8a53c1b20b054e2e0984..512e304f789b800506c8fe4be6fa6e13f6d6f223 100644 (file)
@@ -4,17 +4,18 @@
 //
 // This work is licensed for reuse under an MIT license. Details are
 // given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
 
 try {
 
 Components.utils.import("resource://dactyl/bootstrap.jsm");
 defineModule("options", {
     exports: ["Option", "Options", "ValueError", "options"],
-    require: ["messages", "storage"],
-    use: ["commands", "completion", "config", "prefs", "services", "styles", "template", "util"]
+    require: ["contexts", "messages", "storage"]
 }, this);
 
+this.lazyRequire("config", ["config"]);
+
 /** @scope modules */
 
 let ValueError = Class("ValueError", ErrorBase);
@@ -47,32 +48,13 @@ var Option = Class("Option", {
     init: function init(modules, names, description, defaultValue, extraInfo) {
         this.modules = modules;
         this.name = names[0];
-        this.names = names;
         this.realNames = names;
         this.description = description;
 
         if (extraInfo)
             this.update(extraInfo);
 
-        if (Set.has(this.modules.config.defaults, this.name))
-            defaultValue = this.modules.config.defaults[this.name];
-
-        if (defaultValue !== undefined) {
-            if (this.type === "string")
-                defaultValue = Commands.quote(defaultValue);
-
-            if (isObject(defaultValue))
-                defaultValue = iter(defaultValue).map(function (val) val.map(Option.quote).join(":")).join(",");
-
-            if (isArray(defaultValue))
-                defaultValue = defaultValue.map(Option.quote).join(",");
-
-            this.defaultValue = this.parse(defaultValue);
-        }
-
-        // add no{option} variant of boolean {option} to this.names
-        if (this.type == "boolean")
-            this.names = array([name, "no" + name] for (name in values(names))).flatten().array;
+        this._defaultValue = defaultValue;
 
         if (this.globalValue == undefined && !this.initialValue)
             this.globalValue = this.defaultValue;
@@ -101,8 +83,15 @@ var Option = Class("Option", {
     },
 
     /** @property {value} The option's global value. @see #scope */
-    get globalValue() { try { return options.store.get(this.name, {}).value; } catch (e) { util.reportError(e); throw e; } },
-    set globalValue(val) { options.store.set(this.name, { value: val, time: Date.now() }); },
+    get globalValue() {
+        let val = options.store.get(this.name, {}).value;
+        if (val != null)
+            return val;
+        return this.globalValue = this.defaultValue;
+    },
+    set globalValue(val) {
+        options.store.set(this.name, { value: val, time: Date.now() });
+    },
 
     /**
      * Returns *value* as an array of parsed values if the option type is
@@ -268,8 +257,9 @@ var Option = Class("Option", {
 
     /** @property {string} The option's canonical name. */
     name: null,
+
     /** @property {[string]} All names by which this option is identified. */
-    names: null,
+    names: Class.Memoize(function () this.realNames),
 
     /**
      * @property {string} The option's data type. One of:
@@ -330,7 +320,32 @@ var Option = Class("Option", {
      *     unless the option is explicitly set either interactively or in an RC
      *     file or plugin.
      */
-    defaultValue: null,
+    defaultValue: Class.Memoize(function () {
+        let defaultValue = this._defaultValue;
+        delete this._defaultValue;
+
+        if (Set.has(this.modules.config.optionDefaults, this.name))
+            defaultValue = this.modules.config.optionDefaults[this.name];
+
+        if (defaultValue == null && this.getter)
+            defaultValue = this.getter();
+
+        if (defaultValue == undefined)
+            return null;
+
+        if (this.type === "string")
+            defaultValue = Commands.quote(defaultValue);
+
+        if (isArray(defaultValue))
+            defaultValue = defaultValue.map(Option.quote).join(",");
+        else if (isObject(defaultValue))
+            defaultValue = iter(defaultValue).map(function (val) val.map(Option.quote).join(":")).join(",");
+
+        if (isArray(defaultValue))
+            defaultValue = defaultValue.map(Option.quote).join(",");
+
+        return this.parse(defaultValue);
+    }),
 
     /**
      * @property {function} The function called when the option value is read.
@@ -425,6 +440,7 @@ var Option = Class("Option", {
         let re = util.regexp(Option.dequote(val), flags);
         re.bang = bang;
         re.result = result !== undefined ? result : !bang;
+        re.key = re.bang + Option.quote(util.regexp.getSource(re), /^!|:/);
         re.toString = function () Option.unparseRegexp(this, keepQuotes);
         return re;
     },
@@ -597,6 +613,23 @@ var Option = Class("Option", {
             return null;
         },
 
+        string: function string(operator, values, scope, invert) {
+            if (invert)
+                return values[(values.indexOf(this.value) + 1) % values.length];
+
+            switch (operator) {
+            case "+":
+                return this.value + values;
+            case "-":
+                return this.value.replace(values, "");
+            case "^":
+                return values + this.value;
+            case "=":
+                return values;
+            }
+            return null;
+        },
+
         stringmap: function stringmap(operator, values, scope, invert) {
             let res = update({}, this.value);
 
@@ -654,23 +687,7 @@ var Option = Class("Option", {
         get regexplist() this.stringlist,
         get regexpmap() this.stringlist,
         get sitelist() this.stringlist,
-        get sitemap() this.stringlist,
-
-        string: function string(operator, values, scope, invert) {
-            if (invert)
-                return values[(values.indexOf(this.value) + 1) % values.length];
-            switch (operator) {
-            case "+":
-                return this.value + values;
-            case "-":
-                return this.value.replace(values, "");
-            case "^":
-                return values + this.value;
-            case "=":
-                return values;
-            }
-            return null;
-        }
+        get sitemap() this.stringlist
     },
 
     validIf: function validIf(test, error) {
@@ -686,23 +703,31 @@ var Option = Class("Option", {
      * @param {value|[string]} values The value or array of values to validate.
      * @returns {boolean}
      */
-    validateCompleter: function validateCompleter(values) {
-        if (this.values)
-            var acceptable = this.values.array || this.values;
-        else {
+    validateCompleter: function validateCompleter(vals) {
+        function completions(extra) {
             let context = CompletionContext("");
-            acceptable = context.fork("", 0, this, this.completer);
-            if (!acceptable)
-                acceptable = context.allItems.items.map(function (item) [item.text]);
+            return context.fork("", 0, this, this.completer, extra) ||
+                   context.allItems.items.map(function (item) [item.text]);
+        };
+
+        if (isObject(vals) && !isArray(vals)) {
+            let k = values(completions.call(this, { values: {} })).toObject();
+            let v = values(completions.call(this, { value: "" })).toObject();
+            return Object.keys(vals).every(Set.has(k)) && values(vals).every(Set.has(v));
         }
 
+        if (this.values)
+            var acceptable = this.values.array || this.values;
+        else
+            acceptable = completions.call(this);
+
         if (isArray(acceptable))
             acceptable = Set(acceptable.map(function ([k]) k));
 
         if (this.type === "regexpmap" || this.type === "sitemap")
-            return Array.concat(values).every(function (re) Set.has(acceptable, re.result));
+            return Array.concat(vals).every(function (re) Set.has(acceptable, re.result));
 
-        return Array.concat(values).every(Set.has(acceptable));
+        return Array.concat(vals).every(Set.has(acceptable));
     },
 
     types: {}
@@ -745,6 +770,23 @@ var Option = Class("Option", {
     EXPORTED_SYMBOLS.push(class_.className);
 }, this);
 
+update(BooleanOption.prototype, {
+    names: Class.Memoize(function ()
+                array.flatten([[name, "no" + name] for (name in values(this.realNames))]))
+});
+
+var OptionHive = Class("OptionHive", Contexts.Hive, {
+    init: function init(group) {
+        init.supercall(this, group);
+        this.values = {};
+        this.has = Set.has(this.values);
+    },
+
+    add: function add(names, description, type, defaultValue, extraInfo) {
+        return this.modules.options.add(names, description, type, defaultValue, extraInfo);
+    }
+});
+
 /**
  * @instance options
  */
@@ -752,6 +794,12 @@ var Options = Module("options", {
     Local: function Local(dactyl, modules, window) let ({ contexts } = modules) ({
         init: function init() {
             const self = this;
+
+            update(this, {
+                hives: contexts.Hives("options", Class("OptionHive", OptionHive, { modules: modules })),
+                user: contexts.hives.options.user
+            });
+
             this.needInit = [];
             this._options = [];
             this._optionMap = {};
@@ -764,17 +812,23 @@ var Options = Module("options", {
                     opt.set(opt.globalValue, Option.SCOPE_GLOBAL, true);
             }, window);
 
-            services["dactyl:"].pages["options.dtd"] = function () [null,
+            modules.cache.register("options.dtd", function ()
                 util.makeDTD(
                     iter(([["option", o.name, "default"].join("."),
                            o.type === "string" ? o.defaultValue.replace(/'/g, "''") :
-                           o.value === true    ? "on"  :
-                           o.value === false   ? "off" : o.stringDefaultValue]
+                           o.defaultValue === true  ? "on"  :
+                           o.defaultValue === false ? "off" : o.stringDefaultValue]
                           for (o in self)),
 
                          ([["option", o.name, "type"].join("."), o.type] for (o in self)),
 
-                         config.dtd))];
+                         config.dtd)));
+        },
+
+        signals: {
+            "io.source": function ioSource(context, file, modTime) {
+                cache.flushEntry("options.dtd", modTime);
+            }
         },
 
         dactyl: dactyl,
@@ -811,8 +865,10 @@ var Options = Module("options", {
                             option.pre = "no";
                         option.default = (opt.defaultValue ? "" : "no") + opt.name;
                     }
-                    else if (isArray(opt.value))
-                        option.value = <>={template.map(opt.value, function (v) template.highlight(String(v)), <>,<span style="width: 0; display: inline-block"> </span></>)}</>;
+                    else if (isArray(opt.value) && opt.type != "charlist")
+                        option.value = <>={template.map(opt.value,
+                            function (v) template.highlight(String(v)),
+                            <>,<span style="width: 0; display: inline-block"> </span></>)}</>;
                     else
                         option.value = <>={template.highlight(opt.stringValue)}</>;
                     yield option;
@@ -842,6 +898,12 @@ var Options = Module("options", {
         add: function add(names, description, type, defaultValue, extraInfo) {
             const self = this;
 
+            if (!util.isDactyl(Components.stack.caller))
+                deprecated.warn(add, "options.add", "group.options.add");
+
+            util.assert(type in Option.types, _("option.noSuchType", type),
+                        false);
+
             if (!extraInfo)
                 extraInfo = {};
 
@@ -891,7 +953,7 @@ var Options = Module("options", {
     setPref: deprecated("prefs.set", function setPref() prefs.set.apply(prefs, arguments)),
     withContext: deprecated("prefs.withContext", function withContext() prefs.withContext.apply(prefs, arguments)),
 
-    cleanupPrefs: Class.memoize(function () localPrefs.Branch("cleanup.option.")),
+    cleanupPrefs: Class.Memoize(function () config.prefs.Branch("cleanup.option.")),
 
     cleanup: function cleanup(reason) {
         if (~["disable", "uninstall"].indexOf(reason))
@@ -969,7 +1031,8 @@ var Options = Module("options", {
         res.optionValue = res.option.get(res.scope);
 
         try {
-            res.values = res.option.parse(res.value);
+            if (!res.invert || res.option.type != "number") // Hack.
+                res.values = res.option.parse(res.value);
         }
         catch (e) {
             res.error = e;
@@ -1060,11 +1123,12 @@ var Options = Module("options", {
                     }
                     else {
                         var [matches, name, postfix, valueGiven, operator, value] =
-                            arg.match(/^\s*?([^=]+?)([?&!])?\s*(([-+^]?)=(.*))?\s*$/);
+                            arg.match(/^\s*?((?:[^=\\']|\\.|'[^']*')+?)([?&!])?\s*(([-+^]?)=(.*))?\s*$/);
                         reset = (postfix == "&");
                         invertBoolean = (postfix == "!");
                     }
 
+                    name = Option.dequote(name);
                     if (name == "all" && reset)
                         modules.commandline.input(_("pref.prompt.resetAll", config.host) + " ",
                             function (resp) {
@@ -1442,6 +1506,25 @@ var Options = Module("options", {
             context.advance(Option._splitAt);
             context.filter = Option.dequote(context.filter);
 
+            function val(obj) {
+                if (isArray(opt.defaultValue)) {
+                    let val = array.nth(obj, function (re) re.key == extra.key, 0);
+                    return val && val.result;
+                }
+                if (Set.has(opt.defaultValue, extra.key))
+                    return obj[extra.key];
+            }
+
+            if (extra.key && extra.value != null) {
+                context.fork("default", 0, this, function (context) {
+                    context.completions = [
+                            [val(opt.value), _("option.currentValue")],
+                            [val(opt.defaultValue), _("option.defaultValue")]
+                    ].filter(function (f) f[0] !== "" && f[0] != null);
+                });
+                context = context.fork("stuff", 0);
+            }
+
             context.title = ["Option Value"];
             context.quote = Commands.complQuote[Option._quote] || Commands.complQuote[""];
             // Not Vim compatible, but is a significant enough improvement
@@ -1469,7 +1552,7 @@ var Options = Module("options", {
     },
     javascript: function initJavascript(dactyl, modules, window) {
         const { options, JavaScript } = modules;
-        JavaScript.setCompleter(options.get, [function () ([o.name, o.description] for (o in options))]);
+        JavaScript.setCompleter(Options.prototype.get, [function () ([o.name, o.description] for (o in options))]);
     },
     sanitizer: function initSanitizer(dactyl, modules, window) {
         const { sanitizer } = modules;