@@ -8,7 +8,7 @@ import 'narrow.dart';
8
8
import 'store.dart' ;
9
9
10
10
extension ComposeContentAutocomplete on ComposeContentController {
11
- AutocompleteIntent ? autocompleteIntent () {
11
+ AutocompleteIntent < MentionAutocompleteQuery > ? autocompleteIntent () {
12
12
if (! selection.isValid || ! selection.isNormalized) {
13
13
// We don't require [isCollapsed] to be true because we've seen that
14
14
// autocorrect and even backspace involve programmatically expanding the
@@ -69,7 +69,7 @@ final RegExp mentionAutocompleteMarkerRegex = (() {
69
69
})();
70
70
71
71
/// The content controller's recognition that the user might want autocomplete UI.
72
- class AutocompleteIntent {
72
+ class AutocompleteIntent < Q extends AutocompleteQuery > {
73
73
AutocompleteIntent ({
74
74
required this .syntaxStart,
75
75
required this .query,
@@ -91,7 +91,7 @@ class AutocompleteIntent {
91
91
// that use a custom/subclassed [TextEditingValue], so that's not convenient.
92
92
final int syntaxStart;
93
93
94
- final MentionAutocompleteQuery query; // TODO other autocomplete query types
94
+ final Q query;
95
95
96
96
/// The [TextEditingValue] whose text [syntaxStart] refers to.
97
97
final TextEditingValue textEditingValue;
@@ -151,36 +151,97 @@ class AutocompleteViewManager {
151
151
// void dispose() { … }
152
152
}
153
153
154
- /// A view-model for a mention- autocomplete interaction.
154
+ /// A view-model for an autocomplete interaction.
155
155
///
156
156
/// The owner of one of these objects must call [dispose] when the object
157
157
/// will no longer be used, in order to free resources on the [PerAccountStore] .
158
158
///
159
159
/// Lifecycle:
160
- /// * Create with [init] .
160
+ /// * Create an instance of a concrete subtype .
161
161
/// * Add listeners with [addListener] .
162
162
/// * Use the [query] setter to start a search for a query.
163
163
/// * On reassemble, call [reassemble] .
164
164
/// * When the object will no longer be used, call [dispose] to free
165
165
/// resources on the [PerAccountStore].
166
- class MentionAutocompleteView extends ChangeNotifier {
167
- MentionAutocompleteView ._({
168
- required this .store,
169
- required this .narrow,
170
- required this .sortedUsers,
171
- });
166
+ abstract class AutocompleteView <Q extends AutocompleteQuery , R extends AutocompleteResult , T > extends ChangeNotifier {
167
+ final PerAccountStore store;
172
168
173
- factory MentionAutocompleteView .init ({
174
- required PerAccountStore store,
175
- required Narrow narrow,
176
- }) {
177
- final view = MentionAutocompleteView ._(
178
- store: store,
179
- narrow: narrow,
180
- sortedUsers: _usersByRelevance (store: store, narrow: narrow),
181
- );
182
- store.autocompleteViewManager.registerMentionAutocomplete (view);
183
- return view;
169
+ AutocompleteView ({required this .store});
170
+
171
+ Iterable <T > getSortedItemsToTest (Q query);
172
+
173
+ R ? testItem (Q query, T item);
174
+
175
+ Q ? get query => _query;
176
+ Q ? _query;
177
+ set query (Q ? query) {
178
+ _query = query;
179
+ if (query != null ) {
180
+ _startSearch (query);
181
+ }
182
+ }
183
+
184
+ /// Called when the app is reassembled during debugging, e.g. for hot reload.
185
+ ///
186
+ /// This will redo the search from scratch for the current query, if any.
187
+ void reassemble () {
188
+ if (_query != null ) {
189
+ _startSearch (_query! );
190
+ }
191
+ }
192
+
193
+ Iterable <R > get results => _results;
194
+ List <R > _results = [];
195
+
196
+ Future <void > _startSearch (Q query) async {
197
+ final newResults = await _computeResults (query);
198
+ if (newResults == null ) {
199
+ // Query was old; new search is in progress. Or, no listeners to notify.
200
+ return ;
201
+ }
202
+
203
+ _results = newResults;
204
+ notifyListeners ();
205
+ }
206
+
207
+ Future <List <R >?> _computeResults (Q query) async {
208
+ final List <R > results = [];
209
+ final Iterable <T > data = getSortedItemsToTest (query);
210
+
211
+ final iterator = data.iterator;
212
+ bool isDone = false ;
213
+ while (! isDone) {
214
+ // CPU perf: End this task; enqueue a new one for resuming this work
215
+ await Future (() {});
216
+
217
+ if (query != _query || ! hasListeners) { // false if [dispose] has been called.
218
+ return null ;
219
+ }
220
+
221
+ for (int i = 0 ; i < 1000 ; i++ ) {
222
+ if (! iterator.moveNext ()) {
223
+ isDone = true ;
224
+ break ;
225
+ }
226
+ final T item = iterator.current;
227
+ final result = testItem (query, item);
228
+ if (result != null ) results.add (result);
229
+ }
230
+ }
231
+ return results;
232
+ }
233
+ }
234
+
235
+ class MentionAutocompleteView extends AutocompleteView <MentionAutocompleteQuery , MentionAutocompleteResult , User > {
236
+ final Narrow narrow;
237
+ final List <User > sortedUsers;
238
+
239
+ MentionAutocompleteView .init ({
240
+ required super .store,
241
+ required this .narrow,
242
+ }) : sortedUsers = _usersByRelevance (store: store, narrow: narrow)
243
+ {
244
+ store.autocompleteViewManager.registerMentionAutocomplete (this );
184
245
}
185
246
186
247
static List <User > _usersByRelevance ({
@@ -278,6 +339,19 @@ class MentionAutocompleteView extends ChangeNotifier {
278
339
streamId: streamId, senderId: userB.userId));
279
340
}
280
341
342
+ @override
343
+ Iterable <User > getSortedItemsToTest (MentionAutocompleteQuery query) {
344
+ return sortedUsers;
345
+ }
346
+
347
+ @override
348
+ MentionAutocompleteResult ? testItem (MentionAutocompleteQuery query, User item) {
349
+ if (query.testUser (item, store.autocompleteViewManager.autocompleteDataCache)) {
350
+ return UserMentionAutocompleteResult (userId: item.userId);
351
+ }
352
+ return null ;
353
+ }
354
+
281
355
/// Determines which of the two users is more recent in DM conversations.
282
356
///
283
357
/// Returns a negative number if [userA] is more recent than [userB] ,
@@ -317,110 +391,61 @@ class MentionAutocompleteView extends ChangeNotifier {
317
391
// TODO test that logic (may involve detecting an unhandled Future rejection; how?)
318
392
super .dispose ();
319
393
}
394
+ }
320
395
321
- final PerAccountStore store;
322
- final Narrow narrow ;
323
- final List <User > sortedUsers ;
396
+ abstract class AutocompleteQuery {
397
+ final String raw ;
398
+ final List <String > _lowercaseWords ;
324
399
325
- MentionAutocompleteQuery ? get query => _query;
326
- MentionAutocompleteQuery ? _query;
327
- set query (MentionAutocompleteQuery ? query) {
328
- _query = query;
329
- if (query != null ) {
330
- _startSearch (query);
331
- }
332
- }
400
+ AutocompleteQuery (this .raw) : _lowercaseWords = raw.toLowerCase ().split (' ' );
333
401
334
- /// Called when the app is reassembled during debugging, e.g. for hot reload .
402
+ /// Whether all of this query's words have matches in [words] that appear in order .
335
403
///
336
- /// This will redo the search from scratch for the current query, if any.
337
- void reassemble () {
338
- if (_query != null ) {
339
- _startSearch (_query! );
340
- }
341
- }
342
-
343
- Iterable <MentionAutocompleteResult > get results => _results;
344
- List <MentionAutocompleteResult > _results = [];
345
-
346
- Future <void > _startSearch (MentionAutocompleteQuery query) async {
347
- final newResults = await _computeResults (query);
348
- if (newResults == null ) {
349
- // Query was old; new search is in progress. Or, no listeners to notify.
350
- return ;
351
- }
352
-
353
- _results = newResults;
354
- notifyListeners ();
355
- }
356
-
357
- Future <List <MentionAutocompleteResult >?> _computeResults (MentionAutocompleteQuery query) async {
358
- final List <MentionAutocompleteResult > results = [];
359
- final iterator = sortedUsers.iterator;
360
- bool isDone = false ;
361
- while (! isDone) {
362
- // CPU perf: End this task; enqueue a new one for resuming this work
363
- await Future (() {});
364
-
365
- if (query != _query || ! hasListeners) { // false if [dispose] has been called.
366
- return null ;
404
+ /// A "match" means the word in [words] starts with the query word.
405
+ bool _testContainsQueryWords (List <String > words) {
406
+ // TODO(#237) test with diacritics stripped, where appropriate
407
+ int wordsIndex = 0 ;
408
+ int queryWordsIndex = 0 ;
409
+ while (true ) {
410
+ if (queryWordsIndex == _lowercaseWords.length) {
411
+ return true ;
412
+ }
413
+ if (wordsIndex == words.length) {
414
+ return false ;
367
415
}
368
416
369
- for (int i = 0 ; i < 1000 ; i++ ) {
370
- if (! iterator.moveNext ()) {
371
- isDone = true ;
372
- break ;
373
- }
374
-
375
- final User user = iterator.current;
376
- if (query.testUser (user, store.autocompleteViewManager.autocompleteDataCache)) {
377
- results.add (UserMentionAutocompleteResult (userId: user.userId));
378
- }
417
+ if (words[wordsIndex].startsWith (_lowercaseWords[queryWordsIndex])) {
418
+ queryWordsIndex++ ;
379
419
}
420
+ wordsIndex++ ;
380
421
}
381
- return results;
382
422
}
383
- }
384
423
385
- class MentionAutocompleteQuery {
386
- MentionAutocompleteQuery (this .raw, {this .silent = false })
387
- : _lowercaseWords = raw.toLowerCase ().split (' ' );
424
+ @override
425
+ String toString () {
426
+ return '${objectRuntimeType (this , 'AutocompleteQuery' )}(raw: $raw })' ;
427
+ }
388
428
389
- final String raw;
429
+ @override
430
+ bool operator == (Object other) {
431
+ return other is AutocompleteQuery && other.raw == raw;
432
+ }
390
433
434
+ @override
435
+ int get hashCode => Object .hash ('AutocompleteQuery' , raw);
436
+ }
437
+
438
+ class MentionAutocompleteQuery extends AutocompleteQuery {
391
439
/// Whether the user wants a silent mention (@_query, vs. @query).
392
440
final bool silent;
393
441
394
- final List < String > _lowercaseWords ;
442
+ MentionAutocompleteQuery ( super .raw, { this .silent = false }) ;
395
443
396
444
bool testUser (User user, AutocompleteDataCache cache) {
397
445
// TODO(#236) test email too, not just name
398
-
399
446
if (! user.isActive) return false ;
400
447
401
- return _testName (user, cache);
402
- }
403
-
404
- bool _testName (User user, AutocompleteDataCache cache) {
405
- // TODO(#237) test with diacritics stripped, where appropriate
406
-
407
- final List <String > nameWords = cache.nameWordsForUser (user);
408
-
409
- int nameWordsIndex = 0 ;
410
- int queryWordsIndex = 0 ;
411
- while (true ) {
412
- if (queryWordsIndex == _lowercaseWords.length) {
413
- return true ;
414
- }
415
- if (nameWordsIndex == nameWords.length) {
416
- return false ;
417
- }
418
-
419
- if (nameWords[nameWordsIndex].startsWith (_lowercaseWords[queryWordsIndex])) {
420
- queryWordsIndex++ ;
421
- }
422
- nameWordsIndex++ ;
423
- }
448
+ return _testContainsQueryWords (cache.nameWordsForUser (user));
424
449
}
425
450
426
451
@override
@@ -449,7 +474,9 @@ class AutocompleteDataCache {
449
474
}
450
475
}
451
476
452
- sealed class MentionAutocompleteResult {}
477
+ class AutocompleteResult {}
478
+
479
+ sealed class MentionAutocompleteResult extends AutocompleteResult {}
453
480
454
481
class UserMentionAutocompleteResult extends MentionAutocompleteResult {
455
482
UserMentionAutocompleteResult ({required this .userId});
0 commit comments