Version 2.13.0-69.0.dev

Merge commit 'a7bf05f64dd7e82deb22b24a7d23610f9916770b' into 'dev'
diff --git a/.dart_tool/package_config.json b/.dart_tool/package_config.json
index d3231e9..8400a1c 100644
--- a/.dart_tool/package_config.json
+++ b/.dart_tool/package_config.json
@@ -11,7 +11,7 @@
     "constraint, update this by running tools/generate_package_config.dart."
   ],
   "configVersion": 2,
-  "generated": "2021-02-23T10:05:38.675159",
+  "generated": "2021-02-23T11:38:53.873726",
   "generator": "tools/generate_package_config.dart",
   "packages": [
     {
@@ -458,7 +458,7 @@
       "name": "oauth2",
       "rootUri": "../third_party/pkg/oauth2",
       "packageUri": "lib/",
-      "languageVersion": "2.0"
+      "languageVersion": "2.12"
     },
     {
       "name": "observatory",
diff --git a/DEPS b/DEPS
index 022463e..6c57713 100644
--- a/DEPS
+++ b/DEPS
@@ -126,7 +126,7 @@
   "mime_rev": "c931f4bed87221beaece356494b43731445ce7b8",
   "mockito_rev": "d39ac507483b9891165e422ec98d9fb480037c8b",
   "mustache_rev": "664737ecad027e6b96d0d1e627257efa0e46fcb1",
-  "oauth2_rev": "95b6c8d96dc37a1723480961665d3477a46dd303",
+  "oauth2_rev": "9b8ee16d0dd4538173422a01532f33fc8be2548f",
   "package_config_rev": "249af482de9ebabfc781bf10d6152c938e5ce45e",
   "path_rev": "407ab76187fade41c31e39c745b39661b710106c",
   "pedantic_rev": "df177f6ae531426aaf7bbf0121c90dc89d9c57bf",
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/for_in_loop_type_not_iterable_nullability_error.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/for_in_loop_type_not_iterable_nullability_error.dart
new file mode 100644
index 0000000..4c0d40f
--- /dev/null
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/for_in_loop_type_not_iterable_nullability_error.dart
@@ -0,0 +1,21 @@
+// Copyright (c) 2020, 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.
+
+// This test contains a test case for each condition that can lead to the front
+// end's `ForInLoopTypeNotIterableNullability` or
+// `ForInLoopTypeNotIterablePartNullability` errors, for which we wish to report
+// "why not promoted" context information.
+
+// TODO(paulberry): get this to work with the CFE and add additional test cases
+// if needed.
+
+class C1 {
+  List<int>? bad;
+}
+
+test(C1 c) {
+  if (c.bad == null) return;
+  for (var x
+      in /*analyzer.notPromoted(propertyNotPromoted(member:C1.bad))*/ c.bad) {}
+}
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/invalid_assignment_error_nullability_error.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/invalid_assignment_error_nullability_error.dart
new file mode 100644
index 0000000..b8e021a
--- /dev/null
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/invalid_assignment_error_nullability_error.dart
@@ -0,0 +1,20 @@
+// Copyright (c) 2020, 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.
+
+// This test contains a test case for each condition that can lead to the front
+// end's `InvalidAssignmentErrorNullability` or
+// `InvalidAssignmentErrorPartNullability` errors, for which we wish to report
+// "why not promoted" context information.
+
+// TODO(paulberry): get this to work with the CFE and add additional test cases
+// if needed.
+
+class C1 {
+  List<int>? bad;
+}
+
+test(C1 c) sync* {
+  if (c.bad == null) return;
+  yield* /*analyzer.notPromoted(propertyNotPromoted(member:C1.bad))*/ c.bad;
+}
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/nullable_expression_call_error.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/nullable_expression_call_error.dart
index c603def..3e33316 100644
--- a/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/nullable_expression_call_error.dart
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/nullable_expression_call_error.dart
@@ -16,7 +16,7 @@
 
 instance_method_invocation(C1 c) {
   if (c.bad == null) return;
-  c.bad
+  /*analyzer.notPromoted(propertyNotPromoted(member:C1.bad))*/ c.bad
       /*cfe.invoke: notPromoted(propertyNotPromoted(member:C1.bad))*/
       ();
 }
@@ -42,7 +42,7 @@
   if (c.ok == null) return;
   c.ok();
   if (c.bad == null) return;
-  c.bad
+  /*analyzer.notPromoted(propertyNotPromoted(member:C3.bad))*/ c.bad
       /*cfe.invoke: notPromoted(propertyNotPromoted(member:C3.bad))*/
       ();
 }
@@ -57,7 +57,7 @@
 
 instance_getter_invocation(C6 c) {
   if (c.bad == null) return;
-  c.bad
+  /*analyzer.notPromoted(propertyNotPromoted(member:C6.bad))*/ c.bad
       /*cfe.invoke: notPromoted(propertyNotPromoted(member:C6.bad))*/
       ();
 }
@@ -83,7 +83,7 @@
   if (c.ok == null) return;
   c.ok();
   if (c.bad == null) return;
-  c.bad
+  /*analyzer.notPromoted(propertyNotPromoted(member:C8.bad))*/ c.bad
       /*cfe.invoke: notPromoted(propertyNotPromoted(member:C8.bad))*/
       ();
 }
@@ -94,7 +94,7 @@
 
 function_invocation(C11 c) {
   if (c.bad == null) return;
-  c.bad
+  /*analyzer.notPromoted(propertyNotPromoted(member:C11.bad))*/ c.bad
       /*cfe.invoke: notPromoted(propertyNotPromoted(member:C11.bad))*/
       ();
 }
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/nullable_spread_error.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/nullable_spread_error.dart
new file mode 100644
index 0000000..d3f6451
--- /dev/null
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted/data/nullable_spread_error.dart
@@ -0,0 +1,21 @@
+// Copyright (c) 2020, 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.
+
+// This test contains a test case for each condition that can lead to the front
+// end's `NullableSpreadError` error, for which we wish to report "why not
+// promoted" context information.
+
+// TODO(paulberry): get this to work with the CFE and add additional test cases
+// if needed.
+
+class C1 {
+  List<int>? bad;
+}
+
+test(C1 c) {
+  if (c.bad == null) return;
+  return [
+    ... /*analyzer.notPromoted(propertyNotPromoted(member:C1.bad))*/ c.bad
+  ];
+}
diff --git a/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart b/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart
index 16daa2d..6a1ba57 100644
--- a/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart
+++ b/pkg/analysis_server/lib/lsp_protocol/protocol_custom_generated.dart
@@ -293,6 +293,7 @@
       {@required this.libId,
       @required this.displayUri,
       @required this.rOffset,
+      @required this.iLength,
       @required this.rLength,
       @required this.file,
       @required this.offset}) {
@@ -305,6 +306,9 @@
     if (rOffset == null) {
       throw 'rOffset is required but was not provided';
     }
+    if (iLength == null) {
+      throw 'iLength is required but was not provided';
+    }
     if (rLength == null) {
       throw 'rLength is required but was not provided';
     }
@@ -319,6 +323,7 @@
     final libId = json['libId'];
     final displayUri = json['displayUri'];
     final rOffset = json['rOffset'];
+    final iLength = json['iLength'];
     final rLength = json['rLength'];
     final file = json['file'];
     final offset = json['offset'];
@@ -326,6 +331,7 @@
         libId: libId,
         displayUri: displayUri,
         rOffset: rOffset,
+        iLength: iLength,
         rLength: rLength,
         file: file,
         offset: offset);
@@ -333,6 +339,7 @@
 
   final String displayUri;
   final String file;
+  final num iLength;
   final num libId;
   final num offset;
   final num rLength;
@@ -345,6 +352,8 @@
         displayUri ?? (throw 'displayUri is required but was not set');
     __result['rOffset'] =
         rOffset ?? (throw 'rOffset is required but was not set');
+    __result['iLength'] =
+        iLength ?? (throw 'iLength is required but was not set');
     __result['rLength'] =
         rLength ?? (throw 'rLength is required but was not set');
     __result['file'] = file ?? (throw 'file is required but was not set');
@@ -405,6 +414,23 @@
       } finally {
         reporter.pop();
       }
