feat(todo): quick add keeps a word as text where it stands, and from Backspace (ADR 0033, D29)

A word tapped was kept as text by its text, so in "Watch The Daily Show
Daily" tapping the first "Daily" also stopped the second one making the
task repeat (the developer's example). Kept words are now ranges of the
title that follow its edits (`shiftKept`: ranges after the edit move by
its length, one it touches is dropped), and the parser leaves a token
overlapping one of them as text, wherever else the same word stands.

Backspace right after a highlighted word keeps it as text instead of
deleting its last character, as Todoist's; the next Backspace deletes.
It is an input formatter, so the keyboard is given the text back. The
hint under the title says so.

Whats-New: In Add task, only the word you tap stays text; the same word elsewhere still counts.
Whats-New: In Add task, Backspace right after a highlighted word keeps it as text.
Whats-New-De: In „Aufgabe hinzufügen“ bleibt nur das angetippte Wort Text; dasselbe Wort an anderer Stelle zählt weiter.
Whats-New-De: In „Aufgabe hinzufügen“ behält die Rücktaste direkt nach einem hervorgehobenen Wort es als Text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-04 22:23:03 +01:00
commit 8b13cbcf2b
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -1348,7 +1348,10 @@ void main() {
);
expect(find.widgetWithText(QuickAddButton, '1 label'), findsOneWidget);
expect(
find.text('Tap a highlighted word to keep it as text.'),
find.text(
'Tap a highlighted word, or press Backspace right after it, to '
'keep it as text.',
),
findsOneWidget,
);
await add(tester);
@ -1453,7 +1456,10 @@ void main() {
await tester.tapAt(render.localToGlobal(caret.center));
await tester.pumpAndSettle();
expect(
find.text('Tap a highlighted word to keep it as text.'),
find.text(
'Tap a highlighted word, or press Backspace right after it, to '
'keep it as text.',
),
findsNothing,
);
await add(tester);
@ -1463,6 +1469,85 @@ void main() {
expect(stored.single.content?.date, isNull);
});
testWidgets('a word tapped stays text there alone: the same word '
"elsewhere still counts (the developer's, 4 Nov 2026)", (
tester,
) async {
await start(tester, usedBefore: true);
await goTab(tester, 'Inbox');
const text = 'Watch The Daily Show Daily';
await open(tester, text);
final editable = tester.state<EditableTextState>(
find.descendant(
of: find.byKey(quickAddTitleKey),
matching: find.byType(EditableText),
),
);
final render = editable.renderEditable;
final caret = render.getLocalRectForCaret(
TextPosition(offset: text.indexOf('Daily') + 2),
);
await tester.tapAt(render.localToGlobal(caret.center));
await tester.pumpAndSettle();
await add(tester);
final stored = await tester.runAsync(() => tasks().all());
final content = stored!.single.content!;
expect(content.title, 'Watch The Daily Show');
expect(content.date?.repeat, const Repeat(RepeatUnit.day));
});
testWidgets('Backspace right after a highlighted word keeps it as text, '
"as Todoist's; the next Backspace deletes", (tester) async {
await start(tester, usedBefore: true);
await goTab(tester, 'Inbox');
await tester.tap(find.byTooltip('Add task'));
await tester.pumpAndSettle();
await tester.showKeyboard(find.byKey(quickAddTitleKey));
TextEditingValue at(String text) => TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: text.length),
);
tester.testTextInput.updateEditingValue(at('Call today'));
await tester.pumpAndSettle();
final title = tester
.widget<TextField>(find.byKey(quickAddTitleKey))
.controller!;
// As a keyboard sends it: the text without the character before
// the caret.
tester.testTextInput.updateEditingValue(at('Call toda'));
await tester.pumpAndSettle();
expect(title.value.text, 'Call today', reason: 'nothing deleted');
expect(
title.value.selection,
const TextSelection.collapsed(offset: 10),
);
expect(
find.widgetWithText(QuickAddButton, 'Today'),
findsNothing,
reason: 'kept as text',
);
tester.testTextInput.updateEditingValue(at('Call toda'));
await tester.pumpAndSettle();
expect(title.value.text, 'Call toda');
tester.testTextInput.updateEditingValue(at('Call today'));
await tester.pumpAndSettle();
expect(
find.widgetWithText(QuickAddButton, 'Today'),
findsOneWidget,
reason: 'the word typed anew is a new word, read again',
);
await tester.sendKeyEvent(LogicalKeyboardKey.backspace);
await tester.pumpAndSettle();
expect(title.value.text, 'Call today', reason: "a desktop's key too");
expect(find.widgetWithText(QuickAddButton, 'Today'), findsNothing);
});
testWidgets('a token is highlighted while the keyboard still composes '
'it, the composing underline over it (Gboard, Japanese IMEs)', (
tester,