diff --git a/tests/ui_tools/test_boxes.py b/tests/ui_tools/test_boxes.py index 5da2c6dab3..3d4a178300 100644 --- a/tests/ui_tools/test_boxes.py +++ b/tests/ui_tools/test_boxes.py @@ -123,6 +123,24 @@ def test_generic_autocomplete_set_footer(self, mocker, write_box, ('@_', 3, None), # Reached last match ('@_', 4, None), # Beyond end ('@_', -1, '@_**Human 2**'), + # Complex autocomplete prefixes. + ('(@H', 0, '(@**Human Myself**'), + ('(@H', 1, '(@**Human 1**'), + ('-@G', 0, '-@*Group 1*'), + ('-@G', 1, '-@*Group 2*'), + ('_@H', 0, '_@**Human Myself**'), + ('_@G', 0, '_@*Group 1*'), + ('@@H', 0, '@@**Human Myself**'), + (':@H', 0, ':@**Human Myself**'), + ('#@H', 0, '#@**Human Myself**'), + ('@_@H', 0, '@_@**Human Myself**'), + ('>@_H', 0, '>@_**Human Myself**'), + ('>@_H', 1, '>@_**Human 1**'), + ('@_@_H', 0, '@_@_**Human Myself**'), + ('@@_H', 0, '@@_**Human Myself**'), + (':@_H', 0, ':@_**Human Myself**'), + ('#@_H', 0, '#@_**Human Myself**'), + ('@@_H', 0, '@@_**Human Myself**'), ]) def test_generic_autocomplete_mentions(self, write_box, text, required_typeahead, state): @@ -155,6 +173,13 @@ def test_generic_autocomplete_mentions(self, write_box, text, ('#Stream 1', 0, '#**Stream 1**', []), # Complete match. ('#nomatch', 0, None, []), ('#ene', 0, None, []), + # Complex autocomplete prefixes. + ('[#Stream', 0, '[#**Stream 1**', []), + ('(#Stream', 1, '(#**Stream 2**', []), + ('@#Stream', 0, '@#**Stream 1**', []), + ('@_#Stream', 0, '@_#**Stream 1**', []), + (':#Stream', 0, ':#**Stream 1**', []), + ('##Stream', 0, '##**Stream 1**', []), # With 'Secret stream' pinned. ('#Stream', 0, '#**Secret stream**', [['Secret stream'], ]), # 2nd-word startswith match (pinned). @@ -197,6 +222,12 @@ def test_generic_autocomplete_streams(self, write_box, text, (':', -1, ':smirk:'), (':nomatch', 0, None), (':nomatch', -1, None), + # Complex autocomplete prefixes. + ('(:smi', 0, '(:smile:'), + ('&:smi', 1, '&:smiley:'), + ('@:smi', 0, '@:smile:'), + ('@_:smi', 0, '@_:smile:'), + ('#:smi', 0, '#:smile:'), ]) def test_generic_autocomplete_emojis(self, write_box, text, mocker, state, required_typeahead): diff --git a/zulipterminal/ui_tools/boxes.py b/zulipterminal/ui_tools/boxes.py index 32bc290782..3e1016c01d 100644 --- a/zulipterminal/ui_tools/boxes.py +++ b/zulipterminal/ui_tools/boxes.py @@ -102,25 +102,45 @@ def generic_autocomplete(self, text: str, state: Optional[int] (':', self.autocomplete_emojis), ]) - for prefix, autocomplete_func in autocomplete_map.items(): - if text.startswith(prefix): - self.is_in_typeahead_mode = True - typeaheads, suggestions = autocomplete_func(text, prefix) - fewer_typeaheads = typeaheads[:num_suggestions] - reduced_suggestions = suggestions[:num_suggestions] - is_truncated = len(fewer_typeaheads) != len(typeaheads) - - if (state is not None and state < len(fewer_typeaheads) - and state >= -len(fewer_typeaheads)): - typeahead = fewer_typeaheads[state] # type: Optional[str] - else: - typeahead = None - state = None - self.view.set_typeahead_footer(reduced_suggestions, - state, is_truncated) - return typeahead - - return text + # Look in a reverse order to find the last autocomplete prefix used in + # the text. For instance, if text='@#example', use '#' as the prefix. + reversed_text = text[::-1] + for reverse_index, char in enumerate(reversed_text): + # Patch for silent mentions. + if (char == '_' and reverse_index + 1 < len(reversed_text) + and reversed_text[reverse_index + 1] == '@'): + char = '@_' + + if char in autocomplete_map: + prefix = char + autocomplete_func = autocomplete_map[prefix] + prefix_index = max(text.rfind(prefix), 0) + break + else: + # Return text if it doesn't have any of the autocomplete prefixes. + return text + + # NOTE: The following block only executes if any of the autocomplete + # prefixes exist. + self.is_in_typeahead_mode = True + typeaheads, suggestions = ( + autocomplete_func(text[prefix_index:], prefix) + ) + fewer_typeaheads = typeaheads[:num_suggestions] + reduced_suggestions = suggestions[:num_suggestions] + is_truncated = len(fewer_typeaheads) != len(typeaheads) + + if (state is not None and state < len(fewer_typeaheads) + and state >= -len(fewer_typeaheads)): + typeahead = fewer_typeaheads[state] # type: Optional[str] + if typeahead: + typeahead = text[:prefix_index] + typeahead + else: + typeahead = None + state = None + self.view.set_typeahead_footer(reduced_suggestions, + state, is_truncated) + return typeahead def autocomplete_mentions(self, text: str, prefix_string: str ) -> Tuple[List[str], List[str]]: