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>
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
11 // TODO: many methods do not work with Thunderbird correctly yet
16 var Tabs = Module("tabs", {
18 // used for the "gb" and "gB" mappings to remember the last :buffer[!] command
19 this._lastBufferSwitchArgs = "";
20 this._lastBufferSwitchSpecial = true;
22 this.xulTabs = document.getElementById("tabbrowser-tabs");
24 // hide tabs initially to prevent flickering when 'stal' would hide them
26 if (config.has("tabbrowser"))
27 config.tabStrip.collapsed = true;
29 this.tabStyle = styles.system.add("tab-strip-hiding", config.styleableChrome,
30 (config.tabStrip.id ? "#" + config.tabStrip.id : ".tabbrowser-strip") +
31 "{ visibility: collapse; }",
34 dactyl.commands["tabs.select"] = function (event) {
35 tabs.switchTo(event.originalTarget.getAttribute("identifier"));
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),
43 this.timeout(function () {
44 for (let { linkedBrowser: { contentDocument } } in values(this.allTabs))
45 if (contentDocument.readyState === "complete")
46 dactyl.initDocument(contentDocument);
49 if (window.TabsInTitlebar)
50 window.TabsInTitlebar.allowedBy("dactyl", false);
54 enter: function enter() {
55 if (window.TabsInTitlebar)
56 window.TabsInTitlebar.allowedBy("dactyl", true);
60 _alternates: Class.Memoize(function () [config.tabbrowser.mCurrentTab, null]),
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)))
67 elem.parentNode.parentNode.removeChild(elem.parentNode);
69 delete tab.dactylOrdinal;
70 tab.removeAttribute("dactylOrdinal");
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");
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);
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);
96 tab.dactylOrdinal = i + 1;
98 statusline.updateTabCount(true);
101 _onTabSelect: function _onTabSelect() {
102 // TODO: is all of that necessary?
105 statusline.updateTabCount(true);
106 this.updateSelectionHistory();
109 get allTabs() Array.slice(config.tabbrowser.tabContainer.childNodes),
112 * @property {Object} The previously accessed tab or null if no tab
113 * other than the current one has been accessed.
115 get alternate() this.allTabs.indexOf(this._alternates[1]) > -1 ? this._alternates[1] : null,
118 * @property {Iterator(Object)} A genenerator that returns all browsers
119 * in the current window.
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]];
129 * @property {number} The number of tabs in the current window.
131 get count() config.tabbrowser.mTabs.length,
134 * @property {Object} The local options store for the current tab.
136 get options() this.localStore.options,
138 get visibleTabs() config.tabbrowser.visibleTabs || this.allTabs.filter(function (tab) !tab.hidden),
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.
144 * @param {number} tabIndex
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;
160 * @property {Object} The local state store for the currently selected
163 get localStore() this.getLocalStore(),
165 localStorePrototype: memoize({
171 * @property {[Object]} The array of closed tabs for the current
174 get closedTabs() JSON.parse(services.sessionStore.getClosedTabData(window)),
177 * Clones the specified *tab* and append it to the tab list.
179 * @param {Object} tab The tab to clone.
180 * @param {boolean} activate Whether to select the newly cloned tab.
182 cloneTab: function cloneTab(tab, activate) {
183 let newTab = config.tabbrowser.addTab("about:blank", { ownerTab: tab });
184 Tabs.copyTab(newTab, tab);
187 config.tabbrowser.mTabContainer.selectedItem = newTab;
193 * Detaches the specified *tab* and open it in a new window. If no tab is
194 * specified the currently selected tab is detached.
196 * @param {Object} tab The tab to detach.
198 detachTab: function detachTab(tab) {
200 tab = config.tabbrowser.mTabContainer.selectedItem;
202 services.windowWatcher
203 .openWindow(window, window.getBrowserURL(), null, "chrome,dialog=no,all", tab);
207 * Returns the index of the tab containing *content*.
209 * @param {Object} content Either a content window or a content
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)
222 * If TabView exists, returns the Panorama window. If the Panorama
223 * is has not yet initialized, this function will not return until
228 getGroups: function getGroups(func) {
229 let iframe = document.getElementById("tab-view");
230 this._groups = iframe ? iframe.contentWindow : null;
232 if ("_groups" in this && !func)
236 func = bind(function (func) { func(this._groups) }, this, func);
238 if (window.TabView && TabView._initFrame)
239 TabView._initFrame(func);
241 this._groups = iframe ? iframe.contentWindow : null;
242 if (this._groups && !func)
243 util.waitFor(function () this._groups.TabItems, this);
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.
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
256 getTab: function getTab(index, visible) {
257 if (index instanceof Node)
260 return this[visible ? "visibleTabs" : "allTabs"][index];
261 return config.tabbrowser.mCurrentTab;
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.
268 * @param {<xul:tab/>} tab A tab from the current tab list.
269 * @param {boolean} visible Whether to consider only visible tabs.
272 index: function index(tab, visible) {
273 let tabs = this[visible ? "visibleTabs" : "allTabs"];
274 return tabs.indexOf(tab || config.tabbrowser.mCurrentTab);
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
285 indexFromSpec: function indexFromSpec(spec, wrap, offset) {
286 if (spec instanceof Node)
287 return this.allTabs.indexOf(spec);
289 let tabs = this.visibleTabs;
290 let position = this.index(null, true);
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);
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;
312 return this.allTabs.indexOf(tabs[position]);
316 * Removes all tabs from the tab list except the specified *tab*.
318 * @param {Object} tab The tab to keep.
320 keepOnly: function keepOnly(tab) {
321 config.tabbrowser.removeAllTabsBut(tab);
325 * Lists all tabs matching *filter*.
327 * @param {string} filter A filter matching a substring of the tab's
328 * document title or URL.
330 list: function list(filter) {
331 completion.listCompleter("buffer", filter);
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.
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.
346 * @param {number|null} count If non-null, return only the
347 * *count*th matching tab.
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.
354 match: function match(filter, count, regexp, all) {
355 if (!filter && count == null)
358 yield dactyl.assert(tabs.getTab(count - 1));
360 let matches = /^(\d+)(?:$|:)/.exec(filter);
362 yield dactyl.assert(count == null &&
363 tabs.getTab(parseInt(matches[1], 10) - 1, !all));
366 regexp = util.regexp(filter, "i");
368 var matcher = Styles.matchFilter(filter);
370 for (let tab in values(tabs[all ? "allTabs" : "visibleTabs"])) {
371 let browser = tab.linkedBrowser;
372 let uri = browser.currentURI;
374 if (uri.spec == "about:blank")
375 title = "(Untitled)";
377 title = browser.contentTitle;
379 if (matcher && matcher(uri)
380 || regexp && (regexp.test(title) || regexp.test(uri.spec)))
381 if (count == null || --count == 0)
389 * Moves a tab to a new position in the tab list.
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.
396 move: function move(tab, spec, wrap) {
397 let index = tabs.indexFromSpec(spec, wrap, -1);
398 config.tabbrowser.moveTabTo(tab, index);
402 * Removes the specified *tab* from the tab list.
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.
408 remove: function remove(tab, count, focusLeftTab) {
410 let res = this.count > count;
412 let tabs = this.visibleTabs;
413 if (tabs.indexOf(tab) < 0)
415 let index = tabs.indexOf(tab);
417 let next = index + (focusLeftTab ? -count : count);
419 next = index + (focusLeftTab ? 1 : -1);
421 this._alternates[0] = tabs[next];
422 config.tabbrowser.mTabContainer.selectedItem = tabs[next];
426 tabs.slice(Math.max(0, index + 1 - count), index + 1).forEach(config.closure.removeTab);
428 tabs.slice(index, index + count).forEach(config.closure.removeTab);
433 * Reloads the specified tab.
435 * @param {Object} tab The tab to reload.
436 * @param {boolean} bypassCache Whether to bypass the cache when
439 reload: function reload(tab, bypassCache) {
442 const flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
443 config.tabbrowser.getBrowserForTab(tab).reloadWithFlags(flags);
446 config.tabbrowser.reloadTab(tab);
448 catch (e if !(e instanceof Error)) {}
454 * @param {boolean} bypassCache Whether to bypass the cache when
457 reloadAll: function reloadAll(bypassCache) {
458 this.visibleTabs.forEach(function (tab) {
460 tabs.reload(tab, bypassCache);
463 dactyl.reportError(e, true);
469 * Selects the tab at the position specified by *spec*.
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.
475 select: function select(spec, wrap) {
476 let index = tabs.indexFromSpec(spec, wrap);
480 config.tabbrowser.mTabContainer.selectedIndex = index;
484 * Selects the alternate tab.
486 selectAlternateTab: function selectAlternateTab() {
487 dactyl.assert(tabs.alternate != null && tabs.getTab() != tabs.alternate,
488 _("buffer.noAlternate"));
489 tabs.select(tabs.alternate);
493 * Stops loading the specified tab.
495 * @param {Object} tab The tab to stop loading.
497 stop: function stop(tab) {
501 tab.linkedBrowser.stop();
505 * Stops loading all tabs.
507 stopAll: function stopAll() {
508 for (let [, browser] in this.browsers)
513 * Selects the tab containing the specified *buffer*.
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
519 * @param {number} count If there are multiple matches select the
521 * @param {boolean} reverse Whether to search the buffer list in
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;
533 buffer = this._lastBufferSwitchArgs;
534 if (allowNonUnique == null) // XXX
535 allowNonUnique = this._lastBufferSwitchSpecial;
539 return tabs.selectAlternateTab();
541 reverse = Boolean(reverse);
542 count = Math.max(1, count || 1) * (1 + -2 * reverse);
544 let matches = buffer.match(/^(\d+):?/);
546 return tabs.select(this.allTabs[parseInt(matches[1], 10) - 1], false);
548 matches = array.nth(tabs.allTabs, function (t) (t.linkedBrowser.lastURI || {}).spec === buffer, 0);
550 return tabs.select(matches, false);
552 matches = completion.runCompleter("buffer", buffer).map(function (obj) obj.tab);
554 if (matches.length == 0)
555 dactyl.echoerr(_("buffer.noMatching", buffer));
556 else if (matches.length > 1 && !allowNonUnique)
557 dactyl.echoerr(_("buffer.multipleMatching", buffer));
559 let start = matches.indexOf(tabs.getTab());
560 if (start == -1 && reverse)
563 let index = (start + count) % matches.length;
565 index = matches.length + index;
566 tabs.select(matches[index], false);
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.
575 * Sets the current and alternate tabs, updating the tab selection
578 * @param {Array(Object)} tabs The current and alternate tab.
579 * @see tabs#alternate
581 updateSelectionHistory: function updateSelectionHistory(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];
588 this._alternates = tabs || [this.getTab(), this._alternates[0]];
591 copyTab: function (to, from) {
593 from = config.tabbrowser.mTabContainer.selectedItem;
595 let tabState = services.sessionStore.getTabState(from);
596 services.sessionStore.setTabState(to, tabState);
599 load: function init_load() {
600 tabs.updateTabCount();
602 commands: function init_commands() {
606 description: "Delete matching buffers",
610 name: ["tabc[lose]"],
611 description: "Delete matching tabs",
614 ].forEach(function (params) {
615 commands.add(params.name, params.description,
618 for (let tab in tabs.match(args[0], args.count, args.bang, !params.visible)) {
619 config.removeTab(tab);
625 dactyl.echomsg(_("buffer.fewerTab" + (removed == 1 ? "" : "s"), removed), 9);
627 dactyl.echoerr(_("buffer.noMatching", args[0]));
632 completer: function (context) completion.buffer(context),
638 commands.add(["pin[tab]"],
639 "Pin tab as an application tab",
641 for (let tab in tabs.match(args[0], args.count))
642 config.browser[!args.bang || !tab.pinned ? "pinTab" : "unpinTab"](tab);
648 completer: function (context, args) {
650 context.filters.push(function ({ item }) !item.tab.pinned);
651 completion.buffer(context);
655 commands.add(["unpin[tab]"],
656 "Unpin tab as an application tab",
658 for (let tab in tabs.match(args[0], args.count))
659 config.browser.unpinTab(tab);
664 completer: function (context, args) {
665 context.filters.push(function ({ item }) item.tab.pinned);
666 completion.buffer(context);
670 commands.add(["keepa[lt]"],
671 "Execute a command without changing the current alternate buffer",
674 dactyl.execute(args[0], null, true);
677 tabs.updateSelectionHistory([tabs.getTab(), tabs.alternate]);
681 completer: function (context) completion.ex(context),
686 commands.add(["tab"],
687 "Execute a command and tell it to output in a new tab",
689 dactyl.withSavedValues(["forceTarget"], function () {
690 this.forceTarget = dactyl.NEW_TAB;
691 dactyl.execute(args[0], null, true);
695 completer: function (context) completion.ex(context),
700 commands.add(["background", "bg"],
701 "Execute a command opening any new tabs in the background",
703 dactyl.withSavedValues(["forceBackground"], function () {
704 this.forceBackground = true;
705 dactyl.execute(args[0], null, true);
709 completer: function (context) completion.ex(context),
714 commands.add(["tabd[o]", "bufd[o]"],
715 "Execute a command in each tab",
717 for (let tab in values(tabs.visibleTabs)) {
719 dactyl.execute(args[0], null, true);
723 completer: function (context) completion.ex(context),
728 commands.add(["tabl[ast]", "bl[ast]"],
729 "Switch to the last tab",
730 function () tabs.select("$", false),
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",
737 let count = args.count;
740 // count is ignored if an arg is specified, as per Vim
742 if (/^\d+$/.test(arg))
743 tabs.select("-" + arg, true);
745 dactyl.echoerr(_("error.trailingCharacters"));
748 tabs.select("-" + count, true);
750 tabs.select("-1", true);
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",
760 let count = args.count;
763 if (arg || count > 0) {
766 // count is ignored if an arg is specified, as per Vim
768 dactyl.assert(/^\d+$/.test(arg), _("error.trailingCharacters"));
774 if (index < tabs.count)
775 tabs.select(index, true);
780 tabs.select("+1", true);
786 commands.add(["tabr[ewind]", "tabfir[st]", "br[ewind]", "bf[irst]"],
787 "Switch to the first tab",
788 function () { tabs.select(0, false); },
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); }, {
798 completer: function (context) completion.buffer(context),
803 commands.add(["buffers", "files", "ls", "tabs"],
804 "Show a list of all buffers",
805 function (args) { tabs.list(args[0] || ""); }, {
810 commands.add(["quita[ll]", "qa[ll]"],
811 "Quit this " + config.appName + " window",
812 function (args) { window.close(); },
815 commands.add(["reloada[ll]"],
816 "Reload all tab pages",
817 function (args) { tabs.reloadAll(args.bang); }, {
822 commands.add(["stopa[ll]"],
823 "Stop loading all tab pages",
824 function () { tabs.stopAll(); },
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",
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));
839 tabs.move(tabs.getTab(), arg, args.bang);
843 completer: function (context, args) completion.buffer(context, true),
847 commands.add(["tabo[nly]"],
848 "Close all other tabs",
849 function () { tabs.keepOnly(tabs.getTab()); },
852 commands.add(["tabopen", "t[open]", "tabnew"],
853 "Open one or more URLs in a new tab",
855 dactyl.open(args[0] || "about:blank",
856 { from: "tabopen", where: dactyl.NEW_TAB, background: args.bang });
859 completer: function (context) completion.url(context),
860 domains: function (args) commands.get("open").domains(args),
865 commands.add(["tabde[tach]"],
866 "Detach current tab to its own window",
867 function () { tabs.detachTab(null); },
870 commands.add(["tabdu[plicate]"],
871 "Duplicate current tab",
873 let tab = tabs.getTab();
875 let activate = args.bang ? true : false;
876 if (options.get("activate").has("tabopen"))
877 activate = !activate;
879 for (let i in util.range(0, Math.max(1, args.count)))
880 tabs.cloneTab(tab, activate);
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",
893 dactyl.assert(args.length <= 2 && !args.some(function (i) !/^\d+(?:$|:)/.test(i)),
894 _("error.trailingCharacters"));
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);
903 let win = dactyl.windows[winIndex - 1];
904 let sourceTab = tabs.getTab();
906 dactyl.assert(win, _("window.noIndex", winIndex));
907 dactyl.assert(win != window, _("window.cantAttachSame"));
909 let modules = win.dactyl.modules;
910 let { browser } = modules.config;
913 let tabList = modules.tabs.visibleTabs;
914 let target = dactyl.assert(tabList[tabIndex]);
915 tabIndex = Array.indexOf(tabs.allTabs, target) - 1;
918 let newTab = browser.addTab("about:blank");
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?
925 let last = modules.tabs.allTabs.length - 1;
928 browser.moveTabTo(newTab, tabIndex);
929 browser.selectedTab = newTab; // required
930 browser.swapBrowsersAndCloseOther(newTab, sourceTab);
934 completer: function (context, args) {
935 switch (args.completeArg) {
938 completion.tabGroup(context);
940 context.filters.push(function ({ item }) item != window);
941 completion.window(context);
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));
950 win.dactyl.modules.commands.get("tabmove").completer(context);
957 names: ["-group", "-g"],
958 description: "Attach to a group rather than a window",
959 type: CommandOption.NOARG
965 if (dactyl.has("tabs_undo")) {
966 commands.add(["u[ndo]"],
967 "Undo closing of a tab",
972 args = args.count || 0;
974 let m = /^(\d+)(:|$)/.exec(args || '1');
976 window.undoCloseTab(Number(m[1]) - 1);
978 for (let [i, item] in Iterator(tabs.closedTabs))
979 if (item.state.entries[item.state.index - 1].url == args) {
980 window.undoCloseTab(i);
984 dactyl.echoerr(_("buffer.noClosed"));
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);
1000 commands.add(["undoa[ll]"],
1001 "Undo closing of all closed tabs",
1003 for (let i in Iterator(tabs.closedTabs))
1004 window.undoCloseTab(0);
1011 if (dactyl.has("session")) {
1012 commands.add(["wqa[ll]", "wq", "xa[ll]"],
1013 "Save the session and quit",
1014 function () { dactyl.quit(true); },
1018 completion: function init_completion() {
1020 completion.buffer = function buffer(context, visible) {
1021 let { tabs } = modules;
1023 let filter = context.filter.toLowerCase();
1025 let defItem = { parent: { getTitle: function () "" } };
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(), []];
1034 group = tabGroups[group.id];
1035 group[1].push([i, tab.linkedBrowser]);
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) }
1042 context.process[1] = function (item, text) template.bookmarkDescription(item, template.highlightFilter(text, this.filter));
1044 context.anchored = false;
1048 indicator: function (item) item.tab === tabs.getTab() ? "%" :
1049 item.tab === tabs.alternate ? "#" : " ",
1052 command: function () "tabs.select"
1054 context.compare = CompletionContext.Sort.number;
1055 context.filters[0] = CompletionContext.Filter.textDescription;
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())
1065 else if (i == tabs.index(tabs.alternate))
1068 let tab = tabs.getTab(i, visible);
1069 let url = browser.contentDocument.location.href;
1073 text: [i + ": " + (tab.label || /*L*/"(Untitled)"), i + ": " + url],
1077 icon: tab.image || BookmarkCache.DEFAULT_FAVICON
1083 completion.tabGroup = function tabGroup(context) {
1084 context.title = ["Tab Groups"];
1087 description: function (group) group.getTitle() ||
1088 group.getChildren().map(function (t) t.tab.label).join(", ")
1090 context.generate = function () {
1091 context.incomplete = true;
1092 tabs.getGroups(function ({ GroupItems }) {
1093 context.incomplete = false;
1094 context.completions = GroupItems.groupItems;
1099 events: function init_events() {
1100 let tabContainer = config.tabbrowser.mTabContainer;
1101 function callback() {
1102 tabs.timeout(function () { this.updateTabCount(); });
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);
1108 mappings: function init_mappings() {
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;
1120 mappings.add([modes.NORMAL], ["g0", "g^"],
1121 "Go to the first tab",
1122 function () { tabs.select(0); });
1124 mappings.add([modes.NORMAL], ["g$"],
1125 "Go to the last tab",
1126 function () { tabs.select("$"); });
1128 mappings.add([modes.NORMAL], ["gt"],
1129 "Go to the next tab",
1130 function ({ count }) {
1132 tabs.select(count - 1, false);
1134 tabs.select("+1", true);
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); },
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); },
1148 if (config.has("tabbrowser")) {
1149 mappings.add([modes.NORMAL], ["b"],
1150 "Open a prompt to switch buffers",
1151 function ({ count }) {
1153 tabs.switchTo(String(count));
1155 CommandExMode().open("buffer! ");
1159 mappings.add([modes.NORMAL], ["B"],
1161 function () { tabs.list(false); });
1163 mappings.add([modes.NORMAL], ["d"],
1164 "Delete current buffer",
1165 function ({ count }) { tabs.remove(tabs.getTab(), count, false); },
1168 mappings.add([modes.NORMAL], ["D"],
1169 "Delete current buffer, focus tab to the left",
1170 function ({ count }) { tabs.remove(tabs.getTab(), count, true); },
1173 mappings.add([modes.NORMAL], ["gb"],
1174 "Repeat last :buffer command",
1175 function ({ count }) { tabs.switchTo(null, null, count, false); },
1178 mappings.add([modes.NORMAL], ["gB"],
1179 "Repeat last :buffer command in reverse direction",
1180 function ({ count }) { tabs.switchTo(null, null, count, true); },
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 }); },
1191 mappings.add([modes.NORMAL], ["<C-^>", "<C-6>"],
1192 "Select the alternate tab or the [count]th tab",
1193 function ({ count }) {
1195 tabs.switchTo(String(count), false);
1197 tabs.selectAlternateTab();
1202 options: function init_options() {
1203 options.add(["showtabline", "stal"],
1204 "Define when the tab bar is visible",
1207 setter: function (value) {
1208 if (value === "never")
1209 tabs.tabStyle.enabled = true;
1211 prefs.safeSet("browser.tabs.autoHide", value === "multitab",
1212 _("option.safeSet", "showtabline"));
1213 tabs.tabStyle.enabled = false;
1216 if (value !== "multitab" || !dactyl.has("Gecko2"))
1218 tabs.xulTabs.visible = value !== "never";
1220 config.tabStrip.collapsed = false;
1222 if (config.tabbrowser.tabContainer._positionPinnedTabs)
1223 config.tabbrowser.tabContainer._positionPinnedTabs();
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"
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"]
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(","),
1252 values: activateGroups,
1253 has: Option.has.toggleAll,
1254 setter: function (newValues) {
1255 let valueSet = Set(newValues);
1256 for (let group in values(activateGroups))
1258 prefs.safeSet("browser.tabs." + group[2],
1259 !(valueSet["all"] ^ valueSet[group[0]]),
1260 _("option.safeSet", "activate"));
1265 options.add(["newtab"],
1266 "Define which commands should output in a new tab by default",
1270 "all": "All commands",
1271 "extoptions": ":exto[ptions] command",
1272 "help": ":h[elp] command",
1273 "prefs": ":pref[erences]! or :prefs! command"
1275 has: Option.has.toggleAll
1278 // TODO: Is this really applicable to Melodactyl?
1279 options.add(["popups", "pps"],
1280 "Where to show requested popup windows",
1281 "stringlist", "tab",
1283 setter: function (values) {
1284 let open = 1, restriction = 0;
1285 for (let [, opt] in Iterator(values)) {
1288 else if (opt == "window")
1290 else if (opt == "resized")
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"));
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"
1310 // vim: set fdm=marker sw=4 ts=4 et: