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 at Gmail>
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("commands", {
13 exports: ["ArgType", "Command", "Commands", "CommandOption", "Ex", "commands"],
14 require: ["contexts", "messages", "util"],
15 use: ["config", "options", "services", "template"]
19 * A structure representing the options available for a command.
21 * Do NOT create instances of this class yourself, use the helper method
22 * {@see Commands#add} instead
24 * @property {[string]} names An array of option names. The first name
25 * is the canonical option name.
26 * @property {number} type The option's value type. This is one of:
27 * (@link CommandOption.NOARG),
28 * (@link CommandOption.STRING),
29 * (@link CommandOption.BOOL),
30 * (@link CommandOption.INT),
31 * (@link CommandOption.FLOAT),
32 * (@link CommandOption.LIST),
33 * (@link CommandOption.ANY)
34 * @property {function} validator A validator function
35 * @property {function (CompletionContext, object)} completer A list of
36 * completions, or a completion function which will be passed a
37 * {@link CompletionContext} and an object like that returned by
38 * {@link commands.parseArgs} with the following additional keys:
39 * completeOpt - The name of the option currently being completed.
40 * @property {boolean} multiple Whether this option can be specified multiple times
41 * @property {string} description A description of the option
42 * @property {object} default The option's default value
45 var CommandOption = Struct("names", "type", "validator", "completer", "multiple", "description", "default");
46 CommandOption.defaultValue("description", function () "");
47 CommandOption.defaultValue("type", function () CommandOption.NOARG);
48 CommandOption.defaultValue("multiple", function () false);
50 var ArgType = Struct("description", "parse");
51 update(CommandOption, {
53 * @property {object} The option argument is unspecified. Any argument
54 * is accepted and caller is responsible for parsing the return
61 * @property {object} The option doesn't accept an argument.
64 NOARG: ArgType("no arg", function (arg) !arg || null),
66 * @property {object} The option accepts a boolean argument.
69 BOOL: ArgType("boolean", function parseBoolArg(val) Commands.parseBool(val)),
71 * @property {object} The option accepts a string argument.
74 STRING: ArgType("string", function (val) val),
76 * @property {object} The option accepts an integer argument.
79 INT: ArgType("int", function parseIntArg(val) parseInt(val)),
81 * @property {object} The option accepts a float argument.
84 FLOAT: ArgType("float", function parseFloatArg(val) parseFloat(val)),
86 * @property {object} The option accepts a string list argument.
90 LIST: ArgType("list", function parseListArg(arg, quoted) Option.splitList(quoted))
94 * A class representing Ex commands. Instances are created by
95 * the {@link Commands} class.
97 * @param {string[]} specs The names by which this command can be invoked.
98 * These are specified in the form "com[mand]" where "com" is a unique
99 * command name prefix.
100 * @param {string} description A short one line description of the command.
101 * @param {function} action The action invoked by this command when executed.
102 * @param {Object} extraInfo An optional extra configuration hash. The
103 * following properties are supported.
104 * argCount - see {@link Command#argCount}
105 * bang - see {@link Command#bang}
106 * completer - see {@link Command#completer}
107 * count - see {@link Command#count}
108 * domains - see {@link Command#domains}
109 * heredoc - see {@link Command#heredoc}
110 * literal - see {@link Command#literal}
111 * options - see {@link Command#options}
112 * privateData - see {@link Command#privateData}
113 * serialize - see {@link Command#serialize}
114 * subCommand - see {@link Command#subCommand}
118 var Command = Class("Command", {
119 init: function init(specs, description, action, extraInfo) {
120 specs = Array.concat(specs); // XXX
121 let parsedSpecs = extraInfo.parsedSpecs || Command.parseSpecs(specs);
124 this.shortNames = array.compact(parsedSpecs.map(function (n) n[1]));
125 this.longNames = parsedSpecs.map(function (n) n[0]);
126 this.name = this.longNames[0];
127 this.names = array.flatten(parsedSpecs);
128 this.description = description;
129 this.action = action;
132 update(this, extraInfo);
134 this.options = this.options.map(CommandOption.fromArray, CommandOption);
135 for each (let option in this.options)
136 option.localeName = ["command", this.name, option.names[0]];
139 get toStringParams() [this.name, this.hive.name],
141 get identifier() this.hive.prefix + this.name,
143 get helpTag() ":" + this.name,
145 get lastCommand() this._lastCommand || commandline.command,
146 set lastCommand(val) { this._lastCommand = val; },
149 * Execute this command.
151 * @param {Args} args The Args object passed to {@link #action}.
152 * @param {Object} modifiers Any modifiers to be passed to {@link #action}.
154 execute: function execute(args, modifiers) {
155 const { dactyl } = this.modules;
157 let context = args.context;
158 if (this.deprecated && !set.add(this.complained, context ? context.file : "[Command Line]")) {
159 let loc = contexts.context ? context.file + ":" + context.line + ": " : "";
160 dactyl.echoerr(loc + ":" + this.name + " is deprecated" +
161 (isString(this.deprecated) ? ": " + this.deprecated : ""));
164 modifiers = modifiers || {};
166 if (args.count != null && !this.count)
167 throw FailedAssertion(_("command.noRange"));
168 if (args.bang && !this.bang)
169 throw FailedAssertion(_("command.noBang"));
171 return !dactyl.trapErrors(function exec() {
172 let extra = this.hive.argsExtra(args);
173 for (let k in properties(extra))
175 Object.defineProperty(args, k, Object.getOwnPropertyDescriptor(extra, k));
178 this.always(args, modifiers);
180 if (!context || !context.noExecute)
181 this.action(args, modifiers);
186 * Returns whether this command may be invoked via *name*.
188 * @param {string} name The candidate name.
191 hasName: function hasName(name) this.parsedSpecs.some(
192 function ([long, short]) name.indexOf(short) == 0 && long.indexOf(name) == 0),
195 * A helper function to parse an argument string.
197 * @param {string} args The argument string to parse.
198 * @param {CompletionContext} complete A completion context.
199 * Non-null when the arguments are being parsed for completion
201 * @param {Object} extra Extra keys to be spliced into the
202 * returned Args object.
204 * @see Commands#parseArgs
206 parseArgs: function parseArgs(args, complete, extra) this.modules.commands.parseArgs(args, {
212 complained: Class.memoize(function () ({})),
215 * @property {string[]} All of this command's name specs. e.g., "com[mand]"
218 /** @property {string[]} All of this command's short names, e.g., "com" */
221 * @property {string[]} All of this command's long names, e.g., "command"
225 /** @property {string} The command's canonical name. */
227 /** @property {string[]} All of this command's long and short names. */
230 /** @property {string} This command's description, as shown in :listcommands */
231 description: Messages.Localized(""),
233 * @property {function (Args)} The function called to execute this command.
237 * @property {string} This command's argument count spec.
238 * @see Commands#parseArguments
242 * @property {function (CompletionContext, Args)} This command's completer.
243 * @see CompletionContext
246 /** @property {boolean} Whether this command accepts a here document. */
249 * @property {boolean} Whether this command may be called with a bang,
254 * @property {boolean} Whether this command may be called with a count,
259 * @property {function(args)} A function which should return a list
260 * of domains referenced in the given args. Used in determining
261 * whether to purge the command from history when clearing
264 domains: function (args) [],
266 * @property {boolean} At what index this command's literal arguments
267 * begin. For instance, with a value of 2, all arguments starting with
268 * the third are parsed as a single string, with all quoting characters
269 * passed literally. This is especially useful for commands which take
270 * key mappings or Ex command lines as arguments.
274 * @property {Array} The options this command takes.
275 * @see Commands@parseArguments
279 optionMap: Class.memoize(function () array(this.options)
280 .map(function (opt) opt.names.map(function (name) [name, opt]))
281 .flatten().toObject()),
283 newArgs: function newArgs(base) {
286 res.__proto__ = this.argsPrototype;
290 argsPrototype: Class.memoize(function argsPrototype() {
291 let res = update([], {
292 __iterator__: function AP__iterator__() array.iterItems(this),
296 explicitOpts: Class.memoize(function () ({})),
298 has: function AP_has(opt) set.has(this.explicitOpts, opt) || typeof opt === "number" && set.has(this, opt),
300 get literalArg() this.command.literal != null && this[this.command.literal] || "",
302 // TODO: string: Class.memoize(function () { ... }),
304 verify: function verify() {
305 if (this.command.argCount) {
306 util.assert((this.length > 0 || !/^[1+]$/.test(this.command.argCount)) &&
307 (this.literal == null || !/[1+]/.test(this.command.argCount) || /\S/.test(this.literalArg || "")),
308 _("error.argumentRequired"));
310 util.assert((this.length == 0 || this.command.argCount !== "0") &&
311 (this.length <= 1 || !/^[01?]$/.test(this.command.argCount)),
312 _("error.trailing"));
317 this.options.forEach(function (opt) {
318 if (opt.default !== undefined)
319 Object.defineProperty(res, opt.names[0],
320 Object.getOwnPropertyDescriptor(opt, "default") ||
321 { configurable: true, enumerable: true, get: function () opt.default });
328 * @property {boolean|function(args)} When true, invocations of this
329 * command may contain private data which should be purged from
330 * saved histories when clearing private data. If a function, it
331 * should return true if an invocation with the given args
332 * contains private data
336 * @property {function} Should return an array of *Object*s suitable to be
337 * passed to {@link Commands#commandToString}, one for each past
338 * invocation which should be restored on subsequent @dactyl startups.
343 * @property {number} If this command takes another ex command as an
344 * argument, the index of that argument. Used in determining whether to
345 * purge the command from history when clearing private data.
349 * @property {boolean} Specifies whether this is a user command. User
350 * commands may be created by plugins, or directly by users, and,
351 * unlike basic commands, may be overwritten. Users and plugin authors
352 * should create only user commands.
356 * @property {string} For commands defined via :command, contains the Ex
357 * command line to be executed upon invocation.
359 replacementText: null
361 // TODO: do we really need more than longNames as a convenience anyway?
363 * Converts command name abbreviation specs of the form
364 * 'shortname[optional-tail]' to short and long versions:
365 * ["abc[def]", "ghijkl"] -> [["abcdef", "abc"], ["ghijlk"]]
367 * @param {Array} specs An array of command name specs to parse.
370 parseSpecs: function parseSpecs(specs) {
371 return specs.map(function (spec) {
372 let [, head, tail] = /([^[]+)(?:\[(.*)])?/.exec(spec);
373 return tail ? [head + tail, head] : [head];
379 var Ex = Module("Ex", {
380 Local: function Local(dactyl, modules, window) ({
381 get commands() modules.commands,
382 get context() modules.contexts.context
385 _args: function E_args(cmd, args) {
386 args = Array.slice(args);
388 let res = cmd.newArgs({ context: this.context });
389 if (isObject(args[0]))
390 for (let [k, v] in Iterator(args.shift()))
396 let opt = cmd.optionMap["-" + k];
397 let val = opt.type && opt.type.parse(v);
398 util.assert(val != null && (typeof val !== "number" || !isNaN(val)),
399 _("option.noSuch", k));
400 Class.replaceProperty(args, opt.names[0], val);
401 args.explicitOpts[opt.names[0]] = val;
403 for (let [i, val] in array.iterItems(args))
404 res[i] = String(val);
408 _complete: function E_complete(cmd) let (self = this)
409 function _complete(context, func, obj, args) {
410 args = self._args(cmd, args);
411 args.completeArg = args.length - 1;
412 if (cmd.completer && args.length)
413 return cmd.completer(context, args);
416 _run: function E_run(name) {
418 let cmd = this.commands.get(name);
419 util.assert(cmd, _("command.noSuch"));
421 return update(function exCommand(options) {
422 let args = self._args(cmd, arguments);
424 return cmd.execute(args);
426 dactylCompleter: self._complete(cmd)
430 __noSuchMethod__: function __noSuchMethod__(meth, args) this._run(meth).apply(this, args)
433 var CommandHive = Class("CommandHive", Contexts.Hive, {
434 init: function init(group) {
435 init.supercall(this, group);
440 /** @property {Iterator(Command)} @private */
441 __iterator__: function __iterator__() array.iterValues(this._list.sort(function (a, b) a.name > b.name)),
443 /** @property {string} The last executed Ex command line. */
447 * Adds a new command to the builtin hive. Accessible only to core
448 * dactyl code. Plugins should use group.commands.add instead.
450 * @param {string[]} names The names by which this command can be
451 * invoked. The first name specified is the command's canonical
453 * @param {string} description A description of the command.
454 * @param {function} action The action invoked by this command.
455 * @param {Object} extra An optional extra configuration hash.
458 add: function add(names, description, action, extra, replace) {
459 const { commands, contexts } = this.modules;
462 if (!extra.definedAt)
463 extra.definedAt = contexts.getCaller(Components.stack.caller);
466 extra.parsedSpecs = Command.parseSpecs(names);
468 let names = array.flatten(extra.parsedSpecs);
471 util.assert(!names.some(function (name) name in commands.builtin._map),
472 _("command.cantReplace", name));
474 util.assert(replace || names.every(function (name) !(name in this._map), this),
475 _("command.wontReplace", name));
477 for (let name in values(names)) {
478 ex.__defineGetter__(name, function () this._run(name));
479 if (name in this._map)
484 let closure = function () self._map[name];
486 memoize(this._map, name, function () commands.Command(names, description, action, extra));
487 memoize(this._list, this._list.length, closure);
488 for (let alias in values(names.slice(1)))
489 memoize(this._map, alias, closure);
494 _add: function _add(names, description, action, extra, replace) {
495 const { contexts } = this.modules;
498 extra.definedAt = contexts.getCaller(Components.stack.caller.caller);
499 return this.add.apply(this, arguments);
503 * Clear all commands.
506 clear: function clear() {
507 util.assert(this.group.modifiable, _("command.cantDelete"));
513 * Returns the command with matching *name*.
515 * @param {string} name The name of the command to return. This can be
516 * any of the command's names.
517 * @param {boolean} full If true, only return a command if one of
518 * its names matches *name* exactly.
521 get: function get(name, full) this._map[name]
522 || !full && array.nth(this._list, function (cmd) cmd.hasName(name), 0)
526 * Remove the user-defined command with matching *name*.
528 * @param {string} name The name of the command to remove. This can be
529 * any of the command's names.
531 remove: function remove(name) {
532 util.assert(this.group.modifiable, _("command.cantDelete"));
534 let cmd = this.get(name);
535 this._list = this._list.filter(function (c) c !== cmd);
536 for (let name in values(cmd.names))
537 delete this._map[name];
544 var Commands = Module("commands", {
547 Local: function Local(dactyl, modules, window) let ({ Group, contexts } = modules) ({
548 init: function init() {
549 this.Command = Class("Command", Command, { modules: modules });
551 hives: contexts.Hives("commands", Class("CommandHive", CommandHive, { modules: modules })),
552 user: contexts.hives.commands.user,
553 builtin: contexts.hives.commands.builtin
557 get context() contexts.context,
559 get readHeredoc() modules.io.readHeredoc,
561 get allHives() contexts.allGroups.commands,
563 get userHives() this.allHives.filter(function (h) h !== this.builtin, this),
566 * Executes an Ex command script.
568 * @param {string} string A string containing the commands to execute.
569 * @param {object} tokens An optional object containing tokens to be
570 * interpolated into the command string.
571 * @param {object} args Optional arguments object to be passed to
573 * @param {object} context An object containing information about
574 * the file that is being or has been sourced to obtain the
577 execute: function execute(string, tokens, silent, args, context) {
578 contexts.withContext(context || this.context || { file: "[Command Line]", line: 1 },
580 modules.io.withSavedValues(["readHeredoc"], function () {
581 this.readHeredoc = function readHeredoc(end) {
583 contexts.context.line++;
584 while (++i < lines.length) {
585 if (lines[i] === end)
586 return res.join("\n");
589 util.assert(false, _("command.eof", end));
592 args = update({}, args || {});
594 if (tokens && !callable(string))
595 string = util.compileMacro(string, true);
596 if (callable(string))
597 string = string(tokens || {});
599 let lines = string.split(/\r\n|[\r\n]/);
600 let startLine = context.line;
602 for (var i = 0; i < lines.length && !context.finished; i++) {
603 // Deal with editors from Silly OSs.
604 let line = lines[i].replace(/\r$/, "");
606 context.line = startLine + i;
608 // Process escaped new lines
609 while (i < lines.length && /^\s*\\/.test(lines[i + 1]))
610 line += "\n" + lines[++i].replace(/^\s*\\/, "");
613 dactyl.execute(line, args);
617 e.message = context.file + ":" + context.line + ": " + e.message;
618 dactyl.reportError(e, true);
627 * Displays a list of user-defined commands.
629 list: function list() {
630 const { commandline, completion } = this.modules;
631 function completerToString(completer) {
633 return [k for ([k, v] in Iterator(config.completers)) if (completer == completion.closure[v])][0] || "custom";
637 if (!this.userHives.some(function (h) h._list.length))
638 dactyl.echomsg(_("command.none"));
640 commandline.commandOutput(
642 <tr highlight="Title">
644 <td style="padding-right: 1em;"></td>
645 <td style="padding-right: 1ex;">Name</td>
646 <td style="padding-right: 1ex;">Args</td>
647 <td style="padding-right: 1ex;">Range</td>
648 <td style="padding-right: 1ex;">Complete</td>
649 <td style="padding-right: 1ex;">Definition</td>
651 <col style="min-width: 6em; padding-right: 1em;"/>
653 template.map(this.userHives, function (hive) let (i = 0)
654 <tr style="height: .5ex;"/> +
655 template.map(hive, function (cmd)
656 template.map(cmd.names, function (name)
658 <td highlight="Title">{!i++ ? hive.name : ""}</td>
659 <td>{cmd.bang ? "!" : " "}</td>
661 <td>{cmd.argCount}</td>
662 <td>{cmd.count ? "0c" : ""}</td>
663 <td>{completerToString(cmd.completer)}</td>
664 <td>{cmd.replacementText || "function () { ... }"}</td>
666 <tr style="height: .5ex;"/>)
673 * @property Indicates that no count was specified for this
674 * command invocation.
679 * @property {number} Indicates that the full buffer range (1,$) was
680 * specified for this command invocation.
683 // FIXME: this isn't a count at all
684 COUNT_ALL: -2, // :%...
686 /** @property {Iterator(Command)} @private */
687 iterator: function iterator() iter.apply(null, this.hives.array)
688 .sort(function (a, b) a.serialGroup - b.serialGroup || a.name > b.name)
691 /** @property {string} The last executed Ex command line. */
694 add: function add() {
695 let group = this.builtin;
696 if (!util.isDactyl(Components.stack.caller)) {
697 deprecated.warn(add, "commands.add", "group.commands.add");
701 return group._add.apply(group, arguments);
703 addUserCommand: deprecated("group.commands.add", { get: function addUserCommand() this.user.closure._add }),
704 getUserCommands: deprecated("iter(group.commands)", function getUserCommands() iter(this.user).toArray()),
705 removeUserCommand: deprecated("group.commands.remove", { get: function removeUserCommand() this.user.closure.remove }),
708 * Returns the specified command invocation object serialized to
709 * an executable Ex command string.
711 * @param {Object} args The command invocation object.
714 commandToString: function commandToString(args) {
715 let res = [args.command + (args.bang ? "!" : "")];
718 if (args.ignoreDefaults)
719 defaults = array(this.options).map(function (opt) [opt.names[0], opt.default])
722 for (let [opt, val] in Iterator(args.options || {})) {
723 if (val != null && defaults[opt] === val)
725 let chr = /^-.$/.test(opt) ? " " : "=";
727 opt += chr + Option.stringify.stringlist(val);
728 else if (val != null)
729 opt += chr + Commands.quote(val);
732 for (let [, arg] in Iterator(args.arguments || []))
733 res.push(Commands.quote(arg));
735 let str = args.literalArg;
737 res.push(!/\n/.test(str) ? str :
738 this.hereDoc && false ? "<<EOF\n" + String.replace(str, /\n$/, "") + "\nEOF"
739 : String.replace(str, /\n/g, "\n" + res[0].replace(/./g, " ").replace(/.$/, "\\")));
740 return res.join(" ");
744 * Returns the command with matching *name*.
746 * @param {string} name The name of the command to return. This can be
747 * any of the command's names.
750 get: function get(name, full) iter(this.hives).map(function ([i, hive]) hive.get(name, full))
751 .nth(util.identity, 0),
754 * Returns true if a command invocation contains a URL referring to the
757 * @param {string} command
758 * @param {string} host
761 hasDomain: function hasDomain(command, host) {
763 for (let [cmd, args] in this.subCommands(command))
764 if (Array.concat(cmd.domains(args)).some(function (domain) util.isSubdomain(domain, host)))
774 * Returns true if a command invocation contains private data which should
775 * be cleared when purging private data.
777 * @param {string} command
780 hasPrivateData: function hasPrivateData(command) {
781 for (let [cmd, args] in this.subCommands(command))
783 return !callable(cmd.privateData) || cmd.privateData(args);
787 // TODO: should it handle comments?
788 // : it might be nice to be able to specify that certain quoting
789 // should be disabled E.g. backslash without having to resort to
790 // using literal etc.
791 // : error messages should be configurable or else we can ditch
792 // Vim compatibility but it actually gives useful messages
793 // sometimes rather than just "Invalid arg"
794 // : I'm not sure documenting the returned object here, and
795 // elsewhere, as type Args rather than simply Object makes sense,
796 // especially since it is further augmented for use in
797 // Command#action etc.
799 * Parses *str* for options and plain arguments.
801 * The returned *Args* object is an augmented array of arguments.
802 * Any key/value pairs of *extra* will be available and the
803 * following additional properties:
804 * -opt - the value of the option -opt if specified
805 * string - the original argument string *str*
806 * literalArg - any trailing literal argument
809 * '-quoted strings - only ' and \ itself are escaped
810 * "-quoted strings - also ", \n and \t are translated
811 * non-quoted strings - everything is taken literally apart from "\
814 * @param {string} str The Ex command-line string to parse. E.g.
815 * "-x=foo -opt=bar arg1 arg2"
816 * @param {[CommandOption]} options The options accepted. These are specified
817 * as an array of {@link CommandOption} structures.
818 * @param {string} argCount The number of arguments accepted.
820 * "1": exactly one argument
821 * "+": one or more arguments
822 * "*": zero or more arguments (default if unspecified)
823 * "?": zero or one arguments
824 * @param {boolean} allowUnknownOptions Whether unspecified options
825 * should cause an error.
826 * @param {number} literal The index at which any literal arg begins.
827 * See {@link Command#literal}.
828 * @param {CompletionContext} complete The relevant completion context
829 * when the args are being parsed for completion.
830 * @param {Object} extra Extra keys to be spliced into the returned
834 parseArgs: function parseArgs(str, params) {
837 function getNextArg(str, _keepQuotes) {
838 if (arguments.length < 2)
839 _keepQuotes = keepQuotes;
841 if (str.substr(0, 2) === "<<" && hereDoc) {
842 let arg = /^<<(\S*)/.exec(str)[1];
843 let count = arg.length + 2;
845 return [count, "", ""];
846 return [count, self.readHeredoc(arg), ""];
849 let [count, arg, quote] = Commands.parseArg(str, null, _keepQuotes);
850 if (quote == "\\" && !complete)
851 return [, , , "Trailing \\"];
852 if (quote && !complete)
853 return [, , , "E114: Missing quote: " + quote];
854 return [count, arg, quote];
859 var { allowUnknownOptions, argCount, complete, extra, hereDoc, literal, options, keepQuotes } = params || {};
867 var args = params.newArgs ? params.newArgs() : [];
868 args.string = str; // for access to the unparsed string
871 for (let [k, v] in Iterator(extra || []))
874 // FIXME: best way to specify these requirements?
875 var onlyArgumentsRemaining = allowUnknownOptions || options.length == 0; // after a -- has been found
881 let matchOpts = function matchOpts(arg) {
882 // Push possible option matches into completions
883 if (complete && !onlyArgumentsRemaining)
884 completeOpts = options.filter(function (opt) opt.multiple || !set.has(args, opt.names[0]));
886 let resetCompletions = function resetCompletions() {
888 args.completeArg = null;
889 args.completeOpt = null;
890 args.completeFilter = null;
891 args.completeStart = i;
892 args.quote = Commands.complQuote[""];
897 args.completeArg = 0;
900 let fail = function fail(error) {
902 complete.message = error;
904 util.assert(false, error);
908 while (i < str.length || complete) {
912 i += re.exec(str)[0].length;
915 args.string = str.slice(0, i);
916 args.trailing = str.slice(i + 1);
919 if (i == str.length && !complete)
925 var sub = str.substr(i);
926 if ((!onlyArgumentsRemaining) && /^--(\s|$)/.test(sub)) {
927 onlyArgumentsRemaining = true;
933 if (!onlyArgumentsRemaining) {
934 for (let [, opt] in Iterator(options)) {
935 for (let [, optname] in Iterator(opt.names)) {
936 if (sub.indexOf(optname) == 0) {
939 let arg, quote, quoted;
941 let sep = sub[optname.length];
942 let argString = sub.substr(optname.length + 1);
943 if (sep == "=" || /\s/.test(sep) && opt.type != CommandOption.NOARG) {
944 [count, quoted, quote, error] = getNextArg(argString, true);
945 arg = Option.dequote(quoted);
946 util.assert(!error, error);
948 // if we add the argument to an option after a space, it MUST not be empty
949 if (sep != "=" && !quote && arg.length == 0)
952 count++; // to compensate the "=" character
954 else if (!/\s/.test(sep) && sep != undefined) // this isn't really an option as it has trailing characters, parse it as an argument
958 if (!complete && quote)
959 fail(_("command.invalidOptArg", optname, argString));
962 if (complete && !/[\s=]/.test(sep))
965 if (complete && count > 0) {
966 args.completeStart += optname.length + 1;
967 args.completeOpt = opt;
968 args.completeFilter = arg;
969 args.quote = Commands.complQuote[quote] || Commands.complQuote[""];
971 if (!complete || arg != null) {
974 arg = opt.type.parse(arg, quoted);
976 if (complete && isArray(arg)) {
977 args.completeFilter = arg[arg.length - 1] || "";
978 args.completeStart += orig.length - args.completeFilter.length;
981 if (arg == null || (typeof arg == "number" && isNaN(arg))) {
982 if (!complete || orig != "" || args.completeStart != str.length)
983 fail(_("command.invalidOptTypeArg", opt.type.description, optname, argString));
985 complete.highlight(args.completeStart, count - 1, "SPELLCHECK");
989 // we have a validator function
990 if (typeof opt.validator == "function") {
991 if (opt.validator(arg, quoted) == false && (arg || !complete)) {
992 fail(_("command.invalidOptArg", optname, argString));
993 if (complete) // Always true.
994 complete.highlight(args.completeStart, count - 1, "SPELLCHECK");
999 if (arg != null || opt.type == CommandOption.NOARG) {
1000 // option allowed multiple times
1002 args[opt.names[0]] = (args[opt.names[0]] || []).concat(arg);
1004 Class.replaceProperty(args, opt.names[0], opt.type == CommandOption.NOARG || arg);
1006 args.explicitOpts[opt.names[0]] = args[opt.names[0]];
1009 i += optname.length + count;
1010 if (i == str.length)
1014 // if it is invalid, just fall through and try the next argument
1023 if (argCount == "0" || args.length > 0 && (/[1?]/.test(argCount)))
1024 complete.highlight(i, sub.length, "SPELLCHECK");
1026 if (args.length === literal) {
1028 args.completeArg = args.length;
1030 let re = /(?:\s*(?=\n)|\s*)([^]*)/gy;
1031 re.lastIndex = argStart || 0;
1032 sub = re.exec(str)[1];
1035 if (sub.substr(0, 2) === "<<" && hereDoc)
1036 let ([count, arg] = getNextArg(sub)) {
1037 sub = arg + sub.substr(count);
1045 // if not an option, treat this token as an argument
1046 let [count, arg, quote, error] = getNextArg(sub);
1047 util.assert(!error, error);
1050 args.quote = Commands.complQuote[quote] || Commands.complQuote[""];
1051 args.completeFilter = arg || "";
1053 else if (count == -1)
1054 fail(_("command.parsing", arg));
1055 else if (!onlyArgumentsRemaining && sub[0] === "-")
1056 fail(_("command.invalidOpt", arg));
1061 args.completeArg = args.length - 1;
1064 if (count <= 0 || i == str.length)
1068 if (complete && args.trailing == null) {
1069 if (args.completeOpt) {
1070 let opt = args.completeOpt;
1071 let context = complete.fork(opt.names[0], args.completeStart);
1072 let arg = args.explicitOpts[opt.names[0]];
1073 context.filter = args.completeFilter;
1076 context.filters.push(function (item) arg.indexOf(item.text) === -1);
1078 if (typeof opt.completer == "function")
1079 var compl = opt.completer(context, args);
1081 compl = opt.completer || [];
1083 context.title = [opt.names[0]];
1084 context.quote = args.quote;
1086 context.completions = compl;
1088 complete.advance(args.completeStart);
1091 description: function (opt) messages.get(["command", params.name, "options", opt.names[0], "description"].join("."), opt.description)
1093 complete.title = ["Options"];
1095 complete.completions = completeOpts;
1103 catch (e if complete && e instanceof FailedAssertion) {
1104 complete.message = e;
1109 nameRegexp: util.regexp(<![CDATA[
1116 forbid: util.regexp(String.replace(<![CDATA[
1117 U0000-U002c // U002d -
1119 U003a-U0040 // U0041-U005a a-z
1120 U005b-U0060 // U0061-U007a A-Z
1122 U02b0-U02ff // Spacing Modifier Letters
1123 U0300-U036f // Combining Diacritical Marks
1124 U1dc0-U1dff // Combining Diacritical Marks Supplement
1125 U2000-U206f // General Punctuation
1126 U20a0-U20cf // Currency Symbols
1127 U20d0-U20ff // Combining Diacritical Marks for Symbols
1128 U2400-U243f // Control Pictures
1129 U2440-U245f // Optical Character Recognition
1130 U2500-U257f // Box Drawing
1131 U2580-U259f // Block Elements
1132 U2700-U27bf // Dingbats
1133 Ufe20-Ufe2f // Combining Half Marks
1134 Ufe30-Ufe4f // CJK Compatibility Forms
1135 Ufe50-Ufe6f // Small Form Variants
1136 Ufe70-Ufeff // Arabic Presentation Forms-B
1137 Uff00-Uffef // Halfwidth and Fullwidth Forms
1138 Ufff0-Uffff // Specials
1139 ]]>, /U/g, "\\u"), "x")
1142 validName: Class.memoize(function validName() util.regexp("^" + this.nameRegexp.source + "$")),
1144 commandRegexp: Class.memoize(function commandRegexp() util.regexp(<![CDATA[
1147 (?P<prespace> [:\s]*)
1148 (?P<count> (?:\d+ | %)? )
1150 (?: (?P<group> <name>) : )?
1151 (?P<cmd> (?:<name> | !)? ))
1160 name: this.nameRegexp
1164 * Parses a complete Ex command.
1166 * The parsed string is returned as an Array like
1167 * [count, command, bang, args]:
1168 * count - any count specified
1169 * command - the Ex command name
1170 * bang - whether the special "bang" version was called
1171 * args - the commands full argument string
1172 * E.g. ":2foo! bar" -> [2, "foo", true, "bar"]
1174 * @param {string} str The Ex command line string.
1177 // FIXME: why does this return an Array rather than Object?
1178 parseCommand: function parseCommand(str) {
1180 str.replace(/\s*".*$/, "");
1182 let matches = this.commandRegexp.exec(str);
1186 let { spec, count, group, cmd, bang, space, args } = matches;
1188 [cmd, bang] = [bang, cmd];
1190 if (!cmd || args && args[0] != "|" && !(space || cmd == "!"))
1195 count = count == "%" ? this.COUNT_ALL : parseInt(count, 10);
1197 count = this.COUNT_NONE;
1199 return [count, cmd, !!bang, args || "", spec.length, group];
1202 parseCommands: function parseCommands(str, complete) {
1203 const { contexts } = this.modules;
1205 let [count, cmd, bang, args, len, group] = commands.parseCommand(str);
1207 var command = this.get(cmd || "");
1208 else if (group = contexts.getGroup(group, "commands"))
1209 command = group.get(cmd || "");
1211 if (command == null) {
1212 yield [null, { commandString: str }];
1217 complete.fork(command.name);
1218 var context = complete.fork("args", len);
1221 if (!complete || /(\w|^)[!\s]/.test(str))
1222 args = command.parseArgs(args, context, { count: count, bang: bang });
1224 args = this.parseArgs(args, { extra: { count: count, bang: bang } });
1225 args.context = this.context;
1226 args.commandName = cmd;
1227 args.commandString = str.substr(0, len) + args.string;
1228 str = args.trailing;
1229 yield [command, args];
1236 subCommands: function subCommands(command) {
1237 let commands = [command];
1238 while (command = commands.shift())
1240 for (let [command, args] in this.parseCommands(command)) {
1242 yield [command, args];
1243 if (command.subCommand && args[command.subCommand])
1244 commands.push(args[command.subCommand]);
1252 get complQuote() Commands.complQuote,
1255 get quoteArg() Commands.quoteArg // XXX: better somewhere else?
1258 // returns [count, parsed_argument]
1259 parseArg: function parseArg(str, sep, keepQuotes) {
1262 let len = str.length;
1264 function fixEscapes(str) str.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4}|(.))/g, function (m, n1) n1 || m);
1269 sep = sep != null ? sep : /\s/;
1270 let re1 = RegExp("^" + (sep.source === "" ? "(?!)" : sep.source));
1271 let re2 = RegExp(/^()((?:[^\\S"']|\\.)+)((?:\\$)?)/.source.replace("S", sep.source));
1273 while (str.length && !re1.test(str)) {
1275 if ((res = re2.exec(str)))
1276 arg += keepQuotes ? res[0] : res[2].replace(/\\(.)/g, "$1");
1277 else if ((res = /^(")((?:[^\\"]|\\.)*)("?)/.exec(str)))
1278 arg += keepQuotes ? res[0] : JSON.parse(fixEscapes(res[0]) + (res[3] ? "" : '"'));
1279 else if ((res = /^(')((?:[^']|'')*)('?)/.exec(str)))
1280 arg += keepQuotes ? res[0] : res[2].replace("''", "'", "g");
1288 str = str.substr(res[0].length);
1291 return [len - str.length, arg, quote];
1294 quote: function quote(str) Commands.quoteArg[
1295 /[\b\f\n\r\t]/.test(str) ? '"' :
1296 /[\s"'\\]|^$|^-/.test(str) ? "'"
1299 completion: function initCompletion(dactyl, modules, window) {
1300 const { completion, contexts } = modules;
1302 completion.command = function command(context, group) {
1303 context.title = ["Command"];
1304 context.keys = { text: "longNames", description: "description" };
1306 context.generate = function () group._list;
1308 context.generate = function () modules.commands.hives.map(function (h) h._list).flatten();
1311 // provides completions for ex commands, including their arguments
1312 completion.ex = function ex(context) {
1313 const { commands } = modules;
1315 // if there is no space between the command name and the cursor
1316 // then get completions of the command name
1317 for (var [command, args] in commands.parseCommands(context.filter, context))
1319 context.advance(args.commandString.length + 1);
1321 args = { commandString: context.filter };
1323 let match = commands.commandRegexp.exec(args.commandString);
1328 context.advance(match.group.length + 1);
1330 context.advance(match.prespace.length + match.count.length);
1331 if (!(match.bang || match.space)) {
1332 context.fork("", 0, this, "command", match.group && contexts.getGroup(match.group, "commands"));
1336 // dynamically get completions as specified with the command's completer function
1337 context.highlight();
1339 context.message = "No such command: " + match.cmd;
1340 context.highlight(0, match.cmd.length, "SPELLCHECK");
1344 let cmdContext = context.fork(command.name, match.fullCmd.length + match.bang.length + match.space.length);
1346 if (!cmdContext.waitingForTab) {
1347 if (!args.completeOpt && command.completer && args.completeStart != null) {
1348 cmdContext.advance(args.completeStart);
1349 cmdContext.quote = args.quote;
1350 cmdContext.filter = args.completeFilter;
1351 command.completer.call(command, cmdContext, args);
1356 util.reportError(e);
1360 completion.userCommand = function userCommand(context, group) {
1361 context.title = ["User Command", "Definition"];
1362 context.keys = { text: "name", description: "replacementText" };
1363 context.completions = group || modules.commands.user;
1367 commands: function initCommands(dactyl, modules, window) {
1368 const { commands, contexts } = modules;
1370 // TODO: Vim allows commands to be defined without {rep} if there are {attr}s
1371 // specified - useful?
1372 commands.add(["com[mand]"],
1373 "List or define commands",
1377 util.assert(!cmd || cmd.split(",").every(commands.validName.closure.test),
1378 _("command.invalidName", cmd));
1380 if (!args.literalArg)
1383 util.assert(args["-group"].modifiable,
1384 _("group.cantChangeBuiltin", _("command.commands")));
1386 let completer = args["-complete"];
1387 let completerFunc = null; // default to no completion for user commands
1390 if (/^custom,/.test(completer)) {
1391 completer = completer.substr(7);
1393 let context = update({}, contexts.context || {});
1394 completerFunc = function (context) {
1396 var result = contextswithSavedValues(["context"], function () {
1397 contexts.context = context;
1398 return dactyl.userEval(completer);
1402 dactyl.echo(":" + this.name + " ...");
1403 dactyl.echoerr(_("command.unknownCompleter", completer));
1407 if (callable(result))
1408 return result.apply(this, Array.slice(arguments));
1410 return context.completions = result;
1414 completerFunc = function (context) modules.completion.closure[config.completers[completer]](context);
1417 let added = args["-group"].add(cmd.split(","),
1418 args["-description"],
1419 contexts.bindMacro(args, "-ex",
1420 function makeParams(args, modifiers) ({
1423 toString: function () this.string,
1425 bang: this.bang && args.bang ? "!" : "",
1426 count: this.count && args.count
1429 argCount: args["-nargs"],
1430 bang: args["-bang"],
1431 count: args["-count"],
1432 completer: completerFunc,
1433 literal: args["-literal"],
1434 persist: !args["-nopersist"],
1435 replacementText: args.literalArg,
1436 context: contexts.context && update({}, contexts.context)
1440 dactyl.echoerr(_("command.exists"));
1444 completer: function (context, args) {
1445 const { completion } = modules;
1446 if (args.completeArg == 0)
1447 completion.userCommand(context, args["-group"]);
1449 args["-javascript"] ? completion.javascript(context) : completion.ex(context);
1453 { names: ["-bang", "-b"], description: "Command may be followed by a !" },
1454 { names: ["-count", "-c"], description: "Command may be preceded by a count" },
1456 // TODO: "E180: invalid complete value: " + arg
1457 names: ["-complete", "-C"],
1458 description: "The argument completion function",
1459 completer: function (context) [[k, ""] for ([k, v] in Iterator(config.completers))],
1460 type: CommandOption.STRING,
1461 validator: function (arg) arg in config.completers || /^custom,/.test(arg),
1464 names: ["-description", "-desc", "-d"],
1465 description: "A user-visible description of the command",
1466 default: "User-defined command",
1467 type: CommandOption.STRING
1469 contexts.GroupFlag("commands"),
1471 names: ["-javascript", "-js", "-j"],
1472 description: "Execute the definition as JavaScript rather than Ex commands"
1475 names: ["-literal", "-l"],
1476 description: "Process the nth ignoring any quoting or meta characters",
1477 type: CommandOption.INT
1480 names: ["-nargs", "-a"],
1481 description: "The allowed number of arguments",
1482 completer: [["0", "No arguments are allowed (default)"],
1483 ["1", "One argument is allowed"],
1484 ["*", "Zero or more arguments are allowed"],
1485 ["?", "Zero or one argument is allowed"],
1486 ["+", "One or more arguments are allowed"]],
1488 type: CommandOption.STRING,
1489 validator: function (arg) /^[01*?+]$/.test(arg)
1492 names: ["-nopersist", "-n"],
1493 description: "Do not save this command to an auto-generated RC file"
1498 serialize: function () array(commands.userHives)
1499 .filter(function (h) h.persist)
1500 .map(function (hive) [
1504 options: iter([v, typeof cmd[k] == "boolean" ? null : cmd[k]]
1505 // FIXME: this map is expressed multiple times
1506 for ([k, v] in Iterator({
1510 description: "-description"
1512 if (cmd[k])).toObject(),
1513 arguments: [cmd.name],
1514 literalArg: cmd.action,
1515 ignoreDefaults: true
1517 for (cmd in hive) if (cmd.persist)
1522 commands.add(["delc[ommand]"],
1523 "Delete the specified user-defined command",
1525 util.assert(args.bang ^ !!args[0], _("error.argumentOrBang"));
1529 args["-group"].clear();
1530 else if (args["-group"].get(name))
1531 args["-group"].remove(name);
1533 dactyl.echoerr(_("command.noSuchUser", name));
1537 completer: function (context, args) modules.completion.userCommand(context, args["-group"]),
1538 options: [contexts.GroupFlag("commands")]
1541 commands.add(["comp[letions]"],
1542 "List the completion results for a given command substring",
1543 function (args) { modules.completion.listCompleter("ex", args[0]); },
1546 completer: function (context, args) modules.completion.ex(context),
1550 dactyl.addUsageCommand({
1551 name: ["listc[ommands]", "lc"],
1552 description: "List all Ex commands along with their short descriptions",
1554 iterate: function (args) commands.iterator().map(function (cmd) ({
1557 cmd.hive == commands.builtin ? "" : <span highlight="Object" style="padding-right: 1em;">{cmd.hive.name}</span>
1560 iterateIndex: function (args) let (tags = services["dactyl:"].HELP_TAGS)
1561 this.iterate(args).filter(function (cmd) cmd.hive === commands.builtin || set.has(cmd.helpTag)),
1563 headings: ["Command", "Group", "Description"],
1564 description: function (cmd) template.linkifyHelp(cmd.description + (cmd.replacementText ? ": " + cmd.action : "")),
1565 help: function (cmd) ":" + cmd.name
1569 commands.add(["y[ank]"],
1570 "Yank the output of the given command to the clipboard",
1572 let cmd = /^:/.test(args[0]) ? args[0] : ":echo " + args[0];
1574 let res = modules.commandline.withOutputToString(commands.execute, commands, cmd);
1576 dactyl.clipboardWrite(res);
1578 let lines = res.split("\n").length;
1579 dactyl.echomsg("Yanked " + lines + " line" + (lines == 1 ? "" : "s"));
1582 completer: function (context) modules.completion[/^:/.test(context.filter) ? "ex" : "javascript"](context),
1586 javascript: function initJavascript(dactyl, modules, window) {
1587 const { JavaScript, commands } = modules;
1589 JavaScript.setCompleter([commands.user.get, commands.user.remove],
1590 [function () [[c.names, c.description] for (c in this)]]);
1591 JavaScript.setCompleter([commands.get],
1592 [function () [[c.names, c.description] for (c in this.iterator())]]);
1594 mappings: function initMappings(dactyl, modules, window) {
1595 const { commands, mappings, modes } = modules;
1597 mappings.add([modes.COMMAND],
1598 ["@:"], "Repeat the last Ex command",
1600 if (commands.repeat) {
1601 for (let i in util.interruptibleRange(0, Math.max(args.count, 1), 100))
1602 dactyl.execute(commands.repeat);
1605 dactyl.echoerr(_("command.noPrevious"));
1613 Commands.quoteMap = {
1617 function quote(q, list, map) {
1618 map = map || Commands.quoteMap;
1619 let re = RegExp("[" + list + "]", "g");
1620 function quote(str) q + String.replace(str, re, function ($0) $0 in map ? map[$0] : ("\\" + $0)) + q;
1625 Commands.quoteArg = {
1626 '"': quote('"', '\n\t"\\\\'),
1627 "'": quote("'", "'", { "'": "''" }),
1628 "": quote("", "|\\\\\\s'\"")
1630 Commands.complQuote = {
1631 '"': ['"', quote("", Commands.quoteArg['"'].list), '"'],
1632 "'": ["'", quote("", Commands.quoteArg["'"].list), "'"],
1633 "": ["", Commands.quoteArg[""], ""]
1636 Commands.parseBool = function (arg) {
1637 if (/^(true|1|on)$/i.test(arg))
1639 if (/^(false|0|off)$/i.test(arg))
1647 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1649 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: