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();