diff options
| -rw-r--r-- | src/completionentry.h | 32 | ||||
| -rw-r--r-- | src/querycompleter.cpp | 60 | ||||
| -rw-r--r-- | src/querycompleter.h | 15 | ||||
| -rw-r--r-- | tests/test_querycompleter.cpp | 35 |
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" |
