Bug 1065303 - Prepare autocomplete.xml/UnifiedComplete for adding new special result types and heuristics. r=mak
authorBlair McBride <bmcbride@mozilla.com>
Fri, 19 Sep 2014 23:58:46 +1200
changeset 206237 86a707d5ffdba458281e6dbfb833b57956c39b9a
parent 206236 8b9340f3a185570285d26f69fb016cf99fb93919
child 206238 3243776bed0e1fbaea3a72fd839031356da08159
push id27517
push userryanvm@gmail.com
push dateFri, 19 Sep 2014 18:13:39 +0000
treeherdermozilla-central@a084c4cfd8a1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak
bugs1065303
milestone35.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1065303 - Prepare autocomplete.xml/UnifiedComplete for adding new special result types and heuristics. r=mak
browser/base/content/test/general/browser_urlbarAutoFillTrimURLs.js
toolkit/components/places/UnifiedComplete.js
toolkit/content/widgets/autocomplete.xml
toolkit/locales/en-US/chrome/global/autocomplete.properties
toolkit/themes/linux/global/autocomplete.css
toolkit/themes/osx/global/autocomplete.css
toolkit/themes/windows/global/autocomplete.css
--- a/browser/base/content/test/general/browser_urlbarAutoFillTrimURLs.js
+++ b/browser/base/content/test/general/browser_urlbarAutoFillTrimURLs.js
@@ -31,30 +31,31 @@ function test() {
                        , visits: [ { transitionType: Ci.nsINavHistoryService.TRANSITION_TYPED
                                    , visitDate:      Date.now() * 1000
                                    } ]
                        }, callback);
 }
 
 function continue_test() {
   function test_autoFill(aTyped, aExpected, aCallback) {
+    info(`Testing with input: ${aTyped}`);
     gURLBar.inputField.value = aTyped.substr(0, aTyped.length - 1);
     gURLBar.focus();
     gURLBar.selectionStart = aTyped.length - 1;
     gURLBar.selectionEnd = aTyped.length - 1;
 
     EventUtils.synthesizeKey(aTyped.substr(-1), {});
     waitForSearchComplete(function () {
       is(gURLBar.value, aExpected, "trim was applied correctly");
       aCallback();
     });
   }
 
   test_autoFill("http://", "http://", function () {
-    test_autoFill("http://a", "http://autofilltrimurl.com/", function () {
+    test_autoFill("http://au", "http://autofilltrimurl.com/", function () {
       test_autoFill("http://www.autofilltrimurl.com", "http://www.autofilltrimurl.com/", function () {
         // Now ensure selecting from the popup correctly trims.
         is(gURLBar.controller.matchCount, 1, "Found the expected number of matches");
         EventUtils.synthesizeKey("VK_DOWN", {});
         is(gURLBar.value, "www.autofilltrimurl.com", "trim was applied correctly");
         gURLBar.closePopup();
         waitForClearHistory(finish);
       });
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -661,57 +661,67 @@ Search.prototype = {
     TelemetryStopwatch.start(TELEMETRY_1ST_RESULT);
     if (this._searchString)
       TelemetryStopwatch.start(TELEMETRY_6_FIRST_RESULTS);
 
     // Since we call the synchronous parseSubmissionURL function later, we must
     // wait for the initialization of PlacesSearchAutocompleteProvider first.
     yield PlacesSearchAutocompleteProvider.ensureInitialized();
 
-    // For any given search, we run many queries:
-    // 1) search engine domains
-    // 2) inline completion
-    // 3) keywords (this._keywordQuery)
-    // 4) adaptive learning (this._adaptiveQuery)
-    // 5) open pages not supported by history (this._switchToTabQuery)
-    // 6) query based on match behavior
+    // For any given search, we run many queries/heuristics:
+    // 1) by alias (as defined in SearchService)
+    // 2) inline completion from search engine resultDomains
+    // 3) inline completion for hosts (this._hostQuery) or urls (this._urlQuery)
+    // 4) directly typed in url (ie, can be navigated to as-is)
+    // 5) submission for the current search engine
+    // 6) keywords (this._keywordQuery)
+    // 7) adaptive learning (this._adaptiveQuery)
+    // 8) open pages not supported by history (this._switchToTabQuery)
+    // 9) query based on match behavior
     //
-    // (3) only gets ran if we get any filtered tokens, since if there are no
-    // tokens, there is nothing to match.
+    // (6) only gets ran if we get any filtered tokens, since if there are no
+    // tokens, there is nothing to match. This is the *first* query we check if
+    // we want to run, but it gets queued to be run later.
+    //
+    // (1), (4), (5) only get run if actions are enabled. When actions are
+    // enabled, the first result is always a special result (resulting from one
+    // of the queries between (1) and (6) inclusive). As such, the UI is
+    // expected to auto-select the first result when actions are enabled. If the
+    // first result is an inline completion result, that will also be the
+    // default result and therefore be autofilled (this also happens if actions
+    // are not enabled).
 
     // Get the final query, based on the tokens found in the search string.
     let queries = [ this._adaptiveQuery,
                     this._switchToTabQuery,
                     this._searchQuery ];
 
-    let hasKeyword = false;
+    // When actions are enabled, we run a series of heuristics to determine what
+    // the first result should be - which is always a special result.
+    // |hasFirstResult| is used to keep track of whether we've obtained such a
+    // result yet, so we can skip further heuristics and not add any additional
+    // special results.
+    let hasFirstResult = false;
+
     if (this._searchTokens.length > 0 &&
         PlacesUtils.bookmarks.getURIForKeyword(this._searchTokens[0])) {
+      // This may be a keyword of a bookmark.
       queries.unshift(this._keywordQuery);
-      hasKeyword = true;
+      hasFirstResult = true;
     }
 
-    if (this._shouldAutofill) {
-      if (this._searchTokens.length == 1 && !hasKeyword)
-        yield this._matchSearchEngineUrl();
+    let shouldAutofill = this._shouldAutofill;
+    if (this.pending && !hasFirstResult && shouldAutofill) {
+      // Or it may look like a URL we know about from search engines.
+      hasFirstResult = yield this._matchSearchEngineUrl();
+    }
 
-      // Hosts have no "/" in them.
-      let lastSlashIndex = this._searchString.lastIndexOf("/");
-      // Search only URLs if there's a slash in the search string...
-      if (lastSlashIndex != -1) {
-        // ...but not if it's exactly at the end of the search string.
-        if (lastSlashIndex < this._searchString.length - 1) {
-          queries.unshift(this._urlQuery);
-        }
-      } else if (this.pending) {
-        // The host query is executed immediately, while any other is delayed
-        // to avoid overloading the connection.
-        let [ query, params ] = this._hostQuery;
-        yield conn.executeCached(query, params, this._onResultRow.bind(this));
-      }
+    if (this.pending && !hasFirstResult && shouldAutofill) {
+      // It may also look like a URL we know from the database.
+      hasFirstResult = yield this._matchKnownUrl(conn, queries);
     }
 
     yield this._sleep(Prefs.delay);
     if (!this.pending)
       return;
 
     for (let [query, params] of queries) {
       yield conn.executeCached(query, params, this._onResultRow.bind(this));
@@ -735,64 +745,100 @@ Search.prototype = {
 
     // If we didn't find enough matches and we have some frecency-driven
     // matches, add them.
     if (this._frecencyMatches) {
       this._frecencyMatches.forEach(this._addMatch, this);
     }
   }),
 
+  _matchKnownUrl: function* (conn, queries) {
+    // Hosts have no "/" in them.
+    let lastSlashIndex = this._searchString.lastIndexOf("/");
+    // Search only URLs if there's a slash in the search string...
+    if (lastSlashIndex != -1) {
+      // ...but not if it's exactly at the end of the search string.
+      if (lastSlashIndex < this._searchString.length - 1) {
+        // We don't want to execute this query right away because it needs to
+        // search the entire DB without an index, but we need to know if we have
+        // a result as it will influence other heuristics. So we guess by
+        // assuming that if we get a result from a *host* query and it *looks*
+        // like a URL, then we'll probably have a result.
+        let gotResult = false;
+        let [ query, params ] = this._urlPredictQuery;
+        yield conn.executeCached(query, params, row => {
+          gotResult = true;
+          queries.unshift(this._urlQuery);
+        });
+        return gotResult;
+      }
+
+      return false;
+    }
+
+    let gotResult = false;
+    let [ query, params ] = this._hostQuery;
+    yield conn.executeCached(query, params, row => {
+      gotResult = true;
+      this._onResultRow(row);
+    });
+
+    return gotResult;
+  },
+
   _matchSearchEngineUrl: function* () {
     if (!Prefs.autofillSearchEngines)
-      return;
+      return false;
 
     let match = yield PlacesSearchAutocompleteProvider.findMatchByToken(
                                                            this._searchString);
-    if (match) {
-      // The match doesn't contain a 'scheme://www.' prefix, but since we have
-      // stripped it from the search string, here we could still be matching
-      // 'https://www.g' to 'google.com'.
-      // There are a couple cases where we don't want to match though:
-      //
-      //  * If the protocol differs we should not match. For example if the user
-      //    searched https we should not return http.
-      try {
-        let prefixURI = NetUtil.newURI(this._strippedPrefix);
-        let finalURI = NetUtil.newURI(match.url);
-        if (prefixURI.scheme != finalURI.scheme)
-          return;
-      } catch (e) {}
+    if (!match)
+      return false;
 
-      //  * If the user typed "www." but the final url doesn't have it, we
-      //    should not match as well, the two urls may point to different pages.
-      if (this._strippedPrefix.endsWith("www.") &&
-          !stripHttpAndTrim(match.url).startsWith("www."))
-        return;
+    // The match doesn't contain a 'scheme://www.' prefix, but since we have
+    // stripped it from the search string, here we could still be matching
+    // 'https://www.g' to 'google.com'.
+    // There are a couple cases where we don't want to match though:
+    //
+    //  * If the protocol differs we should not match. For example if the user
+    //    searched https we should not return http.
+    try {
+      let prefixURI = NetUtil.newURI(this._strippedPrefix);
+      let finalURI = NetUtil.newURI(match.url);
+      if (prefixURI.scheme != finalURI.scheme)
+        return false;
+    } catch (e) {}
 
-      let value = this._strippedPrefix + match.token;
+    //  * If the user typed "www." but the final url doesn't have it, we
+    //    should not match as well, the two urls may point to different pages.
+    if (this._strippedPrefix.endsWith("www.") &&
+        !stripHttpAndTrim(match.url).startsWith("www."))
+      return false;
 
-      // In any case, we should never arrive here with a value that doesn't
-      // match the search string.  If this happens there is some case we
-      // are not handling properly yet.
-      if (!value.startsWith(this._originalSearchString)) {
-        Components.utils.reportError(`Trying to inline complete in-the-middle
-                                      ${this._originalSearchString} to ${value}`);
-        return;
-      }
+    let value = this._strippedPrefix + match.token;
 
-      this._result.setDefaultIndex(0);
-      this._addFrecencyMatch({
-        value: value,
-        comment: match.engineName,
-        icon: match.iconUrl,
-        style: "priority-search",
-        finalCompleteValue: match.url,
-        frecency: FRECENCY_SEARCHENGINES_DEFAULT
-      });
+    // In any case, we should never arrive here with a value that doesn't
+    // match the search string.  If this happens there is some case we
+    // are not handling properly yet.
+    if (!value.startsWith(this._originalSearchString)) {
+      Components.utils.reportError(`Trying to inline complete in-the-middle
+                                    ${this._originalSearchString} to ${value}`);
+      return false;
     }
+
+    this._result.setDefaultIndex(0);
+    this._addFrecencyMatch({
+      value: value,
+      comment: match.engineName,
+      icon: match.iconUrl,
+      style: "priority-search",
+      finalCompleteValue: match.url,
+      frecency: FRECENCY_SEARCHENGINES_DEFAULT
+    });
+    return true;
   },
 
   _onResultRow: function (row) {
     TelemetryStopwatch.finish(TELEMETRY_1ST_RESULT);
     let queryType = row.getResultByIndex(QUERYINDEX_QUERYTYPE);
     let match;
     switch (queryType) {
       case QUERYTYPE_AUTOFILL_HOST:
@@ -912,28 +958,41 @@ Search.prototype = {
     }
   },
 
   _processHostRow: function (row) {
     let match = {};
     let trimmedHost = row.getResultByIndex(QUERYINDEX_URL);
     let untrimmedHost = row.getResultByIndex(QUERYINDEX_TITLE);
     let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY);
+
     // If the untrimmed value doesn't preserve the user's input just
     // ignore it and complete to the found host.
     if (untrimmedHost &&
         !untrimmedHost.toLowerCase().contains(this._trimmedOriginalSearchString.toLowerCase())) {
       untrimmedHost = null;
     }
 
     match.value = this._strippedPrefix + trimmedHost;
     // Remove the trailing slash.
     match.comment = stripHttpAndTrim(trimmedHost);
     match.finalCompleteValue = untrimmedHost;
+
+    try {
+      let iconURI = NetUtil.newURI(untrimmedHost);
+      iconURI.path = "/favicon.ico";
+      match.icon = PlacesUtils.favicons.getFaviconLinkForIcon(iconURI).spec;
+    } catch (e) {
+      // This can fail, which is ok.
+    }
+
+    // Although this has a frecency, this query is executed before any other
+    // queries that would result in frecency matches.
     match.frecency = frecency;
+    match.style = "autofill";
     return match;
   },
 
   _processUrlRow: function (row) {
     let match = {};
     let value = row.getResultByIndex(QUERYINDEX_URL);
     let url = fixupSearchText(value);
     let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY);
@@ -957,17 +1016,20 @@ Search.prototype = {
     if (untrimmedURL &&
         !untrimmedURL.toLowerCase().contains(this._trimmedOriginalSearchString.toLowerCase())) {
       untrimmedURL = null;
      }
 
     match.value = this._strippedPrefix + url;
     match.comment = url;
     match.finalCompleteValue = untrimmedURL;
+    // Although this has a frecency, this query is executed before any other
+    // queries that would result in frecency matches.
     match.frecency = frecency;
+    match.style = "autofill";
     return match;
   },
 
   _processRow: function (row) {
     let match = {};
     match.placeId = row.getResultByIndex(QUERYINDEX_PLACEID);
     let queryType = row.getResultByIndex(QUERYINDEX_QUERYTYPE);
     let escapedURL = row.getResultByIndex(QUERYINDEX_URL);
@@ -1157,16 +1219,19 @@ Search.prototype = {
   /**
    * Whether we should try to autoFill.
    */
   get _shouldAutofill() {
     // First of all, check for the autoFill pref.
     if (!Prefs.autofill)
       return false;
 
+    if (!this._searchTokens.length == 1)
+      return false;
+
     // Then, we should not try to autofill if the behavior is not the default.
     // TODO (bug 751709): Ideally we should have a more fine-grained behavior
     // here, but for now it's enough to just check for default behavior.
     if (Prefs.defaultBehavior != DEFAULT_BEHAVIOR) {
       // autoFill can only cope with history or bookmarks entries
       // (typed or not).
       if (!this.hasBehavior("typed") &&
           !this.hasBehavior("history") &&
@@ -1177,28 +1242,21 @@ Search.prototype = {
       if (this.hasBehavior("title") || this.hasBehavior("tags"))
         return false;
     }
 
     // Don't try to autofill if the search term includes any whitespace.
     // This may confuse completeDefaultIndex cause the AUTOCOMPLETE_MATCH
     // tokenizer ends up trimming the search string and returning a value
     // that doesn't match it, or is even shorter.
-    if (/\s/.test(this._originalSearchString)) {
+    if (/\s/.test(this._originalSearchString))
       return false;
-    }
 
-    // Don't autoFill if the search term is recognized as a keyword, otherwise
-    // it will override default keywords behavior.  Note that keywords are
-    // hashed on first use, so while the first query may delay a little bit,
-    // next ones will just hit the memory hash.
-    if (this._searchString.length == 0 ||
-        PlacesUtils.bookmarks.getURIForKeyword(this._searchString)) {
+    if (this._searchString.length == 0)
       return false;
-    }
 
     return true;
   },
 
   /**
    * Obtains the query to search for autoFill host results.
    *
    * @return an array consisting of the correctly optimized query to search the
@@ -1216,16 +1274,39 @@ Search.prototype = {
       {
         query_type: QUERYTYPE_AUTOFILL_HOST,
         searchString: this._searchString.toLowerCase()
       }
     ];
   },
 
   /**
+   * Obtains a query to predict whether this._urlQuery is likely to return a
+   * result. We do by extracting what should be a host out of the input and
+   * performing a host query based on that.
+   */
+  get _urlPredictQuery() {
+    // We expect this to be a full URL, not just a host. We want to extract the
+    // host and use that as a guess for whether we'll get a result from a URL
+    // query.
+    let slashIndex = this._searchString.indexOf("/");
+
+    let host = this._searchString.substring(0, slashIndex);
+    host = host.toLowerCase();
+
+    return [
+      SQL_HOST_QUERY,
+      {
+        query_type: QUERYTYPE_AUTOFILL_HOST,
+        searchString: host
+      }
+    ];
+  },
+
+  /**
    * Obtains the query to search for autoFill url results.
    *
    * @return an array consisting of the correctly optimized query to search the
    *         database with and an object containing the params to bound.
    */
   get _urlQuery()  {
     let typed = Prefs.autofillTyped || this.hasBehavior("typed");
     let bookmarked =  this.hasBehavior("bookmark");
--- a/toolkit/content/widgets/autocomplete.xml
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -1437,16 +1437,117 @@ extends="chrome://global/content/binding
               // Otherwise, it's plain text
               aDescriptionElement.appendChild(document.createTextNode(text));
             }
           }
           ]]>
         </body>
       </method>
 
+      <!--
+        This will generate an array of emphasis pairs for use with
+        _setUpEmphasisedSections(). Each pair is a tuple (array) that
+        represents a block of text - containing the text of that block, and a
+        boolean for whether that block should have an emphasis styling applied
+        to it.
+
+        These pairs are generated by parsing a localised string (aSourceString)
+        with parameters, in the format that is used by
+        nsIStringBundle.formatStringFromName():
+
+          "textA %1$S textB textC %2$S"
+
+        Or:
+
+          "textA %S"
+
+        Where "%1$S", "%2$S", and "%S" are intended to be replaced by provided
+        replacement strings. These are specified an array of tuples
+        (aReplacements), each containing the replacement text and a boolean for
+        whether that text should have an emphasis styling applied. This is used
+        as a 1-based array - ie, "%1$S" is replaced by the item in the first
+        index of aReplacements, "%2$S" by the second, etc. "%S" will always
+        match the first index.
+      -->
+      <method name="_generateEmphasisPairs">
+        <parameter name="aSourceString"/>
+        <parameter name="aReplacements"/>
+        <body>
+          <![CDATA[
+            let pairs = [];
+
+            // Split on %S, %1$S, %2$S, etc. ie:
+            //   "textA %S"
+            //     becomes ["textA ", "%S"]
+            //   "textA %1$S textB textC %2$S"
+            //     becomes ["textA ", "%1$S", " textB textC ", "%2$S"]
+            let parts = aSourceString.split(/(%(?:[0-9]+\$)?S)/);
+
+            for (let part of parts) {
+              // The above regex will actually give us an empty string at the
+              // end - we don't want that, as we don't want to later generate an
+              // empty text node for it.
+              if (part.length === 0)
+                continue;
+
+              // Determine if this token is a replacement token or a normal text
+              // token. If it is a replacement token, we want to extract the
+              // numerical number. However, we still want to match on "$S".
+              let match = part.match(/^%(?:([0-9]+)\$)?S$/);
+
+              if (match) {
+                // "%S" doesn't have a numerical number in it, but will always
+                // be assumed to be 1. Furthermore, the input string specifies
+                // these with a 1-based index, but we want a 0-based index.
+                let index = (match[1] || 1) - 1;
+
+                if (index >= 0 && index < aReplacements.length) {
+                  let replacement = aReplacements[index];
+                  pairs.push([...replacement]);
+                }
+              } else {
+                pairs.push([part, false]);
+              }
+            }
+
+            return pairs;
+          ]]>
+        </body>
+      </method>
+
+      <!--
+        _setUpEmphasisedSections() has the same use as _setUpDescription,
+        except instead of taking a string and highlighting given tokens, it takes
+        an array of pairs generated by _generateEmphasisPairs(). This allows
+        control over emphasising based on specific blocks of text, rather than
+        search for substrings.
+      -->
+      <method name="_setUpEmphasisedSections">
+        <parameter name="aDescriptionElement"/>
+        <parameter name="aTextPairs"/>
+        <body>
+          <![CDATA[
+          // Get rid of all previous text
+          while (aDescriptionElement.hasChildNodes())
+            aDescriptionElement.firstChild.remove();
+
+          for (let [text, emphasise] of aTextPairs) {
+            if (emphasise) {
+              let span = aDescriptionElement.appendChild(
+                document.createElementNS("http://www.w3.org/1999/xhtml", "span"));
+              span.className = "ac-emphasize-text";
+              span.textContent = text;
+            } else {
+              aDescriptionElement.appendChild(document.createTextNode(text));
+            }
+          }
+          ]]>
+        </body>
+      </method>
+
       <method name="_adjustAcItem">
         <body>
           <![CDATA[
           let url = this.getAttribute("url");
           let title = this.getAttribute("title");
           let type = this.getAttribute("type");
 
           let emphasiseTitle = true;
@@ -1493,16 +1594,30 @@ extends="chrome://global/content/binding
             [title, searchEngine] = title.split(TITLE_SEARCH_ENGINE_SEPARATOR);
             url = this._stringBundle.formatStringFromName("searchWithEngine", [searchEngine], 1);
 
             // Remove the "search" substring so that the correct style, if any,
             // is applied below.
             types.delete("search");
           }
 
+          // Check if we have an auto-fill URL
+          if (types.has("autofill")) {
+            emphasiseUrl = false;
+
+            let sourceStr = this._stringBundle.GetStringFromName("visitURL");
+            title = this._generateEmphasisPairs(sourceStr, [
+                                                 [trimURL(url), true],
+                                                ]);
+
+            types.delete("autofill");
+          }
+
+          type = [...types].join(" ");
+
           // 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;
@@ -1548,17 +1663,21 @@ extends="chrome://global/content/binding
           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, !emphasiseTitle);
+          if (Array.isArray(title))
+            this._setUpEmphasisedSections(this._title, title);
+          else
+            this._setUpDescription(this._title, title, !emphasiseTitle);
+
           this._setUpDescription(this._url, url, !emphasiseUrl);
 
           // 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>
--- a/toolkit/locales/en-US/chrome/global/autocomplete.properties
+++ b/toolkit/locales/en-US/chrome/global/autocomplete.properties
@@ -1,9 +1,12 @@
 # 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/.
 
 # LOCALIZATION NOTE (searchWithEngine): %S will be replaced with
 # the search engine provider's name. This format was chosen because
 # the provider can also end with "Search" (e.g.: MSN Search).
 searchWithEngine = Search with %S
-switchToTab = Switch to tab
\ No newline at end of file
+switchToTab = Switch to tab
+# LOCALIZATION NOTE (visitURL):
+# %S is the URL to visit.
+visitURL = Visit %S
--- a/toolkit/themes/linux/global/autocomplete.css
+++ b/toolkit/themes/linux/global/autocomplete.css
@@ -112,16 +112,25 @@ treechildren.autocomplete-treebody::-moz
   color: HighlightText;
 }
 
 .autocomplete-richlistitem {
   padding: 6px 2px;
   color: MenuText;
 }
 
+.autocomplete-richlistitem[type~="autofill"] .ac-url-box {
+  display: none;
+}
+
+.autocomplete-richlistitem[type~="autofill"] .ac-title-box {
+  margin-top: 12px;
+  margin-bottom: 12px;
+}
+
 .ac-url-box {
   margin-top: 1px;
 }
 
 .ac-site-icon {
   width: 16px; 
   height: 16px;
   margin-bottom: -2px;
--- a/toolkit/themes/osx/global/autocomplete.css
+++ b/toolkit/themes/osx/global/autocomplete.css
@@ -97,16 +97,25 @@ treechildren.autocomplete-treebody::-moz
   color: HighlightText;
   background-image: linear-gradient(rgba(255,255,255,0.3), transparent);
 }
 
 .autocomplete-richlistitem {
   padding: 5px 2px;
 }
 
+.autocomplete-richlistitem[type~="autofill"] .ac-url-box {
+  display: none;
+}
+
+.autocomplete-richlistitem[type~="autofill"] .ac-title-box {
+  margin-top: 12px;
+  margin-bottom: 12px;
+}
+
 .ac-url-box {
   margin-top: 1px;
 }
 
 .ac-site-icon {
   width: 16px; 
   height: 16px;
   margin-bottom: -1px;
--- a/toolkit/themes/windows/global/autocomplete.css
+++ b/toolkit/themes/windows/global/autocomplete.css
@@ -125,16 +125,25 @@ treechildren.autocomplete-treebody::-moz
     border-radius: 6px;
     outline: 1px solid rgb(124,163,206);
     -moz-outline-radius: 3px;
     outline-offset: -2px;
   }
 }
 %endif
 
+.autocomplete-richlistitem[type~="autofill"] .ac-url-box {
+  display: none;
+}
+
+.autocomplete-richlistitem[type~="autofill"] .ac-title-box {
+  margin-top: 12px;
+  margin-bottom: 12px;
+}
+
 .ac-title-box {
   margin-top: 4px;
 }
 
 .ac-url-box {
   margin: 1px 0 4px;
 }