Skip to content

Commit

Permalink
Version 3.3.0-3.0.dev
Browse files Browse the repository at this point in the history
Merge 6f91177 into dev
  • Loading branch information
Dart CI committed Oct 9, 2023
2 parents a6e43b0 + 6f91177 commit 078162e
Show file tree
Hide file tree
Showing 46 changed files with 1,779 additions and 69 deletions.
125 changes: 82 additions & 43 deletions pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ import 'package:analyzer_plugin/src/utilities/completion/completion_target.dart'
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';

/// A record of a [CompletionItem] and a fuzzy score.
typedef _ScoredCompletionItem = ({CompletionItem item, double score});

class CompletionHandler
extends LspMessageHandler<CompletionParams, CompletionList>
with LspPluginRequestHandlerMixin, LspHandlerHelperMixin {
Expand Down Expand Up @@ -208,9 +211,12 @@ class CompletionHandler
if (pluginResults.isError) return failure(pluginResults);

final serverResult = serverResults.result;
final untruncatedRankedItems = serverResult.rankedItems
.followedBy(pluginResults.result.items)
.toList();
// Add in fuzzy scores for completion items.
final pluginResultItems = pluginResults.result.items.map((item) =>
(item: item, score: serverResult.fuzzy.completionItemScore(item)));

final untruncatedRankedItems =
serverResult.rankedItems.followedBy(pluginResultItems).toList();
final unrankedItems = serverResult.unrankedItems;

// Truncate ranked items allowing for all unranked items.
Expand All @@ -223,8 +229,10 @@ class CompletionHandler
maxRankedItems,
);

final truncatedItems =
truncatedRankedItems.followedBy(unrankedItems).toList();
final truncatedItems = truncatedRankedItems
.map((item) => item.item)
.followedBy(unrankedItems)
.toList();

// If we're tracing performance (only Dart), record the number of results
// after truncation.
Expand Down Expand Up @@ -368,7 +376,7 @@ class CompletionHandler
);
final target = completionRequest.target;
final targetPrefix = completionRequest.targetPrefix;
final fuzzy = _FuzzyFilterHelper(targetPrefix);
final fuzzy = _FuzzyScoreHelper(targetPrefix);

