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: 100.0% 21 / 0 / 21
Functions: 100.0% 21 / 0 / 21
Branches: 100.0% 1 / 0 / 1

presentation/MailSearchSession.h
Line Branch Exec Source
1 #pragma once
2
3 #include "MailPresentationModel.h"
4 #include "SearchMenuModel.h"
5 #include "SearchTermModel.h"
6 #include "data/MailCache.h"
7 #include "data/MailRequest.h"
8 #include "data/MailSearchSummary.h"
9 #include "ui/MailListModel.h"
10 #include <QElapsedTimer>
11 #include <QObject>
12 #include <QSet>
13 #include <QTimer>
14 #include <functional>
15
16 class MailSearchReadService;
17 class ServerSearchSession;
18
19 // Sprint 101 S101.1.2 (D-2, D-3, D-6, D-11, D-12, D-29): one search of one
20 // search tab. It owns the tab's canonical query and its history, its own
21 // result rows (the mail-row projection of the folder list, never the folder
22 // list itself — SF-02), infinite paging through the shared search reader,
23 // the summary of every hit (counts, days, facets) and the merge of the
24 // server search when the tab is the server search's owner.
25 //
26 // Everything a view shows — chips, title, field text, the timeline's range
27 // — is derived from query() (rule 8). Results are identities (rule 9).
28 //
29 // Owner: MailSearchSessions (one per search tab). GUI thread only.
30 class MailSearchSession final : public QObject {
31 7 Q_OBJECT
32 Q_PROPERTY(QString tabId READ tabId CONSTANT)
33 Q_PROPERTY(QString accountId READ accountId CONSTANT)
34 Q_PROPERTY(QString query READ query NOTIFY queryChanged)
35 Q_PROPERTY(QString fieldText READ fieldText NOTIFY queryChanged)
36 Q_PROPERTY(QVariantList terms READ terms NOTIFY queryChanged)
37 Q_PROPERTY(QString title READ title NOTIFY queryChanged)
38 Q_PROPERTY(QString dateFrom READ dateFromIso NOTIFY queryChanged)
39 Q_PROPERTY(QString dateTo READ dateToIso NOTIFY queryChanged)
40 Q_PROPERTY(QString state READ state NOTIFY stateChanged)
41 Q_PROPERTY(QString stateText READ stateText NOTIFY stateChanged)
42 Q_PROPERTY(QAbstractItemModel *results READ results CONSTANT)
43 Q_PROPERTY(int loadedCount READ loadedCount NOTIFY rowsChanged)
44 Q_PROPERTY(bool hasMore READ hasMore NOTIFY rowsChanged)
45 Q_PROPERTY(bool pagePending READ pagePending NOTIFY rowsChanged)
46 Q_PROPERTY(qint64 total READ total NOTIFY summaryChanged)
47 Q_PROPERTY(QString summaryState READ summaryState NOTIFY summaryChanged)
48 Q_PROPERTY(bool serverRunning READ serverRunning NOTIFY serverChanged)
49 Q_PROPERTY(int serverDone READ serverDone NOTIFY serverChanged)
50 Q_PROPERTY(int serverTotal READ serverTotal NOTIFY serverChanged)
51 Q_PROPERTY(int serverAdded READ serverAdded NOTIFY serverChanged)
52 // S106-024: folders whose server search the server refused.
53 Q_PROPERTY(int serverFailed READ serverFailed NOTIFY serverChanged)
54 Q_PROPERTY(bool canGoBack READ canGoBack NOTIFY historyChanged)
55 Q_PROPERTY(bool canGoForward READ canGoForward NOTIFY historyChanged)
56 Q_PROPERTY(bool live READ live NOTIFY rowsChanged)
57 // S101.6.1 (D-13): results marked for a bulk action (Ctrl/Shift click, Ctrl+A).
58 Q_PROPERTY(int markedCount READ markedCount NOTIFY markedChanged)
59
60 public:
61 struct Deps {
62 MailCache *cache = nullptr;
63 MailSearchReadService *reader = nullptr;
64 ServerSearchSession *server = nullptr;
65 std::function<bool()> indexReady;
66 std::function<quint64()> indexGeneration;
67 // A cached result of this account in a folder the account lists.
68 std::function<bool(const MailId &, const QString &folderPath)> acceptResult;
69 // The folder paths the account lists: the summary counts only these (G4).
70 std::function<QSet<QString>()> allowedFolders;
71 std::function<bool()> serverEnabled;
72 std::function<SearchTermModel::Lookups()> lookups;
73 // D-32: the folder tree in display order (the sidebar's folder facet).
74 std::function<QList<SearchMenuModel::FolderRow>()> folderTree;
75 };
76 // The states of D-27 and §6.2.4. "empty": no query yet (D-25).
77 enum class State { Empty, Searching, Ready, Limited, Failed, Indexing, Busy };
78 static constexpr int PageSize = 200;
79 static constexpr int MaximumHistory = 50;
80 // D-6: the server search starts after this much quiet typing or on Enter.
81 static constexpr int ServerDelayMs = 800;
82
83 MailSearchSession(QString tabId, QString accountId, Deps deps, QObject *parent = nullptr);
84 ~MailSearchSession() override;
85
86 403 QString tabId() const { return m_tabId; }
87 102 QString accountId() const { return m_accountId; }
88 3517 QString query() const { return m_query; }
89 QString fieldText() const;
90 QVariantList terms() const;
91 QString title() const;
92 QString dateFromIso() const;
93 QString dateToIso() const;
94 QString state() const;
95 5 QString stateText() const { return m_stateText; }
96 89 State stateValue() const { return m_state; }
97 2923 QAbstractItemModel *results() { return &m_results; }
98 89 MailPresentationModel *resultModel() { return &m_results; }
99
1/1
✓ Branch 3 → 4 taken 5309 times.
5309 int loadedCount() const { return m_results.rowCount(); }
100 296 bool hasMore() const { return m_hasMore; }
101 199 bool pagePending() const { return m_pagePending; }
102 // The summary total plus server-only hits; -1 while unknown.
103 qint64 total() const;
104 QString summaryState() const;
105 462 const MailSearchSummary &summary() const { return m_summary; }
106 481 bool serverRunning() const { return m_serverRunning; }
107 1 int serverDone() const { return m_serverDone; }
108 161 int serverTotal() const { return m_serverTotal; }
109 3 int serverAdded() const { return m_serverAdded; }
110 320 int serverFailed() const { return m_serverFailed; }
111 45 bool canGoBack() const { return m_historyIndex > 0; }
112 19 bool canGoForward() const { return m_historyIndex + 1 < m_history.size(); }
113 // D-3: a released session keeps only its query and anchor.
114 10 bool live() const { return m_live; }
115
116 // A new search or refinement: one history step (D-2). `coalesce` replaces
117 // the current step instead (typing in the field, D-6).
118 Q_INVOKABLE void setQuery(const QString &query, bool coalesce = false);
119 // D-6/D-28: the title field. Edits re-run locally after the adaptive
120 // debounce and start the server after ServerDelayMs; submit runs now.
121 Q_INVOKABLE void editFieldText(const QString &text);
122 Q_INVOKABLE void submitFieldText(const QString &text);
123 Q_INVOKABLE void revertFieldText();
124 // The field text `text` stands for after its typed constraint tokens left
125 // the field (D-28); equal to fieldText() when the field is in step.
126 Q_INVOKABLE QString normalizedFieldText(const QString &text) const;
127 // Chip edits (SearchTermModel), each one history step.
128 Q_INVOKABLE void removeTerm(int index);
129 Q_INVOKABLE void replaceTerm(int index, const QString &value);
130 Q_INVOKABLE void negateTerm(int index);
131 Q_INVOKABLE void addTerm(const QString &kind, const QString &value, bool negated = false);
132 Q_INVOKABLE void setState(const QString &kind, int value);
133 Q_INVOKABLE int termState(const QString &kind) const;
134 // CB-05: "Ctrl+Shift+I" / `filter off` in a search tab — the three status
135 // chips (unread, important, attachments) off in one history step.
136 Q_INVOKABLE void clearStatusFilters();
137 // D-15a: "filter unread important" in a search tab — exactly these status
138 // chips on, the others off (one history step); a negated chip that is
139 // not named ("is:read" without "unread") stays.
140 Q_INVOKABLE void setStatusFilters(bool unread, bool flagged, bool attachment);
141 // D-8: ISO dates; empty strings remove the date constraint.
142 Q_INVOKABLE void setDateRange(const QString &from, const QString &to);
143 Q_INVOKABLE bool back();
144 Q_INVOKABLE bool forward();
145 Q_INVOKABLE void fetchMore();
146 Q_INVOKABLE void retry();
147 Q_INVOKABLE void startServerSearch();
148 // §6.2.5/SD-12: after an empty result, the relaxations that bring
149 // results back: {label, query, count}, at most four.
150 Q_INVOKABLE QVariantList relaxations() const;
151 // S101.4.3 (SearchMenuModel): "Filter ▾" and chip menus with the counts
152 // of the hits; applyChoice runs a picked entry as one history step and is
153 // false for ids the view handles (editors, view actions) or that change
154 // nothing.
155 Q_INVOKABLE QVariantList filterMenu() const;
156 Q_INVOKABLE QVariantList chipMenu(int termIndex) const;
157 Q_INVOKABLE bool applyChoice(const QString &id);
158 // SD-09/PT-01: the people among the hits matching the field's last word;
159 // picking one turns that word into a From/To chip (one history step).
160 Q_INVOKABLE QVariantList fieldCompletions(const QString &text) const;
161 Q_INVOKABLE bool pickFieldCompletion(const QString &text, const QString &id);
162 // D-32: the sidebar's facets — the folders with hits in tree order and
163 // the top values of "from" / "label" (SearchMenuModel entries).
164 Q_INVOKABLE QVariantList folderFacet() const;
165 Q_INVOKABLE QVariantList valueFacet(const QString &kind, int limit) const;
166 Q_INVOKABLE int rowForIdentity(const QVariantMap &identity) const;
167 Q_INVOKABLE QVariantMap mailAt(int row) const;
168
169 // The registry's lifecycle calls.
170 void activate(); // the tab became the active one
171 void deactivate(); // another tab became active (D-12: the server pauses)
172 void release(); // D-3: drop the rows, keep the query
173 void refreshAfterIndexReady();
174 void setSelected(const MailId &mail);
175 MailId selected() const { return m_selected; }
176 // The selected result's date (the timeline's caret, D-9); invalid when
177 // nothing or a mail outside the loaded rows is selected.
178 QDateTime selectedDateTime() const;
179 // S101.6.1 (D-13): Ctrl toggles a row, Shift marks the range from the
180 // anchor, a plain click clears; the first mark takes the selected result
181 // along. Only loaded rows can be marked; a new query clears the marks.
182 538 int markedCount() const { return int(m_marked.size()); }
183 // S101.5.9: the result rows' section unit ("year", "month", "week",
184 // "day"; "" = no sections), the timeline's current unit.
185 void setPeriodUnit(const QString &unit);
186 void mark(int row, Qt::KeyboardModifiers modifiers);
187 void markAllLoaded();
188 void clearMarks();
189 QList<MailId> markedIds() const; // in view order
190 // §6.2.4 hour level: the first loaded result (view row) of that local
191 // hour; -1 when none is loaded.
192 Q_INVOKABLE int rowForHour(int hour) const;
193 // The date of a loaded result (view row); invalid outside the loaded rows.
194 QDateTime dateTimeAt(int row) const;
195 bool contains(const MailId &mail) const;
196 // Actions on results (moves, flags) keep the rows in step.
197 void removeRows(qint64 folderId, const QList<qint64> &uids);
198 void updateFlags(qint64 folderId, qint64 uid, quint32 flags);
199 // The visual Seen/Flagged commits of the controller (any folder).
200 void setFlag(qint64 folderId, qint64 uid, quint32 flag, bool on);
201 // The view row of a result and the result of a view row (rule 9: rows
202 // stay inside the session; callers pass identities).
203 int rowOf(const MailId &mail) const;
204 MailId mailIdAt(int row) const;
205 QDateTime dateOf(const MailId &mail) const; // invalid when the mail is not loaded
206 // Called by the registry for the server search it owns.
207 void serverHits(const QList<qint64> &uids, qint64 folderId, const QString &folderPath);
208 void serverProgress(int done, int total);
209 void serverFinished();
210 void serverFolderFailed();
211
212 // D-29: a server hit's header must satisfy the header constraints the
213 // server cannot check (attachment, labels, exclusions, …). Text is not
214 // checked: the server found it, possibly in the body.
215 static bool matchesHeader(const MailHeader &header, const QString &folderPath,
216 const MailCache::SearchFilter &filter);
217
218 signals:
219 void queryChanged();
220 void stateChanged();
221 void rowsChanged();
222 void summaryChanged();
223 void serverChanged();
224 void historyChanged();
225 void selectionChanged();
226 void markedChanged();
227 // D-29/D-31: the rows were replaced; `selected` survived (valid) or the
228 // view should select the first row (invalid) — the registry decides.
229 void rowsReplaced(const MailId &selected);
230 // The session wants to own the shared server search (registry decides).
231 void serverSearchRequested();
232
233 private:
234 void run(const QString &canonical, bool coalesce, bool keepAnchor);
235 // F-27 (SPRINT-104): the shared body of back()/forward().
236 bool navigateHistory(int step);
237 void pushHistory(const QString &canonical, bool coalesce);
238 void startLocal(bool keepAnchor);
239 void requestPage(bool replace);
240 void pageArrived(MailSearchPage page, bool replace, quint64 generation);
241 QList<MailHeader> admit(const QList<MailSearchHit> &hits);
242 void requestSummary();
243 void summaryArrived(MailSearchSummary summary, quint64 generation);
244 void requestRelaxations();
245 void relaxationStep(quint64 generation, int position);
246 void setStateValue(State state, const QString &text = {});
247 void countServerHit(const MailHeader &header, const QString &folderPath);
248 // S106-245: one mail entering (+1) or leaving (-1) the result moves every
249 // count and facet the summary shows, the same way in both directions.
250 void adjustSummary(const MailHeader &header, int delta);
251 void cancelReads();
252 SearchMenuModel::Context menuContext() const;
253 QString identityKey(const MailHeader &header) const;
254
255 QString m_tabId;
256 QString m_accountId;
257 Deps m_deps;
258 QString m_query;
259 QString m_text; // parsed free text (SQL)
260 MailCache::SearchFilter m_filter; // parsed constraints
261 QStringList m_history;
262 int m_historyIndex = -1;
263 bool m_lastStepTyped = false;
264 MailListModel m_rows;
265 MailPresentationModel m_results;
266 QSet<QString> m_identities; // D-11: canonical Message-ID or row key
267 int m_offset = 0;
268 bool m_hasMore = false;
269 bool m_pagePending = false;
270 bool m_live = true;
271 quint64 m_generation = 0;
272 quint64 m_indexGeneration = 0;
273 QObject m_pageOwner;
274 QObject m_summaryOwner;
275 QObject m_relaxationOwner;
276 MailSearchSummary m_summary;
277 bool m_summaryPending = false;
278 QList<SearchTermModel::Relaxation> m_relaxationCandidates;
279 QHash<int, qint64> m_relaxationCounts; // candidate index → count
280 QHash<qint64, QString> m_folderPaths; // folder id → path of the shown rows (facets)
281 State m_state = State::Empty;
282 QString m_stateText;
283 MailId m_selected;
284 QSet<QPair<qint64, qint64>> m_marked; // (folderId, uid)
285 QString m_periodUnit;
286 MailId m_markAnchor;
287 bool m_active = false;
288 bool m_serverWanted = false;
289 bool m_serverRunning = false;
290 int m_serverDone = 0;
291 int m_serverTotal = 0;
292 int m_serverAdded = 0;
293 int m_serverFailed = 0;
294 QTimer m_localDebounce;
295 QTimer m_serverDelay;
296 QString m_pendingFieldText;
297 QElapsedTimer m_editClock;
298 qint64 m_lastEdit = -1;
299 QElapsedTimer m_runClock;
300 };
301