Add LSP support for Extract Method refactor

Change-Id: Id5c9e0657648963d5f96469fbac9269dad4e32a7
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/106348
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Danny Tuppeny <dantup@google.com>
diff --git a/pkg/analysis_server/lib/src/lsp/constants.dart b/pkg/analysis_server/lib/src/lsp/constants.dart
index 1801317..bb6a56f 100644
--- a/pkg/analysis_server/lib/src/lsp/constants.dart
+++ b/pkg/analysis_server/lib/src/lsp/constants.dart
@@ -43,10 +43,12 @@
     sortMembers,
     organizeImports,
     sendWorkspaceEdit,
+    performRefactor,
   ];
   static const sortMembers = 'edit.sortMembers';
   static const organizeImports = 'edit.organizeImports';
   static const sendWorkspaceEdit = 'edit.sendWorkspaceEdit';
+  static const performRefactor = 'refactor.perform';
 }
 
 abstract class CustomMethods {
@@ -84,6 +86,7 @@
   static const FileHasErrors = const ErrorCodes(-32008);
   static const ClientFailedToApplyEdit = const ErrorCodes(-32009);
   static const RenameNotValid = const ErrorCodes(-32010);
+  static const RefactorFailed = const ErrorCodes(-32011);
 
   /// An error raised when the server detects that the server and client are out
   /// of sync and cannot recover. For example if a textDocument/didChange notification
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/commands/perform_refactor.dart b/pkg/analysis_server/lib/src/lsp/handlers/commands/perform_refactor.dart
new file mode 100644
index 0000000..31a99f9
--- /dev/null
+++ b/pkg/analysis_server/lib/src/lsp/handlers/commands/perform_refactor.dart
@@ -0,0 +1,104 @@
+// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
+import 'package:analysis_server/lsp_protocol/protocol_special.dart';
+import 'package:analysis_server/src/lsp/constants.dart';
+import 'package:analysis_server/src/lsp/handlers/commands/simple_edit_handler.dart';
+import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';
+import 'package:analysis_server/src/lsp/mapping.dart';
+import 'package:analysis_server/src/protocol_server.dart';
+import 'package:analysis_server/src/services/refactoring/refactoring.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+
+class PerformRefactorCommandHandler extends SimpleEditCommandHandler {
+  PerformRefactorCommandHandler(LspAnalysisServer server) : super(server);
+
+  @override
+  String get commandName => 'Perform Refactor';
+
+  @override
+  Future<ErrorOr<void>> handle(List<dynamic> arguments) async {
+    if (arguments == null ||
+        arguments.length != 6 ||
+        arguments[0] is! String || // kind
+        arguments[1] is! String || // path
+        (arguments[2] != null && arguments[2] is! int) || // docVersion
+        arguments[3] is! int || // offset
+        arguments[4] is! int || // length
+        // options
+        (arguments[5] != null && arguments[5] is! Map<String, dynamic>)) {
+      // length
+      return ErrorOr.error(new ResponseError(
+        ServerErrorCodes.InvalidCommandArguments,
+        '$commandName requires 6 parameters: RefactoringKind, docVersion, filePath, offset, length, options (optional)',
+        null,
+      ));
+    }
+
+    String kind = arguments[0];
+    String path = arguments[1];
+    int docVersion = arguments[2];
+    int offset = arguments[3];
+    int length = arguments[4];
+    Map<String, dynamic> options = arguments[5];
+
+    final result = await requireResolvedUnit(path);
+    return result.mapResult((result) async {
+      return _getRefactoring(
+              RefactoringKind(kind), result, offset, length, options)
+          .mapResult((refactoring) async {
+        final status = await refactoring.checkAllConditions();
+
+        if (status.hasError) {
+          return error(ServerErrorCodes.RefactorFailed, status.message);
+        }
+
+        final change = await refactoring.createChange();
+
+        // If the file changed while we were validating and preparing the change,
+        // we should fail to avoid sending bad edits.
+        if (docVersion != null &&
+            docVersion != server.getVersionedDocumentIdentifier(path).version) {
+          return error(ErrorCodes.ContentModified,
+              'Content was modified before refactor was applied');
+        }
+
+        final edit = createWorkspaceEdit(server, change.edits);
+        return await sendWorkspaceEditToClient(edit);
+      });
+    });
+  }
+
+  ErrorOr<Refactoring> _getRefactoring(
+    RefactoringKind kind,
+    ResolvedUnitResult result,
+    int offset,
+    int length,
+    Map<String, dynamic> options,
+  ) {
+    switch (kind) {
+      case RefactoringKind.EXTRACT_METHOD:
+        final refactor = ExtractMethodRefactoring(
+            server.searchEngine, result, offset, length);
+        // TODO(dantup): For now we don't have a good way to prompt the user
+        // for a method name so we just use a placeholder and expect them to
+        // rename (this is what C#/Omnisharp does), but there's an open request
+        // to handle this better.
+        // https://github.com/microsoft/language-server-protocol/issues/764
+        refactor.name =
+            (options != null ? options['name'] : null) ?? 'newMethod';
+        // Defaults to true, but may be surprising if users didn't have an option
+        // to opt in.
+        refactor.extractAll = false;
+        return success(refactor);
+
+      default:
+        return error(ServerErrorCodes.InvalidCommandArguments,
+            'Unknown RefactoringKind $kind was supplied to $commandName');
+    }
+  }
+}
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 326b25a..e1926bd 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
@@ -13,12 +13,14 @@
 import 'package:analysis_server/src/lsp/handlers/handlers.dart';
 import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';
 import 'package:analysis_server/src/lsp/mapping.dart';
+import 'package:analysis_server/src/protocol_server.dart';
 import 'package:analysis_server/src/services/correction/assist.dart';
 import 'package:analysis_server/src/services/correction/assist_internal.dart';
 import 'package:analysis_server/src/services/correction/change_workspace.dart';
 import 'package:analysis_server/src/services/correction/fix.dart';
 import 'package:analysis_server/src/services/correction/fix/dart/top_level_declarations.dart';
 import 'package:analysis_server/src/services/correction/fix_internal.dart';
+import 'package:analysis_server/src/services/refactoring/refactoring.dart';
 import 'package:analyzer/dart/analysis/results.dart';
 import 'package:analyzer/dart/analysis/session.dart'
     show InconsistentAnalysisException;
@@ -164,7 +166,7 @@
       _getSourceActions(
           kinds, supportsLiterals, supportsWorkspaceApplyEdit, path),
       _getAssistActions(kinds, supportsLiterals, offset, length, unit),
-      _getRefactorActions(kinds, supportsLiterals, path, range, unit),
+      _getRefactorActions(kinds, supportsLiterals, path, offset, length, unit),
       _getFixActions(kinds, supportsLiterals, range, unit),
     ]);
     final flatResults = results.expand((x) => x).toList();
