Bug 480350 - Show currently loaded URIs in location bar autocomplete results, allow switching to the tab. r=gavin,mak sr=mconnor
authorBlair McBride <bmcbride@mozilla.com>
Fri, 26 Mar 2010 22:59:02 +0100
changeset 39889 dcb347b2d3d48e34ccabaff8c2276f38f09e8326
parent 39888 b69288fdd2f44c1afb93ffb568cd667b1b24ad10
child 39890 523c9e938389671381fc5bdb53f0a8bc9250d5d5
push idunknown
push userunknown
push dateunknown
reviewersgavin, mak, mconnor
bugs480350
milestone1.9.3a4pre
Bug 480350 - Show currently loaded URIs in location bar autocomplete results, allow switching to the tab. r=gavin,mak sr=mconnor
browser/app/profile/firefox.js
browser/base/content/browser.css
browser/base/content/browser.js
browser/base/content/browser.xul
browser/base/content/tabbrowser.xml
browser/base/content/test/Makefile.in
browser/base/content/test/browser_tabMatchesInAwesomebar.js
browser/base/content/urlbarBindings.xml
browser/locales/en-US/chrome/browser/browser.dtd
browser/themes/gnomestripe/browser/actionicon-tab.png
browser/themes/gnomestripe/browser/browser.css
browser/themes/gnomestripe/browser/jar.mn
browser/themes/pinstripe/browser/actionicon-tab.png
browser/themes/pinstripe/browser/browser.css
browser/themes/pinstripe/browser/jar.mn
browser/themes/winstripe/browser/actionicon-tab.png
browser/themes/winstripe/browser/browser.css
browser/themes/winstripe/browser/jar.mn
toolkit/components/places/public/mozIPlacesAutoComplete.idl
toolkit/components/places/public/nsIBrowserHistory.idl
toolkit/components/places/src/nsNavHistory.cpp
toolkit/components/places/src/nsNavHistory.h
toolkit/components/places/src/nsPlacesAutoComplete.js
toolkit/components/places/src/nsPlacesTables.h
toolkit/components/places/src/nsPlacesTriggers.h
toolkit/components/places/tests/autocomplete/head_autocomplete.js
toolkit/components/places/tests/autocomplete/test_tabmatches.js
toolkit/content/widgets/autocomplete.xml
toolkit/content/widgets/browser.xml
toolkit/locales/en-US/chrome/global/actions.dtd
toolkit/locales/jar.mn
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -235,24 +235,26 @@ pref("browser.urlbar.maxRichResults", 12
 pref("browser.urlbar.delay", 50);
 
 // The special characters below can be typed into the urlbar to either restrict
 // the search to visited history, bookmarked, tagged pages; or force a match on
 // just the title text or url.
 pref("browser.urlbar.restrict.history", "^");
 pref("browser.urlbar.restrict.bookmark", "*");
 pref("browser.urlbar.restrict.tag", "+");
+pref("browser.urlbar.restrict.openpage", "%");
 pref("browser.urlbar.restrict.typed", "~");
 pref("browser.urlbar.match.title", "#");
 pref("browser.urlbar.match.url", "@");
 
 // The default behavior for the urlbar can be configured to use any combination
 // of the restrict or match filters with each additional filter restricting
 // more (intersection). Add the following values to set the behavior as the
-// default: 1: history, 2: bookmark, 4: tag, 8: title, 16: url, 32: typed
+// default: 1: history, 2: bookmark, 4: tag, 8: title, 16: url, 32: typed,
+//          64: javascript, 128: tabs
 // E.g., 0 = show all results (no filtering), 1 = only visited pages in history,
 // 2 = only bookmarks, 3 = visited bookmarks, 1+16 = history matching in the url
 pref("browser.urlbar.default.behavior", 0);
 
 // Number of milliseconds to wait for the http headers (and thus
 // the Content-Disposition filename) before giving up and falling back to 
 // picking a filename without that info in hand so that the user sees some
 // feedback from their action.
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -46,16 +46,36 @@ toolbar[mode="icons"] > #reload-button[d
   background-position: bottom left;
 }
 
 /* ::::: location bar ::::: */
 #urlbar {
   -moz-binding: url(chrome://browser/content/urlbarBindings.xml#urlbar);
 }
 
