aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/completionentry.h32
-rw-r--r--src/querycompleter.cpp60
-rw-r--r--src/querycompleter.h15
-rw-r--r--tests/test_querycompleter.cpp35
4 files changed, 142 insertions, 0 deletions
diff --git a/src/completionentry.h b/src/completionentry.h
new file mode 100644
index 0000000..0227182
--- /dev/null
+++ b/src/completionentry.h
@@ -0,0 +1,32 @@
+/*
+ * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs
+ * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#pragma once
+
+#include <QString>
+
+/// One completion candidate and the prose describing it.
+///
+/// This lives in its own header because both Config and QueryCompleter need
+/// it, and QueryCompleter needs Config. Declaring it in querycompleter.h
+/// would make the two headers include each other.
+struct CompletionEntry
+{
+ QString value; ///< Inserted verbatim. Query syntax, never translated.
+ QString description; ///< Shown beside it. Prose, always translated.
+};
diff --git a/src/querycompleter.cpp b/src/querycompleter.cpp
index 6bdd494..77c62c1 100644
--- a/src/querycompleter.cpp
+++ b/src/querycompleter.cpp
@@ -18,8 +18,20 @@
#include "querycompleter.h"
+#include <QCoreApplication>
+
namespace {
+// The vocabulary lives in free functions, not in a QObject, so there is no
+// inherited tr(). Q_DECLARE_TR_FUNCTIONS gives this namespace its own tr()
+// bound to an explicit context, which is what lupdate scans for. Calling
+// QObject::tr() here would compile but file every string under the "QObject"
+// context, mixing the vocabulary in with unrelated strings.
+class VocabularyStrings
+{
+ Q_DECLARE_TR_FUNCTIONS(VocabularyStrings)
+};
+
/// Whether the cursor sits inside a double-quoted literal. Counts quotes from
/// the start: an odd count before the cursor means the quote is still open.
bool insideQuotes(const QString &text, int cursor)
@@ -64,6 +76,54 @@ int tokenEnd(const QString &text, int cursor)
} // namespace
+QList<CompletionEntry> prefixVocabulary()
+{
+ return {
+ { QStringLiteral("tag:"), VocabularyStrings::tr("messages with a tag") },
+ { QStringLiteral("is:"), VocabularyStrings::tr("same as tag:") },
+ { QStringLiteral("from:"), VocabularyStrings::tr("sender address or name") },
+ { QStringLiteral("to:"), VocabularyStrings::tr("recipient, including Cc") },
+ { QStringLiteral("subject:"), VocabularyStrings::tr("words in the subject") },
+ { QStringLiteral("date:"), VocabularyStrings::tr("a date or a range") },
+ { QStringLiteral("attachment:"), VocabularyStrings::tr("attachment filename") },
+ { QStringLiteral("mimetype:"), VocabularyStrings::tr("attachment content type") },
+ { QStringLiteral("folder:"), VocabularyStrings::tr("Maildir folder name") },
+ { QStringLiteral("path:"), VocabularyStrings::tr("directory below the Maildir root") },
+ { QStringLiteral("thread:"), VocabularyStrings::tr("a thread id") },
+ { QStringLiteral("id:"), VocabularyStrings::tr("a single message id") },
+ { QStringLiteral("and"), VocabularyStrings::tr("both conditions") },
+ { QStringLiteral("or"), VocabularyStrings::tr("either condition") },
+ { QStringLiteral("not"), VocabularyStrings::tr("exclude what follows") },
+ };
+}
+
+QList<CompletionEntry> dateVocabulary()
+{
+ return {
+ { QStringLiteral("today"), VocabularyStrings::tr("since midnight") },
+ { QStringLiteral("yesterday"), VocabularyStrings::tr("the previous day") },
+ { QStringLiteral("this_week"), VocabularyStrings::tr("the current week") },
+ { QStringLiteral("last_week"), VocabularyStrings::tr("the week before this one") },
+ { QStringLiteral("this_month"), VocabularyStrings::tr("the current month") },
+ { QStringLiteral("last_month"), VocabularyStrings::tr("the month before this one") },
+ { QStringLiteral("this_year"), VocabularyStrings::tr("the current year") },
+ // These two are complete open-ended ranges, hence the trailing "..".
+ { QStringLiteral("1week.."), VocabularyStrings::tr("the last seven days") },
+ { QStringLiteral("1month.."), VocabularyStrings::tr("the last month") },
+ };
+}
+
+QList<CompletionEntry> mimetypeVocabulary()
+{
+ return {
+ { QStringLiteral("application/pdf"), VocabularyStrings::tr("PDF document") },
+ { QStringLiteral("image/jpeg"), VocabularyStrings::tr("JPEG image") },
+ { QStringLiteral("image/png"), VocabularyStrings::tr("PNG image") },
+ { QStringLiteral("text/html"), VocabularyStrings::tr("HTML document") },
+ { QStringLiteral("application/zip"), VocabularyStrings::tr("ZIP archive") },
+ };
+}
+
CompletionContext completionContext(const QString &text, int cursor)
{
CompletionContext ctx;
diff --git a/src/querycompleter.h b/src/querycompleter.h
index e2a983b..7c00ce2 100644
--- a/src/querycompleter.h
+++ b/src/querycompleter.h
@@ -18,8 +18,11 @@
#pragma once
+#include <QList>
#include <QString>
+#include "completionentry.h"
+
/// Where the cursor sits in a query, and therefore what should be offered.
///
/// A plain value type produced by a pure function so the parsing rules can be
@@ -57,3 +60,15 @@ struct CompletionContext
///
/// `cursor` is an offset into `text`, as QLineEdit::cursorPosition() returns.
CompletionContext completionContext(const QString &text, int cursor);
+
+/// The notmuch query keywords, with descriptions. Hardcoded: notmuch exposes
+/// no way to enumerate its own prefixes, so this list must track releases by
+/// hand. See the spec's Consequences section.
+QList<CompletionEntry> prefixVocabulary();
+
+/// Symbolic and relative date values. Absolute dates are not enumerable and
+/// are covered by the free-form hint in the popup footer instead.
+QList<CompletionEntry> dateVocabulary();
+
+/// The built-in mimetypes, before the user's extra_mimetypes are appended.
+QList<CompletionEntry> mimetypeVocabulary();
diff --git a/tests/test_querycompleter.cpp b/tests/test_querycompleter.cpp
index aead30d..fb14adf 100644
--- a/tests/test_querycompleter.cpp
+++ b/tests/test_querycompleter.cpp
@@ -39,6 +39,8 @@ private slots:
void rangeLowerBoundCompletes();
void bareValueAllowsRelativeEntries();
void rangeSuppressesRelativeEntries();
+ void prefixVocabularyCoversNotmuchKeywords();
+ void dateVocabularySeparatesRelativeEntries();
};
void TestQueryCompleter::emptyTextCompletesPrefix()
@@ -167,5 +169,38 @@ void TestQueryCompleter::rangeSuppressesRelativeEntries()
QVERIFY(!ctx.allowRangeEntries);
}
+void TestQueryCompleter::prefixVocabularyCoversNotmuchKeywords()
+{
+ const QList<CompletionEntry> entries = prefixVocabulary();
+
+ QStringList values;
+ for (const CompletionEntry &entry : entries)
+ values.append(entry.value);
+
+ QVERIFY(values.contains(QStringLiteral("tag:")));
+ QVERIFY(values.contains(QStringLiteral("date:")));
+ QVERIFY(values.contains(QStringLiteral("and")));
+
+ // Every entry carries a description; a blank column teaches nothing.
+ for (const CompletionEntry &entry : entries)
+ QVERIFY(!entry.description.isEmpty());
+}
+
+void TestQueryCompleter::dateVocabularySeparatesRelativeEntries()
+{
+ const QList<CompletionEntry> entries = dateVocabulary();
+
+ bool sawSymbolic = false;
+ bool sawRelative = false;
+ for (const CompletionEntry &entry : entries) {
+ if (entry.value == QStringLiteral("today"))
+ sawSymbolic = true;
+ if (entry.value.contains(QStringLiteral("..")))
+ sawRelative = true;
+ }
+ QVERIFY(sawSymbolic);
+ QVERIFY(sawRelative);
+}
+
QTEST_MAIN(TestQueryCompleter)
#include "test_querycompleter.moc"