@@ -225,18 +227,56 @@
     HashSet<CodeActionKind> clientSupportedCodeActionKinds,
     bool clientSupportsLiteralCodeActions,
     String path,
-    Range range,
+    int offset,
+    int length,
     ResolvedUnitResult unit,
   ) async {
-    // We only support these for clients that advertise codeActionLiteralSupport.
-    if (!clientSupportsLiteralCodeActions ||
+    // The refactor actions supported are only valid for Dart files.
+    if (!AnalysisEngine.isDartFileName(path)) {
+      return const [];
+    }
+
+    // If the client told us what kinds they support but it does not include
+    // Refactor then don't return any.
+    if (clientSupportsLiteralCodeActions &&
         !clientSupportedCodeActionKinds.contains(CodeActionKind.Refactor)) {
       return const [];
     }
 
+    /// Helper to create refactors that execute commands provided with
+    /// the current file, location and document version.
+    createRefactor(
+      CodeActionKind actionKind,
+      String name,
+      RefactoringKind refactorKind, [
+      Map<String, dynamic> options,
+    ]) {
+      return _commandOrCodeAction(
+          clientSupportsLiteralCodeActions,
+          actionKind,
+          new Command(name, Commands.performRefactor, [
+            refactorKind.toJson(),
+            path,
+            server.getVersionedDocumentIdentifier(path).version,
+            offset,
+            length,
+            options,
+          ]));
+    }
+
     try {
-      // TODO(dantup): Implement refactors.
-      return [];
+      final refactorActions = <Either2<Command, CodeAction>>[];
+
+      // Extract Method
+      if (ExtractMethodRefactoring(server.searchEngine, unit, offset, length)
+          .isAvailable()) {
+        refactorActions.add(createRefactor(CodeActionKind.RefactorExtract,
+            'Extract Method', RefactoringKind.EXTRACT_METHOD));
+      }
+
+      // TODO(dantup): Extract Widget
+
+      return refactorActions;
     } on InconsistentAnalysisException {
       // If an InconsistentAnalysisException occurs, it's likely the user modified
       // the source and therefore is no longer interested in the results, so
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_execute_command.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_execute_command.dart
index f552cf6..9e11820 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_execute_command.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_execute_command.dart
@@ -8,6 +8,7 @@
 import 'package:analysis_server/lsp_protocol/protocol_special.dart';
 import 'package:analysis_server/src/lsp/constants.dart';
 import 'package:analysis_server/src/lsp/handlers/commands/organize_imports.dart';
+import 'package:analysis_server/src/lsp/handlers/commands/perform_refactor.dart';
 import 'package:analysis_server/src/lsp/handlers/commands/send_workspace_edit.dart';
 import 'package:analysis_server/src/lsp/handlers/commands/sort_members.dart';
 import 'package:analysis_server/src/lsp/handlers/handlers.dart';
@@ -22,6 +23,7 @@
       : commandHandlers = {
           Commands.sortMembers: new SortMembersCommandHandler(server),
           Commands.organizeImports: new OrganizeImportsCommandHandler(server),
+          Commands.performRefactor: new PerformRefactorCommandHandler(server),
           Commands.sendWorkspaceEdit:
               new SendWorkspaceEditCommandHandler(server),
         },
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_initialize.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_initialize.dart
index 1aa7293..3b1d28a 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_initialize.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_initialize.dart
@@ -105,8 +105,8 @@
         null,
         null,
         true, // foldingRangeProvider
-        new ExecuteCommandOptions(Commands.serverSupportedCommands),
         null, // declarationProvider
+        new ExecuteCommandOptions(Commands.serverSupportedCommands),
         new ServerCapabilitiesWorkspace(
             new ServerCapabilitiesWorkspaceFolders(true, true)),
         null);
diff --git a/pkg/analysis_server/test/lsp/code_actions_abstract.dart b/pkg/analysis_server/test/lsp/code_actions_abstract.dart
index 64aea35..d10c35c 100644
--- a/pkg/analysis_server/test/lsp/code_actions_abstract.dart
+++ b/pkg/analysis_server/test/lsp/code_actions_abstract.dart
@@ -41,11 +41,14 @@
   }
 
   Either2<Command, CodeAction> findCommand(
-      List<Either2<Command, CodeAction>> actions, String commandID) {
+      List<Either2<Command, CodeAction>> actions, String commandID,
+      [String wantedTitle]) {
     for (var codeAction in actions) {
       final id = codeAction.map(
           (cmd) => cmd.command, (action) => action.command.command);
-      if (id == commandID) {
+      final title =
+          codeAction.map((cmd) => cmd.title, (action) => action.title);
+      if (id == commandID && (wantedTitle == null || wantedTitle == title)) {
         return codeAction;
       }
     }
@@ -67,4 +70,64 @@
     }
     return null;
   }
+
+  /// Verifies that executing the given code actions command on the server
+  /// results in an edit being sent in the client that updates the file to match
+  /// the expected content.
+  Future verifyCodeActionEdits(Either2<Command, CodeAction> codeAction,
+      String content, String expectedContent,
+      {bool expectDocumentChanges = false}) async {
+    final command = codeAction.map(
+      (command) => command,
+      (codeAction) => codeAction.command,
+    );
+
+    await verifyCommandEdits(command, content, expectedContent,
+        expectDocumentChanges: expectDocumentChanges);
+  }
+
+  /// Verifies that executing the given command on the server results in an edit
+  /// being sent in the client that updates the file to match the expected
+  /// content.
+  Future<void> verifyCommandEdits(
+      Command command, String content, String expectedContent,
+      {bool expectDocumentChanges = false}) async {
+    ApplyWorkspaceEditParams editParams;
+
+    final commandResponse = await handleExpectedRequest<Object,
+        ApplyWorkspaceEditParams, ApplyWorkspaceEditResponse>(
+      Method.workspace_applyEdit,
+      () => executeCommand(command),
+      handler: (edit) {
+        // When the server sends the edit back, just keep a copy and say we
+        // applied successfully (it'll be verified below).
+        editParams = edit;
+        return new ApplyWorkspaceEditResponse(true, null);
+      },
+    );
+    // Successful edits return an empty success() response.
+    expect(commandResponse, isNull);
+
+    // Ensure the edit came back, and using the expected changes.
+    expect(editParams, isNotNull);
+    if (expectDocumentChanges) {
+      expect(editParams.edit.changes, isNull);
+      expect(editParams.edit.documentChanges, isNotNull);
+    } else {
+      expect(editParams.edit.changes, isNotNull);
+      expect(editParams.edit.documentChanges, isNull);
+    }
+
+    // Ensure applying the changes will give us the expected content.
+    final contents = {
+      mainFilePath: withoutMarkers(content),
+    };
+
+    if (expectDocumentChanges) {
+      applyDocumentChanges(contents, editParams.edit.documentChanges);
+    } else {
+      applyChanges(contents, editParams.edit.changes);
+    }
+    expect(contents[mainFilePath], equals(expectedContent));
+  }
 }
