]> git.donarmstrong.com Git - dactyl.git/blobdiff - common/modules/options.jsm
Imported Upstream version 1.1+hg7904
[dactyl.git] / common / modules / options.jsm
index a09fef257b3dc149ab1b8a53c1b20b054e2e0984..302a1d3400abd40c711470f66dfa84262083566e 100644 (file)
@@ -1,6 +1,6 @@
 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
-// Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
+// Copyright (c) 2008-2014 by Kris Maglione <maglione.k@gmail.com>
 //
 // This work is licensed for reuse under an MIT license. Details are
 // given in the LICENSE.txt file included with this 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,38 +53,19 @@ 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;
     },
 
-    magicalProperties: Set(["cleanupValue"]),
+    magicalProperties: RealSet(["cleanupValue"]),
 
     /**
      * @property {string} This option's description, as shown in :listoptions.
@@ -88,7 +75,7 @@ var Option = Class("Option", {
     get helpTag() "'" + this.name + "'",
 
     initValue: function initValue() {
-        util.trapErrors(function () this.value = this.value, this);
+        util.trapErrors(() => { this.value = this.value; });
     },
 
     get isDefault() this.stringValue === this.stringDefaultValue,
@@ -101,8 +88,17 @@ 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: this.parse(this.stringify(val)),
+                            time: Date.now() });
+    },
 
     /**
      * Returns *value* as an array of parsed values if the option type is
@@ -113,6 +109,8 @@ var Option = Class("Option", {
      */
     parse: function parse(value) Option.dequote(value),
 
+    parseKey: function parseKey(value) value,
+
     /**
      * Returns *values* packed in the appropriate format for the option type.
      *
@@ -146,6 +144,9 @@ var Option = Class("Option", {
         if ((scope & Option.SCOPE_GLOBAL) && (values == undefined))
             values = this.globalValue;
 
+        if (hasOwnProperty(this, "_value"))
+            values = this._value;
+
         if (this.getter)
             return util.trapErrors(this.getter, this, values);
 
@@ -175,6 +176,7 @@ var Option = Class("Option", {
         */
         if ((scope & Option.SCOPE_GLOBAL) && !skipGlobal)
             this.globalValue = newValues;
+        this._value = newValues;
 
         this.hasChanged = true;
         this.setFrom = null;
