Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/rename/browser/rename.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { alert } from '../../../../base/browser/ui/aria/aria.js';
7
import { raceCancellation } from '../../../../base/common/async.js';
8
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
9
import { CancellationError, onUnexpectedError } from '../../../../base/common/errors.js';
10
import { isMarkdownString } from '../../../../base/common/htmlContent.js';
11
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
12
import { DisposableStore } from '../../../../base/common/lifecycle.js';
13
import { assertType } from '../../../../base/common/types.js';
14
import { URI } from '../../../../base/common/uri.js';
15
import * as nls from '../../../../nls.js';
16
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
17
import { ConfigurationScope, Extensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js';
18
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
19
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
20
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
21
import { ILogService } from '../../../../platform/log/common/log.js';
22
import { INotificationService } from '../../../../platform/notification/common/notification.js';
23
import { IEditorProgressService } from '../../../../platform/progress/common/progress.js';
24
import { Registry } from '../../../../platform/registry/common/platform.js';
25
import { ICodeEditor } from '../../../browser/editorBrowser.js';
26
import { EditorAction, EditorCommand, EditorContributionInstantiation, ServicesAccessor, registerEditorAction, registerEditorCommand, registerEditorContribution, registerModelAndPositionCommand } from '../../../browser/editorExtensions.js';
27
import { IBulkEditService } from '../../../browser/services/bulkEditService.js';
28
import { ICodeEditorService } from '../../../browser/services/codeEditorService.js';
29
import { IPosition, Position } from '../../../common/core/position.js';
30
import { Range } from '../../../common/core/range.js';
31
import { IEditorContribution } from '../../../common/editorCommon.js';
32
import { EditorContextKeys } from '../../../common/editorContextKeys.js';
33
import { LanguageFeatureRegistry } from '../../../common/languageFeatureRegistry.js';
34
import { NewSymbolNameTriggerKind, Rejection, RenameLocation, RenameProvider, WorkspaceEdit } from '../../../common/languages.js';
35
import { ITextModel } from '../../../common/model.js';
36
import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';
37
import { ITextResourceConfigurationService } from '../../../common/services/textResourceConfiguration.js';
38
import { EditSources } from '../../../common/textModelEditSource.js';
39
import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from '../../editorState/browser/editorState.js';
40
import { MessageController } from '../../message/browser/messageController.js';
41
import { CONTEXT_RENAME_INPUT_VISIBLE, RenameWidget } from './renameWidget.js';
42
43
class RenameSkeleton {
44
45
private readonly _providers: RenameProvider[];
46
private _providerRenameIdx: number = 0;
47
48
constructor(
49
private readonly model: ITextModel,
50
private readonly position: Position,
51
registry: LanguageFeatureRegistry<RenameProvider>
52
) {
53
this._providers = registry.ordered(model);
54
}
55
56
hasProvider() {
57
return this._providers.length > 0;
58
}
59
60
async resolveRenameLocation(token: CancellationToken): Promise<RenameLocation & Rejection | undefined> {
61
62
const rejects: string[] = [];
63
64
for (this._providerRenameIdx = 0; this._providerRenameIdx < this._providers.length; this._providerRenameIdx++) {
65
const provider = this._providers[this._providerRenameIdx];
66
if (!provider.resolveRenameLocation) {
67
break;
68
}
69
const res = await provider.resolveRenameLocation(this.model, this.position, token);
70
if (!res) {
71
continue;
72
}
73
if (res.rejectReason) {
74
rejects.push(res.rejectReason);
75
continue;
76
}
77
return res;
78
}
79
80
// we are here when no provider prepared a location which means we can
81
// just rely on the word under cursor and start with the first provider
82
this._providerRenameIdx = 0;
83
84
const word = this.model.getWordAtPosition(this.position);
85
if (!word) {
86
return {
87
range: Range.fromPositions(this.position),
88
text: '',
89
rejectReason: rejects.length > 0 ? rejects.join('\n') : undefined
90
};
91
}
92
return {
93
range: new Range(this.position.lineNumber, word.startColumn, this.position.lineNumber, word.endColumn),
94
text: word.word,
95
rejectReason: rejects.length > 0 ? rejects.join('\n') : undefined
96
};
97
}
98
99
async provideRenameEdits(newName: string, token: CancellationToken): Promise<WorkspaceEdit & Rejection> {
100
return this._provideRenameEdits(newName, this._providerRenameIdx, [], token);
101
}
102
103
private async _provideRenameEdits(newName: string, i: number, rejects: string[], token: CancellationToken): Promise<WorkspaceEdit & Rejection> {
104
const provider = this._providers[i];
105
if (!provider) {
106
return {
107
edits: [],
108
rejectReason: rejects.join('\n')
109
};
110
}
111
112
const result = await provider.provideRenameEdits(this.model, this.position, newName, token);
113
if (!result) {
114
return this._provideRenameEdits(newName, i + 1, rejects.concat(nls.localize('no result', "No result.")), token);
115
} else if (result.rejectReason) {
116
return this._provideRenameEdits(newName, i + 1, rejects.concat(result.rejectReason), token);
117
}
118
return result;
119
}
120
}
121
122
export async function rename(registry: LanguageFeatureRegistry<RenameProvider>, model: ITextModel, position: Position, newName: string): Promise<WorkspaceEdit & Rejection> {
123
const skeleton = new RenameSkeleton(model, position, registry);
124
const loc = await skeleton.resolveRenameLocation(CancellationToken.None);
125
if (loc?.rejectReason) {
126
return { edits: [], rejectReason: loc.rejectReason };
127
}
128
return skeleton.provideRenameEdits(newName, CancellationToken.None);
129
}
130
131
// --- register actions and commands
132
133
class RenameController implements IEditorContribution {
134
135
public static readonly ID = 'editor.contrib.renameController';
136
137
static get(editor: ICodeEditor): RenameController | null {
138
return editor.getContribution<RenameController>(RenameController.ID);
139
}
140
141
private readonly _renameWidget: RenameWidget;
142
private readonly _disposableStore = new DisposableStore();
143
private _cts: CancellationTokenSource = new CancellationTokenSource();
144
145
constructor(
146
private readonly editor: ICodeEditor,
147
@IInstantiationService private readonly _instaService: IInstantiationService,
148
@INotificationService private readonly _notificationService: INotificationService,
149
@IBulkEditService private readonly _bulkEditService: IBulkEditService,
150
@IEditorProgressService private readonly _progressService: IEditorProgressService,
151
@ILogService private readonly _logService: ILogService,
152
@ITextResourceConfigurationService private readonly _configService: ITextResourceConfigurationService,
153
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
154
) {
155
this._renameWidget = this._disposableStore.add(this._instaService.createInstance(RenameWidget, this.editor, ['acceptRenameInput', 'acceptRenameInputWithPreview']));
156
}
157
158
dispose(): void {
159
this._disposableStore.dispose();
160
this._cts.dispose(true);
161
}
162
163
async run(): Promise<void> {
164
165
const trace = this._logService.trace.bind(this._logService, '[rename]');
166
167
// set up cancellation token to prevent reentrant rename, this
168
// is the parent to the resolve- and rename-tokens
169
this._cts.dispose(true);
170
this._cts = new CancellationTokenSource();
171
172
if (!this.editor.hasModel()) {
173
trace('editor has no model');
174
return undefined;
175
}
176
177
const position = this.editor.getPosition();
178
const skeleton = new RenameSkeleton(this.editor.getModel(), position, this._languageFeaturesService.renameProvider);
179
180
if (!skeleton.hasProvider()) {
181
trace('skeleton has no provider');
182
return undefined;
183
}
184
185
// part 1 - resolve rename location
186
const cts1 = new EditorStateCancellationTokenSource(this.editor, CodeEditorStateFlag.Position | CodeEditorStateFlag.Value, undefined, this._cts.token);
187
188
let loc: RenameLocation & Rejection | undefined;
189
try {
190
trace('resolving rename location');
191
const resolveLocationOperation = skeleton.resolveRenameLocation(cts1.token);
192
this._progressService.showWhile(resolveLocationOperation, 250);
193
loc = await resolveLocationOperation;
194
trace('resolved rename location');
195
} catch (e: unknown) {
196
if (e instanceof CancellationError) {
197
trace('resolve rename location cancelled', JSON.stringify(e, null, '\t'));
198
} else {
199
trace('resolve rename location failed', e instanceof Error ? e : JSON.stringify(e, null, '\t'));
200
if (typeof e === 'string' || isMarkdownString(e)) {
201
MessageController.get(this.editor)?.showMessage(e || nls.localize('resolveRenameLocationFailed', "An unknown error occurred while resolving rename location"), position);
202
}
203
}
204
return undefined;
205
206
} finally {
207
cts1.dispose();
208
}
209
210
if (!loc) {
211
trace('returning early - no loc');
212
return undefined;
213
}
214
215
if (loc.rejectReason) {
216
trace(`returning early - rejected with reason: ${loc.rejectReason}`, loc.rejectReason);
217
MessageController.get(this.editor)?.showMessage(loc.rejectReason, position);
218
return undefined;
219
}
220
221
if (cts1.token.isCancellationRequested) {
222
trace('returning early - cts1 cancelled');
223
return undefined;
224
}
225
226
// part 2 - do rename at location
227
const cts2 = new EditorStateCancellationTokenSource(this.editor, CodeEditorStateFlag.Position | CodeEditorStateFlag.Value, loc.range, this._cts.token);
228
229
const model = this.editor.getModel(); // @ulugbekna: assumes editor still has a model, otherwise, cts1 should've been cancelled
230
231
const newSymbolNamesProviders = this._languageFeaturesService.newSymbolNamesProvider.all(model);
232
233
const resolvedNewSymbolnamesProviders = await Promise.all(newSymbolNamesProviders.map(async p => [p, await p.supportsAutomaticNewSymbolNamesTriggerKind ?? false] as const));
234
235
const requestRenameSuggestions = (triggerKind: NewSymbolNameTriggerKind, cts: CancellationToken) => {
236
let providers = resolvedNewSymbolnamesProviders.slice();
237
238
if (triggerKind === NewSymbolNameTriggerKind.Automatic) {
239
providers = providers.filter(([_, supportsAutomatic]) => supportsAutomatic);
240
}
241
242
return providers.map(([p,]) => p.provideNewSymbolNames(model, loc.range, triggerKind, cts));
243
};
244
245
trace('creating rename input field and awaiting its result');
246
const supportPreview = this._bulkEditService.hasPreviewHandler() && this._configService.getValue<boolean>(this.editor.getModel().uri, 'editor.rename.enablePreview');
247
const inputFieldResult = await this._renameWidget.getInput(
248
loc.range,
249
loc.text,
250
supportPreview,
251
newSymbolNamesProviders.length > 0 ? requestRenameSuggestions : undefined,
252
cts2
253
);
254
trace('received response from rename input field');
255
256
// no result, only hint to focus the editor or not
257
if (typeof inputFieldResult === 'boolean') {
258
trace(`returning early - rename input field response - ${inputFieldResult}`);
259
if (inputFieldResult) {
260
this.editor.focus();
261
}
262
cts2.dispose();
263
return undefined;
264
}
265
266
this.editor.focus();
267
268
trace('requesting rename edits');
269
const renameOperation = raceCancellation(skeleton.provideRenameEdits(inputFieldResult.newName, cts2.token), cts2.token).then(async renameResult => {
270
271
if (!renameResult) {
272
trace('returning early - no rename edits result');
273
return;
274
}
275
if (!this.editor.hasModel()) {
276
trace('returning early - no model after rename edits are provided');
277
return;
278
}
279
280
if (renameResult.rejectReason) {
281
trace(`returning early - rejected with reason: ${renameResult.rejectReason}`);
282
this._notificationService.info(renameResult.rejectReason);
283
return;
284
}
285
286
// collapse selection to active end
287
this.editor.setSelection(Range.fromPositions(this.editor.getSelection().getPosition()));
288
289
trace('applying edits');
290
291
this._bulkEditService.apply(renameResult, {
292
editor: this.editor,
293
showPreview: inputFieldResult.wantsPreview,
294
label: nls.localize('label', "Renaming '{0}' to '{1}'", loc?.text, inputFieldResult.newName),
295
code: 'undoredo.rename',
296
quotableLabel: nls.localize('quotableLabel', "Renaming {0} to {1}", loc?.text, inputFieldResult.newName),
297
respectAutoSaveConfig: true,
298
reason: EditSources.rename(),
299
}).then(result => {
300
trace('edits applied');
301
if (result.ariaSummary) {
302
alert(nls.localize('aria', "Successfully renamed '{0}' to '{1}'. Summary: {2}", loc.text, inputFieldResult.newName, result.ariaSummary));
303
}
304
}).catch(err => {
305
trace(`error when applying edits ${JSON.stringify(err, null, '\t')}`);
306
this._notificationService.error(nls.localize('rename.failedApply', "Rename failed to apply edits"));
307
this._logService.error(err);
308
});
309
310
}, err => {
311
trace('error when providing rename edits', JSON.stringify(err, null, '\t'));
312
313
this._notificationService.error(nls.localize('rename.failed', "Rename failed to compute edits"));
314
this._logService.error(err);
315
316
}).finally(() => {
317
cts2.dispose();
318
});
319
320
trace('returning rename operation');
321
322
this._progressService.showWhile(renameOperation, 250);
323
return renameOperation;
324
325
}
326
327
acceptRenameInput(wantsPreview: boolean): void {
328
this._renameWidget.acceptInput(wantsPreview);
329
}
330
331
cancelRenameInput(): void {
332
this._renameWidget.cancelInput(true, 'cancelRenameInput command');
333
}
334
335
focusNextRenameSuggestion(): void {
336
this._renameWidget.focusNextRenameSuggestion();
337
}
338
339
focusPreviousRenameSuggestion(): void {
340
this._renameWidget.focusPreviousRenameSuggestion();
341
}
342
}
343
344
// ---- action implementation
345
346
export class RenameAction extends EditorAction {
347
348
constructor() {
349
super({
350
id: 'editor.action.rename',
351
label: nls.localize2('rename.label', "Rename Symbol"),
352
precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasRenameProvider),
353
kbOpts: {
354
kbExpr: EditorContextKeys.editorTextFocus,
355
primary: KeyCode.F2,
356
weight: KeybindingWeight.EditorContrib
357
},
358
contextMenuOpts: {
359
group: '1_modification',
360
order: 1.1
361
}
362
});
363
}
364
365
override runCommand(accessor: ServicesAccessor, args: [URI, IPosition]): void | Promise<void> {
366
const editorService = accessor.get(ICodeEditorService);
367
const [uri, pos] = Array.isArray(args) && args || [undefined, undefined];
368
369
if (URI.isUri(uri) && Position.isIPosition(pos)) {
370
return editorService.openCodeEditor({ resource: uri }, editorService.getActiveCodeEditor()).then(editor => {
371
if (!editor) {
372
return;
373
}
374
editor.setPosition(pos);
375
editor.invokeWithinContext(accessor => {
376
this.reportTelemetry(accessor, editor);
377
return this.run(accessor, editor);
378
});
379
}, onUnexpectedError);
380
}
381
382
return super.runCommand(accessor, args);
383
}
384
385
run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
386
const logService = accessor.get(ILogService);
387
388
const controller = RenameController.get(editor);
389
390
if (controller) {
391
logService.trace('[RenameAction] got controller, running...');
392
return controller.run();
393
}
394
logService.trace('[RenameAction] returning early - controller missing');
395
return Promise.resolve();
396
}
397
}
398
399
registerEditorContribution(RenameController.ID, RenameController, EditorContributionInstantiation.Lazy);
400
registerEditorAction(RenameAction);
401
402
const RenameCommand = EditorCommand.bindToContribution<RenameController>(RenameController.get);
403
404
registerEditorCommand(new RenameCommand({
405
id: 'acceptRenameInput',
406
precondition: CONTEXT_RENAME_INPUT_VISIBLE,
407
handler: x => x.acceptRenameInput(false),
408
kbOpts: {
409
weight: KeybindingWeight.EditorContrib + 99,
410
kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, ContextKeyExpr.not('isComposing')),
411
primary: KeyCode.Enter
412
}
413
}));
414
415
registerEditorCommand(new RenameCommand({
416
id: 'acceptRenameInputWithPreview',
417
precondition: ContextKeyExpr.and(CONTEXT_RENAME_INPUT_VISIBLE, ContextKeyExpr.has('config.editor.rename.enablePreview')),
418
handler: x => x.acceptRenameInput(true),
419
kbOpts: {
420
weight: KeybindingWeight.EditorContrib + 99,
421
kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, ContextKeyExpr.not('isComposing')),
422
primary: KeyMod.CtrlCmd + KeyCode.Enter
423
}
424
}));
425
426
registerEditorCommand(new RenameCommand({
427
id: 'cancelRenameInput',
428
precondition: CONTEXT_RENAME_INPUT_VISIBLE,
429
handler: x => x.cancelRenameInput(),
430
kbOpts: {
431
weight: KeybindingWeight.EditorContrib + 99,
432
kbExpr: EditorContextKeys.focus,
433
primary: KeyCode.Escape,
434
secondary: [KeyMod.Shift | KeyCode.Escape]
435
}
436
}));
437
438
registerAction2(class FocusNextRenameSuggestion extends Action2 {
439
constructor() {
440
super({
441
id: 'focusNextRenameSuggestion',
442
title: {
443
...nls.localize2('focusNextRenameSuggestion', "Focus Next Rename Suggestion"),
444
},
445
precondition: CONTEXT_RENAME_INPUT_VISIBLE,
446
keybinding: [
447
{
448
primary: KeyCode.DownArrow,
449
weight: KeybindingWeight.EditorContrib + 99,
450
}
451
]
452
});
453
}
454
455
override run(accessor: ServicesAccessor): void {
456
const currentEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor();
457
if (!currentEditor) { return; }
458
459
const controller = RenameController.get(currentEditor);
460
if (!controller) { return; }
461
462
controller.focusNextRenameSuggestion();
463
}
464
});
465
466
registerAction2(class FocusPreviousRenameSuggestion extends Action2 {
467
constructor() {
468
super({
469
id: 'focusPreviousRenameSuggestion',
470
title: {
471
...nls.localize2('focusPreviousRenameSuggestion', "Focus Previous Rename Suggestion"),
472
},
473
precondition: CONTEXT_RENAME_INPUT_VISIBLE,
474
keybinding: [
475
{
476
primary: KeyCode.UpArrow,
477
weight: KeybindingWeight.EditorContrib + 99,
478
}
479
]
480
});
481
}
482
483
override run(accessor: ServicesAccessor): void {
484
const currentEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor();
485
if (!currentEditor) { return; }
486
487
const controller = RenameController.get(currentEditor);
488
if (!controller) { return; }
489
490
controller.focusPreviousRenameSuggestion();
491
}
492
});
493
494
// ---- api bridge command
495
496
registerModelAndPositionCommand('_executeDocumentRenameProvider', function (accessor, model, position, ...args) {
497
const [newName] = args;
498
assertType(typeof newName === 'string');
499
const { renameProvider } = accessor.get(ILanguageFeaturesService);
500
return rename(renameProvider, model, position, newName);
501
});
502
503
registerModelAndPositionCommand('_executePrepareRename', async function (accessor, model, position) {
504
const { renameProvider } = accessor.get(ILanguageFeaturesService);
505
const skeleton = new RenameSkeleton(model, position, renameProvider);
506
const loc = await skeleton.resolveRenameLocation(CancellationToken.None);
507
if (loc?.rejectReason) {
508
throw new Error(loc.rejectReason);
509
}
510
return loc;
511
});
512
513
514
//todo@jrieken use editor options world
515
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
516
id: 'editor',
517
properties: {
518
'editor.rename.enablePreview': {
519
scope: ConfigurationScope.LANGUAGE_OVERRIDABLE,
520
description: nls.localize('enablePreview', "Enable/disable the ability to preview changes before renaming"),
521
default: true,
522
type: 'boolean'
523
}
524
}
525
});
526
527