search enhancements WIP.
authoralta88@gmail.com
Sat, 23 Jan 2010 16:11:33 -0700
changeset 1045 47a87898ee7713eda263968e94d853f20f1c8c11
parent 1042 9e75a38979777b034941608e49a2c5ccc3fbc35f
child 1048 596d0f4a64f55a2fc60b15da1ed697af7a4f4c94
child 1050 f3a27d8982b978a2af0009084e0dfd36b5a2dfb0
push id178
push usermyk@mozilla.com
push dateMon, 25 Jan 2010 20:39:38 +0000
search enhancements WIP.
content/collections.js
content/list-sidebar.xul
content/list.js
content/messageContent.css
content/messageHeader.xhtml
content/messagecontent.js
content/snowlSearch.xml
content/toolbarbutton.css
modules/collection.js
modules/datastore.js
--- a/content/collections.js
+++ b/content/collections.js
@@ -92,16 +92,21 @@ let CollectionsView = {
     return this._refreshButton = document.getElementById("snowlRefreshButton");
   },
 
   get _toggleListToolbarButton() {
     delete this._toggleListToolbarButton;
     return this._toggleListToolbarButton = document.getElementById("listToolbarButton");
   },
 
+  get _searchButton() {
+    delete this._searchButton;
+    return this._searchButton = document.getElementById("snowlListViewSearchButton");
+  },
+
   get itemIds() {
     let intArray = [];
     let strArray = this._tree.getAttribute("itemids").split(",");
     for each (let intg in strArray)
       intArray.push(parseInt(intg));
     delete this._itemIds;
     return this._itemIds = intArray;
   },
