]> git.donarmstrong.com Git - dactyl.git/blobdiff - common/modules/options.jsm
Import 1.0b7.1 supporting Firefox up to 8.*
[dactyl.git] / common / modules / options.jsm
index 1c61a40af6bd7bbd239bd79f7f60c3ae86cd11a0..a09fef257b3dc149ab1b8a53c1b20b054e2e0984 100644 (file)
@@ -12,7 +12,7 @@ Components.utils.import("resource://dactyl/bootstrap.jsm");
 defineModule("options", {
     exports: ["Option", "Options", "ValueError", "options"],
     require: ["messages", "storage"],
-    use: ["commands", "completion", "prefs", "services", "styles", "template", "util"]
+    use: ["commands", "completion", "config", "prefs", "services", "styles", "template", "util"]
 }, this);
 
 /** @scope modules */
@@ -25,7 +25,7 @@ let ValueError = Class("ValueError", ErrorBase);
  * A class representing configuration options. Instances are created by the
  * {@link Options} class.
  *
- * @param {string[]} names The names by which this option is identified.
+ * @param {[string]} names The names by which this option is identified.
  * @param {string} description A short one line description of the option.
  * @param {string} type The option's value data type (see {@link Option#type}).
  * @param {string} defaultValue The default value for this option.
@@ -44,44 +44,21 @@ let ValueError = Class("ValueError", ErrorBase);
  * @private
  */
 var Option = Class("Option", {
-    init: function init(names, description, type, defaultValue, extraInfo) {
+    init: function init(modules, names, description, defaultValue, extraInfo) {
+        this.modules = modules;
         this.name = names[0];
         this.names = names;
         this.realNames = names;
-        this.type = type;
         this.description = description;
 
-        if (this.type in Option.getKey)
-            this.getKey = Option.getKey[this.type];
-
-        if (this.type in Option.parse)
-            this.parse = Option.parse[this.type];
-
-        if (this.type in Option.stringify)
-            this.stringify = Option.stringify[this.type];
-
-        if (this.type in Option.domains)
-            this.domains = Option.domains[this.type];
-
-        if (this.type in Option.testValues)
-            this.testValues = Option.testValues[this.type];
-
-        this._op = Option.ops[this.type];
-
-        // Need to trigger setter
-        if (extraInfo && "values" in extraInfo && !extraInfo.__lookupGetter__("values")) {
-            this.values = extraInfo.values;
-            delete extraInfo.values;
-        }
-
         if (extraInfo)
-            update(this, extraInfo);
+            this.update(extraInfo);
 
-        if (set.has(this.modules.config.defaults, this.name))
+        if (Set.has(this.modules.config.defaults, this.name))
             defaultValue = this.modules.config.defaults[this.name];
 
         if (defaultValue !== undefined) {
-            if (this.type == "string")
+            if (this.type === "string")
                 defaultValue = Commands.quote(defaultValue);
 
             if (isObject(defaultValue))
@@ -101,6 +78,8 @@ var Option = Class("Option", {
             this.globalValue = this.defaultValue;
     },
 
+    magicalProperties: Set(["cleanupValue"]),
+
     /**
      * @property {string} This option's description, as shown in :listoptions.
      */
@@ -114,6 +93,13 @@ var Option = Class("Option", {
 
     get isDefault() this.stringValue === this.stringDefaultValue,
 
+    /** @property {value} The value to reset this option to at cleanup time. */
+    get cleanupValue() options.cleanupPrefs.get(this.name),
+    set cleanupValue(value) {
+        if (options.cleanupPrefs.get(this.name) == null)
+            options.cleanupPrefs.set(this.name, value);
+    },
+
     /** @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() }); },
@@ -123,14 +109,14 @@ var Option = Class("Option", {
      * "charlist" or "stringlist" or else unchanged.
      *
      * @param {value} value The option value.
-     * @returns {value|string[]}
+     * @returns {value|[string]}
      */
     parse: function parse(value) Option.dequote(value),
 
     /**
      * Returns *values* packed in the appropriate format for the option type.
      *
-     * @param {value|string[]} values The option value.
+     * @param {value|[string]} values The option value.
      * @returns {value}
      */
     stringify: function stringify(vals) Commands.quote(vals),
@@ -141,7 +127,7 @@ var Option = Class("Option", {
      *
      * @param {number} scope The scope to return these values from (see
      *     {@link Option#scope}).
-     * @returns {value|string[]}
+     * @returns {value|[string]}
      */
     get: function get(scope) {
         if (scope) {
@@ -213,6 +199,7 @@ var Option = Class("Option", {
     set stringValue(value) this.value = this.parse(value),
 
     get stringDefaultValue() this.stringify(this.defaultValue),
+    set stringDefaultValue(val) this.defaultValue = this.parse(val),
 
     getKey: function getKey(key) undefined,
 
@@ -252,7 +239,7 @@ var Option = Class("Option", {
      * Sets the option's value using the specified set *operator*.
      *
      * @param {string} operator The set operator.
-     * @param {value|string[]} values The value (or values) to apply.
+     * @param {value|[string]} values The value (or values) to apply.
      * @param {number} scope The scope to apply this value to (see
      *     {@link #scope}).
      * @param {boolean} invert Whether this is an invert boolean operation.
@@ -262,7 +249,7 @@ var Option = Class("Option", {
         try {
             var newValues = this._op(operator, values, scope, invert);
             if (newValues == null)
-                return "Operator " + operator + " not supported for option type " + this.type;
+                return _("option.operatorNotSupported", operator, this.type);
 
             if (!this.isValidValue(newValues))
                 return this.invalidArgument(str || this.stringify(values), operator);
@@ -281,7 +268,7 @@ var Option = Class("Option", {
 
     /** @property {string} The option's canonical name. */
     name: null,
-    /** @property {string[]} All names by which this option is identified. */
+    /** @property {[string]} All names by which this option is identified. */
     names: null,
 
     /**
@@ -305,13 +292,14 @@ var Option = Class("Option", {
      */
     scope: 1, // Option.SCOPE_GLOBAL // XXX set to BOTH by default someday? - kstep
 
-    cleanupValue: null,
-
     /**
      * @property {function(CompletionContext, Args)} This option's completer.
      * @see CompletionContext
      */
-    completer: function completer(context) {
+    completer: function completer(context, extra) {
+        if (/map$/.test(this.type) && extra.value == null)
+            return;
+
         if (this.values)
             context.completions = this.values;
     },
@@ -451,12 +439,14 @@ var Option = Class("Option", {
         let [, bang, filter] = /^(!?)(.*)/.exec(pattern);
         filter = Option.dequote(filter);
 
+        let quote = this.keepQuotes ? util.identity : Option.quote;
+
         return update(Styles.matchFilter(filter), {
             bang: bang,
             filter: filter,
             result: result !== undefined ? result : !bang,
-            toString: function toString() this.bang + Option.quote(this.filter) +
-                (typeof this.result === "boolean" ? "" : ":" + Option.quote(this.result)),
+            toString: function toString() this.bang + Option.quote(this.filter, /:/) +
+                (typeof this.result === "boolean" ? "" : ":" + quote(this.result)),
         });
     },
 
@@ -466,7 +456,7 @@ var Option = Class("Option", {
 
         regexplist: function regexplist(k, default_) {
             for (let re in values(this.value))
-                if (re(k))
+                if ((re.test || re).call(re, k))
                     return re.result;
             return arguments.length > 1 ? default_ : null;
         },
@@ -495,7 +485,7 @@ var Option = Class("Option", {
 
     parse: {
         number:     function (value) let (val = Option.dequote(value))
-                            Option.validIf(Number(val) % 1 == 0, "Integer value required") && parseInt(val),
+                            Option.validIf(Number(val) % 1 == 0, _("option.intRequired")) && parseInt(val),
 
         boolean:    function boolean(value) Option.dequote(value) == "true" || value == true ? true : false,
 
@@ -512,7 +502,7 @@ var Option = Class("Option", {
                 return [];
             if (!isArray(value))
                 value = Option.splitList(value, true);
-            return value.map(Option.parseSite);
+            return value.map(Option.parseSite, this);
         },
 
         stringmap: function stringmap(value) array.toObject(
@@ -536,7 +526,7 @@ var Option = Class("Option", {
                 if (v.length > count)
                     return prev = parse.call(this, filter, val);
                 else {
-                    util.assert(prev, "Syntax error", false);
+                    util.assert(prev, _("error.syntaxError"), false);
                     prev.result += "," + v;
                 }
             }, this))
@@ -592,7 +582,7 @@ var Option = Class("Option", {
 
             let value = parseInt(values);
             util.assert(Number(values) % 1 == 0,
-                        "E521: Number required after =: " + this.name + "=" + values);
+                        _("command.set.numberRequired", this.name, values));
 
             switch (operator) {
             case "+":
@@ -637,18 +627,23 @@ var Option = Class("Option", {
         stringlist: function stringlist(operator, values, scope, invert) {
             values = Array.concat(values);
 
+            function uniq(ary) {
+                let seen = {};
+                return ary.filter(function (elem) !Set.add(seen, elem));
+            }
+
             switch (operator) {
             case "+":
-                return array.uniq(Array.concat(this.value, values), true);
+                return uniq(Array.concat(this.value, values), true);
             case "^":
                 // NOTE: Vim doesn't prepend if there's a match in the current value
-                return array.uniq(Array.concat(values, this.value), true);
+                return uniq(Array.concat(values, this.value), true);
             case "-":
-                return this.value.filter(function (item) values.indexOf(item) == -1);
+                return this.value.filter(function (item) !Set.has(this, item), Set(values));
             case "=":
                 if (invert) {
-                    let keepValues = this.value.filter(function (item) values.indexOf(item) == -1);
-                    let addValues  = values.filter(function (item) this.value.indexOf(item) == -1, this);
+                    let keepValues = this.value.filter(function (item) !Set.has(this, item), Set(values));
+                    let addValues  = values.filter(function (item) !Set.has(this, item), Set(this.value));
                     return addValues.concat(keepValues);
                 }
                 return values;
@@ -688,7 +683,7 @@ var Option = Class("Option", {
      * Validates the specified *values* against values generated by the
      * option's completer function.
      *
-     * @param {value|string[]} values The value or array of values to validate.
+     * @param {value|[string]} values The value or array of values to validate.
      * @returns {boolean}
      */
     validateCompleter: function validateCompleter(values) {
@@ -702,15 +697,54 @@ var Option = Class("Option", {
         }
 
         if (isArray(acceptable))
-            acceptable = set(acceptable.map(function ([k]) k));
+            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(values).every(function (re) Set.has(acceptable, re.result));
 
-        return Array.concat(values).every(set.has(acceptable));
-    }
+        return Array.concat(values).every(Set.has(acceptable));
+    },
+
+    types: {}
 });
 
+["Boolean",
+ "Charlist",
+ "Number",
+ "RegexpList",
+ "RegexpMap",
+ "SiteList",
+ "SiteMap",
+ "String",
+ "StringList",
+ "StringMap"].forEach(function (name) {
+     let type = name.toLowerCase();
+     let class_ = Class(name + "Option", Option, {
+         type: type,
+
+         _op: Option.ops[type]
+     });
+
+    if (type in Option.getKey)
+        class_.prototype.getKey = Option.getKey[type];
+
+    if (type in Option.parse)
+        class_.prototype.parse = Option.parse[type];
+
+    if (type in Option.stringify)
+        class_.prototype.stringify = Option.stringify[type];
+
+    if (type in Option.domains)
+        class_.prototype.domains = Option.domains[type];
+
+    if (type in Option.testValues)
+        class_.prototype.testValues = Option.testValues[type];
+
+    Option.types[type] = class_;
+    this[class_.className] = class_;
+    EXPORTED_SYMBOLS.push(class_.className);
+}, this);
+
 /**
  * @instance options
  */
@@ -721,7 +755,6 @@ var Options = Module("options", {
             this.needInit = [];
             this._options = [];
             this._optionMap = {};
-            this.Option = Class("Option", Option, { modules: modules });
 
             storage.newMap("options", { store: false });
             storage.addObserver("options", function optionObserver(key, event, option) {
@@ -730,6 +763,18 @@ var Options = Module("options", {
                 if (event == "change" && opt)
                     opt.set(opt.globalValue, Option.SCOPE_GLOBAL, true);
             }, window);
+
+            services["dactyl:"].pages["options.dtd"] = function () [null,
+                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]
+                          for (o in self)),
+
+                         ([["option", o.name, "type"].join("."), o.type] for (o in self)),
+
+                         config.dtd))];
         },
 
         dactyl: dactyl,
@@ -742,7 +787,7 @@ var Options = Module("options", {
          * @param {number} scope Only list options in this scope (see
          *     {@link Option#scope}).
          */
-        list: function (filter, scope) {
+        list: function list(filter, scope) {
             if (!scope)
                 scope = Option.SCOPE_BOTH;
 
@@ -780,13 +825,13 @@ var Options = Module("options", {
         cleanup: function cleanup() {
             for (let opt in this)
                 if (opt.cleanupValue != null)
-                    opt.value = opt.parse(opt.cleanupValue);
+                    opt.stringValue = opt.cleanupValue;
         },
 
         /**
          * Adds a new option.
          *
-         * @param {string[]} names All names for the option.
+         * @param {[string]} names All names for the option.
          * @param {string} description A description of the option.
          * @param {string} type The option type (see {@link Option#type}).
          * @param {value} defaultValue The option's default value.
@@ -794,7 +839,7 @@ var Options = Module("options", {
          *     {@link Map#extraInfo}).
          * @optional
          */
-        add: function (names, description, type, defaultValue, extraInfo) {
+        add: function add(names, description, type, defaultValue, extraInfo) {
             const self = this;
 
             if (!extraInfo)
@@ -810,7 +855,7 @@ var Options = Module("options", {
 
             let closure = function () self._optionMap[name];
 
-            memoize(this._optionMap, name, function () self.Option(names, description, type, defaultValue, extraInfo));
+            memoize(this._optionMap, name, function () Option.types[type](modules, names, description, defaultValue, extraInfo));
             for (let alias in values(names.slice(1)))
                 memoize(this._optionMap, alias, closure);
 
@@ -836,7 +881,7 @@ var Options = Module("options", {
     allPrefs: deprecated("prefs.getNames", function allPrefs() prefs.getNames.apply(prefs, arguments)),
     getPref: deprecated("prefs.get", function getPref() prefs.get.apply(prefs, arguments)),
     invertPref: deprecated("prefs.invert", function invertPref() prefs.invert.apply(prefs, arguments)),
-    listPrefs: deprecated("prefs.list", function listPrefs() { commandline.commandOutput(prefs.list.apply(prefs, arguments)); }),
+    listPrefs: deprecated("prefs.list", function listPrefs() { this.modules.commandline.commandOutput(prefs.list.apply(prefs, arguments)); }),
     observePref: deprecated("prefs.observe", function observePref() prefs.observe.apply(prefs, arguments)),
     popContext: deprecated("prefs.popContext", function popContext() prefs.popContext.apply(prefs, arguments)),
     pushContext: deprecated("prefs.pushContext", function pushContext() prefs.pushContext.apply(prefs, arguments)),
@@ -846,6 +891,13 @@ 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.")),
+
+    cleanup: function cleanup(reason) {
+        if (~["disable", "uninstall"].indexOf(reason))
+            this.cleanupPrefs.resetBranch();
+    },
+
     /**
      * Returns the option with *name* in the specified *scope*.
      *
@@ -887,8 +939,11 @@ var Options = Module("options", {
         }
 
         if (matches) {
-            res.option = this.get(res.name, res.scope);
-            if (!res.option && (res.option = this.get(prefix + res.name, res.scope))) {
+            if (res.option = this.get(res.name, res.scope)) {
+                if (prefix === "no" && res.option.type !== "boolean")
+                    res.option = null;
+            }
+            else if (res.option = this.get(prefix + res.name, res.scope)) {
                 res.name = prefix + res.name;
                 prefix = "";
             }
@@ -953,7 +1008,9 @@ var Options = Module("options", {
             format: {
                 description: function (map) (XML.ignoreWhitespace = false, XML.prettyPrinting = false, <>
                         {options.get("passkeys").has(map.name)
-                            ? <span highlight="URLExtra">(passed by {template.helpLink("'passkeys'")})</span>
+                            ? <span highlight="URLExtra">({
+                                tempate.linkifyHelp(_("option.passkeys.passedBy"))
+                              })</span>
                             : <></>}
                         {template.linkifyHelp(map.description)}
                 </>)
@@ -968,7 +1025,7 @@ var Options = Module("options", {
             format: {
                 description: function (opt) (XML.ignoreWhitespace = false, XML.prettyPrinting = false, <>
                         {opt.scope == Option.SCOPE_LOCAL
-                            ? <span highlight="URLExtra">(buffer local)</span> : ""}
+                            ? <span highlight="URLExtra">({_("option.bufferLocal")})</span> : ""}
                         {template.linkifyHelp(opt.description)}
                 </>),
                 help: function (opt) "'" + opt.name + "'"
@@ -982,12 +1039,12 @@ var Options = Module("options", {
 
             let list = [];
             function flushList() {
-                let names = set(list.map(function (opt) opt.option ? opt.option.name : ""));
+                let names = Set(list.map(function (opt) opt.option ? opt.option.name : ""));
                 if (list.length)
                     if (list.some(function (opt) opt.all))
-                        options.list(function (opt) !(list[0].onlyNonDefault && opt.isDefault) , list[0].scope);
+                        options.list(function (opt) !(list[0].onlyNonDefault && opt.isDefault), list[0].scope);
                     else
-                        options.list(function (opt) set.has(names, opt.name), list[0].scope);
+                        options.list(function (opt) Set.has(names, opt.name), list[0].scope);
                 list = [];
             }
 
@@ -1009,7 +1066,7 @@ var Options = Module("options", {
                     }
 
                     if (name == "all" && reset)
-                        modules.commandline.input("Warning: Resetting all preferences may make " + config.host + " unusable. Continue (yes/[no]): ",
+                        modules.commandline.input(_("pref.prompt.resetAll", config.host) + " ",
                             function (resp) {
                                 if (resp == "yes")
                                     for (let pref in values(prefs.getNames()))
@@ -1017,7 +1074,7 @@ var Options = Module("options", {
                             },
                             { promptHighlight: "WarningMsg" });
                     else if (name == "all")
-                        commandline.commandOutput(prefs.list(onlyNonDefault, ""));
+                        modules.commandline.commandOutput(prefs.list(onlyNonDefault, ""));
                     else if (reset)
                         prefs.reset(name);
                     else if (invertBoolean)
@@ -1044,10 +1101,11 @@ var Options = Module("options", {
                 }
 
                 let opt = modules.options.parseOpt(arg, modifiers);
-                util.assert(opt, "Error parsing :set command: " + arg);
+                util.assert(opt, _("command.set.errorParsing", arg));
+                util.assert(!opt.error, _("command.set.errorParsing", opt.error));
 
                 let option = opt.option;
-                util.assert(option != null || opt.all, "E518: Unknown option: " + opt.name);
+                util.assert(option != null || opt.all, _("command.set.unknownOption", opt.name));
 
                 // reset a variable to its default value
                 if (opt.reset) {
@@ -1099,8 +1157,8 @@ var Options = Module("options", {
 
                     context.pushProcessor(0, function (item, text, next) next(item, text.substr(0, 100)));
                     context.completions = [
-                            [prefs.get(filter), "Current Value"],
-                            [prefs.defaults.get(filter), "Default Value"]
+                            [prefs.get(filter), _("option.currentValue")],
+                            [prefs.defaults.get(filter), _("option.defaultValue")]
                     ].filter(function (k) k[0] != null);
                     return null;
                 }
@@ -1115,7 +1173,7 @@ var Options = Module("options", {
             if (context.filter.indexOf("=") == -1) {
                 if (false && prefix)
                     context.filters.push(function ({ item }) item.type == "boolean" || prefix == "inv" && isArray(item.values));
-                return completion.option(context, opt.scope, prefix);
+                return completion.option(context, opt.scope, opt.name == "inv" ? opt.name : prefix);
             }
 
             function error(length, message) {
@@ -1125,11 +1183,11 @@ var Options = Module("options", {
 
             let option = opt.option;
             if (!option)
-                return error(opt.name.length, "No such option: " + opt.name);
+                return error(opt.name.length, _("option.noSuch", opt.name));
 
             context.advance(context.filter.indexOf("="));
             if (option.type == "boolean")
-                return error(context.filter.length, "Trailing characters");
+                return error(context.filter.length, _("error.trailingCharacters"));
 
             context.advance(1);
             if (opt.error)
@@ -1143,8 +1201,8 @@ var Options = Module("options", {
                     context.title = ["Extra Completions"];
                     context.pushProcessor(0, function (item, text, next) next(item, text.substr(0, 100)));
                     context.completions = [
-                            [option.stringValue, "Current value"],
-                            [option.stringDefaultValue, "Default value"]
+                            [option.stringValue, _("option.currentValue")],
+                            [option.stringDefaultValue, _("option.defaultValue")]
                     ].filter(function (f) f[0] !== "");
                     context.quote = ["", util.identity, ""];
                 });
@@ -1155,12 +1213,12 @@ var Options = Module("options", {
 
             // Fill in the current values if we're removing
             if (opt.operator == "-" && isArray(opt.values)) {
-                let have = set([i.text for (i in values(context.allItems.items))]);
+                let have = Set([i.text for (i in values(context.allItems.items))]);
                 context = context.fork("current-values", 0);
                 context.anchored = optcontext.anchored;
                 context.maxItems = optcontext.maxItems;
 
-                context.filters.push(function (i) !set.has(have, i.text));
+                context.filters.push(function (i) !Set.has(have, i.text));
                 modules.completion.optionValue(context, opt.name, opt.operator, null,
                                        function (context) {
                                            context.generate = function () option.value.map(function (o) [o, ""]);
@@ -1195,7 +1253,7 @@ var Options = Module("options", {
                     if (str.text().length() == str.*.length())
                         dactyl.echomsg(_("variable.none"));
                     else
-                        dactyl.echo(str, commandline.FORCE_MULTILINE);
+                        dactyl.echo(str, modules.commandline.FORCE_MULTILINE);
                     return;
                 }
 
@@ -1206,7 +1264,7 @@ var Options = Module("options", {
 
                     util.assert(scope == "g:" || scope == null,
                                 _("command.let.illegalVar", scope + name));
-                    util.assert(set.has(globalVariables, name) || (expr && !op),
+                    util.assert(Set.has(globalVariables, name) || (expr && !op),
                                 _("command.let.undefinedVar", fullName));
 
                     if (!expr)
@@ -1304,7 +1362,7 @@ var Options = Module("options", {
             function (args) {
                 for (let [, name] in args) {
                     name = name.replace(/^g:/, ""); // throw away the scope prefix
-                    if (!set.has(dactyl._globalVariables, name)) {
+                    if (!Set.has(dactyl._globalVariables, name)) {
                         if (!args.bang)
                             dactyl.echoerr(_("command.let.noSuch", name));
                         return;
@@ -1346,7 +1404,7 @@ var Options = Module("options", {
                 var newValues = opt.parse(context.filter);
             }
             catch (e) {
-                context.message = "Error: " + e;
+                context.message = _("error.error", e);
                 context.completions = [];
                 return;
             }
@@ -1394,6 +1452,14 @@ var Options = Module("options", {
                     context.filters.push(function (i) curValues.indexOf(i.text) == -1);
                 if (op == "-")
                     context.filters.push(function (i) curValues.indexOf(i.text) > -1);
+
+                memoize(extra, "values", function () {
+                    if (op == "+")
+                        return curValues.concat(newValues);
+                    if (op == "-")
+                        return curValues.filter(function (v) newValues.indexOf(val) == -1);
+                    return newValues;
+                });
             }
 
             let res = completer.call(opt, context, extra);