Version 2.17.0-116.0.dev

Merge commit '48326ee2e2462d32f53bd38c2a3cff4fce89ef99' into 'dev'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0e3ef9d..012897a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,21 @@
 - `IdbFactory.supportsDatabaseNames` has been deprecated. It will always return
   `false`.
 
+#### `dart:io`
+
+- **Breaking Change** [#47887](https://github.com/dart-lang/sdk/issues/47887):
+  `HttpClient` has a new `connectionFactory` property, which allows socket
+  creation to be customized. Classes that `implement HttpClient` may be broken
+  by this change. Add the following method to your classes to fix them:
+
+  ```dart
+  void set connectionFactory(
+      Future<ConnectionTask<Socket>> Function(
+              Uri url, String? proxyHost, int? proxyPort)?
+          f) =>
+      throw UnsupportedError("connectionFactory not implemented");
+  ```
+
 ### Tools
 
 #### Dart command line
diff --git a/DEPS b/DEPS
index 20ccf56..dc28abe 100644
--- a/DEPS
+++ b/DEPS
@@ -126,7 +126,7 @@
   "json_rpc_2_rev": "7e00f893440a72de0637970325e4ea44bd1e8c8e",
   "linter_tag": "1.18.0",
   "lints_tag": "f9670df2a66e0ec12eb51554e70c1cbf56c8f5d0",
-  "logging_rev": "575781ef196e4fed4fb737e38fb4b73d62727187",
+  "logging_rev": "dfbe88b890c3b4f7bc06da5a7b3b43e9e263b688",
   "markupsafe_rev": "8f45f5cfa0009d2a70589bcda0349b8cb2b72783",
   "markdown_rev": "7479783f0493f6717e1d7ae31cb37d39a91026b2",
   "matcher_rev": "07595a7739d47a8315caba5a8e58fb9ae3d81261",
diff --git a/pkg/analysis_server/lib/src/lsp/mapping.dart b/pkg/analysis_server/lib/src/lsp/mapping.dart
index ba6a1d6..817937e 100644
--- a/pkg/analysis_server/lib/src/lsp/mapping.dart
+++ b/pkg/analysis_server/lib/src/lsp/mapping.dart
@@ -18,6 +18,7 @@
 import 'package:analysis_server/src/lsp/constants.dart';
 import 'package:analysis_server/src/lsp/dartdoc.dart';
 import 'package:analysis_server/src/lsp/lsp_analysis_server.dart' as lsp;
+import 'package:analysis_server/src/lsp/snippets.dart';
 import 'package:analysis_server/src/lsp/source_edits.dart';
 import 'package:analysis_server/src/protocol_server.dart' as server
     hide AnalysisError;
@@ -68,50 +69,6 @@
           _asMarkup(preferredFormats, content));
 }
 
-/// Builds an LSP snippet string with supplied ranges as tabstops.
-String buildSnippetStringWithTabStops(
-  String? text,
-  List<int>? offsetLengthPairs,
-) {
-  text ??= '';
-  offsetLengthPairs ??= const [];
-
-  // Snippets syntax is documented in the LSP spec:
-  // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#snippet_syntax
-  //
-  // $1, $2, etc. are used for tab stops and ${1:foo} inserts a placeholder of foo.
-
-  final output = [];
-  var offset = 0;
-
-  // When there's only a single tabstop, it should be ${0} as this is treated
-  // specially as the final cursor position (if we use 1, the editor will insert
-  // a 0 at the end of the string which is not what we expect).
-  // When there are multiple, start with ${1} since these are placeholders the
-  // user can tab through and the editor-inserted ${0} at the end is expected.
-  var tabStopNumber = offsetLengthPairs.length <= 2 ? 0 : 1;
-
-  for (var i = 0; i < offsetLengthPairs.length; i += 2) {
-    final pairOffset = offsetLengthPairs[i];
-    final pairLength = offsetLengthPairs[i + 1];
-
-    // Add any text that came before this tabstop to the result.
-    output.add(escapeSnippetString(text.substring(offset, pairOffset)));
-
-    // Add this tabstop
-    final tabStopText = escapeSnippetString(
-        text.substring(pairOffset, pairOffset + pairLength));
-    output.add('\${${tabStopNumber++}:$tabStopText}');
-
-    offset = pairOffset + pairLength;
-  }
-
-  // Add any remaining text that was after the last tabstop.
-  output.add(escapeSnippetString(text.substring(offset)));
-
-  return output.join('');
-}
-
 /// Creates a [lsp.WorkspaceEdit] from simple [server.SourceFileEdit]s.
 ///
 /// Note: This code will fetch the version of each document being modified so
@@ -573,15 +530,6 @@
       .firstWhere(isSupported, orElse: () => lsp.SymbolKind.Obj);
 }
 
-/// Escapes a string to be used in an LSP edit that uses Snippet mode.
-///
-/// Snippets can contain special markup like `${a:b}` so some characters need
-/// escaping (according to the LSP spec, those are `$`, `}` and `\`).
-String escapeSnippetString(String input) => input.replaceAllMapped(
-      RegExp(r'[$}\\]'), // Replace any of $ } \
-      (c) => '\\${c[0]}', // Prefix with a backslash
-    );
-
 String? getCompletionDetail(
   server.CompletionSuggestion suggestion,
   lsp.CompletionItemKind? completionKind,
@@ -942,6 +890,25 @@
   );
 }
 
+/// Creates a SnippetTextEdit for an edit with a selection placeholder.
+///
+/// [selectionOffset] is relative to (and therefore must be within) the edit.
+lsp.SnippetTextEdit snippetTextEditWithSelection(
+  server.LineInfo lineInfo,
+  server.SourceEdit edit, {
+  required int selectionOffsetRelative,
+  int? selectionLength,
+}) {
+  return lsp.SnippetTextEdit(
+    insertTextFormat: lsp.InsertTextFormat.Snippet,
+    range: toRange(lineInfo, edit.offset, edit.length),
+    newText: buildSnippetStringWithTabStops(
+      edit.replacement,
+      [selectionOffsetRelative, selectionLength ?? 0],
+    ),
+  );
+}
+
 lsp.CompletionItemKind? suggestionKindToCompletionItemKind(
   Set<lsp.CompletionItemKind> supportedCompletionKinds,
   server.CompletionSuggestionKind kind,
@@ -1405,20 +1372,6 @@
   );
 }
 
-lsp.SnippetTextEdit toSnippetTextEdit(
-    LspClientCapabilities capabilities,
-    server.LineInfo lineInfo,
-    server.SourceEdit edit,
-    int selectionOffsetRelative,
-    int? selectionLength) {
-  return lsp.SnippetTextEdit(
-    insertTextFormat: lsp.InsertTextFormat.Snippet,
-    range: toRange(lineInfo, edit.offset, edit.length),
-    newText: buildSnippetStringWithTabStops(
-        edit.replacement, [selectionOffsetRelative, selectionLength ?? 0]),
-  );
-}
-
 ErrorOr<server.SourceRange> toSourceRange(
     server.LineInfo lineInfo, Range range) {
   // If there is a range, convert to offsets because that's what
@@ -1467,8 +1420,9 @@
         toTextEdit(lineInfo, edit));
   }
   return Either3<lsp.SnippetTextEdit, lsp.AnnotatedTextEdit, lsp.TextEdit>.t1(
-      toSnippetTextEdit(capabilities, lineInfo, edit, selectionOffsetRelative,
-          selectionLength));
+      snippetTextEditWithSelection(lineInfo, edit,
+          selectionOffsetRelative: selectionOffsetRelative,
+          selectionLength: selectionLength));
 }
 
 lsp.TextEdit toTextEdit(server.LineInfo lineInfo, server.SourceEdit edit) {
@@ -1580,21 +1534,20 @@
       insertTextFormat = lsp.InsertTextFormat.Snippet;
       final hasRequiredParameters =
           (defaultArgumentListTextRanges?.length ?? 0) > 0;
-      final functionCallSuffix = hasRequiredParameters
-          ? buildSnippetStringWithTabStops(
-              defaultArgumentListString,
-              defaultArgumentListTextRanges,
-            )
-          : '\${0:}'; // No required params still gets a tabstop in the parens.
-      insertText = '${escapeSnippetString(insertText)}($functionCallSuffix)';
+      final functionCallSuffix =
+          hasRequiredParameters && defaultArgumentListString != null
+              ? buildSnippetStringWithTabStops(
+                  defaultArgumentListString, defaultArgumentListTextRanges)
+              // No required params still gets a final tab stop in the parens.
+              : SnippetBuilder.finalTabStop;
+      insertText =
+          '${SnippetBuilder.escapeSnippetPlainText(insertText)}($functionCallSuffix)';
     } else if (selectionOffset != 0 &&
-        // We don't need a tabstop if the selection is the end of the string.
+        // We don't need a tab stop if the selection is the end of the string.
         selectionOffset != completion.length) {
       insertTextFormat = lsp.InsertTextFormat.Snippet;
       insertText = buildSnippetStringWithTabStops(
-        completion,
-        [selectionOffset, selectionLength],
-      );
+          completion, [selectionOffset, selectionLength]);
     }
   }
 
