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