diff --git a/lib/model/autocomplete.dart b/lib/model/autocomplete.dart index cf5c6b86e0..0062c19941 100644 --- a/lib/model/autocomplete.dart +++ b/lib/model/autocomplete.dart @@ -26,6 +26,12 @@ extension ComposeContentAutocomplete on ComposeContentController { return null; } + String stripMentionFormatting(String text) { + // Mentions are inserted as @**Name**; strip formatting so backspacing + // reopens autocomplete. + return text.replaceAll('**', ''); + } + // To avoid spending a lot of time searching for autocomplete intents // in long messages, we bound how far back we look for the intent's start. final earliest = max(0, selection.end - 30); @@ -37,11 +43,15 @@ extension ComposeContentAutocomplete on ComposeContentController { } final textUntilCursor = text.substring(0, selection.end); + final rawTextUntilCursor = textUntilCursor; int pos; for (pos = selection.end - 1; pos > selection.start; pos--) { final charAtPos = textUntilCursor[pos]; if (charAtPos == '@') { - final match = _mentionIntentRegex.matchAsPrefix(textUntilCursor, pos); + final candidate = rawTextUntilCursor.substring(pos); + final normalizedCandidate = stripMentionFormatting(candidate); + final normalizedText = rawTextUntilCursor.substring(0, pos) + normalizedCandidate; + final match = _mentionIntentRegex.matchAsPrefix(normalizedText,pos); if (match == null) continue; } else if (charAtPos == ':') { final match = _emojiIntentRegex.matchAsPrefix(textUntilCursor, pos); @@ -57,7 +67,10 @@ extension ComposeContentAutocomplete on ComposeContentController { final charAtPos = textUntilCursor[pos]; final ComposeAutocompleteQuery query; if (charAtPos == '@') { - final match = _mentionIntentRegex.matchAsPrefix(textUntilCursor, pos); + final candidate = rawTextUntilCursor.substring(pos); + final normalizedCandidate = stripMentionFormatting(candidate); + final normalizedText = rawTextUntilCursor.substring(0, pos) + normalizedCandidate; + final match = _mentionIntentRegex.matchAsPrefix(normalizedText,pos); if (match == null) continue; query = MentionAutocompleteQuery(match[2]!, silent: match[1]! == '_'); } else if (charAtPos == ':') { diff --git a/test/model/autocomplete_test.dart b/test/model/autocomplete_test.dart index a560512553..b5e5c869c3 100644 --- a/test/model/autocomplete_test.dart +++ b/test/model/autocomplete_test.dart @@ -129,10 +129,10 @@ void main() { doTest('email support@ with details of the issue^', null); doTest('email support@^ with details of the issue', null); - doTest('Ask @**Chris Bobbe**^', null); doTest('Ask @_**Chris Bobbe**^', null); - doTest('Ask @**Chris Bobbe^**', null); doTest('Ask @_**Chris Bobbe^**', null); - doTest('Ask @**Chris^ Bobbe**', null); doTest('Ask @_**Chris^ Bobbe**', null); - doTest('Ask @**^Chris Bobbe**', null); doTest('Ask @_**^Chris Bobbe**', null); + doTest('Ask ~@**Chris Bobbe**^', mention('Chris Bobbe'));doTest('Ask ~@_**Chris Bobbe**^', silentMention('Chris Bobbe')); + doTest('Ask ~@**Chris Bobbe^**', mention('Chris Bobbe'));doTest('Ask ~@_**Chris Bobbe^**', silentMention('Chris Bobbe')); + doTest('Ask ~@**Chris^ Bobbe**', mention('Chris')); doTest('Ask ~@_**Chris^ Bobbe**', silentMention('Chris')); + doTest('Ask ~@**^Chris Bobbe**', mention('')); doTest('Ask ~@_**^Chris Bobbe**', silentMention('')); doTest('`@chris^', null); doTest('`@_chris^', null); diff --git a/test/widgets/autocomplete_test.dart b/test/widgets/autocomplete_test.dart index 982c6ee3f0..877b6a4b1b 100644 --- a/test/widgets/autocomplete_test.dart +++ b/test/widgets/autocomplete_test.dart @@ -161,6 +161,9 @@ void main() { check(find.text(user.fullName)).findsExactly(expected ? 1 : 0); check(findAvatarImage(user.userId)).findsExactly(expected ? 1 : 0); } + void checkAutocompleteHidden() { + check(find.byType(Autocomplete)).findsNothing(); + } testWidgets('user options appear, disappear, and change correctly', (tester) async { final user1 = eg.user(userId: 1, fullName: 'User One', avatarUrl: 'user1.png'); @@ -185,9 +188,8 @@ void main() { await tester.pump(); check(tester.widget(composeInputFinder).controller!.text) .contains(userMention(user3, users: store)); - checkUserShown(user1, expected: false); - checkUserShown(user2, expected: false); - checkUserShown(user3, expected: false); + checkAutocompleteHidden(); + // Then a new autocomplete intent brings up options again // TODO(#226): Remove this extra edit when this bug is fixed. @@ -200,9 +202,8 @@ void main() { // TODO(#226): Remove one of these edits when this bug is fixed. await tester.enterText(composeInputFinder, ''); await tester.enterText(composeInputFinder, ' '); - checkUserShown(user1, expected: false); - checkUserShown(user2, expected: false); - checkUserShown(user3, expected: false); + checkAutocompleteHidden(); + debugNetworkImageHttpClientProvider = null; }); @@ -279,15 +280,10 @@ void main() { // Finishing autocomplete updates compose box; causes options to disappear await tester.tap(find.text(WildcardMentionOption.channel.canonicalString)); - await tester.pump(); + await tester.pumpAndSettle(); check(tester.widget(composeInputFinder).controller!.text) .contains(wildcardMention(WildcardMentionOption.channel, store: store)); - checkWildcardShown(WildcardMentionOption.channel, expected: false); - checkWildcardShown(WildcardMentionOption.topic, expected: false); - checkWildcardShown(WildcardMentionOption.all, expected: false); - checkWildcardShown(WildcardMentionOption.everyone, expected: false); - checkWildcardShown(WildcardMentionOption.stream, expected: false); - + checkAutocompleteHidden(); debugNetworkImageHttpClientProvider = null; });