Version 2.18.0-81.0.dev
Merge commit '519a83dd29924c2c127b6e40e32629589c57b991' into 'dev'
diff --git a/pkg/analysis_server/lib/src/lsp/client_configuration.dart b/pkg/analysis_server/lib/src/lsp/client_configuration.dart
index a5ef8f7..d31fd8a 100644
--- a/pkg/analysis_server/lib/src/lsp/client_configuration.dart
+++ b/pkg/analysis_server/lib/src/lsp/client_configuration.dart
@@ -195,10 +195,13 @@
int? get lineLength =>
_settings['lineLength'] as int? ?? _fallback?.lineLength;
- /// Maximum number of CompletionItems per completion request.
+ /// Requested maximum number of CompletionItems per completion request.
///
- /// If more than this are available, the list is truncated and isIncomplete
- /// is set to true.
+ /// If more than this are available, ranked items in the list will be
+ /// truncated and `isIncomplete` is set to `true`.
+ ///
+ /// Unranked items are never truncated so it's still possible that more than
+ /// this number of items will be returned.
int get maxCompletionItems =>
_settings['maxCompletionItems'] as int? ??
_fallback?.maxCompletionItems ??
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart
index 8f93e61..ac13d91 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart
@@ -99,7 +99,7 @@
}
final offset = offsetResult.result;
- Future<ErrorOr<CompletionList>>? serverResultsFuture;
+ Future<ErrorOr<_CompletionResults>>? serverResultsFuture;
final pathContext = server.resourceProvider.pathContext;
final fileExtension = pathContext.extension(path.result);
@@ -158,28 +158,31 @@
}
serverResultsFuture ??=
- Future.value(success(CompletionList(isIncomplete: false, items: [])));
+ Future.value(success(_CompletionResults(isIncomplete: false)));
final pluginResultsFuture = _getPluginResults(
clientCapabilities, lineInfo.result, path.result, offset);
- // Await both server + plugin results together to allow async/IO to
- // overlap.
- final serverAndPluginResults =
- await Future.wait([serverResultsFuture, pluginResultsFuture]);
- final serverResults = serverAndPluginResults[0];
- final pluginResults = serverAndPluginResults[1];
+ final serverResults = await serverResultsFuture;
+ final pluginResults = await pluginResultsFuture;
- if (serverResults.isError) return serverResults;
- if (pluginResults.isError) return pluginResults;
+ if (serverResults.isError) return failure(serverResults);
+ if (pluginResults.isError) return failure(pluginResults);
- final untruncatedItems = serverResults.result.items
+ final untruncatedRankedItems = serverResults.result.rankedItems
.followedBy(pluginResults.result.items)
.toList();
+ final unrankedItems = serverResults.result.unrankedItems;
- final truncatedItems = untruncatedItems.length > maxResults
- ? (untruncatedItems..sort(sortTextComparer)).sublist(0, maxResults)
- : untruncatedItems;
+ // Truncate ranked items to allow room for all unranked items.
+ final maxRankedItems = math.max(maxResults - unrankedItems.length, 0);
+ final truncatedRankedItems = untruncatedRankedItems.length > maxRankedItems
+ ? (untruncatedRankedItems..sort(sortTextComparer))
+ .sublist(0, maxRankedItems)
+ : untruncatedRankedItems;
+
+ final truncatedItems =
+ truncatedRankedItems.followedBy(unrankedItems).toList();
// If we're tracing performance (only Dart), record the number of results
// after truncation.
@@ -190,7 +193,7 @@
// marked as such.
isIncomplete: serverResults.result.isIncomplete ||
pluginResults.result.isIncomplete ||
- truncatedItems.length != untruncatedItems.length,
+ truncatedRankedItems.length != untruncatedRankedItems.length,
items: truncatedItems,
));
}
@@ -288,7 +291,7 @@
));
}
- Future<ErrorOr<CompletionList>> _getServerDartItems(
+ Future<ErrorOr<_CompletionResults>> _getServerDartItems(
LspClientCapabilities capabilities,
ResolvedUnitResult unit,
CompletionPerformance completionPerformance,
@@ -310,7 +313,7 @@
if (triggerCharacter != null) {
if (!_triggerCharacterValid(offset, triggerCharacter, target)) {
- return success(CompletionList(isIncomplete: false, items: []));
+ return success(_CompletionResults(isIncomplete: false));
}
}
@@ -364,7 +367,7 @@
? false
: server.clientConfiguration.global.completeFunctionCalls;
- final results = performance.run('mapSuggestions', (performance) {
+ final rankedResults = performance.run('mapSuggestions', (performance) {
return serverSuggestions.map(
(item) {
var itemReplacementOffset =
@@ -510,7 +513,7 @@
.clientConfiguration.global.previewCommitCharacters,
completeFunctionCalls: completeFunctionCalls,
));
- results.addAll(setResults);
+ rankedResults.addAll(setResults);
}
});
}
@@ -522,44 +525,60 @@
// the root, so skip snippets entirely if not.
final isEditableFile =
unit.session.analysisContext.contextRoot.isAnalyzed(unit.path);
+ List<CompletionItem> unrankedResults;
if (capabilities.completionSnippets &&
snippetsEnabled &&
isEditableFile) {
- await performance.runAsync('addSnippets', (performance) async {
- results.addAll(await _getDartSnippetItems(
+ unrankedResults =
+ await performance.runAsync('getSnippets', (performance) async {
+ // `await` required for `performance.runAsync` to count time.
+ return await _getDartSnippetItems(
clientCapabilities: capabilities,
unit: unit,
offset: offset,
lineInfo: unit.lineInfo,
- ));
+ );
});
+ } else {
+ unrankedResults = [];
}
// Perform fuzzy matching based on the identifier in front of the caret to
// reduce the size of the payload.
- final matchingResults = performance.run('fuzzyFilter', (performance) {
- final fuzzyPattern = completionRequest.targetPrefix;
- final fuzzyMatcher =
- FuzzyMatcher(fuzzyPattern, matchStyle: MatchStyle.TEXT);
+ final fuzzyPattern = completionRequest.targetPrefix;
+ final fuzzyMatcher =
+ FuzzyMatcher(fuzzyPattern, matchStyle: MatchStyle.TEXT);
- return results
+ final matchingRankedResults =
+ performance.run('fuzzyFilterRanked', (performance) {
+ return rankedResults
.where((e) => fuzzyMatcher.score(e.filterText ?? e.label) > 0)
.toList();
});
- // Transmitted count will be set after combining with plugins.
- completionPerformance.computedSuggestionCount = matchingResults.length;
+ final matchingUnrankedResults =
+ performance.run('fuzzyFilterRanked', (performance) {
+ return unrankedResults
+ .where((e) => fuzzyMatcher.score(e.filterText ?? e.label) > 0)
+ .toList();
+ });
- return success(
- CompletionList(isIncomplete: false, items: matchingResults));
+ // transmittedCount will be set after combining with plugins + truncation.
+ completionPerformance.computedSuggestionCount =
+ matchingRankedResults.length + matchingUnrankedResults.length;
+
+ return success(_CompletionResults(
+ isIncomplete: false,
+ rankedItems: matchingRankedResults,
+ unrankedItems: matchingUnrankedResults));
} on AbortCompletion {
- return success(CompletionList(isIncomplete: false, items: []));
+ return success(_CompletionResults(isIncomplete: false));
} on InconsistentAnalysisException {
- return success(CompletionList(isIncomplete: false, items: []));
+ return success(_CompletionResults(isIncomplete: false));
}
}
- Future<ErrorOr<CompletionList>> _getServerYamlItems(
+ Future<ErrorOr<_CompletionResults>> _getServerYamlItems(
YamlCompletionGenerator generator,
LspClientCapabilities capabilities,
String path,
@@ -578,7 +597,15 @@
final insertionRange =
toRange(lineInfo, suggestions.replacementOffset, insertLength);
+ // Perform fuzzy matching based on the identifier in front of the caret to
+ // reduce the size of the payload.
+ final fuzzyPattern = suggestions.targetPrefix;
+ final fuzzyMatcher =
+ FuzzyMatcher(fuzzyPattern, matchStyle: MatchStyle.TEXT);
+
final completionItems = suggestions.suggestions
+ .where((item) =>
+ fuzzyMatcher.score(item.displayText ?? item.completion) > 0)
.map(
(item) => toCompletionItem(
capabilities,
@@ -600,7 +627,8 @@
),
)
.toList();
- return success(CompletionList(isIncomplete: false, items: completionItems));
+ return success(_CompletionResults(
+ isIncomplete: false, unrankedItems: completionItems));
}
/// Returns true if [node] is part of an invocation and already has an argument
@@ -726,3 +754,20 @@
return item1Text.compareTo(item2Text);
}
}
+
+/// 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 cannot be ranked, and should avoid being truncated.
+ final List<CompletionItem> unrankedItems;
+
+ final bool isIncomplete;
+
+ _CompletionResults({
+ this.rankedItems = const [],
+ this.unrankedItems = const [],
+ required this.isIncomplete,
+ });
+}
diff --git a/pkg/analysis_server/lib/src/services/completion/yaml/yaml_completion_generator.dart b/pkg/analysis_server/lib/src/services/completion/yaml/yaml_completion_generator.dart
index 53c3de6..ec3e06f 100644
--- a/pkg/analysis_server/lib/src/services/completion/yaml/yaml_completion_generator.dart
+++ b/pkg/analysis_server/lib/src/services/completion/yaml/yaml_completion_generator.dart
@@ -84,17 +84,23 @@
}
}
final node = nodePath.isNotEmpty ? nodePath.last : null;
+ String targetPrefix;
int replacementOffset;
int replacementLength;
if (node is YamlScalar && node.containsOffset(offset)) {
+ targetPrefix = node.span.text.substring(
+ 0,
+ offset - node.span.start.offset,
+ );
replacementOffset = node.span.start.offset;
replacementLength = node.span.length;
} else {
+ targetPrefix = '';
replacementOffset = offset;
replacementLength = 0;
}
return YamlCompletionResults(
- suggestions, replacementOffset, replacementLength);
+ suggestions, targetPrefix, replacementOffset, replacementLength);
}
/// Return the result of parsing the file [content] into a YAML node.
@@ -196,14 +202,16 @@
class YamlCompletionResults {
final List<CompletionSuggestion> suggestions;
+ final String targetPrefix;
final int replacementOffset;
final int replacementLength;
- const YamlCompletionResults(
- this.suggestions, this.replacementOffset, this.replacementLength);
+ const YamlCompletionResults(this.suggestions, this.targetPrefix,
+ this.replacementOffset, this.replacementLength);
const YamlCompletionResults.empty()
: suggestions = const [],
+ targetPrefix = '',
replacementOffset = 0,
replacementLength = 0;
}
diff --git a/pkg/analysis_server/test/lsp/completion_dart_test.dart b/pkg/analysis_server/test/lsp/completion_dart_test.dart
index 3a55f69..9b2f453 100644
--- a/pkg/analysis_server/test/lsp/completion_dart_test.dart
+++ b/pkg/analysis_server/test/lsp/completion_dart_test.dart
@@ -1117,6 +1117,54 @@
expect(res.items.map((item) => item.label).contains('aaa'), isTrue);
}
+ /// Snippet completions should be kept when maxCompletionItems truncates
+ /// because they are not ranked like other completions and might be
+ /// truncated when they are exactly what the user wants.
+ Future<void> test_maxCompletionItems_doesNotExcludeSnippets() async {
+ final content = '''
+import 'a.dart';
+void f() {
+ fo^
+}
+ ''';
+
+ // Create a class with fields for1 to for20 in the other file.
+ newFile(
+ join(projectFolderPath, 'lib', 'a.dart'),
+ [
+ for (var i = 1; i <= 20; i++) ' String for$i = ' ';',
+ ].join('\n'),
+ );
+
+ final initialAnalysis = waitForAnalysisComplete();
+ await provideConfig(
+ () => initialize(
+ textDocumentCapabilities: withCompletionItemSnippetSupport(
+ emptyTextDocumentClientCapabilities),
+ workspaceCapabilities: withApplyEditSupport(
+ withConfigurationSupport(emptyWorkspaceClientCapabilities))),
+ {'maxCompletionItems': 10},
+ );
+ await openFile(mainFileUri, withoutMarkers(content));
+ await initialAnalysis;
+ final res =
+ await getCompletionList(mainFileUri, positionFromMarker(content));
+
+ // Should be capped at 10 and marked as incomplete.
+ expect(res.items, hasLength(10));
+ expect(res.isIncomplete, isTrue);
+
+ // Also ensure the 'for' snippet is included.
+
+ expect(
+ res.items
+ .where((item) => item.kind == CompletionItemKind.Snippet)
+ .map((item) => item.label)
+ .contains('for'),
+ isTrue,
+ );
+ }
+
Future<void> test_namedArg_flutterChildren() async {
final content = '''
import 'package:flutter/widgets.dart';
diff --git a/pkg/analysis_server/test/lsp/completion_yaml_test.dart b/pkg/analysis_server/test/lsp/completion_yaml_test.dart
index cee886f..9d8ff8a 100644
--- a/pkg/analysis_server/test/lsp/completion_yaml_test.dart
+++ b/pkg/analysis_server/test/lsp/completion_yaml_test.dart
@@ -295,7 +295,7 @@
await verifyCompletions(
pubspecFileUri,
content,
- expectCompletions: ['flutter: ', 'sdk: '],
+ expectCompletions: ['sdk: '],
applyEditsFor: 'sdk: ',
expectedContent: expected,
);
@@ -576,6 +576,32 @@
expect(completionResults, isEmpty);
}
+ Future<void> test_prefixFilter() async {
+ httpClient.sendHandler = (BaseRequest request) async {
+ if (request.url.toString().endsWith(PubApi.packageNameListPath)) {
+ return Response(samplePackageList, 200);
+ } else {
+ throw UnimplementedError();
+ }
+ };
+
+ final content = '''
+name: foo
+version: 1.0.0
+
+dependencies:
+ on^''';
+
+ await initialize();
+ await openFile(pubspecFileUri, content);
+ await pumpEventQueue();
+
+ completionResults =
+ await getCompletion(pubspecFileUri, positionFromMarker(content));
+ expect(completionResults.length, equals(1));
+ expect(completionResults.single.label, equals('one: '));
+ }
+
Future<void> test_topLevel() async {
final content = '''
version: 1.0.0
@@ -602,7 +628,7 @@
await verifyCompletions(
pubspecFileUri,
content,
- expectCompletions: ['name: ', 'description: '],
+ expectCompletions: ['name: '],
applyEditsFor: 'name: ',
expectedContent: expected,
);
diff --git a/tools/VERSION b/tools/VERSION
index cb98d08..20af0ab 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
MAJOR 2
MINOR 18
PATCH 0
-PRERELEASE 80
+PRERELEASE 81
PRERELEASE_PATCH 0
\ No newline at end of file