]> 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 1c61a40af6bd7bbd239bd79f7f60c3ae86cd11a0..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", "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 */
 
@@ -25,7 +31,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,63 +50,23 @@ 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);
-
-        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);
-        }
+            this.update(extraInfo);
 
-        // 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;
     },
 
+    magicalProperties: Set(["cleanupValue"]),
+
     /**
      * @property {string} This option's description, as shown in :listoptions.
      */
@@ -114,23 +80,37 @@ 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() }); },
+    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
      * "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 +121,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 +193,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 +233,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 +243,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,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,
+
+    /** @property {[string]} All names by which this option is identified. */
+    names: Class.Memoize(function () this.realNames),
 
     /**
      * @property {string} The option's data type. One of:
@@ -305,13 +287,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;
     },
@@ -342,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.
@@ -437,26 +447,29 @@ 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 : function (v) Option.quote(v, /:/);
 
         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 +479,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;
         },
@@ -485,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,
@@ -495,7 +508,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 +525,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 +549,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 +605,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 "+":
@@ -607,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);
 
@@ -637,18 +667,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;
@@ -659,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) {
@@ -688,26 +707,90 @@ 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) {
-        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));
+            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(vals).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);
+
+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);
+    },
 
-        return Array.concat(values).every(set.has(acceptable));
+    add: function add(names, description, type, defaultValue, extraInfo) {
+        return this.modules.options.add(names, description, type, defaultValue, extraInfo);
     }
 });
 
@@ -718,10 +801,15 @@ 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 = {};
-            this.Option = Class("Option", Option, { modules: modules });
 
             storage.newMap("options", { store: false });
             storage.addObserver("options", function optionObserver(key, event, option) {
@@ -730,6 +818,24 @@ var Options = Module("options", {
                 if (event == "change" && opt)
                     opt.set(opt.globalValue, Option.SCOPE_GLOBAL, true);
             }, window);
+
+            modules.cache.register("options.dtd", function ()
+                util.makeDTD(
+                    iter(([["option", o.name, "default"].join("."),
+                           o.type === "string" ? o.defaultValue.replace(/'/g, "''") :
+                           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)));
+        },
+
+        signals: {
+            "io.source": function ioSource(context, file, modTime) {
+                cache.flushEntry("options.dtd", modTime);
+            }
         },
 
         dactyl: dactyl,
@@ -742,51 +848,56 @@ 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;
 
             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() {
             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,9 +905,15 @@ 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 (!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 = {};
 
@@ -810,7 +927,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 +953,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 +963,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 () config.prefs.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 +1011,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 = "";
             }
@@ -914,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;
@@ -943,34 +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">(passed by {template.helpLink("'passkeys'")})</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">(buffer local)</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 + "'"
             }
         });
@@ -982,12 +1095,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 = [];
             }
 
@@ -1003,13 +1116,14 @@ 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("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 +1131,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 +1158,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 +1214,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 +1230,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 +1240,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 +1258,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 +1270,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, ""]);
@@ -1180,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, commandline.FORCE_MULTILINE);
-                    return;
-                }
+                util.assert(!(!args || args == "g:"));
 
                 let matches = args.match(/^([a-z]:)?([\w]+)(?:\s*([-+.])?=\s*(.*)?)?$/);
                 if (matches) {
@@ -1206,7 +1304,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 +1402,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 +1444,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;
             }
@@ -1384,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
@@ -1394,6 +1511,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);
@@ -1403,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;