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>
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 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"]
20 let ValueError = Class("ValueError", ErrorBase);
22 // do NOT create instances of this class yourself, use the helper method
23 // options.add() instead
25 * A class representing configuration options. Instances are created by the
26 * {@link Options} class.
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}
46 var Option = Class("Option", {
47 init: function init(names, description, type, defaultValue, extraInfo) {
50 this.realNames = names;
52 this.description = description;
54 if (this.type in Option.getKey)
55 this.getKey = Option.getKey[this.type];
57 if (this.type in Option.parse)
58 this.parse = Option.parse[this.type];
60 if (this.type in Option.stringify)
61 this.stringify = Option.stringify[this.type];
63 if (this.type in Option.domains)
64 this.domains = Option.domains[this.type];
66 if (this.type in Option.testValues)
67 this.testValues = Option.testValues[this.type];
69 this._op = Option.ops[this.type];
71 // Need to trigger setter
72 if (extraInfo && "values" in extraInfo && !extraInfo.__lookupGetter__("values")) {
73 this.values = extraInfo.values;
74 delete extraInfo.values;
78 update(this, extraInfo);
80 if (set.has(this.modules.config.defaults, this.name))
81 defaultValue = this.modules.config.defaults[this.name];
83 if (defaultValue !== undefined) {
84 if (this.type == "string")
85 defaultValue = Commands.quote(defaultValue);
87 if (isObject(defaultValue))
88 defaultValue = iter(defaultValue).map(function (val) val.map(Option.quote).join(":")).join(",");
90 if (isArray(defaultValue))
91 defaultValue = defaultValue.map(Option.quote).join(",");
93 this.defaultValue = this.parse(defaultValue);
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;
100 if (this.globalValue == undefined && !this.initialValue)
101 this.globalValue = this.defaultValue;
105 * @property {string} This option's description, as shown in :listoptions.
107 description: Messages.Localized(""),
109 get helpTag() "'" + this.name + "'",
111 initValue: function initValue() {
112 util.trapErrors(function () this.value = this.value, this);
115 get isDefault() this.stringValue === this.stringDefaultValue,
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() }); },
122 * Returns *value* as an array of parsed values if the option type is
123 * "charlist" or "stringlist" or else unchanged.
125 * @param {value} value The option value.
126 * @returns {value|string[]}
128 parse: function parse(value) Option.dequote(value),
131 * Returns *values* packed in the appropriate format for the option type.
133 * @param {value|string[]} values The option value.
136 stringify: function stringify(vals) Commands.quote(vals),
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.
142 * @param {number} scope The scope to return these values from (see
143 * {@link Option#scope}).
144 * @returns {value|string[]}
146 get: function get(scope) {
148 if ((scope & this.scope) == 0) // option doesn't exist in this scope
157 if (config.has("tabs") && (scope & Option.SCOPE_LOCAL))
158 values = tabs.options[this.name];
160 if ((scope & Option.SCOPE_GLOBAL) && (values == undefined))
161 values = this.globalValue;
164 return util.trapErrors(this.getter, this, values);
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.
173 * @param {number} scope The scope to apply these values to (see
174 * {@link Option#scope}).
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
182 newValues = this.setter(newValues);
183 if (newValues === undefined)
187 if (config.has("tabs") && (scope & Option.SCOPE_LOCAL))
188 tabs.options[this.name] = newValues;
190 if ((scope & Option.SCOPE_GLOBAL) && !skipGlobal)
191 this.globalValue = newValues;
193 this.hasChanged = true;
196 // dactyl.triggerObserver("options." + this.name, newValues);
199 getValues: deprecated("Option#get", "get"),
200 setValues: deprecated("Option#set", "set"),
201 joinValues: deprecated("Option#stringify", "stringify"),
202 parseValues: deprecated("Option#parse", "parse"),
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).
209 get value() this.get(),
210 set value(val) this.set(val),
212 get stringValue() this.stringify(this.value),
213 set stringValue(value) this.value = this.parse(value),
215 get stringDefaultValue() this.stringify(this.defaultValue),
217 getKey: function getKey(key) undefined,
220 * Returns whether the option value contains one or more of the specified
225 has: function has() Array.some(arguments, function (val) this.value.indexOf(val) >= 0, this),
228 * Returns whether this option is identified by *name*.
230 * @param {string} name
233 hasName: function hasName(name) this.names.indexOf(name) >= 0,
236 * Returns whether the specified *values* are valid for this option.
237 * @see Option#validator
239 isValidValue: function isValidValue(values) this.validator(values),
241 invalidArgument: function invalidArgument(arg, op) _("error.invalidArgument",
242 this.name + (op || "").replace(/=?$/, "=") + arg),
245 * Resets the option to its default value.
247 reset: function reset() {
248 this.value = this.defaultValue;
252 * Sets the option's value using the specified set *operator*.
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
258 * @param {boolean} invert Whether this is an invert boolean operation.
260 op: function op(operator, values, scope, invert, str) {
263 var newValues = this._op(operator, values, scope, invert);
264 if (newValues == null)
265 return "Operator " + operator + " not supported for option type " + this.type;
267 if (!this.isValidValue(newValues))
268 return this.invalidArgument(str || this.stringify(values), operator);
270 this.set(newValues, scope);
273 if (!(e instanceof ValueError))
275 return this.invalidArgument(str || this.stringify(values), operator) + ": " + e.message;
282 /** @property {string} The option's canonical name. */
284 /** @property {string[]} All names by which this option is identified. */
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"
300 * @property {number} The scope of the option. This can be local, global,
302 * @see Option#SCOPE_LOCAL
303 * @see Option#SCOPE_GLOBAL
304 * @see Option#SCOPE_BOTH
306 scope: 1, // Option.SCOPE_GLOBAL // XXX set to BOTH by default someday? - kstep
311 * @property {function(CompletionContext, Args)} This option's completer.
312 * @see CompletionContext
314 completer: function completer(context) {
316 context.completions = this.values;
320 * @property {[[string, string]]} This option's possible values.
321 * @see CompletionContext
323 values: Messages.Localized(null),
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
334 * @property {function(host, values)} A function which should strip
335 * references to a given domain from the given values.
337 filterDomain: function filterDomain(host, values)
338 Array.filter(values, function (val) !this.domains([val]).some(function (val) util.isSubdomain(val, host)), this),
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
348 * @property {function} The function called when the option value is read.
353 * @property {boolean} When true, this options values will be saved
354 * when generating a configuration file.
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
369 * @property {function} The function called when the option value is set.
373 testValues: function testValues(values, validator) validator(values),
376 * @property {function} The function called to validate the option's value
379 validator: function validator() {
380 if (this.values || this.completer !== Option.prototype.completer)
381 return Option.validateCompleter.apply(this, arguments);
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.
393 * Returns the timestamp when the option's value was last changed.
395 get lastSet() options.store.get(this.name).time,
396 set lastSet(val) { options.store.set(this.name, { value: this.globalValue, time: Date.now() }); },
399 * @property {nsIFile} The script in which this option was last set. null
400 * implies an interactive command.
406 * @property {number} Global option scope.
412 * @property {number} Local option scope. Options in this scope only
413 * apply to the current tab/buffer.
419 * @property {number} Both local and global option scope.
425 toggleAll: function toggleAll() toggleAll.supercall(this, "all") ^ !!toggleAll.superapply(this, arguments),
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;
434 flags = this && this.regexpFlags || "";
436 let [, bang, val] = /^(!?)(.*)/.exec(value);
437 let re = util.regexp(Option.dequote(val), flags);
439 re.result = result !== undefined ? result : !bang;
440 re.toString = function () Option.unparseRegexp(this, keepQuotes);
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))),
447 parseSite: function parseSite(pattern, result, rest) {
448 if (isArray(rest)) // Called by Array.map
451 let [, bang, filter] = /^(!?)(.*)/.exec(pattern);
452 filter = Option.dequote(filter);
454 return update(Styles.matchFilter(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)),
464 stringlist: function stringlist(k) this.value.indexOf(k) >= 0,
465 get charlist() this.stringlist,
467 regexplist: function regexplist(k, default_) {
468 for (let re in values(this.value))
471 return arguments.length > 1 ? default_ : null;
473 get regexpmap() this.regexplist,
474 get sitelist() this.regexplist,
475 get sitemap() this.regexplist
479 sitelist: function (vals) array.compact(vals.map(function (site) util.getHost(site.filter))),
480 get sitemap() this.sitelist
484 charlist: function (vals) Commands.quote(vals.join("")),
486 stringlist: function (vals) vals.map(Option.quote).join(","),
488 stringmap: function (vals) [Option.quote(k, /:/) + ":" + Option.quote(v) for ([k, v] in Iterator(vals))].join(","),
490 regexplist: function (vals) vals.join(","),
491 get regexpmap() this.regexplist,
492 get sitelist() this.regexplist,
493 get sitemap() this.regexplist
497 number: function (value) let (val = Option.dequote(value))
498 Option.validIf(Number(val) % 1 == 0, "Integer value required") && parseInt(val),
500 boolean: function boolean(value) Option.dequote(value) == "true" || value == true ? true : false,
502 charlist: function charlist(value) Array.slice(Option.dequote(value)),
504 stringlist: function stringlist(value) (value === "") ? [] : Option.splitList(value),
506 regexplist: function regexplist(value) (value === "") ? [] :
507 Option.splitList(value, true)
508 .map(function (re) Option.parseRegexp(re, undefined, this.regexpFlags), this),
510 sitelist: function sitelist(value) {
514 value = Option.splitList(value, true);
515 return value.map(Option.parseSite);
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))];
524 regexpmap: function regexpmap(value) Option.parse.list.call(this, value, Option.parseRegexp),
526 sitemap: function sitemap(value) Option.parse.list.call(this, value, Option.parseSite),
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);
532 let val = v.substr(count + 1);
533 if (!this.keepQuotes)
534 val = Option.dequote(val);
536 if (v.length > count)
537 return prev = parse.call(this, filter, val);
539 util.assert(prev, "Syntax error", false);
540 prev.result += "," + v;
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)
552 dequote: function dequote(value) {
554 [, arg, Option._quote] = Commands.parseArg(String(value), "");
559 splitList: function splitList(value, keepQuotes) {
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
568 if (value.length > count)
569 Option._splitAt += count + 1;
570 value = value.slice(count);
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) ? '"' : "'")
581 boolean: function boolean(operator, values, scope, invert) {
589 number: function number(operator, values, scope, invert) {
591 values = values[(values.indexOf(String(this.value)) + 1) % values.length];
593 let value = parseInt(values);
594 util.assert(Number(values) % 1 == 0,
595 "E521: Number required after =: " + this.name + "=" + values);
599 return this.value + value;
601 return this.value - value;
603 return this.value * value;
610 stringmap: function stringmap(operator, values, scope, invert) {
611 let res = update({}, this.value);
614 // The result is the same.
617 return update(res, values);
619 for (let [k, v] in Iterator(values))
625 for (let [k, v] in Iterator(values))
637 stringlist: function stringlist(operator, values, scope, invert) {
638 values = Array.concat(values);
642 return array.uniq(Array.concat(this.value, values), true);
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);
647 return this.value.filter(function (item) values.indexOf(item) == -1);
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);
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,
664 string: function string(operator, values, scope, invert) {
666 return values[(values.indexOf(this.value) + 1) % values.length];
669 return this.value + values;
671 return this.value.replace(values, "");
673 return values + this.value;
681 validIf: function validIf(test, error) {
684 throw ValueError(error);
688 * Validates the specified *values* against values generated by the
689 * option's completer function.
691 * @param {value|string[]} values The value or array of values to validate.
694 validateCompleter: function validateCompleter(values) {
696 var acceptable = this.values.array || this.values;
698 let context = CompletionContext("");
699 acceptable = context.fork("", 0, this, this.completer);
701 acceptable = context.allItems.items.map(function (item) [item.text]);
704 if (isArray(acceptable))
705 acceptable = set(acceptable.map(function ([k]) k));
707 if (this.type === "regexpmap" || this.type === "sitemap")
708 return Array.concat(values).every(function (re) set.has(acceptable, re.result));
710 return Array.concat(values).every(set.has(acceptable));
717 var Options = Module("options", {
718 Local: function Local(dactyl, modules, window) let ({ contexts } = modules) ({
719 init: function init() {
723 this._optionMap = {};
724 this.Option = Class("Option", Option, { modules: modules });
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);
738 * Lists all options in *scope* or only those with changed values if
739 * *onlyNonDefault* is specified.
741 * @param {function(Option)} filter Limit the list
742 * @param {number} scope Only list options in this scope (see
743 * {@link Option#scope}).
745 list: function (filter, scope) {
747 scope = Option.SCOPE_BOTH;
750 for (let opt in Iterator(this)) {
753 isDefault: opt.isDefault,
754 default: opt.stringDefaultValue,
755 pre: "\u00a0\u00a0", // Unicode nonbreaking space.
759 if (filter && !filter(opt))
761 if (!(opt.scope & scope))
764 if (opt.type == "boolean") {
767 option.default = (opt.defaultValue ? "" : "no") + opt.name;
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></>)}</>;
772 option.value = <>={template.highlight(opt.stringValue)}</>;
777 modules.commandline.commandOutput(template.options("Options", opts.call(this), this["verbose"] > 0));
780 cleanup: function cleanup() {
781 for (let opt in this)
782 if (opt.cleanupValue != null)
783 opt.value = opt.parse(opt.cleanupValue);
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}).
797 add: function (names, description, type, defaultValue, extraInfo) {
803 extraInfo.definedAt = contexts.getCaller(Components.stack.caller);
806 if (name in this._optionMap) {
807 this.dactyl.log(_("option.replaceExisting", name.quote()), 1);
811 let closure = function () self._optionMap[name];
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);
817 if (extraInfo.setter && (!extraInfo.scope || extraInfo.scope & Option.SCOPE_GLOBAL))
818 if (this.dactyl.initialized)
819 closure().initValue();
821 memoize(this.needInit, this.needInit.length, closure);
823 this._floptions = (this._floptions || []).concat(name);
824 memoize(this._options, this._options.length, closure);
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; });
832 /** @property {Iterator(Option)} @private */
833 __iterator__: function __iterator__()
834 values(this._options.sort(function (a, b) String.localeCompare(a.name, b.name))),
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)),
850 * Returns the option with *name* in the specified *scope*.
852 * @param {string} name The option's name.
853 * @param {number} scope The option's scope (see {@link Option#scope}).
855 * @returns {Option} The matching option.
857 get: function get(name, scope) {
859 scope = Option.SCOPE_BOTH;
861 if (this._optionMap[name] && (this._optionMap[name].scope & scope))
862 return this._optionMap[name];
867 * Parses a :set command's argument string.
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}
873 * @returns {Object} The parsed command object.
875 parseOpt: function parseOpt(args, modifiers) {
877 let matches, prefix, postfix;
879 [matches, prefix, res.name, postfix, res.valueGiven, res.operator, res.value] =
880 args.match(/^\s*(no|inv)?([^=]+?)([?&!])?\s*(([-+^]?)=(.*))?\s*$/) || [];
883 res.onlyNonDefault = false; // used for :set to print non-default options
886 res.onlyNonDefault = true;
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;
898 res.postfix = postfix;
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");
906 res.scope = modifiers && modifiers.scope;
911 if (res.value === undefined)
914 res.optionValue = res.option.get(res.scope);
917 res.values = res.option.parse(res.value);
927 * Remove the option with matching *name*.
929 * @param {string} name The name of the option to remove. This can be
930 * any of the option's names.
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];
939 /** @property {Object} The options store. */
940 get store() storage.options
943 commands: function initCommands(dactyl, modules, window) {
944 const { commands, contexts, options } = modules;
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 };
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>
958 {template.linkifyHelp(map.description)}
963 dactyl.addUsageCommand({
964 name: ["listo[ptions]", "lo"],
965 description: "List all options along with their short descriptions",
967 iterate: function (args) options,
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)}
974 help: function (opt) "'" + opt.name + "'"
978 function setAction(args, modifiers) {
979 let bang = args.bang;
984 function flushList() {
985 let names = set(list.map(function (opt) opt.option ? opt.option.name : ""));
987 if (list.some(function (opt) opt.all))
988 options.list(function (opt) !(list[0].onlyNonDefault && opt.isDefault) , list[0].scope);
990 options.list(function (opt) set.has(names, opt.name), list[0].scope);
994 for (let [, arg] in args) {
996 let onlyNonDefault = false;
998 let invertBoolean = false;
1000 if (args[0] == "") {
1002 onlyNonDefault = true;
1005 var [matches, name, postfix, valueGiven, operator, value] =
1006 arg.match(/^\s*?([^=]+?)([?&!])?\s*(([-+^]?)=(.*))?\s*$/);
1007 reset = (postfix == "&");
1008 invertBoolean = (postfix == "!");
1011 if (name == "all" && reset)
1012 modules.commandline.input("Warning: Resetting all preferences may make " + config.host + " unusable. Continue (yes/[no]): ",
1015 for (let pref in values(prefs.getNames()))
1018 { promptHighlight: "WarningMsg" });
1019 else if (name == "all")
1020 commandline.commandOutput(prefs.list(onlyNonDefault, ""));
1023 else if (invertBoolean)
1025 else if (valueGiven) {
1026 if (value == undefined)
1028 else if (value == "true")
1030 else if (value == "false")
1032 else if (Number(value) % 1 == 0)
1033 value = parseInt(value);
1035 value = Option.dequote(value);
1038 value = Option.ops[typeof value].call({ value: prefs.get(name) }, operator, value);
1039 prefs.set(name, value);
1042 modules.commandline.commandOutput(prefs.list(onlyNonDefault, name));
1046 let opt = modules.options.parseOpt(arg, modifiers);
1047 util.assert(opt, "Error parsing :set command: " + arg);
1049 let option = opt.option;
1050 util.assert(option != null || opt.all, "E518: Unknown option: " + opt.name);
1052 // reset a variable to its default value
1056 for (let option in modules.options)
1069 if (opt.option.type === "boolean") {
1070 util.assert(!opt.valueGiven, _("error.invalidArgument", arg));
1071 opt.values = !opt.unsetBoolean;
1073 else if (/^(string|number)$/.test(opt.option.type) && opt.invert)
1074 opt.values = Option.splitList(opt.value);
1076 var res = opt.option.op(opt.operator || "=", opt.values, opt.scope, opt.invert,
1083 dactyl.echoerr(res);
1084 option.setFrom = contexts.getCaller(null);
1090 function setCompleter(context, args, modifiers) {
1091 const { completion } = modules;
1093 let filter = context.filter;
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);
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);
1108 return completion.preference(context);
1111 let opt = modules.options.parseOpt(filter, modifiers);
1112 let prefix = opt.prefix;
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);
1121 function error(length, message) {
1122 context.message = message;
1123 context.highlight(0, length, "SPELLCHECK");
1126 let option = opt.option;
1128 return error(opt.name.length, "No such option: " + opt.name);
1130 context.advance(context.filter.indexOf("="));
1131 if (option.type == "boolean")
1132 return error(context.filter.length, "Trailing characters");
1136 return error(context.filter.length, opt.error);
1138 if (opt.get || opt.reset || !option || prefix)
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, ""];
1153 let optcontext = context.fork("values");
1154 modules.completion.optionValue(optcontext, opt.name, opt.operator);
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;
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, ""]);
1168 context.title = ["Current values"];
1172 // TODO: deprecated. This needs to support "g:"-prefixed globals at a
1173 // minimum for now. The coderepos plugins make extensive use of global
1175 commands.add(["let"],
1176 "Set or list a variable",
1178 let globalVariables = dactyl._globalVariables;
1179 args = (args[0] || "").trim();
1180 function fmt(value) (typeof value == "number" ? "#" :
1181 typeof value == "function" ? "*" :
1183 if (!args || args == "g:") {
1187 template.map(globalVariables, function ([i, value]) {
1189 <td style="width: 200px;">{i}</td>
1190 <td>{fmt(value)}</td>
1195 if (str.text().length() == str.*.length())
1196 dactyl.echomsg(_("variable.none"));
1198 dactyl.echo(str, commandline.FORCE_MULTILINE);
1202 let matches = args.match(/^([a-z]:)?([\w]+)(?:\s*([-+.])?=\s*(.*)?)?$/);
1204 let [, scope, name, op, expr] = matches;
1205 let fullName = (scope || "") + name;
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));
1213 dactyl.echo(fullName + "\t\t" + fmt(globalVariables[name]));
1216 var newValue = dactyl.userEval(expr);
1219 util.assert(newValue !== undefined,
1220 _("command.let.invalidExpression", expr));
1222 let value = newValue;
1224 value = globalVariables[name];
1230 value += String(newValue);
1232 globalVariables[name] = value;
1236 dactyl.echoerr(_("command.let.unexpectedChar"));
1239 deprecated: "the options system",
1246 names: ["setl[ocal]"],
1247 description: "Set local option",
1248 modifiers: { scope: Option.SCOPE_LOCAL }
1251 names: ["setg[lobal]"],
1252 description: "Set global option",
1253 modifiers: { scope: Option.SCOPE_GLOBAL }
1257 description: "Set an option",
1260 serialize: function () [
1263 literalArg: [opt.type == "boolean" ? (opt.value ? "" : "no") + opt.name
1264 : opt.name + "=" + opt.stringValue]
1266 for (opt in modules.options)
1267 if (!opt.getter && !opt.isDefault && (opt.scope & Option.SCOPE_GLOBAL))
1271 ].forEach(function (params) {
1272 commands.add(params.names, params.description,
1273 function (args, modifiers) {
1274 setAction(args, update(modifiers, params.modifiers));
1278 completer: setCompleter,
1279 domains: function domains(args) array.flatten(args.map(function (spec) {
1281 let opt = modules.options.parseOpt(spec);
1282 if (opt.option && opt.option.domains)
1283 return opt.option.domains(opt.values);
1286 util.reportError(e);
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));
1297 }, params.extra || {}));
1300 // TODO: deprecated. This needs to support "g:"-prefixed globals at a
1302 commands.add(["unl[et]"],
1303 "Delete a variable",
1305 for (let [, name] in args) {
1306 name = name.replace(/^g:/, ""); // throw away the scope prefix
1307 if (!set.has(dactyl._globalVariables, name)) {
1309 dactyl.echoerr(_("command.let.noSuch", name));
1313 delete dactyl._globalVariables[name];
1319 deprecated: "the options system"
1322 completion: function initCompletion(dactyl, modules, window) {
1323 const { completion } = modules;
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)
1335 context.filters.push(function ({ item }) item.scope & scope);
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)
1345 var curValues = curValue != null ? opt.parse(curValue) : opt.value;
1346 var newValues = opt.parse(context.filter);
1349 context.message = "Error: " + e;
1350 context.completions = [];
1360 newValues = Option.splitList(context.filter);
1365 Option._splitAt = newValues.length;
1370 let vals = Option.splitList(context.filter);
1371 let target = vals.pop() || "";
1373 let [count, key, quote] = Commands.parseArg(target, /:/, true);
1374 let split = Option._splitAt;
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(","));
1380 Option._splitAt = split + (extra.value == null ? 0 : count + 1);
1383 // TODO: Highlight when invalid
1384 context.advance(Option._splitAt);
1385 context.filter = Option.dequote(context.filter);
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);
1394 context.filters.push(function (i) curValues.indexOf(i.text) == -1);
1396 context.filters.push(function (i) curValues.indexOf(i.text) > -1);
1399 let res = completer.call(opt, context, extra);
1401 context.completions = res;
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))]);
1408 sanitizer: function initSanitizer(dactyl, modules, window) {
1409 const { sanitizer } = modules;
1411 sanitizer.addItem("options", {
1412 description: "Options containing hostname data",
1413 action: function sanitize_action(timespan, host) {
1415 for (let opt in values(modules.options._options))
1416 if (timespan.contains(opt.lastSet * 1000) && opt.domains)
1418 opt.value = opt.filterDomain(host, opt.value);
1421 dactyl.reportError(e);
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;
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;
1442 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1444 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: