Version 2.13.0-101.0.dev

Merge commit 'd901f7326a86cecf53ae312a31d2d435d3c773e5' into 'dev'
diff --git a/pkg/analysis_server/lib/src/analysis_server.dart b/pkg/analysis_server/lib/src/analysis_server.dart
index c7afc20..d32f2dd 100644
--- a/pkg/analysis_server/lib/src/analysis_server.dart
+++ b/pkg/analysis_server/lib/src/analysis_server.dart
@@ -54,6 +54,7 @@
 import 'package:analyzer_plugin/protocol/protocol_common.dart' hide Element;
 import 'package:analyzer_plugin/src/utilities/navigation/navigation.dart';
 import 'package:analyzer_plugin/utilities/navigation/navigation_dart.dart';
+import 'package:http/http.dart' as http;
 import 'package:telemetry/crash_reporting.dart';
 import 'package:telemetry/telemetry.dart' as telemetry;
 import 'package:watcher/watcher.dart';
@@ -127,6 +128,7 @@
     DartSdkManager sdkManager,
     CrashReportingAttachmentsBuilder crashReportingAttachmentsBuilder,
     InstrumentationService instrumentationService, {
+    http.Client httpClient,
     RequestStatisticsHelper requestStatistics,
     DiagnosticServer diagnosticServer,
     this.detachableFileSystemManager,
@@ -137,6 +139,7 @@
           crashReportingAttachmentsBuilder,
           baseResourceProvider,
           instrumentationService,
+          httpClient,
           NotificationManager(channel, baseResourceProvider.pathContext),
           requestStatistics: requestStatistics,
         ) {
@@ -421,6 +424,14 @@
 
   /// Set the priority files to the given [files].
   void setPriorityFiles(String requestId, List<String> files) {
+    bool isPubspec(String filePath) =>
+        file_paths.isPubspecYaml(resourceProvider.pathContext, filePath);
+
+    // When a pubspec is opened, trigger package name caching for completion.
+    if (!pubPackageService.isRunning && files.any(isPubspec)) {
+      pubPackageService.beginPackageNamePreload();
+    }
+
     priorityFiles.clear();
     priorityFiles.addAll(files);
     // Set priority files in drivers.
@@ -429,7 +440,12 @@
     });
   }
 
+  @override
   Future<void> shutdown() {
+    super.shutdown();
+
+    pubApi.close();
+
     if (options.analytics != null) {
       options.analytics
           .waitForLastPing(timeout: Duration(milliseconds: 200))
diff --git a/pkg/analysis_server/lib/src/analysis_server_abstract.dart b/pkg/analysis_server/lib/src/analysis_server_abstract.dart
index 0853e07..22595fe 100644
--- a/pkg/analysis_server/lib/src/analysis_server_abstract.dart
+++ b/pkg/analysis_server/lib/src/analysis_server_abstract.dart
@@ -4,6 +4,7 @@
 
 import 'dart:core';
 import 'dart:io' as io;
+import 'dart:io';
 
 import 'package:analysis_server/src/analysis_server.dart';
 import 'package:analysis_server/src/collections.dart';
@@ -15,6 +16,8 @@
 import 'package:analysis_server/src/server/crash_reporting_attachments.dart';
 import 'package:analysis_server/src/server/diagnostic_server.dart';
 import 'package:analysis_server/src/services/correction/namespace.dart';
+import 'package:analysis_server/src/services/pub/pub_api.dart';
+import 'package:analysis_server/src/services/pub/pub_package_service.dart';
 import 'package:analysis_server/src/services/search/element_visitors.dart';
 import 'package:analysis_server/src/services/search/search_engine.dart';
 import 'package:analysis_server/src/services/search/search_engine_internal.dart';
@@ -42,6 +45,8 @@
 import 'package:analyzer/src/generated/sdk.dart';
 import 'package:analyzer/src/services/available_declarations.dart';
 import 'package:analyzer/src/util/file_paths.dart' as file_paths;
+import 'package:http/http.dart' as http;
+import 'package:meta/meta.dart';
 
 /// Implementations of [AbstractAnalysisServer] implement a server that listens
 /// on a [CommunicationChannel] for analysis messages and process them.
@@ -93,6 +98,12 @@
   /// or `null` if the initial analysis is not yet complete
   ServerPerformance performanceAfterStartup;
 
+  /// A client for making requests to the pub.dev API.
+  final PubApi pubApi;
+
+  /// A service for fetching pub.dev package details.
+  PubPackageService pubPackageService;
+
   /// The class into which performance information is currently being recorded.
   /// During startup, this will be the same as [performanceDuringStartup]
   /// and after startup is complete, this switches to [performanceAfterStartup].
@@ -126,9 +137,14 @@
     this.crashReportingAttachmentsBuilder,
     ResourceProvider baseResourceProvider,
     this.instrumentationService,
+    http.Client httpClient,
     this.notificationManager, {
     this.requestStatistics,
-  }) : resourceProvider = OverlayResourceProvider(baseResourceProvider) {
+  })  : resourceProvider = OverlayResourceProvider(baseResourceProvider),
+        pubApi = PubApi(instrumentationService, httpClient,
+            Platform.environment['PUB_HOSTED_URL']) {
+    pubPackageService =
+        PubPackageService(instrumentationService, baseResourceProvider, pubApi);
     performance = performanceDuringStartup;
 
     pluginManager = PluginManager(
@@ -398,6 +414,11 @@
     bool fatal = false,
   });
 
