diff options
| -rw-r--r-- | src/searchterm.cpp | 15 | ||||
| -rw-r--r-- | src/searchterm.h | 18 | ||||
| -rw-r--r-- | tests/test_searchterm.cpp | 39 |
3 files changed, 72 insertions, 0 deletions
diff --git a/src/searchterm.cpp b/src/searchterm.cpp index acc64c6..6bfb521 100644 --- a/src/searchterm.cpp +++ b/src/searchterm.cpp @@ -90,4 +90,19 @@ QString extend(const QString &existing, const QString &addition) return QStringLiteral("(%1) AND (%2)").arg(left, right); } +QString exclude(const QString &existing, const QString &addition) +{ + const QString left = existing.trimmed(); + const QString right = addition.trimmed(); + + if (right.isEmpty()) + return left; + // NOT a replace, unlike extend(): see the header. An empty left would make + // this "everything except", which no right-click asked for. + if (left.isEmpty()) + return QString(); + + return QStringLiteral("(%1) AND NOT (%2)").arg(left, right); +} + } // namespace SearchTerm diff --git a/src/searchterm.h b/src/searchterm.h index d596676..646fd77 100644 --- a/src/searchterm.h +++ b/src/searchterm.h @@ -76,6 +76,24 @@ QString tag(const QString &name); /// matches nothing; an empty `addition` leaves `existing` untouched. QString extend(const QString &existing, const QString &addition); +/// Narrows `existing` by everything that is NOT `addition`, as +/// `(existing) AND NOT (addition)`. +/// +/// **Both sides are parenthesised, for the same load-bearing reason as +/// extend().** The query bar may hold a hand-written disjunction, and +/// `a or b AND NOT c` binds as `a or (b AND NOT c)`: the exclusion would cover +/// only the second term, leaving on screen exactly the mail the user asked to +/// be rid of, with no error reported anywhere. +/// +/// **An empty `existing` yields an EMPTY STRING, unlike extend().** Excluding +/// from nothing would mean the entire Maildir minus one value: a legitimate +/// query, and an implausible thing to have meant by right-clicking a value in +/// a fresh window. The menus grey the entry out when the query bar is empty; +/// this is the second layer, against a caller that forgets the guard. +/// +/// An empty `addition` leaves `existing` untouched. +QString exclude(const QString &existing, const QString &addition); + } // namespace SearchTerm /// One entry a context menu can offer: a finished query and the text naming it. diff --git a/tests/test_searchterm.cpp b/tests/test_searchterm.cpp index e0bdb60..53185f3 100644 --- a/tests/test_searchterm.cpp +++ b/tests/test_searchterm.cpp @@ -41,6 +41,9 @@ private slots: void tagIsNotQuoted(); void extendParenthesisesBothSides(); void extendOntoAnEmptyQueryIsAReplace(); + void excludeParenthesisesBothSides(); + void excludeFromAnEmptyQueryIsEmpty(); + void excludeWithNothingToExcludeLeavesTheQuery(); }; void TestSearchTerm::quotesAPlainValue() @@ -142,5 +145,41 @@ void TestSearchTerm::extendOntoAnEmptyQueryIsAReplace() QStringLiteral("tag:inbox")); } +void TestSearchTerm::excludeParenthesisesBothSides() +{ + // The same trap as extendParenthesisesBothSides, and worse in this + // direction. Unparenthesised, `a or b AND NOT c` binds as + // `a or (b AND NOT c)`: the exclusion covers only the second term, so + // every message matching `a` stays on screen INCLUDING the ones the user + // asked to be rid of. notmuch reports no error for either form, so this + // assertion is the only thing that fails. + QCOMPARE(SearchTerm::exclude(QStringLiteral("tag:inbox or tag:flagged"), + QStringLiteral("from:foo@example.org")), + QStringLiteral( + "(tag:inbox or tag:flagged) AND NOT (from:foo@example.org)")); +} + +void TestSearchTerm::excludeFromAnEmptyQueryIsEmpty() +{ + // Deliberately NOT extend()'s behaviour. extend() returns the addition + // alone, because narrowing nothing by x sensibly means x. Excluding from + // nothing would mean the whole Maildir minus one value: a legitimate + // query, and an implausible thing to have meant by right-clicking a value + // in a fresh window. The menus grey the entry out; this is the second + // layer, against a caller that forgets the guard. + QVERIFY(SearchTerm::exclude(QString(), QStringLiteral("tag:inbox")) + .isEmpty()); + QVERIFY(SearchTerm::exclude(QStringLiteral(" "), + QStringLiteral("tag:inbox")) + .isEmpty()); +} + +void TestSearchTerm::excludeWithNothingToExcludeLeavesTheQuery() +{ + QCOMPARE(SearchTerm::exclude(QStringLiteral("tag:inbox"), QString()), + QStringLiteral("tag:inbox")); + QVERIFY(SearchTerm::exclude(QString(), QString()).isEmpty()); +} + QTEST_MAIN(TestSearchTerm) #include "test_searchterm.moc" |
