@@ -76,12 +76,15 @@ void main() {
7676 ///
7777 /// For example, "~@chris^" means the text is "@chris", the selection is
7878 /// collapsed at index 6, and we expect the syntax to start at index 0.
79- void doTest (String markedText, ComposeAutocompleteQuery ? expectedQuery) {
79+ void doTest (String markedText, ComposeAutocompleteQuery ? expectedQuery, {
80+ int ? maxChannelName,
81+ }) {
8082 final description = expectedQuery != null
8183 ? 'in ${jsonEncode (markedText )}, query ${jsonEncode (expectedQuery .raw )}'
8284 : 'no query in ${jsonEncode (markedText )}' ;
8385 test (description, () {
84- final store = eg.store ();
86+ final store = eg.store (initialSnapshot:
87+ eg.initialSnapshot (maxChannelNameLength: maxChannelName));
8588 final controller = ComposeContentController (store: store);
8689 final parsed = parseMarkedText (markedText);
8790 assert ((expectedQuery == null ) == (parsed.expectedSyntaxStart == null ));
@@ -98,6 +101,7 @@ void main() {
98101
99102 MentionAutocompleteQuery mention (String raw) => MentionAutocompleteQuery (raw, silent: false );
100103 MentionAutocompleteQuery silentMention (String raw) => MentionAutocompleteQuery (raw, silent: true );
104+ ChannelLinkAutocompleteQuery channelLink (String raw) => ChannelLinkAutocompleteQuery (raw);
101105 EmojiAutocompleteQuery emoji (String raw) => EmojiAutocompleteQuery (raw);
102106
103107 doTest ('' , null );
@@ -179,8 +183,13 @@ void main() {
179183 doTest ('~@_Rodion Romanovich Raskolniko^' , silentMention ('Rodion Romanovich Raskolniko' ));
180184 doTest ('~@Родион Романович Раскольников^' , mention ('Родион Романович Раскольников' ));
181185 doTest ('~@_Родион Романович Раскольнико^' , silentMention ('Родион Романович Раскольнико' ));
182- doTest ('If @chris is around, please ask him.^' , null ); // @ sign is too far away from cursor
183- doTest ('If @_chris is around, please ask him.^' , null ); // @ sign is too far away from cursor
186+
187+ // "@" sign can be (3 + 2 * maxChannelName) utf-16 code units
188+ // away to the left of the cursor.
189+ doTest ('If ~@chris^ is around, please ask him.' , mention ('chris' ), maxChannelName: 10 );
190+ doTest ('If ~@_chris is^ around, please ask him.' , silentMention ('chris is' ), maxChannelName: 10 );
191+ doTest ('If @chris is around, please ask him.^' , null , maxChannelName: 10 );
192+ doTest ('If @_chris is around, please ask him.^' , null , maxChannelName: 10 );
184193
185194 // Emoji (":smile:").
186195
@@ -259,6 +268,90 @@ void main() {
259268 doTest (',~:^' , emoji ('' )); doTest (',~:a^' , emoji ('a' ));
260269 doTest (',~:^' , emoji ('' )); doTest (',~:a^' , emoji ('a' ));
261270 doTest ('。~:^' , emoji ('' )); doTest ('。~:a^' , emoji ('a' ));
271+
272+ // #channel links.
273+
274+ doTest ('^#' , null );
275+ doTest ('^#abc' , null );
276+ doTest ('#abc' , null ); // (no cursor)
277+
278+ // Link syntax can be at the start of a string.
279+ doTest ('~#^' , channelLink ('' ));
280+ doTest ('~##^' , channelLink ('#' ));
281+ doTest ('~#abc^' , channelLink ('abc' ));
282+
283+ // Link syntax can contain multiple words.
284+ doTest ('~#abc ^' , channelLink ('abc ' ));
285+ doTest ('~#abc def^' , channelLink ('abc def' ));
286+
287+ // Link syntax can come after a word or space.
288+ doTest ('xyz ~#abc^' , channelLink ('abc' ));
289+ doTest (' ~#abc^' , channelLink ('abc' ));
290+
291+ // Link syntax can come after punctuation…
292+ doTest (':~#abc^' , channelLink ('abc' ));
293+ doTest ('!~#abc^' , channelLink ('abc' ));
294+ doTest (',~#abc^' , channelLink ('abc' ));
295+ doTest ('.~#abc^' , channelLink ('abc' ));
296+ doTest ('(~#abc^' , channelLink ('abc' )); doTest (')~#abc^' , channelLink ('abc' ));
297+ doTest ('{~#abc^' , channelLink ('abc' )); doTest ('}~#abc^' , channelLink ('abc' ));
298+ doTest ('[~#abc^' , channelLink ('abc' )); doTest (']~#abc^' , channelLink ('abc' ));
299+ doTest ('“~#abc^' , channelLink ('abc' )); doTest ('”~#abc^' , channelLink ('abc' ));
300+ doTest ('«~#abc^' , channelLink ('abc' )); doTest ('»~#abc^' , channelLink ('abc' ));
301+ // … except for '#' and '@', because they start
302+ // channel link and mention syntaxes, respectively.
303+ doTest ('~##abc^' , channelLink ('#abc' ));
304+ doTest ('~@#abc^' , mention ('#abc' ));
305+
306+ // Avoid interpreting as queries a URL or a common linkifier syntax.
307+ doTest ('https://example.com/docs#install^' , null );
308+ doTest ('zulip/zulip-flutter#124^' , null );
309+
310+ // Query can't start with a space; channel names don't.
311+ doTest ('# ^' , null );
312+ doTest ('# abc^' , null );
313+
314+ // Query shouldn't be multiple lines.
315+ doTest ('#\n ^' , null ); doTest ('#a\n ^' , null ); doTest ('#\n a^' , null ); doTest ('#a\n b^' , null );
316+ doTest ('#\r ^' , null ); doTest ('#a\r ^' , null ); doTest ('#\r a^' , null ); doTest ('#a\r b^' , null );
317+ doTest ('#\r\n ^' , null ); doTest ('#a\r\n ^' , null ); doTest ('#\r\n a^' , null ); doTest ('#a\r\n b^' , null );
318+
319+ // Query can contain a wide range of characters.
320+ doTest ('~#`^' , channelLink ('`' )); doTest ('~#a`b^' , channelLink ('a`b' ));
321+ doTest ('~#"^' , channelLink ('"' )); doTest ('~#a"b^' , channelLink ('a"b' ));
322+ doTest ('~#>^' , channelLink ('>' )); doTest ('~#a>b^' , channelLink ('a>b' ));
323+ doTest ('~#&^' , channelLink ('&' )); doTest ('~#a&b^' , channelLink ('a&b' ));
324+ doTest ('~#_^' , channelLink ('_' )); doTest ('~#a_b^' , channelLink ('a_b' ));
325+ doTest ('~#*^' , channelLink ('*' )); doTest ('~#a*b^' , channelLink ('a*b' ));
326+
327+ // Avoid interpreting already-entered `#**foo**` syntax as queries.
328+ doTest ('#**abc**^' , null );
329+ doTest ('#**abc** ^' , null );
330+ doTest ('#**abc** def^' , null );
331+
332+ // Accept syntax like "#**foo" (as from the user finishing an autocomplete
333+ // and then hitting backspace to edit it), but leave the "**" out of the query.
334+ doTest ('~#**^' , channelLink ('' ));
335+ doTest ('~#**abc^' , channelLink ('abc' ));
336+ doTest ('~#**abc ^' , channelLink ('abc ' ));
337+ doTest ('~#**abc def^' , channelLink ('abc def' ));
338+ doTest ('~#**ab*c^' , channelLink ('ab*c' ));
339+ doTest ('~#**abc*^' , channelLink ('abc*' ));
340+ doTest ('#** ^' , null );
341+ doTest ('#** abc^' , null );
342+ doTest ('#**a\n ^' , null ); doTest ('#**\n a^' , null ); doTest ('#**a\n b^' , null );
343+ doTest ('#**a\r ^' , null ); doTest ('#**\r a^' , null ); doTest ('#**a\r b^' , null );
344+ doTest ('#**a\r\n ^' , null ); doTest ('#**\r\n a^' , null ); doTest ('#**a\r\n b^' , null );
345+
346+ // "#" sign can be (3 + 2 * maxChannelName) utf-16 code units
347+ // away to the left of the cursor.
348+ doTest ('check ~#**mobile dev^ team' , channelLink ('mobile dev' ), maxChannelName: 5 );
349+ doTest ('check ~#mobile dev t^eam' , channelLink ('mobile dev t' ), maxChannelName: 5 );
350+ doTest ('check #mobile dev te^am' , null , maxChannelName: 5 );
351+ doTest ('check #mobile dev team for more info^' , null , maxChannelName: 5 );
352+ // '🙂' is 2 utf-16 code units.
353+ doTest ('check ~#**🙂🙂🙂🙂🙂^' , channelLink ('🙂🙂🙂🙂🙂' ), maxChannelName: 5 );
354+ doTest ('check #**🙂🙂🙂🙂🙂🙂^' , null , maxChannelName: 5 );
262355 });
263356
264357 test ('MentionAutocompleteView misc' , () async {
0 commit comments