# Query bar completion Design for backlog item 17 of `docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md`. ## Problem The query bar is a bare `QLineEdit` (`mainwindow.cpp:220`) with the placeholder `notmuch query, e.g. tag:inbox`. Writing a query means knowing notmuch's vocabulary from memory, and recalling tag hierarchies exactly: `shopping/amazon` is not guessable. Two distinct users are underserved: - Someone who has never used notmuch or neomutt cannot discover that prefixes exist at all. An empty box teaches nothing. - Someone fluent in notmuch still cannot recall every tag they have created. The goal is a bar that documents the query language while being typed, without slowing down a user who already knows it. ## Scope In scope: completion of query prefixes, tag values, date values, path values and mimetype values, each with a human description shown alongside. Out of scope, deliberately: - **Address completion** for `from:` and `to:`. libnotmuch exposes no all-addresses call; it would require shelling out to `notmuch address` or scanning messages. Both prefixes still appear in the prefix list so the vocabulary reads complete, but they complete no values. Deferred to its own backlog item. - **Saved query names.** A saved query name is not valid notmuch syntax, so completing one into the bar would produce a query that errors. Making it useful would mean expanding the name to its query text, which is substitution rather than completion, and a different feature. The saved query buttons already sit directly below the bar. - **Absolute date completion.** `2026-01-15` is not enumerable. See the free-form hint below. ## Architecture A new class owns all of it. `mainwindow.cpp` is already 35K and gains only construction plus two connections. ``` MainWindow └ QueryCompleter src/querycompleter.{h,cpp} ├ completionContext() free function, pure, unit-tested ├ static vocabulary prefixes, dates, mimetypes (+ descriptions) ├ config-derived path values from Account::maildir └ dynamic tag list, supplied by MainWindow from the worker NotmuchWorker ├ slot requestAllTags(quint64 generation) └ signal allTagsReady(const QStringList &tags, quint64 generation) ``` `QueryCompleter` is constructed with the `QLineEdit` it manages and a `const Config &`. It installs a `QCompleter` on the line edit, swaps that completer's model as the cursor moves between contexts, and exposes `setTags(const QStringList &)` for the worker result. It emits no signals. The separation matters for testing: `completionContext()` is where the defects will be, and it needs neither a widget nor a database to exercise. ## The tokenizer `completionContext(const QString &text, int cursor)` returns: ```cpp struct CompletionContext { enum Kind { None, Prefix, Value }; Kind kind = None; QString prefix; ///< For Value: the keyword left of ':', lowercased. QString stem; ///< The text being matched. int replaceFrom = 0; int replaceLength = 0; }; ``` `replaceFrom`/`replaceLength` describe exactly the span an accepted completion overwrites, so accepting never disturbs neighbouring text. Rules: 1. Scan back from the cursor to the nearest unquoted whitespace, `(` or the start of the string. That span is the current token. 2. Within the token, the first `:` splits it. Text before is the prefix, text after is the value being typed. 3. A token with no `:` is a prefix completion. 4. Inside double quotes, return `None`. In `subject:"foo bar|` the cursor sits in a literal, not a keyword position. 5. A value containing `..` is a **range of two independent values**. The cursor before the separator completes the lower bound, after it the upper bound; both draw on the same model, and the replace span covers only the side being edited. `date:today..yes|` completes `yesterday` as the upper bound without touching `today`. Matching is case-insensitive; insertion is always lowercase, since notmuch's prefixes are lowercase. `is:` and `tag:` resolve to the same model. notmuch treats `is:x` as a synonym for `tag:x`, so offering tags for both is correct rather than a convenience. ### Range entries and the `..` trap The relative date entries (`1week..`, `1month..`) contain `..` themselves: each is already a complete open-ended range meaning "from then until now". Offering one as a bound inside an existing range yields `date:1week....today`, which is malformed. Therefore: **entries containing `..` are offered only when the value has no `..` yet.** Once a range is underway, only the bare symbolic values are candidates on either side. This is a required test case, not an optimisation. ## Vocabulary A `QStandardItemModel` per context: completion text in column 0, description in column 1. `QCompleter` is set to match on column 0 only, so descriptions never influence matching. A `QStyledItemDelegate` draws the description greyed and right-aligned. Every description passes through `tr()`. Keywords do not: they are syntax. **Prefixes** — `tag:`, `is:`, `from:`, `to:`, `subject:`, `date:`, `attachment:`, `mimetype:`, `folder:`, `path:`, `thread:`, `id:`, and the operators `and`, `or`, `not`. **Dates** — symbolic: `today`, `yesterday`, `this_week`, `last_week`, `this_month`, `last_month`, `this_year`. Relative (contain `..`, subject to the rule above): `1week..`, `1month..`. **Mimetypes** — `application/pdf`, `image/jpeg`, `image/png`, `text/html`, `application/zip`. **Paths** — `Account::maildir` for each configured account, offered after `path:`. Config-derived rather than scanned: the Maildir root is deliberately not duplicated in this project's config, and the configured accounts are the directories actually queried. `Account::scopedQuery()` (`config.cpp:27`) builds `path:"/**"`, so `path:` is the prefix these values genuinely belong to. `folder:` is a different matcher in notmuch, against the Maildir folder name rather than the directory path, and its values are not enumerable from config. `folder:` therefore completes as a prefix only, like `from:` and `to:`. The recursive form is worth offering directly: each account contributes both `` and `/**`, the latter described as including subdirectories, since `/**` is the form the application itself uses and is not guessable. **Tags** — supplied at runtime, see below. ### Free-form date hint Date values cannot be enumerated, so the completion list cannot reveal that notmuch accepts free-form dates at all. When the date model is active the popup carries a footer label: > also accepts free-form dates, e.g. `2026-01-15` or `15/01/2026..today` It is a label beneath the popup's list view, **not a model row**. A row would be dropped by `QCompleter`'s filter model on the first keystroke that did not match it, and could in principle be selected and inserted, producing a broken query. A footer sidesteps both: it cannot be filtered away and cannot be accepted. The wording is illustrative, not a specification. notmuch's date parser is permissive and its exact accepted set varies by build, so the hint must not read as a promise. ## Tag fetching and freshness `NotmuchWorker` gains `requestAllTags(quint64 generation)`, wrapping `notmuch_database_get_all_tags()` (notmuch.h:942) and emitting a sorted `QStringList`. A `NmTags` RAII alias is added to `nmraii.h` alongside the existing ones. No notmuch pointer crosses the thread boundary; the generation counter follows the existing pattern. The list refreshes: - at startup, - after each sync completes, since a sync can introduce tags, - after an `applyTags` that added a tag not already in the list. The third case is a set membership test in `MainWindow`, not a query. Without it, a tag the user has just created would be missing from completion until the next sync, which is precisely the tag they are most likely to type again. Never per keystroke. ## Triggering Config gains `[general] completion_on_focus`, a bool defaulting to **false**. When true, focusing an empty query bar opens the popup showing the full prefix list. `QCompleter` does not do this unaided: it requires an explicit `complete()` call on the focus event. A manual trigger always works, independent of that toggle: a new `complete_query` action added to `KeyMap::defaultBindings()`, default `Ctrl+Space`. Going through `KeyMap` rather than hardcoding means it appears in the shortcut reference automatically and is rebindable in `[keys]`, consistent with every other binding. Ordinary typing completes as soon as a token is underway. That is `QCompleter`'s normal behaviour and is not gated by the toggle; the toggle governs only the empty-bar-on-focus case. ## Testing TDD, tokenizer first. `tests/test_querycompleter.cpp`: - Every tokenizer rule, including the quote escape and both sides of a range. - The `..` trap: relative entries offered on a bare value, withheld once a range exists. - Replace spans, asserted by applying the completion and comparing the resulting string. - Model selection through the public API against a stub `Config`: `date:` selects dates, `path:` selects the configured maildirs in both bare and `/**` forms, `is:` selects the same model as `tag:`, and `folder:` selects nothing. `requestAllTags` is tested in the existing `tests/test_notmuchworker.cpp` against its throwaway notmuch database, asserting the tags generated into that fixture come back sorted. Register with `add_qtmaildir_test(querycompleter)` in `tests/CMakeLists.txt`, and add `querycompleter.cpp` to the `qtmaildir_lib` list in `src/CMakeLists.txt`. ## Consequences - `mainwindow.cpp` grows by construction and two connections only. - `Config` gains one accessor. - `KeyMap` gains one default binding, which the shortcut reference picks up without further change. - `NotmuchWorker` gains one slot and one signal. - The prefix list is a maintenance point: notmuch may add prefixes, and this list will not track them automatically.