Version 2.18.0-219.0.dev

Merge commit '14796fafa867d6db4bf3bc8307c82e02fcb0d45f' into 'dev'
diff --git a/pkg/analysis_server/lib/src/handler/legacy/edit_get_available_refactorings.dart b/pkg/analysis_server/lib/src/handler/legacy/edit_get_available_refactorings.dart
index 04b8ac1..5fa0b4a 100644
--- a/pkg/analysis_server/lib/src/handler/legacy/edit_get_available_refactorings.dart
+++ b/pkg/analysis_server/lib/src/handler/legacy/edit_get_available_refactorings.dart
@@ -61,10 +61,9 @@
       if (element != null) {
         // try CONVERT_METHOD_TO_GETTER
         if (element is ExecutableElement) {
-          Refactoring refactoring = ConvertMethodToGetterRefactoring(
-              searchEngine, resolvedUnit.session, element);
-          var status = await refactoring.checkInitialConditions();
-          if (!status.hasFatalError) {
+          if (ConvertMethodToGetterRefactoring(
+                  searchEngine, resolvedUnit.session, element)
+              .isAvailable()) {
             kinds.add(RefactoringKind.CONVERT_METHOD_TO_GETTER);
           }
         }
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_code_actions.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_code_actions.dart
index b34d459..d37962b 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_code_actions.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_code_actions.dart
@@ -612,7 +612,10 @@
         final node = NodeLocator(offset).searchWithin(unit.unit);
         final element = server.getElementOfNode(node);
         // Getter to Method
-        if (element is PropertyAccessorElement) {
+        if (element is PropertyAccessorElement &&
+            ConvertGetterToMethodRefactoring(
+                    server.searchEngine, unit.session, element)
+                .isAvailable()) {
           refactorActions.add(createRefactor(
               CodeActionKind.RefactorRewrite,
               'Convert Getter to Method',
@@ -621,7 +624,9 @@
 
         // Method to Getter
         if (element is ExecutableElement &&
-            element is! PropertyAccessorElement) {
+            ConvertMethodToGetterRefactoring(
+                    server.searchEngine, unit.session, element)
+                .isAvailable()) {
           refactorActions.add(createRefactor(
               CodeActionKind.RefactorRewrite,
               'Convert Method to Getter',
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 ccea475..07ab252 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
@@ -85,7 +85,9 @@
           return cancelled();
         }
 
-        var newInsertText = item.insertText ?? item.label;
+        var newInsertText = item.textEdit
+                ?.map((edit) => edit.newText, (edit) => edit.newText) ??
+            item.label;
         final builder = ChangeBuilder(session: session);
         await builder.addDartFileEdit(file, (builder) {
           final result = builder.importLibraryElement(libraryUri);
@@ -151,7 +153,6 @@
           preselect: item.preselect,
           sortText: item.sortText,
           filterText: item.filterText,
-          insertText: newInsertText,
           insertTextFormat: item.insertTextFormat,
           insertTextMode: item.insertTextMode,
           textEdit: supportsInsertReplace && insertionRange != replacementRange
@@ -262,7 +263,6 @@
       preselect: item.preselect,
       sortText: item.sortText,
       filterText: item.filterText,
-      insertText: item.insertText,
       insertTextFormat: item.insertTextFormat,
       textEdit: item.textEdit,
       additionalTextEdits: item.additionalTextEdits,
diff --git a/pkg/analysis_server/lib/src/lsp/mapping.dart b/pkg/analysis_server/lib/src/lsp/mapping.dart
index 518bf16..77a4353 100644
--- a/pkg/analysis_server/lib/src/lsp/mapping.dart
+++ b/pkg/analysis_server/lib/src/lsp/mapping.dart
@@ -360,9 +360,6 @@
     filterText: completion != label
         ? completion
         : null, // filterText uses label if not set
-    insertText: insertText != label
-        ? insertText
-        : null, // insertText uses label if not set
     insertTextFormat: insertTextFormat != lsp.InsertTextFormat.PlainText
         ? insertTextFormat
         : null, // Defaults to PlainText if not supplied
@@ -1124,8 +1121,8 @@
   LspClientCapabilities capabilities,
   server.LineInfo lineInfo,
   server.CompletionSuggestion suggestion, {
-  Range? replacementRange,
-  Range? insertionRange,
+  required Range replacementRange,
+  required Range insertionRange,
   required bool includeCommitCharacters,
   required bool completeFunctionCalls,
   CompletionItemResolutionInfo? resolutionData,
@@ -1234,31 +1231,26 @@
     filterText: filterText != label
         ? filterText
         : null, // filterText uses label if not set
-    insertText: insertText != label
-        ? insertText
-        : null, // insertText uses label if not set
     insertTextFormat: insertTextFormat != lsp.InsertTextFormat.PlainText
         ? insertTextFormat
         : null, // Defaults to PlainText if not supplied
     insertTextMode: supportsAsIsInsertMode && isMultilineCompletion
         ? InsertTextMode.asIs
         : null,
-    textEdit: (insertionRange == null || replacementRange == null)
-        ? null
-        : supportsInsertReplace && insertionRange != replacementRange
-            ? Either2<InsertReplaceEdit, TextEdit>.t1(
-                InsertReplaceEdit(
-                  insert: insertionRange,
-                  replace: replacementRange,
-                  newText: insertText,
-                ),
-              )
-            : Either2<InsertReplaceEdit, TextEdit>.t2(
-                TextEdit(
-                  range: replacementRange,
-                  newText: insertText,
-                ),
-              ),
+    textEdit: supportsInsertReplace && insertionRange != replacementRange
+        ? Either2<InsertReplaceEdit, TextEdit>.t1(
+            InsertReplaceEdit(
+              insert: insertionRange,
+              replace: replacementRange,
+              newText: insertText,
+            ),
+          )
+        : Either2<InsertReplaceEdit, TextEdit>.t2(
+            TextEdit(
+              range: replacementRange,
+              newText: insertText,
+            ),
+          ),
   );
 }
 
diff --git a/pkg/analysis_server/lib/src/services/refactoring/convert_getter_to_method.dart b/pkg/analysis_server/lib/src/services/refactoring/convert_getter_to_method.dart
index 5aaf8ad..e226e3e 100644
--- a/pkg/analysis_server/lib/src/services/refactoring/convert_getter_to_method.dart
+++ b/pkg/analysis_server/lib/src/services/refactoring/convert_getter_to_method.dart
@@ -70,7 +70,13 @@
     return change;
   }
 
-  RefactoringStatus _checkInitialConditions() {
+  @override
+  bool isAvailable() {
+    return !_checkElement().hasFatalError;
+  }
+
+  /// Checks if [element] is valid to perform this refactor.
+  RefactoringStatus _checkElement() {
     if (!element.isGetter || element.isSynthetic) {
       return RefactoringStatus.fatal(
           'Only explicit getters can be converted to methods.');
@@ -78,6 +84,10 @@
     return RefactoringStatus();
   }
 
+  RefactoringStatus _checkInitialConditions() {
+    return _checkElement();
+  }
+
   Future<void> _updateElementDeclaration(
       PropertyAccessorElement element) async {
     // prepare "get" keyword
diff --git a/pkg/analysis_server/lib/src/services/refactoring/convert_method_to_getter.dart b/pkg/analysis_server/lib/src/services/refactoring/convert_method_to_getter.dart
index 95ce6f0..dd6724c 100644
--- a/pkg/analysis_server/lib/src/services/refactoring/convert_method_to_getter.dart
+++ b/pkg/analysis_server/lib/src/services/refactoring/convert_method_to_getter.dart
@@ -39,6 +39,37 @@
 
   @override
   Future<RefactoringStatus> checkInitialConditions() async {
+    return _checkElement();
+  }
+
+  @override
+  Future<SourceChange> createChange() async {
+    change = SourceChange(refactoringName);
+    // FunctionElement
+    final element = this.element;
+    if (element is FunctionElement) {
+      await _updateElementDeclaration(element);
+      await _updateElementReferences(element);
+    }
+    // MethodElement
+    if (element is MethodElement) {
+      var elements = await getHierarchyMembers(searchEngine, element);
+      await Future.forEach(elements, (Element element) async {
+        await _updateElementDeclaration(element);
+        return _updateElementReferences(element);
+      });
+    }
+    // done
+    return change;
+  }
+
+  @override
+  bool isAvailable() {
+    return !_checkElement().hasFatalError;
+  }
+
+  /// Checks if [element] is valid to perform this refactor.
+  RefactoringStatus _checkElement() {
     // check Element type
     if (element is FunctionElement) {
       if (element.enclosingElement is! CompilationUnitElement) {
@@ -63,27 +94,6 @@
     return RefactoringStatus();
   }
 
-  @override
-  Future<SourceChange> createChange() async {
-    change = SourceChange(refactoringName);
-    // FunctionElement
-    final element = this.element;
-    if (element is FunctionElement) {
-      await _updateElementDeclaration(element);
-      await _updateElementReferences(element);
-    }
-    // MethodElement
-    if (element is MethodElement) {
-      var elements = await getHierarchyMembers(searchEngine, element);
-      await Future.forEach(elements, (Element element) async {
-        await _updateElementDeclaration(element);
-        return _updateElementReferences(element);
-      });
-    }
-    // done
-    return change;
-  }
-
   Future<void> _updateElementDeclaration(Element element) async {
     // prepare parameters
     FormalParameterList? parameters;
diff --git a/pkg/analysis_server/lib/src/services/refactoring/refactoring.dart b/pkg/analysis_server/lib/src/services/refactoring/refactoring.dart
index 08e544a..7d76a4a 100644
--- a/pkg/analysis_server/lib/src/services/refactoring/refactoring.dart
+++ b/pkg/analysis_server/lib/src/services/refactoring/refactoring.dart
@@ -39,6 +39,16 @@
       AnalysisSession session, PropertyAccessorElement element) {
     return ConvertGetterToMethodRefactoringImpl(searchEngine, session, element);
   }
+
+  /// Return `true` if refactoring is available, possibly without checking all
+  /// initial conditions.
+  ///
+  /// This value may be used to control the visibility of the refactor in the UI
+  /// so that it doesn't show up in locations that are obviously not
+  /// appropriate. Initial conditions may perform additional checks (and provide
+  /// user-friendly messages) for locations where a user might reasonably expect
+  /// to see the refactor but it's not valid for a less obvious reason.
+  bool isAvailable();
 }
 
 /// [Refactoring] to convert normal [MethodDeclaration]s into getters.
@@ -49,6 +59,16 @@
       AnalysisSession session, ExecutableElement element) {
     return ConvertMethodToGetterRefactoringImpl(searchEngine, session, element);
   }
+
+  /// Return `true` if refactoring is available, possibly without checking all
+  /// initial conditions.
+  ///
+  /// This value may be used to control the visibility of the refactor in the UI
+  /// so that it doesn't show up in locations that are obviously not
+  /// appropriate. Initial conditions may perform additional checks (and provide
+  /// user-friendly messages) for locations where a user might reasonably expect
+  /// to see the refactor but it's not valid for a less obvious reason.
+  bool isAvailable();
 }
 
 /// [Refactoring] to extract an expression into a local variable declaration.
@@ -100,6 +120,12 @@
 
   /// Return `true` if refactoring is available, possibly without checking all
   /// initial conditions.
+  ///
+  /// This value may be used to control the visibility of the refactor in the UI
+  /// so that it doesn't show up in locations that are obviously not
+  /// appropriate. Initial conditions may perform additional checks (and provide
+  /// user-friendly messages) for locations where a user might reasonably expect
+  /// to see the refactor but it's not valid for a less obvious reason.
   bool isAvailable();
 }
 
@@ -168,6 +194,12 @@
 
   /// Return `true` if refactoring is available, possibly without checking all
   /// initial conditions.
+  ///
+  /// This value may be used to control the visibility of the refactor in the UI
+  /// so that it doesn't show up in locations that are obviously not
+  /// appropriate. Initial conditions may perform additional checks (and provide
+  /// user-friendly messages) for locations where a user might reasonably expect
+  /// to see the refactor but it's not valid for a less obvious reason.
   bool isAvailable();
 }
 
@@ -195,6 +227,12 @@
 
   /// Return `true` if refactoring is available, possibly without checking all
   /// initial conditions.
+  ///
+  /// This value may be used to control the visibility of the refactor in the UI
+  /// so that it doesn't show up in locations that are obviously not
+  /// appropriate. Initial conditions may perform additional checks (and provide
+  /// user-friendly messages) for locations where a user might reasonably expect
+  /// to see the refactor but it's not valid for a less obvious reason.
   bool isAvailable();
 }
 
@@ -214,6 +252,12 @@
 
   /// Return `true` if refactoring is available, possibly without checking all
   /// initial conditions.
+  ///
+  /// This value may be used to control the visibility of the refactor in the UI
+  /// so that it doesn't show up in locations that are obviously not
+  /// appropriate. Initial conditions may perform additional checks (and provide
+  /// user-friendly messages) for locations where a user might reasonably expect
+  /// to see the refactor but it's not valid for a less obvious reason.
   bool isAvailable();
 }
 
@@ -246,6 +290,12 @@
 
   /// Return `true` if refactoring is available, possibly without checking all
   /// initial conditions.
+  ///
+  /// This value may be used to control the visibility of the refactor in the UI
+  /// so that it doesn't show up in locations that are obviously not
+  /// appropriate. Initial conditions may perform additional checks (and provide
+  /// user-friendly messages) for locations where a user might reasonably expect
+  /// to see the refactor but it's not valid for a less obvious reason.
   bool isAvailable();
 }
 
diff --git a/pkg/analysis_server/test/lsp/code_actions_refactor_test.dart b/pkg/analysis_server/test/lsp/code_actions_refactor_test.dart
index 029f542..9a6951c 100644
--- a/pkg/analysis_server/test/lsp/code_actions_refactor_test.dart
+++ b/pkg/analysis_server/test/lsp/code_actions_refactor_test.dart
@@ -57,12 +57,45 @@
     await verifyCodeActionEdits(
         codeAction, withoutMarkers(content), expectedContent);
   }
+
+  Future<void> test_setter_notAvailable() async {
+    const content = '''
+set ^a(String value) {}
+
+''';
+    newFile(mainFilePath, withoutMarkers(content));
+    await initialize();
+
+    final codeActions = await getCodeActions(mainFileUri.toString(),
+        position: positionFromMarker(content));
+    final codeAction =
+        findCommand(codeActions, Commands.performRefactor, refactorTitle);
+
+    expect(codeAction, isNull);
+  }
 }
 
 @reflectiveTest
 class ConvertMethodToGetterCodeActionsTest extends AbstractCodeActionsTest {
   final refactorTitle = 'Convert Method to Getter';
 
+  Future<void> test_constructor_notAvailable() async {
+    const content = '''
+class A {
+  ^A();
+}
+''';
+    newFile(mainFilePath, withoutMarkers(content));
+    await initialize();
+
+    final codeActions = await getCodeActions(mainFileUri.toString(),
+        position: positionFromMarker(content));
+    final codeAction =
+        findCommand(codeActions, Commands.performRefactor, refactorTitle);
+
+    expect(codeAction, isNull);
+  }
+
   Future<void> test_refactor() async {
     const content = '''
 int ^test() => 42;
diff --git a/pkg/analysis_server/test/lsp/completion_dart_test.dart b/pkg/analysis_server/test/lsp/completion_dart_test.dart
index f55c2bd..9010d16 100644
--- a/pkg/analysis_server/test/lsp/completion_dart_test.dart
+++ b/pkg/analysis_server/test/lsp/completion_dart_test.dart
@@ -32,7 +32,7 @@
     with CompletionTestMixin {
   Future<void> checkCompleteFunctionCallInsertText(
       String content, String completion,
-      {required String? insertText, InsertTextFormat? insertTextFormat}) async {
+      {required String? editText, InsertTextFormat? insertTextFormat}) async {
     await provideConfig(
       () => initialize(
         textDocumentCapabilities: withCompletionItemSnippetSupport(
@@ -51,12 +51,13 @@
     );
 
     expect(item.insertTextFormat, equals(insertTextFormat));
-    expect(item.insertText, equals(insertText));
+    // We always expect `insertText` to be `null` now, as we always use
+    // `textEdit`.
+    expect(item.insertText, isNull);
 
+    // And the expected text should be in the `textEdit`.
     final textEdit = toTextEdit(item.textEdit!);
-    // newText in the edit will always be set, so if insertText is null we need
-    // fall back to item.label for the expected value.
-    expect(textEdit.newText, equals(item.insertText ?? item.label));
+    expect(textEdit.newText, equals(editText));
     expect(textEdit.range, equals(rangeFromMarkers(content)));
   }
 
@@ -188,7 +189,7 @@
         ''',
         'Aaaaa(…)',
         insertTextFormat: InsertTextFormat.Snippet,
-        insertText: r'Aaaaa(${0:a})',
+        editText: r'Aaaaa(${0:a})',
       );
 
   Future<void> test_completeFunctionCalls_escapesDollarArgs() =>
@@ -201,7 +202,7 @@
         'myFunction(…)',
         insertTextFormat: InsertTextFormat.Snippet,
         // The dollar should have been escaped.
-        insertText: r'myFunction(${1:a\$a}, ${2:b})',
+        editText: r'myFunction(${1:a\$a}, ${2:b})',
       );
 
   Future<void> test_completeFunctionCalls_escapesDollarName() =>
@@ -214,7 +215,7 @@
         r'myFunc$tion(…)',
         insertTextFormat: InsertTextFormat.Snippet,
         // The dollar should have been escaped.
-        insertText: r'myFunc\$tion(${1:a}, ${2:b})',
+        editText: r'myFunc\$tion(${1:a}, ${2:b})',
       );
 
   Future<void> test_completeFunctionCalls_existingArgList_constructor() =>
@@ -228,7 +229,7 @@
         }
         ''',
         'Aaaaa(…)',
-        insertText: 'Aaaaa',
+        editText: 'Aaaaa',
       );
 
   Future<void> test_completeFunctionCalls_existingArgList_expression() =>
@@ -239,7 +240,7 @@
         }
         ''',
         'myFunction(…)',
-        insertText: 'myFunction',
+        editText: 'myFunction',
       );
 
   Future<void> test_completeFunctionCalls_existingArgList_member_noPrefix() =>
@@ -254,7 +255,7 @@
         }
         ''',
         'foo(…)',
-        insertText: 'foo',
+        editText: 'foo',
       );
 
   Future<void> test_completeFunctionCalls_existingArgList_namedConstructor() =>
@@ -268,7 +269,7 @@
         }
         ''',
         'foo(…)',
-        insertText: 'foo',
+        editText: 'foo',
       );
 
   Future<void> test_completeFunctionCalls_existingArgList_statement() =>
@@ -279,7 +280,7 @@
         }
         ''',
         'f(…)',
-        insertText: 'f',
+        editText: 'f',
       );
 
   Future<void> test_completeFunctionCalls_existingArgList_suggestionSets() =>
@@ -290,7 +291,7 @@
         }
         ''',
         'print(…)',
-        insertText: 'print',
+        editText: 'print',
       );
 
   Future<void> test_completeFunctionCalls_existingPartialArgList() =>
@@ -304,7 +305,7 @@
         }
         ''',
         'Aaaaa(…)',
-        insertText: 'Aaaaa',
+        editText: 'Aaaaa',
       );
 
   Future<void> test_completeFunctionCalls_expression() =>
@@ -316,7 +317,7 @@
         ''',
         'myFunction(…)',
         insertTextFormat: InsertTextFormat.Snippet,
