From 8e65d8f5eba78acc9558e26f9b728521f323b1a6 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 3 Aug 2026 20:49:25 +0200 Subject: feat(worker): list every tag in the database Query bar completion cannot offer tag names without a way to enumerate them, and libnotmuch had no call wired up for it. Follows the existing generation-counter pattern; the result crosses the thread boundary as a QStringList. Co-Authored-By: Claude Opus 5 --- src/notmuchworker.cpp | 21 +++++++++++++++++++++ src/notmuchworker.h | 6 ++++++ tests/test_notmuchworker.cpp | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/src/notmuchworker.cpp b/src/notmuchworker.cpp index ed97e39..a5096d2 100644 --- a/src/notmuchworker.cpp +++ b/src/notmuchworker.cpp @@ -332,3 +332,24 @@ void NotmuchWorker::applyTags(const TagChange &change) emit tagsApplied(change); } + +void NotmuchWorker::requestAllTags(quint64 generation) +{ + if (!openReadOnly()) + return; + + NmTags tags(notmuch_database_get_all_tags(m_db)); + if (!tags) { + emit errorOccurred(QStringLiteral("Cannot list tags")); + return; + } + + QStringList result; + for (; notmuch_tags_valid(tags.get()); notmuch_tags_move_to_next(tags.get())) + result.append(QString::fromUtf8(notmuch_tags_get(tags.get()))); + + // Sorted once here so no consumer has to sort again. notmuch returns tags + // in Xapian term order, which is byte order, not the user's locale order. + result.sort(); + emit allTagsReady(result, generation); +} diff --git a/src/notmuchworker.h b/src/notmuchworker.h index e0c80c6..88187ec 100644 --- a/src/notmuchworker.h +++ b/src/notmuchworker.h @@ -69,11 +69,17 @@ public slots: const QStringList &remove, const QString &description); + /// Every tag in the database, sorted. Feeds query bar completion, which + /// cannot offer tag names it has no way to enumerate. Called at startup, + /// after a sync, and after a tag mutation introduces an unknown tag. + void requestAllTags(quint64 generation); + signals: void threadsReady(const QVector &threads, quint64 generation); void queryFinished(int totalThreads, quint64 generation); void threadLoaded(const QVector &messages, quint64 generation); void tagsApplied(const TagChange &change); + void allTagsReady(const QStringList &tags, quint64 generation); void errorOccurred(const QString &message); private: diff --git a/tests/test_notmuchworker.cpp b/tests/test_notmuchworker.cpp index 699f72c..488b630 100644 --- a/tests/test_notmuchworker.cpp +++ b/tests/test_notmuchworker.cpp @@ -55,6 +55,9 @@ private slots: void applyTagsToThreadsSpansMultipleThreads(); void applyTagsToThreadsWithNoThreadsDoesNothing(); + void requestAllTagsReturnsSortedTags(); + void requestAllTagsOnUnreadableConfigEmitsError(); + private: /// Tags of one message, read back through a fresh worker query. QStringList tagsOf(const QString &messageId); @@ -433,5 +436,41 @@ void TestNotmuchWorker::applyTagsToThreadsWithNoThreadsDoesNothing() QVERIFY(errors.isEmpty()); } +void TestNotmuchWorker::requestAllTagsReturnsSortedTags() +{ + NotmuchWorker worker(m_fixture.configPath()); + QSignalSpy spy(&worker, &NotmuchWorker::allTagsReady); + + worker.requestAllTags(7); + + QCOMPARE(spy.count(), 1); + const QStringList tags = spy.at(0).at(0).toStringList(); + const quint64 generation = spy.at(0).at(1).value(); + + QCOMPARE(generation, quint64(7)); + QVERIFY(tags.contains(QStringLiteral("inbox"))); + QVERIFY(tags.contains(QStringLiteral("unread"))); + + // Completion offers these in order, so the worker sorts once rather than + // every consumer sorting again. + QStringList sorted = tags; + sorted.sort(); + QCOMPARE(tags, sorted); +} + +void TestNotmuchWorker::requestAllTagsOnUnreadableConfigEmitsError() +{ + // Fails closed like every other entry point: never silently fall through to + // the user's real database. + NotmuchWorker worker(QStringLiteral("/nonexistent/qtmaildir-test/config")); + QSignalSpy ready(&worker, &NotmuchWorker::allTagsReady); + QSignalSpy errors(&worker, &NotmuchWorker::errorOccurred); + + worker.requestAllTags(1); + + QCOMPARE(errors.size(), 1); + QVERIFY(ready.isEmpty()); +} + QTEST_MAIN(TestNotmuchWorker) #include "test_notmuchworker.moc" -- cgit v1.2.3