[DAS] Fix auto-complete update combinators
We already have a quick-fix that allows us to use the same existing import by editing the combinators. The auto-completer now should do the same.
Bug: https://github.com/Dart-Code/Dart-Code/issues/5238
Change-Id: I0f3a50299abb9890370a6c8934b49036f7e03196
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/404525
Auto-Submit: Felipe Morschel <git@fmorschel.dev>
Commit-Queue: Phil Quitslund <pquitslund@google.com>
Reviewed-by: Phil Quitslund <pquitslund@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion_resolve.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion_resolve.dart
index 73065c5..75bfb67 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion_resolve.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion_resolve.dart
@@ -86,10 +86,17 @@
return cancelled(token);
}
+ var element =
+ elementReference != null
+ ? await ElementLocation2.decode(
+ elementReference,
+ ).locateIn(session)
+ : null;
+
var builder = ChangeBuilder(session: session);
await builder.addDartFileEdit(file, (builder) {
for (var uri in importUris) {
- builder.importLibraryElement(uri);
+ builder.importLibraryElement(uri, showName: element?.name3);
}
});
@@ -123,12 +130,6 @@
// Look up documentation if we can get an element for this item.
Either2<MarkupContent, String>? documentation;
- var element =
- elementReference != null
- ? await ElementLocation2.decode(
- elementReference,
- ).locateIn(session)
- : null;
if (element != null) {
var formats = clientCapabilities.completionDocumentationFormats;
var dartDocInfo = server.getDartdocDirectiveInfoForSession(session);
diff --git a/pkg/analysis_server/lib/src/services/correction/dart/import_library.dart b/pkg/analysis_server/lib/src/services/correction/dart/import_library.dart
index aa3f692..f8bec04 100644
--- a/pkg/analysis_server/lib/src/services/correction/dart/import_library.dart
+++ b/pkg/analysis_server/lib/src/services/correction/dart/import_library.dart
@@ -96,6 +96,8 @@
for (var instantiatedExtension in instantiatedExtensions) {
// If the import has a combinator that needs to be updated, then offer
// to update it.
+ // TODO(FMorschel): We should fix all combinators for the import, if
+ // we don't, we may not import at all.
var combinators = import.combinators;
if (combinators.length == 1) {
var combinator = combinators[0];
@@ -624,7 +626,7 @@
_uriText = builder.importLibraryWithAbsoluteUri(
_library,
prefix: _prefix,
- shownName: _show,
+ showName: _show,
useShow: _show != null,
);
}
@@ -873,7 +875,7 @@
_uriText = builder.importLibraryWithRelativeUri(
_library,
prefix: _prefix,
- shownName: _show,
+ showName: _show,
useShow: _show != null,
);
}
diff --git a/pkg/analysis_server/test/lsp/completion_dart_test.dart b/pkg/analysis_server/test/lsp/completion_dart_test.dart
index 6bbbb75..59cc809 100644
--- a/pkg/analysis_server/test/lsp/completion_dart_test.dart
+++ b/pkg/analysis_server/test/lsp/completion_dart_test.dart
@@ -3663,6 +3663,131 @@
expect(res.isIncomplete, isTrue);
}
+ Future<void> test_unimportedSymbols_libraryImported_hidingMultiple() async {
+ newFile(join(projectFolderPath, 'lib', 'my_classes.dart'), '''
+class MyClass1 {}
+class MyClass2 {}
+class MyClass3 {}
+''');
+
+ var content = '''
+import 'package:test/my_classes.dart' hide MyClass1, MyClass2;
+void f() {
+ MyClas^
+}
+''';
+
+ var expectedContent = '''
+import 'package:test/my_classes.dart' hide MyClass2;
+void f() {
+ MyClass1
+}
+''';
+
+ var completionLabel = 'MyClass1';
+
+ await _checkCompletionEdits(
+ mainFileUri,
+ content,
+ completionLabel,
+ expectedContent,
+ );
+ }
+
+ Future<void> test_unimportedSymbols_libraryImported_hidingOne() async {
+ newFile(join(projectFolderPath, 'lib', 'my_classes.dart'), '''
+class MyClass1 {}
+class MyClass2 {}
+''');
+
+ var content = '''
+import 'package:test/my_classes.dart' hide MyClass1;
+void f() {
+ MyClas^
+}
+''';
+
+ var expectedContent = '''
+import 'package:test/my_classes.dart';
+void f() {
+ MyClass1
+}
+''';
+
+ var completionLabel = 'MyClass1';
+
+ await _checkCompletionEdits(
+ mainFileUri,
+ content,
+ completionLabel,
+ expectedContent,
+ );
+ }
+
+ Future<void> test_unimportedSymbols_libraryImported_showingOther() async {
+ newFile(join(projectFolderPath, 'lib', 'my_classes.dart'), '''
+class MyClass1 {}
+class MyClass2 {}
+''');
+
+ var content = '''
+import 'package:test/my_classes.dart' show MyClass2;
+void f() {
+ MyClas^
+}
+''';
+
+ var expectedContent = '''
+import 'package:test/my_classes.dart' show MyClass1, MyClass2;
+void f() {
+ MyClass1
+}
+''';
+
+ var completionLabel = 'MyClass1';
+
+ await _checkCompletionEdits(
+ mainFileUri,
+ content,
+ completionLabel,
+ expectedContent,
+ );
+ }
+
+ // Code completion doesn't include prefixes for auto-imports so when an
+ // auto-import is added it must be unprefixed even if the library exists with
+ // a prefix (we cannot modify the inserted text during resolve).
+ Future<void> test_unimportedSymbols_libraryImported_withPrefix() async {
+ newFile(join(projectFolderPath, 'lib', 'my_classes.dart'), '''
+class MyClass1 {}
+class MyClass2 {}
+''');
+
+ var content = '''
+import 'package:test/my_classes.dart' as p1 show MyClass2;
+void f() {
+ MyClas^
+}
+''';
+
+ var expectedContent = '''
+import 'package:test/my_classes.dart' as p1 show MyClass2;
+import 'package:test/my_classes.dart';
+void f() {
+ MyClass1
+}
+''';
+
+ var completionLabel = 'MyClass1';
+
+ await _checkCompletionEdits(
+ mainFileUri,
+ content,
+ completionLabel,
+ expectedContent,
+ );
+ }
+
/// This test reproduces a bug where the pathKey hash used in
/// available_declarations.dart would not change with the contents of the file
/// (as it always used 0 as the modification stamp) which would prevent
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 ae41b16..ee84cee 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
@@ -1608,7 +1608,7 @@
var newImport = elementLibrariesToImport[element] = _importLibrary(
uriToImport,
isExplicitImport: false,
- shownName: element.name3,
+ showName: element.name3,
useShow: useShow,
);
@@ -1620,15 +1620,24 @@
}
@override
- String importLibrary(Uri uri,
- {String? prefix, String? showName, bool useShow = false}) {
+ String importLibrary(
+ Uri uri, {
+ String? prefix,
+ String? showName,
+ bool useShow = false,
+ }) {
return _importLibrary(uri,
- prefix: prefix, shownName: showName, useShow: useShow)
+ prefix: prefix, showName: showName, useShow: useShow)
.uriText;
}
@override
- ImportLibraryElementResult importLibraryElement(Uri uri) {
+ ImportLibraryElementResult importLibraryElement(
+ Uri uri, {
+ String? prefix,
+ String? showName,
+ bool useShow = false,
+ }) {
if (resolvedUnit.libraryElement2.uri == uri) {
return ImportLibraryElementResultImpl(null);
}
@@ -1637,24 +1646,32 @@
in resolvedUnit.libraryElement2.firstFragment.libraryImports2) {
var importedLibrary = import.importedLibrary2;
if (importedLibrary != null && importedLibrary.uri == uri) {
- return ImportLibraryElementResultImpl(import.prefix2?.element.name3);
+ var importPrefix = import.prefix2?.element.name3;
+ if (import.hasCombinator) {
+ if (importPrefix == null && showName != null) {
+ _handleCombinators(import, showName);
+ return ImportLibraryElementResultImpl(importPrefix);
+ }
+ } else {
+ return ImportLibraryElementResultImpl(importPrefix);
+ }
}
}
- importLibrary(uri);
+ importLibrary(uri, prefix: prefix, showName: showName, useShow: useShow);
return ImportLibraryElementResultImpl(null);
}
String importLibraryWithAbsoluteUri(
Uri uri, {
String? prefix,
- String? shownName,
+ String? showName,
bool useShow = false,
}) {
return _importLibrary(
uri,
prefix: prefix,
- shownName: shownName,
+ showName: showName,
useShow: useShow,
forceAbsolute: true,
).uriText;
@@ -1663,13 +1680,13 @@
String importLibraryWithRelativeUri(
Uri uri, {
String? prefix,
- String? shownName,
+ String? showName,
bool useShow = false,
}) {
return _importLibrary(
uri,
prefix: prefix,
- shownName: shownName,
+ showName: showName,
useShow: useShow,
forceAbsolute: true,
forceRelative: true,
@@ -2281,6 +2298,44 @@
return uri.toString();
}
+ /// If the [import] already has combinators, update them to import [showName].
+ ///
+ /// If the combinator is a `show`, then [showName] will be added to the list
+ /// of shown names.
+ ///
+ /// If the combinator is a `hide`, then [showName] will be removed from the
+ /// list of hidden names.
+ void _handleCombinators(LibraryImport import, String showName) {
+ for (var show in import.showCombinators) {
+ var names = show.shownNames.toList();
+ if (!names.contains(showName)) {
+ names.add(showName);
+ names.sort();
+ addSimpleReplacement(
+ range.startOffsetEndOffset(show.offset, show.end),
+ 'show ${names.join(', ')}',
+ );
+ }
+ }
+ for (var hide in import.hideCombinators) {
+ var names = hide.hiddenNames.toList();
+ if (names.contains(showName)) {
+ names.remove(showName);
+ if (names.isEmpty) {
+ addSimpleReplacement(
+ range.startOffsetEndOffset(hide.offset - 1, hide.end),
+ '',
+ );
+ } else {
+ addSimpleReplacement(
+ range.startOffsetEndOffset(hide.offset, hide.end),
+ 'hide ${names.join(', ')}',
+ );
+ }
+ }
+ }
+ }
+
/// Arranges to have an import added for the library with the given [uri].
///
/// [uri] may be converted from an absolute URI to a relative URI depending on
@@ -2291,28 +2346,28 @@
/// If [prefix] is null, will use [importPrefixGenerator] to generate one or
/// reuse an existing prefix for this import.
///
- /// If [shownName] is supplied then any new import will show only this
+ /// 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? shownName,
+ String? showName,
bool isExplicitImport = true,
bool useShow = false,
bool forceAbsolute = false,
bool forceRelative = false,
}) {
var import = (libraryChangeBuilder ?? this).librariesToImport[uri];
- var shownNames = <List<String>>[];
- var hiddenNames = <List<String>>[];
+ var existingShownNames = <List<String>>[];
+ var existingHiddenNames = <List<String>>[];
if (import != null) {
if (prefix != null) {
import.prefixes.add(prefix);
}
- if (shownName != null) {
- import.ensureShown(shownName, useShow: useShow);
+ if (showName != null) {
+ import.ensureShown(showName, useShow: useShow);
}
// If this was an explicit import request, ensure the existing import
// is marked as such so it cannot be removed by other optimizations.
@@ -2342,9 +2397,9 @@
for (var combinator in element.combinators) {
switch (combinator) {
case ShowElementCombinator():
- shownNames.add(combinator.shownNames.toList());
+ existingShownNames.add(combinator.shownNames.toList());
case HideElementCombinator():
- hiddenNames.add(combinator.hiddenNames.toList());
+ existingHiddenNames.add(combinator.hiddenNames.toList());
}
}
}
@@ -2354,11 +2409,11 @@
uriText: uriText,
prefix: prefix ?? '',
isExplicitlyImported: isExplicitImport,
- shownNames: shownNames,
- hiddenNames: hiddenNames,
+ shownNames: existingShownNames,
+ hiddenNames: existingHiddenNames,
);
- if (shownName != null) {
- import.ensureShown(shownName, useShow: useShow);
+ if (showName != null) {
+ import.ensureShown(showName, useShow: useShow);
}
(libraryChangeBuilder ?? this).librariesToImport[uri] = import;
}
@@ -2802,3 +2857,13 @@
};
}
}
+
+extension on LibraryImport {
+ bool get hasCombinator => combinators.isNotEmpty;
+
+ Iterable<HideElementCombinator> get hideCombinators =>
+ combinators.whereType<HideElementCombinator>();
+
+ Iterable<ShowElementCombinator> get showCombinators =>
+ combinators.whereType<ShowElementCombinator>();
+}
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 2314e49..985c5e0 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
@@ -445,11 +445,28 @@
/// Ensures that the library with the given [uri] is imported.
///
- /// If there is already an import for the requested library, returns the
- /// import prefix of the existing import directive.
- ///
/// If there is no existing import, a new import is added.
- ImportLibraryElementResult importLibraryElement(Uri uri);
+ ///
+ /// If a [prefix] is provided new imports will use it in the import directive.
+ ///
+ /// If there is already an import for the requested library, returns the
+ /// import prefix of the existing import directive unless [showName] is
+ /// provided, in which case the rules below apply.
+ ///
+ /// If [showName] is provided, one of the following can occur:
+ /// - If the library is already imported with a prefix, a new import with no
+ /// prefix will be added.
+ /// - If the library is already imported without a prefix, the existing import
+ /// will be modified to show the given [showName] if it contains a combinator.
+ ///
+ /// If [useShow] is `true`, new imports will show only the given [showName],
+ /// instead of importing the library without a show clause.
+ ImportLibraryElementResult importLibraryElement(
+ Uri uri, {
+ String? prefix,
+ String? showName,
+ bool useShow = false,
+ });
/// Returns whether the given library [uri] is already imported or will be
/// imported by a scheduled edit.
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 7c873d6..78b15d9d 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
@@ -7,6 +7,7 @@
import 'package:analyzer/dart/element/element2.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
+import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/source/source_range.dart';
import 'package:analyzer/src/dart/element/inheritance_manager3.dart';
import 'package:analyzer/src/test_utilities/find_node.dart';
@@ -27,6 +28,7 @@
defineReflectiveTests(DartFileEditBuilderImplTest);
defineReflectiveTests(DartLinkedEditBuilderImplTest);
defineReflectiveTests(ImportLibraryTest);
+ defineReflectiveTests(ImportLibraryElementTest);
defineReflectiveTests(WriteOverrideTest);
});
}
@@ -2886,6 +2888,206 @@
}
@reflectiveTest
+class ImportLibraryElementTest extends AbstractContextTest
+ with DartChangeBuilderMixin {
+ Future<void> test_show_name_differentCombinators() async {
+ _createOther();
+ await _assertImportLibraryElement(
+ initialCode: '''
+import 'dart:aaa';
+import 'other.dart' hide B, C show A, D;
+
+class A {}
+''',
+ uriString: 'package:test/other.dart',
+ showName: 'B',
+ expectedCode: '''
+import 'dart:aaa';
+import 'other.dart' hide C show A, B, D;
+
+class A {}
+''',
+ );
+ }
+
+ Future<void> test_show_name_hideCombinator_multipleNames() async {
+ _createOther();
+ await _assertImportLibraryElement(
+ initialCode: '''
+import 'dart:aaa';
+import 'other.dart' hide A, B;
+
+class A {}
+''',
+ uriString: 'package:test/other.dart',
+ showName: 'B',
+ expectedCode: '''
+import 'dart:aaa';
+import 'other.dart' hide A;
+
+class A {}
+''',
+ );
+ }
+
+ Future<void> test_show_name_hideCombinator_name() async {
+ _createOther();
+ await _assertImportLibraryElement(
+ initialCode: '''
+import 'dart:aaa';
+import 'other.dart' hide B;
+
+class A {}
+''',
+ uriString: 'package:test/other.dart',
+ showName: 'B',
+ expectedCode: '''
+import 'dart:aaa';
+import 'other.dart';
+
+class A {}
+''',
+ );
+ }
+
+ Future<void> test_show_name_multipleHide() async {
+ _createOther();
+ await _assertImportLibraryElement(
+ initialCode: '''
+import 'dart:aaa';
+import 'other.dart' hide B hide A, D;
+
+class A {}
+''',
+ uriString: 'package:test/other.dart',
+ showName: 'B',
+ expectedCode: '''
+import 'dart:aaa';
+import 'other.dart' hide A, D;
+
+class A {}
+''',
+ );
+ }
+
+ Future<void> test_show_name_multipleShow() async {
+ _createOther();
+ await _assertImportLibraryElement(
+ initialCode: '''
+import 'dart:aaa';
+import 'other.dart' show B show A, D;
+
+class A {}
+''',
+ uriString: 'package:test/other.dart',
+ showName: 'B',
+ expectedCode: '''
+import 'dart:aaa';
+import 'other.dart' show B show A, B, D;
+
+class A {}
+''',
+ );
+ }
+
+ Future<void> test_show_name_no_combinator() async {
+ _createOther();
+ await _assertImportLibraryElement(
+ initialCode: '''
+import 'dart:aaa';
+
+class A {}
+''',
+ uriString: 'package:test/other.dart',
+ showName: 'B',
+ expectedCode: '''
+import 'dart:aaa';
+
+import 'package:test/other.dart';
+
+class A {}
+''',
+ );
+ }
+
+ Future<void> test_show_name_prefix() async {
+ _createOther();
+ await _assertImportLibraryElement(
+ initialCode: '''
+import 'dart:aaa';
+
+import 'package:test/other.dart' as b;
+
+class A {}
+''',
+ uriString: 'package:test/other.dart',
+ showName: 'B',
+ expectedCode: '''
+import 'dart:aaa';
+
+import 'package:test/other.dart' as b;
+
+class A {}
+''',
+ );
+ }
+
+ Future<void> test_show_name_showCombinator() async {
+ _createOther();
+ await _assertImportLibraryElement(
+ initialCode: '''
+import 'dart:aaa';
+import 'other.dart' show A;
+
+class A {}
+''',
+ uriString: 'package:test/other.dart',
+ showName: 'B',
+ expectedCode: '''
+import 'dart:aaa';
+import 'other.dart' show A, B;
+
+class A {}
+''',
+ );
+ }
+
+ Future<void> _assertImportLibraryElement({
+ required String initialCode,
+ required String uriString,
+ required String expectedCode,
+ String? prefix,
+ String? showName,
+ bool createEditsForImports = true,
+ }) async {
+ var path = convertPath('/home/test/lib/test.dart');
+ addSource(path, initialCode);
+ var builder = await newBuilder();
+ await builder.addDartFileEdit(path,
+ createEditsForImports: createEditsForImports, (builder) {
+ var uri = Uri.parse(uriString);
+ builder.importLibraryElement(uri, prefix: prefix, showName: showName);
+ });
+
+ var resultCode = initialCode;
+ var edits = getEdits(builder);
+ for (var edit in edits) {
+ resultCode = edit.apply(resultCode);
+ }
+ expect(resultCode, expectedCode);
+ }
+
+ File _createOther() {
+ return newFile(convertPath('/home/test/lib/other.dart'), '''
+class A {}
+class B {}
+class C {}
+class D {}
+''');
+ }
+}
+
+@reflectiveTest
class ImportLibraryTest extends AbstractContextTest
with DartChangeBuilderMixin {
Future<void> test_dart_afterDart_last() async {