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
5333 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 function hasProvider(registry: LanguageFeatureRegistry<RenameProvider>, model: ITextModel): boolean {
123
const providers = registry.ordered(model);
124
return providers.length > 0;
125
}
126
127
export async function prepareRename(registry: LanguageFeatureRegistry<RenameProvider>, model: ITextModel, position: Position, cancellationToken?: CancellationToken): Promise<RenameLocation & Rejection | undefined> {
128
const skeleton = new RenameSkeleton(model, position, registry);
129
return skeleton.resolveRenameLocation(cancellationToken ?? CancellationToken.None);
130
}
131
132
export async function rawRename(registry: LanguageFeatureRegistry<RenameProvider>, model: ITextModel, position: Position, newName: string, cancellationToken?: CancellationToken): Promise<WorkspaceEdit & Rejection> {
133
const skeleton = new RenameSkeleton(model, position, registry);
134
return skeleton.provideRenameEdits(newName, cancellationToken ?? CancellationToken.None);
135
}
136
137
export async function rename(registry: LanguageFeatureRegistry<RenameProvider>, model: ITextModel, position: Position, newName: string): Promise<WorkspaceEdit & Rejection> {
138
const skeleton = new RenameSkeleton(model, position, registry);
139
const loc = await skeleton.resolveRenameLocation(CancellationToken.None);
140
if (loc?.rejectReason) {
141
return { edits: [], rejectReason: loc.rejectReason };
142
}
143
return skeleton.provideRenameEdits(newName, CancellationToken.None);
144
}
145
146
// --- register actions and commands
147
148
class RenameController implements IEditorContribution {
149
150
public static readonly ID = 'editor.contrib.renameController';
151
152
static get(editor: ICodeEditor): RenameController | null {
153
return editor.getContribution<RenameController>(RenameController.ID);
154
}
155
156
private readonly _renameWidget: RenameWidget;
157
private readonly _disposableStore = new DisposableStore();
158
private _cts: CancellationTokenSource = new CancellationTokenSource();
159
160
constructor(
161
private readonly editor: ICodeEditor,
162
@IInstantiationService private readonly _instaService: IInstantiationService,
163
@INotificationService private readonly _notificationService: INotificationService,
164
@IBulkEditService private readonly _bulkEditService: IBulkEditService,
165
@IEditorProgressService private readonly _progressService: IEditorProgressService,
166
@ILogService private readonly _logService: ILogService,
167
@ITextResourceConfigurationService private readonly _configService: ITextResourceConfigurationService,
168
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
169
) {
170
this._renameWidget = this._disposableStore.add(this._instaService.createInstance(RenameWidget, this.editor, ['acceptRenameInput', 'acceptRenameInputWithPreview']));
171
}
172
173
dispose(): void {
174
this._disposableStore.dispose();
175
this._cts.dispose(true);
176
}
177
178
async run(): Promise<void> {
179
180
const trace = this._logService.trace.bind(this._logService, '[rename]');
181
182
// set up cancellation token to prevent reentrant rename, this
183
// is the parent to the resolve- and rename-tokens
184
this._cts.dispose(true);
185
this._cts = new CancellationTokenSource();
186
187
if (!this.editor.hasModel()) {
188
trace('editor has no model');
189
return undefined;
190
}
191
192
const position = this.editor.getPosition();
193
const skeleton = new RenameSkeleton(this.editor.getModel(), position, this._languageFeaturesService.renameProvider);
194
195
if (!skeleton.hasProvider()) {
196
trace('skeleton has no provider');
197
return undefined;
198
}
199
200
// part 1 - resolve rename location
201
const cts1 = new EditorStateCancellationTokenSource(this.editor, CodeEditorStateFlag.Position | CodeEditorStateFlag.Value, undefined, this._cts.token);
202
203
let loc: RenameLocation & Rejection | undefined;
204
try {
205
trace('resolving rename location');
206
const resolveLocationOperation = skeleton.resolveRenameLocation(cts1.token);
207
this._progressService.showWhile(resolveLocationOperation, 250);
208
loc = await resolveLocationOperation;
209
trace('resolved rename location');
210
} catch (e: unknown) {
211
if (e instanceof CancellationError) {
212
trace('resolve rename location cancelled', JSON.stringify(e, null, '\t'));
213
} else {
214
trace('resolve rename location failed', e instanceof Error ? e : JSON.stringify(e, null, '\t'));
215
if (typeof e === 'string' || isMarkdownString(e)) {
216
MessageController.get(this.editor)?.showMessage(e || nls.localize('resolveRenameLocationFailed', "An unknown error occurred while resolving rename location"), position);
217
}
218
}
219
return undefined;
220
221
} finally {
222
cts1.dispose();
223
}
224
225
if (!loc) {
226
trace('returning early - no loc');
227
return undefined;
228
}
229
230
if (loc.rejectReason) {
231
trace(`returning early - rejected with reason: ${loc.rejectReason}`, loc.rejectReason);
232
MessageController.get(this.editor)?.showMessage(loc.rejectReason, position);
233
return undefined;
234
}
235
236
if (cts1.token.isCancellationRequested) {
237
trace('returning early - cts1 cancelled');
238
return undefined;
239
}
240
241
// part 2 - do rename at location
242
const cts2 = new EditorStateCancellationTokenSource(this.editor, CodeEditorStateFlag.Position | CodeEditorStateFlag.Value, loc.range, this._cts.token);
243
244
const model = this.editor.getModel(); // @ulugbekna: assumes editor still has a model, otherwise, cts1 should've been cancelled
245
246
const newSymbolNamesProviders = this._languageFeaturesService.newSymbolNamesProvider.all(model);
247
248
const resolvedNewSymbolnamesProviders = await Promise.all(newSymbolNamesProviders.map(async p => [p, await p.supportsAutomaticNewSymbolNamesTriggerKind ?? false] as const));
249
250
const requestRenameSuggestions = (triggerKind: NewSymbolNameTriggerKind, cts: CancellationToken) => {
251
let providers = resolvedNewSymbolnamesProviders.slice();
252
253
if (triggerKind === NewSymbolNameTriggerKind.Automatic) {
254
providers = providers.filter(([_, supportsAutomatic]) => supportsAutomatic);
255
}
256
257
return providers.map(([p,]) => p.provideNewSymbolNames(model, loc.range, triggerKind, cts));
258
};
259
260
trace('creating rename input field and awaiting its result');
261
const supportPreview = this._bulkEditService.hasPreviewHandler() && this._configService.getValue<boolean>(this.editor.getModel().uri, 'editor.rename.enablePreview');
262
const inputFieldResult = await this._renameWidget.getInput(
263
loc.range,
264
loc.text,
265
supportPreview,
266
newSymbolNamesProviders.length > 0 ? requestRenameSuggestions : undefined,
267
cts2
268
);
269
trace('received response from rename input field');
270
271
// no result, only hint to focus the editor or not
272
if (typeof inputFieldResult === 'boolean') {
273
trace(`returning early - rename input field response - ${inputFieldResult}`);
274
if (inputFieldResult) {
275
this.editor.focus();
276
}
277
cts2.dispose();
278
return undefined;
279
}
280
281
this.editor.focus();
282
283
trace('requesting rename edits');
284
const renameOperation = raceCancellation(skeleton.provideRenameEdits(inputFieldResult.newName, cts2.token), cts2.token).then(async renameResult => {
285
286
if (!renameResult) {
287
trace('returning early - no rename edits result');
288
return;
289
}
290
if (!this.editor.hasModel()) {
291
trace('returning early - no model after rename edits are provided');
292
return;
293
}
294
295
if (renameResult.rejectReason) {
296
trace(`returning early - rejected with reason: ${renameResult.rejectReason}`);
297
this._notificationService.info(renameResult.rejectReason);
298
return;
299
}
300
301
// collapse selection to active end
302
this.editor.setSelection(Range.fromPositions(this.editor.getSelection().getPosition()));
303
304
trace('applying edits');
305
306
this._bulkEditService.apply(renameResult, {
307
editor: this.editor,
308
showPreview: inputFieldResult.wantsPreview,
309
label: nls.localize('label', "Renaming '{0}' to '{1}'", loc?.text, inputFieldResult.newName),
310
code: 'undoredo.rename',
311
quotableLabel: nls.localize('quotableLabel', "Renaming {0} to {1}", loc?.text, inputFieldResult.newName),
312
respectAutoSaveConfig: true,
313
reason: EditSources.rename(loc?.text, inputFieldResult.newName),
314
}).then(result => {
315
trace('edits applied');
316
if (result.ariaSummary) {
317
alert(nls.localize('aria', "Successfully renamed '{0}' to '{1}'. Summary: {2}", loc.text, inputFieldResult.newName, result.ariaSummary));
318
}
319
}).catch(err => {
320
trace(`error when applying edits ${JSON.stringify(err, null, '\t')}`);
321
this._notificationService.error(nls.localize('rename.failedApply', "Rename failed to apply edits"));
322
this._logService.error(err);
323
});
324
325
}, err => {
326
trace('error when providing rename edits', JSON.stringify(err, null, '\t'));
327
328
this._notificationService.error(nls.localize('rename.failed', "Rename failed to compute edits"));
329
this._logService.error(err);
330
331
}).finally(() => {
332
cts2.dispose();
333
});
334
335
trace('returning rename operation');
336
337
this._progressService.showWhile(renameOperation, 250);
338
return renameOperation;
339
340
}
341
342
acceptRenameInput(wantsPreview: boolean): void {
343
this._renameWidget.acceptInput(wantsPreview);
344
}
345
346
cancelRenameInput(): void {
347
this._renameWidget.cancelInput(true, 'cancelRenameInput command');
348
}
349
350
focusNextRenameSuggestion(): void {
351
this._renameWidget.focusNextRenameSuggestion();
352
}
353
354
focusPreviousRenameSuggestion(): void {
355
this._renameWidget.focusPreviousRenameSuggestion();
356
}
357
}
358
359
// ---- action implementation
360
361
export class RenameAction extends EditorAction {
362
363
constructor() {
364
super({
365
id: 'editor.action.rename',
366
label: nls.localize2('rename.label', "Rename Symbol"),
367
precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasRenameProvider),
368
kbOpts: {
369
kbExpr: EditorContextKeys.editorTextFocus,
370
primary: KeyCode.F2,
371
weight: KeybindingWeight.EditorContrib
372
},
373
contextMenuOpts: {
374
group: '1_modification',
375
order: 1.1
376
},
377
canTriggerInlineEdits: true,
378
});
379
}
380
381
override runCommand(accessor: ServicesAccessor, args: [URI, IPosition]): void | Promise<void> {
382
const editorService = accessor.get(ICodeEditorService);
383
const [uri, pos] = Array.isArray(args) && args || [undefined, undefined];
384
385
if (URI.isUri(uri) && Position.isIPosition(pos)) {
386
return editorService.openCodeEditor({ resource: uri }, editorService.getActiveCodeEditor()).then(editor => {
387
if (!editor) {
388
return;
389
}
390
editor.setPosition(pos);
391
editor.invokeWithinContext(accessor => {
392
this.reportTelemetry(accessor, editor);
393
return this.run(accessor, editor);
394
});
395
}, onUnexpectedError);
396
}
397
398
return super.runCommand(accessor, args);
399
}
400
401
run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
402
const logService = accessor.get(ILogService);
403
404
const controller = RenameController.get(editor);
405
406
if (controller) {
407
logService.trace('[RenameAction] got controller, running...');
408
return controller.run();
409
}
410
logService.trace('[RenameAction] returning early - controller missing');
411
return Promise.resolve();
412
}
413
}
414
415
registerEditorContribution(RenameController.ID, RenameController, EditorContributionInstantiation.Lazy);
416
registerEditorAction(RenameAction);
417
418
const RenameCommand = EditorCommand.bindToContribution<RenameController>(RenameController.get);
419
420
registerEditorCommand(new RenameCommand({
421
id: 'acceptRenameInput',
422
precondition: CONTEXT_RENAME_INPUT_VISIBLE,
423
handler: x => x.acceptRenameInput(false),
424
kbOpts: {
425
weight: KeybindingWeight.EditorContrib + 99,
426
kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, ContextKeyExpr.not('isComposing')),
427
primary: KeyCode.Enter
428
}
429
}));
430
431
registerEditorCommand(new RenameCommand({
432
id: 'acceptRenameInputWithPreview',
433
precondition: ContextKeyExpr.and(CONTEXT_RENAME_INPUT_VISIBLE, ContextKeyExpr.has('config.editor.rename.enablePreview')),
434
handler: x => x.acceptRenameInput(true),
435
kbOpts: {
436
weight: KeybindingWeight.EditorContrib + 99,
437
kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, ContextKeyExpr.not('isComposing')),
438
primary: KeyMod.CtrlCmd + KeyCode.Enter
439
}
440
}));
441
442
registerEditorCommand(new RenameCommand({
443
id: 'cancelRenameInput',
444
precondition: CONTEXT_RENAME_INPUT_VISIBLE,
445
handler: x => x.cancelRenameInput(),
446
kbOpts: {
447
weight: KeybindingWeight.EditorContrib + 99,
448
kbExpr: EditorContextKeys.focus,
449
primary: KeyCode.Escape,
450
secondary: [KeyMod.Shift | KeyCode.Escape]
451
}
452
}));
453
454
registerAction2(class FocusNextRenameSuggestion extends Action2 {
455
constructor() {
456
super({
457
id: 'focusNextRenameSuggestion',
458
title: {
459
...nls.localize2('focusNextRenameSuggestion', "Focus Next Rename Suggestion"),
460
},
461
precondition: CONTEXT_RENAME_INPUT_VISIBLE,
462
keybinding: [
463
{
464
primary: KeyCode.DownArrow,
465
weight: KeybindingWeight.EditorContrib + 99,
466
}
467
]
468
});
469
}
470
471
override run(accessor: ServicesAccessor): void {
472
const currentEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor();
473
if (!currentEditor) { return; }
474
475
const controller = RenameController.get(currentEditor);
476
if (!controller) { return; }
477
478
controller.focusNextRenameSuggestion();
479
}
480
});
481
482
registerAction2(class FocusPreviousRenameSuggestion extends Action2 {
483
constructor() {
484
super({
485
id: 'focusPreviousRenameSuggestion',
486
title: {
487
...nls.localize2('focusPreviousRenameSuggestion', "Focus Previous Rename Suggestion"),
488
},
489
precondition: CONTEXT_RENAME_INPUT_VISIBLE,
490
keybinding: [
491
{
492
primary: KeyCode.UpArrow,
493
weight: KeybindingWeight.EditorContrib + 99,
494
}
495
]
496
});
497
}
498
499
override run(accessor: ServicesAccessor): void {
500
const currentEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor();
501
if (!currentEditor) { return; }
502
503
const controller = RenameController.get(currentEditor);
504
if (!controller) { return; }
505
506
controller.focusPreviousRenameSuggestion();
507
}
508
});
509
510
// ---- api bridge command
511
512
registerModelAndPositionCommand('_executeDocumentRenameProvider', function (accessor, model, position, ...args) {
513
const [newName] = args;
514
assertType(typeof newName === 'string');
515
const { renameProvider } = accessor.get(ILanguageFeaturesService);
516
return rename(renameProvider, model, position, newName);
517
});
518
519
registerModelAndPositionCommand('_executePrepareRename', async function (accessor, model, position) {
520
const { renameProvider } = accessor.get(ILanguageFeaturesService);
521
const skeleton = new RenameSkeleton(model, position, renameProvider);
522
const loc = await skeleton.resolveRenameLocation(CancellationToken.None);
523
if (loc?.rejectReason) {
524
throw new Error(loc.rejectReason);
525
}
526
return loc;
527
});
528
529
530
//todo@jrieken use editor options world
531
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
532
id: 'editor',
533
properties: {
534
'editor.rename.enablePreview': {
535
scope: ConfigurationScope.LANGUAGE_OVERRIDABLE,
536
description: nls.localize('enablePreview', "Enable/disable the ability to preview changes before renaming"),
537
default: true,
538
type: 'boolean'
539
}
540
}
541
});
542
543