-        insertText: r'myFunction(${1:a}, ${2:b})',
+        editText: r'myFunction(${1:a}, ${2:b})',
       );
 
   Future<void> test_completeFunctionCalls_flutterSetState() async {
@@ -360,9 +361,9 @@
     // 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, isNull);
     final textEdit = toTextEdit(item.textEdit!);
-    expect(textEdit.newText, equals(item.insertText));
+    expect(textEdit.newText, 'setState(() {\n      \$0\n    });');
     expect(textEdit.range, equals(rangeFromMarkers(content)));
   }
 
@@ -378,7 +379,7 @@
         ''',
         'foo(…)',
         insertTextFormat: InsertTextFormat.Snippet,
-        insertText: r'foo(${0:a})',
+        editText: r'foo(${0:a})',
       );
 
   Future<void> test_completeFunctionCalls_noParameters() async {
@@ -393,10 +394,7 @@
     await checkCompleteFunctionCallInsertText(
       content,
       'myFunction()',
-      // With no params, we don't put a tab stop inside the parens. This results
-      // in the insertText being the same as the label, which means it will be
-      // set to null so that it falls back without needing to repeat the value.
-      insertText: null,
+      editText: 'myFunction()',
       insertTextFormat: InsertTextFormat.Snippet,
     );
   }
@@ -414,7 +412,7 @@
       content,
       'myFunction(…)',
       // With optional params, there should still be parens/tab stop inside.
-      insertText: r'myFunction($0)',
+      editText: r'myFunction($0)',
       insertTextFormat: InsertTextFormat.Snippet,
     );
   }
@@ -443,9 +441,9 @@
     // Ensure the snippet comes through in the expected format with the expected
     // placeholders.
     expect(item.insertTextFormat, equals(InsertTextFormat.Snippet));
-    expect(item.insertText, equals(r'myFunction(${1:a}, ${2:b}, c: ${3:c})'));
+    expect(item.insertText, isNull);
     final textEdit = toTextEdit(item.textEdit!);
-    expect(textEdit.newText, equals(item.insertText));
+    expect(textEdit.newText, r'myFunction(${1:a}, ${2:b}, c: ${3:c})');
     expect(textEdit.range, equals(rangeFromMarkers(content)));
   }
 
@@ -479,19 +477,67 @@
     // Ensure the snippet comes through in the expected format with the expected
     // placeholders.
     expect(item.insertTextFormat, equals(InsertTextFormat.Snippet));
-    expect(item.insertText, equals(r'myFunction(${1:a}, ${2:b}, c: ${3:c})'));
+    expect(item.insertText, isNull);
     expect(item.textEdit, isNotNull);
     final originalTextEdit = item.textEdit;
 
     // Ensure the item can be resolved and retains the correct textEdit (since
     // textEdit may be recomputed during resolve).
     final resolved = await resolveCompletion(item);
+    expect(resolved.insertText, isNull);
     expect(resolved.textEdit, originalTextEdit);
     final textEdit = toTextEdit(resolved.textEdit!);
-    expect(textEdit.newText, equals(item.insertText));
+    expect(textEdit.newText, r'myFunction(${1:a}, ${2:b}, c: ${3:c})');
     expect(textEdit.range, equals(rangeFromMarkers(content)));
   }
 
+  Future<void>
+      test_completeFunctionCalls_resolve_producesCorrectEditWithoutInsertText() async {
+    // Ensure our `resolve` call does not rely on the presence of `insertText`
+    // to compute the correct edits. This is something we did incorrectly in the
+    // past and broke with
+    // https://github.com/dart-lang/sdk/commit/40e25ebad0bd008615b1c1d8021cb27839f00dcd
+    // because the way these are combined in the VS Code LSP client means we are
+    // not provided both `insertText` and `textEdit` back in the resolve call.
+    //
+    // Now, we never supply `insertText` and always use `textEdit`.
+    final content = '''
+final a = Stri^
+    ''';
+
+    /// Helper to verify a completion is as expected.
+    void expectCorrectCompletion(CompletionItem item) {
+      // Ensure this completion looks as we'd expect.
+      expect(item.label, 'String.fromCharCode(…)');
+      expect(item.insertText, isNull);
+      expect(
+        item.textEdit!.map((edit) => edit.newText, (edit) => edit.newText),
+        r'String.fromCharCode(${0:charCode})',
+      );
+    }
+
+    final initialAnalysis = waitForAnalysisComplete();
+    await provideConfig(
+      () => initialize(
+        textDocumentCapabilities: withCompletionItemSnippetSupport(
+            emptyTextDocumentClientCapabilities),
+        workspaceCapabilities:
+            withConfigurationSupport(emptyWorkspaceClientCapabilities),
+      ),
+      {'completeFunctionCalls': true},
+    );
+    await openFile(mainFileUri, withoutMarkers(content));
+    await initialAnalysis;
+    final res = await getCompletion(mainFileUri, positionFromMarker(content));
+
+    final completion =
+        res.singleWhere((c) => c.label == 'String.fromCharCode(…)');
+    expectCorrectCompletion(completion);
+
+    final resolved = await resolveCompletion(completion);
+    expectCorrectCompletion(resolved);
+  }
+
   Future<void> test_completeFunctionCalls_show() async {
     final content = '''
     import 'dart:math' show mi^
@@ -512,9 +558,9 @@
     // The insert text should be a simple string with no parens/args and
     // no need for snippets.
     expect(item.insertTextFormat, isNull);
-    expect(item.insertText, equals(r'min'));
+    expect(item.insertText, isNull);
     final textEdit = toTextEdit(item.textEdit!);
-    expect(textEdit.newText, equals(item.insertText));
+    expect(textEdit.newText, r'min');
   }
 
   Future<void> test_completeFunctionCalls_statement() =>
@@ -526,7 +572,7 @@
         ''',
         'f(…)',
         insertTextFormat: InsertTextFormat.Snippet,
-        insertText: r'f(${0:a})',
+        editText: r'f(${0:a})',
       );
 
   Future<void> test_completeFunctionCalls_suggestionSets() =>
@@ -538,7 +584,7 @@
         ''',
         'print(…)',
         insertTextFormat: InsertTextFormat.Snippet,
-        insertText: r'print(${0:object})',
+        editText: r'print(${0:object})',
       );
 
   Future<void> test_completionKinds_default() async {
@@ -726,7 +772,9 @@
     expect(item, isNotNull);
     expect(item!.label, equals('name => …'));
     expect(item.filterText, isNull); // Falls back to label
-    expect(item.insertText, equals('''@override
+    expect(item.insertText, isNull);
+    final textEdit = toTextEdit(item.textEdit!);
+    expect(textEdit.newText, equals('''@override
   // TODO: implement name
   String get name => throw UnimplementedError();'''));
   }
@@ -936,7 +984,9 @@
     final item = res.singleWhere((c) => c.label.startsWith('setState'));
 
     // Multiline completions should always set insertTextMode.asIs.
-    expect(item.insertText, contains('\n'));
+    expect(item.insertText, isNull);
+    final textEdit = toTextEdit(item.textEdit!);
+    expect(textEdit.newText, contains('\n'));
     expect(item.insertTextMode, equals(InsertTextMode.asIs));
   }
 
@@ -956,7 +1006,9 @@
 
     // Single line completions should never set insertTextMode.asIs to
     // avoid bloating payload size where it wouldn't matter.
-    expect(item.insertText, isNot(contains('\n')));
+    expect(item.insertText, isNull);
+    final textEdit = toTextEdit(item.textEdit!);
+    expect(textEdit.newText, isNot(contains('\n')));
     expect(item.insertTextMode, isNull);
   }
 
@@ -1386,7 +1438,9 @@
     final item = res.singleWhere((c) => c.label == 'one: ');
     expect(item.insertTextFormat,
         anyOf(equals(InsertTextFormat.PlainText), isNull));
-    expect(item.insertText, anyOf(equals('test'), isNull));
+    expect(item.insertText, isNull);
+    final textEdit = toTextEdit(item.textEdit!);
+    expect(textEdit.newText, item.label);
     final updated = applyTextEdits(
       withoutMarkers(content),
       [toTextEdit(item.textEdit!)],
@@ -1444,7 +1498,7 @@
     // 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, isNull);
     final textEdit = toTextEdit(item.textEdit!);
     expect(textEdit.newText, equals(r'one: $0,'));
     expect(
@@ -1483,7 +1537,7 @@
     expect(completion.detail, '(int? a, [int b = 1]) → String?');
   }
 
-  Future<void> test_parensNotInFilterTextInsertText() async {
+  Future<void> test_parensNotInFilterTextOrEditText() async {
     final content = '''
     class MyClass {}
 
@@ -1497,8 +1551,10 @@
     final res = await getCompletion(mainFileUri, positionFromMarker(content));
     expect(res.any((c) => c.label == 'MyClass()'), isTrue);
     final item = res.singleWhere((c) => c.label == 'MyClass()');
-    expect(item.filterText, equals('MyClass'));
-    expect(item.insertText, equals('MyClass'));
+    expect(item.filterText, 'MyClass');
+    expect(item.insertText, isNull);
+    final textEdit = toTextEdit(item.textEdit!);
+    expect(textEdit.newText, 'MyClass');
   }
 
   Future<void> test_plainText() async {
diff --git a/runtime/vm/heap/heap.cc b/runtime/vm/heap/heap.cc
index 8b6c799..2b021a0 100644
--- a/runtime/vm/heap/heap.cc
+++ b/runtime/vm/heap/heap.cc
@@ -193,7 +193,7 @@
     }
     CollectGarbage(thread, GCType::kMarkSweep, GCReason::kExternal);
   } else {
-    CheckConcurrentMarking(thread, GCReason::kExternal);
+    CheckConcurrentMarking(thread, GCReason::kExternal, 0);
   }
 }
 
@@ -459,7 +459,7 @@
         CollectOldSpaceGarbage(thread, GCType::kMarkSweep,
                                GCReason::kPromotion);
       } else {
-        CheckConcurrentMarking(thread, GCReason::kPromotion);
+        CheckConcurrentMarking(thread, GCReason::kPromotion, 0);
       }
     }
   }
@@ -561,7 +561,9 @@
   WaitForSweeperTasks(thread);
 }
 
-void Heap::CheckConcurrentMarking(Thread* thread, GCReason reason) {
+void Heap::CheckConcurrentMarking(Thread* thread,
+                                  GCReason reason,
+                                  intptr_t size) {
   PageSpace::Phase phase;
   {
     MonitorLocker ml(old_space_.tasks_lock());
@@ -570,11 +572,9 @@
 
   switch (phase) {
     case PageSpace::kMarking:
-      // TODO(rmacnak): Make the allocator of a large page mark size equals to
-      // the large page?
-      COMPILE_ASSERT(kOldPageSize == 512 * KB);
-      COMPILE_ASSERT(kNewPageSize == 512 * KB);
-      old_space_.IncrementalMarkWithSizeBudget(512 * KB);
+      if (size != 0) {
+        old_space_.IncrementalMarkWithSizeBudget(size);
+      }
       return;
     case PageSpace::kSweepingLarge:
     case PageSpace::kSweepingRegular:
diff --git a/runtime/vm/heap/heap.h b/runtime/vm/heap/heap.h
index 6a0dc2a..f4fb748 100644
--- a/runtime/vm/heap/heap.h
+++ b/runtime/vm/heap/heap.h
@@ -125,7 +125,7 @@
   void CollectAllGarbage(GCReason reason = GCReason::kFull,
                          bool compact = false);
 
-  void CheckConcurrentMarking(Thread* thread, GCReason reason);
+  void CheckConcurrentMarking(Thread* thread, GCReason reason, intptr_t size);
   void StartConcurrentMarking(Thread* thread, GCReason reason);
   void WaitForMarkerTasks(Thread* thread);
   void WaitForSweeperTasks(Thread* thread);
diff --git a/runtime/vm/heap/pages.cc b/runtime/vm/heap/pages.cc
index eb2bbd8..1d66e97 100644
--- a/runtime/vm/heap/pages.cc
+++ b/runtime/vm/heap/pages.cc
@@ -473,7 +473,8 @@
   if (growth_policy != kForceGrowth) {
     ASSERT(GrowthControlState());
     if (heap_ != nullptr) {  // Some unit tests.
-      heap_->CheckConcurrentMarking(Thread::Current(), GCReason::kOldSpace);
+      heap_->CheckConcurrentMarking(Thread::Current(), GCReason::kOldSpace,
+                                    kOldPageSize);
     }
   }
 
@@ -514,7 +515,8 @@
   if (growth_policy != kForceGrowth) {
     ASSERT(GrowthControlState());
     if (heap_ != nullptr) {  // Some unit tests.
-      heap_->CheckConcurrentMarking(Thread::Current(), GCReason::kOldSpace);
+      heap_->CheckConcurrentMarking(Thread::Current(), GCReason::kOldSpace,
+                                    size);
     }
   }
 
diff --git a/runtime/vm/heap/safepoint.cc b/runtime/vm/heap/safepoint.cc
index 9933bed..d75d06a 100644
--- a/runtime/vm/heap/safepoint.cc
+++ b/runtime/vm/heap/safepoint.cc
@@ -65,7 +65,7 @@
     if (heap->old_space()->ReachedHardThreshold()) {
       heap->CollectGarbage(T, GCType::kMarkSweep, GCReason::kOldSpace);
     } else {
-      heap->CheckConcurrentMarking(T, GCReason::kOldSpace);
+      heap->CheckConcurrentMarking(T, GCReason::kOldSpace, 0);
     }
   }
 }
diff --git a/runtime/vm/heap/scavenger.cc b/runtime/vm/heap/scavenger.cc
index 17155e2..5bce434 100644
--- a/runtime/vm/heap/scavenger.cc
+++ b/runtime/vm/heap/scavenger.cc
@@ -1680,7 +1680,7 @@
 
   if (can_safepoint) {
     ASSERT(thread->no_safepoint_scope_depth() == 0);
-    heap_->CheckConcurrentMarking(thread, GCReason::kNewSpace);
+    heap_->CheckConcurrentMarking(thread, GCReason::kNewSpace, kNewPageSize);
   }
 
   MutexLocker ml(&space_lock_);
diff --git a/tools/VERSION b/tools/VERSION
index 927f53e..5848a35 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 18
 PATCH 0
-PRERELEASE 218
+PRERELEASE 219
 PRERELEASE_PATCH 0
\ No newline at end of file