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