fix threading with quick search, r=standard8, sr=neil, and augment the view unit test to test quick search threading
authorDavid Bienvenu <bienvenu@nventure.com>
Tue, 01 Sep 2009 08:04:08 -0700
changeset 3471 0297ec4fb4d4a3c81c6752bf61716f104845676e
parent 3470 9397894f6421c3a3b2670870a0e0a8fc106f2165
child 3472 2c185d4050c25030e358d0f39a479fc81a6c18ac
push idunknown
push userunknown
push dateunknown
reviewersstandard8, neil, and
fix threading with quick search, r=standard8, sr=neil, and augment the view unit test to test quick search threading
mailnews/base/src/nsMsgDBView.h
mailnews/base/src/nsMsgQuickSearchDBView.cpp
mailnews/base/src/nsMsgQuickSearchDBView.h
mailnews/base/test/unit/test_nsMsgDBView.js
--- a/mailnews/base/src/nsMsgDBView.h
+++ b/mailnews/base/src/nsMsgDBView.h
@@ -326,17 +326,20 @@ protected:
   virtual nsMsgViewIndex FindKey(nsMsgKey key, PRBool expand);
   virtual nsresult GetDBForViewIndex(nsMsgViewIndex index, nsIMsgDatabase **db);
   virtual nsCOMArray<nsIMsgFolder>* GetFolders();
   virtual nsresult GetFolderFromMsgURI(const char *aMsgURI, nsIMsgFolder **aFolder);
 
   virtual nsresult ListIdsInThread(nsIMsgThread *threadHdr, nsMsgViewIndex viewIndex, PRUint32 *pNumListed);
   nsresult ListUnreadIdsInThread(nsIMsgThread *threadHdr, nsMsgViewIndex startOfThreadViewIndex, PRUint32 *pNumListed);
   nsMsgViewIndex FindParentInThread(nsMsgKey parentKey, nsMsgViewIndex startOfThreadViewIndex);
-  nsresult ListIdsInThreadOrder(nsIMsgThread *threadHdr, nsMsgKey parentKey, PRInt32 level, nsMsgViewIndex *viewIndex, PRUint32 *pNumListed);
+  virtual nsresult ListIdsInThreadOrder(nsIMsgThread *threadHdr,
+                                        nsMsgKey parentKey, PRInt32 level,
+                                        nsMsgViewIndex *viewIndex,
+                                        PRUint32 *pNumListed);
   PRInt32  GetSize(void) {return(m_keys.Length());}
 
   // notification api's
   void  EnableChangeUpdates();
   void  DisableChangeUpdates();
   void  NoteChange(nsMsgViewIndex firstlineChanged, PRInt32 numChanged,
                     nsMsgViewNotificationCodeValue changeType);
   void  NoteStartChange(nsMsgViewIndex firstlineChanged, PRInt32 numChanged,
--- a/mailnews/base/src/nsMsgQuickSearchDBView.cpp
+++ b/mailnews/base/src/nsMsgQuickSearchDBView.cpp
@@ -520,19 +520,22 @@ nsresult nsMsgQuickSearchDBView::SortThr
           continue;
         displayRootHdr->GetMessageKey(&rootKey);
         displayRootHdr->GetFlags(&rootFlags);
         rootFlags |= MSG_VIEW_FLAG_ISTHREAD;
         m_keys.AppendElement(rootKey);
         m_flags.AppendElement(rootFlags);
         m_levels.AppendElement(0);
 
-        nsMsgViewIndex startOfThreadViewIndex = m_keys.Length() - 1;
-        PRUint32 numListed;
-        ListIdsInThread(threadHdr, startOfThreadViewIndex, &numListed);
+        nsMsgViewIndex startOfThreadViewIndex = m_keys.Length();
+        nsMsgViewIndex rootIndex = startOfThreadViewIndex - 1;
+        PRUint32 numListed = 0;
+        ListIdsInThreadOrder(threadHdr, rootKey, 1, &startOfThreadViewIndex, &numListed);
+        if (numListed > 0)
+          m_flags[rootIndex] = rootFlags | MSG_VIEW_FLAG_HASCHILDREN;
       }
     }
   }
   return NS_OK;
 }
 
 nsresult
 nsMsgQuickSearchDBView::ListCollapsedChildren(nsMsgViewIndex viewIndex,
@@ -569,16 +572,23 @@ nsMsgQuickSearchDBView::ListCollapsedChi
       }
     }
   }
   return NS_OK;
 }
 
 nsresult nsMsgQuickSearchDBView::ListIdsInThread(nsIMsgThread *threadHdr, nsMsgViewIndex startOfThreadViewIndex, PRUint32 *pNumListed)
 {
+
+  if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay && ! (m_viewFlags & nsMsgViewFlagsType::kGroupBySort))
+  {
+    nsMsgKey parentKey = m_keys[startOfThreadViewIndex++];
+    return ListIdsInThreadOrder(threadHdr, parentKey, 1, &startOfThreadViewIndex, pNumListed);
+  }
+
   PRUint32 numChildren;
   threadHdr->GetNumChildren(&numChildren);
   PRUint32 i;
   PRUint32 viewIndex = startOfThreadViewIndex + 1;
   nsCOMPtr<nsIMsgDBHdr> rootHdr;
   nsMsgKey rootKey;
   PRUint32 rootFlags = m_flags[startOfThreadViewIndex];
   *pNumListed = 0;