+/* Some child nodes want to be ordered based on the locale's direction, while
+   everything else should be ltr. */
+#urlbar:-moz-locale-dir(rtl) > .autocomplete-textbox-container > .textbox-input-box {
+  direction: rtl;
+}
+
+#urlbar html|*.autocomplete-textbox {
+  direction: ltr;
+}
+
+/* For results that are actions, their description text is shown instead of
+   the URL - this needs to follow the locale's direction, unlike URLs. */
+richlistitem[type="action"]:-moz-locale-dir(rtl) > .ac-url-box {
+  direction: rtl;
+}
+
+#urlbar:not([actiontype]) > #urlbar-display {
+  display: none;
+}
+
 #wrapper-urlbar-container > #urlbar-container > #urlbar {
   -moz-user-input: disabled;
   cursor: -moz-grab;
 }
 
 #PopupAutoComplete {
   -moz-binding: url("chrome://browser/content/urlbarBindings.xml#browser-autocomplete-result-popup");
 }
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -3800,17 +3800,16 @@ var XULBrowserWindow = {
   // Stored Status, Link and Loading values
   status: "",
   defaultStatus: "",
   jsStatus: "",
   jsDefaultStatus: "",
   overLink: "",
   startTime: 0,
   statusText: "",
-  lastURI: null,
   isBusy: false,
 
   _progressCollapseTimer: 0,
 
   QueryInterface: function (aIID) {
     if (aIID.equals(Ci.nsIWebProgressListener) ||
         aIID.equals(Ci.nsIWebProgressListener2) ||
         aIID.equals(Ci.nsISupportsWeakReference) ||
@@ -3864,17 +3863,16 @@ var XULBrowserWindow = {
     // XXXjag to avoid leaks :-/, see bug 60729
     delete this.throbberElement;
     delete this.statusMeter;
     delete this.stopCommand;
     delete this.reloadCommand;
     delete this.statusTextField;
     delete this.securityButton;
     delete this.statusText;
-    delete this.lastURI;
   },
 
   setJSStatus: function (status) {
     this.jsStatus = status;
     this.updateStatusField();
   },
 
   setJSDefaultStatus: function (status) {
@@ -4079,17 +4077,16 @@ var XULBrowserWindow = {
         newSpec = newSpec.substr(0, newSpec.indexOf("#"));
       if (newSpec != oldSpec) {
         // Remove all the notifications, except for those which want to
         // persist across the first location change.
         let nBox = gBrowser.getNotificationBox(selectedBrowser);
         nBox.removeTransientNotifications();
       }
     }
-    selectedBrowser.lastURI = aLocationURI;
 
     // Disable menu entries for images, enable otherwise
     if (content.document && mimeTypeIsTextBased(content.document.contentType))
       this.isImage.removeAttribute('disabled');
     else
       this.isImage.setAttribute('disabled', 'true');
 
     this.setOverLink("", null);
@@ -7584,8 +7581,47 @@ var LightWeightThemeWebInstaller = {
     return pm.testPermission(uri, "install") == pm.ALLOW_ACTION;
   },
 
   _getThemeFromNode: function (node) {
     return this._manager.parseTheme(node.getAttribute("data-browsertheme"),
                                     node.baseURI);
   }
 }
+
+function switchToTabHavingURI(aURI) {
+  function switchIfURIInWindow(aWindow) {
+    if (!("gBrowser" in aWindow))
+      return false;
+    let browsers = aWindow.gBrowser.browsers;
+    for (let i = 0; i < browsers.length; i++) {
+      let browser = browsers[i];
+      if (browser.currentURI.equals(aURI)) {
+        gURLBar.handleRevert();
+        aWindow.focus();
+        aWindow.gBrowser.tabContainer.selectedIndex = i;
+        return true;
+      }
+    }
+    return false;
+  }
+
+  // This can be passed either nsIURI or a string.
+  if (!(aURI instanceof Ci.nsIURI))
+    aURI = makeURI(aURI);
+
+  // Prioritise this window.
+  if (switchIfURIInWindow(window))
+    return true;
+
+  let winEnum = Services.wm.getEnumerator("navigator:browser");
+  while (winEnum.hasMoreElements()) {
+    let browserWin = winEnum.getNext();
+    // Skip closed (but not yet destroyed) windows,
+    // and the current window (which was checked earlier).
+    if (browserWin.closed || browserWin == window)
+      continue;
+    if (switchIfURIInWindow(browserWin))
+      return true;
+  }
+  // No opened tab has that url.
+  return false;
+}
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -399,16 +399,17 @@
                    title="&locationItem.title;" class="chromeclass-location" removable="true">
         <textbox id="urlbar" flex="1"
                  bookmarkhistoryplaceholder="&urlbar.bookmarkhistory.emptyText;"
                  bookmarkplaceholder="&urlbar.bookmark.emptyText;"
                  historyplaceholder="&urlbar.history.emptyText;"
                  noneplaceholder="&urlbar.none.emptyText;"
                  type="autocomplete"
                  autocompletesearch="history"
+                 autocompletesearchparam="enable-actions"
                  autocompletepopup="PopupAutoCompleteRichResult"
                  completeselectedindex="true"
                  tabscrolling="true"
                  showcommentcolumn="true"
                  showimagecolumn="true"
                  enablehistory="true"
                  maxrows="6"
                  newlines="stripsurroundingwhitespace"
@@ -437,16 +438,17 @@
                        onerror="this.removeAttribute('src');"/>
               </stack>
               <hbox id="identity-icon-labels">
                 <label id="identity-icon-label" class="plain" flex="1"/>
                 <label id="identity-icon-country-label" class="plain"/>
               </hbox>
             </hbox>
           </box>
+          <label id="urlbar-display" value="&urlbar.switchToTab.label;"/>
           <hbox id="urlbar-icons">
             <button type="menu"
                     style="-moz-user-focus: none"
                     class="plain urlbar-icon"
                     id="feed-button"
                     collapsed="true"
                     tooltiptext="&feedButton.tooltip;"
                     onclick="return FeedHandler.onFeedButtonClick(event);">
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -91,16 +91,20 @@
       <field name="mURIFixup" readonly="true">
         Components.classes["@mozilla.org/docshell/urifixup;1"]
                   .getService(Components.interfaces.nsIURIFixup);
       </field>
       <field name="mFaviconService" readonly="true">
         Components.classes["@mozilla.org/browser/favicon-service;1"]
                   .getService(Components.interfaces.nsIFaviconService);
       </field>
+      <field name="mBrowserHistory" readonly="true">
+        Components.classes["@mozilla.org/browser/nav-history-service;1"]
+                  .getService(Components.interfaces.nsIBrowserHistory);
+      </field>
       <field name="mTabBox" readonly="true">
         document.getAnonymousElementByAttribute(this, "anonid", "tabbox");
       </field>
       <field name="mPanelContainer" readonly="true">
         document.getAnonymousElementByAttribute(this, "anonid", "panelcontainer");
       </field>
       <field name="mStringBundle">
         document.getAnonymousElementByAttribute(this, "anonid", "tbstringbundle");
@@ -447,42 +451,49 @@
                   aWebProgress.isLoadingDocument &&
                   !(this.mBrowser.docShell.loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE)) {
                 this.mBrowser.mIconURL = null;
               }
 
               // changing location, clear out the missing plugins list
               this.mBrowser.missingPlugins = null;
 
-              if (this.mBlank)
-                return;
-
-              if (this.mTabBrowser.mCurrentTab == this.mTab) {
-                for (let i = 0; i < this.mTabBrowser.mProgressListeners.length; i++) {
-                  let p = this.mTabBrowser.mProgressListeners[i];
+              var browserHistory = this.mTabBrowser.mBrowserHistory;
+              if ("lastURI" in this.mBrowser && this.mBrowser.lastURI)
+                browserHistory.unregisterOpenPage(this.mBrowser.lastURI);
+              browserHistory.registerOpenPage(aLocation);
+
+              if (!this.mBlank) {
+                if (this.mTabBrowser.mCurrentTab == this.mTab) {
+                  for (let i = 0; i < this.mTabBrowser.mProgressListeners.length; i++) {
+                    let p = this.mTabBrowser.mProgressListeners[i];
+                    if (p)
+                      try {
+                        p.onLocationChange(aWebProgress, aRequest, aLocation);
+                      } catch (e) {
+                        // don't inhibit other listeners
+                        Components.utils.reportError(e);
+                      }
+                  }
+                }
+
+                for (let i = 0; i < this.mTabBrowser.mTabsProgressListeners.length; i++) {
+                  let p = this.mTabBrowser.mTabsProgressListeners[i];
                   if (p)
                     try {
-                      p.onLocationChange(aWebProgress, aRequest, aLocation);
+                      p.onLocationChange(this.mBrowser, aWebProgress, aRequest, aLocation);
                     } catch (e) {
                       // don't inhibit other listeners
                       Components.utils.reportError(e);
                     }
                 }
               }
 
-              for (let i = 0; i < this.mTabBrowser.mTabsProgressListeners.length; i++) {
-                let p = this.mTabBrowser.mTabsProgressListeners[i];
-                if (p)
-                  try {
-                    p.onLocationChange(this.mBrowser, aWebProgress, aRequest, aLocation);
-                  } catch (e) {
-                    // don't inhibit other listeners
-                    Components.utils.reportError(e);
-                  }
-              }
+              this.mBrowser.lastURI = aLocation;
+
             },
 
             onStatusChange : function(aWebProgress, aRequest, aStatus, aMessage)
             {
               if (this.mBlank)
                 return;
 
               if (this.mTabBrowser.mCurrentTab == this.mTab) {
@@ -1414,16 +1425,20 @@
             aTab.dispatchEvent(evt);
 
             // Remove the tab's filter and progress listener.
             const filter = this.mTabFilters[aTab._tPos];
             browser.webProgress.removeProgressListener(filter);
             filter.removeProgressListener(this.mTabListeners[aTab._tPos]);
             this.mTabListeners[aTab._tPos].destroy();
 
+            let closedBrowser = this.getBrowserForTab(aTab);
+            if (closedBrowser.currentURI)
+              this.mBrowserHistory.unregisterOpenPage(closedBrowser.currentURI);
+
             // We are no longer the primary content area.
             browser.setAttribute("type", "content-targetable");
 
             // Remove this tab as the owner of any other tabs, since it's going away.
             Array.forEach(this.tabs, function (tab) {
               if ("owner" in tab && tab.owner == aTab)
                 // |tab| is a child of the tab we're removing, make it an orphan
                 tab.owner = null;
@@ -2255,17 +2270,19 @@
           this.appendChild(this._autoScrollPopup);
           this.mCurrentBrowser.setAttribute("autoscrollpopup", this._autoScrollPopup.id);
         ]]>
       </constructor>
 
       <destructor>
         <![CDATA[
           for (var i = 0; i < this.mTabListeners.length; ++i) {
-            this.getBrowserAtIndex(i).webProgress.removeProgressListener(this.mTabFilters[i]);
+            let browser = this.getBrowserAtIndex(i);
+            this.mBrowserHistory.unregisterOpenPage(browser.currentURI);
+            browser.webProgress.removeProgressListener(this.mTabFilters[i]);
             this.mTabFilters[i].removeProgressListener(this.mTabListeners[i]);
             this.mTabFilters[i] = null;
             this.mTabListeners[i].destroy();
             this.mTabListeners[i] = null;
           }
           document.removeEventListener("keypress", this, false);
         ]]>
       </destructor>
--- a/browser/base/content/test/Makefile.in
+++ b/browser/base/content/test/Makefile.in
@@ -145,16 +145,17 @@ include $(topsrcdir)/config/rules.mk
                  feed_tab.html \
                  plugin_unknown.html \
                  plugin_test.html \
                  plugin_both.html \
                  plugin_both2.html \
                  alltabslistener.html \
                  zoom_test.html \
                  dummy_page.html \
+                 browser_tabMatchesInAwesomebar.js \
     $(NULL)
 
 ifneq (cocoa,$(MOZ_WIDGET_TOOLKIT))
 _BROWSER_FILES += browser_bug462289.js
 else
 _BROWSER_FILES += browser_customize.js
 endif
 
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/browser_tabMatchesInAwesomebar.js
@@ -0,0 +1,231 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim:set ts=2 sw=2 sts=2 et:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Places Test Code.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Blair McBride <bmcbride@mozilla.com> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+const TEST_URL_BASES = [
+  "http://example.org/browser/browser/base/content/test/dummy_page.html#tabmatch",
+  "http://example.org/browser/browser/base/content/test/moz.png#tabmatch"
+];
+
+var gPrivateBrowsing = Cc["@mozilla.org/privatebrowsing;1"].
+                         getService(Ci.nsIPrivateBrowsingService);
+
+
+var gTabWaitCount = 0;
+var gTabCounter = 0;
+
+var gTestSteps = [
+  function() {
+    info("Running step 1");
+    for (let i = 0; i < 10; i++) {
+      let tab = gBrowser.addTab();
+      loadTab(tab, TEST_URL_BASES[0] + (++gTabCounter));
+    }
+  },
+  function() {
+    info("Running step 2");
+    gBrowser.selectTabAtIndex(1);
+    gBrowser.removeCurrentTab();
+    gBrowser.selectTabAtIndex(1);
+    gBrowser.removeCurrentTab();
+    for (let i = 1; i < gBrowser.tabs.length; i++)
+      loadTab(gBrowser.tabs[i], TEST_URL_BASES[1] + (++gTabCounter));
+  },
+  function() {
+    info("Running step 3");
+    for (let i = 1; i < gBrowser.tabs.length; i++)
+      loadTab(gBrowser.tabs[i], TEST_URL_BASES[0] + gTabCounter);
+  },
+  function() {
+    info("Running step 4");
+    let ps = Services.prefs;
+    ps.setBoolPref("browser.privatebrowsing.keep_current_session", true);
+    ps.setBoolPref("browser.tabs.warnOnClose", false);
+
+    gPrivateBrowsing.privateBrowsingEnabled = true;
+
+    executeSoon(function() {
+      ensure_opentabs_match_db();
+      nextStep();
+    });
+  },
+  function() {
+    info("Running step 5");
+    gPrivateBrowsing.privateBrowsingEnabled = false;
+
+    executeSoon(function() {
+      let ps = Services.prefs;
+      try {
+        ps.clearUserPref("browser.privatebrowsing.keep_current_session");
+      } catch (ex) {}
+      try {
+        ps.clearUserPref("browser.tabs.warnOnClose");
+      } catch (ex) {}
+
+      ensure_opentabs_match_db();
+      nextStep()
+    });
+  }
+];
+
+
+
+function test() {
+  waitForExplicitFinish();
+  nextStep();
+}
+
+function loadTab(tab, url) {
+  tab.linkedBrowser.addEventListener("load", function (event) {
+    tab.linkedBrowser.removeEventListener("load", arguments.callee, true);
+
+    if (--gTabWaitCount > 0)
+      return;
+    is(gTabWaitCount, 0,
+       "sanity check, gTabWaitCount should not be decremented below 0");
+
+    try {
+      ensure_opentabs_match_db();
+    } catch (e) {
+      ok(false, "exception from ensure_openpages_match_db: " + e);
+    }
+
+    executeSoon(nextStep);
+  }, true);
+  gTabWaitCount++;
+  tab.linkedBrowser.loadURI(url);
+}
+
+function nextStep() {
+  if (gTestSteps.length == 0) {
+    while (gBrowser.tabs.length > 1) {
+      gBrowser.selectTabAtIndex(1);
+      gBrowser.removeCurrentTab();
+    }
+
+    waitForClearHistory(finish);
+
+    return;
+  }
+
+  var stepFunc = gTestSteps.shift();
+  stepFunc();
+}
+
+function ensure_opentabs_match_db() {
+  var tabs = {};
+
+  var winEnum = Services.wm.getEnumerator("navigator:browser");
+  while (winEnum.hasMoreElements()) {
+    let browserWin = winEnum.getNext();
+    // skip closed-but-not-destroyed windows
+    if (browserWin.closed)
+      continue;
+
+    for (let i = 0; i < browserWin.gBrowser.tabContainer.childElementCount; i++) {
+      let browser = browserWin.gBrowser.getBrowserAtIndex(i);
+      let url = browser.currentURI.spec;
+      if (!(url in tabs))
+        tabs[url] = 1;
+      else
+        tabs[url]++;
+    }
+  }
+
+  var db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
+                              .DBConnection;
+
+  try {
+    var stmt = db.createStatement(
+                          "SELECT IFNULL(p_t.url, p.url) AS url, open_count, place_id " +
+                          "FROM moz_openpages_temp " +
+                          "LEFT JOIN moz_places p ON p.id=place_id " +
+                          "LEFT JOIN moz_places_temp p_t ON p_t.id=place_id");
+  } catch (e) {
+    ok(false, "error creating db statement: " + e);
+    return;
+  }
+
+  var dbtabs = [];
+  try {
+    while (stmt.executeStep()) {
+      ok(stmt.row.url in tabs,
+         "url is in db, should be in tab: " + stmt.row.url);
+      is(tabs[stmt.row.url], stmt.row.open_count,
+         "db count (" + stmt.row.open_count + ") " +
+         "should match actual open tab count " +
+         "(" + tabs[stmt.row.url] + "): " + stmt.row.url);
+      dbtabs.push(stmt.row.url);
+    }
+  } finally {
+    stmt.finalize();
+  }
+
+  for (let url in tabs) {
+    // ignore URLs that should never be in the places db
+    if (!is_expected_in_db(url))
+      continue;
+    ok(dbtabs.indexOf(url) > -1,
+       "tab is open (" + tabs[url] + " times) and should recorded in db: " + url);
+  }
+}
+
+function is_expected_in_db(url) {
+  var uri = Services.io.newURI(url, null, null);
+  return PlacesUtils.history.canAddURI(uri);
+}
+
+/**
+ * Clears history invoking callback when done.
+ */
+function waitForClearHistory(aCallback) {
+  const TOPIC_EXPIRATION_FINISHED = "places-expiration-finished";
+  let observer = {
+    observe: function(aSubject, aTopic, aData) {
+      Services.obs.removeObserver(this, TOPIC_EXPIRATION_FINISHED);
+      aCallback();
+    }
+  };
+  Services.obs.addObserver(observer, TOPIC_EXPIRATION_FINISHED, false);
+
+  let hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+           getService(Ci.nsINavHistoryService);
+  hs.QueryInterface(Ci.nsIBrowserHistory).removeAllPages();
+}
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -56,31 +56,66 @@
         this._setPlaceholder();
 
         this._urlTooltip = document.getElementById("urlTooltip");
 
         this.inputField.controllers.insertControllerAt(0, this._copyCutController);
         this.inputField.addEventListener("mousedown", this, false);
         this.inputField.addEventListener("mousemove", this, false);
         this.inputField.addEventListener("mouseout", this, false);
-        this.inputField.addEventListener("overflow", this, false);
-        this.inputField.addEventListener("underflow", this, false);
+        this.inputField.addEventListener("overflow", this, false);
+        this.inputField.addEventListener("underflow", this, false);
       ]]></constructor>
 
       <destructor><![CDATA[
         this._prefs.removeObserver("", this);
         this._prefs = null;
         this.inputField.controllers.removeController(this._copyCutController);
         this.inputField.removeEventListener("mousedown", this, false);
         this.inputField.removeEventListener("mousemove", this, false);
         this.inputField.removeEventListener("mouseout", this, false);
-        this.inputField.removeEventListener("overflow", this, false);
-        this.inputField.removeEventListener("underflow", this, false);
+        this.inputField.removeEventListener("overflow", this, false);
+        this.inputField.removeEventListener("underflow", this, false);
       ]]></destructor>
 
+      <field name="_value"></field>
+
+      <!--
+        onBeforeValueGet is called by the base-binding's .value getter.
+        It can return an object with a "value" property, to override the
+        return value of the getter.
+      -->
+      <method name="onBeforeValueGet">
+        <body><![CDATA[
+          if (this.hasAttribute("actiontype"))
+            return {value: this._value};
+          return null;
+        ]]></body>
+      </method>
+
+      <!--
+        onBeforeValueSet is called by the base-binding's .value setter.
+        It should return the value that the setter should use.
+      -->
+      <method name="onBeforeValueSet">
+        <parameter name="aValue"/>
+        <body><![CDATA[
+          this._value = aValue;
+          var returnValue = aValue;
+          var action = this._parseActionUrl(aValue);
+          if (action) {
+            returnValue = action.param;
+            this.setAttribute("actiontype", action.type);
+          } else {
+            this.removeAttribute("actiontype");
+          }
+          return returnValue;
+        ]]></body>
+      </method>
+
       <method name="handleRevert">
         <body><![CDATA[
           var isScrolling = this.popupOpen;
 
           gBrowser.userTypedValue = null;
 
           // don't revert to last valid url unless page is NOT loading
           // and user is NOT key-scrolling through autocomplete list
@@ -103,16 +138,23 @@
         <body><![CDATA[
           if (aTriggeringEvent instanceof MouseEvent && aTriggeringEvent.button == 2)
             return; // Do nothing for right clicks
 
           var [url, postData] = this._canonizeURL(aTriggeringEvent);
           if (!url)
             return;
 
+          var action = this._parseActionUrl(url);
+          if (action) {
+            if (action.type == "switchtab")
+              switchToTabHavingURI(action.param);
+            return;
+          }
+
           this.value = url;
           gBrowser.userTypedValue = url;
           try {
             addToUrlbarHistory(url);
           } catch (ex) {
             // Things may go wrong when adding url to session history,
             // but don't let that interfere with the loading of the url.
             Cu.reportError(ex);
@@ -211,22 +253,22 @@
           }
 
           var postData = {};
           url = getShortcutOrURI(url, postData);
 
           return [url, postData.value];
         ]]></body>
       </method>
-
-      <field name="_contentIsCropped">false</field>
+
+      <field name="_contentIsCropped">false</field>
 
       <method name="_initURLTooltip">
         <body><![CDATA[
-          if (this.focused || !this._contentIsCropped)
+          if (this.focused || !this._contentIsCropped)
             return;
           if (this._tooltipTimer)
             clearTimeout(this._tooltipTimer);
           this._tooltipTimer = setTimeout(function (self) {
             self._tooltipTimer = 0;
             var label = self._urlTooltip.firstChild;
             label.value = self.value;
             var bO = self.boxObject;
@@ -384,23 +426,23 @@
               }
               break;
             case "mousemove":
               this._initURLTooltip();
               break;
             case "mouseout":
               this._hideURLTooltip();
               break;
-            case "overflow":
-              this._contentIsCropped = true;
-              break;
-            case "underflow":
-              this._contentIsCropped = false;
-              this._hideURLTooltip();
-              break;
+            case "overflow":
+              this._contentIsCropped = true;
+              break;
+            case "underflow":
+              this._contentIsCropped = false;
+              this._hideURLTooltip();
+              break;
           }
         ]]></body>
       </method>
 
       <property name="textValue"
                 onget="return this.value;">
         <setter>
           <![CDATA[
@@ -437,16 +479,28 @@
               case 2:
                 type = "bookmark";
                 break;
             }
           }
           this.placeholder = this.getAttribute(type + "placeholder");
         ]]></body>
       </method>
+
+      <method name="_parseActionUrl">
+        <parameter name="aUrl"/>
+        <body><![CDATA[
+          if (!/^moz-action:/.test(aUrl))
+            return null;
+
+          // url is in the format moz-action:ACTION,PARAM
+          let [, action, param] = aUrl.match(/^moz-action:([^,]+),(.*)$/);
+          return {type: action, param: param};
+        ]]></body>
+      </method>
     </implementation>
 
     <handlers>
       <handler event="draggesture" phase="capturing"><![CDATA[
         // TODO: This should use dragstart but editor code is still using
         //       the old drag & drop APIs, so we have to handle draggesture.
         //       This can be changed once editor code is updated to the new API.
         //       See bug 499008 for details.
@@ -608,16 +662,25 @@
           // Check for middle-click or modified clicks on the URL bar
           if (gURLBar && this.mInput == gURLBar) {
             var url = controller.getValueAt(this.selectedIndex);
 
             // close the autocomplete popup and revert the entered address
             this.closePopup();
             controller.handleEscape();
 
+            // Check if this is meant to be an action
+            let action = this.mInput._parseActionUrl(url);
+            if (action) {
+              if (action.type == "switchtab")
+                url = action.param;
+              else
+                return;
+            }
+
             // respect the usual clicking subtleties
             openUILink(url, aEvent);
           }
         ]]>
         </body>
       </method>
 
       <method name="createResultLabel">
@@ -635,9 +698,10 @@
             }
             return label;
           ]]>
         </body>
       </method>
 
     </implementation>
   </binding>
