]> git.donarmstrong.com Git - dactyl.git/blobdiff - common/content/buffer.js
Import 1.0b7.1 supporting Firefox up to 8.*
[dactyl.git] / common / content / buffer.js
index af099e747e8d58bde657158747088141187b5654..d0c732cd10fa9983cda1940b77eb110646841c14 100644 (file)
@@ -19,6 +19,24 @@ var Buffer = Module("buffer", {
         this.evaluateXPath = util.evaluateXPath;
         this.pageInfo = {};
 
+        this.addPageInfoSection("e", "Search Engines", function (verbose) {
+
+            let n = 1;
+            let nEngines = 0;
+            for (let { document: doc } in values(buffer.allFrames())) {
+                let engines = util.evaluateXPath(["link[@href and @rel='search' and @type='application/opensearchdescription+xml']"], doc);
+                nEngines += engines.snapshotLength;
+
+                if (verbose)
+                    for (let link in engines)
+                        yield [link.title || /*L*/ "Engine " + n++,
+                               <a xmlns={XHTML} href={link.href} onclick="if (event.button == 0) { window.external.AddSearchProvider(this.href); return false; }" highlight="URL">{link.href}</a>];
+            }
+
+            if (!verbose && nEngines)
+                yield nEngines + /*L*/" engine" + (nEngines > 1 ? "s" : "");
+        });
+
         this.addPageInfoSection("f", "Feeds", function (verbose) {
             const feedTypes = {
                 "application/rss+xml": "RSS",
@@ -75,7 +93,7 @@ var Buffer = Module("buffer", {
             }
 
             if (!verbose && nFeed)
-                yield nFeed + " feed" + (nFeed > 1 ? "s" : "");
+                yield nFeed + /*L*/" feed" + (nFeed > 1 ? "s" : "");
         });
 
         this.addPageInfoSection("g", "General Info", function (verbose) {
@@ -110,7 +128,7 @@ var Buffer = Module("buffer", {
 
             if (!verbose) {
                 if (pageSize[0])
-                    yield (pageSize[1] || pageSize[0]) + " bytes";
+                    yield (pageSize[1] || pageSize[0]) + /*L*/" bytes";
                 yield lastMod;
                 return;
             }
@@ -134,6 +152,9 @@ var Buffer = Module("buffer", {
         });
 
         this.addPageInfoSection("m", "Meta Tags", function (verbose) {
+            if (!verbose)
+                return [];
+
             // get meta tag data, sort and put into pageMeta[]
             let metaNodes = buffer.focusedFrame.document.getElementsByTagName("meta");
 
@@ -141,9 +162,48 @@ var Buffer = Module("buffer", {
                         .sort(function (a, b) util.compareIgnoreCase(a[0], b[0]));
         });
 
+        let identity = window.gIdentityHandler;
+        this.addPageInfoSection("s", "Security", function (verbose) {
+            if (!verbose || !identity)
+                return; // For now
+
+            // Modified from Firefox
+            function location(data) array.compact([
+                data.city, data.state, data.country
+            ]).join(", ");
+
+            switch (statusline.security) {
+            case "secure":
+            case "extended":
+                var data = identity.getIdentityData();
+
+                yield ["Host", identity.getEffectiveHost()];
+
+                if (statusline.security === "extended")
+                    yield ["Owner", data.subjectOrg];
+                else
+                    yield ["Owner", _("pageinfo.s.ownerUnverified", data.subjectOrg)];
+
+                if (location(data).length)
+                    yield ["Location", location(data)];
+
+                yield ["Verified by", data.caOrg];
+
+                if (identity._overrideService.hasMatchingOverride(identity._lastLocation.hostname,
+                                                              (identity._lastLocation.port || 443),
+                                                              data.cert, {}, {}))
+                    yield ["User exception", /*L*/"true"];
+                break;
+            }
+        });
+
         dactyl.commands["buffer.viewSource"] = function (event) {
             let elem = event.originalTarget;
-            buffer.viewSource([elem.getAttribute("href"), Number(elem.getAttribute("line"))]);
+            let obj = { url: elem.getAttribute("href"), line: Number(elem.getAttribute("line")) };
+            if (elem.hasAttribute("column"))
+                obj.column = elem.getAttribute("column");
+
+            buffer.viewSource(obj);
         };
     },
 
@@ -311,7 +371,7 @@ var Buffer = Module("buffer", {
     allFrames: function allFrames(win, focusedFirst) {
         let frames = [];
         (function rec(frame) {
-            if (frame.document.body instanceof HTMLBodyElement)
+            if (true || frame.document.body instanceof HTMLBodyElement)
                 frames.push(frame);
             Array.forEach(frame.frames, rec);
         })(win || content);
@@ -340,7 +400,7 @@ var Buffer = Module("buffer", {
      * @returns {string}
      */
     get currentWord() Buffer.currentWord(this.focusedFrame),
-    getCurrentWord: deprecated("buffer.currentWord", function getCurrentWord() this.currentWord),
+    getCurrentWord: deprecated("buffer.currentWord", function getCurrentWord() Buffer.currentWord(this.focusedFrame, true)),
 
     /**
      * Returns true if a scripts are allowed to focus the given input
@@ -352,8 +412,16 @@ var Buffer = Module("buffer", {
     focusAllowed: function focusAllowed(elem) {
         if (elem instanceof Window && !Editor.getEditor(elem))
             return true;
+
         let doc = elem.ownerDocument || elem.document || elem;
-        return !options["strictfocus"] || doc.dactylFocusAllowed;
+        switch (options.get("strictfocus").getKey(doc.documentURIObject || util.newURI(doc.documentURI), "moderate")) {
+        case "despotic":
+            return elem.dactylFocusAllowed || elem.frameElement && elem.frameElement.dactylFocusAllowed;
+        case "moderate":
+            return doc.dactylFocusAllowed || elem.frameElement && elem.frameElement.ownerDocument.dactylFocusAllowed;
+        default:
+            return true;
+        }
     },
 
     /**
@@ -365,6 +433,7 @@ var Buffer = Module("buffer", {
      */
     focusElement: function focusElement(elem) {
         let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem;
+        elem.dactylFocusAllowed = true;
         win.document.dactylFocusAllowed = true;
 
         if (isinstance(elem, [HTMLFrameElement, HTMLIFrameElement]))
@@ -413,7 +482,7 @@ var Buffer = Module("buffer", {
     },
 
     /**
-     * Find the counth last link on a page matching one of the given
+     * Find the *count*th last link on a page matching one of the given
      * regular expressions, or with a @rel or @rev attribute matching
      * the given relation. Each frame is searched beginning with the
      * last link and progressing to the first, once checking for
@@ -483,7 +552,7 @@ var Buffer = Module("buffer", {
      */
     followLink: function followLink(elem, where) {
         let doc = elem.ownerDocument;
-        let view = doc.defaultView;
+        let win = doc.defaultView;
         let { left: offsetX, top: offsetY } = elem.getBoundingClientRect();
 
         if (isinstance(elem, [HTMLFrameElement, HTMLIFrameElement]))
@@ -526,6 +595,8 @@ var Buffer = Module("buffer", {
                     ctrlKey: ctrlKey, shiftKey: shiftKey, metaKey: ctrlKey
                 }));
             });
+            let sel = util.selectionController(win);
+            sel.getSelection(sel.SELECTION_FOCUS_REGION).collapseToStart();
         });
     },
 
@@ -533,9 +604,7 @@ var Buffer = Module("buffer", {
      * @property {nsISelectionController} The current document's selection
      *     controller.
      */
-    get selectionController() config.browser.docShell
-            .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay)
-            .QueryInterface(Ci.nsISelectionController),
+    get selectionController() util.selectionController(this.focusedFrame),
 
     /**
      * Opens the appropriate context menu for *elem*.
@@ -561,7 +630,7 @@ var Buffer = Module("buffer", {
         try {
             window.urlSecurityCheck(uri.spec, doc.nodePrincipal);
 
-            io.CommandFileMode("Save link: ", {
+            io.CommandFileMode(_("buffer.prompt.saveLink") + " ", {
                 onSubmit: function (path) {
                     let file = io.File(path);
                     if (file.exists() && file.isDirectory())
@@ -598,16 +667,16 @@ var Buffer = Module("buffer", {
                              | persist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
 
         let downloadListener = new window.DownloadListener(window,
-                services.Transfer(uri, services.io.newFileURI(file), "",
+                services.Transfer(uri, File(file).URI, "",
                                   null, null, null, persist));
 
         persist.progressListener = update(Object.create(downloadListener), {
-            onStateChange: function onStateChange(progress, request, flag, status) {
-                if (callback && (flag & Ci.nsIWebProgressListener.STATE_STOP) && status == 0)
-                    dactyl.trapErrors(callback, self, uri, file, progress, request, flag, status);
+            onStateChange: util.wrapCallback(function onStateChange(progress, request, flags, status) {
+                if (callback && (flags & Ci.nsIWebProgressListener.STATE_STOP) && status == 0)
+                    dactyl.trapErrors(callback, self, uri, file, progress, request, flags, status);
 
                 return onStateChange.superapply(this, arguments);
-            }
+            })
         });
 
         persist.saveURI(uri, null, null, null, null, file);
@@ -672,7 +741,7 @@ var Buffer = Module("buffer", {
      */
     findScrollable: function findScrollable(dir, horizontal) {
         function find(elem) {
-            while (!(elem instanceof Element) && elem.parentNode)
+            while (elem && !(elem instanceof Element) && elem.parentNode)
                 elem = elem.parentNode;
             for (; elem && elem.parentNode instanceof Element; elem = elem.parentNode)
                 if (Buffer.isScrollable(elem, dir, horizontal))
@@ -702,7 +771,7 @@ var Buffer = Module("buffer", {
                         doc.documentElement);
         }
         let doc = this.focusedFrame.document;
-        return elem || doc.body || doc.documentElement;
+        return dactyl.assert(elem || doc.body || doc.documentElement);
     },
 
     /**
@@ -728,12 +797,43 @@ var Buffer = Module("buffer", {
         return win;
     },
 
+    /**
+     * Finds the next visible element for the node path in 'jumptags'
+     * for *arg*.
+     *
+     * @param {string} arg The element in 'jumptags' to use for the search.
+     * @param {number} count The number of elements to jump.
+     *      @optional
+     * @param {boolean} reverse If true, search backwards. @optional
+     */
+    findJump: function findJump(arg, count, reverse) {
+        const FUDGE = 10;
+
+        let path = options["jumptags"][arg];
+        dactyl.assert(path, _("error.invalidArgument", arg));
+
+        let distance = reverse ? function (rect) -rect.top : function (rect) rect.top;
+        let elems = [[e, distance(e.getBoundingClientRect())] for (e in path.matcher(this.focusedFrame.document))]
+                        .filter(function (e) e[1] > FUDGE)
+                        .sort(function (a, b) a[1] - b[1])
+
+        let idx = Math.min((count || 1) - 1, elems.length);
+        dactyl.assert(idx in elems);
+
+        let elem = elems[idx][0];
+        elem.scrollIntoView(true);
+
+        let sel = elem.ownerDocument.defaultView.getSelection();
+        sel.removeAllRanges();
+        sel.addRange(RangeFind.endpoint(RangeFind.nodeRange(elem), true));
+    },
+
     // TODO: allow callback for filtering out unwanted frames? User defined?
     /**
      * Shifts the focus to another frame within the buffer. Each buffer
      * contains at least one frame.
      *
-     * @param {number} count The number of frames to skip through.  A negative
+     * @param {number} count The number of frames to skip through. A negative
      *     count skips backwards.
      */
     shiftFrameFocus: function shiftFrameFocus(count) {
@@ -785,7 +885,7 @@ var Buffer = Module("buffer", {
      * @param {Node} elem The element to query.
      */
     showElementInfo: function showElementInfo(elem) {
-        dactyl.echo(<>Element:<br/>{util.objectToString(elem, true)}</>, commandline.FORCE_MULTILINE);
+        dactyl.echo(<><!--L-->Element:<br/>{util.objectToString(elem, true)}</>, commandline.FORCE_MULTILINE);
     },
 
     /**
@@ -798,15 +898,15 @@ var Buffer = Module("buffer", {
     showPageInfo: function showPageInfo(verbose, sections) {
         // Ctrl-g single line output
         if (!verbose) {
-            let file = content.location.pathname.split("/").pop() || "[No Name]";
-            let title = content.document.title || "[No Title]";
+            let file = content.location.pathname.split("/").pop() || _("buffer.noName");
+            let title = content.document.title || _("buffer.noTitle");
 
-            let info = template.map("gf",
+            let info = template.map(sections || options["pageinfo"],
                 function (opt) template.map(buffer.pageInfo[opt].action(), util.identity, ", "),
                 ", ");
 
             if (bookmarkcache.isBookmarked(this.URL))
-                info += ", bookmarked";
+                info += ", " + _("buffer.bookmarked");
 
             let pageInfoText = <>{file.quote()} [{info}] {title}</>;
             dactyl.echo(pageInfoText, commandline.FORCE_SINGLELINE);
@@ -852,26 +952,34 @@ var Buffer = Module("buffer", {
      * specified *url*. Either the default viewer or the configured external
      * editor is used.
      *
-     * @param {string} url The URL of the source.
-     * @default The current buffer.
+     * @param {string|object|null} loc If a string, the URL of the source,
+     *      otherwise an object with some or all of the following properties:
+     *
+     *          url: The URL to view.
+     *          doc: The document to view.
+     *          line: The line to select.
+     *          column: The column to select.
+     *
+     *      If no URL is provided, the current document is used.
+     *  @default The current buffer.
      * @param {boolean} useExternalEditor View the source in the external editor.
      */
-    viewSource: function viewSource(url, useExternalEditor) {
+    viewSource: function viewSource(loc, useExternalEditor) {
         let doc = this.focusedFrame.document;
 
-        if (isArray(url)) {
-            if (options.get("editor").has("line"))
-                this.viewSourceExternally(url[0] || doc, url[1]);
+        if (isObject(loc)) {
+            if (options.get("editor").has("line") || !loc.url)
+                this.viewSourceExternally(loc.doc || loc.url || doc, loc);
             else
                 window.openDialog("chrome://global/content/viewSource.xul",
                                   "_blank", "all,dialog=no",
-                                  url[0], null, null, url[1]);
+                                  loc.url, null, null, loc.line);
         }
         else {
             if (useExternalEditor)
-                this.viewSourceExternally(url || doc);
+                this.viewSourceExternally(loc || doc);
             else {
-                url = url || doc.location.href;
+                let url = loc || doc.location.href;
                 const PREFIX = "view-source:";
                 if (url.indexOf(PREFIX) == 0)
                     url = url.substr(PREFIX.length);
@@ -894,13 +1002,19 @@ var Buffer = Module("buffer", {
      * immediately.
      *
      * @param {Document} doc The document to view.
+     * @param {function|object} callback If a function, the callback to be
+     *      called with two arguments: the nsIFile of the file, and temp, a
+     *      boolean which is true if the file is temporary. Otherwise, an object
+     *      with line and column properties used to determine where to open the
+     *      source.
+     *      @optional
      */
     viewSourceExternally: Class("viewSourceExternally",
         XPCOM([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]), {
         init: function init(doc, callback) {
             this.callback = callable(callback) ? callback :
                 function (file, temp) {
-                    editor.editFileExternally({ file: file.path, line: callback },
+                    editor.editFileExternally(update({ file: file.path }, callback || {}),
                                               function () { temp && file.remove(false); });
                     return true;
                 };
@@ -928,8 +1042,8 @@ var Buffer = Module("buffer", {
             return null;
         },
 
-        onStateChange: function onStateChange(progress, request, flag, status) {
-            if ((flag & this.STATE_STOP) && status == 0) {
+        onStateChange: function onStateChange(progress, request, flags, status) {
+            if ((flags & this.STATE_STOP) && status == 0) {
                 try {
                     var ok = this.callback(this.file, true);
                 }
@@ -997,14 +1111,14 @@ var Buffer = Module("buffer", {
      * Adjusts the page zoom of the current buffer relative to the
      * current zoom level.
      *
-     * @param {number} steps The integral number of natural fractions by
-     *   which to adjust the current page zoom. If positive, the zoom
-     *   level is increased, if negative it is decreased.
+     * @param {number} steps The integral number of natural fractions by which
+     *     to adjust the current page zoom. If positive, the zoom level is
+     *     increased, if negative it is decreased.
      * @param {boolean} fullZoom If true, zoom all content of the page,
-     *   including raster images. If false, zoom only text. If omitted,
-     *   use the current zoom function. @optional
-     * @throws {FailedAssertion} if the buffer's zoom level is already
-     *  at its extreme in the given direction.
+     *     including raster images. If false, zoom only text. If omitted, use
+     *     the current zoom function. @optional
+     * @throws {FailedAssertion} if the buffer's zoom level is already at its
+     *     extreme in the given direction.
      */
     bumpZoomLevel: function bumpZoomLevel(steps, fullZoom) {
         if (fullZoom === undefined)
@@ -1019,7 +1133,7 @@ var Buffer = Module("buffer", {
         this.setZoom(Math.round(values[i] * 100), fullZoom);
     },
 
-    getAllFrames: deprecated("buffer.allFrames", function getAllFrames() buffer.getAllFrames.apply(buffer, arguments)),
+    getAllFrames: deprecated("buffer.allFrames", "allFrames"),
     scrollTop: deprecated("buffer.scrollToPercent", function scrollTop() buffer.scrollToPercent(null, 0)),
     scrollBottom: deprecated("buffer.scrollToPercent", function scrollBottom() buffer.scrollToPercent(null, 100)),
     scrollStart: deprecated("buffer.scrollToPercent", function scrollStart() buffer.scrollToPercent(0, null)),
@@ -1027,7 +1141,7 @@ var Buffer = Module("buffer", {
     scrollColumns: deprecated("buffer.scrollHorizontal", function scrollColumns(cols) buffer.scrollHorizontal("columns", cols)),
     scrollPages: deprecated("buffer.scrollHorizontal", function scrollPages(pages) buffer.scrollVertical("pages", pages)),
     scrollTo: deprecated("Buffer.scrollTo", function scrollTo(x, y) content.scrollTo(x, y)),
-    textZoom: deprecated("buffer.zoomValue and buffer.fullZoom", function textZoom() config.browser.markupDocumentViewer.textZoom * 100)
+    textZoom: deprecated("buffer.zoomValue/buffer.fullZoom", function textZoom() config.browser.markupDocumentViewer.textZoom * 100)
 }, {
     PageInfo: Struct("PageInfo", "name", "title", "action")
                         .localize("title"),
@@ -1044,17 +1158,21 @@ var Buffer = Module("buffer", {
      *
      * @returns {string}
      */
-    currentWord: function currentWord(win) {
+    currentWord: function currentWord(win, select) {
         let selection = win.getSelection();
         if (selection.rangeCount == 0)
             return "";
 
         let range = selection.getRangeAt(0).cloneRange();
-        if (range.collapsed) {
+        if (range.collapsed && range.startContainer instanceof Text) {
             let re = options.get("iskeyword").regexp;
             Editor.extendRange(range, true,  re, true);
             Editor.extendRange(range, false, re, true);
         }
+        if (select) {
+            selection.removeAllRanges();
+            selection.addRange(range);
+        }
         return util.domToString(range);
     },
 
@@ -1079,13 +1197,13 @@ var Buffer = Module("buffer", {
 
         var names = [];
         if (node.title)
-            names.push([node.title, "Page Name"]);
+            names.push([node.title, /*L*/"Page Name"]);
 
         if (node.alt)
-            names.push([node.alt, "Alternate Text"]);
+            names.push([node.alt, /*L*/"Alternate Text"]);
 
         if (!isinstance(node, Document) && node.textContent)
-            names.push([node.textContent, "Link Text"]);
+            names.push([node.textContent, /*L*/"Link Text"]);
 
         names.push([decodeURIComponent(url.replace(/.*?([^\/]*)\/*$/, "$1")), "File Name"]);
 
@@ -1133,6 +1251,11 @@ var Buffer = Module("buffer", {
             elem.scrollLeft = left;
         if (top != null)
             elem.scrollTop = top;
+
+        if (util.haveGecko("2.0") && !util.haveGecko("7.*"))
+            elem.ownerDocument.defaultView
+                .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
+                .redraw();
     },
 
     /**
@@ -1156,10 +1279,11 @@ var Buffer = Module("buffer", {
         else
             throw Error();
 
+        dactyl.assert(number < 0 ? elem.scrollLeft > 0 : elem.scrollLeft < elem.scrollWidth - elem.clientWidth);
+
         let left = elem.dactylScrollDestX !== undefined ? elem.dactylScrollDestX : elem.scrollLeft;
         elem.dactylScrollDestX = undefined;
 
-        dactyl.assert(number < 0 ? left > 0 : left < elem.scrollWidth - elem.clientWidth);
         Buffer.scrollTo(elem, left + number * increment, null);
     },
 
@@ -1184,10 +1308,11 @@ var Buffer = Module("buffer", {
         else
             throw Error();
 
+        dactyl.assert(number < 0 ? elem.scrollTop > 0 : elem.scrollTop < elem.scrollHeight - elem.clientHeight);
+
         let top = elem.dactylScrollDestY !== undefined ? elem.dactylScrollDestY : elem.scrollTop;
         elem.dactylScrollDestY = undefined;
 
-        dactyl.assert(number < 0 ? top > 0 : top < elem.scrollHeight - elem.clientHeight);
         Buffer.scrollTo(elem, null, top + number * increment);
     },
 
@@ -1212,7 +1337,7 @@ var Buffer = Module("buffer", {
     },
 
     openUploadPrompt: function openUploadPrompt(elem) {
-        io.CommandFileMode("Upload file: ", {
+        io.CommandFileMode(_("buffer.prompt.uploadFile") + " ", {
             onSubmit: function onSubmit(path) {
                 let file = io.File(path);
                 dactyl.assert(file.exists());
@@ -1238,12 +1363,21 @@ var Buffer = Module("buffer", {
 
                 // FIXME: arg handling is a bit of a mess, check for filename
                 dactyl.assert(!arg || arg[0] == ">" && !util.OS.isWindows,
-                              _("error.trailing"));
+                              _("error.trailingCharacters"));
+
+                const PRINTER = "PostScript/default";
+                const BRANCH  = "print.printer_" + PRINTER + ".";
 
                 prefs.withContext(function () {
                     if (arg) {
-                        prefs.set("print.print_to_file", "true");
-                        prefs.set("print.print_to_filename", io.File(arg.substr(1)).path);
+                        prefs.set("print.print_printer", PRINTER);
+
+                        prefs.set(   "print.print_to_file", true);
+                        prefs.set(BRANCH + "print_to_file", true);
+
+                        prefs.set(   "print.print_to_filename", io.File(arg.substr(1)).path);
+                        prefs.set(BRANCH + "print_to_filename", io.File(arg.substr(1)).path);
+
                         dactyl.echomsg(_("print.toFile", arg.substr(1)));
                     }
                     else
@@ -1417,7 +1551,7 @@ var Buffer = Module("buffer", {
                     level = Math.constrain(level, Buffer.ZOOM_MIN, Buffer.ZOOM_MAX);
                 }
                 else
-                    dactyl.assert(false, _("error.trailing"));
+                    dactyl.assert(false, _("error.trailingCharacters"));
 
                 buffer.setZoom(level, args.bang);
             },
@@ -1434,21 +1568,24 @@ var Buffer = Module("buffer", {
             let styles = iter([s.title, []] for (s in values(buffer.alternateStyleSheets))).toObject();
 
             buffer.alternateStyleSheets.forEach(function (style) {
-                styles[style.title].push(style.href || "inline");
+                styles[style.title].push(style.href || _("style.inline"));
             });
 
             context.completions = [[title, href.join(", ")] for ([title, href] in Iterator(styles))];
         };
 
-        completion.buffer = function buffer(context) {
+        completion.buffer = function buffer(context, visible) {
             let filter = context.filter.toLowerCase();
+
             let defItem = { parent: { getTitle: function () "" } };
+
             let tabGroups = {};
             tabs.getGroups();
-            tabs.allTabs.forEach(function (tab, i) {
+            tabs[visible ? "visibleTabs" : "allTabs"].forEach(function (tab, i) {
                 let group = (tab.tabItem || tab._tabViewTabItem || defItem).parent || defItem.parent;
-                if (!set.has(tabGroups, group.id))
+                if (!Set.has(tabGroups, group.id))
                     tabGroups[group.id] = [group.getTitle(), []];
+
                 group = tabGroups[group.id];
                 group[1].push([i, tab.linkedBrowser]);
             });
@@ -1470,7 +1607,7 @@ var Buffer = Module("buffer", {
                 command: function () "tabs.select"
             };
             context.compare = CompletionContext.Sort.number;
-            context.filters = [CompletionContext.Filter.textDescription];
+            context.filters[0] = CompletionContext.Filter.textDescription;
 
             for (let [id, vals] in Iterator(tabGroups))
                 context.fork(id, 0, this, function (context, [name, browsers]) {
@@ -1483,12 +1620,12 @@ var Buffer = Module("buffer", {
                             else if (i == tabs.index(tabs.alternate))
                                 indicator = "#";
 
-                            let tab = tabs.getTab(i);
+                            let tab = tabs.getTab(i, visible);
                             let url = browser.contentDocument.location.href;
                             i = i + 1;
 
                             return {
-                                text: [i + ": " + (tab.label || "(Untitled)"), i + ": " + url],
+                                text: [i + ": " + (tab.label || /*L*/"(Untitled)"), i + ": " + url],
                                 tab: tab,
                                 id: i - 1,
                                 url: url,
@@ -1514,21 +1651,21 @@ var Buffer = Module("buffer", {
             function () { dactyl.clipboardWrite(buffer.uri.spec, true); });
 
         mappings.add([modes.NORMAL],
-            ["<C-a>"], "Increment last number in URL",
+            ["<C-a>", "<increment-url-path>"], "Increment last number in URL",
             function (args) { buffer.incrementURL(Math.max(args.count, 1)); },
             { count: true });
 
         mappings.add([modes.NORMAL],
-            ["<C-x>"], "Decrement last number in URL",
+            ["<C-x>", "<decrement-url-path>"], "Decrement last number in URL",
             function (args) { buffer.incrementURL(-Math.max(args.count, 1)); },
             { count: true });
 
-        mappings.add([modes.NORMAL], ["gu"],
+        mappings.add([modes.NORMAL], ["gu", "<open-parent-path>"],
             "Go to parent directory",
             function (args) { buffer.climbUrlPath(Math.max(args.count, 1)); },
             { count: true });
 
-        mappings.add([modes.NORMAL], ["gU"],
+        mappings.add([modes.NORMAL], ["gU", "<open-root-path>"],
             "Go to the root of the website",
             function () { buffer.climbUrlPath(-1); });
 
@@ -1542,11 +1679,11 @@ var Buffer = Module("buffer", {
             },
             { count: true });
 
-        mappings.add([modes.COMMAND], ["i", "<Insert>"],
-            "Start caret mode",
+        mappings.add([modes.NORMAL], ["i", "<Insert>"],
+            "Start Caret mode",
             function () { modes.push(modes.CARET); });
 
-        mappings.add([modes.COMMAND], ["<C-c>"],
+        mappings.add([modes.NORMAL], ["<C-c>", "<stop-load>"],
             "Stop loading the current web page",
             function () { ex.stop(); });
 
@@ -1579,12 +1716,12 @@ var Buffer = Module("buffer", {
             "Scroll to the absolute right of the document",
             function () { buffer.scrollToPercent(100, null); });
 
-        mappings.add([modes.COMMAND], ["gg", "<Home>"],
+        mappings.add([modes.COMMAND], ["gg", "<Home>", "<scroll-top>"],
             "Go to the top of the document",
             function (args) { buffer.scrollToPercent(null, args.count != null ? args.count : 0); },
             { count: true });
 
-        mappings.add([modes.COMMAND], ["G", "<End>"],
+        mappings.add([modes.COMMAND], ["G", "<End>", "<scroll-bottom>"],
             "Go to the end of the document",
             function (args) { buffer.scrollToPercent(null, args.count != null ? args.count : 100); },
             { count: true });
@@ -1607,55 +1744,84 @@ var Buffer = Module("buffer", {
             function (args) { buffer._scrollByScrollSize(args.count, false); },
             { count: true });
 
-        mappings.add([modes.COMMAND], ["<C-b>", "<PageUp>", "<S-Space>", "<scroll-page-up>"],
+        mappings.add([modes.COMMAND], ["<C-b>", "<PageUp>", "<S-Space>", "<scroll-up-page>"],
             "Scroll up a full page",
             function (args) { buffer.scrollVertical("pages", -Math.max(args.count, 1)); },
             { count: true });
 
-        mappings.add([modes.COMMAND], ["<C-f>", "<PageDown>", "<Space>", "<scroll-page-down>"],
+        mappings.add([modes.COMMAND], ["<Space>"],
+            "Scroll down a full page",
+            function (args) {
+                if (isinstance(content.document.activeElement, [HTMLInputElement, HTMLButtonElement]))
+                    return Events.PASS;
+                buffer.scrollVertical("pages", Math.max(args.count, 1));
+            },
+            { count: true });
+
+        mappings.add([modes.COMMAND], ["<C-f>", "<PageDown>", "<scroll-down-page>"],
             "Scroll down a full page",
             function (args) { buffer.scrollVertical("pages", Math.max(args.count, 1)); },
             { count: true });
 
-        mappings.add([modes.COMMAND], ["]f", "<previous-frame>"],
+        mappings.add([modes.NORMAL], ["]f", "<previous-frame>"],
             "Focus next frame",
             function (args) { buffer.shiftFrameFocus(Math.max(args.count, 1)); },
             { count: true });
 
-        mappings.add([modes.COMMAND], ["[f", "<next-frame>"],
+        mappings.add([modes.NORMAL], ["[f", "<next-frame>"],
             "Focus previous frame",
             function (args) { buffer.shiftFrameFocus(-Math.max(args.count, 1)); },
             { count: true });
 
-        mappings.add([modes.COMMAND], ["]]", "<next-page>"],
+        mappings.add([modes.NORMAL], ["["],
+            "Jump to the previous element as defined by 'jumptags'",
+            function (args) { buffer.findJump(args.arg, args.count, true); },
+            { arg: true, count: true });
+
+        mappings.add([modes.NORMAL], ["]"],
+            "Jump to the next element as defined by 'jumptags'",
+            function (args) { buffer.findJump(args.arg, args.count, false); },
+            { arg: true, count: true });
+
+        mappings.add([modes.NORMAL], ["{"],
+            "Jump to the previous paragraph",
+            function (args) { buffer.findJump("p", args.count, true); },
+            { count: true });
+
+        mappings.add([modes.NORMAL], ["}"],
+            "Jump to the next paragraph",
+            function (args) { buffer.findJump("p", args.count, false); },
+            { count: true });
+
+        mappings.add([modes.NORMAL], ["]]", "<next-page>"],
             "Follow the link labeled 'next' or '>' if it exists",
             function (args) {
                 buffer.findLink("next", options["nextpattern"], (args.count || 1) - 1, true);
             },
             { count: true });
 
-        mappings.add([modes.COMMAND], ["[[", "<previous-page>"],
+        mappings.add([modes.NORMAL], ["[[", "<previous-page>"],
             "Follow the link labeled 'prev', 'previous' or '<' if it exists",
             function (args) {
                 buffer.findLink("previous", options["previouspattern"], (args.count || 1) - 1, true);
             },
             { count: true });
 
-        mappings.add([modes.COMMAND], ["gf", "<view-source>"],
+        mappings.add([modes.NORMAL], ["gf", "<view-source>"],
             "Toggle between rendered and source view",
             function () { buffer.viewSource(null, false); });
 
-        mappings.add([modes.COMMAND], ["gF", "<view-source-externally>"],
+        mappings.add([modes.NORMAL], ["gF", "<view-source-externally>"],
             "View source with an external editor",
             function () { buffer.viewSource(null, true); });
 
-        mappings.add([modes.COMMAND], ["gi", "<focus-input>"],
+        mappings.add([modes.NORMAL], ["gi", "<focus-input>"],
             "Focus last used input field",
             function (args) {
                 let elem = buffer.lastInputField;
 
                 if (args.count >= 1 || !elem || !events.isContentNode(elem)) {
-                    let xpath = ["frame", "iframe", "input", "textarea[not(@disabled) and not(@readonly)]"];
+                    let xpath = ["frame", "iframe", "input", "xul:textbox", "textarea[not(@disabled) and not(@readonly)]"];
 
                     let frames = buffer.allFrames(null, true);
 
@@ -1664,13 +1830,14 @@ var Buffer = Module("buffer", {
                         if (isinstance(elem, [HTMLFrameElement, HTMLIFrameElement]))
                             return Editor.getEditor(elem.contentWindow);
 
-                        if (elem.readOnly || elem instanceof HTMLInputElement && !set.has(util.editableInputs, elem.type))
+                        if (elem.readOnly || elem instanceof HTMLInputElement && !Set.has(util.editableInputs, elem.type))
                             return false;
 
                         let computedStyle = util.computedStyle(elem);
                         let rect = elem.getBoundingClientRect();
                         return computedStyle.visibility != "hidden" && computedStyle.display != "none" &&
-                            computedStyle.MozUserFocus != "ignore" && rect.width && rect.height;
+                            (elem instanceof Ci.nsIDOMXULTextBoxElement || computedStyle.MozUserFocus != "ignore") &&
+                            rect.width && rect.height;
                     });
 
                     dactyl.assert(elements.length > 0);
@@ -1681,36 +1848,40 @@ var Buffer = Module("buffer", {
             },
             { count: true });
 
-        mappings.add([modes.COMMAND], ["gP"],
-            "Open (]put) a URL based on the current clipboard contents in a new buffer",
+        function url() {
+            let url = dactyl.clipboardRead();
+            dactyl.assert(url, _("error.clipboardEmpty"));
+
+            let proto = /^([-\w]+):/.exec(url);
+            if (proto && "@mozilla.org/network/protocol;1?name=" + proto[1] in Cc && !RegExp(options["urlseparator"]).test(url))
+                return url.replace(/\s+/g, "");
+            return url;
+        }
+
+        mappings.add([modes.NORMAL], ["gP"],
+            "Open (put) a URL based on the current clipboard contents in a new background buffer",
             function () {
-                let url = dactyl.clipboardRead();
-                dactyl.assert(url, _("error.clipboardEmpty"));
-                dactyl.open(url, { from: "paste", where: dactyl.NEW_TAB, background: true });
+                dactyl.open(url(), { from: "paste", where: dactyl.NEW_TAB, background: true });
             });
 
-        mappings.add([modes.COMMAND], ["p", "<MiddleMouse>", "<open-clipboard-url>"],
+        mappings.add([modes.NORMAL], ["p", "<MiddleMouse>", "<open-clipboard-url>"],
             "Open (put) a URL based on the current clipboard contents in the current buffer",
             function () {
-                let url = dactyl.clipboardRead();
-                dactyl.assert(url, _("error.clipboardEmpty"));
-                dactyl.open(url);
+                dactyl.open(url());
             });
 
-        mappings.add([modes.COMMAND], ["P", "<tab-open-clipboard-url>"],
+        mappings.add([modes.NORMAL], ["P", "<tab-open-clipboard-url>"],
             "Open (put) a URL based on the current clipboard contents in a new buffer",
             function () {
-                let url = dactyl.clipboardRead();
-                dactyl.assert(url, _("error.clipboardEmpty"));
-                dactyl.open(url, { from: "paste", where: dactyl.NEW_TAB });
+                dactyl.open(url(), { from: "paste", where: dactyl.NEW_TAB });
             });
 
         // reloading
-        mappings.add([modes.COMMAND], ["r", "<reload>"],
+        mappings.add([modes.NORMAL], ["r", "<reload>"],
             "Reload the current web page",
             function () { tabs.reload(tabs.getTab(), false); });
 
-        mappings.add([modes.COMMAND], ["R", "<full-reload>"],
+        mappings.add([modes.NORMAL], ["R", "<full-reload>"],
             "Reload while skipping the cache",
             function () { tabs.reload(tabs.getTab(), true); });
 
@@ -1724,62 +1895,62 @@ var Buffer = Module("buffer", {
             });
 
         // zooming
-        mappings.add([modes.COMMAND], ["zi", "+", "<text-zoom-in>"],
+        mappings.add([modes.NORMAL], ["zi", "+", "<text-zoom-in>"],
             "Enlarge text zoom of current web page",
             function (args) { buffer.zoomIn(Math.max(args.count, 1), false); },
             { count: true });
 
-        mappings.add([modes.COMMAND], ["zm", "<text-zoom-more>"],
+        mappings.add([modes.NORMAL], ["zm", "<text-zoom-more>"],
             "Enlarge text zoom of current web page by a larger amount",
             function (args) { buffer.zoomIn(Math.max(args.count, 1) * 3, false); },
             { count: true });
 
-        mappings.add([modes.COMMAND], ["zo", "-", "<text-zoom-out>"],
+        mappings.add([modes.NORMAL], ["zo", "-", "<text-zoom-out>"],
             "Reduce text zoom of current web page",
             function (args) { buffer.zoomOut(Math.max(args.count, 1), false); },
             { count: true });
 
-        mappings.add([modes.COMMAND], ["zr", "<text-zoom-reduce>"],
+        mappings.add([modes.NORMAL], ["zr", "<text-zoom-reduce>"],
             "Reduce text zoom of current web page by a larger amount",
             function (args) { buffer.zoomOut(Math.max(args.count, 1) * 3, false); },
             { count: true });
 
-        mappings.add([modes.COMMAND], ["zz", "<text-zoom>"],
+        mappings.add([modes.NORMAL], ["zz", "<text-zoom>"],
             "Set text zoom value of current web page",
             function (args) { buffer.setZoom(args.count > 1 ? args.count : 100, false); },
             { count: true });
 
-        mappings.add([modes.COMMAND], ["ZI", "zI", "<full-zoom-in>"],
+        mappings.add([modes.NORMAL], ["ZI", "zI", "<full-zoom-in>"],
             "Enlarge full zoom of current web page",
             function (args) { buffer.zoomIn(Math.max(args.count, 1), true); },
             { count: true });
 
-        mappings.add([modes.COMMAND], ["ZM", "zM", "<full-zoom-more>"],
+        mappings.add([modes.NORMAL], ["ZM", "zM", "<full-zoom-more>"],
             "Enlarge full zoom of current web page by a larger amount",
             function (args) { buffer.zoomIn(Math.max(args.count, 1) * 3, true); },
             { count: true });
 
-        mappings.add([modes.COMMAND], ["ZO", "zO", "<full-zoom-out>"],
+        mappings.add([modes.NORMAL], ["ZO", "zO", "<full-zoom-out>"],
             "Reduce full zoom of current web page",
             function (args) { buffer.zoomOut(Math.max(args.count, 1), true); },
             { count: true });
 
-        mappings.add([modes.COMMAND], ["ZR", "zR", "<full-zoom-reduce>"],
+        mappings.add([modes.NORMAL], ["ZR", "zR", "<full-zoom-reduce>"],
             "Reduce full zoom of current web page by a larger amount",
             function (args) { buffer.zoomOut(Math.max(args.count, 1) * 3, true); },
             { count: true });
 
-        mappings.add([modes.COMMAND], ["zZ", "<full-zoom>"],
+        mappings.add([modes.NORMAL], ["zZ", "<full-zoom>"],
             "Set full zoom value of current web page",
             function (args) { buffer.setZoom(args.count > 1 ? args.count : 100, true); },
             { count: true });
 
         // page info
-        mappings.add([modes.COMMAND], ["<C-g>", "<page-info>"],
+        mappings.add([modes.NORMAL], ["<C-g>", "<page-info>"],
             "Print the current file name",
             function () { buffer.showPageInfo(false); });
 
-        mappings.add([modes.COMMAND], ["g<C-g>", "<more-page-info>"],
+        mappings.add([modes.NORMAL], ["g<C-g>", "<more-page-info>"],
             "Print file information",
             function () { buffer.showPageInfo(true); });
     },
@@ -1817,6 +1988,23 @@ var Buffer = Module("buffer", {
                 validator: function (value) RegExp(value)
             });
 
+        options.add(["jumptags", "jt"],
+            "XPath or CSS selector strings of jumpable elements for extended hint modes",
+            "stringmap", {
+                "p": "p,table,ul,ol,blockquote",
+                "h": "h1,h2,h3,h4,h5,h6"
+            },
+            {
+                keepQuotes: true,
+                setter: function (vals) {
+                    for (let [k, v] in Iterator(vals))
+                        vals[k] = update(new String(v), { matcher: util.compileMatcher(Option.splitList(v)) });
+                    return vals;
+                },
+                validator: function (value) util.validateMatcher.call(this, value)
+                    && Object.keys(value).every(function (v) v.length == 1)
+            });
+
         options.add(["nextpattern"],
             "Patterns to use when guessing the next page in a document sequence",
             "regexplist", UTF8("'\\bnext\\b',^>$,^(>>|»)$,^(>|»),(>|»)$,'\\bmore\\b'"),
@@ -1829,7 +2017,7 @@ var Buffer = Module("buffer", {
 
         options.add(["pageinfo", "pa"],
             "Define which sections are shown by the :pageinfo command",
-            "charlist", "gfm",
+            "charlist", "gesfm",
             { get values() values(buffer.pageInfo).toObject() });
 
         options.add(["scroll", "scr"],