From fc3073f452e28733c3910ceda403531b3e17404d Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 3 Aug 2026 21:11:30 +0200 Subject: feat(completion): wire query completion into the main window Adds the complete_query action, whose registration the assertion in registerActions() has been demanding since keymap gained the name: a known action nothing implements would have been a silently dead binding. Also lands the QueryCompleter half of the manual trigger, which the keymap commit left behind: triggerCompletion() and the focus-in filter gated on completion_on_focus. Tags refresh at startup, after a sync, and after a mutation introduces a tag not already known, since that is the tag most likely to be typed again. onAllTagsReady deliberately drops the generation the signal carries: a tag list is not an ordered query result, so a later one is always at least as good as an earlier one and discarding on staleness could only throw away a good list. --- src/mainwindow.cpp | 50 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) (limited to 'src/mainwindow.cpp') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 517941a..8ca6aa9 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -47,6 +47,7 @@ #include "messageview.h" #include "mimeparser.h" #include "notmuchworker.h" +#include "querycompleter.h" #include "tagchip.h" #include "threadlistmodel.h" #include "version.h" @@ -221,6 +222,7 @@ void MainWindow::buildUi() m_queryEdit->setPlaceholderText(tr("notmuch query, e.g. tag:inbox")); connect(m_queryEdit, &QLineEdit::returnPressed, this, &MainWindow::runCurrentQuery); + m_queryCompleter = new QueryCompleter(m_queryEdit, m_config, this); m_syncLog = new QPlainTextEdit(central); m_syncLog->setReadOnly(true); @@ -449,6 +451,13 @@ void MainWindow::registerActions() if (m_sync->isAvailable()) m_sync->start(); }); + addAction(QStringLiteral("complete_query"), tr("&Complete query"), + tr("Offer completions for the query bar"), [this]() { + // Focus first: the popup anchors on the line edit, and the binding is + // reachable from the thread list where the bar has no focus at all. + m_queryEdit->setFocus(); + m_queryCompleter->triggerCompletion(); + }); addAction(QStringLiteral("quit"), tr("&Quit"), tr("Quit qtmaildir"), [this]() { close(); }); @@ -469,6 +478,7 @@ void MainWindow::buildMenus() editMenu->addAction(m_actions.value(QStringLiteral("undo"))); editMenu->addSeparator(); editMenu->addAction(m_actions.value(QStringLiteral("focus_query"))); + editMenu->addAction(m_actions.value(QStringLiteral("complete_query"))); auto *messageMenu = menuBar()->addMenu(tr("&Message")); messageMenu->addAction(m_actions.value(QStringLiteral("archive"))); @@ -656,15 +666,51 @@ void MainWindow::wireWorker() this, &MainWindow::onThreadLoaded); connect(m_worker, &NotmuchWorker::errorOccurred, this, &MainWindow::onWorkerError); + connect(m_worker, &NotmuchWorker::allTagsReady, + this, &MainWindow::onAllTagsReady); // A confirmed write clears the pending revert: without this, a later // unrelated error would roll back a change that actually succeeded. - connect(m_worker, &NotmuchWorker::tagsApplied, this, [this](const TagChange &) { + connect(m_worker, &NotmuchWorker::tagsApplied, + this, [this](const TagChange &change) { m_pendingChange = {}; m_pendingThreadIds.clear(); + + // A tag the user has just created is the one they are most likely to + // type again, so do not wait for the next sync to offer it. A set + // membership test, not a query. + for (const QString &tag : change.added) { + if (!m_knownTags.contains(tag)) { + requestAllTags(); + break; + } + } }); m_workerThread.start(); + + // Queued behind the thread start, so the completer has real tags as soon + // as the database can be read. Nothing waits on the answer: requestAllTags + // stays silent when the database cannot be opened. + requestAllTags(); +} + +void MainWindow::requestAllTags() +{ + // The generation is unused by the tag path, see onAllTagsReady(). + QMetaObject::invokeMethod(m_worker, "requestAllTags", Qt::QueuedConnection, + Q_ARG(quint64, 0)); +} + +void MainWindow::onAllTagsReady(const QStringList &tags) +{ + // The signal carries a generation, this slot deliberately does not take + // it. A tag list is not an ordered query result: a later one is always at + // least as good as an earlier one, and there is no partial state a stale + // arrival could corrupt. Discarding on generation would only be able to + // throw away a good list. + m_knownTags = tags; + m_queryCompleter->setTags(tags); } void MainWindow::showWarnings() @@ -827,6 +873,8 @@ void MainWindow::onSyncFinished(bool success, int exitCode) if (success) { m_statusLabel->setText(tr("Sync complete")); runCurrentQuery(); + // A sync is the usual way new tags enter the database. + requestAllTags(); } else { m_statusLabel->setText(tr("Sync failed (exit %1)").arg(exitCode)); m_syncLog->show(); -- cgit v1.2.3