diff --git a/lib/model/autocomplete.dart b/lib/model/autocomplete.dart index cf9a9505a5..a227728bae 100644 --- a/lib/model/autocomplete.dart +++ b/lib/model/autocomplete.dart @@ -192,16 +192,12 @@ abstract class AutocompleteView getSortedItemsToTest(QueryT query); - - ResultT? testItem(QueryT query, CandidateT item); - QueryT? get query => _query; QueryT? _query; set query(QueryT? query) { _query = query; if (query != null) { - _startSearch(query); + _startSearch(); } } @@ -210,15 +206,15 @@ abstract class AutocompleteView get results => _results; List _results = []; - Future _startSearch(QueryT query) async { - final newResults = await _computeResults(query); + Future _startSearch() async { + final newResults = await computeResults(); if (newResults == null) { // Query was old; new search is in progress. Or, no listeners to notify. return; @@ -228,31 +224,63 @@ abstract class AutocompleteView?> _computeResults(QueryT query) async { - final List results = []; - final Iterable data = getSortedItemsToTest(query); + /// Compute the autocomplete results for the current query, + /// returning null if the search aborts early. + /// + /// Implementations should call [shouldStop] at regular intervals, + /// and abort if it completes with true. + /// Consider using [filterCandidates]. + @protected + Future?> computeResults(); + + /// Completes in a later microtask, returning true if evaluation + /// of the current query should stop and false if it should continue. + /// + /// The deferral to a later microtask allows other code in the app to run. + /// A long CPU-intensive loop should call this regularly + /// (e.g. every 1000 iterations) so that the UI remains responsive. + @protected + Future shouldStop() async { + final query = _query; + await Future(() {}); - final iterator = data.iterator; - bool isDone = false; - while (!isDone) { - // CPU perf: End this task; enqueue a new one for resuming this work - await Future(() {}); + // If the query has changed, stop work on the old query. + if (query != _query) return true; - if (query != _query || !hasListeners) { // false if [dispose] has been called. - return null; - } + // If there are no listeners to get the result, stop work. + // This happens in particular if [dispose] was called. + if (!hasListeners) return true; + + return false; + } + + /// Examine the given candidates against `query`, adding matches to `results`. + /// + /// This function chunks its work for interruption using [shouldStop], + /// and returns true if the search was aborted. + @protected + Future filterCandidates({ + required ResultT? Function(QueryT query, T candidate) filter, + required Iterable candidates, + required List results, + }) async { + assert(_query != null); + final query = _query!; + + final iterator = candidates.iterator; + outer: while (true) { + assert(_query == query); + if (await shouldStop()) return true; + assert(_query == query); for (int i = 0; i < 1000; i++) { - if (!iterator.moveNext()) { - isDone = true; - break; - } - final CandidateT item = iterator.current; - final result = testItem(query, item); + if (!iterator.moveNext()) break outer; + final item = iterator.current; + final result = filter(query, item); if (result != null) results.add(result); } } - return results; + return false; } } @@ -279,6 +307,23 @@ class MentionAutocompleteView extends AutocompleteView sortedUsers; + @override + Future?> computeResults() async { + final results = []; + if (await filterCandidates(filter: _testUser, + candidates: sortedUsers, results: results)) { + return null; + } + return results; + } + + MentionAutocompleteResult? _testUser(MentionAutocompleteQuery query, User user) { + if (query.testUser(user, store.autocompleteViewManager.autocompleteDataCache)) { + return UserMentionAutocompleteResult(userId: user.userId); + } + return null; + } + static List _usersByRelevance({ required PerAccountStore store, required Narrow narrow, @@ -385,19 +430,6 @@ class MentionAutocompleteView extends AutocompleteView getSortedItemsToTest(MentionAutocompleteQuery query) { - return sortedUsers; - } - - @override - MentionAutocompleteResult? testItem(MentionAutocompleteQuery query, User item) { - if (query.testUser(item, store.autocompleteViewManager.autocompleteDataCache)) { - return UserMentionAutocompleteResult(userId: item.userId); - } - return null; - } - /// Determines which of the two users is more recent in DM conversations. /// /// Returns a negative number if [userA] is more recent than [userB], @@ -582,16 +614,22 @@ class TopicAutocompleteView extends AutocompleteView e.name); _isFetching = false; - if (_query != null) _startSearch(_query!); + if (_query != null) _startSearch(); } @override - Iterable getSortedItemsToTest(TopicAutocompleteQuery query) => _topics; + Future?> computeResults() async { + final results = []; + if (await filterCandidates(filter: _testTopic, + candidates: _topics, results: results)) { + return null; + } + return results; + } - @override - TopicAutocompleteResult? testItem(TopicAutocompleteQuery query, String item) { - if (query.testTopic(item)) { - return TopicAutocompleteResult(topic: item); + TopicAutocompleteResult? _testTopic(TopicAutocompleteQuery query, String topic) { + if (query.testTopic(topic)) { + return TopicAutocompleteResult(topic: topic); } return null; }