+
 </bindings>
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -202,16 +202,17 @@
 <!ENTITY showAllHistoryCmd.commandkey "H">
 
 <!ENTITY openCmd.commandkey           "l">
 <!ENTITY urlbar.bookmarkhistory.emptyText "Search Bookmarks and History">
 <!ENTITY urlbar.bookmark.emptyText    "Search Bookmarks">
 <!ENTITY urlbar.history.emptyText     "Search History">
 <!ENTITY urlbar.none.emptyText        "Type a Web address">
 <!ENTITY urlbar.accesskey             "d">
+<!ENTITY urlbar.switchToTab.label     "Switch to tab:">
 
 <!-- 
   Comment duplicated from browser-sets.inc:
 
   Search Command Key Logic works like this:
   
   Unix: Ctrl+J (0.8, 0.9 support)
         Ctrl+K (cross platform binding)
new file mode 100644
index 0000000000000000000000000000000000000000..fed3d7f7cde2835388bf7ad5694e18d8042dba77
GIT binary patch
literal 293
zc%17D@N?(olHy`uVBq!ia0vp^;y^6G!3HG%>OYYHQY`6?zK#qG8~eHcB(eheoCO|{
z#S9F5hd`K7RKu$QC@5Lt8c`CQpH@<ySd_{TkeHcQqUYxtqEKe6XR2rD_uS$qP|+e!
z7sn6_|F;vpxta|`T<<zPJGWV5`olAGBYp)oZP_xnL}O|5I-PsWTzo#JP6r$L`550H
z|1e4AcTOU6eC#9Td)x=T(=Ki)+Iu9}=7=qWl-!<_XUD7OH_z!^aa5Hd%J|dy>HRWO
z=DSET@Sb0_Q?vA6U|*YhU5{b-Qmw)nKN#<?x+fg}ZjSbvt;Y}C%Qmt3c-HI1!k~$~
m{APuVmaaba{la~-3yjQFY5TRMSrdV-W$<+Mb6Mw<&;$U6Ic<Od
--- a/browser/themes/gnomestripe/browser/browser.css
+++ b/browser/themes/gnomestripe/browser/browser.css
@@ -795,16 +795,27 @@ toolbar[iconsize="small"] #fullscreen-bu
 #urlbar-search-splitter + #search-container > #searchbar > .searchbar-textbox {
   -moz-margin-start: 0;
 }
 
 #wrapper-urlbar-container #urlbar > .autocomplete-history-dropmarker {
   display: none;
 }
 
+#urlbar-display {
+  margin-top: -2px;
+  margin-bottom: -2px;
+  padding-top: 3px;
+  padding-bottom: 2px;
+  -moz-padding-end: 3px;
+  color: GrayText;
+  -moz-border-end: 1px solid #AAA;
+  -moz-margin-end: 3px;
+}
+
 #PopupAutoComplete,
 #PopupAutoCompleteRichResult {
   direction: ltr !important;
 }
 
 #PopupAutoComplete:-moz-locale-dir(rtl) > tree > treerows {
   direction: rtl;
 }
@@ -993,16 +1004,20 @@ toolbar[iconsize="small"] #fullscreen-bu
 .ac-extra > .ac-comment {
   font-size: inherit;
 }
 
 .ac-url-text {
   color: -moz-nativehyperlinktext;
 }
 
