diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart index 634324a3a88..372522063b1 100644 --- a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart +++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart @@ -357,11 +357,15 @@ class CompletionHandler final key = _createImportedSymbolKey(nameKey, declaringUri); final importingUris = alreadyImportedSymbols[key]; - // Keep it only if there are either: - // - no URIs importing it - // - the URIs importing it include this one + // Keep it only if: + // - no existing imports include it + // (in which case all libraries will be offered as + // auto-imports) + // - this is the first imported URI that includes it + // (we don't want to repeat it for each imported library that + // includes it) return importingUris == null || - importingUris.contains('${library.uri}'); + importingUris.first == '${library.uri}'; }).map((item) => declarationToCompletionItem( capabilities, unit.path!, diff --git a/pkg/analysis_server/test/lsp/completion_dart_test.dart b/pkg/analysis_server/test/lsp/completion_dart_test.dart index 920891e4cba..d6ba22ef787 100644 --- a/pkg/analysis_server/test/lsp/completion_dart_test.dart +++ b/pkg/analysis_server/test/lsp/completion_dart_test.dart @@ -1192,6 +1192,95 @@ main() { ''')); } + Future + test_suggestionSets_doesNotDuplicate_importedViaMultipleLibraries() async { + // An item that's already imported through multiple libraries that + // export it should not result in multiple entries. + newFile( + join(projectFolderPath, 'lib/source_file.dart'), + content: ''' + class MyExportedClass {} + ''', + ); + newFile( + join(projectFolderPath, 'lib/reexport1.dart'), + content: ''' + export 'source_file.dart'; + ''', + ); + newFile( + join(projectFolderPath, 'lib/reexport2.dart'), + content: ''' + export 'source_file.dart'; + ''', + ); + + final content = ''' +import 'reexport1.dart'; +import 'reexport2.dart'; + +main() { + MyExported^ +} + '''; + + final initialAnalysis = waitForAnalysisComplete(); + await initialize( + workspaceCapabilities: + withApplyEditSupport(emptyWorkspaceClientCapabilities)); + await openFile(mainFileUri, withoutMarkers(content)); + await initialAnalysis; + + final res = await getCompletion(mainFileUri, positionFromMarker(content)); + + final completions = res.where((c) => c.label == 'MyExportedClass').toList(); + expect(completions, hasLength(1)); + } + + Future + test_suggestionSets_doesNotDuplicate_importedViaSingleLibrary() async { + // An item that's already imported through a library that exports it + // should not result in multiple entries. + newFile( + join(projectFolderPath, 'lib/source_file.dart'), + content: ''' + class MyExportedClass {} + ''', + ); + newFile( + join(projectFolderPath, 'lib/reexport1.dart'), + content: ''' + export 'source_file.dart'; + ''', + ); + newFile( + join(projectFolderPath, 'lib/reexport2.dart'), + content: ''' + export 'source_file.dart'; + ''', + ); + + final content = ''' +import 'reexport1.dart'; + +main() { + MyExported^ +} + '''; + + final initialAnalysis = waitForAnalysisComplete(); + await initialize( + workspaceCapabilities: + withApplyEditSupport(emptyWorkspaceClientCapabilities)); + await openFile(mainFileUri, withoutMarkers(content)); + await initialAnalysis; + + final res = await getCompletion(mainFileUri, positionFromMarker(content)); + + final completions = res.where((c) => c.label == 'MyExportedClass').toList(); + expect(completions, hasLength(1)); + } + Future test_suggestionSets_doesNotFilterSymbolsWithSameName() async { // Classes here are not re-exports, so should not be filtered out. newFile(