]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/options.jsm
Import r6976 from upstream hg supporting Firefox up to 25.*
[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-2013 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(() => { this.value = this.value; });
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, val => this.value.indexOf(val) >= 0),
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 {{{
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, val => !this.domains([val]).some(val => util.isSubdomain(val, host))),
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(val => val.map(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      * @property {number} Returns the timestamp when the option's value was
404      *     last changed.
405      */
406     get lastSet() options.store.get(this.name).time,
407     set lastSet(val) { options.store.set(this.name, { value: this.globalValue, time: Date.now() }); },
408
409     /**
410      * @property {nsIFile} The script in which this option was last set. null
411      *     implies an interactive command.
412      */
413     setFrom: null
414
415     //}}}
416 }, {
417     /**
418      * @property {number} Global option scope.
419      * @final
420      */
421     SCOPE_GLOBAL: 1,
422
423     /**
424      * @property {number} Local option scope. Options in this scope only
425      *     apply to the current tab/buffer.
426      * @final
427      */
428     SCOPE_LOCAL: 2,
429
430     /**
431      * @property {number} Both local and global option scope.
432      * @final
433      */
434     SCOPE_BOTH: 3,
435
436     has: {
437         toggleAll: function toggleAll() toggleAll.supercall(this, "all") ^ !!toggleAll.superapply(this, arguments),
438     },
439
440     parseRegexp: function parseRegexp(value, result, flags) {
441         let keepQuotes = this && this.keepQuotes;
442         if (isArray(flags)) // Called by Array.map
443             result = flags = undefined;
444
445         if (flags == null)
446             flags = this && this.regexpFlags || "";
447
448         let [, bang, val] = /^(!?)(.*)/.exec(value);
449         let re = util.regexp(Option.dequote(val), flags);
450         re.bang = bang;
451         re.result = result !== undefined ? result : !bang;
452         re.key = re.bang + Option.quote(util.regexp.getSource(re), /^!|:/);
453         re.toString = function () Option.unparseRegexp(this, keepQuotes);
454         return re;
455     },
456
457     unparseRegexp: function unparseRegexp(re, quoted) re.bang + Option.quote(util.regexp.getSource(re), /^!|:/) +
458         (typeof re.result === "boolean" ? "" : ":" + (quoted ? re.result : Option.quote(re.result, /:/))),
459
460     parseSite: function parseSite(pattern, result, rest) {
461         if (isArray(rest)) // Called by Array.map
462             result = undefined;
463
464         let [, bang, filter] = /^(!?)(.*)/.exec(pattern);
465         filter = Option.dequote(filter).trim();
466
467         let quote = this.keepQuotes ? v => v
468                                     : v => Option.quote(v, /:/);
469
470         return update(Styles.matchFilter(filter), {
471             bang: bang,
472             filter: filter,
473             result: result !== undefined ? result : !bang,
474             toString: function toString() this.bang + Option.quote(this.filter, /:/) +
475                 (typeof this.result === "boolean" ? "" : ":" + quote(this.result)),
476         });
477     },
478
479     getKey: {
480         stringlist: function stringlist(k) this.value.indexOf(k) >= 0,
481         get charlist() this.stringlist,
482
483         regexplist: function regexplist(k, default_=null) {
484             for (let re in values(this.value))
485                 if ((re.test || re).call(re, k))
486                     return re.result;
487             return default_;
488         },
489         get regexpmap() this.regexplist,
490         get sitelist() this.regexplist,
491         get sitemap() this.regexplist
492     },
493
494     domains: {
495         sitelist: function (vals) array.compact(vals.map(site => util.getHost(site.filter))),
496         get sitemap() this.sitelist
497     },
498
499     stringify: {
500         charlist:    function (vals) Commands.quote(vals.join("")),
501
502         stringlist:  function (vals) vals.map(Option.quote).join(","),
503
504         stringmap:   function (vals) [Option.quote(k, /:/) + ":" + Option.quote(v, /:/) for ([k, v] in Iterator(vals))].join(","),
505
506         regexplist:  function (vals) vals.join(","),
507         get regexpmap() this.regexplist,
508         get sitelist() this.regexplist,
509         get sitemap() this.regexplist
510     },
511
512     parse: {
513         number:     function (value) let (val = Option.dequote(value))
514                             Option.validIf(Number(val) % 1 == 0, _("option.intRequired")) && parseInt(val),
515
516         boolean:    function boolean(value) Option.dequote(value) == "true" || value == true ? true : false,
517
518         charlist:   function charlist(value) Array.slice(Option.dequote(value)),
519
520         stringlist: function stringlist(value) (value === "") ? [] : Option.splitList(value),
521
522         regexplist: function regexplist(value) (value === "") ? [] :
523             Option.splitList(value, true)
524                   .map(re => Option.parseRegexp(re, undefined, this.regexpFlags)),
525
526         sitelist: function sitelist(value) {
527             if (value === "")
528                 return [];
529             if (!isArray(value))
530                 value = Option.splitList(value, true);
531             return value.map(Option.parseSite, this);
532         },
533
534         stringmap: function stringmap(value) array.toObject(
535             Option.splitList(value, true).map(function (v) {
536                 let [count, key, quote] = Commands.parseArg(v, /:/);
537                 return [key, Option.dequote(v.substr(count + 1))];
538             })),
539
540         regexpmap: function regexpmap(value) Option.parse.list.call(this, value, Option.parseRegexp),
541
542         sitemap: function sitemap(value) Option.parse.list.call(this, value, Option.parseSite),
543
544         list: function list(value, parse) let (prev = null)
545             array.compact(Option.splitList(value, true).map(function (v) {
546                 let [count, filter, quote] = Commands.parseArg(v, /:/, true);
547
548                 let val = v.substr(count + 1);
549                 if (!this.keepQuotes)
550                     val = Option.dequote(val);
551
552                 if (v.length > count)
553                     return prev = parse.call(this, filter, val);
554                 else {
555                     util.assert(prev, _("error.syntaxError"), false);
556                     prev.result += "," + v;
557                 }
558             }, this))
559     },
560
561     testValues: {
562         regexpmap:  function regexpmap(vals, validator) vals.every(re => validator(re.result)),
563         get sitemap() this.regexpmap,
564         stringlist: function stringlist(vals, validator) vals.every(validator, this),
565         stringmap:  function stringmap(vals, validator) values(vals).every(validator, this)
566     },
567
568     dequote: function dequote(value) {
569         let arg;
570         [, arg, Option._quote] = Commands.parseArg(String(value), "");
571         Option._splitAt = 0;
572         return arg;
573     },
574
575     splitList: function splitList(value, keepQuotes) {
576         let res = [];
577         Option._splitAt = 0;
578         while (value.length) {
579             if (count !== undefined)
580                 value = value.slice(1);
581             var [count, arg, quote] = Commands.parseArg(value, /,/, keepQuotes);
582             Option._quote = quote; // FIXME
583             res.push(arg);
584             if (value.length > count)
585                 Option._splitAt += count + 1;
586             value = value.slice(count);
587         }
588         return res;
589     },
590
591     quote: function quote(str, re) isArray(str) ? str.map(s => quote(s, re)).join(",") :
592         Commands.quoteArg[/[\s|"'\\,]|^$/.test(str) || re && re.test && re.test(str)
593             ? (/[\b\f\n\r\t]/.test(str) ? '"' : "'")
594             : ""](str, re),
595
596     ops: {
597         boolean: function boolean(operator, values, scope, invert) {
598             if (operator != "=")
599                 return null;
600             if (invert)
601                 return !this.value;
602             return values;
603         },
604
605         number: function number(operator, values, scope, invert) {
606             if (invert)
607                 values = values[(values.indexOf(String(this.value)) + 1) % values.length];
608
609             let value = parseInt(values);
610             util.assert(Number(values) % 1 == 0,
611                         _("command.set.numberRequired", this.name, values));
612
613             switch (operator) {
614             case "+":
615                 return this.value + value;
616             case "-":
617                 return this.value - value;
618             case "^":
619                 return this.value * value;
620             case "=":
621                 return value;
622             }
623             return null;
624         },
625
626         string: function string(operator, values, scope, invert) {
627             if (invert)
628                 return values[(values.indexOf(this.value) + 1) % values.length];
629
630             switch (operator) {
631             case "+":
632                 return this.value + values;
633             case "-":
634                 return this.value.replace(values, "");
635             case "^":
636                 return values + this.value;
637             case "=":
638                 return values;
639             }
640             return null;
641         },
642
643         stringmap: function stringmap(operator, values, scope, invert) {
644             let res = update({}, this.value);
645
646             switch (operator) {
647             // The result is the same.
648             case "+":
649             case "^":
650                 return update(res, values);
651             case "-":
652                 for (let [k, v] in Iterator(values))
653                     if (v === res[k])
654                         delete res[k];
655                 return res;
656             case "=":
657                 if (invert) {
658                     for (let [k, v] in Iterator(values))
659                         if (v === res[k])
660                             delete res[k];
661                         else
662                             res[k] = v;
663                     return res;
664                 }
665                 return values;
666             }
667             return null;
668         },
669
670         stringlist: function stringlist(operator, values, scope, invert) {
671             values = Array.concat(values);
672
673             function uniq(ary) {
674                 let seen = {};
675                 return ary.filter(elem => !Set.add(seen, elem));
676             }
677
678             switch (operator) {
679             case "+":
680                 return uniq(Array.concat(this.value, values), true);
681             case "^":
682                 // NOTE: Vim doesn't prepend if there's a match in the current value
683                 return uniq(Array.concat(values, this.value), true);
684             case "-":
685                 return this.value.filter(function (item) !Set.has(this, item), Set(values));
686             case "=":
687                 if (invert) {
688                     let keepValues = this.value.filter(function (item) !Set.has(this, item), Set(values));
689                     let addValues  = values.filter(function (item) !Set.has(this, item), Set(this.value));
690                     return addValues.concat(keepValues);
691                 }
692                 return values;
693             }
694             return null;
695         },
696         get charlist() this.stringlist,
697         get regexplist() this.stringlist,
698         get regexpmap() this.stringlist,
699         get sitelist() this.stringlist,
700         get sitemap() this.stringlist
701     },
702
703     validIf: function validIf(test, error) {
704         if (test)
705             return true;
706         throw ValueError(error);
707     },
708
709     /**
710      * Validates the specified *values* against values generated by the
711      * option's completer function.
712      *
713      * @param {value|[string]} values The value or array of values to validate.
714      * @returns {boolean}
715      */
716     validateCompleter: function validateCompleter(vals) {
717         function completions(extra) {
718             let context = CompletionContext("");
719             return context.fork("", 0, this, this.completer, extra) ||
720                    context.allItems.items.map(item => [item.text]);
721         };
722
723         if (isObject(vals) && !isArray(vals)) {
724             let k = values(completions.call(this, { values: {} })).toObject();
725             let v = values(completions.call(this, { value: "" })).toObject();
726             return Object.keys(vals).every(Set.has(k)) && values(vals).every(Set.has(v));
727         }
728
729         if (this.values)
730             var acceptable = this.values.array || this.values;
731         else
732             acceptable = completions.call(this);
733
734         if (isArray(acceptable))
735             acceptable = Set(acceptable.map(([k]) => (k)));
736
737         if (this.type === "regexpmap" || this.type === "sitemap")
738             return Array.concat(vals).every(re => Set.has(acceptable, re.result));
739
740         return Array.concat(vals).every(Set.has(acceptable));
741     },
742
743     types: {}
744 });
745
746 ["Boolean",
747  "Charlist",
748  "Number",
749  "RegexpList",
750  "RegexpMap",
751  "SiteList",
752  "SiteMap",
753  "String",
754  "StringList",
755  "StringMap"].forEach(function (name) {
756      let type = name.toLowerCase();
757      let class_ = Class(name + "Option", Option, {
758          type: type,
759
760          _op: Option.ops[type]
761      });
762
763     if (type in Option.getKey)
764         class_.prototype.getKey = Option.getKey[type];
765
766     if (type in Option.parse)
767         class_.prototype.parse = Option.parse[type];
768
769     if (type in Option.stringify)
770         class_.prototype.stringify = Option.stringify[type];
771
772     if (type in Option.domains)
773         class_.prototype.domains = Option.domains[type];
774
775     if (type in Option.testValues)
776         class_.prototype.testValues = Option.testValues[type];
777
778     Option.types[type] = class_;
779     this[class_.className] = class_;
780     EXPORTED_SYMBOLS.push(class_.className);
781 }, this);
782
783 update(BooleanOption.prototype, {
784     names: Class.Memoize(function ()
785                 array.flatten([[name, "no" + name] for (name in values(this.realNames))]))
786 });
787
788 var OptionHive = Class("OptionHive", Contexts.Hive, {
789     init: function init(group) {
790         init.supercall(this, group);
791         this.values = {};
792         this.has = Set.has(this.values);
793     },
794
795     add: function add(names, description, type, defaultValue, extraInfo) {
796         return this.modules.options.add(names, description, type, defaultValue, extraInfo);
797     }
798 });
799
800 /**
801  * @instance options
802  */
803 var Options = Module("options", {
804     Local: function Local(dactyl, modules, window) let ({ contexts } = modules) ({
805         init: function init() {
806             const self = this;
807
808             update(this, {
809                 hives: contexts.Hives("options", Class("OptionHive", OptionHive, { modules: modules })),
810                 user: contexts.hives.options.user
811             });
812
813             this.needInit = [];
814             this._options = [];
815             this._optionMap = {};
816
817             storage.newMap("options", { store: false });
818             storage.addObserver("options", function optionObserver(key, event, option) {
819                 // Trigger any setters.
820                 let opt = self.get(option);
821                 if (event == "change" && opt)
822                     opt.set(opt.globalValue, Option.SCOPE_GLOBAL, true);
823             }, window);
824
825             modules.cache.register("options.dtd", () =>
826                 util.makeDTD(
827                     iter(([["option", o.name, "default"].join("."),
828                            o.type === "string" ? o.defaultValue.replace(/'/g, "''") :
829                            o.defaultValue === true  ? "on"  :
830                            o.defaultValue === false ? "off" : o.stringDefaultValue]
831                           for (o in self)),
832
833                          ([["option", o.name, "type"].join("."), o.type] for (o in self)),
834
835                          config.dtd)));
836         },
837
838         signals: {
839             "io.source": function ioSource(context, file, modTime) {
840                 cache.flushEntry("options.dtd", modTime);
841             }
842         },
843
844         dactyl: dactyl,
845
846         /**
847          * Lists all options in *scope* or only those with changed values if
848          * *onlyNonDefault* is specified.
849          *
850          * @param {function(Option)} filter Limit the list
851          * @param {number} scope Only list options in this scope (see
852          *     {@link Option#scope}).
853          */
854         list: function list(filter, scope) {
855             if (!scope)
856                 scope = Option.SCOPE_BOTH;
857
858             function opts(opt) {
859                 for (let opt in Iterator(this)) {
860                     if (filter && !filter(opt))
861                         continue;
862                     if (!(opt.scope & scope))
863                         continue;
864
865                     let option = {
866                         __proto__: opt,
867                         isDefault: opt.isDefault,
868                         default:   opt.stringDefaultValue,
869                         pre:       "\u00a0\u00a0", // Unicode nonbreaking space.
870                         value:     []
871                     };
872
873                     if (opt.type == "boolean") {
874                         if (!opt.value)
875                             option.pre = "no";
876                         option.default = (opt.defaultValue ? "" : "no") + opt.name;
877                     }
878                     else if (isArray(opt.value) && opt.type != "charlist")
879                         option.value = ["", "=",
880                                         template.map(opt.value,
881                                                      v => template.highlight(String(v)),
882                                                      ["", ",",
883                                                       ["span", { style: "width: 0; display: inline-block" }, " "]])];
884                     else
885                         option.value = ["", "=", template.highlight(opt.stringValue)];
886                     yield option;
887                 }
888             };
889
890             modules.commandline.commandOutput(
891                 template.options("Options", opts.call(this), this["verbose"] > 0));
892         },
893
894         cleanup: function cleanup() {
895             for (let opt in this)
896                 if (opt.cleanupValue != null)
897                     opt.stringValue = opt.cleanupValue;
898         },
899
900         /**
901          * Adds a new option.
902          *
903          * @param {[string]} names All names for the option.
904          * @param {string} description A description of the option.
905          * @param {string} type The option type (see {@link Option#type}).
906          * @param {value} defaultValue The option's default value.
907          * @param {Object} extra An optional extra configuration hash (see
908          *     {@link Map#extraInfo}).
909          * @optional
910          */
911         add: function add(names, description, type, defaultValue, extraInfo) {
912             if (!util.isDactyl(Components.stack.caller))
913                 deprecated.warn(add, "options.add", "group.options.add");
914
915             util.assert(type in Option.types, _("option.noSuchType", type),
916                         false);
917
918             if (!extraInfo)
919                 extraInfo = {};
920
921             extraInfo.definedAt = contexts.getCaller(Components.stack.caller);
922
923             let name = names[0];
924             if (name in this._optionMap) {
925                 this.dactyl.log(_("option.replaceExisting", name.quote()), 1);
926                 this.remove(name);
927             }
928
929             let closure = () => this._optionMap[name];
930
931             memoize(this._optionMap, name,
932                     function () Option.types[type](modules, names, description, defaultValue, extraInfo));
933
934             for (let alias in values(names.slice(1)))
935                 memoize(this._optionMap, alias, closure);
936
937             if (extraInfo.setter && (!extraInfo.scope || extraInfo.scope & Option.SCOPE_GLOBAL))
938                 if (this.dactyl.initialized)
939                     closure().initValue();
940                 else
941                     memoize(this.needInit, this.needInit.length, closure);
942
943             this._floptions = (this._floptions || []).concat(name);
944             memoize(this._options, this._options.length, closure);
945
946             // quickly access options with options["wildmode"]:
947             this.__defineGetter__(name, function () this._optionMap[name].value);
948             this.__defineSetter__(name, function (value) { this._optionMap[name].value = value; });
949         }
950     }),
951
952     /** @property {Iterator(Option)} @private */
953     __iterator__: function __iterator__()
954         values(this._options.sort((a, b) => String.localeCompare(a.name, b.name))),
955
956     allPrefs: deprecated("prefs.getNames", function allPrefs() prefs.getNames.apply(prefs, arguments)),
957     getPref: deprecated("prefs.get", function getPref() prefs.get.apply(prefs, arguments)),
958     invertPref: deprecated("prefs.invert", function invertPref() prefs.invert.apply(prefs, arguments)),
959     listPrefs: deprecated("prefs.list", function listPrefs() { this.modules.commandline.commandOutput(prefs.list.apply(prefs, arguments)); }),
960     observePref: deprecated("prefs.observe", function observePref() prefs.observe.apply(prefs, arguments)),
961     popContext: deprecated("prefs.popContext", function popContext() prefs.popContext.apply(prefs, arguments)),
962     pushContext: deprecated("prefs.pushContext", function pushContext() prefs.pushContext.apply(prefs, arguments)),
963     resetPref: deprecated("prefs.reset", function resetPref() prefs.reset.apply(prefs, arguments)),
964     safeResetPref: deprecated("prefs.safeReset", function safeResetPref() prefs.safeReset.apply(prefs, arguments)),
965     safeSetPref: deprecated("prefs.safeSet", function safeSetPref() prefs.safeSet.apply(prefs, arguments)),
966     setPref: deprecated("prefs.set", function setPref() prefs.set.apply(prefs, arguments)),
967     withContext: deprecated("prefs.withContext", function withContext() prefs.withContext.apply(prefs, arguments)),
968
969     cleanupPrefs: Class.Memoize(() => config.prefs.Branch("cleanup.option.")),
970
971     cleanup: function cleanup(reason) {
972         if (~["disable", "uninstall"].indexOf(reason))
973             this.cleanupPrefs.resetBranch();
974     },
975
976     /**
977      * Returns the option with *name* in the specified *scope*.
978      *
979      * @param {string} name The option's name.
980      * @param {number} scope The option's scope (see {@link Option#scope}).
981      * @optional
982      * @returns {Option} The matching option.
983      */
984     get: function get(name, scope) {
985         if (!scope)
986             scope = Option.SCOPE_BOTH;
987
988         if (this._optionMap[name] && (this._optionMap[name].scope & scope))
989             return this._optionMap[name];
990         return null;
991     },
992
993     /**
994      * Parses a :set command's argument string.
995      *
996      * @param {string} args The :set command's argument string.
997      * @param {Object} modifiers A hash of parsing modifiers. These are:
998      *     scope - see {@link Option#scope}
999      * @optional
1000      * @returns {Object} The parsed command object.
1001      */
1002     parseOpt: function parseOpt(args, modifiers) {
1003         let res = {};
1004         let matches, prefix, postfix;
1005
1006         [matches, prefix, res.name, postfix, res.valueGiven, res.operator, res.value] =
1007         args.match(/^\s*(no|inv)?([^=]+?)([?&!])?\s*(([-+^]?)=(.*))?\s*$/) || [];
1008
1009         res.args = args;
1010         res.onlyNonDefault = false; // used for :set to print non-default options
1011         if (!args) {
1012             res.name = "all";
1013             res.onlyNonDefault = true;
1014         }
1015
1016         if (matches) {
1017             if (res.option = this.get(res.name, res.scope)) {
1018                 if (prefix === "no" && res.option.type !== "boolean")
1019                     res.option = null;
1020             }
1021             else if (res.option = this.get(prefix + res.name, res.scope)) {
1022                 res.name = prefix + res.name;
1023                 prefix = "";
1024             }
1025         }
1026
1027         res.prefix = prefix;
1028         res.postfix = postfix;
1029
1030         res.all = (res.name == "all");
1031         res.get = (res.all || postfix == "?" || (res.option && res.option.type != "boolean" && !res.valueGiven));
1032         res.invert = (prefix == "inv" || postfix == "!");
1033         res.reset = (postfix == "&");
1034         res.unsetBoolean = (prefix == "no");
1035
1036         res.scope = modifiers && modifiers.scope;
1037
1038         if (!res.option)
1039             return res;
1040
1041         if (res.value === undefined)
1042             res.value = "";
1043
1044         res.optionValue = res.option.get(res.scope);
1045
1046         try {
1047             if (!res.invert || res.option.type != "number") // Hack.
1048                 res.values = res.option.parse(res.value);
1049         }
1050         catch (e) {
1051             res.error = e;
1052         }
1053
1054         return res;
1055     },
1056
1057     /**
1058      * Remove the option with matching *name*.
1059      *
1060      * @param {string} name The name of the option to remove. This can be
1061      *     any of the option's names.
1062      */
1063     remove: function remove(name) {
1064         let opt = this.get(name);
1065         this._options = this._options.filter(o => o != opt);
1066         for (let name in values(opt.names))
1067             delete this._optionMap[name];
1068     },
1069
1070     /** @property {Object} The options store. */
1071     get store() storage.options
1072 }, {
1073 }, {
1074     commands: function initCommands(dactyl, modules, window) {
1075         const { commands, contexts, options } = modules;
1076
1077         dactyl.addUsageCommand({
1078             name: ["listo[ptions]", "lo"],
1079             description: "List all options along with their short descriptions",
1080             index: "option",
1081             iterate: function (args) options,
1082             format: {
1083                 description: function (opt) [
1084                         opt.scope == Option.SCOPE_LOCAL
1085                             ? ["span", { highlight: "URLExtra" },
1086                                   "(" + _("option.bufferLocal") + ")"]
1087                             : "",
1088                         template.linkifyHelp(opt.description)
1089                 ],
1090                 help: function (opt) "'" + opt.name + "'"
1091             }
1092         });
1093
1094         function setAction(args, modifiers) {
1095             let bang = args.bang;
1096             if (!args.length)
1097                 args[0] = "";
1098
1099             let list = [];
1100             function flushList() {
1101                 let names = Set(list.map(opt => opt.option ? opt.option.name : ""));
1102                 if (list.length)
1103                     if (list.some(opt => opt.all))
1104                         options.list(opt => !(list[0].onlyNonDefault && opt.isDefault),
1105                                      list[0].scope);
1106                     else
1107                         options.list(opt => Set.has(names, opt.name),
1108                                      list[0].scope);
1109                 list = [];
1110             }
1111
1112             for (let [, arg] in args) {
1113                 if (bang) {
1114                     let onlyNonDefault = false;
1115                     let reset = false;
1116                     let invertBoolean = false;
1117
1118                     if (args[0] == "") {
1119                         var name = "all";
1120                         onlyNonDefault = true;
1121                     }
1122                     else {
1123                         var [matches, name, postfix, valueGiven, operator, value] =
1124                             arg.match(/^\s*?((?:[^=\\']|\\.|'[^']*')+?)([?&!])?\s*(([-+^]?)=(.*))?\s*$/);
1125                         reset = (postfix == "&");
1126                         invertBoolean = (postfix == "!");
1127                     }
1128
1129                     name = Option.dequote(name);
1130                     if (name == "all" && reset)
1131                         modules.commandline.input(_("pref.prompt.resetAll", config.host) + " ",
1132                             function (resp) {
1133                                 if (resp == "yes")
1134                                     for (let pref in values(prefs.getNames()))
1135                                         prefs.reset(pref);
1136                             },
1137                             { promptHighlight: "WarningMsg" });
1138                     else if (name == "all")
1139                         modules.commandline.commandOutput(prefs.list(onlyNonDefault, ""));
1140                     else if (reset)
1141                         prefs.reset(name);
1142                     else if (invertBoolean)
1143                         prefs.toggle(name);
1144                     else if (valueGiven) {
1145                         if (value == undefined)
1146                             value = "";
1147                         else if (value == "true")
1148                             value = true;
1149                         else if (value == "false")
1150                             value = false;
1151                         else if (Number(value) % 1 == 0)
1152                             value = parseInt(value);
1153                         else
1154                             value = Option.dequote(value);
1155
1156                         if (operator)
1157                             value = Option.ops[typeof value].call({ value: prefs.get(name) }, operator, value);
1158                         prefs.set(name, value);
1159                     }
1160                     else
1161                         modules.commandline.commandOutput(prefs.list(onlyNonDefault, name));
1162                     return;
1163                 }
1164
1165                 let opt = modules.options.parseOpt(arg, modifiers);
1166                 util.assert(opt, _("command.set.errorParsing", arg));
1167                 util.assert(!opt.error, _("command.set.errorParsing", opt.error));
1168
1169                 let option = opt.option;
1170                 util.assert(option != null || opt.all, _("command.set.unknownOption", opt.name));
1171
1172                 // reset a variable to its default value
1173                 if (opt.reset) {
1174                     flushList();
1175                     if (opt.all) {
1176                         for (let option in modules.options)
1177                             option.reset();
1178                     }
1179                     else {
1180                         option.reset();
1181                     }
1182                 }
1183                 // read access
1184                 else if (opt.get)
1185                     list.push(opt);
1186                 // write access
1187                 else {
1188                     flushList();
1189                     if (opt.option.type === "boolean") {
1190                         util.assert(!opt.valueGiven, _("error.invalidArgument", arg));
1191                         opt.values = !opt.unsetBoolean;
1192                     }
1193                     else if (/^(string|number)$/.test(opt.option.type) && opt.invert)
1194                         opt.values = Option.splitList(opt.value);
1195                     try {
1196                         var res = opt.option.op(opt.operator || "=", opt.values, opt.scope, opt.invert,
1197                                                 opt.value);
1198                     }
1199                     catch (e) {
1200                         res = e;
1201                     }
1202                     if (res)
1203                         dactyl.echoerr(res);
1204                     option.setFrom = contexts.getCaller(null);
1205                 }
1206             }
1207             flushList();
1208         }
1209
1210         function setCompleter(context, args, modifiers) {
1211             const { completion } = modules;
1212
1213             let filter = context.filter;
1214
1215             if (args.bang) { // list completions for about:config entries
1216                 if (filter[filter.length - 1] == "=") {
1217                     context.advance(filter.length);
1218                     filter = filter.substr(0, filter.length - 1);
1219
1220                     context.pushProcessor(0, (item, text, next) => next(item, text.substr(0, 100)));
1221                     context.completions = [
1222                             [prefs.get(filter), _("option.currentValue")],
1223                             [prefs.defaults.get(filter), _("option.defaultValue")]
1224                     ].filter(k => k[0] != null);
1225                     return null;
1226                 }
1227
1228                 return completion.preference(context);
1229             }
1230
1231             let opt = modules.options.parseOpt(filter, modifiers);
1232             let prefix = opt.prefix;
1233
1234             context.highlight();
1235             if (context.filter.indexOf("=") == -1) {
1236                 if (false && prefix)
1237                     context.filters.push(({ item }) => (item.type == "boolean" ||
1238                                                         prefix == "inv" && isArray(item.values)));
1239
1240                 return completion.option(context, opt.scope,
1241                                          opt.name == "inv" ? opt.name
1242                                                            : prefix);
1243             }
1244
1245             function error(length, message) {
1246                 context.message = message;
1247                 context.highlight(0, length, "SPELLCHECK");
1248             }
1249
1250             let option = opt.option;
1251             if (!option)
1252                 return error(opt.name.length, _("option.noSuch", opt.name));
1253
1254             context.advance(context.filter.indexOf("="));
1255             if (option.type == "boolean")
1256                 return error(context.filter.length, _("error.trailingCharacters"));
1257
1258             context.advance(1);
1259             if (opt.error)
1260                 return error(context.filter.length, opt.error);
1261
1262             if (opt.get || opt.reset || !option || prefix)
1263                 return null;
1264
1265             if (!opt.value && !opt.operator && !opt.invert) {
1266                 context.fork("default", 0, this, function (context) {
1267                     context.title = ["Extra Completions"];
1268                     context.pushProcessor(0, (item, text, next) => next(item, text.substr(0, 100)));
1269                     context.completions = [
1270                             [option.stringValue, _("option.currentValue")],
1271                             [option.stringDefaultValue, _("option.defaultValue")]
1272                     ].filter(f => f[0] !== "");
1273                     context.quote = ["", util.identity, ""];
1274                 });
1275             }
1276
1277             let optcontext = context.fork("values");
1278             modules.completion.optionValue(optcontext, opt.name, opt.operator);
1279
1280             // Fill in the current values if we're removing
1281             if (opt.operator == "-" && isArray(opt.values)) {
1282                 let have = Set([i.text for (i in values(context.allItems.items))]);
1283                 context = context.fork("current-values", 0);
1284                 context.anchored = optcontext.anchored;
1285                 context.maxItems = optcontext.maxItems;
1286
1287                 context.filters.push(i => !Set.has(have, i.text));
1288                 modules.completion.optionValue(context, opt.name, opt.operator, null,
1289                                        function (context) {
1290                                            context.generate = () => option.value.map(o => [o, ""]);
1291                                        });
1292                 context.title = ["Current values"];
1293             }
1294         }
1295
1296         // TODO: deprecated. This needs to support "g:"-prefixed globals at a
1297         // minimum for now.  The coderepos plugins make extensive use of global
1298         // variables.
1299         commands.add(["let"],
1300             "Set or list a variable",
1301             function (args) {
1302                 let globalVariables = dactyl._globalVariables;
1303                 args = (args[0] || "").trim();
1304                 function fmt(value) (typeof value == "number"   ? "#" :
1305                                      typeof value == "function" ? "*" :
1306                                                                   " ") + value;
1307                 util.assert(!(!args || args == "g:"));
1308
1309                 let matches = args.match(/^([a-z]:)?([\w]+)(?:\s*([-+.])?=\s*(.*)?)?$/);
1310                 if (matches) {
1311                     let [, scope, name, op, expr] = matches;
1312                     let fullName = (scope || "") + name;
1313
1314                     util.assert(scope == "g:" || scope == null,
1315                                 _("command.let.illegalVar", scope + name));
1316                     util.assert(Set.has(globalVariables, name) || (expr && !op),
1317                                 _("command.let.undefinedVar", fullName));
1318
1319                     if (!expr)
1320                         dactyl.echo(fullName + "\t\t" + fmt(globalVariables[name]));
1321                     else {
1322                         try {
1323                             var newValue = dactyl.userEval(expr);
1324                         }
1325                         catch (e) {}
1326                         util.assert(newValue !== undefined,
1327                             _("command.let.invalidExpression", expr));
1328
1329                         let value = newValue;
1330                         if (op) {
1331                             value = globalVariables[name];
1332                             if (op == "+")
1333                                 value += newValue;
1334                             else if (op == "-")
1335                                 value -= newValue;
1336                             else if (op == ".")
1337                                 value += String(newValue);
1338                         }
1339                         globalVariables[name] = value;
1340                     }
1341                 }
1342                 else
1343                     dactyl.echoerr(_("command.let.unexpectedChar"));
1344             },
1345             {
1346                 deprecated: "the options system",
1347                 literal: 0
1348             }
1349         );
1350
1351         [
1352             {
1353                 names: ["setl[ocal]"],
1354                 description: "Set local option",
1355                 modifiers: { scope: Option.SCOPE_LOCAL }
1356             },
1357             {
1358                 names: ["setg[lobal]"],
1359                 description: "Set global option",
1360                 modifiers: { scope: Option.SCOPE_GLOBAL }
1361             },
1362             {
1363                 names: ["se[t]"],
1364                 description: "Set an option",
1365                 modifiers: {},
1366                 extra: {
1367                     serialize: function () [
1368                         {
1369                             command: this.name,
1370                             literalArg: [opt.type == "boolean" ? (opt.value ? "" : "no") + opt.name
1371                                                                : opt.name + "=" + opt.stringValue]
1372                         }
1373                         for (opt in modules.options)
1374                         if (!opt.getter && !opt.isDefault && (opt.scope & Option.SCOPE_GLOBAL))
1375                     ]
1376                 }
1377             }
1378         ].forEach(function (params) {
1379             commands.add(params.names, params.description,
1380                 function (args, modifiers) {
1381                     setAction(args, update(modifiers, params.modifiers));
1382                 },
1383                 update({
1384                     bang: true,
1385                     completer: setCompleter,
1386                     domains: function domains(args) array.flatten(args.map(function (spec) {
1387                         try {
1388                             let opt = modules.options.parseOpt(spec);
1389                             if (opt.option && opt.option.domains)
1390                                 return opt.option.domains(opt.values);
1391                         }
1392                         catch (e) {
1393                             util.reportError(e);
1394                         }
1395                         return [];
1396                     })),
1397                     keepQuotes: true,
1398                     privateData: function privateData(args) args.some(function (spec) {
1399                         let opt = modules.options.parseOpt(spec);
1400                         return opt.option && opt.option.privateData &&
1401                             (!callable(opt.option.privateData) ||
1402                              opt.option.privateData(opt.values));
1403                     })
1404                 }, params.extra || {}));
1405         });
1406
1407         // TODO: deprecated. This needs to support "g:"-prefixed globals at a
1408         // minimum for now.
1409         commands.add(["unl[et]"],
1410             "Delete a variable",
1411             function (args) {
1412                 for (let [, name] in args) {
1413                     name = name.replace(/^g:/, ""); // throw away the scope prefix
1414                     if (!Set.has(dactyl._globalVariables, name)) {
1415                         if (!args.bang)
1416                             dactyl.echoerr(_("command.let.noSuch", name));
1417                         return;
1418                     }
1419
1420                     delete dactyl._globalVariables[name];
1421                 }
1422             },
1423             {
1424                 argCount: "+",
1425                 bang: true,
1426                 deprecated: "the options system"
1427             });
1428     },
1429     completion: function initCompletion(dactyl, modules, window) {
1430         const { completion } = modules;
1431
1432         completion.option = function option(context, scope, prefix) {
1433             context.title = ["Option"];
1434             context.keys = { text: "names", description: "description" };
1435             context.anchored = false;
1436             context.completions = modules.options;
1437             if (prefix == "inv")
1438                 context.keys.text = opt =>
1439                     opt.type == "boolean" || isArray(opt.value) ? opt.names.map(n => "inv" + n)
1440                                                                 : opt.names;
1441             if (scope)
1442                 context.filters.push(({ item }) => item.scope & scope);
1443         };
1444
1445         completion.optionValue = function (context, name, op, curValue, completer) {
1446             let opt = modules.options.get(name);
1447             completer = completer || opt.completer;
1448             if (!completer || !opt)
1449                 return;
1450
1451             try {
1452                 var curValues = curValue != null ? opt.parse(curValue) : opt.value;
1453                 var newValues = opt.parse(context.filter);
1454             }
1455             catch (e) {
1456                 context.message = _("error.error", e);
1457                 context.completions = [];
1458                 return;
1459             }
1460
1461             let extra = {};
1462             switch (opt.type) {
1463             case "boolean":
1464                 return;
1465             case "sitelist":
1466             case "regexplist":
1467                 newValues = Option.splitList(context.filter);
1468                 // Fallthrough
1469             case "stringlist":
1470                 break;
1471             case "charlist":
1472                 Option._splitAt = newValues.length;
1473                 break;
1474             case "stringmap":
1475             case "sitemap":
1476             case "regexpmap":
1477                 let vals = Option.splitList(context.filter);
1478                 let target = vals.pop() || "";
1479
1480                 let [count, key, quote] = Commands.parseArg(target, /:/, true);
1481                 let split = Option._splitAt;
1482
1483                 extra.key = Option.dequote(key);
1484                 extra.value = count < target.length ? Option.dequote(target.substr(count + 1)) : null;
1485                 extra.values = opt.parse(vals.join(","));
1486
1487                 Option._splitAt = split + (extra.value == null ? 0 : count + 1);
1488                 break;
1489             }
1490             // TODO: Highlight when invalid
1491             context.advance(Option._splitAt);
1492             context.filter = Option.dequote(context.filter);
1493
1494             function val(obj) {
1495                 if (isArray(opt.defaultValue)) {
1496                     let val = array.nth(obj, re => (re.key == extra.key),
1497                                         0);
1498                     return val && val.result;
1499                 }
1500                 if (Set.has(opt.defaultValue, extra.key))
1501                     return obj[extra.key];
1502             }
1503
1504             if (extra.key && extra.value != null) {
1505                 context.fork("default", 0, this, function (context) {
1506                     context.completions = [
1507                             [val(opt.value), _("option.currentValue")],
1508                             [val(opt.defaultValue), _("option.defaultValue")]
1509                     ].filter(f => (f[0] !== "" && f[0] != null));
1510                 });
1511                 context = context.fork("stuff", 0);
1512             }
1513
1514             context.title = ["Option Value"];
1515             context.quote = Commands.complQuote[Option._quote] || Commands.complQuote[""];
1516             // Not Vim compatible, but is a significant enough improvement
1517             // that it's worth breaking compatibility.
1518             if (isArray(newValues)) {
1519                 context.filters.push(i => newValues.indexOf(i.text) == -1);
1520                 if (op == "+")
1521                     context.filters.push(i => curValues.indexOf(i.text) == -1);
1522                 if (op == "-")
1523                     context.filters.push(i => curValues.indexOf(i.text) > -1);
1524
1525                 memoize(extra, "values", function () {
1526                     if (op == "+")
1527                         return curValues.concat(newValues);
1528                     if (op == "-")
1529                         return curValues.filter(v => newValues.indexOf(val) == -1);
1530                     return newValues;
1531                 });
1532             }
1533
1534             let res = completer.call(opt, context, extra);
1535             if (res)
1536                 context.completions = res;
1537         };
1538     },
1539     javascript: function initJavascript(dactyl, modules, window) {
1540         const { options, JavaScript } = modules;
1541         JavaScript.setCompleter(Options.prototype.get, [() => ([o.name, o.description] for (o in options))]);
1542     },
1543     sanitizer: function initSanitizer(dactyl, modules, window) {
1544         const { sanitizer } = modules;
1545
1546         sanitizer.addItem("options", {
1547             description: "Options containing hostname data",
1548             action: function sanitize_action(timespan, host) {
1549                 if (host)
1550                     for (let opt in values(modules.options._options))
1551                         if (timespan.contains(opt.lastSet * 1000) && opt.domains)
1552                             try {
1553                                 opt.value = opt.filterDomain(host, opt.value);
1554                             }
1555                             catch (e) {
1556                                 dactyl.reportError(e);
1557                             }
1558             },
1559             privateEnter: function privateEnter() {
1560                 for (let opt in values(modules.options._options))
1561                     if (opt.privateData && (!callable(opt.privateData) || opt.privateData(opt.value)))
1562                         opt.oldValue = opt.value;
1563             },
1564             privateLeave: function privateLeave() {
1565                 for (let opt in values(modules.options._options))
1566                     if (opt.oldValue != null) {
1567                         opt.value = opt.oldValue;
1568                         opt.oldValue = null;
1569                     }
1570             }
1571         });
1572     }
1573 });
1574
1575 endModule();
1576
1577 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1578
1579 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: