Add a refactor command

The command name is a bit unfortunate in that it isn't very readable for
debugging purposes, but it has the advantage that there isn't any
opportunity for it to get out of sync. That might be the wrong thing to
optimize for. Let me know what you think.

Change-Id: Iec5c2ad9703f51c7702d4d5094f0548b48fbaafd
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/254321
Reviewed-by: Danny Tuppeny <danny@tuppeny.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
Brian Wilkerson 2022-08-10 22:52:41 +00:00 committed by Commit Bot
parent e139fb1527
commit 94d797ebee
4 changed files with 117 additions and 11 deletions

View file

@ -0,0 +1,86 @@
// 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/lsp_protocol/protocol.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/mapping.dart';
import 'package:analysis_server/src/lsp/progress.dart';
import 'package:analysis_server/src/lsp/source_edits.dart';
import 'package:analysis_server/src/services/refactoring/framework/refactoring_context.dart';
import 'package:analysis_server/src/services/refactoring/framework/refactoring_processor.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
/// A command handler for any of the commands used to implement refactorings.
class RefactorCommandHandler extends SimpleEditCommandHandler {
@override
final String commandName;
final ProducerGenerator generator;
RefactorCommandHandler(super.server, this.commandName, this.generator);
@override
Future<ErrorOr<void>> handle(Map<String, Object?> parameters,
ProgressReporter progress, CancellationToken cancellationToken) async {
if (parameters['filePath'] is! String ||
parameters['selectionOffset'] is! int ||
parameters['selectionLength'] is! int ||
parameters['arguments'] is! List<String>) {
return ErrorOr.error(ResponseError(
code: ServerErrorCodes.InvalidCommandArguments,
message: 'Refactoring operations require 4 parameters: '
'filePath: String, '
'offset: int, '
'length: int, '
'arguments: Map<String, String>'));
}
var filePath = parameters['filePath'] as String;
var offset = parameters['selectionOffset'] as int;
var length = parameters['selectionLength'] as int;
var arguments = parameters['arguments'] as List<String>;
final clientCapabilities = server.clientCapabilities;
if (clientCapabilities == null) {
// This should not happen unless a client misbehaves.
return serverNotInitializedError;
}
final result = await requireResolvedUnit(filePath);
return result.mapResult((result) async {
var context = RefactoringContext(
server: server,
resolvedResult: result,
selectionOffset: offset,
selectionLength: length);
var producer = generator(context);
var builder = ChangeBuilder(
workspace: context.workspace, eol: context.utils.endOfLine);
await producer.compute(arguments, builder);
var edits = builder.sourceChange.edits;
if (edits.isEmpty) {
return success(null);
}
var fileEdits = <FileEditInformation>[];
for (var edit in edits) {
var path = edit.file;
final fileResult = context.session.getFile(path);
if (fileResult is! FileResult) {
return ErrorOr.error(ResponseError(
code: ServerErrorCodes.FileAnalysisFailed,
message: 'Could not access "$path".'));
}
final docIdentifier = server.getVersionedDocumentIdentifier(path);
fileEdits.add(FileEditInformation(
docIdentifier, fileResult.lineInfo, edit.edits));
}
final workspaceEdit = toWorkspaceEdit(clientCapabilities, fileEdits);
return sendWorkspaceEditToClient(workspaceEdit);
});
}
}

View file

@ -7,14 +7,16 @@ import 'package:analysis_server/src/lsp/constants.dart';
import 'package:analysis_server/src/lsp/handlers/commands/fix_all.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/refactor_command_handler.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/commands/validate_refactor.dart';
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
import 'package:analysis_server/src/lsp/progress.dart';
import 'package:analysis_server/src/services/refactoring/framework/refactoring_processor.dart';
/// Handles workspace/executeCommand messages by delegating to a specific handler
/// based on the command.
/// Handles workspace/executeCommand messages by delegating to a specific
/// handler based on the command.
class ExecuteCommandHandler
extends MessageHandler<ExecuteCommandParams, Object?> {
final Map<String, CommandHandler> commandHandlers;
@ -27,6 +29,9 @@ class ExecuteCommandHandler
Commands.performRefactor: PerformRefactorCommandHandler(server),
Commands.validateRefactor: ValidateRefactorCommandHandler(server),
Commands.sendWorkspaceEdit: SendWorkspaceEditCommandHandler(server),
// Add commands for each of the refactorings.
for (var entry in RefactoringProcessor.generators.entries)
entry.key: RefactorCommandHandler(server, entry.key, entry.value),
};
@override

View file

@ -3,9 +3,12 @@
// BSD-style license that can be found in the LICENSE file.
import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';
import 'package:analysis_server/src/services/correction/change_workspace.dart';
import 'package:analysis_server/src/services/correction/util.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/session.dart';
import 'package:analyzer/src/dart/analysis/session_helper.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_workspace.dart';
/// The context in which a refactoring was requested.
class RefactoringContext {
@ -21,10 +24,16 @@ class RefactoringContext {
/// The number of selected characters.
final int selectionLength;
/// Utilities available to be used in the process of computing the edits.
late final CorrectionUtils utils = CorrectionUtils(resolvedResult);
/// The helper used to efficiently access resolved units.
late final AnalysisSessionHelper sessionHelper =
AnalysisSessionHelper(session);
/// The change workspace associated with this refactoring.
late final ChangeWorkspace workspace = DartChangeWorkspace([session]);
/// Initialize a newly created context based on the [resolvedResult].
RefactoringContext({
required this.server,

View file

@ -11,9 +11,9 @@ typedef ProducerGenerator = RefactoringProducer Function(RefactoringContext);
class RefactoringProcessor {
/// A list of the generators used to produce refactorings.
static const List<ProducerGenerator> generators = [
// MoveTopLevelToFile.new,
];
static const Map<String, ProducerGenerator> generators = {
// 'move_to_file': MoveTopLevelToFile.new,
};
/// The context in which the refactorings could be applied.
final RefactoringContext context;
@ -24,8 +24,8 @@ class RefactoringProcessor {
/// are available in the current context.
Future<List<CodeAction>> compute() async {
var refactorings = <CodeAction>[];
for (var i = 0; i < generators.length; i++) {
var generator = generators[i];
for (var entry in RefactoringProcessor.generators.entries) {
var generator = entry.value;
var producer = generator(context);
if (producer.isAvailable()) {
refactorings.add(
@ -33,13 +33,19 @@ class RefactoringProcessor {
title: producer.title,
kind: producer.kind,
command: Command(
command: producer.commandName,
command: entry.key,
title: producer.title,
arguments: [
{
'filePath': context.resolvedResult.path,
'selectionOffset': context.selectionOffset,
'selectionLength': context.selectionLength,
'arguments':
producer.parameters.map((param) => param.defaultValue),
}
],
),
data: {
'filePath': context.resolvedResult.path,
'selectionOffset': context.selectionOffset,
'selectionLength': context.selectionLength,
'parameters': producer.parameters,
}),
);