@@ -209,7 +211,7 @@ var Option = Class("Option", {
      *
      * @returns {boolean}
      */
-    has: function has() Array.some(arguments, function (val) this.value.indexOf(val) >= 0, this),
+    has: function has() Array.some(arguments, val => this.value.indexOf(val) >= 0),
 
     /**
      * Returns whether this option is identified by *name*.
@@ -264,12 +266,13 @@ var Option = Class("Option", {
         return null;
     },
 
-    // Properties {{{2
+    // Properties {{{
 
     /** @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:
@@ -323,14 +326,41 @@ var Option = Class("Option", {
      *     references to a given domain from the given values.
      */
     filterDomain: function filterDomain(host, values)
-        Array.filter(values, function (val) !this.domains([val]).some(function (val) util.isSubdomain(val, host)), this),
+        Array.filter(values, val => !this.domains([val]).some(val => util.isSubdomain(val, host))),
 
     /**
      * @property {value} The option's default value. This value will be used
      *     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 (hasOwnProperty(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(val => val.map(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.
@@ -378,7 +408,8 @@ var Option = Class("Option", {
     hasChanged: false,
 
     /**
-     * Returns the timestamp when the option's value was last changed.
+     * @property {number} Returns the timestamp when the option's value was
+     *     last changed.
      */
     get lastSet() options.store.get(this.name).time,
     set lastSet(val) { options.store.set(this.name, { value: this.globalValue, time: Date.now() }); },
@@ -389,6 +420,7 @@ var Option = Class("Option", {
      */
     setFrom: null
 
+    //}}}
 }, {
     /**
      * @property {number} Global option scope.
@@ -425,21 +457,23 @@ 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 ? v => v
+                                    : v => Option.quote(v, /:/);
 
         return update(Styles.matchFilter(filter), {
             bang: bang,
@@ -454,11 +488,11 @@ var Option = Class("Option", {
         stringlist: function stringlist(k) this.value.indexOf(k) >= 0,
         get charlist() this.stringlist,
 
-        regexplist: function regexplist(k, default_) {
+        regexplist: function regexplist(k, default_=null) {
             for (let re in values(this.value))
                 if ((re.test || re).call(re, k))
                     return re.result;
-            return arguments.length > 1 ? default_ : null;
+            return default_;
         },
         get regexpmap() this.regexplist,
         get sitelist() this.regexplist,
@@ -466,7 +500,7 @@ var Option = Class("Option", {
     },
 
     domains: {
-        sitelist: function (vals) array.compact(vals.map(function (site) util.getHost(site.filter))),
+        sitelist: function (vals) array.compact(vals.map(site => util.getHost(site.filter))),
         get sitemap() this.sitelist
     },
 
@@ -475,7 +509,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 +529,7 @@ var Option = Class("Option", {
 
         regexplist: function regexplist(value) (value === "") ? [] :
             Option.splitList(value, true)
-                  .map(function (re) Option.parseRegexp(re, undefined, this.regexpFlags), this),
+                  .map(re => Option.parseRegexp(re, undefined, this.regexpFlags)),
 
         sitelist: function sitelist(value) {
             if (value === "")
@@ -532,8 +566,13 @@ var Option = Class("Option", {
             }, this))
     },
 
+    parseKey: {
+        number: Number,
+        boolean: function boolean(value) value == "true" || value == true ? true : false,
+    },
+
     testValues: {
-        regexpmap:  function regexpmap(vals, validator) vals.every(function (re) validator(re.result)),
+        regexpmap:  function regexpmap(vals, validator) vals.every(re => validator(re.result)),
         get sitemap() this.regexpmap,
         stringlist: function stringlist(vals, validator) vals.every(validator, this),
         stringmap:  function stringmap(vals, validator) values(vals).every(validator, this)
@@ -562,7 +601,7 @@ var Option = Class("Option", {
         return res;
     },
 
-    quote: function quote(str, re) isArray(str) ? str.map(function (s) quote(s, re)).join(",") :
+    quote: function quote(str, re) isArray(str) ? str.map(s => quote(s, re)).join(",") :
         Commands.quoteArg[/[\s|"'\\,]|^$/.test(str) || re && re.test && re.test(str)
             ? (/[\b\f\n\r\t]/.test(str) ? '"' : "'")
             : ""](str, re),
@@ -597,6 +636,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);
 
@@ -628,8 +684,8 @@ var Option = Class("Option", {
             values = Array.concat(values);
 
             function uniq(ary) {
-                let seen = {};
-                return ary.filter(function (elem) !Set.add(seen, elem));
+                let seen = RealSet();
+                return ary.filter(elem => !seen.add(elem));
             }
 
             switch (operator) {
@@ -639,11 +695,11 @@ var Option = Class("Option", {
                 // NOTE: Vim doesn't prepend if there's a match in the current value
                 return uniq(Array.concat(values, this.value), true);
             case "-":
-                return this.value.filter(function (item) !Set.has(this, item), Set(values));
+                return this.value.filter(function (item) !this.has(item), RealSet(values));
             case "=":
                 if (invert) {
-                    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));
+                    let keepValues = this.value.filter(function (item) !this.has(item), RealSet(values));
+                    let addValues  = values.filter(function (item) !this.has(item), RealSet(this.value));
                     return addValues.concat(keepValues);
                 }
                 return values;
@@ -654,23 +710,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 +726,36 @@ 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(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(hasOwnProperty.bind(null, k)) &&
+                   values(vals).every(hasOwnProperty.bind(null, 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 = RealSet(acceptable.map(([k]) => k));
+        else
+            acceptable = RealSet(this.parseKey(k)
+                                 for (k of Object.keys(acceptable)));
 
         if (this.type === "regexpmap" || this.type === "sitemap")
-            return Array.concat(values).every(function (re) Set.has(acceptable, re.result));
+            return Array.concat(vals).every(re => acceptable.has(re.result));
 
-        return Array.concat(values).every(Set.has(acceptable));
+        return Array.concat(vals).every(v => acceptable.has(v));
     },
 
     types: {}
@@ -731,6 +784,9 @@ var Option = Class("Option", {
     if (type in Option.parse)
         class_.prototype.parse = Option.parse[type];
 
+    if (type in Option.parseKey)
+        class_.prototype.parseKey = Option.parse[type];
+
     if (type in Option.stringify)
         class_.prototype.stringify = Option.stringify[type];
 
@@ -745,6 +801,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 = v => hasOwnProperty(this.values, v);
+    },
+
+    add: function add(names, description, type, defaultValue, extraInfo) {
+        return this.modules.options.add(names, description, type, defaultValue, extraInfo);
+    }
+});
+
 /**
  * @instance options
  */
@@ -752,6 +825,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 +843,24 @@ var Options = Module("options", {
                     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)),
+            modules.cache.register("options.dtd",
+                () => 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)),
+                             ([["option", o.name, "type"].join("."), o.type] for (o in self)),
 
-                         config.dtd))];
+                             config.dtd)),
+                true);
+        },
+
+        signals: {
+            "io.source": function ioSource(context, file, modTime) {
+                cache.flushEntry("options.dtd", modTime);
+            }
         },
 
         dactyl: dactyl,
