1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2014 by Kris Maglione <maglione.k@gmail.com>
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
11 defineModule("options", {
12 exports: ["Option", "Options", "ValueError", "options"],
13 require: ["contexts", "messages", "storage"]
16 lazyRequire("cache", ["cache"]);
17 lazyRequire("config", ["config"]);
18 lazyRequire("commands", ["Commands"]);
19 lazyRequire("completion", ["CompletionContext"]);
20 lazyRequire("prefs", ["prefs"]);
21 lazyRequire("styles", ["Styles"]);
22 lazyRequire("template", ["template"]);
26 let ValueError = Class("ValueError", ErrorBase);
28 // do NOT create instances of this class yourself, use the helper method
29 // options.add() instead
31 * A class representing configuration options. Instances are created by the
32 * {@link Options} class.
34 * @param {[string]} names The names by which this option is identified.
35 * @param {string} description A short one line description of the option.
36 * @param {string} type The option's value data type (see {@link Option#type}).
37 * @param {string} defaultValue The default value for this option.
38 * @param {Object} extraInfo An optional extra configuration hash. The
39 * following properties are supported.
40 * completer - see {@link Option#completer}
41 * domains - see {@link Option#domains}
42 * getter - see {@link Option#getter}
43 * initialValue - Initial value is loaded from getter
44 * persist - see {@link Option#persist}
45 * privateData - see {@link Option#privateData}
46 * scope - see {@link Option#scope}
47 * setter - see {@link Option#setter}
48 * validator - see {@link Option#validator}
52 var Option = Class("Option", {
53 init: function init(modules, names, description, defaultValue, extraInfo) {
54 this.modules = modules;
56 this.realNames = names;
57 this.description = description;
60 this.update(extraInfo);
62 this._defaultValue = defaultValue;
64 if (this.globalValue == undefined && !this.initialValue)
65 this.globalValue = this.defaultValue;
68 magicalProperties: RealSet(["cleanupValue"]),
71 * @property {string} This option's description, as shown in :listoptions.
73 description: Messages.Localized(""),
75 get helpTag() "'" + this.name + "'",
77 initValue: function initValue() {
78 util.trapErrors(() => { this.value = this.value; });
81 get isDefault() this.stringValue === this.stringDefaultValue,
83 /** @property {value} The value to reset this option to at cleanup time. */
84 get cleanupValue() options.cleanupPrefs.get(this.name),
85 set cleanupValue(value) {
86 if (options.cleanupPrefs.get(this.name) == null)
87 options.cleanupPrefs.set(this.name, value);
90 /** @property {value} The option's global value. @see #scope */
92 let val = options.store.get(this.name, {}).value;
95 return this.globalValue = this.defaultValue;
97 set globalValue(val) {
98 options.store.set(this.name,
99 { value: this.parse(this.stringify(val)),
104 * Returns *value* as an array of parsed values if the option type is
105 * "charlist" or "stringlist" or else unchanged.
107 * @param {value} value The option value.
108 * @returns {value|[string]}
110 parse: function parse(value) Option.dequote(value),
112 parseKey: function parseKey(value) value,
115 * Returns *values* packed in the appropriate format for the option type.
117 * @param {value|[string]} values The option value.
120 stringify: function stringify(vals) Commands.quote(vals),
123 * Returns the option's value as an array of parsed values if the option
124 * type is "charlist" or "stringlist" or else the simple value.
126 * @param {number} scope The scope to return these values from (see
127 * {@link Option#scope}).
128 * @returns {value|[string]}
130 get: function get(scope) {
132 if ((scope & this.scope) == 0) // option doesn't exist in this scope
141 if (config.has("tabs") && (scope & Option.SCOPE_LOCAL))
142 values = tabs.options[this.name];
144 if ((scope & Option.SCOPE_GLOBAL) && (values == undefined))
145 values = this.globalValue;
147 if (hasOwnProperty(this, "_value"))
148 values = this._value;
151 return util.trapErrors(this.getter, this, values);
157 * Sets the option's value from an array of values if the option type is
158 * "charlist" or "stringlist" or else the simple value.
160 * @param {number} scope The scope to apply these values to (see
161 * {@link Option#scope}).
163 set: function set(newValues, scope, skipGlobal) {
164 scope = scope || this.scope;
165 if ((scope & this.scope) == 0) // option doesn't exist in this scope
169 newValues = this.setter(newValues);
170 if (newValues === undefined)
174 if (config.has("tabs") && (scope & Option.SCOPE_LOCAL))
175 tabs.options[this.name] = newValues;
177 if ((scope & Option.SCOPE_GLOBAL) && !skipGlobal)
178 this.globalValue = newValues;
179 this._value = newValues;
181 this.hasChanged = true;
184 // dactyl.triggerObserver("options." + this.name, newValues);
187 getValues: deprecated("Option#get", "get"),
188 setValues: deprecated("Option#set", "set"),
189 joinValues: deprecated("Option#stringify", "stringify"),
190 parseValues: deprecated("Option#parse", "parse"),
193 * @property {value} The option's current value. The option's local value,
194 * or if no local value is set, this is equal to the
195 * (@link #globalValue).
197 get value() this.get(),
198 set value(val) this.set(val),
200 get stringValue() this.stringify(this.value),
201 set stringValue(value) this.value = this.parse(value),
203 get stringDefaultValue() this.stringify(this.defaultValue),
204 set stringDefaultValue(val) this.defaultValue = this.parse(val),
206 getKey: function getKey(key) undefined,
209 * Returns whether the option value contains one or more of the specified
214 has: function has() Array.some(arguments, val => this.value.indexOf(val) >= 0),
217 * Returns whether this option is identified by *name*.
219 * @param {string} name
222 hasName: function hasName(name) this.names.indexOf(name) >= 0,
225 * Returns whether the specified *values* are valid for this option.
226 * @see Option#validator
228 isValidValue: function isValidValue(values) this.validator(values),
230 invalidArgument: function invalidArgument(arg, op) _("error.invalidArgument",
231 this.name + (op || "").replace(/=?$/, "=") + arg),
234 * Resets the option to its default value.
236 reset: function reset() {
237 this.value = this.defaultValue;
241 * Sets the option's value using the specified set *operator*.
243 * @param {string} operator The set operator.
244 * @param {value|[string]} values The value (or values) to apply.
245 * @param {number} scope The scope to apply this value to (see
247 * @param {boolean} invert Whether this is an invert boolean operation.
249 op: function op(operator, values, scope, invert, str) {
252 var newValues = this._op(operator, values, scope, invert);
253 if (newValues == null)
254 return _("option.operatorNotSupported", operator, this.type);
256 if (!this.isValidValue(newValues))
257 return this.invalidArgument(str || this.stringify(values), operator);
259 this.set(newValues, scope);
262 if (!(e instanceof ValueError))
264 return this.invalidArgument(str || this.stringify(values), operator) + ": " + e.message;
271 /** @property {string} The option's canonical name. */
274 /** @property {[string]} All names by which this option is identified. */
275 names: Class.Memoize(function () this.realNames),
278 * @property {string} The option's data type. One of:
279 * "boolean" - Boolean, e.g., true
280 * "number" - Integer, e.g., 1
281 * "string" - String, e.g., "Pentadactyl"
282 * "charlist" - Character list, e.g., "rb"
283 * "regexplist" - Regexp list, e.g., "^foo,bar$"
284 * "stringmap" - String map, e.g., "key:v,foo:bar"
285 * "regexpmap" - Regexp map, e.g., "^key:v,foo$:bar"
290 * @property {number} The scope of the option. This can be local, global,
292 * @see Option#SCOPE_LOCAL
293 * @see Option#SCOPE_GLOBAL
294 * @see Option#SCOPE_BOTH
296 scope: 1, // Option.SCOPE_GLOBAL // XXX set to BOTH by default someday? - kstep
299 * @property {function(CompletionContext, Args)} This option's completer.
300 * @see CompletionContext
302 completer: function completer(context, extra) {
303 if (/map$/.test(this.type) && extra.value == null)
307 context.completions = this.values;
311 * @property {[[string, string]]} This option's possible values.
312 * @see CompletionContext
314 values: Messages.Localized(null),
317 * @property {function(host, values)} A function which should return a list
318 * of domains referenced in the given values. Used in determining whether
319 * to purge the command from history when clearing private data.
320 * @see Command#domains
325 * @property {function(host, values)} A function which should strip
326 * references to a given domain from the given values.
328 filterDomain: function filterDomain(host, values)
329 Array.filter(values, val => !this.domains([val]).some(val => util.isSubdomain(val, host))),
332 * @property {value} The option's default value. This value will be used
333 * unless the option is explicitly set either interactively or in an RC
336 defaultValue: Class.Memoize(function () {
337 let defaultValue = this._defaultValue;
338 delete this._defaultValue;
340 if (hasOwnProperty(this.modules.config.optionDefaults, this.name))
341 defaultValue = this.modules.config.optionDefaults[this.name];
343 if (defaultValue == null && this.getter)
344 defaultValue = this.getter();
346 if (defaultValue == undefined)
349 if (this.type === "string")
350 defaultValue = Commands.quote(defaultValue);
352 if (isArray(defaultValue))
353 defaultValue = defaultValue.map(Option.quote).join(",");
354 else if (isObject(defaultValue))
355 defaultValue = iter(defaultValue).map(val => val.map(v => Option.quote(v, /:/))
359 if (isArray(defaultValue))
360 defaultValue = defaultValue.map(Option.quote).join(",");
362 return this.parse(defaultValue);
366 * @property {function} The function called when the option value is read.
371 * @property {boolean} When true, this options values will be saved
372 * when generating a configuration file.
378 * @property {boolean|function(values)} When true, values of this
379 * option may contain private data which should be purged from
380 * saved histories when clearing private data. If a function, it
381 * should return true if an invocation with the given values
382 * contains private data
387 * @property {function} The function called when the option value is set.
391 testValues: function testValues(values, validator) validator(values),
394 * @property {function} The function called to validate the option's value
397 validator: function validator() {
398 if (this.values || this.completer !== Option.prototype.completer)
399 return Option.validateCompleter.apply(this, arguments);
404 * @property {boolean} Set to true whenever the option is first set. This
405 * is useful to see whether it was changed from its default value
406 * interactively or by some RC file.
411 * @property {number} Returns the timestamp when the option's value was
414 get lastSet() options.store.get(this.name).time,
415 set lastSet(val) { options.store.set(this.name, { value: this.globalValue, time: Date.now() }); },
418 * @property {nsIFile} The script in which this option was last set. null
419 * implies an interactive command.
426 * @property {number} Global option scope.
432 * @property {number} Local option scope. Options in this scope only
433 * apply to the current tab/buffer.
439 * @property {number} Both local and global option scope.
445 toggleAll: function toggleAll() toggleAll.supercall(this, "all") ^ !!toggleAll.superapply(this, arguments),
448 parseRegexp: function parseRegexp(value, result, flags) {
449 let keepQuotes = this && this.keepQuotes;
450 if (isArray(flags)) // Called by Array.map
451 result = flags = undefined;
454 flags = this && this.regexpFlags || "";
456 let [, bang, val] = /^(!?)(.*)/.exec(value);
457 let re = util.regexp(Option.dequote(val), flags);
459 re.result = result !== undefined ? result : !bang;
460 re.key = re.bang + Option.quote(util.regexp.getSource(re), /^!|:/);
461 re.toString = function () Option.unparseRegexp(this, keepQuotes);
465 unparseRegexp: function unparseRegexp(re, quoted) re.bang + Option.quote(util.regexp.getSource(re), /^!|:/) +
466 (typeof re.result === "boolean" ? "" : ":" + (quoted ? re.result : Option.quote(re.result, /:/))),
468 parseSite: function parseSite(pattern, result, rest) {
469 if (isArray(rest)) // Called by Array.map
472 let [, bang, filter] = /^(!?)(.*)/.exec(pattern);
473 filter = Option.dequote(filter).trim();
475 let quote = this.keepQuotes ? v => v
476 : v => Option.quote(v, /:/);
478 return update(Styles.matchFilter(filter), {
481 result: result !== undefined ? result : !bang,
482 toString: function toString() this.bang + Option.quote(this.filter, /:/) +
483 (typeof this.result === "boolean" ? "" : ":" + quote(this.result)),
488 stringlist: function stringlist(k) this.value.indexOf(k) >= 0,
489 get charlist() this.stringlist,
491 regexplist: function regexplist(k, default_=null) {
492 for (let re in values(this.value))
493 if ((re.test || re).call(re, k))
497 get regexpmap() this.regexplist,
498 get sitelist() this.regexplist,
499 get sitemap() this.regexplist
503 sitelist: function (vals) array.compact(vals.map(site => util.getHost(site.filter))),
504 get sitemap() this.sitelist
508 charlist: function (vals) Commands.quote(vals.join("")),
510 stringlist: function (vals) vals.map(Option.quote).join(","),
512 stringmap: function (vals) [Option.quote(k, /:/) + ":" + Option.quote(v, /:/) for ([k, v] in Iterator(vals))].join(","),
514 regexplist: function (vals) vals.join(","),
515 get regexpmap() this.regexplist,
516 get sitelist() this.regexplist,
517 get sitemap() this.regexplist
521 number: function (value) let (val = Option.dequote(value))
522 Option.validIf(Number(val) % 1 == 0, _("option.intRequired")) && parseInt(val),
524 boolean: function boolean(value) Option.dequote(value) == "true" || value == true ? true : false,
526 charlist: function charlist(value) Array.slice(Option.dequote(value)),
528 stringlist: function stringlist(value) (value === "") ? [] : Option.splitList(value),
530 regexplist: function regexplist(value) (value === "") ? [] :
531 Option.splitList(value, true)
532 .map(re => Option.parseRegexp(re, undefined, this.regexpFlags)),
534 sitelist: function sitelist(value) {
538 value = Option.splitList(value, true);
539 return value.map(Option.parseSite, this);
542 stringmap: function stringmap(value) array.toObject(
543 Option.splitList(value, true).map(function (v) {
544 let [count, key, quote] = Commands.parseArg(v, /:/);
545 return [key, Option.dequote(v.substr(count + 1))];
548 regexpmap: function regexpmap(value) Option.parse.list.call(this, value, Option.parseRegexp),
550 sitemap: function sitemap(value) Option.parse.list.call(this, value, Option.parseSite),
552 list: function list(value, parse) let (prev = null)
553 array.compact(Option.splitList(value, true).map(function (v) {
554 let [count, filter, quote] = Commands.parseArg(v, /:/, true);
556 let val = v.substr(count + 1);
557 if (!this.keepQuotes)
558 val = Option.dequote(val);
560 if (v.length > count)
561 return prev = parse.call(this, filter, val);
563 util.assert(prev, _("error.syntaxError"), false);
564 prev.result += "," + v;
571 boolean: function boolean(value) value == "true" || value == true ? true : false,
575 regexpmap: function regexpmap(vals, validator) vals.every(re => validator(re.result)),
576 get sitemap() this.regexpmap,
577 stringlist: function stringlist(vals, validator) vals.every(validator, this),
578 stringmap: function stringmap(vals, validator) values(vals).every(validator, this)
581 dequote: function dequote(value) {
583 [, arg, Option._quote] = Commands.parseArg(String(value), "");
588 splitList: function splitList(value, keepQuotes) {
591 while (value.length) {
592 if (count !== undefined)
593 value = value.slice(1);
594 var [count, arg, quote] = Commands.parseArg(value, /,/, keepQuotes);
595 Option._quote = quote; // FIXME
597 if (value.length > count)
598 Option._splitAt += count + 1;
599 value = value.slice(count);
604 quote: function quote(str, re) isArray(str) ? str.map(s => quote(s, re)).join(",") :
605 Commands.quoteArg[/[\s|"'\\,]|^$/.test(str) || re && re.test && re.test(str)
606 ? (/[\b\f\n\r\t]/.test(str) ? '"' : "'")
610 boolean: function boolean(operator, values, scope, invert) {
618 number: function number(operator, values, scope, invert) {
620 values = values[(values.indexOf(String(this.value)) + 1) % values.length];
622 let value = parseInt(values);
623 util.assert(Number(values) % 1 == 0,
624 _("command.set.numberRequired", this.name, values));
628 return this.value + value;
630 return this.value - value;
632 return this.value * value;
639 string: function string(operator, values, scope, invert) {
641 return values[(values.indexOf(this.value) + 1) % values.length];
645 return this.value + values;
647 return this.value.replace(values, "");
649 return values + this.value;
656 stringmap: function stringmap(operator, values, scope, invert) {
657 let res = update({}, this.value);
660 // The result is the same.
663 return update(res, values);
665 for (let [k, v] in Iterator(values))
671 for (let [k, v] in Iterator(values))
683 stringlist: function stringlist(operator, values, scope, invert) {
684 values = Array.concat(values);
687 let seen = RealSet();
688 return ary.filter(elem => !seen.add(elem));
693 return uniq(Array.concat(this.value, values), true);
695 // NOTE: Vim doesn't prepend if there's a match in the current value
696 return uniq(Array.concat(values, this.value), true);
698 return this.value.filter(function (item) !this.has(item), RealSet(values));
701 let keepValues = this.value.filter(function (item) !this.has(item), RealSet(values));
702 let addValues = values.filter(function (item) !this.has(item), RealSet(this.value));
703 return addValues.concat(keepValues);
709 get charlist() this.stringlist,
710 get regexplist() this.stringlist,
711 get regexpmap() this.stringlist,
712 get sitelist() this.stringlist,
713 get sitemap() this.stringlist
716 validIf: function validIf(test, error) {
719 throw ValueError(error);
723 * Validates the specified *values* against values generated by the
724 * option's completer function.
726 * @param {value|[string]} values The value or array of values to validate.
729 validateCompleter: function validateCompleter(vals) {
730 function completions(extra) {
731 let context = CompletionContext("");
732 return context.fork("", 0, this, this.completer, extra) ||
733 context.allItems.items.map(item => [item.text]);
736 if (isObject(vals) && !isArray(vals)) {
737 let k = values(completions.call(this, { values: {} })).toObject();
738 let v = values(completions.call(this, { value: "" })).toObject();
740 return Object.keys(vals).every(hasOwnProperty.bind(null, k)) &&
741 values(vals).every(hasOwnProperty.bind(null, v));
745 var acceptable = this.values.array || this.values;
747 acceptable = completions.call(this);
749 if (isArray(acceptable))
750 acceptable = RealSet(acceptable.map(([k]) => k));
752 acceptable = RealSet(this.parseKey(k)
753 for (k of Object.keys(acceptable)));
755 if (this.type === "regexpmap" || this.type === "sitemap")
756 return Array.concat(vals).every(re => acceptable.has(re.result));
758 return Array.concat(vals).every(v => acceptable.has(v));
773 "StringMap"].forEach(function (name) {
774 let type = name.toLowerCase();
775 let class_ = Class(name + "Option", Option, {
778 _op: Option.ops[type]
781 if (type in Option.getKey)
782 class_.prototype.getKey = Option.getKey[type];
784 if (type in Option.parse)
785 class_.prototype.parse = Option.parse[type];
787 if (type in Option.parseKey)
788 class_.prototype.parseKey = Option.parse[type];
790 if (type in Option.stringify)
791 class_.prototype.stringify = Option.stringify[type];
793 if (type in Option.domains)
794 class_.prototype.domains = Option.domains[type];
796 if (type in Option.testValues)
797 class_.prototype.testValues = Option.testValues[type];
799 Option.types[type] = class_;
800 this[class_.className] = class_;
801 EXPORTED_SYMBOLS.push(class_.className);
804 update(BooleanOption.prototype, {
805 names: Class.Memoize(function ()
806 array.flatten([[name, "no" + name] for (name in values(this.realNames))]))
809 var OptionHive = Class("OptionHive", Contexts.Hive, {
810 init: function init(group) {
811 init.supercall(this, group);
813 this.has = v => hasOwnProperty(this.values, v);
816 add: function add(names, description, type, defaultValue, extraInfo) {
817 return this.modules.options.add(names, description, type, defaultValue, extraInfo);
824 var Options = Module("options", {
825 Local: function Local(dactyl, modules, window) let ({ contexts } = modules) ({
826 init: function init() {
830 hives: contexts.Hives("options", Class("OptionHive", OptionHive, { modules: modules })),
831 user: contexts.hives.options.user
836 this._optionMap = {};
838 storage.newMap("options", { store: false });
839 storage.addObserver("options", function optionObserver(key, event, option) {
840 // Trigger any setters.
841 let opt = self.get(option);
842 if (event == "change" && opt)
843 opt.set(opt.globalValue, Option.SCOPE_GLOBAL, true);
846 modules.cache.register("options.dtd",
848 iter(([["option", o.name, "default"].join("."),
849 o.type === "string" ? o.defaultValue.replace(/'/g, "''") :
850 o.defaultValue === true ? "on" :
851 o.defaultValue === false ? "off" : o.stringDefaultValue]
854 ([["option", o.name, "type"].join("."), o.type] for (o in self)),
861 "io.source": function ioSource(context, file, modTime) {
862 cache.flushEntry("options.dtd", modTime);
869 * Lists all options in *scope* or only those with changed values if
870 * *onlyNonDefault* is specified.
872 * @param {function(Option)} filter Limit the list
873 * @param {number} scope Only list options in this scope (see
874 * {@link Option#scope}).
876 list: function list(filter, scope) {
878 scope = Option.SCOPE_BOTH;
881 for (let opt in Iterator(this)) {
882 if (filter && !filter(opt))
884 if (!(opt.scope & scope))
889 isDefault: opt.isDefault,
890 default: opt.stringDefaultValue,
891 pre: "\u00a0\u00a0", // Unicode nonbreaking space.
895 if (opt.type == "boolean") {
898 option.default = (opt.defaultValue ? "" : "no") + opt.name;
900 else if (isArray(opt.value) && opt.type != "charlist")
901 option.value = ["", "=",
902 template.map(opt.value,
903 v => template.highlight(String(v)),
905 ["span", { style: "width: 0; display: inline-block" }, " "]])];
907 option.value = ["", "=", template.highlight(opt.stringValue)];
912 modules.commandline.commandOutput(
913 template.options("Options", opts.call(this), this["verbose"] > 0));
916 cleanup: function cleanup() {
917 for (let opt in this)
918 if (opt.cleanupValue != null)
919 opt.stringValue = opt.cleanupValue;
925 * @param {[string]} names All names for the option.
926 * @param {string} description A description of the option.
927 * @param {string} type The option type (see {@link Option#type}).
928 * @param {value} defaultValue The option's default value.
929 * @param {Object} extra An optional extra configuration hash (see
930 * {@link Map#extraInfo}).
933 add: function add(names, description, type, defaultValue, extraInfo) {
934 if (!util.isDactyl(Components.stack.caller))
935 deprecated.warn(add, "options.add", "group.options.add");
937 util.assert(type in Option.types, _("option.noSuchType", type),
943 extraInfo.definedAt = contexts.getCaller(Components.stack.caller);
946 if (name in this._optionMap) {
947 this.dactyl.log(_("option.replaceExisting", name.quote()), 1);
951 let closure = () => this._optionMap[name];
953 memoize(this._optionMap, name,
954 function () Option.types[type](modules, names, description, defaultValue, extraInfo));
956 for (let alias in values(names.slice(1)))
957 memoize(this._optionMap, alias, closure);
959 if (extraInfo.setter && (!extraInfo.scope || extraInfo.scope & Option.SCOPE_GLOBAL))
960 if (this.dactyl.initialized)
961 closure().initValue();
963 memoize(this.needInit, this.needInit.length, closure);
965 this._floptions = (this._floptions || []).concat(name);
966 memoize(this._options, this._options.length, closure);
968 // quickly access options with options["wildmode"]:
969 this.__defineGetter__(name, function () this._optionMap[name].value);
970 this.__defineSetter__(name, function (value) { this._optionMap[name].value = value; });
974 /** @property {Iterator(Option)} @private */
975 __iterator__: function __iterator__()
976 values(this._options.sort((a, b) => String.localeCompare(a.name, b.name))),
978 allPrefs: deprecated("prefs.getNames", function allPrefs() prefs.getNames.apply(prefs, arguments)),
979 getPref: deprecated("prefs.get", function getPref() prefs.get.apply(prefs, arguments)),
980 invertPref: deprecated("prefs.invert", function invertPref() prefs.invert.apply(prefs, arguments)),
981 listPrefs: deprecated("prefs.list", function listPrefs() { this.modules.commandline.commandOutput(prefs.list.apply(prefs, arguments)); }),
982 observePref: deprecated("prefs.observe", function observePref() prefs.observe.apply(prefs, arguments)),
983 popContext: deprecated("prefs.popContext", function popContext() prefs.popContext.apply(prefs, arguments)),
984 pushContext: deprecated("prefs.pushContext", function pushContext() prefs.pushContext.apply(prefs, arguments)),
985 resetPref: deprecated("prefs.reset", function resetPref() prefs.reset.apply(prefs, arguments)),
986 safeResetPref: deprecated("prefs.safeReset", function safeResetPref() prefs.safeReset.apply(prefs, arguments)),
987 safeSetPref: deprecated("prefs.safeSet", function safeSetPref() prefs.safeSet.apply(prefs, arguments)),
988 setPref: deprecated("prefs.set", function setPref() prefs.set.apply(prefs, arguments)),
989 withContext: deprecated("prefs.withContext", function withContext() prefs.withContext.apply(prefs, arguments)),
991 cleanupPrefs: Class.Memoize(() => config.prefs.Branch("cleanup.option.")),
993 cleanup: function cleanup(reason) {
994 if (~["disable", "uninstall"].indexOf(reason))
995 this.cleanupPrefs.resetBranch();
999 * Returns the option with *name* in the specified *scope*.
1001 * @param {string} name The option's name.
1002 * @param {number} scope The option's scope (see {@link Option#scope}).
1004 * @returns {Option} The matching option.
1006 get: function get(name, scope) {
1008 scope = Option.SCOPE_BOTH;
1010 if (this._optionMap[name] && (this._optionMap[name].scope & scope))
1011 return this._optionMap[name];
1016 * Parses a :set command's argument string.
1018 * @param {string} args The :set command's argument string.
1019 * @param {Object} modifiers A hash of parsing modifiers. These are:
1020 * scope - see {@link Option#scope}
1022 * @returns {Object} The parsed command object.
1024 parseOpt: function parseOpt(args, modifiers) {
1026 let matches, prefix, postfix;
1028 [matches, prefix, res.name, postfix, res.valueGiven, res.operator, res.value] =
1029 args.match(/^\s*(no|inv)?([^=]+?)([?&!])?\s*(([-+^]?)=(.*))?\s*$/) || [];
1032 res.onlyNonDefault = false; // used for :set to print non-default options
1035 res.onlyNonDefault = true;
1039 if (res.option = this.get(res.name, res.scope)) {
1040 if (prefix === "no" && res.option.type !== "boolean")
1043 else if (res.option = this.get(prefix + res.name, res.scope)) {
1044 res.name = prefix + res.name;
1049 res.prefix = prefix;
1050 res.postfix = postfix;
1052 res.all = (res.name == "all");
1053 res.get = (res.all || postfix == "?" || (res.option && res.option.type != "boolean" && !res.valueGiven));
1054 res.invert = (prefix == "inv" || postfix == "!");
1055 res.reset = (postfix == "&");
1056 res.unsetBoolean = (prefix == "no");
1058 res.scope = modifiers && modifiers.scope;
1063 if (res.value === undefined)
1066 res.optionValue = res.option.get(res.scope);
1069 if (!res.invert || res.option.type != "number") // Hack.
1070 res.values = res.option.parse(res.value);
1080 * Remove the option with matching *name*.
1082 * @param {string} name The name of the option to remove. This can be
1083 * any of the option's names.
1085 remove: function remove(name) {
1086 let opt = this.get(name);
1087 this._options = this._options.filter(o => o != opt);
1088 for (let name in values(opt.names))
1089 delete this._optionMap[name];
1092 /** @property {Object} The options store. */
1093 get store() storage.options
1096 commands: function initCommands(dactyl, modules, window) {
1097 const { commands, contexts, options } = modules;
1099 dactyl.addUsageCommand({
1100 name: ["listo[ptions]", "lo"],
1101 description: "List all options along with their short descriptions",
1103 iterate: function (args) options,
1105 description: function (opt) [
1106 opt.scope == Option.SCOPE_LOCAL
1107 ? ["span", { highlight: "URLExtra" },
1108 "(" + _("option.bufferLocal") + ")"]
1110 template.linkifyHelp(opt.description)
1112 help: function (opt) "'" + opt.name + "'"
1116 function setAction(args, modifiers) {
1117 let bang = args.bang;
1122 function flushList() {
1123 let names = RealSet(list.map(opt => opt.option ? opt.option.name : ""));
1125 if (list.some(opt => opt.all))
1126 options.list(opt => !(list[0].onlyNonDefault && opt.isDefault),
1129 options.list(opt => names.has(opt.name),
1134 for (let [, arg] in args) {
1136 let onlyNonDefault = false;
1138 let invertBoolean = false;
1140 if (args[0] == "") {
1142 onlyNonDefault = true;
1145 var [matches, name, postfix, valueGiven, operator, value] =
1146 arg.match(/^\s*?((?:[^=\\']|\\.|'[^']*')+?)([?&!])?\s*(([-+^]?)=(.*))?\s*$/);
1147 reset = (postfix == "&");
1148 invertBoolean = (postfix == "!");
1151 name = Option.dequote(name);
1152 if (name == "all" && reset)
1153 modules.commandline.input(_("pref.prompt.resetAll", config.host) + " ",
1156 for (let pref in values(prefs.getNames()))
1159 { promptHighlight: "WarningMsg" });
1160 else if (name == "all")
1161 modules.commandline.commandOutput(prefs.list(onlyNonDefault, ""));
1164 else if (invertBoolean)
1166 else if (valueGiven) {
1167 if (value == undefined)
1169 else if (value == "true")
1171 else if (value == "false")
1173 else if (Number(value) % 1 == 0)
1174 value = parseInt(value);
1176 value = Option.dequote(value);
1179 value = Option.ops[typeof value].call({ value: prefs.get(name) }, operator, value);
1180 prefs.set(name, value);
1183 modules.commandline.commandOutput(prefs.list(onlyNonDefault, name));
1187 let opt = modules.options.parseOpt(arg, modifiers);
1188 util.assert(opt, _("command.set.errorParsing", arg));
1189 util.assert(!opt.error, _("command.set.errorParsing", opt.error));
1191 let option = opt.option;
1192 util.assert(option != null || opt.all, _("command.set.unknownOption", opt.name));
1194 // reset a variable to its default value
1198 for (let option in modules.options)
1211 if (opt.option.type === "boolean") {
1212 util.assert(!opt.valueGiven, _("error.invalidArgument", arg));
1213 opt.values = !opt.unsetBoolean;
1215 else if (/^(string|number)$/.test(opt.option.type) && opt.invert)
1216 opt.values = Option.splitList(opt.value);
1218 var res = opt.option.op(opt.operator || "=", opt.values, opt.scope, opt.invert,
1225 dactyl.echoerr(res);
1226 option.setFrom = contexts.getCaller(null);
1232 function setCompleter(context, args, modifiers) {
1233 const { completion } = modules;
1235 let filter = context.filter;
1237 if (args.bang) { // list completions for about:config entries
1238 if (filter[filter.length - 1] == "=") {
1239 context.advance(filter.length);
1240 filter = filter.substr(0, filter.length - 1);
1242 context.pushProcessor(0, (item, text, next) => next(item, text.substr(0, 100)));
1243 context.completions = [
1244 [prefs.get(filter), _("option.currentValue")],
1245 [prefs.defaults.get(filter), _("option.defaultValue")]
1246 ].filter(k => k[0] != null);
1250 return completion.preference(context);
1253 let opt = modules.options.parseOpt(filter, modifiers);
1254 let prefix = opt.prefix;
1256 context.highlight();
1257 if (context.filter.indexOf("=") == -1) {
1258 if (false && prefix)
1259 context.filters.push(({ item }) => (item.type == "boolean" ||
1260 prefix == "inv" && isArray(item.values)));
1262 return completion.option(context, opt.scope,
1263 opt.name == "inv" ? opt.name
1267 function error(length, message) {
1268 context.message = message;
1269 context.highlight(0, length, "SPELLCHECK");
1272 let option = opt.option;
1274 return error(opt.name.length, _("option.noSuch", opt.name));
1276 context.advance(context.filter.indexOf("="));
1277 if (option.type == "boolean")
1278 return error(context.filter.length, _("error.trailingCharacters"));
1282 return error(context.filter.length, opt.error);
1284 if (opt.get || opt.reset || !option || prefix)
1287 if (!opt.value && !opt.operator && !opt.invert) {
1288 context.fork("default", 0, this, function (context) {
1289 context.title = ["Extra Completions"];
1290 context.pushProcessor(0, (item, text, next) => next(item, text.substr(0, 100)));
1291 context.completions = [
1292 [option.stringValue, _("option.currentValue")],
1293 [option.stringDefaultValue, _("option.defaultValue")]
1294 ].filter(f => f[0] !== "");
1295 context.quote = ["", util.identity, ""];
1299 let optcontext = context.fork("values");
1300 modules.completion.optionValue(optcontext, opt.name, opt.operator);
1302 // Fill in the current values if we're removing
1303 if (opt.operator == "-" && isArray(opt.values)) {
1304 let have = RealSet((i.text for (i in values(context.allItems.items))));
1305 context = context.fork("current-values", 0);
1306 context.anchored = optcontext.anchored;
1307 context.maxItems = optcontext.maxItems;
1309 context.filters.push(i => !have.has(i.text));
1310 modules.completion.optionValue(context, opt.name, opt.operator, null,
1311 function (context) {
1312 context.generate = () => option.value.map(o => [o, ""]);
1314 context.title = ["Current values"];
1318 // TODO: deprecated. This needs to support "g:"-prefixed globals at a
1319 // minimum for now. The coderepos plugins make extensive use of global
1321 commands.add(["let"],
1322 "Set or list a variable",
1324 let globalVariables = dactyl._globalVariables;
1325 args = (args[0] || "").trim();
1326 function fmt(value) (typeof value == "number" ? "#" :
1327 typeof value == "function" ? "*" :
1329 util.assert(!(!args || args == "g:"));
1331 let matches = args.match(/^([a-z]:)?([\w]+)(?:\s*([-+.])?=\s*(.*)?)?$/);
1333 let [, scope, name, op, expr] = matches;
1334 let fullName = (scope || "") + name;
1336 util.assert(scope == "g:" || scope == null,
1337 _("command.let.illegalVar", scope + name));
1338 util.assert(hasOwnProperty(globalVariables, name) || (expr && !op),
1339 _("command.let.undefinedVar", fullName));
1342 dactyl.echo(fullName + "\t\t" + fmt(globalVariables[name]));
1345 var newValue = dactyl.userEval(expr);
1348 util.assert(newValue !== undefined,
1349 _("command.let.invalidExpression", expr));
1351 let value = newValue;
1353 value = globalVariables[name];
1359 value += String(newValue);
1361 globalVariables[name] = value;
1365 dactyl.echoerr(_("command.let.unexpectedChar"));
1368 deprecated: "the options system",
1375 names: ["setl[ocal]"],
1376 description: "Set local option",
1377 modifiers: { scope: Option.SCOPE_LOCAL }
1380 names: ["setg[lobal]"],
1381 description: "Set global option",
1382 modifiers: { scope: Option.SCOPE_GLOBAL }
1386 description: "Set an option",
1389 serialize: function () [
1392 literalArg: [opt.type == "boolean" ? (opt.value ? "" : "no") + opt.name
1393 : opt.name + "=" + opt.stringValue]
1395 for (opt in modules.options)
1396 if (!opt.getter && !opt.isDefault && (opt.scope & Option.SCOPE_GLOBAL))
1400 ].forEach(function (params) {
1401 commands.add(params.names, params.description,
1402 function (args, modifiers) {
1403 setAction(args, update(modifiers, params.modifiers));
1407 completer: setCompleter,
1408 domains: function domains(args) array.flatten(args.map(function (spec) {
1410 let opt = modules.options.parseOpt(spec);
1411 if (opt.option && opt.option.domains)
1412 return opt.option.domains(opt.values);
1415 util.reportError(e);
1420 privateData: function privateData(args) args.some(function (spec) {
1421 let opt = modules.options.parseOpt(spec);
1422 return opt.option && opt.option.privateData &&
1423 (!callable(opt.option.privateData) ||
1424 opt.option.privateData(opt.values));
1426 }, params.extra || {}));
1429 // TODO: deprecated. This needs to support "g:"-prefixed globals at a
1431 commands.add(["unl[et]"],
1432 "Delete a variable",
1434 for (let [, name] in args) {
1435 name = name.replace(/^g:/, ""); // throw away the scope prefix
1436 if (!hasOwnProperty(dactyl._globalVariables, name)) {
1438 dactyl.echoerr(_("command.let.noSuch", name));
1442 delete dactyl._globalVariables[name];
1448 deprecated: "the options system"
1451 completion: function initCompletion(dactyl, modules, window) {
1452 const { completion } = modules;
1454 completion.option = function option(context, scope, prefix) {
1455 context.title = ["Option"];
1456 context.keys = { text: "names", description: "description" };
1457 context.anchored = false;
1458 context.completions = modules.options;
1459 if (prefix == "inv")
1460 context.keys.text = opt =>
1461 opt.type == "boolean" || isArray(opt.value) ? opt.names.map(n => "inv" + n)
1464 context.filters.push(({ item }) => item.scope & scope);
1467 completion.optionValue = function (context, name, op, curValue, completer) {
1468 let opt = modules.options.get(name);
1469 completer = completer || opt.completer;
1470 if (!completer || !opt)
1474 var curValues = curValue != null ? opt.parse(curValue) : opt.value;
1475 var newValues = opt.parse(context.filter);
1478 context.message = _("error.error", e);
1479 context.completions = [];
1489 newValues = Option.splitList(context.filter);
1494 Option._splitAt = newValues.length;
1499 let vals = Option.splitList(context.filter);
1500 let target = vals.pop() || "";
1502 let [count, key, quote] = Commands.parseArg(target, /:/, true);
1503 let split = Option._splitAt;
1505 extra.key = Option.dequote(key);
1506 extra.value = count < target.length ? Option.dequote(target.substr(count + 1)) : null;
1507 extra.values = opt.parse(vals.join(","));
1509 Option._splitAt = split + (extra.value == null ? 0 : count + 1);
1512 // TODO: Highlight when invalid
1513 context.advance(Option._splitAt);
1514 context.filter = Option.dequote(context.filter);
1517 if (isArray(opt.defaultValue)) {
1518 let val = [].find.call(obj, re => (re.key == extra.key));
1519 return val && val.result;
1521 if (hasOwnProperty(opt.defaultValue, extra.key))
1522 return obj[extra.key];
1525 if (extra.key && extra.value != null) {
1526 context.fork("default", 0, this, function (context) {
1527 context.completions = [
1528 [val(opt.value), _("option.currentValue")],
1529 [val(opt.defaultValue), _("option.defaultValue")]
1530 ].filter(f => (f[0] !== "" && f[0] != null));
1532 context = context.fork("stuff", 0);
1535 context.title = ["Option Value"];
1536 context.quote = Commands.complQuote[Option._quote] || Commands.complQuote[""];
1537 // Not Vim compatible, but is a significant enough improvement
1538 // that it's worth breaking compatibility.
1539 if (isArray(newValues)) {
1540 context.filters.push(i => newValues.indexOf(i.text) == -1);
1542 context.filters.push(i => curValues.indexOf(i.text) == -1);
1544 context.filters.push(i => curValues.indexOf(i.text) > -1);
1546 memoize(extra, "values", function () {
1548 return curValues.concat(newValues);
1550 return curValues.filter(v => newValues.indexOf(val) == -1);
1555 let res = completer.call(opt, context, extra);
1557 context.completions = res;
1560 javascript: function initJavascript(dactyl, modules, window) {
1561 const { options, JavaScript } = modules;
1562 JavaScript.setCompleter(Options.prototype.get, [() => ([o.name, o.description] for (o in options))]);
1564 sanitizer: function initSanitizer(dactyl, modules, window) {
1565 const { sanitizer } = modules;
1567 sanitizer.addItem("options", {
1568 description: "Options containing hostname data",
1569 action: function sanitize_action(timespan, host) {
1571 for (let opt in values(modules.options._options))
1572 if (timespan.contains(opt.lastSet * 1000) && opt.domains)
1574 opt.value = opt.filterDomain(host, opt.value);
1577 dactyl.reportError(e);
1580 privateEnter: function privateEnter() {
1581 for (let opt in values(modules.options._options))
1582 if (opt.privateData && (!callable(opt.privateData) || opt.privateData(opt.value)))
1583 opt.oldValue = opt.value;
1585 privateLeave: function privateLeave() {
1586 for (let opt in values(modules.options._options))
1587 if (opt.oldValue != null) {
1588 opt.value = opt.oldValue;
1589 opt.oldValue = null;
1598 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1600 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: