diff --git a/common/content/abbreviations.js b/common/content/abbreviations.js index 8e1847eb..cd44fd08 100644 --- a/common/content/abbreviations.js +++ b/common/content/abbreviations.js @@ -232,11 +232,8 @@ const Abbreviations = Module("abbreviations", { dactyl.assert(lhs != null, "E474: Invalid argument"); if (rhs) { - if (args["-javascript"]) { - let expr = rhs; - rhs = dactyl.userFunc("editor", expr); - rhs.toString = function () expr; - } + if (args["-javascript"]) + rhs = Command.bindMacro({ literalArg: rhs }, "-javascript", ["editor"]); abbreviations.add(modes, lhs, rhs); } else { @@ -258,7 +255,8 @@ const Abbreviations = Module("abbreviations", { return completion.javascript(context); }, literal: 0, - serial: function () [ { + serialize: function () [ + { command: this.name, arguments: [abbr.lhs], literalArg: abbr.rhs, diff --git a/common/content/commands.js b/common/content/commands.js index 5a95bae0..3f087133 100644 --- a/common/content/commands.js +++ b/common/content/commands.js @@ -25,6 +25,7 @@ * (@link CommandOption.FLOAT), * (@link CommandOption.LIST), * (@link CommandOption.ANY) + * @property {object} default The option's default value * @property {function} validator A validator function * @property {function (CompletionContext, object)} completer A list of * completions, or a completion function which will be passed a @@ -285,7 +286,44 @@ const Command = Class("Command", { */ replacementText: null }, { - bindMacro: function (args, default_) { + bindMacro: function (args, default_, params) { + let makeParams = function makeParams() + let (args = arguments) + params.map(function (name, i) [name, args[i]]).toObject(); + if (callable(params)) + makeParams = params; + else + params = array(params); + + let rhs = args.literalArg; + let type = ["-builtin", "-ex", "-javascript", "-keys"].reduce(function (a, b) args[b] ? b : a, default_); + switch (type) { + case "-builtin": + let noremap = true; + /* fallthrough */ + case "-keys": + let silent = args["-silent"]; + rhs = events.canonicalKeys(rhs); + var action = function action(count) + events.feedkeys(commands.replaceTokens(rhs, { count: count }), + noremap, silent); + break; + case "-ex": + action = function action() commands.execute(rhs, makeParams.apply(this, arguments), + false, null, action.sourcing); + action.sourcing = io.sourcing && update({}, io.sourcing); + break; + case "-javascript": + if (callable(params)) + action = dactyl.userEval("(function action() { with (action.makeParams.apply(this, arguments)) {" + args.literalArg + "} })") + else + action = dactyl.userFunc.apply(dactyl, params.concat(args.literalArg).array); + action.makeParams = makeParams; + break; + } + action.toString = function toString() (type === default_ ? "" : type + " ") + rhs; + args = null; + return action; }, // TODO: do we really need more than longNames as a convenience anyway? @@ -389,7 +427,6 @@ const Commands = Module("commands", { addUserCommand: function (names, description, action, extra, replace) { extra = extra || {}; extra.user = true; - description = description || "User defined command"; return this._addCommand([names, description, action, extra], replace); }, @@ -404,7 +441,13 @@ const Commands = Module("commands", { commandToString: function (args) { let res = [args.command + (args.bang ? "!" : "")]; + let defaults = {}; + if (args.ignoreDefaults) + defaults = array(this.options).map(function (opt) [opt.names[0], opt.default]).toObject(); + for (let [opt, val] in Iterator(args.options || {})) { + if (val != null && defaults[opt] === val) + continue; let chr = /^-.$/.test(opt) ? " " : "="; if (val != null) opt += chr + Commands.quote(val); @@ -416,8 +459,8 @@ const Commands = Module("commands", { let str = args.literalArg; if (str) res.push(!/\n/.test(str) ? str : - this.hereDoc ? "< 1 && /^[01?]$/.test(argCount)) fail("E488: Trailing characters"); + for (let opt in values(options)) + if (set.has(opt, "default") && args[opt.names[0]] === undefined) + args[opt.names[0]] = opt.default; + return args; }, @@ -970,8 +1017,10 @@ const Commands = Module("commands", { if (token == "lt") // Don't quote, as in Vim (but, why so in Vim? You'd think people wouldn't say if they didn't want it) return "<"; let res = tokens[token]; - if (res == undefined) // Ignore anything undefined + if (res === undefined) // Ignore anything undefined res = "<" + token + ">"; + if (res === null) + res = ""; if (quote && typeof res != "number") return Commands.quoteArg['"'](res); return res; @@ -1074,16 +1123,6 @@ const Commands = Module("commands", { }, commands: function () { - function userCommand(args, modifiers) { - let tokens = { - args: this.argCount && args.string, - bang: this.bang && args.bang ? "!" : "", - count: this.count && args.count - }; - - commands.execute(this.replacementText, tokens, false, null, this.sourcing); - } - // TODO: offer completion.ex? // : make this config specific var completeOptionMap = { @@ -1108,12 +1147,7 @@ const Commands = Module("commands", { dactyl.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 completeOpt = args["-complete"]; let completeFunc = null; // default to no completion for user commands if (completeOpt) { @@ -1140,12 +1174,19 @@ const Commands = Module("commands", { } let added = commands.addUserCommand([cmd], - descriptionOpt, - userCommand, { - argCount: nargsOpt, - bang: bangOpt, - count: countOpt, + args["-description"], + Command.bindMacro(args, "-ex", + function makeParams(args, modifiers) ({ + args: this.argCount && args.string, + bang: this.bang && args.bang ? "!" : "", + count: this.count && args.count + })), + { + argCount: args["-nargs"], + bang: args["-bang"], + count: args["-count"], completer: completeFunc, + persist: !args["-nopersist"], replacementText: args.literalArg, sourcing: io.sourcing && update({}, io.sourcing) }, args.bang); @@ -1190,16 +1231,20 @@ const Commands = Module("commands", { { names: ["-bang", "-b"], description: "Command may be proceeded by a !" }, { names: ["-count", "-c"], description: "Command may be preceeded by a count" }, { - names: ["-description", "-desc", "-d"], - description: "A user-visible description of the command", - type: CommandOption.STRING - }, { // TODO: "E180: invalid complete value: " + arg names: ["-complete", "-C"], description: "The argument completion function", completer: function (context) [[k, ""] for ([k, v] in Iterator(completeOptionMap))], type: CommandOption.STRING, validator: function (arg) arg in completeOptionMap || /custom,\w+/.test(arg), + }, { + names: ["-description", "-desc", "-d"], + description: "A user-visible description of the command", + default: "User-defined command", + type: CommandOption.STRING + }, { + names: ["-javascript", "-js", "-j"], + description: "Execute this mapping as JavaScript rather than keys" }, { names: ["-nargs", "-a"], description: "The allowed number of arguments", @@ -1208,9 +1253,13 @@ const Commands = Module("commands", { ["*", "Zero or more arguments are allowed"], ["?", "Zero or one argument is allowed"], ["+", "One or more arguments are allowed"]], + default: "0", type: CommandOption.STRING, validator: function (arg) /^[01*?+]$/.test(arg) - }, + }, { + names: ["-nopersist", "-n"], + description: "Do not save this command to an auto-generated RC file" + } ], literal: 1, serialize: function () [ { @@ -1220,13 +1269,13 @@ const Commands = Module("commands", { [[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")]), + if (cmd[k])]), arguments: [cmd.name], - literalArg: cmd.replacementText + literalArg: cmd.action, + ignoreDefaults: true } for ([k, cmd] in Iterator(commands._exCommands)) - if (cmd.user && cmd.replacementText) + if (cmd.user && cmd.persist) ] }); diff --git a/common/content/dactyl.js b/common/content/dactyl.js index 1b71592f..ae40e705 100644 --- a/common/content/dactyl.js +++ b/common/content/dactyl.js @@ -307,9 +307,8 @@ const Dactyl = Module("dactyl", { */ userFunc: function () { return this.userEval( - "(function userFunction(" + - Array.slice(arguments, 0, -1).join(", ") + - ") { " + arguments[arguments.length - 1] + " })"); + "(function userFunction(" + Array.slice(arguments, 0, -1).join(", ") + ")" + + " { " + arguments[arguments.length - 1] + " })"); }, /** diff --git a/common/content/mappings.js b/common/content/mappings.js index da98c0c6..613e7990 100644 --- a/common/content/mappings.js +++ b/common/content/mappings.js @@ -181,7 +181,7 @@ const Mappings = Module("mappings", { modes = modes.slice(); return (map for ([i, map] in Iterator(stack[modes.shift()].sort(function (m1, m2) String.localeCompare(m1.name, m2.name)))) if (map.rhs && modes.every(function (mode) stack[mode]. - some(function (m) m.rhs && array.equals(m.rhs, map.rhs) && m.name == map.name)))) + some(function (m) m.rhs && m.rhs === map.rhs && m.name === map.name)))) }, // NOTE: just normal mode for now @@ -347,7 +347,7 @@ const Mappings = Module("mappings", { {modeSign} {name} {map.noremap ? "*" : " "} - {map.rhs ? map.rhs.join(" ") : "function () { ... }"} + {map.rhs || map.action.toSource()} )) } ; @@ -361,7 +361,6 @@ const Mappings = Module("mappings", { }, { }, { commands: function () { - const stockDescription = "User defined mapping"; function addMapCommands(ch, mapmodes, modeDescription) { // 0 args -> list all maps // 1 arg -> list the maps starting with args @@ -374,35 +373,20 @@ const Mappings = Module("mappings", { } let [lhs, rhs] = args; + if (noremap) + args["-builtin"] = true; if (!rhs) // list the mapping mappings.list(mapmodes, mappings._expandLeader(lhs)); else { - if (args["-javascript"]) { - rhs = ["-javascript", rhs]; - var action = dactyl.userFunc("count", rhs); - } - else if (args["-ex"]) { - rhs = ["-ex", rhs]; - action = function action(count) commands.execute(rhs[1], { count: count }, - false, null, action.sourcing); - action.sourcing = io.sourcing && update({}, io.sourcing); - } - else { - rhs = [events.canonicalKeys(rhs)]; - action = function (count) { - events.feedkeys(commands.replaceTokens(rhs[0], { count: count }), - this.noremap, this.silent); - }; - } - mappings.addUserMap(mapmodes, [lhs], - args["-description"] || stockDescription, - action, { + args["-description"], + Command.bindMacro(args, "-keys", ["count"]), + { count: args["-count"], - rhs: rhs, - noremap: "-builtin" in args || noremap, + noremap: "-builtin" in args, persist: !args["-nopersist"], + get rhs() String(this.action), silent: "-silent" in args }); } @@ -451,8 +435,9 @@ const Mappings = Module("mappings", { }, { names: ["-description", "-d"], - type: CommandOption.STRING, - description: "A description of this mapping" + description: "A description of this mapping", + default: "User defined mapping", + type: CommandOption.STRING }, { names: ["-ex", "-e"], @@ -485,14 +470,13 @@ const Mappings = Module("mappings", { command: this.name, options: array([ ["-modes", uniqueModes(map.modes)], - map.noremap && ["-builtin"], - map.description != stockDescription && ["-description", map.description], - map.rhs.length > 1 && [map.rhs[0]], + ["-description", map.description], map.silent && ["-silent"]]) .filter(util.identity) .toObject(), arguments: [map.names[0]], - literalArg: map.rhs[map.rhs.length - 1] + literalArg: map.rhs, + ignoreDefaults: true } for (map in userMappings()) if (map.persist)