@@ -615,16 +625,88 @@ nsresult nsMsgQuickSearchDBView::ListIds
       {
         rootKeySkipped = PR_TRUE;
       }
     }
   }
   return NS_OK;
 }
 
+nsresult
+nsMsgQuickSearchDBView::ListIdsInThreadOrder(nsIMsgThread *threadHdr,
+                                             nsMsgKey parentKey, PRInt32 level,
+                                             nsMsgKey keyToSkip,
+                                             nsMsgViewIndex *viewIndex,
+                                             PRUint32 *pNumListed)
+{
+  nsCOMPtr <nsISimpleEnumerator> msgEnumerator;
+  nsresult rv = threadHdr->EnumerateMessages(parentKey, getter_AddRefs(msgEnumerator));
+  NS_ENSURE_SUCCESS(rv, rv);
+  PRBool hasMore;
+  nsCOMPtr <nsISupports> supports;
+  nsCOMPtr <nsIMsgDBHdr> msgHdr;
+  while (NS_SUCCEEDED(rv) && NS_SUCCEEDED(rv = msgEnumerator->HasMoreElements(&hasMore)) && hasMore)
+  {
+    rv = msgEnumerator->GetNext(getter_AddRefs(supports));
+    if (NS_SUCCEEDED(rv) && supports)
+    {
+      msgHdr = do_QueryInterface(supports);
+      nsMsgKey msgKey;
+      msgHdr->GetMessageKey(&msgKey);
+      if (msgKey == keyToSkip)
+        continue;
+
+      PRInt32 childLevel = level;
+      if (m_origKeys.BinaryIndexOf(msgKey) != -1)
+      {
+        PRUint32 msgFlags;
+        msgHdr->GetFlags(&msgFlags);
+        InsertMsgHdrAt(*viewIndex, msgHdr, msgKey, msgFlags & ~MSG_VIEW_FLAGS, level);
+        (*pNumListed)++;
+        (*viewIndex)++;
+        childLevel++;
+      }
+      rv = ListIdsInThreadOrder(threadHdr, msgKey, childLevel, keyToSkip, viewIndex, pNumListed);
+    }
+  }
+  return rv;
+}
+
+nsresult
+nsMsgQuickSearchDBView::ListIdsInThreadOrder(nsIMsgThread *threadHdr,
+                                             nsMsgKey parentKey, PRInt32 level,
+                                             nsMsgViewIndex *viewIndex,
+                                             PRUint32 *pNumListed)
+{
+  nsresult rv = ListIdsInThreadOrder(threadHdr, parentKey, level, nsMsgKey_None,
+                                     viewIndex, pNumListed);
+  // Because a quick search view might not have the actual thread root
+  // as its root, and thus might have a message that potentially has siblings
+  // as its root, and the enumerator will miss the siblings, we might need to
+  // make a pass looking for the siblings of the non-root root. We'll put
+  // those after the potential children of the root. So we will list the children
+  // of the faux root's parent, ignoring the faux root.
+  if (level == 1)
+  {
+    nsCOMPtr<nsIMsgDBHdr> root;
+    nsCOMPtr<nsIMsgDBHdr> rootParent;
+    nsMsgKey rootKey;
+    PRInt32 rootIndex;
+    threadHdr->GetRootHdr(&rootIndex, getter_AddRefs(rootParent));
+    if (rootParent)
+    {
+      rootParent->GetMessageKey(&rootKey);
+      if (rootKey != parentKey)
+        rv = ListIdsInThreadOrder(threadHdr, rootKey, level, parentKey, viewIndex,
+                                  pNumListed);
+    }
+  }
+  return rv;
+}
+
 nsresult nsMsgQuickSearchDBView::ExpansionDelta(nsMsgViewIndex index, PRInt32 *expansionDelta)
 {
   *expansionDelta = 0;
   if (index >= ((nsMsgViewIndex) m_keys.Length()))
     return NS_MSG_MESSAGE_NOT_FOUND;
 
   char flags = m_flags[index];
 
--- a/mailnews/base/src/nsMsgQuickSearchDBView.h
+++ b/mailnews/base/src/nsMsgQuickSearchDBView.h
@@ -84,15 +84,24 @@ protected:
   virtual nsresult OnNewHeader(nsIMsgDBHdr *newHdr, nsMsgKey aParentKey, PRBool ensureListed);
   virtual nsresult DeleteMessages(nsIMsgWindow *window, nsMsgViewIndex *indices, PRInt32 numIndices, PRBool deleteStorage);
   virtual nsresult SortThreads(nsMsgViewSortTypeValue sortType, nsMsgViewSortOrderValue sortOrder);
   virtual nsresult GetFirstMessageHdrToDisplayInThread(nsIMsgThread *threadHdr, nsIMsgDBHdr **result);
   virtual nsresult ExpansionDelta(nsMsgViewIndex index, PRInt32 *expansionDelta);
   virtual nsresult ListCollapsedChildren(nsMsgViewIndex viewIndex,
                                          nsIMutableArray *messageArray);
   virtual nsresult ListIdsInThread(nsIMsgThread *threadHdr, nsMsgViewIndex startOfThreadViewIndex, PRUint32 *pNumListed);
+  virtual nsresult ListIdsInThreadOrder(nsIMsgThread *threadHdr,
+                                        nsMsgKey parentKey, PRInt32 level,
+                                        nsMsgViewIndex *viewIndex,
+                                        PRUint32 *pNumListed);
+  virtual nsresult ListIdsInThreadOrder(nsIMsgThread *threadHdr,
+                                        nsMsgKey parentKey, PRInt32 level,
+                                        nsMsgKey keyToSkip,
+                                        nsMsgViewIndex *viewIndex,
+                                        PRUint32 *pNumListed);
   virtual nsresult GetMessageEnumerator(nsISimpleEnumerator **enumerator);
   void      SavePreSearchInfo();
   void      ClearPreSearchInfo();
 
 };
 
 #endif
