Add CiderCompletionComputer.compute2(), tests.
R=brianwilkerson@google.com, keertip@google.com
Change-Id: Ie6acad3873f78060cde85a350ebd48b854ea1314
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/144345
Reviewed-by: Keerti Parthasarathy <keertip@google.com>
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
diff --git a/pkg/analysis_server/lib/src/cider/completion.dart b/pkg/analysis_server/lib/src/cider/completion.dart
index 0655cf8..2e32b66 100644
--- a/pkg/analysis_server/lib/src/cider/completion.dart
+++ b/pkg/analysis_server/lib/src/cider/completion.dart
@@ -10,10 +10,12 @@
import 'package:analysis_server/src/services/completion/dart/completion_manager.dart';
import 'package:analysis_server/src/services/completion/dart/local_library_contributor.dart';
import 'package:analyzer/dart/element/element.dart' show LibraryElement;
+import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/dart/analysis/performance_logger.dart';
import 'package:analyzer/src/dart/micro/resolve_file.dart';
import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart';
+import 'package:meta/meta.dart';
/// The cache that can be reuse for across multiple completion request.
///
@@ -29,12 +31,47 @@
final FileResolver _fileResolver;
DartCompletionRequestImpl _dartCompletionRequest;
+ final List<String> _computedImportedLibraries = [];
CiderCompletionComputer(this._logger, this._cache, this._fileResolver);
+ @deprecated
Future<List<CompletionSuggestion>> compute(String path, int offset) async {
+ var file = _fileResolver.resourceProvider.getFile(path);
+ var content = file.readAsStringSync();
+
+ var lineInfo = LineInfo.fromContent(content);
+ var location = lineInfo.getLocation(offset);
+
+ var result = await compute2(
+ path: path,
+ line: location.lineNumber - 1,
+ character: location.columnNumber - 1,
+ );
+
+ return result.suggestions;
+ }
+
+ /// Return completion suggestions for the file and position.
+ ///
+ /// The [path] must be the absolute and normalized path of the file.
+ ///
+ /// The content of the file has already been updated.
+ ///
+ /// The [line] and [character] are zero based.
+ Future<CiderCompletionResult> compute2({
+ @required String path,
+ @required int line,
+ @required int character,
+ }) async {
+ var file = _fileResolver.resourceProvider.getFile(path);
+ var content = file.readAsStringSync();
+
var resolvedUnit = _fileResolver.resolve(path);
+ var lineInfo = LineInfo.fromContent(content);
+ var offset = lineInfo.getOffsetOfLine(line) + character;
+
var completionRequest = CompletionRequestImpl(
resolvedUnit,
offset,
@@ -71,7 +108,10 @@
);
});
- return suggestions;
+ return CiderCompletionResult._(
+ suggestions,
+ _computedImportedLibraries,
+ );
}
/// Return suggestions from libraries imported into the [target].
@@ -99,6 +139,7 @@
var cacheEntry = _cache._importedLibraries[path];
if (cacheEntry == null || cacheEntry.signature != signature) {
+ _computedImportedLibraries.add(path);
var suggestions = _librarySuggestions(element);
cacheEntry = _CiderImportedLibrarySuggestions(
signature,
@@ -124,7 +165,16 @@
class CiderCompletionResult {
final List<CompletionSuggestion> suggestions;
- CiderCompletionResult(this.suggestions);
+ /// Paths of imported libraries for which suggestions were (re)computed
+ /// during processing of this request. Does not include libraries that were
+ /// processed during previous requests, and reused from the cache now.
+ @visibleForTesting
+ final List<String> computedImportedLibraries;
+
+ CiderCompletionResult._(
+ this.suggestions,
+ this.computedImportedLibraries,
+ );
}
class _CiderImportedLibrarySuggestions {
diff --git a/pkg/analysis_server/test/src/cider/cider_service.dart b/pkg/analysis_server/test/src/cider/cider_service.dart
new file mode 100644
index 0000000..e2c8bb1
--- /dev/null
+++ b/pkg/analysis_server/test/src/cider/cider_service.dart
@@ -0,0 +1,69 @@
+// 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:convert';
+
+import 'package:analyzer/src/dart/analysis/byte_store.dart';
+import 'package:analyzer/src/dart/analysis/performance_logger.dart';
+import 'package:analyzer/src/dart/micro/resolve_file.dart';
+import 'package:analyzer/src/test_utilities/mock_sdk.dart';
+import 'package:analyzer/src/test_utilities/resource_provider_mixin.dart';
+import 'package:analyzer/src/workspace/bazel.dart';
+import 'package:crypto/crypto.dart';
+import 'package:linter/src/rules.dart';
+
+class CiderServiceTest with ResourceProviderMixin {
+ static final String _testPath = '/workspace/dart/test/lib/test.dart';
+
+ final ByteStore byteStore = MemoryByteStore();
+
+ final StringBuffer logBuffer = StringBuffer();
+ PerformanceLog logger;
+ MockSdk sdk;
+
+ FileResolver fileResolver;
+
+ String get testPath => _testPath;
+
+ /// Create a new [FileResolver] into [fileResolver].
+ ///
+ /// We do this the first time, and to test reusing results from [byteStore].
+ void createFileResolver() {
+ var workspace = BazelWorkspace.find(
+ resourceProvider,
+ convertPath(_testPath),
+ );
+
+ fileResolver = FileResolver(
+ logger,
+ resourceProvider,
+ byteStore,
+ workspace.createSourceFactory(sdk, null),
+ (String path) => _getDigest(path),
+ null,
+ workspace: workspace,
+ );
+ fileResolver.testView = FileResolverTestView();
+ }
+
+ void setUp() {
+ registerLintRules();
+
+ logger = PerformanceLog(logBuffer);
+ sdk = MockSdk(resourceProvider: resourceProvider);
+
+ newFile('/workspace/WORKSPACE', content: '');
+ createFileResolver();
+ }
+
+ String _getDigest(String path) {
+ try {
+ var content = resourceProvider.getFile(path).readAsStringSync();
+ var contentBytes = utf8.encode(content);
+ return md5.convert(contentBytes).toString();
+ } catch (_) {
+ return '';
+ }
+ }
+}
diff --git a/pkg/analysis_server/test/src/cider/completion_test.dart b/pkg/analysis_server/test/src/cider/completion_test.dart
new file mode 100644
index 0000000..1d14888
--- /dev/null
+++ b/pkg/analysis_server/test/src/cider/completion_test.dart
@@ -0,0 +1,234 @@
+// 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 'package:analysis_server/src/cider/completion.dart';
+import 'package:analyzer/source/line_info.dart';
+import 'package:analyzer_plugin/protocol/protocol_common.dart'
+ show CompletionSuggestion, ElementKind;
+import 'package:meta/meta.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'cider_service.dart';
+
+void main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(CiderCompletionComputerTest);
+ });
+}
+
+@reflectiveTest
+class CiderCompletionComputerTest extends CiderServiceTest {
+ final CiderCompletionCache _completionCache = CiderCompletionCache();
+
+ CiderCompletionResult _completionResult;
+ List<CompletionSuggestion> _suggestions;
+
+ @override
+ void setUp() {
+ super.setUp();
+ }
+
+ Future<void> test_compute() async {
+ var context = _updateFile(r'''
+class A {}
+
+int a = 0;
+
+main(int b) {
+ int c = 0;
+ ^
+}
+''');
+
+ // ignore: deprecated_member_use_from_same_package
+ _suggestions = await _newComputer().compute(
+ testPath,
+ context.offset,
+ );
+
+ _assertHasCompletion(text: 'a');
+ _assertHasCompletion(text: 'b');
+ _assertHasCompletion(text: 'c');
+ _assertHasClass(text: 'A');
+ _assertHasClass(text: 'String');
+
+ _assertNoClass(text: 'Random');
+ }
+
+ Future<void> test_compute2() async {
+ await _compute2(r'''
+class A {}
+
+int a = 0;
+
+main(int b) {
+ int c = 0;
+ ^
+}
+''');
+
+ _assertHasCompletion(text: 'a');
+ _assertHasCompletion(text: 'b');
+ _assertHasCompletion(text: 'c');
+ _assertHasClass(text: 'A');
+ _assertHasClass(text: 'String');
+
+ _assertNoClass(text: 'Random');
+ }
+
+ Future<void> test_compute2_updateImportedLibrary() async {
+ var corePath = convertPath('/sdk/lib/core/core.dart');
+
+ var aPath = convertPath('/workspace/dart/test/lib/a.dart');
+ newFile(aPath, content: r'''
+class A {}
+''');
+
+ var content = r'''
+import 'a.dart';
+^
+''';
+
+ _createFileResolver();
+ await _compute2(content);
+ _assertComputedImportedLibraries([corePath, aPath]);
+ _assertHasClass(text: 'A');
+
+ // Repeat the query, still has 'A'.
+ _createFileResolver();
+ await _compute2(content);
+ _assertComputedImportedLibraries([]);
+ _assertHasClass(text: 'A');
+
+ // Update the imported library, has 'B', but not 'A'.
+ newFile(aPath, content: r'''
+class B {}
+''');
+ _createFileResolver();
+ await _compute2(content);
+ _assertComputedImportedLibraries([aPath]);
+ _assertHasClass(text: 'B');
+ _assertNoClass(text: 'A');
+ }
+
+ Future<void> test_compute2_updateImports() async {
+ var corePath = convertPath('/sdk/lib/core/core.dart');
+
+ var aPath = convertPath('/workspace/dart/test/lib/a.dart');
+ newFile(aPath, content: r'''
+class A {}
+''');
+
+ _createFileResolver();
+ await _compute2(r'''
+var a = ^;
+''');
+ _assertComputedImportedLibraries([corePath]);
+ _assertHasClass(text: 'String');
+
+ // Repeat the query, still has 'A'.
+ _createFileResolver();
+ await _compute2(r'''
+import 'a.dart';
+var a = ^;
+''');
+ _assertComputedImportedLibraries([aPath]);
+ _assertHasClass(text: 'A');
+ _assertHasClass(text: 'String');
+ }
+
+ void _assertComputedImportedLibraries(List<String> expected) {
+ expected = expected.map(convertPath).toList();
+ expect(
+ _completionResult.computedImportedLibraries,
+ unorderedEquals(expected),
+ );
+ }
+
+ void _assertHasClass({@required String text}) {
+ var matching = _matchingCompletions(
+ text: text,
+ elementKind: ElementKind.CLASS,
+ );
+ expect(matching, hasLength(1), reason: 'Expected exactly one completion');
+ }
+
+ void _assertHasCompletion({@required String text}) {
+ var matching = _matchingCompletions(text: text);
+ expect(matching, hasLength(1), reason: 'Expected exactly one completion');
+ }
+
+ void _assertNoClass({@required String text}) {
+ var matching = _matchingCompletions(
+ text: text,
+ elementKind: ElementKind.CLASS,
+ );
+ expect(matching, isEmpty, reason: 'Expected zero completions');
+ }
+
+ Future _compute2(String content) async {
+ var context = _updateFile(content);
+
+ _completionResult = await _newComputer().compute2(
+ path: testPath,
+ line: context.line,
+ character: context.character,
+ );
+ _suggestions = _completionResult.suggestions;
+ }
+
+ /// TODO(scheglov) Implement incremental updating
+ void _createFileResolver() {
+ createFileResolver();
+ }
+
+ List<CompletionSuggestion> _matchingCompletions({
+ @required String text,
+ ElementKind elementKind,
+ }) {
+ return _suggestions.where((e) {
+ if (e.completion != text) {
+ return false;
+ }
+
+ if (elementKind != null && e.element.kind != elementKind) {
+ return false;
+ }
+
+ return true;
+ }).toList();
+ }
+
+ CiderCompletionComputer _newComputer() {
+ return CiderCompletionComputer(logger, _completionCache, fileResolver);
+ }
+
+ _CompletionContext _updateFile(String content) {
+ newFile(testPath, content: content);
+
+ var offset = content.indexOf('^');
+ expect(offset, isPositive, reason: 'Expected to find ^');
+ expect(content.indexOf('^', offset + 1), -1, reason: 'Expected only one ^');
+
+ var lineInfo = LineInfo.fromContent(content);
+ var location = lineInfo.getLocation(offset);
+
+ return _CompletionContext(
+ content,
+ offset,
+ location.lineNumber - 1,
+ location.columnNumber - 1,
+ );
+ }
+}
+
+class _CompletionContext {
+ final String content;
+ final int offset;
+ final int line;
+ final int character;
+
+ _CompletionContext(this.content, this.offset, this.line, this.character);
+}
diff --git a/pkg/analysis_server/test/src/cider/test_all.dart b/pkg/analysis_server/test/src/cider/test_all.dart
new file mode 100644
index 0000000..dfae1ef
--- /dev/null
+++ b/pkg/analysis_server/test/src/cider/test_all.dart
@@ -0,0 +1,13 @@
+// Copyright (c) 2017, 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 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'completion_test.dart' as completion;
+
+void main() {
+ defineReflectiveSuite(() {
+ completion.main();
+ });
+}
diff --git a/pkg/analysis_server/test/src/test_all.dart b/pkg/analysis_server/test/src/test_all.dart
index 2f1638e..1681ecc 100644
--- a/pkg/analysis_server/test/src/test_all.dart
+++ b/pkg/analysis_server/test/src/test_all.dart
@@ -4,6 +4,7 @@
import 'package:test_reflective_loader/test_reflective_loader.dart';
+import 'cider/test_all.dart' as cider;
import 'computer/test_all.dart' as computer;
import 'domain_abstract_test.dart' as domain_abstract;
import 'domains/test_all.dart' as domains;
@@ -17,6 +18,7 @@
void main() {
defineReflectiveSuite(() {
+ cider.main();
computer.main();
domain_abstract.main();
domains.main();