diff --git a/pkg/analysis_server/test/lsp/code_actions_refactor_test.dart b/pkg/analysis_server/test/lsp/code_actions_refactor_test.dart
new file mode 100644
index 0000000..7c082d2
--- /dev/null
+++ b/pkg/analysis_server/test/lsp/code_actions_refactor_test.dart
@@ -0,0 +1,64 @@
+// Copyright (c) 2019, 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/constants.dart';
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'code_actions_abstract.dart';
+
+main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(ExtractMethodRefactorCodeActionsTest);
+  });
+}
+
+@reflectiveTest
+class ExtractMethodRefactorCodeActionsTest extends AbstractCodeActionsTest {
+  final extractMethodTitle = 'Extract Method';
+  test_appliesCorrectEdits() async {
+    const content = '''
+main() {
+  print('Test!');
+  [[print('Test!');]]
+}
+    ''';
+    const expectedContent = '''
+main() {
+  print('Test!');
+  newMethod();
+}
+
+void newMethod() {
+  print('Test!');
+}
+    ''';
+    await newFile(mainFilePath, content: withoutMarkers(content));
+    await initialize();
+
+    final codeActions = await getCodeActions(mainFileUri.toString(),
+        range: rangeFromMarkers(content));
+    final codeAction =
+        findCommand(codeActions, Commands.performRefactor, extractMethodTitle);
+    expect(codeAction, isNotNull);
+
+    await verifyCodeActionEdits(
+        codeAction, withoutMarkers(content), expectedContent);
+  }
+
+  test_invalidLocation() async {
+    const content = '''
+import 'dart:convert';
+^
+main() {}
+    ''';
+    await newFile(mainFilePath, content: content);
+    await initialize();
+
+    final codeActions = await getCodeActions(mainFileUri.toString());
+    final codeAction =
+        findCommand(codeActions, Commands.performRefactor, extractMethodTitle);
+    expect(codeAction, isNull);
+  }
+}
diff --git a/pkg/analysis_server/test/lsp/code_actions_source_test.dart b/pkg/analysis_server/test/lsp/code_actions_source_test.dart
index fb1daa5..6a9307f 100644
--- a/pkg/analysis_server/test/lsp/code_actions_source_test.dart
+++ b/pkg/analysis_server/test/lsp/code_actions_source_test.dart
@@ -44,38 +44,8 @@
     final codeAction = findCommand(codeActions, Commands.organizeImports);
     expect(codeAction, isNotNull);
 