+      reporter.push('iLength');
+      try {
+        if (!obj.containsKey('iLength')) {
+          reporter.reportError('must not be undefined');
+          return false;
+        }
+        if (obj['iLength'] == null) {
+          reporter.reportError('must not be null');
+          return false;
+        }
+        if (!(obj['iLength'] is num)) {
+          reporter.reportError('must be of type num');
+          return false;
+        }
+      } finally {
+        reporter.pop();
+      }
       reporter.push('rLength');
       try {
         if (!obj.containsKey('rLength')) {
@@ -470,6 +496,7 @@
       return libId == other.libId &&
           displayUri == other.displayUri &&
           rOffset == other.rOffset &&
+          iLength == other.iLength &&
           rLength == other.rLength &&
           file == other.file &&
           offset == other.offset &&
@@ -484,6 +511,7 @@
     hash = JenkinsSmiHash.combine(hash, libId.hashCode);
     hash = JenkinsSmiHash.combine(hash, displayUri.hashCode);
     hash = JenkinsSmiHash.combine(hash, rOffset.hashCode);
+    hash = JenkinsSmiHash.combine(hash, iLength.hashCode);
     hash = JenkinsSmiHash.combine(hash, rLength.hashCode);
     hash = JenkinsSmiHash.combine(hash, file.hashCode);
     hash = JenkinsSmiHash.combine(hash, offset.hashCode);
diff --git a/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart b/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart
index 251eafe..3438d4b 100644
--- a/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart
+++ b/pkg/analysis_server/lib/lsp_protocol/protocol_generated.dart
@@ -6144,8 +6144,17 @@
     final insertTextMode = json['insertTextMode'] != null
         ? InsertTextMode.fromJson(json['insertTextMode'])
         : null;
-    final textEdit =
-        json['textEdit'] != null ? TextEdit.fromJson(json['textEdit']) : null;
+    final textEdit = TextEdit.canParse(json['textEdit'], nullLspJsonReporter)
+        ? Either2<TextEdit, InsertReplaceEdit>.t1(json['textEdit'] != null
+            ? TextEdit.fromJson(json['textEdit'])
+            : null)
+        : (InsertReplaceEdit.canParse(json['textEdit'], nullLspJsonReporter)
+            ? Either2<TextEdit, InsertReplaceEdit>.t2(json['textEdit'] != null
+                ? InsertReplaceEdit.fromJson(json['textEdit'])
+                : null)
+            : (json['textEdit'] == null
+                ? null
+                : (throw '''${json['textEdit']} was not one of (TextEdit, InsertReplaceEdit)''')));
     final additionalTextEdits = json['additionalTextEdits']
         ?.map((item) => item != null ? TextEdit.fromJson(item) : null)
         ?.cast<TextEdit>()
@@ -6282,7 +6291,7 @@
   /// must be a prefix of the edit's replace range, that means it must be
   /// contained and starting at the same position.
   ///  @since 3.16.0 additional type `InsertReplaceEdit`
-  final TextEdit textEdit;
+  final Either2<TextEdit, InsertReplaceEdit> textEdit;
 
   Map<String, dynamic> toJson() {
     var __result = <String, dynamic>{};
@@ -6321,7 +6330,7 @@
       __result['insertTextMode'] = insertTextMode.toJson();
     }
     if (textEdit != null) {
-      __result['textEdit'] = textEdit.toJson();
+      __result['textEdit'] = textEdit;
     }
     if (additionalTextEdits != null) {
       __result['additionalTextEdits'] = additionalTextEdits;
@@ -6468,8 +6477,10 @@
       reporter.push('textEdit');
       try {
         if (obj['textEdit'] != null &&
-            !(TextEdit.canParse(obj['textEdit'], reporter))) {
-          reporter.reportError('must be of type TextEdit');
+            !((TextEdit.canParse(obj['textEdit'], reporter) ||
+                InsertReplaceEdit.canParse(obj['textEdit'], reporter)))) {
+          reporter.reportError(
+              'must be of type Either2<TextEdit, InsertReplaceEdit>');
           return false;
         }
       } finally {
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 ba9c66d..b84ae47 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:collection';
+import 'dart:math' as math;
 
 import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
 import 'package:analysis_server/lsp_protocol/protocol_special.dart';
@@ -183,6 +184,17 @@
     return alreadyImportedSymbols;
   }
 
+  /// The insert length is the shorter of the replacementLength or the
+  /// difference between the replacementOffset and the caret position.
+  int _computeInsertLength(
+      int offset, int replacementOffset, int replacementLength) {
+    final insertLength =
+        math.min(offset - replacementOffset, replacementLength);
+    assert(insertLength >= 0);
+    assert(insertLength <= replacementLength);
+    return insertLength;
+  }
+
   String _createImportedSymbolKey(String name, Uri declaringUri) =>
       '$name/$declaringUri';
 
@@ -205,6 +217,7 @@
       completionCapabilities,
       clientSupportedCompletionKinds,
       lineInfo,
+      offset,
       pluginResults,
     ).toList());
   }
@@ -252,6 +265,12 @@
           completionRequest,
         );
 
+        final insertLength = _computeInsertLength(
+          offset,
+          completionRequest.replacementOffset,
+          completionRequest.replacementLength,
+        );
+
         if (token.isCancellationRequested) {
           return cancelled();
         }
@@ -264,6 +283,7 @@
                 unit.lineInfo,
                 item,
                 completionRequest.replacementOffset,
+                insertLength,
                 completionRequest.replacementLength,
                 // TODO(dantup): Including commit characters in every completion
                 // increases the payload size. The LSP spec is ambigious
@@ -358,6 +378,7 @@
                     unit.lineInfo,
                     item,
                     completionRequest.replacementOffset,
+                    insertLength,
                     completionRequest.replacementLength,
                     // TODO(dantup): Including commit characters in every completion
                     // increases the payload size. The LSP spec is ambigious
