]> git.donarmstrong.com Git - dactyl.git/blob - common/content/tabs.js
Import r6976 from upstream hg supporting Firefox up to 25.*
[dactyl.git] / common / content / tabs.js
1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2013 Kris Maglione <maglione.k at Gmail>
4 //
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
7 "use strict";
8
9 /** @scope modules */
10
11 // TODO: many methods do not work with Thunderbird correctly yet
12
13 /**
14  * @instance tabs
15  */
16 var Tabs = Module("tabs", {
17     init: function () {
18         // used for the "gb" and "gB" mappings to remember the last :buffer[!] command
19         this._lastBufferSwitchArgs = "";
20         this._lastBufferSwitchSpecial = true;
21
22         this.xulTabs = document.getElementById("tabbrowser-tabs");
23
24         // hide tabs initially to prevent flickering when 'stal' would hide them
25         // on startup
26         if (config.has("tabbrowser"))
27             config.tabStrip.collapsed = true;
28
29         this.tabStyle = styles.system.add("tab-strip-hiding", config.styleableChrome,
30                                           (config.tabStrip.id ? "#" + config.tabStrip.id : ".tabbrowser-strip") +
31                                               "{ visibility: collapse; }",
32                                           false, true);
33
34         dactyl.commands["tabs.select"] = function (event) {
35             tabs.switchTo(event.originalTarget.getAttribute("identifier"));
36         };
37
38         this.tabBinding = styles.system.add("tab-binding", "chrome://browser/content/browser.xul", literal(/*
39                 xul|tab { -moz-binding: url(chrome://dactyl/content/bindings.xml#tab) !important; }
40             */).replace(/tab-./g, m => config.OS.isMacOSX ? "tab-mac" : m),
41             false, true);
42
43         this.timeout(function () {
44             for (let { linkedBrowser: { contentDocument } } in values(this.allTabs))
45                 if (contentDocument.readyState === "complete")
46                     dactyl.initDocument(contentDocument);
47         }, 1000);
48
49         if (window.TabsInTitlebar)
50             window.TabsInTitlebar.allowedBy("dactyl", false);
51     },
52
53     signals: {
54         enter: function enter() {
55             if (window.TabsInTitlebar)
56                 window.TabsInTitlebar.allowedBy("dactyl", true);
57         }
58     },
59
60     _alternates: Class.Memoize(() => [config.tabbrowser.mCurrentTab, null]),
61
62     cleanup: function cleanup() {
63         for (let [i, tab] in Iterator(this.allTabs)) {
64             let node = function node(class_) document.getAnonymousElementByAttribute(tab, "class", class_);
65             for (let elem in values(["dactyl-tab-icon-number", "dactyl-tab-number"].map(node)))
66                 if (elem)
67                     elem.parentNode.parentNode.removeChild(elem.parentNode);
68
69             delete tab.dactylOrdinal;
70             tab.removeAttribute("dactylOrdinal");
71         }
72     },
73
74     updateTabCount: function updateTabCount() {
75         for (let [i, tab] in Iterator(this.visibleTabs)) {
76             if (dactyl.has("Gecko2")) {
77                 let node = function node(class_) document.getAnonymousElementByAttribute(tab, "class", class_);
78                 if (!node("dactyl-tab-number")) {
79                     let img = node("tab-icon-image");
80                     if (img) {
81                         let dom = DOM([
82                             ["xul:hbox", { highlight: "tab-number" },
83                                 ["xul:label", { key: "icon", align: "center", highlight: "TabIconNumber",
84                                                 class: "dactyl-tab-icon-number" }]],
85                             ["xul:hbox", { highlight: "tab-number" },
86                                 ["html:div", { key: "label", highlight: "TabNumber",
87                                                class: "dactyl-tab-number" }]]],
88                             document).appendTo(img.parentNode);
89
90                         update(tab, {
91                             get dactylOrdinal() Number(dom.nodes.icon.value),
92                             set dactylOrdinal(i) {
93                                 dom.nodes.icon.value = dom.nodes.label.textContent = i;
94                                 this.setAttribute("dactylOrdinal", i);
95                             }
96                         });
97                     }
98                 }
99             }
100             tab.dactylOrdinal = i + 1;
101         }
102         statusline.updateTabCount(true);
103     },
104
105     _onTabSelect: function _onTabSelect() {
106         // TODO: is all of that necessary?
107         //       I vote no. --Kris
108         modes.reset();
109         statusline.updateTabCount(true);
110         this.updateSelectionHistory();
111     },
112
113     get allTabs() Array.slice(config.tabbrowser.tabContainer.childNodes),
114
115     /**
116      * @property {Object} The previously accessed tab or null if no tab
117      *     other than the current one has been accessed.
118      */
119     get alternate() this.allTabs.indexOf(this._alternates[1]) > -1 ? this._alternates[1] : null,
120
121     /**
122      * @property {Iterator(Object)} A genenerator that returns all browsers
123      *     in the current window.
124      */
125     get browsers() {
126         let browsers = config.tabbrowser.browsers;
127         for (let i = 0; i < browsers.length; i++)
128             if (browsers[i] !== undefined) // Bug in Google's Page Speed add-on.
129                 yield [i, browsers[i]];
130     },
131
132     /**
133      * @property {number} The number of tabs in the current window.
134      */
135     get count() config.tabbrowser.mTabs.length,
136
137     /**
138      * @property {Object} The local options store for the current tab.
139      */
140     get options() this.localStore.options,
141
142     get visibleTabs() config.tabbrowser.visibleTabs || this.allTabs.filter(tab => !tab.hidden),
143
144     /**
145      * Returns the local state store for the tab at the specified *tabIndex*.
146      * If *tabIndex* is not specified then the current tab is used.
147      *
148      * @param {number} tabIndex
149      * @returns {Object}
150      */
151     // FIXME: why not a tab arg? Why this and the property?
152     //      : To the latter question, because this works for any tab, the
153     //        property doesn't. And the property is so oft-used that it's
154     //        convenient. To the former question, because I think this is mainly
155     //        useful for autocommands, and they get index arguments. --Kris
156     getLocalStore: function getLocalStore(tabIndex) {
157         let tab = this.getTab(tabIndex);
158         if (!tab.dactylStore)
159             tab.dactylStore = Object.create(this.localStorePrototype);
160         return tab.dactylStore.instance = tab.dactylStore;
161     },
162
163     /**
164      * @property {Object} The local state store for the currently selected
165      *     tab.
166      */
167     get localStore() this.getLocalStore(),
168
169     localStorePrototype: memoize({
170         instance: {},
171         get options() ({})
172     }),
173
174     /**
175      * @property {[Object]} The array of closed tabs for the current
176      *     session.
177      */
178     get closedTabs() JSON.parse(services.sessionStore.getClosedTabData(window)),
179
180     /**
181      * Clones the specified *tab* and append it to the tab list.
182      *
183      * @param {Object} tab The tab to clone.
184      * @param {boolean} activate Whether to select the newly cloned tab.
185      */
186     cloneTab: function cloneTab(tab, activate) {
187         let newTab = config.tabbrowser.addTab("about:blank", { ownerTab: tab });
188         Tabs.copyTab(newTab, tab);
189
190         if (activate)
191             config.tabbrowser.mTabContainer.selectedItem = newTab;
192
193         return newTab;
194     },
195
196     /**
197      * Detaches the specified *tab* and open it in a new window. If no tab is
198      * specified the currently selected tab is detached.
199      *
200      * @param {Object} tab The tab to detach.
201      */
202     detachTab: function detachTab(tab) {
203         if (!tab)
204             tab = config.tabbrowser.mTabContainer.selectedItem;
205
206         services.windowWatcher
207                 .openWindow(window, window.getBrowserURL(), null, "chrome,dialog=no,all", tab);
208     },
209
210     /**
211      * Returns the index of the tab containing *content*.
212      *
213      * @param {Object} content Either a content window or a content
214      *     document.
215      */
216     // FIXME: Only called once...necessary?
217     getContentIndex: function getContentIndex(content) {
218         for (let [i, browser] in this.browsers) {
219             if (browser.contentWindow == content || browser.contentDocument == content)
220                 return i;
221         }
222         return -1;
223     },
224
225     /**
226      * If TabView exists, returns the Panorama window. If the Panorama
227      * is has not yet initialized, this function will not return until
228      * it has.
229      *
230      * @returns {Window}
231      */
232     getGroups: function getGroups(func) {
233         let iframe = document.getElementById("tab-view");
234         this._groups = iframe ? iframe.contentWindow : null;
235
236         if ("_groups" in this && !func)
237             return this._groups;
238
239         if (func)
240             func = bind(function (func) { func(this._groups); }, this, func);
241
242         if (window.TabView && window.TabView._initFrame)
243             window.TabView._initFrame(func);
244
245         this._groups = iframe ? iframe.contentWindow : null;
246         if (this._groups && !func)
247             util.waitFor(() => this._groups.TabItems);
248         return this._groups;
249     },
250
251     /**
252      * Returns the tab at the specified *index* or the currently selected tab
253      * if *index* is not specified. This is a 0-based index.
254      *
255      * @param {number|Node} index The index of the tab required or the tab itself
256      * @param {boolean} visible If true, consider only visible tabs rather than
257      *      all tabs.
258      * @returns {Object}
259      */
260     getTab: function getTab(index, visible) {
261         if (index instanceof Node)
262             return index;
263         if (index != null)
264             return this[visible ? "visibleTabs" : "allTabs"][index];
265         return config.tabbrowser.mCurrentTab;
266     },
267
268     /**
269      * Returns the index of *tab* or the index of the currently selected tab if
270      * *tab* is not specified. This is a 0-based index.
271      *
272      * @param {<xul:tab/>} tab A tab from the current tab list.
273      * @param {boolean} visible Whether to consider only visible tabs.
274      * @returns {number}
275      */
276     index: function index(tab, visible) {
277         let tabs = this[visible ? "visibleTabs" : "allTabs"];
278         return tabs.indexOf(tab || config.tabbrowser.mCurrentTab);
279     },
280
281     /**
282      * @param spec can either be:
283      * - an absolute integer
284      * - "" for the current tab
285      * - "+1" for the next tab
286      * - "-3" for the tab, which is 3 positions left of the current
287      * - "$" for the last tab
288      */
289     indexFromSpec: function indexFromSpec(spec, wrap, offset) {
290         if (spec instanceof Node)
291             return this.allTabs.indexOf(spec);
292
293         let tabs     = this.visibleTabs;
294         let position = this.index(null, true);
295
296         if (spec == null)
297             return -1;
298
299         if (spec === "")
300             return position;
301
302         if (/^\d+$/.test(spec))
303             position = parseInt(spec, 10) + (offset || 0);
304         else if (spec === "$")
305             position = tabs.length - 1;
306         else if (/^[+-]\d+$/.test(spec))
307             position += parseInt(spec, 10);
308         else
309             return -1;
310
311         if (position >= tabs.length)
312             position = wrap ? position % tabs.length : tabs.length - 1;
313         else if (position < 0)
314             position = wrap ? (position % tabs.length) + tabs.length : 0;
315
316         return this.allTabs.indexOf(tabs[position]);
317     },
318
319     /**
320      * Removes all tabs from the tab list except the specified *tab*.
321      *
322      * @param {Object} tab The tab to keep.
323      */
324     keepOnly: function keepOnly(tab) {
325         config.tabbrowser.removeAllTabsBut(tab);
326     },
327
328     /**
329      * Lists all tabs matching *filter*.
330      *
331      * @param {string} filter A filter matching a substring of the tab's
332      *     document title or URL.
333      */
334     list: function list(filter) {
335         completion.listCompleter("buffer", filter);
336     },
337
338     /**
339      * Return an iterator of tabs matching the given filter. If no
340      * *filter* or *count* is provided, returns the currently selected
341      * tab. If *filter* is a number or begins with a number followed
342      * by a colon, the tab of that ordinal is returned. Otherwise,
343      * tabs matching the filter as below are returned.
344      *
345      * @param {string} filter The filter. If *regexp*, this is a
346      *      regular expression against which the tab's URL or title
347      *      must match. Otherwise, it is a site filter.
348      *      @optional
349      * @param {number|null} count If non-null, return only the
350      *      *count*th matching tab.
351      *      @optional
352      * @param {boolean} regexp Whether to interpret *filter* as a
353      *      regular expression.
354      * @param {boolean} all If true, match against all tabs. If
355      *      false, match only tabs in the current tab group.
356      */
357     match: function match(filter, count, regexp, all) {
358         if (!filter && count == null)
359             yield tabs.getTab();
360         else if (!filter)
361             yield dactyl.assert(tabs.getTab(count - 1));
362         else {
363             let matches = /^(\d+)(?:$|:)/.exec(filter);
364             if (matches)
365                 yield dactyl.assert(count == null &&
366                                     tabs.getTab(parseInt(matches[1], 10) - 1, !all));
367             else {
368                 if (regexp)
369                     regexp = util.regexp(filter, "i");
370                 else
371                     var matcher = Styles.matchFilter(filter);
372
373                 for (let tab in values(tabs[all ? "allTabs" : "visibleTabs"])) {
374                     let browser = tab.linkedBrowser;
375                     let uri = browser.currentURI;
376                     let title;
377                     if (uri.spec == "about:blank")
378                         title = "(Untitled)";
379                     else
380                         title = browser.contentTitle;
381
382                     if (matcher && matcher(uri)
383                         || regexp && (regexp.test(title) || regexp.test(uri.spec)))
384                         if (count == null || --count == 0)
385                             yield tab;
386                 }
387             }
388         }
389     },
390
391     /**
392      * Moves a tab to a new position in the tab list.
393      *
394      * @param {Object} tab The tab to move.
395      * @param {string} spec See {@link Tabs.indexFromSpec}.
396      * @param {boolean} wrap Whether an out of bounds *spec* causes the
397      *     destination position to wrap around the start/end of the tab list.
398      */
399     move: function move(tab, spec, wrap) {
400         let index = tabs.indexFromSpec(spec, wrap, -1);
401         config.tabbrowser.moveTabTo(tab, index);
402     },
403
404     /**
405      * Removes the specified *tab* from the tab list.
406      *
407      * @param {Object} tab The tab to remove.
408      * @param {number} count How many tabs to remove.
409      * @param {boolean} focusLeftTab Focus the tab to the left of the removed tab.
410      */
411     remove: function remove(tab, count = 1, focusLeftTab = false) {
412         let res = this.count > count;
413
414         let tabs = this.visibleTabs;
415         if (tabs.indexOf(tab) < 0)
416             tabs = this.allTabs;
417         let index = tabs.indexOf(tab);
418
419         let next = index + (focusLeftTab ? -count : count);
420         if (!(next in tabs))
421             next = index + (focusLeftTab ? 1 : -1);
422         if (next in tabs) {
423             this._alternates[0] = tabs[next];
424             config.tabbrowser.mTabContainer.selectedItem = tabs[next];
425         }
426
427         if (focusLeftTab)
428             tabs.slice(Math.max(0, index + 1 - count),
429                        index + 1)
430                 .forEach(config.closure.removeTab);
431         else
432             tabs.slice(index,
433                        index + count)
434                 .forEach(config.closure.removeTab);
435         return res;
436     },
437
438     /**
439      * Reloads the specified tab.
440      *
441      * @param {Object} tab The tab to reload.
442      * @param {boolean} bypassCache Whether to bypass the cache when
443      *     reloading.
444      */
445     reload: function reload(tab, bypassCache) {
446         try {
447             if (bypassCache) {
448                 const flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
449                 config.tabbrowser.getBrowserForTab(tab).reloadWithFlags(flags);
450             }
451             else
452                 config.tabbrowser.reloadTab(tab);
453         }
454         catch (e if !(e instanceof Error)) {}
455     },
456
457     /**
458      * Reloads all tabs.
459      *
460      * @param {boolean} bypassCache Whether to bypass the cache when
461      *     reloading.
462      */
463     reloadAll: function reloadAll(bypassCache) {
464         this.visibleTabs.forEach(function (tab) {
465             try {
466                 tabs.reload(tab, bypassCache);
467             }
468             catch (e) {
469                 dactyl.reportError(e, true);
470             }
471         });
472     },
473
474     /**
475      * Selects the tab at the position specified by *spec*.
476      *
477      * @param {string} spec See {@link Tabs.indexFromSpec}
478      * @param {boolean} wrap Whether an out of bounds *spec* causes the
479      *     selection position to wrap around the start/end of the tab list.
480      */
481     select: function select(spec, wrap) {
482         let index = tabs.indexFromSpec(spec, wrap);
483         if (index == -1)
484             dactyl.beep();
485         else
486             config.tabbrowser.mTabContainer.selectedIndex = index;
487     },
488
489     /**
490      * Selects the alternate tab.
491      */
492     selectAlternateTab: function selectAlternateTab() {
493         dactyl.assert(tabs.alternate != null && tabs.getTab() != tabs.alternate,
494                       _("buffer.noAlternate"));
495         tabs.select(tabs.alternate);
496     },
497
498     /**
499      * Stops loading the specified tab.
500      *
501      * @param {Object} tab The tab to stop loading.
502      */
503     stop: function stop(tab) {
504         if (config.stop)
505             config.stop(tab);
506         else
507             tab.linkedBrowser.stop();
508     },
509
510     /**
511      * Stops loading all tabs.
512      */
513     stopAll: function stopAll() {
514         for (let [, browser] in this.browsers)
515             browser.stop();
516     },
517
518     /**
519      * Selects the tab containing the specified *buffer*.
520      *
521      * @param {string} buffer A string which matches the URL or title of a
522      *     buffer, if it is null, the last used string is used again.
523      * @param {boolean} allowNonUnique Whether to select the first of
524      *     multiple matches.
525      * @param {number} count If there are multiple matches select the
526      *     *count*th match.
527      * @param {boolean} reverse Whether to search the buffer list in
528      *     reverse order.
529      *
530      */
531     // FIXME: help!
532     switchTo: function switchTo(buffer, allowNonUnique, count, reverse) {
533         if (buffer != null) {
534             // store this command, so it can be repeated with "B"
535             this._lastBufferSwitchArgs = buffer;
536             this._lastBufferSwitchSpecial = allowNonUnique;
537         }
538         else {
539             buffer = this._lastBufferSwitchArgs;
540             if (allowNonUnique == null) // XXX
541                 allowNonUnique = this._lastBufferSwitchSpecial;
542         }
543
544         if (buffer == "#")
545             return tabs.selectAlternateTab();
546
547         reverse = Boolean(reverse);
548         count = Math.max(1, count || 1) * (1 + -2 * reverse);
549
550         let matches = buffer.match(/^(\d+):?/);
551         if (matches)
552             return tabs.select(this.allTabs[parseInt(matches[1], 10) - 1], false);
553
554         matches = array.nth(tabs.allTabs, t => (t.linkedBrowser.lastURI || {}).spec === buffer, 0);
555         if (matches)
556             return tabs.select(matches, false);
557
558         matches = completion.runCompleter("buffer", buffer).map(obj => obj.tab);
559
560         if (matches.length == 0)
561             dactyl.echoerr(_("buffer.noMatching", buffer));
562         else if (matches.length > 1 && !allowNonUnique)
563             dactyl.echoerr(_("buffer.multipleMatching", buffer));
564         else {
565             let start = matches.indexOf(tabs.getTab());
566             if (start == -1 && reverse)
567                 start++;
568
569             let index = (start + count) % matches.length;
570             if (index < 0)
571                 index = matches.length + index;
572             tabs.select(matches[index], false);
573         }
574     },
575
576     // NOTE: when restarting a session FF selects the first tab and then the
577     // tab that was selected when the session was created.  As a result the
578     // alternate after a restart is often incorrectly tab 1 when there
579     // shouldn't be one yet.
580     /**
581      * Sets the current and alternate tabs, updating the tab selection
582      * history.
583      *
584      * @param {Array(Object)} tabs The current and alternate tab.
585      * @see tabs#alternate
586      */
587     updateSelectionHistory: function updateSelectionHistory(tabs) {
588         if (!tabs) {
589             if (this.getTab() == this._alternates[0]
590                 || this.alternate && this.allTabs.indexOf(this._alternates[0]) == -1
591                 || this.alternate && config.tabbrowser._removingTabs && config.tabbrowser._removingTabs.indexOf(this._alternates[0]) >= 0)
592                 tabs = [this.getTab(), this.alternate];
593         }
594         this._alternates = tabs || [this.getTab(), this._alternates[0]];
595     }
596 }, {
597     copyTab: function (to, from) {
598         if (!from)
599             from = config.tabbrowser.mTabContainer.selectedItem;
600
601         let tabState = services.sessionStore.getTabState(from);
602         services.sessionStore.setTabState(to, tabState);
603     }
604 }, {
605     load: function initLoad() {
606         tabs.updateTabCount();
607     },
608     commands: function initCommands() {
609         [
610             {
611                 name: ["bd[elete]"],
612                 description: "Delete matching buffers",
613                 visible: false
614             },
615             {
616                 name: ["tabc[lose]"],
617                 description: "Delete matching tabs",
618                 visible: true
619             }
620         ].forEach(function (params) {
621             commands.add(params.name, params.description,
622                 function (args) {
623                     let removed = 0;
624                     for (let tab in tabs.match(args[0], args.count, args.bang, !params.visible)) {
625                         config.removeTab(tab);
626                         removed++;
627                     }
628
629                     if (args[0])
630                         if (removed > 0)
631                             dactyl.echomsg(_("buffer.fewerTab" + (removed == 1 ? "" : "s"), removed), 9);
632                         else
633                             dactyl.echoerr(_("buffer.noMatching", args[0]));
634                 }, {
635                     argCount: "?",
636                     bang: true,
637                     count: true,
638                     completer: function (context) completion.buffer(context),
639                     literal: 0,
640                     privateData: true
641                 });
642         });
643
644         commands.add(["pin[tab]"],
645             "Pin tab as an application tab",
646             function (args) {
647                 for (let tab in tabs.match(args[0], args.count))
648                     config.browser[!args.bang || !tab.pinned ? "pinTab" : "unpinTab"](tab);
649             },
650             {
651                 argCount: "?",
652                 bang: true,
653                 count: true,
654                 completer: function (context, args) {
655                     if (!args.bang)
656                         context.filters.push(({ item }) => !item.tab.pinned);
657                     completion.buffer(context);
658                 }
659             });
660
661         commands.add(["unpin[tab]"],
662             "Unpin tab as an application tab",
663             function (args) {
664                 for (let tab in tabs.match(args[0], args.count))
665                     config.browser.unpinTab(tab);
666             },
667             {
668                 argCount: "?",
669                 count: true,
670                 completer: function (context, args) {
671                     context.filters.push(({ item }) => item.tab.pinned);
672                     completion.buffer(context);
673                 }
674             });
675
676         commands.add(["keepa[lt]"],
677             "Execute a command without changing the current alternate buffer",
678             function (args) {
679                 try {
680                     dactyl.execute(args[0], null, true);
681                 }
682                 finally {
683                     tabs.updateSelectionHistory([tabs.getTab(), tabs.alternate]);
684                 }
685             }, {
686                 argCount: "1",
687                 completer: function (context) completion.ex(context),
688                 literal: 0,
689                 subCommand: 0
690             });
691
692         commands.add(["tab"],
693             "Execute a command and tell it to output in a new tab",
694             function (args) {
695                 dactyl.withSavedValues(["forceTarget"], function () {
696                     this.forceTarget = dactyl.NEW_TAB;
697                     dactyl.execute(args[0], null, true);
698                 });
699             }, {
700                 argCount: "1",
701                 completer: function (context) completion.ex(context),
702                 literal: 0,
703                 subCommand: 0
704             });
705
706         commands.add(["background", "bg"],
707             "Execute a command opening any new tabs in the background",
708             function (args) {
709                 dactyl.withSavedValues(["forceBackground"], function () {
710                     this.forceBackground = true;
711                     dactyl.execute(args[0], null, true);
712                 });
713             }, {
714                 argCount: "1",
715                 completer: function (context) completion.ex(context),
716                 literal: 0,
717                 subCommand: 0
718             });
719
720         commands.add(["tabd[o]", "bufd[o]"],
721             "Execute a command in each tab",
722             function (args) {
723                 for (let tab in values(tabs.visibleTabs)) {
724                     tabs.select(tab);
725                     dactyl.execute(args[0], null, true);
726                 }
727             }, {
728                 argCount: "1",
729                 completer: function (context) completion.ex(context),
730                 literal: 0,
731                 subCommand: 0
732             });
733
734         commands.add(["tabl[ast]", "bl[ast]"],
735             "Switch to the last tab",
736             function () { tabs.select("$", false); },
737             { argCount: "0" });
738
739         // TODO: "Zero count" if 0 specified as arg
740         commands.add(["tabp[revious]", "tp[revious]", "tabN[ext]", "tN[ext]", "bp[revious]", "bN[ext]"],
741             "Switch to the previous tab or go [count] tabs back",
742             function (args) {
743                 let count = args.count;
744                 let arg   = args[0];
745
746                 // count is ignored if an arg is specified, as per Vim
747                 if (arg) {
748                     if (/^\d+$/.test(arg))
749                         tabs.select("-" + arg, true);
750                     else
751                         dactyl.echoerr(_("error.trailingCharacters"));
752                 }
753                 else if (count > 0)
754                     tabs.select("-" + count, true);
755                 else
756                     tabs.select("-1", true);
757             }, {
758                 argCount: "?",
759                 count: true
760             });
761
762         // TODO: "Zero count" if 0 specified as arg
763         commands.add(["tabn[ext]", "tn[ext]", "bn[ext]"],
764             "Switch to the next or [count]th tab",
765             function (args) {
766                 let count = args.count;
767                 let arg   = args[0];
768
769                 if (arg || count > 0) {
770                     let index;
771
772                     // count is ignored if an arg is specified, as per Vim
773                     if (arg) {
774                         dactyl.assert(/^\d+$/.test(arg), _("error.trailingCharacters"));
775                         index = arg - 1;
776                     }
777                     else
778                         index = count - 1;
779
780                     if (index < tabs.count)
781                         tabs.select(index, true);
782                     else
783                         dactyl.beep();
784                 }
785                 else
786                     tabs.select("+1", true);
787             }, {
788                 argCount: "?",
789                 count: true
790             });
791
792         commands.add(["tabr[ewind]", "tabfir[st]", "br[ewind]", "bf[irst]"],
793             "Switch to the first tab",
794             function () { tabs.select(0, false); },
795             { argCount: "0" });
796
797         if (config.has("tabbrowser")) {
798             commands.add(["b[uffer]"],
799                 "Switch to a buffer",
800                 function (args) {
801                     if (args.length)
802                         tabs.switchTo(args[0], args.bang, args.count);
803                     else if (args.count)
804                         tabs.switchTo(String(args.count));
805                 }, {
806                     argCount: "?",
807                     bang: true,
808                     count: true,
809                     completer: function (context) completion.buffer(context),
810                     literal: 0,
811                     privateData: true
812                 });
813
814             commands.add(["buffers", "files", "ls", "tabs"],
815                 "Show a list of all buffers",
816                 function (args) { tabs.list(args[0] || ""); }, {
817                     argCount: "?",
818                     literal: 0
819                 });
820
821             commands.add(["quita[ll]", "qa[ll]"],
822                 "Quit this " + config.appName + " window",
823                 function (args) { window.close(); },
824                 { argCount: "0" });
825
826             commands.add(["reloada[ll]"],
827                 "Reload all tab pages",
828                 function (args) { tabs.reloadAll(args.bang); }, {
829                     argCount: "0",
830                     bang: true
831                 });
832
833             commands.add(["stopa[ll]"],
834                 "Stop loading all tab pages",
835                 function () { tabs.stopAll(); },
836                 { argCount: "0" });
837
838             // TODO: add count and bang multimatch support - unify with :buffer nonsense
839             commands.add(["tabm[ove]"],
840                 "Move the current tab to the position of tab N",
841                 function (args) {
842                     let arg = args[0];
843
844                     if (tabs.indexFromSpec(arg) == -1) {
845                         let list = [tab for (tab in tabs.match(args[0], args.count, true))];
846                         dactyl.assert(list.length, _("error.invalidArgument", arg));
847                         dactyl.assert(list.length == 1, _("buffer.multipleMatching", arg));
848                         arg = list[0];
849                     }
850                     tabs.move(tabs.getTab(), arg, args.bang);
851                 }, {
852                     argCount: "1",
853                     bang: true,
854                     completer: function (context, args) completion.buffer(context, true),
855                     literal: 0
856                 });
857
858             commands.add(["tabo[nly]"],
859                 "Close all other tabs",
860                 function () { tabs.keepOnly(tabs.getTab()); },
861                 { argCount: "0" });
862
863             commands.add(["tabopen", "t[open]", "tabnew"],
864                 "Open one or more URLs in a new tab",
865                 function (args) {
866                     dactyl.open(args[0] || "about:blank",
867                                 { from: "tabopen", where: dactyl.NEW_TAB, background: args.bang });
868                 }, {
869                     bang: true,
870                     completer: function (context) completion.url(context),
871                     domains: function (args) commands.get("open").domains(args),
872                     literal: 0,
873                     privateData: true
874                 });
875
876             commands.add(["tabde[tach]"],
877                 "Detach current tab to its own window",
878                 function () { tabs.detachTab(null); },
879                 { argCount: "0" });
880
881             commands.add(["tabdu[plicate]"],
882                 "Duplicate current tab",
883                 function (args) {
884                     let tab = tabs.getTab();
885
886                     let activate = args.bang ? true : false;
887                     if (options.get("activate").has("tabopen"))
888                         activate = !activate;
889
890                     for (let i in util.range(0, Math.max(1, args.count)))
891                         tabs.cloneTab(tab, activate);
892                 }, {
893                     argCount: "0",
894                     bang: true,
895                     count: true
896                 });
897
898             // TODO: match window by title too?
899             //     : accept the full :tabmove arg spec for the tab index arg?
900             //     : better name or merge with :tabmove?
901             commands.add(["taba[ttach]"],
902                 "Attach the current tab to another window",
903                 function (args) {
904                     dactyl.assert(args.length <= 2 && !args.some(i => !/^\d+(?:$|:)/.test(i)),
905                                   _("error.trailingCharacters"));
906
907                     let [winIndex, tabIndex] = args.map(arg => parseInt(arg));
908                     if (args["-group"]) {
909                         util.assert(args.length == 1);
910                         window.TabView.moveTabTo(tabs.getTab(), winIndex);
911                         return;
912                     }
913
914                     let win = dactyl.windows[winIndex - 1];
915                     let sourceTab = tabs.getTab();
916
917                     dactyl.assert(win, _("window.noIndex", winIndex));
918                     dactyl.assert(win != window, _("window.cantAttachSame"));
919
920                     let modules     = win.dactyl.modules;
921                     let { browser } = modules.config;
922
923                     let newTab = browser.addTab("about:blank");
924                     browser.stop();
925                     // XXX: the implementation of DnD in tabbrowser.xml suggests
926                     // that we may not be guaranteed of having a docshell here
927                     // without this reference?
928                     browser.docShell;
929
930                     if (args[1]) {
931                         let { visibleTabs, allTabs } = modules.tabs;
932                         tabIndex = Math.constrain(tabIndex, 1, visibleTabs.length);
933                         let target = visibleTabs[tabIndex - 1];
934                         browser.moveTabTo(newTab, Array.indexOf(allTabs, target));
935                     }
936
937                     browser.selectedTab = newTab; // required
938                     browser.swapBrowsersAndCloseOther(newTab, sourceTab);
939                 }, {
940                     argCount: "+",
941                     literal: 1,
942                     completer: function (context, args) {
943                         switch (args.completeArg) {
944                         case 0:
945                             if (args["-group"])
946                                 completion.tabGroup(context);
947                             else {
948                                 context.filters.push(({ item }) => item != window);
949                                 completion.window(context);
950                             }
951                             break;
952                         case 1:
953                             if (!args["-group"]) {
954                                 let win = dactyl.windows[Number(args[0]) - 1];
955                                 if (!win || !win.dactyl)
956                                     context.message = _("Error", _("window.noIndex", winIndex));
957                                 else
958                                     win.dactyl.modules.commands.get("tabmove").completer(context);
959                             }
960                             break;
961                         }
962                     },
963                     options: [
964                         {
965                             names: ["-group", "-g"],
966                             description: "Attach to a group rather than a window",
967                             type: CommandOption.NOARG
968                         }
969                     ]
970                 });
971         }
972
973         if (dactyl.has("tabs_undo")) {
974             commands.add(["u[ndo]"],
975                 "Undo closing of a tab",
976                 function (args) {
977                     if (args.length)
978                         args = args[0];
979                     else
980                         args = args.count || 0;
981
982                     let m = /^(\d+)(:|$)/.exec(args || '1');
983                     if (m)
984                         window.undoCloseTab(Number(m[1]) - 1);
985                     else if (args) {
986                         for (let [i, item] in Iterator(tabs.closedTabs))
987                             if (item.state.entries[item.state.index - 1].url == args) {
988                                 window.undoCloseTab(i);
989                                 return;
990                             }
991
992                         dactyl.echoerr(_("buffer.noClosed"));
993                     }
994                 }, {
995                     argCount: "?",
996                     completer: function (context) {
997                         context.anchored = false;
998                         context.compare = CompletionContext.Sort.unsorted;
999                         context.filters = [CompletionContext.Filter.textDescription];
1000                         context.keys = { text: function ([i, { state: s }]) (i + 1) + ": " + s.entries[s.index - 1].url,
1001                                          description: "[1].title",
1002                                          icon: "[1].image" };
1003                         context.completions = Iterator(tabs.closedTabs);
1004                     },
1005                     count: true,
1006                     literal: 0,
1007                     privateData: true
1008                 });
1009
1010             commands.add(["undoa[ll]"],
1011                 "Undo closing of all closed tabs",
1012                 function (args) {
1013                     for (let i in Iterator(tabs.closedTabs))
1014                         window.undoCloseTab(0);
1015
1016                 },
1017                 { argCount: "0" });
1018
1019         }
1020
1021         if (dactyl.has("session")) {
1022             commands.add(["wqa[ll]", "wq", "xa[ll]"],
1023                 "Save the session and quit",
1024                 function () { dactyl.quit(true); },
1025                 { argCount: "0" });
1026         }
1027     },
1028     completion: function initCompletion() {
1029
1030         completion.buffer = function buffer(context, visible) {
1031             let { tabs } = modules;
1032
1033             let filter = context.filter.toLowerCase();
1034
1035             let defItem = { parent: { getTitle: function () "" } };
1036
1037             let tabGroups = {};
1038             tabs.getGroups();
1039             tabs[visible ? "visibleTabs" : "allTabs"].forEach(function (tab, i) {
1040                 let group = (tab.tabItem || tab._tabViewTabItem || defItem).parent || defItem.parent;
1041                 if (!Set.has(tabGroups, group.id))
1042                     tabGroups[group.id] = [group.getTitle(), []];
1043
1044                 group = tabGroups[group.id];
1045                 group[1].push([i, tab.linkedBrowser]);
1046             });
1047
1048             context.pushProcessor(0, function (item, text, next) [
1049                 ["span", { highlight: "Indicator", style: "display: inline-block;" },
1050                     item.indicator],
1051                 next.call(this, item, text)
1052             ]);
1053             context.process[1] = function (item, text) template.bookmarkDescription(item, template.highlightFilter(text, this.filter));
1054
1055             context.anchored = false;
1056             context.keys = {
1057                 text: "text",
1058                 description: "url",
1059                 indicator: function (item) item.tab === tabs.getTab()  ? "%" :
1060                                            item.tab === tabs.alternate ? "#" : " ",
1061                 icon: "icon",
1062                 id: "id",
1063                 command: function () "tabs.select"
1064             };
1065             context.compare = CompletionContext.Sort.number;
1066             context.filters[0] = CompletionContext.Filter.textDescription;
1067
1068             for (let [id, vals] in Iterator(tabGroups))
1069                 context.fork(id, 0, this, function (context, [name, browsers]) {
1070                     context.title = [name || "Buffers"];
1071                     context.generate = () =>
1072                         Array.map(browsers, function ([i, browser]) {
1073                             let indicator = " ";
1074                             if (i == tabs.index())
1075                                 indicator = "%";
1076                             else if (i == tabs.index(tabs.alternate))
1077                                 indicator = "#";
1078
1079                             let tab = tabs.getTab(i, visible);
1080                             let url = browser.contentDocument.location.href;
1081                             i = i + 1;
1082
1083                             return {
1084                                 text: [i + ": " + (tab.label || /*L*/"(Untitled)"), i + ": " + url],
1085                                 tab: tab,
1086                                 id: i,
1087                                 url: url,
1088                                 icon: tab.image || BookmarkCache.DEFAULT_FAVICON
1089                             };
1090                         });
1091                 }, vals);
1092         };
1093
1094         completion.tabGroup = function tabGroup(context) {
1095             context.title = ["Tab Groups"];
1096             context.keys = {
1097                 text: "id",
1098                 description: function (group) group.getTitle() ||
1099                     group.getChildren().map(t => t.tab.label).join(", ")
1100             };
1101             context.generate = function () {
1102                 context.incomplete = true;
1103                 tabs.getGroups(function ({ GroupItems }) {
1104                     context.incomplete = false;
1105                     context.completions = GroupItems.groupItems;
1106                 });
1107             };
1108         };
1109     },
1110     events: function initEvents() {
1111         let tabContainer = config.tabbrowser.mTabContainer;
1112         function callback() {
1113             tabs.timeout(function () { this.updateTabCount(); });
1114         }
1115         for (let event in values(["TabMove", "TabOpen", "TabClose"]))
1116             events.listen(tabContainer, event, callback, false);
1117         events.listen(tabContainer, "TabSelect", tabs.closure._onTabSelect, false);
1118     },
1119     mappings: function initMappings() {
1120
1121         mappings.add([modes.COMMAND], ["<C-t>", "<new-tab-next>"],
1122             "Execute the next mapping in a new tab",
1123             function ({ count }) {
1124                 dactyl.forceTarget = dactyl.NEW_TAB;
1125                 mappings.afterCommands((count || 1) + 1, function () {
1126                     dactyl.forceTarget = null;
1127                 });
1128             },
1129             { count: true });
1130
1131         mappings.add([modes.NORMAL], ["g0", "g^"],
1132             "Go to the first tab",
1133             function () { tabs.select(0); });
1134
1135         mappings.add([modes.NORMAL], ["g$"],
1136             "Go to the last tab",
1137             function () { tabs.select("$"); });
1138
1139         mappings.add([modes.NORMAL], ["gt"],
1140             "Go to the next tab",
1141             function ({ count }) {
1142                 if (count != null)
1143                     tabs.select(count - 1, false);
1144                 else
1145                     tabs.select("+1", true);
1146             },
1147             { count: true });
1148
1149         mappings.add([modes.NORMAL], ["<C-n>", "<C-Tab>", "<C-PageDown>"],
1150             "Go to the next tab",
1151             function ({ count }) { tabs.select("+" + (count || 1), true); },
1152             { count: true });
1153
1154         mappings.add([modes.NORMAL], ["gT", "<C-p>", "<C-S-Tab>", "<C-PageUp>"],
1155            "Go to previous tab",
1156             function ({ count }) { tabs.select("-" + (count || 1), true); },
1157             { count: true });
1158
1159         if (config.has("tabbrowser")) {
1160             mappings.add([modes.NORMAL], ["b"],
1161                 "Open a prompt to switch buffers",
1162                 function ({ count }) {
1163                     if (count != null)
1164                         tabs.switchTo(String(count));
1165                     else
1166                         CommandExMode().open("buffer! ");
1167                 },
1168                 { count: true });
1169
1170             mappings.add([modes.NORMAL], ["B"],
1171                 "Show buffer list",
1172                 function () { tabs.list(false); });
1173
1174             mappings.add([modes.NORMAL], ["d"],
1175                 "Delete current buffer",
1176                 function ({ count }) { tabs.remove(tabs.getTab(), count || 1, false); },
1177                 { count: true });
1178
1179             mappings.add([modes.NORMAL], ["D"],
1180                 "Delete current buffer, focus tab to the left",
1181                 function ({ count }) { tabs.remove(tabs.getTab(), count || 1, true); },
1182                 { count: true });
1183
1184             mappings.add([modes.NORMAL], ["gb"],
1185                 "Repeat last :buffer command",
1186                 function ({ count }) { tabs.switchTo(null, null, count, false); },
1187                 { count: true });
1188
1189             mappings.add([modes.NORMAL], ["gB"],
1190                 "Repeat last :buffer command in reverse direction",
1191                 function ({ count }) { tabs.switchTo(null, null, count, true); },
1192                 { count: true });
1193
1194             // TODO: feature dependencies - implies "session"?
1195             if (dactyl.has("tabs_undo")) {
1196                 mappings.add([modes.NORMAL], ["u"],
1197                     "Undo closing of a tab",
1198                     function ({ count }) { ex.undo({ "#": count }); },
1199                     { count: true });
1200             }
1201
1202             mappings.add([modes.NORMAL], ["<C-^>", "<C-6>"],
1203                 "Select the alternate tab or the [count]th tab",
1204                 function ({ count }) {
1205                     if (count != null)
1206                         tabs.switchTo(String(count), false);
1207                     else
1208                         tabs.selectAlternateTab();
1209                 },
1210                 { count: true });
1211         }
1212     },
1213     options: function initOptions() {
1214         options.add(["showtabline", "stal"],
1215             "Define when the tab bar is visible",
1216             "string", true,
1217             {
1218                 setter: function (value) {
1219                     if (value === "never")
1220                         tabs.tabStyle.enabled = true;
1221                     else {
1222                         prefs.safeSet("browser.tabs.autoHide", value === "multitab",
1223                                       _("option.safeSet", "showtabline"));
1224                         tabs.tabStyle.enabled = false;
1225                     }
1226
1227                     if (value !== "multitab" || !dactyl.has("Gecko2"))
1228                         if (tabs.xulTabs)
1229                             tabs.xulTabs.visible = value !== "never";
1230                         else
1231                             config.tabStrip.collapsed = false;
1232
1233                     if (config.tabbrowser.tabContainer._positionPinnedTabs)
1234                         config.tabbrowser.tabContainer._positionPinnedTabs();
1235                     return value;
1236                 },
1237                 values: {
1238                     "never":    "Never show the tab bar",
1239                     "multitab": "Show the tab bar when there are multiple tabs",
1240                     "always":   "Always show the tab bar"
1241                 }
1242             });
1243
1244         if (config.has("tabbrowser")) {
1245             let activateGroups = [
1246                 ["all", "Activate everything"],
1247                 ["addons", ":addo[ns] command"],
1248                 ["bookmarks", "Tabs loaded from bookmarks", "loadBookmarksInBackground"],
1249                 ["diverted", "Links with targets set to new tabs", "loadDivertedInBackground"],
1250                 ["downloads", ":downl[oads] command"],
1251                 ["extoptions", ":exto[ptions] command"],
1252                 ["help", ":h[elp] command"],
1253                 ["homepage", "gH mapping"],
1254                 ["links", "Middle- or Control-clicked links", "loadInBackground"],
1255                 ["quickmark", "go and gn mappings"],
1256                 ["tabopen", ":tabopen[!] command"],
1257                 ["paste", "P and gP mappings"]
1258             ];
1259             options.add(["activate", "act"],
1260                 "Define when newly created tabs are automatically activated",
1261                 "stringlist", [g[0] for (g in values(activateGroups.slice(1))) if (!g[2] || !prefs.get("browser.tabs." + g[2]))].join(","),
1262                 {
1263                     values: activateGroups,
1264                     has: Option.has.toggleAll,
1265                     setter: function (newValues) {
1266                         let valueSet = Set(newValues);
1267                         for (let group in values(activateGroups))
1268                             if (group[2])
1269                                 prefs.safeSet("browser.tabs." + group[2],
1270                                               !(valueSet["all"] ^ valueSet[group[0]]),
1271                                               _("option.safeSet", "activate"));
1272                         return newValues;
1273                     }
1274                 });
1275
1276             options.add(["newtab"],
1277                 "Define which commands should output in a new tab by default",
1278                 "stringlist", "",
1279                 {
1280                     values: {
1281                         "all": "All commands",
1282                         "extoptions": ":exto[ptions] command",
1283                         "help": ":h[elp] command",
1284                         "prefs": ":pref[erences]! or :prefs! command"
1285                     },
1286                     has: Option.has.toggleAll
1287                 });
1288
1289             // TODO: Is this really applicable to Melodactyl?
1290             options.add(["popups", "pps"],
1291                 "Where to show requested popup windows",
1292                 "stringlist", "tab",
1293                 {
1294                     setter: function (values) {
1295                         let open = 1, restriction = 0;
1296                         for (let [, opt] in Iterator(values)) {
1297                             if (opt == "tab")
1298                                 open = 3;
1299                             else if (opt == "window")
1300                                 open = 2;
1301                             else if (opt == "resized")
1302                                 restriction = 2;
1303                         }
1304
1305                         prefs.safeSet("browser.link.open_newwindow", open,
1306                                       _("option.safeSet", "popups"));
1307                         prefs.safeSet("browser.link.open_newwindow.restriction", restriction,
1308                                       _("option.safeSet", "popups"));
1309                         return values;
1310                     },
1311                     values: {
1312                         "tab":     "Open popups in a new tab",
1313                         "window":  "Open popups in a new window",
1314                         "resized": "Open resized popups in a new window"
1315                     }
1316                 });
1317         }
1318     }
1319 });
1320
1321 // vim: set fdm=marker sw=4 sts=4 ts=8 et: