-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
Copy pathhandler_rename.dart
274 lines (237 loc) · 10.4 KB
/
handler_rename.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
// 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_generated.dart';
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
import 'package:analysis_server/src/lsp/client_configuration.dart';
import 'package:analysis_server/src/lsp/constants.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/services/refactoring/refactoring.dart';
import 'package:analysis_server/src/services/refactoring/rename_unit_member.dart';
import 'package:analyzer/dart/element/element.dart';
class PrepareRenameHandler
extends MessageHandler<TextDocumentPositionParams, RangeAndPlaceholder?> {
PrepareRenameHandler(LspAnalysisServer server) : super(server);
@override
Method get handlesMessage => Method.textDocument_prepareRename;
@override
LspJsonHandler<TextDocumentPositionParams> get jsonHandler =>
TextDocumentPositionParams.jsonHandler;
@override
Future<ErrorOr<RangeAndPlaceholder?>> handle(
TextDocumentPositionParams params, CancellationToken token) async {
if (!isDartDocument(params.textDocument)) {
return success(null);
}
final pos = params.position;
final path = pathOfDoc(params.textDocument);
final unit = await path.mapResult(requireResolvedUnit);
final offset = await unit.mapResult((unit) => toOffset(unit.lineInfo, pos));
return offset.mapResult((offset) async {
final node = await server.getNodeAtOffset(path.result, offset);
final element = server.getElementOfNode(node);
if (node == null || element == null) {
return success(null);
}
final refactorDetails =
RenameRefactoring.getElementToRename(node, element);
if (refactorDetails == null) {
return success(null);
}
final refactoring = RenameRefactoring.create(
server.refactoringWorkspace, unit.result, refactorDetails.element);
if (refactoring == null) {
return success(null);
}
// Check the rename is valid here.
final initStatus = await refactoring.checkInitialConditions();
if (initStatus.hasFatalError) {
return error(
ServerErrorCodes.RenameNotValid, initStatus.problem!.message, null);
}
return success(RangeAndPlaceholder(
range: toRange(
unit.result.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 MessageHandler<RenameParams, WorkspaceEdit?> {
final _upperCasePattern = RegExp('[A-Z]');
RenameHandler(LspAnalysisServer server) : super(server);
LspGlobalClientConfiguration get config => server.clientConfiguration.global;
@override
Method get handlesMessage => Method.textDocument_rename;
@override
LspJsonHandler<RenameParams> get jsonHandler => RenameParams.jsonHandler;
/// Checks whether a client supports Rename resource operations.
bool get _clientSupportsRename {
final capabilities = server.clientCapabilities;
return (capabilities?.documentChanges ?? false) &&
(capabilities?.renameResourceOperations ?? false);
}
@override
Future<ErrorOr<WorkspaceEdit?>> handle(
RenameParams params, CancellationToken token) async {
if (!isDartDocument(params.textDocument)) {
return success(null);
}
final pos = params.position;
final textDocument = params.textDocument;
final path = pathOfDoc(params.textDocument);
// If the client provided us a version doc identifier, we'll use it to ensure
// we're not computing a rename for an old document. If not, we'll just assume
// the version the server had at the time of recieving the request is valid
// and then use it to verify the document hadn't changed again before we
// send the edits.
final docIdentifier = await path.mapResult((path) => success(
textDocument is OptionalVersionedTextDocumentIdentifier
? textDocument
: textDocument is VersionedTextDocumentIdentifier
? OptionalVersionedTextDocumentIdentifier(
uri: textDocument.uri, version: textDocument.version)
: server.getVersionedDocumentIdentifier(path)));
final unit = await path.mapResult(requireResolvedUnit);
final offset = await unit.mapResult((unit) => toOffset(unit.lineInfo, pos));
return offset.mapResult((offset) async {
final node = await server.getNodeAtOffset(path.result, offset);
final element = server.getElementOfNode(node);
if (node == null || element == null) {
return success(null);
}
final refactorDetails =
RenameRefactoring.getElementToRename(node, element);
if (refactorDetails == null) {
return success(null);
}
final refactoring = RenameRefactoring.create(
server.refactoringWorkspace, unit.result, refactorDetails.element);
if (refactoring == null) {
return success(null);
}
// Check the rename is valid here.
final initStatus = await refactoring.checkInitialConditions();
if (token.isCancellationRequested) {
return cancelled();
}
if (initStatus.hasFatalError) {
return error(
ServerErrorCodes.RenameNotValid, initStatus.problem!.message, null);
}
// Check the name is valid.
refactoring.newName = params.newName;
final optionsStatus = refactoring.checkNewName();
if (optionsStatus.hasError) {
return error(ServerErrorCodes.RenameNotValid,
optionsStatus.problem!.message, null);
}
// Final validation.
final finalStatus = await refactoring.checkFinalConditions();
if (token.isCancellationRequested) {
return cancelled();
}
if (finalStatus.hasFatalError) {
return error(ServerErrorCodes.RenameNotValid,
finalStatus.problem!.message, null);
} else if (finalStatus.hasError || finalStatus.hasWarning) {
// Ask the user whether to proceed with the rename.
final userChoice = await server.showUserPrompt(
MessageType.Warning,
finalStatus.message!,
[
MessageActionItem(title: UserPromptActions.renameAnyway),
MessageActionItem(title: UserPromptActions.cancel),
],
);
if (token.isCancellationRequested) {
return cancelled();
}
if (userChoice.title != 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.
final change = await refactoring.createChange();
if (token.isCancellationRequested) {
return cancelled();
}
// Before we send anything back, ensure the original file didn't change
// while we were computing changes.
if (fileHasBeenModified(path.result, docIdentifier.result.version)) {
return fileModifiedError;
}
var workspaceEdit = createWorkspaceEdit(server, change);
// Check whether we should handle renaming the file to match the class.
if (_clientSupportsRename && _isClassRename(refactoring)) {
final pathContext = server.resourceProvider.pathContext;
// The rename must always be performed on the file that defines the
// class which is not necessarily the one where the rename was invoked.
final declaringFile = (refactoring as RenameUnitMemberRefactoringImpl)
.element
.declaration
?.source
?.fullName;
if (declaringFile != null) {
final folder = pathContext.dirname(declaringFile);
final actualFilename = pathContext.basename(declaringFile);
final oldComputedFilename =
_fileNameForClassName(refactoring.oldName);
final newFilename = _fileNameForClassName(params.newName);
// Only if the existing filename matches exactly what we'd expect for
// the original class name will we consider renaming.
if (actualFilename == oldComputedFilename) {
final renameConfig = config.renameFilesWithClasses;
final shouldRename = renameConfig == 'always' ||
(renameConfig == 'prompt' &&
await _promptToRenameFile(actualFilename, newFilename));
if (shouldRename) {
final newPath = pathContext.join(folder, newFilename);
final renameEdit = createRenameEdit(declaringFile, newPath);
workspaceEdit = mergeWorkspaceEdits([workspaceEdit, renameEdit]);
}
}
}
}
return success(workspaceEdit);
});
}
/// Computes a filename for a given class name (convert from PascalCase to
/// snake_case).
String _fileNameForClassName(String className) {
final fileName = className
.replaceAllMapped(_upperCasePattern,
(match) => match.start == 0 ? match[0]! : '_${match[0]}')
.toLowerCase();
return '$fileName.dart';
}
bool _isClassRename(RenameRefactoring refactoring) =>
refactoring is RenameUnitMemberRefactoringImpl &&
refactoring.element is ClassElement;
/// Asks the user whether they would like to rename the file along with the
/// class.
Future<bool> _promptToRenameFile(
String oldFilename, String newFilename) async {
final userChoice = await server.showUserPrompt(
MessageType.Info,
"Rename '$oldFilename' to '$newFilename'?",
[
MessageActionItem(title: UserPromptActions.yes),
MessageActionItem(title: UserPromptActions.no),
],
);
return userChoice.title == UserPromptActions.yes;
}
}