diff --git a/common/components/protocols.js b/common/components/protocols.js index a3c87ae5..f37a0ebf 100644 --- a/common/components/protocols.js +++ b/common/components/protocols.js @@ -111,6 +111,7 @@ Dactyl.prototype = { for (let [k, v] in Iterator(obj[prop] || {})) this[prop][k] = v; } + this.initialized = true; }, scheme: "dactyl", diff --git a/common/content/commandline.js b/common/content/commandline.js index b9e2147d..cd7f8c22 100644 --- a/common/content/commandline.js +++ b/common/content/commandline.js @@ -940,14 +940,18 @@ const CommandLine = Module("commandline", { dactyl.open(event.target.href, where); } + let command = event.originalTarget.getAttributeNS(NS.uri, "command"); + if (command && dactyl.commands[command]) { + return dactyl.withSavedValues(["forceNewTab"], function () { + dactyl.forceNewTab = event.ctrlKey || event.shiftKey || event.button == 1; + dactyl.commands[command](event); + }); + } + switch (key) { case "": event.preventDefault(); - let command = event.originalTarget.getAttributeNS(NS.uri, "command"); - if (command && dactyl.commands[command]) - return dactyl.commands[command](event); - else - openLink(dactyl.CURRENT_TAB); + openLink(dactyl.CURRENT_TAB); return false; case "": case "": @@ -1708,7 +1712,7 @@ const CommandLine = Module("commandline", { { validator: function (value) value >= 1 }); options.add(["messages", "msgs"], - "Number of messages to store in the :message history", + "Number of messages to store in the :messages history", "number", 100, { validator: function (value) value >= 0 }); diff --git a/common/content/commands.js b/common/content/commands.js index 46f9a4fb..5c16bc8e 100644 --- a/common/content/commands.js +++ b/common/content/commands.js @@ -1395,6 +1395,15 @@ const Commands = Module("commands", { completer: function (context) completion.userCommand(context) }); + dactyl.addUsageCommand({ + name: ["listc[ommands]", "lc"], + description: "List all Ex commands along with their short descriptions", + iterate: function (args) commands, + format: { + description: function (cmd) template.linkifyHelp(cmd.description + (cmd.replacementText ? ": " + cmd.action : "")) + } + }); + function checkStack(cmd) { util.assert(io.sourcing && io.sourcing.stack && io.sourcing.stack[cmd] && io.sourcing.stack[cmd].length, @@ -1454,13 +1463,14 @@ const Commands = Module("commands", { commands.add(["y[ank]"], "Yanks the output of the given command to the clipboard", function (args) { - let res = commandline.withOutputToString(commands.execute, commands, args[0]); + let cmd = /^:/.test(args[0]) ? args[0] : ":echo " + args[0]; + let res = commandline.withOutputToString(commands.execute, commands, cmd); dactyl.clipboardWrite(res); let lines = res.split("\n").length; dactyl.echomsg("Yanked " + lines + " line" + (lines == 1 ? "" : "s")); }, { - completer: function (context) completion.ex(context), + completer: function (context) completion[/^:/.test(context.filter) ? "ex" : "javascript"](context), literal: 0 }); }, diff --git a/common/content/dactyl.js b/common/content/dactyl.js index 84731c65..bf2b0bdc 100644 --- a/common/content/dactyl.js +++ b/common/content/dactyl.js @@ -48,6 +48,10 @@ const Dactyl = Module("dactyl", { this.commands = {}; this.modules = modules; this.observers = {}; + + this.commands["dactyl.help"] = function (event) { + dactyl.help(event.originalTarget.textContent); + }; }, /** @property {string} The name of the current user profile. */ @@ -139,6 +143,35 @@ const Dactyl = Module("dactyl", { }); }, + addUsageCommand: function (params) { + commands.add(params.name, params.description, + function (args) { + let results = array(params.iterate(args)) + .sort(function (a, b) String.localeCompare(a.name, b.name)); + if (args.length) + results = results.filter(function (item) args.map(String.toLowerCase) + .every(function (arg) (item.name + item.description).toLowerCase().indexOf(arg) >= 0)); + commandline.commandOutput( + template.usage(results, params.format)); + }, + { + argCount: "*", + completer: function (context, args) { + context.keys.text = util.identity; + context.keys.description = function () seen[this.text] + " matching items"; + let seen = {}; + context.completions = array(item.description.toLowerCase().split(/[()\s]+/) + for (item in params.iterate(args))) + .flatten().filter(function (w) /^\w[\w-_']+$/.test(w)) + .map(function (k) { + seen[k] = (seen[k] || 0) + 1; + return k; + }).uniq() + }, + options: params.options || [] + }); + }, + /** * Triggers the application bell to notify the user of an error. The * bell may be either audible or visual depending on the value of the @@ -257,8 +290,7 @@ const Dactyl = Module("dactyl", { if (typeof str == "object" && "echoerr" in str) str = str.echoerr; else if (isinstance(str, ["Error"])) - str = str.fileName.replace(/^.*? -> /, "") - + ":" + str.lineNumber + ": " + str; + str = <>{str.fileName.replace(/^.*? -> /, "")}: {str.lineNumber}: {str}; if (options["errorbells"]) dactyl.beep(); @@ -1044,7 +1076,7 @@ const Dactyl = Module("dactyl", { reportError: function reportError(error, echo) { if (error instanceof FailedAssertion || error.message === "Interrupted") { if (error.message) - dactyl.echoerr(error.message); + dactyl.echoerr(template.linkifyHelp(error.message)); else dactyl.beep(); return; @@ -1935,7 +1967,6 @@ const Dactyl = Module("dactyl", { argCount: "0", bang: true }); - }, completion: function () { diff --git a/common/content/mappings.js b/common/content/mappings.js index 08c71404..db256085 100644 --- a/common/content/mappings.js +++ b/common/content/mappings.js @@ -185,16 +185,17 @@ const Mappings = Module("mappings", { some(function (m) m.rhs && m.rhs === map.rhs && m.name === map.name)))) }, - // NOTE: just normal mode for now - /** @property {Iterator(Map)} @private */ - __iterator__: function () { - let mode = modes.NORMAL; + iterate: function (mode) { let seen = {}; for (let map in iterAll(values(this._user[mode]), values(this._main[mode]))) if (!set.add(seen, map.name)) yield map; }, + // NOTE: just normal mode for now + /** @property {Iterator(Map)} */ + __iterator__: function () this.iterate(modes.NORMAL), + // used by :mkpentadactylrc to save mappings /** * Returns a user-defined mappings iterator for the specified *mode*. @@ -381,19 +382,6 @@ const Mappings = Module("mappings", { } } - function findMode(name) { - for (let mode in modes.mainModes) - if (name == mode || name == mode.char || String.toLowerCase(name).replace(/-/g, "_") == mode.name.toLowerCase()) - return mode.mask; - return null; - } - function uniqueModes(modes) { - modes = modes.map(modules.modes.closure.getMode); - let chars = [k for ([k, v] in Iterator(modules.modes.modeChars)) - if (v.every(function (mode) modes.indexOf(mode) >= 0))]; - return array.uniq(modes.filter(function (m) chars.indexOf(m.char) < 0).concat(chars)); - } - const opts = { completer: function (context, args) { if (args.length == 1) @@ -511,8 +499,66 @@ const Mappings = Module("mappings", { }); } + function findMode(name) { + for (let mode in modes.mainModes) + if (name == mode || name == mode.char || String.toLowerCase(name).replace(/-/g, "_") == mode.name.toLowerCase()) + return mode.mask; + return null; + } + function uniqueModes(modes) { + modes = modes.map(modules.modes.closure.getMode); + let chars = [k for ([k, v] in Iterator(modules.modes.modeChars)) + if (v.every(function (mode) modes.indexOf(mode) >= 0))]; + return array.uniq(modes.filter(function (m) chars.indexOf(m.char) < 0).concat(chars)); + } + addMapCommands("", [modes.NORMAL, modes.VISUAL], ""); + let args = { + getMode: function (args) findMode(args["-mode"]), + iterate: function (args) { + for (let map in mappings.iterate(this.getMode(args))) + for (let name in values(map.names)) + yield { name: name, __proto__: map }; + }, + format: { + description: function (map) (XML.ignoreWhitespace = false, XML.prettyPrinting = false, <> + {options.get("passkeys").has(map.name) + ? (passed by {template.helpLink("'passkeys'")}) + : <>} + {template.linkifyHelp(map.description + (map.rhs ? ": " + map.rhs : ""))} + ) + } + } + + dactyl.addUsageCommand({ + __proto__: args, + name: ["listk[eys]", "lk"], + description: "List all mappings along with their short descriptions", + options: [ + { + names: ["-mode", "-m"], + type: CommandOption.STRING, + description: "The mode for which to list mappings", + default: "n", + completer: function () [[array.compact([mode.name.toLowerCase().replace(/_/g, "-"), mode.char]), mode.disp] + for (mode in modes.mainModes)], + validator: function (m) findMode(m) + } + ] + }); + + forEach(modes.mainModes, function (mode) { + if (mode.char && !commands.get(mode.char + "listkeys", true)) + dactyl.addUsageCommand({ + __proto__: args, + name: [mode.char + "listk[eys]", mode.char + "lk"], + description: "List all " + mode.name + " mode mappings along with their short descriptions", + getMode: function (args) mode, + options: [] + }); + }); + for (let mode in modes.mainModes) if (mode.char && !commands.get(mode.char + "map", true)) addMapCommands(mode.char, diff --git a/common/content/options.js b/common/content/options.js index 99e6d51f..5454ec62 100644 --- a/common/content/options.js +++ b/common/content/options.js @@ -817,6 +817,36 @@ const Options = Module("options", { }, { }, { commands: function () { + let args = { + getMode: function (args) findMode(args["-mode"]), + iterate: function (args) { + for (let map in mappings.iterate(this.getMode(args))) + for (let name in values(map.names)) + yield { name: name, __proto__: map }; + }, + format: { + description: function (map) (XML.ignoreWhitespace = false, XML.prettyPrinting = false, <> + {options.get("passkeys").has(map.name) + ? (passed by {template.helpLink("'passkeys'")}) + : <>} + {template.linkifyHelp(map.description)} + ) + } + } + + dactyl.addUsageCommand({ + name: ["listo[ptions]", "lo"], + description: "List all options along with their short descriptions", + iterate: function (args) options, + format: { + description: function (opt) (XML.ignoreWhitespace = false, XML.prettyPrinting = false, <> + {opt.scope == Option.SCOPE_LOCAL + ? (buffer local) : ""} + {template.linkifyHelp(opt.description)} + ) + } + }); + function setAction(args, modifiers) { let bang = args.bang; if (!args.length) diff --git a/common/locale/en-US/various.xml b/common/locale/en-US/various.xml index db65a99e..b45202ee 100644 --- a/common/locale/en-US/various.xml +++ b/common/locale/en-US/various.xml @@ -113,9 +113,12 @@ :yank :y - :y[ank] cmd + :y[ank] :cmd + :y[ank] js -

Yanks the output of the given command to the clipboard.

+

+ Yanks the output of the given Ex command cmd or JavaScript js to the clipboard. +

diff --git a/common/modules/prefs.jsm b/common/modules/prefs.jsm index 8c852395..d5c935e3 100644 --- a/common/modules/prefs.jsm +++ b/common/modules/prefs.jsm @@ -131,7 +131,7 @@ const Prefs = Module("prefs", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference if (saved == null && curval != defval || curval != saved) { let msg = "Warning: setting preference " + name + ", but it's changed from its default value."; if (message) - msg += " " + message; + msg = template.linkifyHelp(msg + " " + message); util.dactyl.echomsg(msg); } }, diff --git a/common/modules/template.jsm b/common/modules/template.jsm index 15f31b9b..b0437aa9 100644 --- a/common/modules/template.jsm +++ b/common/modules/template.jsm @@ -7,7 +7,8 @@ Components.utils.import("resource://dactyl/base.jsm"); defineModule("template", { exports: ["Template", "template"], - require: ["util"] + require: ["util"], + use: ["services"] }); default xml namespace = XHTML; @@ -78,6 +79,16 @@ const Template = Module("Template", { // }, + helpLink: function (topic, type) { + if (services["dactyl:"].initialized && !set.has(services["dactyl:"].HELP_TAGS, topic)) + return <>{topic}; + + XML.ignoreWhitespace = false; XML.prettyPrinting = false; + type = type || /^'.*'$/.test(topic) ? "HelpOpt" : + /^:\w/.test(topic) ? "HelpEx" : "HelpKey"; + return {topic} + }, + // if "processStrings" is true, any passed strings will be surrounded by " and // any line breaks are displayed as \n highlight: function highlight(arg, processStrings, clip) { @@ -199,6 +210,21 @@ const Template = Module("Template", { // }, + linkifyHelp: function linkifyHelp(str) { + util.dactyl.initHelp(); + + let re = util.regexp( ) + (?=[[!,;./\s]|$) + ]]>, "g"); + return this.highlightSubstrings(str, (function () { + let res; + while ((res = re.exec(str)) && res[2].length) + yield [res.index + res[1].length, res[2].length]; + })(), template.helpLink); + }, + options: function options(title, opts) { XML.ignoreWhitespace = false; XML.prettyPrinting = false; // @@ -285,8 +311,9 @@ const Template = Module("Template", { // }, - usage: function usage(iter) { + usage: function usage(iter, format) { XML.ignoreWhitespace = false; XML.prettyPrinting = false; + let desc = format && format.description || function (item) template.linkifyHelp(item.description); // return { @@ -298,7 +325,7 @@ const Template = Module("Template", { {name} + <> + Defined at {template.sourceLink(frame)} } - + ) }
{item.description}{desc(item)}
; diff --git a/common/modules/util.jsm b/common/modules/util.jsm index 0e8c7734..2f3372f8 100644 --- a/common/modules/util.jsm +++ b/common/modules/util.jsm @@ -30,7 +30,7 @@ memoize(this, "Commands", function () { const FailedAssertion = Class("FailedAssertion", Error, { init: function (message) { - this.message = message; + update(this, Error(message)) } }); @@ -102,7 +102,7 @@ const Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]) */ assert: function (condition, message) { if (!condition) - throw new FailedAssertion(message); + throw FailedAssertion(message); }, get chromePackages() { diff --git a/common/skin/dactyl.css b/common/skin/dactyl.css index ea5d71e8..5e90659c 100644 --- a/common/skin/dactyl.css +++ b/common/skin/dactyl.css @@ -76,6 +76,14 @@ } +@-moz-document + url-prefix(chrome://dactyl/) { + +*:-moz-any-link:hover { + text-decoration: underline; +} +} + /* Applied to completion buffer, MOW, browser window */ @-moz-document url-prefix(chrome://) {