if (triggerCharacter != null) {
if (!_triggerCharacterValid(offset, triggerCharacter, target)) {
Expand Down Expand Up @@ -499,8 +507,17 @@ class CompletionHandler

final rankedResults = performance.run('mapSuggestions', (performance) {
return serverSuggestions
.where(fuzzy.completionSuggestionMatches)
.map(suggestionToCompletionItem)
// Compute the fuzzy score which we can use both for filtering here
// and for truncation sorting later on.
.map((item) => (item: item, score: fuzzy.suggestionScore(item)))
// Filter out the non-matches.
.where((scoredItem) => scoredItem.score > 0)
// Convert to CompletionItem and re-attach the score to be used for
// truncation later.
.map((scoredItem) => (
item: suggestionToCompletionItem(scoredItem.item),
score: scoredItem.score
))
.toList();
});

Expand Down Expand Up @@ -554,7 +571,7 @@ class CompletionHandler

return success(_CompletionResults(
isIncomplete: isIncomplete,
targetPrefix: targetPrefix,
fuzzy: fuzzy,
rankedItems: rankedResults,
unrankedItems: unrankedResults,
defaults: defaults,
Expand Down Expand Up @@ -734,43 +751,52 @@ class CompletionHandler
return true; // Any other trigger character can be handled always.
}

/// Truncates [items] to [maxItems] but additionally includes any items that
/// exactly match [prefix].
Iterable<CompletionItem> _truncateResults(
List<CompletionItem> items,
/// Truncates [items] to [maxItems] after sorting by fuzzy score (then
/// relevance/sortText) but always includes any items that exactly match
/// [prefix].
Iterable<_ScoredCompletionItem> _truncateResults(
List<_ScoredCompletionItem> items,
String prefix,
int maxItems,
) {
// Take the top `maxRankedItem` plus any exact matches.
final prefixLower = prefix.toLowerCase();
bool isExactMatch(CompletionItem item) =>
(item.filterText ?? item.label).toLowerCase() == prefixLower;

// Sort the items by relevance using sortText.
items.sort(sortTextComparer);
// Sort the items by fuzzy score and then relevance (sortText).
items.sort(_scoreCompletionItemComparer);

// Skip the text comparisons if we don't have a prefix (plugin results, or
// just no prefix when completion was invoked).
final shouldInclude = prefixLower.isEmpty
? (int index, CompletionItem item) => index < maxItems
: (int index, CompletionItem item) =>
index < maxItems || isExactMatch(item);
? (int index, _ScoredCompletionItem item) => index < maxItems
: (int index, _ScoredCompletionItem item) =>
index < maxItems || isExactMatch(item.item);

return items.whereIndexed(shouldInclude);
}

/// Compares [CompletionItem]s by the `sortText` field, which is derived from
/// relevance.
/// Compares [_ScoredCompletionItem]s by their fuzzy match score and then
/// `sortText` field (which is derived from relevance).
///
/// For items with the same relevance, shorter items are sorted first so that
/// truncation always removes longer items first (which can be included by
/// typing more of their characters).
static int sortTextComparer(CompletionItem item1, CompletionItem item2) {
/// For items with the same fuzzy score/relevance, shorter items are sorted
/// first so that truncation always removes longer items first (which can be
/// included by typing more of their characters).
static int _scoreCompletionItemComparer(
_ScoredCompletionItem item1,
_ScoredCompletionItem item2,
) {
// First try to sort by fuzzy score.
if (item1.score != item2.score) {
return item2.score.compareTo(item1.score);
}

// Otherwise, use sortText.
// Note: It should never be the case that we produce items without sortText
// but if they're null, fall back to label which is what the client would do
// when sorting.
final item1Text = item1.sortText ?? item1.label;
final item2Text = item2.sortText ?? item2.label;
final item1Text = item1.item.sortText ?? item1.item.label;
final item2Text = item2.item.sortText ?? item2.item.label;

// If both items have the same text, this means they had the same relevance.
// In this case, sort by the length of the name ascending, so that shorter
Expand All @@ -786,7 +812,7 @@ class CompletionHandler
//
// Typing 'aaa' should not allow 'aaa' to be truncated before 'aaa1'.
if (item1Text == item2Text) {
return item1.label.length.compareTo(item2.label.length);
return item1.item.label.length.compareTo(item2.item.label.length);
}

return item1Text.compareTo(item2Text);
Expand Down Expand Up @@ -859,14 +885,15 @@ class CompletionRegistrations extends FeatureRegistration

/// A set of completion items split into ranked and unranked items.
class _CompletionResults {
/// Items that can be ranked using their relevance/sortText.
final List<CompletionItem> rankedItems;
/// Items that can be ranked using their relevance/sortText, returned with
/// their fuzzy match (if [targetPrefix] was provided).
final List<_ScoredCompletionItem> rankedItems;

/// Items that cannot be ranked, and should avoid being truncated.
final List<CompletionItem> unrankedItems;

/// Any prefixed used to filter the results.
final String targetPrefix;
/// The fuzzy filter used to score results.
final _FuzzyScoreHelper fuzzy;

final bool isIncomplete;

Expand All @@ -878,42 +905,54 @@ class _CompletionResults {
_CompletionResults({
this.rankedItems = const [],
this.unrankedItems = const [],
required this.targetPrefix,
required this.fuzzy,
required this.isIncomplete,
this.defaults,
});

_CompletionResults.empty() : this(targetPrefix: '', isIncomplete: false);
_CompletionResults.empty()
: this(fuzzy: _FuzzyScoreHelper.empty, isIncomplete: false);

/// An empty result set marked as incomplete because an error occurred.
_CompletionResults.emptyIncomplete()
: this(targetPrefix: '', isIncomplete: true);
: this(fuzzy: _FuzzyScoreHelper.empty, isIncomplete: true);

_CompletionResults.unranked(
List<CompletionItem> unrankedItems, {
required bool isIncomplete,
}) : this(
unrankedItems: unrankedItems,
targetPrefix: '',
fuzzy: _FuzzyScoreHelper.empty,
isIncomplete: isIncomplete,
);

/// Any prefix used to filter the results.
String get targetPrefix => fuzzy.prefix;
}

/// Helper to simplify fuzzy filtering.
/// Helper to simplify fuzzy scoring.
///
/// Used to perform fuzzy matching based on the identifier in front of the caret to
/// reduce the size of the payload.
class _FuzzyFilterHelper {
/// Used to sort results for truncation and to filter out items that don't
/// match the characters in front of the caret to reduce the size of the
/// payload.
class _FuzzyScoreHelper {
static final empty = _FuzzyScoreHelper('');

final String prefix;

final FuzzyMatcher _matcher;

_FuzzyFilterHelper(String prefix)
_FuzzyScoreHelper(this.prefix)
: _matcher = FuzzyMatcher(prefix, matchStyle: MatchStyle.TEXT);

bool completionItemMatches(CompletionItem item) =>
stringMatches(item.filterText ?? item.label);

bool completionSuggestionMatches(CompletionSuggestion item) =>
stringMatches(item.displayText ?? item.completion);
double completionItemScore(CompletionItem item) =>
_matcher.score(item.filterText ?? item.label);

bool stringMatches(String input) => _matcher.score(input) > 0;

double suggestionScore(CompletionSuggestion item) =>
_matcher.score(item.displayText ?? item.completion);
}
9 changes: 6 additions & 3 deletions pkg/analysis_server/test/lsp/completion.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ mixin CompletionTestMixin on AbstractLspAnalysisServerTest {
String? expectedContentIfInserting,
bool verifyInsertReplaceRanges = false,
bool openCloseFile = true,
bool expectNoAdditionalItems = false,
}) async {
final code = TestCode.parse(content);
// If verifyInsertReplaceRanges is true, we need both expected contents.
Expand All @@ -48,11 +49,13 @@ mixin CompletionTestMixin on AbstractLspAnalysisServerTest {
await closeFile(fileUri);
}

// Sort the completions by sortText and filter to those we expect, so the ordering
// can be compared.
final sortedResults = completionResults
.where((r) => expectCompletions.contains(r.label))
// Filter to those we expect (unless `expectNoAdditionalItems` which
// indicates the test wants to ensure no additional unmatched items)
.where((r) =>
expectNoAdditionalItems || expectCompletions.contains(r.label))
.toList()
// Sort using sortText as a client would.
..sort(sortTextSorter);

expect(sortedResults.map((item) => item.label), equals(expectCompletions));
Expand Down
47 changes: 47 additions & 0 deletions pkg/analysis_server/test/lsp/completion_dart_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2663,6 +2663,53 @@ void f() { }
);
}

Future<void> test_sort_sortsByRelevance() async {
final content = '''
class UniquePrefixABC {}
class UniquePrefixAaBbCc {}
final a = UniquePrefixab^
''';

await verifyCompletions(
mainFileUri,
content,
expectCompletions: [
// Constructors should all come before the class names, as they have
// higher relevance in this position.
'UniquePrefixABC()',
'UniquePrefixAaBbCc()',
'UniquePrefixABC',
'UniquePrefixAaBbCc',
],
);
}

Future<void> test_sort_truncatesByFuzzyScore() async {
final content = '''
class UniquePrefixABC {}
class UniquePrefixAaBbCc {}
final a = UniquePrefixab^
''';

// Enable truncation after 2 items so we can verify which
// items were dropped.
await provideConfig(initialize, {'maxCompletionItems': 2});
await verifyCompletions(
mainFileUri,
content,
expectNoAdditionalItems: true,
expectCompletions: [
// Although constructors are more relevant, when truncating we will use
// fuzzy score, so the closer matches are kept instead and we'll get
// constructor+class from the closer match.
'UniquePrefixABC()',
'UniquePrefixABC',
],
);
}

Future<void> test_unimportedSymbols() async {
newFile(
join(projectFolderPath, 'other_file.dart'),
Expand Down
4 changes: 4 additions & 0 deletions pkg/analyzer/lib/src/dart/element/inheritance_manager3.dart
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,10 @@ class InheritanceManager3 {
/// from the superclasses and mixins.
Map<Name, ExecutableElement> getInheritedConcreteMap2(
InterfaceElement element) {
if (element is ExtensionTypeElement) {
return const {};
}

var interface = getInterface(element);
return interface.superImplemented.last;
}
Expand Down
13 changes: 3 additions & 10 deletions pkg/analyzer/lib/src/summary2/linked_element_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -194,18 +194,11 @@ class LinkedElementFactory {
return createLibraryElementForReading(uri);
}

// Should be `@method`, `@constructor`, etc.
// If a duplicates container, skip it up.
var containerInClass = reference.parent!;
if (containerInClass.name == '@def') {
containerInClass = containerInClass.parent!.parent!;
}
final parentRef = reference.parentNotContainer;
final parentElement = elementOfReference(parentRef);

// Only classes delay creating children.
final classRef = containerInClass.parent!;
final parentElement = elementOfReference(classRef);

if (parentElement is InstanceElementImpl) {
if (parentElement is ClassElementImpl) {
parentElement.linkedData?.readMembers(parentElement);
}

Expand Down
Loading

0 comments on commit 078162e

Please sign in to comment.