aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/searchterm.cpp15
-rw-r--r--src/searchterm.h18
-rw-r--r--tests/test_searchterm.cpp39
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"