| // 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: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/handlers/handlers.dart'; |
| import 'package:analysis_server/src/lsp/lsp_analysis_server.dart'; |
| import 'package:analysis_server/src/lsp/progress.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'; |
| import 'package:analyzer/dart/element/element.dart'; |
| import 'package:analyzer/src/dart/ast/utilities.dart'; |
| |
| final _manager = LspRefactorManager._(); |
| |
| /// A base class for refactoring commands that need to create Refactorings from |
| /// client-supplied arguments. |
| abstract class AbstractRefactorCommandHandler extends SimpleEditCommandHandler { |
| AbstractRefactorCommandHandler(LspAnalysisServer server) : super(server); |
| |
| @override |
| String get commandName => 'Perform Refactor'; |
| |
| LspRefactorManager get manager => _manager; |
| |
| FutureOr<ErrorOr<void>> execute( |
| String path, |
| String kind, |
| int offset, |
| int length, |
| Map<String, Object?>? options, |
| CancellationToken cancellationToken, |
| ProgressReporter reporter, |
| int? docVersion); |
| |
| Future<ErrorOr<Refactoring>> getRefactoring( |
| RefactoringKind kind, |
| ResolvedUnitResult result, |
| int offset, |
| int length, |
| Map<String, dynamic>? options, |
| ) async { |
| switch (kind) { |
| case RefactoringKind.EXTRACT_METHOD: |
| final refactor = ExtractMethodRefactoring( |
| server.searchEngine, result, offset, length); |
| |
| var preferredName = options != null ? options['name'] as String : null; |
| // checkInitialConditions will populate names with suggestions. |
| if (preferredName == null) { |
| await refactor.checkInitialConditions(); |
| if (refactor.names.isNotEmpty) { |
| preferredName = refactor.names.first; |
| } |
| } |
| refactor.name = preferredName ?? 'newMethod'; |
| |
| // Defaults to true, but may be surprising if users didn't have an option |
| // to opt in. |
| refactor.extractAll = false; |
| return success(refactor); |
| |
| case RefactoringKind.EXTRACT_LOCAL_VARIABLE: |
| final refactor = ExtractLocalRefactoring(result, offset, length); |
| |
| var preferredName = options != null ? options['name'] as String : null; |
| // checkInitialConditions will populate names with suggestions. |
| if (preferredName == null) { |
| await refactor.checkInitialConditions(); |
| if (refactor.names.isNotEmpty) { |
| preferredName = refactor.names.first; |
| } |
| } |
| refactor.name = preferredName ?? 'newVariable'; |
| |
| // Defaults to true, but may be surprising if users didn't have an option |
| // to opt in. |
| refactor.extractAll = false; |
| return success(refactor); |
| |
| case RefactoringKind.EXTRACT_WIDGET: |
| final refactor = ExtractWidgetRefactoring( |
| server.searchEngine, result, offset, length); |
| // Provide a default name for clients that do not have any custom |
| // handling. |
| // Clients can use the information documented for refactor.perform to |
| // inject their own user-provided names until LSP has some native |
| // support: |
| // https://github.com/microsoft/language-server-protocol/issues/764 |
| refactor.name = |
| options != null ? options['name'] as String : 'NewWidget'; |
| return success(refactor); |
| |
| case RefactoringKind.INLINE_LOCAL_VARIABLE: |
| final refactor = |
| InlineLocalRefactoring(server.searchEngine, result, offset); |
| return success(refactor); |
| |
| case RefactoringKind.INLINE_METHOD: |
| final refactor = |
| InlineMethodRefactoring(server.searchEngine, result, offset); |
| return success(refactor); |
| |
| case RefactoringKind.CONVERT_GETTER_TO_METHOD: |
| final node = NodeLocator(offset).searchWithin(result.unit); |
| final element = server.getElementOfNode(node); |
| if (element != null) { |
| if (element is PropertyAccessorElement) { |
| final refactor = ConvertGetterToMethodRefactoring( |
| server.searchEngine, result.session, element); |
| return success(refactor); |
| } |
| } |
| return error(ServerErrorCodes.InvalidCommandArguments, |
| 'Location supplied to $commandName $kind is not longer valid'); |
| |
| case RefactoringKind.CONVERT_METHOD_TO_GETTER: |
| final node = NodeLocator(offset).searchWithin(result.unit); |
| final element = server.getElementOfNode(node); |
| if (element != null) { |
| if (element is ExecutableElement) { |
| final refactor = ConvertMethodToGetterRefactoring( |
| server.searchEngine, result.session, element); |
| return success(refactor); |
| } |
| } |
| return error(ServerErrorCodes.InvalidCommandArguments, |
| 'Location supplied to $commandName $kind is not longer valid'); |
| |
| default: |
| return error(ServerErrorCodes.InvalidCommandArguments, |
| 'Unknown RefactoringKind $kind was supplied to $commandName'); |
| } |
| } |
| |
| @override |
| Future<ErrorOr<void>> handle(List<Object?>? arguments, |
| ProgressReporter reporter, CancellationToken cancellationToken) 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 |
| // Important: This arguments position is documented in |
| // tool/lsp_spec/README.md to allow clients with custom code (such as |
| // VS Code) to intercept the request and inject options (such as a |
| // user-provided name). Any changes to these arguments must be backwards |
| // compatible, keeping the options in this position. |
| (arguments[5] != null && arguments[5] is! Map<String, Object?>)) { |
| return ErrorOr.error(ResponseError( |
| code: ServerErrorCodes.InvalidCommandArguments, |
| message: |
| '$commandName requires 6 parameters: RefactoringKind, docVersion, filePath, offset, length, options (optional)', |
| )); |
| } |
| |
| final kind = arguments[0] as String; |
| final path = arguments[1] as String; |
| final docVersion = arguments[2] as int?; |
| final offset = arguments[3] as int; |
| final length = arguments[4] as int; |
| final options = arguments[5] as Map<String, Object?>?; |
| |
| return execute(path, kind, offset, length, options, cancellationToken, |
| reporter, docVersion); |
| } |
| } |
| |
| /// Manages a running refactor to help ensure only one refactor runs at a time. |
| class LspRefactorManager { |
| /// The cancellation token for the current in-progress refactor (or null). |
| CancelableToken? _currentRefactoringCancellationToken; |
| |
| LspRefactorManager._(); |
| |
| /// 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; |
| } |
| } |
| } |