+  @mustCallSuper
+  void shutdown() {
+    pubPackageService.shutdown();
+  }
+
   /// Return the path to the location of the byte store on disk, or `null` if
   /// there is no on-disk byte store.
   String _getByteStorePath() {
diff --git a/pkg/analysis_server/lib/src/domain_completion.dart b/pkg/analysis_server/lib/src/domain_completion.dart
index a9c9aa1..ea54b61 100644
--- a/pkg/analysis_server/lib/src/domain_completion.dart
+++ b/pkg/analysis_server/lib/src/domain_completion.dart
@@ -163,7 +163,7 @@
       var generator = FixDataGenerator(provider);
       return generator.getSuggestions(file, offset);
     } else if (file_paths.isPubspecYaml(pathContext, file)) {
-      var generator = PubspecGenerator(provider);
+      var generator = PubspecGenerator(provider, server.pubPackageService);
       return generator.getSuggestions(file, offset);
     }
     return const YamlCompletionResults.empty();
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 e581d84..4268054 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart
@@ -115,7 +115,8 @@
         } else if (file_paths.isFixDataYaml(pathContext, path.result)) {
           generator = FixDataGenerator(server.resourceProvider);
         } else if (file_paths.isPubspecYaml(pathContext, path.result)) {
-          generator = PubspecGenerator(server.resourceProvider);
+          generator = PubspecGenerator(
+              server.resourceProvider, server.pubPackageService);
         }
         if (generator != null) {
           serverResultsFuture = _getServerYamlItems(
diff --git a/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart b/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
index 6a5bbc1..46e1fe1 100644
--- a/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
+++ b/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
@@ -43,10 +43,11 @@
 import 'package:analyzer/src/dart/analysis/driver.dart' as nd;
 import 'package:analyzer/src/dart/analysis/status.dart' as nd;
 import 'package:analyzer/src/generated/sdk.dart';
+import 'package:analyzer/src/util/file_paths.dart' as file_paths;
 import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin;
 import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
 import 'package:analyzer_plugin/src/protocol/protocol_internal.dart' as plugin;
-import 'package:path/path.dart';
+import 'package:http/http.dart' as http;
 import 'package:watcher/watcher.dart';
 
 /// Instances of the class [LspAnalysisServer] implement an LSP-based server
@@ -134,6 +135,7 @@
     DartSdkManager sdkManager,
     CrashReportingAttachmentsBuilder crashReportingAttachmentsBuilder,
     InstrumentationService instrumentationService, {
+    http.Client httpClient,
     DiagnosticServer diagnosticServer,
   }) : super(
           options,
@@ -142,6 +144,7 @@
           crashReportingAttachmentsBuilder,
           baseResourceProvider,
           instrumentationService,
+          httpClient,
           LspNotificationManager(channel, baseResourceProvider.pathContext),
         ) {
     notificationManager.server = this;
@@ -185,8 +188,14 @@
   RefactoringWorkspace get refactoringWorkspace => _refactoringWorkspace ??=
       RefactoringWorkspace(driverMap.values, searchEngine);
 
-  void addPriorityFile(String path) {
-    final didAdd = priorityFiles.add(path);
+  void addPriorityFile(String filePath) {
+    // When a pubspec is opened, trigger package name caching for completion.
+    if (!pubPackageService.isRunning &&
+        file_paths.isPubspecYaml(resourceProvider.pathContext, filePath)) {
+      pubPackageService.beginPackageNamePreload();
+    }
+
+    final didAdd = priorityFiles.add(filePath);
     assert(didAdd);
     if (didAdd) {
       _updateDriversAndPluginsPriorityFiles();
@@ -616,7 +625,10 @@
     return MessageActionItem.fromJson(response.result);
   }
 
+  @override
   Future<void> shutdown() {
+    super.shutdown();
+
     // Defer closing the channel so that the shutdown response can be sent and
     // logged.
     Future(() {
@@ -664,13 +676,15 @@
       ..addAll(_temporaryAnalysisRoots.values);
 
     final excludedPaths = clientConfiguration.analysisExcludedFolders
-        .expand((excludePath) => isAbsolute(excludePath)
+        .expand((excludePath) => resourceProvider.pathContext
+                .isAbsolute(excludePath)
             ? [excludePath]
             // Apply the relative path to each open workspace folder.
             // TODO(dantup): Consider supporting per-workspace config by
             // calling workspace/configuration whenever workspace folders change
             // and caching the config for each one.
-            : _explicitAnalysisRoots.map((root) => join(root, excludePath)))
+            : _explicitAnalysisRoots.map(
+                (root) => resourceProvider.pathContext.join(root, excludePath)))
         .toSet();
 
     // If the roots didn't actually change from the last time they were set
diff --git a/pkg/analysis_server/lib/src/lsp/mapping.dart b/pkg/analysis_server/lib/src/lsp/mapping.dart
index 720155c..7125eaa 100644
--- a/pkg/analysis_server/lib/src/lsp/mapping.dart
+++ b/pkg/analysis_server/lib/src/lsp/mapping.dart
@@ -308,12 +308,10 @@
   return lsp.CompletionItem(
     label: label,
     kind: completionKind,
-    tags: supportedTags.isNotEmpty
-        ? [
-            if (supportsDeprecatedTag && declaration.isDeprecated)
-              lsp.CompletionItemTag.Deprecated
-          ]
-        : null,
+    tags: nullIfEmpty([
+      if (supportsDeprecatedTag && declaration.isDeprecated)
+        lsp.CompletionItemTag.Deprecated
+    ]),
     commitCharacters:
         includeCommitCharacters ? lsp.dartCompletionCommitCharacters : null,
     detail: getDeclarationCompletionDetail(declaration, completionKind,
@@ -631,6 +629,8 @@
   );
 }
 
+List<T> nullIfEmpty<T>(List<T> items) => items.isEmpty ? null : items;
+
 /// Returns the file system path for a TextDocumentIdentifier.
 ErrorOr<String> pathOfDoc(lsp.TextDocumentIdentifier doc) =>
     pathOfUri(Uri.tryParse(doc?.uri));
@@ -782,6 +782,8 @@
         return const [lsp.CompletionItemKind.Variable];
       case server.CompletionSuggestionKind.PARAMETER:
         return const [lsp.CompletionItemKind.Value];
+      case server.CompletionSuggestionKind.PACKAGE_NAME:
+        return const [lsp.CompletionItemKind.Module];
       default:
         return const [];
     }
@@ -894,12 +896,10 @@
   return lsp.CompletionItem(
     label: label,
     kind: completionKind,
-    tags: supportedTags.isNotEmpty
-        ? [
-            if (supportsDeprecatedTag && suggestion.isDeprecated)
-              lsp.CompletionItemTag.Deprecated
-          ]
-        : null,
+    tags: nullIfEmpty([
+      if (supportsDeprecatedTag && suggestion.isDeprecated)
+        lsp.CompletionItemTag.Deprecated
+    ]),
     commitCharacters:
         includeCommitCharacters ? dartCompletionCommitCharacters : null,
     data: resolutionData,
diff --git a/pkg/analysis_server/lib/src/services/completion/yaml/analysis_options_generator.dart b/pkg/analysis_server/lib/src/services/completion/yaml/analysis_options_generator.dart
index cd4d7bd..7f60edd 100644
--- a/pkg/analysis_server/lib/src/services/completion/yaml/analysis_options_generator.dart
+++ b/pkg/analysis_server/lib/src/services/completion/yaml/analysis_options_generator.dart
@@ -47,7 +47,7 @@
   /// Initialize a newly created suggestion generator for analysis options
   /// files.
   AnalysisOptionsGenerator(ResourceProvider resourceProvider)
-      : super(resourceProvider);
+      : super(resourceProvider, null);
 
   @override
   Producer get topLevelProducer => analysisOptionsProducer;
diff --git a/pkg/analysis_server/lib/src/services/completion/yaml/fix_data_generator.dart b/pkg/analysis_server/lib/src/services/completion/yaml/fix_data_generator.dart
index 81bc727..15ef594 100644
--- a/pkg/analysis_server/lib/src/services/completion/yaml/fix_data_generator.dart
+++ b/pkg/analysis_server/lib/src/services/completion/yaml/fix_data_generator.dart
@@ -72,7 +72,8 @@
   }));
 
   /// Initialize a newly created suggestion generator for fix data files.
-  FixDataGenerator(ResourceProvider resourceProvider) : super(resourceProvider);
+  FixDataGenerator(ResourceProvider resourceProvider)
+      : super(resourceProvider, null);
 
   @override
   Producer get topLevelProducer => fixDataProducer;
diff --git a/pkg/analysis_server/lib/src/services/completion/yaml/producer.dart b/pkg/analysis_server/lib/src/services/completion/yaml/producer.dart
index 31c1975..06a5401 100644
--- a/pkg/analysis_server/lib/src/services/completion/yaml/producer.dart
+++ b/pkg/analysis_server/lib/src/services/completion/yaml/producer.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:analysis_server/src/protocol_server.dart';
+import 'package:analysis_server/src/services/pub/pub_package_service.dart';
 import 'package:analyzer/file_system/file_system.dart';
 import 'package:meta/meta.dart';
 import 'package:path/path.dart' as path;
@@ -179,6 +180,12 @@
       CompletionSuggestion(CompletionSuggestionKind.IDENTIFIER, relevance,
           identifier, identifier.length, 0, false, false);
 
+  /// A utility method used to create a suggestion for the package [packageName].
+  CompletionSuggestion packageName(String packageName,
+          {int relevance = 1000}) =>
+      CompletionSuggestion(CompletionSuggestionKind.PACKAGE_NAME, relevance,
+          packageName, packageName.length, 0, false, false);
+
   /// Return the completion suggestions appropriate to this location.
   Iterable<CompletionSuggestion> suggestions(YamlCompletionRequest request);
 }
@@ -188,6 +195,9 @@
   /// The resource provider used to access the file system.
   final ResourceProvider resourceProvider;
 
+  /// The Pub package service used for looking up package names/versions.
+  final PubPackageService pubPackageService;
+
   /// The absolute path of the file in which completions are being requested.
   final String filePath;
 
@@ -198,5 +208,6 @@
   YamlCompletionRequest(
       {@required this.filePath,
       @required this.precedingText,
-      @required this.resourceProvider});
+      @required this.resourceProvider,
+      @required this.pubPackageService});
 }