+richlistitem[type="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-icon {
+  list-style-image: url("chrome://browser/skin/actionicon-tab.png");
+}
+
 .autocomplete-treebody::-moz-tree-cell-text(treecolAutoCompleteComment) {
   color: GrayText;
 }
 
 .ac-comment[selected="true"], .ac-url-text[selected="true"] {
   color: inherit !important;
 }
 
--- a/browser/themes/gnomestripe/browser/jar.mn
+++ b/browser/themes/gnomestripe/browser/jar.mn
@@ -1,16 +1,17 @@
 browser.jar:
 % skin browser classic/1.0 %skin/classic/browser/
 % override chrome://global/skin/icons/warning-16.png moz-icon://stock/gtk-dialog-warning?size=menu
   skin/classic/browser/sanitizeDialog.css             (sanitizeDialog.css)
 * skin/classic/browser/aboutPrivateBrowsing.css             (aboutPrivateBrowsing.css)
 * skin/classic/browser/aboutSessionRestore.css        (aboutSessionRestore.css)
   skin/classic/browser/aboutSessionRestore-window-icon.png
   skin/classic/browser/aboutCertError.css             (aboutCertError.css)
+  skin/classic/browser/actionicon-tab.png
 * skin/classic/browser/browser.css                    (browser.css)
 * skin/classic/browser/engineManager.css              (engineManager.css)
   skin/classic/browser/fullscreen-video.css
   skin/classic/browser/Geo.png
   skin/classic/browser/Go-arrow.png
   skin/classic/browser/identity.png
   skin/classic/browser/Info.png
   skin/classic/browser/KUI-close.png
new file mode 100644
index 0000000000000000000000000000000000000000..fed3d7f7cde2835388bf7ad5694e18d8042dba77
GIT binary patch
literal 293
zc%17D@N?(olHy`uVBq!ia0vp^;y^6G!3HG%>OYYHQY`6?zK#qG8~eHcB(eheoCO|{
z#S9F5hd`K7RKu$QC@5Lt8c`CQpH@<ySd_{TkeHcQqUYxtqEKe6XR2rD_uS$qP|+e!
z7sn6_|F;vpxta|`T<<zPJGWV5`olAGBYp)oZP_xnL}O|5I-PsWTzo#JP6r$L`550H
z|1e4AcTOU6eC#9Td)x=T(=Ki)+Iu9}=7=qWl-!<_XUD7OH_z!^aa5Hd%J|dy>HRWO
z=DSET@Sb0_Q?vA6U|*YhU5{b-Qmw)nKN#<?x+fg}ZjSbvt;Y}C%Qmt3c-HI1!k~$~
m{APuVmaaba{la~-3yjQFY5TRMSrdV-W$<+Mb6Mw<&;$U6Ic<Od
--- a/browser/themes/pinstripe/browser/browser.css
+++ b/browser/themes/pinstripe/browser/browser.css
@@ -863,16 +863,27 @@ toolbar[iconsize="small"] #unified-back-
 #wrapper-urlbar-container .autocomplete-history-dropmarker > .dropmarker-icon {
   display: none;
 }
 
 #wrapper-urlbar-container .autocomplete-history-dropmarker {
   width: 10px;
 }
 
+#urlbar-display {
+  margin-top: -2px;
+  margin-bottom: -2px;
+  padding-top: 3px;
+  padding-bottom: 2px;
+  -moz-padding-end: 3px;
+  color: GrayText;
+  -moz-border-end: 1px solid #AAA;
+  -moz-margin-end: 3px;
+}
+
 #PopupAutoCompleteRichResult {
   direction: ltr !important;
   margin-top: 2px;
 }
 
 statusbarpanel#statusbar-display {
   -moz-padding-start: 0;
 }
@@ -924,16 +935,20 @@ richlistitem[selected="true"][current="t
   font-size: inherit;
 }
 
 .ac-url-text {
   color: -moz-nativehyperlinktext;
   font-size: 0.95em;
 }
 
+richlistitem[type="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-icon {
+  list-style-image: url("chrome://browser/skin/actionicon-tab.png");
+}
+
 .autocomplete-treebody::-moz-tree-cell-text(treecolAutoCompleteComment) {
   color: GrayText;
 }
 
 .ac-comment[selected="true"], .ac-url-text[selected="true"] {
   color: inherit !important;
 }
 
--- a/browser/themes/pinstripe/browser/jar.mn
+++ b/browser/themes/pinstripe/browser/jar.mn
@@ -1,15 +1,16 @@
 browser.jar:
 % skin browser classic/1.0 %skin/classic/browser/
   skin/classic/browser/sanitizeDialog.css                   (sanitizeDialog.css)
 * skin/classic/browser/aboutPrivateBrowsing.css             (aboutPrivateBrowsing.css)
 * skin/classic/browser/aboutSessionRestore.css              (aboutSessionRestore.css)
   skin/classic/browser/aboutSessionRestore-window-icon.png
   skin/classic/browser/aboutCertError.css                   (aboutCertError.css)
+  skin/classic/browser/actionicon-tab.png
 * skin/classic/browser/browser.css                          (browser.css)
 * skin/classic/browser/engineManager.css                    (engineManager.css)
   skin/classic/browser/feed-icons.png
   skin/classic/browser/fullscreen-video.css
   skin/classic/browser/Geo.png
   skin/classic/browser/Go-arrow.png
   skin/classic/browser/home.png
   skin/classic/browser/hud-panel.png
new file mode 100644
index 0000000000000000000000000000000000000000..fed3d7f7cde2835388bf7ad5694e18d8042dba77
GIT binary patch
literal 293
zc%17D@N?(olHy`uVBq!ia0vp^;y^6G!3HG%>OYYHQY`6?zK#qG8~eHcB(eheoCO|{
z#S9F5hd`K7RKu$QC@5Lt8c`CQpH@<ySd_{TkeHcQqUYxtqEKe6XR2rD_uS$qP|+e!
z7sn6_|F;vpxta|`T<<zPJGWV5`olAGBYp)oZP_xnL}O|5I-PsWTzo#JP6r$L`550H
z|1e4AcTOU6eC#9Td)x=T(=Ki)+Iu9}=7=qWl-!<_XUD7OH_z!^aa5Hd%J|dy>HRWO
z=DSET@Sb0_Q?vA6U|*YhU5{b-Qmw)nKN#<?x+fg}ZjSbvt;Y}C%Qmt3c-HI1!k~$~
m{APuVmaaba{la~-3yjQFY5TRMSrdV-W$<+Mb6Mw<&;$U6Ic<Od
--- a/browser/themes/winstripe/browser/browser.css
+++ b/browser/themes/winstripe/browser/browser.css
@@ -605,16 +605,27 @@ toolbar:not([iconsize="small"])[mode="ic
   display: none;
 }
 
 #urlbar > .autocomplete-textbox-container {
   direction: ltr;
   -moz-box-align: stretch;
 }
 
+#urlbar-display {
+  margin-top: -2px;
+  margin-bottom: -2px;
+  padding-top: 3px;
+  padding-bottom: 2px;
+  -moz-padding-end: 3px;
+  color: GrayText;
+  -moz-border-end: 1px solid #AAA;
+  -moz-margin-end: 3px;
+}
+
 /* identity box */
 
 #identity-box {
   background-color: -moz-dialog;
   background-image: -moz-linear-gradient(rgba(255,255,255,.25), rgba(0,0,0,.15));
   color: -moz-dialogText;
   -moz-border-radius: 2px;
 }
@@ -761,16 +772,20 @@ toolbar:not([iconsize="small"])[mode="ic
 .ac-url-text {
   color: -moz-nativehyperlinktext;
 }
 
 .ac-url-text:-moz-system-metric(windows-default-theme) {
   color: #006600;
 }
 
+richlistitem[type="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-icon {
+  list-style-image: url("chrome://browser/skin/actionicon-tab.png");
+}
+
 .autocomplete-treebody::-moz-tree-cell-text(treecolAutoCompleteComment) {
   color: GrayText;
 }
 
 .ac-comment[selected="true"], .ac-url-text[selected="true"] {
   color: inherit !important;
 }
 
--- a/browser/themes/winstripe/browser/jar.mn
+++ b/browser/themes/winstripe/browser/jar.mn
@@ -3,16 +3,17 @@ browser.jar:
 % skin browser classic/1.0 %skin/classic/browser/ os!=WINNT
 # NOTE: If you add a new file here, you'll need to add it to the aero
 # section at the bottom of this file
         skin/classic/browser/sanitizeDialog.css                      (sanitizeDialog.css)
 *       skin/classic/browser/aboutPrivateBrowsing.css                (aboutPrivateBrowsing.css)
 *       skin/classic/browser/aboutSessionRestore.css                 (aboutSessionRestore.css)
         skin/classic/browser/aboutSessionRestore-window-icon.png     (aboutSessionRestore-window-icon.png)
         skin/classic/browser/aboutCertError.css                      (aboutCertError.css)
+        skin/classic/browser/actionicon-tab.png
 *       skin/classic/browser/browser.css                             (browser.css)
 *       skin/classic/browser/engineManager.css                       (engineManager.css)
         skin/classic/browser/fullscreen-video.css
         skin/classic/browser/Geo.png                                 (Geo.png)
         skin/classic/browser/Info.png                                (Info.png)
         skin/classic/browser/identity.png                            (identity.png)
         skin/classic/browser/keyhole-forward-mask.svg
         skin/classic/browser/KUI-background.png
@@ -88,16 +89,17 @@ browser.jar:
 #ifdef XP_WIN
 browser.jar:
 % skin browser classic/1.0 %skin/classic/aero/browser/ os=WINNT osversion>=6
         skin/classic/aero/browser/sanitizeDialog.css                       (sanitizeDialog.css)
 *       skin/classic/aero/browser/aboutPrivateBrowsing.css           (aboutPrivateBrowsing.css)
 *       skin/classic/aero/browser/aboutSessionRestore.css            (aboutSessionRestore.css)
         skin/classic/aero/browser/aboutSessionRestore-window-icon.png (aboutSessionRestore-window-icon-aero.png)
         skin/classic/aero/browser/aboutCertError.css                 (aboutCertError.css)
+        skin/classic/aero/browser/actionicon-tab.png                 (actionicon-tab.png)
 *       skin/classic/aero/browser/browser.css                        (browser-aero.css)
 *       skin/classic/aero/browser/engineManager.css                  (engineManager.css)
         skin/classic/aero/browser/fullscreen-video.css
         skin/classic/aero/browser/Geo.png                            (Geo-aero.png)
         skin/classic/aero/browser/Info.png                           (Info-aero.png)
         skin/classic/aero/browser/identity.png                       (identity-aero.png)
         skin/classic/aero/browser/keyhole-forward-mask.svg
         skin/classic/aero/browser/KUI-background.png
--- a/toolkit/components/places/public/mozIPlacesAutoComplete.idl
+++ b/toolkit/components/places/public/mozIPlacesAutoComplete.idl
@@ -102,9 +102,18 @@ interface mozIPlacesAutoComplete : nsISu
    * Search for typed pages.
    */
   const long BEHAVIOR_TYPED = 1 << 5;
 
   /**
    * Search javascript: URLs.
    */
   const long BEHAVIOR_JAVASCRIPT = 1 << 6;
+
+  /**
+   * Search for pages that have been marked as being opened, such as a tab
+   * in a tabbrowser, via:
+   *   nsIBrowserHistory.registerOpenPage(url)
+   *
+   * @see nsIBrowserHistory.idl
+   */
+  const long BEHAVIOR_OPENPAGE = 1 << 7;
 };