diff --git a/pkg/analysis_server/lib/src/lsp/snippets.dart b/pkg/analysis_server/lib/src/lsp/snippets.dart
new file mode 100644
index 0000000..6f59d8e
--- /dev/null
+++ b/pkg/analysis_server/lib/src/lsp/snippets.dart
@@ -0,0 +1,241 @@
+// Copyright (c) 2022, 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:math' as math;
+
+import 'package:collection/collection.dart';
+
+/// Builds an LSP snippet string with supplied ranges as tab stops.
+///
+/// [tabStopOffsetLengthPairs] are relative to the supplied text.
+String buildSnippetStringWithTabStops(
+  String text,
+  List<int>? tabStopOffsetLengthPairs,
+) {
+  tabStopOffsetLengthPairs ??= const [];
+  assert(tabStopOffsetLengthPairs.length % 2 == 0);
+
+  // Convert selection/tab stops/edit groups all into a common format
+  // (`SnippetPlaceholder`) so they can be handled in a single pass through
+  // the text.
+  final placeholders = [
+    // Tab stops.
+    for (var i = 0; i < tabStopOffsetLengthPairs.length - 1; i += 2)
+      SnippetPlaceholder(
+        tabStopOffsetLengthPairs[i],
+        tabStopOffsetLengthPairs[i + 1],
+        // If there's only a single tab stop, mark
+        // it as the final stop so it exit "snippet mode" when tabbed to.
+        isFinal: tabStopOffsetLengthPairs.length == 2,
+      ),
+    // TODO(dantup): Add edit group/selection support.
+  ];
+
+  // Remove any groups outside of the range (it's possible the edit groups apply
+  // to a different edit in the collection).
+  placeholders.removeWhere((placeholder) =>
+      placeholder.offset < 0 ||
+      placeholder.offset + placeholder.length > text.length);
+
+  final builder = SnippetBuilder()..appendPlaceholders(text, placeholders);
+  return builder.value;
+}
+
+/// A helper for building for snippets using LSP/TextMate syntax.
+///
+/// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#snippet_syntax
+///
+///  - $1, $2, etc. are used for tab stops
+///  - ${1:foo} inserts a placeholder of foo
+///  - ${1|foo,bar|} inserts a placeholder of foo with a selection list
+///      containing "foo" and "bar"
+class SnippetBuilder {
+  /// The constant `$0` used do indicate a final tab stop in the snippet syntax.
+  static const finalTabStop = r'$0';
+
+  final _buffer = StringBuffer();
+
+  var _nextPlaceholder = 1;
+
+  /// The built snippet text using the LSP snippet syntax.
+  String get value => _buffer.toString();
+
+  /// Appends a placeholder with a set of choices to choose from.
+  ///
+  /// If there are 0 or 1 choices, a placeholder will be inserted instead.
+  ///
+  /// Returns the placeholder number used.
+  int appendChoice(Iterable<String> choices, {int? placeholderNumber}) {
+    final uniqueChoices = choices.where((item) => item.isNotEmpty).toSet();
+
+    // If there's only 0/1 items, we can downgrade this to a placeholder.
+    if (uniqueChoices.length <= 1) {
+      return appendPlaceholder(
+        uniqueChoices.firstOrNull ?? '',
+        placeholderNumber: placeholderNumber,
+      );
+    }
+
+    placeholderNumber = _usePlaceholerNumber(placeholderNumber);
+
+    final escapedChoices = uniqueChoices.map(escapeSnippetChoiceText).join(',');
+    _buffer.write('\${$placeholderNumber|$escapedChoices|}');
+
+    return placeholderNumber;
+  }
+
+  /// Appends a placeholder with the given text.
+  ///
+  /// If the text is empty, inserts a tab stop instead.
+  ///
+  /// Returns the placeholder number used.
+  int appendPlaceholder(String text, {int? placeholderNumber}) {
+    // If there's no text, we can downgrade this to a tab stop.
+    if (text.isEmpty) {
+      return appendTabStop(placeholderNumber: placeholderNumber);
+    }
+
+    placeholderNumber = _usePlaceholerNumber(placeholderNumber);
+
+    final escapedText = escapeSnippetVariableText(text);
+    _buffer.write('\${$placeholderNumber:$escapedText}');
+
+    return placeholderNumber;
+  }
+
+  /// Appends a tab stop.
+  ///
+  /// Returns the placeholder number used.
+  int appendTabStop({int? placeholderNumber}) {
+    placeholderNumber = _usePlaceholerNumber(placeholderNumber);
+
+    _buffer.write('\$$placeholderNumber');
+
+    return placeholderNumber;
+  }
+
+  /// Appends normal text (escaping it as required).
+  void appendText(String text) {
+    _buffer.write(escapeSnippetPlainText(text));
+  }
+
+  /// Generates the current and next placeholder numbers.
+  int _usePlaceholerNumber(int? placeholderNumber) {
+    // If a number was not supplied, use thenext available one.
+    placeholderNumber ??= _nextPlaceholder;
+    // If the number we used was the highest seen, set the next one after it.
+    _nextPlaceholder = math.max(_nextPlaceholder, placeholderNumber + 1);
+
+    return placeholderNumber;
+  }
+
+  /// Escapes a string use inside a "choice" in a snippet.
+  ///
+  /// Similar to [escapeSnippetPlainText], but choices are delimited/separated
+  /// by pipes and commas (`${1:|a,b,c|}`).
+  static String escapeSnippetChoiceText(String input) => _escapeCharacters(
+        input,
+        RegExp(r'[$}\\\|,]'), // Replace any of $ } \ | ,
+      );
+
+  /// Escapes a string to be used in an LSP edit that uses Snippet mode where the
+  /// text is outside of a snippet token.
+  ///
+  /// Snippets can contain special markup like `${a:b}` so `$` needs escaping
+  /// as does `\` so it's not interpreted as an escape.
+  static String escapeSnippetPlainText(String input) => _escapeCharacters(
+        input,
+        RegExp(r'[$\\]'), // Replace any of $ \
+      );
+
+  /// Escapes a string to be used inside a snippet token.
+  ///
+  /// Similar to [escapeSnippetPlainText] but additionally escapes `}` so that the
+  /// token is not ended early if the included text contains braces.
+  static String escapeSnippetVariableText(String input) => _escapeCharacters(
+        input,
+        RegExp(r'[$}\\]'), // Replace any of $ } \
+      );
+
+  /// Escapes [pattern] in [input] with backslashes.
+  static String _escapeCharacters(String input, Pattern pattern) =>
+      input.replaceAllMapped(pattern, (c) => '\\${c[0]}');
+}
+
+/// Information about an individual placeholder/tab stop in a piece of code.
+///
+/// Each placeholder represents a single position into the code, so a linked
+/// edit group with 2 positions will be represented as two instances of this
+/// class (with the same [linkedGroupId]).
+class SnippetPlaceholder {
+  final int offset;
+  final int length;
+  final List<String>? suggestions;
+  final int? linkedGroupId;
+  final bool isFinal;
+
+  SnippetPlaceholder(
+    this.offset,
+    this.length, {
+    this.suggestions,
+    this.linkedGroupId,
+    this.isFinal = false,
+  });
+}
+
+/// Helpers for [SnippetBuilder] that do not relate to building the main snippet
+/// syntax (for example, converting from intermediate structures).
+extension SnippetBuilderExtensions on SnippetBuilder {
+  void appendPlaceholders(String text, List<SnippetPlaceholder> placeholders) {
+    // Ensure placeholders are in the order they're visible in the source so
+    // tabbing through them doesn't appear to jump around.
+    placeholders.sortBy<num>((placeholder) => placeholder.offset);
+
+    // We need to use the same placeholder number for all placeholders in the
+    // same linked group, so the first time we see a linked item, store its
+    // placeholder number here, so subsequent placeholders for the same linked
+    // group can reuse it.
+    final placeholderIdForLinkedGroupId = <int, int>{};
+
+    var offset = 0;
+    for (final placeholder in placeholders) {
+      // Add any text that came before this placeholder to the result.
+      appendText(text.substring(offset, placeholder.offset));
+
+      final linkedGroupId = placeholder.linkedGroupId;
+      int? thisPaceholderNumber;
+      // Override the placeholder number if it's the final one (0) or needs to
+      // re-use an existing one for a linked group.
+      if (placeholder.isFinal) {
+        thisPaceholderNumber = 0;
+      } else if (linkedGroupId != null) {
+        thisPaceholderNumber = placeholderIdForLinkedGroupId[linkedGroupId];
+      }
+
+      // Append the placeholder/choices.
+      final placeholderText = text.substring(
+        placeholder.offset,
+        placeholder.offset + placeholder.length,
+      );
+      // appendChoice handles mapping empty/single suggestions to a normal
+      // placeholder.
+      thisPaceholderNumber = appendChoice(
+        [placeholderText, ...?placeholder.suggestions],
+        placeholderNumber: thisPaceholderNumber,
+      );
+
+      // Track where we're up to.
+      offset = placeholder.offset + placeholder.length;
+
+      // Store the placeholder number used for linked groups so it can be reused
+      // by subsequent references to it.
+      if (linkedGroupId != null) {
+        placeholderIdForLinkedGroupId[linkedGroupId] = thisPaceholderNumber;
+      }
+    }
+
+    // Add any remaining text that was after the last placeholder.
+    appendText(text.substring(offset));
+  }
+}
diff --git a/pkg/analysis_server/test/lsp/completion.dart b/pkg/analysis_server/test/lsp/completion.dart
index 6d70a26..ea8d26b 100644
--- a/pkg/analysis_server/test/lsp/completion.dart
+++ b/pkg/analysis_server/test/lsp/completion.dart
@@ -101,10 +101,10 @@
     return null;
   }
 
-  /// Replaces the LSP snippet placeholder '${0:}' with '^' for easier verifying
+  /// Replaces the LSP snippet placeholder '$0' with '^' for easier verifying
   /// of the cursor position in completions.
   String withCaret(String contents, InsertTextFormat? format) =>
       format == InsertTextFormat.Snippet
-          ? contents.replaceFirst(r'${0:}', '^')
+          ? contents.replaceFirst(r'$0', '^')
           : contents;
 }
diff --git a/pkg/analysis_server/test/lsp/completion_dart_test.dart b/pkg/analysis_server/test/lsp/completion_dart_test.dart
index 01f2b09..1f749a6 100644
--- a/pkg/analysis_server/test/lsp/completion_dart_test.dart
+++ b/pkg/analysis_server/test/lsp/completion_dart_test.dart
@@ -354,7 +354,7 @@
     // Ensure the snippet comes through in the expected format with the expected
     // placeholders.
     expect(item.insertTextFormat, equals(InsertTextFormat.Snippet));
-    expect(item.insertText, equals('setState(() {\n      \${0:}\n    \\});'));
+    expect(item.insertText, equals('setState(() {\n      \$0\n    });'));
     final textEdit = toTextEdit(item.textEdit!);
     expect(textEdit.newText, equals(item.insertText));
     expect(textEdit.range, equals(rangeFromMarkers(content)));
@@ -396,9 +396,9 @@
     await openFile(mainFileUri, withoutMarkers(content));
     final res = await getCompletion(mainFileUri, positionFromMarker(content));
     final item = res.singleWhere((c) => c.label == 'myFunction(…)');
-    // With no required params, there should still be parens and a tabstop inside.
+    // With no required params, there should still be parens/tab stop inside.
     expect(item.insertTextFormat, equals(InsertTextFormat.Snippet));
-    expect(item.insertText, equals(r'myFunction(${0:})'));
+    expect(item.insertText, equals(r'myFunction($0)'));
     final textEdit = toTextEdit(item.textEdit!);
     expect(textEdit.newText, equals(item.insertText));
     expect(textEdit.range, equals(rangeFromMarkers(content)));
@@ -1143,9 +1143,9 @@
     // Ensure the snippet comes through in the expected format with the expected
     // placeholder.
     expect(item.insertTextFormat, equals(InsertTextFormat.Snippet));
-    expect(item.insertText, equals(r'one: ${0:},'));
+    expect(item.insertText, equals(r'one: $0,'));
     final textEdit = toTextEdit(item.textEdit!);
