mirror of
https://github.com/gryf/pentadactyl-pm.git
synced 2025-12-20 16:17:59 +01:00
* Standard module format. All modules are explicitly declared as modules, they're created via a constructor and instantiated automatically. They're dependency aware. They stringify properly. * Classes are declared the same way (rather like Structs already were). They also stringify properly. Plus, each instance has a rather nifty closure member that closes all of its methods around 'this', so you can pass them to map, forEach, setTimeout, etc. Modules are themselves classes, with a special metaclass, as it were. * Doug Crockford is dead, metaphorically speaking. Closure-based classes just don't fit into any of the common JavaScript frameworks, and they're inefficient and confusing. Now, all class and module members are accessed explicitly via 'this', which makes it very clear that they're class members and not (e.g.) local variables, without anything nasty like Hungarian notation. * Strictly one module per file. Classes that belong to a module live in the same file. * For the moment, there are quite a few utility functions sitting in base.c, because my class implementation used them, and I haven't had the time or inclination to sort them out. I plan to reconcile them with the current mess that is the util namespace. * Changed bracing style.
1145 lines
45 KiB
JavaScript
1145 lines
45 KiB
JavaScript
// Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
|
|
//
|
|
// This work is licensed for reuse under an MIT license. Details are
|
|
// given in the LICENSE.txt file included with this file.
|
|
|
|
|
|
/** @scope modules */
|
|
|
|
// Do NOT create instances of this class yourself, use the helper method
|
|
// commands.add() instead
|
|
|
|
/**
|
|
* 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}
|
|
* heredoc - see {@link Command#heredoc}
|
|
* literal - see {@link Command#literal}
|
|
* options - see {@link Command#options}
|
|
* serial - see {@link Command#serial}
|
|
* privateData - see {@link Command#privateData}
|
|
* @optional
|
|
* @private
|
|
*/
|
|
const Command = Class("Command", {
|
|
init: function (specs, description, action, extraInfo) {
|
|
specs = Array.concat(specs);
|
|
|
|
if (!extraInfo)
|
|
extraInfo = {};
|
|
|
|
let expandedSpecs = Command.parseSpecs(specs);
|
|
/**
|
|
* @property {string[]} All of this command's name specs. e.g., "com[mand]"
|
|
*/
|
|
this.specs = specs;
|
|
/** @property {string[]} All of this command's short names, e.g., "com" */
|
|
this.shortNames = expandedSpecs.shortNames;
|
|
/**
|
|
* @property {string[]} All of this command's long names, e.g., "command"
|
|
*/
|
|
this.longNames = expandedSpecs.longNames;
|
|
|
|
/** @property {string} The command's canonical name. */
|
|
this.name = this.longNames[0];
|
|
/** @property {string[]} All of this command's long and short names. */
|
|
this.names = expandedSpecs.names; // return all command name aliases
|
|
|
|
/** @property {string} This command's description, as shown in :exusage */
|
|
this.description = description || "";
|
|
/**
|
|
* @property {function (Args)} The function called to execute this command.
|
|
*/
|
|
this.action = action;
|
|
/**
|
|
* @property {string} This command's argument count spec.
|
|
* @see Commands#parseArguments
|
|
*/
|
|
this.argCount = extraInfo.argCount || 0;
|
|
/**
|
|
* @property {function (CompletionContext, Args)} This command's completer.
|
|
* @see CompletionContext
|
|
*/
|
|
this.completer = extraInfo.completer || null;
|
|
/** @property {boolean} Whether this command accepts a here document. */
|
|
this.hereDoc = extraInfo.hereDoc || false;
|
|
/**
|
|
* @property {Array} The options this command takes.
|
|
* @see Commands@parseArguments
|
|
*/
|
|
this.options = extraInfo.options || [];
|
|
/**
|
|
* @property {boolean} Whether this command may be called with a bang,
|
|
* e.g., :com!
|
|
*/
|
|
this.bang = extraInfo.bang || false;
|
|
/**
|
|
* @property {boolean} Whether this command may be called with a count,
|
|
* e.g., :12bdel
|
|
*/
|
|
this.count = extraInfo.count || false;
|
|
/**
|
|
* @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.
|
|
*/
|
|
this.literal = extraInfo.literal == null ? null : extraInfo.literal;
|
|
/**
|
|
* @property {function} Should return an array of <b>Object</b>s suitable
|
|
* to be passed to {@link Commands#commandToString}, one for each past
|
|
* invocation which should be restored on subsequent @liberator
|
|
* startups.
|
|
*/
|
|
this.serial = extraInfo.serial;
|
|
/**
|
|
* @property {boolean} When true, invocations of this command
|
|
* may contain private data which should be purged from
|
|
* saved histories when clearing private data.
|
|
*/
|
|
this.privateData = Boolean(extraInfo.privateData);
|
|
|
|
/**
|
|
* @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.
|
|
*/
|
|
this.user = extraInfo.user || false;
|
|
/**
|
|
* @property {string} For commands defined via :command, contains the Ex
|
|
* command line to be executed upon invocation.
|
|
*/
|
|
this.replacementText = extraInfo.replacementText || null;
|
|
},
|
|
/**
|
|
* Execute this command.
|
|
*
|
|
* @param {string} args The args to be parsed and passed to
|
|
* {@link #action}.
|
|
* @param {boolean} bang Whether this command was executed with a trailing
|
|
* bang.
|
|
* @deprecated
|
|
* @param {number} count Whether this command was executed with a leading
|
|
* count.
|
|
* @deprecated
|
|
* @param {Object} modifiers Any modifiers to be passed to {@link #action}.
|
|
*/
|
|
execute: function (args, bang, count, modifiers) {
|
|
// XXX
|
|
bang = !!bang;
|
|
count = (count === undefined) ? -1 : count;
|
|
modifiers = modifiers || {};
|
|
|
|
let self = this;
|
|
function exec(args) {
|
|
// FIXME: Move to parseCommand?
|
|
args = self.parseArgs(args);
|
|
if (!args)
|
|
return;
|
|
args.count = count;
|
|
args.bang = bang;
|
|
liberator.trapErrors(self.action, self, args, modifiers)
|
|
}
|
|
|
|
if (this.hereDoc) {
|
|
let matches = args.match(/(.*)<<\s*(\S+)$/);
|
|
if (matches && matches[2]) {
|
|
commandline.inputMultiline(RegExp("^" + matches[2] + "$", "m"),
|
|
function (args) { exec(matches[1] + "\n" + args); });
|
|
return;
|
|
}
|
|
}
|
|
|
|
exec(args);
|
|
},
|
|
|
|
/**
|
|
* Returns whether this command may be invoked via <b>name</b>.
|
|
*
|
|
* @param {string} name The candidate name.
|
|
* @returns {boolean}
|
|
*/
|
|
hasName: function (name) {
|
|
for (let [, spec] in Iterator(this.specs)) {
|
|
let fullName = spec.replace(/\[(\w+)]$/, "$1");
|
|
let index = spec.indexOf("[");
|
|
let min = index == -1 ? fullName.length : index;
|
|
|
|
if (fullName.indexOf(name) == 0 && name.length >= min)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* 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 (args, complete, extra) commands.parseArgs(args, this.options, this.argCount, false, this.literal, complete, extra)
|
|
|
|
}, {
|
|
|
|
/**
|
|
* convert command name abbreviation specs of the form
|
|
* 'shortname[optional-tail]' to short and long versions Eg. 'abc[def]' ->
|
|
* 'abc', 'abcdef'
|
|
*/
|
|
parseSpecs: function (specs) {
|
|
// Whoever wrote the following should be ashamed. :(
|
|
// Good grief! I have no words... -- djk ;-)
|
|
// let shortNames = longNames = names = [];
|
|
let names = [];
|
|
let longNames = [];
|
|
let shortNames = [];
|
|
|
|
for (let [, spec] in Iterator(specs)) {
|
|
let matches = spec.match(/(\w+)\[(\w+)\](\w*)/);
|
|
|
|
if (matches) {
|
|
shortNames.push(matches[1] + matches[3]);
|
|
longNames.push(matches[1] + matches[2] + matches[3]);
|
|
// order as long1, short1, long2, short2
|
|
names.push(matches[1] + matches[2]);
|
|
names.push(matches[1]);
|
|
}
|
|
else {
|
|
longNames.push(spec);
|
|
names.push(spec);
|
|
}
|
|
}
|
|
|
|
return { names: names, longNames: longNames, shortNames: shortNames };
|
|
},
|
|
});
|
|
|
|
/**
|
|
* @instance commands
|
|
*/
|
|
const ArgType = new Struct("description", "parse");
|
|
const Commands = Module("commands", {
|
|
init: function () {
|
|
this._exCommands = [];
|
|
},
|
|
|
|
// FIXME: remove later, when our option handler is better
|
|
/**
|
|
* @property {number} The option argument is unspecified. Any argument
|
|
* is accepted and caller is responsible for parsing the return
|
|
* value.
|
|
* @final
|
|
*/
|
|
OPTION_ANY: 0,
|
|
|
|
/**
|
|
* @property {number} The option doesn't accept an argument.
|
|
* @final
|
|
*/
|
|
OPTION_NOARG: 1,
|
|
/**
|
|
* @property {number} The option accepts a boolean argument.
|
|
* @final
|
|
*/
|
|
OPTION_BOOL: 2,
|
|
/**
|
|
* @property {number} The option accepts a string argument.
|
|
* @final
|
|
*/
|
|
OPTION_STRING: 3,
|
|
/**
|
|
* @property {number} The option accepts an integer argument.
|
|
* @final
|
|
*/
|
|
OPTION_INT: 4,
|
|
/**
|
|
* @property {number} The option accepts a float argument.
|
|
* @final
|
|
*/
|
|
OPTION_FLOAT: 5,
|
|
/**
|
|
* @property {number} The option accepts a string list argument.
|
|
* E.g. "foo,bar"
|
|
* @final
|
|
*/
|
|
OPTION_LIST: 6,
|
|
|
|
/**
|
|
* @property {number} Indicates that no count was specified for this
|
|
* command invocation.
|
|
* @final
|
|
*/
|
|
COUNT_NONE: -1,
|
|
/**
|
|
* @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 () {
|
|
let sorted = this._exCommands.sort(function (a, b) a.name > b.name);
|
|
return util.Array.itervalues(sorted);
|
|
},
|
|
|
|
/** @property {string} The last executed Ex command line. */
|
|
repeat: null,
|
|
|
|
_addCommand: function (command, replace) {
|
|
if (this._exCommands.some(function (c) c.hasName(command.name))) {
|
|
if (command.user && replace)
|
|
commands.removeUserCommand(command.name);
|
|
else {
|
|
liberator.log("Warning: :" + command.name + " already exists, NOT replacing existing command.", 1);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
this._exCommands.push(command);
|
|
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Adds a new default command.
|
|
*
|
|
* @param {string[]} names 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 (names, description, action, extra) {
|
|
return this._addCommand(new Command(names, description, action, extra), false);
|
|
},
|
|
|
|
/**
|
|
* Adds a new user-defined command.
|
|
*
|
|
* @param {string[]} names 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.
|
|
* @param {boolean} replace Overwrite an existing command with the same
|
|
* canonical name.
|
|
*/
|
|
addUserCommand: function (names, description, action, extra, replace) {
|
|
extra = extra || {};
|
|
extra.user = true;
|
|
description = description || "User defined command";
|
|
|
|
return this._addCommand(new Command(names, description, action, extra), replace);
|
|
},
|
|
|
|
/**
|
|
* Returns the specified command invocation object serialized to
|
|
* an executable Ex command string.
|
|
*
|
|
* @param {Object} args The command invocation object.
|
|
* @returns {string}
|
|
*/
|
|
commandToString: function (args) {
|
|
let res = [args.command + (args.bang ? "!" : "")];
|
|
function quote(str) Commands.quoteArg[/[\s"'\\]|^$/.test(str) ? '"' : ""](str);
|
|
|
|
for (let [opt, val] in Iterator(args.options || {})) {
|
|
let char = /^-.$/.test(opt) ? " " : "=";
|
|
if (val != null)
|
|
opt += char + quote(val)
|
|
res.push(opt);
|
|
}
|
|
for (let [, arg] in Iterator(args.arguments || []))
|
|
res.push(quote(arg));
|
|
|
|
let str = args.literalArg;
|
|
if (str)
|
|
res.push(/\n/.test(str) ? "<<EOF\n" + str.replace(/\n$/, "") + "\nEOF" : str);
|
|
return res.join(" ");
|
|
},
|
|
|
|
/**
|
|
* Returns the command with matching <b>name</b>.
|
|
*
|
|
* @param {string} name The name of the command to return. This can be
|
|
* any of the command's names.
|
|
* @returns {Command}
|
|
*/
|
|
get: function (name) {
|
|
return this._exCommands.filter(function (cmd) cmd.hasName(name))[0] || null;
|
|
},
|
|
|
|
/**
|
|
* Returns the user-defined command with matching <b>name</b>.
|
|
*
|
|
* @param {string} name The name of the command to return. This can be
|
|
* any of the command's names.
|
|
* @returns {Command}
|
|
*/
|
|
getUserCommand: function (name) {
|
|
return this._exCommands.filter(function (cmd) cmd.user && cmd.hasName(name))[0] || null;
|
|
},
|
|
|
|
/**
|
|
* Returns all user-defined commands.
|
|
*
|
|
* @returns {Command[]}
|
|
*/
|
|
getUserCommands: function () {
|
|
return this._exCommands.filter(function (cmd) cmd.user);
|
|
},
|
|
|
|
// 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 <b>str</b> for options and plain arguments.
|
|
*
|
|
* The returned <b>Args</b> object is an augmented array of arguments.
|
|
* Any key/value pairs of <b>extra</b> will be available and the
|
|
* following additional properties:
|
|
* -opt - the value of the option -opt if specified
|
|
* string - the original argument string <b>str</b>
|
|
* 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 {Array} options The options accepted. These are specified as
|
|
* an array [names, type, validator, completions, multiple].
|
|
* names - an array of option names. The first name is the
|
|
* canonical option name.
|
|
* type - the option's value type. This is one of:
|
|
* (@link Commands#OPTION_NOARG),
|
|
* (@link Commands#OPTION_STRING),
|
|
* (@link Commands#OPTION_BOOL),
|
|
* (@link Commands#OPTION_INT),
|
|
* (@link Commands#OPTION_FLOAT),
|
|
* (@link Commands#OPTION_LIST),
|
|
* (@link Commands#OPTION_ANY)
|
|
* validator - a validator function
|
|
* completer - a list of completions, or a completion function
|
|
* multiple - whether this option can be specified multiple times
|
|
* E.g.
|
|
* options = [[["-force"], OPTION_NOARG],
|
|
* [["-fullscreen", "-f"], OPTION_BOOL],
|
|
* [["-language"], OPTION_STRING, validateFunc, ["perl", "ruby"]],
|
|
* [["-speed"], OPTION_INT],
|
|
* [["-acceleration"], OPTION_FLOAT],
|
|
* [["-accessories"], OPTION_LIST, null, ["foo", "bar"], true],
|
|
* [["-other"], OPTION_ANY]];
|
|
* @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 (str, options, argCount, allowUnknownOptions, literal, complete, extra) {
|
|
function getNextArg(str) {
|
|
let [count, arg, quote] = Commands.parseArg(str);
|
|
if (quote == "\\" && !complete)
|
|
return [,,,"Trailing \\"];
|
|
if (quote && !complete)
|
|
return [,,,"E114: Missing quote: " + quote];
|
|
return [count, arg, quote];
|
|
}
|
|
|
|
if (!options)
|
|
options = [];
|
|
|
|
if (!argCount)
|
|
argCount = "*";
|
|
|
|
var args = []; // parsed options
|
|
args.__iterator__ = function () util.Array.iteritems(this);
|
|
args.string = str; // for access to the unparsed string
|
|
args.literalArg = "";
|
|
|
|
// FIXME!
|
|
for (let [k, v] in Iterator(extra || []))
|
|
args[k] = v;
|
|
|
|
var invalid = false;
|
|
// FIXME: best way to specify these requirements?
|
|
var onlyArgumentsRemaining = allowUnknownOptions || options.length == 0 || false; // after a -- has been found
|
|
var arg = null;
|
|
var count = 0; // the length of the argument
|
|
var i = 0;
|
|
var completeOpts;
|
|
|
|
// XXX
|
|
function matchOpts(arg) {
|
|
// Push possible option matches into completions
|
|
if (complete && !onlyArgumentsRemaining)
|
|
completeOpts = [[opt[0], opt[0][0]] for ([i, opt] in Iterator(options)) if (!(opt[0][0] in args))];
|
|
}
|
|
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;
|
|
}
|
|
|
|
function echoerr(error) {
|
|
if (complete)
|
|
complete.message = error;
|
|
else
|
|
liberator.echoerr(error);
|
|
}
|
|
|
|
outer:
|
|
while (i < str.length || complete) {
|
|
// skip whitespace
|
|
while (/\s/.test(str[i]) && i < str.length)
|
|
i++;
|
|
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[0])) {
|
|
if (sub.indexOf(optname) == 0) {
|
|
invalid = false;
|
|
arg = null;
|
|
quote = null;
|
|
count = 0;
|
|
let sep = sub[optname.length];
|
|
if (sep == "=" || /\s/.test(sep) && opt[1] != this.OPTION_NOARG) {
|
|
[count, arg, quote, error] = getNextArg(sub.substr(optname.length + 1));
|
|
liberator.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)
|
|
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) {
|
|
liberator.echoerr("Invalid argument for option " + optname);
|
|
return null;
|
|
}
|
|
|
|
if (!invalid) {
|
|
if (complete && count > 0) {
|
|
args.completeStart += optname.length + 1;
|
|
args.completeOpt = opt;
|
|
args.completeFilter = arg;
|
|
args.quote = Commands.complQuote[quote] || Commands.complQuote[""];
|
|
}
|
|
let type = Commands.argTypes[opt[1]];
|
|
if (type && (!complete || arg != null)) {
|
|
let orig = arg;
|
|
arg = type.parse(arg);
|
|
if (arg == null || (typeof arg == "number" && isNaN(arg))) {
|
|
if (!complete || orig != "" || args.completeStart != str.length)
|
|
echoerr("Invalid argument for " + type.description + " option: " + optname);
|
|
if (complete)
|
|
complete.highlight(args.completeStart, count - 1, "SPELLCHECK");
|
|
else
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// we have a validator function
|
|
if (typeof opt[2] == "function") {
|
|
if (opt[2].call(this, arg) == false) {
|
|
echoerr("Invalid argument for option: " + optname);
|
|
if (complete)
|
|
complete.highlight(args.completeStart, count - 1, "SPELLCHECK");
|
|
else
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// option allowed multiple times
|
|
if (!!opt[4])
|
|
args[opt[0][0]] = (args[opt[0][0]] || []).concat(arg);
|
|
else
|
|
args[opt[0][0]] = opt[1] == this.OPTION_NOARG || arg;
|
|
|
|
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;
|
|
args.literalArg = sub;
|
|
args.push(sub);
|
|
args.quote = null;
|
|
break;
|
|
}
|
|
|
|
// if not an option, treat this token as an argument
|
|
let [count, arg, quote, error] = getNextArg(sub);
|
|
liberator.assert(!error, error);
|
|
|
|
if (complete) {
|
|
args.quote = Commands.complQuote[quote] || Commands.complQuote[""];
|
|
args.completeFilter = arg || "";
|
|
}
|
|
else if (count == -1) {
|
|
liberator.echoerr("Error parsing arguments: " + arg);
|
|
return null;
|
|
}
|
|
else if (!onlyArgumentsRemaining && /^-/.test(arg)) {
|
|
liberator.echoerr("Invalid option: " + arg);
|
|
return null;
|
|
}
|
|
|
|
if (arg != null)
|
|
args.push(arg);
|
|
if (complete)
|
|
args.completeArg = args.length - 1;
|
|
|
|
i += count;
|
|
if (count <= 0 || i == str.length)
|
|
break;
|
|
}
|
|
|
|
if (complete) {
|
|
if (args.completeOpt) {
|
|
let opt = args.completeOpt;
|
|
let context = complete.fork(opt[0][0], args.completeStart);
|
|
context.filter = args.completeFilter;
|
|
if (typeof opt[3] == "function")
|
|
var compl = opt[3](context, args);
|
|
else
|
|
compl = opt[3] || [];
|
|
context.title = [opt[0][0]];
|
|
context.quote = args.quote;
|
|
context.completions = compl;
|
|
}
|
|
complete.advance(args.completeStart);
|
|
complete.title = ["Options"];
|
|
if (completeOpts)
|
|
complete.completions = completeOpts;
|
|
}
|
|
|
|
// check for correct number of arguments
|
|
if (args.length == 0 && /^[1+]$/.test(argCount) ||
|
|
literal != null && /[1+]/.test(argCount) && !/\S/.test(args.literalArg || "")) {
|
|
if (!complete) {
|
|
liberator.echoerr("E471: Argument required");
|
|
return null;
|
|
}
|
|
}
|
|
else if (args.length == 1 && (argCount == "0") ||
|
|
args.length > 1 && /^[01?]$/.test(argCount)) {
|
|
echoerr("E488: Trailing characters");
|
|
return null;
|
|
}
|
|
|
|
return args;
|
|
},
|
|
|
|
/**
|
|
* 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 (str) {
|
|
// remove comments
|
|
str.replace(/\s*".*$/, "");
|
|
|
|
// 0 - count, 1 - cmd, 2 - special, 3 - args
|
|
let matches = str.match(/^[:\s]*(\d+|%)?([a-zA-Z]+|!)(!)?(?:\s*(.*?))?$/);
|
|
//var matches = str.match(/^:*(\d+|%)?([a-zA-Z]+|!)(!)?(?:\s*(.*?)\s*)?$/);
|
|
if (!matches)
|
|
return [null, null, null, null];
|
|
|
|
let [, count, cmd, special, args] = matches;
|
|
|
|
// parse count
|
|
if (count)
|
|
count = count == "%" ? this.COUNT_ALL: parseInt(count, 10);
|
|
else
|
|
count = this.COUNT_NONE;
|
|
|
|
return [count, cmd, !!special, args || ""];
|
|
},
|
|
|
|
/** @property */
|
|
get complQuote() Commands.complQuote,
|
|
|
|
/** @property */
|
|
get quoteArg() Commands.quoteArg, // XXX: better somewhere else?
|
|
|
|
/**
|
|
* Remove the user-defined command with matching <b>name</b>.
|
|
*
|
|
* @param {string} name The name of the command to remove. This can be
|
|
* any of the command's names.
|
|
*/
|
|
removeUserCommand: function (name) {
|
|
this._exCommands = this._exCommands.filter(function (cmd) !(cmd.user && cmd.hasName(name)));
|
|
},
|
|
|
|
// FIXME: still belong here? Also used for autocommand parameters.
|
|
/**
|
|
* Returns a string with all tokens in <b>string</b> matching "<key>"
|
|
* replaced with "value". Where "key" is a property of the specified
|
|
* <b>tokens</b> object and "value" is the corresponding value. The
|
|
* <lt> token can be used to include a literal "<" in the returned
|
|
* string. Any tokens prefixed with "q-" will be quoted except for
|
|
* <q-lt> which is treated like <lt>.
|
|
*
|
|
* @param {string} str The string with tokens to replace.
|
|
* @param {Object} tokens A map object whose keys are replaced with its
|
|
* values.
|
|
* @returns {string}
|
|
*/
|
|
replaceTokens: function replaceTokens(str, tokens) {
|
|
return str.replace(/<((?:q-)?)([a-zA-Z]+)?>/g, function (match, quote, token) {
|
|
if (token == "lt") // Don't quote, as in Vim (but, why so in Vim? You'd think people wouldn't say <q-lt> if they didn't want it)
|
|
return "<";
|
|
let res = tokens[token];
|
|
if (res == undefined) // Ignore anything undefined
|
|
res = "<" + token + ">";
|
|
if (quote && typeof res != "number")
|
|
return Commands.quoteArg['"'](res);
|
|
return res;
|
|
});
|
|
}
|
|
}, {
|
|
QUOTE_STYLE: "rc-ish",
|
|
|
|
// returns [count, parsed_argument]
|
|
parseArg: function (str) {
|
|
let arg = "";
|
|
let quote = null;
|
|
let len = str.length;
|
|
|
|
while (str.length && !/^\s/.test(str)) {
|
|
let res;
|
|
|
|
switch (Commands.QUOTE_STYLE) {
|
|
case "vim-sucks":
|
|
if (res = str.match = str.match(/^()((?:[^\\\s]|\\.)+)((?:\\$)?)/))
|
|
arg += res[2].replace(/\\(.)/g, "$1");
|
|
break;
|
|
|
|
case "vimperator":
|
|
if (res = str.match(/^()((?:[^\\\s"']|\\.)+)((?:\\$)?)/))
|
|
arg += res[2].replace(/\\(.)/g, "$1");
|
|
else if (res = str.match(/^(")((?:[^\\"]|\\.)*)("?)/))
|
|
arg += eval(res[0] + (res[3] ? "" : '"'));
|
|
else if (res = str.match(/^(')((?:[^\\']|\\.)*)('?)/))
|
|
arg += res[2].replace(/\\(.)/g, function (n0, n1) /[\\']/.test(n1) ? n1 : n0);
|
|
break;
|
|
|
|
case "rc-ish":
|
|
if (res = str.match = str.match(/^()((?:[^\\\s"']|\\.)+)((?:\\$)?)/))
|
|
arg += res[2].replace(/\\(.)/g, "$1");
|
|
else if (res = str.match(/^(")((?:[^\\"]|\\.)*)("?)/))
|
|
arg += eval(res[0] + (res[3] ? "" : '"'));
|
|
else if (res = str.match(/^(')((?:[^']|'')*)('?)/))
|
|
arg += res[2].replace("''", "'", "g");
|
|
break;
|
|
|
|
case "pythonesque":
|
|
if (res = str.match = str.match(/^()((?:[^\\\s"']|\\.)+)((?:\\$)?)/))
|
|
arg += res[2].replace(/\\(.)/g, "$1");
|
|
else if (res = str.match(/^(""")((?:.?.?[^"])*)((?:""")?)/))
|
|
arg += res[2];
|
|
else if (res = str.match(/^(")((?:[^\\"]|\\.)*)("?)/))
|
|
arg += eval(res[0] + (res[3] ? "" : '"'));
|
|
else if (res = str.match(/^(')((?:[^\\']|\\.)*)('?)/))
|
|
arg += res[2].replace(/\\(.)/g, function (n0, n1) /[\\']/.test(n1) ? n1 : n0);
|
|
break;
|
|
}
|
|
|
|
if (!res)
|
|
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];
|
|
},
|
|
}, {
|
|
mappings: function () {
|
|
mappings.add(config.browserModes,
|
|
["@:"], "Repeat the last Ex command",
|
|
function (count) {
|
|
if (commands.repeat) {
|
|
for (let i in util.interruptibleRange(0, Math.max(count, 1), 100))
|
|
liberator.execute(commands.repeat);
|
|
}
|
|
else
|
|
liberator.echoerr("E30: No previous command line");
|
|
},
|
|
{ count: true });
|
|
},
|
|
|
|
completion: function () {
|
|
completion.setFunctionCompleter(commands.get, [function () ([c.name, c.description] for (c in commands))]);
|
|
|
|
completion.command = function command(context) {
|
|
context.title = ["Command"];
|
|
context.keys = { text: "longNames", description: "description" };
|
|
context.completions = [k for (k in commands)];
|
|
};
|
|
|
|
// provides completions for ex commands, including their arguments
|
|
completion.ex = function ex(context) {
|
|
// if there is no space between the command name and the cursor
|
|
// then get completions of the command name
|
|
let [count, cmd, bang, args] = commands.parseCommand(context.filter);
|
|
let [, prefix, junk] = context.filter.match(/^(:*\d*)\w*(.?)/) || [];
|
|
context.advance(prefix.length);
|
|
if (!junk)
|
|
return void context.fork("", 0, this, "command");
|
|
|
|
// dynamically get completions as specified with the command's completer function
|
|
let command = commands.get(cmd);
|
|
if (!command) {
|
|
context.highlight(0, cmd.length, "SPELLCHECK");
|
|
return;
|
|
}
|
|
|
|
[prefix] = context.filter.match(/^(?:\w*[\s!]|!)\s*/);
|
|
let cmdContext = context.fork(cmd, prefix.length);
|
|
let argContext = context.fork("args", prefix.length);
|
|
args = command.parseArgs(cmdContext.filter, argContext, { count: count, bang: bang });
|
|
if (args) {
|
|
// FIXME: Move to parseCommand
|
|
args.count = count;
|
|
args.bang = bang;
|
|
if (!args.completeOpt && command.completer) {
|
|
cmdContext.advance(args.completeStart);
|
|
cmdContext.quote = args.quote;
|
|
cmdContext.filter = args.completeFilter;
|
|
try {
|
|
let compObject = command.completer.call(command, cmdContext, args);
|
|
if (compObject instanceof Array) // for now at least, let completion functions return arrays instead of objects
|
|
compObject = { start: compObject[0], items: compObject[1] };
|
|
if (compObject != null) {
|
|
cmdContext.advance(compObject.start);
|
|
cmdContext.filterFunc = null;
|
|
cmdContext.completions = compObject.items;
|
|
}
|
|
}
|
|
catch (e) {
|
|
liberator.reportError(e);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
completion.userCommand = function userCommand(context) {
|
|
context.title = ["User Command", "Definition"];
|
|
context.keys = { text: "name", description: "replacementText" };
|
|
context.completions = commands.getUserCommands();
|
|
};
|
|
},
|
|
|
|
commands: function () {
|
|
function userCommand(args, modifiers) {
|
|
let tokens = {
|
|
args: this.argCount && args.string,
|
|
bang: this.bang && args.bang ? "!" : "",
|
|
count: this.count && args.count
|
|
};
|
|
|
|
liberator.execute(commands.replaceTokens(this.replacementText, tokens));
|
|
}
|
|
|
|
// TODO: offer completion.ex?
|
|
// : make this config specific
|
|
var completeOptionMap = {
|
|
abbreviation: "abbreviation", altstyle: "alternateStyleSheet",
|
|
bookmark: "bookmark", buffer: "buffer", color: "colorScheme",
|
|
command: "command", dialog: "dialog", dir: "directory",
|
|
environment: "environment", event: "autocmdEvent", file: "file",
|
|
help: "help", highlight: "highlightGroup", javascript: "javascript",
|
|
macro: "macro", mapping: "userMapping", menu: "menuItem",
|
|
option: "option", preference: "preference", search: "search",
|
|
shellcmd: "shellCommand", sidebar: "sidebar", url: "url",
|
|
usercommand: "userCommand"
|
|
};
|
|
|
|
// TODO: Vim allows commands to be defined without {rep} if there are {attr}s
|
|
// specified - useful?
|
|
commands.add(["com[mand]"],
|
|
"List and define commands",
|
|
function (args) {
|
|
let cmd = args[0];
|
|
|
|
liberator.assert(!/\W/.test(cmd || ''), "E182: Invalid command name");
|
|
|
|
if (args.literalArg) {
|
|
let nargsOpt = args["-nargs"] || "0";
|
|
let bangOpt = "-bang" in args;
|
|
let countOpt = "-count" in args;
|
|
let descriptionOpt = args["-description"] || "User-defined command";
|
|
let completeOpt = args["-complete"];
|
|
|
|
let completeFunc = null; // default to no completion for user commands
|
|
|
|
if (completeOpt) {
|
|
if (/^custom,/.test(completeOpt)) {
|
|
completeOpt = completeOpt.substr(7);
|
|
completeFunc = function () {
|
|
try {
|
|
var completer = liberator.eval(completeOpt);
|
|
|
|
if (!(completer instanceof Function))
|
|
throw new TypeError("User-defined custom completer '" + completeOpt + "' is not a function");
|
|
}
|
|
catch (e) {
|
|
liberator.echo(":" + this.name + " ...");
|
|
liberator.echoerr("E117: Unknown function: " + completeOpt);
|
|
liberator.log(e);
|
|
return undefined;
|
|
}
|
|
return completer.apply(this, Array.slice(arguments));
|
|
};
|
|
}
|
|
else
|
|
completeFunc = completion[completeOptionMap[completeOpt]];
|
|
}
|
|
|
|
let added = commands.addUserCommand([cmd],
|
|
descriptionOpt,
|
|
userCommand, {
|
|
argCount: nargsOpt,
|
|
bang: bangOpt,
|
|
count: countOpt,
|
|
completer: completeFunc,
|
|
replacementText: args.literalArg
|
|
}, args.bang);
|
|
|
|
if (!added)
|
|
liberator.echoerr("E174: Command already exists: add ! to replace it");
|
|
}
|
|
else {
|
|
function completerToString(completer) {
|
|
if (completer)
|
|
return [k for ([k, v] in Iterator(completeOptionMap)) if (completer == completion[v])][0] || "custom";
|
|
else
|
|
return "";
|
|
}
|
|
|
|
// TODO: using an array comprehension here generates flakey results across repeated calls
|
|
// : perhaps we shouldn't allow options in a list call but just ignore them for now
|
|
let cmds = this._exCommands.filter(function (c) c.user && (!cmd || c.name.match("^" + cmd)));
|
|
|
|
if (cmds.length > 0) {
|
|
let str = template.tabular(["", "Name", "Args", "Range", "Complete", "Definition"], ["padding-right: 2em;"],
|
|
([cmd.bang ? "!" : " ",
|
|
cmd.name,
|
|
cmd.argCount,
|
|
cmd.count ? "0c" : "",
|
|
completerToString(cmd.completer),
|
|
cmd.replacementText || "function () { ... }"]
|
|
for ([, cmd] in Iterator(cmds))));
|
|
|
|
commandline.echo(str, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
|
|
}
|
|
else
|
|
liberator.echomsg("No user-defined commands found");
|
|
}
|
|
}, {
|
|
bang: true,
|
|
completer: function (context, args) {
|
|
if (args.completeArg == 0)
|
|
completion.userCommand(context);
|
|
else
|
|
completion.ex(context);
|
|
},
|
|
options: [
|
|
[["-nargs"], commands.OPTION_STRING,
|
|
function (arg) /^[01*?+]$/.test(arg),
|
|
[["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 is allowed"]]],
|
|
[["-bang"], commands.OPTION_NOARG],
|
|
[["-count"], commands.OPTION_NOARG],
|
|
[["-description"], commands.OPTION_STRING],
|
|
// TODO: "E180: invalid complete value: " + arg
|
|
[["-complete"], commands.OPTION_STRING,
|
|
function (arg) arg in completeOptionMap || /custom,\w+/.test(arg),
|
|
function (context) [[k, ""] for ([k, v] in Iterator(completeOptionMap))]]
|
|
],
|
|
literal: 1,
|
|
serial: function () [ {
|
|
command: this.name,
|
|
bang: true,
|
|
options: util.Array.toObject(
|
|
[[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" }))
|
|
// FIXME: add support for default values to parseArgs
|
|
if (k in cmd && cmd[k] != "0" && cmd[k] != "User-defined command")]),
|
|
arguments: [cmd.name],
|
|
literalArg: cmd.replacementText
|
|
}
|
|
for ([k, cmd] in Iterator(this._exCommands))
|
|
if (cmd.user && cmd.replacementText)
|
|
]
|
|
});
|
|
|
|
commands.add(["comc[lear]"],
|
|
"Delete all user-defined commands",
|
|
function () {
|
|
commands.getUserCommands().forEach(function (cmd) { commands.removeUserCommand(cmd.name); });
|
|
},
|
|
{ argCount: "0" });
|
|
|
|
commands.add(["delc[ommand]"],
|
|
"Delete the specified user-defined command",
|
|
function (args) {
|
|
let name = args[0];
|
|
|
|
if (commands.get(name))
|
|
commands.removeUserCommand(name);
|
|
else
|
|
liberator.echoerr("E184: No such user-defined command: " + name);
|
|
}, {
|
|
argCount: "1",
|
|
completer: function (context) completion.userCommand(context)
|
|
});
|
|
}
|
|
});
|
|
|
|
(function () {
|
|
|
|
Commands.quoteMap = {
|
|
"\n": "n",
|
|
"\t": "t"
|
|
};
|
|
function quote(q, list) {
|
|
let re = RegExp("[" + list + "]", "g");
|
|
return function (str) q + String.replace(str, re, function ($0) $0 in Commands.quoteMap ? Commands.quoteMap[$0] : ("\\" + $0)) + q;
|
|
};
|
|
Commands.complQuote = { // FIXME
|
|
'"': ['"', quote("", '\n\t"\\\\'), '"'],
|
|
"'": ["'", quote("", "\\\\'"), "'"],
|
|
"": ["", quote("", "\\\\ "), ""]
|
|
};
|
|
Commands.quoteArg = {
|
|
'"': quote('"', '\n\t"\\\\'),
|
|
"'": quote("'", "\\\\'"),
|
|
"": quote("", "\\\\ ")
|
|
};
|
|
|
|
Commands.parseBool = function (arg) {
|
|
if (/^(true|1|on)$/i.test(arg))
|
|
return true;
|
|
if (/^(false|0|off)$/i.test(arg))
|
|
return false;
|
|
return NaN;
|
|
};
|
|
Commands.argTypes = [
|
|
null,
|
|
ArgType("no arg", function (arg) !arg || null),
|
|
ArgType("boolean", Commands.parseBool),
|
|
ArgType("string", function (val) val),
|
|
ArgType("int", parseInt),
|
|
ArgType("float", parseFloat),
|
|
ArgType("list", function (arg) arg && arg.split(/\s*,\s*/))
|
|
];
|
|
})();
|
|
|
|
// vim: set fdm=marker sw=4 ts=4 et:
|