MailJD nbsp;Β·nbsp; Test Dashboard nbsp;Β·nbsp; Coverage
LCOV - code coverage report
Current view: top level - app - SearchCoordinator.cpp (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 87.6 % 251 220
Test Date: 2026-06-21 21:10:19 Functions: 100.0 % 20 20
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 53.4 % 352 188

             Branch data     Line data    Source code
       1                 :             : #include "SearchCoordinator.h"
       2                 :             : 
       3                 :             : #include <QTimer>
       4                 :             : 
       5                 :             : #include "app/SearchQuery.h"
       6                 :             : #include "controller/MailController.h"
       7                 :             : #include "ui/CommandBar.h"
       8                 :             : #include "ui/FolderTree.h"
       9                 :             : #include "ui/MailFilterProxyModel.h"
      10                 :             : #include "ui/MailListModel.h"
      11                 :             : #include "ui/SearchPanel.h"
      12                 :             : 
      13                 :             : // Page size for the local FTS result list (additional pages via :search-more).
      14                 :             : static constexpr int kGlobalSearchPageSize = 200;
      15                 :             : 
      16                 :          57 : SearchCoordinator::SearchCoordinator(const Deps &deps, QObject *parent)
      17         [ +  - ]:          57 :     : QObject(parent), m_d(deps) {
      18                 :             :   // Re-selecting the virtual "Suche" node restores the search results view.
      19         [ +  - ]:          57 :   connect(m_d.folderTree, &FolderTree::searchNodeSelected, this, [this]() {
      20         [ #  # ]:           0 :     if (!m_searchNodeQuery.isEmpty())
      21                 :           0 :       runSearch(m_searchNodeQuery);
      22                 :           0 :   });
      23                 :             : 
      24                 :             :   // Closing the "Suche" node (context menu) cancels and dismisses the search.
      25                 :          57 :   connect(m_d.folderTree, &FolderTree::searchNodeCloseRequested, this,
      26         [ +  - ]:          57 :           &SearchCoordinator::exitSearch);
      27                 :             : 
      28                 :             :   // Enter in search: show the first local result page in the main mail list.
      29                 :          57 :   connect(m_d.commandBar, &CommandBar::searchSubmitted, this,
      30         [ +  - ]:          58 :           [this](const QString &query) { runSearch(query); });
      31                 :             : 
      32                 :             :   // Sprint 59 (U3): the SearchPanel runs through the SAME runSearch() path.
      33                 :             :   // It serializes its (freeText, filter) state into the canonical query, so
      34                 :             :   // panel and bar stay in sync β€” the filter state has a single home.
      35         [ +  - ]:          57 :   connect(m_d.searchPanel, &SearchPanel::searchRequested, this, [this]() {
      36                 :           1 :     m_searchDebounce->stop(); // explicit run cancels any pending debounce
      37   [ +  -  +  -  :           1 :     runSearch(buildSearchQuery(m_d.searchPanel->freeText(),
                   +  - ]
      38         [ +  - ]:           2 :                                m_d.searchPanel->filter()));
      39                 :           1 :   });
      40                 :             :   // "Reset" in the panel leaves search mode entirely.
      41                 :          57 :   connect(m_d.searchPanel, &SearchPanel::cleared, this,
      42         [ +  - ]:          57 :           &SearchCoordinator::exitSearch);
      43                 :             : 
      44                 :             :   // Sprint 60 (A1): live filtering. Each control change in the panel updates
      45                 :             :   // the search string in the CommandBar IMMEDIATELY (visible feedback) and
      46                 :             :   // arms a short debounce timer; when it fires, the search runs with the
      47                 :             :   // current state.
      48   [ +  -  +  -  :          57 :   m_searchDebounce = new QTimer(this);
             -  +  -  - ]
      49         [ +  - ]:          57 :   m_searchDebounce->setSingleShot(true);
      50         [ +  - ]:          57 :   m_searchDebounce->setInterval(300);
      51         [ +  - ]:          57 :   connect(m_d.searchPanel, &SearchPanel::filterEdited, this, [this]() {
      52         [ +  - ]:          68 :     const QString q = buildSearchQuery(m_d.searchPanel->freeText(),
      53   [ +  -  +  - ]:         102 :                                        m_d.searchPanel->filter());
      54         [ +  - ]:          34 :     m_d.commandBar->setInputText(q);
      55         [ +  - ]:          34 :     m_searchDebounce->start();
      56                 :          34 :   });
      57         [ +  - ]:          57 :   connect(m_searchDebounce, &QTimer::timeout, this, [this]() {
      58   [ +  -  +  -  :          14 :     runSearch(buildSearchQuery(m_d.searchPanel->freeText(),
                   +  - ]
      59         [ +  - ]:          28 :                                m_d.searchPanel->filter()));
      60                 :          14 :   });
      61                 :             : 
      62                 :             :   // Receive server-side search results from the dedicated search connection.
      63                 :          57 :   connect(m_d.controller, &MailController::serverSearchResultReceived, this,
      64         [ +  - ]:          57 :           [this](const QList<qint64> &uids, qint64 folderId,
      65                 :             :                  const QString &folderPath) {
      66         [ +  + ]:          22 :             if (m_lastSearchQuery.isEmpty())
      67                 :           6 :               return;
      68                 :             : 
      69                 :          16 :             int added = 0;
      70                 :          16 :             QList<MailHeader> extra;
      71         [ +  + ]:          53 :             for (qint64 uid : uids) {
      72                 :             :               // T-407: Use the folder context from the search signal
      73         [ +  - ]:          37 :               auto hdr = m_d.cache->header(folderId, uid);
      74         [ -  + ]:          37 :               if (!hdr)
      75                 :           0 :                 continue;
      76                 :             : 
      77                 :             :               // T-189: Skip if messageId already in results
      78         [ +  + ]:          37 :               if (!hdr->messageId.isEmpty()) {
      79         [ +  - ]:          35 :                 if (m_searchMessageIds.contains(hdr->messageId))
      80                 :          35 :                   continue;
      81         [ #  # ]:           0 :                 m_searchMessageIds.insert(hdr->messageId);
      82                 :             :               }
      83                 :             : 
      84                 :             :               // T-190: Server prefix with folder name
      85                 :           2 :               hdr->subject =
      86         [ +  - ]:           4 :                   QStringLiteral("[%1] %2").arg(folderPath, hdr->subject);
      87         [ +  - ]:           2 :               extra.append(*hdr);
      88                 :           2 :               ++added;
      89         [ +  + ]:          37 :             }
      90         [ +  + ]:          16 :             if (!extra.isEmpty()) {
      91         [ +  - ]:           2 :               m_d.listModel->appendHeaders(extra);
      92   [ +  -  +  - ]:           2 :               m_d.folderTree->updateSearchCount(m_d.listModel->rowCount());
      93                 :             :               // Update total count + show per-folder increment
      94         [ +  - ]:           2 :               emit statusMessage(
      95         [ +  - ]:           2 :                   QString::fromUtf8("Suche: %1 Treffer f\xc3\xbcr '%2'")
      96   [ +  -  +  - ]:           4 :                       .arg(m_d.listModel->rowCount())
      97         [ +  - ]:           4 :                       .arg(m_lastSearchQuery));
      98         [ +  - ]:           2 :               emit keyedStatusMessage(QStringLiteral("search"),
      99         [ +  - ]:           2 :                                       tr("\xf0\x9f\x94\x8d +%1 from %2")
     100         [ +  - ]:           4 :                                           .arg(added)
     101         [ +  - ]:           6 :                                           .arg(folderPath),
     102                 :             :                                       5000);
     103                 :             :             }
     104                 :          16 :           });
     105                 :             : 
     106                 :             :   // Server-side search complete (all folders searched).
     107                 :          57 :   connect(m_d.controller, &MailController::serverSearchComplete, this,
     108         [ +  - ]:          57 :           [this]() {
     109         [ +  - ]:           8 :             if (m_lastSearchQuery.isEmpty())
     110                 :           8 :               return;
     111         [ #  # ]:           0 :             emit keyedStatusMessage(
     112                 :           0 :                 QStringLiteral("search"),
     113         [ #  # ]:           0 :                 tr("πŸ” Server search complete (%1 results)")
     114   [ #  #  #  # ]:           0 :                     .arg(m_d.listModel->rowCount()),
     115                 :             :                 5000);
     116                 :             :           });
     117                 :          57 : }
     118                 :             : 
     119                 :          36 : QList<MailHeader> SearchCoordinator::headersForSearchResults(
     120                 :             :     const QList<MailCache::SearchResult> &results) {
     121                 :          36 :   QList<MailHeader> headers;
     122         [ +  + ]:         106 :   for (const auto &r : results) {
     123         [ +  - ]:          70 :     auto hdr = m_d.cache->header(r.folderId, r.uid);
     124         [ -  + ]:          70 :     if (!hdr)
     125                 :           0 :       continue;
     126                 :             : 
     127                 :             :     // T-189: Skip if messageId already seen
     128         [ +  + ]:          70 :     if (!hdr->messageId.isEmpty()) {
     129         [ +  + ]:          45 :       if (m_searchMessageIds.contains(hdr->messageId))
     130                 :           1 :         continue;
     131         [ +  - ]:          44 :       m_searchMessageIds.insert(hdr->messageId);
     132                 :             :     }
     133                 :             : 
     134                 :             :     // T-190: Folder prefix in subject
     135         [ +  - ]:          69 :     hdr->subject = QStringLiteral("[%1] %2").arg(r.folderPath, hdr->subject);
     136         [ +  - ]:          69 :     headers.append(*hdr);
     137         [ +  + ]:          70 :   }
     138                 :          36 :   return headers;
     139                 :           0 : }
     140                 :             : 
     141                 :          48 : void SearchCoordinator::resetSearchPanel() {
     142                 :          48 :   m_d.searchPanel->hide();
     143   [ +  -  -  -  :          48 :   m_d.searchPanel->setState(QString(), MailCache::SearchFilter{});
             -  -  -  - ]
     144                 :          48 : }
     145                 :             : 
     146                 :          17 : void SearchCoordinator::exitSearch() {
     147                 :             :   // Cancel any in-flight server search so it stops scanning folders.
     148                 :          17 :   m_d.controller->cancelServerSearch();
     149                 :             :   // Sprint 60 (A1): cancel any pending live-search debounce.
     150                 :          17 :   m_searchDebounce->stop();
     151                 :          17 :   m_lastSearchQuery.clear();
     152                 :          17 :   clearLocalSearchPagination();
     153                 :          17 :   m_searchMessageIds.clear();
     154                 :          17 :   m_searchNodeQuery.clear();
     155         [ +  - ]:          17 :   m_d.proxy->setFilterText({});
     156         [ +  - ]:          17 :   emit windowTitleChangeRequested(QStringLiteral("MailJD"));
     157         [ +  - ]:          17 :   emit statusCleared(QStringLiteral("search"));
     158                 :          17 :   m_d.folderTree->hideSearchNode();
     159                 :             :   // Sprint 59 (U2): hide and reset the refinement panel.
     160                 :          17 :   resetSearchPanel();
     161         [ +  + ]:          17 :   if (!m_preSearchFolder.isEmpty()) {
     162                 :           2 :     m_d.folderTree->selectFolder(m_preSearchFolder);
     163                 :             :     // Force controller reload β€” tree may not re-emit folderSelected if the
     164                 :             :     // same folder was already selected.
     165                 :           2 :     m_d.controller->onFolderSelected(m_preSearchFolder);
     166                 :           2 :     m_preSearchFolder.clear();
     167                 :             :   }
     168                 :          17 :   emit mailListFocusRequested();
     169                 :          17 : }
     170                 :             : 
     171                 :          30 : void SearchCoordinator::onRealFolderSelected() {
     172         [ +  + ]:          30 :   if (!m_lastSearchQuery.isEmpty()) {
     173                 :           1 :     m_lastSearchQuery.clear();
     174                 :           1 :     clearLocalSearchPagination();
     175         [ +  - ]:           1 :     emit windowTitleChangeRequested(QStringLiteral("MailJD"));
     176                 :             :   }
     177                 :             :   // Sprint 60 (A2): the SearchPanel belongs to search mode only β€” hide and
     178                 :             :   // reset it whenever a real folder is selected, and cancel any pending
     179                 :             :   // live-search debounce so it cannot fire after we left.
     180                 :          30 :   m_searchDebounce->stop();
     181                 :          30 :   resetSearchPanel();
     182                 :          30 : }
     183                 :             : 
     184                 :           3 : void SearchCoordinator::onSearchBarEscape() {
     185         [ +  + ]:           3 :   if (m_lastSearchQuery.isEmpty())
     186                 :           2 :     return;
     187                 :           1 :   m_lastSearchQuery.clear();
     188                 :           1 :   clearLocalSearchPagination();
     189                 :           1 :   m_searchMessageIds.clear();
     190         [ +  - ]:           1 :   emit windowTitleChangeRequested(QStringLiteral("MailJD"));
     191         [ +  - ]:           1 :   emit statusCleared(QStringLiteral("search"));
     192                 :           1 :   m_searchNodeQuery.clear();
     193                 :           1 :   m_d.folderTree->hideSearchNode();
     194                 :           1 :   m_searchDebounce->stop();
     195                 :           1 :   resetSearchPanel();
     196         [ -  + ]:           1 :   if (!m_preSearchFolder.isEmpty()) {
     197                 :           0 :     m_d.folderTree->selectFolder(m_preSearchFolder);
     198                 :             :     // Force controller reload β€” tree may not re-emit folderSelected
     199                 :             :     // if the same folder was already selected
     200                 :           0 :     m_d.controller->onFolderSelected(m_preSearchFolder);
     201                 :           0 :     m_preSearchFolder.clear();
     202                 :             :   }
     203                 :             : }
     204                 :             : 
     205                 :           4 : void SearchCoordinator::onCommandBarCancelled() {
     206         [ +  + ]:           4 :   if (m_lastSearchQuery.isEmpty())
     207                 :           3 :     return;
     208                 :           1 :   m_lastSearchQuery.clear();
     209                 :           1 :   clearLocalSearchPagination();
     210                 :           1 :   m_searchMessageIds.clear();
     211                 :             :   // Cancel any ongoing server search
     212                 :           1 :   m_d.controller->cancelServerSearch();
     213         [ +  - ]:           1 :   emit statusCleared(QStringLiteral("search"));
     214         [ +  - ]:           1 :   emit windowTitleChangeRequested(QStringLiteral("MailJD"));
     215         [ -  + ]:           1 :   if (!m_preSearchFolder.isEmpty()) {
     216                 :           0 :     m_d.folderTree->selectFolder(m_preSearchFolder);
     217                 :           0 :     m_preSearchFolder.clear();
     218                 :             :   }
     219                 :             : }
     220                 :             : 
     221                 :          39 : void SearchCoordinator::runSearch(const QString &query) {
     222                 :             :   // T-192: Parse filter prefixes from the query.
     223                 :          39 :   MailCache::SearchFilter filter;
     224         [ +  - ]:          39 :   QString ftsQuery = parseSearchQuery(query, filter);
     225                 :             : 
     226                 :             :   // Sprint 59 (A1): a search is valid when there is free text OR any facet.
     227                 :             :   // A bare-empty input (no text, no facets) is not a search.
     228   [ +  +  +  -  :          39 :   if (ftsQuery.isEmpty() && filter.isEmpty())
             +  +  +  + ]
     229                 :           3 :     return;
     230                 :             : 
     231                 :             :   // Sprint 59: the canonical query is the single source of truth for the
     232                 :             :   // window title, the "Suche" node and the search bar. For a pure facets-only
     233                 :             :   // search it contains only prefixes (empty free text).
     234         [ +  - ]:          36 :   const QString canonicalQuery = buildSearchQuery(ftsQuery, filter);
     235                 :             : 
     236                 :             :   // T-188: Save current folder for Esc restore β€” but only when ENTERING
     237                 :             :   // search, not when refining an already-active search (otherwise we'd lose
     238                 :             :   // the folder to return to).
     239                 :          36 :   const bool alreadySearching = !m_lastSearchQuery.isEmpty();
     240         [ +  + ]:          36 :   if (!alreadySearching)
     241                 :          19 :     m_preSearchFolder = m_d.controller->currentFolder();
     242                 :             : 
     243                 :             :   // T-189: Dedup set + T-190: folder prefix
     244                 :          36 :   m_searchMessageIds.clear();
     245                 :          36 :   m_lastLocalSearchQuery = ftsQuery;
     246                 :          36 :   m_lastSearchFilter = filter;
     247                 :          36 :   m_localSearchOffset = 0;
     248                 :          36 :   m_localSearchHasMore = false;
     249                 :             :   // Keep the canonical query so re-selecting the "Suche" node restores the
     250                 :             :   // exact same filter state.
     251                 :          36 :   m_searchNodeQuery = canonicalQuery;
     252                 :             : 
     253                 :             :   // Sprint 59 (U2/U3): push the parsed state into the panel and the bar so
     254                 :             :   // all three (state, panel, bar) agree. Signals are blocked by setState()
     255                 :             :   // and setInputText() respectively, so this never re-triggers a search.
     256         [ +  - ]:          36 :   m_d.searchPanel->setKnownFolders(m_knownFolders);
     257         [ +  - ]:          36 :   if (m_d.cache)
     258   [ +  -  +  - ]:          36 :     m_d.searchPanel->setKnownTags(m_d.cache->knownLabels());
     259         [ +  - ]:          36 :   m_d.searchPanel->setState(ftsQuery, filter);
     260         [ +  - ]:          36 :   m_d.searchPanel->show();
     261         [ +  - ]:          36 :   m_d.commandBar->setInputText(canonicalQuery);
     262                 :             : 
     263                 :             :   // 67.A3: a re-run (live-filter debounce, refinement) rebuilds the
     264                 :             :   // result list via setHeaders β†’ model reset β†’ the clicked row would
     265                 :             :   // silently lose its highlight. Snapshot the selection to restore it
     266                 :             :   // afterwards. Entering search fresh intentionally starts unselected.
     267                 :          36 :   QPair<qint64, qint64> previousSelection{-1, -1};
     268   [ +  +  +  -  :          36 :   if (alreadySearching && m_d.currentSelection)
                   +  + ]
     269         [ +  - ]:          17 :     previousSelection = m_d.currentSelection();
     270                 :             : 
     271                 :             :   // Populate the main list
     272                 :             :   // T-197: Clear selection first to prevent currentChanged from
     273                 :             :   // firing onMailSelected for the auto-selected first row
     274         [ +  - ]:          36 :   emit mailSelectionClearRequested();
     275         [ +  - ]:          36 :   appendLocalSearchPage(canonicalQuery, true);
     276                 :             : 
     277                 :             :   // 67.A3: restore the selection when the mail is still in the results.
     278   [ +  +  +  + ]:          37 :   if (previousSelection.second > 0 &&
     279   [ +  -  +  - ]:           1 :       m_d.listModel->rowForUid(previousSelection.second,
     280                 :             :                                previousSelection.first) >= 0) {
     281         [ +  - ]:           1 :     emit mailRevealRequested(previousSelection.first,
     282                 :             :                              previousSelection.second);
     283                 :             :   }
     284                 :             : 
     285                 :             :   // Also trigger server-side IMAP SEARCH. Sprint 59 (S1): the full filter is
     286                 :             :   // passed through; the controller maps the server-friendly facets to a
     287                 :             :   // composite IMAP SEARCH and skips the scan when nothing is server-mappable
     288                 :             :   // (e.g. a has:attachment-only search stays local).
     289                 :          36 :   m_lastSearchQuery = canonicalQuery;
     290         [ +  - ]:          36 :   emit keyedStatusMessage(
     291                 :          72 :       QStringLiteral("search"),
     292         [ +  - ]:          72 :       QString::fromUtf8("\xf0\x9f\x94\x8d Serversuche l\xc3\xa4uft\xe2\x80\xa6"),
     293                 :             :       0);
     294         [ +  - ]:          36 :   m_d.controller->serverSearch(ftsQuery, filter);
     295   [ +  +  +  + ]:          42 : }
     296                 :             : 
     297                 :          36 : int SearchCoordinator::appendLocalSearchPage(const QString &displayQuery,
     298                 :             :                                              bool replace) {
     299                 :             :   // Sprint 59 (A1): there is an active search when there is free text OR a
     300                 :             :   // facet. Previously this only checked the free text, so a facets-only
     301                 :             :   // search ("all unread mails with an attachment") looked like "no search".
     302   [ +  +  +  -  :          36 :   if (m_lastLocalSearchQuery.isEmpty() && m_lastSearchFilter.isEmpty()) {
             -  +  -  + ]
     303         [ #  # ]:           0 :     if (replace) {
     304         [ #  # ]:           0 :       m_d.listModel->setHeaders({});
     305         [ #  # ]:           0 :       m_d.folderTree->showSearchNode(0);
     306                 :             :     }
     307                 :           0 :     m_localSearchHasMore = false;
     308                 :           0 :     return 0;
     309                 :             :   }
     310                 :             : 
     311                 :             :   auto results =
     312                 :          36 :       m_d.cache->searchFts(m_lastLocalSearchQuery, m_lastSearchFilter,
     313         [ +  - ]:          36 :                            kGlobalSearchPageSize + 1, m_localSearchOffset);
     314                 :          36 :   m_localSearchHasMore = results.size() > kGlobalSearchPageSize;
     315         [ -  + ]:          36 :   if (m_localSearchHasMore)
     316                 :           0 :     results.removeLast();
     317                 :          36 :   m_localSearchOffset += results.size();
     318                 :             : 
     319         [ +  - ]:          36 :   const auto headers = headersForSearchResults(results);
     320         [ +  - ]:          36 :   if (replace)
     321         [ +  - ]:          36 :     m_d.listModel->setHeaders(headers);
     322         [ #  # ]:           0 :   else if (!headers.isEmpty())
     323         [ #  # ]:           0 :     m_d.listModel->appendHeaders(headers);
     324                 :             : 
     325         [ +  - ]:          36 :   const int visibleCount = m_d.listModel->rowCount();
     326         [ +  - ]:          36 :   m_d.folderTree->showSearchNode(visibleCount);
     327         [ +  - ]:          36 :   emit windowTitleChangeRequested(
     328         [ +  - ]:          36 :       QString::fromUtf8("Suchergebnisse f\xc3\xbcr '%1' - MailJD")
     329         [ +  - ]:          72 :           .arg(displayQuery));
     330         [ -  + ]:          36 :   if (m_localSearchHasMore) {
     331         [ #  # ]:           0 :     emit statusMessage(
     332         [ #  # ]:           0 :         QString::fromUtf8(
     333                 :             :             "Suche: %1 Treffer f\xc3\xbcr '%2' (weitere mit :search-more)")
     334         [ #  # ]:           0 :             .arg(visibleCount)
     335         [ #  # ]:           0 :             .arg(displayQuery));
     336                 :             :   } else {
     337   [ +  -  +  - ]:          72 :     emit statusMessage(QString::fromUtf8("Suche: %1 Treffer f\xc3\xbcr '%2'")
     338         [ +  - ]:          72 :                            .arg(visibleCount)
     339         [ +  - ]:         108 :                            .arg(displayQuery));
     340                 :             :   }
     341                 :          36 :   return headers.size();
     342                 :          36 : }
     343                 :             : 
     344                 :           3 : void SearchCoordinator::loadMoreLocalResults() {
     345   [ +  +  -  +  :           3 :   if (m_lastSearchQuery.isEmpty() || m_lastLocalSearchQuery.isEmpty()) {
                   +  + ]
     346         [ +  - ]:           2 :     emit statusMessage(QStringLiteral("Keine lokale Suche aktiv"));
     347                 :           2 :     return;
     348                 :             :   }
     349         [ +  - ]:           1 :   if (!m_localSearchHasMore) {
     350         [ +  - ]:           1 :     emit statusMessage(QStringLiteral("Keine weiteren lokalen Suchtreffer"));
     351                 :           1 :     return;
     352                 :             :   }
     353                 :           0 :   appendLocalSearchPage(m_lastSearchQuery, false);
     354                 :             : }
     355                 :             : 
     356                 :          20 : void SearchCoordinator::clearLocalSearchPagination() {
     357                 :          20 :   m_lastLocalSearchQuery.clear();
     358                 :          20 :   m_lastSearchFilter = {};
     359                 :          20 :   m_localSearchOffset = 0;
     360                 :          20 :   m_localSearchHasMore = false;
     361                 :          20 : }
     362                 :             : 
     363                 :          14 : void SearchCoordinator::updateQuickResults(const QString &query) {
     364         [ +  - ]:          14 :   m_quickResults.clear();
     365   [ +  -  +  + ]:          14 :   if (query.trimmed().isEmpty()) {
     366         [ +  - ]:           1 :     m_d.commandBar->setSearchResults({});
     367                 :           1 :     return;
     368                 :             :   }
     369                 :             :   // T-192: Parse filter prefixes
     370                 :          13 :   MailCache::SearchFilter filter;
     371         [ +  - ]:          13 :   QString ftsQuery = parseSearchQuery(query, filter);
     372         [ -  + ]:          13 :   if (ftsQuery.isEmpty()) {
     373         [ #  # ]:           0 :     m_d.commandBar->setSearchResults({});
     374                 :           0 :     return;
     375                 :             :   }
     376                 :             :   // Local FTS5 search with filters
     377         [ +  - ]:          13 :   auto results = m_d.cache->searchFts(ftsQuery, filter, 20);
     378                 :          13 :   QStringList display;
     379   [ +  -  +  -  :          48 :   for (const auto &r : results) {
                   +  + ]
     380         [ +  - ]:         105 :     display.append(QStringLiteral("[%1] %2 β€” %3")
     381   [ +  -  +  -  :          70 :                        .arg(r.folderPath, r.subject.left(60), r.from.left(40)));
                   +  - ]
     382         [ +  - ]:          35 :     m_quickResults.append(r);
     383                 :             :   }
     384         [ +  - ]:          13 :   m_d.commandBar->setSearchResults(display);
     385         [ +  + ]:          13 :   if (results.isEmpty()) {
     386         [ +  - ]:           2 :     emit statusMessage(
     387         [ +  - ]:           6 :         QStringLiteral("Keine Treffer fΓΌr '%1'").arg(ftsQuery));
     388                 :             :   } else {
     389   [ +  -  +  - ]:          22 :     emit statusMessage(QStringLiteral("%1 Treffer").arg(results.size()));
     390                 :             :   }
     391   [ +  -  +  - ]:          13 : }
     392                 :             : 
     393                 :           3 : MailCache::SearchResult SearchCoordinator::quickResultAt(int index) const {
     394   [ +  +  +  +  :           3 :   if (index < 0 || index >= m_quickResults.size())
                   +  + ]
     395                 :           2 :     return {};
     396                 :           1 :   return m_quickResults.at(index);
     397                 :             : }
        

Generated by: LCOV version 2.0-1