-    final command = codeAction.map(
-      (command) => command,
-      (codeAction) => codeAction.command,
-    );
-
-    ApplyWorkspaceEditParams editParams;
-
-    final commandResponse = await handleExpectedRequest<Object,
-        ApplyWorkspaceEditParams, ApplyWorkspaceEditResponse>(
-      Method.workspace_applyEdit,
-      () => executeCommand(command),
-      handler: (edit) {
-        // When the server sends the edit back, just keep a copy and say we
-        // applied successfully (it'll be verified below).
-        editParams = edit;
-        return new ApplyWorkspaceEditResponse(true, null);
-      },
-    );
-    // Successful edits return an empty success() response.
-    expect(commandResponse, isNull);
-
-    // Ensure the edit came back, and using the documentChanges.
-    expect(editParams, isNotNull);
-    expect(editParams.edit.documentChanges, isNotNull);
-    expect(editParams.edit.changes, isNull);
-
-    // Ensure applying the changes will give us the expected content.
-    final contents = {
-      mainFilePath: withoutMarkers(content),
-    };
-    applyDocumentChanges(contents, editParams.edit.documentChanges);
-    expect(contents[mainFilePath], equals(expectedContent));
+    await verifyCodeActionEdits(codeAction, content, expectedContent,
+        expectDocumentChanges: true);
   }
 
   test_appliesCorrectEdits_withoutDocumentChangesSupport() async {
@@ -103,38 +73,7 @@
     final codeAction = findCommand(codeActions, Commands.organizeImports);
     expect(codeAction, isNotNull);
 
-    final command = codeAction.map(
-      (command) => command,
-      (codeAction) => codeAction.command,
-    );
-
-    ApplyWorkspaceEditParams editParams;
-
-    final commandResponse = await handleExpectedRequest<Object,
-        ApplyWorkspaceEditParams, ApplyWorkspaceEditResponse>(
-      Method.workspace_applyEdit,
-      () => executeCommand(command),
-      handler: (edit) {
-        // When the server sends the edit back, just keep a copy and say we
-        // applied successfully (it'll be verified below).
-        editParams = edit;
-        return new ApplyWorkspaceEditResponse(true, null);
-      },
-    );
-    // Successful edits return an empty success() response.
-    expect(commandResponse, isNull);
-
-    // Ensure the edit came back, and using changes.
-    expect(editParams, isNotNull);
-    expect(editParams.edit.changes, isNotNull);
-    expect(editParams.edit.documentChanges, isNull);
-
-    // Ensure applying the changes will give us the expected content.
-    final contents = {
-      mainFilePath: withoutMarkers(content),
-    };
-    applyChanges(contents, editParams.edit.changes);
-    expect(contents[mainFilePath], equals(expectedContent));
+    await verifyCodeActionEdits(codeAction, content, expectedContent);
   }
 
   test_availableAsCodeActionLiteral() async {
@@ -261,38 +200,8 @@
     final codeAction = findCommand(codeActions, Commands.sortMembers);
     expect(codeAction, isNotNull);
 
-    final command = codeAction.map(
-      (command) => command,
-      (codeAction) => codeAction.command,
-    );
-
-    ApplyWorkspaceEditParams editParams;
-
-    final commandResponse = await handleExpectedRequest<Object,
-        ApplyWorkspaceEditParams, ApplyWorkspaceEditResponse>(
-      Method.workspace_applyEdit,
-      () => executeCommand(command),
-      handler: (edit) {
-        // When the server sends the edit back, just keep a copy and say we
-        // applied successfully (it'll be verified below).
-        editParams = edit;
-        return new ApplyWorkspaceEditResponse(true, null);
-      },
-    );
-    // Successful edits return an empty success() response.
-    expect(commandResponse, isNull);
-
-    // Ensure the edit came back, and using the documentChanges.
-    expect(editParams, isNotNull);
-    expect(editParams.edit.documentChanges, isNotNull);
-    expect(editParams.edit.changes, isNull);
-
-    // Ensure applying the changes will give us the expected content.
-    final contents = {
-      mainFilePath: withoutMarkers(content),
-    };
-    applyDocumentChanges(contents, editParams.edit.documentChanges);
-    expect(contents[mainFilePath], equals(expectedContent));
+    await verifyCodeActionEdits(codeAction, content, expectedContent,
+        expectDocumentChanges: true);
   }
 
   test_appliesCorrectEdits_withoutDocumentChangesSupport() async {
@@ -313,38 +222,7 @@
     final codeAction = findCommand(codeActions, Commands.sortMembers);
     expect(codeAction, isNotNull);
 
-    final command = codeAction.map(
-      (command) => command,
-      (codeAction) => codeAction.command,
-    );
-
-    ApplyWorkspaceEditParams editParams;
-
-    final commandResponse = await handleExpectedRequest<Object,
-        ApplyWorkspaceEditParams, ApplyWorkspaceEditResponse>(
-      Method.workspace_applyEdit,
-      () => executeCommand(command),
-      handler: (edit) {
-        // When the server sends the edit back, just keep a copy and say we
-        // applied successfully (it'll be verified below).
-        editParams = edit;
-        return new ApplyWorkspaceEditResponse(true, null);
-      },
-    );
-    // Successful edits return an empty success() response.
-    expect(commandResponse, isNull);
-
-    // Ensure the edit came back, and using changes.
-    expect(editParams, isNotNull);
-    expect(editParams.edit.changes, isNotNull);
-    expect(editParams.edit.documentChanges, isNull);
-
-    // Ensure applying the changes will give us the expected content.
-    final contents = {
-      mainFilePath: withoutMarkers(content),
-    };
-    applyChanges(contents, editParams.edit.changes);
-    expect(contents[mainFilePath], equals(expectedContent));
+    await verifyCodeActionEdits(codeAction, content, expectedContent);
   }
 
   test_availableAsCodeActionLiteral() async {
diff --git a/pkg/analysis_server/test/lsp/server_abstract.dart b/pkg/analysis_server/test/lsp/server_abstract.dart
index a531618..ca3f95e 100644
--- a/pkg/analysis_server/test/lsp/server_abstract.dart
+++ b/pkg/analysis_server/test/lsp/server_abstract.dart
@@ -147,34 +147,51 @@
     // Complex text manipulations are described with an array of TextEdit's,
     // representing a single change to the document.
     //
-    //  All text edits ranges refer to positions in the original document. Text
+    // All text edits ranges refer to positions in the original document. Text
     // edits ranges must never overlap, that means no part of the original
-    // document must be manipulated by more than one edit. However, it is possible
-    // that multiple edits have the same start position: multiple inserts, or any
-    // number of inserts followed by a single remove or replace edit. If multiple
-    // inserts have the same position, the order in the array defines the order in
-    // which the inserted strings appear in the resulting text.
+    // document must be manipulated by more than one edit. It is possible
+    // that multiple edits have the same start position (eg. multiple inserts in
+    // reverse order), however since that involves complicated tracking and we
+    // only apply edits here sequentially, we don't supported them. We do sort
+    // edits to ensure we apply the later ones first, so we can assume the locations
+    // in the edit are still valid against the new string as each edit is applied.
 
     /// Ensures changes are simple enough to apply easily without any complicated
     /// logic.
     void validateChangesCanBeApplied() {
-      bool intersectsWithOrComesAfter(Position pos, Position other) =>
-          pos.line > other.line ||
-          (pos.line == other.line || pos.character >= other.character);
+      /// Check if a position is before (but not equal) to another position.
+      bool isBefore(Position p, Position other) =>
+          p.line < other.line ||
+          (p.line == other.line && p.character < other.character);
 
-      Position earliestPositionChanged;
-      for (final change in changes) {
-        if (earliestPositionChanged != null &&
-            intersectsWithOrComesAfter(
-                change.range.end, earliestPositionChanged)) {
-          throw 'Test helper applyTextEdits does not support applying multiple edits '
-              'where the edits are not in reverse order.';
+      /// Check if a position is after (but not equal) to another position.
+      bool isAfter(Position p, Position other) =>
+          p.line > other.line ||
+          (p.line == other.line && p.character > other.character);
+      // Check if two ranges intersect or touch.
+      bool rangesIntersect(Range r1, Range r2) {
+        bool endsBefore = isBefore(r1.end, r2.start);
+        bool startsAfter = isAfter(r1.start, r2.end);
+        return !(endsBefore || startsAfter);
+      }
+
+      for (final change1 in changes) {
+        for (final change2 in changes) {
+          if (change1 != change2 &&
+              rangesIntersect(change1.range, change2.range)) {
+            throw 'Test helper applyTextEdits does not support applying multiple edits '
+                'where the edits are not in reverse order.';
+          }
         }
-        earliestPositionChanged = change.range.start;
       }
     }
 
     validateChangesCanBeApplied();
+    changes.sort(
+      (c1, c2) =>
+          positionCompare(c1.range.start, c2.range.start) *
+          -1, // Multiply by -1 to get descending sort.
+    );
     for (final change in changes) {
       newContent = applyTextEdit(newContent, change);
     }
@@ -623,6 +640,16 @@
     await pumpEventQueue();
   }
 
+  int positionCompare(Position p1, Position p2) {
+    if (p1.line < p2.line) return -1;
+    if (p1.line > p2.line) return 1;
+
+    if (p1.character < p2.character) return -1;
+    if (p1.character > p2.character) return -1;
+
+    return 0;
+  }
+
   Position positionFromMarker(String contents) =>
       positionFromOffset(withoutRangeMarkers(contents).indexOf('^'), contents);
 
diff --git a/pkg/analysis_server/test/lsp/test_all.dart b/pkg/analysis_server/test/lsp/test_all.dart
index f1cc08c..6793f81 100644
--- a/pkg/analysis_server/test/lsp/test_all.dart
+++ b/pkg/analysis_server/test/lsp/test_all.dart
@@ -8,9 +8,10 @@
 import 'analyzer_status_test.dart' as analyzer_status;
 import 'cancel_request_test.dart' as cancel_request;
 import 'change_workspace_folders_test.dart' as change_workspace_folders;
-import 'code_actions_assists_test.dart' as code_actions_assists;
 import 'closing_labels_test.dart' as closing_labels;
+import 'code_actions_assists_test.dart' as code_actions_assists;
 import 'code_actions_fixes_test.dart' as code_actions_fixes;
+import 'code_actions_refactor_test.dart' as code_actions_refactor;
 import 'code_actions_source_test.dart' as code_actions_source;
 import 'completion_test.dart' as completion;
 import 'definition_test.dart' as definition;
@@ -20,7 +21,6 @@
 import 'file_modification_test.dart' as file_modification;
 import 'folding_test.dart' as folding;
 import 'format_test.dart' as format;
-import 'super_test.dart' as get_super;
 import 'hover_test.dart' as hover;
 import 'implementation_test.dart' as implementation;
 import 'initialization_test.dart' as initialization;
@@ -30,6 +30,7 @@
 import 'rename_test.dart' as rename;
 import 'server_test.dart' as server;
 import 'signature_help_test.dart' as signature_help;
+import 'super_test.dart' as get_super;
 import 'workspace_symbols_test.dart' as workspace_symbols;
 
 main() {
@@ -41,6 +42,7 @@
     code_actions_assists.main();
     code_actions_fixes.main();
     code_actions_source.main();
+    code_actions_refactor.main();
     completion.main();
     definition.main();
     diagnostic.main();