--- a/mailnews/base/test/unit/test_nsMsgDBView.js
+++ b/mailnews/base/test/unit/test_nsMsgDBView.js
@@ -10,34 +10,51 @@
 // this will be migrated out of gloda soon...
 load("../../mailnews/resources/messageGenerator.js");
 load("../../mailnews/resources/asyncTestUtils.js");
 
 var gMessageGenerator;
 var gScenarioFactory;
 
 var gTestFolder;
+var gSiblingsMissingParentsSubject;
 
 function setup_globals(aNextFunc) {
   loadLocalMailAccount();
   gMessageGenerator = new MessageGenerator();
   gScenarioFactory = new MessageScenarioFactory(gMessageGenerator);
 
   // build up a diverse list of messages
   let messages = [];
   messages = messages.concat(gScenarioFactory.directReply(10));
   // the message generator uses a constanty incrementing counter, so we need to
   //  mix up the order of messages ourselves to ensure that the timestamp
   //  ordering is not already in order.  (a poor test of sorting otherwise.)
   messages = gScenarioFactory.directReply(6).concat(messages);
+
   messages = messages.concat(gScenarioFactory.fullPyramid(3,3));
-  messages = messages.concat(gScenarioFactory.siblingsMissingParent());
+  let siblingMessages = gScenarioFactory.siblingsMissingParent();
+  // cut off "Re: " part
+  gSiblingsMissingParentsSubject = siblingMessages[0].subject.slice(4);
+  dump("siblings subect = " + gSiblingsMissingParentsSubject + "\n");
+  messages = messages.concat(siblingMessages);
   messages = messages.concat(gScenarioFactory.missingIntermediary());
   messages.concat(gMessageGenerator.makeMessage({age: {days: 2, hours: 1}}));
-  
+
+  // build a hierarchy like this (the UID order corresponds to the date order)
+  //   1
+  //    2
+  //     4
+  //    3
+  let msg1 = gMessageGenerator.makeMessage();
+  let msg2 = gMessageGenerator.makeMessage({inReplyTo: msg1});
+  let msg3 = gMessageGenerator.makeMessage({inReplyTo: msg1});
+  let msg4 = gMessageGenerator.makeMessage({inReplyTo: msg2});
+  messages = messages.concat([msg1, msg2, msg3, msg4]);
+
   let mboxName = "dbviewy";
   writeMessagesToMbox(messages, gProfileDir,
                       "Mail", "Local Folders", mboxName);
   gTestFolder = gLocalIncomingServer.rootMsgFolder.addSubfolder(mboxName);
   updateFolderAndNotify(gTestFolder, async_driver);
   return false;
 }
 
@@ -241,17 +258,17 @@ function setup_view(aViewType, aViewFlag
   var outCount = {};
   gDBView.open(aViewType != "search" ? aTestFolder : null,
                SortType.byDate,
                aViewType != "search" ? SortOrder.ascending : SortOrder.descending,
                aViewFlags, outCount);
   dump("  View Out Count: " + outCount.value + "\n");
 
   // we need to cram messages into the search via nsIMsgSearchNotify interface
-  if (aViewType == "search") {
+  if (aViewType == "search" || aViewType == "quicksearch") {
     let searchNotify = gDBView.QueryInterface(
       Components.interfaces.nsIMsgSearchNotify);
     searchNotify.onNewSearch();
     let enumerator = aTestFolder.msgDatabase.EnumerateMessages();
     while (enumerator.hasMoreElements()) {
       let msgHdr = enumerator.getNext().QueryInterface(
         Components.interfaces.nsIMsgDBHdr);
       searchNotify.onSearchHit(msgHdr, msgHdr.folder);
@@ -459,16 +476,62 @@ function test_msg_added_to_search_view()
     let synMsg = make_and_add_message();
     let msgHdr = gTestFolder.msgDatabase.getMsgHdrForMessageID(synMsg.messageId);
     gDBView.QueryInterface(Components.interfaces.nsIMsgSearchNotify)
             .onSearchHit(msgHdr, msgHdr.folder);
     assert_view_message_at_indices(synMsg, 0);
   }
 }
 
+function IsHdrChildOf(possibleParent, possibleChild) {
+  let parentHdrId = possibleParent.messageId;
+  let numRefs = possibleChild.numReferences;
+  for (let refIndex = 0; refIndex < numRefs; refIndex++) {
+    if (parentHdrId == possibleChild.getStringReference(refIndex))
+      return true;
+  }
+  return false;
+}
+
+// This could be part of ensure_view_ordering() but I don't want to make that
+// function any harder to read.
+function test_threading_levels() {
+
+  if (!gTreeView.rowCount)
+    do_throw("There are no rows in my folder! I can't test anything!");
+  // only look at threaded, non-grouped views.
+  if ((gDBView.viewFlags & ViewFlags.kGroupBySort) || 
+      ! (gDBView.viewFlags & ViewFlags.kThreadedDisplay))
+    return;
+
+  let prevLevel = 1;
+  let prevMsgHdr;
+  for (let iViewIndex = 0; iViewIndex < gTreeView.rowCount; iViewIndex++) {
+    let msgHdr = gDBView.getMsgHdrAt(iViewIndex);
+    let level = gTreeView.getLevel(iViewIndex);
+    if (level > prevLevel && msgHdr.subject != gSiblingsMissingParentsSubject) {
+      if (!IsHdrChildOf(prevMsgHdr, msgHdr))
+        view_throw("indented message not child of parent");
+    }
+    prevLevel = level;
+    prevMsgHdr = msgHdr;
+  }
+}
+
+function test_qs_results() {
+  // This just tests that bug 505967 hasn't regressed.
+  if (gTreeView.getLevel(0) != 0)
+    view_throw("first message should be at level 0");
+  if (gTreeView.getLevel(1) != 1)
+    view_throw("second message should be at level 1");
+  if (gTreeView.getLevel(2) != 2)
+    view_throw("third message should be at level 2");
+  test_threading_levels();
+}
+
 function test_group_dummies_under_mutation_by_date() {
   // - start with an empty folder
   let save_gTestFolder = gTestFolder;
   gTestFolder = make_empty_folder();
 
   // - create the view
   setup_view("group", ViewFlags.kGroupBySort);
   gDBView.sort(SortType.byDate, SortOrder.ascending);
@@ -517,16 +580,17 @@ function test_group_dummies_under_mutati
   assert_view_index_is_dummy(0);
   assert_view_index_is_not_dummy(1);
   // now the dummy should be based off the remaining older one
   assert_view_message_at_indices(older, 0, 1);
 }
 
 var view_types = [
   ["threaded", ViewFlags.kThreadedDisplay],
+  ["quicksearch", ViewFlags.kThreadedDisplay],
   ["search", ViewFlags.kThreadedDisplay],
   ["search", ViewFlags.kGroupBySort],
    // group does unspeakable things to gTestFolder, so put it last.
   ["group", ViewFlags.kGroupBySort]
 ];
 
 var tests_for_all_views = [
   test_sort_columns
@@ -535,16 +599,19 @@ var tests_for_all_views = [
 var tests_for_specific_views = {
   group: [
     test_group_dummies_under_mutation_by_date
   ],
   threaded: [
   ],
   search: [
     test_msg_added_to_search_view
+  ],
+  quicksearch: [
+    test_qs_results
   ]
 };
 
 function run_test() {
   do_test_pending();
   async_run({func: actually_run_test});
 }