Skip to content

Commit c661d3f

Browse files
authored
Limit suggestions to 50 in search completion. (#8220)
1 parent e192bc9 commit c661d3f

File tree

3 files changed

+61
-22
lines changed

3 files changed

+61
-22
lines changed

pkg/web_app/lib/src/widget/completion/suggest.dart

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import 'package:collection/collection.dart';
99

1010
typedef Suggestions = List<Suggestion>;
1111

12-
class Suggestion {
12+
class Suggestion implements Comparable<Suggestion> {
1313
final int start;
1414
final int end;
1515
final String value;
@@ -32,15 +32,25 @@ class Suggestion {
3232
'html': html,
3333
'score': score,
3434
};
35+
36+
@override
37+
int compareTo(Suggestion other) {
38+
final sc = -score.compareTo(other.score);
39+
if (sc != 0) return sc;
40+
final lc = value.length.compareTo(other.value.length);
41+
if (lc != 0) return lc;
42+
return value.compareTo(other.value);
43+
}
3544
}
3645

3746
/// Given [data] and [caret] position inside [text] what suggestions do we
3847
/// want to offer and should completion be automatically triggered?
39-
({bool trigger, Suggestions suggestions}) suggest(
48+
({bool trigger, Suggestions suggestions, bool isTrimmed}) suggest(
4049
CompletionData data,
4150
String text,
42-
int caret,
43-
) {
51+
int caret, {
52+
int maxOptionCount = 50,
53+
}) {
4454
// Get position before caret
4555
final beforeCaret = caret > 0 ? caret - 1 : 0;
4656
// Get position of space after the caret
@@ -81,6 +91,7 @@ class Suggestion {
8191
return (
8292
trigger: false,
8393
suggestions: [],
94+
isTrimmed: false,
8495
);
8596
}
8697

@@ -94,6 +105,7 @@ class Suggestion {
94105
return (
95106
trigger: false,
96107
suggestions: [],
108+
isTrimmed: false,
97109
);
98110
}
99111
// We don't to auto trigger completion unless there is an option that is
@@ -106,7 +118,7 @@ class Suggestion {
106118
// Terminate suggestion with a ' ' suffix, if this is a terminal completion
107119
final suffix = completion.terminal ? ' ' : '';
108120

109-
final suggestions = completion.options.map((option) {
121+
var suggestions = completion.options.map((option) {
110122
final overlap = _lcs(prefix, option);
111123
var html = option;
112124
// highlight the overlapping part of the text
@@ -129,15 +141,32 @@ class Suggestion {
129141
html: html,
130142
score: score,
131143
);
132-
}).sorted((a, b) {
133-
final x = -a.score.compareTo(b.score);
134-
if (x != 0) return x;
135-
return a.value.compareTo(b.value);
136-
});
144+
}).toList();
145+
final isTrimmed = suggestions.length > maxOptionCount;
146+
if (!isTrimmed) {
147+
suggestions.sort();
148+
} else {
149+
// List of score bucket entries ordered by decreasing score.
150+
final buckets = suggestions
151+
.groupListsBy((s) => s.score.floor())
152+
.entries
153+
.toList()
154+
..sort((a, b) => -a.key.compareTo(b.key));
155+
suggestions = [];
156+
for (final bucket in buckets) {
157+
bucket.value.sort();
158+
suggestions
159+
.addAll(bucket.value.take(maxOptionCount - suggestions.length));
160+
if (suggestions.length >= maxOptionCount) {
161+
break;
162+
}
163+
}
164+
}
137165

138166
return (
139167
trigger: trigger,
140168
suggestions: suggestions,
169+
isTrimmed: isTrimmed,
141170
);
142171
}
143172

pkg/web_app/lib/src/widget/completion/widget.dart

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ final class _State {
118118
/// Selected suggestion
119119
final int selectedIndex;
120120

121+
/// Whether the suggestion list is trimmed for space consideratoins.
122+
final bool isTrimmed;
123+
121124
_State({
122125
this.inactive = false,
123126
this.closed = false,
@@ -127,6 +130,7 @@ final class _State {
127130
this.caret = 0,
128131
this.suggestions = const [],
129132
this.selectedIndex = 0,
133+
this.isTrimmed = false,
130134
});
131135

132136
_State update({
@@ -138,6 +142,7 @@ final class _State {
138142
int? caret,
139143
Suggestions? suggestions,
140144
int? selectedIndex,
145+
bool? isTrimmed,
141146
}) =>
142147
_State(
143148
inactive: inactive ?? this.inactive,
@@ -148,11 +153,12 @@ final class _State {
148153
caret: caret ?? this.caret,
149154
suggestions: suggestions ?? this.suggestions,
150155
selectedIndex: selectedIndex ?? this.selectedIndex,
156+
isTrimmed: isTrimmed ?? this.isTrimmed,
151157
);
152158

153159
@override
154160
String toString() =>
155-
'_State(forced: $forced, triggered: $triggered, caret: $caret, text: $text, selected: $selectedIndex)';
161+
'_State(forced: $forced, triggered: $triggered, caret: $caret, text: $text, selected: $selectedIndex, isTrimmed: $isTrimmed)';
156162
}
157163

158164
final class _CompletionWidget {
@@ -212,7 +218,7 @@ final class _CompletionWidget {
212218
delta = state.text.substring(caret, state.caret);
213219
}
214220
final crossedWordBoundary = delta.contains(_whitespace);
215-
final (:trigger, :suggestions) = suggest(
221+
final (:trigger, :suggestions, :isTrimmed) = suggest(
216222
data,
217223
text,
218224
caret,
@@ -223,6 +229,7 @@ final class _CompletionWidget {
223229
suggestions: suggestions,
224230
text: text,
225231
caret: caret,
232+
isTrimmed: isTrimmed,
226233
);
227234
update();
228235
}
@@ -262,6 +269,9 @@ final class _CompletionWidget {
262269
..setAttribute('data-completion-option-index', i.toString())
263270
..classList.add(optionClass));
264271
}
272+
if (state.isTrimmed) {
273+
dropdown.appendChild(HTMLDivElement()..textContent = '[...]');
274+
}
265275
}
266276
_renderedSuggestions = state.suggestions;
267277

pkg/web_app/test/widget/completion/suggest_test.dart

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,15 @@ void main() {
4646
{
4747
'start': 0,
4848
'end': 6,
49-
'value': 'has:',
50-
'html': 'has:',
49+
'value': 'is:',
50+
'html': 'is:',
5151
'score': 0.0,
5252
},
5353
{
5454
'start': 0,
5555
'end': 6,
56-
'value': 'is:',
57-
'html': 'is:',
56+
'value': 'has:',
57+
'html': 'has:',
5858
'score': 0.0,
5959
},
6060
]);
@@ -98,16 +98,16 @@ void main() {
9898
{
9999
'start': 0,
100100
'end': 5,
101-
'value': 'is:null-safe ',
102-
'html': '<span class="completion-overlap">is:</span>null-safe',
103-
'score': 0.0,
101+
'value': 'is:plugin ',
102+
'html': '<span class="completion-overlap">is:</span>plugin',
103+
'score': 0.0
104104
},
105105
{
106106
'start': 0,
107107
'end': 5,
108-
'value': 'is:plugin ',
109-
'html': '<span class="completion-overlap">is:</span>plugin',
110-
'score': 0.0
108+
'value': 'is:null-safe ',
109+
'html': '<span class="completion-overlap">is:</span>null-safe',
110+
'score': 0.0,
111111
},
112112
{
113113
'start': 0,

0 commit comments

Comments
 (0)