diff --git a/lib/src/autocomplete_trigger.dart b/lib/src/autocomplete_trigger.dart index 73ebcdf..009a570 100644 --- a/lib/src/autocomplete_trigger.dart +++ b/lib/src/autocomplete_trigger.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:multi_trigger_autocomplete/src/autocomplete_query.dart'; @@ -17,20 +19,37 @@ class AutocompleteTrigger { required this.optionsViewBuilder, this.triggerOnlyAtStart = false, this.triggerOnlyAfterSpace = true, + this.allowSpacesInSuggestions = false, this.minimumRequiredCharacters = 0, - }); + this.triggerSet, + }) : assert( + !(allowSpacesInSuggestions && triggerSet == null), + 'Error: Triggers cannot be empty if allowSpacesInSuggestions is true.', + ); /// The trigger character. /// /// eg. '@', '#', ':' final String trigger; + /// All trigger characters. + /// Needed if [allowSpacesInSuggestions] is set to true. + /// + /// eg. {'@', '#', ':'} + final Set? triggerSet; + /// Whether the [trigger] should only be recognised at the start of the input. final bool triggerOnlyAtStart; /// Whether the [trigger] should only be recognised after a space. final bool triggerOnlyAfterSpace; + /// Whether the [trigger] should recognise autocomplete options + /// containing spaces. If set to true, suggestions like "@luke skywalker" + /// would be considered valid. If set to false, the first space character + /// would end the suggestion. + final bool allowSpacesInSuggestions; + /// The minimum required characters for the [trigger] to start recognising /// a autocomplete options. final int minimumRequiredCharacters; @@ -68,12 +87,25 @@ class AutocompleteTrigger { if (!selection.isValid) return null; final cursorPosition = selection.baseOffset; - // Find the first [trigger] location before the input cursor. + // Find the first [triggerSet] item location before the input cursor. + final triggersRegExp = RegExp( + (triggerSet ?? {trigger}).map((e) => RegExp.escape(e)).join('|')); final firstTriggerIndexBeforeCursor = - text.substring(0, cursorPosition).lastIndexOf(trigger); + text.substring(0, cursorPosition).lastIndexOf(triggersRegExp); // If the [trigger] is not found before the cursor, then it's not a trigger. - if (firstTriggerIndexBeforeCursor == -1) return null; + if (firstTriggerIndexBeforeCursor == -1) { + return null; + } + + // If the [trigger] is not at [firstTriggerIndexBeforeCursor], then it's not a trigger. + final triggerFromText = text.substring( + firstTriggerIndexBeforeCursor, + min(firstTriggerIndexBeforeCursor + trigger.length, text.length), + ); + if (triggerFromText != trigger) { + return null; + } // If the [trigger] is found before the cursor, but the [trigger] is only // recognised at the start of the input, then it's not a trigger. @@ -97,11 +129,16 @@ class AutocompleteTrigger { final suggestionEnd = cursorPosition; if (suggestionStart > suggestionEnd) return null; - // Fetch the suggestion text. The suggestions can't have spaces. - // valid example: "@luke_skywa..." - // invalid example: "@luke skywa..." + // Fetch the suggestion text. final suggestionText = text.substring(suggestionStart, suggestionEnd); - if (suggestionText.contains(' ')) return null; + + // If [allowSpacesInSuggestions] is false, the suggestions can't have spaces. + // If true, suggestions like "@luke skywalker" would be considered valid. + // If false, suggestions like "@luke skywalker" would be considered invalid, + // and only examples like "@luke_skywalker" would be valid. + if (!allowSpacesInSuggestions && suggestionText.contains(' ')) { + return null; + } // A minimum number of characters can be provided to only show // suggestions after the customer has input enough characters. diff --git a/test/autocomplete_trigger_test.dart b/test/autocomplete_trigger_test.dart index 974db2a..41ee453 100644 --- a/test/autocomplete_trigger_test.dart +++ b/test/autocomplete_trigger_test.dart @@ -297,4 +297,66 @@ void main() { expect(trigger1, isNot(trigger2)); }); }); + + group('Autocomplete trigger with and without `allowSpacesInSuggestions`', () { + final triggerWithSpaces = AutocompleteTrigger( + trigger: '@', + allowSpacesInSuggestions: true, + triggerSet: {'@'}, + optionsViewBuilder: ( + context, + autocompleteQuery, + textEditingController, + ) { + return const SizedBox.shrink(); + }, + ); + + final triggerWithoutSpaces = AutocompleteTrigger( + trigger: '@', + allowSpacesInSuggestions: false, + optionsViewBuilder: ( + context, + autocompleteQuery, + textEditingController, + ) { + return const SizedBox.shrink(); + }, + ); + + test( + 'should return query if `@` is invoked and the word contains spaces when `allowSpacesInSuggestions` is true', + () { + const text = 'Hey @Sahil Kumar'; + final invoked = triggerWithSpaces.invokingTrigger( + const TextEditingValue( + text: text, + selection: TextSelection.collapsed(offset: text.length), + ), + ); + + expect(invoked, isNotNull); + expect(invoked!.query, 'Sahil Kumar'); + expect( + invoked.selection, + const TextSelection(baseOffset: 5, extentOffset: 16), + ); + }, + ); + + test( + "should return null if `@` is invoked and the word contains spaces when `allowSpacesInSuggestions` is false", + () { + const text = 'Hey @Sahil Kumar'; + final invoked = triggerWithoutSpaces.invokingTrigger( + const TextEditingValue( + text: text, + selection: TextSelection.collapsed(offset: text.length), + ), + ); + + expect(invoked, isNull); + }, + ); + }); }