|
Reported by the user: in the Edit tags fields, the first tag suggested
existing tags and the second did not. Typing a comma, a space and a
letter offered nothing.
QLineEdit::setCompleter hands completion to the line edit, which
overwrites the completer's prefix with the widget's ENTIRE text on every
keystroke. These fields hold a comma-separated list, so once one reads
"unread, fl" that whole string is matched against the tag names, nothing
matches, and completion silently stops after the first tag. Confirmed
with a probe: the prefix really is "unread, fl" and the completion count
really is zero.
Attach with setWidget instead, which keeps the popup anchored without
ceding control of the prefix, and drive it from the token under the
cursor on every edit. Setting the prefix from a textEdited handler while
leaving setCompleter in place does NOT work, which was the first attempt:
the line edit sets it again afterwards.
Accepting a candidate needed the same treatment, and is the other half of
the fix. QCompleter's own insertion replaces the whole field, so taking
"flagged" from the popup would have discarded every tag already typed.
replaceCurrentToken() overwrites only the token under the cursor and
keeps the separator's spacing, so the result is "unread, flagged" rather
than "unread,flagged".
This is the same defect QueryCompleter hit in c98b179. Having now cost
two debugging rounds, it is written into CLAUDE.md as a Qt trap rather
than a property of either class, together with the reason a test using
setText() passes against it: setText does not drive a completer at all,
so the keys have to be typed.
Both new tests were confirmed to fail against setCompleter before the fix
was kept.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Five hardcoded tags were the only ones reachable from the UI: archive,
delete, spam, flag and toggle_unread. For an application whose purpose is
organising mail by tag, applying any other one meant leaving for a
terminal. Item 26 of the usability backlog, raised by the user asking how
to add a tag and finding they could not.
One dialog rather than separate add and remove actions, at the user's
choice: filing something under a new tag while dropping inbox is one
thought, not two. Type tags to add or remove, comma separated, or clear a
checkbox to drop a tag already on the selection without retyping its
name.
Both fields complete against the tag list MainWindow already holds for
the query completer. Completion is a guard against typing shoppping
beside shopping, never a whitelist: inventing a tag is the entire point,
so any valid name goes through whether or not it exists yet.
Tri-state checkboxes carry the multi-thread case, and are where the risk
is. A tag on some selected threads shows partially checked, and leaving
it alone changes nothing; the opposite reading would silently tag threads
the user never looked at. A tag already on every thread and left checked
is likewise not a change and is not sent as one.
Tag names are validated before anything is applied, through a free
function so the rules are testable on their own. Empty, a leading dash
(notmuch's CLI reads it as removal, making such a tag a trap), whitespace
and control characters are refused by name and reason. Nothing is applied
until the whole set passes, since the user cannot tell which half of a
partial change landed.
TagDialog is pure UI: handed the vocabulary and the current state,
returning two lists, contacting no worker. That is what lets its fifteen
tests run without a notmuch database. Integration is a single call to the
existing tagSelected(), so undo, the optimistic model update, the
combined multi-row query and the completer refresh for a brand-new tag
all come for free.
One test assumption was wrong and the code was right: a case asserted
that QStringLiteral("null\0byte") truncates at the null and reads as
empty. It does not, so the null is caught as a control character. The
test was corrected rather than the validator.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|