@@ -316,19 +321,19 @@ this._log.info("onClick: START itemIds -
     else
       this._tree.currentSelectedIndex = this._tree.currentIndex;
 
     // Mod key click will deselect a row; for a 0 count notify view to clear if
     // not in a search.
     if (this._tree.view.selection.count == 0) {
       this._tree.currentSelectedIndex = -1;
       this.itemIds = -1;
-      let searchMsgs = this._searchFilter.getAttribute("searchtype") == "messages";
+      let searchCols = this._searchFilter.getAttribute("searchtype") == "collections";
       let searchEmpty = this._searchFilter.getAttribute("empty") == "true";
-      if (!isBookmark && (!searchMsgs || searchEmpty)) {
+      if (!isBookmark && (searchCols || searchEmpty)) {
         gMessageViewWindow.SnowlMessageView.onCollectionsDeselect();
         return;
       }
     }
 
     // See if it can be opened like a bookmark.
     if (isBookmark) {
       this.itemIds = -1;
@@ -340,20 +345,24 @@ this._log.info("onClick: START itemIds -
     if (!modKey && this.itemIds != -1)
       // Unset new status for (about to be) formerly selected collection(s).
       this.markCollectionNewState();
 
     // Get constraints based on selected rows
     let constraints = this.getSelectionConstraints();
 
     if (!constraints) {
-        gMessageViewWindow.SnowlMessageView.onCollectionsDeselect();
-        return;
+      gMessageViewWindow.SnowlMessageView.onCollectionsDeselect();
+      return;
     }
 
+    if (this._searchFilter.value)
+      // Switching selection while in search, show busy.
+      this._searchButton.parentNode.setBusy(true);
+
     let collection = new SnowlCollection(null,
                                          name,
                                          null, 
                                          constraints,
                                          null);
     gMessageViewWindow.SnowlMessageView.setCollection(collection, this.Filters);
   },
 
@@ -378,129 +387,213 @@ this._log.info("onClick: START itemIds -
     aEvent.target.checked = !aEvent.target.checked;
     if (this._listToolbar.hasAttribute("hidden"))
       this._listToolbar.removeAttribute("hidden");
     else
       this._listToolbar.setAttribute("hidden", true);
   },
 
   onSearch: function(aValue) {
+    let searchType = this._searchFilter.getAttribute("searchtype");
+//    let searchTypeRE = this._searchFilter.getAttribute("searchtype");
+//    let searchRE = searchType == "subject" ||
+//                   searchType == "sender" ||
+//                   searchType == "headers" ||
+//                   searchType == "messages";
+    let searchMsgsFTS = searchType == "messagesFTS";
+    let searchCols = searchType == "collections";
+
+    // Edit check routine.
     let term, terms = [], filterTerms = [];
-    let searchMsgs = this._searchFilter.getAttribute("searchtype") == "messages";
-    let searchCols = this._searchFilter.getAttribute("searchtype") == "collections";
-    let quotes = aValue.match("\"", "g");
+    let quotes = aValue.match(/^"|[^\\]"/g);
     let quotesClosed = (!quotes || quotes.length%2 == 0) ? true : false;
+//    let quotesEscaped = aValue.match(/\\"/g);
+//    let quotesClosedEscaped = (!quotesEscaped || quotesEscaped.length%2 == 0) ? true : false;
     let oneNegation = false;
     // XXX: It would be nice to do unicode properly, \p{L} - Bug 258974.
     let invalidInitial = new RegExp("^[^\"\\w\\u0080-\\uFFFF]|\\\|(?=\\s*[\|\-])+|\~(?!\\d(?=$|\\s)|\\s|$)");
     let invalidNegation = new RegExp("\-.*[^\\w\\u0080-\\uFFFF]");
     let invalidUnquoted = new RegExp("^[^\-|\~|\\w\\u0080-\\uFFFF]{1}?|.(?=[^\\w\\u0080-\\uFFFF])");
     let invalidQuoted = new RegExp("\"(?=[^\'\\w\\u0080-\\uFFFF])");
 
-    if ((aValue != "" && aValue.match(invalidInitial)) ||
-        (!quotesClosed && aValue.substr(aValue.lastIndexOf("\""), aValue.length).
-                                 search(invalidQuoted) != -1)) {
+    if (aValue.match(/[^\\]""/g) ||
+        (aValue != "" && (searchMsgsFTS && aValue.match(invalidInitial))) ||
+        (searchMsgsFTS && !quotesClosed &&
+         aValue.substr(aValue.lastIndexOf("\""), aValue.length).
+                search(invalidQuoted) != -1)) {
       this._searchFilter.setAttribute("invalid", true);
-      return;
+      return false;
     }
+this._log.info("onSearch: aValue - "+ aValue);
 
-    terms = aValue.match("[^\\s\"']+|\"[^\"]*\"*|'[^']*'*", "g");
+    // Negation - is its own term for messages, included in term for headers.
+    terms = searchMsgsFTS ? aValue.match("[^\\s\"']+|\"[^\"]*\"*|'[^']*'*", "g") :
+                            aValue.match(/(-?"(?:[^\\"]+|\\.)*?"|\S+)/g);
+//                            aValue.match("(-?\"[^\"]+\"|[^\"\\s]+)", "g");
+this._log.info("onSearch: terms - "+ terms);
+this._log.info("onSearch: quotesClosed - "+ quotesClosed);
 
     while (terms && (term = terms.shift())) {
-      if (term.match(/\"/g)) {
-        // Quoted term: invalid if already have negation term; cannot be empty or
-        // end in space(s); cannot start with space(s) or invalid chars.
-        if (oneNegation || term.match(/[\s\"](?=")/g) || term.match(invalidQuoted)) {
-          this._searchFilter.setAttribute("invalid", true);
-          return;
+this._log.info("onSearch: term - "+ term);
+      if (searchMsgsFTS) {
+        // SQLite FTS based search.
+        if (term.match(/\"/g)) {
+          // Quoted term: invalid if already have negation term; cannot be empty
+          // or end in space(s); cannot start with space(s) or invalid chars.
+          if (oneNegation || term.match(/[\s\"](?=")/g) || term.match(invalidQuoted)) {
+            this._searchFilter.setAttribute("invalid", true);
+            return false;
+          }
+        }
+        else {
+          // Unquoted term: invalid if already have negation term; | term cannot
+          // lead a term and must be standalone; must start with valid chars and
+          // cannot contain invalid chars.
+          if (oneNegation || term.match(/\|(?=\S)/) || term.match(invalidUnquoted)) {
+            this._searchFilter.setAttribute("invalid", true);
+            return false;
+          }
+
+          // Negation: can only have one negation term and it must be the last
+          // term (error on rest of search string ending in space indicates this)
+          // and cannot contain invalid chars.
+          if (term[0] == "-") {
+            oneNegation = true;
+            if (aValue.substr(aValue.lastIndexOf("\-"), aValue.length).
+                                     search(invalidNegation) > -1) {
+              this._searchFilter.setAttribute("invalid", true);
+              return false;
+            }
+          }
+
+          if (term == "|")
+            // Unquoted | means OR.  We do not use OR because it is 1)twice the
+            // typing, 2)english-centric.
+            term = "OR"
+          else if (term[0] == "~")
+            // Unquoted ~ means NEAR.  We do not use NEAR because it is 1)4x the
+            // typing, 2)english-centric.
+            term = "NEAR" + (term[1] ? "/" + term[1] : "");
+          else
+            // Add asterisk to non quoted term for sqlite fts non-exact match.
+            term = SnowlUtils.appendAsterisks(term);
         }
       }
       else {
-        // Unquoted term: invalid if already have negation term; | term cannot
-        // lead a term and must be standalone; must start with valid chars and
-        // cannot contain invalid chars.
-        if (oneNegation || term.match(/\|(?=\S)/)|| term.match(invalidUnquoted)) {
-          this._searchFilter.setAttribute("invalid", true);
-          return;
-        }
+//this._log.info("onSearch: quotesClosed - "+ quotesClosed);
+        // Regex based search.
+        if (!quotesClosed)
+          return false;
 
-        // Negation: can only have one negation term and it must be the last
-        // term (error on rest of search string ending in space indicates this)
-        // and cannot contain invalid chars.
-        if (term[0] == "-") {
+        // At least one negation term, all subsequent negation terms must follow
+        // each other and come at the end of the search string.
+        if (term[0] == "-")
           oneNegation = true;
-          if (aValue.substr(aValue.lastIndexOf("\-"), aValue.length).
-                                   search(invalidNegation) > -1) {
-            this._searchFilter.setAttribute("invalid", true);
-            return;
-          }
+
+        // Term: invalid if already have negation term and this term is not also
+        // a negation term; NEAR ~ invalid.
+        if ((oneNegation && !term.match(/^[-|]/)) ||
+            term[0] == "~") {
+          this._searchFilter.setAttribute("invalid", true);
+          return false;
         }
 
         if (term == "|")
           // Unquoted | means OR.  We do not use OR because it is 1)twice the
           // typing, 2)english-centric.
           term = "OR"
-        else if (term[0] == "~")
-          // Unquoted ~ means NEAR.  We do not use NEAR because it is 1)4x the
-          // typing, 2)english-centric.
-          term = "NEAR" + (term[1] ? "/" + term[1] : "");
-        else
-          // Add asterisk to non quoted term for sqlite fts non-exact match.
-          term = SnowlUtils.appendAsterisks(term);
       }
 
       filterTerms.push(term);
     }
 
     this._searchFilter.removeAttribute("invalid");
 
     if (aValue.charAt(aValue.length - 1).match(/\s|\||\~/) ||
+//        aValue.match(/^[-\\]$/) ||
         (aValue.charAt(aValue.length - 1).match(/-/) &&
          aValue.charAt(aValue.length - 2).match(/\s/)) ||
         (aValue.charAt(aValue.length - 1).match(/\d/) &&
          aValue.charAt(aValue.length - 2).match(/\~/)) ||
+//        (aValue.charAt(aValue.length - 1).match(/\\/) &&
+//         aValue.charAt(aValue.length - 2).match(/\s|-/)) ||
         !quotesClosed)
       // Don't run search on a space, or OR |, or negation -, or unclosed quote ",
-      // or NEAR ~ number.
-      return;
+      // or NEAR ~ number, or \ for escaped quotes.
+      return false;
+
+    this._searchButton.parentNode.setBusy(true);
 
     // Save string.
-    this.Filters["searchterms"] = aValue ? filterTerms.join(" ") : null;
+    if (searchMsgsFTS)
+      this.Filters["searchterms"] = aValue ? filterTerms.join(" ") : null;
+
+    // Post edit check processing for regex terms string.
+    else {
+      let regexTerms = [];
+this._log.info("onSearch: searchRE filterTerms - "+ filterTerms);
+
+      // Array for highlighter.
+      this.Filters["searchterms"] = aValue ? filterTerms : null;
+
+      for each (term in filterTerms)
+        // Extra special handling to escape \ unless it escapes a "; escape
+        // special chars; remove leading "; remove last quote " making sure to
+        // preserve negation and escaped quotes; unescape escaped quotes \".
+        regexTerms.push(term.replace(/(\\)(?=[^"]|$)/g, "\\\\\\$1").
+                             replace(/([.^$*+?&<>()[{}|\]])/g, "\\$1").
+                             replace(/^"/, "").
+                             replace(/(-?"|[^\\])"/g, "$1").
+                             replace(/\\"/g, '"'));
+this._log.info("onSearch: searchRE regexTerms - "+ regexTerms);
+
+      // Unquoted array for regex.
+      SnowlDatastore._dbRegexp.termsArray = regexTerms;
+    }
+
+//this._log.info("onSearch: this.Filters[searchterms] - "+ this.Filters["searchterms"]);
 
     if (aValue) {
       if (searchCols)
-        // Search collections.
-        this.searchCollections(aValue);
-      if (searchMsgs)
+        // Search collections.  Do this on a timeout to allow css to decorate
+        // throbber.
+        setTimeout(function() {
+          CollectionsView.searchCollections(aValue);
+        }, 0)
+      else
         // Search selected or 'All Messages' if no explicit selection.
         gMessageViewWindow.SnowlMessageView.onFilter(this.Filters);
     }
     else {
-      if (searchMsgs) {
-        // XXX: Reloads current page from cache to clear highlights.  But maybe
-        // nicer to unhighlight and avoid reload stutter..
-        // TODO: Clear cache for all snowl xhtml pages in history, above not an issue.
-        gMessageViewWindow.BrowserReload();
+      if (searchCols){
+        // Reset the collections tree.
+        this._tree.place = this._tree.place;
+        this.noSelect = true;
+        this._tree.restoreSelection();
+        this._searchButton.parentNode.setBusy(false);
+      }
+      else {
+        // TODO: Clear highlights, if any, in bf cache.
+        let browserWin = gMessageViewWindow.gBrowser.contentWindow;
+        browserWin.wrappedJSObject.messageHeaderUtils.higlightClear();
 
         if (this.itemIds == -1)
           // If no selection and clearing searchbox, clear list (don't select 'All').
           gMessageViewWindow.SnowlMessageView.onCollectionsDeselect();
         else
           // Re-select the selected collections.
           gMessageViewWindow.SnowlMessageView.onFilter(this.Filters);
       }
-      if (searchCols){
-        // Reset the collections tree.
-        this._tree.place = this._tree.place;
-        this.noSelect = true;
-        this._tree.restoreSelection();
-      }
     }
   },
 
+  onSearchIgnoreCase: function(aChecked) {
+    SnowlDatastore._dbRegexp.ignoreCase = aChecked;
+  },
+
   onSearchHelp: function() {
     openDialog("chrome://snowl/content/searchHelp.xul",
                "title",
                "chrome,dialog,resizable=yes");
   },
 
   onCommandUnreadButton: function(aEvent) {
     // Unfortunately, css cannot be used to hide a treechildren row using
@@ -700,37 +793,50 @@ this._log.info("onClick: START itemIds -
       this._tree.treeBoxObject.invalidate();
     }
   },
 
   markCollectionNewState: function() {
     // Mark all selected source/author collection messages as not new (unread)
     // upon the collection being no longer selected.  Note: shift-click on a
     // collection will leave new state when unselected.
-    let itemId, uri, sources = [], authors = [], query, all = false;
+    let itemId, uri, query, collID, nodeStats, all = false;
+    let sources = [], authors = [];
     let itemIds = this.itemIds;
     let currentIndexItemId =
         this._tree.view.nodeForTreeIndex(this._tree.currentIndex).itemId
 
     for each (itemId in itemIds) {
       // If selecting item currently in selection, leave as new.
       if (itemId == currentIndexItemId)
         continue;
 
       // Create places query object from places item uri.
       try {
         uri = PlacesUtils.bookmarks.getBookmarkURI(itemId).spec;
       } catch (ex) { continue;} // Not a query item.
+
       query = new SnowlQuery(uri);
       if (query.queryFolder == SnowlPlaces.collectionsSystemID ||
           query.queryFolder == SnowlPlaces.collectionsSourcesID ||
           query.queryFolder == SnowlPlaces.collectionsAuthorsID) {
         all = true;
         break;
       }
+
+      collID = query.queryTypeSource ? "s" + query.queryID :
+               query.queryTypeAuthor ? "a" + query.queryID :
+              (query.queryFolder == SnowlPlaces.collectionsSystemID ||
+               query.queryFolder == SnowlPlaces.collectionsSourcesID ||
+               query.queryFolder == SnowlPlaces.collectionsAuthorsID) ? "all" : null;
+      nodeStats = SnowlService.getCollectionStatsByCollectionID()[collID];
+      if (!nodeStats || nodeStats.n == 0)
+        // No new messages in this collection, go on.
+        continue;
+
       if (query.queryTypeSource && sources.indexOf(query.queryID, 0) < 0)
         sources.push(query.queryID);
       if (query.queryTypeAuthor && authors.indexOf(query.queryID, 0) < 0)
         authors.push(query.queryID);
     }
 
     query = "";
     if (!all) {
@@ -1017,31 +1123,31 @@ this._log.info("onClick: START itemIds -
   },
 
   searchCollections: function(aSearchString) {
     // XXX: Bug 479903, place queries have no way of excluding search in uri,
     // which may not be meaningful for our usage.
     let searchFolders = [];
     let view = this._collectionsViewMenu.value;
     if (view && view != "default")
-      // Limit search to authors/sources/custom if that view selected, else all
+      // Limit search to authors/sources/custom if that view selected, else all.
       searchFolders = view == "sources" ? [SnowlPlaces.collectionsSourcesID] :
                       view == "authors" ? [SnowlPlaces.collectionsAuthorsID] :
                                           [parseInt(view)];
     else {
       // XXX Get selected items and search only those
       searchFolders = [SnowlPlaces.collectionsSystemID];
     }
 
-    if (!aSearchString)
-      this._tree.place = this._tree.place;
-    else {
+//    if (!aSearchString)
+//      this._tree.place = this._tree.place;
+//    else {
       this._tree.applyFilter(aSearchString, searchFolders);
-      
-    }
+      this._searchButton.parentNode.setBusy(false);
+//    }
   },
 
   isMessageForSelectedCollection: function(aMessage) {
     // Determine if source or author of new message is currently selected in the
     // collections list.
     // XXX: see if there is a Places event/mechanism we can use instead?
     let query, uri, rangeFirst = { }, rangeLast = { }, refreshFlag = false;
     let numRanges = this._tree.view.selection.getRangeCount();
@@ -1095,16 +1201,18 @@ this._log.info("onClick: START itemIds -
         selectedItemIds.push(itemId);
         uri = node.uri;
         let query = new SnowlQuery(uri);
 
         if (query.queryFolder == SnowlPlaces.collectionsSystemID) {
           // Collection folder that returns all records, reset constraints
           // and break.  There may be other such 'system' collections but more
           // likely collections will be rows which are user defined snowl: queries.
+//          constraints = [{expression:"sources.id > :zero",
+//                          parameters:"0"}];
           constraints = [];
           stop = true;
           break;
         }
         else if (node.hasChildren &&
                  query.queryFolder != SnowlPlaces.collectionsAuthorsID &&
                  query.queryFolder != SnowlPlaces.collectionsSourcesID) {
           // Container: user created folder is selected; get array with query
--- a/content/list-sidebar.xul
+++ b/content/list-sidebar.xul
@@ -80,20 +80,19 @@
              accesskey="&search.accesskey;"
              control="snowlFilter"/>
       <!-- type="timed" timeout="200"  -->
       <textbox id="snowlFilter"
                flex="1"
                type="search"
                emptytext=""
                searchtype="messages"
-               persist="searchtype"
+               persist="searchtype ignorecase"
                oncommand="CollectionsView.onSearch(this.value)"/>
     </toolbaritem>
-    <toolbarspring/>
     <toolbaritem id="viewBox"
                  align="center">
       <label value="&view.label;"
              accesskey="&view.accesskey;"
              control="collectionsViewMenu"/>
       <menulist id="collectionsViewMenu"
                 sizetopopup="none"
                 selectedindex=""
--- a/content/list.js
+++ b/content/list.js
@@ -130,16 +130,20 @@ let SnowlMessageView = {
   setTree: function(treebox){ this._treebox = treebox; },
 
   get rowCount() {
 //this._log.info("get rowCount: " + this._collection.messages.length);
     return this._collection.messages.length;
   },
 
   getCellText: function(aRow, aColumn) {
+    if (!this._collection.messages[aRow])
+      // Prevent transient throw.
+      return null;
+
     // FIXME: use _columnProperties instead of hardcoding column
     // IDs and property names here.
     switch(aColumn.id) {
       case "snowlSourceCol":
         return this._collection.messages[aRow].source.name;
 
       case "snowlAuthorCol":
         return this._collection.messages[aRow].author ?
@@ -156,16 +160,20 @@ let SnowlMessageView = {
         return SnowlDateUtils._formatDate(this._collection.messages[aRow].received);
 
       default:
         return null;
     }
   },
 
   getCellProperties: function (aRow, aColumn, aProperties) {
+    if (!this._collection.messages[aRow])
+      // Prevent transient throw.
+      return null;
+
     // We have to set this on each cell rather than on the row as a whole
     // because the text styling we apply to unread/deleted messages has to be
     // specified by the ::-moz-tree-cell-text pseudo-element, which inherits
     // only the cell's properties.
     if (this._collection.messages[aRow].read == MESSAGE_UNREAD ||
         this._collection.messages[aRow].read == MESSAGE_NEW)
       aProperties.AppendElement(this._atomSvc.getAtom("unread"));
 
@@ -188,20 +196,19 @@ let SnowlMessageView = {
     aProperties.AppendElement(this._atomSvc.getAtom("col-" + aColumn.id));
   },
 
   cycleCell: function(aRow, aColumn) {
     if (aColumn.id == "snowlFlaggedCol")
       this._setFlagged(aRow);
     if (aColumn.id == "snowlReadCol") {
       let read = this._collection.messages[aRow].read;
-      this._collection.messages[aRow].read = (read == MESSAGE_UNREAD ||
-                                              read == MESSAGE_NEW) ?
-                                              MESSAGE_READ : MESSAGE_UNREAD;
-      this._collection.messages[aRow].persist();
+      read = read == MESSAGE_UNREAD || read == MESSAGE_NEW ? MESSAGE_READ :
+                                                             MESSAGE_UNREAD;
+      this._setRead(read, aRow);
     }
   },
 
   cycleHeader: function(aColumn) {},
   isContainer: function(aRow) { return false },
   isSeparator: function(aRow) { return false },
   isSorted: function() { return false },
   getLevel: function(aRow) { return 0 },
@@ -296,29 +303,60 @@ let SnowlMessageView = {
       filters.push({ expression: "(current = " + MESSAGE_NON_CURRENT_DELETED + " OR" +
                                  " current = " + MESSAGE_CURRENT_DELETED + ")",
                      parameters: {} });
     else
       filters.push({ expression: "(current = " + MESSAGE_NON_CURRENT + " OR" +
                                  " current = " + MESSAGE_CURRENT + ")",
                      parameters: {} });
 
-    // FIXME: use a left join here once the SQLite bug breaking left joins to
-    // virtual tables has been fixed (i.e. after we upgrade to SQLite 3.5.7+).
     if (this.Filters["searchterms"]) {
-      filters.push({ expression: "messages.id IN " +
-                                 "(SELECT messageID FROM parts" +
-                                 " JOIN partsText ON parts.id = partsText.docid" +
-                                 " WHERE partsText.content MATCH :filter)",
-                     parameters: { filter: this.Filters["searchterms"] } });
+      let searchExpressions = {
+        "subject"     : "(messages.subject REGEXP :filter)",
+        "sender"      : "(identities_externalID REGEXP :filter OR" +
+                        " people_name REGEXP :filter)",
+        "headers"     : "(messages.headers REGEXP :filter)",
+        // FIXME: use a left join here once the SQLite bug breaking left joins to
+        // virtual tables has been fixed (i.e. after we upgrade to SQLite 3.5.7+).
+        "messages"    : "messages.id IN " +
+                        "(SELECT messageID FROM parts" +
+                        " JOIN partsText ON parts.id = partsText.docid" +
+                        " WHERE partsText.content REGEXP :filter)",
+        "messagesFTS" : "messages.id IN " +
+                        "(SELECT messageID FROM parts" +
+                        " JOIN partsText ON parts.id = partsText.docid" +
+                        " WHERE partsText.content MATCH :filter)",
+//        "messages" : "(partsText_content REGEXP :filter)",
+//        "messages" : "(summary_content REGEXP :filter OR content_content REGEXP :filter)",
+        "XXX"      : "XXX",
+      };
+      let searchFilters = {
+        "subject"     : "SUBJECT",
+        "sender"      : "SENDER",
+        "headers"     : "HEADERS",
+        "messages"    : "MESSAGES",
+        "messagesFTS" : this.Filters["searchterms"],
+        "XXX"         : "XXX" //this.Filters["searchterms"]
+      };
+
+      let searchType = this.CollectionsView._searchFilter.getAttribute("searchtype");
+      let searchExpression = searchExpressions[searchType];
+      let searchFilter = searchFilters[searchType];
+//this._log.info("_applyFilters: searchFilter - "+ searchFilter);
+
+      filters.push({ expression: searchExpression,
+                     parameters: { filter: searchFilter } });
     }
 
     this._collection.filters = filters;
     this._collection.invalidate();
-    this._rebuildView();
+
+    setTimeout(function() {
+      SnowlMessageView._rebuildView();
+    }, 0)
   },
 
   setCollection: function(collection, aFilters) {
     this._collection = collection;
     this.Filters = aFilters;
     this._applyFilters();
   },
 
@@ -333,22 +371,24 @@ let SnowlMessageView = {
     // by reinitializing it instead of merely invalidating the box object
     // (which wouldn't accommodate changes to the number of rows).
     // XXX Is there a better way to do this?
     // this._tree.view = this; <- doesn't work for all DOM moves..
     this._tree.boxObject.QueryInterface(Ci.nsITreeBoxObject).view = this;
 
     this._sort();
 
-    if (this.Filters["searchterms"])
+    this.CollectionsView._searchButton.parentNode.setBusy(false);
+    if (this.Filters["searchterms"]) {
       if (this._collection.messages[0])
         // Select first item when searching.
         this._tree.view.selection.select(0);
       else
         this._collection._messages = [];
+    }
 
     // Scroll back to the top of the tree.
     // XXX: need to preserve selection.
 //    this._tree.boxObject.scrollToRow(this._tree.boxObject.getFirstVisibleRow());
   },
 
   switchLayout: function(layout) {
     // Build the layout
@@ -589,18 +629,18 @@ let SnowlMessageView = {
     let readState = message.read == MESSAGE_UNREAD ? MESSAGE_READ : MESSAGE_UNREAD;
 
     if (aAll)
       this._setAllRead(readState);
     else
       this._setRead(readState);
   },
 
-  _setRead: function(aRead) {
-    let row = this._tree.currentIndex;
+  _setRead: function(aRead, aRow) {
+    let row = aRow == null ? this._tree.currentIndex : aRow;
     let message = this._collection.messages[row];
     message.read = aRead;
     message.persist();
     this._tree.boxObject.invalidateRow(row);
 
     // It would be nicer to update just the source/author stats object for
     // this message rather than rebuild the cache from db, but would be only a
     // small saving.
--- a/content/messageContent.css
+++ b/content/messageContent.css
@@ -58,17 +58,17 @@
   height: 20px;
 }
 
 #body {
   margin: 0px;
 }
 
 #contentBody {
-  margin: 8px;
+  margin: 22px 8px 8px 8px;
 }
 
 table {
   font-family: sans-serif;
   border-spacing: 0px 0px;
   width: 100%;
 }
 
@@ -160,45 +160,65 @@ body.wrap > table > tr > .headerLabel + 
 .fullHeaderRowSeparator {
   border-top: 1px solid threedshadow;
   margin: 0 10px;
 }
 
 /* The search highlight generator will create as many unique classnames as there
  * are search terms, in the form .hl#, and 7 styles are defined here, the rest
  * default. Theme here is 'Blue Variations' but any customization is possible. */
-.hldefault, .hl0 {
+.hl, .hl0,
+.hl > a, .hl0 > a {
   background: #0000CD;
   color: white;
 }
-.hl1 {
+.hl1,
+.hl1 > a {
   background: #4682B4;
   color: white;
 }
-.hl2 {
+.hl2,
+.hl2 > a {
   background: #4169E1;
   color: white;
 }
-.hl3 {
+.hl3,
+.hl3 > a {
   background: #6495ED;
   color: white;
 }
-.hl4 {
+.hl4,
+.h4l > a {
   background: #1E90FF;
   color: white;
 }
-.hl5 {
+.hl5,
+.hl5 > a {
   background: #87CEFA;
   color: white;
 }
-.hl6 {
+.hl6,
+.hl6 > a {
   background: #00BFFF;
   color: white;
 }
-
+#hlMarkerContainer {
+  position:fixed;
+  top:0;
+  right:0;
+  width:10px;
+  height:100%;
+  background:#eee;
+}
+.hlMarker {
+  position:absolute;
+  width:12px;
+  height:3px;
+  cursor:pointer;
+}
 /* Icon courtesy of http://sozai.7gates.net/en/docs/pushpin_icon01/ */
 #pinButton {
   list-style-image: url("chrome://snowl/content/icons/pushpin.gif");
 }
 
 #headerDeck[header="brief"] > tr > td > #headerButton {
   list-style-image: url("chrome://snowl/content/icons/application_split.png");
 }
--- a/content/messageHeader.xhtml
+++ b/content/messageHeader.xhtml
@@ -32,17 +32,18 @@
    - 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 html [
-  <!ENTITY %    htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+  <!ENTITY %    htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+                               "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
   %htmlDTD;
   <!ENTITY %  globalDTD SYSTEM "chrome://global/locale/global.dtd">
   %globalDTD;
   <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
   %browserDTD;
   <!ENTITY % messageDTD SYSTEM "chrome://snowl/locale/message.dtd">
   %messageDTD;
 ]>
@@ -54,24 +55,24 @@
   <head>
     <link rel="stylesheet" type="text/css" media="all"
           href="chrome://snowl/content/messageContent.css"/>
 
     <script type="application/x-javascript"
             src="chrome://snowl/content/messagecontent.js"/>
 
   </head>
-<!--  -->
+  <!-- Use onpageshow instead of onload -->
   <body id="body"
         dir="&locale.dir;"
-        onload="messageContent.createHeader();"
         onclick="messageHeaderUtils.onClick(event);"
         onresize="messageHeaderUtils.onResize();"
         onmousemove="messageHeaderUtils.onMouseMove(event);"
-        onpageshow="messageHeaderUtils.init();">
+        onpageshow="messageHeaderUtils.init();"
+        onunloadXX="messageHeaderUtils.onUnload();">
     <table id="headerDeck">
       <tr id="briefHeaderButtons">
         <td class="headerButtons" valign="top" rowspan="5">
           <xul:toolbarbutton id="pinButton"
                              class="toolbarbutton-1"
                              type="checkbox"
                              onbroadcast="messageHeaderUtils.init()"
                              oncommand="messageHeaderUtils.togglePin(event)"
@@ -103,17 +104,17 @@
         </td>
      
       </tr>
 
       <tr id="briefHeaderRow">
         <!-- Do not leave any space/comments or wrap/indent between headerLabel
              and headerData </td><td elements as this will create a #text node and 
              disrupt nextSibling usage. -->
-        <td class="headerLabel headerLabelSubject altrowA" align="right">&subject.label;</td><td
+        <td class="headerLabel headerLabelSubject" align="right">&subject.label;</td><td
             class="headerData headerDataSubject"><a id="subject"/></td>
         <td class="briefHeader flexer"/>
 
         <!-- Using an xul:description allows for cropping and ellipses, but can't
              highlight using html methods.
         <td class="briefHeader">
           <xul:description id="briefAuthor" crop="end" class="headerData"/>
         </td> -->
@@ -124,22 +125,22 @@
         <td class="headerLabel briefHeader">&timestamp.label;</td><td
             class="headerData briefHeader headerDataLast"><a id="briefTimestamp"/></td>
       </tr>
 
       <tr><td class="headerPadBottom" colspan="2"/></tr>
 
 
       <tr class="basicHeaderRow">
-        <td class="headerLabel altrowB">&author.label;</td><td
+        <td class="headerLabel">&author.label;</td><td
             class="headerData"><a id="author"/></td>
       </tr>
 
       <tr class="basicHeaderRow">
-        <td class="headerLabel altrowA" colspan="2">&timestamp.label;</td><td
+        <td class="headerLabel" colspan="2">&timestamp.label;</td><td
             class="headerData"><a id="timestamp" colspan="3"/></td>
       </tr>
 <!-- -->
     </table>
 
     <!-- Full headers table rows are dynamically created. -->
     <table id="headerFullTable" class="fullHeaderTable">
       <tr class="fullHeaderRow">
--- a/content/messagecontent.js
+++ b/content/messagecontent.js
@@ -55,16 +55,17 @@ var strings = new StringBundle("chrome:/
 
 //****************************************************************************//
 // Create headers and content.
 
 var messageContent = {
   id: null,
   title: null,
   message: null,
+  hilightMode: false,
 
   _headers: null,
   get headers() {
     if (this._headers)
       return this._headers;
 
     return this._headers = this.message.headers;
   },
@@ -97,77 +98,81 @@ var messageContent = {
   },
 
   createTitle: function() {
     this.title = this.message ? this.message.subject || this.message.excerpt :
                                 strings.get("messageNotFoundTitle", [this.id]);
     top.document.title = this.title;
   },
 
-  createHeader: function() {
+  createHeader: function(headerDeck) {
     // The message is found in the scope of the parent frameset document.
     var messageContent = parent.wrappedJSObject.messageContent;
     var id = messageContent.id;
     var message = messageContent.message;
-    var headerDeck = document.getElementById("headerDeck");
+    var hdDoc = document.getElementById("messageHeader").contentDocument;
+    var subjectLink = hdDoc.getElementById("subject");
 
-    if (!id)
-      // Not in a message.
+    if (!id || subjectLink.childNodes.length > 0)
+      // Not in a message.  Also return if the header is already built (in a
+      // pageshow on back/forward navigation).
       return;
 
     if (message) {
       // Brief headers
-      var subjectLink = document.getElementById("subject");
-      subjectLink.appendChild(document.createTextNode(message.subject || message.excerpt));
+      subjectLink.appendChild(document.createTextNode(message.subject ||
+                                                      message.excerpt));
       if (message.link) {
         SnowlUtils.safelySetURIAttribute(subjectLink,
                                          "href",
                                          message.link.spec,
                                          message.source.principal);
         subjectLink.target = "messageBody";
       }
 
       if (message.author && message.author.person)
-        document.getElementById("briefAuthor").
-                 appendChild(document.createTextNode(message.author.person.name));
+        hdDoc.getElementById("briefAuthor").appendChild(
+              document.createTextNode(message.author.person.name));
         // If using xul:description...
 //                 setAttribute("value", message.author.person.name);
-      document.getElementById("briefTimestamp").
-               appendChild(document.createTextNode(SnowlDateUtils._formatDate(message.timestamp)));
+      hdDoc.getElementById("briefTimestamp").appendChild(
+            document.createTextNode(SnowlDateUtils._formatDate(message.timestamp)));
 
       // Basic headers
       if (message.author && message.author.person)
-        document.getElementById("author").
-                 appendChild(document.createTextNode(message.author.person.name));
-      document.getElementById("timestamp").
-               appendChild(document.createTextNode(SnowlDateUtils._formatDate(message.timestamp)));
+        hdDoc.getElementById("author").appendChild(
+              document.createTextNode(message.author.person.name));
+      hdDoc.getElementById("timestamp").appendChild(
+            document.createTextNode(SnowlDateUtils._formatDate(message.timestamp)));
 
       headerDeck.removeAttribute("notfound");
 
       if (message.current == MESSAGE_NON_CURRENT_DELETED ||
           message.current == MESSAGE_CURRENT_DELETED)
         headerDeck.setAttribute("deleted", true);
 
       // Highlight search text, if any.
-//      headerDeck.innerHTML = messageHeaderUtils.highlight("headers", headerDeck.innerHTML);
+      var window = parent.document.getElementById("messageHeader").contentWindow;
+      messageHeaderUtils.highlight("basicheaders", window);
     }
     else {
       // Message no longer exists (removed source/author/message) but is in history.
       headerDeck.setAttribute("notfound", true);
     }
   },
 
   createFullHeader: function(headerDeck) {
 //window.SnowlUtils._log.info("createHeader: headers - "+this.headers.toSource());
     if (!this.headers)
       return;
 
     // Iterate through message headers object and create full header.
     var name, value, headerRow, headerRowLabel, headerRowData;
-    var fullHeaderTable = headerDeck.parentNode.getElementsByClassName("fullHeaderTable")[0];
+    var fullHeaderTable = headerDeck.parentNode.
+                                     getElementsByClassName("fullHeaderTable")[0];
     if (fullHeaderTable.className != "fullHeaderTable")
       return;
 
     for ([name, value] in Iterator(this.headers)) {
       headerRow = document.createElementNS(HTML_NS, "tr");
       headerRow.className = "fullHeaderRow";
 
       headerRowLabel = document.createElementNS(HTML_NS, "td");
@@ -181,81 +186,108 @@ var messageContent = {
       headerRowDataA.textContent = value;
       headerRowData.appendChild(headerRowDataA);
       headerRow.appendChild(headerRowData);
 
       fullHeaderTable.appendChild(headerRow);
     }
 
     // Highlight search text, if any.
-//    headerDeck.parentNode.innerHTML = 
-//        messageHeaderUtils.highlight("headers", headerDeck.parentNode.innerHTML);
+    var window = parent.document.getElementById("messageHeader").contentWindow;
+    messageHeaderUtils.highlight("headers", window);
   },
 
   createBody: function(aType) {
+//    var doc = parent.document.getElementById("messageBody").top.document; //contentWindow;
+//    var win = parent.document.getElementById("messageBody").contentWindow;
+//    messageHeaderUtils.highlight("messages", window);
+//document.getElementById("body"); //
+    // Fires after load completely done, see if highlighing is on.
+//    frame.addEventListener("load",
+//                           function () {
+//                             messageHeaderUtils.highlight("messages", win)
+//                           },
+//                           false);
+
     // The message is found in the scope of the parent frameset document.
     var messageContent = parent.wrappedJSObject.messageContent;
     var id = messageContent.id;
     var message = messageContent.message;
     var content;
 
     if (message) {
       content = message.content || message.summary;
     }
     else {
-      // Message no longer exists (removed source/author/message) but is in history.
+      // Message no longer exists (removed source/author/message) but is in
+      // history.
       content = Cc["@mozilla.org/feed-textconstruct;1"].
                 createInstance(Ci.nsIFeedTextConstruct);
       var notFound = strings.get("messageNotFound", [id]);
       content.text = "<p><strong>" + notFound + "</strong></p>";
+
       content.type = "html";
       content.base = null;
       content.lang = null;
     }
-
     if (content) {
       var contentBody = document.getElementById("contentBody");
 
-      // Highlight search text, if any.
-      content.text = messageHeaderUtils.highlight("messages", content.text);
-
       if (!contentBody)
-        // If no contentBody element, we are going back in history to a new message
-        // but which contains a linked page and not message content; just return.
+        // If no contentBody element, we are going back in history to a new
+        // message but which contains a linked page and not message content;
+        // just return.
         return;
 
       if (content.type == "text") {
         SnowlUtils.linkifyText(content.text,
                                contentBody,
                                message.source.principal);
       }
       else {
         // content.type == "html" or "xhtml"
         if (content.base)
           document.body.setAttributeNS(XML_NS, "base", content.base.spec);
 
         var docFragment = content.createDocumentFragment(contentBody);
         if (docFragment)
           contentBody.appendChild(docFragment);
       }
+window.SnowlUtils._log.info("createBody: DONE");
+    var win = parent.document.getElementById("messageBody").contentWindow;
+    var frame = parent.document; //.document.getElementById("messageBody");
+    frame.load = messageHeaderUtils.highlight("messages", win);
+//    win.load = function () {
+//  window.setTimeout(function () { messageHeaderUtils.highlight("messages", win) }, 550);
+//}
+    
+//    messageHeaderUtils.highlight("messages", win);
+
+    // Highlight search text, if any.
+//    var window = parent.document.getElementById("messageBody").contentWindow;
+//    messageHeaderUtils.highlight("messages", window);
+
+//    if (this.hilightMode)
+//      messageHeaderUtils.higlightSetMarkers();
     }
   }
 
 };
 
 //****************************************************************************//
 // Utils for headers.
 
 var messageHeaderUtils = {
   ROWS_BRIEF: "30,*",
   origWidth: null,
   origHeight: null,
   noResize: false,
 
   init: function() {
+window.SnowlUtils._log.info("init: ");
     var pin = document.getElementById("pinButton");
     var headerBcaster = gBrowserWindow.document.
                                        getElementById("viewSnowlHeader");
     var wrap = gBrowserWindow.document.
                               getElementById("viewSnowlHeaderWrap");
     var body = document.getElementById("body");
     var headerDeck = document.getElementById("headerDeck");
     var noHeader = parent.document.
@@ -280,26 +312,33 @@ var messageHeaderUtils = {
 
     if (wrap.getAttribute("checked") == "true")
       body.classList.add("wrap");
     else
       body.classList.remove("wrap");
 
     // Fires after onresize done, store new width and height.
     window.addEventListener("MozScrolledAreaChanged",
-                            function () {
-                              messageHeaderUtils.origWidth = 
-                                  parent.document.body.
-                                         clientWidth;
-                              messageHeaderUtils.origHeight = 
-                                  parent.document.getElementById("messageHeader").
-                                         clientHeight; },
+                            function () { messageHeaderUtils.setDimensions() },
                             false);
   },
 
+  setDimensions: function() {
+    messageHeaderUtils.origWidth = parent.document.body.clientWidth;
+    messageHeaderUtils.origHeight =
+        parent.document.getElementById("messageHeader").clientHeight;
+  },
+
+  onUnload: function() {
+window.SnowlUtils._log.info("onUnload: ");
+    window.removeEventListener("MozScrolledAreaChanged",
+                               function () { messageHeaderUtils.setDimensions() },
+                               false);
+  },
+
   onMouseOver: function(aEvent) {
     var node = aEvent.target;
     var messageHeader = document.getElementById("messageHeader");
     var body = messageHeader.contentDocument.getElementById("body");
     var headerDeck = messageHeader.contentDocument.getElementById("headerDeck");
     var pin = messageHeader.contentDocument.getElementById("pinButton");
     if (node.id != "noHeader" || pin.hasAttribute("checked"))
       return;
@@ -410,35 +449,36 @@ var messageHeaderUtils = {
     var headerBcaster = gBrowserWindow.document.getElementById("viewSnowlHeader");
     var headerIndex = parseInt(headerBcaster.getAttribute("headerIndex"));
     var body = aBody ? aBody :
                        document.getElementById("body");
     var headerDeck = aHeaderDeck ? aHeaderDeck :
                                    document.getElementById("headerDeck");
 
     if (aType == "toggle") {
-      // Toggled to next in 3 way
+      // Toggle to next in 3 way
       headerIndex = ++headerIndex > 2 ? 0 : headerIndex++;
       headerBcaster.setAttribute("headerIndex", headerIndex);
     }
 
     var headerType = headerIndex == 0 ? "brief" :
                      headerIndex == 1 ? "basic" : "full";
     headerDeck.setAttribute("header", headerType);
     parent.document.body.setAttribute("header", headerType);
     parent.document.body.setAttribute("border", "6");
 
-    // The message is found in the scope of the parent frameset document.
-    var messageContent = parent.wrappedJSObject.messageContent;
-    if (headerIndex == 2 && !messageContent._headers)
-      messageContent.createFullHeader(headerDeck);
-
     // Set the size to persisted values or make sure toggling results in nice
     // headers wrapped content flow.
     this.setHeaderSize(body, headerDeck);
+
+    // The message is found in the scope of the parent frameset document.
+    var messageContent = parent.wrappedJSObject.messageContent;
+    messageContent.createHeader(headerDeck);
+    if (headerIndex == 2 && !messageContent._headers)
+      messageContent.createFullHeader(headerDeck);
   },
 
   onDeleteMessageButton: function() {
     // Delete button.
     var messageContent = parent.wrappedJSObject.messageContent;
     gBrowserWindow.SnowlMessageView.onDeleteMessage([messageContent.message])
   },
 
@@ -499,98 +539,262 @@ var messageHeaderUtils = {
         headerBcaster.setAttribute("rowsFull", parent.document.body.rows);
     }
   },
 
   tooltip: function(aEvent, aShow) {
     // Need to handle tooltips manually in xul-embedded-in-xhtml; tooltip
     // element cannot be in the xhtml document either.
     var tooltip = gBrowserWindow.document.getElementById("snowlXulInXhtmlTooltip");
+    var tooltipdelay = parseInt(aEvent.target.getAttribute("tooltipdelay"));
+    tooltipdelay = isNaN(tooltipdelay) ? 800 : tooltipdelay;
     if (aShow == 'show') {
       this.tiptimer = window.setTimeout(function() {
-                        tooltip.label = aEvent.target.tooltipText;
+                        tooltip.label = aEvent.target.getAttribute("tooltiptext");
                         tooltip.openPopup(aEvent.target,
                                           "after_start",
                                           0, 0, false, false);
-                      }, 800);
+                      }, tooltipdelay);
     }
     else if (aShow == 'hide') {
       window.clearTimeout(this.tiptimer);
       delete this.tiptimer;
       tooltip.label = "";
       tooltip.hidePopup();
       // Remove focus (for header button, arrow key binding issue).
       aEvent.target.blur();
     }
   },
 
-  // Highlight given phrase, skipping html tags & entities.
-  highlight: function(aWhat, aContent) {
-    var term, terms = [], highlightTerms = [], hlindex = 0;
+  // Highlight search terms.
+  highlight: function(aType, aWindow) {
+    var term, terms = [], regexTerms = [], highlightTerms = [],
+        selection, range, rangeCount, hlnode, hlindex = 0;
     var sidebarWin = gBrowserWindow.document.
                                     getElementById("sidebar").contentWindow;
     var collectionsView = sidebarWin.CollectionsView;
 
-    if (!collectionsView)
-      return aContent;
+    if (!collectionsView || !collectionsView.Filters["searchterms"])
+      return;
 
-    var searchWhat = collectionsView._searchFilter.
+    var searchType = collectionsView._searchFilter.
                                      getAttribute("searchtype");
     var searchTerms = collectionsView.Filters["searchterms"];
+    var searchMsgsFTS = searchType == "messagesFTS";
     var searchIgnoreCase = collectionsView._searchFilter.
                                            getAttribute("ignorecase") == "true" ?
                                            true : false;
-    var flags = "g" + (searchIgnoreCase ? "i" : "");
+//    var flags = "g" + (searchIgnoreCase ? "i" : "");
+
+    var findbar = gBrowserWindow.document.getElementById("FindToolbar");
+    var controller = findbar._getSelectionController(aWindow);
+
+    if ((aType == "headers" && searchType != "headers") ||
+        (aType == "messages" &&
+          (searchType != "messages" && searchType != "messagesFTS")) ||
+        (aType == "basicheaders" &&
+          (searchType == "messages" || searchType == "messagesFTS")))
+      return;
+
+window.SnowlUtils._log.info("highlight: aType:searchType:searchTerms - "+
+  aType+" : "+searchType+" : "+searchTerms);
 
-    if (!searchTerms || (searchWhat != aWhat && searchWhat != "msghdr"))
-      return aContent;
-
-//window.SnowlUtils._log.info("highlight: searchWhat - "+searchWhat);
+    if (searchMsgsFTS) {
+      // SQLite FTS based search results.
+      terms = searchTerms.match("[^\\s\"']+|\"[^\"]*\"|'[^']*'", "g");
+//window.SnowlUtils._log.info("highlight: SearchMsgsFTS terms - "+terms);
+      while (terms && (term = terms.shift())) {
+        // Remove negation term -, OR term, NEAR[/n] term, quotes ", and
+        // last wildcard *.
+        term = term.replace(/^-.*|^OR|^NEAR($|\/{1}[1-9]{1}$)|\"|\*\"$|\*$/g, "");
+        // Replace all non word symbols with . since sqlite does not match
+        // symbols exactly, ie for term of "one-off", "one---off", "one++off"
+        // sqlite returns a match for "one off"; for "one off" sqlite returns
+        // "one-off" etc. etc. and we need to highlight these.
+        // XXX: term that sqlite does not match (exact quoted term) will be
+        // hightlighted if it's nevertheless in a valid result page.
+////        term = term.replace(/[^\w\u0080-\uFFFFF]+/g, ".");
+        // Make lower case for highlight array.
+        // XXX: unicode? Bug 394604.  Result is that while sqlite may match
+        // the record, unless the user input is exactly what is on the page,
+        // it won't show.
+        term = term.toLowerCase();
+  
+        if (term)
+          // Add term to array, term's index creates hilight classname.
+          highlightTerms.push(term);
+      }
 
-    // Both sqlite fts and regex terms are stored in Filters["searchterms"]
-    // the same way.  So highlight processing is the same for both, even
-    // though the actual match retrieval query strings are different.
-    terms = searchTerms.match("[^\\s\"']+|\"[^\"]*\"|'[^']*'", "g");
-    while (terms && (term = terms.shift())) {
-      // Remove negation term, OR term, NEAR[/n] term, quotes ", last wildcard *.
-      term = term.replace(/^-.*|^OR|^NEAR($|\/{1}[1-9]{1}$)|\"|\*\"$|\*$/g, "");
-      // Replace all non word symbols with . since sqlite does not match
-      // symbols exactly, ie for term of "one-off", "one---off", "one++off"
-      // sqlite returns a match for "one off"; for "one off" sqlite returns
-      // "one-off" etc. etc. and we need to highlight these.
-      // XXX: term that sqlite does not match (exact quoted term) will be
-      // hightlighted if it's nevertheless in a valid result page.
-      term = term.replace(/[^\w\u0080-\uFFFFF]+/g, ".");
-      // Make lower case for highlight array.
-      // XXX: unicode? Bug 394604.  Result is that while sqlite may match
-      // the record, unless the user input is exactly what is on the page,
-      // it won't show.
-      term = term.toLowerCase();
+      regexTerms = highlightTerms;
+    }
+    else {
+      // Regex based search results.
+      searchTerms.forEach(function(term) {
+        if (term.charAt(0) == "-" || term.match(/^OR$/))
+          // Don't include negation term or the OR term.
+          return;
+  
+        term = searchIgnoreCase ? term.toLowerCase() : term;
+  
+        // Remove leading quote "; remove last quote " making sure to retain
+        // escaped quote; convert some special html escaped char entities.  Use
+        // this array to exactly match content with term and get its index for
+        // the hl[index] class.
+        highlightTerms.push(term.replace(/^"/, "").
+                                 replace(/([^\\])"/g, "$1").
+                                 replace(/\\"/g, '"').
+                                 replace(/&/g, "&amp;").
+                                 replace(/</g, "&lt;").
+                                 replace(/>/g, "&gt;"));
+                                 
+        // Extra special handling to escape \ unless it escapes a "; escape
+        // special chars; remove leading "; remove last quote " making sure to
+        // retain escaped quote (negation already passed by).  Use this
+        // array to match html entities and other escaped chars.
+        regexTerms.push(term.replace(/(\\)(?=[^"]|$)/g, "\\$1").
+                             replace(/([.^$*+?\-()[{}|\]])/g, "\\$1").
+                             replace(/^"/, "").
+                             replace(/([^\\])"/g, "$1").
+                             replace(/&/g, "&amp;").
+                             replace(/</g, "&lt;").
+                             replace(/>/g, "&gt;"));
+      })
+    }
+
+    // No terms (negation terms removed).
+    if (highlightTerms.length == 0)
+      return;
+
+    messageContent.hilightMode = true;
+//window.SnowlUtils._log.info("highlight: regexTerms - "+regexTerms);
+
+    for each (regexTerm in regexTerms) {
+      // Use findbar based selection highlight to get the elements, wrap them
+      // in a <span> element for multiselection highlighting.  Highlighting
+      // full or partial text broken up by or within other html tags
+      // (<a>, <b>, etc.) is possible with this method.
+//window.SnowlUtils._log.info("highlight: process regexTerm - "+regexTerm);
+      hlindex = highlightTerms.indexOf(searchIgnoreCase ? regexTerm.toLowerCase() :
+                                                          regexTerm);
+      findbar._highlightDoc(true, regexTerm, aWindow);
+      selection = controller.getSelection(findbar._findSelection);
+      rangeCount = controller.getSelection(findbar._findSelection).rangeCount;
+
+      // Must go backwards to process ranges in a node, to preserve offsets..
+      for (var i = rangeCount; i > 0; i--) {
+//window.SnowlUtils._log.info("highlight: sel:i - "+sel+" : "+i);
+        range = selection.getRangeAt(i-1);
+//window.SnowlUtils._log.info("highlight: range:startOffset:endOffset - "+
+//  range+" : "+range.startOffset+" : "+range.endOffset+" : "+x);
+
+        hlnode = document.createElementNS(HTML_NS, "span");
+        hlnode.setAttribute("class", "hlterms hl hl" + hlindex);
+        hlnode.setAttribute("i", i);
 
-      if (term)
-        // Add term to array, term's index creates hilight classname.
-        highlightTerms.push(term);
+//        range.surroundContents(hlnode);
+        hlnode.appendChild(range.extractContents());
+        range.insertNode(hlnode);
+        selection.removeRange(range);
+        delete hlnode;
+      }
+
+      selection.removeAllRanges();
     }
-    
-    // Create | delimited string of strings and words for highligher.
-    searchTerms = highlightTerms.join("|");
+
+    if (aType == "messages")
+      window.setTimeout(function () {
+                          messageHeaderUtils.highlightSetMarkers();
+                        }, 0);
+//      this.highlightSetMarkers();
+  },
+
+  /*
+   * The highlight marker code is adapted from the Search Marker extension by
+   * Matt Kenison<matt@penguinus.com>
+   * http://www.penguinus.com/dev/searchmarker/
+   */
+  highlightSetMarkers : function() {
+window.SnowlUtils._log.info("highlightSetMarkers: START");
+//    if (!this.hilightMode)
+//      return;
+//window.SnowlUtils._log.info("highlightSetMarkers:");
 
-//window.SnowlUtils._log.info("highlight: searchTerms - "+searchTerms);
-    var headerLabel = false;
+    var messageBody = parent.document.getElementById("messageBody");
+    var body = messageBody.contentDocument.body;
+    var contentBody = messageBody.contentDocument.getElementById("contentBody");
+
+    // Calculate space taken by scrollbar buttons, markers should not be above
+    // or below the scroll track (if scrollbar buttons are visible).  Can't get
+    // real height of button, so get scrollbar width and assume they're square.
+    // XXX: these hardcoded values worked best, figure out better calcs to
+    // match marker with scrollbar.
+    var width = messageBody.clientWidth - body.clientWidth;
+    var scrollBarOffsetTop = width ? 28 : width;
+    var scrollBarOffsetBot = width ? 48 : width;
+
+//    if (!width)
+      // No need for marker unless there's a scrollbar.
+//      return;
+
+    // Create element to contain the markers.
+    var markerDiv = document.createElementNS(HTML_NS, "div");
+    markerDiv.setAttribute("id", "hlMarkerContainer");
+    body.appendChild(markerDiv);
+//window.SnowlUtils._log.info("highlightSetMarkers: markerDiv");
 
-    var regexp = new RegExp("(<[\\s\\S]*?>|&.*?;)|(" + searchTerms + ")", flags);
-    return aContent.replace(regexp, function($0, $1, $2) {
-//window.SnowlUtils._log.info("highlight: regex $0:$1:$2 - "+$0+" : "+$1+" : "+$2);
-      if ($1)
-        // Matched a tag, remember if it's a headerLabel and don't highlight
-        // any perchance match of a header name.
-        headerLabel = $1.match(/class="headerLabel/g) ? true : false;
-      if ($2) {
-        var hlterm = $2;
-        hlindex = highlightTerms.indexOf(hlterm.replace(/[^\w\u0080-\uFFFFF]+/g, ".").
-                                                toLowerCase());
-      }
-      return $1 || (headerLabel ?
-          $2 : '<span class="hldefault hl' + hlindex +'">' + $2 + "</span>");
-    });
+    // Find all hilighted <span> entries.
+    var searchResults = body.getElementsByClassName("hlterms");
+    for (var i = 0; i < searchResults.length; ++i) {
+      var searchResult = searchResults[i];
+//window.SnowlUtils._log.info("highlightSetMarkers: searchResult - "+searchResult);
+
+      // Get the absolute y location of the term as a percentage and calculate
+      // position based on visible area height, adjusted for scrollbarbuttons.
+      var absoluteTop = this.hilightGetAbsoluteTop(searchResult);
+      var markerTop = absoluteTop / body.parentNode.scrollHeight * 100;
+      markerTop += scrollBarOffsetTop / body.parentNode.clientHeight * 100;
+      markerTop -= markerTop * (scrollBarOffsetBot / body.parentNode.clientHeight);
+
+      // Add marker to container
+      var marker = document.createElementNS(HTML_NS, "div");
+      marker.setAttribute("class", "hlMarker " +
+                                   searchResult.className.replace("hlterms", ""));
+      marker.setAttribute("style", "top:" + markerTop + "% !important");
+      marker.setAttribute("tooltiptext", searchResult.textContent);
+      marker.setAttribute("tooltipdelay", "0");
+      marker.setAttribute("onclick", "window.scrollTo(0," + absoluteTop + ")");
+      marker.setAttribute("onmouseover", "messageHeaderUtils.tooltip(event, 'show');");
+      marker.setAttribute("onmousedown", "messageHeaderUtils.tooltip(event, 'hide');");
+      marker.setAttribute("onmouseout", "messageHeaderUtils.tooltip(event, 'hide');");
+      markerDiv.appendChild(marker);
+    }
+  },
+
+  // Recursively add the relative top of the parent element.
+  hilightGetAbsoluteTop : function(element) {
+    return element == null ? 0 :
+        this.hilightGetAbsoluteTop(element.offsetParent) + element.offsetTop;
+  },
+
+  higlightClear : function() {
+    var messageBody = document.getElementById("messageBody");
+    var doc = messageBody.contentDocument;
+    var body = messageBody.contentDocument.getElementById("body");
+    var hlspans = doc.getElementsByClassName("hlterms");
+    var hlspan;
+
+    // Remove our <span> nodes, moving their children to the span's location in
+    // the DOM.  DOM nodes collection is dynamic, so have to go backwards..
+    // Note: this method leaves DOM text nodes fragmented if <span> overlaps
+    // with other tags, but there is no visual issue and regex would be worse.
+    // It is also possible to simply reload the page, but the result is stutter.
+    for (var i = hlspans.length; i > 0 ; i--) {
+      hlspan = hlspans[i-1];
+      while (hlspan.hasChildNodes())
+        hlspan.parentNode.insertBefore(hlspan.firstChild, hlspan )
+      hlspan.parentNode.removeChild(hlspan);
+    }
+
+    if (doc.getElementById("hlMarkerContainer"))
+      body.removeChild(doc.getElementById("hlMarkerContainer"));
   }
 
 };
--- a/content/snowlSearch.xml
+++ b/content/snowlSearch.xml
@@ -144,31 +144,76 @@
     <content>
       <xul:toolbarbutton id="snowlListViewSearchButton"
                          class="tabbable"
                          chromedir="&locale.dir;"
                          context=""
                          xbl:inherits="disabled"
                          oncommand="document.getBindingParent(this).openSearchPopup(event);">
         <xul:menupopup id="message-search-popup">
+          <xul:menuitem id="searchSubjectMenuitem"
+                        label="&searchSubject.label;"
+                        accesskey="&searchSubject.accesskey;"
+                        type="radio"
+                        name="searchPopup"
+                        value="subject"
+                        oncommand="document.getBindingParent(this).itemChecked(event);"/>
+          <xul:menuitem id="searchSenderMenuitem"
+                        label="&searchSender.label;"
+                        accesskey="&searchSender.accesskey;"
+                        type="radio"
+                        name="searchPopup"
+                        value="sender"
+                        oncommand="document.getBindingParent(this).itemChecked(event);"/>
+          <xul:menuitem id="searchRecipientMenuitem"
+                        label="&searchRecipient.label;"
+                        accesskey="&searchRecipient.accesskey;"
+                        type="radio"
+                        disabled="true"
+                        name="searchPopup"
+                        value="recipient"
+                        oncommand="document.getBindingParent(this).itemChecked(event);"/>
+          <xul:menuitem id="searchHeadersMenuitem"
+                        label="&searchHeaders.label;"
+                        accesskey="&searchHeaders.accesskey;"
+                        type="radio"
+                        name="searchPopup"
+                        value="headers"
+                        oncommand="document.getBindingParent(this).itemChecked(event);"/>
+          <xul:menuseparator id="searchMsgsSep"/>
           <xul:menuitem id="searchMessagesMenuitem"
                         label="&searchMessages.label;"
                         accesskey="&searchMessages.accesskey;"
                         type="radio"
                         name="searchPopup"
+                        value="messagesFTS"
+                        oncommand="document.getBindingParent(this).itemChecked(event);"/>
+          <xul:menuitem id="searchMessagesREMenuitem"
+                        label="&searchMessagesRE.label;"
+                        accesskey="&searchMessagesRE.accesskey;"
+                        type="radio"
+                        name="searchPopup"
                         value="messages"
                         oncommand="document.getBindingParent(this).itemChecked(event);"/>
+          <xul:menuseparator id="searchColasSep"/>
           <xul:menuitem id="searchCollectionsMenuitem"
                         label="&searchCollections.label;"
                         accesskey="&searchCollections.accesskey;"
                         type="radio"
                         name="searchPopup"
                         value="collections"
                         oncommand="document.getBindingParent(this).itemChecked(event);"/>
           <xul:menuseparator id="searchHelpSep"/>
+          <xul:menuitem id="searchIgnoreCaseMenuitem"
+                        label="&searchIgnoreCase.label;"
+                        accesskey="&searchIgnoreCase.accesskey;"
+                        type="checkbox"
+                        checked="true"
+                        oncommand="document.getBindingParent(this).ignoreCaseChecked(event);"/>
+          <xul:menuseparator id="searchHelpSep"/>
           <xul:menuitem id="searchHelpMenuitem"
                         label="&searchHelp.label;"
                         accesskey="&searchHelp.accesskey;"
                         oncommand="CollectionsView.onSearchHelp();"/>
         </xul:menupopup>
         <xul:stack flex="1">
           <xul:hbox align="center">
             <xul:image class="textbox-search-icon" xbl:inherits="src"/>
@@ -185,22 +230,24 @@
                  chromedir="&locale.dir;"/>
       <!-- Can't exclude/override nodes via xbl, so leave these beyond the deck's index -->
       <children/>
     </content>
 
     <implementation>
       <constructor><![CDATA[
         // Set checkstate of menupopup items.
-        if (this.searchType == "messages")
-          document.getElementById("searchMessagesMenuitem").
-                   setAttribute('checked', true);
-        else
-          document.getElementById("searchCollectionsMenuitem").
-                   setAttribute('checked', true);
+        document.getElementById("message-search-popup").
+                 getElementsByAttribute("value", this.searchType)[0].
+                 setAttribute('checked', true);
+        // Set ignorecase menuitem.
+        document.getElementById("searchIgnoreCaseMenuitem").
+                 setAttribute('checked', this.ignoreCase);
+        CollectionsView.onSearchIgnoreCase(this.ignoreCase);
+          
 
         // Need to create string bundle manually instead of using <xul:stringbundle/>
         // see bug 63370 for details (per other workarounds).
         var bundleURL = "chrome://snowl/locale/search.properties";
         var appLocale = Cc["@mozilla.org/intl/nslocaleservice;1"].
                         getService(Ci.nsILocaleService).
                         getApplicationLocale();
         this._strings = Cc["@mozilla.org/intl/stringbundle;1"].
@@ -210,53 +257,85 @@
         this.setEmptyText();
       ]]></constructor>
 
       <field name="searchFilter">document.getElementById("snowlFilter");</field>
 
       <field name="_strings">null</field>
 
       <field name="_searchFilterEmptytext" readonly="true"><![CDATA[({
-        0: this._strings.GetStringFromName("searchMessagesEmptyText"),
-        1: this._strings.GetStringFromName("searchCollectionsEmptyText")
+        "subject"     : this._strings.GetStringFromName("searchSubjectEmptyText"),
+        "sender"      : this._strings.GetStringFromName("searchSenderEmptyText"),
+        "recipient"   : this._strings.GetStringFromName("searchRecipientEmptyText"),
+        "headers"     : this._strings.GetStringFromName("searchHeadersEmptyText"),
+        "messagesFTS" : this._strings.GetStringFromName("searchMessagesFTSEmptyText"),
+        "messages"    : this._strings.GetStringFromName("searchMessagesEmptyText"),
+        "collections" : this._strings.GetStringFromName("searchCollectionsEmptyText")
       })]]></field>
 
       <property name="searchType"
                 onget="let st = this.searchFilter.getAttribute('searchtype');
                        return st;"
                 onset="this.searchFilter.setAttribute('searchtype', val);
                        document.persist('searchFilter', 'searchtype');
                        return val;">
       </property>
 
+      <property name="ignoreCase"
+                onget="let ic = this.searchFilter.getAttribute('ignorecase') == 'true' ?
+                                true : false;
+                       return ic;"
+                onset="this.searchFilter.setAttribute('ignorecase', val);
+                       document.persist('searchFilter', 'ignorecase');
+                       return val;">
+      </property>
+
       <method name="itemChecked">
         <parameter name="aEvent"/>
         <body><![CDATA[
           this.searchType = aEvent.target.value;
           this.setEmptyText();
         ]]></body>
       </method>
 
+      <method name="ignoreCaseChecked">
+        <parameter name="aEvent"/>
+        <body><![CDATA[
+          this.ignoreCase = aEvent.target.hasAttribute('checked');
+          CollectionsView.onSearchIgnoreCase(this.ignoreCase);
+        ]]></body>
+      </method>
+
       <method name="openSearchPopup">
         <parameter name="aEvent"/>
         <body><![CDATA[
           aEvent.stopPropagation();
           let mpopup = document.getElementById("message-search-popup");
           mpopup.openPopup(aEvent.target, "after_start");
         ]]></body>
       </method>
 
       <method name="setEmptyText">
         <body><![CDATA[
-          let index = this.searchType == "messages" ? 0 : 1;
-          this.searchFilter.setAttribute("emptytext",
-                                         this._searchFilterEmptytext[index]);
+          this.searchFilter.
+               setAttribute("emptytext",
+                            this._searchFilterEmptytext[this.searchType]);
           let input = document.getAnonymousElementByAttribute(this.searchFilter,
                                                               "anonid",
                                                               "input");
           input.focus();
           input.blur();
         ]]></body>
       </method>
+
+      <method name="setBusy">
+        <parameter name="aBusy"/>
+        <body><![CDATA[
+          if (aBusy)
+            this.searchFilter.setAttribute("busy", true);
+          else
+            this.searchFilter.removeAttribute("busy");
+        ]]></body>
+      </method>
     </implementation>
   </binding>
 
 </bindings>
--- a/content/toolbarbutton.css
+++ b/content/toolbarbutton.css
@@ -58,16 +58,24 @@
 
 #snowlListViewSearchButton {
   margin: 0px;
   border: 0px;
   padding: 0px;
   cursor: default;
 }
 
+#snowlFilter[busy] .textbox-search-clear {
+  list-style-image: url("chrome://global/skin/icons/loading_16.png");
+}
+
+#snowlFilter[busy] .textbox-search-clear:hover {
+  list-style-image: url("chrome://global/skin/icons/Search-close.png");
+}
+
 #snowlSubscribeButton {
   list-style-image: url("chrome://snowl/content/icons/add.png");
 }
 
 #snowlRefreshButton {
   list-style-image: url("chrome://snowl/content/icons/arrow_refresh_small.png");
 }
 
--- a/modules/collection.js
+++ b/modules/collection.js
@@ -199,29 +199,60 @@ SnowlCollection.prototype = {
       "identities.id AS identities_id",
       "identities.sourceID AS identities_sourceID",
       "identities.externalID AS identities_externalID",
       "identities.personID AS identities_personID",
       "people.id AS people_id",
       "people.name AS people_name",
       "people.placeID AS people_placeID",
       "people.homeURL AS people_homeURL",
-      "people.iconURL AS people_iconURL"
+      "people.iconURL AS people_iconURL" /*,
+      "content.id AS content_id",
+      "content.content AS content_content",
+      "content.mediaType AS content_mediaType",
+      "content.baseURI AS content_baseURI",
+      "content.languageTag AS content_languageTag",
+      "summary.id AS summary_id",
+      "summary.content AS summary_content",
+      "summary.mediaType AS summary_mediaType",
+      "summary.baseURI AS summary_baseURI",
+      "summary.languageTag AS summary_languageTag",
+      "partsText.content AS partsText_content"
+*/
     ];
 
     if (this.groupIDColumn) {
       columns.push(this.groupIDColumn + " AS groupID");
       columns.push(this.groupNameColumn + " AS groupName");
     }
 
     let query = 
       "SELECT " + columns.join(", ") + " FROM sources " +
       "JOIN messages ON sources.id = messages.sourceID " +
       "LEFT JOIN identities ON messages.authorID = identities.id " +
-      "LEFT JOIN people ON identities.personID = people.id ";
+      "LEFT JOIN people ON identities.personID = people.id " +
+
+      // The partType conditions for the next two LEFT JOINS have to be
+      // in the join constraints because if they were in the WHERE clause
+      // they would exclude messages without parts, whereas we want
+      // to retrieve messages whether or not they have these parts.
+/*
+      "LEFT JOIN parts AS content ON messages.id = content.messageID " +
+      "AND content.partType = " + PART_TYPE_CONTENT + " " +
+
+      "LEFT JOIN parts AS summary ON messages.id = summary.messageID " +
+      "AND summary.partType = " + PART_TYPE_SUMMARY + " " +
+
+      "LEFT JOIN partsText ON content.id = partsText.rowid " +
+*/
+      "";
+
+//    if ()
+//      query += 
+//      "LEFT JOIN partsText AS partsText ON parts.id = partsText.docid ";
 
     let conditions = [], operator;
 
     for each (let condition in this.constraints) {
       operator = condition.operator ? condition.operator : "AND";
       if (conditions.length == 0)
         conditions.push(" WHERE (");
       else
--- a/modules/datastore.js
+++ b/modules/datastore.js
@@ -445,31 +445,115 @@ let SnowlDatastore = {
   /**
    * Define regexp test function for sqlite.  This method creates a global
    * regexp object to be reused by sqlite.  Getter names are used as tokens
    * in the sqlite statement and any |test| expression can be added.
    * XXX:  Using executeAsync will cause Fx to freeze after a few runs of a
    * statement with a regexp function.
    *
    * Usage:
-   * (0) = Name of getter returning regex object to test.
+   * (0) = Name of string passed from the sqlite query, for a tokenized regexp.
+   *       For a dynamic regex, the string is first set by calling regExp(val),
+   *       then executing the sqlite query with the dynamic function token.
    * (1) = Column value to test.
-   * Statement eg, "WHERE attributes REGEXP_TEST 'FLAGGED_TRUE'"
+   * Statement eg, "WHERE attributes REGEXP 'FLAGGED_TRUE'"
    *
    * @return {Boolean} indicates if regex test successful; record returned if true.
    */
   _dbRegexp: {
-     get FLAGGED_TRUE() {
-       if (!this._FLAGGED_TRUE)
-         this._FLAGGED_TRUE = new RegExp('"flagged":true');
-       return this._FLAGGED_TRUE;
-     },
+    // Valid tokens for the sql regexp expression for test.
+    get ValidTokens() {
+      if (!this._ValidTokens)
+        this._ValidTokens = {
+          "FLAGGED_TRUE" : /"flagged":true/g
+        };
+      return this._ValidTokens;
+    },
+
+    // Valid parms for the sql regexp expression and preprocessing re for match.
+    get ValidParms() {
+      if (!this._ValidParms)
+        this._ValidParms = {
+          "SUBJECT"  : null,
+          "SENDER"   : null,
+          "MESSAGES" : null,
+          // Remove header names, of format |"name":| - only search values.
+          "HEADERS"  : /[{,]\"[^\"]*\":/g
+        };
+      return this._ValidParms;
+    },
+
+    get termsArray() {
+      return this._termsArray;
+    },
+
+    set termsArray(newVal) {
+      this._termsArray = newVal;
+    },
+
+    get ignoreCase() {
+      return this._ignoreCase;
+    },
+
+    set ignoreCase(newVal) {
+      this._ignoreCase = newVal ? true : false;
+    },
+
+    get headerToSearchRE() {
+      return this._headerToSearchRE;
+    },
+
+    set headerToSearchRE(newStr) {
+      this._headerToSearchRE = newStr;
+    },
+
+    // Dynamic regexp for headers using the search string placed in termsArray.
+    RegexMatch: function(aStr, aParm) {
+//SnowlDatastore._log.info("RegexMatch: START:aStr - "); //+" : "+str);
+      let str;
+      let match = true;
+      let matchOr = false;
+      let matchNot = false;
+      let flags = "g" + (this.ignoreCase ? "i" : "");
+
+      str = this.ValidParms[aParm] ? aStr.replace(this.ValidParms[aParm], "") :
+                                     aStr;
+      str = str == null ? "" : str;
+//SnowlDatastore._log.info("RegexMatch: str - " +str);
+
+      for each(term in this.termsArray) {
+//SnowlDatastore._log.info("RegexMatch: term:match:matchOr:matchNot - " +
+//    term+" : "+match+" : " +matchOr+" : " +matchNot);
+        if (/^OR?/.test(term)) {
+          matchOr = match || matchOr;
+          match = true;
+          continue;
+        }
+
+        if (term[0] == "-") {
+          if (str.match(term.slice(1), flags))
+            return false;
+          else
+            continue;
+        }
+
+        match = (str.match(term, flags) && match ? true : false);
+//SnowlDatastore._log.info("RegexMatch: match - "+match);
+      }
+
+      return match || matchOr;
+    },
 
     onFunctionCall: function(aArgs) {
-      return this[aArgs.getString(0)].test(aArgs.getString(1)) ? true : false;
+      if (aArgs.getString(0) in this.ValidParms)
+        return this.RegexMatch(aArgs.getString(1), aArgs.getString(0));
+      else if (aArgs.getString(0) in this.ValidTokens)
+        return aArgs.getString(1).match(this.ValidTokens[aArgs.getString(0)]) ? true : false;
+        // For some reason, test does not return all matches..
+//        return (this.ValidTokens[aArgs.getString(0)]).test(aArgs.getString(1)) ? true : false;
     }
   },
 
   /**
    * Migrate the database schema from one version to another.  Calls out to
    * version pair specific migrator functions below.  Handles migrations from
    * all older to newer versions of Snowl per this Snowl to DB version map:
    *   0.1      : 4
@@ -1048,19 +1132,23 @@ let SnowlDatastore = {
         break;
     }
 
     return this.createStatement(query);
   },
 
   collectionStatsByCollectionID: function() {
     // Stats object for collections tree properties.
-    let statement, type, types = ["all", "source", "author"];
+    let statement, type, types = ["all", "source"];
     let collectionID, Total, Read, New, collectionStats = {};
 
+    if (SnowlPlaces.collectionsAuthorsID != -1)
+      // Authors collection being built, also calc author stats.
+      types.push("author");
+
     try {
       for each (type in types) {
         statement = this._collectionStatsStatement(type);
         while (statement.step()) {
           if (statement.row["collectionID"] == null)
             continue;
 
           collectionID = type == "all" ?