Version 2.15.0-264.0.dev

Merge commit 'a9752ad140eed5b48f1d23879758dae5c0e16f80' into 'dev'
diff --git a/pkg/analysis_server/lib/src/domain_completion.dart b/pkg/analysis_server/lib/src/domain_completion.dart
index 85c3ac9..b1b2b36 100644
--- a/pkg/analysis_server/lib/src/domain_completion.dart
+++ b/pkg/analysis_server/lib/src/domain_completion.dart
@@ -97,13 +97,9 @@
         librariesToImport: librariesToImport,
       );
 
-      try {
-        suggestions.addAll(
-          await manager.computeSuggestions(request, performance),
-        );
-      } on AbortCompletion {
-        suggestions.clear();
-      }
+      suggestions.addAll(
+        await manager.computeSuggestions(request, performance),
+      );
     });
     // TODO (danrubel) if request is obsolete (processAnalysisRequest returns
     // false) then send empty results
@@ -212,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);
@@ -261,14 +314,28 @@
         ),
         documentationCache: server.getDocumentationCacheFor(resolvedUnit),
       );
+      setNewRequest(completionRequest);
 
       var librariesToImport = <Uri>[];
-      var suggestions = await computeSuggestions(
-        budget: budget,
-        performance: performance,
-        request: completionRequest,
-        librariesToImport: librariesToImport,
-      );
+      var suggestions = <CompletionSuggestion>[];
+      try {
+        suggestions = await computeSuggestions(
+          budget: budget,
+          performance: performance,
+          request: completionRequest,
+          librariesToImport: librariesToImport,
+        );
+      } on AbortCompletion {
+        return server.sendResponse(
+          CompletionGetSuggestions2Result(
+            completionRequest.replacementOffset,
+            completionRequest.replacementLength,
+            [],
+            [],
+            true,
+          ).toResponse(request.id),
+        );
+      }
 
       performance.run('filter', (performance) {
         performance.getDataInt('count').add(suggestions.length);
@@ -311,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;
@@ -436,14 +506,19 @@
 
       // Compute suggestions in the background
       try {
-        var suggestions = await computeSuggestions(
-          budget: budget,
-          performance: perf,
-          request: completionRequest,
-          includedElementKinds: includedElementKinds,
-          includedElementNames: includedElementNames,
-          includedSuggestionRelevanceTags: includedSuggestionRelevanceTags,
-        );
+        var suggestions = <CompletionSuggestion>[];
+        try {
+          suggestions = await computeSuggestions(
+            budget: budget,
+            performance: perf,
+            request: completionRequest,
+            includedElementKinds: includedElementKinds,
+            includedElementNames: includedElementNames,
+            includedSuggestionRelevanceTags: includedSuggestionRelevanceTags,
+          );
+        } on AbortCompletion {
+          // Continue with empty suggestions list.
+        }
         String? libraryFile;
         var includedSuggestionSets = <IncludedSuggestionSet>[];
         if (includedElementKinds != null && includedElementNames != null) {
diff --git a/pkg/analysis_server/lib/src/services/completion/dart/not_imported_contributor.dart b/pkg/analysis_server/lib/src/services/completion/dart/not_imported_contributor.dart
index 4fba896..938228c 100644
--- a/pkg/analysis_server/lib/src/services/completion/dart/not_imported_contributor.dart
+++ b/pkg/analysis_server/lib/src/services/completion/dart/not_imported_contributor.dart
@@ -16,9 +16,14 @@
 import 'package:analyzer/src/lint/pub.dart';
 import 'package:analyzer/src/workspace/pub.dart';
 import 'package:collection/collection.dart';
+import 'package:meta/meta.dart';
 
 /// A contributor of suggestions from not yet imported libraries.
 class NotImportedContributor extends DartCompletionContributor {
+  /// Tests set this function to abort the current request.
+  @visibleForTesting
+  static void Function(FileState)? onFile;
+
   final CompletionBudget budget;
   final List<Uri> librariesToImport;
 
@@ -49,6 +54,9 @@
 
     var knownFiles = fsState.knownFiles.toList();
     for (var file in knownFiles) {
+      onFile?.call(file);
+      request.checkAborted();
+
       if (budget.isEmpty) {
         return;
       }
diff --git a/pkg/analysis_server/test/domain_completion_test.dart b/pkg/analysis_server/test/domain_completion_test.dart
index ca7daef..615fc09 100644
--- a/pkg/analysis_server/test/domain_completion_test.dart
+++ b/pkg/analysis_server/test/domain_completion_test.dart
@@ -2,6 +2,8 @@
 // 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 'package:analysis_server/src/analysis_server.dart';
 import 'package:analysis_server/src/domain_analysis.dart';
 import 'package:analysis_server/src/domain_completion.dart';
@@ -9,6 +11,7 @@
 import 'package:analysis_server/src/protocol_server.dart';
 import 'package:analysis_server/src/provisional/completion/dart/completion_dart.dart';
 import 'package:analysis_server/src/server/crash_reporting_attachments.dart';
+import 'package:analysis_server/src/services/completion/dart/not_imported_contributor.dart';
 import 'package:analysis_server/src/utilities/mocks.dart';
 import 'package:analyzer/file_system/file_system.dart';
 import 'package:analyzer/instrumentation/service.dart';
@@ -27,74 +30,237 @@
 
 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);
   }
 
+  void tearDown() {
+    NotImportedContributor.onFile = null;
+  }
+
+  Future<void> test_notImported_abort() async {
+    await _configureWithWorkspaceRoot();
+
+    NotImportedContributor.onFile = (file) {
+      if (file.uriStr == 'dart:math') {
+        unawaited(
+          _getSuggestions(
+            path: convertPath(testFilePath),
+            completionOffset: 0,
+            maxResults: 100,
+          ),
+        );
+      }
+    };
+
+    var responseValidator = await _getTestCodeSuggestions('''
+void f() {
+  Rand^
+}
+''');
+
+    responseValidator
+      ..assertIncomplete()
+      ..assertReplacementBack(4)
+      ..assertLibrariesToImport(includes: [], excludes: [
+        'dart:core',
+        'dart:math',
+      ]);
+
+    responseValidator.suggestions.assertEmpty();
+  }
+
   Future<void> test_notImported_emptyBudget() async {
     await _configureWithWorkspaceRoot();
 
@@ -932,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,
@@ -1011,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
@@ -1959,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;
@@ -2003,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;
diff --git a/runtime/observatory/tests/service/service_kernel.status b/runtime/observatory/tests/service/service_kernel.status
index af626d9..df6722b 100644
--- a/runtime/observatory/tests/service/service_kernel.status
+++ b/runtime/observatory/tests/service/service_kernel.status
@@ -3,6 +3,7 @@
 # BSD-style license that can be found in the LICENSE file.
 
 complex_reload_test: Skip # http://dartbug.com/47130
+coverage_closure_call_test: Skip # https://github.com/dart-lang/sdk/issues/47568
 valid_source_locations_test: Pass, Slow
 
 [ $compiler == app_jitk ]
diff --git a/runtime/observatory_2/tests/service_2/service_2_kernel.status b/runtime/observatory_2/tests/service_2/service_2_kernel.status
index 322b922..5627906 100644
--- a/runtime/observatory_2/tests/service_2/service_2_kernel.status
+++ b/runtime/observatory_2/tests/service_2/service_2_kernel.status
@@ -3,6 +3,7 @@
 # BSD-style license that can be found in the LICENSE file.
 
 complex_reload_test: Skip # http://dartbug.com/47130
+coverage_closure_call_test: Skip # https://github.com/dart-lang/sdk/issues/47568
 valid_source_locations_test: Pass, Slow
 
 [ $compiler == app_jitk ]
diff --git a/tools/VERSION b/tools/VERSION
index 54f7e08..298c9a4 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 15
 PATCH 0
-PRERELEASE 263
+PRERELEASE 264
 PRERELEASE_PATCH 0
\ No newline at end of file