]> git.donarmstrong.com Git - dactyl.git/blobdiff - common/modules/options.jsm
Import r6923 from upstream hg supporting Firefox up to 22.0a1
[dactyl.git] / common / modules / options.jsm
index a09fef257b3dc149ab1b8a53c1b20b054e2e0984..1440e23cd6e7b0ad6d5dc4abc944db69fed729ce 100644 (file)
@@ -8,12 +8,18 @@
 
 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"]
-}, this);
+    require: ["contexts", "messages", "storage"]
+});
+
+lazyRequire("cache", ["cache"]);
+lazyRequire("config", ["config"]);
+lazyRequire("commands", ["Commands"]);
+lazyRequire("completion", ["CompletionContext"]);
+lazyRequire("prefs", ["prefs"]);
+lazyRequire("styles", ["Styles"]);
+lazyRequire("template", ["template"]);
 
 /** @scope modules */
 
@@ -47,32 +53,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 +88,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 +262,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 +325,34 @@ 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(function (v) Option.quote(v, /:/))
+                                                                    .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,21 +447,22 @@ 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;
     },
 
     unparseRegexp: function unparseRegexp(re, quoted) re.bang + Option.quote(util.regexp.getSource(re), /^!|:/) +
-        (typeof re.result === "boolean" ? "" : ":" + (quoted ? re.result : Option.quote(re.result))),
+        (typeof re.result === "boolean" ? "" : ":" + (quoted ? re.result : Option.quote(re.result, /:/))),
 
     parseSite: function parseSite(pattern, result, rest) {
         if (isArray(rest)) // Called by Array.map
             result = undefined;
 
         let [, bang, filter] = /^(!?)(.*)/.exec(pattern);
-        filter = Option.dequote(filter);
+        filter = Option.dequote(filter).trim();
 
-        let quote = this.keepQuotes ? util.identity : Option.quote;
+        let quote = this.keepQuotes ? util.identity : function (v) Option.quote(v, /:/);
 
         return update(Styles.matchFilter(filter), {
             bang: bang,
@@ -475,7 +498,7 @@ var Option = Class("Option", {
 
         stringlist:  function (vals) vals.map(Option.quote).join(","),
 
-        stringmap:   function (vals) [Option.quote(k, /:/) + ":" + Option.quote(v) for ([k, v] in Iterator(vals))].join(","),
+        stringmap:   function (vals) [Option.quote(k, /:/) + ":" + Option.quote(v, /:/) for ([k, v] in Iterator(vals))].join(","),
 
         regexplist:  function (vals) vals.join(","),
         get regexpmap() this.regexplist,
@@ -597,6 +620,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 +694,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 +710,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 +777,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 +801,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 +819,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,
@@ -793,33 +854,38 @@ var Options = Module("options", {
 
             function opts(opt) {
                 for (let opt in Iterator(this)) {
+                    if (filter && !filter(opt))
+                        continue;
+                    if (!(opt.scope & scope))
+                        continue;
+
                     let option = {
                         __proto__: opt,
                         isDefault: opt.isDefault,
                         default:   opt.stringDefaultValue,
                         pre:       "\u00a0\u00a0", // Unicode nonbreaking space.
-                        value:     <></>
+                        value:     []
                     };
 
-                    if (filter && !filter(opt))
-                        continue;
-                    if (!(opt.scope & scope))
-                        continue;
-
                     if (opt.type == "boolean") {
                         if (!opt.value)
                             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" }, " "]])];
                     else
-                        option.value = <>={template.highlight(opt.stringValue)}</>;
+                        option.value = ["", "=", template.highlight(opt.stringValue)];
                     yield option;
                 }
             };
 
-            modules.commandline.commandOutput(template.options("Options", opts.call(this), this["verbose"] > 0));
+            modules.commandline.commandOutput(
+                template.options("Options", opts.call(this), this["verbose"] > 0));
         },
 
         cleanup: function cleanup() {
@@ -842,6 +908,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 +963,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 +1041,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;
@@ -998,36 +1071,19 @@ var Options = Module("options", {
     commands: function initCommands(dactyl, modules, window) {
         const { commands, contexts, options } = modules;
 
-        let args = {
-            getMode: function (args) findMode(args["-mode"]),
-            iterate: function (args) {
-                for (let map in mappings.iterate(this.getMode(args)))
-                    for (let name in values(map.names))
-                        yield { name: name, __proto__: map };
-            },
-            format: {
-                description: function (map) (XML.ignoreWhitespace = false, XML.prettyPrinting = false, <>
-                        {options.get("passkeys").has(map.name)
-                            ? <span highlight="URLExtra">({
-                                tempate.linkifyHelp(_("option.passkeys.passedBy"))
-                              })</span>
-                            : <></>}
-                        {template.linkifyHelp(map.description)}
-                </>)
-            }
-        };
-
         dactyl.addUsageCommand({
             name: ["listo[ptions]", "lo"],
             description: "List all options along with their short descriptions",
             index: "option",
             iterate: function (args) options,
             format: {
-                description: function (opt) (XML.ignoreWhitespace = false, XML.prettyPrinting = false, <>
-                        {opt.scope == Option.SCOPE_LOCAL
-                            ? <span highlight="URLExtra">({_("option.bufferLocal")})</span> : ""}
-                        {template.linkifyHelp(opt.description)}
-                </>),
+                description: function (opt) [
+                        opt.scope == Option.SCOPE_LOCAL
+                            ? ["span", { highlight: "URLExtra" },
+                                  "(" + _("option.bufferLocal") + ")"]
+                            : "",
+                        template.linkifyHelp(opt.description)
+                ],
                 help: function (opt) "'" + opt.name + "'"
             }
         });
@@ -1060,11 +1116,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) {
@@ -1238,24 +1295,7 @@ var Options = Module("options", {
                 function fmt(value) (typeof value == "number"   ? "#" :
                                      typeof value == "function" ? "*" :
                                                                   " ") + value;
-                if (!args || args == "g:") {
-                    let str =
-                        <table>
-                        {
-                            template.map(globalVariables, function ([i, value]) {
-                                return <tr>
-                                            <td style="width: 200px;">{i}</td>
-                                            <td>{fmt(value)}</td>
-                                       </tr>;
-                            })
-                        }
-                        </table>;
-                    if (str.text().length() == str.*.length())
-                        dactyl.echomsg(_("variable.none"));
-                    else
-                        dactyl.echo(str, modules.commandline.FORCE_MULTILINE);
-                    return;
-                }
+                util.assert(!(!args || args == "g:"));
 
                 let matches = args.match(/^([a-z]:)?([\w]+)(?:\s*([-+.])?=\s*(.*)?)?$/);
                 if (matches) {
@@ -1442,6 +1482,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 +1528,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;