mirror of
https://github.com/gryf/pentadactyl-pm.git
synced 2025-12-19 02:10:17 +01:00
1686 lines
66 KiB
JavaScript
1686 lines
66 KiB
JavaScript
// Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
|
|
// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
|
|
// Copyright (c) 2008-2011 by Kris Maglione <maglione.k at Gmail>
|
|
//
|
|
// This work is licensed for reuse under an MIT license. Details are
|
|
// given in the LICENSE.txt file included with this file.
|
|
"use strict";
|
|
|
|
try {
|
|
|
|
Components.utils.import("resource://dactyl/bootstrap.jsm");
|
|
defineModule("commands", {
|
|
exports: ["ArgType", "Command", "Commands", "CommandOption", "Ex", "commands"],
|
|
require: ["contexts", "messages", "util"]
|
|
}, this);
|
|
|
|
/**
|
|
* A structure representing the options available for a command.
|
|
*
|
|
* Do NOT create instances of this class yourself, use the helper method
|
|
* {@see Commands#add} instead
|
|
*
|
|
* @property {[string]} names An array of option names. The first name
|
|
* is the canonical option name.
|
|
* @property {number} type The option's value type. This is one of:
|
|
* (@link CommandOption.NOARG),
|
|
* (@link CommandOption.STRING),
|
|
* (@link CommandOption.STRINGMAP),
|
|
* (@link CommandOption.BOOL),
|
|
* (@link CommandOption.INT),
|
|
* (@link CommandOption.FLOAT),
|
|
* (@link CommandOption.LIST),
|
|
* (@link CommandOption.ANY)
|
|
* @property {function} validator A validator function
|
|
* @property {function (CompletionContext, object)} completer A list of
|
|
* completions, or a completion function which will be passed a
|
|
* {@link CompletionContext} and an object like that returned by
|
|
* {@link commands.parseArgs} with the following additional keys:
|
|
* completeOpt - The name of the option currently being completed.
|
|
* @property {boolean} multiple Whether this option can be specified multiple times
|
|
* @property {string} description A description of the option
|
|
* @property {object} default The option's default value
|
|
*/
|
|
|
|
var CommandOption = Struct("names", "type", "validator", "completer", "multiple", "description", "default");
|
|
CommandOption.defaultValue("description", function () "");
|
|
CommandOption.defaultValue("type", function () CommandOption.NOARG);
|
|
CommandOption.defaultValue("multiple", function () false);
|
|
|
|
var ArgType = Struct("description", "parse");
|
|
update(CommandOption, {
|
|
/**
|
|
* @property {object} The option argument is unspecified. Any argument
|
|
* is accepted and caller is responsible for parsing the return
|
|
* value.
|
|
* @final
|
|
*/
|
|
ANY: 0,
|
|
|
|
/**
|
|
* @property {object} The option doesn't accept an argument.
|
|
* @final
|
|
*/
|
|
NOARG: ArgType("no arg", function (arg) !arg || null),
|
|
/**
|
|
* @property {object} The option accepts a boolean argument.
|
|
* @final
|
|
*/
|
|
BOOL: ArgType("boolean", function parseBoolArg(val) Commands.parseBool(val)),
|
|
/**
|
|
* @property {object} The option accepts a string argument.
|
|
* @final
|
|
*/
|
|
STRING: ArgType("string", function (val) val),
|
|
/**
|
|
* @property {object} The option accepts a stringmap argument.
|
|
* @final
|
|
*/
|
|
STRINGMAP: ArgType("stringmap", function (val, quoted) Option.parse.stringmap(quoted)),
|
|
/**
|
|
* @property {object} The option accepts an integer argument.
|
|
* @final
|
|
*/
|
|
INT: ArgType("int", function parseIntArg(val) parseInt(val)),
|
|
/**
|
|
* @property {object} The option accepts a float argument.
|
|
* @final
|
|
*/
|
|
FLOAT: ArgType("float", function parseFloatArg(val) parseFloat(val)),
|
|
/**
|
|
* @property {object} The option accepts a string list argument.
|
|
* E.g. "foo,bar"
|
|
* @final
|
|
*/
|
|
LIST: ArgType("list", function parseListArg(arg, quoted) Option.splitList(quoted))
|
|
});
|
|
|
|
/**
|
|
* A class representing Ex commands. Instances are created by
|
|
* the {@link Commands} class.
|
|
*
|
|
* @param {[string]} specs The names by which this command can be invoked.
|
|
* These are specified in the form "com[mand]" where "com" is a unique
|
|
* command name prefix.
|
|
* @param {string} description A short one line description of the command.
|
|
* @param {function} action The action invoked by this command when executed.
|
|
* @param {Object} extraInfo An optional extra configuration hash. The
|
|
* following properties are supported.
|
|
* argCount - see {@link Command#argCount}
|
|
* bang - see {@link Command#bang}
|
|
* completer - see {@link Command#completer}
|
|
* count - see {@link Command#count}
|
|
* domains - see {@link Command#domains}
|
|
* heredoc - see {@link Command#heredoc}
|
|
* literal - see {@link Command#literal}
|
|
* options - see {@link Command#options}
|
|
* privateData - see {@link Command#privateData}
|
|
* serialize - see {@link Command#serialize}
|
|
* subCommand - see {@link Command#subCommand}
|
|
* @optional
|
|
* @private
|
|
*/
|
|
var Command = Class("Command", {
|
|
init: function init(specs, description, action, extraInfo) {
|
|
specs = Array.concat(specs); // XXX
|
|
let parsedSpecs = extraInfo.parsedSpecs || Command.parseSpecs(specs);
|
|
|
|
this.specs = specs;
|
|
this.shortNames = array.compact(parsedSpecs.map(function (n) n[1]));
|
|
this.longNames = parsedSpecs.map(function (n) n[0]);
|
|
this.name = this.longNames[0];
|
|
this.names = array.flatten(parsedSpecs);
|
|
this.description = description;
|
|
this.action = action;
|
|
|
|
if (extraInfo)
|
|
this.update(extraInfo);
|
|
if (this.options)
|
|
this.options = this.options.map(CommandOption.fromArray, CommandOption);
|
|
for each (let option in this.options)
|
|
option.localeName = ["command", this.name, option.names[0]];
|
|
},
|
|
|
|
get toStringParams() [this.name, this.hive.name],
|
|
|
|
get identifier() this.hive.prefix + this.name,
|
|
|
|
get helpTag() ":" + this.name,
|
|
|
|
get lastCommand() this._lastCommand || this.modules.commandline.command,
|
|
set lastCommand(val) { this._lastCommand = val; },
|
|
|
|
/**
|
|
* Execute this command.
|
|
*
|
|
* @param {Args} args The Args object passed to {@link #action}.
|
|
* @param {Object} modifiers Any modifiers to be passed to {@link #action}.
|
|
*/
|
|
execute: function execute(args, modifiers) {
|
|
const { dactyl } = this.modules;
|
|
|
|
let context = args.context;
|
|
if (this.deprecated)
|
|
this.warn(context, "deprecated", _("warn.deprecated", ":" + this.name, this.deprecated));
|
|
|
|
modifiers = modifiers || {};
|
|
|
|
if (args.count != null && !this.count)
|
|
throw FailedAssertion(_("command.noCount"));
|
|
if (args.bang && !this.bang)
|
|
throw FailedAssertion(_("command.noBang"));
|
|
|
|
args.doc = this.hive.group.lastDocument;
|
|
|
|
return !dactyl.trapErrors(function exec() {
|
|
let extra = this.hive.argsExtra(args);
|
|
|
|
for (let k in properties(extra))
|
|
if (!(k in args))
|
|
Object.defineProperty(args, k, Object.getOwnPropertyDescriptor(extra, k));
|
|
|
|
if (this.always)
|
|
this.always(args, modifiers);
|
|
|
|
if (!context || !context.noExecute)
|
|
this.action(args, modifiers);
|
|
}, this);
|
|
},
|
|
|
|
/**
|
|
* Returns whether this command may be invoked via *name*.
|
|
*
|
|
* @param {string} name The candidate name.
|
|
* @returns {boolean}
|
|
*/
|
|
hasName: function hasName(name) this.parsedSpecs.some(
|
|
function ([long, short]) name.indexOf(short) == 0 && long.indexOf(name) == 0),
|
|
|
|
/**
|
|
* A helper function to parse an argument string.
|
|
*
|
|
* @param {string} args The argument string to parse.
|
|
* @param {CompletionContext} complete A completion context.
|
|
* Non-null when the arguments are being parsed for completion
|
|
* purposes.
|
|
* @param {Object} extra Extra keys to be spliced into the
|
|
* returned Args object.
|
|
* @returns {Args}
|
|
* @see Commands#parseArgs
|
|
*/
|
|
parseArgs: function parseArgs(args, complete, extra) this.modules.commands.parseArgs(args, {
|
|
__proto__: this,
|
|
complete: complete,
|
|
extra: extra
|
|
}),
|
|
|
|
complained: Class.Memoize(function () ({})),
|
|
|
|
/**
|
|
* @property {[string]} All of this command's name specs. e.g., "com[mand]"
|
|
*/
|
|
specs: null,
|
|
/** @property {[string]} All of this command's short names, e.g., "com" */
|
|
shortNames: null,
|
|
/**
|
|
* @property {[string]} All of this command's long names, e.g., "command"
|
|
*/
|
|
longNames: null,
|
|
|
|
/** @property {string} The command's canonical name. */
|
|
name: null,
|
|
/** @property {[string]} All of this command's long and short names. */
|
|
names: null,
|
|
|
|
/** @property {string} This command's description, as shown in :listcommands */
|
|
description: Messages.Localized(""),
|
|
|
|
/** @property {string|null} If set, the deprecation message for this command. */
|
|
deprecated: Messages.Localized(null),
|
|
|
|
/**
|
|
* @property {function (Args)} The function called to execute this command.
|
|
*/
|
|
action: null,
|
|
/**
|
|
* @property {string} This command's argument count spec.
|
|
* @see Commands#parseArguments
|
|
*/
|
|
argCount: 0,
|
|
/**
|
|
* @property {function (CompletionContext, Args)} This command's completer.
|
|
* @see CompletionContext
|
|
*/
|
|
completer: null,
|
|
/** @property {boolean} Whether this command accepts a here document. */
|
|
hereDoc: false,
|
|
/**
|
|
* @property {boolean} Whether this command may be called with a bang,
|
|
* e.g., :com!
|
|
*/
|
|
bang: false,
|
|
/**
|
|
* @property {boolean} Whether this command may be called with a count,
|
|
* e.g., :12bdel
|
|
*/
|
|
count: false,
|
|
/**
|
|
* @property {function(args)} A function which should return a list
|
|
* of domains referenced in the given args. Used in determining
|
|
* whether to purge the command from history when clearing
|
|
* private data.
|
|
*/
|
|
domains: function (args) [],
|
|
/**
|
|
* @property {boolean} At what index this command's literal arguments
|
|
* begin. For instance, with a value of 2, all arguments starting with
|
|
* the third are parsed as a single string, with all quoting characters
|
|
* passed literally. This is especially useful for commands which take
|
|
* key mappings or Ex command lines as arguments.
|
|
*/
|
|
literal: null,
|
|
/**
|
|
* @property {Array} The options this command takes.
|
|
* @see Commands@parseArguments
|
|
*/
|
|
options: [],
|
|
|
|
optionMap: Class.Memoize(function () array(this.options)
|
|
.map(function (opt) opt.names.map(function (name) [name, opt]))
|
|
.flatten().toObject()),
|
|
|
|
newArgs: function newArgs(base) {
|
|
let res = [];
|
|
update(res, base);
|
|
res.__proto__ = this.argsPrototype;
|
|
return res;
|
|
},
|
|
|
|
argsPrototype: Class.Memoize(function argsPrototype() {
|
|
let res = update([], {
|
|
__iterator__: function AP__iterator__() array.iterItems(this),
|
|
|
|
command: this,
|
|
|
|
explicitOpts: Class.Memoize(function () ({})),
|
|
|
|
has: function AP_has(opt) Set.has(this.explicitOpts, opt) || typeof opt === "number" && Set.has(this, opt),
|
|
|
|
get literalArg() this.command.literal != null && this[this.command.literal] || "",
|
|
|
|
// TODO: string: Class.Memoize(function () { ... }),
|
|
|
|
verify: function verify() {
|
|
if (this.command.argCount) {
|
|
util.assert((this.length > 0 || !/^[1+]$/.test(this.command.argCount)) &&
|
|
(this.literal == null || !/[1+]/.test(this.command.argCount) || /\S/.test(this.literalArg || "")),
|
|
_("error.argumentRequired"));
|
|
|
|
util.assert((this.length == 0 || this.command.argCount !== "0") &&
|
|
(this.length <= 1 || !/^[01?]$/.test(this.command.argCount)),
|
|
_("error.trailingCharacters"));
|
|
}
|
|
}
|
|
});
|
|
|
|
this.options.forEach(function (opt) {
|
|
if (opt.default !== undefined)
|
|
Object.defineProperty(res, opt.names[0],
|
|
Object.getOwnPropertyDescriptor(opt, "default") ||
|
|
{ configurable: true, enumerable: true, get: function () opt.default });
|
|
});
|
|
|
|
return res;
|
|
}),
|
|
|
|
/**
|
|
* @property {boolean|function(args)} When true, invocations of this
|
|
* command may contain private data which should be purged from
|
|
* saved histories when clearing private data. If a function, it
|
|
* should return true if an invocation with the given args
|
|
* contains private data
|
|
*/
|
|
privateData: true,
|
|
/**
|
|
* @property {function} Should return an array of *Object*s suitable to be
|
|
* passed to {@link Commands#commandToString}, one for each past
|
|
* invocation which should be restored on subsequent @dactyl startups.
|
|
*/
|
|
serialize: null,
|
|
serialGroup: 50,
|
|
/**
|
|
* @property {number} If this command takes another ex command as an
|
|
* argument, the index of that argument. Used in determining whether to
|
|
* purge the command from history when clearing private data.
|
|
*/
|
|
subCommand: null,
|
|
/**
|
|
* @property {boolean} Specifies whether this is a user command. User
|
|
* commands may be created by plugins, or directly by users, and,
|
|
* unlike basic commands, may be overwritten. Users and plugin authors
|
|
* should create only user commands.
|
|
*/
|
|
user: false,
|
|
/**
|
|
* @property {string} For commands defined via :command, contains the Ex
|
|
* command line to be executed upon invocation.
|
|
*/
|
|
replacementText: null,
|
|
|
|
/**
|
|
* Warns of a misuse of this command once per warning type per file.
|
|
*
|
|
* @param {object} context The calling context.
|
|
* @param {string} type The type of warning.
|
|
* @param {string} warning The warning message.
|
|
*/
|
|
warn: function warn(context, type, message) {
|
|
let loc = !context ? "" : [context.file, context.line, " "].join(":");
|
|
|
|
if (!Set.add(this.complained, type + ":" + (context ? context.file : "[Command Line]")))
|
|
this.modules.dactyl.warn(loc + message);
|
|
}
|
|
}, {
|
|
// TODO: do we really need more than longNames as a convenience anyway?
|
|
/**
|
|
* Converts command name abbreviation specs of the form
|
|
* 'shortname[optional-tail]' to short and long versions:
|
|
* ["abc[def]", "ghijkl"] -> [["abcdef", "abc"], ["ghijlk"]]
|
|
*
|
|
* @param {Array} specs An array of command name specs to parse.
|
|
* @returns {Array}
|
|
*/
|
|
parseSpecs: function parseSpecs(specs) {
|
|
return specs.map(function (spec) {
|
|
let [, head, tail] = /([^[]+)(?:\[(.*)])?/.exec(spec);
|
|
return tail ? [head + tail, head] : [head];
|
|
});
|
|
}
|
|
});
|
|
|
|
// Prototype.
|
|
var Ex = Module("Ex", {
|
|
Local: function Local(dactyl, modules, window) ({
|
|
get commands() modules.commands,
|
|
get context() modules.contexts.context
|
|
}),
|
|
|
|
_args: function E_args(cmd, args) {
|
|
args = Array.slice(args);
|
|
|
|
let res = cmd.newArgs({ context: this.context });
|
|
if (isObject(args[0]))
|
|
for (let [k, v] in Iterator(args.shift()))
|
|
if (k == "!")
|
|
res.bang = v;
|
|
else if (k == "#")
|
|
res.count = v;
|
|
else {
|
|
let opt = cmd.optionMap["-" + k];
|
|
let val = opt.type && opt.type.parse(v);
|
|
|
|
util.assert(val != null && (typeof val !== "number" || !isNaN(val)),
|
|
_("option.noSuch", k));
|
|
|
|
Class.replaceProperty(res, opt.names[0], val);
|
|
res.explicitOpts[opt.names[0]] = val;
|
|
}
|
|
for (let [i, val] in array.iterItems(args))
|
|
res[i] = String(val);
|
|
return res;
|
|
},
|
|
|
|
_complete: function E_complete(cmd) let (self = this)
|
|
function _complete(context, func, obj, args) {
|
|
args = self._args(cmd, args);
|
|
args.completeArg = args.length - 1;
|
|
if (cmd.completer && args.length)
|
|
return cmd.completer(context, args);
|
|
},
|
|
|
|
_run: function E_run(name) {
|
|
const self = this;
|
|
let cmd = this.commands.get(name);
|
|
util.assert(cmd, _("command.noSuch"));
|
|
|
|
return update(function exCommand(options) {
|
|
let args = self._args(cmd, arguments);
|
|
args.verify();
|
|
return cmd.execute(args);
|
|
}, {
|
|
dactylCompleter: self._complete(cmd)
|
|
});
|
|
},
|
|
|
|
__noSuchMethod__: function __noSuchMethod__(meth, args) this._run(meth).apply(this, args)
|
|
});
|
|
|
|
var CommandHive = Class("CommandHive", Contexts.Hive, {
|
|
init: function init(group) {
|
|
init.supercall(this, group);
|
|
this._map = {};
|
|
this._list = [];
|
|
},
|
|
|
|
/** @property {Iterator(Command)} @private */
|
|
__iterator__: function __iterator__() array.iterValues(this._list.sort(function (a, b) a.name > b.name)),
|
|
|
|
/** @property {string} The last executed Ex command line. */
|
|
repeat: null,
|
|
|
|
/**
|
|
* Adds a new command to the builtin hive. Accessible only to core
|
|
* dactyl code. Plugins should use group.commands.add instead.
|
|
*
|
|
* @param {[string]} specs The names by which this command can be
|
|
* invoked. The first name specified is the command's canonical
|
|
* name.
|
|
* @param {string} description A description of the command.
|
|
* @param {function} action The action invoked by this command.
|
|
* @param {Object} extra An optional extra configuration hash.
|
|
* @optional
|
|
*/
|
|
add: function add(specs, description, action, extra, replace) {
|
|
const { commands, contexts } = this.modules;
|
|
|
|
extra = extra || {};
|
|
if (!extra.definedAt)
|
|
extra.definedAt = contexts.getCaller(Components.stack.caller);
|
|
|
|
extra.hive = this;
|
|
extra.parsedSpecs = Command.parseSpecs(specs);
|
|
|
|
let names = array.flatten(extra.parsedSpecs);
|
|
let name = names[0];
|
|
|
|
util.assert(!names.some(function (name) name in commands.builtin._map),
|
|
_("command.cantReplace", name));
|
|
|
|
util.assert(replace || names.every(function (name) !(name in this._map), this),
|
|
_("command.wontReplace", name));
|
|
|
|
for (let name in values(names)) {
|
|
ex.__defineGetter__(name, function () this._run(name));
|
|
if (name in this._map)
|
|
this.remove(name);
|
|
}
|
|
|
|
let self = this;
|
|
let closure = function () self._map[name];
|
|
|
|
memoize(this._map, name, function () commands.Command(specs, description, action, extra));
|
|
if (!extra.hidden)
|
|
memoize(this._list, this._list.length, closure);
|
|
for (let alias in values(names.slice(1)))
|
|
memoize(this._map, alias, closure);
|
|
|
|
return name;
|
|
},
|
|
|
|
_add: function _add(names, description, action, extra, replace) {
|
|
const { contexts } = this.modules;
|
|
|
|
extra = extra || {};
|
|
extra.definedAt = contexts.getCaller(Components.stack.caller.caller);
|
|
return this.add.apply(this, arguments);
|
|
},
|
|
|
|
/**
|
|
* Clear all commands.
|
|
* @returns {Command}
|
|
*/
|
|
clear: function clear() {
|
|
util.assert(this.group.modifiable, _("command.cantDelete"));
|
|
this._map = {};
|
|
this._list = [];
|
|
},
|
|
|
|
/**
|
|
* Returns the command with matching *name*.
|
|
*
|
|
* @param {string} name The name of the command to return. This can be
|
|
* any of the command's names.
|
|
* @param {boolean} full If true, only return a command if one of
|
|
* its names matches *name* exactly.
|
|
* @returns {Command}
|
|
*/
|
|
get: function get(name, full) this._map[name]
|
|
|| !full && array.nth(this._list, function (cmd) cmd.hasName(name), 0)
|
|
|| null,
|
|
|
|
/**
|
|
* Remove the user-defined command with matching *name*.
|
|
*
|
|
* @param {string} name The name of the command to remove. This can be
|
|
* any of the command's names.
|
|
*/
|
|
remove: function remove(name) {
|
|
util.assert(this.group.modifiable, _("command.cantDelete"));
|
|
|
|
let cmd = this.get(name);
|
|
this._list = this._list.filter(function (c) c !== cmd);
|
|
for (let name in values(cmd.names))
|
|
delete this._map[name];
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @instance commands
|
|
*/
|
|
var Commands = Module("commands", {
|
|
lazyInit: true,
|
|
|
|
Local: function Local(dactyl, modules, window) let ({ Group, contexts } = modules) ({
|
|
init: function init() {
|
|
this.Command = Class("Command", Command, { modules: modules });
|
|
update(this, {
|
|
hives: contexts.Hives("commands", Class("CommandHive", CommandHive, { modules: modules })),
|
|
user: contexts.hives.commands.user,
|
|
builtin: contexts.hives.commands.builtin
|
|
});
|
|
},
|
|
|
|
get context() contexts.context,
|
|
|
|
get readHeredoc() modules.io.readHeredoc,
|
|
|
|
get allHives() contexts.allGroups.commands,
|
|
|
|
get userHives() this.allHives.filter(function (h) h !== this.builtin, this),
|
|
|
|
/**
|
|
* Executes an Ex command script.
|
|
*
|
|
* @param {string} string A string containing the commands to execute.
|
|
* @param {object} tokens An optional object containing tokens to be
|
|
* interpolated into the command string.
|
|
* @param {object} args Optional arguments object to be passed to
|
|
* command actions.
|
|
* @param {object} context An object containing information about
|
|
* the file that is being or has been sourced to obtain the
|
|
* command string.
|
|
*/
|
|
execute: function execute(string, tokens, silent, args, context) {
|
|
contexts.withContext(context || this.context || { file: "[Command Line]", line: 1 },
|
|
function (context) {
|
|
modules.io.withSavedValues(["readHeredoc"], function () {
|
|
this.readHeredoc = function readHeredoc(end) {
|
|
let res = [];
|
|
contexts.context.line++;
|
|
while (++i < lines.length) {
|
|
if (lines[i] === end)
|
|
return res.join("\n");
|
|
res.push(lines[i]);
|
|
}
|
|
util.assert(false, _("command.eof", end));
|
|
};
|
|
|
|
args = update({}, args || {});
|
|
|
|
if (tokens && !callable(string))
|
|
string = util.compileMacro(string, true);
|
|
if (callable(string))
|
|
string = string(tokens || {});
|
|
|
|
let lines = string.split(/\r\n|[\r\n]/);
|
|
let startLine = context.line;
|
|
|
|
for (var i = 0; i < lines.length && !context.finished; i++) {
|
|
// Deal with editors from Silly OSs.
|
|
let line = lines[i].replace(/\r$/, "");
|
|
|
|
context.line = startLine + i;
|
|
|
|
// Process escaped new lines
|
|
while (i < lines.length && /^\s*\\/.test(lines[i + 1]))
|
|
line += "\n" + lines[++i].replace(/^\s*\\/, "");
|
|
|
|
try {
|
|
dactyl.execute(line, args);
|
|
}
|
|
catch (e) {
|
|
if (!silent) {
|
|
e.message = context.file + ":" + context.line + ": " + e.message;
|
|
dactyl.reportError(e, true);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Lists all user-defined commands matching *filter* and optionally
|
|
* *hives*.
|
|
*
|
|
* @param {string} filter Limits the list to those commands with a name
|
|
* matching this anchored substring.
|
|
* @param {[Hive]} hives List of hives.
|
|
* @optional
|
|
*/
|
|
list: function list(filter, hives) {
|
|
const { commandline, completion } = this.modules;
|
|
function completerToString(completer) {
|
|
if (completer)
|
|
return [k for ([k, v] in Iterator(config.completers)) if (completer == completion.closure[v])][0] || "custom";
|
|
return "";
|
|
}
|
|
// TODO: allow matching of aliases?
|
|
function cmds(hive) hive._list.filter(function (cmd) cmd.name.indexOf(filter || "") == 0)
|
|
|
|
let hives = (hives || this.userHives).map(function (h) [h, cmds(h)]).filter(function ([h, c]) c.length);
|
|
|
|
let list = <table>
|
|
<tr highlight="Title">
|
|
<td/>
|
|
<td style="padding-right: 1em;"></td>
|
|
<td style="padding-right: 1ex;">{_("title.Name")}</td>
|
|
<td style="padding-right: 1ex;">{_("title.Args")}</td>
|
|
<td style="padding-right: 1ex;">{_("title.Range")}</td>
|
|
<td style="padding-right: 1ex;">{_("title.Complete")}</td>
|
|
<td style="padding-right: 1ex;">{_("title.Definition")}</td>
|
|
</tr>
|
|
<col style="min-width: 6em; padding-right: 1em;"/>
|
|
{
|
|
template.map(hives, function ([hive, cmds]) let (i = 0)
|
|
<tr style="height: .5ex;"/> +
|
|
template.map(cmds, function (cmd)
|
|
<tr>
|
|
<td highlight="Title">{!i++ ? hive.name : ""}</td>
|
|
<td>{cmd.bang ? "!" : " "}</td>
|
|
<td>{cmd.name}</td>
|
|
<td>{cmd.argCount}</td>
|
|
<td>{cmd.count ? "0c" : ""}</td>
|
|
<td>{completerToString(cmd.completer)}</td>
|
|
<td>{cmd.replacementText || "function () { ... }"}</td>
|
|
</tr>) +
|
|
<tr style="height: .5ex;"/>)
|
|
}
|
|
</table>;
|
|
|
|
if (list.*.length() === list.text().length() + 2)
|
|
dactyl.echomsg(_("command.none"));
|
|
else
|
|
commandline.commandOutput(list);
|
|
}
|
|
}),
|
|
|
|
/**
|
|
* @property Indicates that no count was specified for this
|
|
* command invocation.
|
|
* @final
|
|
*/
|
|
COUNT_NONE: null,
|
|
/**
|
|
* @property {number} Indicates that the full buffer range (1,$) was
|
|
* specified for this command invocation.
|
|
* @final
|
|
*/
|
|
// FIXME: this isn't a count at all
|
|
COUNT_ALL: -2, // :%...
|
|
|
|
/** @property {Iterator(Command)} @private */
|
|
iterator: function iterator() iter.apply(null, this.hives.array)
|
|
.sort(function (a, b) a.serialGroup - b.serialGroup || a.name > b.name)
|
|
.iterValues(),
|
|
|
|
/** @property {string} The last executed Ex command line. */
|
|
repeat: null,
|
|
|
|
add: function add() {
|
|
let group = this.builtin;
|
|
if (!util.isDactyl(Components.stack.caller)) {
|
|
deprecated.warn(add, "commands.add", "group.commands.add");
|
|
group = this.user;
|
|
}
|
|
|
|
return group._add.apply(group, arguments);
|
|
},
|
|
addUserCommand: deprecated("group.commands.add", { get: function addUserCommand() this.user.closure._add }),
|
|
getUserCommands: deprecated("iter(group.commands)", function getUserCommands() iter(this.user).toArray()),
|
|
removeUserCommand: deprecated("group.commands.remove", { get: function removeUserCommand() this.user.closure.remove }),
|
|
|
|
/**
|
|
* Returns the specified command invocation object serialized to
|
|
* an executable Ex command string.
|
|
*
|
|
* @param {Object} args The command invocation object.
|
|
* @returns {string}
|
|
*/
|
|
commandToString: function commandToString(args) {
|
|
let res = [args.command + (args.bang ? "!" : "")];
|
|
|
|
let defaults = {};
|
|
if (args.ignoreDefaults)
|
|
defaults = array(this.options).map(function (opt) [opt.names[0], opt.default])
|
|
.toObject();
|
|
|
|
for (let [opt, val] in Iterator(args.options || {})) {
|
|
if (val != null && defaults[opt] === val)
|
|
continue;
|
|
let chr = /^-.$/.test(opt) ? " " : "=";
|
|
if (isArray(val))
|
|
opt += chr + Option.stringify.stringlist(val);
|
|
else if (val != null)
|
|
opt += chr + Commands.quote(val);
|
|
res.push(opt);
|
|
}
|
|
for (let [, arg] in Iterator(args.arguments || []))
|
|
res.push(Commands.quote(arg));
|
|
|
|
let str = args.literalArg;
|
|
if (str)
|
|
res.push(!/\n/.test(str) ? str :
|
|
this.serializeHereDoc ? "<<EOF\n" + String.replace(str, /\n$/, "") + "\nEOF"
|
|
: String.replace(str, /\n/g, "\n" + res[0].replace(/./g, " ").replace(/.$/, "\\")));
|
|
return res.join(" ");
|
|
},
|
|
|
|
/**
|
|
* Returns the command with matching *name*.
|
|
*
|
|
* @param {string} name The name of the command to return. This can be
|
|
* any of the command's names.
|
|
* @returns {Command}
|
|
*/
|
|
get: function get(name, full) iter(this.hives).map(function ([i, hive]) hive.get(name, full))
|
|
.nth(util.identity, 0),
|
|
|
|
/**
|
|
* Returns true if a command invocation contains a URL referring to the
|
|
* domain *host*.
|
|
*
|
|
* @param {string} command
|
|
* @param {string} host
|
|
* @returns {boolean}
|
|
*/
|
|
hasDomain: function hasDomain(command, host) {
|
|
try {
|
|
for (let [cmd, args] in this.subCommands(command))
|
|
if (Array.concat(cmd.domains(args)).some(function (domain) util.isSubdomain(domain, host)))
|
|
return true;
|
|
}
|
|
catch (e) {
|
|
util.reportError(e);
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Returns true if a command invocation contains private data which should
|
|
* be cleared when purging private data.
|
|
*
|
|
* @param {string} command
|
|
* @returns {boolean}
|
|
*/
|
|
hasPrivateData: function hasPrivateData(command) {
|
|
for (let [cmd, args] in this.subCommands(command))
|
|
if (cmd.privateData)
|
|
return !callable(cmd.privateData) || cmd.privateData(args);
|
|
return false;
|
|
},
|
|
|
|
// TODO: should it handle comments?
|
|
// : it might be nice to be able to specify that certain quoting
|
|
// should be disabled E.g. backslash without having to resort to
|
|
// using literal etc.
|
|
// : error messages should be configurable or else we can ditch
|
|
// Vim compatibility but it actually gives useful messages
|
|
// sometimes rather than just "Invalid arg"
|
|
// : I'm not sure documenting the returned object here, and
|
|
// elsewhere, as type Args rather than simply Object makes sense,
|
|
// especially since it is further augmented for use in
|
|
// Command#action etc.
|
|
/**
|
|
* Parses *str* for options and plain arguments.
|
|
*
|
|
* The returned *Args* object is an augmented array of arguments.
|
|
* Any key/value pairs of *extra* will be available and the
|
|
* following additional properties:
|
|
* -opt - the value of the option -opt if specified
|
|
* string - the original argument string *str*
|
|
* literalArg - any trailing literal argument
|
|
*
|
|
* Quoting rules:
|
|
* '-quoted strings - only ' and \ itself are escaped
|
|
* "-quoted strings - also ", \n and \t are translated
|
|
* non-quoted strings - everything is taken literally apart from "\
|
|
* " and "\\"
|
|
*
|
|
* @param {string} str The Ex command-line string to parse. E.g.
|
|
* "-x=foo -opt=bar arg1 arg2"
|
|
* @param {[CommandOption]} options The options accepted. These are specified
|
|
* as an array of {@link CommandOption} structures.
|
|
* @param {string} argCount The number of arguments accepted.
|
|
* "0": no arguments
|
|
* "1": exactly one argument
|
|
* "+": one or more arguments
|
|
* "*": zero or more arguments (default if unspecified)
|
|
* "?": zero or one arguments
|
|
* @param {boolean} allowUnknownOptions Whether unspecified options
|
|
* should cause an error.
|
|
* @param {number} literal The index at which any literal arg begins.
|
|
* See {@link Command#literal}.
|
|
* @param {CompletionContext} complete The relevant completion context
|
|
* when the args are being parsed for completion.
|
|
* @param {Object} extra Extra keys to be spliced into the returned
|
|
* Args object.
|
|
* @returns {Args}
|
|
*/
|
|
parseArgs: function parseArgs(str, params) {
|
|
const self = this;
|
|
|
|
function getNextArg(str, _keepQuotes) {
|
|
if (arguments.length < 2)
|
|
_keepQuotes = keepQuotes;
|
|
|
|
if (str.substr(0, 2) === "<<" && hereDoc) {
|
|
let arg = /^<<(\S*)/.exec(str)[1];
|
|
let count = arg.length + 2;
|
|
if (complete)
|
|
return [count, "", ""];
|
|
return [count, self.readHeredoc(arg), ""];
|
|
}
|
|
|
|
let [count, arg, quote] = Commands.parseArg(str, null, _keepQuotes);
|
|
if (quote == "\\" && !complete)
|
|
return [, , , _("error.trailingCharacters", "\\")];
|
|
if (quote && !complete)
|
|
return [, , , _("error.missingQuote", quote)];
|
|
return [count, arg, quote];
|
|
}
|
|
|
|
try {
|
|
|
|
var { allowUnknownOptions, argCount, complete, extra, hereDoc, literal, options, keepQuotes } = params || {};
|
|
|
|
if (!options)
|
|
options = [];
|
|
|
|
if (!argCount)
|
|
argCount = "*";
|
|
|
|
var args = params.newArgs ? params.newArgs() : [];
|
|
args.string = str; // for access to the unparsed string
|
|
|
|
// FIXME!
|
|
for (let [k, v] in Iterator(extra || []))
|
|
args[k] = v;
|
|
|
|
// FIXME: best way to specify these requirements?
|
|
var onlyArgumentsRemaining = allowUnknownOptions || options.length == 0; // after a -- has been found
|
|
var arg = null;
|
|
var i = 0;
|
|
var completeOpts;
|
|
|
|
// XXX
|
|
let matchOpts = function matchOpts(arg) {
|
|
// Push possible option matches into completions
|
|
if (complete && !onlyArgumentsRemaining)
|
|
completeOpts = options.filter(function (opt) opt.multiple || !Set.has(args, opt.names[0]));
|
|
};
|
|
let resetCompletions = function resetCompletions() {
|
|
completeOpts = null;
|
|
args.completeArg = null;
|
|
args.completeOpt = null;
|
|
args.completeFilter = null;
|
|
args.completeStart = i;
|
|
args.quote = Commands.complQuote[""];
|
|
};
|
|
if (complete) {
|
|
resetCompletions();
|
|
matchOpts("");
|
|
args.completeArg = 0;
|
|
}
|
|
|
|
let fail = function fail(error) {
|
|
if (complete)
|
|
complete.message = error;
|
|
else
|
|
util.assert(false, error);
|
|
};
|
|
|
|
outer:
|
|
while (i < str.length || complete) {
|
|
var argStart = i;
|
|
let re = /\s*/gy;
|
|
re.lastIndex = i;
|
|
i += re.exec(str)[0].length;
|
|
|
|
if (str[i] == "|") {
|
|
args.string = str.slice(0, i);
|
|
args.trailing = str.slice(i + 1);
|
|
break;
|
|
}
|
|
if (i == str.length && !complete)
|
|
break;
|
|
|
|
if (complete)
|
|
resetCompletions();
|
|
|
|
var sub = str.substr(i);
|
|
if ((!onlyArgumentsRemaining) && /^--(\s|$)/.test(sub)) {
|
|
onlyArgumentsRemaining = true;
|
|
i += 2;
|
|
continue;
|
|
}
|
|
|
|
var optname = "";
|
|
if (!onlyArgumentsRemaining) {
|
|
for (let [, opt] in Iterator(options)) {
|
|
for (let [, optname] in Iterator(opt.names)) {
|
|
if (sub.indexOf(optname) == 0) {
|
|
let count = 0;
|
|
let invalid = false;
|
|
let arg, quote, quoted;
|
|
|
|
let sep = sub[optname.length];
|
|
let argString = sub.substr(optname.length + 1);
|
|
if (sep == "=" || /\s/.test(sep) && opt.type != CommandOption.NOARG) {
|
|
[count, quoted, quote, error] = getNextArg(argString, true);
|
|
arg = Option.dequote(quoted);
|
|
util.assert(!error, error);
|
|
|
|
// if we add the argument to an option after a space, it MUST not be empty
|
|
if (sep != "=" && !quote && arg.length == 0 && !complete)
|
|
arg = null;
|
|
|
|
count++; // to compensate the "=" character
|
|
}
|
|
else if (!/\s/.test(sep) && sep != undefined) // this isn't really an option as it has trailing characters, parse it as an argument
|
|
invalid = true;
|
|
|
|
let context = null;
|
|
if (!complete && quote)
|
|
fail(_("command.invalidOptArg", optname, argString));
|
|
|
|
if (!invalid) {
|
|
if (complete && !/[\s=]/.test(sep))
|
|
matchOpts(sub);
|
|
|
|
if (complete && count > 0) {
|
|
args.completeStart += optname.length + 1;
|
|
args.completeOpt = opt;
|
|
args.completeFilter = arg;
|
|
args.quote = Commands.complQuote[quote] || Commands.complQuote[""];
|
|
}
|
|
if (!complete || arg != null) {
|
|
if (opt.type) {
|
|
let orig = arg;
|
|
arg = opt.type.parse(arg, quoted);
|
|
|
|
if (complete && isArray(arg)) {
|
|
args.completeFilter = arg[arg.length - 1] || "";
|
|
args.completeStart += orig.length - args.completeFilter.length;
|
|
}
|
|
|
|
if (arg == null || (typeof arg == "number" && isNaN(arg))) {
|
|
if (!complete || orig != "" || args.completeStart != str.length)
|
|
fail(_("command.invalidOptTypeArg", opt.type.description, optname, argString));
|
|
if (complete)
|
|
complete.highlight(args.completeStart, count - 1, "SPELLCHECK");
|
|
}
|
|
}
|
|
|
|
// we have a validator function
|
|
if (typeof opt.validator == "function") {
|
|
if (opt.validator(arg, quoted) == false && (arg || !complete)) {
|
|
fail(_("command.invalidOptArg", optname, argString));
|
|
if (complete) // Always true.
|
|
complete.highlight(args.completeStart, count - 1, "SPELLCHECK");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (arg != null || opt.type == CommandOption.NOARG) {
|
|
// option allowed multiple times
|
|
if (opt.multiple)
|
|
args[opt.names[0]] = (args[opt.names[0]] || []).concat(arg);
|
|
else
|
|
Class.replaceProperty(args, opt.names[0], opt.type == CommandOption.NOARG || arg);
|
|
|
|
args.explicitOpts[opt.names[0]] = args[opt.names[0]];
|
|
}
|
|
|
|
i += optname.length + count;
|
|
if (i == str.length)
|
|
break outer;
|
|
continue outer;
|
|
}
|
|
// if it is invalid, just fall through and try the next argument
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
matchOpts(sub);
|
|
|
|
if (complete)
|
|
if (argCount == "0" || args.length > 0 && (/[1?]/.test(argCount)))
|
|
complete.highlight(i, sub.length, "SPELLCHECK");
|
|
|
|
if (args.length === literal) {
|
|
if (complete)
|
|
args.completeArg = args.length;
|
|
|
|
let re = /(?:\s*(?=\n)|\s*)([^]*)/gy;
|
|
re.lastIndex = argStart || 0;
|
|
sub = re.exec(str)[1];
|
|
|
|
// Hack.
|
|
if (sub.substr(0, 2) === "<<" && hereDoc)
|
|
let ([count, arg] = getNextArg(sub)) {
|
|
sub = arg + sub.substr(count);
|
|
}
|
|
|
|
args.push(sub);
|
|
args.quote = null;
|
|
break;
|
|
}
|
|
|
|
// if not an option, treat this token as an argument
|
|
let [count, arg, quote, error] = getNextArg(sub);
|
|
util.assert(!error, error);
|
|
|
|
if (complete) {
|
|
args.quote = Commands.complQuote[quote] || Commands.complQuote[""];
|
|
args.completeFilter = arg || "";
|
|
}
|
|
else if (count == -1)
|
|
fail(_("command.parsing", arg));
|
|
else if (!onlyArgumentsRemaining && sub[0] === "-")
|
|
fail(_("command.invalidOpt", arg));
|
|
|
|
if (arg != null)
|
|
args.push(arg);
|
|
if (complete)
|
|
args.completeArg = args.length - 1;
|
|
|
|
i += count;
|
|
if (count <= 0 || i == str.length)
|
|
break;
|
|
}
|
|
|
|
if (complete && args.trailing == null) {
|
|
if (args.completeOpt) {
|
|
let opt = args.completeOpt;
|
|
let context = complete.fork(opt.names[0], args.completeStart);
|
|
let arg = args.explicitOpts[opt.names[0]];
|
|
context.filter = args.completeFilter;
|
|
|
|
if (isArray(arg))
|
|
context.filters.push(function (item) arg.indexOf(item.text) === -1);
|
|
|
|
if (typeof opt.completer == "function")
|
|
var compl = opt.completer(context, args);
|
|
else
|
|
compl = opt.completer || [];
|
|
|
|
context.title = [opt.names[0]];
|
|
context.quote = args.quote;
|
|
if (compl)
|
|
context.completions = compl;
|
|
}
|
|
complete.advance(args.completeStart);
|
|
complete.keys = {
|
|
text: "names",
|
|
description: function (opt) messages.get(["command", params.name, "options", opt.names[0], "description"].join("."), opt.description)
|
|
};
|
|
complete.title = ["Options"];
|
|
if (completeOpts)
|
|
complete.completions = completeOpts;
|
|
}
|
|
|
|
if (args.verify)
|
|
args.verify();
|
|
|
|
return args;
|
|
}
|
|
catch (e if complete && e instanceof FailedAssertion) {
|
|
complete.message = e;
|
|
return args;
|
|
}
|
|
},
|
|
|
|
nameRegexp: util.regexp(<![CDATA[
|
|
[^
|
|
0-9
|
|
<forbid>
|
|
]
|
|
[^ <forbid> ]*
|
|
]]>, "gx", {
|
|
forbid: util.regexp(String.replace(<![CDATA[
|
|
U0000-U002c // U002d -
|
|
U002e-U002f
|
|
U003a-U0040 // U0041-U005a a-z
|
|
U005b-U0060 // U0061-U007a A-Z
|
|
U007b-U00bf
|
|
U02b0-U02ff // Spacing Modifier Letters
|
|
U0300-U036f // Combining Diacritical Marks
|
|
U1dc0-U1dff // Combining Diacritical Marks Supplement
|
|
U2000-U206f // General Punctuation
|
|
U20a0-U20cf // Currency Symbols
|
|
U20d0-U20ff // Combining Diacritical Marks for Symbols
|
|
U2400-U243f // Control Pictures
|
|
U2440-U245f // Optical Character Recognition
|
|
U2500-U257f // Box Drawing
|
|
U2580-U259f // Block Elements
|
|
U2700-U27bf // Dingbats
|
|
Ufe20-Ufe2f // Combining Half Marks
|
|
Ufe30-Ufe4f // CJK Compatibility Forms
|
|
Ufe50-Ufe6f // Small Form Variants
|
|
Ufe70-Ufeff // Arabic Presentation Forms-B
|
|
Uff00-Uffef // Halfwidth and Fullwidth Forms
|
|
Ufff0-Uffff // Specials
|
|
]]>, /U/g, "\\u"), "x")
|
|
}),
|
|
|
|
validName: Class.Memoize(function validName() util.regexp("^" + this.nameRegexp.source + "$")),
|
|
|
|
commandRegexp: Class.Memoize(function commandRegexp() util.regexp(<![CDATA[
|
|
^
|
|
(?P<spec>
|
|
(?P<prespace> [:\s]*)
|
|
(?P<count> (?:\d+ | %)? )
|
|
(?P<fullCmd>
|
|
(?: (?P<group> <name>) : )?
|
|
(?P<cmd> (?:-? [()] | <name> | !)? ))
|
|
(?P<bang> !?)
|
|
(?P<space> \s*)
|
|
)
|
|
(?P<args>
|
|
(?:. | \n)*?
|
|
)?
|
|
$
|
|
]]>, "x", {
|
|
name: this.nameRegexp
|
|
})),
|
|
|
|
/**
|
|
* Parses a complete Ex command.
|
|
*
|
|
* The parsed string is returned as an Array like
|
|
* [count, command, bang, args]:
|
|
* count - any count specified
|
|
* command - the Ex command name
|
|
* bang - whether the special "bang" version was called
|
|
* args - the commands full argument string
|
|
* E.g. ":2foo! bar" -> [2, "foo", true, "bar"]
|
|
*
|
|
* @param {string} str The Ex command line string.
|
|
* @returns {Array}
|
|
*/
|
|
// FIXME: why does this return an Array rather than Object?
|
|
parseCommand: function parseCommand(str) {
|
|
// remove comments
|
|
str.replace(/\s*".*$/, "");
|
|
|
|
let matches = this.commandRegexp.exec(str);
|
|
if (!matches)
|
|
return [];
|
|
|
|
let { spec, count, group, cmd, bang, space, args } = matches;
|
|
if (!cmd && bang)
|
|
[cmd, bang] = [bang, cmd];
|
|
|
|
if (!cmd || args && args[0] != "|" && !(space || cmd == "!"))
|
|
return [];
|
|
|
|
// parse count
|
|
if (count)
|
|
count = count == "%" ? this.COUNT_ALL : parseInt(count, 10);
|
|
else
|
|
count = this.COUNT_NONE;
|
|
|
|
return [count, cmd, !!bang, args || "", spec.length, group];
|
|
},
|
|
|
|
parseCommands: function parseCommands(str, complete) {
|
|
const { contexts } = this.modules;
|
|
do {
|
|
let [count, cmd, bang, args, len, group] = commands.parseCommand(str);
|
|
if (!group)
|
|
var command = this.get(cmd || "");
|
|
else if (group = contexts.getGroup(group, "commands"))
|
|
command = group.get(cmd || "");
|
|
|
|
if (command == null) {
|
|
yield [null, { commandString: str }];
|
|
return;
|
|
}
|
|
|
|
if (complete) {
|
|
complete.fork(command.name);
|
|
var context = complete.fork("args", len);
|
|
}
|
|
|
|
if (!complete || /(\w|^)[!\s]/.test(str))
|
|
args = command.parseArgs(args, context, { count: count, bang: bang });
|
|
else
|
|
args = this.parseArgs(args, { extra: { count: count, bang: bang } });
|
|
args.context = this.context;
|
|
args.commandName = cmd;
|
|
args.commandString = str.substr(0, len) + args.string;
|
|
str = args.trailing;
|
|
yield [command, args];
|
|
if (args.break)
|
|
break;
|
|
}
|
|
while (str);
|
|
},
|
|
|
|
subCommands: function subCommands(command) {
|
|
let commands = [command];
|
|
while (command = commands.shift())
|
|
try {
|
|
for (let [command, args] in this.parseCommands(command)) {
|
|
if (command) {
|
|
yield [command, args];
|
|
if (command.subCommand && args[command.subCommand])
|
|
commands.push(args[command.subCommand]);
|
|
}
|
|
}
|
|
}
|
|
catch (e) {}
|
|
},
|
|
|
|
/** @property */
|
|
get complQuote() Commands.complQuote,
|
|
|
|
/** @property */
|
|
get quoteArg() Commands.quoteArg // XXX: better somewhere else?
|
|
|
|
}, {
|
|
// returns [count, parsed_argument]
|
|
parseArg: function parseArg(str, sep, keepQuotes) {
|
|
let arg = "";
|
|
let quote = null;
|
|
let len = str.length;
|
|
|
|
function fixEscapes(str) str.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4}|(.))/g, function (m, n1) n1 || m);
|
|
|
|
// Fix me.
|
|
if (isString(sep))
|
|
sep = RegExp(sep);
|
|
sep = sep != null ? sep : /\s/;
|
|
let re1 = RegExp("^" + (sep.source === "" ? "(?!)" : sep.source));
|
|
let re2 = RegExp(/^()((?:[^\\S"']|\\.)+)((?:\\$)?)/.source.replace("S", sep.source));
|
|
|
|
while (str.length && !re1.test(str)) {
|
|
let res;
|
|
if ((res = re2.exec(str)))
|
|
arg += keepQuotes ? res[0] : res[2].replace(/\\(.)/g, "$1");
|
|
else if ((res = /^(")((?:[^\\"]|\\.)*)("?)/.exec(str)))
|
|
arg += keepQuotes ? res[0] : JSON.parse(fixEscapes(res[0]) + (res[3] ? "" : '"'));
|
|
else if ((res = /^(')((?:[^']|'')*)('?)/.exec(str)))
|
|
arg += keepQuotes ? res[0] : res[2].replace("''", "'", "g");
|
|
else
|
|
break;
|
|
|
|
if (!res[3])
|
|
quote = res[1];
|
|
if (!res[1])
|
|
quote = res[3];
|
|
str = str.substr(res[0].length);
|
|
}
|
|
|
|
return [len - str.length, arg, quote];
|
|
},
|
|
|
|
quote: function quote(str) Commands.quoteArg[
|
|
/[\b\f\n\r\t]/.test(str) ? '"' :
|
|
/[\s"'\\]|^$|^-/.test(str) ? "'"
|
|
: ""](str)
|
|
}, {
|
|
completion: function initCompletion(dactyl, modules, window) {
|
|
const { completion, contexts } = modules;
|
|
|
|
completion.command = function command(context, group) {
|
|
context.title = ["Command"];
|
|
context.keys = { text: "longNames", description: "description" };
|
|
if (group)
|
|
context.generate = function () group._list;
|
|
else
|
|
context.generate = function () modules.commands.hives.map(function (h) h._list).flatten();
|
|
};
|
|
|
|
// provides completions for ex commands, including their arguments
|
|
completion.ex = function ex(context) {
|
|
const { commands } = modules;
|
|
|
|
// if there is no space between the command name and the cursor
|
|
// then get completions of the command name
|
|
for (var [command, args] in commands.parseCommands(context.filter, context))
|
|
if (args.trailing)
|
|
context.advance(args.commandString.length + 1);
|
|
if (!args)
|
|
args = { commandString: context.filter };
|
|
|
|
let match = commands.commandRegexp.exec(args.commandString);
|
|
if (!match)
|
|
return;
|
|
|
|
if (match.group)
|
|
context.advance(match.group.length + 1);
|
|
|
|
context.advance(match.prespace.length + match.count.length);
|
|
if (!(match.bang || match.space)) {
|
|
context.fork("", 0, this, "command", match.group && contexts.getGroup(match.group, "commands"));
|
|
return;
|
|
}
|
|
|
|
// dynamically get completions as specified with the command's completer function
|
|
context.highlight();
|
|
if (!command) {
|
|
context.message = _("command.noSuch", match.cmd);
|
|
context.highlight(0, match.cmd.length, "SPELLCHECK");
|
|
return;
|
|
}
|
|
|
|
let cmdContext = context.fork(command.name, match.fullCmd.length + match.bang.length + match.space.length);
|
|
try {
|
|
if (!cmdContext.waitingForTab) {
|
|
if (!args.completeOpt && command.completer && args.completeStart != null) {
|
|
cmdContext.advance(args.completeStart);
|
|
cmdContext.quote = args.quote;
|
|
cmdContext.filter = args.completeFilter;
|
|
command.completer.call(command, cmdContext, args);
|
|
}
|
|
}
|
|
}
|
|
catch (e) {
|
|
util.reportError(e);
|
|
cmdContext.message = _("error.error", e);
|
|
}
|
|
};
|
|
|
|
completion.userCommand = function userCommand(context, group) {
|
|
context.title = ["User Command", "Definition"];
|
|
context.keys = { text: "name", description: "replacementText" };
|
|
context.completions = group || modules.commands.user;
|
|
};
|
|
},
|
|
|
|
commands: function initCommands(dactyl, modules, window) {
|
|
const { commands, contexts } = modules;
|
|
|
|
commands.add(["(", "-("], "",
|
|
function (args) { dactyl.echoerr(_("dactyl.cheerUp")); },
|
|
{ hidden: true });
|
|
commands.add([")", "-)"], "",
|
|
function (args) { dactyl.echoerr(_("dactyl.somberDown")); },
|
|
{ hidden: true });
|
|
|
|
commands.add(["com[mand]"],
|
|
"List or define commands",
|
|
function (args) {
|
|
let cmd = args[0];
|
|
|
|
util.assert(!cmd || cmd.split(",").every(commands.validName.closure.test),
|
|
_("command.invalidName", cmd));
|
|
|
|
if (args.length <= 1)
|
|
commands.list(cmd, args.explicitOpts["-group"] ? [args["-group"]] : null);
|
|
else {
|
|
util.assert(args["-group"].modifiable,
|
|
_("group.cantChangeBuiltin", _("command.commands")));
|
|
|
|
let completer = args["-complete"];
|
|
let completerFunc = null; // default to no completion for user commands
|
|
|
|
if (completer) {
|
|
if (/^custom,/.test(completer)) {
|
|
completer = completer.substr(7);
|
|
|
|
if (contexts.context)
|
|
var ctxt = update({}, contexts.context || {});
|
|
completerFunc = function (context) {
|
|
var result = contexts.withSavedValues(["context"], function () {
|
|
contexts.context = ctxt;
|
|
return dactyl.userEval(completer);
|
|
});
|
|
if (callable(result))
|
|
return result.apply(this, Array.slice(arguments));
|
|
else
|
|
return context.completions = result;
|
|
};
|
|
}
|
|
else
|
|
completerFunc = function (context) modules.completion.closure[config.completers[completer]](context);
|
|
}
|
|
|
|
let added = args["-group"].add(cmd.split(","),
|
|
args["-description"],
|
|
contexts.bindMacro(args, "-ex",
|
|
function makeParams(args, modifiers) ({
|
|
args: {
|
|
__proto__: args,
|
|
toString: function () this.string,
|
|
},
|
|
bang: this.bang && args.bang ? "!" : "",
|
|
count: this.count && args.count
|
|
})),
|
|
{
|
|
argCount: args["-nargs"],
|
|
bang: args["-bang"],
|
|
count: args["-count"],
|
|
completer: completerFunc,
|
|
literal: args["-literal"],
|
|
persist: !args["-nopersist"],
|
|
replacementText: args.literalArg,
|
|
context: contexts.context && update({}, contexts.context)
|
|
}, args.bang);
|
|
|
|
if (!added)
|
|
dactyl.echoerr(_("command.exists"));
|
|
}
|
|
}, {
|
|
bang: true,
|
|
completer: function (context, args) {
|
|
const { completion } = modules;
|
|
if (args.completeArg == 0)
|
|
completion.userCommand(context, args["-group"]);
|
|
else
|
|
args["-javascript"] ? completion.javascript(context) : completion.ex(context);
|
|
},
|
|
hereDoc: true,
|
|
options: [
|
|
{ names: ["-bang", "-b"], description: "Command may be followed by a !" },
|
|
{ names: ["-count", "-c"], description: "Command may be preceded by a count" },
|
|
{
|
|
// TODO: "E180: invalid complete value: " + arg
|
|
names: ["-complete", "-C"],
|
|
description: "The argument completion function",
|
|
completer: function (context) [[k, ""] for ([k, v] in Iterator(config.completers))],
|
|
type: CommandOption.STRING,
|
|
validator: function (arg) arg in config.completers || /^custom,/.test(arg),
|
|
},
|
|
{
|
|
names: ["-description", "-desc", "-d"],
|
|
description: "A user-visible description of the command",
|
|
default: "User-defined command",
|
|
type: CommandOption.STRING
|
|
},
|
|
contexts.GroupFlag("commands"),
|
|
{
|
|
names: ["-javascript", "-js", "-j"],
|
|
description: "Execute the definition as JavaScript rather than Ex commands"
|
|
},
|
|
{
|
|
names: ["-literal", "-l"],
|
|
description: "Process the specified argument ignoring any quoting or meta characters",
|
|
type: CommandOption.INT
|
|
},
|
|
{
|
|
names: ["-nargs", "-a"],
|
|
description: "The allowed number of arguments",
|
|
completer: [["0", "No arguments are allowed (default)"],
|
|
["1", "One argument is allowed"],
|
|
["*", "Zero or more arguments are allowed"],
|
|
["?", "Zero or one argument is allowed"],
|
|
["+", "One or more arguments are allowed"]],
|
|
default: "0",
|
|
type: CommandOption.STRING,
|
|
validator: bind("test", /^[01*?+]$/)
|
|
},
|
|
{
|
|
names: ["-nopersist", "-n"],
|
|
description: "Do not save this command to an auto-generated RC file"
|
|
}
|
|
],
|
|
literal: 1,
|
|
|
|
serialize: function () array(commands.userHives)
|
|
.filter(function (h) h.persist)
|
|
.map(function (hive) [
|
|
{
|
|
command: this.name,
|
|
bang: true,
|
|
options: iter([v, typeof cmd[k] == "boolean" ? null : cmd[k]]
|
|
// FIXME: this map is expressed multiple times
|
|
for ([k, v] in Iterator({
|
|
argCount: "-nargs",
|
|
bang: "-bang",
|
|
count: "-count",
|
|
description: "-description"
|
|
}))
|
|
if (cmd[k])).toObject(),
|
|
arguments: [cmd.name],
|
|
literalArg: cmd.action,
|
|
ignoreDefaults: true
|
|
}
|
|
for (cmd in hive) if (cmd.persist)
|
|
], this)
|
|
.flatten().array
|
|
});
|
|
|
|
commands.add(["delc[ommand]"],
|
|
"Delete the specified user-defined command",
|
|
function (args) {
|
|
util.assert(args.bang ^ !!args[0], _("error.argumentOrBang"));
|
|
let name = args[0];
|
|
|
|
if (args.bang)
|
|
args["-group"].clear();
|
|
else if (args["-group"].get(name))
|
|
args["-group"].remove(name);
|
|
else
|
|
dactyl.echoerr(_("command.noSuchUser", name));
|
|
}, {
|
|
argCount: "?",
|
|
bang: true,
|
|
completer: function (context, args) modules.completion.userCommand(context, args["-group"]),
|
|
options: [contexts.GroupFlag("commands")]
|
|
});
|
|
|
|
commands.add(["comp[letions]"],
|
|
"List the completion results for a given command substring",
|
|
function (args) { modules.completion.listCompleter("ex", args[0]); },
|
|
{
|
|
argCount: "1",
|
|
completer: function (context, args) modules.completion.ex(context),
|
|
literal: 0
|
|
});
|
|
|
|
dactyl.addUsageCommand({
|
|
name: ["listc[ommands]", "lc"],
|
|
description: "List all Ex commands along with their short descriptions",
|
|
index: "ex-cmd",
|
|
iterate: function (args) commands.iterator().map(function (cmd) ({
|
|
__proto__: cmd,
|
|
columns: [
|
|
cmd.hive == commands.builtin ? "" : <span highlight="Object" style="padding-right: 1em;">{cmd.hive.name}</span>
|
|
]
|
|
})),
|
|
iterateIndex: function (args) let (tags = help.tags)
|
|
this.iterate(args).filter(function (cmd) cmd.hive === commands.builtin || Set.has(tags, cmd.helpTag)),
|
|
format: {
|
|
headings: ["Command", "Group", "Description"],
|
|
description: function (cmd) template.linkifyHelp(cmd.description + (cmd.replacementText ? ": " + cmd.action : "")),
|
|
help: function (cmd) ":" + cmd.name
|
|
}
|
|
});
|
|
|
|
commands.add(["y[ank]"],
|
|
"Yank the output of the given command to the clipboard",
|
|
function (args) {
|
|
let cmd = /^:/.test(args[0]) ? args[0] : ":echo " + args[0];
|
|
|
|
let res = modules.commandline.withOutputToString(commands.execute, commands, cmd);
|
|
|
|
dactyl.clipboardWrite(res);
|
|
|
|
let lines = res.split("\n").length;
|
|
dactyl.echomsg(_("command.yank.yankedLine" + (lines == 1 ? "" : "s"), lines));
|
|
},
|
|
{
|
|
argCount: "1",
|
|
completer: function (context) modules.completion[/^:/.test(context.filter) ? "ex" : "javascript"](context),
|
|
literal: 0
|
|
});
|
|
},
|
|
javascript: function initJavascript(dactyl, modules, window) {
|
|
const { JavaScript, commands } = modules;
|
|
|
|
JavaScript.setCompleter([commands.user.get, commands.user.remove],
|
|
[function () [[c.names, c.description] for (c in this)]]);
|
|
JavaScript.setCompleter([commands.get],
|
|
[function () [[c.names, c.description] for (c in this.iterator())]]);
|
|
},
|
|
mappings: function initMappings(dactyl, modules, window) {
|
|
const { commands, mappings, modes } = modules;
|
|
|
|
mappings.add([modes.COMMAND],
|
|
["@:"], "Repeat the last Ex command",
|
|
function (args) {
|
|
if (commands.repeat) {
|
|
for (let i in util.interruptibleRange(0, Math.max(args.count, 1), 100))
|
|
dactyl.execute(commands.repeat);
|
|
}
|
|
else
|
|
dactyl.echoerr(_("command.noPrevious"));
|
|
},
|
|
{ count: true });
|
|
}
|
|
});
|
|
|
|
(function () {
|
|
|
|
Commands.quoteMap = {
|
|
"\n": "\\n",
|
|
"\t": "\\t",
|
|
};
|
|
function quote(q, list, map) {
|
|
map = map || Commands.quoteMap;
|
|
let re = RegExp("[" + list + "]", "g");
|
|
function quote(str) q + String.replace(str, re, function ($0) $0 in map ? map[$0] : ("\\" + $0)) + q;
|
|
quote.list = list;
|
|
return quote;
|
|
};
|
|
|
|
Commands.quoteArg = {
|
|
'"': quote('"', '\n\t"\\\\'),
|
|
"'": quote("'", "'", { "'": "''" }),
|
|
"": quote("", "|\\\\\\s'\"")
|
|
};
|
|
Commands.complQuote = {
|
|
'"': ['"', quote("", Commands.quoteArg['"'].list), '"'],
|
|
"'": ["'", quote("", Commands.quoteArg["'"].list), "'"],
|
|
"": ["", Commands.quoteArg[""], ""]
|
|
};
|
|
|
|
Commands.parseBool = function (arg) {
|
|
if (/^(true|1|on)$/i.test(arg))
|
|
return true;
|
|
if (/^(false|0|off)$/i.test(arg))
|
|
return false;
|
|
return NaN;
|
|
};
|
|
})();
|
|
|
|
endModule();
|
|
|
|
} catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
|
|
|
|
// vim: set fdm=marker sw=4 ts=4 et ft=javascript:
|