// 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 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> receivedSuggestionsCompleters = {}; List suggestions = []; Map> allSuggestions = {}; final Map idToSetMap = {}; final Map uriToSetMap = {}; final Map idToSuggestions = {}; final Map fileToExistingImports = {}; final Map> 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? packageRoots}) { super.createProject(packageRoots: packageRoots); if (supportsAvailableSuggestions) { var request = CompletionSetSubscriptionsParams( [CompletionService.AVAILABLE_SUGGESTION_SETS]).toRequest('0'); handleSuccessfulRequest(request, handler: completionHandler); } } Future> 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 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 = {}; var includedSuggestionRelevanceTags = params.includedSuggestionRelevanceTags; if (includedSuggestionRelevanceTags != null) { for (var includedRelevanceTag in includedSuggestionRelevanceTags) { includedRelevanceTags[includedRelevanceTag.tag] = includedRelevanceTag; } } // // Identify imported libraries. // var importedLibraryUris = {}; 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 = []; var notImportedSets = []; 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 = {}; // 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 waitForSetWithUri(String uri) async { while (true) { var result = uriToSetMap[uri]; if (result != null) { return result; } await Future.delayed(const Duration(milliseconds: 1)); } } Completer _getResultsCompleter(String id) => receivedSuggestionsCompleters.putIfAbsent(id, () => Completer()); }