@@ -401,6 +422,11 @@
     CancellationToken token,
   ) async {
     final suggestions = generator.getSuggestions(path, offset);
+    final insertLength = _computeInsertLength(
+      offset,
+      suggestions.replacementOffset,
+      suggestions.replacementLength,
+    );
     final completionItems = suggestions.suggestions
         .map(
           (item) => toCompletionItem(
@@ -409,6 +435,7 @@
             lineInfo,
             item,
             suggestions.replacementOffset,
+            insertLength,
             suggestions.replacementLength,
             includeCommitCharacters: false,
             completeFunctionCalls: false,
@@ -422,6 +449,7 @@
     CompletionClientCapabilities completionCapabilities,
     HashSet<CompletionItemKind> clientSupportedCompletionKinds,
     LineInfo lineInfo,
+    int offset,
     List<plugin.CompletionGetSuggestionsResult> pluginResults,
   ) {
     return pluginResults.expand((result) {
@@ -432,6 +460,11 @@
           lineInfo,
           item,
           result.replacementOffset,
+          _computeInsertLength(
+            offset,
+            result.replacementOffset,
+            result.replacementLength,
+          ),
           result.replacementLength,
           // Plugins cannot currently contribute commit characters and we should
           // not assume that the Dart ones would be correct for all of their
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 3157a79..dcf76e2 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
@@ -156,6 +156,9 @@
         // Documentation is added on during resolve for LSP.
         final formats = server.clientCapabilities?.textDocument?.completion
             ?.completionItem?.documentationFormat;
+        final supportsInsertReplace = server.clientCapabilities?.textDocument
+                ?.completion?.completionItem?.insertReplaceSupport ==
+            true;
         final dartDoc =
             analyzer.getDartDocPlainText(requestedElement.documentationComment);
         final documentation = asStringOrMarkupContent(formats, dartDoc);
@@ -175,10 +178,20 @@
           filterText: item.filterText,
           insertText: newInsertText,
           insertTextFormat: item.insertTextFormat,
-          textEdit: TextEdit(
-            range: toRange(lineInfo, data.rOffset, data.rLength),
-            newText: newInsertText,
-          ),
+          textEdit: supportsInsertReplace
+              ? Either2<TextEdit, InsertReplaceEdit>.t2(
+                  InsertReplaceEdit(
+                    insert: toRange(lineInfo, data.rOffset, data.iLength),
+                    replace: toRange(lineInfo, data.rOffset, data.rLength),
+                    newText: newInsertText,
+                  ),
+                )
+              : Either2<TextEdit, InsertReplaceEdit>.t1(
+                  TextEdit(
+                    range: toRange(lineInfo, data.rOffset, data.rLength),
+                    newText: newInsertText,
+                  ),
+                ),
           additionalTextEdits: thisFilesChanges
               .expand((change) =>
                   change.edits.map((edit) => toTextEdit(lineInfo, edit)))
diff --git a/pkg/analysis_server/lib/src/lsp/mapping.dart b/pkg/analysis_server/lib/src/lsp/mapping.dart
index 87231e2..f7500f4 100644
--- a/pkg/analysis_server/lib/src/lsp/mapping.dart
+++ b/pkg/analysis_server/lib/src/lsp/mapping.dart
@@ -220,6 +220,7 @@
   server.LineInfo lineInfo,
   dec.Declaration declaration,
   int replacementOffset,
+  int insertLength,
   int replacementLength, {
   @required bool includeCommitCharacters,
   @required bool completeFunctionCalls,
@@ -341,6 +342,7 @@
         libId: includedSuggestionSet.id,
         displayUri: includedSuggestionSet.displayUri ?? library.uri?.toString(),
         rOffset: replacementOffset,
+        iLength: insertLength,
         rLength: replacementLength),
   );
 }
@@ -816,6 +818,7 @@
   server.LineInfo lineInfo,
   server.CompletionSuggestion suggestion,
   int replacementOffset,
+  int insertLength,
   int replacementLength, {
   @required bool includeCommitCharacters,
   @required bool completeFunctionCalls,
@@ -860,6 +863,8 @@
   final formats = completionCapabilities?.completionItem?.documentationFormat;
   final supportsSnippets =
       completionCapabilities?.completionItem?.snippetSupport == true;
+  final supportsInsertReplace =
+      completionCapabilities?.completionItem?.insertReplaceSupport == true;
 
   final completionKind = suggestion.element != null
       ? elementKindToCompletionItemKind(
@@ -917,10 +922,20 @@
     insertTextFormat: insertTextFormat != lsp.InsertTextFormat.PlainText
         ? insertTextFormat
         : null, // Defaults to PlainText if not supplied
-    textEdit: lsp.TextEdit(
-      range: toRange(lineInfo, replacementOffset, replacementLength),
-      newText: insertText,
-    ),
+    textEdit: supportsInsertReplace
+        ? Either2<TextEdit, InsertReplaceEdit>.t2(
+            InsertReplaceEdit(
+              insert: toRange(lineInfo, replacementOffset, insertLength),
+              replace: toRange(lineInfo, replacementOffset, replacementLength),
+              newText: insertText,
+            ),
+          )
+        : Either2<TextEdit, InsertReplaceEdit>.t1(
+            TextEdit(
+              range: toRange(lineInfo, replacementOffset, replacementLength),
+              newText: insertText,
+            ),
+          ),
   );
 }
 
diff --git a/pkg/analysis_server/test/lsp/completion_dart_test.dart b/pkg/analysis_server/test/lsp/completion_dart_test.dart
index 2214c75..ffb6cb0 100644
--- a/pkg/analysis_server/test/lsp/completion_dart_test.dart
+++ b/pkg/analysis_server/test/lsp/completion_dart_test.dart
@@ -126,11 +126,9 @@
     // placeholders.
     expect(item.insertTextFormat, equals(InsertTextFormat.Snippet));
     expect(item.insertText, equals(r'myFunction(${1:a}, ${2:b})'));
-    expect(item.textEdit.newText, equals(item.insertText));
-    expect(
-      item.textEdit.range,
-      equals(rangeFromMarkers(content)),
-    );
+    final textEdit = toTextEdit(item.textEdit);
+    expect(textEdit.newText, equals(item.insertText));
+    expect(textEdit.range, equals(rangeFromMarkers(content)));
   }
 
   Future<void> test_completeFunctionCalls_flutterSetState() async {
@@ -175,11 +173,9 @@
     // placeholders.
     expect(item.insertTextFormat, equals(InsertTextFormat.Snippet));
     expect(item.insertText, equals('setState(() {\n      \${0:}\n    \\});'));
-    expect(item.textEdit.newText, equals(item.insertText));
-    expect(
-      item.textEdit.range,
-      equals(rangeFromMarkers(content)),
-    );
+    final textEdit = toTextEdit(item.textEdit);
+    expect(textEdit.newText, equals(item.insertText));
+    expect(textEdit.range, equals(rangeFromMarkers(content)));
   }
 
   Future<void> test_completeFunctionCalls_noRequiredParameters() async {
@@ -206,11 +202,9 @@
     // With no required params, there should still be parens and a tabstop inside.
     expect(item.insertTextFormat, equals(InsertTextFormat.Snippet));
     expect(item.insertText, equals(r'myFunction(${0:})'));
-    expect(item.textEdit.newText, equals(item.insertText));
-    expect(
-      item.textEdit.range,
-      equals(rangeFromMarkers(content)),
-    );
+    final textEdit = toTextEdit(item.textEdit);
+    expect(textEdit.newText, equals(item.insertText));
+    expect(textEdit.range, equals(rangeFromMarkers(content)));
   }
 
   Future<void> test_completeFunctionCalls_show() async {
@@ -234,7 +228,8 @@
     // no need for snippets.
     expect(item.insertTextFormat, isNull);
     expect(item.insertText, equals(r'min'));
-    expect(item.textEdit.newText, equals(item.insertText));
+    final textEdit = toTextEdit(item.textEdit);
+    expect(textEdit.newText, equals(item.insertText));
   }
 
   Future<void> test_completeFunctionCalls_suggestionSets() async {
@@ -267,11 +262,9 @@
     // Ensure the item can be resolved and gets a proper TextEdit.
     final resolved = await resolveCompletion(item);
     expect(resolved.textEdit, isNotNull);
-    expect(resolved.textEdit.newText, equals(item.insertText));
-    expect(
-      resolved.textEdit.range,
-      equals(rangeFromMarkers(content)),
-    );
+    final textEdit = toTextEdit(resolved.textEdit);
+    expect(textEdit.newText, equals(item.insertText));
+    expect(textEdit.range, equals(rangeFromMarkers(content)));
   }
 
   Future<void> test_completionKinds_default() async {
@@ -510,6 +503,42 @@
     });
   }
 