--- a/toolkit/components/places/public/nsIBrowserHistory.idl
+++ b/toolkit/components/places/public/nsIBrowserHistory.idl
@@ -37,17 +37,17 @@
 
 /*
  * browser-specific interface to global history
  */
 
 #include "nsISupports.idl"
 #include "nsIGlobalHistory2.idl"
 
-[scriptable, uuid(124a8db3-59da-48ac-a563-6dcf58e035b4)]
+[scriptable, uuid(540aca25-1e01-467f-b24c-df89cbe40f8d)]
 interface nsIBrowserHistory : nsIGlobalHistory2
 {
     /**
      * addPageWithDetails
      *
      * Adds a page to history with specific time stamp information. This is used in
      * the History migrator. 
      */
@@ -151,9 +151,23 @@ interface nsIBrowserHistory : nsIGlobalH
 
     /**
      * markPageAsFollowedLink
      *
      * Designate the url as coming from a link explicitly followed by
      * the user (for example by clicking on it).
      */
     void markPageAsFollowedLink(in nsIURI aURI);
+
+    /**
+     * Mark a page as being currently open.
+     */
+    void registerOpenPage(in nsIURI aURI);
+
+    /**
+     * Mark a page as no longer being open (either by closing the window or tab,
+     * or by navigating away from that page).
+     *
+     * Note that when Private Browsing mode is entered/exited, pages need to be
+     * manually unregistered/registered.
+     */
+    void unregisterOpenPage(in nsIURI aURI);
 };
--- a/toolkit/components/places/src/nsNavHistory.cpp
+++ b/toolkit/components/places/src/nsNavHistory.cpp
@@ -986,16 +986,23 @@ nsNavHistory::InitTempTables()
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = mDBConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_HISTORYVISITS_TEMP_VISITDATE);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = mDBConn->ExecuteSimpleSQL(CREATE_MOZ_HISTORYVISITS_SYNC_TRIGGER);
   NS_ENSURE_SUCCESS(rv, rv);
 
+  // moz_openpages_temp
+  rv = mDBConn->ExecuteSimpleSQL(CREATE_MOZ_OPENPAGES_TEMP);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = mDBConn->ExecuteSimpleSQL(CREATE_REMOVEOPENPAGE_CLEANUP_TRIGGER);
+  NS_ENSURE_SUCCESS(rv, rv);
+
   return NS_OK;
 }
 
 nsresult
 nsNavHistory::InitViews()
 {
   nsresult rv;
 
@@ -1212,16 +1219,37 @@ nsNavHistory::InitStatements()
 
   // mDBSetPlaceTitle
   rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
       "UPDATE moz_places_view "
       "SET title = ?1 "
       "WHERE url = ?2"),
     getter_AddRefs(mDBSetPlaceTitle));
   NS_ENSURE_SUCCESS(rv, rv);
+
+  // mDBRegisterOpenPage
+  rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
+      "INSERT OR REPLACE INTO moz_openpages_temp (place_id, open_count) "
+      "VALUES (?1, "
+        "IFNULL("
+          "(SELECT open_count + 1 FROM moz_openpages_temp WHERE place_id = ?1), "
+          "1"
+        ")"
+      ")"),
+    getter_AddRefs(mDBRegisterOpenPage));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // mDBUnregisterOpenPage
+  rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
+      "UPDATE moz_openpages_temp "
+      "SET open_count = open_count - 1 "
+      "WHERE place_id = ?1"),
+    getter_AddRefs(mDBUnregisterOpenPage));
+  NS_ENSURE_SUCCESS(rv, rv);
+
   // mDBVisitsForFrecency
   // NOTE: This is not limited to visits with "visit_type NOT IN (0,4,7,8)"
   // because otherwise mDBVisitsForFrecency would return no visits
   // for places with only embed (or undefined) visits.  That would
   // cause an incorrect frecency, see CalculateFrecencyInternal().
   // In such a case a place with only EMBED visits would result in a non-zero
   // frecency.
   // In case of a temporary or permanent redirect, calculate the frecency as if
@@ -5030,16 +5058,82 @@ nsNavHistory::MarkPageAsFollowedLink(nsI
   if (mRecentLink.Count() > RECENT_EVENT_QUEUE_MAX_LENGTH)
     ExpireNonrecentEvents(&mRecentLink);
 
   mRecentLink.Put(uriString, GetNow());
   return NS_OK;
 }
 
 
+NS_IMETHODIMP
+nsNavHistory::RegisterOpenPage(nsIURI* aURI)
+{
+  NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+  NS_ENSURE_ARG(aURI);
+
+  // Don't add any pages while in Private Browsing mode, so as to avoid leaking
+  // information about other windows that might otherwise stay hidden
+  // and private.
+  if (InPrivateBrowsingMode())
+    return NS_OK;
+
+  PRBool canAdd = PR_FALSE;
+  nsresult rv = CanAddURI(aURI, &canAdd);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  PRInt64 placeId;
+  // Note: If the URI has never been added to history (but can be added),
+  // LAZY_ADD will cause this to add an orphan page, until the visit is added.
+  rv = GetUrlIdFor(aURI, &placeId, canAdd);
+  NS_ENSURE_SUCCESS(rv, rv);
+  if (placeId == 0)
+    return NS_OK;
+
+  mozStorageStatementScoper scoper(mDBRegisterOpenPage);
+
+  rv = mDBRegisterOpenPage->BindInt64Parameter(0, placeId);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = mDBRegisterOpenPage->Execute();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  return NS_OK;
+}
+
+
+NS_IMETHODIMP
+nsNavHistory::UnregisterOpenPage(nsIURI* aURI)
+{
+  NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+  NS_ENSURE_ARG(aURI);
+
+  // Entering Private Browsing mode will unregister all open pages, therefore
+  // there shouldn't be anything in the moz_openpages_temp table. So we can stop
+  // now without doing any unnecessary work.
+  if (InPrivateBrowsingMode())
+    return NS_OK;
+
+  PRInt64 placeId;
+  nsresult rv = GetUrlIdFor(aURI, &placeId, PR_FALSE);
+  NS_ENSURE_SUCCESS(rv, rv);
+  if (placeId == 0)
+    return NS_OK;
+
+  mozStorageStatementScoper scoper(mDBUnregisterOpenPage);
+
+  rv = mDBUnregisterOpenPage->BindInt64Parameter(0, placeId);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = mDBUnregisterOpenPage->Execute();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  return NS_OK;
+}
+
+
 // nsNavHistory::SetCharsetForURI
 //
 // Sets the character-set for a URI.
 // If aCharset is empty remove character-set annotation for aURI.
 
 NS_IMETHODIMP
 nsNavHistory::SetCharsetForURI(nsIURI* aURI,
                                const nsAString& aCharset)