@@ -793,33 +879,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,
+                                                     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() {
@@ -840,7 +931,11 @@ var Options = Module("options", {
          * @optional
          */
         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 = {};
@@ -853,9 +948,11 @@ var Options = Module("options", {
                 this.remove(name);
             }
 
-            let closure = function () self._optionMap[name];
+            let closure = () => this._optionMap[name];
+
+            memoize(this._optionMap, name,
+                    function () Option.types[type](modules, names, description, 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);
 
@@ -876,7 +973,7 @@ var Options = Module("options", {
 
     /** @property {Iterator(Option)} @private */
     __iterator__: function __iterator__()
-        values(this._options.sort(function (a, b) String.localeCompare(a.name, b.name))),
+        values(this._options.sort((a, b) => String.localeCompare(a.name, b.name))),
 
     allPrefs: deprecated("prefs.getNames", function allPrefs() prefs.getNames.apply(prefs, arguments)),
     getPref: deprecated("prefs.get", function getPref() prefs.get.apply(prefs, arguments)),
@@ -891,7 +988,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(() => config.prefs.Branch("cleanup.option.")),
 
     cleanup: function cleanup(reason) {
         if (~["disable", "uninstall"].indexOf(reason))
@@ -969,7 +1066,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;
@@ -986,7 +1084,7 @@ var Options = Module("options", {
      */
     remove: function remove(name) {
         let opt = this.get(name);
-        this._options = this._options.filter(function (o) o != opt);
+        this._options = this._options.filter(o => o != opt);
         for (let name in values(opt.names))
             delete this._optionMap[name];
     },
@@ -998,36 +1096,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 + "'"
             }
         });
@@ -1039,12 +1120,14 @@ var Options = Module("options", {
 
             let list = [];
             function flushList() {
-                let names = Set(list.map(function (opt) opt.option ? opt.option.name : ""));
+                let names = RealSet(list.map(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);
+                    if (list.some(opt => opt.all))
+                        options.list(opt => !(list[0].onlyNonDefault && opt.isDefault),
+                                     list[0].scope);
                     else
-                        options.list(function (opt) Set.has(names, opt.name), list[0].scope);
+                        options.list(opt => names.has(opt.name),
+                                     list[0].scope);
                 list = [];
             }
 
@@ -1060,11 +1143,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) {
@@ -1155,11 +1239,11 @@ var Options = Module("options", {
                     context.advance(filter.length);
                     filter = filter.substr(0, filter.length - 1);
 
-                    context.pushProcessor(0, function (item, text, next) next(item, text.substr(0, 100)));
+                    context.pushProcessor(0, (item, text, next) => next(item, text.substr(0, 100)));
                     context.completions = [
                             [prefs.get(filter), _("option.currentValue")],
                             [prefs.defaults.get(filter), _("option.defaultValue")]
-                    ].filter(function (k) k[0] != null);
+                    ].filter(k => k[0] != null);
                     return null;
                 }
 
@@ -1172,8 +1256,12 @@ var Options = Module("options", {
             context.highlight();
             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, opt.name == "inv" ? opt.name : prefix);
+                    context.filters.push(({ item }) => (item.type == "boolean" ||
+                                                        prefix == "inv" && isArray(item.values)));
+
+                return completion.option(context, opt.scope,
+                                         opt.name == "inv" ? opt.name
+                                                           : prefix);
             }
 
             function error(length, message) {
@@ -1199,11 +1287,11 @@ var Options = Module("options", {
             if (!opt.value && !opt.operator && !opt.invert) {
                 context.fork("default", 0, this, function (context) {
                     context.title = ["Extra Completions"];
-                    context.pushProcessor(0, function (item, text, next) next(item, text.substr(0, 100)));
+                    context.pushProcessor(0, (item, text, next) => next(item, text.substr(0, 100)));
                     context.completions = [
                             [option.stringValue, _("option.currentValue")],
                             [option.stringDefaultValue, _("option.defaultValue")]
-                    ].filter(function (f) f[0] !== "");
+                    ].filter(f => f[0] !== "");
                     context.quote = ["", util.identity, ""];
                 });
             }
@@ -1213,15 +1301,15 @@ 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 = RealSet((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(i => !have.has(i.text));
                 modules.completion.optionValue(context, opt.name, opt.operator, null,
                                        function (context) {
-                                           context.generate = function () option.value.map(function (o) [o, ""]);
+                                           context.generate = () => option.value.map(o => [o, ""]);
                                        });
                 context.title = ["Current values"];
             }
@@ -1238,24 +1326,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) {
@@ -1264,7 +1335,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(hasOwnProperty(globalVariables, name) || (expr && !op),
                                 _("command.let.undefinedVar", fullName));
 
                     if (!expr)
@@ -1362,7 +1433,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 (!hasOwnProperty(dactyl._globalVariables, name)) {
                         if (!args.bang)
                             dactyl.echoerr(_("command.let.noSuch", name));
                         return;
@@ -1386,11 +1457,11 @@ var Options = Module("options", {
             context.anchored = false;
             context.completions = modules.options;
             if (prefix == "inv")
-                context.keys.text = function (opt)
-                    opt.type == "boolean" || isArray(opt.value) ? opt.names.map(function (n) "inv" + n)
+                context.keys.text = opt =>
+                    opt.type == "boolean" || isArray(opt.value) ? opt.names.map(n => "inv" + n)
                                                                 : opt.names;
             if (scope)
-                context.filters.push(function ({ item }) item.scope & scope);
+                context.filters.push(({ item }) => item.scope & scope);
         };
 
         completion.optionValue = function (context, name, op, curValue, completer) {
@@ -1442,22 +1513,41 @@ var Options = Module("options", {
             context.advance(Option._splitAt);
             context.filter = Option.dequote(context.filter);
 
+            function val(obj) {
+                if (isArray(opt.defaultValue)) {
+                    let val = [].find.call(obj, re => (re.key == extra.key));
+                    return val && val.result;
+                }
+                if (hasOwnProperty(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(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
             // that it's worth breaking compatibility.
             if (isArray(newValues)) {
-                context.filters.push(function (i) newValues.indexOf(i.text) == -1);
+                context.filters.push(i => newValues.indexOf(i.text) == -1);
                 if (op == "+")
-                    context.filters.push(function (i) curValues.indexOf(i.text) == -1);
+                    context.filters.push(i => curValues.indexOf(i.text) == -1);
                 if (op == "-")
-                    context.filters.push(function (i) curValues.indexOf(i.text) > -1);
+                    context.filters.push(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 curValues.filter(v => newValues.indexOf(val) == -1);
                     return newValues;
                 });
             }
@@ -1469,7 +1559,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, [() => ([o.name, o.description] for (o in options))]);
     },
     sanitizer: function initSanitizer(dactyl, modules, window) {
         const { sanitizer } = modules;
@@ -1507,4 +1597,4 @@ endModule();
 
 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
 
-// vim: set fdm=marker sw=4 ts=4 et ft=javascript:
+// vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: