[analysis_server] Add support for adding imports with show/hide + update Move to File

Change-Id: I5c732c969a9a89af76b82e60ac652cf00bb878bc
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/306920
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/services/refactoring/move_top_level_to_file.dart b/pkg/analysis_server/lib/src/services/refactoring/move_top_level_to_file.dart
index 0208349..e228d86 100644
--- a/pkg/analysis_server/lib/src/services/refactoring/move_top_level_to_file.dart
+++ b/pkg/analysis_server/lib/src/services/refactoring/move_top_level_to_file.dart
@@ -39,7 +39,7 @@
   MoveTopLevelToFile(super.context);
 
   @override
-  bool get isExperimental => true;
+  bool get isExperimental => false;
 
   @override
   CodeActionKind get kind => DartCodeActionKind.RefactorMove;
@@ -183,17 +183,20 @@
   void _addImportsForMovingDeclarations(
       DartFileEditBuilder builder, ImportAnalyzer analyzer) {
     for (var entry in analyzer.movingReferences.entries) {
+      var element = entry.key;
       var imports = entry.value;
       for (var import in imports) {
         var library = import.importedLibrary;
         if (library == null || library.isDartCore) {
           continue;
         }
-        // TODO(dantup): This does not support show/hide. We should be able to
-        //  pass them in (and have them merge with any existing imports or
-        //  pending imports).
-        builder.importLibrary(library.source.uri,
-            prefix: import.prefix?.element.name);
+        var hasShowCombinator =
+            import.combinators.whereType<ShowElementCombinator>().isNotEmpty;
+        builder.importLibrary(
+          library.source.uri,
+          prefix: import.prefix?.element.name,
+          showName: hasShowCombinator ? element.name : null,
+        );
       }
     }
   }
diff --git a/pkg/analysis_server/test/src/services/refactoring/move_top_level_to_file_test.dart b/pkg/analysis_server/test/src/services/refactoring/move_top_level_to_file_test.dart
index 081d9fd..9a8a252 100644
--- a/pkg/analysis_server/test/src/services/refactoring/move_top_level_to_file_test.dart
+++ b/pkg/analysis_server/test/src/services/refactoring/move_top_level_to_file_test.dart
@@ -944,6 +944,119 @@
         modifiedOtherFileContent: modifiedOtherFileContent);
   }
 
+  /// Test moving declarations to a file that imports a library that exports a
+  /// referenced declaration, but currently hides it.
+  Future<void> test_imports_showHide_destinationHides() async {
+    var libFilePath = join(projectFolderPath, 'lib', 'a.dart');
+    var srcFilePath = join(projectFolderPath, 'lib', 'src', 'a.dart');
+    var destinationFileName = 'moving.dart';
+    var destinationFilePath =
+        join(projectFolderPath, 'lib', destinationFileName);
+    addSource(libFilePath, 'export "src/a.dart";');
+    addSource(srcFilePath, 'class A {}');
+    addSource(destinationFilePath, '''
+import 'package:test/a.dart' hide A;
+''');
+    var originalSource = '''
+import 'package:test/a.dart';
+
+A? staying;
+A? mov^ing;
+''';
+    var modifiedSource = '''
+import 'package:test/a.dart';
+
+A? staying;
+''';
+    var declarationName = 'moving';
+
+    var expectedDestinationContent = '''
+import 'package:test/a.dart' hide A;
+import 'package:test/a.dart';
+
+A? moving;
+''';
+    await _singleDeclaration(
+        originalSource: originalSource,
+        modifiedSource: modifiedSource,
+        declarationName: declarationName,
+        newFileName: destinationFileName,
+        newFileContent: expectedDestinationContent);
+  }
+
+  /// Test moving declarations to a file that imports a library that exports a
+  /// referenced declaration, but currently hides it.
+  Future<void> test_imports_showHide_destinationHides_sourceShows() async {
+    var libFilePath = join(projectFolderPath, 'lib', 'a.dart');
+    var srcFilePath = join(projectFolderPath, 'lib', 'src', 'a.dart');
+    var destinationFileName = 'moving.dart';
+    var destinationFilePath =
+        join(projectFolderPath, 'lib', destinationFileName);
+    addSource(libFilePath, 'export "src/a.dart";');
+    addSource(srcFilePath, 'class A {}');
+    addSource(destinationFilePath, '''
+import 'package:test/a.dart' hide A;
+''');
+    var originalSource = '''
+import 'package:test/a.dart' show A;
+
+A? staying;
+A? mov^ing;
+''';
+    var modifiedSource = '''
+import 'package:test/a.dart' show A;
+
+A? staying;
+''';
+    var declarationName = 'moving';
+
+    var expectedDestinationContent = '''
+import 'package:test/a.dart' hide A;
+import 'package:test/a.dart' show A;
+
+A? moving;
+''';
+    await _singleDeclaration(
+        originalSource: originalSource,
+        modifiedSource: modifiedSource,
+        declarationName: declarationName,
+        newFileName: destinationFileName,
+        newFileContent: expectedDestinationContent);
+  }
+
+  /// Test that if the moving declaration was imported with 'show' that any new
+  /// import added to the destination also only shows it.
+  Future<void> test_imports_showHide_sourceShows() async {
+    var libFilePath = join(projectFolderPath, 'lib', 'a.dart');
+    var srcFilePath = join(projectFolderPath, 'lib', 'src', 'a.dart');
+    addSource(libFilePath, 'export "src/a.dart";');
+    addSource(srcFilePath, 'class A {}');
+    var originalSource = '''
+import 'package:test/a.dart' show A;
+
+A? staying;
+A? mov^ing;
+''';
+    var modifiedSource = '''
+import 'package:test/a.dart' show A;
+
+A? staying;
+''';
+    var declarationName = 'moving';
+    var newFileName = 'moving.dart';
+    var newFileContent = '''
+import 'package:test/a.dart' show A;
+
+A? moving;
+''';
+    await _singleDeclaration(
+        originalSource: originalSource,
+        modifiedSource: modifiedSource,
+        declarationName: declarationName,
+        newFileName: newFileName,
+        newFileContent: newFileContent);
+  }
+
   Future<void> test_kind_class() async {
     var originalSource = '''
 class A {}
@@ -1084,6 +1197,12 @@
     await expectCodeAction(simpleClassRefactorTitle);
   }
 
+  Future<void> test_protocol_available_withoutExperimentalOptIn() async {
+    addTestSource(simpleClassContent);
+    await initializeServer(experimentalOptInFlag: false);
+    await expectCodeAction(simpleClassRefactorTitle);
+  }
+
   Future<void> test_protocol_clientModifiedValues() async {
     addTestSource(simpleClassContent);
 
@@ -1105,12 +1224,6 @@
     expect(content[newFilePath], expectedNewFileContent);
   }
 
-  Future<void> test_protocol_unavailable_withoutExperimentalOptIn() async {
-    addTestSource(simpleClassContent);
-    await initializeServer(experimentalOptInFlag: false);
-    await expectNoCodeAction(simpleClassRefactorTitle);
-  }
-
   Future<void> test_protocol_unavailable_withoutFileCreateSupport() async {
     addTestSource(simpleClassContent);
     await initializeServer(fileCreateSupport: false);
diff --git a/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart b/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart
index c6e10cc..2b14f8c 100644
--- a/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart
+++ b/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart
@@ -1549,9 +1549,21 @@
   ///
   /// If the library [element] is declared in is inside the `src` folder, will
   /// try to locate a public URI to import instead.
+  ///
+  /// If [useShow] is `true`, new imports will be added that `show` only the
+  /// requested element (or if there is a pending import for the library, added
+  /// to its `show` combinator).
   Future<void> importElementLibrary(Element element,
-      {Map<Element, LibraryElement?>? resultCache}) async {
-    if (_isDefinedLocally(element) || _getImportElement(element) != null) {
+      {Map<Element, LibraryElement?>? resultCache,
+      bool useShow = false}) async {
+    if (_isDefinedLocally(element)) {
+      return;
+    }
+
+    var existingImport = _getImportElement(element);
+    var name = element.name;
+    if (existingImport != null && name != null) {
+      existingImport.ensureShown(name);
       return;
     }
 
@@ -1559,21 +1571,23 @@
         await TopLevelDeclarations(resolvedUnit)
             .publiclyExporting(element, resultCache: resultCache);
     if (libraryWithElement != null) {
-      _elementLibrariesToImport[element] =
-          _importLibrary(libraryWithElement.source.uri);
+      _elementLibrariesToImport[element] = _importLibrary(
+        libraryWithElement.source.uri,
+        showName: useShow ? element.name : null,
+      );
       return;
     }
 
     // If we didn't find one, use the original URI.
     var uri = element.source?.uri;
     if (uri != null) {
-      _importLibrary(uri);
+      _importLibrary(uri, showName: useShow ? element.name : null);
     }
   }
 
   @override
-  String importLibrary(Uri uri, {String? prefix}) {
-    return _importLibrary(uri, prefix: prefix).uriText;
+  String importLibrary(Uri uri, {String? prefix, String? showName}) {
+    return _importLibrary(uri, prefix: prefix, showName: showName).uriText;
   }
 
   @override
@@ -1680,6 +1694,14 @@
           builder.write(' as ');
           builder.write(prefix);
         }
+        if (import.showNames.isNotEmpty) {
+          builder.write(' show ');
+          builder.write(import.showNames.join(', '));
+        }
+        if (import.hideNames.isNotEmpty) {
+          builder.write(' hide ');
+          builder.write(import.hideNames.join(', '));
+        }
         builder.write(';');
       }
     }
@@ -1885,8 +1907,9 @@
       var definedNames = import.namespace.definedNames;
       if (definedNames.containsValue(element)) {
         return _LibraryImport(
-            uriText: import.librarySource.uri.toString(),
-            prefix: import.prefix?.element.displayName ?? '');
+          uriText: import.librarySource.uri.toString(),
+          prefix: import.prefix?.element.displayName ?? '',
+        );
       }
     }
 
@@ -1945,9 +1968,14 @@
   /// If [prefix] is an empty string, adds the import without a prefix.
   /// If [prefix] is null, will use [importPrefixGenerator] to generate one or
   /// reuse an existing prefix for this import.
+  ///
+  /// If [showName] is supplied then any new import will show only this element,
+  /// or if an import already exists it will be added to 'show' or removed from
+  /// 'hide' if appropriate.
   _LibraryImport _importLibrary(
     Uri uri, {
     String? prefix,
+    String? showName,
     bool forceAbsolute = false,
     bool forceRelative = false,
   }) {
@@ -1956,12 +1984,19 @@
       if (prefix != null) {
         import.prefixes.add(prefix);
       }
+      if (showName != null) {
+        import.ensureShown(showName);
+      }
     } else {
       var uriText = _getLibraryUriText(uri,
           forceAbsolute: forceAbsolute, forceRelative: forceRelative);
       prefix ??=
           importPrefixGenerator != null ? importPrefixGenerator!(uri) : null;
-      import = _LibraryImport(uriText: uriText, prefix: prefix ?? '');
+      import = _LibraryImport(
+        uriText: uriText,
+        prefix: prefix ?? '',
+        showNames: showName != null ? {showName} : null,
+      );
       (libraryChangeBuilder ?? this).librariesToImport[uri] = import;
     }
 
@@ -2077,7 +2112,19 @@
   /// with other prefixes for a library that is both prefixed and unprefixed.
   final Set<String> prefixes = {};
 
-  _LibraryImport({required this.uriText, required String prefix}) {
+  /// Names this import has in its `show` combinator.
+  final Set<String> showNames;
+
+  /// Names this import has in its `hide` combinator.
+  final Set<String> hideNames;
+
+  _LibraryImport({
+    required this.uriText,
+    required String prefix,
+    Set<String>? showNames,
+    Set<String>? hideNames,
+  })  : showNames = showNames ?? {},
+        hideNames = hideNames ?? {} {
     prefixes.add(prefix);
   }
 
@@ -2095,4 +2142,15 @@
         other.uriText == uriText &&
         !const SetEquality().equals(other.prefixes, prefixes);
   }
+
+  /// Ensures [name] is visible for this import.
+  ///
+  /// If the import already has a show combinator, this name will be added.
+  /// If the import hides this name, it will be unhidden.
+  void ensureShown(String name) {
+    if (showNames.isNotEmpty) {
+      showNames.add(name);
+    }
+    hideNames.remove(name);
+  }
 }
diff --git a/pkg/analyzer_plugin/lib/utilities/change_builder/change_builder_dart.dart b/pkg/analyzer_plugin/lib/utilities/change_builder/change_builder_dart.dart
index 3d5694d..4c97c44 100644
--- a/pkg/analyzer_plugin/lib/utilities/change_builder/change_builder_dart.dart
+++ b/pkg/analyzer_plugin/lib/utilities/change_builder/change_builder_dart.dart
@@ -355,12 +355,14 @@
   /// Arrange to have an import added for the library with the given [uri]. If a
   /// [prefix] is provided it will be used in the import directive.
   ///
+  /// If [showName] is provided, new imports will show only this name.
+  ///
   /// Return the text of the URI that will be used in the import directive.
   /// It can be different than the given [uri].
   ///
   /// The [uri] may be converted from an absolute URI to a relative URI
   /// depending on user preferences/lints.
-  String importLibrary(Uri uri, {String? prefix});
+  String importLibrary(Uri uri, {String? prefix, String? showName});
 
   /// Ensure that the library with the given [uri] is imported.
   ///
diff --git a/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_dart_test.dart b/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_dart_test.dart
index bc3ccd0..715aa54 100644
--- a/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_dart_test.dart
+++ b/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_dart_test.dart
@@ -2122,6 +2122,152 @@
     expect(cache[futureOrElement], resolvedFakeUnit.libraryElement);
   }
 
+  /// Test that importing elements where the library is already imported
+  /// (without a show/hide cmobinator) produces no additional edits.
+  Future<void> test_importElementLibrary_useShow_existingImport() async {
+    var content = '''
+import 'package:test/a.dart';
+''';
+    var otherContent = '''
+class A {}
+class B {}
+class C {}
+''';
+    var otherUnit = await resolveContent('/home/test/lib/a.dart', otherContent);
+    var unit = await resolveContent('/home/test/lib/test.dart', content);
+
+    final edits = await _getClassImportEdits(
+      destinationUnit: unit,
+      importedUnit: otherUnit,
+      classNames: {'A', 'B'},
+      useShow: true,
+    );
+    expect(edits, isEmpty);
+  }
+
+  /// Test that importing elements where the library is already imported but
+  /// hides the element causes the element to be shown.
+  Future<void>
+      test_importElementLibrary_useShow_existingImport_hidesElement() async {
+    var content = '''
+import 'package:test/a.dart' hide A;
+''';
+    var otherContent = '''
+class A {}
+class B {}
+class C {}
+''';
+    var otherUnit = await resolveContent('/home/test/lib/a.dart', otherContent);
+    var unit = await resolveContent('/home/test/lib/test.dart', content);
+
+    final edits = await _getClassImportEdits(
+      destinationUnit: unit,
+      importedUnit: otherUnit,
+      classNames: {'A', 'B'},
+      useShow: true,
+    );
+    final resultContent = SourceEdit.applySequence(content, edits);
+    // TODO(dantup): Currently we can only add new imports and not update
+    //  existing ones.
+    expect(
+        resultContent,
+        equals(
+          '''
+import 'package:test/a.dart' hide A;
+import 'package:test/a.dart' show A;
+''',
+        ));
+  }
+
+  /// Test that importing elements where the library is already imported and
+  /// already shows the elements produces no edits.
+  Future<void>
+      test_importElementLibrary_useShow_existingImport_showsElement() async {
+    var content = '''
+import 'package:test/a.dart' show A, B, C;
+''';
+    var otherContent = '''
+class A {}
+class B {}
+class C {}
+''';
+    var otherUnit = await resolveContent('/home/test/lib/a.dart', otherContent);
+    var unit = await resolveContent('/home/test/lib/test.dart', content);
+
+    final edits = await _getClassImportEdits(
+      destinationUnit: unit,
+      importedUnit: otherUnit,
+      classNames: {'A', 'B'},
+      useShow: true,
+    );
+    expect(edits, isEmpty);
+  }
+
+  /// Test that importing elements where the library is already imported but it
+  /// only shows other elements results in the element being shown.
+  Future<void>
+      test_importElementLibrary_useShow_existingImport_showsOthers() async {
+    var content = '''
+import 'package:test/a.dart' show C;
+''';
+    var otherContent = '''
+class A {}
+class B {}
+class C {}
+''';
+    var otherUnit = await resolveContent('/home/test/lib/a.dart', otherContent);
+    var unit = await resolveContent('/home/test/lib/test.dart', content);
+
+    final edits = await _getClassImportEdits(
+      destinationUnit: unit,
+      importedUnit: otherUnit,
+      classNames: {'A', 'B'},
+      useShow: true,
+    );
+    final resultContent = SourceEdit.applySequence(content, edits);
+    // TODO(dantup): Currently we can only add new imports and not update
+    //  existing ones (such as to add to 'show' or remove 'hide'). Updating
+    //  existing imports requires some changes to how prefixes are currently
+    //  handles so we can separate prefixes from the shown elements.
+    expect(
+        resultContent,
+        equals(
+          '''
+import 'package:test/a.dart' show C;
+import 'package:test/a.dart' show A, B;
+''',
+        ));
+  }
+
+  /// Test that that we can add a new import elements using 'show'.
+  Future<void> test_importElementLibrary_useShow_newImport() async {
+    var otherUnit = await resolveContent(
+      '/home/test/lib/a.dart',
+      '''
+class A {}
+class B {}
+''',
+    );
+    var unit = await resolveContent(
+      '/home/test/lib/test.dart',
+      '',
+    );
+
+    final edits = await _getClassImportEdits(
+      destinationUnit: unit,
+      importedUnit: otherUnit,
+      classNames: {'A', 'B'},
+      useShow: true,
+    );
+    expect(edits, hasLength(1));
+    expect(
+        edits[0].replacement,
+        equals(
+          '''import 'package:test/a.dart' show A, B;
+''',
+        ));
+  }
+
   Future<void> test_multipleEdits_concurrently() async {
     var initialCode = '00';
     var path = convertPath('/home/test/lib/test.dart');
@@ -2174,6 +2320,30 @@
     expect(edits, hasLength(1));
     expect(edits[0].replacement, equalsIgnoringWhitespace('Future<String>'));
   }
+
+  /// Computes edits to import the class [className] from [importedUnit] into
+  /// [destinationUnit].
+  ///
+  /// If [useShow] is set, will try to add a show combinator for this class.
+  Future<List<SourceEdit>> _getClassImportEdits({
+    required ResolvedUnitResult destinationUnit,
+    required ResolvedUnitResult importedUnit,
+    required Set<String> classNames,
+    bool useShow = false,
+  }) async {
+    var findNode = FindNode(importedUnit.content, importedUnit.unit);
+    var builder = await newBuilder();
+    await builder.addDartFileEdit(destinationUnit.path, (builder) async {
+      var builderImpl = builder as DartFileEditBuilderImpl;
+      for (var className in classNames) {
+        var classElement =
+            findNode.classDeclaration(className).declaredElement!;
+        await builderImpl.importElementLibrary(classElement, useShow: useShow);
+      }
+    });
+
+    return getEdits(builder);
+  }
 }
 
 @reflectiveTest
diff --git a/pkg/analyzer_plugin/test/src/utilities/change_builder/dart/dart_change_builder_mixin.dart b/pkg/analyzer_plugin/test/src/utilities/change_builder/dart/dart_change_builder_mixin.dart
index 10af9f1..a6b4a7b 100644
--- a/pkg/analyzer_plugin/test/src/utilities/change_builder/dart/dart_change_builder_mixin.dart
+++ b/pkg/analyzer_plugin/test/src/utilities/change_builder/dart/dart_change_builder_mixin.dart
@@ -20,11 +20,12 @@
     expect(sourceChange, isNotNull);
 
     var fileEdits = sourceChange.edits;
-    expect(fileEdits, hasLength(1));
+    if (fileEdits.isEmpty) {
+      return [];
+    }
 
-    var fileEdit = fileEdits[0];
-    expect(fileEdit, isNotNull);
-    return fileEdit.edits;
+    expect(fileEdits, hasLength(1));
+    return fileEdits[0].edits;
   }
 
   /// Return a newly created Dart change builder.