Skip to content

Commit 03429b2

Browse files
scheglovcommit-bot@chromium.org
authored andcommitted
Use FuzzyMatcher for Cider completion filtering and sorting.
R=brianwilkerson@google.com, keertip@google.com Change-Id: I1b8360e95327dfd4eaefaa8990105bd4750c2acc Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/144864 Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
1 parent c1edd26 commit 03429b2

File tree

2 files changed

+233
-3
lines changed

2 files changed

+233
-3
lines changed

pkg/analysis_server/lib/src/cider/completion.dart

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ import 'package:analysis_server/src/services/completion/completion_core.dart';
99
import 'package:analysis_server/src/services/completion/completion_performance.dart';
1010
import 'package:analysis_server/src/services/completion/dart/completion_manager.dart';
1111
import 'package:analysis_server/src/services/completion/dart/local_library_contributor.dart';
12+
import 'package:analysis_server/src/services/completion/filtering/fuzzy_matcher.dart';
13+
import 'package:analyzer/dart/ast/ast.dart';
1214
import 'package:analyzer/dart/element/element.dart' show LibraryElement;
1315
import 'package:analyzer/src/dart/analysis/performance_logger.dart';
1416
import 'package:analyzer/src/dart/micro/resolve_file.dart';
1517
import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart';
18+
import 'package:analyzer/src/test_utilities/function_ast_visitor.dart';
1619
import 'package:analyzer_plugin/protocol/protocol_common.dart';
1720
import 'package:meta/meta.dart';
1821

@@ -131,6 +134,13 @@ class CiderCompletionComputer {
131134
});
132135
}
133136

137+
_logger.run('Filter suggestions', () {
138+
suggestions = _FilterSort(
139+
_dartCompletionRequest,
140+
suggestions,
141+
).perform();
142+
});
143+
134144
var result = CiderCompletionResult._(suggestions);
135145

136146
_cache._lastResult =
@@ -200,6 +210,74 @@ class _CiderImportedLibrarySuggestions {
200210
_CiderImportedLibrarySuggestions(this.signature, this.suggestions);
201211
}
202212

213+
class _FilterSort {
214+
final DartCompletionRequestImpl _request;
215+
final List<CompletionSuggestion> _suggestions;
216+
217+
FuzzyMatcher _matcher;
218+
219+
_FilterSort(this._request, this._suggestions);
220+
221+
List<CompletionSuggestion> perform() {
222+
var pattern = _matchingPattern();
223+
_matcher = FuzzyMatcher(pattern, matchStyle: MatchStyle.SYMBOL);
224+
225+
var scored = _suggestions
226+
.map((e) => _FuzzyScoredSuggestion(e, _score(e)))
227+
.where((e) => e.score > 0)
228+
.toList();
229+
230+
scored.sort((a, b) {
231+
// Prefer what the user requested by typing.
232+
if (a.score > b.score) {
233+
return -1;
234+
} else if (a.score < b.score) {
235+
return 1;
236+
}
237+
238+
// Then prefer what is more relevant in the context.
239+
if (a.suggestion.relevance != b.suggestion.relevance) {
240+
return -(a.suggestion.relevance - b.suggestion.relevance);
241+
}
242+
243+
// Other things being equal, sort by name.
244+
return a.suggestion.completion.compareTo(b.suggestion.completion);
245+
});
246+
247+
return scored.map((e) => e.suggestion).toList();
248+
}
249+
250+
/// Return the pattern to match suggestions against, from the identifier
251+
/// to the left of the caret. Return the empty string if cannot find the
252+
/// identifier.
253+
String _matchingPattern() {
254+
SimpleIdentifier patternNode;
255+
_request.target.containingNode.accept(
256+
FunctionAstVisitor(simpleIdentifier: (node) {
257+
if (node.end == _request.offset) {
258+
patternNode = node;
259+
}
260+
}),
261+
);
262+
263+
if (patternNode != null) {
264+
return patternNode.name;
265+
}
266+
267+
return '';
268+
}
269+
270+
double _score(CompletionSuggestion e) => _matcher.score(e.completion);
271+
}
272+
273+
/// [CompletionSuggestion] scored using [FuzzyMatcher].
274+
class _FuzzyScoredSuggestion {
275+
final CompletionSuggestion suggestion;
276+
final double score;
277+
278+
_FuzzyScoredSuggestion(this.suggestion, this.score);
279+
}
280+
203281
class _LastCompletionResult {
204282
final String path;
205283
final String signature;

pkg/analysis_server/test/src/cider/completion_test.dart

Lines changed: 155 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,120 @@ var a = ^;
156156
_assertHasClass(text: 'String');
157157
}
158158