-    expect(textEdit.newText, equals(r'one: ${0:},'));
+    expect(textEdit.newText, equals(r'one: $0,'));
     expect(
       textEdit.range,
       equals(Range(
diff --git a/pkg/analysis_server/test/lsp/mapping_test.dart b/pkg/analysis_server/test/lsp/mapping_test.dart
index b55938b..196b547 100644
--- a/pkg/analysis_server/test/lsp/mapping_test.dart
+++ b/pkg/analysis_server/test/lsp/mapping_test.dart
@@ -117,38 +117,6 @@
     expect(results2, equals(expectedOrder));
   }
 
-  Future<void> test_tabStopsInSnippets_contains() async {
-    var result = lsp.buildSnippetStringWithTabStops('a, b, c', [3, 1]);
-    expect(result, equals(r'a, ${0:b}, c'));
-  }
-
-  Future<void> test_tabStopsInSnippets_empty() async {
-    var result = lsp.buildSnippetStringWithTabStops('a, b', []);
-    expect(result, equals(r'a, b'));
-  }
-
-  Future<void> test_tabStopsInSnippets_endsWith() async {
-    var result = lsp.buildSnippetStringWithTabStops('a, b', [3, 1]);
-    expect(result, equals(r'a, ${0:b}'));
-  }
-
-  Future<void> test_tabStopsInSnippets_escape() async {
-    var result = lsp.buildSnippetStringWithTabStops(
-        r'te$tstri}ng, te$tstri}ng, te$tstri}ng', [13, 11]);
-    expect(result, equals(r'te\$tstri\}ng, ${0:te\$tstri\}ng}, te\$tstri\}ng'));
-  }
-
-  Future<void> test_tabStopsInSnippets_multiple() async {
-    var result =
-        lsp.buildSnippetStringWithTabStops('a, b, c', [0, 1, 3, 1, 6, 1]);
-    expect(result, equals(r'${1:a}, ${2:b}, ${3:c}'));
-  }
-
-  Future<void> test_tabStopsInSnippets_startsWith() async {
-    var result = lsp.buildSnippetStringWithTabStops('a, b', [0, 1]);
-    expect(result, equals(r'${0:a}, b'));
-  }
-
   /// Verifies that [kind] maps to [expectedKind] when the client supports
   /// [supportedKinds].
   void verifyCompletionItemKind({
diff --git a/pkg/analysis_server/test/lsp/snippets_test.dart b/pkg/analysis_server/test/lsp/snippets_test.dart
new file mode 100644
index 0000000..ba25212
--- /dev/null
+++ b/pkg/analysis_server/test/lsp/snippets_test.dart
@@ -0,0 +1,168 @@
+// Copyright (c) 2022, 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/lsp/snippets.dart' as lsp;
+import 'package:analysis_server/src/lsp/snippets.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+void main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(SnippetsTest);
+    defineReflectiveTests(SnippetBuilderTest);
+  });
+}
+
+@reflectiveTest
+class SnippetBuilderTest {
+  Future<void> test_appendChoice() async {
+    final builder = SnippetBuilder()
+      ..appendChoice({r'a'})
+      ..appendChoice([r'a', r'b', r'a'])
+      ..appendChoice([], placeholderNumber: 6)
+      ..appendChoice([r'aaa', r'bbb'], placeholderNumber: 12)
+      ..appendChoice([r'aaa', r'bbb $ bbb | bbb } bbb']);
+
+    expect(
+      builder.value,
+      r'${1:a}'
+      r'${2|a,b|}'
+      r'$6'
+      r'${12|aaa,bbb|}'
+      r'${13|aaa,bbb \$ bbb \| bbb \} bbb|}',
+    );
+  }
+
+  Future<void> test_appendPlaceholder() async {
+    final builder = SnippetBuilder()
+      ..appendPlaceholder(r'placeholder $ 1')
+      ..appendPlaceholder(r'')
+      ..appendPlaceholder(r'placeholder } 3', placeholderNumber: 6);
+
+    expect(
+      builder.value,
+      r'${1:placeholder \$ 1}'
+      r'$2'
+      r'${6:placeholder \} 3}',
+    );
+  }
+
+  Future<void> test_appendTabStop() async {
+    final builder = SnippetBuilder()
+      ..appendTabStop()
+      ..appendTabStop(placeholderNumber: 10)
+      ..appendTabStop();
+
+    expect(
+      builder.value,
+      r'$1'
+      r'$10'
+      r'$11',
+    );
+  }
+
+  Future<void> test_appendText() async {
+    final builder = SnippetBuilder()
+      ..appendText(r'text 1')
+      ..appendText(r'text ${that needs} escaping $0')
+      ..appendText(r'text 2');
+
+    expect(
+      builder.value,
+      r'text 1'
+      r'text \${that needs} escaping \$0'
+      r'text 2',
+    );
+  }
+
+  Future<void> test_extension_appendPlaceholders() async {
+    final code = r'''
+012345678
+012345678
+012345678
+012345678
+012345678
+''';
+
+    final placeholders = [
+      lsp.SnippetPlaceholder(2, 2),
+      lsp.SnippetPlaceholder(12, 2, isFinal: true),
+      lsp.SnippetPlaceholder(22, 2, suggestions: ['aaa', 'bbb']),
+      lsp.SnippetPlaceholder(32, 2, linkedGroupId: 123),
+      lsp.SnippetPlaceholder(42, 2, linkedGroupId: 123),
+    ];
+
+    final builder = SnippetBuilder()..appendPlaceholders(code, placeholders);
+
+    expect(builder.value, r'''
+01${1:23}45678
+01${0:23}45678
+01${2|23,aaa,bbb|}45678
+01${3:23}45678
+01${3:23}45678
+''');
+  }
+
+  Future<void> test_mixed() async {
+    final builder = SnippetBuilder()
+      ..appendText('text1')
+      ..appendPlaceholder('placeholder')
+      ..appendText('text2')
+      ..appendChoice(['aaa', 'bbb'])
+      ..appendText('text3')
+      ..appendTabStop()
+      ..appendText('text4');
+
+    expect(
+      builder.value,
+      r'text1'
+      r'${1:placeholder}'
+      r'text2'
+      r'${2|aaa,bbb|}'
+      r'text3'
+      r'$3'
+      r'text4',
+    );
+  }
+}
+
+@reflectiveTest
+class SnippetsTest {
+  Future<void> test_tabStops_contains() async {
+    var result = lsp.buildSnippetStringWithTabStops('a, b, c', [3, 1]);
+    expect(result, equals(r'a, ${0:b}, c'));
+  }
+
+  Future<void> test_tabStops_empty() async {
+    var result = lsp.buildSnippetStringWithTabStops('a, b', []);
+    expect(result, equals(r'a, b'));
+  }
+
+  Future<void> test_tabStops_endsWith() async {
+    var result = lsp.buildSnippetStringWithTabStops('a, b', [3, 1]);
+    expect(result, equals(r'a, ${0:b}'));
+  }
+
+  Future<void> test_tabStops_escape() async {
+    var result = lsp.buildSnippetStringWithTabStops(
+        r'te$tstri}ng, te$tstri}ng, te$tstri}ng', [13, 11]);
+    expect(result, equals(r'te\$tstri}ng, ${0:te\$tstri\}ng}, te\$tstri}ng'));
+  }
+
+  Future<void> test_tabStops_multiple() async {
+    var result =
+        lsp.buildSnippetStringWithTabStops('a, b, c', [0, 1, 3, 1, 6, 1]);
+    expect(result, equals(r'${1:a}, ${2:b}, ${3:c}'));
+  }
+
+  Future<void> test_tabStops_null() async {
+    var result = lsp.buildSnippetStringWithTabStops('a, b', null);
+    expect(result, equals(r'a, b'));
+  }
+
+  Future<void> test_tabStops_startsWith() async {
+    var result = lsp.buildSnippetStringWithTabStops('a, b', [0, 1]);
+    expect(result, equals(r'${0:a}, b'));
+  }
+}
diff --git a/pkg/analysis_server/test/lsp/test_all.dart b/pkg/analysis_server/test/lsp/test_all.dart
index bd22081..5e5c5f5 100644
--- a/pkg/analysis_server/test/lsp/test_all.dart
+++ b/pkg/analysis_server/test/lsp/test_all.dart
@@ -41,6 +41,7 @@
 import 'semantic_tokens_test.dart' as semantic_tokens;
 import 'server_test.dart' as server;
 import 'signature_help_test.dart' as signature_help;
+import 'snippets_test.dart' as snippets;
 import 'super_test.dart' as get_super;
 import 'will_rename_files_test.dart' as will_rename_files;
 import 'workspace_symbols_test.dart' as workspace_symbols;
@@ -85,6 +86,7 @@
     semantic_tokens.main();
     server.main();
     signature_help.main();
+    snippets.main();
     will_rename_files.main();
     workspace_symbols.main();
   }, name: 'lsp');
diff --git a/pkg/analysis_server/tool/spec/codegen_protocol_constants.dart b/pkg/analysis_server/tool/spec/codegen_protocol_constants.dart
index f6cf4f0..3c16118 100644
--- a/pkg/analysis_server/tool/spec/codegen_protocol_constants.dart
+++ b/pkg/analysis_server/tool/spec/codegen_protocol_constants.dart
@@ -71,7 +71,7 @@
     writeln(';');
   }
 
-  /// Generate all of the constants associates with the [api].
+  /// Generate all of the constants associated with the [api].
   void generateConstants() {
     writeln("const String PROTOCOL_VERSION = '${api.version}';");
     writeln();
diff --git a/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_core.dart b/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_core.dart
index ebb810a..a1bd021 100644
--- a/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_core.dart
+++ b/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_core.dart
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:collection';
-import 'dart:math' as math;
 
 import 'package:analyzer/dart/analysis/results.dart';
 import 'package:analyzer/dart/analysis/session.dart';
@@ -541,17 +540,16 @@
     _addEdit(edit);
   }
 
-  /// Add the edit from the given [edit] to the edits associates with the
+  /// Add the edit from the given [edit] to the edits associated with the
   /// current file.
   void _addEdit(SourceEdit edit) {
     fileEdit.add(edit);
     var delta = _editDelta(edit);
-    changeBuilder._updatePositions(
-        edit.offset + math.max<int>(0, delta), delta);
+    changeBuilder._updatePositions(edit.offset, delta);
     changeBuilder._lockedPositions.clear();
   }
 
-  /// Add the edit from the given [builder] to the edits associates with the
+  /// Add the edit from the given [builder] to the edits associated with the
   /// current file.
   void _addEditBuilder(EditBuilderImpl builder) {
     var edit = builder.sourceEdit;
diff --git a/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_core_test.dart b/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_core_test.dart
index a571f04..39598e0 100644
--- a/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_core_test.dart
+++ b/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_core_test.dart
@@ -384,6 +384,25 @@
     });
   }
 
