@@ -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
@@ -68,8 +68,8 @@ final RegExp mentionAutocompleteMarkerRegex = (() {
68
68
unicode: true );
69
69
})();
70
70
71
- /// The content controller's recognition that the user might want autocomplete UI.
72
- class AutocompleteIntent {
71
+ /// The text controller's recognition that the user might want autocomplete UI.
72
+ class AutocompleteIntent < QueryT 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 QueryT query;
95
95
96
96
/// The [TextEditingValue] whose text [syntaxStart] refers to.
97
97
final TextEditingValue textEditingValue;
@@ -151,21 +151,90 @@ 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 {
166
+ abstract class AutocompleteView <QueryT extends AutocompleteQuery , ResultT extends AutocompleteResult , CandidateT > extends ChangeNotifier {
167
+ AutocompleteView ({required this .store});
168
+
169
+ final PerAccountStore store;
170
+
171
+ Iterable <CandidateT > getSortedItemsToTest (QueryT query);
172
+
173
+ ResultT ? testItem (QueryT query, CandidateT item);
174
+
175
+ QueryT ? get query => _query;
176
+ QueryT ? _query;
177
+ set query (QueryT ? 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 <ResultT > get results => _results;
194
+ List <ResultT > _results = [];
195
+
196
+ Future <void > _startSearch (QueryT 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 <ResultT >?> _computeResults (QueryT query) async {
208
+ final List <ResultT > results = [];
209
+ final Iterable <CandidateT > 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 CandidateT 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 > {
167
236
MentionAutocompleteView ._({
168
- required this .store,
237
+ required super .store,
169
238
required this .narrow,
170
239
required this .sortedUsers,
171
240
});
@@ -183,6 +252,9 @@ class MentionAutocompleteView extends ChangeNotifier {
183
252
return view;
184
253
}
185
254
255
+ final Narrow narrow;
256
+ final List <User > sortedUsers;
257
+
186
258
static List <User > _usersByRelevance ({
187
259
required PerAccountStore store,
188
260
required Narrow narrow,
@@ -289,6 +361,19 @@ class MentionAutocompleteView extends ChangeNotifier {
289
361
streamId: streamId, senderId: userB.userId));
290
362
}
291
363
364
+ @override
365
+ Iterable <User > getSortedItemsToTest (MentionAutocompleteQuery query) {
366
+ return sortedUsers;
367
+ }
368
+
369
+ @override
370
+ MentionAutocompleteResult ? testItem (MentionAutocompleteQuery query, User item) {
371
+ if (query.testUser (item, store.autocompleteViewManager.autocompleteDataCache)) {
372
+ return UserMentionAutocompleteResult (userId: item.userId);
373
+ }
374
+ return null ;
375
+ }
376
+
292
377
/// Determines which of the two users is more recent in DM conversations.
293
378
///
294
379
/// Returns a negative number if [userA] is more recent than [userB] ,
@@ -349,82 +434,44 @@ class MentionAutocompleteView extends ChangeNotifier {
349
434
// TODO test that logic (may involve detecting an unhandled Future rejection; how?)
350
435
super .dispose ();
351
436
}
437
+ }
352
438
353
- final PerAccountStore store;
354
- final Narrow narrow;
355
- final List < User > sortedUsers ;
439
+ abstract class AutocompleteQuery {
440
+ AutocompleteQuery ( this .raw)
441
+ : _lowercaseWords = raw. toLowerCase (). split ( ' ' ) ;
356
442
357
- MentionAutocompleteQuery ? get query => _query;
358
- MentionAutocompleteQuery ? _query;
359
- set query (MentionAutocompleteQuery ? query) {
360
- _query = query;
361
- if (query != null ) {
362
- _startSearch (query);
363
- }
364
- }
443
+ final String raw;
444
+ final List <String > _lowercaseWords;
365
445
366
- /// Called when the app is reassembled during debugging, e.g. for hot reload .
446
+ /// Whether all of this query's words have matches in [words] that appear in order .
367
447
///
368
- /// This will redo the search from scratch for the current query, if any.
369
- void reassemble () {
370
- if (_query != null ) {
371
- _startSearch (_query! );
372
- }
373
- }
374
-
375
- Iterable <MentionAutocompleteResult > get results => _results;
376
- List <MentionAutocompleteResult > _results = [];
377
-
378
- Future <void > _startSearch (MentionAutocompleteQuery query) async {
379
- final newResults = await _computeResults (query);
380
- if (newResults == null ) {
381
- // Query was old; new search is in progress. Or, no listeners to notify.
382
- return ;
383
- }
384
-
385
- _results = newResults;
386
- notifyListeners ();
387
- }
388
-
389
- Future <List <MentionAutocompleteResult >?> _computeResults (MentionAutocompleteQuery query) async {
390
- final List <MentionAutocompleteResult > results = [];
391
- final iterator = sortedUsers.iterator;
392
- bool isDone = false ;
393
- while (! isDone) {
394
- // CPU perf: End this task; enqueue a new one for resuming this work
395
- await Future (() {});
396
-
397
- if (query != _query || ! hasListeners) { // false if [dispose] has been called.
398
- return null ;
448
+ /// A "match" means the word in [words] starts with the query word.
449
+ bool _testContainsQueryWords (List <String > words) {
450
+ // TODO(#237) test with diacritics stripped, where appropriate
451
+ int wordsIndex = 0 ;
452
+ int queryWordsIndex = 0 ;
453
+ while (true ) {
454
+ if (queryWordsIndex == _lowercaseWords.length) {
455
+ return true ;
456
+ }
457
+ if (wordsIndex == words.length) {
458
+ return false ;
399
459
}
400
460
401
- for (int i = 0 ; i < 1000 ; i++ ) {
402
- if (! iterator.moveNext ()) {
403
- isDone = true ;
404
- break ;
405
- }
406
-
407
- final User user = iterator.current;
408
- if (query.testUser (user, store.autocompleteViewManager.autocompleteDataCache)) {
409
- results.add (UserMentionAutocompleteResult (userId: user.userId));
410
- }
461
+ if (words[wordsIndex].startsWith (_lowercaseWords[queryWordsIndex])) {
462
+ queryWordsIndex++ ;
411
463
}
464
+ wordsIndex++ ;
412
465
}
413
- return results;
414
466
}
415
467
}
416
468
417
- class MentionAutocompleteQuery {
418
- MentionAutocompleteQuery (this .raw, {this .silent = false })
419
- : _lowercaseWords = raw.toLowerCase ().split (' ' );
420
-
421
- final String raw;
469
+ class MentionAutocompleteQuery extends AutocompleteQuery {
470
+ MentionAutocompleteQuery (super .raw, {this .silent = false });
422
471
423
472
/// Whether the user wants a silent mention (@_query, vs. @query).
424
473
final bool silent;
425
474
426
- final List <String > _lowercaseWords;
427
-
428
475
bool testUser (User user, AutocompleteDataCache cache) {
429
476
// TODO(#236) test email too, not just name
430
477
@@ -434,25 +481,7 @@ class MentionAutocompleteQuery {
434
481
}
435
482
436
483
bool _testName (User user, AutocompleteDataCache cache) {
437
- // TODO(#237) test with diacritics stripped, where appropriate
438
-
439
- final List <String > nameWords = cache.nameWordsForUser (user);
440
-
441
- int nameWordsIndex = 0 ;
442
- int queryWordsIndex = 0 ;
443
- while (true ) {
444
- if (queryWordsIndex == _lowercaseWords.length) {
445
- return true ;
446
- }
447
- if (nameWordsIndex == nameWords.length) {
448
- return false ;
449
- }
450
-
451
- if (nameWords[nameWordsIndex].startsWith (_lowercaseWords[queryWordsIndex])) {
452
- queryWordsIndex++ ;
453
- }
454
- nameWordsIndex++ ;
455
- }
484
+ return _testContainsQueryWords (cache.nameWordsForUser (user));
456
485
}
457
486
458
487
@override
@@ -489,7 +518,9 @@ class AutocompleteDataCache {
489
518
}
490
519
}
491
520
492
- sealed class MentionAutocompleteResult {}
521
+ class AutocompleteResult {}
522
+
523
+ sealed class MentionAutocompleteResult extends AutocompleteResult {}
493
524
494
525
class UserMentionAutocompleteResult extends MentionAutocompleteResult {
495
526
UserMentionAutocompleteResult ({required this .userId});
0 commit comments