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