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