X-Git-Url: https://git.donarmstrong.com/?a=blobdiff_plain;f=common%2Fmodules%2Fcommands.jsm;fp=common%2Fmodules%2Fcommands.jsm;h=eca375bfd65d502493183bbd545ff34a871f07ce;hb=9044153cb63835e39b9de8ec4ade237c03e3888a;hp=7d7093e99a41befb806ccd157b0a3dd2b3cb6c86;hpb=70740024f9c028c1fd63e1a1850ab062ff956054;p=dactyl.git diff --git a/common/modules/commands.jsm b/common/modules/commands.jsm index 7d7093e..eca375b 100644 --- a/common/modules/commands.jsm +++ b/common/modules/commands.jsm @@ -4,15 +4,14 @@ // // 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); /** @@ -26,6 +25,7 @@ defineModule("commands", { * @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), @@ -72,6 +72,11 @@ update(CommandOption, { * @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 @@ -118,22 +123,17 @@ update(CommandOption, { 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], @@ -165,8 +165,11 @@ var Command = Class("Command", { 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)); @@ -185,8 +188,7 @@ var Command = Class("Command", { * @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. @@ -206,23 +208,27 @@ var Command = Class("Command", { 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(""), @@ -275,9 +281,15 @@ var Command = Class("Command", { * @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()), @@ -288,19 +300,19 @@ var Command = Class("Command", { 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) { @@ -316,10 +328,14 @@ var Command = Class("Command", { }); 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; @@ -373,6 +389,10 @@ var Command = Class("Command", { 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 @@ -450,12 +470,55 @@ var Ex = Module("Ex", { 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, @@ -478,6 +541,8 @@ var CommandHive = Class("CommandHive", Contexts.Hive, { 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); @@ -485,15 +550,17 @@ var CommandHive = Class("CommandHive", Contexts.Hive, { 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); } @@ -501,7 +568,8 @@ var CommandHive = Class("CommandHive", Contexts.Hive, { 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); @@ -535,9 +603,22 @@ var CommandHive = Class("CommandHive", Contexts.Hive, { * 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*. @@ -560,6 +641,7 @@ var CommandHive = Class("CommandHive", Contexts.Hive, { */ var Commands = Module("commands", { lazyInit: true, + lazyDepends: true, Local: function Local(dactyl, modules, window) let ({ Group, contexts } = modules) ({ init: function init() { @@ -571,6 +653,13 @@ var Commands = Module("commands", { }); }, + reallyInit: function reallyInit() { + if (false) + this.builtin.cache(); + else + this.modules.moduleManager.initDependencies("commands"); + }, + get context() contexts.context, get readHeredoc() modules.io.readHeredoc, @@ -747,8 +836,11 @@ var Commands = Module("commands", { .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); @@ -756,13 +848,14 @@ var Commands = Module("commands", { 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 ? "<, /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( (?P [:\s]*) (?P (?:\d+ | %)? ) (?P - (?: (?P ) : )? - (?P (?: | !)? )) + (?: (?P ) : )? + (?P (?:-? [()] | | !)? )) (?P !?) (?P \s*) ) @@ -1240,10 +1333,8 @@ var Commands = Module("commands", { 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 }); @@ -1368,7 +1459,7 @@ var Commands = Module("commands", { 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) { @@ -1385,6 +1476,40 @@ var Commands = Module("commands", { } }; + 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" }; @@ -1395,6 +1520,13 @@ var Commands = Module("commands", { 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) { @@ -1410,7 +1542,7 @@ var Commands = Module("commands", { _("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)) { @@ -1439,7 +1571,7 @@ var Commands = Module("commands", { 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 @@ -1505,7 +1637,7 @@ var Commands = Module("commands", { ["+", "One or more arguments are allowed"]], default: "0", type: CommandOption.STRING, - validator: function (arg) /^[01*?+]$/.test(arg) + validator: bind("test", /^[01*?+]$/) }, { names: ["-nopersist", "-n"], @@ -1576,7 +1708,7 @@ var Commands = Module("commands", { cmd.hive == commands.builtin ? "" : {cmd.hive.name} ] })), - 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"], @@ -1606,9 +1738,9 @@ var Commands = Module("commands", { 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) {