Implement completion.getSuggestionDetails2

Change-Id: Ic4b39c723d0cf90936a1db3ad3716533e6e8ca79
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/218542
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
diff --git a/pkg/analysis_server/lib/src/domain_completion.dart b/pkg/analysis_server/lib/src/domain_completion.dart
index 9936e74..b1b2b36 100644
--- a/pkg/analysis_server/lib/src/domain_completion.dart
+++ b/pkg/analysis_server/lib/src/domain_completion.dart
@@ -208,6 +208,63 @@
     );
   }
 
+  /// Process a `completion.getSuggestionDetails2` request.
+  void getSuggestionDetails2(Request request) async {
+    var params = CompletionGetSuggestionDetails2Params.fromRequest(request);
+
+    var file = params.file;
+    if (server.sendResponseErrorIfInvalidFilePath(request, file)) {
+      return;
+    }
+
+    var libraryUri = Uri.tryParse(params.libraryUri);
+    if (libraryUri == null) {
+      server.sendResponse(
+        Response.invalidParameter(request, 'libraryUri', 'Cannot parse'),
+      );
+      return;
+    }
+
+    var budget = CompletionBudget(
+      const Duration(milliseconds: 1000),
+    );
+    var id = ++_latestGetSuggestionDetailsId;
+    while (id == _latestGetSuggestionDetailsId && !budget.isEmpty) {
+      try {
+        var analysisDriver = server.getAnalysisDriver(file);
+        if (analysisDriver == null) {
+          server.sendResponse(Response.fileNotAnalyzed(request, file));
+          return;
+        }
+        var session = analysisDriver.currentSession;
+
+        var completion = params.completion;
+        var builder = ChangeBuilder(session: session);
+        await builder.addDartFileEdit(file, (builder) {
+          var result = builder.importLibraryElement(libraryUri);
+          if (result.prefix != null) {
+            completion = '${result.prefix}.$completion';
+          }
+        });
+
+        server.sendResponse(
+          CompletionGetSuggestionDetails2Result(
+            completion,
+            builder.sourceChange,
+          ).toResponse(request.id),
+        );
+        return;
+      } on InconsistentAnalysisException {
+        // Loop around to try again.
+      }
+    }
+
+    // Timeout or abort, send the empty response.
+    server.sendResponse(
+      CompletionGetSuggestionDetailsResult('').toResponse(request.id),
+    );
+  }
+
   /// Implement the 'completion.getSuggestions2' request.
   void getSuggestions2(Request request) async {
     var budget = CompletionBudget(budgetDuration);
@@ -321,6 +378,9 @@
       if (requestName == COMPLETION_REQUEST_GET_SUGGESTION_DETAILS) {
         getSuggestionDetails(request);
         return Response.DELAYED_RESPONSE;
+      } else if (requestName == COMPLETION_REQUEST_GET_SUGGESTION_DETAILS2) {
+        getSuggestionDetails2(request);
+        return Response.DELAYED_RESPONSE;
       } else if (requestName == COMPLETION_REQUEST_GET_SUGGESTIONS) {
         processRequest(request);
         return Response.DELAYED_RESPONSE;
diff --git a/pkg/analysis_server/test/domain_completion_test.dart b/pkg/analysis_server/test/domain_completion_test.dart
index 5021221..615fc09 100644
--- a/pkg/analysis_server/test/domain_completion_test.dart
+++ b/pkg/analysis_server/test/domain_completion_test.dart
@@ -30,71 +30,198 @@
 
 void main() {
   defineReflectiveSuite(() {
+    defineReflectiveTests(CompletionDomainHandlerGetSuggestionDetails2Test);
     defineReflectiveTests(CompletionDomainHandlerGetSuggestions2Test);
     defineReflectiveTests(CompletionDomainHandlerGetSuggestionsTest);
   });
 }
 
 @reflectiveTest
-class CompletionDomainHandlerGetSuggestions2Test with ResourceProviderMixin {
-  late final MockServerChannel serverChannel;
-  late final AnalysisServer server;
+class CompletionDomainHandlerGetSuggestionDetails2Test
+    extends PubPackageAnalysisServerTest {
+  Future<void> test_alreadyImported() async {
+    await _configureWithWorkspaceRoot();
 
-  AnalysisDomainHandler get analysisDomain {
-    return server.handlers.whereType<AnalysisDomainHandler>().single;
+    var validator = await _getTestCodeDetails('''
+import 'dart:math';
+void f() {
+  Rand^
+}
+''', completion: 'Random', libraryUri: 'dart:math');
+    validator
+      ..hasCompletion('Random')
+      ..hasChange().assertNoFileEdits();
   }
 
-  CompletionDomainHandler get completionDomain {
-    return server.handlers.whereType<CompletionDomainHandler>().single;
+  Future<void> test_import_dart() async {
+    await _configureWithWorkspaceRoot();
+
+    var validator = await _getTestCodeDetails('''
+void f() {
+  R^
+}
+''', completion: 'Random', libraryUri: 'dart:math');
+    validator
+      ..hasCompletion('Random')
+      ..hasChange()
+          .hasFileEdit(testFilePathPlatform)
+          .whenApplied(testFileContent, r'''
+import 'dart:math';
+
+void f() {
+  R
+}
+''');
   }
 
-  String get testFilePath => '$testPackageLibPath/test.dart';
+  Future<void> test_import_package_dependencies() async {
+    // TODO(scheglov) Switch to PubspecYamlFileConfig
+    newPubspecYamlFile(testPackageRootPath, r'''
+name: test
+dependencies:
+  aaa: any
+''');
 
-  String get testPackageLibPath => '$testPackageRootPath/lib';
+    var aaaRoot = getFolder('$workspaceRootPath/packages/aaa');
+    newFile('${aaaRoot.path}/lib/f.dart', content: '''
+class Test {}
+''');
 
-  Folder get testPackageRoot => getFolder(testPackageRootPath);
+    writeTestPackageConfig(
+      config: PackageConfigFileBuilder()
+        ..add(name: 'aaa', rootPath: aaaRoot.path),
+    );
 
-  String get testPackageRootPath => '$workspaceRootPath/test';
+    await _configureWithWorkspaceRoot();
 
-  String get testPackageTestPath => '$testPackageRootPath/test';
+    var validator = await _getTestCodeDetails('''
+void f() {
+  T^
+}
+''', completion: 'Test', libraryUri: 'package:aaa/a.dart');
+    validator
+      ..hasCompletion('Test')
+      ..hasChange()
+          .hasFileEdit(testFilePathPlatform)
+          .whenApplied(testFileContent, r'''
+import 'package:aaa/a.dart';
 
-  String get workspaceRootPath => '/home';
+void f() {
+  T
+}
+''');
+  }
 
-  Future<void> setRoots({
-    required List<String> included,
-    required List<String> excluded,
+  Future<void> test_import_package_this() async {
+    newFile('$testPackageLibPath/a.dart', content: '''
+class Test {}
+''');
+
+    await _configureWithWorkspaceRoot();
+
+    var validator = await _getTestCodeDetails('''
+void f() {
+  T^
+}
+''', completion: 'Test', libraryUri: 'package:test/a.dart');
+    validator
+      ..hasCompletion('Test')
+      ..hasChange()
+          .hasFileEdit(testFilePathPlatform)
+          .whenApplied(testFileContent, r'''
+import 'package:test/a.dart';
+
+void f() {
+  T
+}
+''');
+  }
+
+  Future<void> test_invalidLibraryUri() async {
+    await _configureWithWorkspaceRoot();
+
+    var request = CompletionGetSuggestionDetails2Params(
+            testFilePathPlatform, 0, 'Random', '[foo]:bar')
+        .toRequest('0');
+
+    var response = await _handleRequest(request);
+    expect(response.error?.code, RequestErrorCode.INVALID_PARAMETER);
+    // TODO(scheglov) Check that says "libraryUri".
+  }
+
+  Future<void> test_invalidPath() async {
+    await _configureWithWorkspaceRoot();
+
+    var request =
+        CompletionGetSuggestionDetails2Params('foo', 0, 'Random', 'dart:math')
+            .toRequest('0');
+
+    var response = await _handleRequest(request);
+    expect(response.error?.code, RequestErrorCode.INVALID_FILE_PATH_FORMAT);
+  }
+
+  Future<GetSuggestionDetails2Validator> _getCodeDetails({
+    required String path,
+    required String content,
+    required String completion,
+    required String libraryUri,
   }) async {
-    var includedConverted = included.map(convertPath).toList();
-    var excludedConverted = excluded.map(convertPath).toList();
-    await _handleSuccessfulRequest(
-      AnalysisSetAnalysisRootsParams(
-        includedConverted,
-        excludedConverted,
-        packageRoots: {},
-      ).toRequest('0'),
+    var completionOffset = content.indexOf('^');
+    expect(completionOffset, isNot(equals(-1)), reason: 'missing ^');
+
+    var nextOffset = content.indexOf('^', completionOffset + 1);
+    expect(nextOffset, equals(-1), reason: 'too many ^');
+
+    newFile(path,
+        content: content.substring(0, completionOffset) +
+            content.substring(completionOffset + 1));
+
+    return await _getDetails(
+      path: path,
+      completionOffset: completionOffset,
+      completion: completion,
+      libraryUri: libraryUri,
     );
   }
 
+  Future<GetSuggestionDetails2Validator> _getDetails({
+    required String path,
+    required int completionOffset,
+    required String completion,
+    required String libraryUri,
+  }) async {
+    var request = CompletionGetSuggestionDetails2Params(
+      path,
+      completionOffset,
+      completion,
+      libraryUri,
+    ).toRequest('0');
+
+    var response = await _handleSuccessfulRequest(request);
+    var result = CompletionGetSuggestionDetails2Result.fromResponse(response);
+    return GetSuggestionDetails2Validator(result);
+  }
+
+  Future<GetSuggestionDetails2Validator> _getTestCodeDetails(
+    String content, {
+    required String completion,
+    required String libraryUri,
+  }) async {
+    return _getCodeDetails(
+      path: convertPath(testFilePath),
+      content: content,
+      completion: completion,
+      libraryUri: libraryUri,
+    );
+  }
+}
+
+@reflectiveTest
+class CompletionDomainHandlerGetSuggestions2Test
+    extends PubPackageAnalysisServerTest {
+  @override
   void setUp() {
-    serverChannel = MockServerChannel();
-
-    var sdkRoot = newFolder('/sdk');
-    createMockSdk(
-      resourceProvider: resourceProvider,
-      root: sdkRoot,
-    );
-
-    writeTestPackageConfig();
-
-    server = AnalysisServer(
-      serverChannel,
-      resourceProvider,
-      AnalysisServerOptions(),
-      DartSdkManager(sdkRoot.path),
-      CrashReportingAttachmentsBuilder.empty,
-      InstrumentationService.NULL_SERVICE,
-    );
-
+    super.setUp();
     completionDomain.budgetDuration = const Duration(seconds: 30);
   }
 
@@ -971,37 +1098,6 @@
     suggestionsValidator.assertCompletions(['foo02', 'foo01']);
   }
 
-  void writePackageConfig(Folder root, PackageConfigFileBuilder config) {
-    newPackageConfigJsonFile(
-      root.path,
-      content: config.toContent(toUriStr: toUriStr),
-    );
-  }
-
-  void writeTestPackageConfig({
-    PackageConfigFileBuilder? config,
-    String? languageVersion,
-  }) {
-    if (config == null) {
-      config = PackageConfigFileBuilder();
-    } else {
-      config = config.copy();
-    }
-
-    config.add(
-      name: 'test',
-      rootPath: testPackageRootPath,
-      languageVersion: languageVersion,
-    );
-
-    writePackageConfig(testPackageRoot, config);
-  }
-
-  Future<void> _configureWithWorkspaceRoot() async {
-    await setRoots(included: [workspaceRootPath], excluded: []);
-    await server.onAnalysisComplete;
-  }
-
   Future<CompletionGetSuggestions2ResponseValidator> _getCodeSuggestions({
     required String path,
     required String content,
@@ -1050,13 +1146,6 @@
       maxResults: maxResults,
     );
   }
-
-  /// Validates that the given [request] is handled successfully.
-  Future<Response> _handleSuccessfulRequest(Request request) async {
-    var response = await serverChannel.sendRequest(request);
-    expect(response, isResponseSuccess(request.id));
-    return response;
-  }
 }
 
 @reflectiveTest
@@ -1998,6 +2087,129 @@
   }
 }
 
+class GetSuggestionDetails2Validator {
+  final CompletionGetSuggestionDetails2Result result;
+
+  GetSuggestionDetails2Validator(this.result);
+
+  SourceChangeValidator hasChange() {
+    return SourceChangeValidator(result.change);
+  }
+
+  void hasCompletion(Object completion) {
+    expect(result.completion, completion);
+  }
+}
+
+class PubPackageAnalysisServerTest with ResourceProviderMixin {
+  late final MockServerChannel serverChannel;
+  late final AnalysisServer server;
+
+  AnalysisDomainHandler get analysisDomain {
+    return server.handlers.whereType<AnalysisDomainHandler>().single;
+  }
+
+  CompletionDomainHandler get completionDomain {
+    return server.handlers.whereType<CompletionDomainHandler>().single;
+  }
+
+  String get testFileContent => getFile(testFilePath).readAsStringSync();
+
+  String get testFilePath => '$testPackageLibPath/test.dart';
+
+  String get testFilePathPlatform => convertPath(testFilePath);
+
+  String get testPackageLibPath => '$testPackageRootPath/lib';
+
+  Folder get testPackageRoot => getFolder(testPackageRootPath);
+
+  String get testPackageRootPath => '$workspaceRootPath/test';
+
+  String get testPackageTestPath => '$testPackageRootPath/test';
+
+  String get workspaceRootPath => '/home';
+
+  Future<void> setRoots({
+    required List<String> included,
+    required List<String> excluded,
+  }) async {
+    var includedConverted = included.map(convertPath).toList();
+    var excludedConverted = excluded.map(convertPath).toList();
+    await _handleSuccessfulRequest(
+      AnalysisSetAnalysisRootsParams(
+        includedConverted,
+        excludedConverted,
+        packageRoots: {},
+      ).toRequest('0'),
+    );
+  }
+
+  void setUp() {
+    serverChannel = MockServerChannel();
+
+    var sdkRoot = newFolder('/sdk');
+    createMockSdk(
+      resourceProvider: resourceProvider,
+      root: sdkRoot,
+    );
+
+    writeTestPackageConfig();
+
+    server = AnalysisServer(
+      serverChannel,
+      resourceProvider,
+      AnalysisServerOptions(),
+      DartSdkManager(sdkRoot.path),
+      CrashReportingAttachmentsBuilder.empty,
+      InstrumentationService.NULL_SERVICE,
+    );
+
+    completionDomain.budgetDuration = const Duration(seconds: 30);
+  }
+
+  void writePackageConfig(Folder root, PackageConfigFileBuilder config) {
+    newPackageConfigJsonFile(
+      root.path,
+      content: config.toContent(toUriStr: toUriStr),
+    );
+  }
+
+  void writeTestPackageConfig({
+    PackageConfigFileBuilder? config,
+    String? languageVersion,
+  }) {
+    if (config == null) {
+      config = PackageConfigFileBuilder();
+    } else {
+      config = config.copy();
+    }
+
+    config.add(
+      name: 'test',
+      rootPath: testPackageRootPath,
+      languageVersion: languageVersion,
+    );
+
+    writePackageConfig(testPackageRoot, config);
+  }
+
+  Future<void> _configureWithWorkspaceRoot() async {
+    await setRoots(included: [workspaceRootPath], excluded: []);
+    await server.onAnalysisComplete;
+  }
+
+  Future<Response> _handleRequest(Request request) async {
+    return await serverChannel.sendRequest(request);
+  }
+
+  /// Validates that the given [request] is handled successfully.
+  Future<Response> _handleSuccessfulRequest(Request request) async {
+    var response = await _handleRequest(request);
+    expect(response, isResponseSuccess(request.id));
+    return response;
+  }
+}
+
 class SingleSuggestionValidator {
   final CompletionSuggestion suggestion;
   final List<String>? libraryUrisToImport;
@@ -2042,6 +2254,32 @@
   }
 }
 
+class SourceChangeValidator {
+  final SourceChange change;
+
+  SourceChangeValidator(this.change);
+
+  void assertNoFileEdits() {
+    expect(change.edits, isEmpty);
+  }
+
+  SourceFileEditValidator hasFileEdit(String path) {
+    var edit = change.edits.singleWhere((e) => e.file == path);
+    return SourceFileEditValidator(edit);
+  }
+}
+
+class SourceFileEditValidator {
+  final SourceFileEdit edit;
+
+  SourceFileEditValidator(this.edit);
+
+  void whenApplied(String applyTo, Object expected) {
+    var actual = SourceEdit.applySequence(applyTo, edit.edits);
+    expect(actual, expected);
+  }
+}
+
 class SuggestionsValidator {
   final List<CompletionSuggestion> suggestions;
   final List<String>? libraryUrisToImport;