Bug 395161 - Make it possible to restrict the url bar autocomplete results to bookmarks/tagged/history entries and match only url/title. r=dietrich

This commit is contained in:
Edward Lee 2008-07-19 01:51:34 -07:00
parent 032f06cdc0
commit 26fd4468ea
5 changed files with 302 additions and 25 deletions

View File

@ -227,6 +227,15 @@ pref("browser.urlbar.maxRichResults", 12);
pref("browser.urlbar.search.chunkSize", 1000);
pref("browser.urlbar.search.timeout", 100);
// 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.match.title", "#");
pref("browser.urlbar.match.url", "@");
// 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

View File

@ -117,6 +117,11 @@
#define PREF_AUTOCOMPLETE_FILTER_JAVASCRIPT "urlbar.filter.javascript"
#define PREF_AUTOCOMPLETE_ENABLED "urlbar.autocomplete.enabled"
#define PREF_AUTOCOMPLETE_MAX_RICH_RESULTS "urlbar.maxRichResults"
#define PREF_AUTOCOMPLETE_RESTRICT_HISTORY "urlbar.restrict.history"
#define PREF_AUTOCOMPLETE_RESTRICT_BOOKMARK "urlbar.restrict.bookmark"
#define PREF_AUTOCOMPLETE_RESTRICT_TAG "urlbar.restrict.tag"
#define PREF_AUTOCOMPLETE_MATCH_TITLE "urlbar.match.title"
#define PREF_AUTOCOMPLETE_MATCH_URL "urlbar.match.url"
#define PREF_AUTOCOMPLETE_SEARCH_CHUNK_SIZE "urlbar.search.chunkSize"
#define PREF_AUTOCOMPLETE_SEARCH_TIMEOUT "urlbar.search.timeout"
#define PREF_DB_CACHE_PERCENTAGE "history_cache_percentage"
@ -286,6 +291,7 @@ const PRInt32 nsNavHistory::kAutoCompleteIndex_FaviconURL = 2;
const PRInt32 nsNavHistory::kAutoCompleteIndex_ParentId = 3;
const PRInt32 nsNavHistory::kAutoCompleteIndex_BookmarkTitle = 4;
const PRInt32 nsNavHistory::kAutoCompleteIndex_Tags = 5;
const PRInt32 nsNavHistory::kAutoCompleteIndex_VisitCount = 6;
static const char* gQuitApplicationMessage = "quit-application";
static const char* gXpcomShutdown = "xpcom-shutdown";
@ -331,8 +337,18 @@ nsNavHistory::nsNavHistory() : mBatchLevel(0),
mAutoCompleteOnlyTyped(PR_FALSE),
mAutoCompleteMatchBehavior(MATCH_BOUNDARY_ANYWHERE),
mAutoCompleteMaxResults(25),
mAutoCompleteRestrictHistory(NS_LITERAL_STRING("^")),
mAutoCompleteRestrictBookmark(NS_LITERAL_STRING("*")),
mAutoCompleteRestrictTag(NS_LITERAL_STRING("+")),
mAutoCompleteMatchTitle(NS_LITERAL_STRING("#")),
mAutoCompleteMatchUrl(NS_LITERAL_STRING("@")),
mAutoCompleteSearchChunkSize(100),
mAutoCompleteSearchTimeout(100),
mRestrictHistory(PR_FALSE),
mRestrictBookmark(PR_FALSE),
mRestrictTag(PR_FALSE),
mMatchTitle(PR_FALSE),
mMatchUrl(PR_FALSE),
mPreviousChunkOffset(-1),
mAutoCompleteFinishedSearch(PR_FALSE),
mExpireDaysMin(0),
@ -472,6 +488,11 @@ nsNavHistory::Init()
pbi->AddObserver(PREF_AUTOCOMPLETE_MATCH_BEHAVIOR, this, PR_FALSE);
pbi->AddObserver(PREF_AUTOCOMPLETE_FILTER_JAVASCRIPT, this, PR_FALSE);
pbi->AddObserver(PREF_AUTOCOMPLETE_MAX_RICH_RESULTS, this, PR_FALSE);
pbi->AddObserver(PREF_AUTOCOMPLETE_RESTRICT_HISTORY, this, PR_FALSE);
pbi->AddObserver(PREF_AUTOCOMPLETE_RESTRICT_BOOKMARK, this, PR_FALSE);
pbi->AddObserver(PREF_AUTOCOMPLETE_RESTRICT_TAG, this, PR_FALSE);
pbi->AddObserver(PREF_AUTOCOMPLETE_MATCH_TITLE, this, PR_FALSE);
pbi->AddObserver(PREF_AUTOCOMPLETE_MATCH_URL, this, PR_FALSE);
pbi->AddObserver(PREF_AUTOCOMPLETE_SEARCH_CHUNK_SIZE, this, PR_FALSE);
pbi->AddObserver(PREF_AUTOCOMPLETE_SEARCH_TIMEOUT, this, PR_FALSE);
pbi->AddObserver(PREF_BROWSER_HISTORY_EXPIRE_DAYS_MAX, this, PR_FALSE);
@ -1871,6 +1892,23 @@ nsNavHistory::LoadPrefs(PRBool aInitializing)
&mAutoCompleteSearchChunkSize);
mPrefBranch->GetIntPref(PREF_AUTOCOMPLETE_SEARCH_TIMEOUT,
&mAutoCompleteSearchTimeout);
nsXPIDLCString prefStr;
mPrefBranch->GetCharPref(PREF_AUTOCOMPLETE_RESTRICT_HISTORY,
getter_Copies(prefStr));
mAutoCompleteRestrictHistory = NS_ConvertUTF8toUTF16(prefStr);
mPrefBranch->GetCharPref(PREF_AUTOCOMPLETE_RESTRICT_BOOKMARK,
getter_Copies(prefStr));
mAutoCompleteRestrictBookmark = NS_ConvertUTF8toUTF16(prefStr);
mPrefBranch->GetCharPref(PREF_AUTOCOMPLETE_RESTRICT_TAG,
getter_Copies(prefStr));
mAutoCompleteRestrictTag = NS_ConvertUTF8toUTF16(prefStr);
mPrefBranch->GetCharPref(PREF_AUTOCOMPLETE_MATCH_TITLE,
getter_Copies(prefStr));
mAutoCompleteMatchTitle = NS_ConvertUTF8toUTF16(prefStr);
mPrefBranch->GetCharPref(PREF_AUTOCOMPLETE_MATCH_URL,
getter_Copies(prefStr));
mAutoCompleteMatchUrl = NS_ConvertUTF8toUTF16(prefStr);
if (!aInitializing && oldCompleteOnlyTyped != mAutoCompleteOnlyTyped) {
// update the autocomplete statements if the option has changed.
nsresult rv = CreateAutoCompleteQueries();

View File

@ -642,7 +642,12 @@ protected:
static const PRInt32 kAutoCompleteIndex_ParentId;
static const PRInt32 kAutoCompleteIndex_BookmarkTitle;
static const PRInt32 kAutoCompleteIndex_Tags;
static const PRInt32 kAutoCompleteIndex_VisitCount;
nsCOMPtr<mozIStorageStatement> mDBCurrentQuery; // kAutoCompleteIndex_* results
nsCOMPtr<mozIStorageStatement> mDBAutoCompleteQuery; // kAutoCompleteIndex_* results
nsCOMPtr<mozIStorageStatement> mDBAutoCompleteHistoryQuery; // kAutoCompleteIndex_* results
nsCOMPtr<mozIStorageStatement> mDBAutoCompleteStarQuery; // kAutoCompleteIndex_* results
nsCOMPtr<mozIStorageStatement> mDBAutoCompleteTagsQuery; // kAutoCompleteIndex_* results
nsCOMPtr<mozIStorageStatement> mDBPreviousQuery; // kAutoCompleteIndex_* results
nsCOMPtr<mozIStorageStatement> mDBAdaptiveQuery; // kAutoCompleteIndex_* results
nsCOMPtr<mozIStorageStatement> mDBKeywordQuery; // kAutoCompleteIndex_* results
@ -664,10 +669,21 @@ protected:
MatchType mAutoCompleteMatchBehavior;
PRBool mAutoCompleteFilterJavascript;
PRInt32 mAutoCompleteMaxResults;
nsString mAutoCompleteRestrictHistory;
nsString mAutoCompleteRestrictBookmark;
nsString mAutoCompleteRestrictTag;
nsString mAutoCompleteMatchTitle;
nsString mAutoCompleteMatchUrl;
PRInt32 mAutoCompleteSearchChunkSize;
PRInt32 mAutoCompleteSearchTimeout;
nsCOMPtr<nsITimer> mAutoCompleteTimer;
PRBool mRestrictHistory;
PRBool mRestrictBookmark;
PRBool mRestrictTag;
PRBool mMatchTitle;
PRBool mMatchUrl;
// Original search string for case-sensitive usage
nsString mOrigSearchString;
// Search string and tokens for case-insensitive matching
@ -675,6 +691,7 @@ protected:
nsStringArray mCurrentSearchTokens;
void GenerateSearchTokens();
void AddSearchToken(nsAutoString &aToken);
void ProcessTokensForSpecialSearch();
nsresult AutoCompleteFeedback(PRInt32 aIndex,
nsIAutoCompleteController *aController);
@ -704,8 +721,7 @@ protected:
*/
enum QueryType {
QUERY_KEYWORD,
QUERY_ADAPTIVE,
QUERY_FULL
QUERY_FILTERED
};
nsresult AutoCompleteProcessSearch(mozIStorageStatement* aQuery,
const QueryType aType,

View File

@ -252,15 +252,13 @@ nsNavHistory::InitAutoComplete()
nsresult
nsNavHistory::CreateAutoCompleteQueries()
{
nsCString sql = NS_LITERAL_CSTRING(
"SELECT h.url, h.title, f.url") + BOOK_TAG_SQL + NS_LITERAL_CSTRING(" "
// Define common pieces of various queries
nsCString sqlHead = NS_LITERAL_CSTRING(
"SELECT h.url, h.title, f.url") + BOOK_TAG_SQL + NS_LITERAL_CSTRING(", "
"h.visit_count "
"FROM moz_places h "
"LEFT OUTER JOIN moz_favicons f ON f.id = h.favicon_id "
"WHERE h.frecency <> 0 ");
if (mAutoCompleteOnlyTyped)
sql += NS_LITERAL_CSTRING("AND h.typed = 1 ");
// NOTE:
// after migration or clear all private data, we might end up with
// a lot of places with frecency < 0 (until idle)
@ -268,14 +266,32 @@ nsNavHistory::CreateAutoCompleteQueries()
// XXX bug 412736
// in the case of a frecency tie, break it with h.typed and h.visit_count
// which is better than nothing. but this is slow, so not doing it yet.
sql += NS_LITERAL_CSTRING(
nsCString sqlTail = NS_LITERAL_CSTRING(
"ORDER BY h.frecency DESC LIMIT ?2 OFFSET ?3");
nsresult rv = mDBConn->CreateStatement(sql,
nsresult rv = mDBConn->CreateStatement(sqlHead + (mAutoCompleteOnlyTyped ?
NS_LITERAL_CSTRING("AND h.typed = 1 ") : EmptyCString()) + sqlTail,
getter_AddRefs(mDBAutoCompleteQuery));
NS_ENSURE_SUCCESS(rv, rv);
sql = NS_LITERAL_CSTRING(
rv = mDBConn->CreateStatement(sqlHead +
NS_LITERAL_CSTRING("AND h.visit_count > 0 ") + sqlTail,
getter_AddRefs(mDBAutoCompleteHistoryQuery));
NS_ENSURE_SUCCESS(rv, rv);
rv = mDBConn->CreateStatement(sqlHead +
NS_LITERAL_CSTRING("AND bookmark IS NOT NULL ") + sqlTail,
getter_AddRefs(mDBAutoCompleteStarQuery));
NS_ENSURE_SUCCESS(rv, rv);
rv = mDBConn->CreateStatement(sqlHead +
NS_LITERAL_CSTRING("AND tags IS NOT NULL ") + sqlTail,
getter_AddRefs(mDBAutoCompleteTagsQuery));
NS_ENSURE_SUCCESS(rv, rv);
nsCString sql = NS_LITERAL_CSTRING(
"SELECT h.url, h.title, f.url") + BOOK_TAG_SQL + NS_LITERAL_CSTRING(", "
"h.visit_count, "
"ROUND(MAX(((i.input = ?2) + (SUBSTR(i.input, 1, LENGTH(?2)) = ?2)) * "
"i.use_count), 1) rank "
"FROM moz_inputhistory i "
@ -293,7 +309,7 @@ nsNavHistory::CreateAutoCompleteQueries()
"JOIN moz_favicons f ON f.id = r.favicon_id "
"WHERE r.rev_host = s.rev_host "
"ORDER BY r.frecency DESC LIMIT 1)), "
"b.parent, b.title, NULL "
"b.parent, b.title, NULL, h.visit_count "
"FROM moz_keywords k "
"JOIN moz_bookmarks b ON b.keyword_id = k.id "
"JOIN moz_places s ON s.id = b.fk "
@ -510,7 +526,8 @@ nsNavHistory::StartSearch(const nsAString & aSearchString,
// search left off, but first we want to create an optimized query that
// only searches through the urls that were previously found
nsCString sql = NS_LITERAL_CSTRING(
"SELECT h.url, h.title, f.url") + BOOK_TAG_SQL + NS_LITERAL_CSTRING(" "
"SELECT h.url, h.title, f.url") + BOOK_TAG_SQL + NS_LITERAL_CSTRING(", "
"h.visit_count "
"FROM moz_places h "
"LEFT OUTER JOIN moz_favicons f ON f.id = h.favicon_id "
"WHERE h.url IN (");
@ -557,9 +574,16 @@ nsNavHistory::StartSearch(const nsAString & aSearchString,
mLivemarkFeedItemIds.Clear();
mLivemarkFeedURIs.Clear();
// Reset the special searches
mRestrictHistory = mRestrictBookmark = mRestrictTag = PR_FALSE;
mMatchTitle = mMatchUrl = PR_FALSE;
// Make the array of search tokens from the search string
GenerateSearchTokens();
// Figure out if we need to do special searches
ProcessTokensForSpecialSearch();
// find all the items that have the "livemark/feedURI" annotation
// and save off their item ids and URIs. when doing autocomplete,
// if a result's parent item id matches a saved item id, the result
@ -634,6 +658,40 @@ nsNavHistory::AddSearchToken(nsAutoString &aToken)
mCurrentSearchTokens.AppendString(aToken);
}
void
nsNavHistory::ProcessTokensForSpecialSearch()
{
// Determine which special searches to apply
for (PRInt32 i = mCurrentSearchTokens.Count(); --i >= 0; ) {
PRBool needToRemove = PR_TRUE;
const nsString *token = mCurrentSearchTokens.StringAt(i);
if (token->Equals(mAutoCompleteRestrictHistory))
mRestrictHistory = PR_TRUE;
else if (token->Equals(mAutoCompleteRestrictBookmark))
mRestrictBookmark = PR_TRUE;
else if (token->Equals(mAutoCompleteRestrictTag))
mRestrictTag = PR_TRUE;
else if (token->Equals(mAutoCompleteMatchTitle))
mMatchTitle = PR_TRUE;
else if (token->Equals(mAutoCompleteMatchUrl))
mMatchUrl = PR_TRUE;
else
needToRemove = PR_FALSE;
// Remove the token if it's special search token
if (needToRemove)
(void)mCurrentSearchTokens.RemoveStringAt(i);
}
// We can use optimized queries for restricts, so check for the most
// restrictive query first
mDBCurrentQuery = mRestrictTag ? mDBAutoCompleteTagsQuery :
mRestrictBookmark ? mDBAutoCompleteStarQuery :
mRestrictHistory ? mDBAutoCompleteHistoryQuery :
mDBAutoCompleteQuery;
}
nsresult
nsNavHistory::AutoCompleteKeywordSearch()
{
@ -673,7 +731,7 @@ nsNavHistory::AutoCompleteAdaptiveSearch()
rv = mDBAdaptiveQuery->BindStringParameter(1, mCurrentSearchString);
NS_ENSURE_SUCCESS(rv, rv);
rv = AutoCompleteProcessSearch(mDBAdaptiveQuery, QUERY_ADAPTIVE);
rv = AutoCompleteProcessSearch(mDBAdaptiveQuery, QUERY_FILTERED);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
@ -685,7 +743,7 @@ nsNavHistory::AutoCompletePreviousSearch()
nsresult rv = mDBPreviousQuery->BindInt32Parameter(0, GetTagsFolder());
NS_ENSURE_SUCCESS(rv, rv);
rv = AutoCompleteProcessSearch(mDBPreviousQuery, QUERY_FULL);
rv = AutoCompleteProcessSearch(mDBPreviousQuery, QUERY_FILTERED);
NS_ENSURE_SUCCESS(rv, rv);
// Don't use this query more than once
@ -706,18 +764,18 @@ nsNavHistory::AutoCompletePreviousSearch()
nsresult
nsNavHistory::AutoCompleteFullHistorySearch(PRBool* aHasMoreResults)
{
mozStorageStatementScoper scope(mDBAutoCompleteQuery);
mozStorageStatementScoper scope(mDBCurrentQuery);
nsresult rv = mDBAutoCompleteQuery->BindInt32Parameter(0, GetTagsFolder());
nsresult rv = mDBCurrentQuery->BindInt32Parameter(0, GetTagsFolder());
NS_ENSURE_SUCCESS(rv, rv);
rv = mDBAutoCompleteQuery->BindInt32Parameter(1, mAutoCompleteSearchChunkSize);
rv = mDBCurrentQuery->BindInt32Parameter(1, mAutoCompleteSearchChunkSize);
NS_ENSURE_SUCCESS(rv, rv);
rv = mDBAutoCompleteQuery->BindInt32Parameter(2, mCurrentChunkOffset);
rv = mDBCurrentQuery->BindInt32Parameter(2, mCurrentChunkOffset);
NS_ENSURE_SUCCESS(rv, rv);
rv = AutoCompleteProcessSearch(mDBAutoCompleteQuery, QUERY_FULL, aHasMoreResults);
rv = AutoCompleteProcessSearch(mDBCurrentQuery, QUERY_FILTERED, aHasMoreResults);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
@ -781,6 +839,9 @@ nsNavHistory::AutoCompleteProcessSearch(mozIStorageStatement* aQuery,
nsAutoString entryTags;
rv = aQuery->GetString(kAutoCompleteIndex_Tags, entryTags);
NS_ENSURE_SUCCESS(rv, rv);
PRInt32 visitCount = 0;
rv = aQuery->GetInt32(kAutoCompleteIndex_VisitCount, &visitCount);
NS_ENSURE_SUCCESS(rv, rv);
// Always prefer the bookmark title unless it's empty
nsAutoString title =
@ -799,17 +860,24 @@ nsNavHistory::AutoCompleteProcessSearch(mozIStorageStatement* aQuery,
break;
}
case QUERY_FULL: {
case QUERY_FILTERED: {
// If we get any results, there's potentially another chunk to proces
if (aHasMoreResults)
*aHasMoreResults = PR_TRUE;
// Keep track of if we've matched all the filter requirements such as
// only history items, only bookmarks, only tags. If a given restrict
// is active, make sure a corresponding condition is *not* true. If
// any are violated, matchAll will be false.
PRBool matchAll = !((mRestrictHistory && visitCount == 0) ||
(mRestrictBookmark && !parentId) ||
(mRestrictTag && entryTags.IsEmpty()));
// Unescape the url to search for unescaped terms
nsString entryURL = FixupURIText(escapedEntryURL);
// Determine if every token matches either the bookmark title, tags,
// page title, or page url
PRBool matchAll = PR_TRUE;
for (PRInt32 i = 0; i < mCurrentSearchTokens.Count() && matchAll; i++) {
const nsString *token = mCurrentSearchTokens.StringAt(i);
@ -817,11 +885,20 @@ nsNavHistory::AutoCompleteProcessSearch(mozIStorageStatement* aQuery,
PRBool matchTags = (*tokenMatchesTarget)(*token, entryTags);
// Check if the title matches the search term
PRBool matchTitle = (*tokenMatchesTarget)(*token, title);
// Make sure we match something in the title or tags if we have to
matchAll = matchTags || matchTitle;
if (mMatchTitle && !matchAll)
break;
// Check if the url matches the search term
PRBool matchUrl = (*tokenMatchesTarget)(*token, entryURL);
// True if any of them match; false makes us quit the loop
matchAll = matchTags || matchTitle || matchUrl;
// If we don't match the url when we have to, reset matchAll to
// false; otherwise keep track that we did match the current search
if (mMatchUrl && !matchUrl)
matchAll = PR_FALSE;
else
matchAll |= matchUrl;
}
// Skip if we don't match all terms in the bookmark, tag, title or url

View File

@ -0,0 +1,137 @@
/* ***** 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
* Edward Lee <edward.lee@engineering.uiuc.edu>.
* Portions created by the Initial Developer are Copyright (C) 2008
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
*
* 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 ***** */
/**
* Test for bug 395161 that allows special searches that restrict results to
* history/bookmark/tagged items and title/url matches.
*/
// Define some shared uris and titles (each page needs its own uri)
let kURIs = [
"http://url/",
"http://url/2",
"http://foo.bar/",
"http://foo.bar/2",
"http://url/star",
"http://url/star/2",
"http://foo.bar/star",
"http://foo.bar/star/2",
"http://url/tag",
"http://url/tag/2",
"http://foo.bar/tag",
"http://foo.bar/tag/2",
];
let kTitles = [
"title",
"foo.bar",
];
// Plain page visits
addPageBook(0, 0); // plain page
addPageBook(1, 1); // title
addPageBook(2, 0); // url
addPageBook(3, 1); // title and url
// Bookmarked pages (no tag)
addPageBook(4, 0, 0); // bookmarked page
addPageBook(5, 1, 1); // title
addPageBook(6, 0, 0); // url
addPageBook(7, 1, 1); // title and url
// Tagged pages
addPageBook(8, 0, 0, [1]); // tagged page
addPageBook(9, 1, 1, [1]); // title
addPageBook(10, 0, 0, [1]); // url
addPageBook(11, 1, 1, [1]); // title and url
// Remove pages from history to treat them as unvisited, so pages that do have
// visits are 0,1,2,3,5,10
for each (let uri in [4,6,7,8,9,11])
histsvc.removePage(toURI(kURIs[uri]));
// Provide for each test: description; search terms; array of gPages indices of
// pages that should match; optional function to be run before the test
let gTests = [
// Test restricting searches
["0: History restrict",
"^", [0,1,2,3,5,10]],
["1: Star restrict",
"*", [4,5,6,7,8,9,10,11]],
["2: Tag restrict",
"+", [8,9,10,11]],
// Test specials as any word position
["3: Special as first word",
"^ foo bar", [1,2,3,5,10]],
["4: Special as middle word",
"foo ^ bar", [1,2,3,5,10]],
["5: Special as last word",
"foo bar ^", [1,2,3,5,10]],
// Test restricting and matching searches with a term
["6: foo ^ -> history",
"foo ^", [1,2,3,5,10]],
["7: foo * -> is star",
"foo *", [5,6,7,8,9,10,11]],
["8: foo # -> in title",
"foo #", [1,3,5,7,8,9,10,11]],
["9: foo @ -> in url",
"foo @", [2,3,6,7,10,11]],
["10: foo + -> is tag",
"foo +", [8,9,10,11]],
// Test various pairs of special searches
["11: foo ^ * -> history, is star",
"foo ^ *", [5,10]],
["12: foo ^ # -> history, in title",
"foo ^ #", [1,3,5,10]],
["13: foo ^ @ -> history, in url",
"foo ^ @", [2,3,10]],
["14: foo ^ + -> history, is tag",
"foo ^ +", [10]],
["15: foo * # -> is star, in title",
"foo * #", [5,7,8,9,10,11]],
["16: foo * @ -> is star, in url",
"foo * @", [6,7,10,11]],
["17: foo * + -> same as +",
"foo * +", [8,9,10,11]],
["18: foo # @ -> in title, in url",
"foo # @", [3,7,10,11]],
["19: foo # + -> in title, is tag",
"foo # +", [8,9,10,11]],
["20: foo @ + -> in url, is tag",
"foo @ +", [10,11]],
];