[Analyzer] Cancel LSP refactor requests when another refactor request arrives

See https://github.com/dart-lang/sdk/issues/43459.

Change-Id: I52bf26e80b0d6ddb577c89b5a18ff7563e614cb4
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/165321
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Danny Tuppeny <danny@tuppeny.com>
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/commands/organize_imports.dart b/pkg/analysis_server/lib/src/lsp/handlers/commands/organize_imports.dart
index 85f8c67..8fa665f 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/commands/organize_imports.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/commands/organize_imports.dart
@@ -6,6 +6,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/simple_edit_handler.dart';
+import 'package:analysis_server/src/lsp/handlers/handlers.dart';
 import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';
 import 'package:analysis_server/src/services/correction/organize_imports.dart';
 
@@ -16,7 +17,8 @@
   String get commandName => 'Organize Imports';
 
   @override
-  Future<ErrorOr<void>> handle(List<dynamic> arguments) async {
+  Future<ErrorOr<void>> handle(
+      List<dynamic> arguments, CancellationToken cancellationToken) async {
     if (arguments == null || arguments.length != 1 || arguments[0] is! String) {
       return ErrorOr.error(ResponseError(
         code: ServerErrorCodes.InvalidCommandArguments,
@@ -32,6 +34,11 @@
     final docIdentifier = server.getVersionedDocumentIdentifier(path);
 
     final result = await requireResolvedUnit(path);
+
+    if (cancellationToken.isCancellationRequested) {
+      return error(ErrorCodes.RequestCancelled, 'Request was cancelled');
+    }
+
     return result.mapResult((result) {
       final code = result.content;
       final unit = result.unit;
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
index c81db2e..714c7ad 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/commands/perform_refactor.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/commands/perform_refactor.dart
@@ -6,12 +6,15 @@
 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/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/refactoring/refactoring.dart';
 import 'package:analyzer/dart/analysis/results.dart';
 
+final _manager = _RefactorManager();
+
 class PerformRefactorCommandHandler extends SimpleEditCommandHandler {
   PerformRefactorCommandHandler(LspAnalysisServer server) : super(server);
 
@@ -19,7 +22,8 @@
   String get commandName => 'Perform Refactor';
 
   @override
-  Future<ErrorOr<void>> handle(List<dynamic> arguments) async {
+  Future<ErrorOr<void>> handle(
+      List<dynamic> arguments, CancellationToken cancellationToken) async {
     if (arguments == null ||
         arguments.length != 6 ||
         arguments[0] is! String || // kind
@@ -49,24 +53,45 @@
       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);
+        // If the token we were given is not cancellable, replace it with one that
+        // is for the rest of this request, as a future refactor may need to cancel
+        // this request.
+        if (cancellationToken is! CancelableToken) {
+          cancellationToken = CancelableToken();
         }
+        _manager.begin(cancellationToken);
 
-        final change = await refactoring.createChange();
+        try {
+          final status = await refactoring.checkAllConditions();
 
-        // 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');
+          if (status.hasError) {
+            return error(ServerErrorCodes.RefactorFailed, status.message);
+          }
+
+          if (cancellationToken.isCancellationRequested) {
+            return error(ErrorCodes.RequestCancelled, 'Request was cancelled');
+          }
+
+          final change = await refactoring.createChange();
+
+          if (cancellationToken.isCancellationRequested) {
+            return error(ErrorCodes.RequestCancelled, 'Request was cancelled');
+          }
+
+          // 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);
+        } finally {
+          _manager.end(cancellationToken);
         }
-
-        final edit = createWorkspaceEdit(server, change.edits);
-        return await sendWorkspaceEditToClient(edit);
       });
     });
   }
@@ -112,3 +137,22 @@
     }
   }
 }
+
+/// Manages a running refactor to help ensure only one refactor runs at a time.
+class _RefactorManager {
+  /// The cancellation token for the current in-progress refactor (or null).
+  CancelableToken _currentRefactoringCancellationToken;
+
+  /// Begins a new refactor, cancelling any other in-progress refactors.
+  void begin(CancelableToken cancelToken) {
+    _currentRefactoringCancellationToken?.cancel();
+    _currentRefactoringCancellationToken = cancelToken;
+  }
+
+  /// Marks a refactor as no longer current.
+  void end(CancelableToken cancelToken) {
+    if (_currentRefactoringCancellationToken == cancelToken) {
+      _currentRefactoringCancellationToken = null;
+    }
+  }
+}
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/commands/send_workspace_edit.dart b/pkg/analysis_server/lib/src/lsp/handlers/commands/send_workspace_edit.dart
index c918eb6..20a0f06 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/commands/send_workspace_edit.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/commands/send_workspace_edit.dart
@@ -6,6 +6,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/simple_edit_handler.dart';
+import 'package:analysis_server/src/lsp/handlers/handlers.dart';
 import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';
 
 /// This command allows a client to request the server send it a
@@ -22,7 +23,8 @@
   String get commandName => 'Send Workspace Edit';
 
   @override
-  Future<ErrorOr<void>> handle(List<dynamic> arguments) async {
+  Future<ErrorOr<void>> handle(
+      List<dynamic> arguments, CancellationToken cancellationToken) async {
     if (arguments == null ||
         arguments.length != 1 ||
         arguments[0] is! Map<String, dynamic>) {
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/commands/sort_members.dart b/pkg/analysis_server/lib/src/lsp/handlers/commands/sort_members.dart
index c1a5b8f..304deba 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/commands/sort_members.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/commands/sort_members.dart
@@ -6,6 +6,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/simple_edit_handler.dart';
+import 'package:analysis_server/src/lsp/handlers/handlers.dart';
 import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';
 import 'package:analysis_server/src/services/correction/sort_members.dart';
 
@@ -16,7 +17,8 @@
   String get commandName => 'Sort Members';
 
   @override
-  Future<ErrorOr<void>> handle(List<dynamic> arguments) async {
+  Future<ErrorOr<void>> handle(
+      List<dynamic> arguments, CancellationToken cancellationToken) async {
     if (arguments == null || arguments.length != 1 || arguments[0] is! String) {
       return ErrorOr.error(ResponseError(
         code: ServerErrorCodes.InvalidCommandArguments,
@@ -33,6 +35,11 @@
 
     var driver = server.getAnalysisDriver(path);
     final result = await driver?.parseFile(path);
+
+    if (cancellationToken.isCancellationRequested) {
+      return error(ErrorCodes.RequestCancelled, 'Request was cancelled');
+    }
+
     if (result == null) {
       return ErrorOr.error(ResponseError(
         code: ServerErrorCodes.FileNotAnalyzed,
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 22b99d1..b966d78 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
@@ -41,6 +41,6 @@
       return error(ServerErrorCodes.UnknownCommand,
           '${params.command} is not a valid command identifier', null);
     }
-    return handler.handle(params.arguments);
+    return handler.handle(params.arguments, token);
   }
 }
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handlers.dart b/pkg/analysis_server/lib/src/lsp/handlers/handlers.dart
index c87b9c7..8e86fb0 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handlers.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handlers.dart
@@ -47,7 +47,8 @@
     this.server = server;
   }
 
-  Future<ErrorOr<void>> handle(List<dynamic> arguments);
+  Future<ErrorOr<void>> handle(
+      List<dynamic> arguments, CancellationToken cancellationToken);
 }
 
 mixin Handler<P, R> {
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 2701b41..19589b0 100644
--- a/pkg/analysis_server/test/lsp/code_actions_refactor_test.dart
+++ b/pkg/analysis_server/test/lsp/code_actions_refactor_test.dart
@@ -2,11 +2,13 @@
 // 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/lsp_protocol/protocol_generated.dart';
 import 'package:analysis_server/src/lsp/constants.dart';
 import 'package:test/test.dart';
 import 'package:test_reflective_loader/test_reflective_loader.dart';
 
 import '../src/utilities/mock_packages.dart';
+import '../tool/lsp_spec/matchers.dart';
 import 'code_actions_abstract.dart';
 
 void main() {
@@ -49,6 +51,60 @@
         codeAction, withoutMarkers(content), expectedContent);
   }
 
+  Future<void> test_cancelsInProgress() async {
+    const content = '''
+main() {
+  print('Test!');
+  [[print('Test!');]]
+}
+    ''';
+    const expectedContent = '''
+main() {
+  print('Test!');
+  newMethod();
+}
+
+void newMethod() {
+  print('Test!');
+}
+    ''';
+    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);
+
+    // Respond to any applyEdit requests from the server with successful responses
+    // and capturing the last edit.
+    WorkspaceEdit edit;
+    requestsFromServer.listen((request) async {
+      if (request.method == Method.workspace_applyEdit) {
+        final params = ApplyWorkspaceEditParams.fromJson(request.params);
+        edit = params.edit;
+        respondTo(request, ApplyWorkspaceEditResponse(applied: true));
+      }
+    });
+
+    // Send two requests together.
+    final req1 = executeCodeAction(codeAction);
+    final req2 = executeCodeAction(codeAction);
+
+    // Expect the first will have cancelled the second.
+    await expectLater(
+        req1, throwsA(isResponseError(ErrorCodes.RequestCancelled)));
+    await req2;
+
+    // Ensure applying the changes will give us the expected content.
+    final contents = {
+      mainFilePath: withoutMarkers(content),
+    };
+    applyChanges(contents, edit.changes);
+    expect(contents[mainFilePath], equals(expectedContent));
+  }
+
   Future<void> test_invalidLocation() async {
     const content = '''
 import 'dart:convert';
diff --git a/pkg/analysis_server/test/lsp/server_abstract.dart b/pkg/analysis_server/test/lsp/server_abstract.dart
index 5029f4c..b62837b 100644
--- a/pkg/analysis_server/test/lsp/server_abstract.dart
+++ b/pkg/analysis_server/test/lsp/server_abstract.dart
@@ -585,6 +585,15 @@
     await sendNotificationToServer(notification);
   }
 
+  Future<Object> executeCodeAction(
+      Either2<Command, CodeAction> codeAction) async {
+    final command = codeAction.map(
+      (command) => command,
+      (codeAction) => codeAction.command,
+    );
+    return executeCommand(command);
+  }
+
   Future<Object> executeCommand(Command command) async {
     final request = makeRequest(
       Method.workspace_executeCommand,
@@ -939,13 +948,20 @@
     @required FutureOr<RR> Function(R) handler,
     Duration timeout = const Duration(seconds: 5),
   }) async {
-    FutureOr<T> outboundRequest;
+    Future<T> outboundRequest;
 
     // Run [f] and wait for the incoming request from the server.
     final incomingRequest = await expectRequest(method, () {
       // Don't return/await the response yet, as this may not complete until
       // after we have handled the request that comes from the server.
       outboundRequest = f();
+
+      // Because we don't await this future until "later", if it throws the
+      // error is treated as unhandled and will fail the test. Attaching an
+      // error handler prevents that, though since the Future completed with
+      // an error it will still be handled as such when the future is later
+      // awaited.
+      outboundRequest.catchError((_) {});
     });
 
     // Handle the request from the server and send the response back.
diff --git a/pkg/analysis_server/test/mocks.dart b/pkg/analysis_server/test/mocks.dart
index 396a19f..c166c2c 100644
--- a/pkg/analysis_server/test/mocks.dart
+++ b/pkg/analysis_server/test/mocks.dart
@@ -186,7 +186,8 @@
         (throwOnError &&
             message is lsp.NotificationMessage &&
             message.method == Method.window_showMessage &&
-            message.params?.type == MessageType.Error));
+            lsp.ShowMessageParams.fromJson(message.params).type ==
+                MessageType.Error));
 
     if (response is lsp.ResponseMessage) {
       return response;