diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/notmuchworker.cpp | 77 | ||||
| -rw-r--r-- | src/notmuchworker.h | 38 |
2 files changed, 84 insertions, 31 deletions
diff --git a/src/notmuchworker.cpp b/src/notmuchworker.cpp index d41b54b..e5f5afa 100644 --- a/src/notmuchworker.cpp +++ b/src/notmuchworker.cpp @@ -362,12 +362,19 @@ static const int kSenderCountsMetaType = static const int kThreadDigestMetaType = qRegisterMetaType<ThreadDigest>("ThreadDigest"); +/// And for applyRule()'s answer, which crosses the queued ruleApplied +/// connection as a container. An unregistered one is dropped with only a +/// warning, and the window would then never build the rule's undo entry. +static const int kTagChangesMetaType = + qRegisterMetaType<QVector<TagChange>>("QVector<TagChange>"); + NotmuchWorker::NotmuchWorker(const QString ¬muchConfigPath, QObject *parent) : QObject(parent), m_configPath(notmuchConfigPath) { Q_UNUSED(kSortOrderMetaType); Q_UNUSED(kSenderCountsMetaType); Q_UNUSED(kThreadDigestMetaType); + Q_UNUSED(kTagChangesMetaType); } NotmuchWorker::~NotmuchWorker() @@ -1083,25 +1090,39 @@ void NotmuchWorker::applyTagsToThreads(const QStringList &threadIds, applyTags(TagChange{ messageIds, add, remove, description }); } -void NotmuchWorker::applyTagsToQuery(const QString &query, - const QStringList &add, - const QStringList &remove, - const QString &description) +void NotmuchWorker::applyRule(const QString &query, const QStringList &add, + const QStringList &remove, + const QString &description, quint64 token) { - if (!openReadOnly()) + QVector<TagChange> changes; + if (!openReadOnly()) { + emit ruleApplied(token, changes); return; + } QStringList messageIds; if (!collectMessageIds(m_db, SearchTerm::changes(query, add, remove), &messageIds)) { emit errorOccurred(QStringLiteral("Cannot resolve the rule's query")); + emit ruleApplied(token, changes); return; } - // Empty is not an error: the rule has nothing left to do. applyTags() - // returns at once on an empty list, and stays silent, as it must - // (CLAUDE.md, item 176). - applyTags(TagChange{ messageIds, add, remove, description }); + QList<TagChange> ops; + for (const QString &tag : add) + ops.append(TagChange{ messageIds, { tag }, {}, description }); + for (const QString &tag : remove) + ops.append(TagChange{ messageIds, {}, { tag }, description }); + + for (TagChange &op : ops) { + QStringList changed; + // An empty list writes nothing; writeTags() is not even asked to open. + if (!messageIds.isEmpty() && !writeTags(op, &changed)) + break; // errorOccurred() already emitted; report what landed + op.messageIds = changed; + changes.append(op); + } + emit ruleApplied(token, changes); } void NotmuchWorker::applyTags(const TagChange &change) @@ -1109,6 +1130,25 @@ void NotmuchWorker::applyTags(const TagChange &change) if (change.messageIds.isEmpty()) return; + QStringList changedIds; + if (!writeTags(change, &changedIds)) + return; + + // Nothing moved, so there is nothing to record as pending, nothing to sync + // and nothing to undo. Emitting an empty change would push an undo entry + // whose inverse adds a tag no message ever carried, which is item 176 one + // step further along. The same silence as the empty-id early return above, + // for the same reason. + if (changedIds.isEmpty()) + return; + + emit tagsApplied(TagChange{ changedIds, change.added, change.removed, + change.description }); +} + +bool NotmuchWorker::writeTags(const TagChange &change, QStringList *changedIds) +{ + // The read-only handle must be closed first: notmuch allows only one open // handle per process. close(); @@ -1139,7 +1179,7 @@ void NotmuchWorker::applyTags(const TagChange &change) .arg(QString::fromUtf8(error ? error : notmuch_status_to_string(status)))); free(error); - return; + return false; } // Item 176. What the caller ASKED for is not what the write does: a @@ -1149,8 +1189,8 @@ void NotmuchWorker::applyTags(const TagChange &change) // unread", measured on real mail as 2 unread becoming 43. With // maildir.synchronize_flags on, that rewrites filenames and reaches the // server. - QStringList changedIds; - changedIds.reserve(change.messageIds.size()); + changedIds->clear(); + changedIds->reserve(change.messageIds.size()); for (const QString &id : change.messageIds) { notmuch_message_t *raw = nullptr; @@ -1196,7 +1236,7 @@ void NotmuchWorker::applyTags(const TagChange &change) moves = moves || !before.contains(tag); moves = moves || !strippedOrigins.isEmpty(); if (moves) - changedIds.append(id); + changedIds->append(id); notmuch_message_freeze(message.get()); for (const QString &tag : change.removed) @@ -1215,16 +1255,7 @@ void NotmuchWorker::applyTags(const TagChange &change) notmuch_database_close(db); notmuch_database_destroy(db); - // Nothing moved, so there is nothing to record as pending, nothing to sync - // and nothing to undo. Emitting an empty change would push an undo entry - // whose inverse adds a tag no message ever carried, which is item 176 one - // step further along. The same silence as the empty-id early return above, - // for the same reason. - if (changedIds.isEmpty()) - return; - - emit tagsApplied(TagChange{ changedIds, change.added, change.removed, - change.description }); + return true; } void NotmuchWorker::moveMessages(const QStringList &messageIds, diff --git a/src/notmuchworker.h b/src/notmuchworker.h index fa223e6..a175d7a 100644 --- a/src/notmuchworker.h +++ b/src/notmuchworker.h @@ -232,14 +232,24 @@ public slots: const QStringList &remove, const QString &description); - /// Applies `+add -remove` to every message the change would move within - /// `query` (item 212, a rule applied to existing mail). The query is - /// narrowed with SearchTerm::changes() so already-correct messages are - /// never written, then the ids go through applyTags(), which reports the - /// effective set for undo. Emits nothing when nothing would change. - void applyTagsToQuery(const QString &query, const QStringList &add, - const QStringList &remove, - const QString &description); + /// Applies a tagging rule to existing mail (item 212). Resolves the + /// messages the WHOLE rule would move once, with SearchTerm::changes(), + /// then writes one tag at a time over that fixed list: `+a` for each add, + /// `-r` for each remove. + /// + /// One write per tag because the effective set is per MESSAGE: a single + /// `+promo -inbox` write reports a message that already had promo (it lost + /// inbox), and the undo built from it would strip promo. One resolution + /// because a query may name a tag the rule removes, and re-resolving after + /// the first write would match nothing for the rest, writing half the + /// rule where the hook writes all of it. + /// + /// Answers with ruleApplied() ALWAYS, even when nothing moved, and never + /// with tagsApplied(): the window builds the undo from the answer rather + /// than from the shared confirmation queue. + void applyRule(const QString &query, const QStringList &add, + const QStringList &remove, const QString &description, + quint64 token); /// Resolves whole threads to the message ids and file paths they contain. /// @@ -386,6 +396,13 @@ signals: void threadDigestLoaded(const ThreadDigest &digest, quint64 generation); void tagsApplied(const TagChange &change); + /// applyRule()'s answer: one TagChange per single-tag operation, in the + /// order applyRule() documents, each carrying the ids that operation + /// actually moved (possibly none). `token` is echoed from the request. + /// Emitted even after a write error, carrying what was written before it, + /// so the window can still offer undo for that part. + void ruleApplied(quint64 token, const QVector<TagChange> &changes); + /// One file entered or left the index outside a query (item 192). /// /// Emitted by indexDraftFile() and removeIndexedFile(), the two entry @@ -483,6 +500,11 @@ private: bool openReadOnly(); void close(); + /// applyTags() without the signal: writes `change` and fills `changedIds` + /// with the messages whose tags really moved. False, with errorOccurred() + /// emitted, when the database could not be opened for writing. + bool writeTags(const TagChange &change, QStringList *changedIds); + QByteArray configPathArg() const; QString m_configPath; |
