]> git.donarmstrong.com Git - dactyl.git/blobdiff - common/content/tabs.js
Imported Upstream version 1.1+hg7904
[dactyl.git] / common / content / tabs.js
index f8584da2c4c3ef08aa3eebb852670c1975df0490..9bf588b92f54391400a58bd65092ba78e581a3b5 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 at Gmail>
+// Copyright (c) 2008-2014 Kris Maglione <maglione.k at Gmail>
 //
 // This work is licensed for reuse under an MIT license. Details are
 // given in the LICENSE.txt file included with this file.
@@ -19,33 +19,46 @@ var Tabs = Module("tabs", {
         this._lastBufferSwitchArgs = "";
         this._lastBufferSwitchSpecial = true;
 
+        this.xulTabs = document.getElementById("tabbrowser-tabs");
+
         // hide tabs initially to prevent flickering when 'stal' would hide them
         // on startup
-        if (config.hasTabbrowser)
+        if (config.has("tabbrowser"))
             config.tabStrip.collapsed = true;
 
         this.tabStyle = styles.system.add("tab-strip-hiding", config.styleableChrome,
-                                          (config.tabStrip.id ? "#" + config.tabStrip.id : ".tabbrowser-strip") +
+                                          (config.tabStrip.id ? "#" + config.tabStrip.id
+                                                              : ".tabbrowser-strip") +
                                               "{ visibility: collapse; }",
                                           false, true);
 
         dactyl.commands["tabs.select"] = function (event) {
-            tabs.select(event.originalTarget.getAttribute("identifier"));
+            tabs.switchTo(event.originalTarget.getAttribute("identifier"));
         };
 
-        this.tabBinding = styles.system.add("tab-binding", "chrome://browser/content/browser.xul", String.replace(<><![CDATA[
+        this.tabBinding = styles.system.add("tab-binding", "chrome://browser/content/browser.xul", literal(/*
                 xul|tab { -moz-binding: url(chrome://dactyl/content/bindings.xml#tab) !important; }
-            ]]></>, /tab-./g, function (m) util.OS.isMacOSX ? "tab-mac" : m),
+            */).replace(/tab-./g, m => config.OS.isMacOSX ? "tab-mac" : m),
             false, true);
 
         this.timeout(function () {
             for (let { linkedBrowser: { contentDocument } } in values(this.allTabs))
                 if (contentDocument.readyState === "complete")
                     dactyl.initDocument(contentDocument);
-        });
+        }, 1000);
+
+        if (window.TabsInTitlebar)
+            window.TabsInTitlebar.allowedBy("dactyl", false);
     },
 
-    _alternates: Class.memoize(function () [config.tabbrowser.mCurrentTab, null]),
+    signals: {
+        enter: function enter() {
+            if (window.TabsInTitlebar)
+                window.TabsInTitlebar.allowedBy("dactyl", true);
+        }
+    },
+
+    _alternates: Class.Memoize(() => [config.tabbrowser.mCurrentTab, null]),
 
     cleanup: function cleanup() {
         for (let [i, tab] in Iterator(this.allTabs)) {
@@ -53,34 +66,42 @@ var Tabs = Module("tabs", {
             for (let elem in values(["dactyl-tab-icon-number", "dactyl-tab-number"].map(node)))
                 if (elem)
                     elem.parentNode.parentNode.removeChild(elem.parentNode);
+
+            delete tab.dactylOrdinal;
+            tab.removeAttribute("dactylOrdinal");
         }
     },
 
-    updateTabCount: function () {
+    updateTabCount: function updateTabCount() {
         for (let [i, tab] in Iterator(this.visibleTabs)) {
-            if (dactyl.has("Gecko2")) {
-                let node = function node(class_) document.getAnonymousElementByAttribute(tab, "class", class_);
-                if (!node("dactyl-tab-number")) {
-                    let img = node("tab-icon-image");
-                    if (img) {
-                        let nodes = {};
-                        let dom = util.xmlToDom(<xul xmlns:xul={XUL} xmlns:html={XHTML}
-                            ><xul:hbox highlight="tab-number"><xul:label key="icon" align="center" highlight="TabIconNumber" class="dactyl-tab-icon-number"/></xul:hbox
-                            ><xul:hbox highlight="tab-number"><html:div key="label" highlight="TabNumber" class="dactyl-tab-number"/></xul:hbox
-                        ></xul>.*, document, nodes);
-                        img.parentNode.appendChild(dom);
-                        tab.__defineGetter__("dactylOrdinal", function () Number(nodes.icon.value));
-                        tab.__defineSetter__("dactylOrdinal", function (i) nodes.icon.value = nodes.label.textContent = i);
-                    }
+            let node = function node(class_) document.getAnonymousElementByAttribute(tab, "class", class_);
+            if (!node("dactyl-tab-number")) {
+                let img = node("tab-icon-image");
+                if (img) {
+                    let dom = DOM([
+                        ["xul:hbox", { highlight: "tab-number" },
+                            ["xul:label", { key: "icon", align: "center", highlight: "TabIconNumber",
+                                            class: "dactyl-tab-icon-number" }]],
+                        ["xul:hbox", { highlight: "tab-number" },
+                            ["html:div", { key: "label", highlight: "TabNumber",
+                                           class: "dactyl-tab-number" }]]],
+                        document).appendTo(img.parentNode);
+
+                    update(tab, {
+                        get dactylOrdinal() Number(dom.nodes.icon.value),
+                        set dactylOrdinal(i) {
+                            dom.nodes.icon.value = dom.nodes.label.textContent = i;
+                            this.setAttribute("dactylOrdinal", i);
+                        }
+                    });
                 }
             }
-            tab.setAttribute("dactylOrdinal", i + 1);
             tab.dactylOrdinal = i + 1;
         }
         statusline.updateTabCount(true);
     },
 
-    _onTabSelect: function () {
+    _onTabSelect: function _onTabSelect() {
         // TODO: is all of that necessary?
         //       I vote no. --Kris
         modes.reset();
@@ -103,7 +124,8 @@ var Tabs = Module("tabs", {
     get browsers() {
         let browsers = config.tabbrowser.browsers;
         for (let i = 0; i < browsers.length; i++)
-            yield [i, browsers[i]];
+            if (browsers[i] !== undefined) // Bug in Google's Page Speed add-on.
+                yield [i, browsers[i]];
     },
 
     /**
@@ -114,14 +136,9 @@ var Tabs = Module("tabs", {
     /**
      * @property {Object} The local options store for the current tab.
      */
-    get options() {
-        let store = this.localStore;
-        if (!("options" in store))
-            store.options = {};
-        return store.options;
-    },
+    get options() this.localStore.options,
 
-    get visibleTabs() config.tabbrowser.visibleTabs || this.allTabs.filter(function (tab) !tab.hidden),
+    get visibleTabs() config.tabbrowser.visibleTabs || this.allTabs.filter(tab => !tab.hidden),
 
     /**
      * Returns the local state store for the tab at the specified *tabIndex*.
@@ -135,11 +152,11 @@ var Tabs = Module("tabs", {
     //        property doesn't. And the property is so oft-used that it's
     //        convenient. To the former question, because I think this is mainly
     //        useful for autocommands, and they get index arguments. --Kris
-    getLocalStore: function (tabIndex) {
+    getLocalStore: function getLocalStore(tabIndex) {
         let tab = this.getTab(tabIndex);
         if (!tab.dactylStore)
-            tab.dactylStore = {};
-        return tab.dactylStore;
+            tab.dactylStore = Object.create(this.localStorePrototype);
+        return tab.dactylStore.instance = tab.dactylStore;
     },
 
     /**
@@ -148,11 +165,16 @@ var Tabs = Module("tabs", {
      */
     get localStore() this.getLocalStore(),
 
+    localStorePrototype: memoize({
+        instance: {},
+        get options() ({})
+    }),
+
     /**
-     * @property {Object[]} The array of closed tabs for the current
+     * @property {[Object]} The array of closed tabs for the current
      *     session.
      */
-    get closedTabs() services.json.decode(services.sessionStore.getClosedTabData(window)),
+    get closedTabs() JSON.parse(services.sessionStore.getClosedTabData(window)),
 
     /**
      * Clones the specified *tab* and append it to the tab list.
@@ -160,8 +182,8 @@ var Tabs = Module("tabs", {
      * @param {Object} tab The tab to clone.
      * @param {boolean} activate Whether to select the newly cloned tab.
      */
-    cloneTab: function (tab, activate) {
-        let newTab = config.tabbrowser.addTab();
+    cloneTab: function cloneTab(tab, activate) {
+        let newTab = config.tabbrowser.addTab("about:blank", { ownerTab: tab });
         Tabs.copyTab(newTab, tab);
 
         if (activate)
@@ -176,7 +198,7 @@ var Tabs = Module("tabs", {
      *
      * @param {Object} tab The tab to detach.
      */
-    detachTab: function (tab) {
+    detachTab: function detachTab(tab) {
         if (!tab)
             tab = config.tabbrowser.mTabContainer.selectedItem;
 
@@ -191,7 +213,7 @@ var Tabs = Module("tabs", {
      *     document.
      */
     // FIXME: Only called once...necessary?
-    getContentIndex: function (content) {
+    getContentIndex: function getContentIndex(content) {
         for (let [i, browser] in this.browsers) {
             if (browser.contentWindow == content || browser.contentDocument == content)
                 return i;
@@ -206,17 +228,22 @@ var Tabs = Module("tabs", {
      *
      * @returns {Window}
      */
-    getGroups: function () {
-        if ("_groups" in this)
+    getGroups: function getGroups(func) {
+        let iframe = document.getElementById("tab-view");
+        this._groups = iframe ? iframe.contentWindow : null;
+
+        if ("_groups" in this && !func)
             return this._groups;
 
-        if (window.TabView && TabView._initFrame)
-            TabView._initFrame();
+        if (func)
+            func = bind(function (func) { func(this._groups); }, this, func);
+
+        if (window.TabView && window.TabView._initFrame)
+            window.TabView._initFrame(func);
 
-        let iframe = document.getElementById("tab-view");
         this._groups = iframe ? iframe.contentWindow : null;
-        if (this._groups)
-            util.waitFor(function () this._groups.TabItems, this);
+        if (this._groups && !func)
+            util.waitFor(() => this._groups.TabItems);
         return this._groups;
     },
 
@@ -225,13 +252,15 @@ var Tabs = Module("tabs", {
      * if *index* is not specified. This is a 0-based index.
      *
      * @param {number|Node} index The index of the tab required or the tab itself
+     * @param {boolean} visible If true, consider only visible tabs rather than
+     *      all tabs.
      * @returns {Object}
      */
-    getTab: function (index) {
+    getTab: function getTab(index, visible) {
         if (index instanceof Node)
             return index;
         if (index != null)
-            return config.tabbrowser.mTabs[index];
+            return this[visible ? "visibleTabs" : "allTabs"][index];
         return config.tabbrowser.mCurrentTab;
     },
 
@@ -243,7 +272,7 @@ var Tabs = Module("tabs", {
      * @param {boolean} visible Whether to consider only visible tabs.
      * @returns {number}
      */
-    index: function (tab, visible) {
+    index: function index(tab, visible) {
         let tabs = this[visible ? "visibleTabs" : "allTabs"];
         return tabs.indexOf(tab || config.tabbrowser.mCurrentTab);
     },
@@ -256,24 +285,25 @@ var Tabs = Module("tabs", {
      * - "-3" for the tab, which is 3 positions left of the current
      * - "$" for the last tab
      */
-    indexFromSpec: function (spec, wrap) {
+    indexFromSpec: function indexFromSpec(spec, wrap, offset) {
         if (spec instanceof Node)
             return this.allTabs.indexOf(spec);
 
         let tabs     = this.visibleTabs;
         let position = this.index(null, true);
 
-        if (spec == null || spec === "")
+        if (spec == null)
+            return -1;
+
+        if (spec === "")
             return position;
 
-        if (typeof spec === "number")
-            position = spec;
+        if (/^\d+$/.test(spec))
+            position = parseInt(spec, 10) + (offset || 0);
         else if (spec === "$")
             position = tabs.length - 1;
         else if (/^[+-]\d+$/.test(spec))
             position += parseInt(spec, 10);
-        else if (/^\d+$/.test(spec))
-            position = parseInt(spec, 10);
         else
             return -1;
 
@@ -290,7 +320,7 @@ var Tabs = Module("tabs", {
      *
      * @param {Object} tab The tab to keep.
      */
-    keepOnly: function (tab) {
+    keepOnly: function keepOnly(tab) {
         config.tabbrowser.removeAllTabsBut(tab);
     },
 
@@ -300,10 +330,63 @@ var Tabs = Module("tabs", {
      * @param {string} filter A filter matching a substring of the tab's
      *     document title or URL.
      */
-    list: function (filter) {
+    list: function list(filter) {
         completion.listCompleter("buffer", filter);
     },
 
+    /**
+     * Return an iterator of tabs matching the given filter. If no
+     * *filter* or *count* is provided, returns the currently selected
+     * tab. If *filter* is a number or begins with a number followed
+     * by a colon, the tab of that ordinal is returned. Otherwise,
+     * tabs matching the filter as below are returned.
+     *
+     * @param {string} filter The filter. If *regexp*, this is a
+     *      regular expression against which the tab's URL or title
+     *      must match. Otherwise, it is a site filter.
+     *      @optional
+     * @param {number|null} count If non-null, return only the
+     *      *count*th matching tab.
+     *      @optional
+     * @param {boolean} regexp Whether to interpret *filter* as a
+     *      regular expression.
+     * @param {boolean} all If true, match against all tabs. If
+     *      false, match only tabs in the current tab group.
+     */
+    match: function match(filter, count, regexp, all) {
+        if (!filter && count == null)
+            yield tabs.getTab();
+        else if (!filter)
+            yield dactyl.assert(tabs.getTab(count - 1));
+        else {
+            let matches = /^(\d+)(?:$|:)/.exec(filter);
+            if (matches)
+                yield dactyl.assert(count == null &&
+                                    tabs.getTab(parseInt(matches[1], 10) - 1, !all));
+            else {
+                if (regexp)
+                    regexp = util.regexp(filter, "i");
+                else
+                    var matcher = Styles.matchFilter(filter);
+
+                for (let tab in values(tabs[all ? "allTabs" : "visibleTabs"])) {
+                    let browser = tab.linkedBrowser;
+                    let uri = browser.currentURI;
+                    let title;
+                    if (uri.spec == "about:blank")
+                        title = "(Untitled)";
+                    else
+                        title = browser.contentTitle;
+
+                    if (matcher && matcher(uri)
+                        || regexp && (regexp.test(title) || regexp.test(uri.spec)))
+                        if (count == null || --count == 0)
+                            yield tab;
+                }
+            }
+        }
+    },
+
     /**
      * Moves a tab to a new position in the tab list.
      *
@@ -312,8 +395,8 @@ var Tabs = Module("tabs", {
      * @param {boolean} wrap Whether an out of bounds *spec* causes the
      *     destination position to wrap around the start/end of the tab list.
      */
-    move: function (tab, spec, wrap) {
-        let index = tabs.indexFromSpec(spec, wrap);
+    move: function move(tab, spec, wrap) {
+        let index = tabs.indexFromSpec(spec, wrap, -1);
         config.tabbrowser.moveTabTo(tab, index);
     },
 
@@ -324,8 +407,7 @@ var Tabs = Module("tabs", {
      * @param {number} count How many tabs to remove.
      * @param {boolean} focusLeftTab Focus the tab to the left of the removed tab.
      */
-    remove: function (tab, count, focusLeftTab) {
-        count = count || 1;
+    remove: function remove(tab, count=1, focusLeftTab=false) {
         let res = this.count > count;
 
         let tabs = this.visibleTabs;
@@ -342,9 +424,13 @@ var Tabs = Module("tabs", {
         }
 
         if (focusLeftTab)
-            tabs.slice(Math.max(0, index + 1 - count), index + 1).forEach(config.closure.removeTab);
+            tabs.slice(Math.max(0, index + 1 - count),
+                       index + 1)
+                .forEach(config.bound.removeTab);
         else
-            tabs.slice(index, index + count).forEach(config.closure.removeTab);
+            tabs.slice(index,
+                       index + count)
+                .forEach(config.bound.removeTab);
         return res;
     },
 
@@ -355,7 +441,7 @@ var Tabs = Module("tabs", {
      * @param {boolean} bypassCache Whether to bypass the cache when
      *     reloading.
      */
-    reload: function (tab, bypassCache) {
+    reload: function reload(tab, bypassCache) {
         try {
             if (bypassCache) {
                 const flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
@@ -373,20 +459,15 @@ var Tabs = Module("tabs", {
      * @param {boolean} bypassCache Whether to bypass the cache when
      *     reloading.
      */
-    reloadAll: function (bypassCache) {
-        if (bypassCache) {
-            for (let i = 0; i < config.tabbrowser.mTabs.length; i++) {
-                try {
-                    this.reload(config.tabbrowser.mTabs[i], bypassCache);
-                }
-                catch (e) {
-                    // FIXME: can we do anything useful here without stopping the
-                    //        other tabs from reloading?
-                }
+    reloadAll: function reloadAll(bypassCache) {
+        this.visibleTabs.forEach(function (tab) {
+            try {
+                tabs.reload(tab, bypassCache);
             }
-        }
-        else
-            config.tabbrowser.reloadAllTabs();
+            catch (e) {
+                dactyl.reportError(e, true);
+            }
+        });
     },
 
     /**
@@ -396,7 +477,7 @@ var Tabs = Module("tabs", {
      * @param {boolean} wrap Whether an out of bounds *spec* causes the
      *     selection position to wrap around the start/end of the tab list.
      */
-    select: function (spec, wrap) {
+    select: function select(spec, wrap) {
         let index = tabs.indexFromSpec(spec, wrap);
         if (index == -1)
             dactyl.beep();
@@ -407,7 +488,7 @@ var Tabs = Module("tabs", {
     /**
      * Selects the alternate tab.
      */
-    selectAlternateTab: function () {
+    selectAlternateTab: function selectAlternateTab() {
         dactyl.assert(tabs.alternate != null && tabs.getTab() != tabs.alternate,
                       _("buffer.noAlternate"));
         tabs.select(tabs.alternate);
@@ -418,7 +499,7 @@ var Tabs = Module("tabs", {
      *
      * @param {Object} tab The tab to stop loading.
      */
-    stop: function (tab) {
+    stop: function stop(tab) {
         if (config.stop)
             config.stop(tab);
         else
@@ -428,7 +509,7 @@ var Tabs = Module("tabs", {
     /**
      * Stops loading all tabs.
      */
-    stopAll: function () {
+    stopAll: function stopAll() {
         for (let [, browser] in this.browsers)
             browser.stop();
     },
@@ -447,7 +528,7 @@ var Tabs = Module("tabs", {
      *
      */
     // FIXME: help!
-    switchTo: function (buffer, allowNonUnique, count, reverse) {
+    switchTo: function switchTo(buffer, allowNonUnique, count, reverse) {
         if (buffer != null) {
             // store this command, so it can be repeated with "B"
             this._lastBufferSwitchArgs = buffer;
@@ -469,11 +550,11 @@ var Tabs = Module("tabs", {
         if (matches)
             return tabs.select(this.allTabs[parseInt(matches[1], 10) - 1], false);
 
-        matches = array.nth(tabs.allTabs, function (t) (t.linkedBrowser.lastURI || {}).spec === buffer, 0);
+        matches = tabs.allTabs.find(t => (t.linkedBrowser.lastURI || {}).spec === buffer);
         if (matches)
             return tabs.select(matches, false);
 
-        matches = completion.runCompleter("buffer", buffer).map(function (obj) obj.tab);
+        matches = completion.runCompleter("buffer", buffer).map(obj => obj.tab);
 
         if (matches.length == 0)
             dactyl.echoerr(_("buffer.noMatching", buffer));
@@ -502,7 +583,7 @@ var Tabs = Module("tabs", {
      * @param {Array(Object)} tabs The current and alternate tab.
      * @see tabs#alternate
      */
-    updateSelectionHistory: function (tabs) {
+    updateSelectionHistory: function updateSelectionHistory(tabs) {
         if (!tabs) {
             if (this.getTab() == this._alternates[0]
                 || this.alternate && this.allTabs.indexOf(this._alternates[0]) == -1
@@ -520,79 +601,88 @@ var Tabs = Module("tabs", {
         services.sessionStore.setTabState(to, tabState);
     }
 }, {
-    load: function () {
+    load: function initLoad() {
         tabs.updateTabCount();
     },
-    commands: function () {
-        commands.add(["bd[elete]", "bw[ipeout]", "bun[load]", "tabc[lose]"],
-            "Delete current buffer",
-            function (args) {
-                let special = args.bang;
-                let count   = args.count;
-                let arg     = args[0] || "";
-
-                if (arg) {
+    commands: function initCommands() {
+        [
+            {
+                name: ["bd[elete]"],
+                description: "Delete matching buffers",
+                visible: false
+            },
+            {
+                name: ["tabc[lose]"],
+                description: "Delete matching tabs",
+                visible: true
+            }
+        ].forEach(function (params) {
+            commands.add(params.name, params.description,
+                function (args) {
                     let removed = 0;
-                    let matches = arg.match(/^(\d+):?/);
-
-                    if (matches) {
-                        config.removeTab(tabs.getTab(parseInt(matches[1], 10) - 1));
-                        removed = 1;
+                    for (let tab in tabs.match(args[0], args.count, args.bang, !params.visible)) {
+                        config.removeTab(tab);
+                        removed++;
                     }
-                    else {
-                        let str = arg.toLowerCase();
-                        let browsers = config.tabbrowser.browsers;
-
-                        for (let i = browsers.length - 1; i >= 0; i--) {
-                            let host, title, uri = browsers[i].currentURI.spec;
-                            if (browsers[i].currentURI.schemeIs("about")) {
-                                host = "";
-                                title = "(Untitled)";
-                            }
-                            else {
-                                host = browsers[i].currentURI.host;
-                                title = browsers[i].contentTitle;
-                            }
 
-                            [host, title, uri] = [host, title, uri].map(String.toLowerCase);
-
-                            if (host.indexOf(str) >= 0 || uri == str ||
-                                (special && (title.indexOf(str) >= 0 || uri.indexOf(str) >= 0))) {
-                                config.removeTab(tabs.getTab(i));
-                                removed++;
-                            }
-                        }
-                    }
+                    if (args[0])
+                        if (removed > 0)
+                            dactyl.echomsg(_("buffer.fewerTab" + (removed == 1 ? "" : "s"), removed), 9);
+                        else
+                            dactyl.echoerr(_("buffer.noMatching", args[0]));
+                }, {
+                    argCount: "?",
+                    bang: true,
+                    count: true,
+                    completer: function (context) completion.buffer(context),
+                    literal: 0,
+                    privateData: true
+                });
+        });
 
-                    if (removed > 0)
-                        dactyl.echomsg(_("buffer.fewer", removed, removed == 1 ? "" : "s"), 9);
-                    else
-                        dactyl.echoerr(_("buffer.noMatching", arg));
-                }
-                else // just remove the current tab
-                    tabs.remove(tabs.getTab(), Math.max(count, 1), special);
-            }, {
+        commands.add(["pin[tab]"],
+            "Pin tab as an application tab",
+            function (args) {
+                for (let tab in tabs.match(args[0], args.count))
+                    config.browser[!args.bang || !tab.pinned ? "pinTab" : "unpinTab"](tab);
+            },
+            {
                 argCount: "?",
                 bang: true,
                 count: true,
-                completer: function (context) completion.buffer(context),
-                literal: 0,
-                privateData: true
+                completer: function (context, args) {
+                    if (!args.bang)
+                        context.filters.push(({ item }) => !item.tab.pinned);
+                    completion.buffer(context);
+                }
+            });
+
+        commands.add(["unpin[tab]"],
+            "Unpin tab as an application tab",
+            function (args) {
+                for (let tab in tabs.match(args[0], args.count))
+                    config.browser.unpinTab(tab);
+            },
+            {
+                argCount: "?",
+                count: true,
+                completer: function (context, args) {
+                    context.filters.push(({ item }) => item.tab.pinned);
+                    completion.buffer(context);
+                }
             });
 
         commands.add(["keepa[lt]"],
             "Execute a command without changing the current alternate buffer",
             function (args) {
-                let alternate = tabs.alternate;
-
                 try {
-                    commands.execute(args[0] || "", null, true);
+                    dactyl.execute(args[0], null, true);
                 }
                 finally {
-                    tabs.updateSelectionHistory([tabs.getTab(), alternate]);
+                    tabs.updateSelectionHistory([tabs.getTab(), tabs.alternate]);
                 }
             }, {
-                argCount: "+",
+                argCount: "1",
                 completer: function (context) completion.ex(context),
                 literal: 0,
                 subCommand: 0
@@ -601,12 +691,26 @@ var Tabs = Module("tabs", {
         commands.add(["tab"],
             "Execute a command and tell it to output in a new tab",
             function (args) {
-                dactyl.withSavedValues(["forceNewTab"], function () {
-                    this.forceNewTab = true;
-                    commands.execute(args[0] || "", null, true);
+                dactyl.withSavedValues(["forceTarget"], function () {
+                    this.forceTarget = dactyl.NEW_TAB;
+                    dactyl.execute(args[0], null, true);
                 });
             }, {
-                argCount: "+",
+                argCount: "1",
+                completer: function (context) completion.ex(context),
+                literal: 0,
+                subCommand: 0
+            });
+
+        commands.add(["background", "bg"],
+            "Execute a command opening any new tabs in the background",
+            function (args) {
+                dactyl.withSavedValues(["forceBackground"], function () {
+                    this.forceBackground = true;
+                    dactyl.execute(args[0], null, true);
+                });
+            }, {
+                argCount: "1",
                 completer: function (context) completion.ex(context),
                 literal: 0,
                 subCommand: 0
@@ -628,7 +732,7 @@ var Tabs = Module("tabs", {
 
         commands.add(["tabl[ast]", "bl[ast]"],
             "Switch to the last tab",
-            function () tabs.select("$", false),
+            function () { tabs.select("$", false); },
             { argCount: "0" });
 
         // TODO: "Zero count" if 0 specified as arg
@@ -643,7 +747,7 @@ var Tabs = Module("tabs", {
                     if (/^\d+$/.test(arg))
                         tabs.select("-" + arg, true);
                     else
-                        dactyl.echoerr(_("error.trailing"));
+                        dactyl.echoerr(_("error.trailingCharacters"));
                 }
                 else if (count > 0)
                     tabs.select("-" + count, true);
@@ -666,7 +770,7 @@ var Tabs = Module("tabs", {
 
                     // count is ignored if an arg is specified, as per Vim
                     if (arg) {
-                        dactyl.assert(/^\d+$/.test(arg), _("error.trailing"));
+                        dactyl.assert(/^\d+$/.test(arg), _("error.trailingCharacters"));
                         index = arg - 1;
                     }
                     else
@@ -689,10 +793,15 @@ var Tabs = Module("tabs", {
             function () { tabs.select(0, false); },
             { argCount: "0" });
 
-        if (config.hasTabbrowser) {
+        if (config.has("tabbrowser")) {
             commands.add(["b[uffer]"],
                 "Switch to a buffer",
-                function (args) { tabs.switchTo(args[0], args.bang, args.count); }, {
+                function (args) {
+                    if (args.length)
+                        tabs.switchTo(args[0], args.bang, args.count);
+                    else if (args.count)
+                        tabs.switchTo(String(args.count));
+                }, {
                     argCount: "?",
                     bang: true,
                     count: true,
@@ -709,11 +818,9 @@ var Tabs = Module("tabs", {
                 });
 
             commands.add(["quita[ll]", "qa[ll]"],
-                "Quit " + config.appName,
-                function (args) { dactyl.quit(false, args.bang); }, {
-                    argCount: "0",
-                    bang: true
-                });
+                "Quit this " + config.appName + " window",
+                function (args) { window.close(); },
+                { argCount: "0" });
 
             commands.add(["reloada[ll]"],
                 "Reload all tab pages",
@@ -727,21 +834,24 @@ var Tabs = Module("tabs", {
                 function () { tabs.stopAll(); },
                 { argCount: "0" });
 
-            // TODO: add count support
+            // TODO: add count and bang multimatch support - unify with :buffer nonsense
             commands.add(["tabm[ove]"],
-                "Move the current tab after tab N",
+                "Move the current tab to the position of tab N",
                 function (args) {
                     let arg = args[0];
 
-                    // FIXME: tabmove! N should probably produce an error
-                    dactyl.assert(!arg || /^([+-]?\d+)$/.test(arg),
-                                  _("error.trailing"));
-
-                    // if not specified, move to after the last tab
-                    tabs.move(config.tabbrowser.mCurrentTab, arg || "$", args.bang);
+                    if (tabs.indexFromSpec(arg) == -1) {
+                        let list = [tab for (tab in tabs.match(args[0], args.count, true))];
+                        dactyl.assert(list.length, _("error.invalidArgument", arg));
+                        dactyl.assert(list.length == 1, _("buffer.multipleMatching", arg));
+                        arg = list[0];
+                    }
+                    tabs.move(tabs.getTab(), arg, args.bang);
                 }, {
-                    argCount: "?",
-                    bang: true
+                    argCount: "1",
+                    bang: true,
+                    completer: function (context, args) completion.buffer(context, true),
+                    literal: 0
                 });
 
             commands.add(["tabo[nly]"],
@@ -752,7 +862,8 @@ var Tabs = Module("tabs", {
             commands.add(["tabopen", "t[open]", "tabnew"],
                 "Open one or more URLs in a new tab",
                 function (args) {
-                    dactyl.open(args[0] || "about:blank", { from: "tabopen", where: dactyl.NEW_TAB, background: args.bang });
+                    dactyl.open(args[0] || "about:blank",
+                                { from: "tabopen", where: dactyl.NEW_TAB, background: args.bang });
                 }, {
                     bang: true,
                     completer: function (context) completion.url(context),
@@ -789,36 +900,72 @@ var Tabs = Module("tabs", {
             commands.add(["taba[ttach]"],
                 "Attach the current tab to another window",
                 function (args) {
-                    dactyl.assert(args.length <= 2 && !args.some(function (i) !/^\d+$/.test(i)),
-                                  _("error.trailing"));
+                    dactyl.assert(args.length <= 2 && !args.some(i => !/^\d+(?:$|:)/.test(i)),
+                                  _("error.trailingCharacters"));
+
+                    let [winIndex, tabIndex] = args.map(arg => parseInt(arg));
+                    if (args["-group"]) {
+                        util.assert(args.length == 1);
+                        window.TabView.moveTabTo(tabs.getTab(), winIndex);
+                        return;
+                    }
 
-                    let [winIndex, tabIndex] = args.map(parseInt);
                     let win = dactyl.windows[winIndex - 1];
+                    let sourceTab = tabs.getTab();
 
                     dactyl.assert(win, _("window.noIndex", winIndex));
                     dactyl.assert(win != window, _("window.cantAttachSame"));
 
-                    let browser = win.getBrowser();
-                    let dummy = browser.addTab("about:blank");
+                    let modules     = win.dactyl.modules;
+                    let { browser } = modules.config;
+
+                    let newTab = browser.addTab("about:blank");
                     browser.stop();
                     // XXX: the implementation of DnD in tabbrowser.xml suggests
                     // that we may not be guaranteed of having a docshell here
                     // without this reference?
                     browser.docShell;
 
-                    let last = browser.mTabs.length - 1;
+                    if (args[1]) {
+                        let { visibleTabs, allTabs } = modules.tabs;
+                        tabIndex = Math.constrain(tabIndex, 1, visibleTabs.length);
+                        let target = visibleTabs[tabIndex - 1];
+                        browser.moveTabTo(newTab, Array.indexOf(allTabs, target));
+                    }
 
-                    browser.moveTabTo(dummy, Math.constrain(tabIndex || last, 0, last));
-                    browser.selectedTab = dummy; // required
-                    browser.swapBrowsersAndCloseOther(dummy, config.tabbrowser.mCurrentTab);
+                    browser.selectedTab = newTab; // required
+                    browser.swapBrowsersAndCloseOther(newTab, sourceTab);
                 }, {
                     argCount: "+",
+                    literal: 1,
                     completer: function (context, args) {
-                        if (args.completeArg == 0) {
-                            context.filters.push(function ({ item }) item != window);
-                            completion.window(context);
+                        switch (args.completeArg) {
+                        case 0:
+                            if (args["-group"])
+                                completion.tabGroup(context);
+                            else {
+                                context.filters.push(({ item }) => item != window);
+                                completion.window(context);
+                            }
+                            break;
+                        case 1:
+                            if (!args["-group"]) {
+                                let win = dactyl.windows[Number(args[0]) - 1];
+                                if (!win || !win.dactyl)
+                                    context.message = _("Error", _("window.noIndex", winIndex));
+                                else
+                                    win.dactyl.modules.commands.get("tabmove").completer(context);
+                            }
+                            break;
                         }
-                    }
+                    },
+                    options: [
+                        {
+                            names: ["-group", "-g"],
+                            description: "Attach to a group rather than a window",
+                            type: CommandOption.NOARG
+                        }
+                    ]
                 });
         }
 
@@ -849,7 +996,9 @@ var Tabs = Module("tabs", {
                         context.anchored = false;
                         context.compare = CompletionContext.Sort.unsorted;
                         context.filters = [CompletionContext.Filter.textDescription];
-                        context.keys = { text: function ([i, { state: s }]) (i + 1) + ": " + s.entries[s.index - 1].url, description: "[1].title", icon: "[1].image" };
+                        context.keys = { text: function ([i, { state: s }]) (i + 1) + ": " + s.entries[s.index - 1].url,
+                                         description: "[1].title",
+                                         icon: "[1].image" };
                         context.completions = Iterator(tabs.closedTabs);
                     },
                     count: true,
@@ -875,16 +1024,109 @@ var Tabs = Module("tabs", {
                 { argCount: "0" });
         }
     },
-    events: function () {
+    completion: function initCompletion() {
+
+        completion.buffer = function buffer(context, visible) {
+            let { tabs } = modules;
+
+            let filter = context.filter.toLowerCase();
+
+            let defItem = { parent: { getTitle: function () "" } };
+
+            let tabGroups = {};
+            tabs.getGroups();
+            tabs[visible ? "visibleTabs" : "allTabs"].forEach(function (tab, i) {
+                let group = (tab.tabItem || tab._tabViewTabItem || defItem).parent || defItem.parent;
+                if (!hasOwnProperty(tabGroups, group.id))
+                    tabGroups[group.id] = [group.getTitle(), []];
+
+                group = tabGroups[group.id];
+                group[1].push([i, tab.linkedBrowser]);
+            });
+
+            context.pushProcessor(0, function (item, text, next) [
+                ["span", { highlight: "Indicator", style: "display: inline-block;" },
+                    item.indicator],
+                next.call(this, item, text)
+            ]);
+            context.process[1] = function (item, text) template.bookmarkDescription(item, template.highlightFilter(text, this.filter));
+
+            context.anchored = false;
+            context.keys = {
+                text: "text",
+                description: "url",
+                indicator: function (item) item.tab === tabs.getTab()  ? "%" :
+                                           item.tab === tabs.alternate ? "#" : " ",
+                icon: "icon",
+                id: "id",
+                command: function () "tabs.select"
+            };
+            context.compare = CompletionContext.Sort.number;
+            context.filters[0] = CompletionContext.Filter.textDescription;
+
+            for (let [id, vals] in Iterator(tabGroups))
+                context.fork(id, 0, this, function (context, [name, browsers]) {
+                    context.title = [name || "Buffers"];
+                    context.generate = () =>
+                        Array.map(browsers, function ([i, browser]) {
+                            let indicator = " ";
+                            if (i == tabs.index())
+                                indicator = "%";
+                            else if (i == tabs.index(tabs.alternate))
+                                indicator = "#";
+
+                            let tab = tabs.getTab(i, visible);
+                            let url = browser.contentDocument.location.href;
+                            i = i + 1;
+
+                            return {
+                                text: [i + ": " + (tab.label || /*L*/"(Untitled)"), i + ": " + url],
+                                tab: tab,
+                                id: i,
+                                url: url,
+                                icon: tab.image || BookmarkCache.DEFAULT_FAVICON
+                            };
+                        });
+                }, vals);
+        };
+
+        completion.tabGroup = function tabGroup(context) {
+            context.title = ["Tab Groups"];
+            context.keys = {
+                text: "id",
+                description: function (group) group.getTitle() ||
+                    group.getChildren().map(t => t.tab.label).join(", ")
+            };
+            context.generate = function () {
+                context.incomplete = true;
+                tabs.getGroups(function ({ GroupItems }) {
+                    context.incomplete = false;
+                    context.completions = GroupItems.groupItems;
+                });
+            };
+        };
+    },
+    events: function initEvents() {
         let tabContainer = config.tabbrowser.mTabContainer;
         function callback() {
             tabs.timeout(function () { this.updateTabCount(); });
         }
         for (let event in values(["TabMove", "TabOpen", "TabClose"]))
             events.listen(tabContainer, event, callback, false);
-        events.listen(tabContainer, "TabSelect", tabs.closure._onTabSelect, false);
+        events.listen(tabContainer, "TabSelect", tabs.bound._onTabSelect, false);
     },
-    mappings: function () {
+    mappings: function initMappings() {
+
+        mappings.add([modes.COMMAND], ["<C-t>", "<new-tab-next>"],
+            "Execute the next mapping in a new tab",
+            function ({ count }) {
+                dactyl.forceTarget = dactyl.NEW_TAB;
+                mappings.afterCommands((count || 1) + 1, function () {
+                    dactyl.forceTarget = null;
+                });
+            },
+            { count: true });
+
         mappings.add([modes.NORMAL], ["g0", "g^"],
             "Go to the first tab",
             function () { tabs.select(0); });
@@ -913,7 +1155,7 @@ var Tabs = Module("tabs", {
             function ({ count }) { tabs.select("-" + (count || 1), true); },
             { count: true });
 
-        if (config.hasTabbrowser) {
+        if (config.has("tabbrowser")) {
             mappings.add([modes.NORMAL], ["b"],
                 "Open a prompt to switch buffers",
                 function ({ count }) {
@@ -930,21 +1172,21 @@ var Tabs = Module("tabs", {
 
             mappings.add([modes.NORMAL], ["d"],
                 "Delete current buffer",
-                function ({ count }) { tabs.remove(tabs.getTab(), count, false); },
+                function ({ count }) { tabs.remove(tabs.getTab(), count || 1, false); },
                 { count: true });
 
             mappings.add([modes.NORMAL], ["D"],
                 "Delete current buffer, focus tab to the left",
-                function ({ count }) { tabs.remove(tabs.getTab(), count, true); },
+                function ({ count }) { tabs.remove(tabs.getTab(), count || 1, true); },
                 { count: true });
 
             mappings.add([modes.NORMAL], ["gb"],
-                "Repeat last :buffer[!] command",
+                "Repeat last :buffer command",
                 function ({ count }) { tabs.switchTo(null, null, count, false); },
                 { count: true });
 
             mappings.add([modes.NORMAL], ["gB"],
-                "Repeat last :buffer[!] command in reverse direction",
+                "Repeat last :buffer command in reverse direction",
                 function ({ count }) { tabs.switchTo(null, null, count, true); },
                 { count: true });
 
@@ -967,21 +1209,26 @@ var Tabs = Module("tabs", {
                 { count: true });
         }
     },
-    options: function () {
+    options: function initOptions() {
         options.add(["showtabline", "stal"],
             "Define when the tab bar is visible",
-            "string", config.defaults["showtabline"],
+            "string", true,
             {
                 setter: function (value) {
                     if (value === "never")
                         tabs.tabStyle.enabled = true;
                     else {
                         prefs.safeSet("browser.tabs.autoHide", value === "multitab",
-                                      "See 'showtabline' option.");
+                                      _("option.safeSet", "showtabline"));
                         tabs.tabStyle.enabled = false;
                     }
-                    if (value !== "multitab" || !dactyl.has("Gecko2"))
-                        config.tabStrip.collapsed = false;
+
+                    if (value !== "multitab")
+                        if (tabs.xulTabs)
+                            tabs.xulTabs.visible = value !== "never";
+                        else
+                            config.tabStrip.collapsed = false;
+
                     if (config.tabbrowser.tabContainer._positionPinnedTabs)
                         config.tabbrowser.tabContainer._positionPinnedTabs();
                     return value;
@@ -993,7 +1240,7 @@ var Tabs = Module("tabs", {
                 }
             });
 
-        if (config.hasTabbrowser) {
+        if (config.has("tabbrowser")) {
             let activateGroups = [
                 ["all", "Activate everything"],
                 ["addons", ":addo[ns] command"],
@@ -1015,12 +1262,12 @@ var Tabs = Module("tabs", {
                     values: activateGroups,
                     has: Option.has.toggleAll,
                     setter: function (newValues) {
-                        let valueSet = set(newValues);
+                        let valueSet = RealSet(newValues);
                         for (let group in values(activateGroups))
                             if (group[2])
                                 prefs.safeSet("browser.tabs." + group[2],
-                                              !(valueSet["all"] ^ valueSet[group[0]]),
-                                              "See the 'activate' option");
+                                              !(valueSet.has("all") ^ valueSet.has(group[0])),
+                                              _("option.safeSet", "activate"));
                         return newValues;
                     }
                 });
@@ -1031,11 +1278,8 @@ var Tabs = Module("tabs", {
                 {
                     values: {
                         "all": "All commands",
-                        "addons": ":addo[ns] command",
-                        "downloads": ":downl[oads] command",
                         "extoptions": ":exto[ptions] command",
                         "help": ":h[elp] command",
-                        "javascript": ":javascript! or :js! command",
                         "prefs": ":pref[erences]! or :prefs! command"
                     },
                     has: Option.has.toggleAll
@@ -1058,9 +1302,9 @@ var Tabs = Module("tabs", {
                         }
 
                         prefs.safeSet("browser.link.open_newwindow", open,
-                                      "See 'popups' option.");
+                                      _("option.safeSet", "popups"));
                         prefs.safeSet("browser.link.open_newwindow.restriction", restriction,
-                                      "See 'popups' option.");
+                                      _("option.safeSet", "popups"));
                         return values;
                     },
                     values: {
@@ -1073,4 +1317,4 @@ var Tabs = Module("tabs", {
     }
 });
 
-// vim: set fdm=marker sw=4 ts=4 et:
+// vim: set fdm=marker sw=4 sts=4 ts=8 et: