MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 91.8% 997 / 0 / 1086
Functions: 93.2% 82 / 0 / 88
Branches: 78.1% 1163 / 0 / 1489

presentation/QuickThreadListModel.cpp
Line Branch Exec Source
1 #include "QuickThreadListModel.h"
2
3 #include "MailPresentationModel.h"
4 #include <QLocale>
5 #include "WorkspaceState.h"
6 #include "ui/MailListModel.h"
7 #include <QElapsedTimer>
8 #include <QLoggingCategory>
9 #include <QPointer>
10 #include <QTimer>
11 #include <algorithm>
12 #include <memory>
13 #include <utility>
14
15
4/7
✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 8 taken 112 times.
✓ Branch 4 → 5 taken 4 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 4 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
116 Q_LOGGING_CATEGORY(lcThreadProjection, "mailjd.threadprojection")
16
17 823 QuickThreadListModel::QuickThreadListModel(MailThreadModel *source, QString accountId, QObject *parent)
18
1/1
✓ Branch 7 → 8 taken 823 times.
823 : QAbstractListModel(parent), m_source(source), m_accountId(std::move(accountId)) {
19
1/1
✓ Branch 31 → 32 taken 823 times.
823 m_indexPool.setMaxThreadCount(1);
20 // Page turns and revision catch-ups should reuse their SQLite handle and
21 // TEMP projection workspace while this view is alive.
22
1/1
✓ Branch 32 → 33 taken 823 times.
823 m_indexPool.setExpiryTimeout(-1);
23
1/1
✓ Branch 33 → 34 taken 823 times.
823 connect(source, &QAbstractItemModel::modelReset, this, &QuickThreadListModel::rebuild);
24
1/1
✓ Branch 35 → 36 taken 823 times.
823 connect(source, &QAbstractItemModel::layoutChanged, this, &QuickThreadListModel::rebuild);
25 // Structural tree changes are full rebuilds upstream (setHeaders/clear/
26 // removeByUid all reset), so a rebuild here is the established cost class.
27
1/1
✓ Branch 37 → 38 taken 823 times.
823 connect(source, &QAbstractItemModel::rowsInserted, this, &QuickThreadListModel::rebuild);
28
1/1
✓ Branch 39 → 40 taken 823 times.
823 connect(source, &QAbstractItemModel::rowsRemoved, this, &QuickThreadListModel::rebuild);
29 // Flag/label updates arrive incrementally. The source also notifies every
30 // affected ancestor (unread propagation), so mapping each changed tree
31 // index through the key table covers this row and its visible ancestors.
32 823 connect(source, &QAbstractItemModel::dataChanged, this,
33
1/1
✓ Branch 41 → 42 taken 823 times.
823 [this](const QModelIndex &first, const QModelIndex &last) {
34
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 6 times.
6 if (m_indexed) return;
35
2/2
✓ Branch 28 → 5 taken 6 times.
✓ Branch 28 → 29 taken 6 times.
12 for (int treeRow = first.row(); treeRow <= last.row(); ++treeRow) {
36
2/2
✓ Branch 5 → 6 taken 6 times.
✓ Branch 6 → 7 taken 6 times.
6 const auto *header = m_source->headerAt(first.siblingAtRow(treeRow));
37
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 6 times.
6 if (!header) continue;
38 6 const auto it = m_rowByKey.constFind(MailKey{header->folderId, header->uid});
39
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 6 times.
6 if (it == m_rowByKey.constEnd()) continue;
40
3/3
✓ Branch 18 → 19 taken 6 times.
✓ Branch 21 → 22 taken 6 times.
✓ Branch 22 → 23 taken 6 times.
6 emit dataChanged(index(*it, 0), index(*it, 0), {});
41 }
42 });
43
1/1
✓ Branch 43 → 44 taken 823 times.
823 connect(source, &QObject::destroyed, this, [this] {
44 ✗ m_source = nullptr;
45 ✗ rebuild();
46 ✗ });
47
1/1
✓ Branch 45 → 46 taken 823 times.
823 rebuild();
48 823 }
49
50 823 QuickThreadListModel::~QuickThreadListModel() {
51 // Destruction really cancels: nothing can retain the result afterwards.
52
2/2
✓ Branch 3 → 4 taken 11 times.
✓ Branch 3 → 6 taken 812 times.
823 if (m_indexCancelled) m_indexCancelled->store(true);
53
1/2
✗ Branch 13 → 8 not taken.
✓ Branch 13 → 14 taken 823 times.
823 for (auto &token : m_activeRequests) token->store(true);
54 823 m_indexCancelled.reset();
55 823 m_indexPool.clear();
56 823 m_indexPool.waitForDone();
57 823 }
58
59 template <typename T>
60 68 void pruneCache(QHash<MailThreadModel::MailKey, T> &cache,
61 const QSet<MailThreadModel::MailKey> &present) {
62
9/9
void pruneCache<QList<QVariant> >(QHash<std::pair<long long, long long>, QList<QVariant> >&, QSet<std::pair<long long, long long> > const&):
✓ Branch 2 → 3 taken 17 times.
✓ Branch 13 → 4 taken 263 times.
✓ Branch 13 → 14 taken 17 times.
void pruneCache<QString>(QHash<std::pair<long long, long long>, QString>&, QSet<std::pair<long long, long long> > const&):
✓ Branch 2 → 3 taken 34 times.
✓ Branch 13 → 4 taken 664 times.
✓ Branch 13 → 14 taken 34 times.
void pruneCache<bool>(QHash<std::pair<long long, long long>, bool>&, QSet<std::pair<long long, long long> > const&):
✓ Branch 2 → 3 taken 17 times.
✓ Branch 13 → 4 taken 401 times.
✓ Branch 13 → 14 taken 17 times.
1396 for (auto it = cache.begin(); it != cache.end();) {
63
6/6
void pruneCache<QList<QVariant> >(QHash<std::pair<long long, long long>, QList<QVariant> >&, QSet<std::pair<long long, long long> > const&):
✓ Branch 6 → 7 taken 230 times.
✓ Branch 6 → 8 taken 33 times.
void pruneCache<QString>(QHash<std::pair<long long, long long>, QString>&, QSet<std::pair<long long, long long> > const&):
✓ Branch 6 → 7 taken 460 times.
✓ Branch 6 → 8 taken 204 times.
void pruneCache<bool>(QHash<std::pair<long long, long long>, bool>&, QSet<std::pair<long long, long long> > const&):
✓ Branch 6 → 7 taken 230 times.
✓ Branch 6 → 8 taken 171 times.
1328 if (present.contains(it.key())) ++it;
64
3/3
void pruneCache<QList<QVariant> >(QHash<std::pair<long long, long long>, QList<QVariant> >&, QSet<std::pair<long long, long long> > const&):
✓ Branch 9 → 10 taken 33 times.
void pruneCache<QString>(QHash<std::pair<long long, long long>, QString>&, QSet<std::pair<long long, long long> > const&):
✓ Branch 9 → 10 taken 204 times.
void pruneCache<bool>(QHash<std::pair<long long, long long>, bool>&, QSet<std::pair<long long, long long> > const&):
✓ Branch 9 → 10 taken 171 times.
408 else it = cache.erase(it);
65 }
66 68 }
67
68 // S106-306: one QML delegate serves both presentations; the role numbers
69 // are equal by construction.
70 static_assert(int(QuickThreadListModel::SizeText) == int(MailPresentationModel::SizeText));
71 static_assert(int(QuickThreadListModel::Period) == int(MailPresentationModel::Period));
72 static_assert(int(QuickThreadListModel::MatchedInText) == int(MailPresentationModel::MatchedInText));
73 static_assert(int(QuickThreadListModel::Answered) == int(MailPresentationModel::Answered));
74
75 317 QHash<int, QByteArray> QuickThreadListModel::roleNames() const {
76 ✗ return {{Identity, "mailIdentity"}, {Subject, "subject"}, {Sender, "sender"},
77 ✗ {Date, "dateText"}, {Unread, "unread"}, {Starred, "starred"},
78 ✗ {Labels, "labels"}, {Attachment, "hasAttachment"}, {Selected, "mailSelected"},
79 ✗ {Folder, "folderBadge"}, {Suggestion, "folderSuggestion"},
80 ✗ {Depth, "depth"}, {ThreadCount, "threadCount"},
81 ✗ {HasChildren, "threadHasChildren"}, {Expanded, "threadExpanded"},
82 ✗ {LabelChips, "labelChips"}, {OpenTask, "hasOpenTask"}, {RuleMatch, "ruleMatched"},
83
2/4
✓ Branch 26 → 27 taken 6974 times.
✓ Branch 26 → 28 taken 317 times.
✗ Branch 31 → 32 not taken.
✗ Branch 31 → 33 not taken.
7291 {Recipient, "recipient"}, {SizeText, "sizeText"}, {MatchedInText, "matchedInText"}, {Answered, "answered"}};
84 317 }
85
86 898595 int QuickThreadListModel::rowCount(const QModelIndex &parent) const {
87
2/2
✓ Branch 3 → 4 taken 328 times.
✓ Branch 3 → 5 taken 898267 times.
898595 if (parent.isValid()) return 0;
88
2/2
✓ Branch 5 → 6 taken 475 times.
✓ Branch 5 → 8 taken 897792 times.
898267 if (m_holding) return m_heldRows.size();
89
2/2
✓ Branch 8 → 9 taken 881233 times.
✓ Branch 8 → 11 taken 16559 times.
897792 return m_indexed ? m_indexRows.size() : m_rows.size();
90 }
91
92 91677 QVariant QuickThreadListModel::data(const QModelIndex &index, int role) const {
93 91677 const auto *header = headerAtRow(index.row());
94
2/2
✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 6 taken 91674 times.
91677 if (!header) return {};
95
23/23
✓ Branch 6 → 7 taken 4312 times.
✓ Branch 6 → 15 taken 4103 times.
✓ Branch 6 → 16 taken 4100 times.
✓ Branch 6 → 17 taken 4029 times.
✓ Branch 6 → 29 taken 4031 times.
✓ Branch 6 → 31 taken 4031 times.
✓ Branch 6 → 33 taken 4033 times.
✓ Branch 6 → 35 taken 3055 times.
✓ Branch 6 → 36 taken 4029 times.
✓ Branch 6 → 37 taken 4147 times.
✓ Branch 6 → 47 taken 4029 times.
✓ Branch 6 → 51 taken 4085 times.
✓ Branch 6 → 63 taken 4040 times.
✓ Branch 6 → 75 taken 4034 times.
✓ Branch 6 → 87 taken 4033 times.
✓ Branch 6 → 100 taken 5072 times.
✓ Branch 6 → 125 taken 4029 times.
✓ Branch 6 → 131 taken 4029 times.
✓ Branch 6 → 137 taken 4083 times.
✓ Branch 6 → 138 taken 4030 times.
✓ Branch 6 → 148 taken 4029 times.
✓ Branch 6 → 149 taken 4029 times.
✓ Branch 6 → 161 taken 2282 times.
91674 switch (role) {
96 4312 case Identity:
97
2/2
✓ Branch 7 → 8 taken 59 times.
✓ Branch 7 → 9 taken 4253 times.
4312 if (m_holding) return {};
98 4253 return WorkspaceState::encode({m_accountId, header->folderId, header->uid});
99 4103 case Subject: return header->subject;
100 4100 case Sender: return header->from;
101 4029 case Date: {
102 4029 const MailKey key{header->folderId, header->uid};
103 4029 const auto cached = m_dateTextCache.constFind(key);
104
2/2
✓ Branch 21 → 22 taken 3111 times.
✓ Branch 21 → 24 taken 918 times.
4029 if (cached != m_dateTextCache.constEnd()) return cached.value();
105
1/1
✓ Branch 24 → 25 taken 918 times.
918 const QString text = MailListModel::formatDate(header->date);
106
1/1
✓ Branch 25 → 26 taken 918 times.
918 m_dateTextCache.insert(key, text);
107 918 return text;
108 918 }
109 4031 case Unread: return !header->isSeen();
110 4031 case Starred: return header->isFlagged();
111 4033 case Answered: return header->isAnswered();
112 3055 case Labels: return header->labels;
113 4029 case Attachment: return header->hasAttachments;
114 4147 case Selected: {
115
4/5
✓ Branch 38 → 39 taken 4147 times.
✗ Branch 38 → 42 not taken.
✓ Branch 39 → 40 taken 4147 times.
✓ Branch 40 → 41 taken 165 times.
✓ Branch 40 → 42 taken 3982 times.
4147 const bool selected = m_selected && m_selected(header->folderId, header->uid);
116
2/2
✓ Branch 43 → 44 taken 4147 times.
✓ Branch 44 → 45 taken 4147 times.
4147 m_selectedCache.insert(qMakePair(header->folderId, header->uid), selected);
117 4147 return selected;
118 }
119 4029 case Folder: return QString{};
120 4085 case Suggestion: {
121
2/6
✓ Branch 52 → 53 taken 4085 times.
✗ Branch 52 → 55 not taken.
✓ Branch 56 → 57 taken 4085 times.
✗ Branch 56 → 58 not taken.
✗ Branch 175 → 176 not taken.
✗ Branch 175 → 177 not taken.
8170 const QString suggestion = m_suggestion ? m_suggestion({m_accountId, header->folderId, header->uid}) : QString{};
122
2/2
✓ Branch 58 → 59 taken 4085 times.
✓ Branch 59 → 60 taken 4085 times.
4085 m_suggestionCache.insert(qMakePair(header->folderId, header->uid), suggestion);
123 4085 return suggestion;
124 4085 }
125 4040 case Depth:
126
2/2
✓ Branch 63 → 64 taken 58 times.
✓ Branch 63 → 67 taken 3982 times.
4040 if (m_holding) return m_heldRows[index.row()].depth;
127
2/2
✓ Branch 67 → 68 taken 3411 times.
✓ Branch 67 → 71 taken 571 times.
3982 return m_indexed ? m_indexRows[index.row()].depth : m_rows[index.row()].depth;
128 4034 case ThreadCount:
129
2/2
✓ Branch 75 → 76 taken 57 times.
✓ Branch 75 → 79 taken 3977 times.
4034 if (m_holding) return m_heldRows[index.row()].descendants;
130
2/2
✓ Branch 79 → 80 taken 3412 times.
✓ Branch 79 → 83 taken 565 times.
3977 return m_indexed ? m_indexRows[index.row()].descendants : m_rows[index.row()].source->cachedDescendants;
131 4033 case HasChildren:
132
2/2
✓ Branch 87 → 88 taken 57 times.
✓ Branch 87 → 91 taken 3976 times.
4033 if (m_holding) return m_heldRows[index.row()].descendants > 0;
133
2/2
✓ Branch 91 → 92 taken 3411 times.
✓ Branch 91 → 95 taken 565 times.
3976 return m_indexed ? m_indexRows[index.row()].descendants > 0 : !m_rows[index.row()].source->children.empty();
134 5072 case Expanded:
135
2/2
✓ Branch 100 → 101 taken 57 times.
✓ Branch 100 → 104 taken 5015 times.
5072 if (m_holding) return m_heldExpanded.value(index.row());
136
1/4
✗ Branch 107 → 108 not taken.
✓ Branch 107 → 111 taken 4411 times.
✗ Branch 109 → 110 not taken.
✗ Branch 109 → 111 not taken.
4411 return m_indexed ? m_indexRows[index.row()].descendants > 0 && !m_indexRequest.collapsed.contains(header->uid)
137
6/6
✓ Branch 104 → 105 taken 4411 times.
✓ Branch 104 → 113 taken 604 times.
✓ Branch 116 → 117 taken 76 times.
✓ Branch 116 → 121 taken 528 times.
✓ Branch 119 → 120 taken 69 times.
✓ Branch 119 → 121 taken 7 times.
9426 : !m_rows[index.row()].source->children.empty() && !m_collapsed.contains(MailKey{header->folderId, header->uid});
138
1/2
✓ Branch 126 → 127 taken 4029 times.
✗ Branch 126 → 129 not taken.
4029 case OpenTask: return m_taskLink ? m_taskLink(header->messageId) : false;
139
1/2
✓ Branch 132 → 133 taken 4029 times.
✗ Branch 132 → 135 not taken.
4029 case RuleMatch: return m_ruleMatch ? m_ruleMatch(*header) : false;
140 4083 case Recipient: return header->to;
141
6/8
✓ Branch 138 → 139 taken 205 times.
✓ Branch 138 → 142 taken 3825 times.
✓ Branch 139 → 140 taken 205 times.
✓ Branch 141 → 143 taken 205 times.
✓ Branch 145 → 146 taken 205 times.
✓ Branch 145 → 147 taken 3825 times.
✗ Branch 188 → 189 not taken.
✗ Branch 188 → 190 not taken.
4030 case SizeText: return header->size > 0 ? QLocale().formattedDataSize(header->size, 0) : QString();
142 4029 case MatchedInText: return false;
143 4029 case LabelChips: {
144 4029 const MailKey key{header->folderId, header->uid};
145 4029 const auto cached = m_chipCache.constFind(key);
146
2/2
✓ Branch 153 → 154 taken 3111 times.
✓ Branch 153 → 156 taken 918 times.
4029 if (cached != m_chipCache.constEnd()) return cached.value();
147
1/1
✓ Branch 156 → 157 taken 918 times.
918 const QVariantList chips = MailPresentationModel::labelChipsFor(header->labels);
148
1/1
✓ Branch 157 → 158 taken 918 times.
918 m_chipCache.insert(key, chips);
149 918 return chips;
150 918 }
151 2282 default: return {};
152 }
153 8338 }
154
155 796 void QuickThreadListModel::setAccountId(const QString &accountId) {
156
2/2
✓ Branch 3 → 4 taken 16 times.
✓ Branch 3 → 5 taken 780 times.
796 if (m_accountId == accountId) return;
157 780 releaseHeldRows();
158 780 leaveIndexedFolder();
159 780 m_indexSnapshots.clear();
160 780 m_accountId = accountId;
161
6/6
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 22 taken 779 times.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 16 → 17 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
780 if (rowCount() > 0) emit dataChanged(index(0, 0), index(rowCount() - 1, 0), {Identity});
162 }
163
164 32 bool QuickThreadListModel::matches(const MailHeader &header) const {
165
2/2
✓ Branch 2 → 3 taken 15 times.
✓ Branch 2 → 21 taken 17 times.
32 if (m_showUnread) {
166
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 6 taken 11 times.
15 if (header.folderId != m_shownUnreadFolder) {
167 4 m_shownUnread.clear();
168 4 m_shownUnreadFolder = header.folderId;
169 }
170 15 const MailKey key{header.folderId, header.uid};
171
6/6
✓ Branch 8 → 9 taken 10 times.
✓ Branch 8 → 12 taken 5 times.
✓ Branch 10 → 11 taken 9 times.
✓ Branch 10 → 12 taken 1 time.
✓ Branch 13 → 14 taken 9 times.
✓ Branch 13 → 15 taken 6 times.
15 if (header.isSeen() && !m_shownUnread.contains(key)) return false;
172
3/3
✓ Branch 16 → 17 taken 5 times.
✓ Branch 16 → 19 taken 1 time.
✓ Branch 17 → 18 taken 5 times.
6 if (!header.isSeen()) m_shownUnread.insert(key);
173 }
174
5/6
✓ Branch 21 → 22 taken 3 times.
✓ Branch 21 → 25 taken 20 times.
✓ Branch 23 → 24 taken 3 times.
✗ Branch 23 → 25 not taken.
✓ Branch 26 → 27 taken 3 times.
✓ Branch 26 → 28 taken 20 times.
23 if (m_showStarred && !header.isFlagged()) return false;
175
1/4
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 31 taken 20 times.
✗ Branch 29 → 30 not taken.
✗ Branch 29 → 31 not taken.
20 if (m_showAttachments && !header.hasAttachments) return false;
176 20 if (!m_filterText.isEmpty() &&
177
6/6
✓ Branch 32 → 33 taken 14 times.
✓ Branch 32 → 38 taken 6 times.
✓ Branch 34 → 35 taken 12 times.
✓ Branch 34 → 38 taken 2 times.
✓ Branch 39 → 40 taken 5 times.
✓ Branch 39 → 41 taken 15 times.
32 !header.subject.contains(m_filterText, Qt::CaseInsensitive) &&
178
2/2
✓ Branch 36 → 37 taken 5 times.
✓ Branch 36 → 38 taken 7 times.
17 !header.from.contains(m_filterText, Qt::CaseInsensitive)) return false;
179 15 return true;
180 }
181
182 19 void QuickThreadListModel::forgetShownUnread() {
183 19 m_shownUnread.clear();
184 19 m_shownUnreadFolder = -1;
185 19 m_indexRequest.retainedUids.clear();
186 19 }
187
188 32 bool QuickThreadListModel::collectMatches(
189 const ThreadNode *node, QSet<const ThreadNode *> *matching) const {
190
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 32 times.
32 if (!node) return false;
191 32 bool found = matches(node->header);
192
2/2
✓ Branch 16 → 7 taken 10 times.
✓ Branch 16 → 17 taken 32 times.
42 for (const auto &child : node->children)
193
4/5
✓ Branch 9 → 10 taken 10 times.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 9 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 1 time.
10 found = collectMatches(child.get(), matching) || found;
194
3/3
✓ Branch 17 → 18 taken 20 times.
✓ Branch 17 → 20 taken 12 times.
✓ Branch 18 → 19 taken 20 times.
32 if (found) matching->insert(node);
195 32 return found;
196 }
197
198 47 void QuickThreadListModel::appendVisible(
199 ThreadNode *node, int depth, QVector<Row> *rows,
200 const QSet<const ThreadNode *> *matching) const {
201
7/8
✓ Branch 2 → 3 taken 47 times.
✗ Branch 2 → 6 not taken.
✓ Branch 3 → 4 taken 27 times.
✓ Branch 3 → 7 taken 20 times.
✓ Branch 5 → 6 taken 11 times.
✓ Branch 5 → 7 taken 16 times.
✓ Branch 8 → 9 taken 11 times.
✓ Branch 8 → 10 taken 36 times.
47 if (!node || (matching && !matching->contains(node))) return;
202
1/1
✓ Branch 10 → 11 taken 36 times.
36 rows->append({node, depth});
203
2/2
✓ Branch 13 → 14 taken 31 times.
✓ Branch 13 → 23 taken 5 times.
36 if (!m_collapsed.contains(MailKey{node->header.folderId, node->header.uid})) {
204
2/2
✓ Branch 21 → 16 taken 9 times.
✓ Branch 21 → 22 taken 31 times.
40 for (const auto &child : node->children)
205
1/1
✓ Branch 18 → 19 taken 9 times.
9 appendVisible(child.get(), depth + 1, rows, matching);
206 }
207 }
208
209 3402 void QuickThreadListModel::rebuild() {
210
2/2
✓ Branch 2 → 3 taken 8 times.
✓ Branch 2 → 4 taken 3394 times.
3402 if (m_indexed) return;
211 // Held rows stay until the next folder has content; an empty rebuild
212 // (the thread source was cleared for the switch) changes nothing visible.
213 3394 const bool silent = m_holding;
214
2/2
✓ Branch 4 → 5 taken 3382 times.
✓ Branch 4 → 6 taken 12 times.
3394 if (!silent) beginResetModel();
215 3394 m_dateTextCache.clear();
216 3394 m_chipCache.clear();
217 // S106-304: the identity caches fill on every read of the Selected and
218 // Suggestion roles; the fast path below skips reindexRows(), their only
219 // prune, so a reset (now, or the swap after a hold) empties them.
220 3394 m_selectedCache.clear();
221 3394 m_suggestionCache.clear();
222 3394 m_rows = {};
223 3394 m_rowByKey = {};
224
1/2
✓ Branch 14 → 15 taken 3394 times.
✗ Branch 14 → 58 not taken.
3394 if (m_source) {
225 3394 const auto prepared = m_source->prepared();
226
8/8
✓ Branch 17 → 18 taken 3386 times.
✓ Branch 17 → 23 taken 8 times.
✓ Branch 19 → 20 taken 3384 times.
✓ Branch 19 → 23 taken 2 times.
✓ Branch 21 → 22 taken 3377 times.
✓ Branch 21 → 23 taken 7 times.
✓ Branch 24 → 25 taken 3377 times.
✓ Branch 24 → 29 taken 17 times.
3394 if (!filterActive() && m_collapsed.isEmpty() && m_sort.isDefault()) {
227 // Both containers are implicitly shared. The worker already built
228 // the default order and identity lookup: GUI publication is O(1).
229 3377 m_rows = prepared->rows;
230 3377 m_rowByKey = prepared->rowByKey;
231 } else {
232 17 QSet<const ThreadNode *> matching;
233 17 const QSet<const ThreadNode *> *visible = nullptr;
234
2/2
✓ Branch 31 → 32 taken 8 times.
✓ Branch 31 → 42 taken 9 times.
17 if (filterActive()) {
235
2/2
✓ Branch 40 → 35 taken 20 times.
✓ Branch 40 → 41 taken 8 times.
28 for (const auto &root : prepared->roots)
236
1/1
✓ Branch 37 → 38 taken 20 times.
20 collectMatches(root.get(), &matching);
237 8 visible = &matching;
238 }
239
5/5
✓ Branch 43 → 44 taken 17 times.
✓ Branch 44 → 45 taken 17 times.
✓ Branch 45 → 46 taken 17 times.
✓ Branch 51 → 47 taken 24 times.
✓ Branch 51 → 52 taken 17 times.
41 for (ThreadNode *root : orderedRoots(prepared->roots))
240
1/1
✓ Branch 48 → 49 taken 24 times.
41 appendVisible(root, 0, &m_rows, visible);
241
1/1
✓ Branch 53 → 54 taken 17 times.
17 reindexRows();
242 17 }
243 3394 }
244
2/2
✓ Branch 58 → 59 taken 3382 times.
✓ Branch 58 → 60 taken 12 times.
3394 if (!silent) endResetModel();
245 12 else swapHeldRowsForContent();
246 }
247
248 namespace {
249 // P90.12: coalesce the rows whose cached role value actually changed into
250 // the minimal set of contiguous ranges.
251 template <typename Cache, typename Compute>
252 126 QVector<QPair<int, int>> roleDiffRanges(int rowCount,
253 const std::function<const MailHeader *(int)> &headerAt,
254 Cache &cache, const Compute &compute) {
255 126 QVector<int> changed;
256
2/2
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, QString>, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, QString>&, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1} const&):
✓ Branch 2 → 3 taken 105 times.
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, bool>, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, bool>&, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1} const&):
✓ Branch 2 → 3 taken 21 times.
126 changed.reserve(16);
257
6/6
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, QString>, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, QString>&, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1} const&):
✓ Branch 22 → 23 taken 31 times.
✓ Branch 22 → 25 taken 11512 times.
✓ Branch 27 → 4 taken 11543 times.
✓ Branch 27 → 28 taken 105 times.
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, bool>, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, bool>&, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1} const&):
✓ Branch 22 → 4 taken 2614 times.
✓ Branch 22 → 23 taken 21 times.
25826 for (int row = 0; row < rowCount; ++row) {
258
2/2
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, QString>, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, QString>&, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1} const&):
✓ Branch 4 → 5 taken 11543 times.
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, bool>, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, bool>&, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1} const&):
✓ Branch 4 → 5 taken 2614 times.
14157 const auto *header = headerAt(row);
259
2/4
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, QString>, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, QString>&, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1} const&):
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 11543 times.
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, bool>, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, bool>&, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1} const&):
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 2614 times.
28222 if (!header) continue;
260
2/2
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, QString>, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, QString>&, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1} const&):
✓ Branch 7 → 8 taken 11543 times.
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, bool>, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, bool>&, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1} const&):
✓ Branch 7 → 8 taken 2614 times.
14157 const auto key = qMakePair(header->folderId, header->uid);
261
2/2
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, QString>, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, QString>&, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1} const&):
✓ Branch 8 → 9 taken 11543 times.
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, bool>, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, bool>&, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1} const&):
✓ Branch 8 → 9 taken 2614 times.
14157 const auto value = compute(header);
262 14157 const auto cached = cache.constFind(key);
263 // No entry: no view has read this row's value (data() records every
264 // read), so a delegate created later reads the current value itself.
265
4/4
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, QString>, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, QString>&, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1} const&):
✓ Branch 12 → 13 taken 9807 times.
✓ Branch 12 → 14 taken 1736 times.
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, bool>, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, bool>&, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1} const&):
✓ Branch 12 → 13 taken 2056 times.
✓ Branch 12 → 14 taken 558 times.
14157 if (cached == cache.constEnd()) continue;
266
4/4
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, QString>, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, QString>&, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1} const&):
✓ Branch 16 → 17 taken 1705 times.
✓ Branch 16 → 18 taken 31 times.
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, bool>, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, bool>&, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1} const&):
✓ Branch 15 → 16 taken 497 times.
✓ Branch 15 → 17 taken 61 times.
2294 if (cached.value() == value) continue;
267
2/2
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, QString>, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, QString>&, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1} const&):
✓ Branch 18 → 19 taken 31 times.
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, bool>, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, bool>&, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1} const&):
✓ Branch 17 → 18 taken 61 times.
92 cache.insert(key, value);
268
2/2
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, QString>, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, QString>&, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1} const&):
✓ Branch 19 → 20 taken 31 times.
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, bool>, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, bool>&, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1} const&):
✓ Branch 18 → 19 taken 61 times.
92 changed.append(row);
269 }
270 126 QVector<QPair<int, int>> ranges;
271
4/4
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, QString>, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, QString>&, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1} const&):
✓ Branch 45 → 29 taken 11 times.
✓ Branch 45 → 46 taken 105 times.
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, bool>, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, bool>&, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1} const&):
✓ Branch 40 → 24 taken 15 times.
✓ Branch 40 → 41 taken 21 times.
152 for (int at = 0; at < changed.size();) {
272 26 int end = at;
273
10/12
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, QString>, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, QString>&, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1} const&):
✓ Branch 32 → 33 taken 20 times.
✓ Branch 32 → 37 taken 11 times.
✓ Branch 35 → 36 taken 20 times.
✗ Branch 35 → 37 not taken.
✓ Branch 38 → 30 taken 20 times.
✓ Branch 38 → 39 taken 11 times.
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, bool>, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, bool>&, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1} const&):
✓ Branch 27 → 28 taken 46 times.
✓ Branch 27 → 32 taken 15 times.
✓ Branch 30 → 31 taken 46 times.
✗ Branch 30 → 32 not taken.
✓ Branch 33 → 25 taken 46 times.
✓ Branch 33 → 34 taken 15 times.
92 while (end + 1 < changed.size() && changed.at(end + 1) == changed.at(end) + 1) ++end;
274
2/2
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, QString>, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, QString>&, QuickThreadListModel::suggestionsChanged()::{lambda(MailHeader const*)#1} const&):
✓ Branch 42 → 43 taken 11 times.
QList<std::pair<int, int> > (anonymous namespace)::roleDiffRanges<QHash<std::pair<long long, long long>, bool>, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1}>(int, std::function<MailHeader const* (int)> const&, QHash<std::pair<long long, long long>, bool>&, QuickThreadListModel::selectionChanged()::{lambda(MailHeader const*)#1} const&):
✓ Branch 37 → 38 taken 15 times.
26 ranges.append({changed.at(at), changed.at(end)});
275 26 at = end + 1;
276 }
277 126 return ranges;
278 126 }
279 }
280
281 146 void QuickThreadListModel::reindexRows(int from) {
282 // The identity map describes the underlying rows, never held rows of the
283 // previous folder (rowCount()/headerAtRow() show those while holding; a
284 // reindex during a hold mapped the old folder and every identity lookup
285 // failed after the swap).
286
2/2
✓ Branch 2 → 3 taken 115 times.
✓ Branch 2 → 12 taken 31 times.
146 if (m_indexed) {
287
2/2
✓ Branch 10 → 5 taken 48068 times.
✓ Branch 10 → 11 taken 115 times.
48183 for (int row = qMax(0, from); row < m_indexRows.size(); ++row) {
288 48068 const auto &header = m_indexRows.at(row).header;
289
1/1
✓ Branch 7 → 8 taken 48068 times.
48068 m_rowByKey.insert(MailKey{header.folderId, header.uid}, row);
290 }
291
1/2
✓ Branch 12 → 13 taken 31 times.
✗ Branch 12 → 24 not taken.
31 } else if (m_source) {
292
2/2
✓ Branch 22 → 15 taken 32 times.
✓ Branch 22 → 23 taken 31 times.
63 for (int row = qMax(0, from); row < m_rows.size(); ++row) {
293
1/2
✓ Branch 16 → 17 taken 32 times.
✗ Branch 16 → 20 not taken.
32 if (const auto *node = m_rows.at(row).source)
294
1/1
✓ Branch 18 → 19 taken 32 times.
32 m_rowByKey.insert(MailKey{node->header.folderId, node->header.uid}, row);
295 }
296 }
297 // P90.12: role caches are keyed by identity, so surviving keys keep their
298 // last published value; dropped identities are pruned to bound memory.
299
1/2
✓ Branch 27 → 28 taken 129 times.
✗ Branch 27 → 32 not taken.
275 if (!m_selectedCache.isEmpty() || !m_suggestionCache.isEmpty() ||
300
6/8
✓ Branch 25 → 26 taken 129 times.
✓ Branch 25 → 32 taken 17 times.
✓ Branch 29 → 30 taken 129 times.
✗ Branch 29 → 32 not taken.
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 129 times.
✓ Branch 34 → 35 taken 17 times.
✓ Branch 34 → 44 taken 129 times.
275 !m_dateTextCache.isEmpty() || !m_chipCache.isEmpty()) {
301
1/1
✓ Branch 37 → 38 taken 17 times.
17 const QSet<MailKey> present(m_rowByKey.keyBegin(), m_rowByKey.keyEnd());
302
1/1
✓ Branch 38 → 39 taken 17 times.
17 pruneCache(m_selectedCache, present);
303
1/1
✓ Branch 39 → 40 taken 17 times.
17 pruneCache(m_suggestionCache, present);
304
1/1
✓ Branch 40 → 41 taken 17 times.
17 pruneCache(m_dateTextCache, present);
305
1/1
✓ Branch 41 → 42 taken 17 times.
17 pruneCache(m_chipCache, present);
306 17 }
307 146 }
308
309 17 QList<ThreadNode *> QuickThreadListModel::orderedRoots(const std::vector<std::unique_ptr<ThreadNode>> &roots) const {
310 17 QList<ThreadNode *> ordered;
311
1/1
✓ Branch 3 → 4 taken 17 times.
17 ordered.reserve(qsizetype(roots.size()));
312
3/3
✓ Branch 8 → 9 taken 24 times.
✓ Branch 11 → 6 taken 24 times.
✓ Branch 11 → 12 taken 17 times.
41 for (const auto &root : roots) ordered.append(root.get());
313 using Column = MailSortOrder::Column;
314
2/2
✓ Branch 12 → 13 taken 12 times.
✓ Branch 12 → 18 taken 5 times.
17 if (m_sort.column == Column::Date) {
315 // ThreadBuilder lists the newest thread first.
316
5/5
✓ Branch 13 → 14 taken 2 times.
✓ Branch 13 → 17 taken 10 times.
✓ Branch 14 → 15 taken 2 times.
✓ Branch 15 → 16 taken 2 times.
✓ Branch 16 → 17 taken 2 times.
12 if (m_sort.ascending) std::reverse(ordered.begin(), ordered.end());
317 12 return ordered;
318 }
319 // Same order as the thread index: the first mail's key (A to Z, or its
320 // reverse), then the root UID ascending.
321 5 QHash<const ThreadNode *, QByteArray> keys;
322
1/2
✗ Branch 31 → 21 not taken.
✓ Branch 31 → 32 taken 5 times.
5 for (const ThreadNode *root : std::as_const(ordered))
323 ✗ keys.insert(root, MailSort::key(root->header, m_sort.column).toString().toUtf8());
324
3/3
✓ Branch 32 → 33 taken 5 times.
✓ Branch 33 → 34 taken 5 times.
✓ Branch 34 → 35 taken 5 times.
5 std::stable_sort(ordered.begin(), ordered.end(), [&](const ThreadNode *left, const ThreadNode *right) {
325 ✗ const int compared = keys.value(left).compare(keys.value(right));
326 ✗ if (compared != 0) return m_sort.ascending ? compared < 0 : compared > 0;
327 ✗ return left->header.uid < right->header.uid;
328 });
329 5 return ordered;
330 5 }
331
332 806 void QuickThreadListModel::setSortOrder(const MailSortOrder &order) {
333
1/1
✓ Branch 2 → 3 taken 806 times.
806 const MailSortOrder threads = MailSort::threadOrder(order);
334
2/2
✓ Branch 4 → 5 taken 795 times.
✓ Branch 4 → 6 taken 11 times.
806 if (m_sort == threads) return;
335 11 m_sort = threads;
336 // Retained pages and cursors belong to the previous order.
337 11 m_indexSnapshots.clear();
338
2/2
✓ Branch 7 → 8 taken 4 times.
✓ Branch 7 → 10 taken 7 times.
11 if (m_indexed) {
339 4 m_nextLoadAnnounced = true;
340 4 m_collapsedSpans.clear();
341 4 m_collapsedSpanRows = 0;
342
1/1
✓ Branch 9 → 12 taken 4 times.
4 loadIndexed(PageMode::Replace);
343
1/2
✓ Branch 10 → 11 taken 7 times.
✗ Branch 10 → 12 not taken.
7 } else if (!m_holding) {
344
1/1
✓ Branch 11 → 12 taken 7 times.
7 rebuild();
345 }
346 }
347
348 13 void QuickThreadListModel::filter(const QString &text, bool unreadOnly, bool starredOnly, bool withAttachments) {
349
8/8
✓ Branch 3 → 4 taken 7 times.
✓ Branch 3 → 8 taken 6 times.
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 8 taken 5 times.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 8 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 12 times.
14 if (m_filterText == text && m_showUnread == unreadOnly && m_showStarred == starredOnly &&
350
1/2
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 8 not taken.
2 m_showAttachments == withAttachments) return;
351 12 m_filterText = text;
352 12 m_showUnread = unreadOnly;
353 12 m_showStarred = starredOnly;
354 12 m_showAttachments = withAttachments;
355 12 forgetShownUnread();
356 // S106-305: held rows of the previous folder ignore filters; a filter
357 // change ends the hold so the list shows what the filter selects.
358 12 releaseHeldRows();
359
2/2
✓ Branch 14 → 15 taken 6 times.
✓ Branch 14 → 17 taken 6 times.
12 if (m_indexed) {
360 6 m_nextLoadAnnounced = true;
361 6 m_collapsedSpans.clear();
362 6 m_collapsedSpanRows = 0;
363 6 loadIndexed(PageMode::Replace);
364 }
365 6 else rebuild();
366 12 emit mailFilterChanged();
367 }
368
369 800 void QuickThreadListModel::clearFilters() {
370
2/2
✓ Branch 3 → 4 taken 793 times.
✓ Branch 3 → 5 taken 7 times.
800 if (!filterActive()) return;
371 7 m_filterText.clear();
372 7 m_showUnread = false;
373 7 m_showStarred = false;
374 7 m_showAttachments = false;
375 7 forgetShownUnread();
376 7 releaseHeldRows(); // S106-305
377
2/2
✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 11 taken 4 times.
7 if (m_indexed) {
378 3 m_nextLoadAnnounced = true;
379 3 m_collapsedSpans.clear();
380 3 m_collapsedSpanRows = 0;
381 3 loadIndexed(PageMode::Replace);
382 }
383 4 else rebuild();
384 7 emit mailFilterChanged();
385 }
386
387 23 void QuickThreadListModel::toggleExpanded(int row) {
388
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 22 times.
31 if (m_holding) return;
389
3/3
✓ Branch 4 → 5 taken 6 times.
✓ Branch 4 → 7 taken 16 times.
✓ Branch 5 → 6 taken 6 times.
22 if (m_indexed) { toggleIndexed(row); return; }
390 16 const auto *header = headerAtRow(row);
391
3/4
✓ Branch 8 → 9 taken 14 times.
✓ Branch 8 → 10 taken 2 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 14 times.
16 if (!header || !m_source) return;
392 14 const MailKey key{header->folderId, header->uid};
393
1/1
✓ Branch 12 → 13 taken 14 times.
14 auto *sourceNode = m_rows[row].source;
394
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 14 times.
14 if (!sourceNode) return;
395
1/1
✓ Branch 15 → 16 taken 14 times.
14 const int depth = m_rows[row].depth;
396 14 QSet<const ThreadNode *> matching;
397 14 const QSet<const ThreadNode *> *visible = nullptr;
398
2/2
✓ Branch 18 → 19 taken 2 times.
✓ Branch 18 → 21 taken 12 times.
14 if (filterActive()) {
399
1/1
✓ Branch 19 → 20 taken 2 times.
2 collectMatches(sourceNode, &matching);
400 2 visible = &matching;
401 }
402
2/2
✓ Branch 22 → 23 taken 6 times.
✓ Branch 22 → 47 taken 8 times.
14 if (m_collapsed.contains(key)) {
403
1/1
✓ Branch 23 → 24 taken 6 times.
6 m_collapsed.remove(key);
404 6 QVector<Row> inserted;
405
2/2
✓ Branch 31 → 26 taken 6 times.
✓ Branch 31 → 32 taken 6 times.
12 for (const auto &child : sourceNode->children)
406
1/1
✓ Branch 28 → 29 taken 6 times.
6 appendVisible(child.get(), depth + 1, &inserted, visible);
407
1/2
✓ Branch 33 → 34 taken 6 times.
✗ Branch 33 → 45 not taken.
6 if (!inserted.isEmpty()) {
408
1/1
✓ Branch 36 → 37 taken 6 times.
6 beginInsertRows({}, row + 1, row + inserted.size());
409
4/4
✓ Branch 38 → 39 taken 9 times.
✓ Branch 39 → 40 taken 9 times.
✓ Branch 42 → 38 taken 9 times.
✓ Branch 42 → 43 taken 6 times.
15 for (int i = 0; i < inserted.size(); ++i) m_rows.insert(row + 1 + i, inserted[i]);
410
1/1
✓ Branch 43 → 44 taken 6 times.
6 endInsertRows();
411
1/1
✓ Branch 44 → 45 taken 6 times.
6 reindexRows(row + 1);
412 }
413 6 } else {
414 // Visible subtree rows form one contiguous DFS range. Count them with
415 // the same visibility rules (a collapsed intermediate node hides its
416 // own descendants; the filter decides which rows exist at all).
417 8 QVector<Row> subtree;
418
2/2
✓ Branch 54 → 49 taken 8 times.
✓ Branch 54 → 55 taken 8 times.
16 for (const auto &child : sourceNode->children)
419
1/1
✓ Branch 51 → 52 taken 8 times.
8 appendVisible(child.get(), depth + 1, &subtree, visible);
420
1/2
✗ Branch 56 → 57 not taken.
✓ Branch 56 → 58 taken 8 times.
8 if (subtree.isEmpty()) return;
421 // Drop the removed rows' identity entries before reindexing, so a
422 // hidden mail never resolves to a stale row.
423
4/4
✓ Branch 58 → 59 taken 8 times.
✓ Branch 59 → 60 taken 8 times.
✓ Branch 68 → 61 taken 11 times.
✓ Branch 68 → 69 taken 8 times.
19 for (const auto &removed : subtree) {
424
1/2
✓ Branch 62 → 63 taken 11 times.
✗ Branch 62 → 66 not taken.
11 if (const auto *header = &removed.source->header)
425
1/1
✓ Branch 64 → 65 taken 11 times.
11 m_rowByKey.remove(MailKey{header->folderId, header->uid});
426 }
427
1/1
✓ Branch 71 → 72 taken 8 times.
8 beginRemoveRows({}, row + 1, row + subtree.size());
428
1/1
✓ Branch 73 → 74 taken 8 times.
8 m_rows.remove(row + 1, subtree.size());
429
1/1
✓ Branch 74 → 75 taken 8 times.
8 endRemoveRows();
430
1/1
✓ Branch 75 → 76 taken 8 times.
8 reindexRows(row + 1);
431
1/1
✓ Branch 76 → 77 taken 8 times.
8 m_collapsed.insert(key);
432 8 }
433 // The toggled row's Expanded role changed even when no child rows moved.
434
4/4
✓ Branch 83 → 84 taken 14 times.
✓ Branch 85 → 86 taken 14 times.
✓ Branch 87 → 88 taken 14 times.
✓ Branch 88 → 89 taken 14 times.
14 emit dataChanged(index(row, 0), index(row, 0), {Expanded});
435 14 }
436
437 3 bool QuickThreadListModel::isExpanded(int row) const {
438
3/3
✓ Branch 3 → 4 taken 3 times.
✓ Branch 4 → 5 taken 3 times.
✓ Branch 5 → 6 taken 3 times.
3 return data(index(row, 0), Expanded).toBool();
439 }
440
441 127540 const MailHeader *QuickThreadListModel::headerAtRow(int row) const {
442
4/6
✓ Branch 2 → 3 taken 2060 times.
✓ Branch 2 → 10 taken 125480 times.
✓ Branch 3 → 4 taken 2060 times.
✗ Branch 3 → 8 not taken.
✓ Branch 5 → 6 taken 2060 times.
✗ Branch 5 → 8 not taken.
127540 if (m_holding) return row >= 0 && row < m_heldRows.size() ? &m_heldRows[row].header : nullptr;
443
5/6
✓ Branch 10 → 11 taken 112462 times.
✓ Branch 10 → 18 taken 13018 times.
✓ Branch 11 → 12 taken 112440 times.
✓ Branch 11 → 16 taken 22 times.
✓ Branch 13 → 14 taken 112440 times.
✗ Branch 13 → 16 not taken.
125480 if (m_indexed) return row >= 0 && row < m_indexRows.size() ? &m_indexRows[row].header : nullptr;
444
7/8
✓ Branch 18 → 19 taken 13002 times.
✓ Branch 18 → 22 taken 16 times.
✓ Branch 20 → 21 taken 13001 times.
✓ Branch 20 → 22 taken 1 time.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 13001 times.
✓ Branch 24 → 25 taken 17 times.
✓ Branch 24 → 26 taken 13001 times.
13018 if (row < 0 || row >= m_rows.size() || !m_source) return nullptr;
445 13001 return &m_rows[row].source->header;
446 }
447
448 ✗ QuickThreadListModel::MailKey QuickThreadListModel::keyAtRow(int row) const {
449 ✗ const auto *header = headerAtRow(row);
450 ✗ return header ? MailKey{header->folderId, header->uid} : MailKey{-1, -1};
451 }
452
453 73 QVariantMap QuickThreadListModel::mailAt(int row) const {
454
7/8
✓ Branch 2 → 3 taken 72 times.
✓ Branch 2 → 7 taken 1 time.
✓ Branch 5 → 6 taken 72 times.
✗ Branch 5 → 7 not taken.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 71 times.
✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 11 taken 71 times.
73 if (row < 0 || row >= rowCount() || m_holding) return {};
455
2/2
✓ Branch 12 → 13 taken 71 times.
✓ Branch 13 → 14 taken 71 times.
71 return {{"identity", data(index(row, 0), Identity)},
456
2/2
✓ Branch 16 → 17 taken 71 times.
✓ Branch 17 → 18 taken 71 times.
142 {"subject", data(index(row, 0), Subject)},
457
4/6
✓ Branch 20 → 21 taken 71 times.
✓ Branch 21 → 22 taken 71 times.
✓ Branch 25 → 26 taken 213 times.
✓ Branch 25 → 27 taken 71 times.
✗ Branch 33 → 34 not taken.
✗ Branch 33 → 35 not taken.
426 {"sender", data(index(row, 0), Sender)}};
458 284 }
459
460 1427 int QuickThreadListModel::rowForIdentity(const QVariantMap &identity) const {
461
1/1
✓ Branch 2 → 3 taken 1427 times.
1427 const auto id = WorkspaceState::decode(identity);
462
6/6
✓ Branch 4 → 5 taken 1359 times.
✓ Branch 4 → 6 taken 68 times.
✓ Branch 5 → 6 taken 18 times.
✓ Branch 5 → 7 taken 1341 times.
✓ Branch 8 → 9 taken 86 times.
✓ Branch 8 → 10 taken 1341 times.
1427 if (id.accountId != m_accountId || m_holding) return -1;
463 1341 return m_rowByKey.value(MailKey{id.folderId, id.uid}, -1);
464 1427 }
465
466 21711 MailSelectionKey QuickThreadListModel::keyAt(int row) const {
467
2/2
✓ Branch 2 → 3 taken 21 times.
✓ Branch 2 → 5 taken 21690 times.
21732 if (m_holding) return {};
468 21690 const auto *header = headerAtRow(row);
469
2/2
✓ Branch 6 → 7 taken 21656 times.
✓ Branch 6 → 8 taken 34 times.
21690 return header ? MailSelectionKey{header->folderId, header->uid, header->date} : MailSelectionKey{};
470 21 }
471
472 202 const MailHeader *QuickThreadListModel::indexedHeader(qint64 folder, qint64 uid) const {
473
3/4
✓ Branch 2 → 3 taken 56 times.
✓ Branch 2 → 4 taken 146 times.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 56 times.
202 if (!m_indexed || folder != m_indexRequest.folderId) return nullptr;
474 56 const int row = m_rowByKey.value({folder, uid}, -1);
475
3/4
✓ Branch 7 → 8 taken 51 times.
✓ Branch 7 → 12 taken 5 times.
✓ Branch 9 → 10 taken 51 times.
✗ Branch 9 → 12 not taken.
56 return row >= 0 && row < m_indexRows.size() ? &m_indexRows.at(row).header : nullptr;
476 }
477
478
479 124 void QuickThreadListModel::selectionChanged() {
480 // P90.12/P90.17: a click invalidates at most the rows whose membership
481 // flipped (typically old + new selection row), never the whole model.
482
2/2
✓ Branch 4 → 5 taken 103 times.
✓ Branch 4 → 7 taken 21 times.
124 if (!rowCount()) { m_selectedCache.clear(); return; }
483
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 20 taken 21 times.
21 if (!m_selected) {
484 ✗ m_selectedCache.clear();
485 ✗ emit dataChanged(index(0, 0), index(rowCount() - 1, 0), {Selected});
486 ✗ return;
487 }
488 2656 for (const auto &range : roleDiffRanges(rowCount(), [this](int row) { return headerAtRow(row); },
489 21 m_selectedCache, [this](const MailHeader *header) {
490 2614 return m_selected(header->folderId, header->uid);
491
5/5
✓ Branch 23 → 24 taken 21 times.
✓ Branch 25 → 26 taken 21 times.
✓ Branch 26 → 27 taken 21 times.
✓ Branch 38 → 28 taken 15 times.
✓ Branch 38 → 39 taken 21 times.
57 }))
492
4/4
✓ Branch 29 → 30 taken 15 times.
✓ Branch 31 → 32 taken 15 times.
✓ Branch 33 → 34 taken 15 times.
✓ Branch 34 → 35 taken 15 times.
36 emit dataChanged(index(range.first, 0), index(range.second, 0), {Selected});
493 }
494 2703 void QuickThreadListModel::singleSelectionChanged(MailSelectionKey previous,
495 MailSelectionKey current) {
496
1/3
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 2703 times.
✗ Branch 4 → 5 not taken.
2703 if (!m_selected) { selectionChanged(); return; }
497 3638 const auto update = [this](const MailSelectionKey &key) {
498
3/4
✓ Branch 2 → 3 taken 1293 times.
✓ Branch 2 → 4 taken 2345 times.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1293 times.
4895 if (key.folderId < 0 || key.uid < 0) return;
499 1293 const MailKey identity{key.folderId, key.uid};
500 1293 const int row = m_rowByKey.value(identity, -1);
501
3/3
✓ Branch 7 → 8 taken 1239 times.
✓ Branch 7 → 10 taken 54 times.
✓ Branch 8 → 9 taken 1239 times.
1293 if (row < 0) { m_selectedCache.remove(identity); return; }
502
1/1
✓ Branch 10 → 11 taken 54 times.
54 const bool selected = m_selected(key.folderId, key.uid);
503 54 const auto cached = m_selectedCache.constFind(identity);
504
6/6
✓ Branch 14 → 15 taken 48 times.
✓ Branch 14 → 18 taken 6 times.
✓ Branch 16 → 17 taken 18 times.
✓ Branch 16 → 18 taken 30 times.
✓ Branch 19 → 20 taken 18 times.
✓ Branch 19 → 21 taken 36 times.
54 if (cached != m_selectedCache.cend() && cached.value() == selected) return;
505
1/1
✓ Branch 21 → 22 taken 36 times.
36 m_selectedCache.insert(identity, selected);
506
4/4
✓ Branch 22 → 23 taken 36 times.
✓ Branch 24 → 25 taken 36 times.
✓ Branch 26 → 27 taken 36 times.
✓ Branch 27 → 28 taken 36 times.
36 emit dataChanged(index(row, 0), index(row, 0), {Selected});
507 2703 };
508
1/1
✓ Branch 6 → 7 taken 2703 times.
2703 update(previous);
509
4/4
✓ Branch 7 → 8 taken 2014 times.
✓ Branch 7 → 9 taken 689 times.
✓ Branch 8 → 9 taken 246 times.
✓ Branch 8 → 10 taken 1768 times.
2703 if (previous.folderId != current.folderId || previous.uid != current.uid)
510
1/1
✓ Branch 9 → 10 taken 935 times.
935 update(current);
511 }
512
513 8565 void QuickThreadListModel::suggestionsChanged() {
514 // P90.12: only identities whose prediction actually moved re-render.
515
2/2
✓ Branch 4 → 5 taken 8460 times.
✓ Branch 4 → 7 taken 105 times.
8565 if (!rowCount()) { m_suggestionCache.clear(); return; }
516
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 20 taken 105 times.
105 if (!m_suggestion) {
517 ✗ m_suggestionCache.clear();
518 ✗ emit dataChanged(index(0, 0), index(rowCount() - 1, 0), {Suggestion});
519 ✗ return;
520 }
521 11753 for (const auto &range : roleDiffRanges(rowCount(), [this](int row) { return headerAtRow(row); },
522 105 m_suggestionCache, [this](const MailHeader *header) {
523
1/3
✓ Branch 3 → 4 taken 11543 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
23086 return m_suggestion(MailId{m_accountId, header->folderId, header->uid});
524
5/5
✓ Branch 23 → 24 taken 105 times.
✓ Branch 25 → 26 taken 105 times.
✓ Branch 26 → 27 taken 105 times.
✓ Branch 38 → 28 taken 11 times.
✓ Branch 38 → 39 taken 105 times.
221 }))
525
4/4
✓ Branch 29 → 30 taken 11 times.
✓ Branch 31 → 32 taken 11 times.
✓ Branch 33 → 34 taken 11 times.
✓ Branch 34 → 35 taken 11 times.
116 emit dataChanged(index(range.first, 0), index(range.second, 0), {Suggestion});
526 }
527
528 namespace {
529 140 qint64 indexRowsBytes(const QList<ThreadProjectionRow> &rows) {
530 140 qint64 bytes = rows.capacity() * sizeof(ThreadProjectionRow) + rows.size() * 64;
531
2/2
✓ Branch 21 → 6 taken 53730 times.
✓ Branch 21 → 22 taken 140 times.
53870 for (const auto &row : rows) {
532 53730 bytes += 96 + 2 * (row.header.subject.capacity() + row.header.from.capacity() + row.header.to.capacity());
533 53730 bytes += row.cursor.path.capacity() + 32 + row.header.labels.capacity()*sizeof(QString);
534
1/2
✗ Branch 18 → 14 not taken.
✓ Branch 18 → 19 taken 53730 times.
53730 for (const auto &label : row.header.labels) bytes += 2*label.capacity() + 32;
535 }
536 140 return bytes;
537 }
538 }
539
540 ✗ qint64 QuickThreadListModel::retainedRowBytes() const {
541 ✗ return indexRowsBytes(m_holding ? m_heldRows : m_indexRows);
542 }
543
544 150 QString QuickThreadListModel::indexedKey() const {
545 150 return snapshotKey(m_indexDatabase, m_indexRequest);
546 }
547
548 159 QString QuickThreadListModel::snapshotKey(const QString &database,
549 const ThreadProjectionRequest &request) {
550
4/4
✓ Branch 14 → 15 taken 159 times.
✓ Branch 16 → 17 taken 159 times.
✓ Branch 17 → 18 taken 159 times.
✓ Branch 18 → 19 taken 159 times.
477 return QString::fromLatin1(database.toUtf8().toBase64()) + QLatin1Char(':') +
551
5/5
✓ Branch 8 → 9 taken 159 times.
✓ Branch 10 → 11 taken 159 times.
✓ Branch 11 → 12 taken 159 times.
✓ Branch 19 → 20 taken 159 times.
✓ Branch 20 → 21 taken 159 times.
795 QString::fromLatin1(request.account.toUtf8().toBase64()) + QLatin1Char(':') +
552
5/5
✓ Branch 2 → 3 taken 159 times.
✓ Branch 5 → 6 taken 159 times.
✓ Branch 21 → 22 taken 159 times.
✓ Branch 22 → 23 taken 159 times.
✓ Branch 23 → 24 taken 159 times.
477 QString::number(request.folderId) + QLatin1Char(':') + QString::number(request.uidValidity);
553 }
554
555 157 void QuickThreadListModel::retainIndexedSnapshot() {
556
8/8
✓ Branch 2 → 3 taken 119 times.
✓ Branch 2 → 6 taken 38 times.
✓ Branch 3 → 4 taken 114 times.
✓ Branch 3 → 6 taken 5 times.
✓ Branch 4 → 5 taken 112 times.
✓ Branch 4 → 6 taken 2 times.
✓ Branch 5 → 6 taken 3 times.
✓ Branch 5 → 7 taken 109 times.
157 if (!m_indexed || m_holding || m_indexPending || m_indexRevision < 0) return;
557 109 const qint64 bytes = indexRowsBytes(m_indexRows) + m_indexRequest.collapsed.size()*32 + sizeof(IndexSnapshot);
558
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 109 times.
109 if (bytes > qint64(m_indexSnapshots.maxCost())*1024) return;
559
2/5
✓ Branch 16 → 17 taken 109 times.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 21 taken 109 times.
✗ Branch 31 → 32 not taken.
✗ Branch 31 → 34 not taken.
109 m_indexSnapshots.insert(indexedKey(), new IndexSnapshot{m_indexRows, m_indexRequest,
560 109 m_indexRevision, m_indexTotalCount, m_indexUnreadCount,
561
2/6
✓ Branch 14 → 15 taken 109 times.
✓ Branch 15 → 16 taken 109 times.
✗ Branch 25 → 26 not taken.
✗ Branch 25 → 27 not taken.
✗ Branch 28 → 29 not taken.
✗ Branch 28 → 30 not taken.
109 m_indexHasBefore, m_indexHasAfter, m_viewportUid, m_viewportOffset}, int((bytes+1023)/1024));
562 }
563
564 2538 void QuickThreadListModel::cancelIndexedRequest() {
565 2538 ++m_indexGeneration;
566 // P90.2: switching folders orphans the in-flight page load instead of
567 // cancelling it. A build that completes while its owner moved on is
568 // retained as a snapshot, so rapid A→B→A never re-pays the cold build.
569 // Tokens are bounded in loadIndexed (oldest orphan is really cancelled
570 // once more than one is in flight).
571 2538 m_indexCancelled.reset();
572
2/2
✓ Branch 3 → 4 taken 25 times.
✓ Branch 3 → 5 taken 2513 times.
2538 if (m_indexPending) { m_indexPending = false; emit projectionChanged(); }
573 2538 }
574
575 111 void QuickThreadListModel::forgetActiveRequest(const std::shared_ptr<std::atomic_bool> &token) {
576
1/2
✓ Branch 10 → 3 taken 113 times.
✗ Branch 10 → 11 not taken.
113 for (int i = 0; i < m_activeRequests.size(); ++i)
577
2/2
✓ Branch 5 → 6 taken 111 times.
✓ Branch 5 → 8 taken 2 times.
113 if (m_activeRequests.at(i) == token) { m_activeRequests.removeAt(i); return; }
578 }
579
580 8 void QuickThreadListModel::refreshIndexedExclusions(const QSet<qint64> &excludedUids) {
581
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 8 times.
8 if (!m_indexed) return;
582 8 const bool changed = m_indexRequest.excludedUids != excludedUids;
583 8 m_indexRequest.excludedUids = excludedUids;
584
4/4
✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 9 taken 6 times.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 1 time.
8 if (!changed && !m_indexExclusionsDirty) return;
585
2/2
✓ Branch 9 → 10 taken 6 times.
✓ Branch 9 → 11 taken 1 time.
7 if (changed) ++m_indexExclusionGeneration;
586 7 m_indexExclusionsDirty = false;
587 7 m_collapsedSpans.clear();
588 7 m_collapsedSpanRows = 0;
589 // Journal changes can arrive without a matching flat-model removal
590 // (for example after an account-scoped replay). Hide newly excluded
591 // identities in the published window before the replacement page runs.
592
2/2
✓ Branch 28 → 14 taken 1196 times.
✓ Branch 28 → 29 taken 7 times.
1203 for (int row = m_indexRows.size() - 1; row >= 0;) {
593
2/2
✓ Branch 16 → 17 taken 1193 times.
✓ Branch 16 → 18 taken 3 times.
1196 if (!excludedUids.contains(m_indexRows[row].header.uid)) { --row; continue; }
594 3 const int last = row;
595
5/6
✓ Branch 20 → 21 taken 14 times.
✗ Branch 20 → 25 not taken.
✓ Branch 23 → 24 taken 11 times.
✓ Branch 23 → 25 taken 3 times.
✓ Branch 26 → 19 taken 11 times.
✓ Branch 26 → 27 taken 3 times.
14 while (row >= 0 && excludedUids.contains(m_indexRows[row].header.uid)) --row;
596 3 removeIndexedRows(row + 1, last);
597 }
598
1/2
✓ Branch 29 → 30 taken 7 times.
✗ Branch 29 → 34 not taken.
14 if (m_viewportUid > 0 &&
599
4/4
✓ Branch 32 → 33 taken 1 time.
✓ Branch 32 → 34 taken 6 times.
✓ Branch 35 → 36 taken 1 time.
✓ Branch 35 → 42 taken 6 times.
14 !m_rowByKey.contains({m_indexRequest.folderId, m_viewportUid})) {
600
1/2
✗ Branch 37 → 38 not taken.
✓ Branch 37 → 39 taken 1 time.
1 m_viewportUid = m_indexRows.isEmpty() ? 0 : m_indexRows.first().header.uid;
601 1 m_viewportOffset = 0;
602 }
603 7 loadIndexed(PageMode::Replace,
604
1/4
✗ Branch 42 → 43 not taken.
✓ Branch 42 → 47 taken 7 times.
✗ Branch 44 → 45 not taken.
✗ Branch 44 → 47 not taken.
7 m_indexHasBefore && !m_indexRows.isEmpty()
605 ✗ ? m_indexRows.first().header.uid : qint64(0),
606 false);
607 }
608
609 131 void QuickThreadListModel::markIndexedMovePending(qint64 folder, qint64 uid) {
610
7/10
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 7 taken 128 times.
✓ Branch 3 → 4 taken 3 times.
✗ Branch 3 → 7 not taken.
✓ Branch 4 → 5 taken 3 times.
✗ Branch 4 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 3 times.
✓ Branch 9 → 10 taken 128 times.
✓ Branch 9 → 11 taken 3 times.
134 if (!m_indexed || folder != m_indexRequest.folderId || uid <= 0 ||
611 131 m_indexRequest.excludedUids.contains(uid)) return;
612
1/1
✓ Branch 11 → 12 taken 3 times.
3 m_indexRequest.excludedUids.insert(uid);
613 3 m_indexExclusionsDirty = true;
614 3 ++m_indexExclusionGeneration;
615 // A page already in flight captured the old exclusion set. Orphan it
616 // before it can publish the just-moved UID, but retain the current rows.
617 3 ++m_indexGeneration;
618 3 m_indexCancelled.reset();
619 3 removeIndexedUid(folder, uid);
620 }
621
622 ✗ void QuickThreadListModel::abandonIndexedMoveRefresh() {
623 ✗ if (m_indexed && m_indexExclusionsDirty) cancelIndexedRequest();
624 ✗ }
625
626 9 bool QuickThreadListModel::showProvisionalPage(qint64 folder, quint32 epoch,
627 const QList<MailHeader> &headers) {
628
7/7
✓ Branch 2 → 3 taken 9 times.
✓ Branch 3 → 4 taken 6 times.
✓ Branch 3 → 5 taken 3 times.
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 4 times.
✓ Branch 7 → 8 taken 5 times.
✓ Branch 7 → 9 taken 4 times.
9 if (!provisionalPageWanted(folder) || m_indexRequest.uidValidity != epoch) return false;
629 4 QList<ThreadProjectionRow> rows;
630
1/1
✓ Branch 10 → 11 taken 4 times.
4 rows.reserve(headers.size());
631
2/2
✓ Branch 30 → 13 taken 650 times.
✓ Branch 30 → 31 taken 4 times.
654 for (const auto &header : headers) {
632
3/6
✓ Branch 14 → 15 taken 650 times.
✗ Branch 14 → 17 not taken.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 650 times.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 650 times.
650 if (header.folderId != folder || m_indexRequest.excludedUids.contains(header.uid)) continue;
633 650 ThreadProjectionRow row;
634 650 row.header = header;
635
1/1
✓ Branch 24 → 25 taken 650 times.
650 rows.append(std::move(row)); // no cursor: paging stays off until the indexed page
636 650 }
637
1/2
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 4 times.
4 if (rows.isEmpty()) return false;
638
1/2
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 44 taken 4 times.
4 if (m_holding) {
639 ✗ beginResetModel();
640 ✗ m_holding = false;
641 ✗ m_heldRows.clear(); m_heldExpanded.clear();
642 ✗ m_dateTextCache.clear(); m_chipCache.clear();
643 ✗ m_indexRows = std::move(rows);
644 ✗ reindexRows();
645 ✗ endResetModel();
646 } else {
647
1/1
✓ Branch 46 → 47 taken 4 times.
4 beginInsertRows({}, 0, int(rows.size()) - 1);
648 4 m_indexRows = std::move(rows);
649
1/1
✓ Branch 49 → 50 taken 4 times.
4 reindexRows();
650
1/1
✓ Branch 50 → 51 taken 4 times.
4 endInsertRows();
651 }
652 4 m_indexHasBefore = m_indexHasAfter = false;
653
4/4
✓ Branch 51 → 52 taken 4 times.
✓ Branch 55 → 56 taken 4 times.
✓ Branch 73 → 53 taken 4 times.
✓ Branch 73 → 74 taken 4 times.
8 qCDebug(lcThreadProjection).noquote()
654
3/3
✓ Branch 61 → 62 taken 4 times.
✓ Branch 64 → 65 taken 4 times.
✓ Branch 65 → 66 taken 4 times.
12 << QStringLiteral("provisional page folder=%1 rows=%2").arg(folder).arg(m_indexRows.size());
655
1/1
✓ Branch 74 → 75 taken 4 times.
4 emit projectionChanged();
656 4 return true;
657 4 }
658
659 16 void QuickThreadListModel::holdRowsForFolderSwitch() {
660
7/8
✓ Branch 2 → 3 taken 15 times.
✓ Branch 2 → 8 taken 1 time.
✓ Branch 4 → 5 taken 15 times.
✗ Branch 4 → 8 not taken.
✓ Branch 7 → 8 taken 4 times.
✓ Branch 7 → 9 taken 11 times.
✓ Branch 10 → 11 taken 5 times.
✓ Branch 10 → 12 taken 11 times.
16 if (m_holding || filterActive() || rowCount() == 0) return;
661 // The indexed page is retained for a later return before it is held.
662
1/1
✓ Branch 12 → 13 taken 11 times.
11 retainIndexedSnapshot();
663 11 QList<ThreadProjectionRow> held;
664 11 QVector<bool> expanded;
665 11 const int rows = rowCount();
666
1/1
✓ Branch 15 → 16 taken 11 times.
11 held.reserve(rows);
667
1/1
✓ Branch 16 → 17 taken 11 times.
11 expanded.reserve(rows);
668
2/2
✓ Branch 39 → 18 taken 1034 times.
✓ Branch 39 → 40 taken 11 times.
1045 for (int row = 0; row < rows; ++row) {
669
2/2
✓ Branch 18 → 19 taken 1000 times.
✓ Branch 18 → 21 taken 34 times.
1034 if (m_indexed) {
670
1/1
✓ Branch 20 → 31 taken 1000 times.
1000 held.append(m_indexRows.at(row));
671 } else {
672 34 const auto &source = m_rows.at(row);
673
1/2
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 34 times.
34 if (!source.source) continue;
674 34 ThreadProjectionRow copy;
675 34 copy.header = source.source->header;
676 34 copy.depth = source.depth;
677 34 copy.descendants = source.source->cachedDescendants;
678
1/1
✓ Branch 27 → 28 taken 34 times.
34 held.append(std::move(copy));
679 34 }
680
4/4
✓ Branch 32 → 33 taken 1034 times.
✓ Branch 33 → 34 taken 1034 times.
✓ Branch 34 → 35 taken 1034 times.
✓ Branch 35 → 36 taken 1034 times.
1034 expanded.append(data(index(row, 0), Expanded).toBool());
681 }
682 // Same content, so no signal: the view keeps its delegates and viewport.
683 11 m_heldRows = std::move(held);
684 11 m_heldExpanded = std::move(expanded);
685 11 m_holding = true;
686
2/2
✓ Branch 44 → 45 taken 11 times.
✓ Branch 45 → 46 taken 11 times.
11 m_indexRows.clear(); m_rows.clear(); m_rowByKey.clear();
687
4/4
✓ Branch 47 → 48 taken 11 times.
✓ Branch 51 → 52 taken 11 times.
✓ Branch 66 → 49 taken 11 times.
✓ Branch 66 → 67 taken 11 times.
22 qCDebug(lcThreadProjection).noquote()
688
2/2
✓ Branch 58 → 59 taken 11 times.
✓ Branch 59 → 60 taken 11 times.
22 << QStringLiteral("holding %1 rows until the next folder has content").arg(m_heldRows.size());
689 11 }
690
691 16 void QuickThreadListModel::swapHeldRowsForContent() {
692
9/10
✓ Branch 2 → 3 taken 16 times.
✗ Branch 2 → 8 not taken.
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 6 taken 12 times.
✓ Branch 5 → 8 taken 1 time.
✓ Branch 5 → 9 taken 3 times.
✓ Branch 7 → 8 taken 7 times.
✓ Branch 7 → 9 taken 5 times.
✓ Branch 10 → 11 taken 8 times.
✓ Branch 10 → 12 taken 8 times.
16 if (!m_holding || (m_indexed ? m_indexRows.isEmpty() : m_rows.isEmpty())) return;
693 8 beginResetModel();
694 8 m_holding = false;
695 8 m_heldRows.clear(); m_heldExpanded.clear();
696 8 endResetModel();
697 }
698
699 1507 void QuickThreadListModel::releaseHeldRows() {
700
2/2
✓ Branch 2 → 3 taken 1505 times.
✓ Branch 2 → 4 taken 2 times.
1507 if (!m_holding) return;
701 2 beginResetModel();
702 2 m_holding = false;
703 2 m_heldRows.clear(); m_heldExpanded.clear();
704 2 m_dateTextCache.clear(); m_chipCache.clear();
705 2 endResetModel();
706 }
707
708 1537 void QuickThreadListModel::leaveIndexedFolder() {
709
2/2
✓ Branch 2 → 3 taken 1530 times.
✓ Branch 2 → 4 taken 7 times.
1537 if (!m_indexed) return;
710 7 ++m_indexExclusionGeneration;
711 7 m_indexExclusionsDirty = false;
712 7 m_collapsedSpans.clear();
713 7 m_collapsedSpanRows = 0;
714 7 retainIndexedSnapshot();
715 7 cancelIndexedRequest();
716 // Held rows keep showing: the underlying rows are already empty.
717
2/2
✓ Branch 7 → 8 taken 4 times.
✓ Branch 7 → 9 taken 3 times.
7 if (!m_holding) beginResetModel();
718 7 m_indexed = false; m_indexRows.clear(); m_rows.clear(); m_rowByKey.clear();
719 7 m_indexRevision = -1; m_indexTotalCount = m_indexUnreadCount = -1;
720 7 m_indexCatchUps = 0;
721 7 m_dateTextCache.clear(); m_chipCache.clear();
722
2/2
✓ Branch 14 → 15 taken 4 times.
✓ Branch 14 → 16 taken 3 times.
7 if (!m_holding) endResetModel();
723 }
724
725 162 void QuickThreadListModel::configureIndexedFolder(const QString &database, qint64 folder,
726 quint32 epoch, qint64 revision, qint64 anchorUid,
727 const QSet<qint64> &excludedUids) {
728
3/4
✓ Branch 4 → 5 taken 130 times.
✗ Branch 4 → 10 not taken.
✓ Branch 5 → 6 taken 121 times.
✓ Branch 5 → 10 taken 9 times.
130 const bool same = m_indexed && m_indexDatabase == database && m_indexRequest.folderId == folder &&
729
4/6
✓ Branch 2 → 3 taken 130 times.
✓ Branch 2 → 10 taken 32 times.
✓ Branch 6 → 7 taken 121 times.
✗ Branch 6 → 10 not taken.
✓ Branch 8 → 9 taken 121 times.
✗ Branch 8 → 10 not taken.
292 m_indexRequest.uidValidity == epoch && m_indexRequest.account == m_accountId;
730 // Each newly requested revision grants a fresh catch-up budget: the
731 // budget bounds work per request, so pages never loop on their own, but
732 // the last request of a long write storm is still honoured.
733
4/4
✓ Branch 11 → 12 taken 121 times.
✓ Branch 11 → 14 taken 41 times.
✓ Branch 12 → 13 taken 109 times.
✓ Branch 12 → 14 taken 12 times.
162 if (same && revision > m_indexWantedRevision) m_indexCatchUps = 0;
734
2/2
✓ Branch 14 → 15 taken 121 times.
✓ Branch 14 → 17 taken 41 times.
162 m_indexWantedRevision = same ? qMax(m_indexWantedRevision, revision) : revision;
735
2/2
✓ Branch 18 → 19 taken 121 times.
✓ Branch 18 → 26 taken 41 times.
162 if (same) {
736 // A journaled move changes page membership without advancing the
737 // thread revision. A pending page was captured with the old exclusion
738 // set and must not re-admit a visually removed mail when it lands.
739
1/2
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 23 taken 121 times.
121 if (m_indexRequest.excludedUids != excludedUids) {
740 ✗ refreshIndexedExclusions(excludedUids);
741 ✗ return;
742 }
743
4/4
✓ Branch 23 → 24 taken 16 times.
✓ Branch 23 → 25 taken 105 times.
✓ Branch 24 → 25 taken 3 times.
✓ Branch 24 → 26 taken 13 times.
121 if (m_indexPending || m_indexRevision == revision) return;
744 }
745 54 retainIndexedSnapshot();
746
6/6
✓ Branch 27 → 28 taken 41 times.
✓ Branch 27 → 31 taken 13 times.
✓ Branch 29 → 30 taken 7 times.
✓ Branch 29 → 31 taken 34 times.
✓ Branch 32 → 33 taken 7 times.
✓ Branch 32 → 34 taken 47 times.
54 if (!same && enterIndexedFolder(database, folder, epoch, revision, excludedUids))
747 7 return;
748 // A first window must refill from the top so newly arrived or re-admitted
749 // mails before its old head appear. A deeper rolling window stays at its
750 // published page head; scroll continuity is re-pinned at publication.
751
2/2
✓ Branch 34 → 35 taken 13 times.
✓ Branch 34 → 40 taken 34 times.
94 loadIndexed(PageMode::Replace,
752
1/4
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 40 taken 13 times.
✗ Branch 37 → 38 not taken.
✗ Branch 37 → 40 not taken.
13 same && m_indexHasBefore && !m_indexRows.isEmpty()
753
2/2
✓ Branch 40 → 41 taken 13 times.
✓ Branch 40 → 42 taken 34 times.
47 ? m_indexRows.first().header.uid : (same ? qint64(0) : anchorUid));
754 }
755
756
757 // New folder (or new epoch/account): resets the projection, restores a
758 // retained snapshot or provisional rows and starts the first page. Returns
759 // true when it already published or started the page load itself.
760 41 bool QuickThreadListModel::enterIndexedFolder(const QString &database, qint64 folder,
761 quint32 epoch, qint64 revision,
762 const QSet<qint64> &excludedUids) {
763 41 ++m_indexExclusionGeneration;
764 41 m_indexExclusionsDirty = false;
765 41 bool exclusionsChangedSinceSnapshot = false;
766
1/1
✓ Branch 2 → 3 taken 41 times.
41 cancelIndexedRequest();
767 41 m_indexCatchUps = 0;
768 41 m_collapsedSpans.clear();
769 41 m_collapsedSpanRows = 0;
770
1/1
✓ Branch 4 → 5 taken 41 times.
41 QList<ThreadProjectionRow> provisional = provisionalIndexRows(folder, excludedUids);
771
3/3
✓ Branch 5 → 6 taken 37 times.
✓ Branch 5 → 7 taken 4 times.
✓ Branch 6 → 7 taken 37 times.
41 if (!m_holding) beginResetModel();
772
2/2
✓ Branch 7 → 8 taken 41 times.
✓ Branch 8 → 9 taken 41 times.
41 m_indexed = true; m_rows.clear(); m_indexRows.clear(); m_rowByKey.clear();
773 41 m_indexRevision = -1; m_indexTotalCount = m_indexUnreadCount = -1;
774 41 m_viewportUid = 0; m_viewportOffset = 0;
775 41 m_indexDatabase = database;
776 41 m_indexRequest = {};
777 41 m_indexRequest.account = m_accountId; m_indexRequest.folderId = folder; m_indexRequest.uidValidity = epoch;
778 41 m_indexRequest.sort = m_sort;
779 41 m_indexRequest.excludedUids = excludedUids;
780 41 m_indexHasBefore = m_indexHasAfter = false;
781
1/1
✓ Branch 20 → 21 taken 41 times.
41 const auto *cached = m_indexSnapshots.object(indexedKey());
782
3/6
✓ Branch 24 → 25 taken 7 times.
✗ Branch 24 → 33 not taken.
✓ Branch 26 → 27 taken 7 times.
✗ Branch 26 → 33 not taken.
✓ Branch 28 → 29 taken 7 times.
✗ Branch 28 → 33 not taken.
14 if (cached && cached->revision == revision && cached->request.sort == m_sort &&
783 7 cached->request.filter == m_filterText &&
784
6/8
✓ Branch 23 → 24 taken 7 times.
✓ Branch 23 → 33 taken 34 times.
✓ Branch 29 → 30 taken 7 times.
✗ Branch 29 → 33 not taken.
✓ Branch 30 → 31 taken 7 times.
✗ Branch 30 → 33 not taken.
✓ Branch 34 → 35 taken 7 times.
✓ Branch 34 → 63 taken 34 times.
55 cached->request.unread == m_showUnread && cached->request.starred == m_showStarred &&
785
1/2
✓ Branch 31 → 32 taken 7 times.
✗ Branch 31 → 33 not taken.
7 cached->request.attachments == m_showAttachments) {
786 // Move-journal membership can change without advancing the thread
787 // revision. Never re-admit a newly excluded row, and refill rows
788 // that an older snapshot excluded once its move has failed/undone.
789 7 exclusionsChangedSinceSnapshot = cached->request.excludedUids != excludedUids;
790 7 m_indexRows = cached->rows;
791
1/2
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 47 taken 7 times.
7 if (!excludedUids.isEmpty())
792 ✗ for (int i = m_indexRows.size() - 1; i >= 0; --i)
793 ✗ if (excludedUids.contains(m_indexRows[i].header.uid)) m_indexRows.removeAt(i);
794
1/1
✓ Branch 47 → 48 taken 7 times.
7 m_indexRequest = cached->request;
795 7 m_indexRequest.excludedUids = excludedUids;
796 7 m_indexRevision = revision; m_indexHasBefore = cached->before; m_indexHasAfter = cached->after;
797 7 m_indexTotalCount = cached->totalCount;
798 7 m_indexUnreadCount = cached->unreadCount;
799 7 m_viewportUid = cached->viewportUid; m_viewportOffset = cached->viewportOffset;
800
1/1
✓ Branch 49 → 50 taken 7 times.
7 reindexRows();
801
1/2
✓ Branch 50 → 51 taken 7 times.
✗ Branch 50 → 55 not taken.
14 if (m_viewportUid > 0 &&
802
2/4
✗ Branch 53 → 54 not taken.
✓ Branch 53 → 55 taken 7 times.
✗ Branch 56 → 57 not taken.
✓ Branch 56 → 68 taken 7 times.
14 !m_rowByKey.contains({folder, m_viewportUid})) {
803 ✗ m_viewportUid = m_indexRows.isEmpty() ? 0 : m_indexRows.first().header.uid;
804 ✗ m_viewportOffset = 0;
805 }
806
2/2
✓ Branch 64 → 65 taken 1 time.
✓ Branch 64 → 68 taken 33 times.
34 } else if (!provisional.isEmpty()) {
807 1 m_indexRows = std::move(provisional);
808
1/1
✓ Branch 67 → 68 taken 1 time.
1 reindexRows();
809 }
810
3/3
✓ Branch 68 → 69 taken 37 times.
✓ Branch 68 → 70 taken 4 times.
✓ Branch 69 → 71 taken 37 times.
41 if (!m_holding) endResetModel();
811
1/1
✓ Branch 70 → 71 taken 4 times.
4 else swapHeldRowsForContent();
812
2/2
✓ Branch 71 → 72 taken 7 times.
✓ Branch 71 → 89 taken 34 times.
41 if (m_indexRevision == revision) {
813
1/1
✓ Branch 74 → 75 taken 7 times.
7 emit viewportRestoreRequested(WorkspaceState::encode({m_accountId,folder,m_viewportUid}),m_viewportOffset);
814
1/1
✓ Branch 77 → 78 taken 7 times.
7 emit projectionChanged();
815
2/3
✓ Branch 78 → 79 taken 7 times.
✗ Branch 78 → 81 not taken.
✓ Branch 79 → 80 taken 7 times.
7 if (!exclusionsChangedSinceSnapshot) { emit projectionReady(); return true; }
816 // Keep the safe published rows mounted while the current move
817 // exclusions refill the page on the worker.
818 ✗ loadIndexed(PageMode::Replace,
819 ✗ m_indexHasBefore && !m_indexRows.isEmpty()
820 ✗ ? m_indexRows.first().header.uid : qint64(0),
821 false);
822 ✗ return true;
823 }
824 34 return false;
825 89 }
826
827 // A folder that outgrows the flat thread list (first sync crossing
828 // the index threshold) keeps its visible rows as provisional index
829 // rows until the first indexed page replaces them; otherwise the
830 // list would render one empty frame. They carry no cursor, and
831 // paging stays off (no before/after) until that page lands.
832 41 QList<ThreadProjectionRow> QuickThreadListModel::provisionalIndexRows(
833 qint64 folder, const QSet<qint64> &excludedUids) const {
834 41 QList<ThreadProjectionRow> provisional;
835
7/8
✓ Branch 2 → 3 taken 32 times.
✓ Branch 2 → 10 taken 9 times.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 10 taken 31 times.
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 10 not taken.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 39 taken 40 times.
42 if (!m_indexed && !m_rows.isEmpty() && m_rows.first().source &&
836
1/2
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 10 not taken.
1 m_rows.first().source->header.folderId == folder) {
837
1/1
✓ Branch 13 → 14 taken 1 time.
1 provisional.reserve(m_rows.size());
838
2/2
✓ Branch 37 → 17 taken 150 times.
✓ Branch 37 → 38 taken 1 time.
151 for (const auto &row : std::as_const(m_rows)) {
839
5/6
✓ Branch 18 → 19 taken 150 times.
✗ Branch 18 → 21 not taken.
✓ Branch 20 → 21 taken 1 time.
✓ Branch 20 → 22 taken 149 times.
✓ Branch 23 → 24 taken 1 time.
✓ Branch 23 → 25 taken 149 times.
150 if (!row.source || excludedUids.contains(row.source->header.uid)) continue;
840 149 ThreadProjectionRow converted;
841 149 converted.header = row.source->header;
842
2/2
✓ Branch 27 → 28 taken 134 times.
✓ Branch 27 → 29 taken 15 times.
149 converted.parentUid = row.source->parent ? row.source->parent->header.uid : 0;
843 149 converted.depth = row.depth;
844 149 converted.descendants = row.source->cachedDescendants;
845 149 converted.unreadDescendants = row.source->cachedUnreadDescendants;
846
1/1
✓ Branch 31 → 32 taken 149 times.
149 provisional.append(std::move(converted));
847 149 }
848 }
849 41 return provisional;
850 ✗ }
851
852 111 void QuickThreadListModel::loadIndexed(PageMode mode, qint64 anchorUid, bool restoreViewport) {
853
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 111 times.
111 if (!m_indexed) return;
854
1/1
✓ Branch 4 → 5 taken 111 times.
111 cancelIndexedRequest();
855 111 m_indexLoadAnnounced = std::exchange(m_nextLoadAnnounced, false);
856
1/1
✓ Branch 6 → 7 taken 111 times.
111 auto request = indexedPageRequest(mode, anchorUid);
857 111 m_indexPending = true;
858
1/1
✓ Branch 7 → 8 taken 111 times.
111 emit projectionChanged();
859 111 const auto generation = m_indexGeneration;
860
2/2
✓ Branch 8 → 9 taken 33 times.
✓ Branch 8 → 10 taken 78 times.
111 const qreal viewportOffset = request.anchorUid == m_viewportUid ? m_viewportOffset : 0;
861
1/1
✓ Branch 11 → 12 taken 111 times.
111 auto cancelled = std::make_shared<std::atomic_bool>(false);
862 111 m_indexCancelled = cancelled;
863 // P90.2: at most one orphan may keep building in the background; a
864 // second orphan makes the oldest really cancellable so rapid folder
865 // switching cannot pile up background work.
866
1/1
✓ Branch 13 → 14 taken 111 times.
111 m_indexPool.setMaxThreadCount(2);
867
1/2
✗ Branch 20 → 15 not taken.
✓ Branch 20 → 21 taken 111 times.
111 while (m_activeRequests.size() >= 2) {
868 ✗ m_activeRequests.first()->store(true);
869 ✗ m_activeRequests.removeFirst();
870 }
871
1/1
✓ Branch 21 → 22 taken 111 times.
111 m_activeRequests.append(cancelled);
872 111 QElapsedTimer timing;
873 111 timing.start();
874
1/1
✓ Branch 23 → 24 taken 111 times.
111 QPointer<QuickThreadListModel> self(this);
875
2/10
✓ Branch 25 → 26 taken 111 times.
✓ Branch 28 → 29 taken 111 times.
✗ Branch 39 → 40 not taken.
✗ Branch 39 → 41 not taken.
✗ Branch 42 → 43 not taken.
✗ Branch 42 → 44 not taken.
✗ Branch 45 → 46 not taken.
✗ Branch 45 → 47 not taken.
✗ Branch 48 → 49 not taken.
✗ Branch 48 → 50 not taken.
111 m_indexPool.start([self, generation, request, mode, viewportOffset, restoreViewport, database = m_indexDatabase, cancelled, timing = timing]() mutable {
876 111 ThreadProjectionPage result;
877 111 int catchUps = 0;
878
1/2
✓ Branch 4 → 5 taken 111 times.
✗ Branch 4 → 10 not taken.
111 if (!cancelled->load())
879
1/1
✓ Branch 6 → 7 taken 111 times.
111 result = buildIndexedPage(request, mode, database, *cancelled, catchUps);
880
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 19 taken 111 times.
111 if (cancelled->load()) {
881 // Really-cancelled requests never reach the model; forget the
882 // token so the bounded orphan accounting stays exact.
883 ✗ QMetaObject::invokeMethod(self, [self, cancelled] {
884 ✗ if (self) self->forgetActiveRequest(cancelled);
885 ✗ }, Qt::QueuedConnection);
886 ✗ return;
887 }
888 111 const qint64 elapsedMicros = timing.nsecsElapsed() / 1000;
889
2/10
✓ Branch 21 → 22 taken 111 times.
✓ Branch 27 → 28 taken 111 times.
✗ Branch 45 → 46 not taken.
✗ Branch 45 → 47 not taken.
✗ Branch 48 → 49 not taken.
✗ Branch 48 → 50 not taken.
✗ Branch 51 → 52 not taken.
✗ Branch 51 → 53 not taken.
✗ Branch 54 → 55 not taken.
✗ Branch 54 → 56 not taken.
333 QMetaObject::invokeMethod(self, [self, generation, request, mode, viewportOffset, restoreViewport,
890 222 result = std::move(result), elapsedMicros, catchUps, cancelled, database] {
891
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 111 times.
111 if (!self) return; // S106-307: the guard comes first
892 111 self->forgetActiveRequest(cancelled);
893
1/1
✓ Branch 10 → 11 taken 111 times.
222 self->finishIndexedPage(generation, request, mode, viewportOffset, restoreViewport,
894 111 std::move(result), elapsedMicros, catchUps, database);
895 }, Qt::QueuedConnection);
896
1/2
✓ Branch 31 → 32 taken 111 times.
✗ Branch 31 → 34 not taken.
111 });
897 111 }
898
899 111 ThreadProjectionRequest QuickThreadListModel::indexedPageRequest(PageMode mode, qint64 anchorUid) {
900 111 m_indexRequest.filter = m_filterText; m_indexRequest.unread = m_showUnread;
901 111 m_indexRequest.starred = m_showStarred; m_indexRequest.attachments = m_showAttachments;
902 111 m_indexRequest.sort = m_sort;
903 111 auto request = m_indexRequest;
904 111 request.after = {}; request.anchorUid = anchorUid;
905
4/4
✓ Branch 8 → 9 taken 24 times.
✓ Branch 8 → 11 taken 87 times.
✓ Branch 9 → 10 taken 22 times.
✓ Branch 9 → 11 taken 2 times.
111 request.expectedRevision = mode == PageMode::Replace || mode == PageMode::Last ? -1 : m_indexRevision;
906
4/4
✓ Branch 12 → 13 taken 103 times.
✓ Branch 12 → 14 taken 8 times.
✓ Branch 13 → 14 taken 2 times.
✓ Branch 13 → 15 taken 101 times.
111 request.before = mode == PageMode::Prepend || mode == PageMode::Last;
907 // Nothing shown yet: an older, consistent page is better than none.
908
4/4
✓ Branch 16 → 17 taken 87 times.
✓ Branch 16 → 22 taken 24 times.
✓ Branch 18 → 19 taken 43 times.
✓ Branch 18 → 22 taken 44 times.
154 request.acceptOlderRevision = mode == PageMode::Replace && m_indexRows.isEmpty() &&
909
2/2
✓ Branch 20 → 21 taken 29 times.
✓ Branch 20 → 22 taken 14 times.
43 m_acceptIndexedIdentity != nullptr;
910
7/8
✓ Branch 23 → 24 taken 97 times.
✓ Branch 23 → 25 taken 14 times.
✓ Branch 24 → 25 taken 8 times.
✓ Branch 24 → 28 taken 89 times.
✓ Branch 26 → 27 taken 22 times.
✗ Branch 26 → 28 not taken.
✓ Branch 29 → 30 taken 22 times.
✓ Branch 29 → 36 taken 89 times.
111 if ((mode == PageMode::Append || mode == PageMode::Prepend) && !m_indexRows.isEmpty())
911
5/5
✓ Branch 30 → 31 taken 14 times.
✓ Branch 30 → 33 taken 8 times.
✓ Branch 31 → 32 taken 14 times.
✓ Branch 33 → 34 taken 8 times.
✓ Branch 35 → 36 taken 22 times.
22 request.after = mode == PageMode::Append ? m_indexRows.last().cursor : m_indexRows.first().cursor;
912 111 return request;
913 111 }
914
915 // The worker side of a page load: opens the thread-local projection index
916 // for the database/account and reads the page, with the P90.1 bounded
917 // catch-up of Replace/Last reads inside the same pending sequence.
918 111 ThreadProjectionPage QuickThreadListModel::buildIndexedPage(const ThreadProjectionRequest &request,
919 PageMode mode, const QString &database,
920 std::atomic_bool &cancelled, int &catchUps) {
921 111 ThreadProjectionPage result;
922 // QThreadPool can run a superseded folder build beside foreground
923 // work. Each resident thread owns its own connection and TEMP tables.
924
2/2
✓ Branch 2 → 3 taken 34 times.
✓ Branch 2 → 4 taken 77 times.
111 static thread_local std::unique_ptr<ThreadProjectionIndex> index;
925
2/2
✓ Branch 4 → 5 taken 34 times.
✓ Branch 4 → 6 taken 77 times.
111 static thread_local QString indexDatabase;
926
2/2
✓ Branch 6 → 7 taken 34 times.
✓ Branch 6 → 8 taken 77 times.
111 static thread_local QString indexAccount;
927
3/4
✓ Branch 9 → 10 taken 111 times.
✗ Branch 9 → 17 not taken.
✓ Branch 18 → 19 taken 34 times.
✓ Branch 18 → 35 taken 77 times.
222 if (!cancelled.load() &&
928
4/6
✓ Branch 11 → 12 taken 77 times.
✓ Branch 11 → 16 taken 34 times.
✓ Branch 13 → 14 taken 77 times.
✗ Branch 13 → 16 not taken.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 77 times.
111 (!index || indexDatabase != database || indexAccount != request.account)) {
929 34 index.reset();
930 34 indexDatabase.clear();
931 34 indexAccount.clear();
932
1/1
✓ Branch 22 → 23 taken 34 times.
34 index = std::make_unique<ThreadProjectionIndex>();
933
2/3
✓ Branch 26 → 27 taken 34 times.
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 33 taken 34 times.
34 if (!index->open(database)) {
934 ✗ result.error = index->error();
935 ✗ index.reset();
936 } else {
937 34 indexDatabase = database;
938 34 indexAccount = request.account;
939 }
940 }
941
3/6
✓ Branch 36 → 37 taken 111 times.
✗ Branch 36 → 40 not taken.
✓ Branch 38 → 39 taken 111 times.
✗ Branch 38 → 40 not taken.
✓ Branch 41 → 42 taken 111 times.
✗ Branch 41 → 60 not taken.
111 if (!cancelled.load() && index) {
942
1/1
✓ Branch 42 → 43 taken 111 times.
111 auto attempt = request;
943 // P90.1: a revision change during a Replace/Last read is caught
944 // up inside the same pending sequence instead of bouncing the
945 // caller through stale/restart flapping. Cursor-based pages
946 // (Append/Prepend) cannot drop their source revision and keep
947 // the explicit stale contract for their owner.
948 while (true) {
949
1/1
✓ Branch 44 → 45 taken 120 times.
120 result = index->page(attempt, cancelled);
950
1/2
✓ Branch 49 → 50 taken 10 times.
✗ Branch 49 → 53 not taken.
10 if (result.outcome != ThreadProjectionPage::Outcome::Stale ||
951
1/2
✗ Branch 50 → 51 not taken.
✓ Branch 50 → 52 taken 10 times.
20 cancelled.load() ||
952
4/6
✓ Branch 47 → 48 taken 10 times.
✓ Branch 47 → 53 taken 110 times.
✗ Branch 51 → 52 not taken.
✗ Branch 51 → 53 not taken.
✓ Branch 55 → 56 taken 111 times.
✓ Branch 55 → 57 taken 9 times.
130 (mode != PageMode::Replace && mode != PageMode::Last) ||
953
2/2
✓ Branch 52 → 53 taken 1 time.
✓ Branch 52 → 54 taken 9 times.
10 ++catchUps > MaximumCatchUpAttempts)
954 111 break;
955 9 attempt.expectedRevision = -1;
956 }
957 111 }
958 111 return result;
959 ✗ }
960
961 111 void QuickThreadListModel::finishIndexedPage(quint64 generation, const ThreadProjectionRequest &request,
962 PageMode mode, qreal viewportOffset, bool restoreViewport,
963 ThreadProjectionPage result, qint64 elapsedMicros,
964 int catchUps, const QString &database) {
965
3/4
✓ Branch 2 → 3 taken 101 times.
✓ Branch 2 → 4 taken 10 times.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 101 times.
111 if (m_indexGeneration != generation || !m_indexed) {
966 10 retainOrphanedPage(request, mode, result, database);
967 10 return;
968 }
969 // P90.6: exactly one structured completion line per projection
970 // request; identities are folder ids only, never mail content.
971
4/4
✓ Branch 6 → 7 taken 101 times.
✓ Branch 10 → 11 taken 101 times.
✓ Branch 54 → 8 taken 101 times.
✓ Branch 54 → 55 taken 101 times.
202 qCDebug(lcThreadProjection).noquote()
972 202 << QStringLiteral("page mode=%1 outcome=%2 revision=%3 rows=%4 rebuilt=%5 catchups=%6 us=%7 anchor=%8 first=%9")
973
3/3
✓ Branch 16 → 17 taken 101 times.
✓ Branch 18 → 19 taken 101 times.
✓ Branch 20 → 21 taken 101 times.
404 .arg(int(mode)).arg(int(result.outcome)).arg(result.revision)
974
2/2
✓ Branch 23 → 24 taken 101 times.
✓ Branch 25 → 26 taken 101 times.
303 .arg(result.rows.size()).arg(result.rebuiltHeaders)
975
3/3
✓ Branch 27 → 28 taken 101 times.
✓ Branch 29 → 30 taken 101 times.
✓ Branch 31 → 32 taken 101 times.
404 .arg(catchUps).arg(elapsedMicros).arg(request.anchorUid)
976
5/5
✓ Branch 34 → 35 taken 2 times.
✓ Branch 34 → 36 taken 99 times.
✓ Branch 36 → 37 taken 99 times.
✓ Branch 38 → 39 taken 101 times.
✓ Branch 39 → 40 taken 101 times.
202 .arg(result.rows.isEmpty() ? qint64(0) : result.rows.first().header.uid);
977 const bool revisionRejected =
978
4/4
✓ Branch 55 → 56 taken 100 times.
✓ Branch 55 → 61 taken 1 time.
✓ Branch 57 → 58 taken 52 times.
✓ Branch 57 → 61 taken 48 times.
153 result.outcome == ThreadProjectionPage::Outcome::Ready && m_acceptIndexed &&
979
2/2
✓ Branch 59 → 60 taken 18 times.
✓ Branch 59 → 61 taken 34 times.
52 !m_acceptIndexed(request, result.revision);
980 // A complete page of a slightly older revision beats an empty
981 // list: while a fast sync keeps advancing the revision, every
982 // build was rejected until the writes stopped and the list stayed
983 // empty. Publish it when nothing is shown yet (same account,
984 // folder and epoch) and continue with the bounded catch-up.
985 // A newer revision was configured while this page was pending
986 // (configureIndexedFolder() does not interrupt a pending page):
987 // publish the page, it is newer than the shown one, and catch up.
988 302 const bool behindWanted = result.outcome == ThreadProjectionPage::Outcome::Ready &&
989
6/6
✓ Branch 63 → 64 taken 82 times.
✓ Branch 63 → 70 taken 18 times.
✓ Branch 64 → 65 taken 24 times.
✓ Branch 64 → 66 taken 58 times.
✓ Branch 65 → 66 taken 2 times.
✓ Branch 65 → 70 taken 22 times.
100 !revisionRejected && (mode == PageMode::Replace || mode == PageMode::Last) &&
990
3/6
✓ Branch 62 → 63 taken 100 times.
✓ Branch 62 → 70 taken 1 time.
✗ Branch 66 → 67 not taken.
✓ Branch 66 → 70 taken 60 times.
✗ Branch 67 → 68 not taken.
✗ Branch 67 → 70 not taken.
201 result.revision < m_indexWantedRevision && result.revision >= m_indexRevision &&
991 ✗ m_indexCatchUps < MaximumCatchUpAttempts;
992
5/6
✓ Branch 71 → 72 taken 101 times.
✗ Branch 71 → 80 not taken.
✓ Branch 72 → 73 taken 18 times.
✓ Branch 72 → 81 taken 83 times.
✓ Branch 74 → 75 taken 10 times.
✓ Branch 74 → 81 taken 8 times.
111 const bool publishThenCatchUp = behindWanted || (revisionRejected && m_indexRows.isEmpty() &&
993
3/4
✓ Branch 75 → 76 taken 9 times.
✓ Branch 75 → 81 taken 1 time.
✓ Branch 77 → 78 taken 9 times.
✗ Branch 77 → 81 not taken.
19 m_indexCatchUps < MaximumCatchUpAttempts &&
994
2/2
✓ Branch 79 → 80 taken 1 time.
✓ Branch 79 → 81 taken 8 times.
18 m_acceptIndexedIdentity && m_acceptIndexedIdentity(request));
995
2/2
✓ Branch 82 → 83 taken 100 times.
✓ Branch 82 → 93 taken 1 time.
101 if (!publishThenCatchUp &&
996
4/4
✓ Branch 83 → 84 taken 99 times.
✓ Branch 83 → 85 taken 1 time.
✓ Branch 84 → 85 taken 17 times.
✓ Branch 84 → 93 taken 82 times.
100 (result.outcome == ThreadProjectionPage::Outcome::Stale || revisionRejected)) {
997 // The GUI-side revision fence rejected a worker-fresh page:
998 // continue the same pending sequence with one bounded
999 // Replace catch-up instead of dropping pending first.
1000
2/2
✓ Branch 85 → 86 taken 17 times.
✓ Branch 85 → 90 taken 1 time.
18 if (result.outcome == ThreadProjectionPage::Outcome::Ready &&
1001
2/2
✓ Branch 86 → 87 taken 15 times.
✓ Branch 86 → 90 taken 2 times.
17 m_indexCatchUps < MaximumCatchUpAttempts) {
1002 15 ++m_indexCatchUps;
1003 15 m_nextLoadAnnounced = m_indexLoadAnnounced;
1004 15 loadIndexed(PageMode::Replace, catchUpAnchor(request.anchorUid), false);
1005 15 return;
1006 }
1007 3 m_indexPending = false;
1008 3 m_indexCatchUps = 0;
1009 3 emit projectionChanged(); emit projectionStale(); return;
1010 }
1011
2/2
✓ Branch 93 → 94 taken 82 times.
✓ Branch 93 → 95 taken 1 time.
83 if (!publishThenCatchUp) m_indexCatchUps = 0;
1012
1/2
✗ Branch 95 → 96 not taken.
✓ Branch 95 → 100 taken 83 times.
83 if (result.outcome != ThreadProjectionPage::Outcome::Ready) {
1013 ✗ releaseHeldRows();
1014 ✗ m_indexPending = false;
1015 ✗ emit projectionChanged(); emit projectionFailed(result.error); return;
1016 }
1017 83 const bool revisionChanged = result.revision != m_indexRevision;
1018 83 m_indexRevision = result.revision;
1019 83 m_indexTotalCount = result.totalCount;
1020 83 m_indexUnreadCount = result.unreadCount;
1021 83 bool replacedWithReset = false;
1022
4/4
✓ Branch 100 → 101 taken 24 times.
✓ Branch 100 → 102 taken 59 times.
✓ Branch 101 → 102 taken 2 times.
✓ Branch 101 → 104 taken 22 times.
83 if (mode == PageMode::Replace || mode == PageMode::Last)
1023 61 replacedWithReset = publishReplacePage(request, mode, result, revisionChanged);
1024 else
1025 22 publishWindowPage(mode, result);
1026 83 m_indexPending = false;
1027
6/6
✓ Branch 105 → 106 taken 33 times.
✓ Branch 105 → 109 taken 50 times.
✓ Branch 107 → 108 taken 32 times.
✓ Branch 107 → 109 taken 1 time.
✓ Branch 110 → 111 taken 32 times.
✓ Branch 110 → 112 taken 51 times.
83 if (replacedWithReset && !m_indexRows.isEmpty())
1028 32 repinViewportAfterReset(viewportOffset, restoreViewport);
1029
2/2
✓ Branch 112 → 113 taken 1 time.
✓ Branch 112 → 118 taken 82 times.
83 if (publishThenCatchUp) {
1030 // The rows stay visible while the current revision loads;
1031 // an older-revision snapshot is never retained.
1032 1 emit projectionChanged(); emit projectionReady();
1033 1 ++m_indexCatchUps;
1034 1 loadIndexed(PageMode::Replace, catchUpAnchor(request.anchorUid), false);
1035 1 return;
1036 }
1037 82 retainIndexedSnapshot();
1038 82 emit projectionChanged(); emit projectionReady();
1039 }
1040
1041 // P90.2: the owner moved on (folder switched away mid-flight). A completed
1042 // folder-open page is retained as a snapshot so the next visit restores it
1043 // without re-paying the cold build; restore admission still requires the
1044 // exact current revision, UIDVALIDITY and filter state.
1045 10 void QuickThreadListModel::retainOrphanedPage(const ThreadProjectionRequest &request, PageMode mode,
1046 const ThreadProjectionPage &result,
1047 const QString &database) {
1048 30 if (result.outcome == ThreadProjectionPage::Outcome::Ready &&
1049
4/6
✓ Branch 2 → 3 taken 10 times.
✗ Branch 2 → 7 not taken.
✓ Branch 3 → 4 taken 10 times.
✗ Branch 3 → 7 not taken.
✓ Branch 8 → 9 taken 9 times.
✓ Branch 8 → 29 taken 1 time.
20 !request.after.valid &&
1050
2/2
✓ Branch 5 → 6 taken 9 times.
✓ Branch 5 → 7 taken 1 time.
10 !result.rows.isEmpty()) {
1051 9 const qint64 bytes = indexRowsBytes(result.rows) +
1052 9 request.collapsed.size()*32 + sizeof(IndexSnapshot);
1053
1/2
✓ Branch 12 → 13 taken 9 times.
✗ Branch 12 → 29 not taken.
9 if (bytes <= qint64(m_indexSnapshots.maxCost())*1024) {
1054 9 const bool last = mode == PageMode::Last;
1055 18 auto *retained = new IndexSnapshot{result.rows, request, result.revision,
1056 9 result.totalCount, result.unreadCount,
1057 9 last ? result.hasMore : result.hasBefore,
1058 9 last ? request.anchorUid > 0 : result.hasMore,
1059
4/11
✓ Branch 15 → 16 taken 9 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 9 times.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 9 times.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 9 times.
✗ Branch 30 → 31 not taken.
✗ Branch 30 → 32 not taken.
✗ Branch 33 → 34 not taken.
✗ Branch 33 → 35 not taken.
27 result.rows.first().header.uid, 0};
1060
1/1
✓ Branch 26 → 27 taken 9 times.
9 m_indexSnapshots.insert(snapshotKey(database, request),
1061
1/1
✓ Branch 25 → 26 taken 9 times.
9 retained, int((bytes+1023)/1024));
1062 }
1063 }
1064 10 }
1065
1066 // Replace/Last publication: same identities → content update, a small
1067 // structural change → row diff, otherwise a reset. Returns true on reset.
1068 61 bool QuickThreadListModel::publishReplacePage(const ThreadProjectionRequest &request, PageMode mode,
1069 ThreadProjectionPage &result, bool revisionChanged) {
1070 61 bool replacedWithReset = false;
1071
2/2
✓ Branch 2 → 3 taken 41 times.
✓ Branch 2 → 5 taken 20 times.
61 if (revisionChanged) {
1072 41 m_collapsedSpans.clear();
1073 41 m_collapsedSpanRows = 0;
1074 }
1075 // P90.2: a flags/labels refresh often changes the revision
1076 // without changing the visible page's identities or order.
1077 // Keep the delegates and viewport alive in that case; only
1078 // notify rows whose visible roles actually changed.
1079 // A held previous folder is replaced by this page even when
1080 // the page is empty: it is the new folder's content.
1081
4/4
✓ Branch 5 → 6 taken 60 times.
✓ Branch 5 → 10 taken 1 time.
✓ Branch 8 → 9 taken 22 times.
✓ Branch 8 → 10 taken 38 times.
61 bool sameRows = !m_holding && m_indexRows.size() == result.rows.size();
1082
6/6
✓ Branch 19 → 20 taken 1772 times.
✓ Branch 19 → 23 taken 51 times.
✓ Branch 21 → 22 taken 1762 times.
✓ Branch 21 → 23 taken 10 times.
✓ Branch 24 → 12 taken 1762 times.
✓ Branch 24 → 25 taken 61 times.
1823 for (int row = 0; sameRows && row < result.rows.size(); ++row) {
1083 1762 const auto &before = m_indexRows.at(row).header;
1084 1762 const auto &after = result.rows.at(row).header;
1085
3/4
✓ Branch 14 → 15 taken 1762 times.
✗ Branch 14 → 17 not taken.
✓ Branch 15 → 16 taken 1750 times.
✓ Branch 15 → 17 taken 12 times.
1762 sameRows = before.folderId == after.folderId && before.uid == after.uid;
1086 }
1087
2/2
✓ Branch 25 → 26 taken 10 times.
✓ Branch 25 → 31 taken 51 times.
61 if (sameRows) {
1088
1/1
✓ Branch 28 → 29 taken 10 times.
10 publishIndexedContent(std::move(result.rows));
1089
6/6
✓ Branch 31 → 32 taken 50 times.
✓ Branch 31 → 35 taken 1 time.
✓ Branch 33 → 34 taken 18 times.
✓ Branch 33 → 35 taken 32 times.
✓ Branch 36 → 37 taken 18 times.
✓ Branch 36 → 42 taken 33 times.
51 } else if (!m_holding && applyIndexedRowDiff(result.rows)) {
1090
1/1
✓ Branch 39 → 40 taken 18 times.
18 publishIndexedContent(std::move(result.rows));
1091 } else {
1092 33 replacedWithReset = true;
1093 33 beginResetModel();
1094 33 m_holding = false;
1095 33 m_heldRows.clear(); m_heldExpanded.clear();
1096 33 m_dateTextCache.clear(); m_chipCache.clear();
1097 33 m_indexRows = std::move(result.rows); m_rowByKey.clear();
1098 33 reindexRows(); endResetModel();
1099 }
1100
2/2
✓ Branch 52 → 53 taken 2 times.
✓ Branch 52 → 54 taken 59 times.
61 m_indexHasBefore = mode == PageMode::Last ? result.hasMore : result.hasBefore;
1101
2/2
✓ Branch 55 → 56 taken 2 times.
✓ Branch 55 → 57 taken 59 times.
61 m_indexHasAfter = mode == PageMode::Last ? request.anchorUid>0 : result.hasMore;
1102 61 return replacedWithReset;
1103 }
1104
1105 // Append/Prepend publication into the rolling window.
1106 22 void QuickThreadListModel::publishWindowPage(PageMode mode, const ThreadProjectionPage &result) {
1107
2/2
✓ Branch 2 → 3 taken 14 times.
✓ Branch 2 → 4 taken 8 times.
22 if (mode == PageMode::Append) m_indexHasAfter = result.hasMore;
1108 8 else m_indexHasBefore = result.hasMore;
1109
1/2
✓ Branch 6 → 7 taken 22 times.
✗ Branch 6 → 23 not taken.
22 if (!result.rows.isEmpty()) {
1110
2/2
✓ Branch 7 → 8 taken 14 times.
✓ Branch 7 → 10 taken 8 times.
22 const int at = mode == PageMode::Append ? m_indexRows.size() : 0;
1111
1/1
✓ Branch 13 → 14 taken 22 times.
22 beginInsertRows({}, at, at+result.rows.size()-1);
1112
2/2
✓ Branch 14 → 15 taken 14 times.
✓ Branch 14 → 16 taken 8 times.
22 if (mode == PageMode::Append) m_indexRows.append(result.rows);
1113
1/1
✓ Branch 16 → 17 taken 8 times.
8 else m_indexRows = result.rows + m_indexRows;
1114 22 m_rowByKey.clear(); reindexRows(); endInsertRows();
1115 }
1116 // A rolling window bounds GUI work and retained header data,
1117 // including when the user scrolls through a million messages.
1118
5/6
✓ Branch 41 → 42 taken 22 times.
✓ Branch 41 → 46 taken 14 times.
✓ Branch 43 → 44 taken 22 times.
✗ Branch 43 → 47 not taken.
✓ Branch 48 → 24 taken 14 times.
✓ Branch 48 → 49 taken 22 times.
80 while (m_indexRows.size() > 1000 ||
1119
1/2
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 47 taken 22 times.
44 (m_indexRows.size() > 1 && indexRowsBytes(m_indexRows) > 4*1024*1024)) {
1120 14 const int count = qMin(200, int(m_indexRows.size())-1);
1121
2/2
✓ Branch 26 → 27 taken 7 times.
✓ Branch 26 → 29 taken 7 times.
14 const int at = mode == PageMode::Append ? 0 : m_indexRows.size()-count;
1122
1/1
✓ Branch 31 → 32 taken 14 times.
14 beginRemoveRows({}, at, at+count-1);
1123 14 m_indexRows.remove(at, count);
1124 14 m_indexRows.squeeze();
1125 14 m_rowByKey.clear(); reindexRows(); endRemoveRows();
1126
2/2
✓ Branch 37 → 38 taken 7 times.
✓ Branch 37 → 39 taken 7 times.
14 if (mode == PageMode::Append) m_indexHasBefore = true;
1127 7 else m_indexHasAfter = true;
1128 }
1129 22 }
1130
1131 32 void QuickThreadListModel::repinViewportAfterReset(qreal viewportOffset, bool restoreViewport) {
1132 // P90.16: a Replace-driven reset (post-action catch-up, stale
1133 // bounce, folder return) must not jump the viewport when the
1134 // tracked anchor survives: keep the remembered anchor and
1135 // re-pin pixel-exact. Only a vanished anchor falls back to
1136 // the first row — and only then gated by restoreViewport.
1137 32 const qint64 trackedUid = m_viewportUid;
1138 32 const qreal trackedOffset = m_viewportOffset;
1139 32 const int anchorRow = trackedUid > 0
1140
2/2
✓ Branch 2 → 3 taken 5 times.
✓ Branch 2 → 5 taken 27 times.
32 ? m_rowByKey.value({m_indexRequest.folderId, trackedUid}, -1) : -1;
1141
2/2
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 13 taken 31 times.
32 if (anchorRow >= 0) {
1142 1 m_viewportOffset = trackedOffset;
1143
1/1
✓ Branch 9 → 10 taken 1 time.
1 emit viewportRestoreRequested(WorkspaceState::encode({m_accountId,
1144 1 m_indexRequest.folderId, trackedUid}), trackedOffset);
1145 } else {
1146
1/1
✓ Branch 13 → 14 taken 31 times.
31 m_viewportUid = m_indexRows.first().header.uid;
1147 31 m_viewportOffset = viewportOffset;
1148
3/3
✓ Branch 14 → 15 taken 29 times.
✓ Branch 14 → 21 taken 2 times.
✓ Branch 17 → 18 taken 29 times.
60 if (restoreViewport) emit viewportRestoreRequested(WorkspaceState::encode({m_accountId,
1149 29 m_indexRequest.folderId,m_viewportUid}),viewportOffset);
1150 }
1151 62 }
1152
1153 // Replaces the published rows with `next`, which must hold the same
1154 // identities in the same order, and notifies only the roles that changed.
1155 28 void QuickThreadListModel::publishIndexedContent(QList<ThreadProjectionRow> next) {
1156 struct ChangedRow { int row; QList<int> roles; };
1157 28 QList<ChangedRow> changedRows;
1158
2/2
✓ Branch 56 → 3 taken 4656 times.
✓ Branch 56 → 57 taken 28 times.
4684 for (int row = 0; row < next.size(); ++row) {
1159 4656 const auto &before = m_indexRows.at(row);
1160 4656 const auto &after = next.at(row);
1161 4656 const MailKey key{after.header.folderId, after.header.uid};
1162 4656 QList<int> roles;
1163
1/3
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 4656 times.
✗ Branch 8 → 9 not taken.
4656 if (before.header.subject != after.header.subject) roles.append(Subject);
1164
1/3
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 4656 times.
✗ Branch 11 → 12 not taken.
4656 if (before.header.from != after.header.from) roles.append(Sender);
1165
2/3
✓ Branch 12 → 13 taken 4656 times.
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 16 taken 4656 times.
4656 if (before.header.date != after.header.date) {
1166 ✗ m_dateTextCache.remove(key);
1167 ✗ roles.append(Date);
1168 }
1169
1/3
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 4656 times.
✗ Branch 19 → 20 not taken.
4656 if (before.header.isSeen() != after.header.isSeen()) roles.append(Unread);
1170
3/3
✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 24 taken 4655 times.
✓ Branch 23 → 24 taken 1 time.
4656 if (before.header.isFlagged() != after.header.isFlagged()) roles.append(Starred);
1171
2/3
✓ Branch 24 → 25 taken 4656 times.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 29 taken 4656 times.
4656 if (before.header.labels != after.header.labels) {
1172 ✗ m_chipCache.remove(key);
1173 ✗ roles.append(Labels); roles.append(LabelChips);
1174 }
1175
1/3
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 4656 times.
✗ Branch 30 → 31 not taken.
4656 if (before.header.hasAttachments != after.header.hasAttachments) roles.append(Attachment);
1176 // S106-308: every role data() serves from the header is compared.
1177
3/3
✓ Branch 33 → 34 taken 1 time.
✓ Branch 33 → 35 taken 4655 times.
✓ Branch 34 → 35 taken 1 time.
4656 if (before.header.isAnswered() != after.header.isAnswered()) roles.append(Answered);
1178
3/3
✓ Branch 36 → 37 taken 400 times.
✓ Branch 36 → 38 taken 4256 times.
✓ Branch 37 → 38 taken 400 times.
4656 if (before.header.to != after.header.to) roles.append(Recipient);
1179
3/3
✓ Branch 38 → 39 taken 1 time.
✓ Branch 38 → 40 taken 4655 times.
✓ Branch 39 → 40 taken 1 time.
4656 if (before.header.size != after.header.size) roles.append(SizeText);
1180
3/3
✓ Branch 40 → 41 taken 50 times.
✓ Branch 40 → 42 taken 4606 times.
✓ Branch 41 → 42 taken 50 times.
4656 if (before.depth != after.depth) roles.append(Depth);
1181
2/2
✓ Branch 42 → 43 taken 59 times.
✓ Branch 42 → 46 taken 4597 times.
4656 if (before.descendants != after.descendants) {
1182
3/3
✓ Branch 43 → 44 taken 59 times.
✓ Branch 44 → 45 taken 59 times.
✓ Branch 45 → 46 taken 59 times.
59 roles.append(ThreadCount); roles.append(HasChildren); roles.append(Expanded);
1183 }
1184
3/3
✓ Branch 47 → 48 taken 471 times.
✓ Branch 47 → 53 taken 4185 times.
✓ Branch 50 → 51 taken 471 times.
5127 if (!roles.isEmpty()) changedRows.append({row, std::move(roles)});
1185 4656 }
1186 28 m_indexRows = std::move(next);
1187
2/2
✓ Branch 84 → 60 taken 22 times.
✓ Branch 84 → 85 taken 28 times.
50 for (int at = 0; at < changedRows.size();) {
1188 22 int end = at;
1189 471 while (end + 1 < changedRows.size() &&
1190
5/6
✓ Branch 63 → 64 taken 464 times.
✓ Branch 63 → 72 taken 7 times.
✓ Branch 66 → 67 taken 464 times.
✗ Branch 66 → 72 not taken.
✓ Branch 73 → 61 taken 449 times.
✓ Branch 73 → 74 taken 22 times.
935 changedRows.at(end + 1).row == changedRows.at(end).row + 1 &&
1191
3/3
✓ Branch 69 → 70 taken 464 times.
✓ Branch 70 → 71 taken 449 times.
✓ Branch 70 → 72 taken 15 times.
913 changedRows.at(end + 1).roles == changedRows.at(at).roles) ++end;
1192
2/2
✓ Branch 80 → 81 taken 22 times.
✓ Branch 81 → 82 taken 22 times.
22 emit dataChanged(index(changedRows.at(at).row, 0),
1193
1/1
✓ Branch 77 → 78 taken 22 times.
22 index(changedRows.at(end).row, 0),
1194 22 changedRows.at(at).roles);
1195 22 at = end + 1;
1196 }
1197 499 }
1198
1199 // A refreshed page is applied as row removals, moves (a thread that gained
1200 // newer mail moves up) and insertions: delegates and the viewport stay
1201 // mounted instead of a model reset rebuilding the list. Returns false
1202 // (caller resets) when no row survived or the reorder needs more than
1203 // MaximumDiffMoves single-row moves. On success the published identities
1204 // equal `next`.
1205 50 bool QuickThreadListModel::applyIndexedRowDiff(const QList<ThreadProjectionRow> &next) {
1206 34160 const auto keyOf = [](const ThreadProjectionRow &row) {
1207 34160 return MailKey{row.header.folderId, row.header.uid};
1208 };
1209 50 QSet<MailKey> nextKeys, oldKeys;
1210
2/2
✓ Branch 5 → 6 taken 50 times.
✓ Branch 7 → 8 taken 50 times.
50 nextKeys.reserve(next.size()); oldKeys.reserve(m_indexRows.size());
1211
3/3
✓ Branch 12 → 13 taken 8927 times.
✓ Branch 15 → 10 taken 8927 times.
✓ Branch 15 → 16 taken 50 times.
8977 for (const auto &row : next) nextKeys.insert(keyOf(row));
1212 50 QList<MailKey> survivingOld;
1213
2/2
✓ Branch 30 → 19 taken 4864 times.
✓ Branch 30 → 31 taken 50 times.
4914 for (const auto &row : std::as_const(m_indexRows)) {
1214
1/1
✓ Branch 21 → 22 taken 4864 times.
4864 oldKeys.insert(keyOf(row));
1215
3/3
✓ Branch 24 → 25 taken 2178 times.
✓ Branch 24 → 28 taken 2686 times.
✓ Branch 26 → 27 taken 2178 times.
4864 if (nextKeys.contains(keyOf(row))) survivingOld.append(keyOf(row));
1216 }
1217
2/2
✓ Branch 32 → 33 taken 31 times.
✓ Branch 32 → 34 taken 19 times.
50 if (survivingOld.isEmpty()) return false;
1218 19 QList<MailKey> survivingNext;
1219
2/2
✓ Branch 45 → 36 taken 3246 times.
✓ Branch 45 → 46 taken 19 times.
3265 for (const auto &row : next)
1220
3/3
✓ Branch 39 → 40 taken 2178 times.
✓ Branch 39 → 43 taken 1068 times.
✓ Branch 41 → 42 taken 2178 times.
3246 if (oldKeys.contains(keyOf(row))) survivingNext.append(keyOf(row));
1221 // Dry run first: no signal may be emitted before the diff is accepted.
1222 19 int moves = 0;
1223
2/2
✓ Branch 59 → 47 taken 2072 times.
✓ Branch 59 → 60 taken 18 times.
2090 for (int at = 0; at < survivingNext.size(); ++at) {
1224
2/2
✓ Branch 50 → 51 taken 1997 times.
✓ Branch 50 → 52 taken 75 times.
2072 if (survivingOld.at(at) == survivingNext.at(at)) continue;
1225
1/1
✓ Branch 54 → 55 taken 75 times.
75 survivingOld.move(survivingOld.indexOf(survivingNext.at(at), at + 1), at);
1226
2/2
✓ Branch 55 → 56 taken 1 time.
✓ Branch 55 → 57 taken 74 times.
75 if (++moves > MaximumDiffMoves) return false;
1227 }
1228
2/2
✓ Branch 78 → 62 taken 2008 times.
✓ Branch 78 → 79 taken 18 times.
2026 for (int row = m_indexRows.size() - 1; row >= 0;) {
1229
2/2
✓ Branch 65 → 66 taken 1996 times.
✓ Branch 65 → 67 taken 12 times.
2008 if (nextKeys.contains(keyOf(m_indexRows.at(row)))) { --row; continue; }
1230 12 const int last = row;
1231
6/6
✓ Branch 69 → 70 taken 847 times.
✓ Branch 69 → 75 taken 3 times.
✓ Branch 73 → 74 taken 838 times.
✓ Branch 73 → 75 taken 9 times.
✓ Branch 76 → 68 taken 838 times.
✓ Branch 76 → 77 taken 12 times.
850 while (row >= 0 && !nextKeys.contains(keyOf(m_indexRows.at(row)))) --row;
1232
1/1
✓ Branch 77 → 78 taken 12 times.
12 removeIndexedRows(row + 1, last);
1233 }
1234
2/2
✓ Branch 96 → 80 taken 1996 times.
✓ Branch 96 → 97 taken 18 times.
2014 for (int at = 0; at < survivingNext.size(); ++at) {
1235
2/2
✓ Branch 84 → 85 taken 1986 times.
✓ Branch 84 → 86 taken 10 times.
1996 if (keyOf(m_indexRows.at(at)) == survivingNext.at(at)) continue;
1236 10 const int from = m_rowByKey.value(survivingNext.at(at), -1);
1237
1/1
✓ Branch 90 → 91 taken 10 times.
10 beginMoveRows({}, from, from, {}, at);
1238
1/1
✓ Branch 91 → 92 taken 10 times.
10 m_indexRows.move(from, at);
1239
1/1
✓ Branch 92 → 93 taken 10 times.
10 reindexRows(at);
1240
1/1
✓ Branch 93 → 94 taken 10 times.
10 endMoveRows();
1241 }
1242 // Every surviving row before position `at` already sits at its final
1243 // index, so each run of new rows inserts at its target position.
1244
2/2
✓ Branch 125 → 98 taken 2014 times.
✓ Branch 125 → 126 taken 18 times.
2032 for (int at = 0; at < next.size();) {
1245
2/2
✓ Branch 101 → 102 taken 1996 times.
✓ Branch 101 → 103 taken 18 times.
2014 if (oldKeys.contains(keyOf(next.at(at)))) { ++at; continue; }
1246 18 int end = at;
1247
6/6
✓ Branch 106 → 107 taken 1038 times.
✓ Branch 106 → 112 taken 12 times.
✓ Branch 110 → 111 taken 1032 times.
✓ Branch 110 → 112 taken 6 times.
✓ Branch 113 → 104 taken 1032 times.
✓ Branch 113 → 114 taken 18 times.
1050 while (end + 1 < next.size() && !oldKeys.contains(keyOf(next.at(end + 1)))) ++end;
1248
1/1
✓ Branch 115 → 116 taken 18 times.
18 beginInsertRows({}, at, end);
1249
3/3
✓ Branch 118 → 119 taken 1050 times.
✓ Branch 120 → 117 taken 1050 times.
✓ Branch 120 → 121 taken 18 times.
1068 for (int row = at; row <= end; ++row) m_indexRows.insert(row, next.at(row));
1250
1/1
✓ Branch 121 → 122 taken 18 times.
18 reindexRows(at);
1251
1/1
✓ Branch 122 → 123 taken 18 times.
18 endInsertRows();
1252 18 at = end + 1;
1253 }
1254 18 return true;
1255 50 }
1256
1257 // A catch-up keeps a deeper window at its published head. A window at the
1258 // top of the folder reloads from the top, so rows that arrived above its old
1259 // first row (new mail) enter the page instead of being skipped for good.
1260 16 qint64 QuickThreadListModel::catchUpAnchor(qint64 requestedAnchor) const {
1261
2/2
✓ Branch 3 → 4 taken 8 times.
✓ Branch 3 → 5 taken 8 times.
16 if (m_indexRows.isEmpty()) return requestedAnchor;
1262
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 8 taken 8 times.
8 return m_indexHasBefore ? m_indexRows.first().header.uid : qint64(0);
1263 }
1264
1265 58 void QuickThreadListModel::rememberViewport(int row, qreal offset) {
1266
8/10
✓ Branch 2 → 3 taken 37 times.
✓ Branch 2 → 7 taken 21 times.
✓ Branch 3 → 4 taken 28 times.
✓ Branch 3 → 7 taken 9 times.
✓ Branch 4 → 5 taken 28 times.
✗ Branch 4 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 28 times.
✓ Branch 9 → 10 taken 30 times.
✓ Branch 9 → 11 taken 28 times.
58 if (!m_indexed || m_indexPending || row<0 || row>=m_indexRows.size()) return;
1267 28 m_viewportUid=m_indexRows[row].header.uid;
1268 28 m_viewportOffset=qMax(qreal(0),offset);
1269 }
1270
1271 2 void QuickThreadListModel::jumpIndexed(bool last) {
1272 2 m_nextLoadAnnounced = true;
1273
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 1 time.
2 loadIndexed(last ? PageMode::Last : PageMode::Replace,0,false);
1274 2 }
1275
1276 4 void QuickThreadListModel::revealIndexed(qint64 uid, bool before) {
1277 4 m_nextLoadAnnounced = true;
1278
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 3 times.
4 loadIndexed(before ? PageMode::Last : PageMode::Replace,uid,false);
1279 4 }
1280
1281 21 void QuickThreadListModel::fetchIndexed(bool before) {
1282
3/4
✓ Branch 3 → 4 taken 21 times.
✗ Branch 3 → 8 not taken.
✓ Branch 4 → 5 taken 8 times.
✓ Branch 4 → 6 taken 13 times.
21 if (hasMoreIndexed(before)) loadIndexed(before ? PageMode::Prepend : PageMode::Append);
1283 21 }
1284
1285 6 void QuickThreadListModel::toggleIndexed(int row) {
1286
5/9
✓ Branch 2 → 3 taken 6 times.
✗ Branch 2 → 7 not taken.
✓ Branch 4 → 5 taken 6 times.
✗ Branch 4 → 7 not taken.
✓ Branch 5 → 6 taken 6 times.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 6 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 6 times.
8 if (row < 0 || row >= m_indexRows.size() || m_indexRows[row].descendants == 0) return;
1287
1/1
✓ Branch 11 → 12 taken 6 times.
6 const auto header = m_indexRows[row].header;
1288
2/2
✓ Branch 14 → 15 taken 3 times.
✓ Branch 14 → 74 taken 3 times.
6 if (m_indexRequest.collapsed.contains(header.uid)) {
1289
1/1
✓ Branch 15 → 16 taken 3 times.
3 m_indexRequest.collapsed.remove(header.uid);
1290 // P90.12: a revision-fresh retained span re-inserts differentially —
1291 // no worker round trip, no tail reload for the expand click.
1292 3 const auto span = m_collapsedSpans.constFind(header.uid);
1293 // Only a complete subtree re-inserts differentially: a partial span
1294 // (window boundary cut it) must go through the tail reload, which
1295 // fetches the remaining children in their correct sorted position.
1296
4/6
✓ Branch 19 → 20 taken 3 times.
✗ Branch 19 → 27 not taken.
✓ Branch 21 → 22 taken 3 times.
✗ Branch 21 → 27 not taken.
✓ Branch 28 → 29 taken 2 times.
✓ Branch 28 → 56 taken 1 time.
6 if (span != m_collapsedSpans.constEnd() && span->revision == m_indexRevision &&
1297
3/3
✓ Branch 24 → 25 taken 3 times.
✓ Branch 25 → 26 taken 2 times.
✓ Branch 25 → 27 taken 1 time.
3 span->rows.size() == m_indexRows[row].descendants) {
1298
1/1
✓ Branch 29 → 30 taken 2 times.
2 cancelIndexedRequest();
1299
1/1
✓ Branch 33 → 34 taken 2 times.
2 beginInsertRows({}, row+1, row+span->rows.size());
1300
2/2
✓ Branch 41 → 37 taken 18 times.
✓ Branch 41 → 42 taken 2 times.
20 for (int i = span->rows.size() - 1; i >= 0; --i)
1301
1/1
✓ Branch 39 → 40 taken 18 times.
18 m_indexRows.insert(row+1, span->rows.at(i));
1302
2/2
✓ Branch 43 → 44 taken 2 times.
✓ Branch 44 → 45 taken 2 times.
2 m_rowByKey.clear(); reindexRows(); endInsertRows();
1303 2 m_collapsedSpanRows -= span->rows.size();
1304
1/1
✓ Branch 47 → 48 taken 2 times.
2 m_collapsedSpans.erase(span);
1305
4/4
✓ Branch 48 → 49 taken 2 times.
✓ Branch 50 → 51 taken 2 times.
✓ Branch 52 → 53 taken 2 times.
✓ Branch 53 → 54 taken 2 times.
2 emit dataChanged(index(row, 0), index(row, 0), {Expanded});
1306 2 return;
1307 }
1308
1/2
✓ Branch 58 → 59 taken 1 time.
✗ Branch 58 → 62 not taken.
1 if (span != m_collapsedSpans.constEnd()) {
1309 1 m_collapsedSpanRows -= span->rows.size();
1310
1/1
✓ Branch 61 → 62 taken 1 time.
1 m_collapsedSpans.erase(span);
1311 }
1312
1/1
✓ Branch 62 → 63 taken 1 time.
1 cancelIndexedRequest();
1313 // Replace the following tail with a page beginning just after the
1314 // expanded node. A partial subtree must not skip to later roots.
1315
1/2
✓ Branch 64 → 65 taken 1 time.
✗ Branch 64 → 72 not taken.
1 if (row+1 < m_indexRows.size()) {
1316
1/1
✓ Branch 67 → 68 taken 1 time.
1 beginRemoveRows({}, row+1, m_indexRows.size()-1);
1317
3/3
✓ Branch 68 → 69 taken 1 time.
✓ Branch 70 → 71 taken 1 time.
✓ Branch 71 → 72 taken 1 time.
1 m_indexRows.resize(row+1); m_rowByKey.clear(); reindexRows(); endRemoveRows();
1318 }
1319 1 m_indexHasAfter = true;
1320
1/1
✓ Branch 72 → 73 taken 1 time.
1 loadIndexed(PageMode::Append);
1321 } else {
1322
1/2
✗ Branch 75 → 76 not taken.
✓ Branch 75 → 80 taken 3 times.
3 if (m_indexRequest.collapsed.size() >= 4096) {
1323 ✗ emit projectionFailed(tr("Too many collapsed threads. Expand another thread first.")); return;
1324 }
1325
1/1
✓ Branch 80 → 81 taken 3 times.
3 cancelIndexedRequest();
1326
1/1
✓ Branch 81 → 82 taken 3 times.
3 m_indexRequest.collapsed.insert(header.uid);
1327 3 int end = row+1;
1328
8/8
✓ Branch 85 → 86 taken 219 times.
✓ Branch 85 → 90 taken 1 time.
✓ Branch 86 → 87 taken 219 times.
✓ Branch 87 → 88 taken 219 times.
✓ Branch 88 → 89 taken 217 times.
✓ Branch 88 → 90 taken 2 times.
✓ Branch 91 → 83 taken 217 times.
✓ Branch 91 → 92 taken 3 times.
220 while (end < m_indexRows.size() && m_indexRows[end].depth > m_indexRows[row].depth) ++end;
1329
1/2
✓ Branch 92 → 93 taken 3 times.
✗ Branch 92 → 105 not taken.
3 if (end > row+1) {
1330 // P90.12: retain the removed span (revision-fenced) for the
1331 // differential expand; bound the total retained rows coarsely.
1332
1/2
✗ Branch 93 → 94 not taken.
✓ Branch 93 → 96 taken 3 times.
3 if (m_collapsedSpanRows + (end-row-1) > 2000) {
1333 ✗ m_collapsedSpans.clear();
1334 ✗ m_collapsedSpanRows = 0;
1335 }
1336
1/1
✓ Branch 97 → 98 taken 3 times.
3 m_collapsedSpans.insert(header.uid, {m_indexRevision, m_indexRows.mid(row+1, end-row-1)});
1337 3 m_collapsedSpanRows += end-row-1;
1338
1/1
✓ Branch 100 → 101 taken 3 times.
3 beginRemoveRows({}, row+1, end-1);
1339
3/3
✓ Branch 101 → 102 taken 3 times.
✓ Branch 103 → 104 taken 3 times.
✓ Branch 104 → 105 taken 3 times.
3 m_indexRows.remove(row+1, end-row-1); m_rowByKey.clear(); reindexRows(); endRemoveRows();
1340 }
1341
1/1
✓ Branch 105 → 106 taken 3 times.
3 retainIndexedSnapshot();
1342 }
1343
4/4
✓ Branch 106 → 107 taken 4 times.
✓ Branch 108 → 109 taken 4 times.
✓ Branch 110 → 111 taken 4 times.
✓ Branch 111 → 112 taken 4 times.
4 emit dataChanged(index(row, 0), index(row, 0), {Expanded});
1344 9 }
1345
1346 113 void QuickThreadListModel::updateIndexedFlags(qint64 folder, qint64 uid, quint32 flags) {
1347
3/4
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 4 taken 110 times.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 3 times.
113 if (!m_indexed || folder != m_indexRequest.folderId) return;
1348 3 const int row = m_rowByKey.value({folder, uid}, -1);
1349
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 3 times.
3 if (row < 0) return;
1350
1/1
✓ Branch 9 → 10 taken 3 times.
3 auto &current = m_indexRows[row].header.flags;
1351 3 const quint32 changed = current ^ flags;
1352
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 3 times.
3 if (!changed) return;
1353 3 current = flags;
1354
2/10
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 18 taken 3 times.
✗ Branch 13 → 14 not taken.
✗ Branch 13 → 18 not taken.
✗ Branch 14 → 15 not taken.
✗ Branch 14 → 18 not taken.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 18 not taken.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 22 taken 3 times.
3 if (m_showUnread && (changed & MailFlag::Seen) && (flags & MailFlag::Seen) &&
1355 ✗ m_indexRequest.retainedUids.size() < MaximumRetainedUnread)
1356 ✗ m_indexRequest.retainedUids.insert(uid);
1357 3 QList<int> roles;
1358
3/3
✓ Branch 22 → 23 taken 2 times.
✓ Branch 22 → 24 taken 1 time.
✓ Branch 23 → 24 taken 2 times.
3 if (changed & MailFlag::Seen) roles.append(Unread);
1359
2/3
✓ Branch 24 → 25 taken 3 times.
✗ Branch 24 → 26 not taken.
✓ Branch 25 → 26 taken 3 times.
3 if (changed & MailFlag::Flagged) roles.append(Starred);
1360
1/3
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 3 times.
✗ Branch 27 → 28 not taken.
3 if (changed & MailFlag::Answered) roles.append(Answered);
1361
4/5
✓ Branch 29 → 30 taken 3 times.
✗ Branch 29 → 36 not taken.
✓ Branch 31 → 32 taken 3 times.
✓ Branch 33 → 34 taken 3 times.
✓ Branch 34 → 35 taken 3 times.
3 if (!roles.isEmpty()) emit dataChanged(index(row, 0), index(row, 0), roles);
1362 3 }
1363
1364 39 void QuickThreadListModel::updateIndexedLabel(qint64 folder, qint64 uid, const QString &label, bool present) {
1365
1/4
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 39 times.
✗ Branch 3 → 4 not taken.
✗ Branch 3 → 5 not taken.
39 if (!m_indexed || folder != m_indexRequest.folderId) return;
1366 ✗ const int row = m_rowByKey.value({folder, uid}, -1);
1367 ✗ if (row < 0) return;
1368 ✗ auto &labels = m_indexRows[row].header.labels;
1369 ✗ if (present) {
1370 ✗ if (labels.contains(label)) return;
1371 ✗ labels.append(label);
1372 ✗ } else if (!labels.removeAll(label)) return;
1373 ✗ m_chipCache.remove({folder, uid});
1374 ✗ emit dataChanged(index(row, 0), index(row, 0), {Labels, LabelChips});
1375 }
1376
1377 74 void QuickThreadListModel::applyIndexedSeen(qint64 folder, qint64 uid, bool seen) {
1378
3/4
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 73 times.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1 time.
74 if (!m_indexed || folder != m_indexRequest.folderId) return;
1379 1 const int row = m_rowByKey.value({folder, uid}, -1);
1380
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 1 time.
1 if (row < 0) return;
1381 1 auto &flags = m_indexRows[row].header.flags;
1382
1/2
✓ Branch 10 → 11 taken 1 time.
✗ Branch 10 → 12 not taken.
1 const quint32 updated = seen ? (flags | MailFlag::Seen) : (flags & ~MailFlag::Seen);
1383
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 1 time.
1 if (updated == flags) return;
1384 1 flags = updated;
1385
4/8
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 20 not taken.
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 20 not taken.
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 24 not taken.
1 if (m_showUnread && seen && m_indexRequest.retainedUids.size() < MaximumRetainedUnread)
1386
1/1
✓ Branch 22 → 23 taken 1 time.
1 m_indexRequest.retainedUids.insert(uid);
1387
4/4
✓ Branch 24 → 25 taken 1 time.
✓ Branch 26 → 27 taken 1 time.
✓ Branch 28 → 29 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
1 emit dataChanged(index(row, 0), index(row, 0), {Unread});
1388 }
1389
1390 298 void QuickThreadListModel::applyIndexedStar(qint64 folder, qint64 uid, bool starred) {
1391
1/4
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 298 times.
✗ Branch 3 → 4 not taken.
✗ Branch 3 → 5 not taken.
298 if (!m_indexed || folder != m_indexRequest.folderId) return;
1392 ✗ const int row = m_rowByKey.value({folder, uid}, -1);
1393 ✗ if (row < 0) return;
1394 ✗ auto &flags = m_indexRows[row].header.flags;
1395 ✗ const quint32 updated = starred ? (flags | MailFlag::Flagged)
1396 ✗ : (flags & ~MailFlag::Flagged);
1397 ✗ if (updated == flags) return;
1398 ✗ flags = updated;
1399 ✗ emit dataChanged(index(row, 0), index(row, 0), {Starred});
1400 }
1401
1402 3 void QuickThreadListModel::removeIndexedUid(qint64 folder, qint64 uid) {
1403
2/2
✓ Branch 2 → 3 taken 3 times.
✓ Branch 3 → 4 taken 3 times.
3 removeIndexedUids(folder, {uid});
1404 3 }
1405
1406 5 void QuickThreadListModel::removeIndexedUids(qint64 folder, const QSet<qint64> &uids) {
1407
4/8
✓ Branch 2 → 3 taken 5 times.
✗ Branch 2 → 6 not taken.
✓ Branch 3 → 4 taken 5 times.
✗ Branch 3 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 5 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 5 times.
5 if (!m_indexed || folder != m_indexRequest.folderId || uids.isEmpty()) return;
1408 // The pending request keeps its revision/cursor validity: the canonical
1409 // row is only gone after the remote confirmation. A page still in flight
1410 // answers from the authoritative store and may re-admit the row, which
1411 // is the safe direction (never silently drop real data).
1412 5 QList<int> positions;
1413
1/1
✓ Branch 11 → 12 taken 5 times.
5 positions.reserve(uids.size());
1414
2/2
✓ Branch 21 → 14 taken 5 times.
✓ Branch 21 → 22 taken 5 times.
10 for (const qint64 uid : uids) {
1415 5 const int row = m_rowByKey.value({folder, uid}, -1);
1416
3/3
✓ Branch 17 → 18 taken 3 times.
✓ Branch 17 → 19 taken 2 times.
✓ Branch 18 → 19 taken 3 times.
5 if (row >= 0) positions.append(row);
1417 }
1418
3/3
✓ Branch 22 → 23 taken 5 times.
✓ Branch 23 → 24 taken 5 times.
✓ Branch 24 → 25 taken 5 times.
5 std::sort(positions.begin(), positions.end(), std::greater<int>{});
1419 // Positions were captured before the first removal. Descending ranges
1420 // leave every yet-to-remove position stable, and contiguous bulk moves
1421 // need one model notification and tail reindex per visible range.
1422
2/2
✓ Branch 39 → 26 taken 3 times.
✓ Branch 39 → 40 taken 5 times.
8 for (int at = 0; at < positions.size();) {
1423 3 const int last = positions.at(at++);
1424 3 int first = last;
1425
2/6
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 35 taken 3 times.
✗ Branch 33 → 34 not taken.
✗ Branch 33 → 35 not taken.
✗ Branch 36 → 28 not taken.
✓ Branch 36 → 37 taken 3 times.
3 while (at < positions.size() && positions.at(at) == first - 1)
1426 ✗ first = positions.at(at++);
1427
1/1
✓ Branch 37 → 38 taken 3 times.
3 removeIndexedRows(first, last);
1428 }
1429 5 }
1430
1431 18 void QuickThreadListModel::removeIndexedRows(int first, int last) {
1432
4/8
✓ Branch 2 → 3 taken 18 times.
✗ Branch 2 → 6 not taken.
✓ Branch 3 → 4 taken 18 times.
✗ Branch 3 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 18 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 18 times.
18 if (first < 0 || last < first || last >= m_indexRows.size()) return;
1433
1/1
✓ Branch 11 → 12 taken 18 times.
18 beginRemoveRows({}, first, last);
1434
2/2
✓ Branch 21 → 13 taken 852 times.
✓ Branch 21 → 22 taken 18 times.
870 for (int row = first; row <= last; ++row) {
1435 852 const auto &header = m_indexRows.at(row).header;
1436 852 const MailKey key{header.folderId, header.uid};
1437
1/1
✓ Branch 15 → 16 taken 852 times.
852 m_rowByKey.remove(key);
1438
1/1
✓ Branch 16 → 17 taken 852 times.
852 m_selectedCache.remove(key);
1439
1/1
✓ Branch 17 → 18 taken 852 times.
852 m_suggestionCache.remove(key);
1440
1/1
✓ Branch 18 → 19 taken 852 times.
852 m_dateTextCache.remove(key);
1441
1/1
✓ Branch 19 → 20 taken 852 times.
852 m_chipCache.remove(key);
1442 }
1443 18 m_indexRows.remove(first, last - first + 1);
1444 // Rows before first retain their positions. Qt can query identity roles
1445 // synchronously during endRemoveRows, so publish the shifted tail now.
1446
2/2
✓ Branch 29 → 24 taken 896 times.
✓ Branch 29 → 30 taken 18 times.
914 for (int row = first; row < m_indexRows.size(); ++row) {
1447 896 const auto &header = m_indexRows.at(row).header;
1448
1/1
✓ Branch 26 → 27 taken 896 times.
896 m_rowByKey.insert({header.folderId, header.uid}, row);
1449 }
1450 18 endRemoveRows();
1451 }
1452
1453 35 void QuickThreadListModel::ruleMatchesChanged() {
1454
1/6
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 15 taken 35 times.
✗ Branch 5 → 6 not taken.
✗ Branch 9 → 10 not taken.
✗ Branch 11 → 12 not taken.
✗ Branch 12 → 13 not taken.
35 if (rowCount() > 0) emit dataChanged(index(0, 0), index(rowCount() - 1, 0), {RuleMatch});
1455 35 }
1456
1457 29 void QuickThreadListModel::taskLinksChanged() {
1458
1/6
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 15 taken 29 times.
✗ Branch 5 → 6 not taken.
✗ Branch 9 → 10 not taken.
✗ Branch 11 → 12 not taken.
✗ Branch 12 → 13 not taken.
29 if (rowCount() > 0) emit dataChanged(index(0, 0), index(rowCount() - 1, 0), {OpenTask});
1459 29 }
1460