+  Future<void> test_insertReplaceRanges() async {
+    final content = '''
+    class MyClass {
+      String abcdefghij;
+    }
+
+    main() {
+      MyClass a;
+      a.abc^def
+    }
+    ''';
+
+    await initialize(
+      textDocumentCapabilities: withCompletionItemInsertReplaceSupport(
+          emptyTextDocumentClientCapabilities),
+    );
+    await openFile(mainFileUri, withoutMarkers(content));
+    final res = await getCompletion(mainFileUri, positionFromMarker(content));
+    expect(res.any((c) => c.label == 'abcdefghij'), isTrue);
+    final item = res.singleWhere((c) => c.label == 'abcdefghij');
+    // When using the replacement range, we should get exactly the symbol
+    // we expect.
+    final replaced = applyTextEdits(
+      withoutMarkers(content),
+      [textEditForReplace(item.textEdit)],
+    );
+    expect(replaced, contains('a.abcdefghij\n'));
+    // When using the insert range, we should retain what was after the caret
+    // ("def" in this case).
+    final inserted = applyTextEdits(
+      withoutMarkers(content),
+      [textEditForInsert(item.textEdit)],
+    );
+    expect(inserted, contains('a.abcdefghijdef\n'));
+  }
+
   Future<void> test_insideString() async {
     final content = '''
     var a = "This is ^a test"
@@ -633,7 +662,10 @@
     expect(item.insertTextFormat,
         anyOf(equals(InsertTextFormat.PlainText), isNull));
     expect(item.insertText, anyOf(equals('test'), isNull));
-    final updated = applyTextEdits(withoutMarkers(content), [item.textEdit]);
+    final updated = applyTextEdits(
+      withoutMarkers(content),
+      [toTextEdit(item.textEdit)],
+    );
     expect(updated, contains('one: '));
   }
 
@@ -656,9 +688,10 @@
     // need to be provided.
     expect(item.insertTextFormat, isNull);
     expect(item.insertText, isNull);
-    expect(item.textEdit.newText, equals('one: '));
+    final textEdit = toTextEdit(item.textEdit);
+    expect(textEdit.newText, equals('one: '));
     expect(
-      item.textEdit.range,
+      textEdit.range,
       equals(Range(
           start: positionFromMarker(content),
           end: positionFromMarker(content))),
@@ -687,9 +720,10 @@
     // placeholder.
     expect(item.insertTextFormat, equals(InsertTextFormat.Snippet));
     expect(item.insertText, equals(r'one: ${0:},'));
-    expect(item.textEdit.newText, equals(r'one: ${0:},'));
+    final textEdit = toTextEdit(item.textEdit);
+    expect(textEdit.newText, equals(r'one: ${0:},'));
     expect(
-      item.textEdit.range,
+      textEdit.range,
       equals(Range(
           start: positionFromMarker(content),
           end: positionFromMarker(content))),
@@ -742,7 +776,10 @@
     expect(item.insertTextFormat,
         anyOf(equals(InsertTextFormat.PlainText), isNull));
     expect(item.insertText, anyOf(equals('abcdefghij'), isNull));
-    final updated = applyTextEdits(withoutMarkers(content), [item.textEdit]);
+    final updated = applyTextEdits(
+      withoutMarkers(content),
+      [toTextEdit(item.textEdit)],
+    );
     expect(updated, contains('a.abcdefghij'));
   }
 
@@ -864,7 +901,9 @@
     // Apply both the main completion edit and the additionalTextEdits atomically.
     final newContent = applyTextEdits(
       withoutMarkers(content),
-      [resolved.textEdit].followedBy(resolved.additionalTextEdits).toList(),
+      [toTextEdit(resolved.textEdit)]
+          .followedBy(resolved.additionalTextEdits)
+          .toList(),
     );
 
     // Ensure both edits were made - the completion, and the inserted import.
@@ -968,7 +1007,9 @@
     // Apply both the main completion edit and the additionalTextEdits atomically.
     final newContent = applyTextEdits(
       withoutMarkers(content),
-      [resolved.textEdit].followedBy(resolved.additionalTextEdits).toList(),
+      [toTextEdit(resolved.textEdit)]
+          .followedBy(resolved.additionalTextEdits)
+          .toList(),
     );
 
     // Ensure both edits were made - the completion, and the inserted import.
@@ -1115,6 +1156,98 @@
     expectAutoImportCompletion(resolvedCompletions, '../reexport2.dart');
   }
 
+  Future<void> test_suggestionSets_insertReplaceRanges() async {
+    newFile(
+      join(projectFolderPath, 'other_file.dart'),
+      content: '''
+      /// This class is in another file.
+      class InOtherFile {}
+      ''',
+    );
+
+    final content = '''
+main() {
+  InOtherF^il
+}
+    ''';
+
+    final initialAnalysis = waitForAnalysisComplete();
+    await initialize(
+      textDocumentCapabilities: withCompletionItemInsertReplaceSupport(
+          emptyTextDocumentClientCapabilities),
+      workspaceCapabilities:
+          withApplyEditSupport(emptyWorkspaceClientCapabilities),
+    );
+    await openFile(mainFileUri, withoutMarkers(content));
+    await initialAnalysis;
+    final res = await getCompletion(mainFileUri, positionFromMarker(content));
+
+    // Find the completion for the class in the other file.
+    final completion = res.singleWhere((c) => c.label == 'InOtherFile');
+    expect(completion, isNotNull);
+
+    // Expect no docs or text edit, since these are added during resolve.
+    expect(completion.documentation, isNull);
+    expect(completion.textEdit, isNull);
+
+    // Resolve the completion item (via server) to get its edits. This is the
+    // LSP's equiv of getSuggestionDetails() and is invoked by LSP clients to
+    // populate additional info (in our case, the additional edits for inserting
+    // the import).
+    final resolved = await resolveCompletion(completion);
+    expect(resolved, isNotNull);
+
+    // Ensure the detail field was update to show this will auto-import.
+    expect(
+        resolved.detail, startsWith("Auto import from '../other_file.dart'"));
+
+    // Ensure the doc comment was added.
+    expect(
+      resolved.documentation.valueEquals('This class is in another file.'),
+      isTrue,
+    );
+
+    // Ensure the edit was added on.
+    expect(resolved.textEdit, isNotNull);
+
+    // There should be no command for this item because it doesn't need imports
+    // in other files. Same-file completions are in additionalEdits.
+    expect(resolved.command, isNull);
+
+    // Apply both the main completion edit and the additionalTextEdits atomically
+    // then check the contents.
+
+    final newContentReplaceMode = applyTextEdits(
+      withoutMarkers(content),
+      [textEditForReplace(resolved.textEdit)]
+          .followedBy(resolved.additionalTextEdits)
+          .toList(),
+    );
+    final newContentInsertMode = applyTextEdits(
+      withoutMarkers(content),
+      [textEditForInsert(resolved.textEdit)]
+          .followedBy(resolved.additionalTextEdits)
+          .toList(),
+    );
+
+    // Ensure both edits were made - the completion, and the inserted import.
+    expect(newContentReplaceMode, equals('''
+import '../other_file.dart';
+
+main() {
+  InOtherFile
+}
+    '''));
+    // In insert mode, we'd have the trailing "il" still after the caret.
+    expect(newContentInsertMode, equals('''
+import '../other_file.dart';
+
+main() {
+  InOtherFileil
+}
+    '''));
+  }
+
   Future<void> test_suggestionSets_insertsIntoPartFiles() async {
     // File we'll be adding an import for.
     newFile(
@@ -1160,7 +1293,9 @@
     // Apply all current-document edits.
     final newContent = applyTextEdits(
       withoutMarkers(content),
-      [resolved.textEdit].followedBy(resolved.additionalTextEdits).toList(),
+      [toTextEdit(resolved.textEdit)]
+          .followedBy(resolved.additionalTextEdits)
+          .toList(),
     );
     expect(newContent, equals('''
 part of 'parent.dart';
@@ -1262,7 +1397,9 @@
     // Apply both the main completion edit and the additionalTextEdits atomically.
     final newContent = applyTextEdits(
       withoutMarkers(content),
-      [resolved.textEdit].followedBy(resolved.additionalTextEdits).toList(),
+      [toTextEdit(resolved.textEdit)]
+          .followedBy(resolved.additionalTextEdits)
+          .toList(),
     );
 
     // Ensure both edits were made - the completion, and the inserted import.
@@ -1354,7 +1491,9 @@
     // Apply both the main completion edit and the additionalTextEdits atomically.
     final newContent = applyTextEdits(
       withoutMarkers(content),
-      [resolved.textEdit].followedBy(resolved.additionalTextEdits).toList(),
+      [toTextEdit(resolved.textEdit)]
+          .followedBy(resolved.additionalTextEdits)
+          .toList(),
     );
 
     // Ensure both edits were made - the completion, and the inserted import.
@@ -1448,7 +1587,10 @@
     expect(item.insertTextFormat,
         anyOf(equals(InsertTextFormat.PlainText), isNull));
     expect(item.insertText, anyOf(equals('abcdefghij'), isNull));
-    final updated = applyTextEdits(withoutMarkers(content), [item.textEdit]);
+    final updated = applyTextEdits(
+      withoutMarkers(content),
+      [toTextEdit(item.textEdit)],
+    );
     expect(updated, contains('a.abcdefghij'));
   }
 }
@@ -1483,11 +1625,9 @@
     // placeholders.
     expect(item.insertTextFormat, equals(InsertTextFormat.Snippet));
     expect(item.insertText, equals(r'myFunction(${1:a}, ${2:b}, c: ${3:c})'));
-    expect(item.textEdit.newText, equals(item.insertText));
-    expect(
-      item.textEdit.range,
-      equals(rangeFromMarkers(content)),
-    );
+    final textEdit = toTextEdit(item.textEdit);
+    expect(textEdit.newText, equals(item.insertText));
+    expect(textEdit.range, equals(rangeFromMarkers(content)));
   }
 
   Future<void> test_completeFunctionCalls_requiredNamed_suggestionSet() async {
@@ -1527,10 +1667,8 @@
     // Ensure the item can be resolved and gets a proper TextEdit.
     final resolved = await resolveCompletion(item);
     expect(resolved.textEdit, isNotNull);
-    expect(resolved.textEdit.newText, equals(item.insertText));
-    expect(
-      resolved.textEdit.range,
-      equals(rangeFromMarkers(content)),
-    );
+    final textEdit = toTextEdit(resolved.textEdit);
+    expect(textEdit.newText, equals(item.insertText));
+    expect(textEdit.range, equals(rangeFromMarkers(content)));
   }
 }
diff --git a/pkg/analysis_server/test/lsp/completion_yaml_test.dart b/pkg/analysis_server/test/lsp/completion_yaml_test.dart
index 0802d65..90827ef 100644
--- a/pkg/analysis_server/test/lsp/completion_yaml_test.dart
+++ b/pkg/analysis_server/test/lsp/completion_yaml_test.dart
@@ -106,8 +106,19 @@
     List<String> expectCompletions,
     String verifyEditsFor,
     String expectedContent,
