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