blob: 60083d604d19e5b6ea2fcd5bb268f10bec8cb0f7 [file] [log] [blame]
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:math' as math;
import 'package:analysis_server/protocol/protocol.dart';
import 'package:analysis_server/protocol/protocol_constants.dart';
import 'package:analysis_server/protocol/protocol_generated.dart'
hide AnalysisOptions;
import 'package:analysis_server/src/analysis_server.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/memory_file_system.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart';
import 'package:matcher/matcher.dart';
import 'package:meta/meta.dart';
import '../../constants.dart';
import 'abstract_client.dart';
import 'expect_mixin.dart';
CompletionSuggestion _createCompletionSuggestionFromAvailableSuggestion(
AvailableSuggestion suggestion,
int suggestionSetRelevance,
Map<String, IncludedSuggestionRelevanceTag>
includedSuggestionRelevanceTags) {
// https://github.com/JetBrains/intellij-plugins/blob/59018828753973324ea0500fa4bae93563f1aacf/Dart/src/com/jetbrains/lang/dart/ide/completion/DartServerCompletionContributor.java#L568
// https://github.com/Dart-Code/Dart-Code/blob/d4e98d2ca2636be5da7334760d73face12414e70/src/extension/providers/dart_completion_item_provider.ts#L187
var relevanceBoost = 0;
var relevanceTags = suggestion.relevanceTags;
if (relevanceTags != null) {
for (var tag in relevanceTags) {
var relevanceTag = includedSuggestionRelevanceTags[tag];
if (relevanceTag != null) {
relevanceBoost = math.max(relevanceBoost, relevanceTag.relevanceBoost);
}
}
}
return CompletionSuggestion(
// todo (pq): in IDEA, this is "UNKNOWN" but here we need a value; figure out what's up.
CompletionSuggestionKind.INVOCATION,
suggestionSetRelevance + relevanceBoost,
suggestion.label,
0,
0,
suggestion.element.isDeprecated,
false,
element: suggestion.element,
returnType: suggestion.element.returnType,
defaultArgumentListString: suggestion.defaultArgumentListString,
defaultArgumentListTextRanges: suggestion.defaultArgumentListTextRanges,
parameterNames: suggestion.parameterNames,
);
}
class CompletionDriver extends AbstractClient with ExpectMixin {
final bool supportsAvailableSuggestions;
final MemoryResourceProvider _resourceProvider;
Map<String, Completer<void>> receivedSuggestionsCompleters = {};
List<CompletionSuggestion> suggestions = [];
Map<String, List<CompletionSuggestion>> allSuggestions = {};
final Map<int, AvailableSuggestionSet> idToSetMap = {};
final Map<String, AvailableSuggestionSet> uriToSetMap = {};
final Map<String, CompletionResultsParams> idToSuggestions = {};
final Map<String, ExistingImports> fileToExistingImports = {};
final Map<String, List<AnalysisError>> filesErrors = {};
late String completionId;
late int completionOffset;
late int replacementOffset;
late int replacementLength;
CompletionDriver({
required this.supportsAvailableSuggestions,
AnalysisServerOptions? serverOptions,
required MemoryResourceProvider resourceProvider,
required String projectPath,
required String testFilePath,
}) : _resourceProvider = resourceProvider,
super(
serverOptions: serverOptions ?? AnalysisServerOptions(),
projectPath: resourceProvider.convertPath(projectPath),
testFilePath: resourceProvider.convertPath(testFilePath),
sdkPath: resourceProvider.convertPath('/sdk'));
@override
MemoryResourceProvider get resourceProvider => _resourceProvider;
@override
String addTestFile(String content, {int? offset}) {
completionOffset = content.indexOf('^');
if (offset != null) {
expect(completionOffset, -1, reason: 'cannot supply offset and ^');
completionOffset = offset;
return super.addTestFile(content);
}
expect(completionOffset, isNot(equals(-1)), reason: 'missing ^');
var nextOffset = content.indexOf('^', completionOffset + 1);
expect(nextOffset, equals(-1), reason: 'too many ^');
return super.addTestFile(content.substring(0, completionOffset) +
content.substring(completionOffset + 1));
}
@override
void createProject({Map<String, String>? packageRoots}) {
super.createProject(packageRoots: packageRoots);
if (supportsAvailableSuggestions) {
var request = CompletionSetSubscriptionsParams(
[CompletionService.AVAILABLE_SUGGESTION_SETS]).toRequest('0');
handleSuccessfulRequest(request, handler: completionHandler);
}
}
Future<List<CompletionSuggestion>> getSuggestions() async {
await waitForTasksFinished();
var request = CompletionGetSuggestionsParams(testFilePath, completionOffset)
.toRequest('0');
var response = await waitResponse(request);
var result = CompletionGetSuggestionsResult.fromResponse(response);
completionId = result.id;
assertValidId(completionId);
await _getResultsCompleter(completionId).future;
return suggestions;
}
@override
File newFile(String path, String content, [int? stamp]) => resourceProvider
.newFile(resourceProvider.convertPath(path), content, stamp);
@override
Folder newFolder(String path) =>
resourceProvider.newFolder(resourceProvider.convertPath(path));
@override
@mustCallSuper
Future<void> processNotification(Notification notification) async {
if (notification.event == COMPLETION_RESULTS) {
var params = CompletionResultsParams.fromNotification(notification);
var id = params.id;
assertValidId(id);
idToSuggestions[id] = params;
replacementOffset = params.replacementOffset;
replacementLength = params.replacementLength;
suggestions = params.results;
expect(allSuggestions.containsKey(id), isFalse);
allSuggestions[id] = params.results;
var includedKinds = params.includedElementKinds;
//
// Collect relevance information.
//
// https://github.com/JetBrains/intellij-plugins/blob/59018828753973324ea0500fa4bae93563f1aacf/Dart/src/com/jetbrains/lang/dart/analyzer/DartAnalysisServerService.java#L467
var includedRelevanceTags = <String, IncludedSuggestionRelevanceTag>{};
var includedSuggestionRelevanceTags =
params.includedSuggestionRelevanceTags;
if (includedSuggestionRelevanceTags != null) {
for (var includedRelevanceTag in includedSuggestionRelevanceTags) {
includedRelevanceTags[includedRelevanceTag.tag] =
includedRelevanceTag;
}
}
//
// Identify imported libraries.
//
var importedLibraryUris = <String>{};
var existingImports = fileToExistingImports[params.libraryFile];
if (existingImports != null) {
for (var existingImport in existingImports.imports) {
var uri = existingImports.elements.strings[existingImport.uri];
importedLibraryUris.add(uri);
}
}
//
// Partition included suggestion sets into imported and not-imported groups.
//
var importedSets = <IncludedSuggestionSet>[];
var notImportedSets = <IncludedSuggestionSet>[];
for (var set in params.includedSuggestionSets!) {
var id = set.id;
while (!idToSetMap.containsKey(id)) {
await Future.delayed(const Duration(milliseconds: 1));
}
var suggestionSet = idToSetMap[id]!;
if (importedLibraryUris.contains(suggestionSet.uri)) {
importedSets.add(set);
} else {
notImportedSets.add(set);
}
}
//
// Add suggestions.
//
// First from imported then from not-imported sets.
//
void addSuggestion(
AvailableSuggestion suggestion, IncludedSuggestionSet includeSet) {
var kind = suggestion.element.kind;
if (!includedKinds!.contains(kind)) {
return;
}
var completionSuggestion =
_createCompletionSuggestionFromAvailableSuggestion(
suggestion, includeSet.relevance, includedRelevanceTags);
suggestions.add(completionSuggestion);
}
// Track seen elements to ensure they are not duplicated.
var seenElements = <String>{};
// Suggestions can be uniquely identified by kind, label and uri.
String suggestionId(AvailableSuggestion s) =>
'${s.declaringLibraryUri}:${s.element.kind}:${s.label}';
for (var includeSet in importedSets) {
var set = idToSetMap[includeSet.id]!;
for (var suggestion in set.items) {
if (seenElements.add(suggestionId(suggestion))) {
addSuggestion(suggestion, includeSet);
}
}
}
for (var includeSet in notImportedSets) {
var set = idToSetMap[includeSet.id]!;
for (var suggestion in set.items) {
if (!seenElements.contains(suggestionId(suggestion))) {
addSuggestion(suggestion, includeSet);
}
}
}
_getResultsCompleter(id).complete(null);
} else if (notification.event ==
COMPLETION_NOTIFICATION_AVAILABLE_SUGGESTIONS) {
var params = CompletionAvailableSuggestionsParams.fromNotification(
notification,
);
for (var set in params.changedLibraries!) {
idToSetMap[set.id] = set;
uriToSetMap[set.uri] = set;
}
for (var id in params.removedLibraries!) {
var set = idToSetMap.remove(id);
uriToSetMap.remove(set?.uri);
}
} else if (notification.event == COMPLETION_NOTIFICATION_EXISTING_IMPORTS) {
var params = CompletionExistingImportsParams.fromNotification(
notification,
);
fileToExistingImports[params.file] = params.imports;
} else if (notification.event == ANALYSIS_NOTIFICATION_ERRORS) {
var decoded = AnalysisErrorsParams.fromNotification(notification);
filesErrors[decoded.file] = decoded.errors;
} else if (notification.event == SERVER_NOTIFICATION_ERROR) {
throw Exception('server error: ${notification.toJson()}');
} else if (notification.event == SERVER_NOTIFICATION_CONNECTED) {
// Ignored.
} else {
print('Unhandled notififcation: ${notification.event}');
}
}
Future<AvailableSuggestionSet> waitForSetWithUri(String uri) async {
while (true) {
var result = uriToSetMap[uri];
if (result != null) {
return result;
}
await Future.delayed(const Duration(milliseconds: 1));
}
}
Completer<void> _getResultsCompleter(String id) =>
receivedSuggestionsCompleters.putIfAbsent(id, () => Completer<void>());
}