+    String expectedContentIfInserting,
+    bool verifyInsertReplaceRanges = false,
   }) async {
-    await initialize();
+    // If verifyInsertReplaceRanges is true, we need both expected contents.
+    assert(verifyInsertReplaceRanges == false ||
+        (expectedContent != null && expectedContentIfInserting != null));
+
+    final textDocCapabilities = verifyInsertReplaceRanges
+        ? withCompletionItemInsertReplaceSupport(
+            emptyTextDocumentClientCapabilities)
+        : emptyTextDocumentClientCapabilities;
+
+    await initialize(textDocumentCapabilities: textDocCapabilities);
     await openFile(fileUri, withoutMarkers(content));
     final res = await getCompletion(fileUri, positionFromMarker(content));
 
@@ -125,8 +136,27 @@
       final item = res.singleWhere((c) => c.label == verifyEditsFor);
       expect(item.insertTextFormat, isNull);
       expect(item.insertText, isNull);
-      final updated = applyTextEdits(withoutMarkers(content), [item.textEdit]);
-      expect(updated, equals(expectedContent));
+
+      if (verifyInsertReplaceRanges) {
+        // Replacing.
+        final replaced = applyTextEdits(
+          withoutMarkers(content),
+          [textEditForReplace(item.textEdit)],
+        );
+        expect(replaced, equals(expectedContent));
+        // Inserting.
+        final inserted = applyTextEdits(
+          withoutMarkers(content),
+          [textEditForInsert(item.textEdit)],
+        );
+        expect(inserted, equals(expectedContentIfInserting));
+      } else {
+        final updated = applyTextEdits(
+          withoutMarkers(content),
+          [toTextEdit(item.textEdit)],
+        );
+        expect(updated, equals(expectedContent));
+      }
     }
   }
 }