diff --git a/pkg/analysis_server/lib/src/services/completion/yaml/pubspec_generator.dart b/pkg/analysis_server/lib/src/services/completion/yaml/pubspec_generator.dart
index 2b3794f..d167c72 100644
--- a/pkg/analysis_server/lib/src/services/completion/yaml/pubspec_generator.dart
+++ b/pkg/analysis_server/lib/src/services/completion/yaml/pubspec_generator.dart
@@ -2,10 +2,26 @@
 // 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 'package:analysis_server/src/protocol_server.dart';
 import 'package:analysis_server/src/services/completion/yaml/producer.dart';
 import 'package:analysis_server/src/services/completion/yaml/yaml_completion_generator.dart';
+import 'package:analysis_server/src/services/pub/pub_package_service.dart';
 import 'package:analyzer/file_system/file_system.dart';
 
+/// An object that represents the location of a package name.
+class PubPackageNameProducer extends Producer {
+  const PubPackageNameProducer();
+
+  @override
+  Iterable<CompletionSuggestion> suggestions(
+      YamlCompletionRequest request) sync* {
+    final cachedPackages = request.pubPackageService.cachedPackages;
+    var relevance = cachedPackages.length;
+    yield* cachedPackages.map((package) =>
+        packageName('${package.packageName}: ', relevance: relevance--));
+  }
+}
+
 /// A completion generator that can produce completion suggestions for pubspec
 /// files.
 class PubspecGenerator extends YamlCompletionGenerator {
@@ -24,8 +40,8 @@
       'flutter': EmptyProducer(),
       'sdk': EmptyProducer(),
     }),
-    'dependencies': EmptyProducer(),
-    'dev_dependencies': EmptyProducer(),
+    'dependencies': PubPackageNameProducer(),
+    'dev_dependencies': PubPackageNameProducer(),
     // TODO(brianwilkerson) Suggest names already listed under 'dependencies'
     //  and 'dev_dependencies'.
     'dependency_overrides': EmptyProducer(),
@@ -78,7 +94,9 @@
   });
 
   /// Initialize a newly created suggestion generator for pubspec files.
-  PubspecGenerator(ResourceProvider resourceProvider) : super(resourceProvider);
+  PubspecGenerator(
+      ResourceProvider resourceProvider, PubPackageService pubPackageService)
+      : super(resourceProvider, pubPackageService);
 
   @override
   Producer get topLevelProducer => pubspecProducer;
diff --git a/pkg/analysis_server/lib/src/services/completion/yaml/yaml_completion_generator.dart b/pkg/analysis_server/lib/src/services/completion/yaml/yaml_completion_generator.dart
index 2746a76..cfa4242e 100644
--- a/pkg/analysis_server/lib/src/services/completion/yaml/yaml_completion_generator.dart
+++ b/pkg/analysis_server/lib/src/services/completion/yaml/yaml_completion_generator.dart
@@ -4,6 +4,7 @@
 
 import 'package:analysis_server/src/protocol_server.dart';
 import 'package:analysis_server/src/services/completion/yaml/producer.dart';
+import 'package:analysis_server/src/services/pub/pub_package_service.dart';
 import 'package:analysis_server/src/utilities/extensions/yaml.dart';
 import 'package:analyzer/file_system/file_system.dart';
 import 'package:yaml/yaml.dart';
@@ -15,9 +16,13 @@
   /// completion was requested.
   final ResourceProvider resourceProvider;
 
+  /// A service used for collecting Pub package information. May be null for
+  /// generators that do not use Pub packages.
+  final PubPackageService pubPackageService;
+
   /// Initialize a newly created generator to use the [resourceProvider] to
   /// access the content of the file in which completion was requested.
-  YamlCompletionGenerator(this.resourceProvider);
+  YamlCompletionGenerator(this.resourceProvider, this.pubPackageService);
 
   /// Return the producer used to produce suggestions at the top-level of the
   /// file.
@@ -56,7 +61,8 @@
     var request = YamlCompletionRequest(
         filePath: filePath,
         precedingText: precedingText,
-        resourceProvider: resourceProvider);
+        resourceProvider: resourceProvider,
+        pubPackageService: pubPackageService);
     return getSuggestionsForPath(request, nodePath, offset);
   }
 
