]> git.donarmstrong.com Git - dactyl.git/blobdiff - common/content/hints.js
Import r6976 from upstream hg supporting Firefox up to 25.*
[dactyl.git] / common / content / hints.js
index bcf431ce06175ffa7664bae2304ad5f26d819f64..af09ad4dd499498ef16561971e67bb3a09af3c68 100644 (file)
@@ -1,6 +1,6 @@
 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
-// Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
+// Copyright (c) 2008-2013 Kris Maglione <maglione.k@gmail.com>
 //
 // This work is licensed for reuse under an MIT license. Details are
 // given in the LICENSE.txt file included with this file.
 var HintSession = Class("HintSession", CommandMode, {
     get extendedMode() modes.HINTS,
 
-    init: function init(mode, opts) {
+    init: function init(mode, opts = {}) {
         init.supercall(this);
 
-        opts = opts || {};
-
-        // Hack.
-        if (!opts.window && modes.main == modes.OUTPUT_MULTILINE)
-            opts.window = commandline.widgets.multilineOutput.contentWindow;
+        if (!opts.window)
+            opts.window = modes.getStack(0).params.window;
 
         this.hintMode = hints.modes[mode];
         dactyl.assert(this.hintMode);
@@ -27,7 +24,7 @@ var HintSession = Class("HintSession", CommandMode, {
         this.activeTimeout = null; // needed for hinttimeout > 0
         this.continue = Boolean(opts.continue);
         this.docs = [];
-        this.hintKeys = events.fromString(options["hintkeys"]).map(events.closure.toString);
+        this.hintKeys = DOM.Event.parse(options["hintkeys"]).map(DOM.Event.closure.stringify);
         this.hintNumber = 0;
         this.hintString = opts.filter || "";
         this.pageHints = [];
@@ -35,15 +32,17 @@ var HintSession = Class("HintSession", CommandMode, {
         this.usedTabKey = false;
         this.validHints = []; // store the indices of the "hints" array with valid elements
 
+        mappings.pushCommand();
         this.open();
 
         this.top = opts.window || content;
-        this.top.addEventListener("resize", hints.resizeTimer.closure.tell, true);
-        this.top.addEventListener("dactyl-commandupdate", hints.resizeTimer.closure.tell, false, true);
+        this.top.addEventListener("resize", this.closure._onResize, true);
+        this.top.addEventListener("dactyl-commandupdate", this.closure._onResize, false, true);
 
         this.generate();
 
         this.show();
+        this.magic = true;
 
         if (this.validHints.length == 0) {
             dactyl.beep();
@@ -51,7 +50,7 @@ var HintSession = Class("HintSession", CommandMode, {
         }
         else if (this.validHints.length == 1 && !this.continue)
             this.process(false);
-        else // Ticket #185
+        else
             this.checkUnique();
     },
 
@@ -69,6 +68,15 @@ var HintSession = Class("HintSession", CommandMode, {
                 hints.setClass(this.imgSpan, this.valid ? val : null);
         },
 
+        get ambiguous() this.span.hasAttribute("ambiguous"),
+        set ambiguous(val) {
+            let meth = val ? "setAttribute" : "removeAttribute";
+            this.elem[meth]("ambiguous", "true");
+            this.span[meth]("ambiguous", "true");
+            if (this.imgSpan)
+                this.imgSpan[meth]("ambiguous", "true");
+        },
+
         get valid() this._valid,
         set valid(val) {
             this._valid = val,
@@ -76,7 +84,6 @@ var HintSession = Class("HintSession", CommandMode, {
             this.span.style.display = (val ? "" : "none");
             if (this.imgSpan)
                 this.imgSpan.style.display = (val ? "" : "none");
-
             this.active = this.active;
         }
     },
@@ -89,11 +96,13 @@ var HintSession = Class("HintSession", CommandMode, {
         leave.superapply(this, arguments);
 
         if (!stack.push) {
+            mappings.popCommand();
+
             if (hints.hintSession == this)
                 hints.hintSession = null;
             if (this.top) {
-                this.top.removeEventListener("resize", hints.resizeTimer.closure.tell, true);
-                this.top.removeEventListener("dactyl-commandupdate", hints.resizeTimer.closure.tell, true);
+                this.top.removeEventListener("resize", this.closure._onResize, true);
+                this.top.removeEventListener("dactyl-commandupdate", this.closure._onResize, true);
             }
 
             this.removeHints(0);
@@ -155,6 +164,21 @@ var HintSession = Class("HintSession", CommandMode, {
         return res.reverse().join("");
     },
 
+    /**
+     * The reverse of {@link #getHintString}. Given a hint string,
+     * returns its index.
+     *
+     * @param {string} str The hint's string.
+     * @returns {number} The hint's index.
+     */
+    getHintNumber: function getHintNumber(str) {
+        let base = this.hintKeys.length;
+        let res = 0;
+        for (let char in values(str))
+            res = res * base + this.hintKeys.indexOf(char);
+        return res;
+    },
+
     /**
      * Returns true if the given key string represents a
      * pseudo-hint-number.
@@ -231,7 +255,7 @@ var HintSession = Class("HintSession", CommandMode, {
     getContainerOffsets: function _getContainerOffsets(doc) {
         let body = doc.body || doc.documentElement;
         // TODO: getComputedStyle returns null for Facebook channel_iframe doc - probable Gecko bug.
-        let style = util.computedStyle(body);
+        let style = DOM(body).style;
 
         if (style && /^(absolute|fixed|relative)$/.test(style.position)) {
             let rect = body.getClientRects()[0];
@@ -255,6 +279,11 @@ var HintSession = Class("HintSession", CommandMode, {
 
         let doc = win.document;
 
+        memoize(doc, "dactylLabels", () =>
+            iter([l.getAttribute("for"), l]
+                 for (l in array.iterValues(doc.querySelectorAll("label[for]"))))
+             .toObject());
+
         let [offsetX, offsetY] = this.getContainerOffsets(doc);
 
         offsets = offsets || { left: 0, right: 0, top: 0, bottom: 0 };
@@ -263,11 +292,18 @@ var HintSession = Class("HintSession", CommandMode, {
 
         function isVisible(elem) {
             let rect = elem.getBoundingClientRect();
-            if (!rect || !rect.width || !rect.height ||
+            if (!rect ||
                 rect.top > offsets.bottom || rect.bottom < offsets.top ||
                 rect.left > offsets.right || rect.right < offsets.left)
                 return false;
 
+            if (!rect.width || !rect.height)
+                if (!Array.some(elem.childNodes, elem => elem instanceof Element
+                                                      && DOM(elem).style.float != "none"
+                                                      && isVisible(elem)))
+                    if (elem.textContent || !elem.name)
+                        return false;
+
             let computedStyle = doc.defaultView.getComputedStyle(elem, null);
             if (computedStyle.visibility != "visible" || computedStyle.display == "none")
                 return false;
@@ -276,43 +312,49 @@ var HintSession = Class("HintSession", CommandMode, {
 
         let body = doc.body || doc.querySelector("body");
         if (body) {
-            let fragment = util.xmlToDom(<div highlight="hints"/>, doc);
-            body.appendChild(fragment);
-            util.computedStyle(fragment).height; // Force application of binding.
-            let container = doc.getAnonymousElementByAttribute(fragment, "anonid", "hints") || fragment;
+            let fragment = DOM(["div", { highlight: "hints" }], doc).appendTo(body);
+            fragment.style.height; // Force application of binding.
+            let container = doc.getAnonymousElementByAttribute(fragment[0], "anonid", "hints") || fragment[0];
 
-            let baseNodeAbsolute = util.xmlToDom(<span highlight="Hint" style="display: none"/>, doc);
+            let baseNode = DOM(["span", { highlight: "Hint", style: "display: none;" }], doc)[0];
 
             let mode = this.hintMode;
             let res = mode.matcher(doc);
 
             let start = this.pageHints.length;
-            for (let elem in res) {
-                let hint = { elem: elem, showText: false, __proto__: this.Hint };
-
-                if (!isVisible(elem) || mode.filter && !mode.filter(elem))
-                    continue;
+            let _hints = [];
+            for (let elem in res)
+                if (isVisible(elem) && (!mode.filter || mode.filter(elem)))
+                    _hints.push({
+                        elem: elem,
+                        rect: elem.getClientRects()[0] || elem.getBoundingClientRect(),
+                        showText: false,
+                        __proto__: this.Hint
+                    });
+
+            for (let hint in values(_hints)) {
+                let { elem, rect } = hint;
 
                 if (elem.hasAttributeNS(NS, "hint"))
                     [hint.text, hint.showText] = [elem.getAttributeNS(NS, "hint"), true];
-                else if (isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement]))
+                else if (isinstance(elem, [Ci.nsIDOMHTMLInputElement,
+                                           Ci.nsIDOMHTMLSelectElement,
+                                           Ci.nsIDOMHTMLTextAreaElement]))
                     [hint.text, hint.showText] = hints.getInputHint(elem, doc);
-                else if (elem.firstElementChild instanceof HTMLImageElement && /^\s*$/.test(elem.textContent))
+                else if (elem.firstElementChild instanceof Ci.nsIDOMHTMLImageElement && /^\s*$/.test(elem.textContent))
                     [hint.text, hint.showText] = [elem.firstElementChild.alt || elem.firstElementChild.title, true];
                 else
                     hint.text = elem.textContent.toLowerCase();
 
-                hint.span = baseNodeAbsolute.cloneNode(true);
+                hint.span = baseNode.cloneNode(false);
 
-                let rect = elem.getClientRects()[0] || elem.getBoundingClientRect();
                 let leftPos = Math.max((rect.left + offsetX), offsetX);
                 let topPos  = Math.max((rect.top + offsetY), offsetY);
 
-                if (elem instanceof HTMLAreaElement)
+                if (elem instanceof Ci.nsIDOMHTMLAreaElement)
                     [leftPos, topPos] = this.getAreaOffset(elem, leftPos, topPos);
 
-                hint.span.style.left = leftPos + "px";
-                hint.span.style.top =  topPos + "px";
+                hint.span.setAttribute("style", ["display: none; left:", leftPos, "px; top:", topPos, "px"].join(""));
                 container.appendChild(hint.span);
 
                 this.pageHints.push(hint);
@@ -358,13 +400,13 @@ var HintSession = Class("HintSession", CommandMode, {
     },
 
     /**
-     * Handle a hint mode event.
+     * Handle a hints mode event.
      *
      * @param {Event} event The event to handle.
      */
     onKeyPress: function onKeyPress(eventList) {
         const KILL = false, PASS = true;
-        let key = events.toString(eventList[0]);
+        let key = DOM.Event.stringify(eventList[0]);
 
         this.clearTimeout();
 
@@ -399,12 +441,17 @@ var HintSession = Class("HintSession", CommandMode, {
         return PASS;
     },
 
-    onResize: function () {
+    onResize: function onResize() {
         this.removeHints(0);
         this.generate(this.top);
         this.show();
     },
 
+    _onResize: function _onResize() {
+        if (this.magic)
+            hints.resizeTimer.tell();
+    },
+
     /**
      * Finish hinting.
      *
@@ -420,19 +467,13 @@ var HintSession = Class("HintSession", CommandMode, {
 
         // This "followhints" option is *too* confusing. For me, and
         // presumably for users, too. --Kris
-        if (options["followhints"] > 0) {
-            if (!followFirst)
-                return; // no return hit; don't examine uniqueness
-
-            // OK. return hit. But there's more than one hint, and
-            // there's no tab-selected current link. Do not follow in mode 2
-            dactyl.assert(options["followhints"] != 2 || this.validHints.length == 1 || this.hintNumber);
-        }
+        if (options["followhints"] > 0 && !followFirst)
+            return; // no return hit; don't examine uniqueness
 
         if (!followFirst) {
             let firstHref = this.validHints[0].elem.getAttribute("href") || null;
             if (firstHref) {
-                if (this.validHints.some(function (h) h.elem.getAttribute("href") != firstHref))
+                if (this.validHints.some(h => h.elem.getAttribute("href") != firstHref))
                     return;
             }
             else if (this.validHints.length > 1)
@@ -451,31 +492,38 @@ var HintSession = Class("HintSession", CommandMode, {
 
         let n = 5;
         (function next() {
-            let hinted = n || this.validHints.some(function (h) h.elem === elem);
+            if (Cu.isDeadWrapper && Cu.isDeadWrapper(elem))
+                // Hint document has been unloaded.
+                return;
+
+            let hinted = n || this.validHints.some(h => h.elem === elem);
             if (!hinted)
                 hints.setClass(elem, null);
             else if (n)
                 hints.setClass(elem, n % 2);
             else
-                hints.setClass(elem, this.validHints[Math.max(0, this.hintNumber-1)].elem === elem);
+                hints.setClass(elem, this.validHints[Math.max(0, this.hintNumber - 1)].elem === elem);
 
             if (n--)
                 this.timeout(next, 50);
         }).call(this);
 
+        mappings.pushCommand();
         if (!this.continue) {
             modes.pop();
             if (timeout)
                 modes.push(modes.IGNORE, modes.HINTS);
         }
 
+        dactyl.trapErrors("action", this.hintMode,
+                          elem, elem.href || elem.src || "",
+                          this.extendedhintCount, top);
+        mappings.popCommand();
+
         this.timeout(function () {
-            if ((modes.extended & modes.HINTS) && !this.continue)
+            if (modes.main == modes.IGNORE && !this.continue)
                 modes.pop();
             commandline.lastEcho = null; // Hack.
-            dactyl.trapErrors("action", this.hintMode,
-                              elem, elem.href || elem.src || "",
-                              this.extendedhintCount, top);
             if (this.continue && this.top)
                 this.show();
         }, timeout);
@@ -491,10 +539,16 @@ var HintSession = Class("HintSession", CommandMode, {
      */
     removeHints: function _removeHints(timeout) {
         for (let { doc, start, end } in values(this.docs)) {
-            for (let elem in util.evaluateXPath("//*[@dactyl:highlight='hints']", doc))
+            DOM(doc.documentElement).highlight.remove("Hinting");
+            // Goddamn stupid fucking Gecko 1.x security manager bullshit.
+            try { delete doc.dactylLabels; } catch (e) { doc.dactylLabels = undefined; }
+
+            for (let elem in DOM.XPath("//*[@dactyl:highlight='hints']", doc))
                 elem.parentNode.removeChild(elem);
-            for (let i in util.range(start, end + 1))
+            for (let i in util.range(start, end + 1)) {
+                this.pageHints[i].ambiguous = false;
                 this.pageHints[i].valid = false;
+            }
         }
         styles.system.remove("hint-positions");
 
@@ -521,59 +575,71 @@ var HintSession = Class("HintSession", CommandMode, {
     /**
      * Display the hints in pageHints that are still valid.
      */
+    showCount: 0,
     show: function _show() {
+        let count = ++this.showCount;
         let hintnum = 1;
         let validHint = hints.hintMatcher(this.hintString.toLowerCase());
         let activeHint = this.hintNumber || 1;
         this.validHints = [];
 
         for (let { doc, start, end } in values(this.docs)) {
+            DOM(doc.documentElement).highlight.add("Hinting");
             let [offsetX, offsetY] = this.getContainerOffsets(doc);
 
         inner:
             for (let i in (util.interruptibleRange(start, end + 1, 500))) {
+                if (this.showCount != count)
+                    return;
+
                 let hint = this.pageHints[i];
 
                 hint.valid = validHint(hint.text);
                 if (!hint.valid)
                     continue inner;
 
-                if (hint.text == "" && hint.elem.firstChild && hint.elem.firstChild instanceof HTMLImageElement) {
+                if (hint.text == "" && hint.elem.firstChild && hint.elem.firstChild instanceof Ci.nsIDOMHTMLImageElement) {
                     if (!hint.imgSpan) {
                         let rect = hint.elem.firstChild.getBoundingClientRect();
                         if (!rect)
                             continue;
 
-                        hint.imgSpan = util.xmlToDom(<span highlight="Hint" dactyl:hl="HintImage" xmlns:dactyl={NS}/>, doc);
-                        hint.imgSpan.style.display = "none";
-                        hint.imgSpan.style.left = (rect.left + offsetX) + "px";
-                        hint.imgSpan.style.top = (rect.top + offsetY) + "px";
-                        hint.imgSpan.style.width = (rect.right - rect.left) + "px";
-                        hint.imgSpan.style.height = (rect.bottom - rect.top) + "px";
-                        hint.span.parentNode.appendChild(hint.imgSpan);
+                        hint.imgSpan = DOM(["span", { highlight: "Hint", "dactyl:hl": "HintImage" }], doc).css({
+                            display: "none",
+                            left: (rect.left + offsetX) + "px",
+                            top: (rect.top + offsetY) + "px",
+                            width: (rect.right - rect.left) + "px",
+                            height: (rect.bottom - rect.top) + "px"
+                        }).appendTo(hint.span.parentNode)[0];
                     }
                 }
 
                 let str = this.getHintString(hintnum);
                 let text = [];
-                if (hint.elem instanceof HTMLInputElement)
+                if (hint.elem instanceof Ci.nsIDOMHTMLInputElement)
                     if (hint.elem.type === "radio")
                         text.push(UTF8(hint.elem.checked ? "⊙" : "○"));
                     else if (hint.elem.type === "checkbox")
                         text.push(UTF8(hint.elem.checked ? "☑" : "☐"));
-                if (hint.showText)
+                if (hint.showText && !/^\s*$/.test(hint.text))
                     text.push(hint.text.substr(0, 50));
 
                 hint.span.setAttribute("text", str + (text.length ? ": " + text.join(" ") : ""));
                 hint.span.setAttribute("number", str);
                 if (hint.imgSpan)
                     hint.imgSpan.setAttribute("number", str);
+
                 hint.active = activeHint == hintnum;
+
                 this.validHints.push(hint);
                 hintnum++;
             }
         }
 
+        let base = this.hintKeys.length;
+        for (let [i, hint] in Iterator(this.validHints))
+            hint.ambiguous = (i + 1) * base <= this.validHints.length;
+
         if (options["usermode"]) {
             let css = [];
             for (let hint in values(this.pageHints)) {
@@ -668,7 +734,7 @@ var HintSession = Class("HintSession", CommandMode, {
      * Display the current status to the user.
      */
     updateStatusline: function _updateStatusline() {
-        statusline.inputBuffer = (this.escapeNumbers ? options["mapleader"] : "") +
+        statusline.inputBuffer = (this.escapeNumbers ? "\\" : "") +
                                  (this.hintNumber ? this.getHintString(this.hintNumber) : "");
     },
 });
@@ -685,55 +751,75 @@ var Hints = Module("hints", {
             events.listen(appContent, "scroll", this.resizeTimer.closure.tell, false);
 
         const Mode = Hints.Mode;
-        Mode.defaultValue("tags", function () function () options.get("hinttags").matcher);
         Mode.prototype.__defineGetter__("matcher", function ()
-            options.get("extendedhinttags").getKey(this.name, this.tags()));
+            options.get("extendedhinttags").getKey(this.name, options.get("hinttags").matcher));
 
         this.modes = {};
         this.addMode(";", "Focus hint",                           buffer.closure.focusElement);
-        this.addMode("?", "Show information for hint",            function (elem) buffer.showElementInfo(elem));
-        this.addMode("s", "Save hint",                            function (elem) buffer.saveLink(elem, false));
-        this.addMode("f", "Focus frame",                          function (elem) dactyl.focus(elem.ownerDocument.defaultView));
-        this.addMode("F", "Focus frame or pseudo-frame",          buffer.closure.focusElement, null, isScrollable);
-        this.addMode("o", "Follow hint",                          function (elem) buffer.followLink(elem, dactyl.CURRENT_TAB));
-        this.addMode("t", "Follow hint in a new tab",             function (elem) buffer.followLink(elem, dactyl.NEW_TAB));
-        this.addMode("b", "Follow hint in a background tab",      function (elem) buffer.followLink(elem, dactyl.NEW_BACKGROUND_TAB));
-        this.addMode("w", "Follow hint in a new window",          function (elem) buffer.followLink(elem, dactyl.NEW_WINDOW));
-        this.addMode("O", "Generate an ‘:open URL’ prompt",       function (elem, loc) CommandExMode().open("open " + loc));
-        this.addMode("T", "Generate a ‘:tabopen URL’ prompt",     function (elem, loc) CommandExMode().open("tabopen " + loc));
-        this.addMode("W", "Generate a ‘:winopen URL’ prompt",     function (elem, loc) CommandExMode().open("winopen " + loc));
-        this.addMode("a", "Add a bookmark",                       function (elem) bookmarks.addSearchKeyword(elem));
-        this.addMode("S", "Add a search keyword",                 function (elem) bookmarks.addSearchKeyword(elem));
-        this.addMode("v", "View hint source",                     function (elem, loc) buffer.viewSource(loc, false));
-        this.addMode("V", "View hint source in external editor",  function (elem, loc) buffer.viewSource(loc, true));
-        this.addMode("y", "Yank hint location",                   function (elem, loc) dactyl.clipboardWrite(loc, true));
-        this.addMode("Y", "Yank hint description",                function (elem) dactyl.clipboardWrite(elem.textContent || "", true));
-        this.addMode("c", "Open context menu",                    function (elem) buffer.openContextMenu(elem));
-        this.addMode("i", "Show image",                           function (elem) dactyl.open(elem.src));
-        this.addMode("I", "Show image in a new tab",              function (elem) dactyl.open(elem.src, dactyl.NEW_TAB));
-
-        function isScrollable(elem) isinstance(elem, [HTMLFrameElement, HTMLIFrameElement]) ||
+        this.addMode("?", "Show information for hint",            elem => buffer.showElementInfo(elem));
+        // TODO: allow for ! override to overwrite existing paths -- where? --djk
+        this.addMode("s", "Save hint",                            elem => buffer.saveLink(elem, false));
+        this.addMode("f", "Focus frame",                          elem => dactyl.focus(elem.ownerDocument.defaultView));
+        this.addMode("F", "Focus frame or pseudo-frame",          buffer.closure.focusElement, isScrollable);
+        this.addMode("o", "Follow hint",                          elem => buffer.followLink(elem, dactyl.CURRENT_TAB));
+        this.addMode("t", "Follow hint in a new tab",             elem => buffer.followLink(elem, dactyl.NEW_TAB));
+        this.addMode("b", "Follow hint in a background tab",      elem => buffer.followLink(elem, dactyl.NEW_BACKGROUND_TAB));
+        this.addMode("w", "Follow hint in a new window",          elem => buffer.followLink(elem, dactyl.NEW_WINDOW));
+        this.addMode("O", "Generate an ‘:open URL’ prompt",       (elem, loc) => CommandExMode().open("open " + loc));
+        this.addMode("T", "Generate a ‘:tabopen URL’ prompt",     (elem, loc) => CommandExMode().open("tabopen " + loc));
+        this.addMode("W", "Generate a ‘:winopen URL’ prompt",     (elem, loc) => CommandExMode().open("winopen " + loc));
+        this.addMode("a", "Add a bookmark",                       elem => bookmarks.addSearchKeyword(elem));
+        this.addMode("S", "Add a search keyword",                 elem => bookmarks.addSearchKeyword(elem));
+        this.addMode("v", "View hint source",                     (elem, loc) => buffer.viewSource(loc, false));
+        this.addMode("V", "View hint source in external editor",  (elem, loc) => buffer.viewSource(loc, true));
+        this.addMode("y", "Yank hint location",                   (elem, loc) => editor.setRegister(null, loc, true));
+        this.addMode("Y", "Yank hint description",                elem => editor.setRegister(null, elem.textContent || "", true));
+        this.addMode("A", "Yank hint anchor url",                 function (elem) {
+            let uri = elem.ownerDocument.documentURIObject.clone();
+            uri.ref = elem.id || elem.name;
+            dactyl.clipboardWrite(uri.spec, true);
+        });
+        this.addMode("c", "Open context menu",                    elem => DOM(elem).contextmenu());
+        this.addMode("i", "Show image",                           elem => dactyl.open(elem.src));
+        this.addMode("I", "Show image in a new tab",              elem => dactyl.open(elem.src, dactyl.NEW_TAB));
+
+        function isScrollable(elem) isinstance(elem, [Ci.nsIDOMHTMLFrameElement,
+                                                      Ci.nsIDOMHTMLIFrameElement]) ||
             Buffer.isScrollable(elem, 0, true) || Buffer.isScrollable(elem, 0, false);
     },
 
     hintSession: Modes.boundProperty(),
 
     /**
-     * Creates a new hint mode.
+     * Creates a new hints mode.
      *
      * @param {string} mode The letter that identifies this mode.
      * @param {string} prompt The description to display to the user
      *     about this mode.
      * @param {function(Node)} action The function to be called with the
      *     element that matches.
-     * @param {function():string} tags The function that returns an
-     *     XPath expression to decide which elements can be hinted (the
-     *     default returns options["hinttags"]).
-     * @optional
+     * @param {function(Node):boolean} filter A function used to filter
+     *     the returned node set.
+     * @param {[string]} tags A value to add to the default
+     *     'extendedhinttags' value for this mode.
+     *     @optional
      */
-    addMode: function (mode, prompt, action, tags) {
-        arguments[1] = UTF8(prompt);
-        this.modes[mode] = Hints.Mode.apply(Hints.Mode, arguments);
+    addMode: function (mode, prompt, action, filter, tags) {
+        function toString(regexp) RegExp.prototype.toString.call(regexp);
+
+        if (tags != null) {
+            let eht = options.get("extendedhinttags");
+            let update = eht.isDefault;
+
+            let value = eht.parse(Option.quote(util.regexp.escape(mode)) + ":" + tags.map(Option.quote))[0];
+            eht.defaultValue = eht.defaultValue.filter(re => toString(re) != toString(value))
+                                  .concat(value);
+
+            if (update)
+                eht.reset();
+        }
+
+        this.modes[mode] = Hints.Mode(mode, UTF8(prompt), action, filter);
     },
 
     /**
@@ -762,12 +848,12 @@ var Hints = Module("hints", {
 
         let type = elem.type;
 
-        if (elem instanceof HTMLInputElement && set.has(util.editableInputs, elem.type))
+        if (DOM(elem).isInput)
             return [elem.value, false];
         else {
             for (let [, option] in Iterator(options["hintinputs"])) {
                 if (option == "value") {
-                    if (elem instanceof HTMLSelectElement) {
+                    if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
                         if (elem.selectedIndex >= 0)
                             return [elem.item(elem.selectedIndex).text.toLowerCase(), false];
                     }
@@ -776,15 +862,15 @@ var Hints = Module("hints", {
                             return [elem.alt.toLowerCase(), true];
                     }
                     else if (elem.value && type != "password") {
-                        // radio's and checkboxes often use internal ids as values - maybe make this an option too...
+                        // radios and checkboxes often use internal ids as values - maybe make this an option too...
                         if (! ((type == "radio" || type == "checkbox") && !isNaN(elem.value)))
                             return [elem.value.toLowerCase(), (type == "radio" || type == "checkbox")];
                     }
                 }
                 else if (option == "label") {
                     if (elem.id) {
-                        // TODO: (possibly) do some guess work for label-like objects
-                        let label = util.evaluateXPath(["label[@for=" + elem.id.quote() + "]"], doc).snapshotItem(0);
+                        let label = (elem.ownerDocument.dactylLabels || {})[elem.id];
+                        // Urgh.
                         if (label)
                             return [label.textContent.toLowerCase(), true];
                     }
@@ -820,7 +906,7 @@ var Hints = Module("hints", {
          * returns true if each set of characters typed can be found, in any
          * order, in the link.
          *
-         * @param {string} hintString  The string typed by the user.
+         * @param {string} hintString The string typed by the user.
          * @returns {function(String):boolean} A function that takes the text
          *     of a hint and returns true if all the (space-delimited) sets of
          *     characters typed by the user can be found in it.
@@ -829,7 +915,7 @@ var Hints = Module("hints", {
             let tokens = tokenize(/\s+/, hintString);
             return function (linkText) {
                 linkText = linkText.toLowerCase();
-                return tokens.every(function (token) indexOf(linkText, token) >= 0);
+                return tokens.every(token => indexOf(linkText, token) >= 0);
             };
         } //}}}
 
@@ -956,7 +1042,7 @@ var Hints = Module("hints", {
 
         let indexOf = String.indexOf;
         if (options.get("hintmatching").has("transliterated"))
-            indexOf = Hints.indexOf;
+            indexOf = Hints.closure.indexOf;
 
         switch (options["hintmatching"][0]) {
         case "contains"      : return containsMatcher(hintString);
@@ -968,21 +1054,29 @@ var Hints = Module("hints", {
         return null;
     }, //}}}
 
-    open: function open(mode, opts) {
+    open: function open(mode, opts = {}) {
         this._extendedhintCount = opts.count;
-        commandline.input(["Normal", mode], "", {
+
+        mappings.pushCommand();
+        commandline.input(["Normal", mode], null, {
+            autocomplete: false,
             completer: function (context) {
-                context.compare = function () 0;
+                context.compare = () => 0;
                 context.completions = [[k, v.prompt] for ([k, v] in Iterator(hints.modes))];
             },
+            onCancel: mappings.closure.popCommand,
             onSubmit: function (arg) {
                 if (arg)
                     hints.show(arg, opts);
+                mappings.popCommand();
             },
-            onChange: function () {
+            onChange: function (arg) {
+                if (Object.keys(hints.modes).some(m => m != arg && m.indexOf(arg) == 0))
+                    return;
+
                 this.accepted = true;
                 modes.pop();
-            },
+            }
         });
     },
 
@@ -1012,7 +1106,26 @@ var Hints = Module("hints", {
         this.hintSession = HintSession(mode, opts);
     }
 }, {
-    translitTable: Class.memoize(function () {
+    isVisible: function isVisible(elem, offScreen) {
+        let rect = elem.getBoundingClientRect();
+        if (!rect.width || !rect.height)
+            if (!Array.some(elem.childNodes, elem => elem instanceof Element
+                                                  && DOM(elem).style.float != "none"
+                                                  && isVisible(elem)))
+                return false;
+
+        let win = elem.ownerDocument.defaultView;
+        if (offScreen && (rect.top + win.scrollY < 0 || rect.left + win.scrollX < 0 ||
+                          rect.bottom + win.scrollY > win.scrolMaxY + win.innerHeight ||
+                          rect.right + win.scrollX > win.scrolMaxX + win.innerWidth))
+            return false;
+
+        if (!DOM(elem).isVisible)
+            return false;
+        return true;
+    },
+
+    translitTable: Class.Memoize(function () {
         const table = {};
         [
             [0x00c0, 0x00c6, ["A"]], [0x00c7, 0x00c7, ["C"]],
@@ -1078,7 +1191,7 @@ var Hints = Module("hints", {
             [0x24d0, 0x24e9, "a"],
             [0xfb00, 0xfb06, ["ff", "fi", "fl", "ffi", "ffl", "st", "st"]],
             [0xff21, 0xff3a, "A"], [0xff41, 0xff5a, "a"]
-        ].forEach(function (start, stop, val) {
+        ].forEach(function ([start, stop, val]) {
             if (typeof val != "string")
                 for (let i = start; i <= stop; i++)
                     table[String.fromCharCode(i)] = val[(i - start) % val.length];
@@ -1096,7 +1209,7 @@ var Hints = Module("hints", {
         if (src.length == 0)
             return 0;
     outer:
-        for (var i = 0; i < end; i++) {
+        for (var i = 0; i <= end; i++) {
                 var j = i;
                 for (var k = 0; k < src.length;) {
                     var s = dest[j++];
@@ -1112,67 +1225,74 @@ var Hints = Module("hints", {
         return -1;
     },
 
-    Mode: Struct("HintMode", "name", "prompt", "action", "tags", "filter")
+    Mode: Struct("HintMode", "name", "prompt", "action", "filter")
             .localize("prompt")
 }, {
     modes: function initModes() {
         initModes.require("commandline");
         modes.addMode("HINTS", {
             extended: true,
-            description: "Active when selecting elements in QuickHint or ExtendedHint mode",
+            description: "Active when selecting elements with hints",
             bases: [modes.COMMAND_LINE],
             input: true,
             ownsBuffer: true
         });
     },
     mappings: function () {
-        var myModes = config.browserModes.concat(modes.OUTPUT_MULTILINE);
-        mappings.add(myModes, ["f"],
-            "Start QuickHint mode",
+        let bind = function bind(names, description, action, params)
+            mappings.add(config.browserModes, names, description,
+                         action, params);
+
+        bind(["f"],
+            "Start Hints mode",
             function () { hints.show("o"); });
 
-        mappings.add(myModes, ["F"],
-            "Start QuickHint mode, but open link in a new tab",
+        bind(["F"],
+            "Start Hints mode, but open link in a new tab",
             function () { hints.show(options.get("activate").has("links") ? "t" : "b"); });
 
-        mappings.add(myModes, [";"],
-            "Start an extended hint mode",
+        bind([";"],
+            "Start an extended hints mode",
             function ({ count }) { hints.open(";", { count: count }); },
             { count: true });
 
-        mappings.add(myModes, ["g;"],
-            "Start an extended hint mode and stay there until <Esc> is pressed",
+        bind(["g;"],
+            "Start an extended hints mode and stay there until <Esc> is pressed",
             function ({ count }) { hints.open("g;", { continue: true, count: count }); },
             { count: true });
 
-        mappings.add(modes.HINTS, ["<Return>"],
+        let bind = function bind(names, description, action, params)
+            mappings.add([modes.HINTS], names, description,
+                         action, params);
+
+        bind(["<Return>"],
             "Follow the selected hint",
             function ({ self }) { self.update(true); });
 
-        mappings.add(modes.HINTS, ["<Tab>"],
+        bind(["<Tab>"],
             "Focus the next matching hint",
             function ({ self }) { self.tab(false); });
 
-        mappings.add(modes.HINTS, ["<S-Tab>"],
+        bind(["<S-Tab>"],
             "Focus the previous matching hint",
             function ({ self }) { self.tab(true); });
 
-        mappings.add(modes.HINTS, ["<BS>", "<C-h>"],
+        bind(["<BS>", "<C-h>"],
             "Delete the previous character",
             function ({ self }) self.backspace());
 
-        mappings.add(modes.HINTS, ["<Leader>"],
+        bind(["\\"],
             "Toggle hint filtering",
             function ({ self }) { self.escapeNumbers = !self.escapeNumbers; });
     },
     options: function () {
-        function xpath(arg) util.makeXPath(arg);
-
         options.add(["extendedhinttags", "eht"],
             "XPath or CSS selector strings of hintable elements for extended hint modes",
             "regexpmap", {
+                // Make sure to update the docs when you change this.
                 "[iI]": "img",
-                "[asOTivVWy]": ["a[href]", "area[href]", "img[src]", "iframe[src]"],
+                "[asOTvVWy]": [":-moz-any-link", "area[href]", "img[src]", "iframe[src]"],
+                "[A]": ["[id]", "a[name]"],
                 "[f]": "body",
                 "[F]": ["body", "code", "div", "html", "p", "pre", "span"],
                 "[S]": ["input:not([type=hidden])", "textarea", "button", "select"]
@@ -1180,27 +1300,30 @@ var Hints = Module("hints", {
             {
                 keepQuotes: true,
                 getKey: function (val, default_)
-                    let (res = array.nth(this.value, function (re) re.test(val), 0))
+                    let (res = array.nth(this.value, re => let (match = re.exec(val)) match && match[0] == val, 0))
                         res ? res.matcher : default_,
-                setter: function (vals) {
+                parse: function parse(val) {
+                    let vals = parse.supercall(this, val);
                     for (let value in values(vals))
-                        value.matcher = util.compileMatcher(Option.splitList(value.result));
+                        value.matcher = DOM.compileMatcher(Option.splitList(value.result));
                     return vals;
                 },
-                validator: util.validateMatcher
+                testValues: function testValues(vals, validator) vals.every(re => Option.splitList(re).every(validator)),
+                validator: DOM.validateMatcher
             });
 
         options.add(["hinttags", "ht"],
-            "XPath string of hintable elements activated by 'f' and 'F'",
-            "stringlist", "input:not([type=hidden]),a,area,iframe,textarea,button,select," +
+            "XPath or CSS selector strings of hintable elements for Hints mode",
+            // Make sure to update the docs when you change this.
+            "stringlist", ":-moz-any-link,area,button,iframe,input:not([type=hidden]),label[for],select,textarea," +
                           "[onclick],[onmouseover],[onmousedown],[onmouseup],[oncommand]," +
-                          "[tabindex],[role=link],[role=button]",
+                          "[tabindex],[role=link],[role=button],[contenteditable=true]",
             {
                 setter: function (values) {
-                    this.matcher = util.compileMatcher(values);
+                    this.matcher = DOM.compileMatcher(values);
                     return values;
                 },
-                validator: util.validateMatcher
+                validator: DOM.validateMatcher
             });
 
         options.add(["hintkeys", "hk"],
@@ -1212,9 +1335,9 @@ var Hints = Module("hints", {
                     "asdfg;lkjh": "Home Row"
                 },
                 validator: function (value) {
-                    let values = events.fromString(value).map(events.closure.toString);
-                    return Option.validIf(array.uniq(values).length === values.length,
-                                            "Duplicate keys not allowed");
+                    let values = DOM.Event.parse(value).map(DOM.Event.closure.stringify);
+                    return Option.validIf(array.uniq(values).length === values.length && values.length > 1,
+                                          _("option.hintkeys.duplicate"));
                 }
             });
 
@@ -1224,15 +1347,12 @@ var Hints = Module("hints", {
             { validator: function (value) value >= 0 });
 
         options.add(["followhints", "fh"],
-            // FIXME: this description isn't very clear but I can't think of a
-            // better one right now.
-            "Change the behavior of <Return> in hint mode",
+            "Define the conditions under which selected hints are followed",
             "number", 0,
             {
                 values: {
                     "0": "Follow the first hint as soon as typed text uniquely identifies it. Follow the selected hint on <Return>.",
-                    "1": "Follow the selected hint on <Return>.",
-                    "2": "Follow the selected hint on <Return> only it's been <Tab>-selected."
+                    "1": "Follow the selected hint on <Return>."
                 }
             });
 
@@ -1248,7 +1368,8 @@ var Hints = Module("hints", {
                     "transliterated": UTF8("When true, special latin characters are translated to their ASCII equivalents (e.g., é ⇒ e)")
                 },
                 validator: function (values) Option.validateCompleter.call(this, values) &&
-                    1 === values.reduce(function (acc, v) acc + (["contains", "custom", "firstletters", "wordstartswith"].indexOf(v) >= 0), 0)
+                    1 === values.reduce((acc, v) => acc + (["contains", "custom", "firstletters", "wordstartswith"].indexOf(v) >= 0),
+                                        0)
             });
 
         options.add(["wordseparators", "wsp"],
@@ -1269,4 +1390,4 @@ var Hints = Module("hints", {
     }
 });
 
-// vim: set fdm=marker sw=4 ts=4 et:
+// vim: set fdm=marker sw=4 sts=4 ts=8 et: