Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

autocomplete [nfc]: Give subclasses control of computeResults #896

Merged
merged 7 commits into from
Aug 20, 2024
128 changes: 83 additions & 45 deletions lib/model/autocomplete.dart
Original file line number Diff line number Diff line change
Expand Up @@ -192,16 +192,12 @@ abstract class AutocompleteView<QueryT extends AutocompleteQuery, ResultT extend

final PerAccountStore store;

Iterable<CandidateT> 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();
}
}

Expand All @@ -210,15 +206,15 @@ abstract class AutocompleteView<QueryT extends AutocompleteQuery, ResultT extend
/// This will redo the search from scratch for the current query, if any.
void reassemble() {
if (_query != null) {
_startSearch(_query!);
_startSearch();
}
}

Iterable<ResultT> get results => _results;
List<ResultT> _results = [];

Future<void> _startSearch(QueryT query) async {
final newResults = await _computeResults(query);
Future<void> _startSearch() async {
final newResults = await computeResults();
if (newResults == null) {
// Query was old; new search is in progress. Or, no listeners to notify.
return;
Expand All @@ -228,31 +224,63 @@ abstract class AutocompleteView<QueryT extends AutocompleteQuery, ResultT extend
notifyListeners();
}

Future<List<ResultT>?> _computeResults(QueryT query) async {
final List<ResultT> results = [];
final Iterable<CandidateT> 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<List<ResultT>?> 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<bool> 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<bool> filterCandidates<T>({
required ResultT? Function(QueryT query, T candidate) filter,
required Iterable<T> candidates,
required List<ResultT> 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;
}
}

Expand All @@ -279,6 +307,23 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
final Narrow narrow;
final List<User> sortedUsers;

@override
Future<List<MentionAutocompleteResult>?> computeResults() async {
final results = <MentionAutocompleteResult>[];
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<User> _usersByRelevance({
required PerAccountStore store,
required Narrow narrow,
Expand Down Expand Up @@ -385,19 +430,6 @@ class MentionAutocompleteView extends AutocompleteView<MentionAutocompleteQuery,
streamId: streamId, senderId: userB.userId));
}

@override
Iterable<User> 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],
Expand Down Expand Up @@ -582,16 +614,22 @@ class TopicAutocompleteView extends AutocompleteView<TopicAutocompleteQuery, Top
final result = await getStreamTopics(store.connection, streamId: streamId);
_topics = result.topics.map((e) => e.name);
_isFetching = false;
if (_query != null) _startSearch(_query!);
if (_query != null) _startSearch();
}

@override
Iterable<String> getSortedItemsToTest(TopicAutocompleteQuery query) => _topics;
Future<List<TopicAutocompleteResult>?> computeResults() async {
final results = <TopicAutocompleteResult>[];
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;
}
Expand Down