@@ -87,7 +93,7 @@
   /// Return the result of parsing the file [content] into a YAML node.
   YamlNode _parseYaml(String content) {
     try {
-      return loadYamlNode(content);
+      return loadYamlNode(content, recover: true);
     } on YamlException {
       // If the file can't be parsed, then fall through to return `null`.
     }
diff --git a/pkg/analysis_server/lib/src/services/pub/pub_api.dart b/pkg/analysis_server/lib/src/services/pub/pub_api.dart
new file mode 100644
index 0000000..7f43aa0
--- /dev/null
+++ b/pkg/analysis_server/lib/src/services/pub/pub_api.dart
@@ -0,0 +1,147 @@
+// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
+// 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 'dart:convert';
+import 'dart:io';
+
+import 'package:analyzer/instrumentation/service.dart';
+import 'package:http/http.dart' as http;
+import 'package:meta/meta.dart';
+
+/// A class for interacting with the Pub API.
+///
+/// https://github.com/dart-lang/pub/blob/master/doc/repository-spec-v2.md
+///
+/// Failed requests will automatically be retried.
+class PubApi {
+  static const packageNameListPath = '/api/package-name-completion-data';
+
+  /// Maximum number of retries if requests fail.
+  static const maxFailedRequests = 5;
+
+  /// Initial wait period between retries. Doubled for each failure (but restarts
+  /// from this value for each new request).
+  static int _failedRetryInitialDelaySeconds = 1;
+
+  @visibleForTesting
+  static set failedRetryInitialDelaySeconds(int value) {
+    _failedRetryInitialDelaySeconds = value;
+  }
+
+  final InstrumentationService instrumentationService;
+  final http.Client httpClient;
+  final String _pubHostedUrl;
+  final _headers = {
+    'Accept': 'application/vnd.pub.v2+json',
+    'Accept-Encoding': 'gzip',
+    'User-Agent': 'Dart Analysis Server/${Platform.version.split(' ').first}'
+        ' (+https://github.com/dart-lang/sdk)',
+  };
+
+  PubApi(this.instrumentationService, http.Client httpClient,
+      String envPubHostedUrl)
+      : httpClient =
+            httpClient != null ? _NoCloseHttpClient(httpClient) : http.Client(),
+        _pubHostedUrl = _validPubHostedUrl(envPubHostedUrl);
+
+  /// Fetches a list of package names from the Pub API.
+  ///
+  /// Failed requests will be retried a number of times. If no successful response
+  /// is received, will return null.
+  Future<List<PubApiPackage>> allPackages() async {
+    final json = await _getJson('$_pubHostedUrl$packageNameListPath');
+    if (json == null) {
+      return null;
+    }
+
+    final packageNames = json['packages'];
+    return packageNames is List
+        ? packageNames.map((name) => PubApiPackage(name)).toList()
+        : null;
+  }
+
+  void close() {
+    httpClient.close();
+  }
+
+  /// Calls a pub API and decodes the resulting JSON.
+  ///
+  /// Automatically retries the request for specific types of failures after
+  /// [_failedRetryInitialDelaySeconds] doubling each time. After [maxFailedRequests]
+  /// requests or upon a 4XX response, will return `null` and not retry.
+  Future<Map<String, dynamic>> _getJson(String url) async {
+    var requestCount = 0;
+    var retryAfterSeconds = _failedRetryInitialDelaySeconds;
+    while (requestCount++ < maxFailedRequests) {
+      try {
+        final response =
+            await httpClient.get(Uri.parse(url), headers: _headers);
+        if (response.statusCode == 200) {
+          instrumentationService.logInfo('Pub API request successful for $url');
+          return jsonDecode(response.body);
+        } else if (response.statusCode >= 400 && response.statusCode < 500) {
+          // Do not retry 4xx responses.
+          instrumentationService.logError(
+              'Pub API returned ${response.statusCode} ${response.reasonPhrase} '
+              'for $url. Not retrying.');
+          return null;
+        }
+        instrumentationService.logError(
+            'Pub API returned ${response.statusCode} ${response.reasonPhrase} '
+            'for $url on attempt $requestCount');
+      } catch (e) {
+        if (e is! IOException && e is! FormatException) {
+          instrumentationService
+              .logError('Error calling pub API for $url. Not retrying. $e');
+          return null;
+        }
+        instrumentationService.logError('Error calling pub API for $url: $e');
+      }
+      if (requestCount >= maxFailedRequests) {
+        instrumentationService
+            .logInfo('Pub API request failed after $requestCount requests');
+      } else {
+        // Sleep before the next try.
+        await Future.delayed(Duration(seconds: retryAfterSeconds));
+        retryAfterSeconds *= 2;
+      }
+    }
+    return null;
+  }
+
+  /// Returns a valid Pub base URL from [envPubHostedUrl] if valid, otherwise using
+  /// the default 'https://pub.dartlang.org'.
+  static String _validPubHostedUrl(String envPubHostedUrl) {
+    final validUrl = envPubHostedUrl != null &&
+            (Uri.tryParse(envPubHostedUrl)?.isAbsolute ?? false)
+        ? envPubHostedUrl
+        : 'https://pub.dartlang.org';
+
+    // Discard any trailing slashes, as all API paths start with them.
+    return validUrl.endsWith('/')
+        ? validUrl.substring(0, validUrl.length - 1)
+        : validUrl;
+  }
+}
+
+class PubApiPackage {
+  final String packageName;
+
+  PubApiPackage(this.packageName);
+}
+
+/// A wrapper over a package:http Client that does not pass on calls to [close].
+///
+/// This is used to prevent the server closing a client that may be provided to
+/// it (while still allowing it to close any client it creates itself).
+class _NoCloseHttpClient extends http.BaseClient {
+  final http.Client client;
+
+  _NoCloseHttpClient(this.client);
+
+  @override
+  Future<http.StreamedResponse> send(http.BaseRequest request) =>
+      client.send(request);
+}
diff --git a/pkg/analysis_server/lib/src/services/pub/pub_package_service.dart b/pkg/analysis_server/lib/src/services/pub/pub_package_service.dart
new file mode 100644
index 0000000..aca37e4
--- /dev/null
+++ b/pkg/analysis_server/lib/src/services/pub/pub_package_service.dart
@@ -0,0 +1,173 @@
+// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
+// 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 'dart:convert';
+
+import 'package:analysis_server/src/services/pub/pub_api.dart';
+import 'package:analyzer/file_system/file_system.dart';
+import 'package:analyzer/file_system/physical_file_system.dart';
+import 'package:analyzer/instrumentation/service.dart';
+import 'package:meta/meta.dart';
+
+/// Information about Pub packages that can be converted to/from JSON and
+/// cached to disk.
+class PackageDetailsCache {
+  static const cacheVersion = 2;
+  static const maxCacheAge = Duration(hours: 18);
+  final Map<String, PubPackage> packages;
+  DateTime lastUpdatedUtc;
+
+  PackageDetailsCache._(this.packages, DateTime lastUpdated)
+      : lastUpdatedUtc = lastUpdated.toUtc();
+
+  Duration get cacheTimeRemaining {
+    final cacheAge = DateTime.now().toUtc().difference(lastUpdatedUtc);
+    final cacheTimeRemaining = maxCacheAge - cacheAge;
+    return cacheTimeRemaining < Duration.zero
+        ? Duration.zero
+        : cacheTimeRemaining;
+  }
+
+  Map<String, Object> toJson() {
+    return {
+      'version': cacheVersion,
+      'lastUpdated': lastUpdatedUtc.toIso8601String(),
+      'packages': packages.values.toList(),
+    };
+  }
+
+  static PackageDetailsCache empty() {
+    return PackageDetailsCache._({}, DateTime.utc(2000));
+  }
+
+  static PackageDetailsCache fromApiResults(List<PubApiPackage> apiPackages) {
+    final packages = Map.fromEntries(apiPackages.map((package) =>
+        MapEntry(package.packageName, PubPackage.fromName(package))));
+
+    return PackageDetailsCache._(packages, DateTime.now().toUtc());
+  }
+
+  /// Deserialises cached package data from JSON.
+  ///
+  /// If the JSON version does not match the current version, will return null.
+  static PackageDetailsCache fromJson(Map<String, Object> json) {
+    if (json['version'] != cacheVersion) {
+      return null;
+    }
+
+    final packagesJson = json['packages'] as List<Object>;
+    final packages = packagesJson.map((json) => PubPackage.fromJson(json));
+    final packageMap = Map.fromEntries(
+        packages.map((package) => MapEntry(package.packageName, package)));
+    return PackageDetailsCache._(
+        packageMap, DateTime.parse(json['lastUpdated']));
+  }
+}
+
+/// Information about a single Pub package.
+class PubPackage {
+  String packageName;
+
+  PubPackage.fromJson(Map<String, Object> json)
+      : packageName = json['packageName'];
+
+  PubPackage.fromName(PubApiPackage package)
+      : packageName = package.packageName;
+
+  Map<String, Object> toJson() {
+    return {
+      if (packageName != null) 'packageName': packageName,
+    };
+  }
+}
+
+/// A service for providing Pub package information.
+///
+/// Uses a [PubApi] to communicate with Pub and caches to disk using [cacheResourceProvider].
+class PubPackageService {
+  final InstrumentationService _instrumentationService;
+  final PubApi _api;
+  Timer _nextRequestTimer;
+
+  /// [ResourceProvider] used for caching. This should generally be a
+  /// [PhysicalResourceProvider] outside of tests.
+  final ResourceProvider cacheResourceProvider;
+
+  /// The current cache of package information. Initiailly null, but overwritten
+  /// after first read of cache from disk or fetch from the API.
+  @visibleForTesting
+  PackageDetailsCache packageCache;
+
+  PubPackageService(
+      this._instrumentationService, this.cacheResourceProvider, this._api);
+
+  /// Gets the last set of package results or an empty List if no results.
+  List<PubPackage> get cachedPackages =>
+      packageCache?.packages?.values?.toList() ?? [];
+
+  bool get isRunning => _nextRequestTimer != null;
+
+  @visibleForTesting
+  File get packageCacheFile {
+    final cacheFolder = cacheResourceProvider
+        .getStateLocation('.pub-package-details-cache')
+          ..create();
+    return cacheFolder.getChildAssumingFile('packages.json');
+  }
+
+  /// Begin a request to pre-load the package name list.
+  void beginPackageNamePreload() {
+    // If first time, try to read from disk.
+    packageCache ??= readDiskCache() ?? PackageDetailsCache.empty();
+
+    // If there is no queued request, initialize one when the current cache expires.
+    _nextRequestTimer ??=
+        Timer(packageCache.cacheTimeRemaining, _fetchFromServer);
+  }
+
+  PubPackage cachedPackageDetails(String packageName) =>
+      packageCache.packages[packageName];
+
+  @visibleForTesting
+  PackageDetailsCache readDiskCache() {
+    final file = packageCacheFile;
+    if (!file.exists) {
+      return null;
+    }
+    try {
+      final contents = file.readAsStringSync();
+      final json = jsonDecode(contents) as Map<String, Object>;
+      return PackageDetailsCache.fromJson(json);
+    } catch (e) {
+      _instrumentationService.logError('Error reading pub cache file: $e');
+      return null;
+    }
+  }
+
+  void shutdown() => _nextRequestTimer?.cancel();
+
+  @visibleForTesting
+  void writeDiskCache(PackageDetailsCache cache) {
+    final file = packageCacheFile;
+    file.writeAsStringSync(jsonEncode(cache.toJson()));
+  }
+
+  Future<void> _fetchFromServer() async {
+    try {
+      final packages = await _api.allPackages();
+      if (packages == null) {
+        // If we never got a valid response, just skip until the next refresh.
+        return;
+      }
+      packageCache = PackageDetailsCache.fromApiResults(packages);
+      writeDiskCache(packageCache);
+    } catch (e) {
+      _instrumentationService.logError('Failed to fetch packages from Pub: $e');
+    } finally {
+      _nextRequestTimer =
+          Timer(PackageDetailsCache.maxCacheAge, _fetchFromServer);
+    }
+  }
+}
diff --git a/pkg/analysis_server/pubspec.yaml b/pkg/analysis_server/pubspec.yaml
index ef86eeb..855f489 100644
--- a/pkg/analysis_server/pubspec.yaml
+++ b/pkg/analysis_server/pubspec.yaml
@@ -18,6 +18,7 @@
   convert: any
   crypto: any
   dart_style: any
+  http: any
   html: any
   intl: any
   linter: any
@@ -34,7 +35,6 @@
 dev_dependencies:
   analyzer_utilities:
     path: ../analyzer_utilities
-  http: any
   logging: any
   matcher: any
   mockito: any
diff --git a/pkg/analysis_server/test/lsp/completion.dart b/pkg/analysis_server/test/lsp/completion.dart
index 194f9fd..f1f5b9a 100644
--- a/pkg/analysis_server/test/lsp/completion.dart
+++ b/pkg/analysis_server/test/lsp/completion.dart
@@ -8,11 +8,15 @@
 import 'server_abstract.dart';
 
 mixin CompletionTestMixin on AbstractLspAnalysisServerTest {
+  int sortTextSorter(CompletionItem item1, CompletionItem item2) =>
+      (item1.sortText ?? item1.label).compareTo(item2.sortText ?? item2.label);
+
   Future<void> verifyCompletions(
     Uri fileUri,
     String content, {
     List<String> expectCompletions,
-    String verifyEditsFor,
+    String applyEditsFor,
+    bool resolve = false,
     String expectedContent,
     String expectedContentIfInserting,
     bool verifyInsertReplaceRanges = false,
@@ -36,20 +40,24 @@
     final res = await getCompletion(fileUri, positionFromMarker(content));
     await closeFile(fileUri);
 
-    for (final expectedCompletion in expectCompletions) {
-      expect(
-        res.any((c) => c.label == expectedCompletion),
-        isTrue,
-        reason:
-            '"$expectedCompletion" was not in ${res.map((c) => '"${c.label}"')}',
-      );
-    }
+    // Sort the completions by sortText and filter to those we expect, so the ordering
+    // can be compared.
+    final sortedResults = res
+        .where((r) => expectCompletions.contains(r.label))
+        .toList()
+          ..sort(sortTextSorter);
+
+    expect(sortedResults.map((item) => item.label), equals(expectCompletions));
 
     // Check the edits apply correctly.
-    if (verifyEditsFor != null) {
-      final item = res.singleWhere((c) => c.label == verifyEditsFor);
+    if (applyEditsFor != null) {
+      var item = res.singleWhere((c) => c.label == applyEditsFor);
       final insertFormat = item.insertTextFormat;
 
+      if (resolve) {
+        item = await resolveCompletion(item);
+      }
+
       if (verifyInsertReplaceRanges &&
           expectedContent != expectedContentIfInserting) {
         // Replacing.
diff --git a/pkg/analysis_server/test/lsp/completion_dart_test.dart b/pkg/analysis_server/test/lsp/completion_dart_test.dart
index d69a171..0c13828 100644
--- a/pkg/analysis_server/test/lsp/completion_dart_test.dart
+++ b/pkg/analysis_server/test/lsp/completion_dart_test.dart
@@ -682,7 +682,7 @@
         mainFileUri,
         content,
         expectCompletions: [expectedLabel],
-        verifyEditsFor: expectedLabel,
+        applyEditsFor: expectedLabel,
         verifyInsertReplaceRanges: true,
         expectedContent: expectedReplaced,
         expectedContentIfInserting: expectedInserted,
diff --git a/pkg/analysis_server/test/lsp/completion_yaml_test.dart b/pkg/analysis_server/test/lsp/completion_yaml_test.dart
index 9d9711d..9a9da49 100644
--- a/pkg/analysis_server/test/lsp/completion_yaml_test.dart
+++ b/pkg/analysis_server/test/lsp/completion_yaml_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 'package:analysis_server/src/services/pub/pub_api.dart';
+import 'package:http/http.dart';
 import 'package:linter/src/rules.dart';
 import 'package:test_reflective_loader/test_reflective_loader.dart';
 
@@ -43,7 +45,7 @@
         'always_declare_return_types',
         'annotate_overrides',
       ],
-      verifyEditsFor: 'annotate_overrides',
+      applyEditsFor: 'annotate_overrides',
       expectedContent: expected,
     );
   }
@@ -63,7 +65,7 @@
       analysisOptionsUri,
       content,
       expectCompletions: ['annotate_overrides'],
-      verifyEditsFor: 'annotate_overrides',
+      applyEditsFor: 'annotate_overrides',
       expectedContent: expected,
     );
   }
@@ -78,7 +80,7 @@
       analysisOptionsUri,
       content,
       expectCompletions: ['linter: '],
-      verifyEditsFor: 'linter: ',
+      applyEditsFor: 'linter: ',
       expectedContent: expected,
     );
   }
