Don't like ads? PRO users don't see any ads ;-)
Guest

Untitled

By: a guest on Aug 4th, 2011  |  syntax: Diff  |  size: 569.87 KB  |  hits: 127  |  expires: Never
download  |  raw  |  embed  |  report abuse  |  print
Text below is selected. Please press Ctrl+C to copy to your clipboard. (⌘+C on Mac)
  1. diff --git a/.hgtags b/.hgtags
  2. --- a/.hgtags
  3. +++ b/.hgtags
  4. @@ -22,3 +22,7 @@ 2c21fc6135f832c7bbadf43586d2ffe585f02f60
  5.  1f1342f58d8e7d3972928f193e3f40cbf4425230 pentadactyl-1.0b4.1
  6.  962d8a1e823d0855e14aecaa294ef02e718401b1 pentadactyl-1.0b4.2
  7.  d783bcace8c6a24bee7bd22ead22fe44baaebe90 pentadactyl-1.0b4.3
  8. +b83bb8e6d273f71b1278c4ee03376594ad6dd039 pentadactyl-1.0b6
  9. +0000000000000000000000000000000000000000 vimperator-2.2~beta1
  10. +0000000000000000000000000000000000000000 vimperator-2.0~alpha1
  11. +2281deb4c1323643dfc130c43d79f1a14f4a761a pentadactyl-1.0b7
  12. diff --git a/HACKING b/HACKING
  13. --- a/HACKING
  14. +++ b/HACKING
  15. @@ -87,7 +87,7 @@ In general: Just look at the existing so
  16.    https://developer.mozilla.org/en/New_in_JavaScript_1.7#Block_scope_with_let
  17.  
  18.  * Reuse common local variable names E.g. "elem" is generally used for element,
  19. -  "win" for windows, "func" for functions,  "ret" for return values etc.
  20. +  "win" for windows, "func" for functions,  "res" for return values etc.
  21.  
  22.  * Prefer // over /* */ comments (exceptions for big comments are usually OK)
  23.    Right: if (HACK) // TODO: remove hack
  24. @@ -138,12 +138,7 @@ In general: Just look at the existing so
  25.  Functional tests are implemented using the Mozmill automated testing framework
  26.  -- https://developer.mozilla.org/en/Mozmill_Tests.
  27.  
  28. -A fresh profile is created for the duration of the test run, however, passing
  29. -arguments to the host application won't be supported until Mozmill 1.5.2, the
  30. -next release, so any user RC and plugin files should be temporarily disabled.
  31. -This can be done by adding the following to the head of the RC file:
  32. -set loadplugins=
  33. -finish
  34. +A fresh profile is created for the duration of the test run.
  35.  
  36.  The host application binary tested can be overridden via the HOSTAPP_PATH
  37.  makefile variable. E.g.,
  38. diff --git a/common/Makefile b/common/Makefile
  39. --- a/common/Makefile
  40. +++ b/common/Makefile
  41. @@ -1,13 +1,21 @@
  42.  #### configuration
  43.  
  44. +
  45. +AWK       ?= awk
  46. +B64ENCODE ?= base64
  47. +CURL      ?= curl
  48. +SED       := $(shell if [ "xoo" = x$$(echo foo | sed -E 's/f(o)//' 2>/dev/null) ]; \
  49. +                    then echo sed -E; else echo sed -r;                             \
  50. +                    fi)
  51. +
  52.  TOP           = $(shell pwd)
  53.  OS            = $(shell uname -s)
  54.  BUILD_DATE    = $(shell date "+%Y/%m/%d %H:%M:%S")
  55.  BASE          = $(TOP)/../common
  56.  GOOGLE_PROJ   = dactyl
  57.  GOOGLE       = https://$(GOOGLE_PROJ).googlecode.com/files
  58. -VERSION             ?= $(shell sed -n 's/.*em:version\(>\|="\)\(.*\)["<].*/\2/p' $(TOP)/install.rdf | sed 1q)
  59. -UUID                := $(shell sed -n 's/.*em:id\(>\|="\)\(.*\)["<].*/\2/p' $(TOP)/install.rdf | sed 1q)
  60. +VERSION             ?= $(shell $(SED) -n 's/.*em:version(>|=")(.*)["<].*/\2/p' $(TOP)/install.rdf | sed 1q)
  61. +UUID                := $(shell $(SED) -n 's/.*em:id(>|=")(.*)["<].*/\2/p' $(TOP)/install.rdf | sed 1q)
  62.  MANGLE      := $(shell date '+%s' | awk '{ printf "%x", $$1 }')
  63.  MOZMILL       = mozmill
  64.  HOSTAPP_PATH  = $(shell which $(HOSTAPP))
  65. @@ -46,13 +54,6 @@ RDF_IN        = $(RDF).in
  66.  
  67.  BUILD_DIR     = build.$(VERSION).$(OS)
  68.  
  69. -AWK       ?= awk
  70. -B64ENCODE ?= base64
  71. -CURL      ?= curl
  72. -SED       := $(shell if [ "xoo" = x$$(echo foo | sed -E 's/f(o)//' 2>/dev/null) ]; \
  73. -                    then echo sed -E; else echo sed -r;                             \
  74. -                    fi)
  75. -
  76.  .SILENT:
  77.  
  78.  #### rules
  79. diff --git a/common/bootstrap.js b/common/bootstrap.js
  80. --- a/common/bootstrap.js
  81. +++ b/common/bootstrap.js
  82. @@ -29,6 +29,8 @@ const resourceProto = Services.io.getPro
  83.  const categoryManager = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
  84.  const manager = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
  85.  
  86. +const BOOTSTRAP_JSM = "resource://dactyl/bootstrap.jsm";
  87. +
  88.  const BOOTSTRAP_CONTRACT = "@dactyl.googlecode.com/base/bootstrap";
  89.  JSMLoader = JSMLoader || BOOTSTRAP_CONTRACT in Cc && Cc[BOOTSTRAP_CONTRACT].getService().wrappedJSObject.loader;
  90.  
  91. @@ -41,7 +43,6 @@ if (!JSMLoader && "@mozilla.org/fuel/app
  92.                            .getService(Components.interfaces.extIApplication)
  93.                            .storage.get("dactyl.JSMLoader", null);
  94.  
  95. -
  96.  function reportError(e) {
  97.      dump("\ndactyl: bootstrap: " + e + "\n" + (e.stack || Error().stack) + "\n");
  98.      Cu.reportError(e);
  99. @@ -156,7 +157,6 @@ function init() {
  100.      let chars = "0123456789abcdefghijklmnopqrstuv";
  101.      for (let n = Date.now(); n; n = Math.round(n / chars.length))
  102.          suffix += chars[n % chars.length];
  103. -    suffix = "";
  104.  
  105.      for each (let line in manifest.split("\n")) {
  106.          let fields = line.split(/\s+/);
  107. @@ -173,12 +173,22 @@ function init() {
  108.              break;
  109.  
  110.          case "resource":
  111. +            var hardSuffix = /^[^\/]*/.exec(fields[2])[0];
  112. +
  113.              resources.push(fields[1], fields[1] + suffix);
  114.              resourceProto.setSubstitution(fields[1], getURI(fields[2]));
  115.              resourceProto.setSubstitution(fields[1] + suffix, getURI(fields[2]));
  116.          }
  117.      }
  118.  
  119. +    // Flush the cache if necessary, just to be paranoid
  120. +    let pref = "extensions.dactyl.cacheFlushCheck";
  121. +    let val  = addon.version + "-" + hardSuffix;
  122. +    if (!Services.prefs.prefHasUserValue(pref) || Services.prefs.getCharPref(pref) != val) {
  123. +        Services.obs.notifyObservers(null, "startupcache-invalidate", "");
  124. +        Services.prefs.setCharPref(pref, val);
  125. +    }
  126. +
  127.      try {
  128.          module("resource://dactyl-content/disable-acr.jsm").init(addon.id);
  129.      }
  130. @@ -186,16 +196,23 @@ function init() {
  131.          reportError(e);
  132.      }
  133.  
  134. -    if (JSMLoader && JSMLoader.bump !== 4) // Temporary hack
  135. +    if (JSMLoader) {
  136. +        if (Cu.unload) {
  137. +            Cu.unload(BOOTSTRAP_JSM);
  138. +            for (let [name] in Iterator(JSMLoader.globals))
  139. +                Cu.unload(~name.indexOf(":") ? name : "resource://dactyl" + JSMLoader.suffix + "/" + name);
  140. +        }
  141. +        else if (JSMLoader.bump != 5) // Temporary hack
  142.          Services.scriptloader.loadSubScript("resource://dactyl" + suffix + "/bootstrap.jsm",
  143. -            Cu.import("resource://dactyl/bootstrap.jsm", global));
  144. +                Cu.import(BOOTSTRAP_JSM, global));
  145. +    }
  146.  
  147. -    if (!JSMLoader || JSMLoader.bump !== 4)
  148. -        Cu.import("resource://dactyl/bootstrap.jsm", global);
  149. +    if (!JSMLoader || JSMLoader.bump !== 5 || Cu.unload)
  150. +        Cu.import(BOOTSTRAP_JSM, global);
  151.  
  152.      JSMLoader.bootstrap = this;
  153.  
  154. -    JSMLoader.load("resource://dactyl/bootstrap.jsm", global);
  155. +    JSMLoader.load(BOOTSTRAP_JSM, global);
  156.  
  157.      JSMLoader.init(suffix);
  158.      JSMLoader.load("base.jsm", global);
  159. @@ -211,9 +228,9 @@ function init() {
  160.                  wrappedJSObject: {}
  161.              },
  162.              createInstance: function () this.instance
  163. -        })
  164. +        });
  165.  
  166. -    Cc[BOOTSTRAP_CONTRACT].getService().wrappedJSObject.loader = JSMLoader;
  167. +    Cc[BOOTSTRAP_CONTRACT].getService().wrappedJSObject.loader = !Cu.unload && JSMLoader;
  168.  
  169.      for each (let component in components)
  170.          component.register();
  171. @@ -233,11 +250,11 @@ function shutdown(data, reason) {
  172.              reportError(e);
  173.          }
  174.  
  175. -        if ([ADDON_UPGRADE, ADDON_DOWNGRADE, ADDON_UNINSTALL].indexOf(reason) >= 0)
  176. +        if (~[ADDON_UPGRADE, ADDON_DOWNGRADE, ADDON_UNINSTALL].indexOf(reason))
  177.              Services.obs.notifyObservers(null, "dactyl-purge", null);
  178.  
  179. -        Services.obs.notifyObservers(null, "dactyl-cleanup", null);
  180. -        Services.obs.notifyObservers(null, "dactyl-cleanup-modules", null);
  181. +        Services.obs.notifyObservers(null, "dactyl-cleanup", reasonToString(reason));
  182. +        Services.obs.notifyObservers(null, "dactyl-cleanup-modules", reasonToString(reason));
  183.  
  184.          JSMLoader.purge();
  185.          for each (let [category, entry] in categories)
  186. diff --git a/common/components/protocols.js b/common/components/protocols.js
  187. --- a/common/components/protocols.js
  188. +++ b/common/components/protocols.js
  189. @@ -248,7 +248,7 @@ function LocaleChannel(pkg, path, orig)
  190.  
  191.  function StringChannel(data, contentType, uri) {
  192.      let channel = services.StreamChannel(uri);
  193. -    channel.contentStream = services.StringStream(data);
  194. +    channel.contentStream = services.CharsetConv("UTF-8").convertToInputStream(data);
  195.      if (contentType)
  196.          channel.contentType = contentType;
  197.      channel.contentCharset = "UTF-8";
  198. @@ -293,12 +293,12 @@ function XMLChannel(uri, contentType) {
  199.      if (doctype) {
  200.          this.writes.push(doctype + "[\n");
  201.          try {
  202. -            this.writes.push(services.io.newChannel(url, null, null).open())
  203. +            this.writes.push(services.io.newChannel(url, null, null).open());
  204.          }
  205.          catch (e) {}
  206.          if (!open)
  207.              this.writes.push("\n]");
  208. -        this.writes.push(post)
  209. +        this.writes.push(post);
  210.      }
  211.      this.writes.push(channelStream);
  212.  
  213. diff --git a/common/content/abbreviations.js b/common/content/abbreviations.js
  214. --- a/common/content/abbreviations.js
  215. +++ b/common/content/abbreviations.js
  216. @@ -8,6 +8,23 @@
  217.  
  218.  /** @scope modules */
  219.  
  220. +/**
  221. + * A user-defined input mode binding of a typed string to an automatically
  222. + * inserted expansion string.
  223. + *
  224. + * Abbreviations have a left-hand side (LHS) whose text is replaced by that of
  225. + * the right-hand side (RHS) when triggered by an Input mode expansion key.
  226. + * E.g. an abbreviation with a LHS of "gop" and RHS of "Grand Old Party" will
  227. + * replace the former with the latter.
  228. + *
  229. + * @param {[Mode]} modes The modes in which this abbreviation is active.
  230. + * @param {string} lhs The left hand side of the abbreviation; the text to
  231. + *     be replaced.
  232. + * @param {string|function(nsIEditor):string} rhs The right hand side of
  233. + *     the abbreviation; the replacement text. This may either be a string
  234. + *     literal or a function that will be passed the appropriate nsIEditor.
  235. + * @private
  236. + */
  237.  var Abbreviation = Class("Abbreviation", {
  238.      init: function (modes, lhs, rhs) {
  239.          this.modes = modes.sort();
  240. @@ -15,20 +32,61 @@ var Abbreviation = Class("Abbreviation",
  241.          this.rhs = rhs;
  242.      },
  243.  
  244. +    /**
  245. +     * Returns true if this abbreviation's LHS and RHS are equal to those in
  246. +     * *other*.
  247. +     *
  248. +     * @param {Abbreviation} other The abbreviation to test.
  249. +     * @returns {boolean} The result of the comparison.
  250. +     */
  251.      equals: function (other) this.lhs == other.lhs && this.rhs == other.rhs,
  252.  
  253. +    /**
  254. +     * Returns the abbreviation's expansion text.
  255. +     *
  256. +     * @param {nsIEditor} editor The editor in which abbreviation expansion is
  257. +     *     occurring.
  258. +     * @returns {string}
  259. +     */
  260.      expand: function (editor) String(callable(this.rhs) ? this.rhs(editor) : this.rhs),
  261.  
  262. +    /**
  263. +     * Returns true if this abbreviation is defined for all *modes*.
  264. +     *
  265. +     * @param {[Mode]} modes The modes to test.
  266. +     * @returns {boolean} The result of the comparison.
  267. +     */
  268.      modesEqual: function (modes) array.equals(this.modes, modes),
  269.  
  270. +    /**
  271. +     * Returns true if this abbreviation is defined for *mode*.
  272. +     *
  273. +     * @param {Mode} mode The mode to test.
  274. +     * @returns {boolean} The result of the comparison.
  275. +     */
  276.      inMode: function (mode) this.modes.some(function (_mode) _mode == mode),
  277.  
  278. +    /**
  279. +     * Returns true if this abbreviation is defined in any of *modes*.
  280. +     *
  281. +     * @param {[Modes]} modes The modes to test.
  282. +     * @returns {boolean} The result of the comparison.
  283. +     */
  284.      inModes: function (modes) modes.some(function (mode) this.inMode(mode), this),
  285.  
  286. +    /**
  287. +     * Remove *mode* from the list of supported modes for this abbreviation.
  288. +     *
  289. +     * @param {Mode} mode The mode to remove.
  290. +     */
  291.      removeMode: function (mode) {
  292.          this.modes = this.modes.filter(function (m) m != mode).sort();
  293.      },
  294.  
  295. +    /**
  296. +     * @property {string} The mode display characters associated with the
  297. +     *     supported mode combination.
  298. +     */
  299.      get modeChar() Abbreviation.modeChar(this.modes)
  300.  }, {
  301.      modeChar: function (_modes) {
  302. @@ -45,6 +103,7 @@ var AbbrevHive = Class("AbbrevHive", Con
  303.          this._store = {};
  304.      },
  305.  
  306. +    /** @property {boolean} True if there are no abbreviations. */
  307.      get empty() !values(this._store).nth(util.identity, 0),
  308.  
  309.      /**
  310. @@ -68,14 +127,15 @@ var AbbrevHive = Class("AbbrevHive", Con
  311.       *
  312.       * @param {Mode} mode The mode of the abbreviation.
  313.       * @param {string} lhs The LHS of the abbreviation.
  314. +     * @returns {Abbreviation} The matching abbreviation.
  315.       */
  316.      get: function (mode, lhs) {
  317.          let abbrevs = this._store[mode];
  318. -        return abbrevs && set.has(abbrevs, lhs) ? abbrevs[lhs] : null;
  319. +        return abbrevs && Set.has(abbrevs, lhs) ? abbrevs[lhs] : null;
  320.      },
  321.  
  322.      /**
  323. -     * @property {Abbreviation[]} The list of the abbreviations merged from
  324. +     * @property {[Abbreviation]} The list of the abbreviations merged from
  325.       *     each mode.
  326.       */
  327.      get merged() {
  328. @@ -189,13 +249,15 @@ var Abbreviations = Module("abbreviation
  329.      },
  330.  
  331.      /**
  332. -     * Lists all abbreviations matching *modes* and *lhs*.
  333. +     * Lists all abbreviations matching *modes*, *lhs* and optionally *hives*.
  334.       *
  335.       * @param {Array} modes List of modes.
  336.       * @param {string} lhs The LHS of the abbreviation.
  337. +     * @param {[Hive]} hives List of hives.
  338. +     * @optional
  339.       */
  340. -    list: function (modes, lhs) {
  341. -        let hives = contexts.allGroups.abbrevs.filter(function (h) !h.empty);
  342. +    list: function (modes, lhs, hives) {
  343. +        let hives = hives || contexts.allGroups.abbrevs.filter(function (h) !h.empty);
  344.  
  345.          function abbrevs(hive)
  346.              hive.merged.filter(function (abbr) (abbr.inModes(modes) && abbr.lhs.indexOf(lhs) == 0));
  347. @@ -203,9 +265,9 @@ var Abbreviations = Module("abbreviation
  348.          let list = <table>
  349.                  <tr highlight="Title">
  350.                      <td/>
  351. -                    <td style="padding-right: 1em;">Mode</td>
  352. -                    <td style="padding-right: 1em;">Abbrev</td>
  353. -                    <td style="padding-right: 1em;">Replacement</td>
  354. +                    <td style="padding-right: 1em;">{_("title.Mode")}</td>
  355. +                    <td style="padding-right: 1em;">{_("title.Abbrev")}</td>
  356. +                    <td style="padding-right: 1em;">{_("title.Replacement")}</td>
  357.                  </tr>
  358.                  <col style="min-width: 6em; padding-right: 1em;"/>
  359.                  {
  360. @@ -224,7 +286,7 @@ var Abbreviations = Module("abbreviation
  361.  
  362.          // TODO: Move this to an ItemList to show this automatically
  363.          if (list.*.length() === list.text().length() + 2)
  364. -            dactyl.echomsg(_("abbrev.none"));
  365. +            dactyl.echomsg(_("abbreviation.none"));
  366.          else
  367.              commandline.commandOutput(list);
  368.      }
  369. @@ -245,7 +307,6 @@ var Abbreviations = Module("abbreviation
  370.              context.completions = group.merged.filter(fn);
  371.          };
  372.      },
  373. -
  374.      commands: function () {
  375.          function addAbbreviationCommands(modes, ch, modeDescription) {
  376.              modes.sort();
  377. @@ -258,14 +319,17 @@ var Abbreviations = Module("abbreviation
  378.                      dactyl.assert(!args.length || abbreviations._check.test(lhs),
  379.                                    _("error.invalidArgument"));
  380.  
  381. -                    if (!rhs)
  382. -                        abbreviations.list(modes, lhs || "");
  383. +                    if (!rhs) {
  384. +                        let hives = args.explicitOpts["-group"] ? [args["-group"]] : null;
  385. +                        abbreviations.list(modes, lhs || "", hives);
  386. +                    }
  387.                      else {
  388.                          if (args["-javascript"])
  389.                              rhs = contexts.bindMacro({ literalArg: rhs }, "-javascript", ["editor"]);
  390.                          args["-group"].add(modes, lhs, rhs);
  391.                      }
  392.                  }, {
  393. +                    identifier: "abbreviate",
  394.                      completer: function (context, args) {
  395.                          if (args.length == 1)
  396.                              return completion.abbreviation(context, modes, args["-group"]);
  397. @@ -301,7 +365,7 @@ var Abbreviations = Module("abbreviation
  398.                      if (args.bang)
  399.                          args["-group"].clear(modes);
  400.                      else if (!args["-group"].remove(modes, args[0]))
  401. -                        return dactyl.echoerr(_("abbrev.noSuch"));
  402. +                        return dactyl.echoerr(_("abbreviation.noSuch"));
  403.                  }, {
  404.                      argCount: "?",
  405.                      bang: true,
  406. @@ -312,8 +376,9 @@ var Abbreviations = Module("abbreviation
  407.          }
  408.  
  409.          addAbbreviationCommands([modes.INSERT, modes.COMMAND_LINE], "", "");
  410. -        addAbbreviationCommands([modes.INSERT], "i", "insert");
  411. -        addAbbreviationCommands([modes.COMMAND_LINE], "c", "command line");
  412. +        [modes.INSERT, modes.COMMAND_LINE].forEach(function (mode) {
  413. +            addAbbreviationCommands([mode], mode.char, mode.displayName);
  414. +        });
  415.      }
  416.  });
  417.  
  418. diff --git a/common/content/autocommands.js b/common/content/autocommands.js
  419. --- a/common/content/autocommands.js
  420. +++ b/common/content/autocommands.js
  421. @@ -47,7 +47,7 @@ var AutoCmdHive = Class("AutoCmdHive", C
  422.       *
  423.       * @param {string} event The event name filter.
  424.       * @param {string} pattern The URL pattern filter.
  425. -     * @returns {AutoCommand[]}
  426. +     * @returns {[AutoCommand]}
  427.       */
  428.      get: function (event, pattern) {
  429.          return this._store.filter(function (autoCmd) autoCmd.match(event, regexp));
  430. @@ -84,12 +84,17 @@ var AutoCommands = Module("autocommands"
  431.      remove: deprecated("group.autocmd.remove", { get: function remove() autocommands.user.closure.remove }),
  432.  
  433.      /**
  434. -     * Lists all autocommands with a matching *event* and *regexp*.
  435. +     * Lists all autocommands with a matching *event*, *regexp* and optionally
  436. +     * *hives*.
  437.       *
  438.       * @param {string} event The event name filter.
  439.       * @param {string} regexp The URL pattern filter.
  440. +     * @param {[Hive]} hives List of hives.
  441. +     * @optional
  442.       */
  443. -    list: function (event, regexp) {
  444. +    list: function (event, regexp, hives) {
  445. +
  446. +        let hives = hives || this.activeHives;
  447.  
  448.          function cmds(hive) {
  449.              let cmds = {};
  450. @@ -108,7 +113,7 @@ var AutoCommands = Module("autocommands"
  451.                      <td colspan="3">----- Auto Commands -----</td>
  452.                  </tr>
  453.                  {
  454. -                    template.map(this.activeHives, function (hive)
  455. +                    template.map(hives, function (hive)
  456.                          <tr highlight="Title">
  457.                              <td colspan="3">{hive.name}</td>
  458.                          </tr> +
  459. @@ -143,7 +148,7 @@ var AutoCommands = Module("autocommands"
  460.          let lastPattern = null;
  461.          var { url, doc } = args;
  462.          if (url)
  463. -            uri = util.newURI(url);
  464. +            uri = util.createURI(url);
  465.          else
  466.              var { uri, doc } = buffer;
  467.  
  468. @@ -200,7 +205,7 @@ var AutoCommands = Module("autocommands"
  469.                              args["-group"].remove(event, regexp); // remove all
  470.                      }
  471.                      else
  472. -                        autocommands.list(event, regexp); // list all
  473. +                        autocommands.list(event, regexp, args.explicitOpts["-group"] ? [args["-group"]] : null); // list all
  474.                  }
  475.              }, {
  476.                  bang: true,
  477. diff --git a/common/content/bookmarks.js b/common/content/bookmarks.js
  478. --- a/common/content/bookmarks.js
  479. +++ b/common/content/bookmarks.js
  480. @@ -11,6 +11,10 @@ var DEFAULT_FAVICON = "chrome://mozapps/
  481.  // also includes methods for dealing with keywords and search engines
  482.  var Bookmarks = Module("bookmarks", {
  483.      init: function () {
  484. +        this.timer = Timer(0, 100, function () {
  485. +            this.checkBookmarked(buffer.uri);
  486. +        }, this);
  487. +
  488.          storage.addObserver("bookmark-cache", function (key, event, arg) {
  489.              if (["add", "change", "remove"].indexOf(event) >= 0)
  490.                  autocommands.trigger("Bookmark" + event[0].toUpperCase() + event.substr(1),
  491. @@ -20,10 +24,17 @@ var Bookmarks = Module("bookmarks", {
  492.                               valueOf: function () arg
  493.                           }
  494.                       }, arg));
  495. -            statusline.updateStatus();
  496. +            bookmarks.timer.tell();
  497.          }, window);
  498.      },
  499.  
  500. +    signals: {
  501. +        "browser.locationChange": function (webProgress, request, uri) {
  502. +            statusline.bookmarked = false;
  503. +            this.checkBookmarked(uri);
  504. +        }
  505. +    },
  506. +
  507.      get format() ({
  508.          anchored: false,
  509.          title: ["URL", "Info"],
  510. @@ -103,7 +114,7 @@ var Bookmarks = Module("bookmarks", {
  511.       *
  512.       * @param {Element} elem A form element for which to add a keyword.
  513.       */
  514. -    addSearchKeyword: function (elem) {
  515. +    addSearchKeyword: function addSearchKeyword(elem) {
  516.          if (elem instanceof HTMLFormElement || elem.form)
  517.              var [url, post, charset] = util.parseForm(elem);
  518.          else
  519. @@ -119,6 +130,17 @@ var Bookmarks = Module("bookmarks", {
  520.              commands.commandToString({ command: "bmark", options: options, arguments: [url] }) + " -keyword ");
  521.      },
  522.  
  523. +    checkBookmarked: function checkBookmarked(uri) {
  524. +        if (PlacesUtils.asyncGetBookmarkIds)
  525. +            PlacesUtils.asyncGetBookmarkIds(uri, function (ids) {
  526. +                statusline.bookmarked = ids.length;
  527. +            });
  528. +        else
  529. +            this.timeout(function () {
  530. +                statusline.bookmarked = bookmarkcache.isBookmarked(uri);
  531. +            });
  532. +    },
  533. +
  534.      /**
  535.       * Toggles the bookmarked state of the given URL. If the URL is
  536.       * bookmarked, all bookmarks for said URL are removed.
  537. @@ -197,7 +219,7 @@ var Bookmarks = Module("bookmarks", {
  538.              if (!alias)
  539.                  alias = "search"; // for search engines which we can't find a suitable alias
  540.  
  541. -            if (set.has(aliases, alias))
  542. +            if (Set.has(aliases, alias))
  543.                  alias += ++aliases[alias];
  544.              else
  545.                  aliases[alias] = 0;
  546. @@ -225,7 +247,7 @@ var Bookmarks = Module("bookmarks", {
  547.      getSuggestions: function getSuggestions(engineName, query, callback) {
  548.          const responseType = "application/x-suggestions+json";
  549.  
  550. -        let engine = set.has(this.searchEngines, engineName) && this.searchEngines[engineName];
  551. +        let engine = Set.has(this.searchEngines, engineName) && this.searchEngines[engineName];
  552.          if (engine && engine.supportsResponseType(responseType))
  553.              var queryURI = engine.getSubmission(query, responseType).uri.spec;
  554.          if (!queryURI)
  555. @@ -274,7 +296,7 @@ var Bookmarks = Module("bookmarks", {
  556.              param = query.substr(offset + 1);
  557.          }
  558.  
  559. -        var engine = set.has(bookmarks.searchEngines, keyword) && bookmarks.searchEngines[keyword];
  560. +        var engine = Set.has(bookmarks.searchEngines, keyword) && bookmarks.searchEngines[keyword];
  561.          if (engine) {
  562.              if (engine.searchForm && !param)
  563.                  return engine.searchForm;
  564. @@ -382,7 +404,7 @@ var Bookmarks = Module("bookmarks", {
  565.                  let frames = buffer.allFrames();
  566.                  if (!args.bang)
  567.                      return  [
  568. -                        [win.document.title, frames.length == 1 ? "Current Location" : "Frame: " + win.location.href]
  569. +                        [win.document.title, frames.length == 1 ? /*L*/"Current Location" : /*L*/"Frame: " + win.location.href]
  570.                          for ([, win] in Iterator(frames))];
  571.                  context.keys.text = "title";
  572.                  context.keys.description = "url";
  573. @@ -442,7 +464,7 @@ var Bookmarks = Module("bookmarks", {
  574.                          context.title = ["Page URL"];
  575.                          let frames = buffer.allFrames();
  576.                          context.completions = [
  577. -                            [win.document.documentURI, frames.length == 1 ? "Current Location" : "Frame: " + win.document.title]
  578. +                            [win.document.documentURI, frames.length == 1 ? /*L*/"Current Location" : /*L*/"Frame: " + win.document.title]
  579.                              for ([, win] in Iterator(frames))];
  580.                          return;
  581.                      }
  582. @@ -485,11 +507,11 @@ var Bookmarks = Module("bookmarks", {
  583.              "Delete a bookmark",
  584.              function (args) {
  585.                  if (args.bang)
  586. -                    commandline.input("This will delete all bookmarks. Would you like to continue? (yes/[no]) ",
  587. +                    commandline.input(_("bookmark.prompt.deleteAll") + " ",
  588.                          function (resp) {
  589.                              if (resp && resp.match(/^y(es)?$/i)) {
  590.                                  bookmarks.remove(Object.keys(bookmarkcache.bookmarks));
  591. -                                dactyl.echomsg(_("bookmark.allGone"));
  592. +                                dactyl.echomsg(_("bookmark.allDeleted"));
  593.                              }
  594.                          });
  595.                  else {
  596. @@ -499,7 +521,7 @@ var Bookmarks = Module("bookmarks", {
  597.                          let context = CompletionContext(args.join(" "));
  598.                          context.fork("bookmark", 0, completion, "bookmark",
  599.                                       args["-tags"], { keyword: args["-keyword"], title: args["-title"] });
  600. -                        var deletedCount = bookmarks.remove(context.allItems.items.map(function (item) item.item.id));
  601. +                        deletedCount = bookmarks.remove(context.allItems.items.map(function (item) item.item.id));
  602.                      }
  603.  
  604.                      dactyl.echomsg({ message: _("bookmark.deleted", deletedCount) });
  605. @@ -604,7 +626,7 @@ var Bookmarks = Module("bookmarks", {
  606.              if (item && item.url.indexOf("%s") > -1)
  607.                  context.fork("keyword/" + keyword, keyword.length + space.length, null, function (context) {
  608.                      context.format = history.format;
  609. -                    context.title = [keyword + " Quick Search"];
  610. +                    context.title = [/*L*/keyword + " Quick Search"];
  611.                      // context.background = true;
  612.                      context.compare = CompletionContext.Sort.unsorted;
  613.                      context.generate = function () {
  614. @@ -649,7 +671,7 @@ var Bookmarks = Module("bookmarks", {
  615.                      return;
  616.                  let ctxt = context.fork(name, 0);
  617.  
  618. -                ctxt.title = [engine.description + " Suggestions"];
  619. +                ctxt.title = [/*L*/engine.description + " Suggestions"];
  620.                  ctxt.keys = { text: util.identity, description: function () "" };
  621.                  ctxt.compare = CompletionContext.Sort.unsorted;
  622.                  ctxt.filterFunc = null;
  623. diff --git a/common/content/browser.js b/common/content/browser.js
  624. --- a/common/content/browser.js
  625. +++ b/common/content/browser.js
  626. @@ -28,8 +28,11 @@ var Browser = Module("browser", XPCOM(Ci
  627.          "content-document-global-created": function (win, uri) {
  628.              let top = util.topWindow(win);
  629.  
  630. -            if (top == window)
  631. -                this._triggerLoadAutocmd("PageLoadPre", win.document, win.location.href != "null" ? window.location.href : uri);
  632. +            if (uri == "null")
  633. +                uri = null;
  634. +
  635. +            if (top == window && (win.location.href || uri))
  636. +                this._triggerLoadAutocmd("PageLoadPre", win.document, win.location.href || uri);
  637.          }
  638.      },
  639.  
  640. @@ -43,12 +46,18 @@ var Browser = Module("browser", XPCOM(Ci
  641.              title: doc.title
  642.          };
  643.  
  644. -        if (dactyl.has("tabs")) {
  645. +        if (!dactyl.has("tabs"))
  646. +            update(args, { doc: doc, win: doc.defaultView });
  647. +        else {
  648.              args.tab = tabs.getContentIndex(doc) + 1;
  649.              args.doc = {
  650.                  valueOf: function () doc,
  651.                  toString: function () "tabs.getTab(" + (args.tab - 1) + ").linkedBrowser.contentDocument"
  652.              };
  653. +            args.win = {
  654. +                valueOf: function () doc.defaultView,
  655. +                toString: function () "tabs.getTab(" + (args.tab - 1) + ").linkedBrowser.contentWindow"
  656. +            };
  657.          }
  658.  
  659.          autocommands.trigger(name, args);
  660. @@ -95,26 +104,29 @@ var Browser = Module("browser", XPCOM(Ci
  661.      progressListener: {
  662.          // XXX: function may later be needed to detect a canceled synchronous openURL()
  663.          onStateChange: util.wrapCallback(function onStateChange(webProgress, request, flags, status) {
  664. -            onStateChange.superapply(this, arguments);
  665. -            // STATE_IS_DOCUMENT | STATE_IS_WINDOW is important, because we also
  666. -            // receive statechange events for loading images and other parts of the web page
  667. -            if (flags & (Ci.nsIWebProgressListener.STATE_IS_DOCUMENT | Ci.nsIWebProgressListener.STATE_IS_WINDOW)) {
  668. +            const L = Ci.nsIWebProgressListener;
  669. +
  670. +            if (request)
  671.                  dactyl.applyTriggerObserver("browser.stateChange", arguments);
  672. +
  673. +            if (flags & (L.STATE_IS_DOCUMENT | L.STATE_IS_WINDOW)) {
  674.                  // This fires when the load event is initiated
  675.                  // only thrown for the current tab, not when another tab changes
  676. -                if (flags & Ci.nsIWebProgressListener.STATE_START) {
  677. +                if (flags & L.STATE_START) {
  678.                      while (document.commandDispatcher.focusedWindow == webProgress.DOMWindow
  679.                             && modes.have(modes.INPUT))
  680.                          modes.pop();
  681.  
  682.                  }
  683. -                else if (flags & Ci.nsIWebProgressListener.STATE_STOP) {
  684. +                else if (flags & L.STATE_STOP) {
  685.                      // Workaround for bugs 591425 and 606877, dactyl bug #81
  686.                      config.browser.mCurrentBrowser.collapsed = false;
  687.                      if (!dactyl.focusedElement || dactyl.focusedElement === document.documentElement)
  688.                          dactyl.focusContent();
  689.                  }
  690.              }
  691. +
  692. +            onStateChange.superapply(this, arguments);
  693.          }),
  694.          onSecurityChange: util.wrapCallback(function onSecurityChange(webProgress, request, state) {
  695.              onSecurityChange.superapply(this, arguments);
  696. @@ -200,9 +212,13 @@ var Browser = Module("browser", XPCOM(Ci
  697.              ["o"], "Open one or more URLs",
  698.              function () { CommandExMode().open("open "); });
  699.  
  700. +        function decode(uri) util.losslessDecodeURI(uri)
  701. +                                 .replace(/%20(?!(?:%20)*$)/g, " ")
  702. +                                 .replace(RegExp(options["urlseparator"], "g"), encodeURIComponent);
  703. +
  704.          mappings.add([modes.NORMAL], ["O"],
  705.              "Open one or more URLs, based on current location",
  706. -            function () { CommandExMode().open("open " + buffer.uri.spec); });
  707. +            function () { CommandExMode().open("open " + decode(buffer.uri.spec)); });
  708.  
  709.          mappings.add([modes.NORMAL], ["t"],
  710.              "Open one or more URLs in a new tab",
  711. @@ -210,7 +226,7 @@ var Browser = Module("browser", XPCOM(Ci
  712.  
  713.          mappings.add([modes.NORMAL], ["T"],
  714.              "Open one or more URLs in a new tab, based on current location",
  715. -            function () { CommandExMode().open("tabopen " + buffer.uri.spec); });
  716. +            function () { CommandExMode().open("tabopen " + decode(buffer.uri.spec)); });
  717.  
  718.          mappings.add([modes.NORMAL], ["w"],
  719.              "Open one or more URLs in a new window",
  720. @@ -218,24 +234,24 @@ var Browser = Module("browser", XPCOM(Ci
  721.  
  722.          mappings.add([modes.NORMAL], ["W"],
  723.              "Open one or more URLs in a new window, based on current location",
  724. -            function () { CommandExMode().open("winopen " + buffer.uri.spec); });
  725. +            function () { CommandExMode().open("winopen " + decode(buffer.uri.spec)); });
  726.  
  727. -        mappings.add([modes.NORMAL], ["~"],
  728. +        mappings.add([modes.NORMAL], ["<open-home-directory>", "~"],
  729.              "Open home directory",
  730.              function () { dactyl.open("~"); });
  731.  
  732. -        mappings.add([modes.NORMAL], ["gh"],
  733. +        mappings.add([modes.NORMAL], ["<open-homepage>", "gh"],
  734.              "Open homepage",
  735.              function () { BrowserHome(); });
  736.  
  737. -        mappings.add([modes.NORMAL], ["gH"],
  738. +        mappings.add([modes.NORMAL], ["<tab-open-homepage>", "gH"],
  739.              "Open homepage in a new tab",
  740.              function () {
  741.                  let homepages = gHomeButton.getHomePage();
  742.                  dactyl.open(homepages, { from: "homepage", where: dactyl.NEW_TAB });
  743.              });
  744.  
  745. -        mappings.add([modes.MAIN], ["<C-l>"],
  746. +        mappings.add([modes.MAIN], ["<redraw-screen>", "<C-l>"],
  747.              "Redraw the screen",
  748.              function () { ex.redraw(); });
  749.      }
  750. diff --git a/common/content/buffer.js b/common/content/buffer.js
  751. --- a/common/content/buffer.js
  752. +++ b/common/content/buffer.js
  753. @@ -19,6 +19,24 @@ var Buffer = Module("buffer", {
  754.          this.evaluateXPath = util.evaluateXPath;
  755.          this.pageInfo = {};
  756.  
  757. +        this.addPageInfoSection("e", "Search Engines", function (verbose) {
  758. +
  759. +            let n = 1;
  760. +            let nEngines = 0;
  761. +            for (let { document: doc } in values(buffer.allFrames())) {
  762. +                let engines = util.evaluateXPath(["link[@href and @rel='search' and @type='application/opensearchdescription+xml']"], doc);
  763. +                nEngines += engines.snapshotLength;
  764. +
  765. +                if (verbose)
  766. +                    for (let link in engines)
  767. +                        yield [link.title || /*L*/ "Engine " + n++,
  768. +                               <a xmlns={XHTML} href={link.href} onclick="if (event.button == 0) { window.external.AddSearchProvider(this.href); return false; }" highlight="URL">{link.href}</a>];
  769. +            }
  770. +
  771. +            if (!verbose && nEngines)
  772. +                yield nEngines + /*L*/" engine" + (nEngines > 1 ? "s" : "");
  773. +        });
  774. +
  775.          this.addPageInfoSection("f", "Feeds", function (verbose) {
  776.              const feedTypes = {
  777.                  "application/rss+xml": "RSS",
  778. @@ -75,7 +93,7 @@ var Buffer = Module("buffer", {
  779.              }
  780.  
  781.              if (!verbose && nFeed)
  782. -                yield nFeed + " feed" + (nFeed > 1 ? "s" : "");
  783. +                yield nFeed + /*L*/" feed" + (nFeed > 1 ? "s" : "");
  784.          });
  785.  
  786.          this.addPageInfoSection("g", "General Info", function (verbose) {
  787. @@ -110,7 +128,7 @@ var Buffer = Module("buffer", {
  788.  
  789.              if (!verbose) {
  790.                  if (pageSize[0])
  791. -                    yield (pageSize[1] || pageSize[0]) + " bytes";
  792. +                    yield (pageSize[1] || pageSize[0]) + /*L*/" bytes";
  793.                  yield lastMod;
  794.                  return;
  795.              }
  796. @@ -134,6 +152,9 @@ var Buffer = Module("buffer", {
  797.          });
  798.  
  799.          this.addPageInfoSection("m", "Meta Tags", function (verbose) {
  800. +            if (!verbose)
  801. +                return [];
  802. +
  803.              // get meta tag data, sort and put into pageMeta[]
  804.              let metaNodes = buffer.focusedFrame.document.getElementsByTagName("meta");
  805.  
  806. @@ -141,9 +162,48 @@ var Buffer = Module("buffer", {
  807.                          .sort(function (a, b) util.compareIgnoreCase(a[0], b[0]));
  808.          });
  809.  
  810. +        let identity = window.gIdentityHandler;
  811. +        this.addPageInfoSection("s", "Security", function (verbose) {
  812. +            if (!verbose || !identity)
  813. +                return; // For now
  814. +
  815. +            // Modified from Firefox
  816. +            function location(data) array.compact([
  817. +                data.city, data.state, data.country
  818. +            ]).join(", ");
  819. +
  820. +            switch (statusline.security) {
  821. +            case "secure":
  822. +            case "extended":
  823. +                var data = identity.getIdentityData();
  824. +
  825. +                yield ["Host", identity.getEffectiveHost()];
  826. +
  827. +                if (statusline.security === "extended")
  828. +                    yield ["Owner", data.subjectOrg];
  829. +                else
  830. +                    yield ["Owner", _("pageinfo.s.ownerUnverified", data.subjectOrg)];
  831. +
  832. +                if (location(data).length)
  833. +                    yield ["Location", location(data)];
  834. +
  835. +                yield ["Verified by", data.caOrg];
  836. +
  837. +                if (identity._overrideService.hasMatchingOverride(identity._lastLocation.hostname,
  838. +                                                              (identity._lastLocation.port || 443),
  839. +                                                              data.cert, {}, {}))
  840. +                    yield ["User exception", /*L*/"true"];
  841. +                break;
  842. +            }
  843. +        });
  844. +
  845.          dactyl.commands["buffer.viewSource"] = function (event) {
  846.              let elem = event.originalTarget;
  847. -            buffer.viewSource([elem.getAttribute("href"), Number(elem.getAttribute("line"))]);
  848. +            let obj = { url: elem.getAttribute("href"), line: Number(elem.getAttribute("line")) };
  849. +            if (elem.hasAttribute("column"))
  850. +                obj.column = elem.getAttribute("column");
  851. +
  852. +            buffer.viewSource(obj);
  853.          };
  854.      },
  855.  
  856. @@ -311,7 +371,7 @@ var Buffer = Module("buffer", {
  857.      allFrames: function allFrames(win, focusedFirst) {
  858.          let frames = [];
  859.          (function rec(frame) {
  860. -            if (frame.document.body instanceof HTMLBodyElement)
  861. +            if (true || frame.document.body instanceof HTMLBodyElement)
  862.                  frames.push(frame);
  863.              Array.forEach(frame.frames, rec);
  864.          })(win || content);
  865. @@ -340,7 +400,7 @@ var Buffer = Module("buffer", {
  866.       * @returns {string}
  867.       */
  868.      get currentWord() Buffer.currentWord(this.focusedFrame),
  869. -    getCurrentWord: deprecated("buffer.currentWord", function getCurrentWord() this.currentWord),
  870. +    getCurrentWord: deprecated("buffer.currentWord", function getCurrentWord() Buffer.currentWord(this.focusedFrame, true)),
  871.  
  872.      /**
  873.       * Returns true if a scripts are allowed to focus the given input
  874. @@ -352,8 +412,16 @@ var Buffer = Module("buffer", {
  875.      focusAllowed: function focusAllowed(elem) {
  876.          if (elem instanceof Window && !Editor.getEditor(elem))
  877.              return true;
  878. +
  879.          let doc = elem.ownerDocument || elem.document || elem;
  880. -        return !options["strictfocus"] || doc.dactylFocusAllowed;
  881. +        switch (options.get("strictfocus").getKey(doc.documentURIObject || util.newURI(doc.documentURI), "moderate")) {
  882. +        case "despotic":
  883. +            return elem.dactylFocusAllowed || elem.frameElement && elem.frameElement.dactylFocusAllowed;
  884. +        case "moderate":
  885. +            return doc.dactylFocusAllowed || elem.frameElement && elem.frameElement.ownerDocument.dactylFocusAllowed;
  886. +        default:
  887. +            return true;
  888. +        }
  889.      },
  890.  
  891.      /**
  892. @@ -365,6 +433,7 @@ var Buffer = Module("buffer", {
  893.       */
  894.      focusElement: function focusElement(elem) {
  895.          let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem;
  896. +        elem.dactylFocusAllowed = true;
  897.          win.document.dactylFocusAllowed = true;
  898.  
  899.          if (isinstance(elem, [HTMLFrameElement, HTMLIFrameElement]))
  900. @@ -413,7 +482,7 @@ var Buffer = Module("buffer", {
  901.      },
  902.  
  903.      /**
  904. -     * Find the counth last link on a page matching one of the given
  905. +     * Find the *count*th last link on a page matching one of the given
  906.       * regular expressions, or with a @rel or @rev attribute matching
  907.       * the given relation. Each frame is searched beginning with the
  908.       * last link and progressing to the first, once checking for
  909. @@ -483,7 +552,7 @@ var Buffer = Module("buffer", {
  910.       */
  911.      followLink: function followLink(elem, where) {
  912.          let doc = elem.ownerDocument;
  913. -        let view = doc.defaultView;
  914. +        let win = doc.defaultView;
  915.          let { left: offsetX, top: offsetY } = elem.getBoundingClientRect();
  916.  
  917.          if (isinstance(elem, [HTMLFrameElement, HTMLIFrameElement]))
  918. @@ -526,6 +595,8 @@ var Buffer = Module("buffer", {
  919.                      ctrlKey: ctrlKey, shiftKey: shiftKey, metaKey: ctrlKey
  920.                  }));
  921.              });
  922. +            let sel = util.selectionController(win);
  923. +            sel.getSelection(sel.SELECTION_FOCUS_REGION).collapseToStart();
  924.          });
  925.      },
  926.  
  927. @@ -533,9 +604,7 @@ var Buffer = Module("buffer", {
  928.       * @property {nsISelectionController} The current document's selection
  929.       *     controller.
  930.       */
  931. -    get selectionController() config.browser.docShell
  932. -            .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay)
  933. -            .QueryInterface(Ci.nsISelectionController),
  934. +    get selectionController() util.selectionController(this.focusedFrame),
  935.  
  936.      /**
  937.       * Opens the appropriate context menu for *elem*.
  938. @@ -561,7 +630,7 @@ var Buffer = Module("buffer", {
  939.          try {
  940.              window.urlSecurityCheck(uri.spec, doc.nodePrincipal);
  941.  
  942. -            io.CommandFileMode("Save link: ", {
  943. +            io.CommandFileMode(_("buffer.prompt.saveLink") + " ", {
  944.                  onSubmit: function (path) {
  945.                      let file = io.File(path);
  946.                      if (file.exists() && file.isDirectory())
  947. @@ -598,16 +667,16 @@ var Buffer = Module("buffer", {
  948.                               | persist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
  949.  
  950.          let downloadListener = new window.DownloadListener(window,
  951. -                services.Transfer(uri, services.io.newFileURI(file), "",
  952. +                services.Transfer(uri, File(file).URI, "",
  953.                                    null, null, null, persist));
  954.  
  955.          persist.progressListener = update(Object.create(downloadListener), {
  956. -            onStateChange: function onStateChange(progress, request, flag, status) {
  957. -                if (callback && (flag & Ci.nsIWebProgressListener.STATE_STOP) && status == 0)
  958. -                    dactyl.trapErrors(callback, self, uri, file, progress, request, flag, status);
  959. +            onStateChange: util.wrapCallback(function onStateChange(progress, request, flags, status) {
  960. +                if (callback && (flags & Ci.nsIWebProgressListener.STATE_STOP) && status == 0)
  961. +                    dactyl.trapErrors(callback, self, uri, file, progress, request, flags, status);
  962.  
  963.                  return onStateChange.superapply(this, arguments);
  964. -            }
  965. +            })
  966.          });
  967.  
  968.          persist.saveURI(uri, null, null, null, null, file);
  969. @@ -672,7 +741,7 @@ var Buffer = Module("buffer", {
  970.       */
  971.      findScrollable: function findScrollable(dir, horizontal) {
  972.          function find(elem) {
  973. -            while (!(elem instanceof Element) && elem.parentNode)
  974. +            while (elem && !(elem instanceof Element) && elem.parentNode)
  975.                  elem = elem.parentNode;
  976.              for (; elem && elem.parentNode instanceof Element; elem = elem.parentNode)
  977.                  if (Buffer.isScrollable(elem, dir, horizontal))
  978. @@ -702,7 +771,7 @@ var Buffer = Module("buffer", {
  979.                          doc.documentElement);
  980.          }
  981.          let doc = this.focusedFrame.document;
  982. -        return elem || doc.body || doc.documentElement;
  983. +        return dactyl.assert(elem || doc.body || doc.documentElement);
  984.      },
  985.  
  986.      /**
  987. @@ -728,6 +797,37 @@ var Buffer = Module("buffer", {
  988.          return win;
  989.      },
  990.  
  991. +    /**
  992. +     * Finds the next visible element for the node path in 'jumptags'
  993. +     * for *arg*.
  994. +     *
  995. +     * @param {string} arg The element in 'jumptags' to use for the search.
  996. +     * @param {number} count The number of elements to jump.
  997. +     *      @optional
  998. +     * @param {boolean} reverse If true, search backwards. @optional
  999. +     */
  1000. +    findJump: function findJump(arg, count, reverse) {
  1001. +        const FUDGE = 10;
  1002. +
  1003. +        let path = options["jumptags"][arg];
  1004. +        dactyl.assert(path, _("error.invalidArgument", arg));
  1005. +
  1006. +        let distance = reverse ? function (rect) -rect.top : function (rect) rect.top;
  1007. +        let elems = [[e, distance(e.getBoundingClientRect())] for (e in path.matcher(this.focusedFrame.document))]
  1008. +                        .filter(function (e) e[1] > FUDGE)
  1009. +                        .sort(function (a, b) a[1] - b[1])
  1010. +
  1011. +        let idx = Math.min((count || 1) - 1, elems.length);
  1012. +        dactyl.assert(idx in elems);
  1013. +
  1014. +        let elem = elems[idx][0];
  1015. +        elem.scrollIntoView(true);
  1016. +
  1017. +        let sel = elem.ownerDocument.defaultView.getSelection();
  1018. +        sel.removeAllRanges();
  1019. +        sel.addRange(RangeFind.endpoint(RangeFind.nodeRange(elem), true));
  1020. +    },
  1021. +
  1022.      // TODO: allow callback for filtering out unwanted frames? User defined?
  1023.      /**
  1024.       * Shifts the focus to another frame within the buffer. Each buffer
  1025. @@ -785,7 +885,7 @@ var Buffer = Module("buffer", {
  1026.       * @param {Node} elem The element to query.
  1027.       */
  1028.      showElementInfo: function showElementInfo(elem) {
  1029. -        dactyl.echo(<>Element:<br/>{util.objectToString(elem, true)}</>, commandline.FORCE_MULTILINE);
  1030. +        dactyl.echo(<><!--L-->Element:<br/>{util.objectToString(elem, true)}</>, commandline.FORCE_MULTILINE);
  1031.      },
  1032.  
  1033.      /**
  1034. @@ -798,15 +898,15 @@ var Buffer = Module("buffer", {
  1035.      showPageInfo: function showPageInfo(verbose, sections) {
  1036.          // Ctrl-g single line output
  1037.          if (!verbose) {
  1038. -            let file = content.location.pathname.split("/").pop() || "[No Name]";
  1039. -            let title = content.document.title || "[No Title]";
  1040. +            let file = content.location.pathname.split("/").pop() || _("buffer.noName");
  1041. +            let title = content.document.title || _("buffer.noTitle");
  1042.  
  1043. -            let info = template.map("gf",
  1044. +            let info = template.map(sections || options["pageinfo"],
  1045.                  function (opt) template.map(buffer.pageInfo[opt].action(), util.identity, ", "),
  1046.                  ", ");
  1047.  
  1048.              if (bookmarkcache.isBookmarked(this.URL))
  1049. -                info += ", bookmarked";
  1050. +                info += ", " + _("buffer.bookmarked");
  1051.  
  1052.              let pageInfoText = <>{file.quote()} [{info}] {title}</>;
  1053.              dactyl.echo(pageInfoText, commandline.FORCE_SINGLELINE);
  1054. @@ -852,26 +952,34 @@ var Buffer = Module("buffer", {
  1055.       * specified *url*. Either the default viewer or the configured external
  1056.       * editor is used.
  1057.       *
  1058. -     * @param {string} url The URL of the source.
  1059. +     * @param {string|object|null} loc If a string, the URL of the source,
  1060. +     *      otherwise an object with some or all of the following properties:
  1061. +     *
  1062. +     *          url: The URL to view.
  1063. +     *          doc: The document to view.
  1064. +     *          line: The line to select.
  1065. +     *          column: The column to select.
  1066. +     *
  1067. +     *      If no URL is provided, the current document is used.
  1068.       * @default The current buffer.
  1069.       * @param {boolean} useExternalEditor View the source in the external editor.
  1070.       */
  1071. -    viewSource: function viewSource(url, useExternalEditor) {
  1072. +    viewSource: function viewSource(loc, useExternalEditor) {
  1073.          let doc = this.focusedFrame.document;
  1074.  
  1075. -        if (isArray(url)) {
  1076. -            if (options.get("editor").has("line"))
  1077. -                this.viewSourceExternally(url[0] || doc, url[1]);
  1078. +        if (isObject(loc)) {
  1079. +            if (options.get("editor").has("line") || !loc.url)
  1080. +                this.viewSourceExternally(loc.doc || loc.url || doc, loc);
  1081.              else
  1082.                  window.openDialog("chrome://global/content/viewSource.xul",
  1083.                                    "_blank", "all,dialog=no",
  1084. -                                  url[0], null, null, url[1]);
  1085. +                                  loc.url, null, null, loc.line);
  1086.          }
  1087.          else {
  1088.              if (useExternalEditor)
  1089. -                this.viewSourceExternally(url || doc);
  1090. +                this.viewSourceExternally(loc || doc);
  1091.              else {
  1092. -                url = url || doc.location.href;
  1093. +                let url = loc || doc.location.href;
  1094.                  const PREFIX = "view-source:";
  1095.                  if (url.indexOf(PREFIX) == 0)
  1096.                      url = url.substr(PREFIX.length);
  1097. @@ -894,13 +1002,19 @@ var Buffer = Module("buffer", {
  1098.       * immediately.
  1099.       *
  1100.       * @param {Document} doc The document to view.
  1101. +     * @param {function|object} callback If a function, the callback to be
  1102. +     *      called with two arguments: the nsIFile of the file, and temp, a
  1103. +     *      boolean which is true if the file is temporary. Otherwise, an object
  1104. +     *      with line and column properties used to determine where to open the
  1105. +     *      source.
  1106. +     *      @optional
  1107.       */
  1108.      viewSourceExternally: Class("viewSourceExternally",
  1109.          XPCOM([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]), {
  1110.          init: function init(doc, callback) {
  1111.              this.callback = callable(callback) ? callback :
  1112.                  function (file, temp) {
  1113. -                    editor.editFileExternally({ file: file.path, line: callback },
  1114. +                    editor.editFileExternally(update({ file: file.path }, callback || {}),
  1115.                                                function () { temp && file.remove(false); });
  1116.                      return true;
  1117.                  };
  1118. @@ -928,8 +1042,8 @@ var Buffer = Module("buffer", {
  1119.              return null;
  1120.          },
  1121.  
  1122. -        onStateChange: function onStateChange(progress, request, flag, status) {
  1123. -            if ((flag & this.STATE_STOP) && status == 0) {
  1124. +        onStateChange: function onStateChange(progress, request, flags, status) {
  1125. +            if ((flags & this.STATE_STOP) && status == 0) {
  1126.                  try {
  1127.                      var ok = this.callback(this.file, true);
  1128.                  }
  1129. @@ -997,14 +1111,14 @@ var Buffer = Module("buffer", {
  1130.       * Adjusts the page zoom of the current buffer relative to the
  1131.       * current zoom level.
  1132.       *
  1133. -     * @param {number} steps The integral number of natural fractions by
  1134. -     *   which to adjust the current page zoom. If positive, the zoom
  1135. -     *   level is increased, if negative it is decreased.
  1136. +     * @param {number} steps The integral number of natural fractions by which
  1137. +     *     to adjust the current page zoom. If positive, the zoom level is
  1138. +     *     increased, if negative it is decreased.
  1139.       * @param {boolean} fullZoom If true, zoom all content of the page,
  1140. -     *   including raster images. If false, zoom only text. If omitted,
  1141. -     *   use the current zoom function. @optional
  1142. -     * @throws {FailedAssertion} if the buffer's zoom level is already
  1143. -     *  at its extreme in the given direction.
  1144. +     *     including raster images. If false, zoom only text. If omitted, use
  1145. +     *     the current zoom function. @optional
  1146. +     * @throws {FailedAssertion} if the buffer's zoom level is already at its
  1147. +     *     extreme in the given direction.
  1148.       */
  1149.      bumpZoomLevel: function bumpZoomLevel(steps, fullZoom) {
  1150.          if (fullZoom === undefined)
  1151. @@ -1019,7 +1133,7 @@ var Buffer = Module("buffer", {
  1152.          this.setZoom(Math.round(values[i] * 100), fullZoom);
  1153.      },
  1154.  
  1155. -    getAllFrames: deprecated("buffer.allFrames", function getAllFrames() buffer.getAllFrames.apply(buffer, arguments)),
  1156. +    getAllFrames: deprecated("buffer.allFrames", "allFrames"),
  1157.      scrollTop: deprecated("buffer.scrollToPercent", function scrollTop() buffer.scrollToPercent(null, 0)),
  1158.      scrollBottom: deprecated("buffer.scrollToPercent", function scrollBottom() buffer.scrollToPercent(null, 100)),
  1159.      scrollStart: deprecated("buffer.scrollToPercent", function scrollStart() buffer.scrollToPercent(0, null)),
  1160. @@ -1027,7 +1141,7 @@ var Buffer = Module("buffer", {
  1161.      scrollColumns: deprecated("buffer.scrollHorizontal", function scrollColumns(cols) buffer.scrollHorizontal("columns", cols)),
  1162.      scrollPages: deprecated("buffer.scrollHorizontal", function scrollPages(pages) buffer.scrollVertical("pages", pages)),
  1163.      scrollTo: deprecated("Buffer.scrollTo", function scrollTo(x, y) content.scrollTo(x, y)),
  1164. -    textZoom: deprecated("buffer.zoomValue and buffer.fullZoom", function textZoom() config.browser.markupDocumentViewer.textZoom * 100)
  1165. +    textZoom: deprecated("buffer.zoomValue/buffer.fullZoom", function textZoom() config.browser.markupDocumentViewer.textZoom * 100)
  1166.  }, {
  1167.      PageInfo: Struct("PageInfo", "name", "title", "action")
  1168.                          .localize("title"),
  1169. @@ -1044,17 +1158,21 @@ var Buffer = Module("buffer", {
  1170.       *
  1171.       * @returns {string}
  1172.       */
  1173. -    currentWord: function currentWord(win) {
  1174. +    currentWord: function currentWord(win, select) {
  1175.          let selection = win.getSelection();
  1176.          if (selection.rangeCount == 0)
  1177.              return "";
  1178.  
  1179.          let range = selection.getRangeAt(0).cloneRange();
  1180. -        if (range.collapsed) {
  1181. +        if (range.collapsed && range.startContainer instanceof Text) {
  1182.              let re = options.get("iskeyword").regexp;
  1183.              Editor.extendRange(range, true,  re, true);
  1184.              Editor.extendRange(range, false, re, true);
  1185.          }
  1186. +        if (select) {
  1187. +            selection.removeAllRanges();
  1188. +            selection.addRange(range);
  1189. +        }
  1190.          return util.domToString(range);
  1191.      },
  1192.  
  1193. @@ -1079,13 +1197,13 @@ var Buffer = Module("buffer", {
  1194.  
  1195.          var names = [];
  1196.          if (node.title)
  1197. -            names.push([node.title, "Page Name"]);
  1198. +            names.push([node.title, /*L*/"Page Name"]);
  1199.  
  1200.          if (node.alt)
  1201. -            names.push([node.alt, "Alternate Text"]);
  1202. +            names.push([node.alt, /*L*/"Alternate Text"]);
  1203.  
  1204.          if (!isinstance(node, Document) && node.textContent)
  1205. -            names.push([node.textContent, "Link Text"]);
  1206. +            names.push([node.textContent, /*L*/"Link Text"]);
  1207.  
  1208.          names.push([decodeURIComponent(url.replace(/.*?([^\/]*)\/*$/, "$1")), "File Name"]);
  1209.  
  1210. @@ -1133,6 +1251,11 @@ var Buffer = Module("buffer", {
  1211.              elem.scrollLeft = left;
  1212.          if (top != null)
  1213.              elem.scrollTop = top;
  1214. +
  1215. +        if (util.haveGecko("2.0") && !util.haveGecko("7.*"))
  1216. +            elem.ownerDocument.defaultView
  1217. +                .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
  1218. +                .redraw();
  1219.      },
  1220.  
  1221.      /**
  1222. @@ -1156,10 +1279,11 @@ var Buffer = Module("buffer", {
  1223.          else
  1224.              throw Error();
  1225.  
  1226. +        dactyl.assert(number < 0 ? elem.scrollLeft > 0 : elem.scrollLeft < elem.scrollWidth - elem.clientWidth);
  1227. +
  1228.          let left = elem.dactylScrollDestX !== undefined ? elem.dactylScrollDestX : elem.scrollLeft;
  1229.          elem.dactylScrollDestX = undefined;
  1230.  
  1231. -        dactyl.assert(number < 0 ? left > 0 : left < elem.scrollWidth - elem.clientWidth);
  1232.          Buffer.scrollTo(elem, left + number * increment, null);
  1233.      },
  1234.  
  1235. @@ -1184,10 +1308,11 @@ var Buffer = Module("buffer", {
  1236.          else
  1237.              throw Error();
  1238.  
  1239. +        dactyl.assert(number < 0 ? elem.scrollTop > 0 : elem.scrollTop < elem.scrollHeight - elem.clientHeight);
  1240. +
  1241.          let top = elem.dactylScrollDestY !== undefined ? elem.dactylScrollDestY : elem.scrollTop;
  1242.          elem.dactylScrollDestY = undefined;
  1243.  
  1244. -        dactyl.assert(number < 0 ? top > 0 : top < elem.scrollHeight - elem.clientHeight);
  1245.          Buffer.scrollTo(elem, null, top + number * increment);
  1246.      },
  1247.  
  1248. @@ -1212,7 +1337,7 @@ var Buffer = Module("buffer", {
  1249.      },
  1250.  
  1251.      openUploadPrompt: function openUploadPrompt(elem) {
  1252. -        io.CommandFileMode("Upload file: ", {
  1253. +        io.CommandFileMode(_("buffer.prompt.uploadFile") + " ", {
  1254.              onSubmit: function onSubmit(path) {
  1255.                  let file = io.File(path);
  1256.                  dactyl.assert(file.exists());
  1257. @@ -1238,12 +1363,21 @@ var Buffer = Module("buffer", {
  1258.  
  1259.                  // FIXME: arg handling is a bit of a mess, check for filename
  1260.                  dactyl.assert(!arg || arg[0] == ">" && !util.OS.isWindows,
  1261. -                              _("error.trailing"));
  1262. +                              _("error.trailingCharacters"));
  1263. +
  1264. +                const PRINTER = "PostScript/default";
  1265. +                const BRANCH  = "print.printer_" + PRINTER + ".";
  1266.  
  1267.                  prefs.withContext(function () {
  1268.                      if (arg) {
  1269. -                        prefs.set("print.print_to_file", "true");
  1270. +                        prefs.set("print.print_printer", PRINTER);
  1271. +
  1272. +                        prefs.set(   "print.print_to_file", true);
  1273. +                        prefs.set(BRANCH + "print_to_file", true);
  1274. +
  1275.                          prefs.set("print.print_to_filename", io.File(arg.substr(1)).path);
  1276. +                        prefs.set(BRANCH + "print_to_filename", io.File(arg.substr(1)).path);
  1277. +
  1278.                          dactyl.echomsg(_("print.toFile", arg.substr(1)));
  1279.                      }
  1280.                      else
  1281. @@ -1417,7 +1551,7 @@ var Buffer = Module("buffer", {
  1282.                      level = Math.constrain(level, Buffer.ZOOM_MIN, Buffer.ZOOM_MAX);
  1283.                  }
  1284.                  else
  1285. -                    dactyl.assert(false, _("error.trailing"));
  1286. +                    dactyl.assert(false, _("error.trailingCharacters"));
  1287.  
  1288.                  buffer.setZoom(level, args.bang);
  1289.              },
  1290. @@ -1434,21 +1568,24 @@ var Buffer = Module("buffer", {
  1291.              let styles = iter([s.title, []] for (s in values(buffer.alternateStyleSheets))).toObject();
  1292.  
  1293.              buffer.alternateStyleSheets.forEach(function (style) {
  1294. -                styles[style.title].push(style.href || "inline");
  1295. +                styles[style.title].push(style.href || _("style.inline"));
  1296.              });
  1297.  
  1298.              context.completions = [[title, href.join(", ")] for ([title, href] in Iterator(styles))];
  1299.          };
  1300.  
  1301. -        completion.buffer = function buffer(context) {
  1302. +        completion.buffer = function buffer(context, visible) {
  1303.              let filter = context.filter.toLowerCase();
  1304. +
  1305.              let defItem = { parent: { getTitle: function () "" } };
  1306. +
  1307.              let tabGroups = {};
  1308.              tabs.getGroups();
  1309. -            tabs.allTabs.forEach(function (tab, i) {
  1310. +            tabs[visible ? "visibleTabs" : "allTabs"].forEach(function (tab, i) {
  1311.                  let group = (tab.tabItem || tab._tabViewTabItem || defItem).parent || defItem.parent;
  1312. -                if (!set.has(tabGroups, group.id))
  1313. +                if (!Set.has(tabGroups, group.id))
  1314.                      tabGroups[group.id] = [group.getTitle(), []];
  1315. +
  1316.                  group = tabGroups[group.id];
  1317.                  group[1].push([i, tab.linkedBrowser]);
  1318.              });
  1319. @@ -1470,7 +1607,7 @@ var Buffer = Module("buffer", {
  1320.                  command: function () "tabs.select"
  1321.              };
  1322.              context.compare = CompletionContext.Sort.number;
  1323. -            context.filters = [CompletionContext.Filter.textDescription];
  1324. +            context.filters[0] = CompletionContext.Filter.textDescription;
  1325.  
  1326.              for (let [id, vals] in Iterator(tabGroups))
  1327.                  context.fork(id, 0, this, function (context, [name, browsers]) {
  1328. @@ -1483,12 +1620,12 @@ var Buffer = Module("buffer", {
  1329.                              else if (i == tabs.index(tabs.alternate))
  1330.                                  indicator = "#";
  1331.  
  1332. -                            let tab = tabs.getTab(i);
  1333. +                            let tab = tabs.getTab(i, visible);
  1334.                              let url = browser.contentDocument.location.href;
  1335.                              i = i + 1;
  1336.  
  1337.                              return {
  1338. -                                text: [i + ": " + (tab.label || "(Untitled)"), i + ": " + url],
  1339. +                                text: [i + ": " + (tab.label || /*L*/"(Untitled)"), i + ": " + url],
  1340.                                  tab: tab,
  1341.                                  id: i - 1,
  1342.                                  url: url,
  1343. @@ -1514,21 +1651,21 @@ var Buffer = Module("buffer", {
  1344.              function () { dactyl.clipboardWrite(buffer.uri.spec, true); });
  1345.  
  1346.          mappings.add([modes.NORMAL],
  1347. -            ["<C-a>"], "Increment last number in URL",
  1348. +            ["<C-a>", "<increment-url-path>"], "Increment last number in URL",
  1349.              function (args) { buffer.incrementURL(Math.max(args.count, 1)); },
  1350.              { count: true });
  1351.  
  1352.          mappings.add([modes.NORMAL],
  1353. -            ["<C-x>"], "Decrement last number in URL",
  1354. +            ["<C-x>", "<decrement-url-path>"], "Decrement last number in URL",
  1355.              function (args) { buffer.incrementURL(-Math.max(args.count, 1)); },
  1356.              { count: true });
  1357.  
  1358. -        mappings.add([modes.NORMAL], ["gu"],
  1359. +        mappings.add([modes.NORMAL], ["gu", "<open-parent-path>"],
  1360.              "Go to parent directory",
  1361.              function (args) { buffer.climbUrlPath(Math.max(args.count, 1)); },
  1362.              { count: true });
  1363.  
  1364. -        mappings.add([modes.NORMAL], ["gU"],
  1365. +        mappings.add([modes.NORMAL], ["gU", "<open-root-path>"],
  1366.              "Go to the root of the website",
  1367.              function () { buffer.climbUrlPath(-1); });
  1368.  
  1369. @@ -1542,11 +1679,11 @@ var Buffer = Module("buffer", {
  1370.              },
  1371.              { count: true });
  1372.  
  1373. -        mappings.add([modes.COMMAND], ["i", "<Insert>"],
  1374. -            "Start caret mode",
  1375. +        mappings.add([modes.NORMAL], ["i", "<Insert>"],
  1376. +            "Start Caret mode",
  1377.              function () { modes.push(modes.CARET); });
  1378.  
  1379. -        mappings.add([modes.COMMAND], ["<C-c>"],
  1380. +        mappings.add([modes.NORMAL], ["<C-c>", "<stop-load>"],
  1381.              "Stop loading the current web page",
  1382.              function () { ex.stop(); });
  1383.  
  1384. @@ -1579,12 +1716,12 @@ var Buffer = Module("buffer", {
  1385.              "Scroll to the absolute right of the document",
  1386.              function () { buffer.scrollToPercent(100, null); });
  1387.  
  1388. -        mappings.add([modes.COMMAND], ["gg", "<Home>"],
  1389. +        mappings.add([modes.COMMAND], ["gg", "<Home>", "<scroll-top>"],
  1390.              "Go to the top of the document",
  1391.              function (args) { buffer.scrollToPercent(null, args.count != null ? args.count : 0); },
  1392.              { count: true });
  1393.  
  1394. -        mappings.add([modes.COMMAND], ["G", "<End>"],
  1395. +        mappings.add([modes.COMMAND], ["G", "<End>", "<scroll-bottom>"],
  1396.              "Go to the end of the document",
  1397.              function (args) { buffer.scrollToPercent(null, args.count != null ? args.count : 100); },
  1398.              { count: true });
  1399. @@ -1607,55 +1744,84 @@ var Buffer = Module("buffer", {
  1400.              function (args) { buffer._scrollByScrollSize(args.count, false); },
  1401.              { count: true });
  1402.  
  1403. -        mappings.add([modes.COMMAND], ["<C-b>", "<PageUp>", "<S-Space>", "<scroll-page-up>"],
  1404. +        mappings.add([modes.COMMAND], ["<C-b>", "<PageUp>", "<S-Space>", "<scroll-up-page>"],
  1405.              "Scroll up a full page",
  1406.              function (args) { buffer.scrollVertical("pages", -Math.max(args.count, 1)); },
  1407.              { count: true });
  1408.  
  1409. -        mappings.add([modes.COMMAND], ["<C-f>", "<PageDown>", "<Space>", "<scroll-page-down>"],
  1410. +        mappings.add([modes.COMMAND], ["<Space>"],
  1411. +            "Scroll down a full page",
  1412. +            function (args) {
  1413. +                if (isinstance(content.document.activeElement, [HTMLInputElement, HTMLButtonElement]))
  1414. +                    return Events.PASS;
  1415. +                buffer.scrollVertical("pages", Math.max(args.count, 1));
  1416. +            },
  1417. +            { count: true });
  1418. +
  1419. +        mappings.add([modes.COMMAND], ["<C-f>", "<PageDown>", "<scroll-down-page>"],
  1420.              "Scroll down a full page",
  1421.              function (args) { buffer.scrollVertical("pages", Math.max(args.count, 1)); },
  1422.              { count: true });
  1423.  
  1424. -        mappings.add([modes.COMMAND], ["]f", "<previous-frame>"],
  1425. +        mappings.add([modes.NORMAL], ["]f", "<previous-frame>"],
  1426.              "Focus next frame",
  1427.              function (args) { buffer.shiftFrameFocus(Math.max(args.count, 1)); },
  1428.              { count: true });
  1429.  
  1430. -        mappings.add([modes.COMMAND], ["[f", "<next-frame>"],
  1431. +        mappings.add([modes.NORMAL], ["[f", "<next-frame>"],
  1432.              "Focus previous frame",
  1433.              function (args) { buffer.shiftFrameFocus(-Math.max(args.count, 1)); },
  1434.              { count: true });
  1435.  
  1436. -        mappings.add([modes.COMMAND], ["]]", "<next-page>"],
  1437. +        mappings.add([modes.NORMAL], ["["],
  1438. +            "Jump to the previous element as defined by 'jumptags'",
  1439. +            function (args) { buffer.findJump(args.arg, args.count, true); },
  1440. +            { arg: true, count: true });
  1441. +
  1442. +        mappings.add([modes.NORMAL], ["]"],
  1443. +            "Jump to the next element as defined by 'jumptags'",
  1444. +            function (args) { buffer.findJump(args.arg, args.count, false); },
  1445. +            { arg: true, count: true });
  1446. +
  1447. +        mappings.add([modes.NORMAL], ["{"],
  1448. +            "Jump to the previous paragraph",
  1449. +            function (args) { buffer.findJump("p", args.count, true); },
  1450. +            { count: true });
  1451. +
  1452. +        mappings.add([modes.NORMAL], ["}"],
  1453. +            "Jump to the next paragraph",
  1454. +            function (args) { buffer.findJump("p", args.count, false); },
  1455. +            { count: true });
  1456. +
  1457. +        mappings.add([modes.NORMAL], ["]]", "<next-page>"],
  1458.              "Follow the link labeled 'next' or '>' if it exists",
  1459.              function (args) {
  1460.                  buffer.findLink("next", options["nextpattern"], (args.count || 1) - 1, true);
  1461.              },
  1462.              { count: true });
  1463.  
  1464. -        mappings.add([modes.COMMAND], ["[[", "<previous-page>"],
  1465. +        mappings.add([modes.NORMAL], ["[[", "<previous-page>"],
  1466.              "Follow the link labeled 'prev', 'previous' or '<' if it exists",
  1467.              function (args) {
  1468.                  buffer.findLink("previous", options["previouspattern"], (args.count || 1) - 1, true);
  1469.              },
  1470.              { count: true });
  1471.  
  1472. -        mappings.add([modes.COMMAND], ["gf", "<view-source>"],
  1473. +        mappings.add([modes.NORMAL], ["gf", "<view-source>"],
  1474.              "Toggle between rendered and source view",
  1475.              function () { buffer.viewSource(null, false); });
  1476.  
  1477. -        mappings.add([modes.COMMAND], ["gF", "<view-source-externally>"],
  1478. +        mappings.add([modes.NORMAL], ["gF", "<view-source-externally>"],
  1479.              "View source with an external editor",
  1480.              function () { buffer.viewSource(null, true); });
  1481.  
  1482. -        mappings.add([modes.COMMAND], ["gi", "<focus-input>"],
  1483. +        mappings.add([modes.NORMAL], ["gi", "<focus-input>"],
  1484.              "Focus last used input field",
  1485.              function (args) {
  1486.                  let elem = buffer.lastInputField;
  1487.  
  1488.                  if (args.count >= 1 || !elem || !events.isContentNode(elem)) {
  1489. -                    let xpath = ["frame", "iframe", "input", "textarea[not(@disabled) and not(@readonly)]"];
  1490. +                    let xpath = ["frame", "iframe", "input", "xul:textbox", "textarea[not(@disabled) and not(@readonly)]"];
  1491.  
  1492.                      let frames = buffer.allFrames(null, true);
  1493.  
  1494. @@ -1664,13 +1830,14 @@ var Buffer = Module("buffer", {
  1495.                          if (isinstance(elem, [HTMLFrameElement, HTMLIFrameElement]))
  1496.                              return Editor.getEditor(elem.contentWindow);
  1497.  
  1498. -                        if (elem.readOnly || elem instanceof HTMLInputElement && !set.has(util.editableInputs, elem.type))
  1499. +                        if (elem.readOnly || elem instanceof HTMLInputElement && !Set.has(util.editableInputs, elem.type))
  1500.                              return false;
  1501.  
  1502.                          let computedStyle = util.computedStyle(elem);
  1503.                          let rect = elem.getBoundingClientRect();
  1504.                          return computedStyle.visibility != "hidden" && computedStyle.display != "none" &&
  1505. -                            computedStyle.MozUserFocus != "ignore" && rect.width && rect.height;
  1506. +                            (elem instanceof Ci.nsIDOMXULTextBoxElement || computedStyle.MozUserFocus != "ignore") &&
  1507. +                            rect.width && rect.height;
  1508.                      });
  1509.  
  1510.                      dactyl.assert(elements.length > 0);
  1511. @@ -1681,36 +1848,40 @@ var Buffer = Module("buffer", {
  1512.              },
  1513.              { count: true });
  1514.  
  1515. -        mappings.add([modes.COMMAND], ["gP"],
  1516. -            "Open (]put) a URL based on the current clipboard contents in a new buffer",
  1517. -            function () {
  1518. +        function url() {
  1519.                  let url = dactyl.clipboardRead();
  1520.                  dactyl.assert(url, _("error.clipboardEmpty"));
  1521. -                dactyl.open(url, { from: "paste", where: dactyl.NEW_TAB, background: true });
  1522. +
  1523. +            let proto = /^([-\w]+):/.exec(url);
  1524. +            if (proto && "@mozilla.org/network/protocol;1?name=" + proto[1] in Cc && !RegExp(options["urlseparator"]).test(url))
  1525. +                return url.replace(/\s+/g, "");
  1526. +            return url;
  1527. +        }
  1528. +
  1529. +        mappings.add([modes.NORMAL], ["gP"],
  1530. +            "Open (put) a URL based on the current clipboard contents in a new background buffer",
  1531. +            function () {
  1532. +                dactyl.open(url(), { from: "paste", where: dactyl.NEW_TAB, background: true });
  1533.              });
  1534.  
  1535. -        mappings.add([modes.COMMAND], ["p", "<MiddleMouse>", "<open-clipboard-url>"],
  1536. +        mappings.add([modes.NORMAL], ["p", "<MiddleMouse>", "<open-clipboard-url>"],
  1537.              "Open (put) a URL based on the current clipboard contents in the current buffer",
  1538.              function () {
  1539. -                let url = dactyl.clipboardRead();
  1540. -                dactyl.assert(url, _("error.clipboardEmpty"));
  1541. -                dactyl.open(url);
  1542. +                dactyl.open(url());
  1543.              });
  1544.  
  1545. -        mappings.add([modes.COMMAND], ["P", "<tab-open-clipboard-url>"],
  1546. +        mappings.add([modes.NORMAL], ["P", "<tab-open-clipboard-url>"],
  1547.              "Open (put) a URL based on the current clipboard contents in a new buffer",
  1548.              function () {
  1549. -                let url = dactyl.clipboardRead();
  1550. -                dactyl.assert(url, _("error.clipboardEmpty"));
  1551. -                dactyl.open(url, { from: "paste", where: dactyl.NEW_TAB });
  1552. +                dactyl.open(url(), { from: "paste", where: dactyl.NEW_TAB });
  1553.              });
  1554.  
  1555.          // reloading
  1556. -        mappings.add([modes.COMMAND], ["r", "<reload>"],
  1557. +        mappings.add([modes.NORMAL], ["r", "<reload>"],
  1558.              "Reload the current web page",
  1559.              function () { tabs.reload(tabs.getTab(), false); });
  1560.  
  1561. -        mappings.add([modes.COMMAND], ["R", "<full-reload>"],
  1562. +        mappings.add([modes.NORMAL], ["R", "<full-reload>"],
  1563.              "Reload while skipping the cache",
  1564.              function () { tabs.reload(tabs.getTab(), true); });
  1565.  
  1566. @@ -1724,62 +1895,62 @@ var Buffer = Module("buffer", {
  1567.              });
  1568.  
  1569.          // zooming
  1570. -        mappings.add([modes.COMMAND], ["zi", "+", "<text-zoom-in>"],
  1571. +        mappings.add([modes.NORMAL], ["zi", "+", "<text-zoom-in>"],
  1572.              "Enlarge text zoom of current web page",
  1573.              function (args) { buffer.zoomIn(Math.max(args.count, 1), false); },
  1574.              { count: true });
  1575.  
  1576. -        mappings.add([modes.COMMAND], ["zm", "<text-zoom-more>"],
  1577. +        mappings.add([modes.NORMAL], ["zm", "<text-zoom-more>"],
  1578.              "Enlarge text zoom of current web page by a larger amount",
  1579.              function (args) { buffer.zoomIn(Math.max(args.count, 1) * 3, false); },
  1580.              { count: true });
  1581.  
  1582. -        mappings.add([modes.COMMAND], ["zo", "-", "<text-zoom-out>"],
  1583. +        mappings.add([modes.NORMAL], ["zo", "-", "<text-zoom-out>"],
  1584.              "Reduce text zoom of current web page",
  1585.              function (args) { buffer.zoomOut(Math.max(args.count, 1), false); },
  1586.              { count: true });
  1587.  
  1588. -        mappings.add([modes.COMMAND], ["zr", "<text-zoom-reduce>"],
  1589. +        mappings.add([modes.NORMAL], ["zr", "<text-zoom-reduce>"],
  1590.              "Reduce text zoom of current web page by a larger amount",
  1591.              function (args) { buffer.zoomOut(Math.max(args.count, 1) * 3, false); },
  1592.              { count: true });
  1593.  
  1594. -        mappings.add([modes.COMMAND], ["zz", "<text-zoom>"],
  1595. +        mappings.add([modes.NORMAL], ["zz", "<text-zoom>"],
  1596.              "Set text zoom value of current web page",
  1597.              function (args) { buffer.setZoom(args.count > 1 ? args.count : 100, false); },
  1598.              { count: true });
  1599.  
  1600. -        mappings.add([modes.COMMAND], ["ZI", "zI", "<full-zoom-in>"],
  1601. +        mappings.add([modes.NORMAL], ["ZI", "zI", "<full-zoom-in>"],
  1602.              "Enlarge full zoom of current web page",
  1603.              function (args) { buffer.zoomIn(Math.max(args.count, 1), true); },
  1604.              { count: true });
  1605.  
  1606. -        mappings.add([modes.COMMAND], ["ZM", "zM", "<full-zoom-more>"],
  1607. +        mappings.add([modes.NORMAL], ["ZM", "zM", "<full-zoom-more>"],
  1608.              "Enlarge full zoom of current web page by a larger amount",
  1609.              function (args) { buffer.zoomIn(Math.max(args.count, 1) * 3, true); },
  1610.              { count: true });
  1611.  
  1612. -        mappings.add([modes.COMMAND], ["ZO", "zO", "<full-zoom-out>"],
  1613. +        mappings.add([modes.NORMAL], ["ZO", "zO", "<full-zoom-out>"],
  1614.              "Reduce full zoom of current web page",
  1615.              function (args) { buffer.zoomOut(Math.max(args.count, 1), true); },
  1616.              { count: true });
  1617.  
  1618. -        mappings.add([modes.COMMAND], ["ZR", "zR", "<full-zoom-reduce>"],
  1619. +        mappings.add([modes.NORMAL], ["ZR", "zR", "<full-zoom-reduce>"],
  1620.              "Reduce full zoom of current web page by a larger amount",
  1621.              function (args) { buffer.zoomOut(Math.max(args.count, 1) * 3, true); },
  1622.              { count: true });
  1623.  
  1624. -        mappings.add([modes.COMMAND], ["zZ", "<full-zoom>"],
  1625. +        mappings.add([modes.NORMAL], ["zZ", "<full-zoom>"],
  1626.              "Set full zoom value of current web page",
  1627.              function (args) { buffer.setZoom(args.count > 1 ? args.count : 100, true); },
  1628.              { count: true });
  1629.  
  1630.          // page info
  1631. -        mappings.add([modes.COMMAND], ["<C-g>", "<page-info>"],
  1632. +        mappings.add([modes.NORMAL], ["<C-g>", "<page-info>"],
  1633.              "Print the current file name",
  1634.              function () { buffer.showPageInfo(false); });
  1635.  
  1636. -        mappings.add([modes.COMMAND], ["g<C-g>", "<more-page-info>"],
  1637. +        mappings.add([modes.NORMAL], ["g<C-g>", "<more-page-info>"],
  1638.              "Print file information",
  1639.              function () { buffer.showPageInfo(true); });
  1640.      },
  1641. @@ -1817,6 +1988,23 @@ var Buffer = Module("buffer", {
  1642.                  validator: function (value) RegExp(value)
  1643.              });
  1644.  
  1645. +        options.add(["jumptags", "jt"],
  1646. +            "XPath or CSS selector strings of jumpable elements for extended hint modes",
  1647. +            "stringmap", {
  1648. +                "p": "p,table,ul,ol,blockquote",
  1649. +                "h": "h1,h2,h3,h4,h5,h6"
  1650. +            },
  1651. +            {
  1652. +                keepQuotes: true,
  1653. +                setter: function (vals) {
  1654. +                    for (let [k, v] in Iterator(vals))
  1655. +                        vals[k] = update(new String(v), { matcher: util.compileMatcher(Option.splitList(v)) });
  1656. +                    return vals;
  1657. +                },
  1658. +                validator: function (value) util.validateMatcher.call(this, value)
  1659. +                    && Object.keys(value).every(function (v) v.length == 1)
  1660. +            });
  1661. +
  1662.          options.add(["nextpattern"],
  1663.              "Patterns to use when guessing the next page in a document sequence",
  1664.              "regexplist", UTF8("'\\bnext\\b',^>$,^(>>|»)$,^(>|»),(>|»)$,'\\bmore\\b'"),
  1665. @@ -1829,7 +2017,7 @@ var Buffer = Module("buffer", {
  1666.  
  1667.          options.add(["pageinfo", "pa"],
  1668.              "Define which sections are shown by the :pageinfo command",
  1669. -            "charlist", "gfm",
  1670. +            "charlist", "gesfm",
  1671.              { get values() values(buffer.pageInfo).toObject() });
  1672.  
  1673.          options.add(["scroll", "scr"],
  1674. diff --git a/common/content/commandline.js b/common/content/commandline.js
  1675. --- a/common/content/commandline.js
  1676. +++ b/common/content/commandline.js
  1677. @@ -152,6 +152,7 @@ var CommandWidgets = Class("CommandWidge
  1678.                  return this.commandbar;
  1679.              }
  1680.          });
  1681. +        this.updateVisibility();
  1682.      },
  1683.      addElement: function addElement(obj) {
  1684.          const self = this;
  1685. @@ -174,7 +175,7 @@ var CommandWidgets = Class("CommandWidge
  1686.                      if (obj.value != null)
  1687.                          return [obj.value[0],
  1688.                                  obj.get ? obj.get.call(this, elem) : elem.value]
  1689. -                                .concat(obj.value.slice(2))
  1690. +                                .concat(obj.value.slice(2));
  1691.                      return null;
  1692.                  },
  1693.  
  1694. @@ -300,19 +301,21 @@ var CommandWidgets = Class("CommandWidge
  1695.  });
  1696.  
  1697.  var CommandMode = Class("CommandMode", {
  1698. -    init: function init() {
  1699. +    init: function CM_init() {
  1700.          this.keepCommand = userContext.hidden_option_command_afterimage;
  1701.      },
  1702.  
  1703. +    get autocomplete() options["autocomplete"].length,
  1704. +
  1705.      get command() this.widgets.command[1],
  1706.      set command(val) this.widgets.command = val,
  1707.  
  1708.      get prompt() this.widgets.prompt,
  1709.      set prompt(val) this.widgets.prompt = val,
  1710.  
  1711. -    open: function (command) {
  1712. +    open: function CM_open(command) {
  1713.          dactyl.assert(isinstance(this.mode, modes.COMMAND_LINE),
  1714. -                      "Not opening command line in non-command-line mode.");
  1715. +                      /*L*/"Not opening command line in non-command-line mode.");
  1716.  
  1717.          this.messageCount = commandline.messageCount;
  1718.          modes.push(this.mode, this.extendedMode, this.closure);
  1719. @@ -340,7 +343,7 @@ var CommandMode = Class("CommandMode", {
  1720.  
  1721.      get widgets() commandline.widgets,
  1722.  
  1723. -    enter: function (stack) {
  1724. +    enter: function CM_enter(stack) {
  1725.          commandline.commandSession = this;
  1726.          if (stack.pop && commandline.command) {
  1727.              this.onChange(commandline.command);
  1728. @@ -349,7 +352,7 @@ var CommandMode = Class("CommandMode", {
  1729.          }
  1730.      },
  1731.  
  1732. -    leave: function (stack) {
  1733. +    leave: function CM_leave(stack) {
  1734.          if (!stack.push) {
  1735.              commandline.commandSession = null;
  1736.              this.input.dactylKeyPress = undefined;
  1737. @@ -374,7 +377,7 @@ var CommandMode = Class("CommandMode", {
  1738.      },
  1739.  
  1740.      events: {
  1741. -        input: function onInput(event) {
  1742. +        input: function CM_onInput(event) {
  1743.              if (this.completions) {
  1744.                  this.resetCompletions();
  1745.  
  1746. @@ -382,7 +385,7 @@ var CommandMode = Class("CommandMode", {
  1747.              }
  1748.              this.onChange(commandline.command);
  1749.          },
  1750. -        keyup: function onKeyUp(event) {
  1751. +        keyup: function CM_onKeyUp(event) {
  1752.              let key = events.toString(event);
  1753.              if (/-?Tab>$/.test(key) && this.completions)
  1754.                  this.completions.tabTimer.flush();
  1755. @@ -391,23 +394,22 @@ var CommandMode = Class("CommandMode", {
  1756.  
  1757.      keepCommand: false,
  1758.  
  1759. -    onKeyPress: function onKeyPress(events) {
  1760. +    onKeyPress: function CM_onKeyPress(events) {
  1761.          if (this.completions)
  1762.              this.completions.previewClear();
  1763.  
  1764.          return true; /* Pass event */
  1765.      },
  1766.  
  1767. -    onCancel: function (value) {
  1768. -    },
  1769. +    onCancel: function (value) {},
  1770.  
  1771. -    onChange: function (value) {
  1772. -    },
  1773. +    onChange: function (value) {},
  1774.  
  1775. -    onSubmit: function (value) {
  1776. -    },
  1777. +    onHistory: function (value) {},
  1778.  
  1779. -    resetCompletions: function resetCompletions() {
  1780. +    onSubmit: function (value) {},
  1781. +
  1782. +    resetCompletions: function CM_resetCompletions() {
  1783.          if (this.completions) {
  1784.              this.completions.context.cancelAll();
  1785.              this.completions.wildIndex = -1;
  1786. @@ -426,12 +428,12 @@ var CommandExMode = Class("CommandExMode
  1787.  
  1788.      prompt: ["Normal", ":"],
  1789.  
  1790. -    complete: function complete(context) {
  1791. +    complete: function CEM_complete(context) {
  1792.          context.fork("ex", 0, completion, "ex");
  1793.      },
  1794.  
  1795. -    onSubmit: function onSubmit(command) {
  1796. -        contexts.withContext({ file: "[Command Line]", line: 1 },
  1797. +    onSubmit: function CEM_onSubmit(command) {
  1798. +        contexts.withContext({ file: /*L*/"[Command Line]", line: 1 },
  1799.                               function _onSubmit() {
  1800.              io.withSavedValues(["readHeredoc"], function _onSubmit() {
  1801.                  this.readHeredoc = commandline.readHeredoc;
  1802. @@ -449,7 +451,7 @@ var CommandPromptMode = Class("CommandPr
  1803.          init.supercall(this);
  1804.      },
  1805.  
  1806. -    complete: function (context) {
  1807. +    complete: function CPM_complete(context) {
  1808.          if (this.completer)
  1809.              context.forkapply("prompt", 0, this, "completer", Array.slice(arguments, 1));
  1810.      },
  1811. @@ -586,6 +588,9 @@ var CommandLine = Module("commandline",
  1812.  
  1813.      get completionList() {
  1814.          let node = this.widgets.active.commandline;
  1815. +        if (this.commandSession && this.commandSession.completionList)
  1816. +            node = document.getElementById(this.commandSession.completionList);
  1817. +
  1818.          if (!node.completionList) {
  1819.              let elem = document.getElementById("dactyl-completions-" + node.id);
  1820.              util.waitFor(bind(this.widgets._ready, null, elem));
  1821. @@ -648,10 +653,10 @@ var CommandLine = Module("commandline",
  1822.       * @param {XML} xml The output as an E4X XML object.
  1823.       */
  1824.      commandOutput: function commandOutput(xml) {
  1825. -        XML.ignoreWhitespace = false;
  1826. -        XML.prettyPrinting = false;
  1827. +        XML.ignoreWhitespace = XML.prettyPrinting = false;
  1828.          if (this.command)
  1829. -            this.echo(<>:{this.command}</>, this.HIGHLIGHT_NORMAL, this.FORCE_MULTILINE);
  1830. +            this.echo(<><div xmlns={XHTML}>:{this.command}</div>&#x0d;{xml}</>, this.HIGHLIGHT_NORMAL, this.FORCE_MULTILINE);
  1831. +        else
  1832.          this.echo(xml, this.HIGHLIGHT_NORMAL, this.FORCE_MULTILINE);
  1833.          this.command = null;
  1834.      },
  1835. @@ -722,6 +727,9 @@ var CommandLine = Module("commandline",
  1836.  
  1837.          if (flags & this.APPEND_TO_MESSAGES) {
  1838.              let message = isObject(data) ? data : { message: data };
  1839. +
  1840. +            // Make sure the memoized message property is an instance property.
  1841. +            message.message;
  1842.              this._messageHistory.add(update({ highlight: highlightGroup }, message));
  1843.              data = message.message;
  1844.          }
  1845. @@ -737,7 +745,7 @@ var CommandLine = Module("commandline",
  1846.          let single = flags & (this.FORCE_SINGLELINE | this.DISALLOW_MULTILINE);
  1847.          let action = this._echoLine;
  1848.  
  1849. -        if ((flags & this.FORCE_MULTILINE) || (/\n/.test(data) || !isString(data)) && !(flags & this.FORCE_SINGLELINE))
  1850. +        if ((flags & this.FORCE_MULTILINE) || (/\n/.test(data) || !isinstance(data, [_, "String"])) && !(flags & this.FORCE_SINGLELINE))
  1851.              action = mow.closure.echo;
  1852.  
  1853.          if (single)
  1854. @@ -803,12 +811,20 @@ var CommandLine = Module("commandline",
  1855.      // FIXME: Buggy, especially when pasting.
  1856.      inputMultiline: function inputMultiline(end, callback) {
  1857.          let cmd = this.command;
  1858. -        modes.push(modes.INPUT_MULTILINE, null, {
  1859. -            mappingSelf: {
  1860. +        let self = {
  1861.                  end: "\n" + end + "\n",
  1862.                  callback: callback
  1863. -            }
  1864. +        };
  1865. +
  1866. +        modes.push(modes.INPUT_MULTILINE, null, {
  1867. +            holdFocus: true,
  1868. +            leave: function leave() {
  1869. +                if (!self.done)
  1870. +                    self.callback(null);
  1871. +            },
  1872. +            mappingSelf: self
  1873.          });
  1874. +
  1875.          if (cmd != false)
  1876.              this._echoLine(cmd, this.HL_NORMAL);
  1877.  
  1878. @@ -838,7 +854,7 @@ var CommandLine = Module("commandline",
  1879.                      event.target.blur();
  1880.                      dactyl.beep();
  1881.                  }
  1882. -            },
  1883. +            }
  1884.          }
  1885.      ),
  1886.  
  1887. @@ -940,6 +956,7 @@ var CommandLine = Module("commandline",
  1888.              if (this.completions)
  1889.                  this.completions.previewClear();
  1890.              this.input.value = val;
  1891. +            this.session.onHistory(val);
  1892.          },
  1893.  
  1894.          /**
  1895. @@ -1014,9 +1031,9 @@ var CommandLine = Module("commandline",
  1896.              dactyl.registerObserver("events.doneFeeding", this.closure.onDoneFeeding, true);
  1897.  
  1898.              this.autocompleteTimer = Timer(200, 500, function autocompleteTell(tabPressed) {
  1899. -                if (events.feedingKeys)
  1900. +                if (events.feedingKeys && !tabPressed)
  1901.                      this.ignoredCount++;
  1902. -                if (options["autocomplete"].length) {
  1903. +                else if (this.session.autocomplete) {
  1904.                      this.itemList.visible = true;
  1905.                      this.complete(true, false);
  1906.                  }
  1907. @@ -1086,6 +1103,7 @@ var CommandLine = Module("commandline",
  1908.          get wildtype() this.wildtypes[this.wildIndex] || "",
  1909.  
  1910.          complete: function complete(show, tabPressed) {
  1911. +            this.session.ignoredCount = 0;
  1912.              this.context.reset();
  1913.              this.context.tabPressed = tabPressed;
  1914.              this.session.complete(this.context);
  1915. @@ -1230,7 +1248,7 @@ var CommandLine = Module("commandline",
  1916.                      for (let [, context] in Iterator(list)) {
  1917.                          let done = function done() !(idx >= n + context.items.length || idx == -2 && !context.items.length);
  1918.  
  1919. -                        util.waitFor(function () !context.incomplete || done())
  1920. +                        util.waitFor(function () !context.incomplete || done());
  1921.                          if (done())
  1922.                              break;
  1923.  
  1924. @@ -1259,6 +1277,7 @@ var CommandLine = Module("commandline",
  1925.  
  1926.          tab: function tab(reverse, wildmode) {
  1927.              this.autocompleteTimer.flush();
  1928. +            this.ignoredCount = 0;
  1929.  
  1930.              if (this._caret != this.caret)
  1931.                  this.reset();
  1932. @@ -1301,7 +1320,7 @@ var CommandLine = Module("commandline",
  1933.                  if (this.selected == null)
  1934.                      statusline.progress = "";
  1935.                  else
  1936. -                    statusline.progress = "match " + (this.selected + 1) + " of " + this.items.length;
  1937. +                    statusline.progress = _("completion.matchIndex", this.selected + 1, this.items.length);
  1938.              }
  1939.  
  1940.              if (this.items.length == 0)
  1941. @@ -1415,7 +1434,7 @@ var CommandLine = Module("commandline",
  1942.      mappings: function init_mappings() {
  1943.  
  1944.          mappings.add([modes.COMMAND],
  1945. -            [":"], "Enter command-line mode",
  1946. +            [":"], "Enter Command Line mode",
  1947.              function () { CommandExMode().open(""); });
  1948.  
  1949.          mappings.add([modes.INPUT_MULTILINE],
  1950. @@ -1427,6 +1446,7 @@ var CommandLine = Module("commandline",
  1951.  
  1952.                  let index = text.indexOf(self.end);
  1953.                  if (index >= 0) {
  1954. +                    self.done = true;
  1955.                      text = text.substring(1, index);
  1956.                      modes.pop();
  1957.  
  1958. @@ -1614,7 +1634,7 @@ var ItemList = Class("ItemList", {
  1959.      _init: function _init() {
  1960.          this._div = this._dom(
  1961.              <div class="ex-command-output" highlight="Normal" style="white-space: nowrap">
  1962. -                <div highlight="Completions" key="noCompletions"><span highlight="Title">No Completions</span></div>
  1963. +                <div highlight="Completions" key="noCompletions"><span highlight="Title">{_("completion.noCompletions")}</span></div>
  1964.                  <div key="completions"/>
  1965.                  <div highlight="Completions">
  1966.                  {
  1967. @@ -1812,7 +1832,7 @@ var ItemList = Class("ItemList", {
  1968.  
  1969.      onKeyPress: function onKeyPress(event) false
  1970.  }, {
  1971. -    WAITING_MESSAGE: "Generating results..."
  1972. +    WAITING_MESSAGE: _("completion.generating")
  1973.  });
  1974.  
  1975.  // vim: set fdm=marker sw=4 ts=4 et:
  1976. diff --git a/common/content/dactyl.js b/common/content/dactyl.js
  1977. --- a/common/content/dactyl.js
  1978. +++ b/common/content/dactyl.js
  1979. @@ -12,9 +12,6 @@ default xml namespace = XHTML;
  1980.  XML.ignoreWhitespace = false;
  1981.  XML.prettyPrinting = false;
  1982.  
  1983. -var userContext = { __proto__: modules };
  1984. -var _userContext = newContext(userContext);
  1985. -
  1986.  var EVAL_ERROR = "__dactyl_eval_error";
  1987.  var EVAL_RESULT = "__dactyl_eval_result";
  1988.  var EVAL_STRING = "__dactyl_eval_string";
  1989. @@ -41,9 +38,25 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  1990.          };
  1991.  
  1992.          styles.registerSheet("resource://dactyl-skin/dactyl.css");
  1993. +
  1994. +        this.cleanups = [];
  1995. +        this.cleanups.push(util.overlayObject(window, {
  1996. +            focusAndSelectUrlBar: function focusAndSelectUrlBar() {
  1997. +                switch (options.get("strictfocus").getKey(document.documentURIObject || util.newURI(document.documentURI), "moderate")) {
  1998. +                case "laissez-faire":
  1999. +                    if (!Events.isHidden(window.gURLBar, true))
  2000. +                        return focusAndSelectUrlBar.superapply(this, arguments);
  2001. +                default:
  2002. +                    // Evil. Ignore.
  2003. +                }
  2004. +            }
  2005. +        }));
  2006.      },
  2007.  
  2008.      cleanup: function () {
  2009. +        for (let cleanup in values(this.cleanups))
  2010. +            cleanup.call(this);
  2011. +
  2012.          delete window.dactyl;
  2013.          delete window.liberator;
  2014.  
  2015. @@ -57,21 +70,36 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2016.          autocommands.trigger("Leave", {});
  2017.      },
  2018.  
  2019. +    // initially hide all GUI elements, they are later restored unless the user
  2020. +    // has :set go= or something similar in his config
  2021. +    hideGUI: function () {
  2022. +        let guioptions = config.guioptions;
  2023. +        for (let option in guioptions) {
  2024. +            guioptions[option].forEach(function (elem) {
  2025. +                try {
  2026. +                    document.getElementById(elem).collapsed = true;
  2027. +                }
  2028. +                catch (e) {}
  2029. +            });
  2030. +        }
  2031. +    },
  2032. +
  2033. +
  2034.      observers: {
  2035. -        "dactyl-cleanup": function dactyl_cleanup() {
  2036. +        "dactyl-cleanup": function dactyl_cleanup(subject, reason) {
  2037.              let modules = dactyl.modules;
  2038.  
  2039.              for (let mod in values(modules.moduleList.reverse())) {
  2040.                  mod.stale = true;
  2041.                  if ("cleanup" in mod)
  2042. -                    this.trapErrors("cleanup", mod);
  2043. +                    this.trapErrors("cleanup", mod, reason);
  2044.                  if ("destroy" in mod)
  2045. -                    this.trapErrors("destroy", mod);
  2046. +                    this.trapErrors("destroy", mod, reason);
  2047.              }
  2048.  
  2049.              for (let mod in values(modules.ownPropertyValues.reverse()))
  2050.                  if (mod instanceof Class && "INIT" in mod && "cleanup" in mod.INIT)
  2051. -                    this.trapErrors(mod.cleanup, mod, dactyl, modules, window);
  2052. +                    this.trapErrors(mod.cleanup, mod, dactyl, modules, window, reason);
  2053.  
  2054.              for (let name in values(Object.getOwnPropertyNames(modules).reverse()))
  2055.                  try {
  2056. @@ -98,7 +126,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2057.      }),
  2058.  
  2059.      /**
  2060. -     * @property {number} The current main mode.
  2061. +     * @property {Modes.Mode} The current main mode.
  2062.       * @see modes#mainModes
  2063.       */
  2064.      mode: deprecated("modes.main", {
  2065. @@ -106,7 +134,37 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2066.          set: function mode(val) modes.main = val
  2067.      }),
  2068.  
  2069. -    get menuItems() Dactyl.getMenuItems(),
  2070. +    get menuItems() {
  2071. +        function dispatch(node, name) {
  2072. +            let event = node.ownerDocument.createEvent("Events");
  2073. +            event.initEvent(name, false, false);
  2074. +            node.dispatchEvent(event);
  2075. +        }
  2076. +
  2077. +        function addChildren(node, parent) {
  2078. +            if (~["menu", "menupopup"].indexOf(node.localName) && node.children.length)
  2079. +                dispatch(node, "popupshowing");
  2080. +
  2081. +            for (let [, item] in Iterator(node.childNodes)) {
  2082. +                if (item.childNodes.length == 0 && item.localName == "menuitem"
  2083. +                    && !item.hidden
  2084. +                    && !/rdf:http:/.test(item.getAttribute("label"))) { // FIXME
  2085. +                    item.dactylPath = parent + item.getAttribute("label");
  2086. +                    items.push(item);
  2087. +                }
  2088. +                else {
  2089. +                    let path = parent;
  2090. +                    if (item.localName == "menu")
  2091. +                        path += item.getAttribute("label") + ".";
  2092. +                    addChildren(item, path);
  2093. +                }
  2094. +            }
  2095. +        }
  2096. +
  2097. +        let items = [];
  2098. +        addChildren(document.getElementById(config.guioptions["m"][1]), "");
  2099. +        return items;
  2100. +    },
  2101.  
  2102.      // Global constants
  2103.      CURRENT_TAB: "here",
  2104. @@ -184,14 +242,16 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2105.      },
  2106.  
  2107.      addUsageCommand: function (params) {
  2108. +        function keys(item) (item.names || [item.name]).concat(item.description, item.columns || []);
  2109. +
  2110.          let name = commands.add(params.name, params.description,
  2111.              function (args) {
  2112.                  let results = array(params.iterate(args))
  2113.                      .sort(function (a, b) String.localeCompare(a.name, b.name));
  2114.  
  2115. -                let filters = args.map(function (arg) RegExp("\\b" + util.regexp.escape(arg) + "\\b", "i"));
  2116. +                let filters = args.map(function (arg) util.regexp("\\b" + util.regexp.escape(arg) + "\\b", "i"));
  2117.                  if (filters.length)
  2118. -                    results = results.filter(function (item) filters.every(function (re) re.test(item.name + " " + item.description)));
  2119. +                    results = results.filter(function (item) filters.every(function (re) keys(item).some(re.closure.test)));
  2120.  
  2121.                  commandline.commandOutput(
  2122.                      template.usage(results, params.format));
  2123. @@ -200,9 +260,9 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2124.                  argCount: "*",
  2125.                  completer: function (context, args) {
  2126.                      context.keys.text = util.identity;
  2127. -                    context.keys.description = function () seen[this.text] + " matching items";
  2128. +                    context.keys.description = function () seen[this.text] + /*L*/" matching items";
  2129.                      let seen = {};
  2130. -                    context.completions = array(item.description.toLowerCase().split(/[()\s]+/)
  2131. +                    context.completions = array(keys(item).join(" ").toLowerCase().split(/[()\s]+/)
  2132.                                                  for (item in params.iterate(args)))
  2133.                          .flatten().filter(function (w) /^\w[\w-_']+$/.test(w))
  2134.                          .map(function (k) {
  2135. @@ -221,7 +281,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2136.                  let tags = services["dactyl:"].HELP_TAGS;
  2137.                  for (let obj in values(results)) {
  2138.                      let res = dactyl.generateHelp(obj, null, null, true);
  2139. -                    if (!set.has(tags, obj.helpTag))
  2140. +                    if (!Set.has(tags, obj.helpTag))
  2141.                          res[1].@tag = obj.helpTag;
  2142.  
  2143.                      yield res;
  2144. @@ -311,7 +371,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2145.          clipboardHelper.copyString(str);
  2146.  
  2147.          if (verbose) {
  2148. -            let message = { message: "Yanked " + str };
  2149. +            let message = { message: _("dactyl.yank", str) };
  2150.              try {
  2151.                  message.domains = [util.newURI(str).host];
  2152.              }
  2153. @@ -346,7 +406,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2154.      echoerr: function echoerr(str, flags) {
  2155.          flags |= commandline.APPEND_TO_MESSAGES;
  2156.  
  2157. -        if (isinstance(str, ["Error", "Exception"]))
  2158. +        if (isinstance(str, ["DOMException", "Error", "Exception"]) || isinstance(str, ["XPCWrappedNative_NoHelper"]) && /^\[Exception/.test(str))
  2159.              dactyl.reportError(str);
  2160.          if (isObject(str) && "echoerr" in str)
  2161.              str = str.echoerr;
  2162. @@ -414,7 +474,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2163.                  ({ file: fileName, line: lineNumber, context: ctxt }) = info;
  2164.  
  2165.          if (!context && fileName && fileName[0] !== "[")
  2166. -            context = _userContext || ctxt;
  2167. +            context = ctxt || _userContext;
  2168.  
  2169.          if (isinstance(context, ["Sandbox"]))
  2170.              return Cu.evalInSandbox(str, context, "1.8", fileName, lineNumber);
  2171. @@ -559,7 +619,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2172.       * @param {string} feature The feature name.
  2173.       * @returns {boolean}
  2174.       */
  2175. -    has: function (feature) set.has(config.features, feature),
  2176. +    has: function (feature) Set.has(config.features, feature),
  2177.  
  2178.      /**
  2179.       * Returns the URL of the specified help *topic* if it exists.
  2180. @@ -608,6 +668,10 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2181.       * Initialize the help system.
  2182.       */
  2183.      initHelp: function (force) {
  2184. +        // Waits for the add-on to become available, if necessary.
  2185. +        config.addon;
  2186. +        config.version;
  2187. +
  2188.          if (force || !this.helpInitialized) {
  2189.              if ("noscriptOverlay" in window) {
  2190.                  noscriptOverlay.safeAllow("chrome-data:", true, false);
  2191. @@ -665,8 +729,9 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2192.  
  2193.              let body = XML();
  2194.              for (let [, context] in Iterator(plugins.contexts))
  2195. -                if (context && context.INFO instanceof XML) {
  2196. -                    let info = context.INFO;
  2197. +                try {
  2198. +                    let info = contexts.getDocs(context);
  2199. +                    if (info instanceof XML) {
  2200.                      if (info.*.@lang.length()) {
  2201.                          let lang = config.bestLocale(String(a) for each (a in info.*.@lang));
  2202.  
  2203. @@ -677,22 +742,25 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2204.                                  if (elem[attr].length())
  2205.                                      info[attr] = elem[attr];
  2206.                      }
  2207. -                    body += <h2 xmlns={NS.uri} tag={context.INFO.@name + '-plugin'}>{context.INFO.@summary}</h2> +
  2208. -                        context.INFO;
  2209. +                        body += <h2 xmlns={NS.uri} tag={info.@name + '-plugin'}>{info.@summary}</h2> +
  2210. +                            info;
  2211. +                    }
  2212. +                }
  2213. +                catch (e) {
  2214. +                    util.reportError(e);
  2215.                  }
  2216.  
  2217.              let help =
  2218.                  '<?xml version="1.0"?>\n' +
  2219.                  '<?xml-stylesheet type="text/xsl" href="dactyl://content/help.xsl"?>\n' +
  2220.                  '<!DOCTYPE document SYSTEM "resource://dactyl-content/dactyl.dtd">\n' +
  2221. -                unescape(encodeURI( // UTF-8 handling hack.
  2222.                  <document xmlns={NS}
  2223.                      name="plugins" title={config.appName + " Plugins"}>
  2224. -                    <h1 tag="using-plugins">Using Plugins</h1>
  2225. +                    <h1 tag="using-plugins">{_("help.title.Using Plugins")}</h1>
  2226.                      <toc start="2"/>
  2227.  
  2228.                      {body}
  2229. -                </document>.toXMLString()));
  2230. +                </document>.toXMLString();
  2231.              fileMap["plugins"] = function () ['text/xml;charset=UTF-8', help];
  2232.  
  2233.              fileMap["versions"] = function () {
  2234. @@ -725,6 +793,8 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2235.  
  2236.                  default xml namespace = NS;
  2237.                  function rec(text, level, li) {
  2238. +                    XML.ignoreWhitespace = XML.prettyPrinting = false;
  2239. +
  2240.                      let res = <></>;
  2241.                      let list, space, i = 0;
  2242.  
  2243. @@ -735,7 +805,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2244.                              if (!list)
  2245.                                  res += list = <ul/>;
  2246.                              let li = <li/>;
  2247. -                            li.* += rec(match.content.replace(RegExp("^" + match.space, "gm"), ""), level + 1, li)
  2248. +                            li.* += rec(match.content.replace(RegExp("^" + match.space, "gm"), ""), level + 1, li);
  2249.                              list.* += li;
  2250.                          }
  2251.                          else if (match.par) {
  2252. @@ -751,8 +821,10 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2253.                              }
  2254.  
  2255.                              list = null;
  2256. -                            if (level == 0 && /^.*:\n$/.test(match.par))
  2257. -                                res += <h2>{template.linkifyHelp(par.slice(0, -1), true)}</h2>;
  2258. +                            if (level == 0 && /^.*:\n$/.test(match.par)) {
  2259. +                                let text = par.slice(0, -1);
  2260. +                                res += <h2 tag={"news-" + text}>{template.linkifyHelp(text, true)}</h2>;
  2261. +                            }
  2262.                              else {
  2263.                                  let [, a, b] = /^(IMPORTANT:?)?([^]*)/.exec(par);
  2264.                                  res += <p highlight={group + " HelpNews"}>{
  2265. @@ -774,6 +846,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2266.                      return res;
  2267.                  }
  2268.  
  2269. +                XML.ignoreWhitespace = XML.prettyPrinting = false;
  2270.                  let body = rec(NEWS, 0);
  2271.                  for each (let li in body..li) {
  2272.                      let list = li..li.(@NS::highlight == "HelpNewsOld");
  2273. @@ -784,19 +857,17 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2274.                      }
  2275.                  }
  2276.  
  2277. -                XML.prettyPrinting = XML.ignoreWhitespace = false;
  2278.                  return ["application/xml",
  2279.                      '<?xml version="1.0"?>\n' +
  2280.                      '<?xml-stylesheet type="text/xsl" href="dactyl://content/help.xsl"?>\n' +
  2281.                      '<!DOCTYPE document SYSTEM "resource://dactyl-content/dactyl.dtd">\n' +
  2282. -                    unescape(encodeURI( // UTF-8 handling hack.
  2283.                      <document xmlns={NS} xmlns:dactyl={NS}
  2284.                          name="versions" title={config.appName + " Versions"}>
  2285.                          <h1 tag="versions news NEWS">{config.appName} Versions</h1>
  2286.                          <toc start="2"/>
  2287.  
  2288.                          {body}
  2289. -                    </document>.toXMLString()))
  2290. +                    </document>.toXMLString()
  2291.                  ];
  2292.              }
  2293.              addTags("versions", util.httpGet("dactyl://help/versions").responseXML);
  2294. @@ -806,15 +877,26 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2295.  
  2296.              overlayMap["index"] = ['text/xml;charset=UTF-8',
  2297.                  '<?xml version="1.0"?>\n' +
  2298. -                '<overlay xmlns="' + NS + '">\n' +
  2299. -                unescape(encodeURI( // UTF-8 handling hack.
  2300. +                <overlay xmlns={NS}>{
  2301.                  template.map(dactyl.indices, function ([name, iter])
  2302.                      <dl insertafter={name + "-index"}>{
  2303.                          template.map(iter(), util.identity)
  2304. -                    }</dl>, <>{"\n\n"}</>))) +
  2305. -                '\n</overlay>'];
  2306. +                    }</dl>, <>{"\n\n"}</>)
  2307. +                }</overlay>];
  2308. +            addTags("index", util.httpGet("dactyl://help-overlay/index").responseXML);
  2309.  
  2310. -            addTags("index", util.httpGet("dactyl://help-overlay/index").responseXML);
  2311. +            overlayMap["gui"] = ['text/xml;charset=UTF-8',
  2312. +                '<?xml version="1.0"?>\n' +
  2313. +                <overlay xmlns={NS}>
  2314. +                    <dl insertafter="dialog-list">{
  2315. +                    template.map(config.dialogs, function ([name, val])
  2316. +                        (!val[2] || val[2]())
  2317. +                            ? <><dt>{name}</dt><dd>{val[0]}</dd></>
  2318. +                            : undefined,
  2319. +                        <>{"\n"}</>)
  2320. +                    }</dl>
  2321. +                </overlay>];
  2322. +
  2323.  
  2324.              this.helpInitialized = true;
  2325.          }
  2326. @@ -850,7 +932,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2327.                  addURIEntry(file, "data:text/plain;charset=UTF-8," + encodeURI(data));
  2328.          }
  2329.  
  2330. -        let empty = set("area base basefont br col frame hr img input isindex link meta param"
  2331. +        let empty = Set("area base basefont br col frame hr img input isindex link meta param"
  2332.                              .split(" "));
  2333.          function fix(node) {
  2334.              switch(node.nodeType) {
  2335. @@ -860,16 +942,17 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2336.  
  2337.                      data.push("<"); data.push(node.localName);
  2338.                      if (node instanceof HTMLHtmlElement)
  2339. -                        data.push(" xmlns=" + XHTML.uri.quote());
  2340. +                    data.push(" xmlns=" + XHTML.uri.quote(),
  2341. +                              " xmlns:dactyl=" + NS.uri.quote());
  2342.  
  2343.                      for (let { name, value } in array.iterValues(node.attributes)) {
  2344.                          if (name == "dactyl:highlight") {
  2345. -                            set.add(styles, value);
  2346. +                        Set.add(styles, value);
  2347.                              name = "class";
  2348.                              value = "hl-" + value;
  2349.                          }
  2350.                          if (name == "href") {
  2351. -                            value = node.href;
  2352. +                        value = node.href || value;
  2353.                              if (value.indexOf("dactyl://help-tag/") == 0) {
  2354.                                  let uri = services.io.newChannel(value, null, null).originalURI;
  2355.                                  value = uri.spec == value ? "javascript:;" : uri.path.substr(1);
  2356. @@ -881,11 +964,10 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2357.                              chromeFiles[value] = value.replace(/.*\//, "");
  2358.                              value = value.replace(/.*\//, "");
  2359.                          }
  2360. -                        data.push(" ");
  2361. -                        data.push(name);
  2362. -                        data.push('="');
  2363. -                        data.push(<>{value}</>.toXMLString());
  2364. -                        data.push('"');
  2365. +
  2366. +                    data.push(" ", name, '="',
  2367. +                              <>{value}</>.toXMLString().replace(/"/g, "&quot;"),
  2368. +                              '"');
  2369.                      }
  2370.                      if (node.localName in empty)
  2371.                          data.push(" />");
  2372. @@ -894,7 +976,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2373.                          if (node instanceof HTMLHeadElement)
  2374.                              data.push(<link rel="stylesheet" type="text/css" href="help.css"/>.toXMLString());
  2375.                          Array.map(node.childNodes, fix);
  2376. -                        data.push("</"); data.push(node.localName); data.push(">");
  2377. +                    data.push("</", node.localName, ">");
  2378.                      }
  2379.                      break;
  2380.                  case Node.TEXT_NODE:
  2381. @@ -905,9 +987,13 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2382.          let chromeFiles = {};
  2383.          let styles = {};
  2384.          for (let [file, ] in Iterator(services["dactyl:"].FILE_MAP)) {
  2385. -            dactyl.open("dactyl://help/" + file);
  2386. -            dactyl.modules.events.waitForPageLoad();
  2387. -            let data = [
  2388. +            let url = "dactyl://help/" + file;
  2389. +            dactyl.open(url);
  2390. +            util.waitFor(function () content.location.href == url && buffer.loaded
  2391. +                            && content.document.documentElement instanceof HTMLHtmlElement,
  2392. +                         15000);
  2393. +            events.waitForPageLoad();
  2394. +            var data = [
  2395.                  '<?xml version="1.0" encoding="UTF-8"?>\n',
  2396.                  '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"\n',
  2397.                  '          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n'
  2398. @@ -916,7 +1002,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2399.              addDataEntry(file + ".xhtml", data.join(""));
  2400.          }
  2401.  
  2402. -        let data = [h for (h in highlight) if (set.has(styles, h.class) || /^Help/.test(h.class))]
  2403. +        let data = [h for (h in highlight) if (Set.has(styles, h.class) || /^Help/.test(h.class))]
  2404.              .map(function (h) h.selector
  2405.                                 .replace(/^\[.*?=(.*?)\]/, ".hl-$1")
  2406.                                 .replace(/html\|/g, "") + "\t" + "{" + h.cssText + "}")
  2407. @@ -954,7 +1040,13 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2408.          if (obj instanceof Command) {
  2409.              link = function (cmd) <ex>{cmd}</ex>;
  2410.              args = obj.parseArgs("", CompletionContext(str || ""));
  2411. -            spec = function (cmd) cmd + (obj.bang ? <oa>!</oa> : <></>);
  2412. +            spec = function (cmd) <>{
  2413. +                    obj.count ? <oa>count</oa> : <></>
  2414. +                }{
  2415. +                    cmd
  2416. +                }{
  2417. +                    obj.bang ? <oa>!</oa> : <></>
  2418. +                }</>;
  2419.          }
  2420.          else if (obj instanceof Map) {
  2421.              spec = function (map) obj.count ? <><oa>count</oa>{map}</> : <>{map}</>;
  2422. @@ -969,7 +1061,9 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2423.              };
  2424.          }
  2425.          else if (obj instanceof Option) {
  2426. +            tag = spec = function (name) <>'{name}'</>;
  2427.              link = function (opt, name) <o>{name}</o>;
  2428. +            args = { value: "", values: [] };
  2429.          }
  2430.  
  2431.          XML.prettyPrinting = false;
  2432. @@ -1001,7 +1095,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2433.                  <description>{
  2434.                      obj.description ? br + <p>{template.linkifyHelp(obj.description.replace(/\.?$/, "."), true)}</p> : "" }{
  2435.                          extraHelp ? br + extraHelp : "" }{
  2436. -                        !(extraHelp || obj.description) ? br + <p>Sorry, no help available.</p> : "" }
  2437. +                        !(extraHelp || obj.description) ? br + <p><!--L-->Sorry, no help available.</p> : "" }
  2438.                  </description>
  2439.              </item></>;
  2440.  
  2441. @@ -1014,7 +1108,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2442.          }
  2443.  
  2444.          if (obj.completer)
  2445. -            add(completion._runCompleter(obj.completer, "", null, args).items
  2446. +            add(completion._runCompleter(obj.closure.completer, "", null, args).items
  2447.                            .map(function (i) [i.text, i.description]));
  2448.  
  2449.          if (obj.options && obj.options.some(function (o) o.description))
  2450. @@ -1064,7 +1158,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2451.       * These are set and accessed with the "g:" prefix.
  2452.       */
  2453.      _globalVariables: {},
  2454. -    globalVariables: deprecated("the options system", {
  2455. +    globalVariables: deprecated(_("deprecated.for.theOptionsSystem"), {
  2456.          get: function globalVariables() this._globalVariables
  2457.      }),
  2458.  
  2459. @@ -1076,13 +1170,14 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2460.  
  2461.              let loadplugins = options.get("loadplugins");
  2462.              if (args)
  2463. -                loadplugins = { __proto__: loadplugins, value: args.map(Option.parseRegexp) }
  2464. +                loadplugins = { __proto__: loadplugins, value: args.map(Option.parseRegexp) };
  2465.  
  2466.              dir.readDirectory(true).forEach(function (file) {
  2467. -                if (file.isFile() && loadplugins.getKey(file.path) && !(!force && file.path in dactyl.pluginFiles)) {
  2468. +                if (file.isFile() && loadplugins.getKey(file.path)
  2469. +                        && !(!force && file.path in dactyl.pluginFiles && dactyl.pluginFiles[file.path] >= file.lastModifiedTime)) {
  2470.                      try {
  2471.                          io.source(file.path);
  2472. -                        dactyl.pluginFiles[file.path] = true;
  2473. +                        dactyl.pluginFiles[file.path] = file.lastModifiedTime;
  2474.                      }
  2475.                      catch (e) {
  2476.                          dactyl.reportError(e);
  2477. @@ -1151,7 +1246,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2478.      onExecute: function onExecute(event) {
  2479.          let cmd = event.originalTarget.getAttribute("dactyl-execute");
  2480.          commands.execute(cmd, null, false, null,
  2481. -                         { file: "[Command Line]", line: 1 });
  2482. +                         { file: /*L*/"[Command Line]", line: 1 });
  2483.      },
  2484.  
  2485.      /**
  2486. @@ -1187,7 +1282,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2487.              urls = dactyl.parseURLs(urls);
  2488.  
  2489.          if (urls.length > prefs.get("browser.tabs.maxOpenBeforeWarn", 20) && !force)
  2490. -            return commandline.input("This will open " + urls.length + " new tabs. Would you like to continue? (yes/[no]) ",
  2491. +            return commandline.input(_("dactyl.prompt.openMany", urls.length) + " ",
  2492.                  function (resp) {
  2493.                      if (resp && resp.match(/^y(es)?$/i))
  2494.                          dactyl.open(urls, params, true);
  2495. @@ -1270,7 +1365,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2496.       * ['www.google.com/search?q=bla', 'www.osnews.com']
  2497.       *
  2498.       * @param {string} str
  2499. -     * @returns {string[]}
  2500. +     * @returns {[string]}
  2501.       */
  2502.      parseURLs: function parseURLs(str) {
  2503.          let urls;
  2504. @@ -1283,7 +1378,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2505.          return urls.map(function (url) {
  2506.              url = url.trim();
  2507.  
  2508. -            if (/^(\.{0,2}|~)(\/|$)/.test(url)) {
  2509. +            if (/^(\.{0,2}|~)(\/|$)/.test(url) || util.OS.isWindows && /^[a-z]:/i.test(url)) {
  2510.                  try {
  2511.                      // Try to find a matching file.
  2512.                      let file = io.File(url);
  2513. @@ -1296,7 +1391,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2514.              // If it starts with a valid protocol, pass it through.
  2515.              let proto = /^([-\w]+):/.exec(url);
  2516.              if (proto && "@mozilla.org/network/protocol;1?name=" + proto[1] in Cc)
  2517. -                return url.replace(/\s+/g, "");
  2518. +                return url;
  2519.  
  2520.              // Check for a matching search keyword.
  2521.              let searchURL = this.has("bookmarks") && bookmarks.getSearchURL(url, false);
  2522. @@ -1470,48 +1565,13 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2523.      },
  2524.  
  2525.      /**
  2526. -     * @property {Window[]} Returns an array of all the host application's
  2527. +     * @property {[Window]} Returns an array of all the host application's
  2528.       *     open windows.
  2529.       */
  2530. -    get windows() [win for (win in iter(services.windowMediator.getEnumerator("navigator:browser")))],
  2531. +    get windows() [win for (win in iter(services.windowMediator.getEnumerator("navigator:browser"))) if (win.dactyl)],
  2532.  
  2533.  }, {
  2534. -    // initially hide all GUI elements, they are later restored unless the user
  2535. -    // has :set go= or something similar in his config
  2536. -    hideGUI: function () {
  2537. -        let guioptions = config.guioptions;
  2538. -        for (let option in guioptions) {
  2539. -            guioptions[option].forEach(function (elem) {
  2540. -                try {
  2541. -                    document.getElementById(elem).collapsed = true;
  2542. -                }
  2543. -                catch (e) {}
  2544. -            });
  2545. -        }
  2546. -    },
  2547. -
  2548. -    // TODO: move this
  2549. -    getMenuItems: function () {
  2550. -        function addChildren(node, parent) {
  2551. -            for (let [, item] in Iterator(node.childNodes)) {
  2552. -                if (item.childNodes.length == 0 && item.localName == "menuitem"
  2553. -                    && !/rdf:http:/.test(item.getAttribute("label"))) { // FIXME
  2554. -                    item.fullMenuPath = parent + item.getAttribute("label");
  2555. -                    items.push(item);
  2556. -                }
  2557. -                else {
  2558. -                    let path = parent;
  2559. -                    if (item.localName == "menu")
  2560. -                        path += item.getAttribute("label") + ".";
  2561. -                    addChildren(item, path);
  2562. -                }
  2563. -            }
  2564. -        }
  2565. -
  2566. -        let items = [];
  2567. -        addChildren(document.getElementById(config.guioptions["m"][1]), "");
  2568. -        return items;
  2569. -    }
  2570. +    toolbarHidden: function hidden(elem) (elem.getAttribute("autohide") || elem.getAttribute("collapsed")) == "true"
  2571.  }, {
  2572.      events: function () {
  2573.          events.listen(window, "click", dactyl.closure.onClick, true);
  2574. @@ -1542,7 +1602,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2575.                      M: ["Always show messages outside of the status line"]
  2576.                  },
  2577.                  setter: function (opts) {
  2578. -                    if (loaded.commandline)
  2579. +                    if (loaded.commandline || ~opts.indexOf("c"))
  2580.                          commandline.widgets.updateVisibility();
  2581.                  }
  2582.              },
  2583. @@ -1578,7 +1638,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2584.                                        true);
  2585.  
  2586.                      prefs.safeSet("layout.scrollbar.side", opts.indexOf("l") >= 0 ? 3 : 2,
  2587. -                                  "See 'guioptions' scrollbar flags.");
  2588. +                                  _("option.guioptions.safeSet"));
  2589.                  },
  2590.                  validator: function (opts) Option.validIf(!(opts.indexOf("l") >= 0 && opts.indexOf("r") >= 0),
  2591.                                                            UTF8("Only one of ‘l’ or ‘r’ allowed"))
  2592. @@ -1616,7 +1676,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2593.                  // FIXME: cleanup
  2594.                  cleanupValue: config.cleanups.guioptions ||
  2595.                      "r" + [k for ([k, v] in iter(groups[1].opts))
  2596. -                           if (!document.getElementById(v[1][0]).collapsed)].join(""),
  2597. +                           if (!Dactyl.toolbarHidden(document.getElementById(v[1][0])))].join(""),
  2598.  
  2599.                  values: array(groups).map(function (g) [[k, v[0]] for ([k, v] in Iterator(g.opts))]).flatten(),
  2600.  
  2601. @@ -1684,18 +1744,18 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2602.              {
  2603.                  setter: function (value) {
  2604.                      prefs.safeSet("accessibility.typeaheadfind.enablesound", !value,
  2605. -                                  "See 'visualbell' option");
  2606. +                                  _("option.visualbell.safeSet"));
  2607.                      return value;
  2608.                  }
  2609.              });
  2610.      },
  2611.  
  2612.      mappings: function () {
  2613. -        mappings.add([modes.MAIN], ["<F1>"],
  2614. +        mappings.add([modes.MAIN], ["<open-help>", "<F1>"],
  2615.              "Open the introductory help page",
  2616.              function () { dactyl.help(); });
  2617.  
  2618. -        mappings.add([modes.MAIN], ["<A-F1>"],
  2619. +        mappings.add([modes.MAIN], ["<open-single-help>", "<A-F1>"],
  2620.              "Open the single, consolidated help page",
  2621.              function () { ex.helpall(); });
  2622.  
  2623. @@ -1727,7 +1787,6 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2624.                  }
  2625.              }, {
  2626.                  argCount: "1",
  2627. -                bang: true,
  2628.                  completer: function (context) {
  2629.                      context.ignoreCase = true;
  2630.                      completion.dialog(context);
  2631. @@ -1738,15 +1797,17 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2632.              "Execute the specified menu item from the command line",
  2633.              function (args) {
  2634.                  let arg = args[0] || "";
  2635. -                let items = Dactyl.getMenuItems();
  2636. +                let items = dactyl.menuItems;
  2637.  
  2638. -                dactyl.assert(items.some(function (i) i.fullMenuPath == arg),
  2639. +                dactyl.assert(items.some(function (i) i.dactylPath == arg),
  2640.                                _("emenu.notFound", arg));
  2641.  
  2642.                  for (let [, item] in Iterator(items)) {
  2643. -                    if (item.fullMenuPath == arg)
  2644. +                    if (item.dactylPath == arg) {
  2645. +                        dactyl.assert(!item.disabled, _("error.disabled", item.dactylPath));
  2646.                          item.doCommand();
  2647.                  }
  2648. +                }
  2649.              }, {
  2650.                  argCount: "1",
  2651.                  completer: function (context) completion.menuItem(context),
  2652. @@ -1793,7 +1854,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2653.          });
  2654.  
  2655.          commands.add(["loadplugins", "lpl"],
  2656. -            "Load all plugins immediately",
  2657. +            "Load all or matching plugins",
  2658.              function (args) {
  2659.                  dactyl.loadPlugins(args.length ? args : null, args.bang);
  2660.              },
  2661. @@ -1819,6 +1880,15 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2662.                  literal: 0
  2663.              });
  2664.  
  2665. +        commands.add(["exit", "x"],
  2666. +            "Quit " + config.appName,
  2667. +            function (args) {
  2668. +                dactyl.quit(false, args.bang);
  2669. +            }, {
  2670. +                argCount: "0",
  2671. +                bang: true
  2672. +            });
  2673. +
  2674.          commands.add(["q[uit]"],
  2675.              dactyl.has("tabs") ? "Quit current tab" : "Quit application",
  2676.              function (args) {
  2677. @@ -1837,12 +1907,12 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2678.              "Reload the " + config.appName + " add-on",
  2679.              function (args) {
  2680.                  if (args.trailing)
  2681. -                    JSMLoader.rehashCmd = args.trailing; // Hack.
  2682. +                    storage.session.rehashCmd = args.trailing; // Hack.
  2683.                  args.break = true;
  2684.                  util.rehash(args);
  2685.              },
  2686.              {
  2687. -                argCount: "0",
  2688. +                argCount: "0", // FIXME
  2689.                  options: [
  2690.                      {
  2691.                          names: ["+u"],
  2692. @@ -1870,16 +1940,16 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2693.  
  2694.          commands.add(["res[tart]"],
  2695.              "Force " + config.appName + " to restart",
  2696. -            function () { dactyl.restart(); });
  2697. +            function () { dactyl.restart(); },
  2698. +            { argCount: "0" });
  2699.  
  2700.          function findToolbar(name) util.evaluateXPath(
  2701. -            "//*[@toolbarname=" + util.escapeString(name, "'") + "]",
  2702. +            "//*[@toolbarname=" + util.escapeString(name, "'") + " or " +
  2703. +                "@toolbarname=" + util.escapeString(name.trim(), "'") + "]",
  2704.              document).snapshotItem(0);
  2705.  
  2706.          var toolbox = document.getElementById("navigator-toolbox");
  2707.          if (toolbox) {
  2708. -            let hidden = function hidden(elem) (elem.getAttribute("autohide") || elem.getAttribute("collapsed")) == "true";
  2709. -
  2710.              let toolbarCommand = function (names, desc, action, filter) {
  2711.                  commands.add(names, desc,
  2712.                      function (args) {
  2713. @@ -1888,7 +1958,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2714.                          action(toolbar);
  2715.                          events.checkFocus();
  2716.                      }, {
  2717. -                        argcount: "1",
  2718. +                        argCount: "1",
  2719.                          completer: function (context) {
  2720.                              completion.toolbar(context);
  2721.                              if (filter)
  2722. @@ -1900,12 +1970,12 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2723.  
  2724.              toolbarCommand(["toolbars[how]", "tbs[how]"], "Show the named toolbar",
  2725.                  function (toolbar) dactyl.setNodeVisible(toolbar, true),
  2726. -                function ({ item }) hidden(item));
  2727. +                function ({ item }) Dactyl.toolbarHidden(item));
  2728.              toolbarCommand(["toolbarh[ide]", "tbh[ide]"], "Hide the named toolbar",
  2729.                  function (toolbar) dactyl.setNodeVisible(toolbar, false),
  2730. -                function ({ item }) !hidden(item));
  2731. +                function ({ item }) !Dactyl.toolbarHidden(item));
  2732.              toolbarCommand(["toolbart[oggle]", "tbt[oggle]"], "Toggle the named toolbar",
  2733. -                function (toolbar) dactyl.setNodeVisible(toolbar, hidden(toolbar)));
  2734. +                function (toolbar) dactyl.setNodeVisible(toolbar, Dactyl.toolbarHidden(toolbar)));
  2735.          }
  2736.  
  2737.          commands.add(["time"],
  2738. @@ -1916,9 +1986,9 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2739.                  args = args[0] || "";
  2740.  
  2741.                  if (args[0] == ":")
  2742. -                    var method = function () commands.execute(args, null, true);
  2743. +                    var func = function () commands.execute(args, null, false);
  2744.                  else
  2745. -                    method = dactyl.userFunc(args);
  2746. +                    func = dactyl.userFunc(args);
  2747.  
  2748.                  try {
  2749.                      if (count > 1) {
  2750. @@ -1927,7 +1997,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2751.  
  2752.                          for (let i in util.interruptibleRange(0, count, 500)) {
  2753.                              let now = Date.now();
  2754. -                            method();
  2755. +                            func();
  2756.                              total += Date.now() - now;
  2757.                          }
  2758.  
  2759. @@ -1953,16 +2023,16 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2760.                          commandline.commandOutput(
  2761.                                  <table>
  2762.                                      <tr highlight="Title" align="left">
  2763. -                                        <th colspan="3">Code execution summary</th>
  2764. +                                        <th colspan="3">{_("title.Code execution summary")}</th>
  2765.                                      </tr>
  2766. -                                    <tr><td>&#xa0;&#xa0;Executed:</td><td align="right"><span class="times-executed">{count}</span></td><td>times</td></tr>
  2767. -                                    <tr><td>&#xa0;&#xa0;Average time:</td><td align="right"><span class="time-average">{each.toFixed(2)}</span></td><td>{eachUnits}</td></tr>
  2768. -                                    <tr><td>&#xa0;&#xa0;Total time:</td><td align="right"><span class="time-total">{total.toFixed(2)}</span></td><td>{totalUnits}</td></tr>
  2769. +                                    <tr><td>&#xa0;&#xa0;{_("title.Executed")}:</td><td align="right"><span class="times-executed">{count}</span></td><td><!--L-->times</td></tr>
  2770. +                                    <tr><td>&#xa0;&#xa0;{_("title.Average time")}:</td><td align="right"><span class="time-average">{each.toFixed(2)}</span></td><td>{eachUnits}</td></tr>
  2771. +                                    <tr><td>&#xa0;&#xa0;{_("title.Total time")}:</td><td align="right"><span class="time-total">{total.toFixed(2)}</span></td><td>{totalUnits}</td></tr>
  2772.                                  </table>);
  2773.                      }
  2774.                      else {
  2775.                          let beforeTime = Date.now();
  2776. -                        method();
  2777. +                        func();
  2778.  
  2779.                          if (special)
  2780.                              return;
  2781. @@ -1979,7 +2049,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2782.                      dactyl.echoerr(e);
  2783.                  }
  2784.              }, {
  2785. -                argCount: "+",
  2786. +                argCount: "1",
  2787.                  bang: true,
  2788.                  completer: function (context) {
  2789.                      if (/^:/.test(context.filter))
  2790. @@ -2010,7 +2080,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2791.                      vbs.setFrom = setFrom;
  2792.                  }
  2793.              }, {
  2794. -                argCount: "+",
  2795. +                argCount: "1",
  2796.                  completer: function (context) completion.ex(context),
  2797.                  count: true,
  2798.                  literal: 0,
  2799. @@ -2052,8 +2122,12 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2800.          completion.menuItem = function menuItem(context) {
  2801.              context.title = ["Menu Path", "Label"];
  2802.              context.anchored = false;
  2803. -            context.keys = { text: "fullMenuPath", description: function (item) item.getAttribute("label") };
  2804. -            context.completions = dactyl.menuItems;
  2805. +            context.keys = {
  2806. +                text: "dactylPath",
  2807. +                description: function (item) item.getAttribute("label"),
  2808. +                highlight: function (item) item.disabled ? "Disabled" : ""
  2809. +            };
  2810. +            context.generate = function () dactyl.menuItems;
  2811.          };
  2812.  
  2813.          var toolbox = document.getElementById("navigator-toolbox");
  2814. @@ -2076,7 +2150,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2815.  
  2816.          dactyl.timeout(function () {
  2817.              try {
  2818. -                var args = JSMLoader.commandlineArgs || services.commandLineHandler.optionValue;
  2819. +                var args = storage.session.commandlineArgs || services.commandLineHandler.optionValue;
  2820.                  if (isString(args))
  2821.                      args = dactyl.parseCommandLine(args);
  2822.  
  2823. @@ -2107,7 +2181,7 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2824.              }
  2825.  
  2826.              // TODO: we should have some class where all this guioptions stuff fits well
  2827. -            // Dactyl.hideGUI();
  2828. +            // dactyl.hideGUI();
  2829.  
  2830.              if (dactyl.userEval("typeof document", null, "test.js") === "undefined")
  2831.                  jsmodules.__proto__ = XPCSafeJSObjectWrapper(window);
  2832. @@ -2167,9 +2241,9 @@ var Dactyl = Module("dactyl", XPCOM(Ci.n
  2833.                      dactyl.execute(cmd);
  2834.                  });
  2835.  
  2836. -            if (JSMLoader.rehashCmd)
  2837. -                dactyl.execute(JSMLoader.rehashCmd);
  2838. -            JSMLoader.rehashCmd = null;
  2839. +            if (storage.session.rehashCmd)
  2840. +                dactyl.execute(storage.session.rehashCmd);
  2841. +            storage.session.rehashCmd = null;
  2842.  
  2843.              dactyl.fullyInitialized = true;
  2844.              dactyl.triggerObserver("enter", null);
  2845. diff --git a/common/content/disable-acr.jsm b/common/content/disable-acr.jsm
  2846. --- a/common/content/disable-acr.jsm
  2847. +++ b/common/content/disable-acr.jsm
  2848. @@ -14,16 +14,18 @@ const Cu = Components.utils;
  2849.  Cu.import("resource://gre/modules/Services.jsm");
  2850.  Cu.import("resource://gre/modules/XPCOMUtils.jsm");
  2851.  
  2852. +const TOPIC = "chrome-document-global-created";
  2853. +
  2854.  function observe(window, topic, url) {
  2855. -    if (topic === "chrome-document-global-created")
  2856. +    if (topic === TOPIC)
  2857.          checkDocument(window.document);
  2858.  }
  2859.  function init(id) {
  2860.      if (id)
  2861.          ADDON_ID = id;
  2862.  
  2863. -    Services.obs[id ? "addObserver" : "removeObserver"](observe, "chrome-document-global-created", false);
  2864. -    for (let doc in chromeDocuments)
  2865. +    Services.obs[id ? "addObserver" : "removeObserver"](observe, TOPIC, false);
  2866. +    for (let doc in chromeDocuments())
  2867.          checkDocument(doc, !id);
  2868.  }
  2869.  function cleanup() { init(null); }
  2870. @@ -55,7 +57,7 @@ function checkDocument(doc, disable, for
  2871.  }
  2872.  
  2873.  function chromeDocuments() {
  2874. -    let windows = services.windowMediator.getXULWindowEnumerator(null);
  2875. +    let windows = Services.wm.getXULWindowEnumerator(null);
  2876.      while (windows.hasMoreElements()) {
  2877.          let window = windows.getNext().QueryInterface(Ci.nsIXULWindow);
  2878.          for each (let type in ["typeChrome", "typeContent"]) {
  2879. diff --git a/common/content/editor.js b/common/content/editor.js
  2880. --- a/common/content/editor.js
  2881. +++ b/common/content/editor.js
  2882. @@ -1,3 +1,4 @@
  2883. +// Copyright (c) 2008-2011 Kris Maglione <maglione.k at Gmail>
  2884.  // Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
  2885.  //
  2886.  // This work is licensed for reuse under an MIT license. Details are
  2887. @@ -24,12 +25,6 @@ var Editor = Module("editor", {
  2888.      selectedText: function () String(Editor.getEditor(null).selection),
  2889.  
  2890.      pasteClipboard: function (clipboard, toStart) {
  2891. -        // TODO: I don't think this is needed anymore? --djk
  2892. -        if (util.OS.isWindows) {
  2893. -            this.executeCommand("cmd_paste");
  2894. -            return;
  2895. -        }
  2896. -
  2897.          let elem = dactyl.focusedElement;
  2898.          if (elem.inputField)
  2899.              elem = elem.inputField;
  2900. @@ -189,59 +184,50 @@ var Editor = Module("editor", {
  2901.          }
  2902.      },
  2903.  
  2904. -    // returns the position of char
  2905. -    findCharForward: function (ch, count) {
  2906. -        if (!Editor.getEditor())
  2907. +    findChar: function (key, count, backward) {
  2908. +
  2909. +        let editor = Editor.getEditor();
  2910. +        if (!editor)
  2911.              return -1;
  2912.  
  2913. -        let text = Editor.getEditor().value;
  2914.          // XXX
  2915.          if (count == null)
  2916.              count = 1;
  2917.  
  2918. -        for (let i = Editor.getEditor().selectionEnd + 1; i < text.length; i++) {
  2919. -            if (text[i] == "\n")
  2920. -                break;
  2921. -            if (text[i] == ch)
  2922. -                count--;
  2923. -            if (count == 0)
  2924. -                return i + 1; // always position the cursor after the char
  2925. +        let code = events.fromString(key)[0].charCode;
  2926. +        util.assert(code);
  2927. +        let char = String.fromCharCode(code);
  2928. +
  2929. +        let text = editor.value;
  2930. +        let caret = editor.selectionEnd;
  2931. +        if (backward) {
  2932. +            let end = text.lastIndexOf("\n", caret);
  2933. +            while (caret > end && caret >= 0 && count--)
  2934. +                caret = text.lastIndexOf(char, caret - 1);
  2935. +        }
  2936. +        else {
  2937. +            let end = text.indexOf("\n", caret);
  2938. +            if (end == -1)
  2939. +                end = text.length;
  2940. +
  2941. +            while (caret < end && caret >= 0 && count--)
  2942. +                caret = text.indexOf(char, caret + 1);
  2943.          }
  2944.  
  2945. +        if (count > 0)
  2946. +            caret = -1;
  2947. +        if (caret == -1)
  2948.          dactyl.beep();
  2949. -        return -1;
  2950. -    },
  2951. -
  2952. -    // returns the position of char
  2953. -    findCharBackward: function (ch, count) {
  2954. -        if (!Editor.getEditor())
  2955. -            return -1;
  2956. -
  2957. -        let text = Editor.getEditor().value;
  2958. -        // XXX
  2959. -        if (count == null)
  2960. -            count = 1;
  2961. -
  2962. -        for (let i = Editor.getEditor().selectionStart - 1; i >= 0; i--) {
  2963. -            if (text[i] == "\n")
  2964. -                break;
  2965. -            if (text[i] == ch)
  2966. -                count--;
  2967. -            if (count == 0)
  2968. -                return i;
  2969. -        }
  2970. -
  2971. -        dactyl.beep();
  2972. -        return -1;
  2973. +        return caret;
  2974.      },
  2975.  
  2976.      /**
  2977.       * Edits the given file in the external editor as specified by the
  2978.       * 'editor' option.
  2979.       *
  2980. -     * @param {object|File|string} args An object specifying the file,
  2981. -     *  line, and column to edit. If a non-object is specified, it is
  2982. -     *  treated as the file parameter of the object.
  2983. +     * @param {object|File|string} args An object specifying the file, line,
  2984. +     *     and column to edit. If a non-object is specified, it is treated as
  2985. +     *     the file parameter of the object.
  2986.       * @param {boolean} blocking If true, this function does not return
  2987.       *  until the editor exits.
  2988.       */
  2989. @@ -252,7 +238,7 @@ var Editor = Module("editor", {
  2990.  
  2991.          let args = options.get("editor").format(args);
  2992.  
  2993. -        dactyl.assert(args.length >= 1, _("editor.noEditor"));
  2994. +        dactyl.assert(args.length >= 1, _("option.notSet", "editor"));
  2995.  
  2996.          io.run(args.shift(), args, blocking);
  2997.      },
  2998. @@ -266,7 +252,7 @@ var Editor = Module("editor", {
  2999.          let line, column;
  3000.  
  3001.          if (!forceEditing && textBox && textBox.type == "password") {
  3002. -            commandline.input("Editing a password field externally will reveal the password. Would you like to continue? (yes/[no]): ",
  3003. +            commandline.input(_("editor.prompt.editPassword") + " ",
  3004.                  function (resp) {
  3005.                      if (resp && resp.match(/^y(es)?$/i))
  3006.                          editor.editFieldExternally(true);
  3007. @@ -281,10 +267,10 @@ var Editor = Module("editor", {
  3008.              column = 1 + pre.replace(/[^]*\n/, "").length;
  3009.          }
  3010.          else {
  3011. -            var editor = window.GetCurrentEditor ? GetCurrentEditor()
  3012. +            var editor_ = window.GetCurrentEditor ? GetCurrentEditor()
  3013.                                                   : Editor.getEditor(document.commandDispatcher.focusedWindow);
  3014. -            dactyl.assert(editor);
  3015. -            text = Array.map(editor.rootElement.childNodes, function (e) util.domToString(e, true)).join("");
  3016. +            dactyl.assert(editor_);
  3017. +            text = Array.map(editor_.rootElement.childNodes, function (e) util.domToString(e, true)).join("");
  3018.          }
  3019.  
  3020.          let origGroup = textBox && textBox.getAttributeNS(NS, "highlight") || "";
  3021. @@ -318,19 +304,25 @@ var Editor = Module("editor", {
  3022.              lastUpdate = Date.now();
  3023.  
  3024.              let val = tmpfile.read();
  3025. -            if (textBox)
  3026. +            if (textBox) {
  3027.                  textBox.value = val;
  3028. +
  3029. +                textBox.setAttributeNS(NS, "modifiable", true);
  3030. +                util.computedStyle(textBox).MozUserInput;
  3031. +                events.dispatch(textBox, events.create(textBox.ownerDocument, "input", {}));
  3032. +                textBox.removeAttributeNS(NS, "modifiable");
  3033. +            }
  3034.              else {
  3035. -                while (editor.rootElement.firstChild)
  3036. -                    editor.rootElement.removeChild(editor.rootElement.firstChild);
  3037. -                editor.rootElement.innerHTML = val;
  3038. +                while (editor_.rootElement.firstChild)
  3039. +                    editor_.rootElement.removeChild(editor_.rootElement.firstChild);
  3040. +                editor_.rootElement.innerHTML = val;
  3041.              }
  3042.          }
  3043.  
  3044.          try {
  3045.              var tmpfile = io.createTempFile();
  3046.              if (!tmpfile)
  3047. -                throw Error("Couldn't create temporary file");
  3048. +                throw Error(_("io.cantCreateTempFile"));
  3049.  
  3050.              if (textBox) {
  3051.                  highlight.highlightNode(textBox, origGroup + " EditorEditing");
  3052. @@ -338,8 +330,7 @@ var Editor = Module("editor", {
  3053.              }
  3054.  
  3055.              if (!tmpfile.write(text))
  3056. -                throw Error("Input contains characters not valid in the current " +
  3057. -                            "file encoding");
  3058. +                throw Error(_("io.cantEncode"));
  3059.  
  3060.              var lastUpdate = Date.now();
  3061.  
  3062. @@ -414,9 +405,9 @@ var Editor = Module("editor", {
  3063.              elem = dactyl.focusedElement || document.commandDispatcher.focusedWindow;
  3064.          dactyl.assert(elem);
  3065.  
  3066. +        try {
  3067.          if (elem instanceof Element)
  3068.              return elem.QueryInterface(Ci.nsIDOMNSEditableElement).editor;
  3069. -        try {
  3070.              return elem.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
  3071.                         .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIEditingSession)
  3072.                         .getEditorForWindow(elem);
  3073. @@ -658,19 +649,28 @@ var Editor = Module("editor", {
  3074.          mappings.add([modes.INPUT],
  3075.              ["<C-t>"], "Edit text field in Vi mode",
  3076.              function () {
  3077. +                dactyl.assert(dactyl.focusedElement);
  3078.                  dactyl.assert(!editor.isTextEdit);
  3079.                  modes.push(modes.TEXT_EDIT);
  3080.              });
  3081.  
  3082. +        // Ugh.
  3083. +        mappings.add([modes.INPUT, modes.CARET],
  3084. +            ["<*-CR>", "<*-BS>", "<*-Del>", "<*-Left>", "<*-Right>", "<*-Up>", "<*-Down>",
  3085. +             "<*-Home>", "<*-End>", "<*-PageUp>", "<*-PageDown>",
  3086. +             "<M-c>", "<M-v>", "<*-Tab>"],
  3087. +            "Handled by " + config.host,
  3088. +            function () Events.PASS_THROUGH);
  3089. +
  3090.          mappings.add([modes.INSERT],
  3091. -            ["<Space>", "<Return>"], "Expand insert mode abbreviation",
  3092. +            ["<Space>", "<Return>"], "Expand Insert mode abbreviation",
  3093.              function () {
  3094.                  editor.expandAbbreviation(modes.INSERT);
  3095. -                return Events.PASS;
  3096. +                return Events.PASS_THROUGH;
  3097.              });
  3098.  
  3099.          mappings.add([modes.INSERT],
  3100. -            ["<C-]>", "<C-5>"], "Expand insert mode abbreviation",
  3101. +            ["<C-]>", "<C-5>"], "Expand Insert mode abbreviation",
  3102.              function () { editor.expandAbbreviation(modes.INSERT); });
  3103.  
  3104.          // text edit mode
  3105. @@ -723,15 +723,15 @@ var Editor = Module("editor", {
  3106.  
  3107.          // visual mode
  3108.          mappings.add([modes.CARET, modes.TEXT_EDIT],
  3109. -            ["v"], "Start visual mode",
  3110. +            ["v"], "Start Visual mode",
  3111.              function () { modes.push(modes.VISUAL); });
  3112.  
  3113.          mappings.add([modes.VISUAL],
  3114. -            ["v", "V"], "End visual mode",
  3115. +            ["v", "V"], "End Visual mode",
  3116.              function () { modes.pop(); });
  3117.  
  3118.          mappings.add([modes.TEXT_EDIT],
  3119. -            ["V"], "Start visual line mode",
  3120. +            ["V"], "Start Visual Line mode",
  3121.              function () {
  3122.                  modes.push(modes.VISUAL, modes.LINE);
  3123.                  editor.executeCommand("cmd_beginLine", 1);
  3124. @@ -777,7 +777,7 @@ var Editor = Module("editor", {
  3125.          mappings.add([modes.TEXT_EDIT, modes.VISUAL],
  3126.              ["f"], "Move to a character on the current line after the cursor",
  3127.              function ({ arg, count }) {
  3128. -                let pos = editor.findCharForward(arg, Math.max(count, 1));
  3129. +                let pos = editor.findChar(arg, Math.max(count, 1));
  3130.                  if (pos >= 0)
  3131.                      editor.moveToPosition(pos, true, modes.main == modes.VISUAL);
  3132.              },
  3133. @@ -786,7 +786,7 @@ var Editor = Module("editor", {
  3134.          mappings.add([modes.TEXT_EDIT, modes.VISUAL],
  3135.              ["F"], "Move to a character on the current line before the cursor",
  3136.              function ({ arg, count }) {
  3137. -                let pos = editor.findCharBackward(arg, Math.max(count, 1));
  3138. +                let pos = editor.findChar(arg, Math.max(count, 1), true);
  3139.                  if (pos >= 0)
  3140.                      editor.moveToPosition(pos, false, modes.main == modes.VISUAL);
  3141.              },
  3142. @@ -795,7 +795,7 @@ var Editor = Module("editor", {
  3143.          mappings.add([modes.TEXT_EDIT, modes.VISUAL],
  3144.              ["t"], "Move before a character on the current line",
  3145.              function ({ arg, count }) {
  3146. -                let pos = editor.findCharForward(arg, Math.max(count, 1));
  3147. +                let pos = editor.findChar(arg, Math.max(count, 1));
  3148.                  if (pos >= 0)
  3149.                      editor.moveToPosition(pos - 1, true, modes.main == modes.VISUAL);
  3150.              },
  3151. @@ -804,7 +804,7 @@ var Editor = Module("editor", {
  3152.          mappings.add([modes.TEXT_EDIT, modes.VISUAL],
  3153.              ["T"], "Move before a character on the current line, backwards",
  3154.              function ({ arg, count }) {
  3155. -                let pos = editor.findCharBackward(arg, Math.max(count, 1));
  3156. +                let pos = editor.findChar(arg, Math.max(count, 1), true);
  3157.                  if (pos >= 0)
  3158.                      editor.moveToPosition(pos + 1, false, modes.main == modes.VISUAL);
  3159.              },
  3160. @@ -837,10 +837,10 @@ var Editor = Module("editor", {
  3161.          function bind() mappings.add.apply(mappings,
  3162.                                             [[modes.AUTOCOMPLETE]].concat(Array.slice(arguments)))
  3163.  
  3164. -        bind(["<Esc>"], "Return to INSERT mode",
  3165. +        bind(["<Esc>"], "Return to Insert mode",
  3166.               function () Events.PASS_THROUGH);
  3167.  
  3168. -        bind(["<C-[>"], "Return to INSERT mode",
  3169. +        bind(["<C-[>"], "Return to Insert mode",
  3170.               function () { events.feedkeys("<Esc>", { skipmap: true }); });
  3171.  
  3172.          bind(["<Up>"], "Select the previous autocomplete result",
  3173. @@ -859,7 +859,7 @@ var Editor = Module("editor", {
  3174.      options: function () {
  3175.          options.add(["editor"],
  3176.              "The external text editor",
  3177. -            "string", "gvim -f +<line> <file>", {
  3178. +            "string", 'gvim -f +<line> +"sil! call cursor(0, <column>)" <file>', {
  3179.                  format: function (obj, value) {
  3180.                      let args = commands.parseArgs(value || this.value, { argCount: "*", allowUnknownOptions: true })
  3181.                                         .map(util.compileMacro).filter(function (fmt) fmt.valid(obj))
  3182. @@ -868,7 +868,7 @@ var Editor = Module("editor", {
  3183.                          args.push(obj["file"]);
  3184.                      return args;
  3185.                  },
  3186. -                has: function (key) set.has(util.compileMacro(this.value).seen, key),
  3187. +                has: function (key) Set.has(util.compileMacro(this.value).seen, key),
  3188.                  validator: function (value) {
  3189.                      this.format({}, value);
  3190.                      return Object.keys(util.compileMacro(value).seen).every(function (k) ["column", "file", "line"].indexOf(k) >= 0);
  3191. diff --git a/common/content/events.js b/common/content/events.js
  3192. --- a/common/content/events.js
  3193. +++ b/common/content/events.js
  3194. @@ -16,22 +16,27 @@ var ProcessorStack = Class("ProcessorSta
  3195.          this.buffer = "";
  3196.          this.events = [];
  3197.  
  3198. +        events.dbg("STACK " + mode);
  3199. +
  3200.          let main = { __proto__: mode.main, params: mode.params };
  3201. -        let keyModes = array([mode.params.keyModes, main, mode.main.allBases]).flatten().compact();
  3202. +        this.modes = array([mode.params.keyModes, main, mode.main.allBases.slice(1)]).flatten().compact();
  3203.  
  3204.          if (builtin)
  3205.              hives = hives.filter(function (h) h.name === "builtin");
  3206.  
  3207. -        this.processors = keyModes.map(function (m) hives.map(function (h) KeyProcessor(m, h)))
  3208. +        this.processors = this.modes.map(function (m) hives.map(function (h) KeyProcessor(m, h)))
  3209.                                    .flatten().array;
  3210.          this.ownsBuffer = !this.processors.some(function (p) p.main.ownsBuffer);
  3211.  
  3212.          for (let [i, input] in Iterator(this.processors)) {
  3213.              let params = input.main.params;
  3214. +
  3215.              if (params.preExecute)
  3216.                  input.preExecute = params.preExecute;
  3217. +
  3218.              if (params.postExecute)
  3219.                  input.postExecute = params.postExecute;
  3220. +
  3221.              if (params.onKeyPress && input.hive === mappings.builtin)
  3222.                  input.fallthrough = function fallthrough(events) {
  3223.                      return params.onKeyPress(events) === false ? Events.KILL : Events.PASS;
  3224. @@ -39,15 +44,17 @@ var ProcessorStack = Class("ProcessorSta
  3225.              }
  3226.  
  3227.          let hive = options.get("passkeys")[this.main.input ? "inputHive" : "commandHive"];
  3228. -        if (!builtin && hive.active
  3229. -                && (!dactyl.focusedElement || events.isContentNode(dactyl.focusedElement)))
  3230. +        if (!builtin && hive.active && (!dactyl.focusedElement || events.isContentNode(dactyl.focusedElement)))
  3231.              this.processors.unshift(KeyProcessor(modes.BASE, hive));
  3232.      },
  3233.  
  3234. +    passUnknown: Class.memoize(function () options.get("passunknown").getKey(this.modes)),
  3235. +
  3236.      notify: function () {
  3237. +        events.dbg("NOTIFY()");
  3238.          events.keyEvents = [];
  3239.          events.processor = null;
  3240. -        if (!this.execute(Events.KILL, true)) {
  3241. +        if (!this.execute(undefined, true)) {
  3242.              events.processor = this;
  3243.              events.keyEvents = this.keyEvents;
  3244.          }
  3245. @@ -60,64 +67,91 @@ var ProcessorStack = Class("ProcessorSta
  3246.                                  callable(result) ? result.toSource().substr(0, 50) : result),
  3247.  
  3248.      execute: function execute(result, force) {
  3249. +        events.dbg("EXECUTE(" + this._result(result) + ", " + force + ") events:" + this.events.length
  3250. +                   + " processors:" + this.processors.length + " actions:" + this.actions.length);
  3251.  
  3252. -        if (force && this.actions.length)
  3253. -            this.processors.length = 0;
  3254. +        let processors = this.processors;
  3255. +        let length = 1;
  3256. +
  3257. +        if (force)
  3258. +            this.processors = [];
  3259.  
  3260.          if (this.ownsBuffer)
  3261.              statusline.inputBuffer = this.processors.length ? this.buffer : "";
  3262.  
  3263.          if (!this.processors.some(function (p) !p.extended) && this.actions.length) {
  3264. -            if (this._actions.length == 0) {
  3265. -                dactyl.beep();
  3266. -                events.feedingKeys = false;
  3267. -            }
  3268. +            // We have matching actions and no processors other than
  3269. +            // those waiting on further arguments. Execute actions as
  3270. +            // long as they continue to return PASS.
  3271.  
  3272.              for (var action in values(this.actions)) {
  3273.                  while (callable(action)) {
  3274. +                    length = action.eventLength;
  3275.                      action = dactyl.trapErrors(action);
  3276. -                    events.dbg("ACTION RES: " + this._result(action));
  3277. +                    events.dbg("ACTION RES: " + length + " " + this._result(action));
  3278.                  }
  3279.                  if (action !== Events.PASS)
  3280.                      break;
  3281.              }
  3282.  
  3283. +            // Result is the result of the last action. Unless it's
  3284. +            // PASS, kill any remaining argument processors.
  3285.              result = action !== undefined ? action : Events.KILL;
  3286.              if (action !== Events.PASS)
  3287.                  this.processors.length = 0;
  3288.          }
  3289.          else if (this.processors.length) {
  3290. +            // We're still waiting on the longest matching processor.
  3291. +            // Kill the event, set a timeout to give up waiting if applicable.
  3292. +
  3293.              result = Events.KILL;
  3294. -            if (this.actions.length && options["timeout"])
  3295. +            if (options["timeout"] && (this.actions.length || events.hasNativeKey(this.events[0], this.main, this.passUnknown)))
  3296.                  this.timer = services.Timer(this, options["timeoutlen"], services.Timer.TYPE_ONE_SHOT);
  3297.          }
  3298.          else if (result !== Events.KILL && !this.actions.length &&
  3299. -                 (this.events.length > 1 ||
  3300. -                     this.processors.some(function (p) !p.main.passUnknown))) {
  3301. -            result = Events.KILL;
  3302. +                 !(this.events[0].isReplay || this.passUnknown
  3303. +                   || this.modes.some(function (m) m.passEvent(this), this.events[0]))) {
  3304. +            // No patching processors, this isn't a fake, pass-through
  3305. +            // event, we're not in pass-through mode, and we're not
  3306. +            // choosing to pass unknown keys. Kill the event and beep.
  3307. +
  3308. +            result = Events.ABORT;
  3309.              if (!Events.isEscape(this.events.slice(-1)[0]))
  3310.                  dactyl.beep();
  3311.              events.feedingKeys = false;
  3312.          }
  3313.          else if (result === undefined)
  3314. +            // No matching processors, we're willing to pass this event,
  3315. +            // and we don't have a default action from a processor. Just
  3316. +            // pass the event.
  3317.              result = Events.PASS;
  3318.  
  3319. -        events.dbg("RESULT: " + this._result(result));
  3320. -
  3321. -        if (result === Events.PASS || result === Events.PASS_THROUGH)
  3322. -            if (this.events[0].originalTarget)
  3323. -                this.events[0].originalTarget.dactylKeyPress = undefined;
  3324. +        events.dbg("RESULT: " + length + " " + this._result(result) + "\n\n");
  3325.  
  3326.          if (result !== Events.PASS || this.events.length > 1)
  3327. +            if (result !== Events.ABORT || !this.events[0].isReplay)
  3328.              Events.kill(this.events[this.events.length - 1]);
  3329.  
  3330. +        if (result === Events.PASS_THROUGH || result === Events.PASS && this.passUnknown)
  3331. +            events.passing = true;
  3332. +
  3333. +        if (result === Events.PASS_THROUGH && this.keyEvents.length)
  3334. +            events.dbg("PASS_THROUGH:\n\t" + this.keyEvents.map(function (e) [e.type, events.toString(e)]).join("\n\t"));
  3335. +
  3336.          if (result === Events.PASS_THROUGH)
  3337.              events.feedevents(null, this.keyEvents, { skipmap: true, isMacro: true, isReplay: true });
  3338. -        else if (result === Events.PASS || result === Events.ABORT) {
  3339. +        else {
  3340.              let list = this.events.filter(function (e) e.getPreventDefault() && !e.dactylDefaultPrevented);
  3341. -            if (list.length)
  3342. -                events.dbg("REFEED: " + list.map(events.closure.toString).join(""));
  3343. -            events.feedevents(null, list, { skipmap: true, isMacro: true, isReplay: true });
  3344. +
  3345. +            if (result === Events.PASS)
  3346. +                events.dbg("PASS THROUGH: " + list.slice(0, length).filter(function (e) e.type === "keypress").map(events.closure.toString));
  3347. +            if (list.length > length)
  3348. +                events.dbg("REFEED: " + list.slice(length).filter(function (e) e.type === "keypress").map(events.closure.toString));
  3349. +
  3350. +            if (result === Events.PASS)
  3351. +                events.feedevents(null, list.slice(0, length), { skipmap: true, isMacro: true, isReplay: true });
  3352. +            if (list.length > length && this.processors.length === 0)
  3353. +                events.feedevents(null, list.slice(length));
  3354.          }
  3355.  
  3356.          return this.processors.length === 0;
  3357. @@ -137,7 +171,7 @@ var ProcessorStack = Class("ProcessorSta
  3358.          let actions = [];
  3359.          let processors = [];
  3360.  
  3361. -        events.dbg("KEY: " + key + " skipmap: " + event.skipmap + " macro: " + event.isMacro + " replay: " + event.isReplay);
  3362. +        events.dbg("PROCESS(" + key + ") skipmap: " + event.skipmap + " macro: " + event.isMacro + " replay: " + event.isReplay);
  3363.  
  3364.          for (let [i, input] in Iterator(this.processors)) {
  3365.              let res = input.process(event);
  3366. @@ -149,8 +183,6 @@ var ProcessorStack = Class("ProcessorSta
  3367.              if (res === Events.KILL)
  3368.                  break;
  3369.  
  3370. -            buffer = buffer || input.inputBuffer;
  3371. -
  3372.              if (callable(res))
  3373.                  actions.push(res);
  3374.  
  3375. @@ -162,11 +194,15 @@ var ProcessorStack = Class("ProcessorSta
  3376.  
  3377.          events.dbg("RESULT: " + event.getPreventDefault() + " " + this._result(result));
  3378.          events.dbg("ACTIONS: " + actions.length + " " + this.actions.length);
  3379. -        events.dbg("PROCESSORS:", processors);
  3380. +        events.dbg("PROCESSORS:", processors, "\n");
  3381.  
  3382.          this._actions = actions;
  3383.          this.actions = actions.concat(this.actions);
  3384.  
  3385. +        for (let action in values(actions))
  3386. +            if (!("eventLength" in action))
  3387. +                action.eventLength = this.events.length;
  3388. +
  3389.          if (result === Events.KILL)
  3390.              this.actions = [];
  3391.          else if (!this.actions.length && !processors.length)
  3392. @@ -220,8 +256,10 @@ var KeyProcessor = Class("KeyProcessor",
  3393.              function execute() {
  3394.                  if (self.preExecute)
  3395.                      self.preExecute.apply(self, args);
  3396. -                let res = map.execute.call(map, update({ self: self.main.params.mappingSelf || self.main.mappingSelf || map },
  3397. -                                                       args));
  3398. +
  3399. +                args.self = self.main.params.mappingSelf || self.main.mappingSelf || map;
  3400. +                let res = map.execute.call(map, args);
  3401. +
  3402.                  if (self.postExecute)
  3403.                      self.postExecute.apply(self, args);
  3404.                  return res;
  3405. @@ -285,6 +323,10 @@ var KeyArgProcessor = Class("KeyArgProce
  3406.      }
  3407.  });
  3408.  
  3409. +/**
  3410. + * A hive used mainly for tracking event listeners and cleaning them up when a
  3411. + * group is destroyed.
  3412. + */
  3413.  var EventHive = Class("EventHive", Contexts.Hive, {
  3414.      init: function init(group) {
  3415.          init.supercall(this, group);
  3416. @@ -315,6 +357,9 @@ var EventHive = Class("EventHive", Conte
  3417.              [,, capture, allowUntrusted] = arguments;
  3418.          }
  3419.  
  3420. +        if (Set.has(events, "input") && !Set.has(events, "dactyl-input"))
  3421. +            events["dactyl-input"] = events.input;
  3422. +
  3423.          for (let [event, callback] in Iterator(events)) {
  3424.              let args = [Cu.getWeakReference(target),
  3425.                          event,
  3426. @@ -354,7 +399,6 @@ var Events = Module("events", {
  3427.      dbg: function () {},
  3428.  
  3429.      init: function () {
  3430. -        const self = this;
  3431.          this.keyEvents = [];
  3432.  
  3433.          update(this, {
  3434. @@ -369,9 +413,7 @@ var Events = Module("events", {
  3435.          util.overlayWindow(window, {
  3436.              append: <e4x xmlns={XUL}>
  3437.                  <window id={document.documentElement.id}>
  3438. -                    <!--this notifies us also of focus events in the XUL
  3439. -                        from: http://developer.mozilla.org/en/docs/XUL_Tutorial:Updating_Commands !-->
  3440. -                    <!-- I don't think we really need this. ––Kris -->
  3441. +                    <!-- http://developer.mozilla.org/en/docs/XUL_Tutorial:Updating_Commands -->
  3442.                      <commandset id="dactyl-onfocus" commandupdater="true" events="focus"
  3443.                                  oncommandupdate="dactyl.modules.events.onFocusChange(event);"/>
  3444.                      <commandset id="dactyl-onselect" commandupdater="true" events="select"
  3445. @@ -411,11 +453,12 @@ var Events = Module("events", {
  3446.              subtract: ["Minus", "Subtract"]
  3447.          };
  3448.  
  3449. -        this._pseudoKeys = set(["count", "leader", "nop", "pass"]);
  3450. +        this._pseudoKeys = Set(["count", "leader", "nop", "pass"]);
  3451.  
  3452.          this._key_key = {};
  3453.          this._code_key = {};
  3454.          this._key_code = {};
  3455. +        this._code_nativeKey = {};
  3456.  
  3457.          for (let list in values(this._keyTable))
  3458.              for (let v in values(list)) {
  3459. @@ -425,6 +468,8 @@ var Events = Module("events", {
  3460.              }
  3461.  
  3462.          for (let [k, v] in Iterator(KeyEvent)) {
  3463. +            this._code_nativeKey[v] = k.substr(4);
  3464. +
  3465.              k = k.substr(7).toLowerCase();
  3466.              let names = [k.replace(/(^|_)(.)/g, function (m, n1, n2) n2.toUpperCase())
  3467.                            .replace(/^NUMPAD/, "k")];
  3468. @@ -449,29 +494,18 @@ var Events = Module("events", {
  3469.          }
  3470.  
  3471.          this._activeMenubar = false;
  3472. -        this.listen(window, this, "events", true);
  3473. -
  3474. -        dactyl.registerObserver("modeChange", function () {
  3475. -            delete self.processor;
  3476. -        });
  3477. +        this.listen(window, this, "events");
  3478.      },
  3479.  
  3480.      signals: {
  3481.          "browser.locationChange": function (webProgress, request, uri) {
  3482.              options.get("passkeys").flush();
  3483. +        },
  3484. +        "modes.change": function (oldMode, newMode) {
  3485. +            delete this.processor;
  3486.          }
  3487.      },
  3488.  
  3489. -    /**
  3490. -     * Adds an event listener for this session and removes it on
  3491. -     * dactyl shutdown.
  3492. -     *
  3493. -     * @param {Element} target The element on which to listen.
  3494. -     * @param {string} event The event to listen for.
  3495. -     * @param {function} callback The function to call when the event is received.
  3496. -     * @param {boolean} capture When true, listen during the capture
  3497. -     *      phase, otherwise during the bubbling phase.
  3498. -     */
  3499.      get listen() this.builtin.closure.listen,
  3500.      addSessionListener: deprecated("events.listen", { get: function addSessionListener() this.listen }),
  3501.  
  3502. @@ -531,7 +565,7 @@ var Events = Module("events", {
  3503.                  timeRecorded: Date.now()
  3504.              });
  3505.  
  3506. -            dactyl.log("Recorded " + this.recording + ": " + this._macroKeys.join(""), 9);
  3507. +            dactyl.log(_("macro.recorded", this.recording, this._macroKeys.join("")), 9);
  3508.              dactyl.echomsg(_("macro.recorded", this.recording));
  3509.          }
  3510.          this._recording = macro || null;
  3511. @@ -640,10 +674,15 @@ var Events = Module("events", {
  3512.              if (quiet)
  3513.                  commandline.quiet = quiet;
  3514.  
  3515. +            keys = mappings.expandLeader(keys);
  3516. +
  3517.              for (let [, evt_obj] in Iterator(events.fromString(keys))) {
  3518.                  let now = Date.now();
  3519. -                for (let type in values(["keydown", "keyup", "keypress"])) {
  3520. +                let key = events.toString(evt_obj);
  3521. +                for (let type in values(["keydown", "keypress", "keyup"])) {
  3522.                      let evt = update({}, evt_obj, { type: type });
  3523. +                    if (type !== "keypress" && !evt.keyCode)
  3524. +                        evt.keyCode = evt._keyCode || 0;
  3525.  
  3526.                      if (isObject(noremap))
  3527.                          update(evt, noremap);
  3528. @@ -654,9 +693,18 @@ var Events = Module("events", {
  3529.                      evt.dactylSavedEvents = savedEvents;
  3530.                      this.feedingEvent = evt;
  3531.  
  3532. -                    let event = events.create(document.commandDispatcher.focusedWindow.document, type, evt);
  3533. -                    if (!evt_obj.dactylString && !evt_obj.dactylShift && !mode)
  3534. -                        events.dispatch(dactyl.focusedElement || buffer.focusedFrame, event, evt);
  3535. +                    let doc = document.commandDispatcher.focusedWindow.document;
  3536. +                    let event = events.create(doc, type, evt);
  3537. +                    let target = dactyl.focusedElement
  3538. +                              || ["complete", "interactive"].indexOf(doc.readyState) >= 0 && doc.documentElement
  3539. +                              || doc.defaultView;
  3540. +
  3541. +                    if (target instanceof Element && !Events.isInputElement(target) &&
  3542. +                        ["<Return>", "<Space>"].indexOf(key) == -1)
  3543. +                        target = target.ownerDocument.documentElement;
  3544. +
  3545. +                    if (!evt_obj.dactylString && !mode)
  3546. +                        events.dispatch(target, event, evt);
  3547.                      else if (type === "keypress")
  3548.                          events.events.keypress.call(events, event);
  3549.                  }
  3550. @@ -689,8 +737,7 @@ var Events = Module("events", {
  3551.       * @param {Object} opts The pseudo-event. @optional
  3552.       */
  3553.      create: function (doc, type, opts) {
  3554. -        opts = opts || {};
  3555. -        var DEFAULTS = {
  3556. +        const DEFAULTS = {
  3557.              HTML: {
  3558.                  type: type, bubbles: true, cancelable: false
  3559.              },
  3560. @@ -713,22 +760,31 @@ var Events = Module("events", {
  3561.                  relatedTarget: null
  3562.              }
  3563.          };
  3564. -        const TYPES = {
  3565. -            change: "", input: "", submit: "",
  3566. -            click: "Mouse", mousedown: "Mouse", mouseup: "Mouse",
  3567. -            mouseover: "Mouse", mouseout: "Mouse",
  3568. -            keypress: "Key", keyup: "Key", keydown: "Key"
  3569. -        };
  3570. -        var t = TYPES[type];
  3571. +
  3572. +        opts = opts || {};
  3573. +
  3574. +        var t = this._create_types[type];
  3575.          var evt = doc.createEvent((t || "HTML") + "Events");
  3576.  
  3577.          let defaults = DEFAULTS[t || "HTML"];
  3578. -        evt["init" + t + "Event"].apply(evt, Object.keys(defaults)
  3579. -                                                   .map(function (k) k in opts ? opts[k]
  3580. -                                                                               : defaults[k]));
  3581. +
  3582. +        let args = Object.keys(defaults)
  3583. +                         .map(function (k) k in opts ? opts[k] : defaults[k]);
  3584. +
  3585. +        evt["init" + t + "Event"].apply(evt, args);
  3586.          return evt;
  3587.      },
  3588.  
  3589. +    _create_types: Class.memoize(function () iter(
  3590. +        {
  3591. +            Mouse: "click mousedown mouseout mouseover mouseup",
  3592. +            Key:   "keydown keypress keyup",
  3593. +            "":    "change dactyl-input input submit"
  3594. +        }
  3595. +    ).map(function ([k, v]) v.split(" ").map(function (v) [v, k]))
  3596. +     .flatten()
  3597. +     .toObject()),
  3598. +
  3599.      /**
  3600.       * Converts a user-input string of keys into a canonical
  3601.       * representation.
  3602. @@ -755,11 +811,11 @@ var Events = Module("events", {
  3603.          return events.fromString(keys, unknownOk).map(events.closure.toString).join("");
  3604.      },
  3605.  
  3606. -    iterKeys: function (keys) {
  3607. +    iterKeys: function (keys) iter(function () {
  3608.          let match, re = /<.*?>?>|[^<]/g;
  3609.          while (match = re.exec(keys))
  3610.              yield match[0];
  3611. -    },
  3612. +    }()),
  3613.  
  3614.      /**
  3615.       * Dispatches an event to an element as if it were a native event.
  3616. @@ -829,34 +885,40 @@ var Events = Module("events", {
  3617.          let out = [];
  3618.          for (let match in util.regexp.iterate(/<.*?>?>|[^<]|<(?!.*>)/g, input)) {
  3619.              let evt_str = match[0];
  3620. +
  3621.              let evt_obj = { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false,
  3622.                              keyCode: 0, charCode: 0, type: "keypress" };
  3623.  
  3624. -            if (evt_str.length > 1) { // <.*?>
  3625. -                let [match, modifier, keyname] = evt_str.match(/^<((?:[CSMA]-)*)(.+?)>$/i) || [false, '', ''];
  3626. -                modifier = modifier.toUpperCase();
  3627. +            if (evt_str.length == 1) {
  3628. +                evt_obj.charCode = evt_str.charCodeAt(0);
  3629. +                evt_obj._keyCode = this._key_code[evt_str[0].toLowerCase()];
  3630. +                evt_obj.shiftKey = evt_str !== evt_str.toLowerCase();
  3631. +            }
  3632. +            else {
  3633. +                let [match, modifier, keyname] = evt_str.match(/^<((?:[*12CASM⌘]-)*)(.+?)>$/i) || [false, '', ''];
  3634. +                modifier = Set(modifier.toUpperCase());
  3635.                  keyname = keyname.toLowerCase();
  3636.                  evt_obj.dactylKeyname = keyname;
  3637.                  if (/^u[0-9a-f]+$/.test(keyname))
  3638.                      keyname = String.fromCharCode(parseInt(keyname.substr(1), 16));
  3639.  
  3640.                  if (keyname && (unknownOk || keyname.length == 1 || /mouse$/.test(keyname) ||
  3641. -                                this._key_code[keyname] || set.has(this._pseudoKeys, keyname))) {
  3642. -                    evt_obj.ctrlKey  = /C-/.test(modifier);
  3643. -                    evt_obj.altKey   = /A-/.test(modifier);
  3644. -                    evt_obj.shiftKey = /S-/.test(modifier);
  3645. -                    evt_obj.metaKey  = /M-/.test(modifier);
  3646. +                                this._key_code[keyname] || Set.has(this._pseudoKeys, keyname))) {
  3647. +                    evt_obj.globKey  ="*" in modifier;
  3648. +                    evt_obj.ctrlKey  ="C" in modifier;
  3649. +                    evt_obj.altKey   ="A" in modifier;
  3650. +                    evt_obj.shiftKey ="S" in modifier;
  3651. +                    evt_obj.metaKey  ="M" in modifier || "⌘" in modifier;
  3652. +                    evt_obj.dactylShift = evt_obj.shiftKey;
  3653.  
  3654.                      if (keyname.length == 1) { // normal characters
  3655. -                        if (evt_obj.shiftKey) {
  3656. +                        if (evt_obj.shiftKey)
  3657.                              keyname = keyname.toUpperCase();
  3658. -                            if (keyname == keyname.toLowerCase())
  3659. -                                evt_obj.dactylShift = true;
  3660. -                        }
  3661.  
  3662.                          evt_obj.charCode = keyname.charCodeAt(0);
  3663. +                        evt_obj._keyCode = this._key_code[keyname.toLowerCase()];
  3664.                      }
  3665. -                    else if (set.has(this._pseudoKeys, keyname)) {
  3666. +                    else if (Set.has(this._pseudoKeys, keyname)) {
  3667.                          evt_obj.dactylString = "<" + this._key_key[keyname] + ">";
  3668.                      }
  3669.                      else if (/mouse$/.test(keyname)) { // mouse events
  3670. @@ -875,8 +937,6 @@ var Events = Module("events", {
  3671.                      continue;
  3672.                  }
  3673.              }
  3674. -            else // a simple key (no <...>)
  3675. -                evt_obj.charCode = evt_str.charCodeAt(0);
  3676.  
  3677.              // TODO: make a list of characters that need keyCode and charCode somewhere
  3678.              if (evt_obj.keyCode == 32 || evt_obj.charCode == 32)
  3679. @@ -911,6 +971,8 @@ var Events = Module("events", {
  3680.          let key = null;
  3681.          let modifier = "";
  3682.  
  3683. +        if (event.globKey)
  3684. +            modifier += "*-";
  3685.          if (event.ctrlKey)
  3686.              modifier += "C-";
  3687.          if (event.altKey)
  3688. @@ -931,6 +993,7 @@ var Events = Module("events", {
  3689.                              key = key.toUpperCase();
  3690.                          else
  3691.                              key = key.toLowerCase();
  3692. +
  3693.                      if (!modifier && /^[a-z0-9]$/i.test(key))
  3694.                          return key;
  3695.                  }
  3696. @@ -975,7 +1038,7 @@ var Events = Module("events", {
  3697.                  else {
  3698.                      // a shift modifier is only allowed if the key is alphabetical and used in a C-A-M- mapping in the uppercase,
  3699.                      // or if the shift has been forced for a non-alphabetical character by the user while :map-ping
  3700. -                    if (key != key.toLowerCase() && (event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
  3701. +                    if (key !== key.toLowerCase() && (event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
  3702.                          modifier += "S-";
  3703.                      if (/^\s$/.test(key))
  3704.                          key = let (s = charCode.toString(16)) "U" + "0000".substr(4 - s.length) + s;
  3705. @@ -983,8 +1046,11 @@ var Events = Module("events", {
  3706.                          return key;
  3707.                  }
  3708.              }
  3709. -            if (key == null)
  3710. +            if (key == null) {
  3711. +                if (event.shiftKey)
  3712. +                    modifier += "S-";
  3713.                  key = this._key_key[event.dactylKeyname] || event.dactylKeyname;
  3714. +            }
  3715.              if (key == null)
  3716.                  return null;
  3717.          }
  3718. @@ -1015,8 +1081,71 @@ var Events = Module("events", {
  3719.      },
  3720.  
  3721.      /**
  3722. -     * Whether *key* is a key code defined to accept/execute input on the
  3723. -     * command line.
  3724. +     * Returns true if there's a known native key handler for the given
  3725. +     * event in the given mode.
  3726. +     *
  3727. +     * @param {Event} event A keypress event.
  3728. +     * @param {Modes.Mode} mode The main mode.
  3729. +     * @param {boolean} passUnknown Whether unknown keys should be passed.
  3730. +     */
  3731. +    hasNativeKey: function hasNativeKey(event, mode, passUnknown) {
  3732. +        if (mode.input && event.charCode && !(event.ctrlKey || event.metaKey))
  3733. +            return true;
  3734. +
  3735. +        if (!passUnknown)
  3736. +            return false;
  3737. +
  3738. +        var elements = document.getElementsByTagNameNS(XUL, "key");
  3739. +        var filters = [];
  3740. +
  3741. +        if (event.keyCode)
  3742. +            filters.push(["keycode", this._code_nativeKey[event.keyCode]]);
  3743. +        if (event.charCode) {
  3744. +            let key = String.fromCharCode(event.charCode);
  3745. +            filters.push(["key", key.toUpperCase()],
  3746. +                         ["key", key.toLowerCase()]);
  3747. +        }
  3748. +
  3749. +        let accel = util.OS.isMacOSX ? "metaKey" : "ctrlKey";
  3750. +
  3751. +        let access = iter({ 1: "shiftKey", 2: "ctrlKey", 4: "altKey", 8: "metaKey" })
  3752. +                        .filter(function ([k, v]) this & k, prefs.get("ui.key.chromeAccess"))
  3753. +                        .map(function ([k, v]) [v, true])
  3754. +                        .toObject();
  3755. +
  3756. +    outer:
  3757. +        for (let [, key] in iter(elements))
  3758. +            if (filters.some(function ([k, v]) key.getAttribute(k) == v)) {
  3759. +                let keys = { ctrlKey: false, altKey: false, shiftKey: false, metaKey: false };
  3760. +                let needed = { ctrlKey: event.ctrlKey, altKey: event.altKey, shiftKey: event.shiftKey, metaKey: event.metaKey };
  3761. +
  3762. +                let modifiers = (key.getAttribute("modifiers") || "").trim().split(/[\s,]+/);
  3763. +                for (let modifier in values(modifiers))
  3764. +                    switch (modifier) {
  3765. +                        case "access": update(keys, access); break;
  3766. +                        case "accel":  keys[accel] = true; break;
  3767. +                        default:       keys[modifier + "Key"] = true; break;
  3768. +                        case "any":
  3769. +                            if (!iter.some(keys, function ([k, v]) v && needed[k]))
  3770. +                                continue outer;
  3771. +                            for (let [k, v] in iter(keys)) {
  3772. +                                if (v)
  3773. +                                    needed[k] = false;
  3774. +                                keys[k] = false;
  3775. +                            }
  3776. +                            break;
  3777. +                    }
  3778. +
  3779. +                if (iter(needed).every(function ([k, v]) v == keys[k]))
  3780. +                    return key;
  3781. +            }
  3782. +
  3783. +        return false;
  3784. +    },
  3785. +
  3786. +    /**
  3787. +     * Returns true if *key* is a key code defined to accept/execute input on
  3788. +     * the command line.
  3789.       *
  3790.       * @param {string} key The key code to test.
  3791.       * @returns {boolean}
  3792. @@ -1024,14 +1153,21 @@ var Events = Module("events", {
  3793.      isAcceptKey: function (key) key == "<Return>" || key == "<C-j>" || key == "<C-m>",
  3794.  
  3795.      /**
  3796. -     * Whether *key* is a key code defined to reject/cancel input on the
  3797. -     * command line.
  3798. +     * Returns true if *key* is a key code defined to reject/cancel input on
  3799. +     * the command line.
  3800.       *
  3801.       * @param {string} key The key code to test.
  3802.       * @returns {boolean}
  3803.       */
  3804.      isCancelKey: function (key) key == "<Esc>" || key == "<C-[>" || key == "<C-c>",
  3805.  
  3806. +    /**
  3807. +     * Returns true if *node* belongs to the current content document or any
  3808. +     * sub-frame thereof.
  3809. +     *
  3810. +     * @param {Node|Document|Window} node The node to test.
  3811. +     * @returns {boolean}
  3812. +     */
  3813.      isContentNode: function isContentNode(node) {
  3814.          let win = (node.ownerDocument || node).defaultView || node;
  3815.          return XPCNativeWrapper(win).top == content;
  3816. @@ -1047,11 +1183,12 @@ var Events = Module("events", {
  3817.          if (buffer.loaded)
  3818.              return true;
  3819.  
  3820. -        dactyl.echo(_("macro.loadWaiting"), commandline.DISALLOW_MULTILINE);
  3821. +        dactyl.echo(_("macro.loadWaiting"), commandline.FORCE_SINGLELINE);
  3822.  
  3823.          const maxWaitTime = (time || 25);
  3824. -        util.waitFor(function () !events.feedingKeys || buffer.loaded, this, maxWaitTime * 1000, true);
  3825. +        util.waitFor(function () buffer.loaded, this, maxWaitTime * 1000, true);
  3826.  
  3827. +        dactyl.echo("", commandline.FORCE_SINGLELINE);
  3828.          if (!buffer.loaded)
  3829.              dactyl.echoerr(_("macro.loadFailed", maxWaitTime));
  3830.  
  3831. @@ -1115,14 +1252,19 @@ var Events = Module("events", {
  3832.  
  3833.              let win = (elem.ownerDocument || elem).defaultView || elem;
  3834.  
  3835. -            if (events.isContentNode(elem) && !buffer.focusAllowed(elem)
  3836. -                && !(services.focus.getLastFocusMethod(win) & 0x7000)
  3837. +            if (!(services.focus.getLastFocusMethod(win) & 0x7000)
  3838. +                && events.isContentNode(elem)
  3839. +                && !buffer.focusAllowed(elem)
  3840.                  && isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement, Window])) {
  3841. +
  3842.                  if (elem.frameElement)
  3843.                      dactyl.focusContent(true);
  3844.                  else if (!(elem instanceof Window) || Editor.getEditor(elem))
  3845.                      dactyl.focus(window);
  3846.              }
  3847. +
  3848. +            if (elem instanceof Element)
  3849. +                elem.dactylFocusAllowed = undefined;
  3850.          },
  3851.  
  3852.          /*
  3853. @@ -1178,7 +1320,7 @@ var Events = Module("events", {
  3854.                      elem.dactylKeyPress = elem.value;
  3855.                      util.timeout(function () {
  3856.                          if (elem.dactylKeyPress !== undefined && elem.value !== elem.dactylKeyPress)
  3857. -                            events.dispatch(elem, events.create(elem.ownerDocument, "input"));
  3858. +                            events.dispatch(elem, events.create(elem.ownerDocument, "dactyl-input"));
  3859.                          elem.dactylKeyPress = undefined;
  3860.                      });
  3861.                  }
  3862. @@ -1214,19 +1356,18 @@ var Events = Module("events", {
  3863.  
  3864.                      let ignore = false;
  3865.  
  3866. -                    if (modes.main == modes.PASS_THROUGH)
  3867. +                    if (mode.main == modes.PASS_THROUGH)
  3868.                          ignore = !Events.isEscape(key) && key != "<C-v>";
  3869. -                    else if (modes.main == modes.QUOTE) {
  3870. +                    else if (mode.main == modes.QUOTE) {
  3871.                          if (modes.getStack(1).main == modes.PASS_THROUGH) {
  3872. -                            mode.params.mainMode = modes.getStack(2).main;
  3873. +                            mode = Modes.StackElement(modes.getStack(2).main);
  3874.                              ignore = Events.isEscape(key);
  3875.                          }
  3876.                          else if (events.shouldPass(event))
  3877. -                            mode.params.mainMode = modes.getStack(1).main;
  3878. +                            mode = Modes.StackElement(modes.getStack(1).main);
  3879.                          else
  3880.                              ignore = true;
  3881.  
  3882. -                        if (ignore && !Events.isEscape(key))
  3883.                              modes.pop();
  3884.                      }
  3885.                      else if (!event.isMacro && !event.noremap && events.shouldPass(event))
  3886. @@ -1277,25 +1418,40 @@ var Events = Module("events", {
  3887.          },
  3888.  
  3889.          keyup: function onKeyUp(event) {
  3890. +            if (event.type == "keydown")
  3891.              this.keyEvents.push(event);
  3892. +            else if (!this.processor)
  3893. +                this.keyEvents = [];
  3894.  
  3895. -            let pass = this.feedingEvent && this.feedingEvent.isReplay ||
  3896. +            let pass = this.passing && !event.isMacro ||
  3897. +                    this.feedingEvent && this.feedingEvent.isReplay ||
  3898.                      event.isReplay ||
  3899.                      modes.main == modes.PASS_THROUGH ||
  3900.                      modes.main == modes.QUOTE
  3901.                          && modes.getStack(1).main !== modes.PASS_THROUGH
  3902.                          && !this.shouldPass(event) ||
  3903. -                    !modes.passThrough && this.shouldPass(event);
  3904. +                    !modes.passThrough && this.shouldPass(event) ||
  3905. +                    !this.processor && event.type === "keydown"
  3906. +                        && options.get("passunknown").getKey(modes.main.allBases)
  3907. +                        && let (key = events.toString(event))
  3908. +                            !modes.main.allBases.some(
  3909. +                                function (mode) mappings.hives.some(
  3910. +                                    function (hive) hive.get(mode, key) || hive.getCandidates(mode, key)));
  3911.  
  3912. -            events.dbg("ON " + event.type.toUpperCase() + " " + this.toString(event) + " pass: " + pass);
  3913. +            if (event.type === "keydown")
  3914. +                this.passing = pass;
  3915. +
  3916. +            events.dbg("ON " + event.type.toUpperCase() + " " + this.toString(event) + " pass: " + pass + " replay: " + event.isReplay + " macro: " + event.isMacro);
  3917.  
  3918.              // Prevents certain sites from transferring focus to an input box
  3919.              // before we get a chance to process our key bindings on the
  3920.              // "keypress" event.
  3921. -            if (!pass && !Events.isInputElement(dactyl.focusedElement))
  3922. +            if (!pass)
  3923.                  event.stopPropagation();
  3924.          },
  3925.          keydown: function onKeyDown(event) {
  3926. +            if (!event.isMacro)
  3927. +                this.passing = false;
  3928.              this.events.keyup.call(this, event);
  3929.          },
  3930.  
  3931. @@ -1303,8 +1459,11 @@ var Events = Module("events", {
  3932.              let elem = event.target;
  3933.              let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem;
  3934.  
  3935. -            for (; win; win = win != win.parent && win.parent)
  3936. +            for (; win; win = win != win.parent && win.parent) {
  3937. +                for (; elem instanceof Element; elem = elem.parentNode)
  3938. +                    elem.dactylFocusAllowed = true;
  3939.                  win.document.dactylFocusAllowed = true;
  3940. +            }
  3941.          },
  3942.  
  3943.          popupshown: function onPopupShown(event) {
  3944. @@ -1322,8 +1481,7 @@ var Events = Module("events", {
  3945.                      modes.push(modes.MENU);
  3946.          },
  3947.  
  3948. -        popuphidden: function onPopupHidden() {
  3949. -            // gContextMenu is set to NULL, when a context menu is closed
  3950. +        popuphidden: function onPopupHidden(event) {
  3951.              if (window.gContextMenu == null && !this._activeMenubar)
  3952.                  modes.remove(modes.MENU, true);
  3953.              modes.remove(modes.AUTOCOMPLETE);
  3954. @@ -1406,11 +1564,15 @@ var Events = Module("events", {
  3955.              if (elem == null && urlbar && urlbar.inputField == this._lastFocus)
  3956.                  util.threadYield(true); // Why? --Kris
  3957.  
  3958. -            while (modes.main.ownsFocus && !modes.topOfStack.params.holdFocus)
  3959. +            while (modes.main.ownsFocus && modes.topOfStack.params.ownsFocus != elem
  3960. +                    && !modes.topOfStack.params.holdFocus)
  3961.                   modes.pop(null, { fromFocus: true });
  3962.          }
  3963.          finally {
  3964.              this._lastFocus = elem;
  3965. +
  3966. +            if (modes.main.ownsFocus)
  3967. +                modes.topOfStack.params.ownsFocus = elem;
  3968.          }
  3969.      },
  3970.  
  3971. @@ -1445,20 +1607,25 @@ var Events = Module("events", {
  3972.              key === "<Esc>" || key === "<C-[>",
  3973.  
  3974.      isHidden: function isHidden(elem, aggressive) {
  3975. +        if (util.computedStyle(elem).visibility !== "visible")
  3976. +            return true;
  3977. +
  3978. +        if (aggressive)
  3979.          for (let e = elem; e instanceof Element; e = e.parentNode) {
  3980. -            if (util.computedStyle(e).visibility !== "visible" ||
  3981. -                    aggressive && e.boxObject && e.boxObject.height === 0)
  3982. +                if (!/set$/.test(e.localName) && e.boxObject && e.boxObject.height === 0)
  3983.                  return true;
  3984. +                else if (e.namespaceURI == XUL && e.localName === "panel")
  3985. +                    break;
  3986.          }
  3987.          return false;
  3988.      },
  3989.  
  3990.      isInputElement: function isInputElement(elem) {
  3991. -        return elem instanceof HTMLInputElement && set.has(util.editableInputs, elem.type) ||
  3992. -               isinstance(elem, [HTMLIsIndexElement, HTMLEmbedElement,
  3993. +        return elem instanceof HTMLInputElement && Set.has(util.editableInputs, elem.type) ||
  3994. +               isinstance(elem, [HTMLEmbedElement,
  3995.                                   HTMLObjectElement, HTMLSelectElement,
  3996.                                   HTMLTextAreaElement,
  3997. -                                 Ci.nsIDOMXULTreeElement, Ci.nsIDOMXULTextBoxElement]) ||
  3998. +                                 Ci.nsIDOMXULTextBoxElement]) ||
  3999.                 elem instanceof Window && Editor.getEditor(elem);
  4000.      },
  4001.  
  4002. @@ -1480,12 +1647,13 @@ var Events = Module("events", {
  4003.                  else
  4004.                      dactyl.echoerr(_("error.argumentRequired"));
  4005.              }, {
  4006. +                argCount: "?",
  4007.                  bang: true,
  4008.                  completer: function (context) completion.macro(context),
  4009.                  literal: 0
  4010.              });
  4011.  
  4012. -        commands.add(["macros"],
  4013. +        commands.add(["mac[ros]"],
  4014.              "List all macros",
  4015.              function (args) { completion.listCompleter("macro", args[0]); }, {
  4016.                  argCount: "?",
  4017. @@ -1501,7 +1669,7 @@ var Events = Module("events", {
  4018.      mappings: function () {
  4019.  
  4020.          mappings.add([modes.MAIN],
  4021. -            ["<A-b>"], "Process the next key as a builtin mapping",
  4022. +            ["<A-b>", "<pass-next-key-builtin>"], "Process the next key as a builtin mapping",
  4023.              function () {
  4024.                  events.processor = ProcessorStack(modes.getStack(0), mappings.hives.array, true);
  4025.                  events.processor.keyEvents = events.keyEvents;
  4026. @@ -1511,7 +1679,7 @@ var Events = Module("events", {
  4027.              ["<C-z>", "<pass-all-keys>"], "Temporarily ignore all " + config.appName + " key bindings",
  4028.              function () { modes.push(modes.PASS_THROUGH); });
  4029.  
  4030. -        mappings.add([modes.MAIN],
  4031. +        mappings.add([modes.MAIN, modes.PASS_THROUGH, modes.QUOTE],
  4032.              ["<C-v>", "<pass-next-key>"], "Pass through next key",
  4033.              function () {
  4034.                  if (modes.main == modes.QUOTE)
  4035. @@ -1520,6 +1688,10 @@ var Events = Module("events", {
  4036.              });
  4037.  
  4038.          mappings.add([modes.BASE],
  4039. +            ["<CapsLock>"], "Do Nothing",
  4040. +            function () {});
  4041. +
  4042. +        mappings.add([modes.BASE],
  4043.              ["<Nop>"], "Do nothing",
  4044.              function () {});
  4045.  
  4046. @@ -1594,12 +1766,12 @@ var Events = Module("events", {
  4047.              "sitemap", "", {
  4048.                  flush: function flush() {
  4049.                      memoize(this, "filters", function () this.value.filter(function (f) f(buffer.documentURI)));
  4050. -                    memoize(this, "pass", function () set(array.flatten(this.filters.map(function (f) f.keys))));
  4051. +                    memoize(this, "pass", function () Set(array.flatten(this.filters.map(function (f) f.keys))));
  4052.                      memoize(this, "commandHive", function hive() Hive(this.filters, "command"));
  4053.                      memoize(this, "inputHive", function hive() Hive(this.filters, "input"));
  4054.                  },
  4055.  
  4056. -                has: function (key) set.has(this.pass, key) || set.has(this.commandHive.stack.mappings, key),
  4057. +                has: function (key) Set.has(this.pass, key) || Set.has(this.commandHive.stack.mappings, key),
  4058.  
  4059.                  get pass() (this.flush(), this.pass),
  4060.  
  4061. @@ -1611,7 +1783,7 @@ var Events = Module("events", {
  4062.                          filter.keys = events.fromString(vals[0]).map(events.closure.toString);
  4063.  
  4064.                          filter.commandKeys = vals.slice(1).map(events.closure.canonicalKeys);
  4065. -                        filter.inputKeys = filter.commandKeys.filter(/^<[ACM]-/);
  4066. +                        filter.inputKeys = filter.commandKeys.filter(bind("test", /^<[ACM]-/));
  4067.                      });
  4068.                      this.flush();
  4069.                      return values;
  4070. @@ -1620,7 +1792,14 @@ var Events = Module("events", {
  4071.  
  4072.          options.add(["strictfocus", "sf"],
  4073.              "Prevent scripts from focusing input elements without user intervention",
  4074. -            "boolean", true);
  4075. +            "sitemap", "'chrome:*':laissez-faire,*:moderate",
  4076. +            {
  4077. +                values: {
  4078. +                    despotic: "Only allow focus changes when explicitly requested by the user",
  4079. +                    moderate: "Allow focus changes after user-initiated focus change",
  4080. +                    "laissez-faire": "Always allow focus changes"
  4081. +                }
  4082. +            });
  4083.  
  4084.          options.add(["timeout", "tmo"],
  4085.              "Whether to execute a shorter key command after a timeout when a longer command exists",
  4086. diff --git a/common/content/help.xsl b/common/content/help.xsl
  4087. --- a/common/content/help.xsl
  4088. +++ b/common/content/help.xsl
  4089. @@ -185,7 +185,7 @@
  4090.  
  4091.          <xsl:if test="//dactyl:toc[1 and self::*]">
  4092.              <div dactyl:highlight="HelpTOC">
  4093. -                <h2>Contents</h2>
  4094. +                <h2><!--L-->Contents</h2>
  4095.                  <xsl:if test="@start">
  4096.                      <xsl:call-template name="toc">
  4097.                          <xsl:with-param name="level" select="number(@start)"/>
  4098. @@ -240,7 +240,7 @@
  4099.                  </xsl:when>
  4100.                  <xsl:when test="contains($type, 'list') or contains($type, 'map')">
  4101.                      <span dactyl:highlight="HelpString" delim=""><xsl:apply-templates mode="help-1"/></span>
  4102. -                    <xsl:if test=". = ''">(empty)</xsl:if>
  4103. +                    <xsl:if test=". = ''"><!--L-->(empty)</xsl:if>
  4104.                  </xsl:when>
  4105.                  <xsl:otherwise>
  4106.                      <span>
  4107. @@ -378,14 +378,14 @@
  4108.          <a>
  4109.              <xsl:choose>
  4110.                  <xsl:when test="not(@topic)"/>
  4111. -                <xsl:when test="regexp:match(@topic, '^([a-zA-Z]*:|[^/]*#|/)', '')">
  4112. +                <xsl:when test="regexp:match(@topic, '^([a-zA-Z]+:|[^/]*#|/)', '')">
  4113.                      <xsl:attribute name="href"><xsl:value-of select="@topic"/></xsl:attribute>
  4114.                  </xsl:when>
  4115.                  <xsl:otherwise>
  4116.                      <xsl:attribute name="href"><xsl:value-of select="concat('dactyl://help-tag/', @topic)"/></xsl:attribute>
  4117.                  </xsl:otherwise>
  4118.              </xsl:choose>
  4119. -            <xsl:if test="regexp:match(@topic, '^[a-zA-Z]*:', '')
  4120. +            <xsl:if test="regexp:match(@topic, '^[a-zA-Z]+:', '')
  4121.                      and not(starts-with(@topic, 'mailto:') or
  4122.                              starts-with(@topic, 'chrome:') or
  4123.                              starts-with(@topic, 'resource:') or
  4124. @@ -430,7 +430,7 @@
  4125.      <xsl:template match="dactyl:deprecated" mode="help-2">
  4126.          <p style="clear: both;">
  4127.              <xsl:apply-templates select="@*" mode="help-1"/>
  4128. -            <span dactyl:highlight="HelpWarning">Deprecated:</span>
  4129. +            <span dactyl:highlight="HelpWarning"><!--L-->Deprecated:</span>
  4130.              <xsl:text> </xsl:text>
  4131.              <xsl:apply-templates select="node()" mode="help-1"/>
  4132.          </p>
  4133. @@ -439,7 +439,7 @@
  4134.          <p style="clear: both;">
  4135.              <xsl:apply-templates select="@*" mode="help-1"/>
  4136.              <div style="clear: both;"/>
  4137. -            <span dactyl:highlight="HelpNote">Note:</span>
  4138. +            <span dactyl:highlight="HelpNote"><!--L-->Note:</span>
  4139.              <xsl:text> </xsl:text>
  4140.              <xsl:apply-templates select="node()" mode="help-1"/>
  4141.          </p>
  4142. @@ -448,7 +448,7 @@
  4143.          <p style="clear: both;">
  4144.              <xsl:apply-templates select="@*" mode="help-1"/>
  4145.              <div style="clear: both;"/>
  4146. -            <span dactyl:highlight="HelpWarning">Warning:</span>
  4147. +            <span dactyl:highlight="HelpWarning"><!--L-->Warning:</span>
  4148.              <xsl:text> </xsl:text>
  4149.              <xsl:apply-templates select="node()" mode="help-1"/>
  4150.          </p>
  4151. @@ -511,10 +511,21 @@
  4152.              <xsl:apply-templates select="@*|node()" mode="help-1"/>
  4153.          </div>
  4154.      </xsl:template>
  4155. -    <xsl:template match="dactyl:str | dactyl:type" mode="help-2">
  4156. -        <span>
  4157. -            <xsl:if test="self::dactyl:str"><xsl:attribute name="dactyl:highlight">HelpString</xsl:attribute></xsl:if>
  4158. -            <xsl:if test="self::dactyl:type"><xsl:attribute name="dactyl:highlight">HelpType</xsl:attribute></xsl:if>
  4159. +    <xsl:template match="dactyl:type" mode="help-2">
  4160. +        <a dactyl:highlight="HelpType">
  4161. +            <xsl:choose>
  4162. +                <xsl:when test="contains(ancestor::*/@document-tags, concat(' ', ., ' '))">
  4163. +                    <xsl:attribute name="href">#<xsl:value-of select="."/></xsl:attribute>
  4164. +                </xsl:when>
  4165. +                <xsl:when test="not(. = 'boolean' or . = 'number' or . = 'string')">
  4166. +                    <xsl:attribute name="href">dactyl://help-tag/<xsl:value-of select="."/></xsl:attribute>
  4167. +                </xsl:when>
  4168. +            </xsl:choose>
  4169. +            <xsl:apply-templates select="@*|node()" mode="help-1"/>
  4170. +        </a>
  4171. +    </xsl:template>
  4172. +    <xsl:template match="dactyl:str" mode="help-2">
  4173. +        <span dactyl:highlight="HelpString">
  4174.              <xsl:apply-templates select="@*|node()" mode="help-1"/>
  4175.          </span>
  4176.      </xsl:template>
  4177. diff --git a/common/content/hints.js b/common/content/hints.js
  4178. --- a/common/content/hints.js
  4179. +++ b/common/content/hints.js
  4180. @@ -38,12 +38,13 @@ var HintSession = Class("HintSession", C
  4181.          this.open();
  4182.  
  4183.          this.top = opts.window || content;
  4184. -        this.top.addEventListener("resize", hints.resizeTimer.closure.tell, true);
  4185. -        this.top.addEventListener("dactyl-commandupdate", hints.resizeTimer.closure.tell, false, true);
  4186. +        this.top.addEventListener("resize", this.closure._onResize, true);
  4187. +        this.top.addEventListener("dactyl-commandupdate", this.closure._onResize, false, true);
  4188.  
  4189.          this.generate();
  4190.  
  4191.          this.show();
  4192. +        this.magic = true;
  4193.  
  4194.          if (this.validHints.length == 0) {
  4195.              dactyl.beep();
  4196. @@ -51,7 +52,7 @@ var HintSession = Class("HintSession", C
  4197.          }
  4198.          else if (this.validHints.length == 1 && !this.continue)
  4199.              this.process(false);
  4200. -        else // Ticket #185
  4201. +        else
  4202.              this.checkUnique();
  4203.      },
  4204.  
  4205. @@ -69,6 +70,15 @@ var HintSession = Class("HintSession", C
  4206.                  hints.setClass(this.imgSpan, this.valid ? val : null);
  4207.          },
  4208.  
  4209. +        get ambiguous() this.span.hasAttribute("ambiguous"),
  4210. +        set ambiguous(val) {
  4211. +            let meth = val ? "setAttribute" : "removeAttribute";
  4212. +            this.elem[meth]("ambiguous", "true");
  4213. +            this.span[meth]("ambiguous", "true");
  4214. +            if (this.imgSpan)
  4215. +                this.imgSpan[meth]("ambiguous", "true");
  4216. +        },
  4217. +
  4218.          get valid() this._valid,
  4219.          set valid(val) {
  4220.              this._valid = val,
  4221. @@ -76,7 +86,6 @@ var HintSession = Class("HintSession", C
  4222.              this.span.style.display = (val ? "" : "none");
  4223.              if (this.imgSpan)
  4224.                  this.imgSpan.style.display = (val ? "" : "none");
  4225. -
  4226.              this.active = this.active;
  4227.          }
  4228.      },
  4229. @@ -92,8 +101,8 @@ var HintSession = Class("HintSession", C
  4230.              if (hints.hintSession == this)
  4231.                  hints.hintSession = null;
  4232.              if (this.top) {
  4233. -                this.top.removeEventListener("resize", hints.resizeTimer.closure.tell, true);
  4234. -                this.top.removeEventListener("dactyl-commandupdate", hints.resizeTimer.closure.tell, true);
  4235. +                this.top.removeEventListener("resize", this.closure._onResize, true);
  4236. +                this.top.removeEventListener("dactyl-commandupdate", this.closure._onResize, true);
  4237.              }
  4238.  
  4239.              this.removeHints(0);
  4240. @@ -156,6 +165,21 @@ var HintSession = Class("HintSession", C
  4241.      },
  4242.  
  4243.      /**
  4244. +     * The reverse of {@link #getHintString}. Given a hint string,
  4245. +     * returns its index.
  4246. +     *
  4247. +     * @param {string} str The hint's string.
  4248. +     * @returns {number} The hint's index.
  4249. +     */
  4250. +    getHintNumber: function getHintNumber(str) {
  4251. +        let base = this.hintKeys.length;
  4252. +        let res = 0;
  4253. +        for (let char in values(str))
  4254. +            res = res * base + this.hintKeys.indexOf(char);
  4255. +        return res;
  4256. +    },
  4257. +
  4258. +    /**
  4259.       * Returns true if the given key string represents a
  4260.       * pseudo-hint-number.
  4261.       *
  4262. @@ -255,6 +279,11 @@ var HintSession = Class("HintSession", C
  4263.  
  4264.          let doc = win.document;
  4265.  
  4266. +        memoize(doc, "dactylLabels", function ()
  4267. +            iter([l.getAttribute("for"), l]
  4268. +                 for (l in array.iterValues(doc.querySelectorAll("label[for]"))))
  4269. +             .toObject());
  4270. +
  4271.          let [offsetX, offsetY] = this.getContainerOffsets(doc);
  4272.  
  4273.          offsets = offsets || { left: 0, right: 0, top: 0, bottom: 0 };
  4274. @@ -263,11 +292,15 @@ var HintSession = Class("HintSession", C
  4275.  
  4276.          function isVisible(elem) {
  4277.              let rect = elem.getBoundingClientRect();
  4278. -            if (!rect || !rect.width || !rect.height ||
  4279. +            if (!rect ||
  4280.                  rect.top > offsets.bottom || rect.bottom < offsets.top ||
  4281.                  rect.left > offsets.right || rect.right < offsets.left)
  4282.                  return false;
  4283.  
  4284. +            if (!rect.width || !rect.height)
  4285. +                if (!Array.some(elem.childNodes, function (elem) elem instanceof Element && util.computedStyle(elem).float != "none" && isVisible(elem)))
  4286. +                    return false;
  4287. +
  4288.              let computedStyle = doc.defaultView.getComputedStyle(elem, null);
  4289.              if (computedStyle.visibility != "visible" || computedStyle.display == "none")
  4290.                  return false;
  4291. @@ -281,17 +314,24 @@ var HintSession = Class("HintSession", C
  4292.              util.computedStyle(fragment).height; // Force application of binding.
  4293.              let container = doc.getAnonymousElementByAttribute(fragment, "anonid", "hints") || fragment;
  4294.  
  4295. -            let baseNodeAbsolute = util.xmlToDom(<span highlight="Hint" style="display: none"/>, doc);
  4296. +            let baseNodeAbsolute = util.xmlToDom(<span highlight="Hint" style="display: none;"/>, doc);
  4297.  
  4298.              let mode = this.hintMode;
  4299.              let res = mode.matcher(doc);
  4300.  
  4301.              let start = this.pageHints.length;
  4302. -            for (let elem in res) {
  4303. -                let hint = { elem: elem, showText: false, __proto__: this.Hint };
  4304. +            let _hints = [];
  4305. +            for (let elem in res)
  4306. +                if (isVisible(elem) && (!mode.filter || mode.filter(elem)))
  4307. +                    _hints.push({
  4308. +                        elem: elem,
  4309. +                        rect: elem.getClientRects()[0] || elem.getBoundingClientRect(),
  4310. +                        showText: false,
  4311. +                        __proto__: this.Hint
  4312. +                    });
  4313.  
  4314. -                if (!isVisible(elem) || mode.filter && !mode.filter(elem))
  4315. -                    continue;
  4316. +            for (let hint in values(_hints)) {
  4317. +                let { elem, rect } = hint;
  4318.  
  4319.                  if (elem.hasAttributeNS(NS, "hint"))
  4320.                      [hint.text, hint.showText] = [elem.getAttributeNS(NS, "hint"), true];
  4321. @@ -302,17 +342,15 @@ var HintSession = Class("HintSession", C
  4322.                  else
  4323.                      hint.text = elem.textContent.toLowerCase();
  4324.  
  4325. -                hint.span = baseNodeAbsolute.cloneNode(true);
  4326. +                hint.span = baseNodeAbsolute.cloneNode(false);
  4327.  
  4328. -                let rect = elem.getClientRects()[0] || elem.getBoundingClientRect();
  4329.                  let leftPos = Math.max((rect.left + offsetX), offsetX);
  4330.                  let topPos  = Math.max((rect.top + offsetY), offsetY);
  4331.  
  4332.                  if (elem instanceof HTMLAreaElement)
  4333.                      [leftPos, topPos] = this.getAreaOffset(elem, leftPos, topPos);
  4334.  
  4335. -                hint.span.style.left = leftPos + "px";
  4336. -                hint.span.style.top =  topPos + "px";
  4337. +                hint.span.setAttribute("style", ["display: none; left:", leftPos, "px; top:", topPos, "px"].join(""));
  4338.                  container.appendChild(hint.span);
  4339.  
  4340.                  this.pageHints.push(hint);
  4341. @@ -358,7 +396,7 @@ var HintSession = Class("HintSession", C
  4342.      },
  4343.  
  4344.      /**
  4345. -     * Handle a hint mode event.
  4346. +     * Handle a hints mode event.
  4347.       *
  4348.       * @param {Event} event The event to handle.
  4349.       */
  4350. @@ -399,12 +437,17 @@ var HintSession = Class("HintSession", C
  4351.          return PASS;
  4352.      },
  4353.  
  4354. -    onResize: function () {
  4355. +    onResize: function onResize() {
  4356.          this.removeHints(0);
  4357.          this.generate(this.top);
  4358.          this.show();
  4359.      },
  4360.  
  4361. +    _onResize: function _onResize() {
  4362. +        if (this.magic)
  4363. +            hints.resizeTimer.tell();
  4364. +    },
  4365. +
  4366.      /**
  4367.       * Finish hinting.
  4368.       *
  4369. @@ -420,15 +463,9 @@ var HintSession = Class("HintSession", C
  4370.  
  4371.          // This "followhints" option is *too* confusing. For me, and
  4372.          // presumably for users, too. --Kris
  4373. -        if (options["followhints"] > 0) {
  4374. -            if (!followFirst)
  4375. +        if (options["followhints"] > 0 && !followFirst)
  4376.                  return; // no return hit; don't examine uniqueness
  4377.  
  4378. -            // OK. return hit. But there's more than one hint, and
  4379. -            // there's no tab-selected current link. Do not follow in mode 2
  4380. -            dactyl.assert(options["followhints"] != 2 || this.validHints.length == 1 || this.hintNumber);
  4381. -        }
  4382. -
  4383.          if (!followFirst) {
  4384.              let firstHref = this.validHints[0].elem.getAttribute("href") || null;
  4385.              if (firstHref) {
  4386. @@ -469,13 +506,14 @@ var HintSession = Class("HintSession", C
  4387.                  modes.push(modes.IGNORE, modes.HINTS);
  4388.          }
  4389.  
  4390. -        this.timeout(function () {
  4391. -            if ((modes.extended & modes.HINTS) && !this.continue)
  4392. -                modes.pop();
  4393. -            commandline.lastEcho = null; // Hack.
  4394.              dactyl.trapErrors("action", this.hintMode,
  4395.                                elem, elem.href || elem.src || "",
  4396.                                this.extendedhintCount, top);
  4397. +
  4398. +        this.timeout(function () {
  4399. +            if (modes.main == modes.IGNORE && !this.continue)
  4400. +                modes.pop();
  4401. +            commandline.lastEcho = null; // Hack.
  4402.              if (this.continue && this.top)
  4403.                  this.show();
  4404.          }, timeout);
  4405. @@ -491,11 +529,16 @@ var HintSession = Class("HintSession", C
  4406.       */
  4407.      removeHints: function _removeHints(timeout) {
  4408.          for (let { doc, start, end } in values(this.docs)) {
  4409. +            // Goddamn stupid fucking Gecko 1.x security manager bullshit.
  4410. +            try { delete doc.dactylLabels; } catch (e) { doc.dactylLabels = undefined; }
  4411. +
  4412.              for (let elem in util.evaluateXPath("//*[@dactyl:highlight='hints']", doc))
  4413.                  elem.parentNode.removeChild(elem);
  4414. -            for (let i in util.range(start, end + 1))
  4415. +            for (let i in util.range(start, end + 1)) {
  4416. +                this.pageHints[i].ambiguous = false;
  4417.                  this.pageHints[i].valid = false;
  4418.          }
  4419. +        }
  4420.          styles.system.remove("hint-positions");
  4421.  
  4422.          this.reset();
  4423. @@ -561,19 +604,25 @@ var HintSession = Class("HintSession", C
  4424.                          text.push(UTF8(hint.elem.checked ? "⊙" : "○"));
  4425.                      else if (hint.elem.type === "checkbox")
  4426.                          text.push(UTF8(hint.elem.checked ? "☑" : "☐"));
  4427. -                if (hint.showText)
  4428. +                if (hint.showText && !/^\s*$/.test(hint.text))
  4429.                      text.push(hint.text.substr(0, 50));
  4430.  
  4431.                  hint.span.setAttribute("text", str + (text.length ? ": " + text.join(" ") : ""));
  4432.                  hint.span.setAttribute("number", str);
  4433.                  if (hint.imgSpan)
  4434.                      hint.imgSpan.setAttribute("number", str);
  4435. +
  4436.                  hint.active = activeHint == hintnum;
  4437. +
  4438.                  this.validHints.push(hint);
  4439.                  hintnum++;
  4440.              }
  4441.          }
  4442.  
  4443. +        let base = this.hintKeys.length;
  4444. +        for (let [i, hint] in Iterator(this.validHints))
  4445. +            hint.ambiguous = (i + 1) * base <= this.validHints.length;
  4446. +
  4447.          if (options["usermode"]) {
  4448.              let css = [];
  4449.              for (let hint in values(this.pageHints)) {
  4450. @@ -685,16 +734,15 @@ var Hints = Module("hints", {
  4451.              events.listen(appContent, "scroll", this.resizeTimer.closure.tell, false);
  4452.  
  4453.          const Mode = Hints.Mode;
  4454. -        Mode.defaultValue("tags", function () function () options.get("hinttags").matcher);
  4455.          Mode.prototype.__defineGetter__("matcher", function ()
  4456. -            options.get("extendedhinttags").getKey(this.name, this.tags()));
  4457. +            options.get("extendedhinttags").getKey(this.name, options.get("hinttags").matcher));
  4458.  
  4459.          this.modes = {};
  4460.          this.addMode(";", "Focus hint",                           buffer.closure.focusElement);
  4461.          this.addMode("?", "Show information for hint",            function (elem) buffer.showElementInfo(elem));
  4462.          this.addMode("s", "Save hint",                            function (elem) buffer.saveLink(elem, false));
  4463.          this.addMode("f", "Focus frame",                          function (elem) dactyl.focus(elem.ownerDocument.defaultView));
  4464. -        this.addMode("F", "Focus frame or pseudo-frame",          buffer.closure.focusElement, null, isScrollable);
  4465. +        this.addMode("F", "Focus frame or pseudo-frame",          buffer.closure.focusElement, isScrollable);
  4466.          this.addMode("o", "Follow hint",                          function (elem) buffer.followLink(elem, dactyl.CURRENT_TAB));
  4467.          this.addMode("t", "Follow hint in a new tab",             function (elem) buffer.followLink(elem, dactyl.NEW_TAB));
  4468.          this.addMode("b", "Follow hint in a background tab",      function (elem) buffer.followLink(elem, dactyl.NEW_BACKGROUND_TAB));
  4469. @@ -719,21 +767,35 @@ var Hints = Module("hints", {
  4470.      hintSession: Modes.boundProperty(),
  4471.  
  4472.      /**
  4473. -     * Creates a new hint mode.
  4474. +     * Creates a new hints mode.
  4475.       *
  4476.       * @param {string} mode The letter that identifies this mode.
  4477.       * @param {string} prompt The description to display to the user
  4478.       *     about this mode.
  4479.       * @param {function(Node)} action The function to be called with the
  4480.       *     element that matches.
  4481. -     * @param {function():string} tags The function that returns an
  4482. -     *     XPath expression to decide which elements can be hinted (the
  4483. -     *     default returns options["hinttags"]).
  4484. +     * @param {function(Node):boolean} filter A function used to filter
  4485. +     *     the returned node set.
  4486. +     * @param {[string]} tags A value to add to the default
  4487. +     *     'extendedhinttags' value for this mode.
  4488.       * @optional
  4489.       */
  4490. -    addMode: function (mode, prompt, action, tags) {
  4491. -        arguments[1] = UTF8(prompt);
  4492. -        this.modes[mode] = Hints.Mode.apply(Hints.Mode, arguments);
  4493. +    addMode: function (mode, prompt, action, filter, tags) {
  4494. +        function toString(regexp) RegExp.prototype.toString.call(regexp);
  4495. +
  4496. +        if (tags != null) {
  4497. +            let eht = options.get("extendedhinttags");
  4498. +            let update = eht.isDefault;
  4499. +
  4500. +            let value = eht.parse(Option.quote(util.regexp.escape(mode)) + ":" + tags.map(Option.quote))[0];
  4501. +            eht.defaultValue = eht.defaultValue.filter(function (re) toString(re) != toString(value))
  4502. +                                  .concat(value);
  4503. +
  4504. +            if (update)
  4505. +                eht.reset();
  4506. +        }
  4507. +
  4508. +        this.modes[mode] = Hints.Mode(mode, UTF8(prompt), action, filter);
  4509.      },
  4510.  
  4511.      /**
  4512. @@ -762,7 +824,7 @@ var Hints = Module("hints", {
  4513.  
  4514.          let type = elem.type;
  4515.  
  4516. -        if (elem instanceof HTMLInputElement && set.has(util.editableInputs, elem.type))
  4517. +        if (elem instanceof HTMLInputElement && Set.has(util.editableInputs, elem.type))
  4518.              return [elem.value, false];
  4519.          else {
  4520.              for (let [, option] in Iterator(options["hintinputs"])) {
  4521. @@ -776,15 +838,14 @@ var Hints = Module("hints", {
  4522.                              return [elem.alt.toLowerCase(), true];
  4523.                      }
  4524.                      else if (elem.value && type != "password") {
  4525. -                        // radio's and checkboxes often use internal ids as values - maybe make this an option too...
  4526. +                        // radios and checkboxes often use internal ids as values - maybe make this an option too...
  4527.                          if (! ((type == "radio" || type == "checkbox") && !isNaN(elem.value)))
  4528.                              return [elem.value.toLowerCase(), (type == "radio" || type == "checkbox")];
  4529.                      }
  4530.                  }
  4531.                  else if (option == "label") {
  4532.                      if (elem.id) {
  4533. -                        // TODO: (possibly) do some guess work for label-like objects
  4534. -                        let label = util.evaluateXPath(["label[@for=" + elem.id.quote() + "]"], doc).snapshotItem(0);
  4535. +                        let label = elem.ownerDocument.dactylLabels[elem.id];
  4536.                          if (label)
  4537.                              return [label.textContent.toLowerCase(), true];
  4538.                      }
  4539. @@ -971,6 +1032,7 @@ var Hints = Module("hints", {
  4540.      open: function open(mode, opts) {
  4541.          this._extendedhintCount = opts.count;
  4542.          commandline.input(["Normal", mode], "", {
  4543. +            autocomplete: false,
  4544.              completer: function (context) {
  4545.                  context.compare = function () 0;
  4546.                  context.completions = [[k, v.prompt] for ([k, v] in Iterator(hints.modes))];
  4547. @@ -979,10 +1041,13 @@ var Hints = Module("hints", {
  4548.                  if (arg)
  4549.                      hints.show(arg, opts);
  4550.              },
  4551. -            onChange: function () {
  4552. +            onChange: function (arg) {
  4553. +                if (Object.keys(hints.modes).some(function (m) m != arg && m.indexOf(arg) == 0))
  4554. +                    return;
  4555. +
  4556.                  this.accepted = true;
  4557.                  modes.pop();
  4558. -            },
  4559. +            }
  4560.          });
  4561.      },
  4562.  
  4563. @@ -1112,14 +1177,14 @@ var Hints = Module("hints", {
  4564.          return -1;
  4565.      },
  4566.  
  4567. -    Mode: Struct("HintMode", "name", "prompt", "action", "tags", "filter")
  4568. +    Mode: Struct("HintMode", "name", "prompt", "action", "filter")
  4569.              .localize("prompt")
  4570.  }, {
  4571.      modes: function initModes() {
  4572.          initModes.require("commandline");
  4573.          modes.addMode("HINTS", {
  4574.              extended: true,
  4575. -            description: "Active when selecting elements in QuickHint or ExtendedHint mode",
  4576. +            description: "Active when selecting elements with hints",
  4577.              bases: [modes.COMMAND_LINE],
  4578.              input: true,
  4579.              ownsBuffer: true
  4580. @@ -1128,20 +1193,20 @@ var Hints = Module("hints", {
  4581.      mappings: function () {
  4582.          var myModes = config.browserModes.concat(modes.OUTPUT_MULTILINE);
  4583.          mappings.add(myModes, ["f"],
  4584. -            "Start QuickHint mode",
  4585. +            "Start Hints mode",
  4586.              function () { hints.show("o"); });
  4587.  
  4588.          mappings.add(myModes, ["F"],
  4589. -            "Start QuickHint mode, but open link in a new tab",
  4590. +            "Start Hints mode, but open link in a new tab",
  4591.              function () { hints.show(options.get("activate").has("links") ? "t" : "b"); });
  4592.  
  4593.          mappings.add(myModes, [";"],
  4594. -            "Start an extended hint mode",
  4595. +            "Start an extended hints mode",
  4596.              function ({ count }) { hints.open(";", { count: count }); },
  4597.              { count: true });
  4598.  
  4599.          mappings.add(myModes, ["g;"],
  4600. -            "Start an extended hint mode and stay there until <Esc> is pressed",
  4601. +            "Start an extended hints mode and stay there until <Esc> is pressed",
  4602.              function ({ count }) { hints.open("g;", { continue: true, count: count }); },
  4603.              { count: true });
  4604.  
  4605. @@ -1172,7 +1237,7 @@ var Hints = Module("hints", {
  4606.              "XPath or CSS selector strings of hintable elements for extended hint modes",
  4607.              "regexpmap", {
  4608.                  "[iI]": "img",
  4609. -                "[asOTivVWy]": ["a[href]", "area[href]", "img[src]", "iframe[src]"],
  4610. +                "[asOTvVWy]": ["a[href]", "area[href]", "img[src]", "iframe[src]"],
  4611.                  "[f]": "body",
  4612.                  "[F]": ["body", "code", "div", "html", "p", "pre", "span"],
  4613.                  "[S]": ["input:not([type=hidden])", "textarea", "button", "select"]
  4614. @@ -1180,7 +1245,7 @@ var Hints = Module("hints", {
  4615.              {
  4616.                  keepQuotes: true,
  4617.                  getKey: function (val, default_)
  4618. -                    let (res = array.nth(this.value, function (re) re.test(val), 0))
  4619. +                    let (res = array.nth(this.value, function (re) let (match = re.exec(val)) match && match[0] == val, 0))
  4620.                          res ? res.matcher : default_,
  4621.                  setter: function (vals) {
  4622.                      for (let value in values(vals))
  4623. @@ -1191,10 +1256,10 @@ var Hints = Module("hints", {
  4624.              });
  4625.  
  4626.          options.add(["hinttags", "ht"],
  4627. -            "XPath string of hintable elements activated by 'f' and 'F'",
  4628. -            "stringlist", "input:not([type=hidden]),a,area,iframe,textarea,button,select," +
  4629. +            "XPath or CSS selector strings of hintable elements for Hints mode",
  4630. +            "stringlist", "input:not([type=hidden]),a[href],area,iframe,textarea,button,select," +
  4631.                            "[onclick],[onmouseover],[onmousedown],[onmouseup],[oncommand]," +
  4632. -                          "[tabindex],[role=link],[role=button]",
  4633. +                          "[tabindex],[role=link],[role=button],[contenteditable=true]",
  4634.              {
  4635.                  setter: function (values) {
  4636.                      this.matcher = util.compileMatcher(values);
  4637. @@ -1213,8 +1278,8 @@ var Hints = Module("hints", {
  4638.                  },
  4639.                  validator: function (value) {
  4640.                      let values = events.fromString(value).map(events.closure.toString);
  4641. -                    return Option.validIf(array.uniq(values).length === values.length,
  4642. -                                            "Duplicate keys not allowed");
  4643. +                    return Option.validIf(array.uniq(values).length === values.length && values.length > 1,
  4644. +                                          _("option.hintkeys.duplicate"));
  4645.                  }
  4646.              });
  4647.  
  4648. @@ -1224,15 +1289,12 @@ var Hints = Module("hints", {
  4649.              { validator: function (value) value >= 0 });
  4650.  
  4651.          options.add(["followhints", "fh"],
  4652. -            // FIXME: this description isn't very clear but I can't think of a
  4653. -            // better one right now.
  4654. -            "Change the behavior of <Return> in hint mode",
  4655. +            "Define the conditions under which selected hints are followed",
  4656.              "number", 0,
  4657.              {
  4658.                  values: {
  4659.                      "0": "Follow the first hint as soon as typed text uniquely identifies it. Follow the selected hint on <Return>.",
  4660.                      "1": "Follow the selected hint on <Return>.",
  4661. -                    "2": "Follow the selected hint on <Return> only it's been <Tab>-selected."
  4662.                  }
  4663.              });
  4664.  
  4665. diff --git a/common/content/history.js b/common/content/history.js
  4666. --- a/common/content/history.js
  4667. +++ b/common/content/history.js
  4668. @@ -41,7 +41,7 @@ var History = Module("history", {
  4669.              return {
  4670.                  url: node.uri,
  4671.                  title: node.title,
  4672. -                icon: node.icon ? node.icon.spec : DEFAULT_FAVICON
  4673. +                icon: node.icon ? node.icon : DEFAULT_FAVICON
  4674.              };
  4675.          }).toArray();
  4676.          root.containerOpen = false; // close a container after using it!
  4677. @@ -230,8 +230,8 @@ var History = Module("history", {
  4678.                                  "uri",
  4679.                                  "visitcount"
  4680.                              ].map(function (order) [
  4681. -                                  ["+" + order.replace(" ", ""), "Sort by " + order + " ascending"],
  4682. -                                  ["-" + order.replace(" ", ""), "Sort by " + order + " descending"]
  4683. +                                  ["+" + order.replace(" ", ""), /*L*/"Sort by " + order + " ascending"],
  4684. +                                  ["-" + order.replace(" ", ""), /*L*/"Sort by " + order + " descending"]
  4685.                              ]));
  4686.                          }
  4687.                      }
  4688. diff --git a/common/content/mappings.js b/common/content/mappings.js
  4689. --- a/common/content/mappings.js
  4690. +++ b/common/content/mappings.js
  4691. @@ -12,8 +12,8 @@
  4692.   * A class representing key mappings. Instances are created by the
  4693.   * {@link Mappings} class.
  4694.   *
  4695. - * @param {number[]} modes The modes in which this mapping is active.
  4696. - * @param {string[]} keys The key sequences which are bound to
  4697. + * @param {[Modes.Mode]} modes The modes in which this mapping is active.
  4698. + * @param {[string]} keys The key sequences which are bound to
  4699.   *     *action*.
  4700.   * @param {string} description A short one line description of the key mapping.
  4701.   * @param {function} action The action invoked by each key sequence.
  4702. @@ -30,26 +30,21 @@
  4703.   */
  4704.  var Map = Class("Map", {
  4705.      init: function (modes, keys, description, action, extraInfo) {
  4706. -        modes = Array.concat(modes);
  4707. -        if (!modes.every(util.identity))
  4708. -            throw TypeError("Invalid modes: " + modes);
  4709. -
  4710.          this.id = ++Map.id;
  4711.          this.modes = modes;
  4712.          this._keys = keys;
  4713.          this.action = action;
  4714.          this.description = description;
  4715.  
  4716. -        if (Object.freeze)
  4717.              Object.freeze(this.modes);
  4718.  
  4719.          if (extraInfo)
  4720. -            update(this, extraInfo);
  4721. +            this.update(extraInfo);
  4722.      },
  4723.  
  4724.      name: Class.memoize(function () this.names[0]),
  4725.  
  4726. -    /** @property {string[]} All of this mapping's names (key sequences). */
  4727. +    /** @property {[string]} All of this mapping's names (key sequences). */
  4728.      names: Class.memoize(function () this._keys.map(function (k) events.canonicalKeys(k))),
  4729.  
  4730.      get toStringParams() [this.modes.map(function (m) m.name), this.names.map(String.quote)],
  4731. @@ -58,7 +53,7 @@ var Map = Class("Map", {
  4732.  
  4733.      /** @property {number} A unique ID for this mapping. */
  4734.      id: null,
  4735. -    /** @property {number[]} All of the modes for which this mapping applies. */
  4736. +    /** @property {[Modes.Mode]} All of the modes for which this mapping applies. */
  4737.      modes: null,
  4738.      /** @property {function (number)} The function called to execute this mapping. */
  4739.      action: null,
  4740. @@ -96,7 +91,7 @@ var Map = Class("Map", {
  4741.       */
  4742.      hasName: function (name) this.keys.indexOf(name) >= 0,
  4743.  
  4744. -    get keys() this.names.map(mappings.expandLeader),
  4745. +    get keys() array.flatten(this.names.map(mappings.closure.expand)),
  4746.  
  4747.      /**
  4748.       * Execute the action for this mapping.
  4749. @@ -119,7 +114,7 @@ var Map = Class("Map", {
  4750.              mappings.repeat = repeat;
  4751.  
  4752.          if (this.executing)
  4753. -            util.dumpStack("Attempt to execute mapping recursively: " + args.command);
  4754. +            util.dumpStack(_("map.recursive", args.command));
  4755.          dactyl.assert(!this.executing, _("map.recursive", args.command));
  4756.  
  4757.          try {
  4758. @@ -159,10 +154,10 @@ var MapHive = Class("MapHive", Contexts.
  4759.      },
  4760.  
  4761.      /**
  4762. -     * Adds a new default key mapping.
  4763. +     * Adds a new key mapping.
  4764.       *
  4765. -     * @param {number[]} modes The modes that this mapping applies to.
  4766. -     * @param {string[]} keys The key sequences which are bound to *action*.
  4767. +     * @param {[Modes.Mode]} modes The modes that this mapping applies to.
  4768. +     * @param {[string]} keys The key sequences which are bound to *action*.
  4769.       * @param {string} description A description of the key mapping.
  4770.       * @param {function} action The action invoked by each key sequence.
  4771.       * @param {Object} extra An optional extra configuration hash.
  4772. @@ -171,6 +166,10 @@ var MapHive = Class("MapHive", Contexts.
  4773.      add: function (modes, keys, description, action, extra) {
  4774.          extra = extra || {};
  4775.  
  4776. +        modes = Array.concat(modes);
  4777. +        if (!modes.every(util.identity))
  4778. +            throw TypeError(/*L*/"Invalid modes: " + modes);
  4779. +
  4780.          let map = Map(modes, keys, description, action, extra);
  4781.          map.definedAt = contexts.getCaller(Components.stack.caller);
  4782.          map.hive = this;
  4783. @@ -307,13 +306,32 @@ var Mappings = Module("mappings", {
  4784.  
  4785.      get userHives() this.allHives.filter(function (h) h !== this.builtin, this),
  4786.  
  4787. -    expandLeader: function (keyString) keyString.replace(/<Leader>/i, options["mapleader"]),
  4788. +    expandLeader: function expandLeader(keyString) keyString.replace(/<Leader>/i, function () options["mapleader"]),
  4789. +
  4790. +    prefixes: Class.memoize(function () {
  4791. +        let list = Array.map("CASM", function (s) s + "-");
  4792. +
  4793. +        return iter(util.range(0, 1 << list.length)).map(function (mask)
  4794. +            list.filter(function (p, i) mask & (1 << i)).join("")).toArray().concat("*-");
  4795. +    }),
  4796. +
  4797. +    expand: function expand(keys) {
  4798. +        keys = keys.replace(/<leader>/i, options["mapleader"]);
  4799. +        if (!/<\*-/.test(keys))
  4800. +            return keys;
  4801. +
  4802. +        return util.debrace(events.iterKeys(keys).map(function (key) {
  4803. +            if (/^<\*-/.test(key))
  4804. +                return ["<", this.prefixes, key.slice(3)];
  4805. +            return key;
  4806. +        }, this).flatten().array).map(function (k) events.canonicalKeys(k));
  4807. +    },
  4808.  
  4809.      iterate: function (mode) {
  4810.          let seen = {};
  4811.          for (let hive in this.hives.iterValues())
  4812.              for (let map in array(hive.getStack(mode)).iterValues())
  4813. -                if (!set.add(seen, map.name))
  4814. +                if (!Set.add(seen, map.name))
  4815.                      yield map;
  4816.      },
  4817.  
  4818. @@ -330,8 +348,8 @@ var Mappings = Module("mappings", {
  4819.      /**
  4820.       * Adds a new default key mapping.
  4821.       *
  4822. -     * @param {number[]} modes The modes that this mapping applies to.
  4823. -     * @param {string[]} keys The key sequences which are bound to *action*.
  4824. +     * @param {[Modes.Mode]} modes The modes that this mapping applies to.
  4825. +     * @param {[string]} keys The key sequences which are bound to *action*.
  4826.       * @param {string} description A description of the key mapping.
  4827.       * @param {function} action The action invoked by each key sequence.
  4828.       * @param {Object} extra An optional extra configuration hash.
  4829. @@ -352,8 +370,8 @@ var Mappings = Module("mappings", {
  4830.      /**
  4831.       * Adds a new user-defined key mapping.
  4832.       *
  4833. -     * @param {number[]} modes The modes that this mapping applies to.
  4834. -     * @param {string[]} keys The key sequences which are bound to *action*.
  4835. +     * @param {[Modes.Mode]} modes The modes that this mapping applies to.
  4836. +     * @param {[string]} keys The key sequences which are bound to *action*.
  4837.       * @param {string} description A description of the key mapping.
  4838.       * @param {function} action The action invoked by each key sequence.
  4839.       * @param {Object} extra An optional extra configuration hash (see
  4840. @@ -369,7 +387,7 @@ var Mappings = Module("mappings", {
  4841.      /**
  4842.       * Returns the map from *mode* named *cmd*.
  4843.       *
  4844. -     * @param {number} mode The mode to search.
  4845. +     * @param {Modes.Mode} mode The mode to search.
  4846.       * @param {string} cmd The map name to match.
  4847.       * @returns {Map}
  4848.       */
  4849. @@ -379,9 +397,9 @@ var Mappings = Module("mappings", {
  4850.       * Returns an array of maps with names starting with but not equal to
  4851.       * *prefix*.
  4852.       *
  4853. -     * @param {number} mode The mode to search.
  4854. +     * @param {Modes.Mode} mode The mode to search.
  4855.       * @param {string} prefix The map prefix string to match.
  4856. -     * @returns {Map[]}
  4857. +     * @returns {[Map]}
  4858.       */
  4859.      getCandidates: function (mode, prefix)
  4860.          this.hives.map(function (h) h.getCandidates(mode, prefix))
  4861. @@ -389,15 +407,15 @@ var Mappings = Module("mappings", {
  4862.  
  4863.      /**
  4864.       * Lists all user-defined mappings matching *filter* for the specified
  4865. -     * *modes*.
  4866. +     * *modes* in the specified *hives*.
  4867.       *
  4868. -     * @param {number[]} modes An array of modes to search.
  4869. -     * @param {string} filter The filter string to match.
  4870. +     * @param {[Modes.Mode]} modes An array of modes to search.
  4871. +     * @param {string} filter The filter string to match. @optional
  4872. +     * @param {[MapHive]} hives The map hives to list. @optional
  4873.       */
  4874.      list: function (modes, filter, hives) {
  4875. -        let modeSign = "";
  4876. -        modes.filter(function (m)  m.char).forEach(function (m) { modeSign += m.char; });
  4877. -        modes.filter(function (m) !m.char).forEach(function (m) { modeSign += " " + m.name; });
  4878. +        let modeSign = modes.map(function (m) m.char || "").join("")
  4879. +                     + modes.map(function (m) !m.char ? " " + m.name : "").join("");
  4880.          modeSign = modeSign.replace(/^ /, "");
  4881.  
  4882.          hives = (hives || mappings.userHives).map(function (h) [h, maps(h)])
  4883. @@ -413,9 +431,9 @@ var Mappings = Module("mappings", {
  4884.          let list = <table>
  4885.                  <tr highlight="Title">
  4886.                      <td/>
  4887. -                    <td style="padding-right: 1em;">Mode</td>
  4888. -                    <td style="padding-right: 1em;">Command</td>
  4889. -                    <td style="padding-right: 1em;">Action</td>
  4890. +                    <td style="padding-right: 1em;">{_("title.Mode")}</td>
  4891. +                    <td style="padding-right: 1em;">{_("title.Command")}</td>
  4892. +                    <td style="padding-right: 1em;">{_("title.Action")}</td>
  4893.                  </tr>
  4894.                  <col style="min-width: 6em; padding-right: 1em;"/>
  4895.                  {
  4896. @@ -459,6 +477,11 @@ var Mappings = Module("mappings", {
  4897.                      return;
  4898.                  }
  4899.  
  4900. +                if (args[1] && !/^<nop>$/i.test(args[1])
  4901. +                    && !args["-count"] && !args["-ex"] && !args["-javascript"]
  4902. +                    && mapmodes.every(function (m) m.count))
  4903. +                    args[1] = "<count>" + args[1];
  4904. +
  4905.                  let [lhs, rhs] = args;
  4906.                  if (noremap)
  4907.                      args["-builtin"] = true;
  4908. @@ -474,7 +497,7 @@ var Mappings = Module("mappings", {
  4909.                          contexts.bindMacro(args, "-keys", function (params) params),
  4910.                          {
  4911.                              arg: args["-arg"],
  4912. -                            count: args["-count"],
  4913. +                            count: args["-count"] || !(args["-ex"] || args["-javascript"]),
  4914.                              noremap: args["-builtin"],
  4915.                              persist: !args["-nopersist"],
  4916.                              get rhs() String(this.action),
  4917. @@ -484,6 +507,7 @@ var Mappings = Module("mappings", {
  4918.              }
  4919.  
  4920.              const opts = {
  4921. +                identifier: "map",
  4922.                      completer: function (context, args) {
  4923.                          let mapmodes = array.uniq(args["-modes"].map(findMode));
  4924.                          if (args.length == 1)
  4925. @@ -513,7 +537,7 @@ var Mappings = Module("mappings", {
  4926.                          {
  4927.                              names: ["-description", "-desc", "-d"],
  4928.                              description: "A description of this mapping",
  4929. -                            default: "User-defined mapping",
  4930. +                        default: /*L*/"User-defined mapping",
  4931.                              type: CommandOption.STRING
  4932.                          },
  4933.                          {
  4934. @@ -569,7 +593,7 @@ var Mappings = Module("mappings", {
  4935.                  let seen = {};
  4936.                  for (let stack in values(hive.stacks))
  4937.                      for (let map in array.iterValues(stack))
  4938. -                        if (!set.add(seen, map.id))
  4939. +                        if (!Set.add(seen, map.id))
  4940.                              yield map;
  4941.              }
  4942.  
  4943. @@ -606,6 +630,7 @@ var Mappings = Module("mappings", {
  4944.                          dactyl.echoerr(_("map.noSuch", args[0]));
  4945.                  },
  4946.                  {
  4947. +                    identifier: "unmap",
  4948.                      argCount: "?",
  4949.                      bang: true,
  4950.                      completer: opts.completer,
  4951. @@ -627,7 +652,7 @@ var Mappings = Module("mappings", {
  4952.              validator: function (value) Array.concat(value).every(findMode),
  4953.              completer: function () [[array.compact([mode.name.toLowerCase().replace(/_/g, "-"), mode.char]), mode.description]
  4954.                                      for (mode in values(modes.all))
  4955. -                                    if (!mode.hidden)],
  4956. +                                    if (!mode.hidden)]
  4957.          };
  4958.  
  4959.          function findMode(name) {
  4960. @@ -641,7 +666,9 @@ var Mappings = Module("mappings", {
  4961.          function uniqueModes(modes) {
  4962.              let chars = [k for ([k, v] in Iterator(modules.modes.modeChars))
  4963.                           if (v.every(function (mode) modes.indexOf(mode) >= 0))];
  4964. -            return array.uniq(modes.filter(function (m) chars.indexOf(m.char) < 0).concat(chars));
  4965. +            return array.uniq(modes.filter(function (m) chars.indexOf(m.char) < 0)
  4966. +                                   .map(function (m) m.name.toLowerCase())
  4967. +                                   .concat(chars));
  4968.          }
  4969.  
  4970.          commands.add(["feedkeys", "fk"],
  4971. @@ -664,14 +691,14 @@ var Mappings = Module("mappings", {
  4972.              if (mode.char && !commands.get(mode.char + "map", true))
  4973.                  addMapCommands(mode.char,
  4974.                                 [m.mask for (m in modes.mainModes) if (m.char == mode.char)],
  4975. -                               [mode.name.toLowerCase()]);
  4976. +                               mode.displayName);
  4977.  
  4978.          let args = {
  4979.              getMode: function (args) findMode(args["-mode"]),
  4980.              iterate: function (args, mainOnly) {
  4981.                  let modes = [this.getMode(args)];
  4982.                  if (!mainOnly)
  4983. -                    modes = modes.concat(modes[0].bases);
  4984. +                    modes = modes[0].allBases;
  4985.  
  4986.                  let seen = {};
  4987.                  // Bloody hell. --Kris
  4988. @@ -679,7 +706,7 @@ var Mappings = Module("mappings", {
  4989.                      for (let hive in mappings.hives.iterValues())
  4990.                          for (let map in array.iterValues(hive.getStack(mode)))
  4991.                              for (let name in values(map.names))
  4992. -                                if (!set.add(seen, name)) {
  4993. +                                if (!Set.add(seen, name)) {
  4994.                                      yield {
  4995.                                          name: name,
  4996.                                          columns: [
  4997. @@ -725,8 +752,8 @@ var Mappings = Module("mappings", {
  4998.                                   tags = services["dactyl:"].HELP_TAGS)
  4999.                                      ({ helpTag: prefix + map.name, __proto__: map }
  5000.                                       for (map in self.iterate(args, true))
  5001. -                                     if (map.hive === mappings.builtin || set.has(tags, prefix + map.name))),
  5002. -                    description: "List all " + mode.name + " mode mappings along with their short descriptions",
  5003. +                                     if (map.hive === mappings.builtin || Set.has(tags, prefix + map.name))),
  5004. +                    description: "List all " + mode.displayName + " mode mappings along with their short descriptions",
  5005.                      index: mode.char + "-map",
  5006.                      getMode: function (args) mode,
  5007.                      options: []
  5008. diff --git a/common/content/marks.js b/common/content/marks.js
  5009. --- a/common/content/marks.js
  5010. +++ b/common/content/marks.js
  5011. @@ -53,14 +53,14 @@ var Marks = Module("marks", {
  5012.          if (Marks.isURLMark(mark)) {
  5013.              let res = this._urlMarks.set(mark, { location: doc.documentURI, position: position, tab: Cu.getWeakReference(tabs.getTab()), timestamp: Date.now()*1000 });
  5014.              if (!silent)
  5015. -                dactyl.log("Adding URL mark: " + Marks.markToString(mark, res), 5);
  5016. +                dactyl.log(_("mark.addURL", Marks.markToString(mark, res)), 5);
  5017.          }
  5018.          else if (Marks.isLocalMark(mark)) {
  5019.              let marks = this._localMarks.get(doc.documentURI, {});
  5020.              marks[mark] = { location: doc.documentURI, position: position, timestamp: Date.now()*1000 };
  5021.              this._localMarks.changed();
  5022.              if (!silent)
  5023. -                dactyl.log("Adding local mark: " + Marks.markToString(mark, marks[mark]), 5);
  5024. +                dactyl.log(_("mark.addLocal", Marks.markToString(mark, marks[mark])), 5);
  5025.          }
  5026.      },
  5027.  
  5028. @@ -115,7 +115,7 @@ var Marks = Module("marks", {
  5029.                  tabs.select(tab);
  5030.                  let doc = tab.linkedBrowser.contentDocument;
  5031.                  if (doc.documentURI == mark.location) {
  5032. -                    dactyl.log("Jumping to URL mark: " + Marks.markToString(char, mark), 5);
  5033. +                    dactyl.log(_("mark.jumpingToURL", Marks.markToString(char, mark)), 5);
  5034.                      buffer.scrollToPercent(mark.position.x * 100, mark.position.y * 100);
  5035.                  }
  5036.                  else {
  5037. @@ -146,7 +146,7 @@ var Marks = Module("marks", {
  5038.              let mark = (this._localMarks.get(this.localURI) || {})[char];
  5039.              dactyl.assert(mark, _("mark.unset", char));
  5040.  
  5041. -            dactyl.log("Jumping to local mark: " + Marks.markToString(char, mark), 5);
  5042. +            dactyl.log(_("mark.jumpingToLocal", Marks.markToString(char, mark)), 5);
  5043.              buffer.scrollToPercent(mark.position.x * 100, mark.position.y * 100);
  5044.          }
  5045.          else
  5046. @@ -249,7 +249,7 @@ var Marks = Module("marks", {
  5047.              "Mark current location within the web page",
  5048.              function (args) {
  5049.                  let mark = args[0] || "";
  5050. -                dactyl.assert(mark.length <= 1, _("error.trailing"));
  5051. +                dactyl.assert(mark.length <= 1, _("error.trailingCharacters"));
  5052.                  dactyl.assert(/[a-zA-Z]/.test(mark), _("mark.invalid"));
  5053.  
  5054.                  marks.add(mark);
  5055. @@ -270,7 +270,6 @@ var Marks = Module("marks", {
  5056.          completion.mark = function mark(context) {
  5057.              function percent(i) Math.round(i * 100);
  5058.  
  5059. -            // FIXME: Line/Column doesn't make sense with %
  5060.              context.title = ["Mark", "HPos VPos File"];
  5061.              context.keys.description = function ([, m]) percent(m.position.x) + "% " + percent(m.position.y) + "% " + m.location;
  5062.              context.completions = marks.all;
  5063. diff --git a/common/content/modes.js b/common/content/modes.js
  5064. --- a/common/content/modes.js
  5065. +++ b/common/content/modes.js
  5066. @@ -62,8 +62,7 @@ var Modes = Module("modes", {
  5067.              description: "Active when text is selected",
  5068.              display: function () "VISUAL" + (this._extended & modes.LINE ? " LINE" : ""),
  5069.              bases: [this.COMMAND],
  5070. -            ownsFocus: true,
  5071. -            passUnknown: false
  5072. +            ownsFocus: true
  5073.          }, {
  5074.              leave: function (stack, newMode) {
  5075.                  if (newMode.main == modes.CARET) {
  5076. @@ -77,7 +76,7 @@ var Modes = Module("modes", {
  5077.          });
  5078.          this.addMode("CARET", {
  5079.              description: "Active when the caret is visible in the web content",
  5080. -            bases: [this.COMMAND]
  5081. +            bases: [this.NORMAL]
  5082.          }, {
  5083.  
  5084.              get pref()    prefs.get("accessibility.browsewithcaret"),
  5085. @@ -99,13 +98,22 @@ var Modes = Module("modes", {
  5086.              char: "t",
  5087.              description: "Vim-like editing of input elements",
  5088.              bases: [this.COMMAND],
  5089. -            input: true,
  5090. -            ownsFocus: true,
  5091. -            passUnknown: false
  5092. +            ownsFocus: true
  5093. +        }, {
  5094. +            onKeyPress: function (eventList) {
  5095. +                const KILL = false, PASS = true;
  5096. +
  5097. +                // Hack, really.
  5098. +                if (eventList[0].charCode || /^<(?:.-)*(?:BS|Del|C-h|C-w|C-u|C-k)>$/.test(events.toString(eventList[0]))) {
  5099. +                    dactyl.beep();
  5100. +                    return KILL;
  5101. +                }
  5102. +                return PASS;
  5103. +            }
  5104.          });
  5105.          this.addMode("OUTPUT_MULTILINE", {
  5106.              description: "Active when the multi-line output buffer is open",
  5107. -            bases: [this.COMMAND],
  5108. +            bases: [this.NORMAL]
  5109.          });
  5110.  
  5111.          this.addMode("INPUT", {
  5112. @@ -199,6 +207,52 @@ var Modes = Module("modes", {
  5113.  
  5114.              }
  5115.          });
  5116. +
  5117. +        function makeTree() {
  5118. +            let list = modes.all.filter(function (m) m.name !== m.description);
  5119. +
  5120. +            let tree = {};
  5121. +
  5122. +            for (let mode in values(list))
  5123. +                tree[mode.name] = {};
  5124. +
  5125. +            for (let mode in values(list))
  5126. +                for (let base in values(mode.bases))
  5127. +                    tree[base.name][mode.name] = tree[mode.name];
  5128. +
  5129. +            let roots = iter([m.name, tree[m.name]] for (m in values(list)) if (!m.bases.length)).toObject();
  5130. +
  5131. +            default xml namespace = NS;
  5132. +            function rec(obj) {
  5133. +                XML.ignoreWhitespace = XML.prettyPrinting = false;
  5134. +
  5135. +                let res = <ul dactyl:highlight="Dense" xmlns:dactyl={NS}/>;
  5136. +                Object.keys(obj).sort().forEach(function (name) {
  5137. +                    let mode = modes.getMode(name);
  5138. +                    res.* += <li><em>{mode.displayName}</em>: {mode.description}{
  5139. +                        rec(obj[name])
  5140. +                    }</li>;
  5141. +                });
  5142. +
  5143. +                if (res.*.length())
  5144. +                    return res;
  5145. +                return <></>;
  5146. +            }
  5147. +
  5148. +            return rec(roots);
  5149. +        }
  5150. +
  5151. +        util.timeout(function () {
  5152. +            // Waits for the add-on to become available, if necessary.
  5153. +            config.addon;
  5154. +            config.version;
  5155. +
  5156. +            services["dactyl:"].pages["modes.dtd"] = services["dactyl:"].pages["modes.dtd"]();
  5157. +        });
  5158. +
  5159. +        services["dactyl:"].pages["modes.dtd"] = function () [null,
  5160. +            util.makeDTD(iter({ "modes.tree": makeTree() },
  5161. +                              config.dtd))];
  5162.      },
  5163.      cleanup: function cleanup() {
  5164.          modes.reset();
  5165. @@ -214,7 +268,7 @@ var Modes = Module("modes", {
  5166.  
  5167.          let val = this._modeMap[this._main].display();
  5168.          if (val)
  5169. -            return "-- " + val + " --" + macromode;;
  5170. +            return "-- " + val + " --" + macromode;
  5171.          return macromode;
  5172.      },
  5173.  
  5174. @@ -245,7 +299,7 @@ var Modes = Module("modes", {
  5175.          if (!mode.extended)
  5176.              this._mainModes.push(mode);
  5177.  
  5178. -        dactyl.triggerObserver("mode-add", mode);
  5179. +        dactyl.triggerObserver("modes.add", mode);
  5180.      },
  5181.  
  5182.      dumpStack: function dumpStack() {
  5183. @@ -270,9 +324,13 @@ var Modes = Module("modes", {
  5184.  
  5185.      // show the current mode string in the command line
  5186.      show: function show() {
  5187. +        if (!loaded.modes)
  5188. +            return;
  5189. +
  5190.          let msg = null;
  5191. -        if (options.get("showmode").getKey(this.main.name, true))
  5192. +        if (options.get("showmode").getKey(this.main.allBases, false))
  5193.              msg = this._getModeMessage();
  5194. +
  5195.          if (msg || loaded.commandline)
  5196.              commandline.widgets.mode = msg || null;
  5197.      },
  5198. @@ -354,6 +412,7 @@ var Modes = Module("modes", {
  5199.              prev = stack && stack.pop || this.topOfStack;
  5200.              if (push)
  5201.                  this._modeStack.push(push);
  5202. +        });
  5203.  
  5204.              if (stack && stack.pop)
  5205.                  for (let { obj, prop, value, test } in values(this.topOfStack.saved))
  5206. @@ -361,16 +420,15 @@ var Modes = Module("modes", {
  5207.                          dactyl.trapErrors(function () { obj[prop] = value });
  5208.  
  5209.              this.show();
  5210. -        });
  5211. -
  5212. -        delayed.forEach(function ([fn, self]) dactyl.trapErrors(fn, self));
  5213.  
  5214.          if (this.topOfStack.params.enter && prev)
  5215.              dactyl.trapErrors("enter", this.topOfStack.params,
  5216.                                push ? { push: push } : stack || {},
  5217.                                prev);
  5218.  
  5219. -        dactyl.triggerObserver("modeChange", [oldMain, oldExtended], [this._main, this._extended], stack);
  5220. +        delayed.forEach(function ([fn, self]) dactyl.trapErrors(fn, self));
  5221. +
  5222. +        dactyl.triggerObserver("modes.change", [oldMain, oldExtended], [this._main, this._extended], stack);
  5223.          this.show();
  5224.      },
  5225.  
  5226. @@ -389,19 +447,22 @@ var Modes = Module("modes", {
  5227.          while (this._modeStack.length > 1 && this.main != mode) {
  5228.              let a = this._modeStack.pop();
  5229.              this.set(this.topOfStack.main, this.topOfStack.extended, this.topOfStack.params,
  5230. -                     update({ pop: a }, args || {}));
  5231. +                     update({ pop: a },
  5232. +                            args || {}));
  5233.  
  5234.              if (mode == null)
  5235.                  return;
  5236.          }
  5237.      },
  5238.  
  5239. -    replace: function replace(mode, oldMode) {
  5240. +    replace: function replace(mode, oldMode, args) {
  5241.          while (oldMode && this._modeStack.length > 1 && this.main != oldMode)
  5242.              this.pop();
  5243.  
  5244.          if (this._modeStack.length > 1)
  5245. -            this.set(mode, null, null, { push: this.topOfStack, pop: this._modeStack.pop() });
  5246. +            this.set(mode, null, null,
  5247. +                     update({ push: this.topOfStack, pop: this._modeStack.pop() },
  5248. +                            args || {}));
  5249.          this.push(mode);
  5250.      },
  5251.  
  5252. @@ -428,22 +489,27 @@ var Modes = Module("modes", {
  5253.          init: function init(name, options, params) {
  5254.              if (options.bases)
  5255.                  util.assert(options.bases.every(function (m) m instanceof this, this.constructor),
  5256. -                            "Invalid bases", true);
  5257. +                           _("mode.invalidBases"), true);
  5258.  
  5259. -            update(this, {
  5260. +            this.update({
  5261.                  id: 1 << Modes.Mode._id++,
  5262. +                description: name,
  5263.                  name: name,
  5264.                  params: params || {}
  5265.              }, options);
  5266.          },
  5267.  
  5268. +        description: Messages.Localized(""),
  5269. +
  5270. +        displayName: Class.memoize(function () this.name.split("_").map(util.capitalize).join(" ")),
  5271. +
  5272.          isinstance: function isinstance(obj)
  5273. -            this === obj || this.allBases.indexOf(obj) >= 0 || callable(obj) && this instanceof obj,
  5274. +            this.allBases.indexOf(obj) >= 0 || callable(obj) && this instanceof obj,
  5275.  
  5276.          allBases: Class.memoize(function () {
  5277. -            let seen = {}, res = [], queue = this.bases;
  5278. +            let seen = {}, res = [], queue = [this].concat(this.bases);
  5279.              for (let mode in array.iterValues(queue))
  5280. -                if (!set.add(seen, mode)) {
  5281. +                if (!Set.add(seen, mode)) {
  5282.                      res.push(mode);
  5283.                      queue.push.apply(queue, mode.bases);
  5284.                  }
  5285. @@ -454,8 +520,6 @@ var Modes = Module("modes", {
  5286.  
  5287.          get count() !this.insert,
  5288.  
  5289. -        get description() this._display,
  5290. -
  5291.          _display: Class.memoize(function _display() this.name.replace("_", " ", "g")),
  5292.  
  5293.          display: function display() this._display,
  5294. @@ -470,7 +534,9 @@ var Modes = Module("modes", {
  5295.  
  5296.          ownsFocus: Class.memoize(function ownsFocus() this.bases.length && this.bases.some(function (b) b.ownsFocus)),
  5297.  
  5298. -        get passUnknown() this.input,
  5299. +        passEvent: function passEvent(event) this.input && event.charCode && !(event.ctrlKey || event.altKey || event.metaKey),
  5300. +
  5301. +        passUnknown: Class.memoize(function () options.get("passunknown").getKey(this.name)),
  5302.  
  5303.          get mask() this,
  5304.  
  5305. @@ -521,7 +587,7 @@ var Modes = Module("modes", {
  5306.      mappings: function initMappings() {
  5307.          mappings.add([modes.BASE, modes.NORMAL],
  5308.              ["<Esc>", "<C-[>"],
  5309. -            "Return to NORMAL mode",
  5310. +            "Return to Normal mode",
  5311.              function () { modes.reset(); });
  5312.  
  5313.          mappings.add([modes.INPUT, modes.COMMAND, modes.PASS_THROUGH, modes.QUOTE],
  5314. @@ -529,22 +595,60 @@ var Modes = Module("modes", {
  5315.              "Return to the previous mode",
  5316.              function () { modes.pop(); });
  5317.  
  5318. +        mappings.add([modes.MENU], ["<C-c>"],
  5319. +            "Leave Menu mode",
  5320. +            function () { modes.pop(); });
  5321. +
  5322.          mappings.add([modes.MENU], ["<Esc>"],
  5323.              "Close the current popup",
  5324. -            function () {
  5325. -                modes.pop();
  5326. -                return Events.PASS_THROUGH;
  5327. -            });
  5328. +            function () { return Events.PASS_THROUGH; });
  5329.  
  5330.          mappings.add([modes.MENU], ["<C-[>"],
  5331.              "Close the current popup",
  5332.              function () { events.feedkeys("<Esc>"); });
  5333.      },
  5334.      options: function initOptions() {
  5335. +        let opts = {
  5336. +            completer: function completer(context, extra) {
  5337. +                if (extra.value && context.filter[0] == "!")
  5338. +                    context.advance(1);
  5339. +                return completer.superapply(this, arguments);
  5340. +            },
  5341. +
  5342. +            getKey: function getKey(val, default_) {
  5343. +                if (isArray(val))
  5344. +                    return (array.nth(this.value, function (v) val.some(function (m) m.name === v.mode), 0)
  5345. +                                || { result: default_ }).result;
  5346. +
  5347. +                return Set.has(this.valueMap, val) ? this.valueMap[val] : default_;
  5348. +            },
  5349. +
  5350. +            setter: function (vals) {
  5351. + &