completion driver suggestions

The absolute minimum to see suggestions wired through.

Lots of work to flesh out the client-implemented algorithms.

For reference:

* https://github.com/Dart-Code/Dart-Code/blob/master/src/extension/providers/dart_completion_item_provider.ts
* https://github.com/JetBrains/intellij-plugins/blob/master/Dart/src/com/jetbrains/lang/dart/ide/completion/DartServerCompletionContributor.java

Change-Id: Idc78fca787b095e5b6236481b89ddb21c0ed9c94
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/134877
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
pq 2020-02-10 17:07:29 +00:00 committed by Phil Quitslund
parent 160614929a
commit 3d97d22fe5
3 changed files with 99 additions and 10 deletions

View file

@ -2,30 +2,28 @@
// 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 'package:analyzer/src/test_utilities/resource_provider_mixin.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart';
import 'package:meta/meta.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'package:analyzer/src/test_utilities/resource_provider_mixin.dart';
import '../services/completion/dart/completion_contributor_util.dart';
import 'impl/completion_driver.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(BasicCompletionTest);
defineReflectiveTests(CompletionWithSuggestionsTest);
});
}
abstract class AbstractCompletionDriverTest with ResourceProviderMixin {
CompletionDriver driver;
Map<String, String> packageRoots;
List<CompletionSuggestion> suggestions;
bool get supportsAvailableDeclarations;
bool get supportsAvailableSuggestions;
String addTestFile(String content, {int offset}) =>
driver.addTestFile(content, offset: offset);
@ -38,7 +36,7 @@ abstract class AbstractCompletionDriverTest with ResourceProviderMixin {
@mustCallSuper
void setUp() {
driver = CompletionDriver(
supportsAvailableDeclarations: supportsAvailableDeclarations,
supportsAvailableSuggestions: supportsAvailableSuggestions,
resourceProvider: resourceProvider);
driver.createProject(packageRoots: packageRoots);
}
@ -74,9 +72,9 @@ abstract class AbstractCompletionDriverTest with ResourceProviderMixin {
@reflectiveTest
class BasicCompletionTest extends AbstractCompletionDriverTest {
@override
bool get supportsAvailableDeclarations => false;
bool get supportsAvailableSuggestions => false;
/// Duplicates (and potentially replaces DeprecatedMemberRelevanceTest.
/// Duplicates (and potentially replaces DeprecatedMemberRelevanceTest).
Future<void> test_deprecated_member_relevance() async {
addTestFile('''
class A {
@ -105,3 +103,26 @@ void main() {
.relevance));
}
}
@reflectiveTest
class CompletionWithSuggestionsTest extends AbstractCompletionDriverTest {
@override
bool get supportsAvailableSuggestions => true;
Future<void> test_sanity() async {
addTestFile('''
void main() {
^
}
''');
await getSuggestions();
// todo (pq): replace with a "real test"; this just proves we're getting end to end.
expect(
// from dart:math
suggestionWith(
completion: 'tan', kind: CompletionSuggestionKind.INVOCATION),
isNotNull);
}
}

View file

@ -9,6 +9,7 @@ import 'package:analysis_server/protocol/protocol_generated.dart'
hide AnalysisOptions;
import 'package:analysis_server/src/analysis_server.dart';
import 'package:analysis_server/src/domain_analysis.dart';
import 'package:analysis_server/src/domain_completion.dart';
import 'package:analysis_server/src/utilities/mocks.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/instrumentation/instrumentation.dart';
@ -52,6 +53,9 @@ abstract class AbstractClient {
AnalysisDomainHandler get analysisHandler => server.handlers
.singleWhere((handler) => handler is AnalysisDomainHandler);
CompletionDomainHandler get completionHandler =>
server.handlers.whereType<CompletionDomainHandler>().single;
AnalysisOptions get analysisOptions => testDiver.analysisOptions;
ResourceProvider get resourceProvider;
@ -105,6 +109,7 @@ abstract class AbstractClient {
}
/// Create a project at [projectPath].
@mustCallSuper
void createProject({Map<String, String> packageRoots}) {
newFolder(projectPath);
var request = AnalysisSetAnalysisRootsParams([projectPath], [],

View file

@ -18,8 +18,23 @@ import '../../constants.dart';
import 'abstract_client.dart';
import 'expect_mixin.dart';
CompletionSuggestion _createCompletionSuggestionFromAvailableSuggestion(
AvailableSuggestion suggestion) {
// todo (pq): IMPLEMENT
// com.jetbrains.lang.dart.ide.completion.DartServerCompletionContributor#createCompletionSuggestionFromAvailableSuggestion
return CompletionSuggestion(
// todo (pq): in IDEA, this is "UNKNOWN" but here we need a value; figure out what's up.
CompletionSuggestionKind.INVOCATION,
0,
suggestion.label,
0,
0,
suggestion.element.isDeprecated,
false);
}
class CompletionDriver extends AbstractClient with ExpectMixin {
final bool supportsAvailableDeclarations;
final bool supportsAvailableSuggestions;
final MemoryResourceProvider _resourceProvider;
Map<String, Completer<void>> receivedSuggestionsCompleters = {};
@ -27,13 +42,18 @@ class CompletionDriver extends AbstractClient with ExpectMixin {
bool suggestionsDone = false;
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 = {};
String completionId;
int completionOffset;
int replacementOffset;
int replacementLength;
CompletionDriver({
@required this.supportsAvailableDeclarations,
@required this.supportsAvailableSuggestions,
@required MemoryResourceProvider resourceProvider,
}) : _resourceProvider = resourceProvider,
super(
@ -60,6 +80,16 @@ class CompletionDriver extends AbstractClient with ExpectMixin {
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();
@ -71,6 +101,20 @@ class CompletionDriver extends AbstractClient with ExpectMixin {
assertValidId(completionId);
await _getResultsCompleter(completionId).future;
expect(suggestionsDone, isTrue);
if (supportsAvailableSuggestions) {
// todo(pq): limit set(s)
for (var suggestionSet in idToSetMap.values) {
for (var suggestion in suggestionSet.items) {
var completionSuggestion =
_createCompletionSuggestionFromAvailableSuggestion(suggestion
//, includedSet.getRelevance(), includedRelevanceTags
);
suggestions.add(completionSuggestion);
}
}
}
return suggestions;
}
@ -82,6 +126,7 @@ class CompletionDriver extends AbstractClient with ExpectMixin {
Folder newFolder(String path) => resourceProvider.newFolder(path);
@override
@mustCallSuper
Future<void> processNotification(Notification notification) async {
if (notification.event == COMPLETION_RESULTS) {
var params = CompletionResultsParams.fromNotification(notification);
@ -95,6 +140,24 @@ class CompletionDriver extends AbstractClient with ExpectMixin {
expect(allSuggestions.containsKey(id), isFalse);
allSuggestions[id] = params.results;
_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 == SERVER_NOTIFICATION_ERROR) {
throw Exception('server error: ${notification.toJson()}');
}