@@ -8149,16 +8243,18 @@ nsNavHistory::FinalizeStatements() {
     mDBSetPlaceTitle,
     mDBVisitToURLResult,
     mDBVisitToVisitResult,
     mDBBookmarkToUrlResult,
     mDBVisitsForFrecency,
     mDBUpdateFrecencyAndHidden,
     mDBGetPlaceVisitStats,
     mDBFullVisitCount,
+    mDBRegisterOpenPage,
+    mDBUnregisterOpenPage,
   };
 
   for (PRUint32 i = 0; i < NS_ARRAY_LENGTH(stmts); i++) {
     nsresult rv = nsNavHistory::FinalizeStatement(stmts[i]);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   return NS_OK;
--- a/toolkit/components/places/src/nsNavHistory.h
+++ b/toolkit/components/places/src/nsNavHistory.h
@@ -422,16 +422,18 @@ protected:
   nsCOMPtr<mozIStorageStatement> mDBInsertVisit; // used by AddVisit
   nsCOMPtr<mozIStorageStatement> mDBGetPageVisitStats; // used by AddVisit
   nsCOMPtr<mozIStorageStatement> mDBIsPageVisited; // used by IsURIStringVisited
   nsCOMPtr<mozIStorageStatement> mDBUpdatePageVisitStats; // used by AddVisit
   nsCOMPtr<mozIStorageStatement> mDBAddNewPage; // used by InternalAddNewPage
   nsCOMPtr<mozIStorageStatement> mDBGetTags; // used by GetTags
   nsCOMPtr<mozIStorageStatement> mDBGetItemsWithAnno; // used by AutoComplete::StartSearch and FilterResultSet
   nsCOMPtr<mozIStorageStatement> mDBSetPlaceTitle; // used by SetPageTitleInternal
+  nsCOMPtr<mozIStorageStatement> mDBRegisterOpenPage; // used by RegisterOpenPage
+  nsCOMPtr<mozIStorageStatement> mDBUnregisterOpenPage; // used by UnregisterOpenPage
 
   // these are used by VisitIdToResultNode for making new result nodes from IDs
   // Consumers need to use the getters since these statements are lazily created
   mozIStorageStatement *GetDBVisitToURLResult();
   nsCOMPtr<mozIStorageStatement> mDBVisitToURLResult; // kGetInfoIndex_* results
   mozIStorageStatement *GetDBVisitToVisitResult();
   nsCOMPtr<mozIStorageStatement> mDBVisitToVisitResult; // kGetInfoIndex_* results
   mozIStorageStatement *GetDBBookmarkToUrlResult();
--- a/toolkit/components/places/src/nsPlacesAutoComplete.js
+++ b/toolkit/components/places/src/nsPlacesAutoComplete.js
@@ -89,16 +89,17 @@ const kQueryIndexTitle = 1;
 const kQueryIndexFaviconURL = 2;
 const kQueryIndexParentId = 3;
 const kQueryIndexBookmarkTitle = 4;
 const kQueryIndexTags = 5;
 const kQueryIndexVisitCount = 6;
 const kQueryIndexTyped = 7;
 const kQueryIndexPlaceId = 8;
 const kQueryIndexQueryType = 9;
+const kQueryIndexOpenPageCount = 10;
 
 // AutoComplete query type constants.  Describes the various types of queries
 // that we can process.
 const kQueryTypeKeyword = 0;
 const kQueryTypeFiltered = 1;
 
 // This separator is used as an RTL-friendly way to split the title and tags.
 // It can also be used by an nsIAutoCompleteResult consumer to re-split the
@@ -212,19 +213,21 @@ function nsPlacesAutoComplete()
 
   // Define common pieces of various queries.
   // TODO bug 412736 in case of a frecency tie, break it with h.typed and
   // h.visit_count which is better than nothing.  This is slow, so not doing it
   // yet...
   // Note: h.frecency is only selected because we need it for ordering.
   function sql_base_fragment(aTableName) {
     return "SELECT h.url, h.title, f.url, " + kBookTagSQLFragment + ", " +
-                  "h.visit_count, h.typed, h.id, :query_type, h.frecency " +
+                  "h.visit_count, h.typed, h.id, :query_type, t.open_count, " +
+                  "h.frecency " +
            "FROM " + aTableName + " h " +
            "LEFT OUTER JOIN moz_favicons f ON f.id = h.favicon_id " +
+           "LEFT OUTER JOIN moz_openpages_temp t ON t.place_id = h.id " +
            "WHERE h.frecency <> 0 " +
            "AND AUTOCOMPLETE_MATCH(:searchString, h.url, " +
                                   "IFNULL(bookmark, h.title), tags, " +
                                   "h.visit_count, h.typed, parent, " +
                                   ":matchBehavior, :searchBehavior) " +
           "{ADDITIONAL_CONDITIONS} ";
   }
   const SQL_BASE = sql_base_fragment("moz_places_temp") +
@@ -286,16 +289,23 @@ function nsPlacesAutoComplete()
 
   XPCOMUtils.defineLazyGetter(this, "_tagsQuery", function() {
     let replacementText = "AND tags IS NOT NULL";
     return this._db.createStatement(
       SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g")
     );
   });
 
+  XPCOMUtils.defineLazyGetter(this, "_openPagesQuery", function() {
+    let replacementText = "AND t.open_count > 0";
+    return this._db.createStatement(
+      SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g")
+    );
+  });
+
   XPCOMUtils.defineLazyGetter(this, "_typedQuery", function() {
     let replacementText = "AND h.typed = 1";
     return this._db.createStatement(
       SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g")
     );
   });
 
   XPCOMUtils.defineLazyGetter(this, "_adaptiveQuery", function() {
@@ -306,28 +316,29 @@ function nsPlacesAutoComplete()
     // title).
     return this._db.createStatement(
       "/* do not warn (bug 487789) */ " +
       "SELECT IFNULL(h_t.url, h.url) AS c_url, " +
              "IFNULL(h_t.title, h.title) AS c_title, f.url, " +
               kBookTagSQLFragment + ", " +
               "IFNULL(h_t.visit_count, h.visit_count) AS c_visit_count, " +
               "IFNULL(h_t.typed, h.typed) AS c_typed, " +
-              "IFNULL(h_t.id, h.id), :query_type, rank " +
+              "IFNULL(h_t.id, h.id), :query_type, t.open_count, rank " +
       "FROM ( " +
         "SELECT ROUND(MAX(((i.input = :search_string) + " +
                           "(SUBSTR(i.input, 1, LENGTH(:search_string)) = :search_string)) * " +
                           "i.use_count), 1) AS rank, place_id " +
         "FROM moz_inputhistory i " +
         "GROUP BY i.place_id " +
         "HAVING rank > 0 " +
       ") AS i " +
       "LEFT JOIN moz_places h ON h.id = i.place_id " +
       "LEFT JOIN moz_places_temp h_t ON h_t.id = i.place_id " +
       "LEFT JOIN moz_favicons f ON f.id = IFNULL(h_t.favicon_id, h.favicon_id) " + 
+      "LEFT JOIN moz_openpages_temp t ON t.place_id = i.place_id " +
       "WHERE c_url NOTNULL " +
       "AND AUTOCOMPLETE_MATCH(:searchString, c_url, " +
                              "IFNULL(bookmark, c_title), tags, " +
                              "c_visit_count, c_typed, parent, " +
                              ":matchBehavior, :searchBehavior) " +
       "ORDER BY rank DESC, IFNULL(h_t.frecency, h.frecency) DESC"
     );
   });
@@ -338,22 +349,23 @@ function nsPlacesAutoComplete()
       "SELECT IFNULL( " +
           "(SELECT REPLACE(url, '%s', :query_string) FROM moz_places_temp WHERE id = b.fk), " +
           "(SELECT REPLACE(url, '%s', :query_string) FROM moz_places WHERE id = b.fk) " +
         ") AS search_url, IFNULL(h_t.title, h.title), " +
         "COALESCE(f.url, " + best_favicon_for_revhost("moz_places_temp") + "," +
                   best_favicon_for_revhost("moz_places") + "), b.parent, " +
         "b.title, NULL, IFNULL(h_t.visit_count, h.visit_count), " +
         "IFNULL(h_t.typed, h.typed), COALESCE(h_t.id, h.id, b.fk), " +
-        ":query_type " +
+        ":query_type, t.open_count " +
       "FROM moz_keywords k " +
       "JOIN moz_bookmarks b ON b.keyword_id = k.id " +
       "LEFT JOIN moz_places AS h ON h.url = search_url " +
       "LEFT JOIN moz_places_temp AS h_t ON h_t.url = search_url " +
       "LEFT JOIN moz_favicons f ON f.id = IFNULL(h_t.favicon_id, h.favicon_id) " +
+      "LEFT JOIN moz_openpages_temp t ON t.place_id = IFNULL(h_t.id, h.id) " +
       "WHERE LOWER(k.keyword) = LOWER(:keyword) " +
       "ORDER BY IFNULL(h_t.frecency, h.frecency) DESC"
     );
   });
 
   //////////////////////////////////////////////////////////////////////////////
   //// Initialization
 
