]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/options.jsm
Import r6923 from upstream hg supporting Firefox up to 22.0a1
[dactyl.git] / common / modules / options.jsm
1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
4 //
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
7 "use strict";
8
9 try {
10
11 defineModule("options", {
12     exports: ["Option", "Options", "ValueError", "options"],
13     require: ["contexts", "messages", "storage"]
14 });
15
16 lazyRequire("cache", ["cache"]);
17 lazyRequire("config", ["config"]);
18 lazyRequire("commands", ["Commands"]);
19 lazyRequire("completion", ["CompletionContext"]);
20 lazyRequire("prefs", ["prefs"]);
21 lazyRequire("styles", ["Styles"]);
22 lazyRequire("template", ["template"]);
23
24 /** @scope modules */
25
26 let ValueError = Class("ValueError", ErrorBase);
27
28 // do NOT create instances of this class yourself, use the helper method
29 // options.add() instead
30 /**
31  * A class representing configuration options. Instances are created by the
32  * {@link Options} class.
33  *
34  * @param {[string]} names The names by which this option is identified.
35  * @param {string} description A short one line description of the option.
36  * @param {string} type The option's value data type (see {@link Option#type}).
37  * @param {string} defaultValue The default value for this option.
38  * @param {Object} extraInfo An optional extra configuration hash. The
39  *     following properties are supported.
40  *         completer   - see {@link Option#completer}
41  *         domains     - see {@link Option#domains}
42  *         getter      - see {@link Option#getter}
43  *         initialValue - Initial value is loaded from getter
44  *         persist     - see {@link Option#persist}
45  *         privateData - see {@link Option#privateData}
46  *         scope       - see {@link Option#scope}
47  *         setter      - see {@link Option#setter}
48  *         validator   - see {@link Option#validator}
49  * @optional
50  * @private
51  */
52 var Option = Class("Option", {
53     init: function init(modules, names, description, defaultValue, extraInfo) {
54         this.modules = modules;
55         this.name = names[0];
56         this.realNames = names;
57         this.description = description;
58
59         if (extraInfo)
60             this.update(extraInfo);
61
62         this._defaultValue = defaultValue;
63
64         if (this.globalValue == undefined && !this.initialValue)
65             this.globalValue = this.defaultValue;
66     },
67
68     magicalProperties: Set(["cleanupValue"]),
69
70     /**
71      * @property {string} This option's description, as shown in :listoptions.
72      */
73     description: Messages.Localized(""),
74
75     get helpTag() "'" + this.name + "'",
76
77     initValue: function initValue() {
78         util.trapErrors(function () this.value = this.value, this);
79     },
80
81     get isDefault() this.stringValue === this.stringDefaultValue,
82
83     /** @property {value} The value to reset this option to at cleanup time. */
84     get cleanupValue() options.cleanupPrefs.get(this.name),
85     set cleanupValue(value) {
86         if (options.cleanupPrefs.get(this.name) == null)
87             options.cleanupPrefs.set(this.name, value);
88     },
89
90     /** @property {value} The option's global value. @see #scope */
91     get globalValue() {
92         let val = options.store.get(this.name, {}).value;
93         if (val != null)
94             return val;
95         return this.globalValue = this.defaultValue;
96     },
97     set globalValue(val) {
98         options.store.set(this.name, { value: val, time: Date.now() });
99     },
100
101     /**
102      * Returns *value* as an array of parsed values if the option type is
103      * "charlist" or "stringlist" or else unchanged.
104      *
105      * @param {value} value The option value.
106      * @returns {value|[string]}
107      */
108     parse: function parse(value) Option.dequote(value),
109
110     /**
111      * Returns *values* packed in the appropriate format for the option type.
112      *
113      * @param {value|[string]} values The option value.
114      * @returns {value}
115      */
116     stringify: function stringify(vals) Commands.quote(vals),
117
118     /**
119      * Returns the option's value as an array of parsed values if the option
120      * type is "charlist" or "stringlist" or else the simple value.
121      *
122      * @param {number} scope The scope to return these values from (see
123      *     {@link Option#scope}).
124      * @returns {value|[string]}
125      */
126     get: function get(scope) {
127         if (scope) {
128             if ((scope & this.scope) == 0) // option doesn't exist in this scope
129                 return null;
130         }
131         else
132             scope = this.scope;
133
134         let values;
135
136         /*
137         if (config.has("tabs") && (scope & Option.SCOPE_LOCAL))
138             values = tabs.options[this.name];
139          */
140         if ((scope & Option.SCOPE_GLOBAL) && (values == undefined))
141             values = this.globalValue;
142
143         if (this.getter)
144             return util.trapErrors(this.getter, this, values);
145
146         return values;
147     },
148
149     /**
150      * Sets the option's value from an array of values if the option type is
151      * "charlist" or "stringlist" or else the simple value.
152      *
153      * @param {number} scope The scope to apply these values to (see
154      *     {@link Option#scope}).
155      */
156     set: function set(newValues, scope, skipGlobal) {
157         scope = scope || this.scope;
158         if ((scope & this.scope) == 0) // option doesn't exist in this scope
159             return;
160
161         if (this.setter)
162             newValues = this.setter(newValues);
163         if (newValues === undefined)
164             return;
165
166         /*
167         if (config.has("tabs") && (scope & Option.SCOPE_LOCAL))
168             tabs.options[this.name] = newValues;
169         */
170         if ((scope & Option.SCOPE_GLOBAL) && !skipGlobal)
171             this.globalValue = newValues;
172
173         this.hasChanged = true;
174         this.setFrom = null;
175
176         // dactyl.triggerObserver("options." + this.name, newValues);
177     },
178
179     getValues: deprecated("Option#get", "get"),
180     setValues: deprecated("Option#set", "set"),
181     joinValues: deprecated("Option#stringify", "stringify"),
182     parseValues: deprecated("Option#parse", "parse"),
183
184     /**
185      * @property {value} The option's current value. The option's local value,
186      *     or if no local value is set, this is equal to the
187      *     (@link #globalValue).
188      */
189     get value() this.get(),
190     set value(val) this.set(val),
191
192     get stringValue() this.stringify(this.value),
193     set stringValue(value) this.value = this.parse(value),
194
195     get stringDefaultValue() this.stringify(this.defaultValue),
196     set stringDefaultValue(val) this.defaultValue = this.parse(val),
197
198     getKey: function getKey(key) undefined,
199
200     /**
201      * Returns whether the option value contains one or more of the specified
202      * arguments.
203      *
204      * @returns {boolean}
205      */
206     has: function has() Array.some(arguments, function (val) this.value.indexOf(val) >= 0, this),
207
208     /**
209      * Returns whether this option is identified by *name*.
210      *
211      * @param {string} name
212      * @returns {boolean}
213      */
214     hasName: function hasName(name) this.names.indexOf(name) >= 0,
215
216     /**
217      * Returns whether the specified *values* are valid for this option.
218      * @see Option#validator
219      */
220     isValidValue: function isValidValue(values) this.validator(values),
221
222     invalidArgument: function invalidArgument(arg, op) _("error.invalidArgument",
223         this.name + (op || "").replace(/=?$/, "=") + arg),
224
225     /**
226      * Resets the option to its default value.
227      */
228     reset: function reset() {
229         this.value = this.defaultValue;
230     },
231
232     /**
233      * Sets the option's value using the specified set *operator*.
234      *
235      * @param {string} operator The set operator.
236      * @param {value|[string]} values The value (or values) to apply.
237      * @param {number} scope The scope to apply this value to (see
238      *     {@link #scope}).
239      * @param {boolean} invert Whether this is an invert boolean operation.
240      */
241     op: function op(operator, values, scope, invert, str) {
242
243         try {
244             var newValues = this._op(operator, values, scope, invert);
245             if (newValues == null)
246                 return _("option.operatorNotSupported", operator, this.type);
247
248             if (!this.isValidValue(newValues))
249                 return this.invalidArgument(str || this.stringify(values), operator);
250
251             this.set(newValues, scope);
252         }
253         catch (e) {
254             if (!(e instanceof ValueError))
255                 util.reportError(e);
256             return this.invalidArgument(str || this.stringify(values), operator) + ": " + e.message;
257         }
258         return null;
259     },
260
261     // Properties {{{2
262
263     /** @property {string} The option's canonical name. */
264     name: null,
265
266     /** @property {[string]} All names by which this option is identified. */
267     names: Class.Memoize(function () this.realNames),
268
269     /**
270      * @property {string} The option's data type. One of:
271      *     "boolean"    - Boolean, e.g., true
272      *     "number"     - Integer, e.g., 1
273      *     "string"     - String, e.g., "Pentadactyl"
274      *     "charlist"   - Character list, e.g., "rb"
275      *     "regexplist" - Regexp list, e.g., "^foo,bar$"
276      *     "stringmap"  - String map, e.g., "key:v,foo:bar"
277      *     "regexpmap"  - Regexp map, e.g., "^key:v,foo$:bar"
278      */
279     type: null,
280
281     /**
282      * @property {number} The scope of the option. This can be local, global,
283      *     or both.
284      * @see Option#SCOPE_LOCAL
285      * @see Option#SCOPE_GLOBAL
286      * @see Option#SCOPE_BOTH
287      */
288     scope: 1, // Option.SCOPE_GLOBAL // XXX set to BOTH by default someday? - kstep
289
290     /**
291      * @property {function(CompletionContext, Args)} This option's completer.
292      * @see CompletionContext
293      */
294     completer: function completer(context, extra) {
295         if (/map$/.test(this.type) && extra.value == null)
296             return;
297
298         if (this.values)
299             context.completions = this.values;
300     },
301
302     /**
303      * @property {[[string, string]]} This option's possible values.
304      * @see CompletionContext
305      */
306     values: Messages.Localized(null),
307
308     /**
309      * @property {function(host, values)} A function which should return a list
310      *     of domains referenced in the given values. Used in determining whether
311      *     to purge the command from history when clearing private data.
312      * @see Command#domains
313      */
314     domains: null,
315
316     /**
317      * @property {function(host, values)} A function which should strip
318      *     references to a given domain from the given values.
319      */
320     filterDomain: function filterDomain(host, values)
321         Array.filter(values, function (val) !this.domains([val]).some(function (val) util.isSubdomain(val, host)), this),
322
323     /**
324      * @property {value} The option's default value. This value will be used
325      *     unless the option is explicitly set either interactively or in an RC
326      *     file or plugin.
327      */
328     defaultValue: Class.Memoize(function () {
329         let defaultValue = this._defaultValue;
330         delete this._defaultValue;
331
332         if (Set.has(this.modules.config.optionDefaults, this.name))
333             defaultValue = this.modules.config.optionDefaults[this.name];
334
335         if (defaultValue == null && this.getter)
336             defaultValue = this.getter();
337
338         if (defaultValue == undefined)
339             return null;
340
341         if (this.type === "string")
342             defaultValue = Commands.quote(defaultValue);
343
344         if (isArray(defaultValue))
345             defaultValue = defaultValue.map(Option.quote).join(",");
346         else if (isObject(defaultValue))
347             defaultValue = iter(defaultValue).map(function (val) val.map(function (v) Option.quote(v, /:/))
348                                                                     .join(":"))
349                                              .join(",");
350
351         if (isArray(defaultValue))
352             defaultValue = defaultValue.map(Option.quote).join(",");
353
354         return this.parse(defaultValue);
355     }),
356
357     /**
358      * @property {function} The function called when the option value is read.
359      */
360     getter: null,
361
362     /**
363      * @property {boolean} When true, this options values will be saved
364      *     when generating a configuration file.
365      * @default true
366      */
367     persist: true,
368
369     /**
370      * @property {boolean|function(values)} When true, values of this
371      *     option may contain private data which should be purged from
372      *     saved histories when clearing private data. If a function, it
373      *     should return true if an invocation with the given values
374      *     contains private data
375      */
376     privateData: false,
377
378     /**
379      * @property {function} The function called when the option value is set.
380      */
381     setter: null,
382
383     testValues: function testValues(values, validator) validator(values),
384
385     /**
386      * @property {function} The function called to validate the option's value
387      *     when set.
388      */
389     validator: function validator() {
390         if (this.values || this.completer !== Option.prototype.completer)
391             return Option.validateCompleter.apply(this, arguments);
392         return true;
393     },
394
395     /**
396      * @property {boolean} Set to true whenever the option is first set. This
397      *     is useful to see whether it was changed from its default value
398      *     interactively or by some RC file.
399      */
400     hasChanged: false,
401
402     /**
403      * Returns the timestamp when the option's value was last changed.
404      */
405     get lastSet() options.store.get(this.name).time,
406     set lastSet(val) { options.store.set(this.name, { value: this.globalValue, time: Date.now() }); },
407
408     /**
409      * @property {nsIFile} The script in which this option was last set. null
410      *     implies an interactive command.
411      */
412     setFrom: null
413
414 }, {
415     /**
416      * @property {number} Global option scope.
417      * @final
418      */
419     SCOPE_GLOBAL: 1,
420
421     /**
422      * @property {number} Local option scope. Options in this scope only
423      *     apply to the current tab/buffer.
424      * @final
425      */
426     SCOPE_LOCAL: 2,
427
428     /**
429      * @property {number} Both local and global option scope.
430      * @final
431      */
432     SCOPE_BOTH: 3,
433
434     has: {
435         toggleAll: function toggleAll() toggleAll.supercall(this, "all") ^ !!toggleAll.superapply(this, arguments),
436     },
437
438     parseRegexp: function parseRegexp(value, result, flags) {
439         let keepQuotes = this && this.keepQuotes;
440         if (isArray(flags)) // Called by Array.map
441             result = flags = undefined;
442
443         if (flags == null)
444             flags = this && this.regexpFlags || "";
445
446         let [, bang, val] = /^(!?)(.*)/.exec(value);
447         let re = util.regexp(Option.dequote(val), flags);
448         re.bang = bang;
449         re.result = result !== undefined ? result : !bang;
450         re.key = re.bang + Option.quote(util.regexp.getSource(re), /^!|:/);
451         re.toString = function () Option.unparseRegexp(this, keepQuotes);
452         return re;
453     },
454
455     unparseRegexp: function unparseRegexp(re, quoted) re.bang + Option.quote(util.regexp.getSource(re), /^!|:/) +
456         (typeof re.result === "boolean" ? "" : ":" + (quoted ? re.result : Option.quote(re.result, /:/))),
457
458     parseSite: function parseSite(pattern, result, rest) {
459         if (isArray(rest)) // Called by Array.map
460             result = undefined;
461
462         let [, bang, filter] = /^(!?)(.*)/.exec(pattern);
463         filter = Option.dequote(filter).trim();
464
465         let quote = this.keepQuotes ? util.identity : function (v) Option.quote(v, /:/);
466
467         return update(Styles.matchFilter(filter), {
468             bang: bang,
469             filter: filter,
470             result: result !== undefined ? result : !bang,
471             toString: function toString() this.bang + Option.quote(this.filter, /:/) +
472                 (typeof this.result === "boolean" ? "" : ":" + quote(this.result)),
473         });
474     },
475
476     getKey: {
477         stringlist: function stringlist(k) this.value.indexOf(k) >= 0,
478         get charlist() this.stringlist,
479
480         regexplist: function regexplist(k, default_) {
481             for (let re in values(this.value))
482                 if ((re.test || re).call(re, k))
483                     return re.result;
484             return arguments.length > 1 ? default_ : null;
485         },
486         get regexpmap() this.regexplist,
487         get sitelist() this.regexplist,
488         get sitemap() this.regexplist
489     },
490
491     domains: {
492         sitelist: function (vals) array.compact(vals.map(function (site) util.getHost(site.filter))),
493         get sitemap() this.sitelist
494     },
495
496     stringify: {
497         charlist:    function (vals) Commands.quote(vals.join("")),
498
499         stringlist:  function (vals) vals.map(Option.quote).join(","),
500
501         stringmap:   function (vals) [Option.quote(k, /:/) + ":" + Option.quote(v, /:/) for ([k, v] in Iterator(vals))].join(","),
502
503         regexplist:  function (vals) vals.join(","),
504         get regexpmap() this.regexplist,
505         get sitelist() this.regexplist,
506         get sitemap() this.regexplist
507     },
508
509     parse: {
510         number:     function (value) let (val = Option.dequote(value))
511                             Option.validIf(Number(val) % 1 == 0, _("option.intRequired")) && parseInt(val),
512
513         boolean:    function boolean(value) Option.dequote(value) == "true" || value == true ? true : false,
514
515         charlist:   function charlist(value) Array.slice(Option.dequote(value)),
516
517         stringlist: function stringlist(value) (value === "") ? [] : Option.splitList(value),
518
519         regexplist: function regexplist(value) (value === "") ? [] :
520             Option.splitList(value, true)
521                   .map(function (re) Option.parseRegexp(re, undefined, this.regexpFlags), this),
522
523         sitelist: function sitelist(value) {
524             if (value === "")
525                 return [];
526             if (!isArray(value))
527                 value = Option.splitList(value, true);
528             return value.map(Option.parseSite, this);
529         },
530
531         stringmap: function stringmap(value) array.toObject(
532             Option.splitList(value, true).map(function (v) {
533                 let [count, key, quote] = Commands.parseArg(v, /:/);
534                 return [key, Option.dequote(v.substr(count + 1))];
535             })),
536
537         regexpmap: function regexpmap(value) Option.parse.list.call(this, value, Option.parseRegexp),
538
539         sitemap: function sitemap(value) Option.parse.list.call(this, value, Option.parseSite),
540
541         list: function list(value, parse) let (prev = null)
542             array.compact(Option.splitList(value, true).map(function (v) {
543                 let [count, filter, quote] = Commands.parseArg(v, /:/, true);
544
545                 let val = v.substr(count + 1);
546                 if (!this.keepQuotes)
547                     val = Option.dequote(val);
548
549                 if (v.length > count)
550                     return prev = parse.call(this, filter, val);
551                 else {
552                     util.assert(prev, _("error.syntaxError"), false);
553                     prev.result += "," + v;
554                 }
555             }, this))
556     },
557
558     testValues: {
559         regexpmap:  function regexpmap(vals, validator) vals.every(function (re) validator(re.result)),
560         get sitemap() this.regexpmap,
561         stringlist: function stringlist(vals, validator) vals.every(validator, this),
562         stringmap:  function stringmap(vals, validator) values(vals).every(validator, this)
563     },
564
565     dequote: function dequote(value) {
566         let arg;
567         [, arg, Option._quote] = Commands.parseArg(String(value), "");
568         Option._splitAt = 0;
569         return arg;
570     },
571
572     splitList: function splitList(value, keepQuotes) {
573         let res = [];
574         Option._splitAt = 0;
575         while (value.length) {
576             if (count !== undefined)
577                 value = value.slice(1);
578             var [count, arg, quote] = Commands.parseArg(value, /,/, keepQuotes);
579             Option._quote = quote; // FIXME
580             res.push(arg);
581             if (value.length > count)
582                 Option._splitAt += count + 1;
583             value = value.slice(count);
584         }
585         return res;
586     },
587
588     quote: function quote(str, re) isArray(str) ? str.map(function (s) quote(s, re)).join(",") :
589         Commands.quoteArg[/[\s|"'\\,]|^$/.test(str) || re && re.test && re.test(str)
590             ? (/[\b\f\n\r\t]/.test(str) ? '"' : "'")
591             : ""](str, re),
592
593     ops: {
594         boolean: function boolean(operator, values, scope, invert) {
595             if (operator != "=")
596                 return null;
597             if (invert)
598                 return !this.value;
599             return values;
600         },
601
602         number: function number(operator, values, scope, invert) {
603             if (invert)
604                 values = values[(values.indexOf(String(this.value)) + 1) % values.length];
605
606             let value = parseInt(values);
607             util.assert(Number(values) % 1 == 0,
608                         _("command.set.numberRequired", this.name, values));
609
610             switch (operator) {
611             case "+":
612                 return this.value + value;
613             case "-":
614                 return this.value - value;
615             case "^":
616                 return this.value * value;
617             case "=":
618                 return value;
619             }
620             return null;
621         },
622
623         string: function string(operator, values, scope, invert) {
624             if (invert)
625                 return values[(values.indexOf(this.value) + 1) % values.length];
626
627             switch (operator) {
628             case "+":
629                 return this.value + values;
630             case "-":
631                 return this.value.replace(values, "");
632             case "^":
633                 return values + this.value;
634             case "=":
635                 return values;
636             }
637             return null;
638         },
639
640         stringmap: function stringmap(operator, values, scope, invert) {
641             let res = update({}, this.value);
642
643             switch (operator) {
644             // The result is the same.
645             case "+":
646             case "^":
647                 return update(res, values);
648             case "-":
649                 for (let [k, v] in Iterator(values))
650                     if (v === res[k])
651                         delete res[k];
652                 return res;
653             case "=":
654                 if (invert) {
655                     for (let [k, v] in Iterator(values))
656                         if (v === res[k])
657                             delete res[k];
658                         else
659                             res[k] = v;
660                     return res;
661                 }
662                 return values;
663             }
664             return null;
665         },
666
667         stringlist: function stringlist(operator, values, scope, invert) {
668             values = Array.concat(values);
669
670             function uniq(ary) {
671                 let seen = {};
672                 return ary.filter(function (elem) !Set.add(seen, elem));
673             }
674
675             switch (operator) {
676             case "+":
677                 return uniq(Array.concat(this.value, values), true);
678             case "^":
679                 // NOTE: Vim doesn't prepend if there's a match in the current value
680                 return uniq(Array.concat(values, this.value), true);
681             case "-":
682                 return this.value.filter(function (item) !Set.has(this, item), Set(values));
683             case "=":
684                 if (invert) {
685                     let keepValues = this.value.filter(function (item) !Set.has(this, item), Set(values));
686                     let addValues  = values.filter(function (item) !Set.has(this, item), Set(this.value));
687                     return addValues.concat(keepValues);
688                 }
689                 return values;
690             }
691             return null;
692         },
693         get charlist() this.stringlist,
694         get regexplist() this.stringlist,
695         get regexpmap() this.stringlist,
696         get sitelist() this.stringlist,
697         get sitemap() this.stringlist
698     },
699
700     validIf: function validIf(test, error) {
701         if (test)
702             return true;
703         throw ValueError(error);
704     },
705
706     /**
707      * Validates the specified *values* against values generated by the
708      * option's completer function.
709      *
710      * @param {value|[string]} values The value or array of values to validate.
711      * @returns {boolean}
712      */
713     validateCompleter: function validateCompleter(vals) {
714         function completions(extra) {
715             let context = CompletionContext("");
716             return context.fork("", 0, this, this.completer, extra) ||
717                    context.allItems.items.map(function (item) [item.text]);
718         };
719
720         if (isObject(vals) && !isArray(vals)) {
721             let k = values(completions.call(this, { values: {} })).toObject();
722             let v = values(completions.call(this, { value: "" })).toObject();
723             return Object.keys(vals).every(Set.has(k)) && values(vals).every(Set.has(v));
724         }
725
726         if (this.values)
727             var acceptable = this.values.array || this.values;
728         else
729             acceptable = completions.call(this);
730
731         if (isArray(acceptable))
732             acceptable = Set(acceptable.map(function ([k]) k));
733
734         if (this.type === "regexpmap" || this.type === "sitemap")
735             return Array.concat(vals).every(function (re) Set.has(acceptable, re.result));
736
737         return Array.concat(vals).every(Set.has(acceptable));
738     },
739
740     types: {}
741 });
742
743 ["Boolean",
744  "Charlist",
745  "Number",
746  "RegexpList",
747  "RegexpMap",
748  "SiteList",
749  "SiteMap",
750  "String",
751  "StringList",
752  "StringMap"].forEach(function (name) {
753      let type = name.toLowerCase();
754      let class_ = Class(name + "Option", Option, {
755          type: type,
756
757          _op: Option.ops[type]
758      });
759
760     if (type in Option.getKey)
761         class_.prototype.getKey = Option.getKey[type];
762
763     if (type in Option.parse)
764         class_.prototype.parse = Option.parse[type];
765
766     if (type in Option.stringify)
767         class_.prototype.stringify = Option.stringify[type];
768
769     if (type in Option.domains)
770         class_.prototype.domains = Option.domains[type];
771
772     if (type in Option.testValues)
773         class_.prototype.testValues = Option.testValues[type];
774
775     Option.types[type] = class_;
776     this[class_.className] = class_;
777     EXPORTED_SYMBOLS.push(class_.className);
778 }, this);
779
780 update(BooleanOption.prototype, {
781     names: Class.Memoize(function ()
782                 array.flatten([[name, "no" + name] for (name in values(this.realNames))]))
783 });
784
785 var OptionHive = Class("OptionHive", Contexts.Hive, {
786     init: function init(group) {
787         init.supercall(this, group);
788         this.values = {};
789         this.has = Set.has(this.values);
790     },
791
792     add: function add(names, description, type, defaultValue, extraInfo) {
793         return this.modules.options.add(names, description, type, defaultValue, extraInfo);
794     }
795 });
796
797 /**
798  * @instance options
799  */
800 var Options = Module("options", {
801     Local: function Local(dactyl, modules, window) let ({ contexts } = modules) ({
802         init: function init() {
803             const self = this;
804
805             update(this, {
806                 hives: contexts.Hives("options", Class("OptionHive", OptionHive, { modules: modules })),
807                 user: contexts.hives.options.user
808             });
809
810             this.needInit = [];
811             this._options = [];
812             this._optionMap = {};
813
814             storage.newMap("options", { store: false });
815             storage.addObserver("options", function optionObserver(key, event, option) {
816                 // Trigger any setters.
817                 let opt = self.get(option);
818                 if (event == "change" && opt)
819                     opt.set(opt.globalValue, Option.SCOPE_GLOBAL, true);
820             }, window);
821
822             modules.cache.register("options.dtd", function ()
823                 util.makeDTD(
824                     iter(([["option", o.name, "default"].join("."),
825                            o.type === "string" ? o.defaultValue.replace(/'/g, "''") :
826                            o.defaultValue === true  ? "on"  :
827                            o.defaultValue === false ? "off" : o.stringDefaultValue]
828                           for (o in self)),
829
830                          ([["option", o.name, "type"].join("."), o.type] for (o in self)),
831
832                          config.dtd)));
833         },
834
835         signals: {
836             "io.source": function ioSource(context, file, modTime) {
837                 cache.flushEntry("options.dtd", modTime);
838             }
839         },
840
841         dactyl: dactyl,
842
843         /**
844          * Lists all options in *scope* or only those with changed values if
845          * *onlyNonDefault* is specified.
846          *
847          * @param {function(Option)} filter Limit the list
848          * @param {number} scope Only list options in this scope (see
849          *     {@link Option#scope}).
850          */
851         list: function list(filter, scope) {
852             if (!scope)
853                 scope = Option.SCOPE_BOTH;
854
855             function opts(opt) {
856                 for (let opt in Iterator(this)) {
857                     if (filter && !filter(opt))
858                         continue;
859                     if (!(opt.scope & scope))
860                         continue;
861
862                     let option = {
863                         __proto__: opt,
864                         isDefault: opt.isDefault,
865                         default:   opt.stringDefaultValue,
866                         pre:       "\u00a0\u00a0", // Unicode nonbreaking space.
867                         value:     []
868                     };
869
870                     if (opt.type == "boolean") {
871                         if (!opt.value)
872                             option.pre = "no";
873                         option.default = (opt.defaultValue ? "" : "no") + opt.name;
874                     }
875                     else if (isArray(opt.value) && opt.type != "charlist")
876                         option.value = ["", "=",
877                                         template.map(opt.value,
878                                                      function (v) template.highlight(String(v)),
879                                                      ["", ",",
880                                                       ["span", { style: "width: 0; display: inline-block" }, " "]])];
881                     else
882                         option.value = ["", "=", template.highlight(opt.stringValue)];
883                     yield option;
884                 }
885             };
886
887             modules.commandline.commandOutput(
888                 template.options("Options", opts.call(this), this["verbose"] > 0));
889         },
890
891         cleanup: function cleanup() {
892             for (let opt in this)
893                 if (opt.cleanupValue != null)
894                     opt.stringValue = opt.cleanupValue;
895         },
896
897         /**
898          * Adds a new option.
899          *
900          * @param {[string]} names All names for the option.
901          * @param {string} description A description of the option.
902          * @param {string} type The option type (see {@link Option#type}).
903          * @param {value} defaultValue The option's default value.
904          * @param {Object} extra An optional extra configuration hash (see
905          *     {@link Map#extraInfo}).
906          * @optional
907          */
908         add: function add(names, description, type, defaultValue, extraInfo) {
909             const self = this;
910
911             if (!util.isDactyl(Components.stack.caller))
912                 deprecated.warn(add, "options.add", "group.options.add");
913
914             util.assert(type in Option.types, _("option.noSuchType", type),
915                         false);
916
917             if (!extraInfo)
918                 extraInfo = {};
919
920             extraInfo.definedAt = contexts.getCaller(Components.stack.caller);
921
922             let name = names[0];
923             if (name in this._optionMap) {
924                 this.dactyl.log(_("option.replaceExisting", name.quote()), 1);
925                 this.remove(name);
926             }
927
928             let closure = function () self._optionMap[name];
929
930             memoize(this._optionMap, name, function () Option.types[type](modules, names, description, defaultValue, extraInfo));
931             for (let alias in values(names.slice(1)))
932                 memoize(this._optionMap, alias, closure);
933
934             if (extraInfo.setter && (!extraInfo.scope || extraInfo.scope & Option.SCOPE_GLOBAL))
935                 if (this.dactyl.initialized)
936                     closure().initValue();
937                 else
938                     memoize(this.needInit, this.needInit.length, closure);
939
940             this._floptions = (this._floptions || []).concat(name);
941             memoize(this._options, this._options.length, closure);
942
943             // quickly access options with options["wildmode"]:
944             this.__defineGetter__(name, function () this._optionMap[name].value);
945             this.__defineSetter__(name, function (value) { this._optionMap[name].value = value; });
946         }
947     }),
948
949     /** @property {Iterator(Option)} @private */
950     __iterator__: function __iterator__()
951         values(this._options.sort(function (a, b) String.localeCompare(a.name, b.name))),
952
953     allPrefs: deprecated("prefs.getNames", function allPrefs() prefs.getNames.apply(prefs, arguments)),
954     getPref: deprecated("prefs.get", function getPref() prefs.get.apply(prefs, arguments)),
955     invertPref: deprecated("prefs.invert", function invertPref() prefs.invert.apply(prefs, arguments)),
956     listPrefs: deprecated("prefs.list", function listPrefs() { this.modules.commandline.commandOutput(prefs.list.apply(prefs, arguments)); }),
957     observePref: deprecated("prefs.observe", function observePref() prefs.observe.apply(prefs, arguments)),
958     popContext: deprecated("prefs.popContext", function popContext() prefs.popContext.apply(prefs, arguments)),
959     pushContext: deprecated("prefs.pushContext", function pushContext() prefs.pushContext.apply(prefs, arguments)),
960     resetPref: deprecated("prefs.reset", function resetPref() prefs.reset.apply(prefs, arguments)),
961     safeResetPref: deprecated("prefs.safeReset", function safeResetPref() prefs.safeReset.apply(prefs, arguments)),
962     safeSetPref: deprecated("prefs.safeSet", function safeSetPref() prefs.safeSet.apply(prefs, arguments)),
963     setPref: deprecated("prefs.set", function setPref() prefs.set.apply(prefs, arguments)),
964     withContext: deprecated("prefs.withContext", function withContext() prefs.withContext.apply(prefs, arguments)),
965
966     cleanupPrefs: Class.Memoize(function () config.prefs.Branch("cleanup.option.")),
967
968     cleanup: function cleanup(reason) {
969         if (~["disable", "uninstall"].indexOf(reason))
970             this.cleanupPrefs.resetBranch();
971     },
972
973     /**
974      * Returns the option with *name* in the specified *scope*.
975      *
976      * @param {string} name The option's name.
977      * @param {number} scope The option's scope (see {@link Option#scope}).
978      * @optional
979      * @returns {Option} The matching option.
980      */
981     get: function get(name, scope) {
982         if (!scope)
983             scope = Option.SCOPE_BOTH;
984
985         if (this._optionMap[name] && (this._optionMap[name].scope & scope))
986             return this._optionMap[name];
987         return null;
988     },
989
990     /**
991      * Parses a :set command's argument string.
992      *
993      * @param {string} args The :set command's argument string.
994      * @param {Object} modifiers A hash of parsing modifiers. These are:
995      *     scope - see {@link Option#scope}
996      * @optional
997      * @returns {Object} The parsed command object.
998      */
999     parseOpt: function parseOpt(args, modifiers) {
1000         let res = {};
1001         let matches, prefix, postfix;
1002
1003         [matches, prefix, res.name, postfix, res.valueGiven, res.operator, res.value] =
1004         args.match(/^\s*(no|inv)?([^=]+?)([?&!])?\s*(([-+^]?)=(.*))?\s*$/) || [];
1005
1006         res.args = args;
1007         res.onlyNonDefault = false; // used for :set to print non-default options
1008         if (!args) {
1009             res.name = "all";
1010             res.onlyNonDefault = true;
1011         }
1012
1013         if (matches) {
1014             if (res.option = this.get(res.name, res.scope)) {
1015                 if (prefix === "no" && res.option.type !== "boolean")
1016                     res.option = null;
1017             }
1018             else if (res.option = this.get(prefix + res.name, res.scope)) {
1019                 res.name = prefix + res.name;
1020                 prefix = "";
1021             }
1022         }
1023
1024         res.prefix = prefix;
1025         res.postfix = postfix;
1026
1027         res.all = (res.name == "all");
1028         res.get = (res.all || postfix == "?" || (res.option && res.option.type != "boolean" && !res.valueGiven));
1029         res.invert = (prefix == "inv" || postfix == "!");
1030         res.reset = (postfix == "&");
1031         res.unsetBoolean = (prefix == "no");
1032
1033         res.scope = modifiers && modifiers.scope;
1034
1035         if (!res.option)
1036             return res;
1037
1038         if (res.value === undefined)
1039             res.value = "";
1040
1041         res.optionValue = res.option.get(res.scope);
1042
1043         try {
1044             if (!res.invert || res.option.type != "number") // Hack.
1045                 res.values = res.option.parse(res.value);
1046         }
1047         catch (e) {
1048             res.error = e;
1049         }
1050
1051         return res;
1052     },
1053
1054     /**
1055      * Remove the option with matching *name*.
1056      *
1057      * @param {string} name The name of the option to remove. This can be
1058      *     any of the option's names.
1059      */
1060     remove: function remove(name) {
1061         let opt = this.get(name);
1062         this._options = this._options.filter(function (o) o != opt);
1063         for (let name in values(opt.names))
1064             delete this._optionMap[name];
1065     },
1066
1067     /** @property {Object} The options store. */
1068     get store() storage.options
1069 }, {
1070 }, {
1071     commands: function initCommands(dactyl, modules, window) {
1072         const { commands, contexts, options } = modules;
1073
1074         dactyl.addUsageCommand({
1075             name: ["listo[ptions]", "lo"],
1076             description: "List all options along with their short descriptions",
1077             index: "option",
1078             iterate: function (args) options,
1079             format: {
1080                 description: function (opt) [
1081                         opt.scope == Option.SCOPE_LOCAL
1082                             ? ["span", { highlight: "URLExtra" },
1083                                   "(" + _("option.bufferLocal") + ")"]
1084                             : "",
1085                         template.linkifyHelp(opt.description)
1086                 ],
1087                 help: function (opt) "'" + opt.name + "'"
1088             }
1089         });
1090
1091         function setAction(args, modifiers) {
1092             let bang = args.bang;
1093             if (!args.length)
1094                 args[0] = "";
1095
1096             let list = [];
1097             function flushList() {
1098                 let names = Set(list.map(function (opt) opt.option ? opt.option.name : ""));
1099                 if (list.length)
1100                     if (list.some(function (opt) opt.all))
1101                         options.list(function (opt) !(list[0].onlyNonDefault && opt.isDefault), list[0].scope);
1102                     else
1103                         options.list(function (opt) Set.has(names, opt.name), list[0].scope);
1104                 list = [];
1105             }
1106
1107             for (let [, arg] in args) {
1108                 if (bang) {
1109                     let onlyNonDefault = false;
1110                     let reset = false;
1111                     let invertBoolean = false;
1112
1113                     if (args[0] == "") {
1114                         var name = "all";
1115                         onlyNonDefault = true;
1116                     }
1117                     else {
1118                         var [matches, name, postfix, valueGiven, operator, value] =
1119                             arg.match(/^\s*?((?:[^=\\']|\\.|'[^']*')+?)([?&!])?\s*(([-+^]?)=(.*))?\s*$/);
1120                         reset = (postfix == "&");
1121                         invertBoolean = (postfix == "!");
1122                     }
1123
1124                     name = Option.dequote(name);
1125                     if (name == "all" && reset)
1126                         modules.commandline.input(_("pref.prompt.resetAll", config.host) + " ",
1127                             function (resp) {
1128                                 if (resp == "yes")
1129                                     for (let pref in values(prefs.getNames()))
1130                                         prefs.reset(pref);
1131                             },
1132                             { promptHighlight: "WarningMsg" });
1133                     else if (name == "all")
1134                         modules.commandline.commandOutput(prefs.list(onlyNonDefault, ""));
1135                     else if (reset)
1136                         prefs.reset(name);
1137                     else if (invertBoolean)
1138                         prefs.toggle(name);
1139                     else if (valueGiven) {
1140                         if (value == undefined)
1141                             value = "";
1142                         else if (value == "true")
1143                             value = true;
1144                         else if (value == "false")
1145                             value = false;
1146                         else if (Number(value) % 1 == 0)
1147                             value = parseInt(value);
1148                         else
1149                             value = Option.dequote(value);
1150
1151                         if (operator)
1152                             value = Option.ops[typeof value].call({ value: prefs.get(name) }, operator, value);
1153                         prefs.set(name, value);
1154                     }
1155                     else
1156                         modules.commandline.commandOutput(prefs.list(onlyNonDefault, name));
1157                     return;
1158                 }
1159
1160                 let opt = modules.options.parseOpt(arg, modifiers);
1161                 util.assert(opt, _("command.set.errorParsing", arg));
1162                 util.assert(!opt.error, _("command.set.errorParsing", opt.error));
1163
1164                 let option = opt.option;
1165                 util.assert(option != null || opt.all, _("command.set.unknownOption", opt.name));
1166
1167                 // reset a variable to its default value
1168                 if (opt.reset) {
1169                     flushList();
1170                     if (opt.all) {
1171                         for (let option in modules.options)
1172                             option.reset();
1173                     }
1174                     else {
1175                         option.reset();
1176                     }
1177                 }
1178                 // read access
1179                 else if (opt.get)
1180                     list.push(opt);
1181                 // write access
1182                 else {
1183                     flushList();
1184                     if (opt.option.type === "boolean") {
1185                         util.assert(!opt.valueGiven, _("error.invalidArgument", arg));
1186                         opt.values = !opt.unsetBoolean;
1187                     }
1188                     else if (/^(string|number)$/.test(opt.option.type) && opt.invert)
1189                         opt.values = Option.splitList(opt.value);
1190                     try {
1191                         var res = opt.option.op(opt.operator || "=", opt.values, opt.scope, opt.invert,
1192                                                 opt.value);
1193                     }
1194                     catch (e) {
1195                         res = e;
1196                     }
1197                     if (res)
1198                         dactyl.echoerr(res);
1199                     option.setFrom = contexts.getCaller(null);
1200                 }
1201             }
1202             flushList();
1203         }
1204
1205         function setCompleter(context, args, modifiers) {
1206             const { completion } = modules;
1207
1208             let filter = context.filter;
1209
1210             if (args.bang) { // list completions for about:config entries
1211                 if (filter[filter.length - 1] == "=") {
1212                     context.advance(filter.length);
1213                     filter = filter.substr(0, filter.length - 1);
1214
1215                     context.pushProcessor(0, function (item, text, next) next(item, text.substr(0, 100)));
1216                     context.completions = [
1217                             [prefs.get(filter), _("option.currentValue")],
1218                             [prefs.defaults.get(filter), _("option.defaultValue")]
1219                     ].filter(function (k) k[0] != null);
1220                     return null;
1221                 }
1222
1223                 return completion.preference(context);
1224             }
1225
1226             let opt = modules.options.parseOpt(filter, modifiers);
1227             let prefix = opt.prefix;
1228
1229             context.highlight();
1230             if (context.filter.indexOf("=") == -1) {
1231                 if (false && prefix)
1232                     context.filters.push(function ({ item }) item.type == "boolean" || prefix == "inv" && isArray(item.values));
1233                 return completion.option(context, opt.scope, opt.name == "inv" ? opt.name : prefix);
1234             }
1235
1236             function error(length, message) {
1237                 context.message = message;
1238                 context.highlight(0, length, "SPELLCHECK");
1239             }
1240
1241             let option = opt.option;
1242             if (!option)
1243                 return error(opt.name.length, _("option.noSuch", opt.name));
1244
1245             context.advance(context.filter.indexOf("="));
1246             if (option.type == "boolean")
1247                 return error(context.filter.length, _("error.trailingCharacters"));
1248
1249             context.advance(1);
1250             if (opt.error)
1251                 return error(context.filter.length, opt.error);
1252
1253             if (opt.get || opt.reset || !option || prefix)
1254                 return null;
1255
1256             if (!opt.value && !opt.operator && !opt.invert) {
1257                 context.fork("default", 0, this, function (context) {
1258                     context.title = ["Extra Completions"];
1259                     context.pushProcessor(0, function (item, text, next) next(item, text.substr(0, 100)));
1260                     context.completions = [
1261                             [option.stringValue, _("option.currentValue")],
1262                             [option.stringDefaultValue, _("option.defaultValue")]
1263                     ].filter(function (f) f[0] !== "");
1264                     context.quote = ["", util.identity, ""];
1265                 });
1266             }
1267
1268             let optcontext = context.fork("values");
1269             modules.completion.optionValue(optcontext, opt.name, opt.operator);
1270
1271             // Fill in the current values if we're removing
1272             if (opt.operator == "-" && isArray(opt.values)) {
1273                 let have = Set([i.text for (i in values(context.allItems.items))]);
1274                 context = context.fork("current-values", 0);
1275                 context.anchored = optcontext.anchored;
1276                 context.maxItems = optcontext.maxItems;
1277
1278                 context.filters.push(function (i) !Set.has(have, i.text));
1279                 modules.completion.optionValue(context, opt.name, opt.operator, null,
1280                                        function (context) {
1281                                            context.generate = function () option.value.map(function (o) [o, ""]);
1282                                        });
1283                 context.title = ["Current values"];
1284             }
1285         }
1286
1287         // TODO: deprecated. This needs to support "g:"-prefixed globals at a
1288         // minimum for now.  The coderepos plugins make extensive use of global
1289         // variables.
1290         commands.add(["let"],
1291             "Set or list a variable",
1292             function (args) {
1293                 let globalVariables = dactyl._globalVariables;
1294                 args = (args[0] || "").trim();
1295                 function fmt(value) (typeof value == "number"   ? "#" :
1296                                      typeof value == "function" ? "*" :
1297                                                                   " ") + value;
1298                 util.assert(!(!args || args == "g:"));
1299
1300                 let matches = args.match(/^([a-z]:)?([\w]+)(?:\s*([-+.])?=\s*(.*)?)?$/);
1301                 if (matches) {
1302                     let [, scope, name, op, expr] = matches;
1303                     let fullName = (scope || "") + name;
1304
1305                     util.assert(scope == "g:" || scope == null,
1306                                 _("command.let.illegalVar", scope + name));
1307                     util.assert(Set.has(globalVariables, name) || (expr && !op),
1308                                 _("command.let.undefinedVar", fullName));
1309
1310                     if (!expr)
1311                         dactyl.echo(fullName + "\t\t" + fmt(globalVariables[name]));
1312                     else {
1313                         try {
1314                             var newValue = dactyl.userEval(expr);
1315                         }
1316                         catch (e) {}
1317                         util.assert(newValue !== undefined,
1318                             _("command.let.invalidExpression", expr));
1319
1320                         let value = newValue;
1321                         if (op) {
1322                             value = globalVariables[name];
1323                             if (op == "+")
1324                                 value += newValue;
1325                             else if (op == "-")
1326                                 value -= newValue;
1327                             else if (op == ".")
1328                                 value += String(newValue);
1329                         }
1330                         globalVariables[name] = value;
1331                     }
1332                 }
1333                 else
1334                     dactyl.echoerr(_("command.let.unexpectedChar"));
1335             },
1336             {
1337                 deprecated: "the options system",
1338                 literal: 0
1339             }
1340         );
1341
1342         [
1343             {
1344                 names: ["setl[ocal]"],
1345                 description: "Set local option",
1346                 modifiers: { scope: Option.SCOPE_LOCAL }
1347             },
1348             {
1349                 names: ["setg[lobal]"],
1350                 description: "Set global option",
1351                 modifiers: { scope: Option.SCOPE_GLOBAL }
1352             },
1353             {
1354                 names: ["se[t]"],
1355                 description: "Set an option",
1356                 modifiers: {},
1357                 extra: {
1358                     serialize: function () [
1359                         {
1360                             command: this.name,
1361                             literalArg: [opt.type == "boolean" ? (opt.value ? "" : "no") + opt.name
1362                                                                : opt.name + "=" + opt.stringValue]
1363                         }
1364                         for (opt in modules.options)
1365                         if (!opt.getter && !opt.isDefault && (opt.scope & Option.SCOPE_GLOBAL))
1366                     ]
1367                 }
1368             }
1369         ].forEach(function (params) {
1370             commands.add(params.names, params.description,
1371                 function (args, modifiers) {
1372                     setAction(args, update(modifiers, params.modifiers));
1373                 },
1374                 update({
1375                     bang: true,
1376                     completer: setCompleter,
1377                     domains: function domains(args) array.flatten(args.map(function (spec) {
1378                         try {
1379                             let opt = modules.options.parseOpt(spec);
1380                             if (opt.option && opt.option.domains)
1381                                 return opt.option.domains(opt.values);
1382                         }
1383                         catch (e) {
1384                             util.reportError(e);
1385                         }
1386                         return [];
1387                     })),
1388                     keepQuotes: true,
1389                     privateData: function privateData(args) args.some(function (spec) {
1390                         let opt = modules.options.parseOpt(spec);
1391                         return opt.option && opt.option.privateData &&
1392                             (!callable(opt.option.privateData) ||
1393                              opt.option.privateData(opt.values));
1394                     })
1395                 }, params.extra || {}));
1396         });
1397
1398         // TODO: deprecated. This needs to support "g:"-prefixed globals at a
1399         // minimum for now.
1400         commands.add(["unl[et]"],
1401             "Delete a variable",
1402             function (args) {
1403                 for (let [, name] in args) {
1404                     name = name.replace(/^g:/, ""); // throw away the scope prefix
1405                     if (!Set.has(dactyl._globalVariables, name)) {
1406                         if (!args.bang)
1407                             dactyl.echoerr(_("command.let.noSuch", name));
1408                         return;
1409                     }
1410
1411                     delete dactyl._globalVariables[name];
1412                 }
1413             },
1414             {
1415                 argCount: "+",
1416                 bang: true,
1417                 deprecated: "the options system"
1418             });
1419     },
1420     completion: function initCompletion(dactyl, modules, window) {
1421         const { completion } = modules;
1422
1423         completion.option = function option(context, scope, prefix) {
1424             context.title = ["Option"];
1425             context.keys = { text: "names", description: "description" };
1426             context.anchored = false;
1427             context.completions = modules.options;
1428             if (prefix == "inv")
1429                 context.keys.text = function (opt)
1430                     opt.type == "boolean" || isArray(opt.value) ? opt.names.map(function (n) "inv" + n)
1431                                                                 : opt.names;
1432             if (scope)
1433                 context.filters.push(function ({ item }) item.scope & scope);
1434         };
1435
1436         completion.optionValue = function (context, name, op, curValue, completer) {
1437             let opt = modules.options.get(name);
1438             completer = completer || opt.completer;
1439             if (!completer || !opt)
1440                 return;
1441
1442             try {
1443                 var curValues = curValue != null ? opt.parse(curValue) : opt.value;
1444                 var newValues = opt.parse(context.filter);
1445             }
1446             catch (e) {
1447                 context.message = _("error.error", e);
1448                 context.completions = [];
1449                 return;
1450             }
1451
1452             let extra = {};
1453             switch (opt.type) {
1454             case "boolean":
1455                 return;
1456             case "sitelist":
1457             case "regexplist":
1458                 newValues = Option.splitList(context.filter);
1459                 // Fallthrough
1460             case "stringlist":
1461                 break;
1462             case "charlist":
1463                 Option._splitAt = newValues.length;
1464                 break;
1465             case "stringmap":
1466             case "sitemap":
1467             case "regexpmap":
1468                 let vals = Option.splitList(context.filter);
1469                 let target = vals.pop() || "";
1470
1471                 let [count, key, quote] = Commands.parseArg(target, /:/, true);
1472                 let split = Option._splitAt;
1473
1474                 extra.key = Option.dequote(key);
1475                 extra.value = count < target.length ? Option.dequote(target.substr(count + 1)) : null;
1476                 extra.values = opt.parse(vals.join(","));
1477
1478                 Option._splitAt = split + (extra.value == null ? 0 : count + 1);
1479                 break;
1480             }
1481             // TODO: Highlight when invalid
1482             context.advance(Option._splitAt);
1483             context.filter = Option.dequote(context.filter);
1484
1485             function val(obj) {
1486                 if (isArray(opt.defaultValue)) {
1487                     let val = array.nth(obj, function (re) re.key == extra.key, 0);
1488                     return val && val.result;
1489                 }
1490                 if (Set.has(opt.defaultValue, extra.key))
1491                     return obj[extra.key];
1492             }
1493
1494             if (extra.key && extra.value != null) {
1495                 context.fork("default", 0, this, function (context) {
1496                     context.completions = [
1497                             [val(opt.value), _("option.currentValue")],
1498                             [val(opt.defaultValue), _("option.defaultValue")]
1499                     ].filter(function (f) f[0] !== "" && f[0] != null);
1500                 });
1501                 context = context.fork("stuff", 0);
1502             }
1503
1504             context.title = ["Option Value"];
1505             context.quote = Commands.complQuote[Option._quote] || Commands.complQuote[""];
1506             // Not Vim compatible, but is a significant enough improvement
1507             // that it's worth breaking compatibility.
1508             if (isArray(newValues)) {
1509                 context.filters.push(function (i) newValues.indexOf(i.text) == -1);
1510                 if (op == "+")
1511                     context.filters.push(function (i) curValues.indexOf(i.text) == -1);
1512                 if (op == "-")
1513                     context.filters.push(function (i) curValues.indexOf(i.text) > -1);
1514
1515                 memoize(extra, "values", function () {
1516                     if (op == "+")
1517                         return curValues.concat(newValues);
1518                     if (op == "-")
1519                         return curValues.filter(function (v) newValues.indexOf(val) == -1);
1520                     return newValues;
1521                 });
1522             }
1523
1524             let res = completer.call(opt, context, extra);
1525             if (res)
1526                 context.completions = res;
1527         };
1528     },
1529     javascript: function initJavascript(dactyl, modules, window) {
1530         const { options, JavaScript } = modules;
1531         JavaScript.setCompleter(Options.prototype.get, [function () ([o.name, o.description] for (o in options))]);
1532     },
1533     sanitizer: function initSanitizer(dactyl, modules, window) {
1534         const { sanitizer } = modules;
1535
1536         sanitizer.addItem("options", {
1537             description: "Options containing hostname data",
1538             action: function sanitize_action(timespan, host) {
1539                 if (host)
1540                     for (let opt in values(modules.options._options))
1541                         if (timespan.contains(opt.lastSet * 1000) && opt.domains)
1542                             try {
1543                                 opt.value = opt.filterDomain(host, opt.value);
1544                             }
1545                             catch (e) {
1546                                 dactyl.reportError(e);
1547                             }
1548             },
1549             privateEnter: function privateEnter() {
1550                 for (let opt in values(modules.options._options))
1551                     if (opt.privateData && (!callable(opt.privateData) || opt.privateData(opt.value)))
1552                         opt.oldValue = opt.value;
1553             },
1554             privateLeave: function privateLeave() {
1555                 for (let opt in values(modules.options._options))
1556                     if (opt.oldValue != null) {
1557                         opt.value = opt.oldValue;
1558                         opt.oldValue = null;
1559                     }
1560             }
1561         });
1562     }
1563 });
1564
1565 endModule();
1566
1567 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1568
1569 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: