blob: a971c3760c47b93e7924c50e2038ef135151569d [file] [log] [blame]
// 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/lsp_protocol/protocol.dart' hide MessageType;
import 'package:analysis_server/src/analysis_server.dart' show MessageType;
import 'package:analysis_server/src/lsp/client_capabilities.dart';
import 'package:analysis_server/src/lsp/client_configuration.dart';
import 'package:analysis_server/src/lsp/constants.dart';
import 'package:analysis_server/src/lsp/error_or.dart';
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
import 'package:analysis_server/src/lsp/mapping.dart';
import 'package:analysis_server/src/lsp/registration/feature_registration.dart';
import 'package:analysis_server/src/services/refactoring/legacy/refactoring.dart';
import 'package:analysis_server/src/services/refactoring/legacy/rename_unit_member.dart';
import 'package:analysis_server/src/utilities/extensions/string.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/src/dart/ast/utilities.dart';
AstNode? _tweakLocatedNode(AstNode? node, int offset) {
if (node is RepresentationDeclaration) {
var extensionTypeDeclaration = node.parent;
if (extensionTypeDeclaration is ExtensionTypeDeclaration) {
if (extensionTypeDeclaration.name.end == offset) {
node = extensionTypeDeclaration;
}
}
}
return node;
}
typedef StaticOptions = Either2<bool, RenameOptions>;
class PrepareRenameHandler
extends
LspMessageHandler<
TextDocumentPositionParams,
TextDocumentPrepareRenameResult
> {
PrepareRenameHandler(super.server);
@override
Method get handlesMessage => Method.textDocument_prepareRename;
@override
LspJsonHandler<TextDocumentPositionParams> get jsonHandler =>
TextDocumentPositionParams.jsonHandler;
@override
Future<ErrorOr<TextDocumentPrepareRenameResult>> handle(
TextDocumentPositionParams params,
MessageInfo message,
CancellationToken token,
) async {
if (!isDartDocument(params.textDocument)) {
return success(null);
}
var pos = params.position;
var path = pathOfDoc(params.textDocument);
var unit = await path.mapResult(requireResolvedUnit);
var offset = unit.mapResultSync((unit) => toOffset(unit.lineInfo, pos));
return (unit, offset).mapResults((unit, offset) async {
var node = NodeLocator(offset).searchWithin(unit.unit);
node = _tweakLocatedNode(node, offset);
var element = server.getElementOfNode(node);
if (node == null || element == null) {
return success(null);
}
var refactorDetails = RenameRefactoring.getElementToRename(node, element);
if (refactorDetails == null) {
return success(null);
}
var refactoring = RenameRefactoring.create(
server.refactoringWorkspace,
unit,
refactorDetails.element,
);
if (refactoring == null) {
return success(null);
}
// Check the rename is valid here.
var initStatus = await refactoring.checkInitialConditions();
if (initStatus.hasFatalError) {
return error(
ServerErrorCodes.RenameNotValid,
initStatus.problem!.message,
);
}
return success(
TextDocumentPrepareRenameResult.t1(
PrepareRenamePlaceholder(
range: toRange(
unit.lineInfo,
// If the offset is set to -1 it means there is no location for the
// old name. However since we must provide a range for LSP, we'll use
// a 0-character span at the originally requested location to ensure
// it's valid.
refactorDetails.offset == -1 ? offset : refactorDetails.offset,
refactorDetails.length,
),
placeholder: refactoring.oldName,
),
),
);
});
}
}
class RenameHandler extends LspMessageHandler<RenameParams, WorkspaceEdit?> {
RenameHandler(super.server);
LspGlobalClientConfiguration get config =>
server.lspClientConfiguration.global;
@override
Method get handlesMessage => Method.textDocument_rename;
@override
LspJsonHandler<RenameParams> get jsonHandler => RenameParams.jsonHandler;
@override
Future<ErrorOr<WorkspaceEdit?>> handle(
RenameParams params,
MessageInfo message,
CancellationToken token,
) async {
if (!isDartDocument(params.textDocument)) {
return success(null);
}
var clientCapabilities = message.clientCapabilities;
if (clientCapabilities == null) {
return serverNotInitializedError;
}
var pos = params.position;
var textDocument = params.textDocument;
var path = pathOfDoc(params.textDocument);
// Capture the document version so we can verify it hasn't changed after
// we've computed the rename.
var docIdentifier = path.mapResultSync(
(path) => success(extractDocumentVersion(textDocument, path)),
);
var unit = await path.mapResult(requireResolvedUnit);
var offset = unit.mapResultSync((unit) => toOffset(unit.lineInfo, pos));
return (path, docIdentifier, unit, offset).mapResults((
path,
docIdentifier,
unit,
offset,
) async {
var node = NodeLocator(offset).searchWithin(unit.unit);
node = _tweakLocatedNode(node, offset);
var element = server.getElementOfNode(node);
if (node == null || element == null) {
return success(null);
}
var refactorDetails = RenameRefactoring.getElementToRename(node, element);
if (refactorDetails == null) {
return success(null);
}
var refactoring = RenameRefactoring.create(
server.refactoringWorkspace,
unit,
refactorDetails.element,
);
if (refactoring == null) {
return success(null);
}
// Check the rename is valid here.
var initStatus = await refactoring.checkInitialConditions();
if (token.isCancellationRequested) {
return cancelled(token);
}
if (initStatus.hasFatalError) {
return error(
ServerErrorCodes.RenameNotValid,
initStatus.problem!.message,
);
}
// Check the name is valid.
refactoring.newName = params.newName;
var optionsStatus = refactoring.checkNewName();
if (optionsStatus.hasError) {
return error(
ServerErrorCodes.RenameNotValid,
optionsStatus.problem!.message,
);
}
// Final validation.
var finalStatus = await refactoring.checkFinalConditions();
if (token.isCancellationRequested) {
return cancelled(token);
}
if (finalStatus.hasFatalError) {
return error(
ServerErrorCodes.RenameNotValid,
finalStatus.problem!.message,
);
} else if (finalStatus.hasError || finalStatus.hasWarning) {
var prompt = server.userPromptSender;
// If this change would produce errors but we can't prompt the user,
// just fail with the message.
if (prompt == null) {
return error(ServerErrorCodes.RenameNotValid, finalStatus.message!);
}
// Set the completer to complete to show that request is paused, and
// that processing of incoming messages can continue while we wait
// for the user's response.
message.completer?.complete();
// Otherwise, ask the user whether to proceed with the rename.
var userChoice = await prompt(
MessageType.warning,
finalStatus.message!,
[UserPromptActions.renameAnyway, UserPromptActions.cancel],
);
if (token.isCancellationRequested) {
return cancelled(token);
}
if (userChoice != UserPromptActions.renameAnyway) {
// Return an empty workspace edit response so we do not perform any
// rename, but also so we do not cause the client to show the user an
// error after they clicked cancel.
return success(emptyWorkspaceEdit);
}
}
// Compute the actual change.
// Don't include potential edits while we don't have a way for the user
// to opt-in/out.
// https://github.com/Dart-Code/Dart-Code/issues/4131.
// TODO(dantup): Check whether LSP's annotated edits would allow us to
// send potential edits in their own group that can be easily toggled by
// the user.
refactoring.includePotential = false;
var change = await refactoring.createChange();
if (token.isCancellationRequested) {
return cancelled(token);
}
// Before we send anything back, ensure the original file didn't change
// while we were computing changes.
if (fileHasBeenModified(path, docIdentifier.version)) {
return fileModifiedError;
}
var workspaceEdit = createWorkspaceEdit(
server,
clientCapabilities,
change,
);
// Check whether we should handle renaming the file to match the class.
if (_clientSupportsRename(clientCapabilities) &&
_isClassRename(refactoring)) {
// The rename must always be performed on the file that defines the
// class which is not necessarily the one where the rename was invoked.
var declaringFile =
(refactoring as RenameUnitMemberRefactoringImpl)
.element
.declaration
?.source
?.fullName;
if (declaringFile != null) {
var folder = pathContext.dirname(declaringFile);
var actualFilename = pathContext.basename(declaringFile);
var oldComputedFilename = refactoring.oldName.toFileName;
var newFilename = params.newName.toFileName;
// Only if the existing filename matches exactly what we'd expect for
// the original class name will we consider renaming.
if (actualFilename == oldComputedFilename) {
var renameConfig = config.renameFilesWithClasses;
var shouldRename =
renameConfig == 'always' ||
(renameConfig == 'prompt' &&
await _promptToRenameFile(actualFilename, newFilename));
if (shouldRename) {
var newPath = pathContext.join(folder, newFilename);
var renameEdit = createRenameEdit(
uriConverter,
declaringFile,
newPath,
);
workspaceEdit = mergeWorkspaceEdits([workspaceEdit, renameEdit]);
}
}
}
}
return success(workspaceEdit);
});
}
/// Checks whether the client supports Rename resource operations.
bool _clientSupportsRename(LspClientCapabilities clientCapabilities) {
return clientCapabilities.documentChanges &&
clientCapabilities.renameResourceOperations;
}
bool _isClassRename(RenameRefactoring refactoring) =>
refactoring is RenameUnitMemberRefactoringImpl &&
refactoring.element is InterfaceElement;
/// Asks the user whether they would like to rename the file along with the
/// class.
Future<bool> _promptToRenameFile(
String oldFilename,
String newFilename,
) async {
var prompt = server.userPromptSender;
// If we can't prompt, do the same as if they said no.
if (prompt == null) {
return false;
}
var userChoice = await prompt(
MessageType.info,
"Rename '$oldFilename' to '$newFilename'?",
[UserPromptActions.yes, UserPromptActions.no],
);
return userChoice == UserPromptActions.yes;
}
}
class RenameRegistrations extends FeatureRegistration
with SingleDynamicRegistration, StaticRegistration<StaticOptions> {
RenameRegistrations(super.info);
@override
ToJsonable? get options => RenameRegistrationOptions(
documentSelector: fullySupportedTypes,
prepareProvider: true,
);
@override
Method get registrationMethod => Method.textDocument_rename;
@override
StaticOptions get staticOptions =>
clientCapabilities.renameValidation
? Either2<bool, RenameOptions>.t2(
RenameOptions(prepareProvider: true),
)
: Either2<bool, RenameOptions>.t1(true);
@override
bool get supportsDynamic => clientDynamic.rename;
}