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