//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
-"use strict";
+/* use strict */
try {
Components.utils.import("resource://dactyl/bootstrap.jsm");
defineModule("commands", {
exports: ["ArgType", "Command", "Commands", "CommandOption", "Ex", "commands"],
- require: ["contexts", "messages", "util"],
- use: ["config", "options", "services", "template"]
+ require: ["contexts", "messages", "util"]
}, this);
/**
* @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),
* @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
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.options)
+ this._options = extraInfo.options;
+ delete extraInfo.options;
+
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],
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));
* @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),
+ hasName: function hasName(name) Command.hasName(this.parsedSpecs, name),
/**
* A helper function to parse an argument string.
extra: extra
}),
- complained: Class.memoize(function () ({})),
+ complained: Class.Memoize(function () ({})),
/**
* @property {[string]} All of this command's name specs. e.g., "com[mand]"
*/
specs: null,
+ parsedSpecs: Class.Memoize(function () Command.parseSpecs(this.specs)),
+
/** @property {[string]} All of this command's short names, e.g., "com" */
- shortNames: null,
+ shortNames: Class.Memoize(function () array.compact(this.parsedSpecs.map(function (n) n[1]))),
+
/**
* @property {[string]} All of this command's long names, e.g., "command"
*/
- longNames: null,
+ longNames: Class.Memoize(function () this.parsedSpecs.map(function (n) n[0])),
/** @property {string} The command's canonical name. */
- name: null,
+ name: Class.Memoize(function () this.longNames[0]),
+
/** @property {[string]} All of this command's long and short names. */
- names: null,
+ names: Class.Memoize(function () this.names = array.flatten(this.parsedSpecs)),
/** @property {string} This command's description, as shown in :listcommands */
description: Messages.Localized(""),
* @property {Array} The options this command takes.
* @see Commands@parseArguments
*/
- options: [],
+ options: Class.Memoize(function ()
+ this._options.map(function (opt) {
+ let option = CommandOption.fromArray(opt);
+ option.localeName = ["command", this.name, option.names[0]];
+ return option;
+ }, this)),
+ _options: [],
- optionMap: Class.memoize(function () array(this.options)
+ optionMap: Class.Memoize(function () array(this.options)
.map(function (opt) opt.names.map(function (name) [name, opt]))
.flatten().toObject()),
return res;
},
- argsPrototype: Class.memoize(function argsPrototype() {
+ argsPrototype: Class.Memoize(function argsPrototype() {
let res = update([], {
__iterator__: function AP__iterator__() array.iterItems(this),
command: this,
- explicitOpts: Class.memoize(function () ({})),
+ 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 () { ... }),
+ // TODO: string: Class.Memoize(function () { ... }),
verify: function verify() {
if (this.command.argCount) {
});
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 });
+ if (opt.default !== undefined) {
+ let prop = Object.getOwnPropertyDescriptor(opt, "default") ||
+ { configurable: true, enumerable: true, get: function () opt.default };
+
+ if (prop.get && !prop.set)
+ prop.set = function (val) { Class.replaceProperty(this, opt.names[0], val) };
+ Object.defineProperty(res, opt.names[0], prop);
+ }
});
return res;
this.modules.dactyl.warn(loc + message);
}
}, {
+ hasName: function hasName(specs, name)
+ specs.some(function ([long, short])
+ name.indexOf(short) == 0 && long.indexOf(name) == 0),
+
// TODO: do we really need more than longNames as a convenience anyway?
/**
* Converts command name abbreviation specs of the form
var CommandHive = Class("CommandHive", Contexts.Hive, {
init: function init(group) {
init.supercall(this, group);
+
this._map = {};
this._list = [];
+ this._specs = [];
},
+ /**
+ * Caches this command hive.
+ */
+
+ cache: function cache() {
+ let self = this;
+ let { cache } = this.modules;
+ this.cached = true;
+
+ cache.register(this.cacheKey, function () {
+ self.cached = false;
+ this.modules.moduleManager.initDependencies("commands");
+
+ let map = {};
+ for (let [name, cmd] in Iterator(self._map))
+ if (cmd.sourceModule)
+ map[name] = { sourceModule: cmd.sourceModule, isPlaceholder: true };
+
+ let specs = [];
+ for (let cmd in values(self._list))
+ for each (let spec in cmd.parsedSpecs)
+ specs.push(spec.concat(cmd.name));
+
+ return { map: map, specs: specs };
+ });
+
+ let cached = cache.get(this.cacheKey);
+ if (this.cached) {
+ this._specs = cached.specs;
+ for (let [k, v] in Iterator(cached.map))
+ this._map[k] = v;
+ }
+ },
+
+ get cacheKey() "commands/hives/" + this.name + ".json",
+
/** @property {Iterator(Command)} @private */
- __iterator__: function __iterator__() array.iterValues(this._list.sort(function (a, b) a.name > b.name)),
+ __iterator__: function __iterator__() {
+ if (this.cached)
+ this.modules.initDependencies("commands");
+ this.cached = false;
+ return array.iterValues(this._list.sort(function (a, b) a.name > b.name))
+ },
/** @property {string} The last executed Ex command line. */
repeat: null,
extra = extra || {};
if (!extra.definedAt)
extra.definedAt = contexts.getCaller(Components.stack.caller);
+ if (!extra.sourceModule)
+ extra.sourceModule = commands.currentDependency;
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));
+ if (this.name != "builtin") {
+ 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));
+ 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)
+ if (name in this._map && !this._map[name].isPlaceholder)
this.remove(name);
}
let closure = function () self._map[name];
memoize(this._map, name, function () commands.Command(specs, description, action, extra));
- memoize(this._list, this._list.length, closure);
+ if (!extra.hidden)
+ memoize(this._list, this._list.length, closure);
for (let alias in values(names.slice(1)))
memoize(this._map, alias, closure);
* 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,
+ get: function get(name, full) {
+ let cmd = this._map[name]
+ || !full && array.nth(this._list, function (cmd) cmd.hasName(name), 0)
+ || null;
+
+ if (!cmd && full) {
+ let name = array.nth(this.specs, function (spec) Command.hasName(spec, name), 0);
+ return name && this.get(name);
+ }
+
+ if (cmd && cmd.isPlaceholder) {
+ this.modules.moduleManager.initDependencies("commands", [cmd.sourceModule]);
+ cmd = this._map[name];
+ }
+ return cmd;
+ },
/**
* Remove the user-defined command with matching *name*.
*/
var Commands = Module("commands", {
lazyInit: true,
+ lazyDepends: true,
Local: function Local(dactyl, modules, window) let ({ Group, contexts } = modules) ({
init: function init() {
});
},
+ reallyInit: function reallyInit() {
+ if (false)
+ this.builtin.cache();
+ else
+ this.modules.moduleManager.initDependencies("commands");
+ },
+
get context() contexts.context,
get readHeredoc() modules.io.readHeredoc,
.toObject();
for (let [opt, val] in Iterator(args.options || {})) {
+ if (val === undefined)
+ continue;
if (val != null && defaults[opt] === val)
continue;
+
let chr = /^-.$/.test(opt) ? " " : "=";
if (isArray(val))
opt += chr + Option.stringify.stringlist(val);
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.hereDoc && false ? "<<EOF\n" + String.replace(str, /\n$/, "") + "\nEOF"
+ this.serializeHereDoc ? "<<EOF\n" + String.replace(str, /\n$/, "") + "\nEOF"
: String.replace(str, /\n/g, "\n" + res[0].replace(/./g, " ").replace(/.$/, "\\")));
return res.join(" ");
},
]]>, /U/g, "\\u"), "x")
}),
- validName: Class.memoize(function validName() util.regexp("^" + this.nameRegexp.source + "$")),
+ validName: Class.Memoize(function validName() util.regexp("^" + this.nameRegexp.source + "$")),
- commandRegexp: Class.memoize(function commandRegexp() util.regexp(<![CDATA[
+ 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<group> <name>) : )?
+ (?P<cmd> (?:-? [()] | <name> | !)? ))
(?P<bang> !?)
(?P<space> \s*)
)
return;
}
- if (complete) {
- complete.fork(command.name);
- var context = complete.fork("args", len);
- }
+ if (complete)
+ var context = complete.fork(command.name).fork("opts", len);;
if (!complete || /(\w|^)[!\s]/.test(str))
args = command.parseArgs(args, context, { count: count, bang: bang });
return;
}
- let cmdContext = context.fork(command.name, match.fullCmd.length + match.bang.length + match.space.length);
+ let cmdContext = context.fork(command.name + "/args", match.fullCmd.length + match.bang.length + match.space.length);
try {
if (!cmdContext.waitingForTab) {
if (!args.completeOpt && command.completer && args.completeStart != null) {
}
};
+ completion.exMacro = function exMacro(context, args, cmd) {
+ if (!cmd.action.macro)
+ return;
+ let { macro } = cmd.action;
+
+ let start = "«%-d-]'", end = "'[-d-%»";
+
+ let n = /^\d+$/.test(cmd.argCount) ? parseInt(cmd.argCount) : 12;
+ for (let i = args.completeArg; i < n; i++)
+ args[i] = start + i + end;
+
+ let params = {
+ args: { __proto__: args, toString: function () this.join(" ") },
+ bang: args.bang ? "!" : "",
+ count: args.count
+ };
+
+ if (!macro.valid(params))
+ return;
+
+ let str = macro(params);
+ let idx = str.indexOf(start);
+ if (!~idx || !/^(')?(\d+)'/.test(str.substr(idx + start.length))
+ || RegExp.$2 != args.completeArg)
+ return;
+
+ let quote = RegExp.$2;
+ context.quote = null;
+ context.offset -= idx;
+ context.filter = str.substr(0, idx) + (quote ? Option.quote : util.identity)(context.filter);
+
+ context.fork("ex", 0, completion, "ex");
+ };
+
completion.userCommand = function userCommand(context, group) {
context.title = ["User Command", "Definition"];
context.keys = { text: "name", description: "replacementText" };
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) {
_("group.cantChangeBuiltin", _("command.commands")));
let completer = args["-complete"];
- let completerFunc = null; // default to no completion for user commands
+ let completerFunc = function (context, args) modules.completion.exMacro(context, args, this);
if (completer) {
if (/^custom,/.test(completer)) {
function makeParams(args, modifiers) ({
args: {
__proto__: args,
- toString: function () this.string,
+ toString: function () this.string
},
bang: this.bang && args.bang ? "!" : "",
count: this.count && args.count
["+", "One or more arguments are allowed"]],
default: "0",
type: CommandOption.STRING,
- validator: function (arg) /^[01*?+]$/.test(arg)
+ validator: bind("test", /^[01*?+]$/)
},
{
names: ["-nopersist", "-n"],
cmd.hive == commands.builtin ? "" : <span highlight="Object" style="padding-right: 1em;">{cmd.hive.name}</span>
]
})),
- iterateIndex: function (args) let (tags = services["dactyl:"].HELP_TAGS)
+ 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"],
javascript: function initJavascript(dactyl, modules, window) {
const { JavaScript, commands } = modules;
- JavaScript.setCompleter([commands.user.get, commands.user.remove],
+ JavaScript.setCompleter([CommandHive.prototype.get, CommandHive.prototype.remove],
[function () [[c.names, c.description] for (c in this)]]);
- JavaScript.setCompleter([commands.get],
+ JavaScript.setCompleter([Commands.prototype.get],
[function () [[c.names, c.description] for (c in this.iterator())]]);
},
mappings: function initMappings(dactyl, modules, window) {