X-Git-Url: https://git.donarmstrong.com/?a=blobdiff_plain;f=common%2Fmodules%2Fcommands.jsm;h=199cfff13eca5e0b999b0a74c6d1a77504d36661;hb=8b6fcae7eaa413bc62d645d2d0c99835c47265e6;hp=7d7093e99a41befb806ccd157b0a3dd2b3cb6c86;hpb=70740024f9c028c1fd63e1a1850ab062ff956054;p=dactyl.git diff --git a/common/modules/commands.jsm b/common/modules/commands.jsm index 7d7093e..199cfff 100644 --- a/common/modules/commands.jsm +++ b/common/modules/commands.jsm @@ -1,6 +1,6 @@ // Copyright (c) 2006-2008 by Martin Stubenschrott // Copyright (c) 2007-2011 by Doug Kearns -// Copyright (c) 2008-2011 by Kris Maglione +// Copyright (c) 2008-2012 Kris Maglione // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. @@ -8,12 +8,14 @@ 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"] -}, this); + require: ["contexts", "messages", "util"] +}); + +lazyRequire("help", ["help"]); +lazyRequire("options", ["Option"]); +lazyRequire("template", ["template"]); /** * A structure representing the options available for a command. @@ -26,6 +28,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 +75,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 @@ -101,6 +109,7 @@ update(CommandOption, { * @param {function} action The action invoked by this command when executed. * @param {Object} extraInfo An optional extra configuration hash. The * following properties are supported. + * always - see {@link Command#always} * argCount - see {@link Command#argCount} * bang - see {@link Command#bang} * completer - see {@link Command#completer} @@ -118,22 +127,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 +169,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 +192,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 +212,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(""), @@ -234,28 +244,41 @@ var Command = Class("Command", { * @property {function (Args)} The function called to execute this command. */ action: null, + + /** + * @property {function (Args)} A function which is called when this + * command is encountered, even if we are ignoring commands. Used to + * implement control structures. + */ + always: 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 @@ -263,6 +286,7 @@ var Command = Class("Command", { * 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 @@ -271,13 +295,20 @@ var Command = Class("Command", { * key mappings or Ex command lines as arguments. */ literal: null, + /** * @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 +319,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 +347,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 +408,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 +489,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; + + let cached = cache.get(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 +560,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 +569,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 +587,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 +622,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 +660,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 +672,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, @@ -661,38 +769,34 @@ var Commands = Module("commands", { let hives = (hives || this.userHives).map(function (h) [h, cmds(h)]).filter(function ([h, c]) c.length); - let list = - - - - - - - - - - { - template.map(hives, function ([hive, cmds]) let (i = 0) - + - template.map(cmds, function (cmd) - - - - - - - - - ) + - ) - } -
- {_("title.Name")}{_("title.Args")}{_("title.Range")}{_("title.Complete")}{_("title.Definition")}
{!i++ ? hive.name : ""}{cmd.bang ? "!" : " "}{cmd.name}{cmd.argCount}{cmd.count ? "0c" : ""}{completerToString(cmd.completer)}{cmd.replacementText || "function () { ... }"}
; - - if (list.*.length() === list.text().length() + 2) - dactyl.echomsg(_("command.none")); - else - commandline.commandOutput(list); + let list = ["table", {}, + ["tr", { highlight: "Title" }, + ["td"], + ["td", { style: "padding-right: 1em;" }], + ["td", { style: "padding-right: 1ex;" }, _("title.Name")], + ["td", { style: "padding-right: 1ex;" }, _("title.Args")], + ["td", { style: "padding-right: 1ex;" }, _("title.Range")], + ["td", { style: "padding-right: 1ex;" }, _("title.Complete")], + ["td", { style: "padding-right: 1ex;" }, _("title.Definition")]], + ["col", { style: "min-width: 6em; padding-right: 1em;" }], + hives.map(function ([hive, cmds]) let (i = 0) [ + ["tr", { style: "height: .5ex;" }], + cmds.map(function (cmd) + ["tr", {}, + ["td", { highlight: "Title" }, !i++ ? hive.name : ""], + ["td", {}, cmd.bang ? "!" : " "], + ["td", {}, cmd.name], + ["td", {}, cmd.argCount], + ["td", {}, cmd.count ? "0c" : ""], + ["td", {}, completerToString(cmd.completer)], + ["td", {}, cmd.replacementText || "function () { ... }"]]), + ["tr", { style: "height: .5ex;" }]])]; + + // E4X-FIXME + // if (list.*.length() === list.text().length() + 2) + // dactyl.echomsg(_("command.none")); + // else + commandline.commandOutput(list); } }), @@ -747,8 +851,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 +863,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 ? "< ] [^ ]* - ]]>, "gx", { - forbid: util.regexp(String.replace(, /U/g, "\\u"), "x") + */), /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*) ) @@ -1183,7 +1292,7 @@ var Commands = Module("commands", { (?:. | \n)*? )? $ - ]]>, "x", { + */), "x", { name: this.nameRegexp })), @@ -1240,10 +1349,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 +1475,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 +1492,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 +1536,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 +1558,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 +1587,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 +1653,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"], @@ -1573,10 +1721,11 @@ var Commands = Module("commands", { iterate: function (args) commands.iterator().map(function (cmd) ({ __proto__: cmd, columns: [ - cmd.hive == commands.builtin ? "" : {cmd.hive.name} + cmd.hive == commands.builtin ? "" : ["span", { highlight: "Object", style: "padding-right: 1em;" }, + 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 +1755,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) { @@ -1628,39 +1777,37 @@ var Commands = Module("commands", { } }); -(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; - }; -})(); +let quote = 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.quoteMap = { + "\n": "\\n", + "\t": "\\t", +}; + +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();