159+
Future<void> test_filterSort_byPattern_excludeNotMatching() async {
160+
await _compute2(r'''
161+
var a = F^;
162+
''');
163+
164+
_assertHasClass(text: 'Future');
165+
_assertNoClass(text: 'String');
166+
}
167+
168+
Future<void> test_filterSort_byPattern_location_beforeMethod() async {
169+
await _compute2(r'''
170+
class A {
171+
F^
172+
void foo() {}
173+
}
174+
''');
175+
176+
_assertHasClass(text: 'Future');
177+
_assertNoClass(text: 'String');
178+
}
179+
180+
Future<void> test_filterSort_byPattern_location_functionReturnType() async {
181+
await _compute2(r'''
182+
F^ foo() {}
183+
''');
184+
185+
_assertHasClass(text: 'Future');
186+
_assertNoClass(text: 'String');
187+
}
188+
189+
Future<void> test_filterSort_byPattern_location_methodReturnType() async {
190+
await _compute2(r'''
191+
class A {
192+
F^ foo() {}
193+
}
194+
''');
195+
196+
_assertHasClass(text: 'Future');
197+
_assertNoClass(text: 'String');
198+
}
199+
200+
Future<void> test_filterSort_byPattern_location_parameterType() async {
201+
await _compute2(r'''
202+
void foo(F^ a) {}
203+
''');
204+
205+
_assertHasClass(text: 'Future');
206+
_assertNoClass(text: 'String');
207+
}
208+
209+
Future<void> test_filterSort_byPattern_location_parameterType2() async {
210+
await _compute2(r'''
211+
void foo(^a) {}
212+
''');
213+
214+
_assertHasClass(text: 'Future');
215+
_assertHasClass(text: 'String');
216+
}
217+
218+
Future<void> test_filterSort_byPattern_location_statement() async {
219+
await _compute2(r'''
220+
main() {
221+
F^
222+
0;
223+
}
224+
''');
225+
226+
_assertHasClass(text: 'Future');
227+
_assertNoClass(text: 'String');
228+
}
229+
230+
Future<void> test_filterSort_byPattern_preferPrefix() async {
231+
await _compute2(r'''
232+
class Foobar {}
233+
class Falcon {}
234+
var a = Fo^;
235+
''');
236+
237+
_assertOrder([
238+
_assertHasClass(text: 'Foobar'),
239+
_assertHasClass(text: 'Falcon'),
240+
]);
241+
}
242+
243+
Future<void> test_filterSort_preferLocal() async {
244+
await _compute2(r'''
245+
var a = 0;
246+
main() {
247+
var b = 0;
248+
var v = ^;
249+
}
250+
''');
251+
252+
_assertOrder([
253+
_assertHasLocalVariable(text: 'b'),
254+
_assertHasTopLevelVariable(text: 'a'),
255+
]);
256+
}
257+
258+
Future<void> test_filterSort_sortByName() async {
259+
await _compute2(r'''
260+
main() {
261+
var a = 0;
262+
var b = 0;
263+
var v = ^;
264+
}
265+
''');
266+
267+
_assertOrder([
268+
_assertHasLocalVariable(text: 'a'),
269+
_assertHasLocalVariable(text: 'b'),
270+
]);
271+
}
272+
159273
void _assertComputedImportedLibraries(List<String> expected) {
160274
expected = expected.map(convertPath).toList();
161275
expect(
@@ -164,19 +278,46 @@ var a = ^;
164278
);
165279
}
166280

167-
void _assertHasClass({@required String text}) {
281+
CompletionSuggestion _assertHasClass({@required String text}) {
168282
var matching = _matchingCompletions(
169283
text: text,
170284
elementKind: ElementKind.CLASS,
171285
);
172286
expect(matching, hasLength(1), reason: 'Expected exactly one completion');
287+
return matching.single;
173288
}
174289

175290
void _assertHasCompletion({@required String text}) {
176291
var matching = _matchingCompletions(text: text);
177292
expect(matching, hasLength(1), reason: 'Expected exactly one completion');
178293
}
179294

295+
CompletionSuggestion _assertHasLocalVariable({@required String text}) {
296+
var matching = _matchingCompletions(
297+
text: text,
298+
elementKind: ElementKind.LOCAL_VARIABLE,
299+
);
300+
expect(
301+
matching,
302+
hasLength(1),
303+
reason: 'Expected exactly one completion in $_suggestions',
304+
);
305+
return matching.single;
306+
}
307+
308+
CompletionSuggestion _assertHasTopLevelVariable({@required String text}) {
309+
var matching = _matchingCompletions(
310+
text: text,
311+
elementKind: ElementKind.TOP_LEVEL_VARIABLE,
312+
);
313+
expect(
314+
matching,
315+
hasLength(1),
316+
reason: 'Expected exactly one completion in $_suggestions',
317+
);
318+
return matching.single;
319+
}
320+
180321
void _assertNoClass({@required String text}) {
181322
var matching = _matchingCompletions(
182323
text: text,
@@ -185,6 +326,16 @@ var a = ^;
185326
expect(matching, isEmpty, reason: 'Expected zero completions');
186327
}
187328

329+
void _assertOrder(List<CompletionSuggestion> suggestions) {
330+
var lastIndex = -2;
331+
for (var suggestion in suggestions) {
332+
var index = _suggestions.indexOf(suggestion);
333+
expect(index, isNonNegative, reason: '$suggestion');
334+
expect(index, greaterThan(lastIndex), reason: '$suggestion');
335+
lastIndex = index;
336+
}
337+
}
338+
188339
Future _compute2(String content) async {
189340
var context = _updateFile(content);
190341

@@ -227,15 +378,16 @@ var a = ^;
227378
}
228379

229380
_CompletionContext _updateFile(String content) {
230-
newFile(testPath, content: content);
231-
232381
var offset = content.indexOf('^');
233382
expect(offset, isPositive, reason: 'Expected to find ^');
234383
expect(content.indexOf('^', offset + 1), -1, reason: 'Expected only one ^');
235384

236385
var lineInfo = LineInfo.fromContent(content);
237386
var location = lineInfo.getLocation(offset);
238387

388+
content = content.substring(0, offset) + content.substring(offset + 1);
389+
newFile(testPath, content: content);
390+
239391
return _CompletionContext(
240392
content,
241393
offset,

0 commit comments

Comments
 (0)