@@ -220,6 +250,40 @@
 @reflectiveTest
 class PubspecCompletionTest extends AbstractLspAnalysisServerTest
     with CompletionTestMixin {
+  Future<void> test_insertReplaceRanges() async {
+    final content = '''
+name: foo
+version: 1.0.0
+
+environment:
+  s^dk
+''';
+    final expectedReplaced = '''
+name: foo
+version: 1.0.0
+
+environment:
+  sdk: 
+''';
+    final expectedInserted = '''
+name: foo
+version: 1.0.0
+
+environment:
+  sdk: dk
+''';
+
+    await verifyCompletions(
+      pubspecFileUri,
+      content,
+      expectCompletions: ['sdk: '],
+      verifyEditsFor: 'sdk: ',
+      verifyInsertReplaceRanges: true,
+      expectedContent: expectedReplaced,
+      expectedContentIfInserting: expectedInserted,
+    );
+  }
+
   Future<void> test_nested() async {
     final content = '''
 name: foo
diff --git a/pkg/analysis_server/test/lsp/server_abstract.dart b/pkg/analysis_server/test/lsp/server_abstract.dart
index f9d072c..d2ff935 100644
--- a/pkg/analysis_server/test/lsp/server_abstract.dart
+++ b/pkg/analysis_server/test/lsp/server_abstract.dart
@@ -271,6 +271,16 @@
     });
   }
 
+  TextDocumentClientCapabilities withCompletionItemInsertReplaceSupport(
+    TextDocumentClientCapabilities source,
+  ) {
+    return extendTextDocumentCapabilities(source, {
+      'completion': {
+        'completionItem': {'insertReplaceSupport': true}
+      }
+    });
+  }
+
   TextDocumentClientCapabilities withCompletionItemKinds(
     TextDocumentClientCapabilities source,
     List<CompletionItemKind> kinds,
@@ -1496,6 +1506,25 @@
     return expectSuccessfulResponseTo(request, (result) => result);
   }
 
+  /// Creates a [TextEdit] using the `insert` range of a [InsertReplaceEdit].
+  TextEdit textEditForInsert(Either2<TextEdit, InsertReplaceEdit> edit) =>
+      edit.map(
+        (_) => throw 'Expected InsertReplaceEdit, got TextEdit',
+        (e) => TextEdit(range: e.insert, newText: e.newText),
+      );
+
+  /// Creates a [TextEdit] using the `replace` range of a [InsertReplaceEdit].
+  TextEdit textEditForReplace(Either2<TextEdit, InsertReplaceEdit> edit) =>
+      edit.map(
+        (_) => throw 'Expected InsertReplaceEdit, got TextEdit',
+        (e) => TextEdit(range: e.replace, newText: e.newText),
+      );
+
+  TextEdit toTextEdit(Either2<TextEdit, InsertReplaceEdit> edit) => edit.map(
+        (e) => e,
+        (_) => throw 'Expected TextEdit, got InsertReplaceEdit',
+      );
+
   WorkspaceFolder toWorkspaceFolder(Uri uri) {
     return WorkspaceFolder(
       uri: uri.toString(),
diff --git a/pkg/analysis_server/tool/code_completion/relevance_table_generator.dart b/pkg/analysis_server/tool/code_completion/relevance_table_generator.dart
index df9e5d3..53e3fe2 100644
--- a/pkg/analysis_server/tool/code_completion/relevance_table_generator.dart
+++ b/pkg/analysis_server/tool/code_completion/relevance_table_generator.dart
@@ -20,6 +20,7 @@
 import 'package:analyzer/dart/element/type_provider.dart';
 import 'package:analyzer/dart/element/type_system.dart';
 import 'package:analyzer/diagnostic/diagnostic.dart';
+import 'package:analyzer/file_system/file_system.dart';
 import 'package:analyzer/file_system/physical_file_system.dart';
 import 'package:analyzer/src/dart/element/inheritance_manager3.dart';
 import 'package:analyzer/src/generated/engine.dart';
@@ -35,9 +36,6 @@
   var result = parser.parse(args);
 
   if (validArguments(parser, result)) {
-    var rootPath = result.rest[0];
-    print('Analyzing root: "$rootPath"');
-
     var provider = PhysicalResourceProvider.INSTANCE;
     var packageRoot = provider.pathContext.normalize(package_root.packageRoot);
     var generatedFilePath = provider.pathContext.join(
@@ -51,15 +49,61 @@
         'relevance_tables.g.dart');
     var generatedFile = provider.getFile(generatedFilePath);
 
+    void writeRelevanceTable(RelevanceData data, File generatedFile) {
+      var buffer = StringBuffer();
+      var writer = RelevanceTableWriter(buffer);
+      writer.write(data);
+      generatedFile.writeAsStringSync(buffer.toString());
+      DartFormat.formatFile(io.File(generatedFile.path));
+    }
+
+    if (result.wasParsed('reduceDir')) {
+      var data = RelevanceData();
+      var dir = provider.getFolder(result['reduceDir']);
+      for (var child in dir.getChildren()) {
+        if (child is File) {
+          var newData = RelevanceData.fromJson(child.readAsStringSync());
+          data.addData(newData);
+        }
+      }
+      writeRelevanceTable(data, generatedFile);
+      return;
+    }
+
+    var rootPath = result.rest[0];
+    print('Analyzing root: "$rootPath"');
+
+    File uniqueDataFile() {
+      var dataDir = result['mapDir'];
+      var baseFileName = provider.pathContext.basename(rootPath);
+      var index = 1;
+      while (index < 10000) {
+        var suffix = (index++).toString();
+        suffix = '0000'.substring(suffix.length) + suffix + '.json';
+        var fileName = baseFileName + suffix;
+        var filePath = provider.pathContext.join(dataDir, fileName);
+        var file = provider.getFile(filePath);
+        if (!file.exists) {
+          return file;
+        }
+      }
+
+      /// If there are more than 10000 directories with the same name, just
+      /// overwrite a previously generated file.
+      var fileName = baseFileName + '9999';
+      var filePath = provider.pathContext.join(dataDir, fileName);
+      return provider.getFile(filePath);
+    }
+
     var computer = RelevanceMetricsComputer();
-    var stopwatch = Stopwatch();
-    stopwatch.start();
+    var stopwatch = Stopwatch()..start();
     await computer.compute(rootPath, verbose: result['verbose']);
-    var buffer = StringBuffer();
-    var writer = RelevanceTableWriter(buffer);
-    writer.write(computer.data);
-    generatedFile.writeAsStringSync(buffer.toString());
-    DartFormat.formatFile(io.File(generatedFile.path));
+    if (result.wasParsed('mapDir')) {
+      var dataFile = uniqueDataFile();
+      dataFile.writeAsStringSync(computer.data.toJson());
+    } else {
+      writeRelevanceTable(computer.data, generatedFile);
+    }
     stopwatch.stop();
 
     var duration = Duration(milliseconds: stopwatch.elapsedMilliseconds);
@@ -74,6 +118,18 @@
     'help',
     abbr: 'h',
     help: 'Print this help message.',
+    negatable: false,
+  );
+  parser.addOption(
+    'mapDir',
+    help: 'The absolute path of the directory to which the relevance data will '
+        'be written. Using this option will prevent the relevance table from '
+        'being written.',
+  );
+  parser.addOption(
+    'reduceDir',
+    help: 'The absolute path of the directory from which the relevance data '
+        'will be read.',
   );
   parser.addFlag(
     'verbose',
@@ -103,17 +159,32 @@
   if (result.wasParsed('help')) {
     printUsage(parser);
     return false;
+  } else if (result.wasParsed('reduceDir')) {
+    if (result.rest.isNotEmpty) {
+      printUsage(parser,
+          error: 'A package path is not allowed in reduce mode.');
+      return false;
+    }
+    return validateDir(parser, result['reduceDir']);
   } else if (result.rest.length != 1) {
     printUsage(parser, error: 'No package path specified.');
     return false;
   }
-  var rootPath = result.rest[0];
-  if (!PhysicalResourceProvider.INSTANCE.pathContext.isAbsolute(rootPath)) {
-    printUsage(parser, error: 'The package path must be an absolute path.');
+  if (result.wasParsed('mapDir')) {
+    return validateDir(parser, result['mapDir']);
+  }
+  return validateDir(parser, result.rest[0]);
+}
+
+/// Return `true` if the [dirPath] is an absolute path to a directory that
+/// exists.
+bool validateDir(ArgParser parser, String dirPath) {
+  if (!PhysicalResourceProvider.INSTANCE.pathContext.isAbsolute(dirPath)) {
+    printUsage(parser, error: 'The path "$dirPath" must be an absolute path.');
     return false;
   }
-  if (!io.Directory(rootPath).existsSync()) {
-    printUsage(parser, error: 'The directory "$rootPath" does not exist.');
+  if (!io.Directory(dirPath).existsSync()) {
+    printUsage(parser, error: 'The directory "$dirPath" does not exist.');
     return false;
   }
   return true;
@@ -130,10 +201,11 @@
   /// Initialize a newly created set of relevance data based on the content of
   /// the JSON encoded string.
   RelevanceData.fromJson(String encoded) {
-    var map = json.decode(encoded) as Map<String, Map<String, String>>;
+    var map = json.decode(encoded) as Map<String, dynamic>;
     for (var contextEntry in map.entries) {
       var contextMap = byKind.putIfAbsent(contextEntry.key, () => {});
-      for (var kindEntry in contextEntry.value.entries) {
+      for (var kindEntry
+          in (contextEntry.value as Map<String, dynamic>).entries) {
         _Kind kind;
         var key = kindEntry.key;
         if (key.startsWith('e')) {
@@ -143,7 +215,7 @@
         } else {
           throw StateError('Invalid initial character in unique key "$key"');
         }
-        contextMap[kind] = int.parse(kindEntry.value);
+        contextMap[kind] = int.parse(kindEntry.value as String);
       }
     }
   }
@@ -160,11 +232,6 @@
     }
   }
 
-  /// Add the data from the given relevance [data] to this set of data.
-  void addDataFrom(RelevanceData data) {
-    _addToMap(byKind, data.byKind);
-  }
-
   /// Record that an element of the given [kind] was found in the given
   /// [context].
   void recordElementKind(String context, ElementKind kind) {
@@ -192,17 +259,6 @@
     }
     return json.encode(map);
   }
-
-  /// Add the data in the [source] map to the [target] map.
-  void _addToMap<K>(Map<K, Map<K, int>> target, Map<K, Map<K, int>> source) {
-    for (var outerEntry in source.entries) {
-      var innerTarget = target.putIfAbsent(outerEntry.key, () => {});
-      for (var innerEntry in outerEntry.value.entries) {
-        var innerKey = innerEntry.key;
-        innerTarget[innerKey] = (innerTarget[innerKey] ?? 0) + innerEntry.value;
-      }
-    }
-  }
 }
 
 /// An object that visits a compilation unit in order to record the data used to
diff --git a/pkg/analysis_server/tool/lsp_spec/generate_all.dart b/pkg/analysis_server/tool/lsp_spec/generate_all.dart
index ff160b1..ecc3079 100644
--- a/pkg/analysis_server/tool/lsp_spec/generate_all.dart
+++ b/pkg/analysis_server/tool/lsp_spec/generate_all.dart
@@ -261,10 +261,13 @@
     interface(
       'DartCompletionItemResolutionInfo',
       [
+        // These fields have short-ish names because they're on the payload
+        // for all suggestion-set backed completions.
         field('libId', type: 'number'),
         field('displayUri', type: 'string'),
-        field('rOffset', type: 'number'),
-        field('rLength', type: 'number'),
+        field('rOffset', type: 'number'), // replacementOffset
+        field('iLength', type: 'number'), // insertLength
+        field('rLength', type: 'number'), // replacementLength
       ],
       baseType: 'CompletionItemResolutionInfo',
     ),
diff --git a/pkg/analysis_server/tool/lsp_spec/typescript.dart b/pkg/analysis_server/tool/lsp_spec/typescript.dart
index f0a54e8..575d66e 100644
--- a/pkg/analysis_server/tool/lsp_spec/typescript.dart
+++ b/pkg/analysis_server/tool/lsp_spec/typescript.dart
@@ -90,7 +90,6 @@
     'CompletionItem': {
       'kind': 'CompletionItemKind',
       'data': 'CompletionItemResolutionInfo',
-      'textEdit': 'TextEdit',
     },
     'CallHierarchyItem': {
       'data': 'object',
diff --git a/pkg/analyzer/lib/src/dart/resolver/flow_analysis_visitor.dart b/pkg/analyzer/lib/src/dart/resolver/flow_analysis_visitor.dart
index 1f89eb6..3775ad7 100644
--- a/pkg/analyzer/lib/src/dart/resolver/flow_analysis_visitor.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/flow_analysis_visitor.dart
@@ -240,6 +240,19 @@
     flow!.finish();
   }
 
+  /// Transfers any test data that was recorded for [oldNode] so that it is now
+  /// associated with [newNode].  We need to do this when doing AST rewriting,
+  /// so that test data can be found using the rewritten tree.
+  void transferTestData(AstNode oldNode, AstNode newNode) {
+    var dataForTesting = this.dataForTesting;
+    if (dataForTesting != null) {
+      var oldNonPromotionReasons = dataForTesting.nonPromotionReasons[oldNode];
+      if (oldNonPromotionReasons != null) {
+        dataForTesting.nonPromotionReasons[newNode] = oldNonPromotionReasons;
+      }
+    }
+  }
+
   void variableDeclarationList(VariableDeclarationList node) {
     if (flow != null) {
       var variables = node.variables;
diff --git a/pkg/analyzer/lib/src/dart/resolver/function_expression_invocation_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/function_expression_invocation_resolver.dart
index a85d35e..c69fc95 100644
--- a/pkg/analyzer/lib/src/dart/resolver/function_expression_invocation_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/function_expression_invocation_resolver.dart
@@ -42,17 +42,21 @@
       return;
     }
 
-    _nullableDereferenceVerifier.expression(function,
-        errorCode: CompileTimeErrorCode.UNCHECKED_INVOCATION_OF_NULLABLE_VALUE);
-
     var receiverType = function.staticType;
-    if (receiverType is FunctionType) {
-      _resolve(node, receiverType);
+    if (receiverType is InterfaceType) {
+      // Note: in this circumstance it's not necessary to call
+      // `_nullableDereferenceVerifier.expression` because
+      // `_resolveReceiverInterfaceType` calls `TypePropertyResolver.resolve`,
+      // which does the necessary null checking.
+      _resolveReceiverInterfaceType(node, function, receiverType);
       return;
     }
 
-    if (receiverType is InterfaceType) {
-      _resolveReceiverInterfaceType(node, function, receiverType);
+    _nullableDereferenceVerifier.expression(function,
+        errorCode: CompileTimeErrorCode.UNCHECKED_INVOCATION_OF_NULLABLE_VALUE);
+
+    if (receiverType is FunctionType) {
+      _resolve(node, receiverType);
       return;
     }
 
diff --git a/pkg/analyzer/lib/src/dart/resolver/method_invocation_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/method_invocation_resolver.dart
index a975540..e05d2ad 100644
--- a/pkg/analyzer/lib/src/dart/resolver/method_invocation_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/method_invocation_resolver.dart
@@ -752,6 +752,8 @@
           node.methodName,
         );
       }
+      _resolver.flowAnalysis?.flow
+          ?.propertyGet(functionExpression, target, node.methodName.name);
       functionExpression.staticType = targetType;
     }
 
@@ -763,6 +765,7 @@
     NodeReplacer.replace(node, invocation);
     node.setProperty(_rewriteResultKey, invocation);
     InferenceContext.setTypeFromNode(invocation, node);
+    _resolver.flowAnalysis?.transferTestData(node, invocation);
   }
 
   void _setDynamicResolution(MethodInvocation node,
diff --git a/pkg/analyzer/lib/src/dart/resolver/type_property_resolver.dart b/pkg/analyzer/lib/src/dart/resolver/type_property_resolver.dart
index d6204de..99b1277 100644
--- a/pkg/analyzer/lib/src/dart/resolver/type_property_resolver.dart
+++ b/pkg/analyzer/lib/src/dart/resolver/type_property_resolver.dart
@@ -2,7 +2,6 @@
 // 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:_fe_analyzer_shared/src/flow_analysis/flow_analysis.dart';
 import 'package:analyzer/dart/ast/ast.dart';
 import 'package:analyzer/dart/ast/syntactic_entity.dart';
 import 'package:analyzer/dart/element/element.dart';
@@ -13,13 +12,9 @@
 import 'package:analyzer/src/dart/element/type_provider.dart';
 import 'package:analyzer/src/dart/element/type_system.dart';
 import 'package:analyzer/src/dart/resolver/extension_member_resolver.dart';
-import 'package:analyzer/src/dart/resolver/flow_analysis_visitor.dart';
 import 'package:analyzer/src/dart/resolver/resolution_result.dart';
-import 'package:analyzer/src/diagnostic/diagnostic.dart';
 import 'package:analyzer/src/error/codes.dart';
 import 'package:analyzer/src/generated/resolver.dart';
-import 'package:analyzer/src/generated/source.dart';
-import 'package:analyzer/src/util/ast_data_extractor.dart';
 
 /// Helper for resolving properties (getters, setters, or methods).
 class TypePropertyResolver {
@@ -120,35 +115,8 @@
         }
       }
 
-      List<DiagnosticMessage> messages = [];
-      if (receiver != null) {
-        var whyNotPromoted =
-            _resolver.flowAnalysis?.flow?.whyNotPromoted(receiver);
-        if (whyNotPromoted != null) {
-          for (var entry in whyNotPromoted.entries) {
-            var whyNotPromotedVisitor = _WhyNotPromotedVisitor(_resolver.source,
-                receiver, _resolver.flowAnalysis!.dataForTesting);
-            if (_typeSystem.isPotentiallyNullable(entry.key)) continue;
-            var message = entry.value.accept(whyNotPromotedVisitor);
-            if (message != null) {
-              if (_resolver.flowAnalysis!.dataForTesting != null) {
-                var nonPromotionReasonText = entry.value.shortName;
-                if (whyNotPromotedVisitor.propertyReference != null) {
-                  var id =
-                      computeMemberId(whyNotPromotedVisitor.propertyReference!);
-                  nonPromotionReasonText += '($id)';
-                }
-                _resolver.flowAnalysis!.dataForTesting!
-                        .nonPromotionReasons[nameErrorEntity] =
-                    nonPromotionReasonText;
-              }
-              messages = [message];
-            }
-            break;
-          }
-        }
-      }
-
+      List<DiagnosticMessage> messages =
+          _resolver.computeWhyNotPromotedMessages(receiver, nameErrorEntity);
       _resolver.nullableDereferenceVerifier.report(
           receiverErrorNode, receiverType,
           errorCode: errorCode, arguments: [name], messages: messages);
@@ -299,102 +267,3 @@
     );
   }
 }
-
-class _WhyNotPromotedVisitor
-    implements
-        NonPromotionReasonVisitor<DiagnosticMessage?, AstNode, Expression,
-            PromotableElement> {
-  final Source source;
-
-  final Expression _receiver;
-
-  final FlowAnalysisDataForTesting? _dataForTesting;
-
-  PropertyAccessorElement? propertyReference;
-
-  _WhyNotPromotedVisitor(this.source, this._receiver, this._dataForTesting);
-
-  @override
-  DiagnosticMessage? visitDemoteViaExplicitWrite(
-      DemoteViaExplicitWrite<PromotableElement, Expression> reason) {
-    var writeExpression = reason.writeExpression;
-    if (_dataForTesting != null) {
-      _dataForTesting!.nonPromotionReasonTargets[writeExpression] =
-          reason.shortName;
-    }
-    var variableName = reason.variable.name;
-    if (variableName == null) return null;
-    return _contextMessageForWrite(variableName, writeExpression);
-  }
-
-  @override
-  DiagnosticMessage? visitDemoteViaForEachVariableWrite(
-      DemoteViaForEachVariableWrite<PromotableElement, AstNode> reason) {
-    var node = reason.node;
-    var variableName = reason.variable.name;
-    if (variableName == null) return null;
-    ForLoopParts parts;
-    if (node is ForStatement) {
-      parts = node.forLoopParts;
-    } else if (node is ForElement) {
-      parts = node.forLoopParts;
-    } else {
-      assert(false, 'Unexpected node type');
-      return null;
-    }
-    if (parts is ForEachPartsWithIdentifier) {
-      var identifier = parts.identifier;
-      if (_dataForTesting != null) {
-        _dataForTesting!.nonPromotionReasonTargets[identifier] =
-            reason.shortName;
-      }
-      return _contextMessageForWrite(variableName, identifier);
-    } else {
-      assert(false, 'Unexpected parts type');
-      return null;
-    }
-  }
-
-  @override
-  DiagnosticMessage? visitPropertyNotPromoted(PropertyNotPromoted reason) {
-    var receiver = _receiver;
-    Element? receiverElement;
-    if (receiver is SimpleIdentifier) {
-      receiverElement = receiver.staticElement;
-    } else if (receiver is PropertyAccess) {
-      receiverElement = receiver.propertyName.staticElement;
-    } else if (receiver is PrefixedIdentifier) {
-      receiverElement = receiver.identifier.staticElement;
-    } else {
-      assert(false, 'Unrecognized receiver: ${receiver.runtimeType}');
-    }
-    if (receiverElement is PropertyAccessorElement) {
-      propertyReference = receiverElement;
-      return _contextMessageForProperty(receiverElement, reason.propertyName);
-    } else {
-      assert(receiverElement == null,
-          'Unrecognized receiver element: ${receiverElement.runtimeType}');
-      return null;
-    }
-  }
-
-  DiagnosticMessageImpl _contextMessageForProperty(
-      PropertyAccessorElement property, String propertyName) {
-    return DiagnosticMessageImpl(
-        filePath: property.source.fullName,
-        message:
-            "'$propertyName' refers to a property so it could not be promoted.",
-        offset: property.nameOffset,
-        length: property.nameLength);
-  }
-
-  DiagnosticMessageImpl _contextMessageForWrite(
-      String variableName, Expression writeExpression) {
-    return DiagnosticMessageImpl(
-        filePath: source.fullName,
-        message: "Variable '$variableName' could be null due to an intervening "
-            "write.",
-        offset: writeExpression.offset,
-        length: writeExpression.length);
-  }
-}
diff --git a/pkg/analyzer/lib/src/error/nullable_dereference_verifier.dart b/pkg/analyzer/lib/src/error/nullable_dereference_verifier.dart
index 48816ec..6044fab 100644
--- a/pkg/analyzer/lib/src/error/nullable_dereference_verifier.dart
+++ b/pkg/analyzer/lib/src/error/nullable_dereference_verifier.dart
@@ -11,17 +11,23 @@
 import 'package:analyzer/src/dart/element/type.dart';
 import 'package:analyzer/src/dart/element/type_system.dart';
 import 'package:analyzer/src/error/codes.dart';
+import 'package:analyzer/src/generated/resolver.dart';
 
 /// Helper for checking potentially nullable dereferences.
 class NullableDereferenceVerifier {
   final TypeSystemImpl _typeSystem;
   final ErrorReporter _errorReporter;
 
+  /// The resolver driving this participant.
+  final ResolverVisitor _resolver;
+
   NullableDereferenceVerifier({
     required TypeSystemImpl typeSystem,
     required ErrorReporter errorReporter,
+    required ResolverVisitor resolver,
   })   : _typeSystem = typeSystem,
-        _errorReporter = errorReporter;
+        _errorReporter = errorReporter,
+        _resolver = resolver;
 
   bool expression(Expression expression,
       {DartType? type, ErrorCode? errorCode}) {
@@ -59,7 +65,11 @@
       return false;
     }
 
-    report(errorNode, receiverType, errorCode: errorCode);
+    List<DiagnosticMessage>? messages;
+    if (errorNode is Expression) {
+      messages = _resolver.computeWhyNotPromotedMessages(errorNode, errorNode);
+    }
+    report(errorNode, receiverType, errorCode: errorCode, messages: messages);
     return true;
   }
 }
diff --git a/pkg/analyzer/lib/src/generated/resolver.dart b/pkg/analyzer/lib/src/generated/resolver.dart
index 1e34cb0..1411df8 100644
--- a/pkg/analyzer/lib/src/generated/resolver.dart
+++ b/pkg/analyzer/lib/src/generated/resolver.dart
@@ -4,14 +4,17 @@
 
 import 'dart:collection';
 
+import 'package:_fe_analyzer_shared/src/flow_analysis/flow_analysis.dart';
 import 'package:analyzer/dart/analysis/features.dart';
 import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/syntactic_entity.dart';
 import 'package:analyzer/dart/ast/visitor.dart';
 import 'package:analyzer/dart/element/element.dart';
 import 'package:analyzer/dart/element/nullability_suffix.dart';
 import 'package:analyzer/dart/element/scope.dart';
 import 'package:analyzer/dart/element/type.dart';
 import 'package:analyzer/dart/element/type_provider.dart';
+import 'package:analyzer/diagnostic/diagnostic.dart';
 import 'package:analyzer/error/error.dart';
 import 'package:analyzer/error/listener.dart';
 import 'package:analyzer/src/dart/ast/ast.dart';
@@ -47,6 +50,7 @@
 import 'package:analyzer/src/dart/resolver/typed_literal_resolver.dart';
 import 'package:analyzer/src/dart/resolver/variable_declaration_resolver.dart';
 import 'package:analyzer/src/dart/resolver/yield_statement_resolver.dart';
+import 'package:analyzer/src/diagnostic/diagnostic.dart';
 import 'package:analyzer/src/error/bool_expression_verifier.dart';
 import 'package:analyzer/src/error/codes.dart';
 import 'package:analyzer/src/error/dead_code_verifier.dart';
@@ -61,6 +65,7 @@
 import 'package:analyzer/src/generated/this_access_tracker.dart';
 import 'package:analyzer/src/generated/type_promotion_manager.dart';
 import 'package:analyzer/src/generated/variable_type_provider.dart';
+import 'package:analyzer/src/util/ast_data_extractor.dart';
 import 'package:meta/meta.dart';
 
 /// Maintains and manages contextual type information used for
@@ -315,6 +320,7 @@
     nullableDereferenceVerifier = NullableDereferenceVerifier(
       typeSystem: typeSystem,
       errorReporter: errorReporter,
+      resolver: this,
     );
     boolExpressionVerifier = BoolExpressionVerifier(
       typeSystem: typeSystem,
@@ -505,6 +511,39 @@
     nullSafetyDeadCodeVerifier.visitNode(node);
   }
 
+  /// Computes the appropriate set of context messages to report along with an
+  /// error that may have occurred because [receiver] was not type promoted.
+  List<DiagnosticMessage> computeWhyNotPromotedMessages(
+      Expression? receiver, SyntacticEntity errorEntity) {
+    List<DiagnosticMessage> messages = [];
+    if (receiver != null) {
+      var whyNotPromoted = flowAnalysis?.flow?.whyNotPromoted(receiver);
+      if (whyNotPromoted != null) {
+        for (var entry in whyNotPromoted.entries) {
+          var whyNotPromotedVisitor = _WhyNotPromotedVisitor(
+              source, receiver, flowAnalysis!.dataForTesting);
+          if (typeSystem.isPotentiallyNullable(entry.key)) continue;
+          var message = entry.value.accept(whyNotPromotedVisitor);
+          if (message != null) {
+            if (flowAnalysis!.dataForTesting != null) {
+              var nonPromotionReasonText = entry.value.shortName;
+              if (whyNotPromotedVisitor.propertyReference != null) {
+                var id =
+                    computeMemberId(whyNotPromotedVisitor.propertyReference!);
+                nonPromotionReasonText += '($id)';
+              }
+              flowAnalysis!.dataForTesting!.nonPromotionReasons[errorEntity] =
+                  nonPromotionReasonText;
+            }
+            messages = [message];
+          }
+          break;
+        }
+      }
+    }
+    return messages;
+  }
+
   /// Return the static element associated with the given expression whose type
   /// can be overridden, or `null` if there is no element whose type can be
   /// overridden.
@@ -3297,3 +3336,102 @@
     return null;
   }
 }
+
+class _WhyNotPromotedVisitor
+    implements
+        NonPromotionReasonVisitor<DiagnosticMessage?, AstNode, Expression,
+            PromotableElement> {
+  final Source source;
+
+  final Expression _receiver;
+
+  final FlowAnalysisDataForTesting? _dataForTesting;
+
+  PropertyAccessorElement? propertyReference;
+
+  _WhyNotPromotedVisitor(this.source, this._receiver, this._dataForTesting);
+
+  @override
+  DiagnosticMessage? visitDemoteViaExplicitWrite(
+      DemoteViaExplicitWrite<PromotableElement, Expression> reason) {
+    var writeExpression = reason.writeExpression;
+    if (_dataForTesting != null) {
+      _dataForTesting!.nonPromotionReasonTargets[writeExpression] =
+          reason.shortName;
+    }
+    var variableName = reason.variable.name;
+    if (variableName == null) return null;
+    return _contextMessageForWrite(variableName, writeExpression);
+  }
+
+  @override
+  DiagnosticMessage? visitDemoteViaForEachVariableWrite(
+      DemoteViaForEachVariableWrite<PromotableElement, AstNode> reason) {
+    var node = reason.node;
+    var variableName = reason.variable.name;
+    if (variableName == null) return null;
+    ForLoopParts parts;
+    if (node is ForStatement) {
+      parts = node.forLoopParts;
+    } else if (node is ForElement) {
+      parts = node.forLoopParts;
+    } else {
+      assert(false, 'Unexpected node type');
+      return null;
+    }
+    if (parts is ForEachPartsWithIdentifier) {
+      var identifier = parts.identifier;
+      if (_dataForTesting != null) {
+        _dataForTesting!.nonPromotionReasonTargets[identifier] =
+            reason.shortName;
+      }
+      return _contextMessageForWrite(variableName, identifier);
+    } else {
+      assert(false, 'Unexpected parts type');
+      return null;
+    }
+  }
+
+  @override
+  DiagnosticMessage? visitPropertyNotPromoted(PropertyNotPromoted reason) {
+    var receiver = _receiver;
+    Element? receiverElement;
+    if (receiver is SimpleIdentifier) {
+      receiverElement = receiver.staticElement;
+    } else if (receiver is PropertyAccess) {
+      receiverElement = receiver.propertyName.staticElement;
+    } else if (receiver is PrefixedIdentifier) {
+      receiverElement = receiver.identifier.staticElement;
+    } else {
+      assert(false, 'Unrecognized receiver: ${receiver.runtimeType}');
+    }
+    if (receiverElement is PropertyAccessorElement) {
+      propertyReference = receiverElement;
+      return _contextMessageForProperty(receiverElement, reason.propertyName);
+    } else {
+      assert(receiverElement == null,
+          'Unrecognized receiver element: ${receiverElement.runtimeType}');
+      return null;
+    }
+  }
+
+  DiagnosticMessageImpl _contextMessageForProperty(
+      PropertyAccessorElement property, String propertyName) {
+    return DiagnosticMessageImpl(
+        filePath: property.source.fullName,
+        message:
+            "'$propertyName' refers to a property so it could not be promoted.",
+        offset: property.nameOffset,
+        length: property.nameLength);
+  }
+
+  DiagnosticMessageImpl _contextMessageForWrite(
+      String variableName, Expression writeExpression) {
+    return DiagnosticMessageImpl(
+        filePath: source.fullName,
+        message: "Variable '$variableName' could be null due to an intervening "
+            "write.",
+        offset: writeExpression.offset,
+        length: writeExpression.length);
+  }
+}
diff --git a/tools/VERSION b/tools/VERSION
index 54c83b9..ed26e86 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 13
 PATCH 0
-PRERELEASE 68
+PRERELEASE 69
 PRERELEASE_PATCH 0
\ No newline at end of file