@@ -93,7 +95,7 @@
       analysisOptionsUri,
       content,
       expectCompletions: ['linter: '],
-      verifyEditsFor: 'linter: ',
+      applyEditsFor: 'linter: ',
       expectedContent: expected,
     );
   }
@@ -126,7 +128,7 @@
       fixDataUri,
       content,
       expectCompletions: ['kind: '],
-      verifyEditsFor: 'kind: ',
+      applyEditsFor: 'kind: ',
       expectedContent: expected,
     );
   }
@@ -147,7 +149,7 @@
       fixDataUri,
       content,
       expectCompletions: ['kind: '],
-      verifyEditsFor: 'kind: ',
+      applyEditsFor: 'kind: ',
       expectedContent: expected,
     );
   }
@@ -164,7 +166,7 @@
       fixDataUri,
       content,
       expectCompletions: ['transforms:'],
-      verifyEditsFor: 'transforms:',
+      applyEditsFor: 'transforms:',
       expectedContent: expected,
     );
   }
@@ -179,7 +181,7 @@
       fixDataUri,
       content,
       expectCompletions: ['transforms:'],
-      verifyEditsFor: 'transforms:',
+      applyEditsFor: 'transforms:',
       expectedContent: expected,
     );
   }
@@ -188,6 +190,29 @@
 @reflectiveTest
 class PubspecCompletionTest extends AbstractLspAnalysisServerTest
     with CompletionTestMixin {
+  static const samplePackageList = '''
+  { "packages": ["one", "two", "three"] }
+  ''';
+
+  static const samplePackageDetails = '''
+  {
+    "name":"package",
+    "latest":{
+      "version":"1.2.3",
+      "pubspec":{
+        "description":"Description of package"
+      }
+    }
+  }
+  ''';
+
+  @override
+  void setUp() {
+    super.setUp();
+    // Cause retries to run immediately.
+    PubApi.failedRetryInitialDelaySeconds = 0;
+  }
+
   Future<void> test_insertReplaceRanges() async {
     final content = '''
 name: foo
@@ -215,7 +240,7 @@
       pubspecFileUri,
       content,
       expectCompletions: ['sdk: '],
-      verifyEditsFor: 'sdk: ',
+      applyEditsFor: 'sdk: ',
       verifyInsertReplaceRanges: true,
       expectedContent: expectedReplaced,
       expectedContentIfInserting: expectedInserted,
@@ -241,7 +266,7 @@
       pubspecFileUri,
       content,
       expectCompletions: ['flutter: ', 'sdk: '],
-      verifyEditsFor: 'sdk: ',
+      applyEditsFor: 'sdk: ',
       expectedContent: expected,
     );
   }
@@ -265,7 +290,39 @@
       pubspecFileUri,
       content,
       expectCompletions: ['flutter: ', 'sdk: '],
-      verifyEditsFor: 'sdk: ',
+      applyEditsFor: 'sdk: ',
+      expectedContent: expected,
+    );
+  }
+
+  Future<void> test_package_names() async {
+    httpClient.sendHandler = (BaseRequest request) async {
+      if (request.url.toString().endsWith(PubApi.packageNameListPath)) {
+        return Response(samplePackageList, 200);
+      } else {
+        throw UnimplementedError();
+      }
+    };
+
+    final content = '''
+name: foo
+version: 1.0.0
+
+dependencies:
+  ^''';
+
+    final expected = '''
+name: foo
+version: 1.0.0
+
+dependencies:
+  one: ''';
+
+    await verifyCompletions(
+      pubspecFileUri,
+      content,
+      expectCompletions: ['one: ', 'two: ', 'three: '],
+      applyEditsFor: 'one: ',
       expectedContent: expected,
     );
   }
@@ -282,7 +339,7 @@
       pubspecFileUri,
       content,
       expectCompletions: ['name: ', 'description: '],
-      verifyEditsFor: 'name: ',
+      applyEditsFor: 'name: ',
       expectedContent: expected,
     );
   }
@@ -297,7 +354,7 @@
       pubspecFileUri,
       content,
       expectCompletions: ['name: ', 'description: '],
-      verifyEditsFor: 'name: ',
+      applyEditsFor: 'name: ',
       expectedContent: expected,
     );
   }
diff --git a/pkg/analysis_server/test/lsp/pub_package_service_test.dart b/pkg/analysis_server/test/lsp/pub_package_service_test.dart
new file mode 100644
index 0000000..f76aaa9
--- /dev/null
+++ b/pkg/analysis_server/test/lsp/pub_package_service_test.dart
@@ -0,0 +1,309 @@
+// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
+// 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 'package:analysis_server/src/services/pub/pub_api.dart';
+import 'package:analysis_server/src/services/pub/pub_package_service.dart';
+import 'package:analyzer/instrumentation/service.dart';
+import 'package:http/http.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import '../mocks.dart';
+import 'server_abstract.dart';
+
+void main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(PubApiTest);
+    defineReflectiveTests(PubPackageServiceTest);
+  });
+}
+
+@reflectiveTest
+class PubApiTest {
+  static const pubDefaultUrl = 'https://pub.dartlang.org';
+
+  Uri lastCalledUrl;
+  MockHttpClient httpClient;
+
+  PubApi api;
+
+  Future<void> check_pubHostedUrl(String envValue, String expectedBase) async {
+    final api =
+        PubApi(InstrumentationService.NULL_SERVICE, httpClient, envValue);
+    await api.allPackages();
+    expect(lastCalledUrl.toString(),
+        equals('$expectedBase/api/package-name-completion-data'));
+  }
+
+  void setUp() {
+    httpClient = MockHttpClient();
+    lastCalledUrl = null;
+    httpClient.sendHandler = (BaseRequest request) async {
+      lastCalledUrl = request.url;
+      return Response('{}', 200);
+    };
+  }
+
+  Future<void> test_envPubHostedUrl_emptyString() =>
+      check_pubHostedUrl('', pubDefaultUrl);
+
+  Future<void> test_envPubHostedUrl_invalidUrl() =>
+      check_pubHostedUrl('test', pubDefaultUrl);
+
+  Future<void> test_envPubHostedUrl_missingScheme() =>
+      // It's hard to tell that this is intended to be a valid URL minus the scheme
+      // so it will fail validation and fall back to the default.
+      check_pubHostedUrl('pub.example.org', pubDefaultUrl);
+
+  Future<void> test_envPubHostedUrl_null() =>
+      check_pubHostedUrl(null, pubDefaultUrl);
+
+  Future<void> test_envPubHostedUrl_valid() =>
+      check_pubHostedUrl('https://pub.example.org', 'https://pub.example.org');
+
+  Future<void> test_envPubHostedUrl_validTrailingSlash() =>
+      check_pubHostedUrl('https://pub.example.org/', 'https://pub.example.org');
+
+  Future<void> test_httpClient_closesOwn() async {
+    final api = PubApi(InstrumentationService.NULL_SERVICE, null, null);
+    api.close();
+    expect(() => api.httpClient.get(Uri.parse('https://www.google.co.uk/')),
+        throwsA(anything));
+  }
+
+  Future<void> test_httpClient_doesNotCloseProvided() async {
+    final api = PubApi(InstrumentationService.NULL_SERVICE, httpClient, null);
+    api.close();
+    expect(httpClient.wasClosed, isFalse);
+  }
+}
+
+@reflectiveTest
+class PubPackageServiceTest extends AbstractLspAnalysisServerTest {
+  /// A sample API response for package names. This should match the JSON served
+  /// at https://pub.dev/api/package-name-completion-data.
+  static const samplePackageList = '''
+  { "packages": ["one", "two", "three"] }
+  ''';
+
+  void expectPackages(List<String> packageNames) => expect(
+      server.pubPackageService.cachedPackages.map((p) => p.packageName),
+      equals(packageNames));
+
+  void providePubApiPackageList(String jsonResponse) {
+    httpClient.sendHandler = (BaseRequest request) async {
+      if (request.url.toString().endsWith(PubApi.packageNameListPath)) {
+        return Response(jsonResponse, 200);
+      } else {
+        throw UnimplementedError();
+      }
+    };
+  }
+
+  @override
+  void setUp() {
+    super.setUp();
+    // Cause retries to run immediately.
+    PubApi.failedRetryInitialDelaySeconds = 0;
+  }
+
+  Future<void> test_packageCache_diskCacheReplacedIfStale() async {
+    // This test should mirror test_packageCache_diskCacheUsedIfFresh
+    // besides the lastUpdated timestamp + expectations.
+    providePubApiPackageList(samplePackageList);
+
+    // Write a cache that should be considered stale.
+    server.pubPackageService.writeDiskCache(
+      PackageDetailsCache.fromApiResults([PubApiPackage('stalePackage1')])
+        ..lastUpdatedUtc = DateTime.utc(1990),
+    );
+
+    await initialize();
+    await openFile(pubspecFileUri, '');
+    await pumpEventQueue();
+
+    // Expect the sample list to have overwritten the stale cache.
+    expectPackages(['one', 'two', 'three']);
+  }
+
+  Future<void> test_packageCache_diskCacheUsedIfFresh() async {
+    // This test should mirror test_packageCache_diskCacheReplacedIfStale
+    // besides the lastUpdated timestamp + expectations.
+    providePubApiPackageList(samplePackageList);
+
+    // Write a cache that is not stale.
+    server.pubPackageService.writeDiskCache(
+        PackageDetailsCache.fromApiResults([PubApiPackage('freshPackage1')]));
+
+    await initialize();
+    await openFile(pubspecFileUri, '');
+    await pumpEventQueue();
+
+    // Expect the fresh cache to still be used.
+    expectPackages(['freshPackage1']);
+  }
+
+  Future<void> test_packageCache_doesNotRetryOn400() async {
+    var requestNum = 1;
+    httpClient.sendHandler = (BaseRequest request) async {
+      if (request.url.toString().endsWith(PubApi.packageNameListPath)) {
+        return requestNum++ == 1
+            ? Response('ERROR', 400)
+            : Response(samplePackageList, 200);
+      } else {
+        throw UnimplementedError();
+      }
+    };
+
+    await initialize();
+    expectPackages([]);
+
+    await openFile(pubspecFileUri, '');
+    await pumpEventQueue();
+
+    expectPackages([]);
+    expect(httpClient.sendHandlerCalls, equals(1));
+  }
+
+  Future<void> test_packageCache_doesNotRetryUnknownException() async {
+    httpClient.sendHandler =
+        (BaseRequest request) async => throw UnimplementedError();
+
+    await initialize();
+    expectPackages([]);
+
+    await openFile(pubspecFileUri, '');
+    await pumpEventQueue();
+
+    expectPackages([]);
+    expect(httpClient.sendHandlerCalls, equals(1));
+  }
+
+  Future<void> test_packageCache_fetchesFromServer() async {
+    // Provide the sample packages in the web request.
+    providePubApiPackageList(samplePackageList);
+
+    await initialize();
+    expectPackages([]);
+
+    await openFile(pubspecFileUri, '');
+    await pumpEventQueue();
+
+    expectPackages(['one', 'two', 'three']);
+  }
+
+  Future<void> test_packageCache_initializesOnPubspecOpen() async {
+    await initialize();
+
+    expect(server.pubPackageService.isRunning, isFalse);
+    expect(server.pubPackageService.packageCache, isNull);
+    expectPackages([]);
+    await openFile(pubspecFileUri, '');
+    await pumpEventQueue();
+
+    expect(server.pubPackageService.isRunning, isTrue);
+    expect(server.pubPackageService.packageCache, isNotNull);
+    expectPackages([]);
+  }
+
+  Future<void> test_packageCache_readsDiskCache() async {
+    server.pubPackageService.writeDiskCache(
+        PackageDetailsCache.fromApiResults([PubApiPackage('package1')]));
+
+    await initialize();
+    expectPackages([]);
+
+    await openFile(pubspecFileUri, '');
+    await pumpEventQueue();
+
+    expectPackages(['package1']);
+  }
+
+  Future<void> test_packageCache_retriesOn500() async {
+    var requestNum = 1;
+    httpClient.sendHandler = (BaseRequest request) async {
+      if (request.url.toString().endsWith(PubApi.packageNameListPath)) {
+        return requestNum++ == 1
+            ? Response('ERROR', 500)
+            : Response(samplePackageList, 200);
+      } else {
+        throw UnimplementedError();
+      }
+    };
+
+    await initialize();
+    expectPackages([]);
+
+    await openFile(pubspecFileUri, '');
+    await pumpEventQueue();
+
+    expectPackages(['one', 'two', 'three']);
+    expect(httpClient.sendHandlerCalls, equals(2));
+  }
+
+  Future<void> test_packageCache_retriesOnInvalidJson() async {
+    var requestNum = 1;
+    httpClient.sendHandler = (BaseRequest request) async {
+      if (request.url.toString().endsWith(PubApi.packageNameListPath)) {
+        return requestNum++ == 1
+            ? Response('$samplePackageList{{{{{{{', 200)
+            : Response(samplePackageList, 200);
+      } else {
+        throw UnimplementedError();
+      }
+    };
+
+    await initialize();
+    expectPackages([]);
+
+    await openFile(pubspecFileUri, '');
+    await pumpEventQueue();
+
+    expectPackages(['one', 'two', 'three']);
+    expect(httpClient.sendHandlerCalls, equals(2));
+  }
+
+  Future<void> test_packageCache_timeRemaining() async {
+    void expectHoursRemaining(DateTime cacheTime, int expectedHoursRemaining) {
+      final cache = PackageDetailsCache.empty();
+      cache.lastUpdatedUtc = cacheTime.toUtc();
+
+      final remainingHours = cache.cacheTimeRemaining.inHours;
+      expect(remainingHours, isNonNegative);
+      expect(remainingHours, closeTo(expectedHoursRemaining, 1));
+    }
+
+    final maxHours = PackageDetailsCache.maxCacheAge.inHours;
+
+    // Very old cache should have no time remaining.
+    expectHoursRemaining(DateTime(2020, 12, 1), 0);
+
+    // Cache from 1 hour ago should max-1 hours remaining.
+    expectHoursRemaining(DateTime.now().add(Duration(hours: -1)), maxHours - 1);
+
+    // Cache from 10 hours ago should max-10 hours remaining.
+    expectHoursRemaining(
+        DateTime.now().add(Duration(hours: -10)), maxHours - 10);
+
+    // Cache from maxAge ago should have no hours remaining.
+    expectHoursRemaining(
+        DateTime.now().add(-PackageDetailsCache.maxCacheAge), 0);
+  }
+
+  Future<void> test_packageCache_writesDiskCache() async {
+    // Provide the sample packages in the web request.
+    providePubApiPackageList(samplePackageList);
+
+    await initialize();
+    expect(server.pubPackageService.readDiskCache(), isNull);
+
+    await openFile(pubspecFileUri, '');
+    await pumpEventQueue();
+
+    final cache = server.pubPackageService.readDiskCache();
+    final packages = cache.packages.values.toList();
+
+    expect(packages.map((p) => p.packageName), equals(['one', 'two', 'three']));
+  }
+}
diff --git a/pkg/analysis_server/test/lsp/server_abstract.dart b/pkg/analysis_server/test/lsp/server_abstract.dart
index bc85ae8..5ad6556 100644
--- a/pkg/analysis_server/test/lsp/server_abstract.dart
+++ b/pkg/analysis_server/test/lsp/server_abstract.dart
@@ -51,6 +51,7 @@
   MockLspServerChannel channel;
   TestPluginManager pluginManager;
   LspAnalysisServer server;
+  MockHttpClient httpClient;
 
   AnalysisServerOptions get serverOptions => AnalysisServerOptions();
 
@@ -119,6 +120,7 @@
   }
 
   void setUp() {
+    httpClient = MockHttpClient();
     channel = MockLspServerChannel(debugPrintCommunication);
     // Create an SDK in the mock file system.
     MockSdk(resourceProvider: resourceProvider);
@@ -129,7 +131,8 @@
         serverOptions,
         DartSdkManager(convertPath('/sdk')),
         CrashReportingAttachmentsBuilder.empty,
-        InstrumentationService.NULL_SERVICE);
+        InstrumentationService.NULL_SERVICE,
+        httpClient: httpClient);
     server.pluginManager = pluginManager;
 
     projectFolderPath = convertPath('/home/test');
@@ -1064,6 +1067,20 @@
         request, _fromJsonList(Location.fromJson));
   }
 
+  Future<CompletionItem> getResolvedCompletion(
+    Uri uri,
+    Position pos,
+    String label, {
+    CompletionContext context,
+  }) async {
+    final completions = await getCompletion(uri, pos, context: context);
+
+    final completion = completions.singleWhere((c) => c.label == label);
+    expect(completion, isNotNull);
+
+    return resolveCompletion(completion);
+  }
+
   Future<SemanticTokens> getSemanticTokens(Uri uri) {
     final request = makeRequest(
       Method.textDocument_semanticTokens_full,
diff --git a/pkg/analysis_server/test/lsp/test_all.dart b/pkg/analysis_server/test/lsp/test_all.dart
index aa30754..dd27eb3 100644
--- a/pkg/analysis_server/test/lsp/test_all.dart
+++ b/pkg/analysis_server/test/lsp/test_all.dart
@@ -31,6 +31,7 @@
 import 'mapping_test.dart' as mapping;
 import 'outline_test.dart' as outline;
 import 'priority_files_test.dart' as priority_files;
+import 'pub_package_service_test.dart' as pub_package_service;
 import 'reanalyze_test.dart' as reanalyze;
 import 'references_test.dart' as references;
 import 'rename_test.dart' as rename;
@@ -71,6 +72,7 @@
     mapping.main();
     outline.main();
     priority_files.main();
+    pub_package_service.main();
     reanalyze.main();
     references.main();
     rename.main();
diff --git a/pkg/analysis_server/test/mocks.dart b/pkg/analysis_server/test/mocks.dart
index c166c2c..d8c9e0a 100644
--- a/pkg/analysis_server/test/mocks.dart
+++ b/pkg/analysis_server/test/mocks.dart
@@ -13,6 +13,7 @@
 import 'package:analysis_server/src/lsp/channel/lsp_channel.dart';
 import 'package:analyzer/src/generated/source.dart';
 import 'package:analyzer/src/generated/timestamped_data.dart';
+import 'package:http/http.dart' as http;
 import 'package:test/test.dart';
 
 const _jsonEncoder = JsonEncoder.withIndent('    ');
@@ -26,6 +27,34 @@
 /// and no error.
 Matcher isResponseSuccess(String id) => _IsResponseSuccess(id);
 
+class MockHttpClient extends http.BaseClient {
+  Future<http.Response> Function(http.BaseRequest request) sendHandler;
+  int sendHandlerCalls = 0;
+  bool wasClosed = false;
+
+  @override
+  void close() {
+    wasClosed = true;
+  }
+
+  @override
+  dynamic noSuchMethod(Invocation invocation) {
+    return super.noSuchMethod(invocation);
+  }
+
+  @override
+  Future<http.StreamedResponse> send(http.BaseRequest request) {
+    if (wasClosed) {
+      throw Exception('get() called after close()');
+    }
+
+    return sendHandler(request)
+        .then((resp) => http.StreamedResponse(
+            Stream.value(resp.body.codeUnits), resp.statusCode))
+        .whenComplete(() => sendHandlerCalls++);
+  }
+}
+
 /// A mock [LspServerCommunicationChannel] for testing [LspAnalysisServer].
 class MockLspServerChannel implements LspServerCommunicationChannel {
   final StreamController<lsp.Message> _clientToServer =
diff --git a/pkg/analysis_server/test/src/services/completion/yaml/pubspec_generator_test.dart b/pkg/analysis_server/test/src/services/completion/yaml/pubspec_generator_test.dart
index b00f1d4..acfead8 100644
--- a/pkg/analysis_server/test/src/services/completion/yaml/pubspec_generator_test.dart
+++ b/pkg/analysis_server/test/src/services/completion/yaml/pubspec_generator_test.dart
@@ -3,8 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:analysis_server/src/services/completion/yaml/pubspec_generator.dart';
+import 'package:analysis_server/src/services/pub/pub_api.dart';
+import 'package:analysis_server/src/services/pub/pub_package_service.dart';
+import 'package:analyzer/instrumentation/service.dart';
+import 'package:http/http.dart';
+import 'package:test/test.dart';
 import 'package:test_reflective_loader/test_reflective_loader.dart';
 
+import '../../../../mocks.dart';
 import 'yaml_generator_test_support.dart';
 
 void main() {
@@ -15,11 +21,27 @@
 
 @reflectiveTest
 class PubspecGeneratorTest extends YamlGeneratorTest {
+  MockHttpClient httpClient;
+
+  PubPackageService pubPackageService;
   @override
   String get fileName => 'pubspec.yaml';
 
   @override
-  PubspecGenerator get generator => PubspecGenerator(resourceProvider);
+  PubspecGenerator get generator =>
+      PubspecGenerator(resourceProvider, pubPackageService);
+
+  void setUp() {
+    httpClient = MockHttpClient();
+    pubPackageService = PubPackageService(
+        InstrumentationService.NULL_SERVICE,
+        resourceProvider,
+        PubApi(InstrumentationService.NULL_SERVICE, httpClient, null));
+  }
+
+  void tearDown() {
+    pubPackageService.shutdown();
+  }
 
   void test_empty() {
     getCompletions('^');
@@ -254,4 +276,53 @@
 ''');
     assertSuggestion('true');
   }
