Bug 583890 - Tab Title Abridger module and tests. r=ttaubert
☠☠ backed out by 0e5c7b2f7d61 ☠ ☠
authorAdam Dane [:hobophobe] <unusualtears@gmail.com>
Thu, 23 Aug 2012 20:16:13 -0500
changeset 105302 9c2c1987abc41cacfda266117d7cd8bfced5d03f
parent 105301 ecaff5097b0b0b775e56f23bb65734b7658c46e7
child 105303 a2834312d80d216f5586ec84dd94338ddc97b736
push id55
push usershu@rfrn.org
push dateThu, 30 Aug 2012 01:33:09 +0000
reviewersttaubert
bugs583890
milestone17.0a1
Bug 583890 - Tab Title Abridger module and tests. r=ttaubert
browser/app/profile/firefox.js
browser/base/content/browser.js
browser/base/content/test/Makefile.in
browser/base/content/test/browser_bug583890.js
browser/modules/Makefile.in
browser/modules/TabTitleAbridger.jsm
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -384,16 +384,17 @@ pref("browser.tabs.maxOpenBeforeWarn", 1
 pref("browser.tabs.loadInBackground", true);
 pref("browser.tabs.opentabfor.middleclick", true);
 pref("browser.tabs.loadDivertedInBackground", false);
 pref("browser.tabs.loadBookmarksInBackground", false);
 pref("browser.tabs.tabClipWidth", 140);
 pref("browser.tabs.animate", true);
 pref("browser.tabs.onTop", true);
 pref("browser.tabs.drawInTitlebar", true);
+pref("browser.tabs.cropTitleRedundancy", true);
 
 // Where to show tab close buttons:
 // 0  on active tab only
 // 1  on all tabs until tabClipWidth is reached, then active tab only
 // 2  no close buttons at all
 // 3  at the end of the tabstrip
 pref("browser.tabs.closeButtons", 1);
 
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -152,16 +152,22 @@ XPCOMUtils.defineLazyGetter(this, "SafeB
 #endif
 
 XPCOMUtils.defineLazyGetter(this, "gBrowserNewTabPreloader", function () {
   let tmp = {};
   Cu.import("resource://gre/modules/BrowserNewTabPreloader.jsm", tmp);
   return new tmp.BrowserNewTabPreloader();
 });
 
+XPCOMUtils.defineLazyGetter(this, "TabTitleAbridger", function() {
+  let tmp = {};
+  Cu.import("resource:///modules/TabTitleAbridger.jsm", tmp);
+  return new tmp.TabTitleAbridger(window);
+});
+
 let gInitialPages = [
   "about:blank",
   "about:newtab",
   "about:home",
   "about:privatebrowsing",
   "about:sessionrestore"
 ];
 
@@ -1408,16 +1414,17 @@ var gBrowserInit = {
     // Don't preload new tab pages when the toolbar is hidden
     // (i.e. when the current window is a popup window).
     if (window.toolbar.visible) {
       gBrowserNewTabPreloader.init(window);
     }
 
     gBrowserThumbnails.init();
     TabView.init();
+    TabTitleAbridger.init();
 
     setUrlAndSearchBarWidthForConditionalForwardButton();
     window.addEventListener("resize", function resizeHandler(event) {
       if (event.target == window)
         setUrlAndSearchBarWidthForConditionalForwardButton();
     });
 
     // Enable developer toolbar?
@@ -1603,16 +1610,17 @@ var gBrowserInit = {
         Win7Features.onCloseWindow();
 
       gPrefService.removeObserver(ctrlTab.prefName, ctrlTab);
       gPrefService.removeObserver(allTabs.prefName, allTabs);
       ctrlTab.uninit();
       TabView.uninit();
       gBrowserThumbnails.uninit();
       FullZoom.destroy();
+      TabTitleAbridger.destroy();
 
       Services.obs.removeObserver(gSessionHistoryObserver, "browser:purge-session-history");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-disabled");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-started");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-blocked");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-failed");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-complete");
       Services.obs.removeObserver(gFormSubmitObserver, "invalidformsubmit");
--- a/browser/base/content/test/Makefile.in
+++ b/browser/base/content/test/Makefile.in
@@ -129,16 +129,17 @@ endif
                  browser_bug577121.js \
                  browser_bug578534.js \
                  browser_bug579872.js \
                  browser_bug580638.js \
                  browser_bug580956.js \
                  browser_bug581242.js \
                  browser_bug581253.js \
                  browser_bug581947.js \
+                 browser_bug583890.js \
                  browser_bug583890_label.js \
                  browser_bug585785.js \
                  browser_bug585830.js \
                  browser_bug590206.js \
                  browser_bug592338.js \
                  browser_bug594131.js \
                  browser_bug595507.js \
                  browser_bug596687.js \
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/browser_bug583890.js
@@ -0,0 +1,359 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Call the aCallback when aTab's label is equal to the aExpectedLabel.
+ * Either happens immediately, or after a series of TabAttrModified events.
+ * In the case of failure, this will cause timeout of the test.
+ *
+ * @param aTab           the tab whose label is being tested
+ * @param aExpectedLabel the value the tab's label must match
+ * @param aCallback      the callback for use upon success
+ */
+function waitForTabLabel(aTab, aExpectedLabel, aCallback) {
+  if (aTab.visibleLabel == aExpectedLabel) {
+    executeSoon(aCallback);
+  } else {
+    executeSoon(function () { waitForTabLabel(aTab, aExpectedLabel, aCallback); });
+  }
+}
+
+/**
+ * Call the aCallback after adding aCount tabs.
+ *
+ * @param aCount         the number of tabs to add
+ * @param aCallback      the callback for use upon success
+ */
+function addTabs(aCount, aCallback) {
+  let addedTabs = [];
+  for (let i = aCount; i > 0; i--) {
+    addedTabs.push(gBrowser.addTab());
+  }
+  executeSoon(function () { aCallback(addedTabs); });
+}
+
+/**
+ * Call the aCallback after updating aTab's title and waiting for a label update
+ * In the case of failure, this will cause timeout of the test.
+ *
+ * @param aTab           the tab whose title is set
+ * @param aTitle         the value to give the tab title
+ * @param aExpectedLabel the value the tab's label must match
+ * @param aCallback      the callback for use upon success
+ */
+function setTitleForTab(aTab, aTitle, aExpectedLabel, aCallback) {
+  aTab.linkedBrowser.contentDocument.title = aTitle;
+  waitForTabLabel(aTab, aExpectedLabel, aCallback);
+}
+
+/**
+ * Call the aCallback after updating aTab's label and waiting for a label update
+ * In the case of failure, this will cause timeout of the test.
+ *
+ * @param aTab           the tab whose title is set
+ * @param aTitle         the value to give the tab title
+ * @param aExpectedLabel the value the tab's label must match
+ * @param aCallback      the callback for use upon success
+ */
+function setLabelForTab(aTab, aTitle, aExpectedLabel, aCallback) {
+  aTab.label = aTitle;
+  waitForTabLabel(aTab, aExpectedLabel, aCallback);
+}
+
+function GroupTest() {
+  this.groupNumber = 0;
+  this.tabs = [];
+}
+
+GroupTest.prototype = {
+  groups: [
+    [
+      /*
+       * Test proxying and suffix protection
+       */
+      [
+        "Foo - Bar - Baz",
+        "Foo - Baz - Baz",
+        "Foo - Baz - Baz",
+        "Foo - Baz - Qux"
+      ],
+      [
+        [
+          "Bar - Baz",
+          "Baz - Baz"
+        ],
+        [
+          "Bar - Baz",
+          "Baz - Baz",
+          "Baz - Baz"
+        ],
+        [
+          "Bar - Baz",
+          "Baz",
+          "Baz",
+          "Qux"
+        ]
+      ]
+    ],
+    [
+      /*
+       * Test pathmode
+       */
+      [
+        "http://example.com/foo.html",
+        "http://example.com/foo/bar.html",
+        "Browse - ftp://example.com/pub/",
+        "Browse - ftp://example.com/pub/src/"
+      ],
+      [
+        [
+          "foo.html",
+          "foo/bar.html"
+        ],
+        [
+          "foo.html",
+          "foo/bar.html",
+          "Browse - ftp://example.com/pub/"
+        ],
+        [
+          "foo.html",
+          "foo/bar.html",
+          "pub/",
+          "src/"
+        ]
+      ]
+    ],
+    [
+      /*
+       * Test that we don't leave a lone suffix
+       */
+      [
+        "'Zilla and the Foxes - Singles - Musical Monkey",
+        "'Zilla and the Foxes - Biography - Musical Monkey",
+        "'Zilla and the Foxes - Musical Monkey",
+        "'Zilla and the Foxes - Interviews - Musical Monkey"
+      ],
+      [
+        [
+          "Singles - Musical Monkey",
+          "Biography - Musical Monkey"
+        ],
+        [
+          "Singles - Musical Monkey",
+          "Biography - Musical Monkey",
+          "'Zilla and the Foxes - Musical Monkey"
+        ],
+        [
+          "Singles - Musical Monkey",
+          "Biography - Musical Monkey",
+          "'Zilla and the Foxes - Musical Monkey",
+          "Interviews - Musical Monkey"
+        ]
+      ]
+    ],
+    /*
+     * Test short endings for MIN_CHOP
+     */
+    [
+      [
+        "Foo - Bar - 0",
+        "Foo - Bar - 0 - extra - 0",
+        "Foo - Bar - 1",
+        "Foo - Bar - 2 - extra",
+        "Foo - Bar - 3"
+      ],
+      [
+        [
+          "Bar - 0",
+          "0 - extra - 0"
+        ],
+        [
+          "Bar - 0",
+          "0 - extra - 0",
+          "Bar - 1"
+        ],
+        [
+          "Bar - 0",
+          "0 - extra - 0",
+          "Bar - 1",
+          "2 - extra"
+        ],
+        [
+          "Bar - 0",
+          "0 - extra - 0",
+          "Bar - 1",
+          "2 - extra",
+          "Bar - 3"
+        ]
+      ]
+    ],
+    [
+      /*
+       * Test multiple whitespace
+       */
+      [
+        "Foo  - Bar -  Baz",
+        "Foo -  Bar -  Baz",
+        "Foo  -  Bar -  Baz",
+        "Foo  - Baz -  Baz"
+      ],
+      [
+        [
+          "Foo - Bar - Baz",
+          "Foo - Bar - Baz"
+        ],
+        [
+          "Foo - Bar - Baz",
+          "Foo - Bar - Baz",
+          "Foo - Bar - Baz"
+        ],
+        [
+          "Bar - Baz",
+          "Bar - Baz",
+          "Bar - Baz",
+          "Baz - Baz"
+        ]
+      ]
+    ]
+  ],
+
+  /**
+   * Either proceed with the next group, or finish group tests
+   */
+  nextGroup: function GroupTest_nextGroup() {
+    while (this.tabs.length) {
+      gBrowser.removeTab(this.tabs.pop());
+    }
+    if (this.groups.length) {
+      this.groupNumber++;
+      [this.labels, this.expectedLabels] = this.groups.shift();
+      this.nextTab();
+    } else {
+      runNextTest();
+    }
+  },
+
+  /**
+   * Runs tests for existing tabs, and adds the next tab (if group isn't empty)
+   * If the group is empty, starts the next group
+   */
+  nextTab: function GroupTest_nextTab() {
+    if (this.tabs.length > 1) {
+      let ourExpected = this.expectedLabels.shift();
+      for (let i = 0; i < this.tabs.length; i++) {
+        is(this.tabs[i].visibleLabel, ourExpected[i],
+          "Tab " + this.groupNumber + "." + (i + 1) + " has correct visibleLabel");
+      }
+    }
+    if (this.labels.length) {
+      this.tabs.push(gBrowser.addTab(
+        "data:text/html,<title>" + this.labels.shift() + "</title>"));
+      if (this.tabs.length > 1) {
+        waitForTabLabel(this.tabs[this.tabs.length - 1],
+                        this.expectedLabels[0][this.expectedLabels[0].length - 1],
+                        this.nextTab.bind(this));
+      } else {
+        this.nextTab();
+      }
+    } else {
+      this.nextGroup();
+    }
+  }
+};
+
+let TESTS = [
+function test_about_blank() {
+  let tab1 = gBrowser.selectedTab;
+  let tab2;
+  let tab3;
+  addTabs(2, setup1);
+  function setup1(aTabs) {
+    [tab2, tab3] = aTabs
+    waitForTabLabel(tab3, "New Tab", setupComplete);
+  }
+  function setupComplete() {
+    is(tab1.visibleLabel, "New Tab", "First tab has original label");
+    is(tab2.visibleLabel, "New Tab", "Second tab has original label");
+    is(tab3.visibleLabel, "New Tab", "Third tab has original label");
+    runNextTest();
+  }
+},
+
+function test_two_tabs() {
+  let tab1 = gBrowser.selectedTab;
+  addTabs(1, setup1);
+  let tab2;
+  function setup1(aTabs) {
+    tab2 = aTabs[0];
+    setTitleForTab(tab1, "Foo - Bar - Baz", "Foo - Bar - Baz", setup2);
+  }
+  function setup2() {
+    setTitleForTab(tab2, "Foo - Baz - Baz", "Baz - Baz", setupComplete);
+  }
+  function setupComplete() {
+    is(tab1.visibleLabel, "Bar - Baz", "Removed exactly two tokens");
+    is(tab2.visibleLabel, "Baz - Baz", "Removed exactly two tokens");
+    gBrowser.removeTab(tab2);
+    waitForTabLabel(tab1, "Foo - Bar - Baz", afterRemoval);
+  }
+  function afterRemoval() {
+    is (tab1.visibleLabel, "Foo - Bar - Baz", "Single tab has full title");
+    runNextTest();
+  }
+},
+
+function test_direct_label() {
+  let tab1 = gBrowser.selectedTab;
+  addTabs(2, setup1);
+  let tab2;
+  let tab3;
+  function setup1(aTabs) {
+    tab2 = aTabs[0];
+    tab3 = aTabs[1];
+    setLabelForTab(tab1, "Foo  - Bar -  Baz", "Foo  - Bar -  Baz", setup2);
+  }
+  function setup2() {
+    setLabelForTab(tab2, "Foo -  Baz  - Baz", "Foo -  Baz  - Baz", setup3);
+  }
+  function setup3() {
+    setLabelForTab(tab3, "Foo  - Baz -  Baz", "Baz -  Baz", setupComplete);
+  }
+  function setupComplete() {
+    is(tab1.visibleLabel, "Bar -  Baz", "Removed exactly two tokens");
+    is(tab2.visibleLabel, "Foo -  Baz  - Baz", "Irregular spaces mean no match");
+    is(tab3.visibleLabel, "Baz -  Baz", "Removed exactly two tokens");
+    gBrowser.removeTab(tab3);
+    waitForTabLabel(tab1, "Foo  - Bar -  Baz", afterRemoval);
+  }
+  function afterRemoval() {
+    is (tab1.visibleLabel, "Foo  - Bar -  Baz", "Single tab has full title");
+    gBrowser.removeTab(tab2);
+    runNextTest();
+  }
+},
+
+function test_groups() {
+  let g = new GroupTest();
+  g.nextGroup();
+}
+];
+
+function runNextTest() {
+  if (TESTS.length == 0) {
+    finish();
+    return;
+  }
+
+  while (gBrowser.tabs.length > 1) {
+    gBrowser.removeTab(gBrowser.tabs[1]);
+  }
+
+  info("Running " + TESTS[0].name);
+  TESTS.shift()();
+};
+
+function test() {
+  waitForExplicitFinish();
+  runNextTest();
+}
+
--- a/browser/modules/Makefile.in
+++ b/browser/modules/Makefile.in
@@ -15,16 +15,17 @@ TEST_DIRS += test
 
 EXTRA_JS_MODULES = \
 	BrowserNewTabPreloader.jsm \
 	openLocationLastURL.jsm \
 	NetworkPrioritizer.jsm \
 	NewTabUtils.jsm \
 	offlineAppCache.jsm \
 	SignInToWebsite.jsm \
+	TabTitleAbridger.jsm \
 	TelemetryTimestamps.jsm \
 	Social.jsm \
 	webappsUI.jsm \
 	$(NULL)
 
 ifeq ($(MOZ_WIDGET_TOOLKIT),windows)
 EXTRA_JS_MODULES += \
 	WindowsPreviewPerTab.jsm \
new file mode 100644
--- /dev/null
+++ b/browser/modules/TabTitleAbridger.jsm
@@ -0,0 +1,604 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+let EXPORTED_SYMBOLS = ["TabTitleAbridger"];
+
+const Cu = Components.utils;
+const ABRIDGMENT_PREF = "browser.tabs.cropTitleRedundancy";
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gETLDService",
+  "@mozilla.org/network/effective-tld-service;1",
+  "nsIEffectiveTLDService");
+
+function TabTitleAbridger(aBrowserWin) {
+  this._tabbrowser = aBrowserWin.gBrowser;
+}
+
+TabTitleAbridger.prototype = {
+  /*
+   * Events we listen to.  We specifically do not listen for TabCreate, as we
+   * get TabLabelModified at the appropriate times.
+   */
+  _eventNames: [
+    "TabPinned",
+    "TabUnpinned",
+    "TabShow",
+    "TabHide",
+    "TabClose",
+    "TabLabelModified"
+  ],
+
+  init: function TabTitleAbridger_Initialize() {
+    this._cropTitleRedundancy = Services.prefs.getBoolPref(ABRIDGMENT_PREF);
+    Services.prefs.addObserver(ABRIDGMENT_PREF, this, false);
+    if (this._cropTitleRedundancy) {
+      this._domainSets = new DomainSets();
+      this._addListeners();
+    }
+  },
+
+  destroy: function TabTitleAbridger_Destroy() {
+    Services.prefs.removeObserver(ABRIDGMENT_PREF, this);
+    if (this._cropTitleRedundancy) {
+      this._dropListeners();
+    }
+  },
+
+  /**
+   * Preference observer
+   */
+  observe: function TabTitleAbridger_PrefObserver(aSubject, aTopic, aData) {
+    let val = Services.prefs.getBoolPref(aData);
+    if (this._cropTitleRedundancy && !val) {
+      this._dropListeners();
+      this._domainSets.destroy();
+      delete this._domainSets;
+      this._resetTabTitles();
+    } else if (!this._cropTitleRedundancy && val) {
+      this._addListeners();
+      // We're just turned on, so we want to abridge everything
+      this._domainSets = new DomainSets();
+      let domains = this._domainSets.bootstrap(this._tabbrowser.visibleTabs);
+      this._abridgeTabTitles(domains);
+    }
+    this._cropTitleRedundancy = val;
+  },
+
+  /**
+   * Adds all the necessary event listeners and listener-supporting objects for
+   * the instance.
+   */
+  _addListeners: function TabTitleAbridger_addListeners() {
+    let tabContainer = this._tabbrowser.tabContainer;
+    for (let eventName of this._eventNames) {
+      tabContainer.addEventListener(eventName, this, false);
+    }
+  },
+
+  /**
+   * Removes event listeners and listener-supporting objects for the instance.
+   */
+  _dropListeners: function TabTitleAbridger_dropListeners() {
+    let tabContainer = this._tabbrowser.tabContainer;
+    for (let eventName of this._eventNames) {
+      tabContainer.removeEventListener(eventName, this, false);
+    }
+  },
+
+  handleEvent: function TabTitleAbridger_handler(aEvent) {
+    let tab = aEvent.target;
+    let updateSets;
+
+    switch (aEvent.type) {
+      case "TabUnpinned":
+      case "TabShow":
+        updateSets = this._domainSets.addTab(tab);
+        break;
+      case "TabPinned":
+      case "TabHide":
+      case "TabClose":
+        updateSets = this._domainSets.removeTab(tab);
+        tab.visibleLabel = tab.label;
+        break;
+      case "TabLabelModified":
+        if (!tab.hidden && !tab.pinned) {
+          aEvent.preventDefault();
+          updateSets = this._domainSets.updateTab(tab);
+        }
+        break;
+    }
+    this._abridgeTabTitles(updateSets);
+  },
+
+  /**
+   * Make all tabs have their visibleLabels be their labels.
+   */
+  _resetTabTitles: function TabTitleAbridger_resetTabTitles() {
+    // We're freshly disabled, so reset unpinned, visible tabs (see handleEvent)
+    for (let tab of this._tabbrowser.visibleTabs) {
+      if (!tab.pinned && tab.visibleLabel != tab.label) {
+        tab.visibleLabel = tab.label;
+      }
+    }
+  },
+
+  /**
+   * Apply abridgment for the given tabset and chop list.
+   * @param aTabSet   Array of tabs to abridge
+   * @param aChopList Corresponding array of chop points for the tabs
+   */
+  _applyAbridgment: function TabTitleAbridger_applyAbridgment(aTabSet,
+                                                              aChopList) {
+    for (let i = 0; i < aTabSet.length; i++) {
+      let tab = aTabSet[i];
+      let label = tab.label || "";
+      if (label.length > 0) {
+        let chop = aChopList[i] || 0;
+        if (chop > 0) {
+          label = label.substr(chop);
+        }
+      }
+      if (label != tab.visibleLabel) {
+        tab.visibleLabel = label;
+      }
+    }
+  },
+
+  /**
+   * Abridges the tabs sets of tabs in the aTabSets array.
+   * @param aTabSets Array of tab sets needing abridgment
+   */
+  _abridgeTabTitles: function TabTitleAbridger_abridgeTabtitles(aTabSets) {
+    // Process each set
+    for (let tabSet of aTabSets) {
+      // Get a chop list for the set and apply it
+      let chopList = AbridgmentTools.getChopsForSet(tabSet);
+      this._applyAbridgment(tabSet, chopList);
+    }
+  }
+};
+
+/**
+ * Maintains a mapping between tabs and domains, so that only the tabs involved
+ * in a TabLabelModified event need to be modified by the TabTitleAbridger.
+ */
+function DomainSets() {
+  this._domainSets = {};
+  this._tabsMappedToDomains = new WeakMap();
+}
+
+DomainSets.prototype = {
+  _noHostSchemes: {
+    chrome: true,
+    file: true,
+    resource: true,
+    data: true,
+    about: true
+  },
+
+  destroy: function DomainSets_destroy() {
+    delete this._domainSets;
+    delete this._tabsMappedToDomains;
+  },
+
+  /**
+   * Used to build the domainsets when enabled in mid-air, as opposed to when
+   * the window is coming up.
+   * @param The visibleTabs for the browser, or a set of tabs to check.
+   * @return An array containing the tabs in the domains they belong to, or
+   * an empty array if none of the tabs belonged to domains.
+   */
+  bootstrap: function DomainSets_bootstrap(aVisibleTabs) {
+    let needAbridgment = [];
+    for (let tab of aVisibleTabs) {
+      let domainSet = this.addTab(aTab)[0] || null;
+      if (domainSet && needAbridgment.indexOf(domainSet) == -1) {
+        needAbridgment.push(domainSet);
+      }
+    }
+    return needAbridgment;
+  },
+
+  /**
+   * Given a tab, include it in the domain sets.
+   * @param aTab The tab to include in the domain sets
+   * @param aTabDomain [optional] The known domain for the tab
+   * @return An array containing the tabs in the domain the tab was added to.
+   */
+  addTab: function DomainSets_addTab(aTab, aTabDomain) {
+    let tabDomain = aTabDomain || this._getDomainForTab(aTab);
+    if (!this._domainSets.hasOwnProperty(tabDomain)) {
+      this._domainSets[tabDomain] = [];
+    }
+    this._domainSets[tabDomain].push(aTab);
+    this._tabsMappedToDomains.set(aTab, tabDomain);
+    return [this._domainSets[tabDomain]];
+  },
+
+  /**
+   * Given a tab, remove it from the domain sets.
+   * @param aTab The tab to remove from the domain sets
+   * @param aTabDomain [optional] The known domain for the tab
+   * @return An array containing the tabs in the domain the tab was removed
+   * from, or an empty array if the tab was not removed from a domain set.
+   */
+  removeTab: function DomainSets_removeTab(aTab, aTabDomain) {
+    let oldTabDomain = aTabDomain || this._tabsMappedToDomains.get(aTab);
+    if (!this._domainSets.hasOwnProperty(oldTabDomain)) {
+      return [];
+    }
+    let index = this._domainSets[oldTabDomain].indexOf(aTab);
+    if (index == -1) {
+      return [];
+    }
+    this._domainSets[oldTabDomain].splice(index, 1);
+    this._tabsMappedToDomains.delete(aTab);
+    if (!this._domainSets[oldTabDomain].length) {
+      // Keep the sets clean of empty domains
+      delete this._domainSets[oldTabDomain];
+      return [];
+    }
+    return [this._domainSets[oldTabDomain]];
+  },
+
+  /**
+   * Given a tab, update the domain set it belongs to.
+   * @param aTab The tab to update the domain set for
+   * @return An array containing the tabs in the domain the tab belongs to, and
+   * (if changed) the domain the tab was removed from.
+   */
+  updateTab: function DomainSets_updateTab(aTab) {
+    let tabDomain = this._getDomainForTab(aTab);
+    let oldTabDomain = this._tabsMappedToDomains.get(aTab);
+    if (oldTabDomain != tabDomain) {
+      let needAbridgment = [];
+      // Probably swapping domain sets out; we pass the domains along to avoid
+      // re-getting them in addTab/removeTab
+      if (oldTabDomain) {
+        needAbridgment = needAbridgment.concat(
+          this.removeTab(aTab, oldTabDomain));
+      }
+      return needAbridgment.concat(this.addTab(aTab, tabDomain));
+    }
+    // No change was needed
+    return [this._domainSets[tabDomain]];
+  },
+
+  /**
+   * Given a tab, determine the URI scheme or host to categorize it.
+   * @param aTab The tab to get the domain for
+   * @return The domain or scheme for the tab
+   */
+  _getDomainForTab: function DomainSets_getDomainForTab(aTab) {
+    let browserURI = aTab.linkedBrowser.currentURI;
+    if (browserURI.scheme in this._noHostSchemes) {
+      return browserURI.scheme;
+    }
+
+    // throws for empty URI, host is IP, and disallowed characters
+    try {
+      return gETLDService.getBaseDomain(browserURI);
+    }
+    catch (e) {}
+
+    // this nsIURI may not be an nsStandardURL nsIURI, which means it
+    // might throw for the host
+    try {
+      return browserURI.host;
+    }
+    catch (e) {}
+
+    // Treat this URI as unique
+    return browserURI.spec;
+  }
+};
+
+let AbridgmentTools = {
+  /**
+   * Constant for the minimum remaining length allowed if a label is abridged.
+   * I.e., original:"abc - de" might be chopped to just "de", which is too
+   * small, so the label would be reverted to the next-longest version.
+   */
+  MIN_CHOP: 3,
+
+  /**
+   * Helper to determine if aStr is URI-like
+   * \s?            optional leading space
+   * [^\s\/]*       optional scheme or relative path component
+   * ([^\s\/]+:\/)? optional scheme separator, with at least one scheme char
+   * \/             at least one slash
+   * \/?            optional second (or third for eg, file scheme on UNIX) slash
+   * [^\s\/]*       optional path component
+   * ([^\s\/]+\/?)* optional more path components with optional end slash
+   * @param aStr the string to check for URI-likeness
+   * @return boolean value of whether aStr matches
+   */
+  _titleIsURI: function AbridgmentTools_titleIsURI(aStr) {
+    return /^\s?[^\s\/]*([^\s\/]+:\/)?\/\/?[^\s\/]*([^\s\/]+\/?)*$/.test(aStr);
+  },
+
+  /**
+   * Finds the proper abridgment indexes for the given tabs.
+   * @param aTabSet the array of tabs to find abridgments for
+   * @return an array of abridgment indexes corresponding to the tabs
+   */
+  getChopsForSet: function AbridgmentTools_getChopsForSet(aTabSet) {
+    let chopList = [];
+    let pathMode = false;
+
+    aTabSet.sort(function(aTab, bTab) {
+      let aLabel = aTab.label;
+      let bLabel = bTab.label;
+      return (aLabel < bLabel) ? -1 : (aLabel > bLabel) ? 1 : 0;
+    });
+
+    // build and apply the chopList for the set
+    for (let i = 0, next = 1; next < aTabSet.length; i = next++) {
+      next = this._abridgePair(aTabSet, i, next, chopList);
+    }
+    return chopList;
+  },
+
+  /**
+   * Handles the abridgment between aIndex and aNext, or in the case where the
+   * label at aNext is the same as at aIndex, moves aNext forward appropriately.
+   * @param aTabSet   Sorted array of tabs that the indices refer to
+   * @param aIndex    First tab index to use in abridgment
+   * @param aNext     Second tab index to use as the an initial comparison
+   * @param aChopList Array to add chop points to for the given tabs
+   * @return Index to replace aNext with, that is the index of the tab that was
+   * used in abridging the tab at aIndex
+   */
+  _abridgePair: function TabTitleAbridger_abridgePair(aTabSet, aIndex, aNext,
+                                                      aChopList) {
+    let tabStr = aTabSet[aIndex].label;
+    let pathMode = this._titleIsURI(tabStr);
+    let chop = RedundancyFinder.indexOfSep(pathMode, tabStr);
+
+    // Default to no chop
+    if (!aChopList[aIndex]) {
+      aChopList[aIndex] = 0;
+    }
+
+    // Siblings with same label get proxied by the first
+    let nextStr;
+    aNext = this._nextUnproxied(aTabSet, tabStr, aNext);
+    if (aNext < aTabSet.length) {
+      nextStr = aTabSet[aNext].label;
+    }
+
+    // Bail on these strings early, using the first as the basis
+    if (chop == -1 || aNext == aTabSet.length ||
+        !nextStr.startsWith(tabStr.substr(0, chop + 1))) {
+      chop = aChopList[aIndex];
+      if (aNext != aTabSet.length) {
+        aChopList[aNext] = 0;
+      }
+    } else {
+      [pathMode, chop] = this._getCommonChopPoint(pathMode, tabStr, nextStr,
+                                                  chop);
+      [chop, aChopList[aNext]] = this._adjustChops(pathMode, tabStr, nextStr,
+                                                  chop);
+      aChopList[aIndex] = chop;
+    }
+
+    // Mark chop on the relevant tabs
+    for (let j = aIndex; j < aNext; j++) {
+      let oldChop = aChopList[j];
+      if (!oldChop || oldChop < chop) {
+        aChopList[j] = chop;
+      }
+    }
+    return aNext;
+  },
+
+  /**
+   * Gets the index in aTabSet of the next tab that's not equal to aStr.
+   * @param aTabSet Sorted set of tabs to check
+   * @param aStr    Label string to check against
+   * @param aStart  First item to check for proxying
+   * @return The index of the next different tab.
+   */
+  _nextUnproxied: function AbridgmentTools_nextUnproxied(aTabSet, aTabStr,
+                                                              aStart) {
+    let nextStr = aTabSet[aStart].label;
+    while (aStart < aTabSet.length && aTabStr == nextStr) {
+      aStart += 1;
+      if (aStart < aTabSet.length) {
+        nextStr = aTabSet[aStart].label;
+      }
+    }
+    return aStart;
+  },
+
+  /**
+   * Get the common index where the aTabStr and aNextStr diverge.
+   * @param aPathMode Whether to use path mode
+   * @param aTabStr   Tab label
+   * @param aNextStr  Second tab label
+   * @param aChop     Current chop point being considered (index of aTabStr's
+   * first separator)
+   * @return An array containing the resulting path mode (in case it changes)
+   * and the diverence index for the labels.
+   */
+  _getCommonChopPoint: function AbridgmentTools_getCommonChopPoint(aPathMode,
+                                                                   aTabStr,
+                                                                   aNextStr,
+                                                                   aChop) {
+    aChop = RedundancyFinder.findCommonPrefix(aPathMode, aTabStr, aNextStr,
+                                              aChop);
+    // Does a URI remain?
+    if (!aPathMode) {
+      aPathMode = this._titleIsURI(aTabStr.substr(aChop));
+      if (aPathMode) {
+        aChop = RedundancyFinder.findCommonPrefix(aPathMode, aTabStr, aNextStr,
+                                                  aChop);
+      }
+    }
+
+    return [aPathMode, aChop + 1];
+  },
+
+  /**
+   * Adjusts the chop points based on their suffixes and lengths.
+   * @param aPathMode Whether to use path mode
+   * @param aTabStr   Tab label
+   * @param aNextStr  Second tab label
+   * @param aChop     Current chop point being considered
+   * @return An array containing the chop point for the two labels.
+   */
+  _adjustChops: function AbridgmentTools_adjustChops(aPathMode, aTabStr,
+                                                     aNextStr, aChop) {
+    let suffix = RedundancyFinder.findCommonSuffix(aPathMode, aTabStr,
+                                                   aNextStr);
+    let sufPos = aTabStr.length - suffix;
+    let nextSufPos = aNextStr.length - suffix;
+    let nextChop = aChop;
+
+    // Adjust the chop based on the suffix.
+    if (sufPos < aChop) {
+      // Only revert based on suffix for tab and any identicals
+      aChop = RedundancyFinder.lastIndexOfSep(aPathMode, aTabStr,
+                                              sufPos - 1)[1] + 1;
+    } else if (nextSufPos < aChop) {
+      // Only revert based on suffix for 'next'
+      nextChop = RedundancyFinder.lastIndexOfSep(aPathMode, aNextStr,
+                                                 nextSufPos - 1)[1] + 1;
+    }
+
+    if (aTabStr.length - aChop < this.MIN_CHOP) {
+      aChop = RedundancyFinder.lastIndexOfSep(aPathMode, aTabStr,
+                                              aChop - 2)[1] + 1;
+    }
+    if (aNextStr.length - nextChop < this.MIN_CHOP) {
+      nextChop = RedundancyFinder.lastIndexOfSep(aPathMode, aNextStr,
+                                                 nextChop - 2)[1] + 1;
+    }
+    return [aChop, nextChop];
+  }
+};
+
+let RedundancyFinder = {
+  /**
+   * Finds the first index of a matched separator after aStart.
+   * Separators will either be space-padded punctuation or slashes (in pathmode)
+   *
+   * ^.+?      at least one character, non-greedy match
+   * \s+       one or more whitespace characters
+   * [-:>\|]+  one or more separator characters
+   * \s+       one or more whitespace characters
+   *
+   * @param aPathMode true for path mode, false otherwise
+   * @param aStr      the string to look for a separator in
+   * @param aStart    (optional) an index to start the search from
+   * @return the next index of a separator or -1 for none
+   */
+  indexOfSep: function RedundancyFinder_indexOfSep(aPathMode, aStr, aStart) {
+    if (aPathMode) {
+      return aStr.indexOf('/', aStart);
+    }
+
+    let match = aStr.slice(aStart).match(/^.+?\s+[-:>\|]+\s+/);
+    if (match) {
+      return (aStart || 0) + match[0].length - 1;
+    }
+
+    return -1;
+  },
+
+  /**
+   * Compares a pair of strings, seeking an index where their redundancy ends
+   * @param aPathMode true for pathmode, false otherwise
+   * @param aStr      the string to decide an abridgment for
+   * @param aNextStr  the lexicographically next string to compare with
+   * @param aChop     the basis index, a best-known index to begin comparison
+   * @return the index at which aStr's abridged title should begin
+   */
+  findCommonPrefix: function RedundancyFinder_findCommonPrefix(aPathMode, aStr,
+                                                               aNextStr,
+                                                               aChop) {
+    // Advance until the end of the title or the pair diverges
+    do {
+      aChop = this.indexOfSep(aPathMode, aStr, aChop + 1);
+    } while (aChop != -1 && aNextStr.startsWith(aStr.substr(0, aChop + 1)));
+
+    if (aChop < 0) {
+      aChop = aStr.length;
+    }
+
+    // Return the last valid spot
+    return this.lastIndexOfSep(aPathMode, aStr, aChop - 1)[1];
+  },
+
+  /**
+   * Finds the range of a separator earlier than aEnd in aStr
+   * The range is required by findCommonSuffix() needing to know the beginning
+   * of the separator.
+   * Separators will either be space-padded punctuation or slashes (in pathmode)
+   *
+   * .+           one or more initial characters
+   * (            first group
+   *   (          second group
+   *     \s+      one or more whitespace characters
+   *     [-:>\|]+ one or more separator characters
+   *     \s+      one or more whitespace characters
+   *   )          end first group
+   *   .*?        zero or more characters, non-greedy match
+   * )            end second group
+   * $            end of input
+   *
+   * @param aPathMode true for pathmode, false otherwise
+   * @param aStr      the string to look for a separator in
+   * @param aEnd      (optional) an index to start the backwards search from
+   * @return an array containing the endpoints of a separator (-1, -1 for none)
+   */
+  lastIndexOfSep: function RedundancyFinder_lastIndexOfSep(aPathMode, aStr,
+                                                           aEnd) {
+    if (aPathMode) {
+      let path = aStr.lastIndexOf('/', aEnd);
+      return [path, path];
+    }
+
+    let string = aStr.slice(0, aEnd);
+    let match = string.match(/.+((\s+[-:>\|]+\s+).*?)$/);
+    if (match) {
+      let index = string.length - match[1].length;
+      return [index, index + match[2].length - 1];
+    }
+
+    return [-1, -1];
+  },
+
+  /**
+   * Finds a common suffix (redundancy at the end of) a pair of strings.
+   * @param aPathMode true for pathmode, false otherwise
+   * @param aStr      a base string to look for a suffix in
+   * @param aNextStr  a string that may share a common suffix with aStr
+   * @return an index indicating the divergence between the strings
+   */
+  findCommonSuffix: function RedundancyFinder_findCommonSuffix(aPathMode, aStr,
+                                                               aNextStr) {
+    let last = this.lastIndexOfSep(aPathMode, aStr)[0];
+
+    // Is there any suffix match?
+    if (!aNextStr.endsWith(aStr.slice(last))) {
+      return 0;
+    }
+
+    // Move backwards on the main string until the suffix diverges
+    let oldLast;
+    do {
+      oldLast = last;
+      last = this.lastIndexOfSep(aPathMode, aStr, last - 1)[0];
+    } while (last != -1 && aNextStr.endsWith(aStr.slice(last)));
+
+    return aStr.length - oldLast;
+  }
+};
+