+  Future<void> test_addInsertion_updatesLinkedPositions() async {
+    var groupName = 'a';
+    var range = SourceRange(3, 6);
+    await builder.addGenericFileEdit(path, (builder) {
+      builder.addLinkedPosition(range, groupName);
+      // Insert 50 characters before the linked position.
+      builder.addInsertion(0, (builder) => builder.write('// ${'a' * 46}\n'));
+    });
+
+    var group = builder.getLinkedEditGroup(groupName);
+    var positions = group.positions;
+    expect(positions, hasLength(1));
+    var position = positions[0];
+    expect(position.file, path);
+    // Expect the linked position was moved along by the edit.
+    expect(position.offset, range.offset + 50);
+    expect(group.length, range.length);
+  }
+
   Future<void> test_addLinkedPosition() async {
     var groupName = 'a';
     await builder.addGenericFileEdit(path, (builder) {
diff --git a/pkg/analyzer_plugin/tool/spec/codegen_protocol_constants.dart b/pkg/analyzer_plugin/tool/spec/codegen_protocol_constants.dart
index 648859a..611e583 100644
--- a/pkg/analyzer_plugin/tool/spec/codegen_protocol_constants.dart
+++ b/pkg/analyzer_plugin/tool/spec/codegen_protocol_constants.dart
@@ -25,7 +25,7 @@
     codeGeneratorSettings.languageName = 'dart';
   }
 
-  /// Generate all of the constants associates with the [api].
+  /// Generate all of the constants associated with the [api].
   void generateConstants() {
     var visitor = _ConstantVisitor(api);
     visitor.visitApi();
diff --git a/pkg/vm/testcases/transformations/type_flow/transformer/no_such_method.dart.expect b/pkg/vm/testcases/transformations/type_flow/transformer/no_such_method.dart.expect
index f5fb218..1531bb7 100644
--- a/pkg/vm/testcases/transformations/type_flow/transformer/no_such_method.dart.expect
+++ b/pkg/vm/testcases/transformations/type_flow/transformer/no_such_method.dart.expect
@@ -55,7 +55,7 @@
 [@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:1,getterSelectorId:2]  no-such-method-forwarder method foo() → dynamic
     return _in::unsafeCast<dynamic>([@vm.direct-call.metadata=#lib::B.noSuchMethod] [@vm.inferred-type.metadata=#lib::T1 (skip check)] this.{self::B::noSuchMethod}(new core::_InvocationMirror::_withType(#C5, 0, #C2, #C3, [@vm.inferred-type.metadata=dart.collection::UnmodifiableMapView<dart.core::Symbol*, dynamic>] core::Map::unmodifiable<core::Symbol*, dynamic>(#C4))){(core::Invocation) → dynamic});
 [@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:4,getterSelectorId:5] [@vm.unboxing-info.metadata=(i,i,i,i)->b]  no-such-method-forwarder method bazz([@vm.inferred-type.metadata=dart.core::_Smi (value: 1)] dynamic a1, [@vm.inferred-type.metadata=dart.core::_Smi (value: 2)] dynamic a2, [@vm.inferred-type.metadata=dart.core::_Smi (value: 3)] dynamic a3, [@vm.inferred-type.metadata=dart.core::_Smi (value: 4)] dynamic a4) → dynamic
-    return _in::unsafeCast<dynamic>([@vm.direct-call.metadata=#lib::B.noSuchMethod] [@vm.inferred-type.metadata=#lib::T1 (skip check)] this.{self::B::noSuchMethod}(new core::_InvocationMirror::_withType(#C6, 0, #C2, core::List::unmodifiable<dynamic>([@vm.inferred-type.metadata=dart.core::_GrowableList<dynamic>] core::_GrowableList::_literal5<dynamic>()), [@vm.inferred-type.metadata=dart.collection::UnmodifiableMapView<dart.core::Symbol*, dynamic>] core::Map::unmodifiable<core::Symbol*, dynamic>(#C4))){(core::Invocation) → dynamic});
+    return _in::unsafeCast<dynamic>([@vm.direct-call.metadata=#lib::B.noSuchMethod] [@vm.inferred-type.metadata=#lib::T1 (skip check)] this.{self::B::noSuchMethod}(new core::_InvocationMirror::_withType(#C6, 0, #C2, [@vm.inferred-type.metadata=dart.core::_ImmutableList] core::List::unmodifiable<dynamic>([@vm.inferred-type.metadata=dart.core::_GrowableList<dynamic>] core::_GrowableList::_literal5<dynamic>()), [@vm.inferred-type.metadata=dart.collection::UnmodifiableMapView<dart.core::Symbol*, dynamic>] core::Map::unmodifiable<core::Symbol*, dynamic>(#C4))){(core::Invocation) → dynamic});
 }
 abstract class C extends core::Object {
   synthetic constructor •() → self::C
@@ -74,7 +74,7 @@
 [@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:1,getterSelectorId:2]  no-such-method-forwarder method foo() → dynamic
     return _in::unsafeCast<dynamic>([@vm.direct-call.metadata=#lib::C.noSuchMethod] [@vm.inferred-type.metadata=#lib::T2 (skip check)] this.{self::C::noSuchMethod}(new core::_InvocationMirror::_withType(#C5, 0, #C2, #C3, [@vm.inferred-type.metadata=dart.collection::UnmodifiableMapView<dart.core::Symbol*, dynamic>] core::Map::unmodifiable<core::Symbol*, dynamic>(#C4))){(core::Invocation) → dynamic});
 [@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:4,getterSelectorId:5] [@vm.unboxing-info.metadata=(i,i,i,i)->b]  no-such-method-forwarder method bazz([@vm.inferred-type.metadata=dart.core::_Smi (value: 1)] dynamic a1, [@vm.inferred-type.metadata=dart.core::_Smi (value: 2)] dynamic a2, [@vm.inferred-type.metadata=dart.core::_Smi (value: 3)] dynamic a3, [@vm.inferred-type.metadata=dart.core::_Smi (value: 4)] dynamic a4) → dynamic
-    return _in::unsafeCast<dynamic>([@vm.direct-call.metadata=#lib::C.noSuchMethod] [@vm.inferred-type.metadata=#lib::T2 (skip check)] this.{self::C::noSuchMethod}(new core::_InvocationMirror::_withType(#C6, 0, #C2, core::List::unmodifiable<dynamic>([@vm.inferred-type.metadata=dart.core::_GrowableList<dynamic>] core::_GrowableList::_literal5<dynamic>()), [@vm.inferred-type.metadata=dart.collection::UnmodifiableMapView<dart.core::Symbol*, dynamic>] core::Map::unmodifiable<core::Symbol*, dynamic>(#C4))){(core::Invocation) → dynamic});
+    return _in::unsafeCast<dynamic>([@vm.direct-call.metadata=#lib::C.noSuchMethod] [@vm.inferred-type.metadata=#lib::T2 (skip check)] this.{self::C::noSuchMethod}(new core::_InvocationMirror::_withType(#C6, 0, #C2, [@vm.inferred-type.metadata=dart.core::_ImmutableList] core::List::unmodifiable<dynamic>([@vm.inferred-type.metadata=dart.core::_GrowableList<dynamic>] core::_GrowableList::_literal5<dynamic>()), [@vm.inferred-type.metadata=dart.collection::UnmodifiableMapView<dart.core::Symbol*, dynamic>] core::Map::unmodifiable<core::Symbol*, dynamic>(#C4))){(core::Invocation) → dynamic});
 }
 class E extends core::Object implements self::A {
   synthetic constructor •() → self::E
diff --git a/pkg/vm/testcases/transformations/type_flow/transformer/regress_flutter81068.dart.expect b/pkg/vm/testcases/transformations/type_flow/transformer/regress_flutter81068.dart.expect
index 4c3106f..963f8c2 100644
--- a/pkg/vm/testcases/transformations/type_flow/transformer/regress_flutter81068.dart.expect
+++ b/pkg/vm/testcases/transformations/type_flow/transformer/regress_flutter81068.dart.expect
@@ -17,11 +17,11 @@
 [@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasTearOffUses:false,methodOrSetterSelectorId:1,getterSelectorId:2]  method noSuchMethod(core::Invocation i) → dynamic
     return throw "Not implemented";
 [@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:3,getterSelectorId:4]  no-such-method-forwarder method /* from org-dartlang-sdk:///sdk/lib/async/future.dart */ catchError(core::Function onError) → asy::Future<self::B::T%>
-    return [@vm.direct-call.metadata=#lib::B.noSuchMethod] [@vm.inferred-type.metadata=! (skip check)] this.{self::B::noSuchMethod}(new core::_InvocationMirror::_withType(#C1, 0, #C2, core::List::unmodifiable<dynamic>([@vm.inferred-type.metadata=dart.core::_GrowableList<dynamic>] core::_GrowableList::_literal1<dynamic>(onError)), [@vm.inferred-type.metadata=dart.collection::UnmodifiableMapView<dart.core::Symbol*, dynamic>] core::Map::unmodifiable<core::Symbol*, dynamic>(<core::Symbol*, dynamic>{#C3: #C4}))){(core::Invocation) → dynamic} as{TypeError,ForDynamic,ForNonNullableByDefault} asy::Future<self::B::T%>;
+    return [@vm.direct-call.metadata=#lib::B.noSuchMethod] [@vm.inferred-type.metadata=! (skip check)] this.{self::B::noSuchMethod}(new core::_InvocationMirror::_withType(#C1, 0, #C2, [@vm.inferred-type.metadata=dart.core::_ImmutableList] core::List::unmodifiable<dynamic>([@vm.inferred-type.metadata=dart.core::_GrowableList<dynamic>] core::_GrowableList::_literal1<dynamic>(onError)), [@vm.inferred-type.metadata=dart.collection::UnmodifiableMapView<dart.core::Symbol*, dynamic>] core::Map::unmodifiable<core::Symbol*, dynamic>(<core::Symbol*, dynamic>{#C3: #C4}))){(core::Invocation) → dynamic} as{TypeError,ForDynamic,ForNonNullableByDefault} asy::Future<self::B::T%>;
 [@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:5,getterSelectorId:6]  no-such-method-forwarder method /* from org-dartlang-sdk:///sdk/lib/async/future.dart */ whenComplete(() → FutureOr<void>action) → asy::Future<self::B::T%>
-    return [@vm.direct-call.metadata=#lib::B.noSuchMethod] [@vm.inferred-type.metadata=! (skip check)] this.{self::B::noSuchMethod}(new core::_InvocationMirror::_withType(#C5, 0, #C2, core::List::unmodifiable<dynamic>([@vm.inferred-type.metadata=dart.core::_GrowableList<dynamic>] core::_GrowableList::_literal1<dynamic>(action)), [@vm.inferred-type.metadata=dart.collection::UnmodifiableMapView<dart.core::Symbol*, dynamic>] core::Map::unmodifiable<core::Symbol*, dynamic>(#C6))){(core::Invocation) → dynamic} as{TypeError,ForDynamic,ForNonNullableByDefault} asy::Future<self::B::T%>;
+    return [@vm.direct-call.metadata=#lib::B.noSuchMethod] [@vm.inferred-type.metadata=! (skip check)] this.{self::B::noSuchMethod}(new core::_InvocationMirror::_withType(#C5, 0, #C2, [@vm.inferred-type.metadata=dart.core::_ImmutableList] core::List::unmodifiable<dynamic>([@vm.inferred-type.metadata=dart.core::_GrowableList<dynamic>] core::_GrowableList::_literal1<dynamic>(action)), [@vm.inferred-type.metadata=dart.collection::UnmodifiableMapView<dart.core::Symbol*, dynamic>] core::Map::unmodifiable<core::Symbol*, dynamic>(#C6))){(core::Invocation) → dynamic} as{TypeError,ForDynamic,ForNonNullableByDefault} asy::Future<self::B::T%>;
 [@vm.procedure-attributes.metadata=methodOrSetterCalledDynamically:false,getterCalledDynamically:false,hasThisUses:false,hasTearOffUses:false,methodOrSetterSelectorId:7,getterSelectorId:8]  no-such-method-forwarder method /* from org-dartlang-sdk:///sdk/lib/async/future.dart */ then<R extends core::Object? = dynamic>((self::B::T%) → FutureOr<self::B::then::R%>onValue, {core::Function? onError = #C4}) → asy::Future<self::B::then::R%>
-    return [@vm.direct-call.metadata=#lib::B.noSuchMethod] [@vm.inferred-type.metadata=! (skip check)] this.{self::B::noSuchMethod}(new core::_InvocationMirror::_withType(#C7, 0, core::List::unmodifiable<core::Type*>([@vm.inferred-type.metadata=dart.core::_GrowableList<dart.core::Type*>] core::_GrowableList::_literal1<core::Type*>(self::B::then::R%)), core::List::unmodifiable<dynamic>([@vm.inferred-type.metadata=dart.core::_GrowableList<dynamic>] core::_GrowableList::_literal1<dynamic>(onValue)), [@vm.inferred-type.metadata=dart.collection::UnmodifiableMapView<dart.core::Symbol*, dynamic>] core::Map::unmodifiable<core::Symbol*, dynamic>(<core::Symbol*, dynamic>{#C8: onError}))){(core::Invocation) → dynamic} as{TypeError,ForDynamic,ForNonNullableByDefault} asy::Future<self::B::then::R%>;
+    return [@vm.direct-call.metadata=#lib::B.noSuchMethod] [@vm.inferred-type.metadata=! (skip check)] this.{self::B::noSuchMethod}(new core::_InvocationMirror::_withType(#C7, 0, [@vm.inferred-type.metadata=dart.core::_ImmutableList] core::List::unmodifiable<core::Type*>([@vm.inferred-type.metadata=dart.core::_GrowableList<dart.core::Type*>] core::_GrowableList::_literal1<core::Type*>(self::B::then::R%)), [@vm.inferred-type.metadata=dart.core::_ImmutableList] core::List::unmodifiable<dynamic>([@vm.inferred-type.metadata=dart.core::_GrowableList<dynamic>] core::_GrowableList::_literal1<dynamic>(onValue)), [@vm.inferred-type.metadata=dart.collection::UnmodifiableMapView<dart.core::Symbol*, dynamic>] core::Map::unmodifiable<core::Symbol*, dynamic>(<core::Symbol*, dynamic>{#C8: onError}))){(core::Invocation) → dynamic} as{TypeError,ForDynamic,ForNonNullableByDefault} asy::Future<self::B::then::R%>;
 }
 static method createB<T extends core::Object? = dynamic>() → self::B<dynamic>
   return new self::B::•<self::createB::T%>();
diff --git a/pkg/vm_service/test/super_constructor_invocation_test.dart b/pkg/vm_service/test/super_constructor_invocation_test.dart
new file mode 100644
index 0000000..7a0d300
--- /dev/null
+++ b/pkg/vm_service/test/super_constructor_invocation_test.dart
@@ -0,0 +1,183 @@
+// Copyright (c) 2022, 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.
+//
+// SharedOptions=--enable-experiment=super-parameters
+
+// ignore_for_file: experiment_not_enabled
+// @dart=2.17
+
+import 'dart:developer';
+
+import 'package:test/test.dart';
+import 'package:vm_service/vm_service.dart';
+
+import 'common/service_test_common.dart';
+import 'common/test_helper.dart';
+
+class S<T> {
+  num? n;
+  T? t;
+  String constrName;
+  S({this.n, this.t}) : constrName = "S";
+  S.named({this.t, this.n}) : constrName = "S.named";
+}
+
+class C<T> extends S<T> {
+  C.constr1(String s, {super.t});
+  C.constr2(int i, String s, {super.n}) : super();
+  C.constr3(int i, String s, {super.n, super.t}) : super.named() {
+    debugger();
+  }
+}
+
+class R<T> {
+  final f1;
+  var v1;
+  num i1;
+  T t1;
+  R(this.f1, this.v1, this.i1, this.t1);
+}
+
+class B<T> extends R<T> {
+  // ignore: no_default_super_constructor
+  B(super.f1, super.v1, super.i1, super.t1) {
+    debugger();
+  }
+}
+
+void testMain() {
+  debugger();
+  C.constr3(1, 'abc', n: 3.14, t: 42);
+  B('a', 3.14, 2.718, 42);
+}
+
+late final String isolateId;
+late final String rootLibId;
+
+createInstance(VmService service, String expr) async {
+  return await service.evaluate(
+      isolateId,
+      rootLibId,
+      expr,
+      disableBreakpoints: true,
+    );
+}
+
+evaluateGetter(VmService service, String instanceId, String getter) async {
+  dynamic result = await service.evaluate(isolateId, instanceId, getter);
+  return await service.getObject(isolateId, result.id);
+}
+
+final tests = <IsolateTest>[
+  (VmService service, IsolateRef isolateRef) async {
+    // Initialization
+    isolateId = isolateRef.id!;
+    final isolate = await service.getIsolate(isolateId);
+    rootLibId = isolate.rootLib!.id!;
+  },
+  (VmService service, _) async {
+    dynamic instance = await createInstance(service, 'C.constr1("abc", t: 42)');
+    dynamic result = await evaluateGetter(service, instance.id, 'n');
+    expect(result.valueAsString, 'null');
+    result = await evaluateGetter(service, instance.id, 't');
+    expect(result.valueAsString, '42');
+    result = await evaluateGetter(service, instance.id, 'constrName');
+    expect(result.valueAsString, 'S');
+    result = await service.evaluate(isolateId, instance.id, 'T');
+    expect(result.json['name'], 'int');
+
+    instance = await createInstance(service, 'C.constr1("abc", t: "42")');
+    result = await evaluateGetter(service, instance.id, 'n');
+    expect(result.valueAsString, 'null');
+    result = await evaluateGetter(service, instance.id, 't');
+    expect(result.valueAsString, '42');
+    result = await evaluateGetter(service, instance.id, 'constrName');
+    expect(result.valueAsString, 'S');
+    result = await service.evaluate(isolateId, instance.id, 'T');
+    expect(result.json['name'], 'String');
+  },
+  (VmService service, _) async {
+    dynamic instance = await createInstance(service, 'C.constr2(1, "abc", n: 3.14)');
+    dynamic result = await evaluateGetter(service, instance.id, 'n');
+    expect(result.valueAsString, '3.14');
+    result = await evaluateGetter(service, instance.id, 't');
+    expect(result.valueAsString, 'null');
+    result = await evaluateGetter(service, instance.id, 'constrName');
+    expect(result.valueAsString, 'S');
+    result = await service.evaluate(isolateId, instance.id, 'T');
+    expect(result.json['name'], 'dynamic');
+
+    instance = await createInstance(service, 'C.constr2(1, "abc", n: 2)');
+    result = await evaluateGetter(service, instance.id, 'n');
+    expect(result.valueAsString, '2');
+    result = await evaluateGetter(service, instance.id, 't');
+    expect(result.valueAsString, 'null');
+    result = await evaluateGetter(service, instance.id, 'constrName');
+    expect(result.valueAsString, 'S');
+    result = await service.evaluate(isolateId, instance.id, 'T');
+    expect(result.json['name'], 'dynamic');
+  },
+  (VmService service, _) async {
+    dynamic instance = await createInstance(service, 'C.constr3(1, "abc", n: 42, t: 3.14)');
+    dynamic result = await evaluateGetter(service, instance.id, 'n');
+    expect(result.valueAsString, '42');
+    result = await evaluateGetter(service, instance.id, 't');
+    expect(result.valueAsString, '3.14');
+    result = await evaluateGetter(service, instance.id, 'constrName');
+    expect(result.valueAsString, 'S.named');
+    result = await service.evaluate(isolateId, instance.id, 'T');
+    expect(result.json['name'], 'double');
+
+    instance = await createInstance(service, 'C.constr3(1, "abc", n: 3.14, t: 42)');
+    result = await evaluateGetter(service, instance.id, 'n');
+    expect(result.valueAsString, '3.14');
+    result = await evaluateGetter(service, instance.id, 't');
+    expect(result.valueAsString, '42');
+    result = await evaluateGetter(service, instance.id, 'constrName');
+    expect(result.valueAsString, 'S.named');
+    result = await service.evaluate(isolateId, instance.id, 'T');
+    expect(result.json['name'], 'int');
+  },
+  (VmService service, _) async {
+    dynamic instance = await createInstance(service, 'B(1, 2, 3, 4)');
+    dynamic result = await evaluateGetter(service, instance.id, 'f1');
+    expect(result.valueAsString, '1');
+    result = await evaluateGetter(service, instance.id, 'v1');
+    expect(result.valueAsString, '2');
+    result = await evaluateGetter(service, instance.id, 'i1');
+    expect(result.valueAsString, '3');
+    result = await evaluateGetter(service, instance.id, 't1');
+    expect(result.valueAsString, '4');
+  },
+  resumeIsolate,
+  hasStoppedAtBreakpoint,
+  (VmService service, _) async {
+    dynamic result = await service.evaluateInFrame(isolateId, 0, 'n');
+    expect(result.valueAsString, '3.14');
+    result = await service.evaluateInFrame(isolateId, 0, 't');
+    expect(result.valueAsString, '42');
+    result = await service.evaluateInFrame(isolateId, 0, 'constrName');
+    expect(result.valueAsString, 'S.named');
+  },
+  resumeIsolate,
+  hasStoppedAtBreakpoint,
+  (VmService service, _) async {
+    dynamic result = await service.evaluateInFrame(isolateId, 0, 'f1');
+    expect(result.valueAsString, 'a');
+    result = await service.evaluateInFrame(isolateId, 0, 'v1');
+    expect(result.valueAsString, '3.14');
+    result = await service.evaluateInFrame(isolateId, 0, 'i1');
+    expect(result.valueAsString, '2.718');
+    result = await service.evaluateInFrame(isolateId, 0, 't1');
+    expect(result.valueAsString, '42');
+  }
+];
+
+main([args = const <String>[]]) => runIsolateTests(
+      args,
+      tests,
+      'super_constructor_invocation_test.dart',
+      testeeConcurrent: testMain,
+      experiments: ['super-parameters'],
+    );
diff --git a/runtime/vm/compiler/backend/il_riscv.cc b/runtime/vm/compiler/backend/il_riscv.cc
index c9067af..51aa2b4 100644
--- a/runtime/vm/compiler/backend/il_riscv.cc
+++ b/runtime/vm/compiler/backend/il_riscv.cc
@@ -132,8 +132,7 @@
     }
     case kUnboxedDouble: {
       const auto out = locs()->out(0).fpu_reg();
-      const intptr_t kDoubleSizeLog2 = 3;
-      __ slli(TMP, index, kDoubleSizeLog2 - kSmiTagSize);
+      __ slli(TMP, index, kWordSizeLog2 - kSmiTagSize);
       __ add(TMP, TMP, base_reg());
       __ LoadDFromOffset(out, TMP, offset());
       break;
diff --git a/runtime/vm/compiler/recognized_methods_list.h b/runtime/vm/compiler/recognized_methods_list.h
index aa35336..c928e03 100644
--- a/runtime/vm/compiler/recognized_methods_list.h
+++ b/runtime/vm/compiler/recognized_methods_list.h
@@ -398,8 +398,8 @@
   V(::, _isDartStreamEnabled, Timeline_isDartStreamEnabled, 0xc97aafb3)        \
 
 #define INTERNAL_LIB_INTRINSIC_LIST(V)                                         \
-  V(::, allocateOneByteString, AllocateOneByteString, 0x9e774214)              \
-  V(::, allocateTwoByteString, AllocateTwoByteString, 0xa63c7db1)              \
+  V(::, allocateOneByteString, AllocateOneByteString, 0x9e7745d5)              \
+  V(::, allocateTwoByteString, AllocateTwoByteString, 0xa63c8172)              \
   V(::, writeIntoOneByteString, WriteIntoOneByteString, 0xd8729161)            \
   V(::, writeIntoTwoByteString, WriteIntoTwoByteString, 0xcfc7982a)            \
 
diff --git a/sdk/lib/_http/http.dart b/sdk/lib/_http/http.dart
index 06d63d9..5503db6 100644
--- a/sdk/lib/_http/http.dart
+++ b/sdk/lib/_http/http.dart
@@ -1516,6 +1516,46 @@
   /// Add credentials to be used for authorizing HTTP requests.
   void addCredentials(Uri url, String realm, HttpClientCredentials credentials);
 
+  /// Sets the function used to create socket connections.
+  ///
+  /// The URL requested (e.g. through [getUrl]) and proxy configuration
+  /// ([f.proxyHost] and [f.proxyPort]) are passed as arguments. [f.proxyHost]
+  /// and [f.proxyPort] will be `null` if the connection is not made through
+  /// a proxy.
+  ///
+  /// Since connections may be reused based on host and port, it is important
+  /// that the function not ignore [f.proxyHost] and [f.proxyPort] if they are
+  /// not `null`. If proxies are not meaningful for the returned [Socket], you
+  /// can set [findProxy] to use a direct connection.
+  ///
+  /// For example:
+  ///
+  /// ```dart
+  /// import "dart:io";
+  ///
+  /// void main() async {
+  ///   HttpClient client = HttpClient()
+  ///     ..connectionFactory = (Uri uri, String? proxyHost, int? proxyPort) {
+  ///         assert(proxyHost == null);
+  ///         assert(proxyPort == null);
+  ///         var address = InternetAddress("/var/run/docker.sock",
+  ///             type: InternetAddressType.unix);
+  ///         return Socket.startConnect(address, 0);
+  ///     }
+  ///     ..findProxy = (Uri uri) => 'DIRECT';
+  ///
+  ///   final request = await client.getUrl(Uri.parse("http://ignored/v1.41/info"));
+  ///   final response = await request.close();
+  ///   print(response.statusCode);
+  ///   await response.drain();
+  ///   client.close();
+  /// }
+  /// ```
+  void set connectionFactory(
+      Future<ConnectionTask<Socket>> Function(
+              Uri url, String? proxyHost, int? proxyPort)?
+          f);
+
   /// Sets the function used to resolve the proxy server to be used for
   /// opening a HTTP connection to the specified [url]. If this
   /// function is not set, direct connections will always be used.
diff --git a/sdk/lib/_http/http_impl.dart b/sdk/lib/_http/http_impl.dart
index b97025a..bb38ebe 100644
--- a/sdk/lib/_http/http_impl.dart
+++ b/sdk/lib/_http/http_impl.dart
@@ -2338,14 +2338,16 @@
   final int port;
   final bool isSecure;
   final SecurityContext? context;
+  final Future<ConnectionTask<Socket>> Function(Uri, String?, int?)?
+      connectionFactory;
   final Set<_HttpClientConnection> _idle = HashSet();
   final Set<_HttpClientConnection> _active = HashSet();
   final Set<ConnectionTask<Socket>> _socketTasks = HashSet();
   final _pending = ListQueue<void Function()>();
   int _connecting = 0;
 
-  _ConnectionTarget(
-      this.key, this.host, this.port, this.isSecure, this.context);
+  _ConnectionTarget(this.key, this.host, this.port, this.isSecure, this.context,
+      this.connectionFactory);
 
   bool get isEmpty => _idle.isEmpty && _active.isEmpty && _connecting == 0;
 
@@ -2410,8 +2412,8 @@
     }
   }
 
-  Future<_ConnectionInfo> connect(String uriHost, int uriPort, _Proxy proxy,
-      _HttpClient client, _HttpProfileData? profileData) {
+  Future<_ConnectionInfo> connect(Uri uri, String uriHost, int uriPort,
+      _Proxy proxy, _HttpClient client, _HttpProfileData? profileData) {
     if (hasIdle) {
       var connection = takeIdle();
       client._connectionsChanged();
@@ -2422,8 +2424,8 @@
         _active.length + _connecting >= maxConnectionsPerHost) {
       var completer = Completer<_ConnectionInfo>();
       _pending.add(() {
-        completer
-            .complete(connect(uriHost, uriPort, proxy, client, profileData));
+        completer.complete(
+            connect(uri, uriHost, uriPort, proxy, client, profileData));
       });
       return completer.future;
     }
@@ -2434,10 +2436,20 @@
       return currentBadCertificateCallback(certificate, uriHost, uriPort);
     }
 
-    Future<ConnectionTask<Socket>> connectionTask = (isSecure && proxy.isDirect
-        ? SecureSocket.startConnect(host, port,
-            context: context, onBadCertificate: callback)
-        : Socket.startConnect(host, port));
+    Future<ConnectionTask<Socket>> connectionTask;
+    final cf = connectionFactory;
+    if (cf != null) {
+      if (proxy.isDirect) {
+        connectionTask = cf(uri, null, null);
+      } else {
+        connectionTask = cf(uri, host, port);
+      }
+    } else {
+      connectionTask = (isSecure && proxy.isDirect
+          ? SecureSocket.startConnect(host, port,
+              context: context, onBadCertificate: callback)
+          : Socket.startConnect(host, port));
+    }
     _connecting++;
     return connectionTask.then((ConnectionTask<Socket> task) {
       _socketTasks.add(task);
@@ -2506,6 +2518,8 @@
   final List<_Credentials> _credentials = [];
   final List<_ProxyCredentials> _proxyCredentials = [];
   final SecurityContext? _context;
+  Future<ConnectionTask<Socket>> Function(Uri, String?, int?)?
+      _connectionFactory;
   Future<bool> Function(Uri, String scheme, String? realm)? _authenticate;
   Future<bool> Function(String host, int port, String scheme, String? realm)?
       _authenticateProxy;
@@ -2631,6 +2645,12 @@
         _ProxyCredentials(host, port, realm, cr as _HttpClientCredentials));
   }
 
+  void set connectionFactory(
+          Future<ConnectionTask<Socket>> Function(
+                  Uri url, String? proxyHost, int? proxyPort)?
+              f) =>
+      _connectionFactory = f;
+
   set findProxy(String Function(Uri uri)? f) => _findProxy = f;
 
   static void _startRequestTimelineEvent(
@@ -2665,7 +2685,9 @@
     if (method != "CONNECT") {
       if (uri.host.isEmpty) {
         throw ArgumentError("No host specified in URI $uri");
-      } else if (!uri.isScheme("http") && !uri.isScheme("https")) {
+      } else if (this._connectionFactory == null &&
+          !uri.isScheme("http") &&
+          !uri.isScheme("https")) {
         throw ArgumentError("Unsupported scheme '${uri.scheme}' in URI $uri");
       }
     }
@@ -2695,7 +2717,7 @@
     if (HttpClient.enableTimelineLogging) {
       profileData = HttpProfiler.startRequest(method, uri);
     }
-    return _getConnection(uri.host, port, proxyConf, isSecure, profileData)
+    return _getConnection(uri, uri.host, port, proxyConf, isSecure, profileData)
         .then((_ConnectionInfo info) {
       _HttpClientRequest send(_ConnectionInfo info) {
         profileData?.requestEvent('Connection established');
@@ -2706,7 +2728,8 @@
       // If the connection was closed before the request was sent, create
       // and use another connection.
       if (info.connection.closed) {
-        return _getConnection(uri.host, port, proxyConf, isSecure, profileData)
+        return _getConnection(
+                uri, uri.host, port, proxyConf, isSecure, profileData)
             .then(send);
       }
       return send(info);
@@ -2813,12 +2836,14 @@
   _ConnectionTarget _getConnectionTarget(String host, int port, bool isSecure) {
     String key = _HttpClientConnection.makeKey(isSecure, host, port);
     return _connectionTargets.putIfAbsent(key, () {
-      return _ConnectionTarget(key, host, port, isSecure, _context);
+      return _ConnectionTarget(
+          key, host, port, isSecure, _context, _connectionFactory);
     });
   }
 
   // Get a new _HttpClientConnection, from the matching _ConnectionTarget.
   Future<_ConnectionInfo> _getConnection(
+      Uri uri,
       String uriHost,
       int uriPort,
       _ProxyConfiguration proxyConf,
@@ -2832,7 +2857,7 @@
       String host = proxy.isDirect ? uriHost : proxy.host!;
       int port = proxy.isDirect ? uriPort : proxy.port!;
       return _getConnectionTarget(host, port, isSecure)
-          .connect(uriHost, uriPort, proxy, this, profileData)
+          .connect(uri, uriHost, uriPort, proxy, this, profileData)
           // On error, continue with next proxy.
           .catchError(connect);
     }
diff --git a/sdk/lib/_internal/vm/lib/internal_patch.dart b/sdk/lib/_internal/vm/lib/internal_patch.dart
index e0267ab..4c0fa92 100644
--- a/sdk/lib/_internal/vm/lib/internal_patch.dart
+++ b/sdk/lib/_internal/vm/lib/internal_patch.dart
@@ -26,10 +26,12 @@
 
 @patch
 @pragma("vm:external-name", "Internal_makeListFixedLength")
+@pragma("vm:exact-result-type", "dart:core#_List")
 external List<T> makeListFixedLength<T>(List<T> growableList);
 
 @patch
 @pragma("vm:external-name", "Internal_makeFixedListUnmodifiable")
+@pragma("vm:exact-result-type", "dart:core#_ImmutableList")
 external List<T> makeFixedListUnmodifiable<T>(List<T> fixedLengthList);
 
 @patch
@@ -39,6 +41,7 @@
 /// The returned string is a [_OneByteString] with uninitialized content.
 @pragma("vm:recognized", "asm-intrinsic")
 @pragma("vm:external-name", "Internal_allocateOneByteString")
+@pragma("vm:exact-result-type", "dart:core#_OneByteString")
 external String allocateOneByteString(int length);
 
 /// The [string] must be a [_OneByteString]. The [index] must be valid.
@@ -61,6 +64,7 @@
 /// The returned string is a [_TwoByteString] with uninitialized content.
 @pragma("vm:recognized", "asm-intrinsic")
 @pragma("vm:external-name", "Internal_allocateTwoByteString")
+@pragma("vm:exact-result-type", "dart:core#_TwoByteString")
 external String allocateTwoByteString(int length);
 
 /// The [string] must be a [_TwoByteString]. The [index] must be valid.
diff --git a/sdk/lib/html/dart2js/html_dart2js.dart b/sdk/lib/html/dart2js/html_dart2js.dart
index 0c9b5d0..98fe3f6 100644
--- a/sdk/lib/html/dart2js/html_dart2js.dart
+++ b/sdk/lib/html/dart2js/html_dart2js.dart
@@ -33717,7 +33717,7 @@
    * See [EventStreamProvider] for usage information.
    */
   static const EventStreamProvider<BeforeUnloadEvent> beforeUnloadEvent =
-      const _BeforeUnloadEventStreamProvider('beforeunload');
+      const EventStreamProvider('beforeunload');
 
   /// Stream of `beforeunload` events handled by this [Window].
   Stream<Event> get onBeforeUnload => beforeUnloadEvent.forTarget(this);
@@ -33773,65 +33773,6 @@
       ? JS<num>('num', '#.scrollY', this).round()
       : document.documentElement!.scrollTop;
 }
-
-class _BeforeUnloadEvent extends _WrappedEvent implements BeforeUnloadEvent {
-  String _returnValue;
-
-  _BeforeUnloadEvent(Event base)
-      : _returnValue = '',
-        super(base);
-
-  String get returnValue => _returnValue;
-
-  set returnValue(String? value) {
-    // Typed as nullable only to be compatible with the overriden method.
-    _returnValue = value!;
-    // FF and IE use the value as the return value, Chrome will return this from
-    // the event callback function.
-    if (JS<bool>('bool', '("returnValue" in #)', wrapped)) {
-      JS('void', '#.returnValue = #', wrapped, value);
-    }
-  }
-}
-
-class _BeforeUnloadEventStreamProvider
-    implements EventStreamProvider<BeforeUnloadEvent> {
-  final String _eventType;
-
-  const _BeforeUnloadEventStreamProvider(this._eventType);
-
-  Stream<BeforeUnloadEvent> forTarget(EventTarget? e,
-      {bool useCapture: false}) {
-    // Specify the generic type for EventStream only in dart2js.
-    var stream = new _EventStream<BeforeUnloadEvent>(e, _eventType, useCapture);
-    var controller = new StreamController<BeforeUnloadEvent>(sync: true);
-
-    stream.listen((event) {
-      var wrapped = new _BeforeUnloadEvent(event);
-      controller.add(wrapped);
-    });
-
-    return controller.stream;
-  }
-
-  String getEventType(EventTarget target) {
-    return _eventType;
-  }
-
-  ElementStream<BeforeUnloadEvent> forElement(Element e,
-      {bool useCapture: false}) {
-    // Specify the generic type for _ElementEventStreamImpl only in dart2js.
-    return new _ElementEventStreamImpl<BeforeUnloadEvent>(
-        e, _eventType, useCapture);
-  }
-
-  ElementStream<BeforeUnloadEvent> _forElementList(ElementList<Element> e,
-      {bool useCapture: false}) {
-    // Specify the generic type for _ElementEventStreamImpl only in dart2js.
-    return new _ElementListEventStreamImpl<BeforeUnloadEvent>(
-        e, _eventType, useCapture);
-  }
-}
 // Copyright (c) 2012, 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.
diff --git a/tests/standalone/io/http_connection_factory_test.dart b/tests/standalone/io/http_connection_factory_test.dart
new file mode 100644
index 0000000..6cc41cd
--- /dev/null
+++ b/tests/standalone/io/http_connection_factory_test.dart
@@ -0,0 +1,146 @@
+// 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:io";
+import 'dart:convert';
+import "package:expect/expect.dart";
+import 'http_proxy_test.dart' show setupProxyServer;
+import 'test_utils.dart' show withTempDir;
+
+testDirectConnection() async {
+  var server = await HttpServer.bind(InternetAddress.anyIPv6, 0);
+  server.forEach((HttpRequest request) {
+    request.response.write('Hello, world!');
+    request.response.close();
+  });
+  final serverUri = Uri.http("127.0.0.1:${server.port}", "/");
+  var client = HttpClient()
+    ..connectionFactory = (uri, proxyHost, proxyPort) {
+      Expect.isNull(proxyHost);
+      Expect.isNull(proxyPort);
+      Expect.equals(serverUri, uri);
+      return Socket.startConnect(uri.host, uri.port);
+    }
+    ..findProxy = (uri) => 'DIRECT';
+  final response = await client.getUrl(serverUri).then((request) {
+    return request.close();
+  });
+  Expect.equals(200, response.statusCode);
+  final responseText = await response
+      .transform(utf8.decoder)
+      .fold('', (String x, String y) => x + y);
+  Expect.equals("Hello, world!", responseText);
+  client.close();
+  server.close();
+}
+
+testConnectionViaProxy() async {
+  var proxyServer = await setupProxyServer();
+  var server = await HttpServer.bind(InternetAddress.anyIPv6, 0);
+  server.forEach((HttpRequest request) {
+    request.response.write('Hello via Proxy');
+    request.response.close();
+  });
+  final serverUri = Uri.http("127.0.0.1:${server.port}", "/");
+  final client = HttpClient()
+    ..connectionFactory = (uri, proxyHost, proxyPort) {
+      Expect.equals("localhost", proxyHost);
+      Expect.equals(proxyServer.port, proxyPort);
+      Expect.equals(serverUri, uri);
+      return Socket.startConnect(proxyHost, proxyPort as int);
+    }
+    ..findProxy = (uri) => "PROXY localhost:${proxyServer.port}";
+  final response = await client.getUrl(serverUri).then((request) {
+    return request.close();
+  });
+  Expect.equals(200, response.statusCode);
+  final responseText = await response
+      .transform(utf8.decoder)
+      .fold('', (String x, String y) => x + y);
+  Expect.equals("Hello via Proxy", responseText);
+  client.close();
+  server.close();
+  proxyServer.shutdown();
+}
+
+testDifferentAddressFamiliesAndProxySettings(String dir) async {
+  // Test a custom connection factory for Unix domain sockets that also allows
+  // regular INET/INET6 access with and without a proxy.
+  var proxyServer = await setupProxyServer();
+  var inet6Server = await HttpServer.bind(InternetAddress.anyIPv6, 0);
+  inet6Server.forEach((HttpRequest request) {
+    request.response.write('Hello via Proxy');
+    request.response.close();
+  });
+  final inet6ServerUri = Uri.http("127.0.0.1:${inet6Server.port}", "/");
+  final unixPath = '$dir/sock';
+  final unixAddress = InternetAddress(unixPath, type: InternetAddressType.unix);
+  final unixServer = await HttpServer.bind(unixAddress, 0);
+  unixServer.forEach((HttpRequest request) {
+    request.response.write('Hello via Unix');
+    request.response.close();
+  });
+  final client = HttpClient()
+    ..connectionFactory = (uri, proxyHost, proxyPort) {
+      if (uri.scheme == 'unix') {
+        assert(proxyHost == null);
+        assert(proxyPort == null);
+        var address = InternetAddress(unixPath, type: InternetAddressType.unix);
+        return Socket.startConnect(address, 0);
+      } else {
+        if (proxyHost != null && proxyPort != null) {
+          return Socket.startConnect(proxyHost, proxyPort);
+        }
+        return Socket.startConnect(uri.host, uri.port);
+      }
+    }
+    ..findProxy = (uri) {
+      if (uri.scheme == 'unix') {
+        // Proxy settings are not meaningful for Unix domain sockets.
+        return 'DIRECT';
+      } else {
+        return "PROXY localhost:${proxyServer.port}";
+      }
+    };
+  // Fetch a URL from the INET6 server and verify the results.
+  final inet6Response = await client.getUrl(inet6ServerUri).then((request) {
+    return request.close();
+  });
+  Expect.equals(200, inet6Response.statusCode);
+  final inet6ResponseText = await inet6Response
+      .transform(utf8.decoder)
+      .fold('', (String x, String y) => x + y);
+  Expect.equals("Hello via Proxy", inet6ResponseText);
+  // Fetch a URL from the Unix server and verify the results.
+  final unixResponse = await client
+      .getUrl(Uri(
+          scheme: "unix",
+          // Connection pooling is based on the host/port combination
+          // so ensure that the host is unique for unique logical
+          // endpoints. Also, the `host` property is converted to
+          // lowercase so you cannot use it directly for file paths.
+          host: 'dummy',
+          path: "/"))
+      .then((request) {
+    return request.close();
+  });
+  Expect.equals(200, unixResponse.statusCode);
+  final unixResponseText = await unixResponse
+      .transform(utf8.decoder)
+      .fold('', (String x, String y) => x + y);
+  Expect.equals("Hello via Unix", unixResponseText);
+  client.close();
+  inet6Server.close();
+  unixServer.close();
+  proxyServer.shutdown();
+}
+
+main() async {
+  await testDirectConnection();
+  await testConnectionViaProxy();
+  if (Platform.isMacOS || Platform.isLinux || Platform.isAndroid) {
+    await withTempDir('unix_socket_test', (Directory dir) async {
+      await testDifferentAddressFamiliesAndProxySettings('${dir.path}');
+    });
+  }
+}
diff --git a/tests/standalone/io/http_override_test.dart b/tests/standalone/io/http_override_test.dart
index 4be81c7..92270d0 100644
--- a/tests/standalone/io/http_override_test.dart
+++ b/tests/standalone/io/http_override_test.dart
@@ -41,6 +41,10 @@
   set authenticate(Future<bool> f(Uri url, String scheme, String realm)?) {}
   void addCredentials(
       Uri url, String realm, HttpClientCredentials credentials) {}
+  set connectionFactory(
+      Future<ConnectionTask<Socket>> Function(
+              Uri url, String? proxyHost, int? proxyPort)?
+          f) {}
   set findProxy(String f(Uri url)?) {}
   set authenticateProxy(
       Future<bool> f(String host, int port, String scheme, String realm)?) {}
@@ -85,6 +89,10 @@
   set authenticate(Future<bool> f(Uri url, String scheme, String realm)?) {}
   void addCredentials(
       Uri url, String realm, HttpClientCredentials credentials) {}
+  set connectionFactory(
+      Future<ConnectionTask<Socket>> Function(
+              Uri url, String? proxyHost, int? proxyPort)?
+          f) {}
   set findProxy(String f(Uri url)?) {}
   set authenticateProxy(
       Future<bool> f(String host, int port, String scheme, String realm)?) {}
diff --git a/tests/standalone_2/io/http_connection_factory_test.dart b/tests/standalone_2/io/http_connection_factory_test.dart
new file mode 100644
index 0000000..eda4ac2
--- /dev/null
+++ b/tests/standalone_2/io/http_connection_factory_test.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.
+// @dart = 2.9
+import "dart:io";
+import 'dart:convert';
+import "package:expect/expect.dart";
+import 'http_proxy_test.dart' show setupProxyServer;
+import 'test_utils.dart' show withTempDir;
+
+testDirectConnection() async {
+  var server = await HttpServer.bind(InternetAddress.anyIPv6, 0);
+  server.forEach((HttpRequest request) {
+    request.response.write('Hello, world!');
+    request.response.close();
+  });
+  final serverUri = Uri.http("127.0.0.1:${server.port}", "/");
+  var client = HttpClient()
+    ..connectionFactory = (uri, proxyHost, proxyPort) {
+      Expect.isNull(proxyHost);
+      Expect.isNull(proxyPort);
+      Expect.equals(serverUri, uri);
+      return Socket.startConnect(uri.host, uri.port);
+    }
+    ..findProxy = (uri) => 'DIRECT';
+  final response = await client.getUrl(serverUri).then((request) {
+    return request.close();
+  });
+  Expect.equals(200, response.statusCode);
+  final responseText = await response
+      .transform(utf8.decoder)
+      .fold('', (String x, String y) => x + y);
+  Expect.equals("Hello, world!", responseText);
+  client.close();
+  server.close();
+}
+
+testConnectionViaProxy() async {
+  var proxyServer = await setupProxyServer();
+  var server = await HttpServer.bind(InternetAddress.anyIPv6, 0);
+  server.forEach((HttpRequest request) {
+    request.response.write('Hello via Proxy');
+    request.response.close();
+  });
+  final serverUri = Uri.http("127.0.0.1:${server.port}", "/");
+  final client = HttpClient()
+    ..connectionFactory = (uri, proxyHost, proxyPort) {
+      Expect.equals("localhost", proxyHost);
+      Expect.equals(proxyServer.port, proxyPort);
+      Expect.equals(serverUri, uri);
+      return Socket.startConnect(proxyHost, proxyPort as int);
+    }
+    ..findProxy = (uri) => "PROXY localhost:${proxyServer.port}";
+  final response = await client.getUrl(serverUri).then((request) {
+    return request.close();
+  });
+  Expect.equals(200, response.statusCode);
+  final responseText = await response
+      .transform(utf8.decoder)
+      .fold('', (String x, String y) => x + y);
+  Expect.equals("Hello via Proxy", responseText);
+  client.close();
+  server.close();
+  proxyServer.shutdown();
+}
+
+testDifferentAddressFamiliesAndProxySettings(String dir) async {
+  // Test a custom connection factory for Unix domain sockets that also allows
+  // regular INET/INET6 access with and without a proxy.
+  var proxyServer = await setupProxyServer();
+  var inet6Server = await HttpServer.bind(InternetAddress.anyIPv6, 0);
+  inet6Server.forEach((HttpRequest request) {
+    request.response.write('Hello via Proxy');
+    request.response.close();
+  });
+  final inet6ServerUri = Uri.http("127.0.0.1:${inet6Server.port}", "/");
+  final unixPath = '$dir/sock';
+  final unixAddress = InternetAddress(unixPath, type: InternetAddressType.unix);
+  final unixServer = await HttpServer.bind(unixAddress, 0);
+  unixServer.forEach((HttpRequest request) {
+    request.response.write('Hello via Unix');
+    request.response.close();
+  });
+  final client = HttpClient()
+    ..connectionFactory = (uri, proxyHost, proxyPort) {
+      if (uri.scheme == 'unix') {
+        assert(proxyHost == null);
+        assert(proxyPort == null);
+        var address = InternetAddress(unixPath, type: InternetAddressType.unix);
+        return Socket.startConnect(address, 0);
+      } else {
+        if (proxyHost != null && proxyPort != null) {
+          return Socket.startConnect(proxyHost, proxyPort);
+        }
+        return Socket.startConnect(uri.host, uri.port);
+      }
+    }
+    ..findProxy = (uri) {
+      if (uri.scheme == 'unix') {
+        // Proxy settings are not meaningful for Unix domain sockets.
+        return 'DIRECT';
+      } else {
+        return "PROXY localhost:${proxyServer.port}";
+      }
+    };
+// Fetch a URL from the INET6 server and verify the results.
+  final inet6Response = await client.getUrl(inet6ServerUri).then((request) {
+    return request.close();
+  });
+  Expect.equals(200, inet6Response.statusCode);
+  final inet6ResponseText = await inet6Response
+      .transform(utf8.decoder)
+      .fold('', (String x, String y) => x + y);
+  Expect.equals("Hello via Proxy", inet6ResponseText);
+// Fetch a URL from the Unix server and verify the results.
+  final unixResponse = await client
+      .getUrl(Uri(
+          scheme: "unix",
+          // Connection pooling is based on the host/port combination
+          // so ensure that the host is unique for unique logical
+          // endpoints. Also, the `host` property is converted to
+          // lowercase so you cannot use it directly for file paths.
+          host: 'dummy',
+          path: "/"))
+      .then((request) {
+    return request.close();
+  });
+  Expect.equals(200, unixResponse.statusCode);
+  final unixResponseText = await unixResponse
+      .transform(utf8.decoder)
+      .fold('', (String x, String y) => x + y);
+  Expect.equals("Hello via Unix", unixResponseText);
+  client.close();
+  inet6Server.close();
+  unixServer.close();
+  proxyServer.shutdown();
+}
+
+main() async {
+  await testDirectConnection();
+  await testConnectionViaProxy();
+  if (Platform.isMacOS || Platform.isLinux || Platform.isAndroid) {
+    await withTempDir('unix_socket_test', (Directory dir) async {
+      await testDifferentAddressFamiliesAndProxySettings('${dir.path}');
+    });
+  }
+}
diff --git a/tests/standalone_2/io/http_override_test.dart b/tests/standalone_2/io/http_override_test.dart
index 668263b..72b4967 100644
--- a/tests/standalone_2/io/http_override_test.dart
+++ b/tests/standalone_2/io/http_override_test.dart
@@ -39,6 +39,10 @@
   set authenticate(Future<bool> f(Uri url, String scheme, String realm)) {}
   void addCredentials(
       Uri url, String realm, HttpClientCredentials credentials) {}
+  set connectionFactory(
+      Future<ConnectionTask<Socket>> Function(
+              Uri url, String proxyHost, int proxyPort)
+          f) {}
   set findProxy(String f(Uri url)) {}
   set authenticateProxy(
       Future<bool> f(String host, int port, String scheme, String realm)) {}
@@ -79,6 +83,10 @@
   set authenticate(Future<bool> f(Uri url, String scheme, String realm)) {}
   void addCredentials(
       Uri url, String realm, HttpClientCredentials credentials) {}
+  set connectionFactory(
+      Future<ConnectionTask<Socket>> Function(
+              Uri url, String proxyHost, int proxyPort)
+          f) {}
   set findProxy(String f(Uri url)) {}
   set authenticateProxy(
       Future<bool> f(String host, int port, String scheme, String realm)) {}
diff --git a/tools/VERSION b/tools/VERSION
index 106f5a3..6a9c5f7 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 17
 PATCH 0
-PRERELEASE 115
+PRERELEASE 116
 PRERELEASE_PATCH 0
\ No newline at end of file
diff --git a/tools/dom/templates/html/impl/impl_Window.darttemplate b/tools/dom/templates/html/impl/impl_Window.darttemplate
index a433f83..9768d87 100644
--- a/tools/dom/templates/html/impl/impl_Window.darttemplate
+++ b/tools/dom/templates/html/impl/impl_Window.darttemplate
@@ -198,7 +198,7 @@
    * See [EventStreamProvider] for usage information.
    */
   static const EventStreamProvider<BeforeUnloadEvent> beforeUnloadEvent =
-      const _BeforeUnloadEventStreamProvider('beforeunload');
+      const EventStreamProvider('beforeunload');
 
   /// Stream of `beforeunload` events handled by this [Window].
   Stream<Event> get onBeforeUnload => beforeUnloadEvent.forTarget(this);
@@ -254,62 +254,3 @@
       JS<num>('num', '#.scrollY', this).round() :
       document.documentElement$NULLASSERT.scrollTop;
 }
-
-class _BeforeUnloadEvent extends _WrappedEvent implements BeforeUnloadEvent {
-  String _returnValue;
-
-$if NNBD
-  _BeforeUnloadEvent(Event base)
-    : _returnValue = '',
-      super(base);
-$else
-  _BeforeUnloadEvent(Event base) : super(base);
-$endif
-
-  String get returnValue => _returnValue;
-
-  set returnValue(String$NULLABLE value) {
-    // Typed as nullable only to be compatible with the overriden method.
-    _returnValue = value$NULLASSERT;
-    // FF and IE use the value as the return value, Chrome will return this from
-    // the event callback function.
-    if (JS<bool>('bool', '("returnValue" in #)', wrapped)) {
-      JS('void', '#.returnValue = #', wrapped, value);
-    }
-  }
-}
-
-class _BeforeUnloadEventStreamProvider implements
-    EventStreamProvider<BeforeUnloadEvent> {
-  final String _eventType;
-
-  const _BeforeUnloadEventStreamProvider(this._eventType);
-
-  Stream<BeforeUnloadEvent> forTarget(EventTarget$NULLABLE e, {bool useCapture: false}) {
-    // Specify the generic type for EventStream only in dart2js.
-    var stream = new _EventStream<BeforeUnloadEvent>(e, _eventType, useCapture);
-    var controller = new StreamController<BeforeUnloadEvent>(sync: true);
-
-    stream.listen((event) {
-      var wrapped = new _BeforeUnloadEvent(event);
-      controller.add(wrapped);
-    });
-
-    return controller.stream;
-  }
-
-  String getEventType(EventTarget target) {
-    return _eventType;
-  }
-
-  ElementStream<BeforeUnloadEvent> forElement(Element e, {bool useCapture: false}) {
-    // Specify the generic type for _ElementEventStreamImpl only in dart2js.
-    return new _ElementEventStreamImpl<BeforeUnloadEvent>(e, _eventType, useCapture);
-  }
-
-  ElementStream<BeforeUnloadEvent> _forElementList(ElementList<Element> e,
-      {bool useCapture: false}) {
-    // Specify the generic type for _ElementEventStreamImpl only in dart2js.
-    return new _ElementListEventStreamImpl<BeforeUnloadEvent>(e, _eventType, useCapture);
-  }
-}