+
+  void test_packageName() async {
+    const samplePackageList = '''
+  { "packages": ["one", "two", "three"] }
+  ''';
+
+    httpClient.sendHandler = (BaseRequest request) async {
+      if (request.url.toString().endsWith(PubApi.packageNameListPath)) {
+        return Response(samplePackageList, 200);
+      } else {
+        throw UnimplementedError();
+      }
+    };
+
+    pubPackageService.beginPackageNamePreload();
+    await pumpEventQueue();
+
+    getCompletions('''
+dependencies:
+  ^
+''');
+    assertSuggestion('one: ');
+    assertSuggestion('two: ');
+  }
+
+  void test_packageName_invalidYaml() async {
+    const samplePackageList = '''
+  { "packages": ["one", "two", "three"] }
+  ''';
+
+    httpClient.sendHandler = (BaseRequest request) async {
+      if (request.url.toString().endsWith(PubApi.packageNameListPath)) {
+        return Response(samplePackageList, 200);
+      } else {
+        throw UnimplementedError();
+      }
+    };
+
+    pubPackageService.beginPackageNamePreload();
+    await pumpEventQueue();
+
+    getCompletions('''
+dependencies:
+  one:
+  tw^
+  three:
+''');
+    assertSuggestion('two: ');
+  }
 }
diff --git a/tools/VERSION b/tools/VERSION
index 26d7750..176ee19 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 13
 PATCH 0
-PRERELEASE 100
+PRERELEASE 101
 PRERELEASE_PATCH 0
\ No newline at end of file