@@ -382,16 +394,19 @@ nsPlacesAutoComplete.prototype = {
 
     // We want to store the original string with no leading or trailing
     // whitespace for case sensitive searches.
     this._originalSearchString = aSearchString.trim();
 
     this._currentSearchString =
       this._fixupSearchText(this._originalSearchString.toLowerCase());
 
+    var searchParamParts = aSearchParam.split(" ");
+    this._enableActions = searchParamParts.indexOf("enable-actions") != -1;
+
     this._listener = aListener;
     let result = Cc["@mozilla.org/autocomplete/simple-result;1"].
                  createInstance(Ci.nsIAutoCompleteSimpleResult);
     result.setSearchString(aSearchString);
     result.setListener(this);
     this._result = result;
 
     // If we are not enabled, we need to return now.
@@ -514,16 +529,17 @@ nsPlacesAutoComplete.prototype = {
       delete this._prefs;
 
       // Finalize the statements that we have used.
       let stmts = [
         "_defaultQuery",
         "_historyQuery",
         "_bookmarkQuery",
         "_tagsQuery",
+        "_openPagesQuery",
         "_typedQuery",
         "_adaptiveQuery",
         "_keywordQuery",
       ];
       for (let i = 0; i < stmts.length; i++) {
         // We do not want to create any query we haven't already created, so
         // see if it is a getter first.  __lookupGetter__ returns null if it is
         // actually a statement.
@@ -593,16 +609,17 @@ nsPlacesAutoComplete.prototype = {
     delete this._originalSearchString;
     delete this._currentSearchString;
     delete this._searchTokens;
     delete this._listener;
     delete this._result;
     delete this._usedPlaceIds;
     delete this._pendingQuery;
     this._secondPass = false;
+    this._enableActions = false;
   },
 
   /**
    * Executes the given queries asynchronously.
    *
    * @param aQueries
    *        The queries to execute.
    */
@@ -675,23 +692,24 @@ nsPlacesAutoComplete.prototype = {
     this._enabled = safeGetter("autocomplete.enabled", true);
     this._matchBehavior = safeGetter("matchBehavior", MATCH_BOUNDARY_ANYWHERE);
     this._filterJavaScript = safeGetter("filter.javascript", true);
     this._maxRichResults = safeGetter("maxRichResults", 25);
     this._restrictHistoryToken = safeGetter("restrict.history", "^");
     this._restrictBookmarkToken = safeGetter("restrict.bookmark", "*");
     this._restrictTypedToken = safeGetter("restrict.typed", "~");
     this._restrictTagToken = safeGetter("restrict.tag", "+");
+    this._restrictOpenPageToken = safeGetter("restrict.openpage", "%");
     this._matchTitleToken = safeGetter("match.title", "#");
     this._matchURLToken = safeGetter("match.url", "@");
     this._defaultBehavior = safeGetter("default.behavior", 0);
     // Further restrictions to apply for "empty searches" (i.e. searches for "").
-    // By default we use (HISTORY | TYPED) = 33.
+    // By default we use (HISTORY | TYPED | OPENPAGE) = 161.
     this._emptySearchDefaultBehavior = this._defaultBehavior |
-                                       safeGetter("default.behavior.emptyRestriction", 33);
+                                       safeGetter("default.behavior.emptyRestriction", 161);
 
     // Validate matchBehavior; default to MATCH_BOUNDARY_ANYWHERE.
     if (this._matchBehavior != MATCH_ANYWHERE &&
         this._matchBehavior != MATCH_BOUNDARY &&
         this._matchBehavior != MATCH_BEGINNING)
       this._matchBehavior = MATCH_BOUNDARY_ANYWHERE;
 
     // register observer
@@ -722,16 +740,21 @@ nsPlacesAutoComplete.prototype = {
           this._setBehavior("history");
           break;
         case this._restrictBookmarkToken:
           this._setBehavior("bookmark");
           break;
         case this._restrictTagToken:
           this._setBehavior("tag");
           break;
+        case this._restrictOpenPageToken:
+          if (!this._enableActions)
+            continue;
+          this._setBehavior("openpage");
+          break;
         case this._matchTitleToken:
           this._setBehavior("title");
           break;
         case this._matchURLToken:
           this._setBehavior("url");
           break;
         case this._restrictTypedToken:
           this._setBehavior("typed");
@@ -775,16 +798,17 @@ nsPlacesAutoComplete.prototype = {
   {
     // We use more optimized queries for restricted searches, so we will always
     // return the most restrictive one to the least restrictive one if more than
     // one token is found.
     let query = this._hasBehavior("tag") ? this._tagsQuery :
                 this._hasBehavior("bookmark") ? this._bookmarkQuery :
                 this._hasBehavior("typed") ? this._typedQuery :
                 this._hasBehavior("history") ? this._historyQuery :
+                this._hasBehavior("openpage") ? this._openPagesQuery :
                 this._defaultQuery;
 
     // Bind the needed parameters to the query so consumers can use it.
     let (params = query.params) {
       params.parent = this._bs.tagsFolder;
       params.query_type = kQueryTypeFiltered;
       params.matchBehavior = aMatchBehavior;
       params.searchBehavior = this._behavior;
@@ -875,16 +899,17 @@ nsPlacesAutoComplete.prototype = {
 
     let escapedEntryURL = aRow.getResultByIndex(kQueryIndexURL);
     let entryTitle = aRow.getResultByIndex(kQueryIndexTitle) || "";
     let entryFavicon = aRow.getResultByIndex(kQueryIndexFaviconURL) || "";
     let entryParentId = aRow.getResultByIndex(kQueryIndexParentId);
     let entryBookmarkTitle = entryParentId ?
       aRow.getResultByIndex(kQueryIndexBookmarkTitle) : null;
     let entryTags = aRow.getResultByIndex(kQueryIndexTags) || "";
+    let openPageCount = aRow.getResultByIndex(kQueryIndexOpenPageCount) || 0;
 
     // Always prefer the bookmark title unless it is empty
     let title = entryBookmarkTitle || entryTitle;
 
     let style;
     if (aRow.getResultByIndex(kQueryIndexQueryType) == kQueryTypeKeyword) {
       // If we do not have a title, then we must have a keyword, so let the UI
       // know it is a keyword.  Otherwise, we found an exact page match, so just
@@ -921,18 +946,32 @@ nsPlacesAutoComplete.prototype = {
       if (showTags)
         style = "tag";
       else if (entryParentId)
         style = "bookmark";
       else
         style = "favicon";
     }
 
-    // And finally add this to our results.
-    this._addToResults(entryId, escapedEntryURL, title, entryFavicon, style);
+    // If actions aren't enabled, avoid doing any additional work.
+    if (!this._enableActions) {
+      this._addToResults(entryId, escapedEntryURL, title, entryFavicon, style);
+      return true;
+    }
+
+    // Add a special entry for an open-page match.
+    if ((this._hasBehavior("openpage") || this._hasBehavior("everything")) &&
+        openPageCount > 0)
+      this._addToResults(entryId, "moz-action:switchtab," + escapedEntryURL, title, entryFavicon, "action");
+
+    // If restricting to only open-page matches, there should only be the
+    // switch-to-tab results.
+    if (!this._onlyHasBehavior("openpage"))
+      this._addToResults(entryId, escapedEntryURL, title, entryFavicon, style);
+
     return true;
   },
 
   /**
    * Checks to see if the given place has already been added to the results.
    *
    * @param aPlaceId
    *        The place_id to check for.
@@ -976,26 +1015,43 @@ nsPlacesAutoComplete.prototype = {
 
     this._result.appendMatch(aURISpec, aTitle, favicon, aStyle);
   },
 
   /**
    * Determines if the specified AutoComplete behavior is set.
    *
    * @param aType
-   *        The behavior type to test for.
+   *        The behavior type to test for, or "everything" to test if no
+   *        specific behavior has been set.
    * @return true if the behavior is set, false otherwise.
    */
   _hasBehavior: function PAC_hasBehavior(aType)
   {
+    if (aType == "everything")
+      return this._behavior == 0;
     return (this._behavior &
             Ci.mozIPlacesAutoComplete["BEHAVIOR_" + aType.toUpperCase()]);
   },
 
   /**
+   * Determines if the specified AutoComplete behavior is the only behavior set.
+   *
+   * @param aType
+   *        The behavior type to test for.
+   * @return true if the behavior is set and no other behaviors are set,
+   *         false otherwise.
+   */
+  _onlyHasBehavior: function PAC_onlyHasBehavior(aType)
+  {
+    return (this._behavior ==
+            Ci.mozIPlacesAutoComplete["BEHAVIOR_" + aType.toUpperCase()]);
+  },
+
+  /**
    * Enables the desired AutoComplete behavior.
    *
    * @param aType
    *        The behavior type to set.
    */
   _setBehavior: function PAC_setBehavior(aType)
   {
     this._behavior |=
--- a/toolkit/components/places/src/nsPlacesTables.h
+++ b/toolkit/components/places/src/nsPlacesTables.h
@@ -172,9 +172,16 @@
 
 #define CREATE_MOZ_KEYWORDS NS_LITERAL_CSTRING( \
   "CREATE TABLE moz_keywords (" \
     "  id INTEGER PRIMARY KEY AUTOINCREMENT" \
     ", keyword TEXT UNIQUE" \
   ")" \
 )
 
+#define CREATE_MOZ_OPENPAGES_TEMP NS_LITERAL_CSTRING( \
+  "CREATE TEMP TABLE moz_openpages_temp (" \
+    "  place_id INTEGER PRIMARY KEY" \
+    ", open_count INTEGER" \
+  ")" \
+)
+
 #endif // __nsPlacesTables_h__
--- a/toolkit/components/places/src/nsPlacesTriggers.h
+++ b/toolkit/components/places/src/nsPlacesTriggers.h
@@ -89,28 +89,30 @@
             "IFNULL(NEW.visit_count, 0), " /* enforce having a value */ \
             "NEW.hidden, NEW.typed, NEW.favicon_id, NEW.frecency, " \
             "NEW.last_visit_date);" \
   "END" \
 )
 
 /**
  * This trigger allows for the deletion of a record in moz_places_view.  It
- * removes any entry in the temporary table, and any entry in the permanent
- * table as well.
+ * removes any entry in the temporary table, the permanent table, and any
+ * associated entry in moz_openpages_temp.
  */
 #define CREATE_PLACES_VIEW_DELETE_TRIGGER NS_LITERAL_CSTRING( \
   "CREATE TEMPORARY TRIGGER moz_places_view_delete_trigger " \
   "INSTEAD OF DELETE " \
   "ON moz_places_view " \
   "BEGIN " \
     "DELETE FROM moz_places_temp " \
     "WHERE id = OLD.id; " \
     "DELETE FROM moz_places " \
     "WHERE id = OLD.id; " \
+    "DELETE FROM moz_openpages_temp " \
+    "WHERE place_id = OLD.id; " \
   "END" \
 )
 
 /**
  * This trigger allows for updates to a record in moz_places_view.  It first
  * copies the row from the permanent table over to the temp table if it does not
  * exist in the temporary table.  Then, it will update the temporary table with
  * the new data.
@@ -253,9 +255,23 @@
     "WHERE id = OLD.id;" \
   "END" \
 )
 #define CREATE_MOZ_PLACES_SYNC_TRIGGER \
   CREATE_TEMP_SYNC_TRIGGER_BASE("moz_places", MOZ_PLACES_COLUMNS)
 #define CREATE_MOZ_HISTORYVISITS_SYNC_TRIGGER \
   CREATE_TEMP_SYNC_TRIGGER_BASE("moz_historyvisits", MOZ_HISTORYVISITS_COLUMNS)
 
+
+/**
+ * This trigger removes a row from moz_openpages_temp when open_count reaches 0.
+ */
+#define CREATE_REMOVEOPENPAGE_CLEANUP_TRIGGER NS_LITERAL_CSTRING( \
+  "CREATE TEMPORARY TRIGGER moz_openpages_temp_afterupdate_trigger " \
+  "AFTER UPDATE OF open_count ON moz_openpages_temp FOR EACH ROW " \
+  "WHEN NEW.open_count = 0 " \
+  "BEGIN " \
+    "DELETE FROM moz_openpages_temp " \
+    "WHERE place_id = NEW.place_id;" \
+  "END" \
+)
+
 #endif // __nsPlacesTriggers_h__
--- a/toolkit/components/places/tests/autocomplete/head_autocomplete.js
+++ b/toolkit/components/places/tests/autocomplete/head_autocomplete.js
@@ -94,16 +94,19 @@ function ensure_results(aSearch, aExpect
                    getService(Ci.nsIAutoCompleteController);
 
   // Make an AutoCompleteInput that uses our searches
   // and confirms results on search complete
   let input = new AutoCompleteInput(["history"]);
 
   controller.input = input;
 
+  if (typeof kSearchParam == "string")
+    input.searchParam = kSearchParam;
+
   let numSearchesStarted = 0;
   input.onSearchBegin = function() {
     numSearchesStarted++;
     do_check_eq(numSearchesStarted, 1);
   };
 
   input.onSearchComplete = function() {
     do_check_eq(numSearchesStarted, 1);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/autocomplete/test_tabmatches.js
@@ -0,0 +1,104 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim:set ts=2 sw=2 sts=2 et:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Places Test Code.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Blair McBride <bmcbride@mozilla.com> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+let gTabRestrictChar = prefs.getCharPref("browser.urlbar.restrict.openpage");
+
+let kSearchParam = "enable-actions";
+
+let kURIs = [
+  "http://abc.com/",
+  "moz-action:switchtab,http://abc.com/",
+  "http://xyz.net/",
+  "moz-action:switchtab,http://xyz.net/"
+];
+
+let kTitles = [
+  "ABC rocks",
+  "xyz.net - we're better than ABC"
+];
+
+addPageBook(0, 0);
+gPages[1] = [1, 0];
+addPageBook(2, 1);
+gPages[3] = [3, 1];
+
+addOpenPages(0, 1);
+
+
+let gTests = [
+  ["0: single result, that is also a tab match",
+   "abc.com", [0,1]],
+  ["1: two results, one tab match",
+   "abc", [0,1,2]],
+  ["2: two results, both tab matches",
+   "abc", [0,1,2,3],
+   function() {
+     addOpenPages(2, 1);
+   }],
+  ["3: two results, both tab matches, one has multiple tabs",
+   "abc", [0,1,2,3],
+   function() {
+     addOpenPages(2, 5);
+   }],
+  ["4: two results, no tab matches",
+   "abc", [0,2],
+   function() {
+     removeOpenPages(0, 1);
+     removeOpenPages(2, 6);
+   }],
+  ["5: tab match search with restriction character",
+   gTabRestrictChar + " abc", [1],
+   function() {
+    addOpenPages(0, 1);
+   }]
+];
+
+
+function addOpenPages(aUri, aCount) {
+  let num = aCount || 1;
+  for (let i = 0; i < num; i++) {
+    bhist.registerOpenPage(toURI(kURIs[aUri]));
+  }
+}
+
+function removeOpenPages(aUri, aCount) {
+  let num = aCount || 1;
+  for (let i = 0; i < num; i++) {
+    bhist.unregisterOpenPage(toURI(kURIs[aUri]));
+  }
+}
--- a/toolkit/content/widgets/autocomplete.xml
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -37,16 +37,21 @@
 # use your version of this file under the terms of the MPL, indicate your
 # decision by deleting the provisions above and replace them with the notice
 # and other provisions required by the GPL or the LGPL. If you do not delete
 # the provisions above, a recipient may use your version of this file under
 # the terms of any one of the MPL, the GPL or the LGPL.
 #
 # ***** END LICENSE BLOCK *****
 
+<!DOCTYPE bindings [
+<!ENTITY % actionsDTD SYSTEM "chrome://global/locale/actions.dtd">
+%actionsDTD;
+]>
+
 <bindings id="autocompleteBindings"
           xmlns="http://www.mozilla.org/xbl"
           xmlns:html="http://www.w3.org/1999/xhtml"
           xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
           xmlns:xbl="http://www.mozilla.org/xbl">
 
   <binding id="autocomplete"
            extends="chrome://global/content/bindings/textbox.xml#textbox">
@@ -248,20 +253,31 @@
             this.showHistoryPopup();
           else
             this.closePopup();
         ]]></setter>
       </property>
 
       <!-- =================== PUBLIC MEMBERS =================== -->
 
-      <property name="value"
-                onget="return this.inputField.value;">
+      <property name="value">
+        <getter><![CDATA[
+          if (typeof this.onBeforeValueGet == "function") {
+            var result = this.onBeforeValueGet();
+            if (result)
+              return result.value;
+          }
+          return this.inputField.value;
+        ]]></getter>
         <setter><![CDATA[
           this.mIgnoreInput = true;
+
+          if (typeof this.onBeforeValueSet == "function")
+            val = this.onBeforeValueSet(val);
+
           this.inputField.value = val;
           this.mIgnoreInput = false;
           var event = document.createEvent('Events');
           event.initEvent('ValueChange', true, true);
           this.inputField.dispatchEvent(event);
           return val;
         ]]></setter>
       </property>
@@ -511,21 +527,33 @@
         <!-- doesn't reset this.mController -->
         <body><![CDATA[
           this._dontBlur = true;
           this.focus();
           this._dontBlur = false;
         ]]></body>
       </method>
 
+      <method name="resetActionType">
+        <body><![CDATA[
+          if (this.mIgnoreInput)
+            return;
+          this.removeAttribute("actiontype");
+        ]]></body>
+      </method>
+
     </implementation>
 
     <handlers>
-      <handler event="input"
-               action="if (!this.mIgnoreInput &amp;&amp; this.mController.input == this) this.mController.handleText(true);"/>
+      <handler event="input"><![CDATA[
+        if (!this.mIgnoreInput && this.mController.input == this) {
+          this.mController.handleText(true);
+        }
+        this.resetActionType();
+      ]]></handler>
 
       <handler event="keypress" phase="capturing"
                action="return this.onKeyPress(event);"/>
 
       <handler event="compositionstart" phase="capturing"
                action="if (this.mController.input == this) this.mController.handleStartComposition();"/>
 
       <handler event="compositionend" phase="capturing"
@@ -1130,33 +1158,34 @@
                 onget="return this.mInput.controller;"
                 onset="return val;"/>
 
     </implementation>
   </binding>
 
   <binding id="autocomplete-richlistitem" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
     <content>
-      <xul:hbox align="center">
+      <xul:hbox align="center" class="ac-title-box">
         <xul:image xbl:inherits="src=image" class="ac-site-icon"/>
         <xul:hbox anonid="title-box" class="ac-title" flex="1"
                   onunderflow="_doUnderflow('_title');">
           <xul:description anonid="title" class="ac-normal-text ac-comment" xbl:inherits="selected"/>
         </xul:hbox>
         <xul:label anonid="title-overflow-ellipsis" xbl:inherits="selected"
                    class="ac-ellipsis-after ac-comment" hidden="true"/>
         <xul:hbox anonid="extra-box" class="ac-extra" align="center" hidden="true">
           <xul:image class="ac-result-type-tag"/>
           <xul:label class="ac-normal-text ac-comment" xbl:inherits="selected" value=":"/>
           <xul:description anonid="extra" class="ac-normal-text ac-comment" xbl:inherits="selected"/>
         </xul:hbox>
         <xul:image anonid="type-image" class="ac-type-icon"/>
       </xul:hbox>
       <xul:hbox align="center" class="ac-url-box">
         <xul:spacer class="ac-site-icon"/>
+        <xul:image class="ac-action-icon"/>
         <xul:hbox anonid="url-box" class="ac-url" flex="1"
                   onunderflow="_doUnderflow('_url');">
           <xul:description anonid="url" class="ac-normal-text ac-url-text" xbl:inherits="selected"/>
         </xul:hbox>
         <xul:label anonid="url-overflow-ellipsis" xbl:inherits="selected"
                    class="ac-ellipsis-after ac-url-text" hidden="true"/>
         <xul:spacer class="ac-type-icon"/>
       </xul:hbox>
@@ -1320,22 +1349,29 @@
           return false;
           ]]>
         </body>
       </method>
 
       <method name="_setUpDescription">
         <parameter name="aDescriptionElement"/>
         <parameter name="aText"/>
+        <parameter name="aNoEmphasis"/>
         <body>
           <![CDATA[
           // Get rid of all previous text
           while (aDescriptionElement.hasChildNodes())
             aDescriptionElement.removeChild(aDescriptionElement.firstChild);
 
+          // If aNoEmphasis is specified, don't add any emphasis
+          if (aNoEmphasis) {
+            aDescriptionElement.appendChild(document.createTextNode(aText));
+            return;
+          }
+
           // Get the indices that separate match and non-match text
           let search = this.getAttribute("text");
           let tokens = this._getSearchTokens(search);
           let indices = this._getBoundaryIndices(aText, tokens);
 
           // If we're searching for something that needs alternate emphasis,
           // we'll need to check the text that we match
           let checkAlt = this._needsAlternateEmphasis(search);
@@ -1367,16 +1403,20 @@
 
       <method name="_adjustAcItem">
         <body>
           <![CDATA[
           var url = this.getAttribute("url");
           var title = this.getAttribute("title");
           var type = this.getAttribute("type");
 
+          this.removeAttribute("actiontype");
+
+          var setupUrl = true;
+
           // If we have a tag match, show the tags and icon
           if (type == "tag") {
             // Configure the extra box for tags display
             this._extraBox.hidden = false;
             this._extraBox.childNodes[0].hidden = false;
             this._extraBox.childNodes[1].hidden = true;
             this._extraBox.pack = "end";
             this._titleBox.flex = 1;
@@ -1408,33 +1448,43 @@
             if (paramsIndex != -1)
               params = search.substr(paramsIndex + 1);
 
             // Emphasize the keyword parameters
             this._setUpDescription(this._extra, params);
 
             // Don't emphasize keyword searches in the title or url
             this.setAttribute("text", "");
+          } else if (type == "action") {
+            let [,action, param] = url.match(/^moz-action:([^,]+),(.*)$/);
+            this.setAttribute("actiontype", action);
+            url = param;
+            this._extraBox.hidden = true;
+            this._titleBox.flex = 1;
+            let desc = "]]>&action.switchToTab.label;<![CDATA[";
+            this._setUpDescription(this._url, desc, true);
+            setupUrl = false;
           } else {
             // Hide the title's extra box if we don't need extra stuff
             this._extraBox.hidden = true;
             this._titleBox.flex = 1;
           }
 
           // Give the image the icon style and a special one for the type
           this._typeImage.className = "ac-type-icon" +
             (type ? " ac-result-type-" + type : "");
 
           // Show the url as the title if we don't have a title
           if (title == "")
             title = url;
 
           // Emphasize the matching search terms for the description
           this._setUpDescription(this._title, title);
-          this._setUpDescription(this._url, url);
+          if (setupUrl)
+            this._setUpDescription(this._url, url);
 
           // Set up overflow on a timeout because the contents of the box
           // might not have a width yet even though we just changed them
           setTimeout(this._setUpOverflow, 0, this._titleBox, this._titleOverflowEllipsis);
           setTimeout(this._setUpOverflow, 0, this._urlBox, this._urlOverflowEllipsis);
           ]]>
         </body>
       </method>
--- a/toolkit/content/widgets/browser.xml
+++ b/toolkit/content/widgets/browser.xml
@@ -618,16 +618,19 @@
       </field>
       
       <field name="isShowingMessage">
         false
       </field>
 
       <field name="mIconURL">null</field>
 
+      <!-- This is managed by the tabbrowser -->
+      <field name="lastURI">null</field>
+
       <field name="mDestroyed">false</field>
 
       <constructor>
         <![CDATA[
           try {
             if (!this.hasAttribute("disablehistory")) {
               var os = Components.classes["@mozilla.org/observer-service;1"]
                                  .getService(Components.interfaces.nsIObserverService);
@@ -694,16 +697,18 @@
           this.detachFormFill();
           
           this._fastFind = null;
           this._webBrowserFind = null;
           
           // The feeds cache can keep the document inside this browser alive.
           this.feeds = null;
 
+          this.lastURI = null;
+
           this.removeEventListener("pageshow", this.onPageShow, true);
           this.removeEventListener("pagehide", this.onPageHide, true);
           this.removeEventListener("DOMPopupBlocked", this.onPopupBlocked, true);
 
           if (this._autoScrollNeedsCleanup) {
             // we polluted the global scope, so clean it up
             this._autoScrollPopup.parentNode.removeChild(this._autoScrollPopup);
           }
new file mode 100644
--- /dev/null
+++ b/toolkit/locales/en-US/chrome/global/actions.dtd
@@ -0,0 +1,1 @@
+<!ENTITY action.switchToTab.label "Switch to tab">
--- a/toolkit/locales/jar.mn
+++ b/toolkit/locales/jar.mn
@@ -1,16 +1,17 @@
 #filter substitution
 
 @AB_CD@.jar:
 % locale global @AB_CD@ %locale/@AB_CD@/global/
   locale/@AB_CD@/global/about.dtd                       (%chrome/global/about.dtd)
   locale/@AB_CD@/global/aboutAbout.dtd                  (%chrome/global/aboutAbout.dtd)
   locale/@AB_CD@/global/aboutRights.dtd                 (%chrome/global/aboutRights.dtd)
   locale/@AB_CD@/global/aboutRights.properties          (%chrome/global/aboutRights.properties)
+  locale/@AB_CD@/global/actions.dtd                     (%chrome/global/actions.dtd)
   locale/@AB_CD@/global/appPicker.dtd                   (%chrome/global/appPicker.dtd)
   locale/@AB_CD@/global/brand.dtd                       (generic/chrome/global/brand.dtd)
 + locale/@AB_CD@/global/browser.properties              (%chrome/global/browser.properties)
 + locale/@AB_CD@/global/charsetOverlay.dtd              (%chrome/global/charsetOverlay.dtd)
 + locale/@AB_CD@/global/commonDialog.dtd                (%chrome/global/commonDialog.dtd)
 + locale/@AB_CD@/global/commonDialogs.properties        (%chrome/global/commonDialogs.properties)
 + locale/@AB_CD@/global/config.dtd                      (%chrome/global/config.dtd)
 + locale/@AB_CD@/global/config.properties               (%chrome/global/config.properties)