Path: blob/main/src/vs/editor/contrib/rename/browser/rename.ts
5333 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { alert } from '../../../../base/browser/ui/aria/aria.js';6import { raceCancellation } from '../../../../base/common/async.js';7import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';8import { CancellationError, onUnexpectedError } from '../../../../base/common/errors.js';9import { isMarkdownString } from '../../../../base/common/htmlContent.js';10import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';11import { DisposableStore } from '../../../../base/common/lifecycle.js';12import { assertType } from '../../../../base/common/types.js';13import { URI } from '../../../../base/common/uri.js';14import * as nls from '../../../../nls.js';15import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';16import { ConfigurationScope, Extensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js';17import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';18import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';19import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';20import { ILogService } from '../../../../platform/log/common/log.js';21import { INotificationService } from '../../../../platform/notification/common/notification.js';22import { IEditorProgressService } from '../../../../platform/progress/common/progress.js';23import { Registry } from '../../../../platform/registry/common/platform.js';24import { ICodeEditor } from '../../../browser/editorBrowser.js';25import { EditorAction, EditorCommand, EditorContributionInstantiation, ServicesAccessor, registerEditorAction, registerEditorCommand, registerEditorContribution, registerModelAndPositionCommand } from '../../../browser/editorExtensions.js';26import { IBulkEditService } from '../../../browser/services/bulkEditService.js';27import { ICodeEditorService } from '../../../browser/services/codeEditorService.js';28import { IPosition, Position } from '../../../common/core/position.js';29import { Range } from '../../../common/core/range.js';30import { IEditorContribution } from '../../../common/editorCommon.js';31import { EditorContextKeys } from '../../../common/editorContextKeys.js';32import { LanguageFeatureRegistry } from '../../../common/languageFeatureRegistry.js';33import { NewSymbolNameTriggerKind, Rejection, RenameLocation, RenameProvider, WorkspaceEdit } from '../../../common/languages.js';34import { ITextModel } from '../../../common/model.js';35import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';36import { ITextResourceConfigurationService } from '../../../common/services/textResourceConfiguration.js';37import { EditSources } from '../../../common/textModelEditSource.js';38import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from '../../editorState/browser/editorState.js';39import { MessageController } from '../../message/browser/messageController.js';40import { CONTEXT_RENAME_INPUT_VISIBLE, RenameWidget } from './renameWidget.js';4142class RenameSkeleton {4344private readonly _providers: RenameProvider[];45private _providerRenameIdx: number = 0;4647constructor(48private readonly model: ITextModel,49private readonly position: Position,50registry: LanguageFeatureRegistry<RenameProvider>51) {52this._providers = registry.ordered(model);53}5455hasProvider() {56return this._providers.length > 0;57}5859async resolveRenameLocation(token: CancellationToken): Promise<RenameLocation & Rejection | undefined> {6061const rejects: string[] = [];6263for (this._providerRenameIdx = 0; this._providerRenameIdx < this._providers.length; this._providerRenameIdx++) {64const provider = this._providers[this._providerRenameIdx];65if (!provider.resolveRenameLocation) {66break;67}68const res = await provider.resolveRenameLocation(this.model, this.position, token);69if (!res) {70continue;71}72if (res.rejectReason) {73rejects.push(res.rejectReason);74continue;75}76return res;77}7879// we are here when no provider prepared a location which means we can80// just rely on the word under cursor and start with the first provider81this._providerRenameIdx = 0;8283const word = this.model.getWordAtPosition(this.position);84if (!word) {85return {86range: Range.fromPositions(this.position),87text: '',88rejectReason: rejects.length > 0 ? rejects.join('\n') : undefined89};90}91return {92range: new Range(this.position.lineNumber, word.startColumn, this.position.lineNumber, word.endColumn),93text: word.word,94rejectReason: rejects.length > 0 ? rejects.join('\n') : undefined95};96}9798async provideRenameEdits(newName: string, token: CancellationToken): Promise<WorkspaceEdit & Rejection> {99return this._provideRenameEdits(newName, this._providerRenameIdx, [], token);100}101102private async _provideRenameEdits(newName: string, i: number, rejects: string[], token: CancellationToken): Promise<WorkspaceEdit & Rejection> {103const provider = this._providers[i];104if (!provider) {105return {106edits: [],107rejectReason: rejects.join('\n')108};109}110111const result = await provider.provideRenameEdits(this.model, this.position, newName, token);112if (!result) {113return this._provideRenameEdits(newName, i + 1, rejects.concat(nls.localize('no result', "No result.")), token);114} else if (result.rejectReason) {115return this._provideRenameEdits(newName, i + 1, rejects.concat(result.rejectReason), token);116}117return result;118}119}120121export function hasProvider(registry: LanguageFeatureRegistry<RenameProvider>, model: ITextModel): boolean {122const providers = registry.ordered(model);123return providers.length > 0;124}125126export async function prepareRename(registry: LanguageFeatureRegistry<RenameProvider>, model: ITextModel, position: Position, cancellationToken?: CancellationToken): Promise<RenameLocation & Rejection | undefined> {127const skeleton = new RenameSkeleton(model, position, registry);128return skeleton.resolveRenameLocation(cancellationToken ?? CancellationToken.None);129}130131export async function rawRename(registry: LanguageFeatureRegistry<RenameProvider>, model: ITextModel, position: Position, newName: string, cancellationToken?: CancellationToken): Promise<WorkspaceEdit & Rejection> {132const skeleton = new RenameSkeleton(model, position, registry);133return skeleton.provideRenameEdits(newName, cancellationToken ?? CancellationToken.None);134}135136export async function rename(registry: LanguageFeatureRegistry<RenameProvider>, model: ITextModel, position: Position, newName: string): Promise<WorkspaceEdit & Rejection> {137const skeleton = new RenameSkeleton(model, position, registry);138const loc = await skeleton.resolveRenameLocation(CancellationToken.None);139if (loc?.rejectReason) {140return { edits: [], rejectReason: loc.rejectReason };141}142return skeleton.provideRenameEdits(newName, CancellationToken.None);143}144145// --- register actions and commands146147class RenameController implements IEditorContribution {148149public static readonly ID = 'editor.contrib.renameController';150151static get(editor: ICodeEditor): RenameController | null {152return editor.getContribution<RenameController>(RenameController.ID);153}154155private readonly _renameWidget: RenameWidget;156private readonly _disposableStore = new DisposableStore();157private _cts: CancellationTokenSource = new CancellationTokenSource();158159constructor(160private readonly editor: ICodeEditor,161@IInstantiationService private readonly _instaService: IInstantiationService,162@INotificationService private readonly _notificationService: INotificationService,163@IBulkEditService private readonly _bulkEditService: IBulkEditService,164@IEditorProgressService private readonly _progressService: IEditorProgressService,165@ILogService private readonly _logService: ILogService,166@ITextResourceConfigurationService private readonly _configService: ITextResourceConfigurationService,167@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,168) {169this._renameWidget = this._disposableStore.add(this._instaService.createInstance(RenameWidget, this.editor, ['acceptRenameInput', 'acceptRenameInputWithPreview']));170}171172dispose(): void {173this._disposableStore.dispose();174this._cts.dispose(true);175}176177async run(): Promise<void> {178179const trace = this._logService.trace.bind(this._logService, '[rename]');180181// set up cancellation token to prevent reentrant rename, this182// is the parent to the resolve- and rename-tokens183this._cts.dispose(true);184this._cts = new CancellationTokenSource();185186if (!this.editor.hasModel()) {187trace('editor has no model');188return undefined;189}190191const position = this.editor.getPosition();192const skeleton = new RenameSkeleton(this.editor.getModel(), position, this._languageFeaturesService.renameProvider);193194if (!skeleton.hasProvider()) {195trace('skeleton has no provider');196return undefined;197}198199// part 1 - resolve rename location200const cts1 = new EditorStateCancellationTokenSource(this.editor, CodeEditorStateFlag.Position | CodeEditorStateFlag.Value, undefined, this._cts.token);201202let loc: RenameLocation & Rejection | undefined;203try {204trace('resolving rename location');205const resolveLocationOperation = skeleton.resolveRenameLocation(cts1.token);206this._progressService.showWhile(resolveLocationOperation, 250);207loc = await resolveLocationOperation;208trace('resolved rename location');209} catch (e: unknown) {210if (e instanceof CancellationError) {211trace('resolve rename location cancelled', JSON.stringify(e, null, '\t'));212} else {213trace('resolve rename location failed', e instanceof Error ? e : JSON.stringify(e, null, '\t'));214if (typeof e === 'string' || isMarkdownString(e)) {215MessageController.get(this.editor)?.showMessage(e || nls.localize('resolveRenameLocationFailed', "An unknown error occurred while resolving rename location"), position);216}217}218return undefined;219220} finally {221cts1.dispose();222}223224if (!loc) {225trace('returning early - no loc');226return undefined;227}228229if (loc.rejectReason) {230trace(`returning early - rejected with reason: ${loc.rejectReason}`, loc.rejectReason);231MessageController.get(this.editor)?.showMessage(loc.rejectReason, position);232return undefined;233}234235if (cts1.token.isCancellationRequested) {236trace('returning early - cts1 cancelled');237return undefined;238}239240// part 2 - do rename at location241const cts2 = new EditorStateCancellationTokenSource(this.editor, CodeEditorStateFlag.Position | CodeEditorStateFlag.Value, loc.range, this._cts.token);242243const model = this.editor.getModel(); // @ulugbekna: assumes editor still has a model, otherwise, cts1 should've been cancelled244245const newSymbolNamesProviders = this._languageFeaturesService.newSymbolNamesProvider.all(model);246247const resolvedNewSymbolnamesProviders = await Promise.all(newSymbolNamesProviders.map(async p => [p, await p.supportsAutomaticNewSymbolNamesTriggerKind ?? false] as const));248249const requestRenameSuggestions = (triggerKind: NewSymbolNameTriggerKind, cts: CancellationToken) => {250let providers = resolvedNewSymbolnamesProviders.slice();251252if (triggerKind === NewSymbolNameTriggerKind.Automatic) {253providers = providers.filter(([_, supportsAutomatic]) => supportsAutomatic);254}255256return providers.map(([p,]) => p.provideNewSymbolNames(model, loc.range, triggerKind, cts));257};258259trace('creating rename input field and awaiting its result');260const supportPreview = this._bulkEditService.hasPreviewHandler() && this._configService.getValue<boolean>(this.editor.getModel().uri, 'editor.rename.enablePreview');261const inputFieldResult = await this._renameWidget.getInput(262loc.range,263loc.text,264supportPreview,265newSymbolNamesProviders.length > 0 ? requestRenameSuggestions : undefined,266cts2267);268trace('received response from rename input field');269270// no result, only hint to focus the editor or not271if (typeof inputFieldResult === 'boolean') {272trace(`returning early - rename input field response - ${inputFieldResult}`);273if (inputFieldResult) {274this.editor.focus();275}276cts2.dispose();277return undefined;278}279280this.editor.focus();281282trace('requesting rename edits');283const renameOperation = raceCancellation(skeleton.provideRenameEdits(inputFieldResult.newName, cts2.token), cts2.token).then(async renameResult => {284285if (!renameResult) {286trace('returning early - no rename edits result');287return;288}289if (!this.editor.hasModel()) {290trace('returning early - no model after rename edits are provided');291return;292}293294if (renameResult.rejectReason) {295trace(`returning early - rejected with reason: ${renameResult.rejectReason}`);296this._notificationService.info(renameResult.rejectReason);297return;298}299300// collapse selection to active end301this.editor.setSelection(Range.fromPositions(this.editor.getSelection().getPosition()));302303trace('applying edits');304305this._bulkEditService.apply(renameResult, {306editor: this.editor,307showPreview: inputFieldResult.wantsPreview,308label: nls.localize('label', "Renaming '{0}' to '{1}'", loc?.text, inputFieldResult.newName),309code: 'undoredo.rename',310quotableLabel: nls.localize('quotableLabel', "Renaming {0} to {1}", loc?.text, inputFieldResult.newName),311respectAutoSaveConfig: true,312reason: EditSources.rename(loc?.text, inputFieldResult.newName),313}).then(result => {314trace('edits applied');315if (result.ariaSummary) {316alert(nls.localize('aria', "Successfully renamed '{0}' to '{1}'. Summary: {2}", loc.text, inputFieldResult.newName, result.ariaSummary));317}318}).catch(err => {319trace(`error when applying edits ${JSON.stringify(err, null, '\t')}`);320this._notificationService.error(nls.localize('rename.failedApply', "Rename failed to apply edits"));321this._logService.error(err);322});323324}, err => {325trace('error when providing rename edits', JSON.stringify(err, null, '\t'));326327this._notificationService.error(nls.localize('rename.failed', "Rename failed to compute edits"));328this._logService.error(err);329330}).finally(() => {331cts2.dispose();332});333334trace('returning rename operation');335336this._progressService.showWhile(renameOperation, 250);337return renameOperation;338339}340341acceptRenameInput(wantsPreview: boolean): void {342this._renameWidget.acceptInput(wantsPreview);343}344345cancelRenameInput(): void {346this._renameWidget.cancelInput(true, 'cancelRenameInput command');347}348349focusNextRenameSuggestion(): void {350this._renameWidget.focusNextRenameSuggestion();351}352353focusPreviousRenameSuggestion(): void {354this._renameWidget.focusPreviousRenameSuggestion();355}356}357358// ---- action implementation359360export class RenameAction extends EditorAction {361362constructor() {363super({364id: 'editor.action.rename',365label: nls.localize2('rename.label', "Rename Symbol"),366precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasRenameProvider),367kbOpts: {368kbExpr: EditorContextKeys.editorTextFocus,369primary: KeyCode.F2,370weight: KeybindingWeight.EditorContrib371},372contextMenuOpts: {373group: '1_modification',374order: 1.1375},376canTriggerInlineEdits: true,377});378}379380override runCommand(accessor: ServicesAccessor, args: [URI, IPosition]): void | Promise<void> {381const editorService = accessor.get(ICodeEditorService);382const [uri, pos] = Array.isArray(args) && args || [undefined, undefined];383384if (URI.isUri(uri) && Position.isIPosition(pos)) {385return editorService.openCodeEditor({ resource: uri }, editorService.getActiveCodeEditor()).then(editor => {386if (!editor) {387return;388}389editor.setPosition(pos);390editor.invokeWithinContext(accessor => {391this.reportTelemetry(accessor, editor);392return this.run(accessor, editor);393});394}, onUnexpectedError);395}396397return super.runCommand(accessor, args);398}399400run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {401const logService = accessor.get(ILogService);402403const controller = RenameController.get(editor);404405if (controller) {406logService.trace('[RenameAction] got controller, running...');407return controller.run();408}409logService.trace('[RenameAction] returning early - controller missing');410return Promise.resolve();411}412}413414registerEditorContribution(RenameController.ID, RenameController, EditorContributionInstantiation.Lazy);415registerEditorAction(RenameAction);416417const RenameCommand = EditorCommand.bindToContribution<RenameController>(RenameController.get);418419registerEditorCommand(new RenameCommand({420id: 'acceptRenameInput',421precondition: CONTEXT_RENAME_INPUT_VISIBLE,422handler: x => x.acceptRenameInput(false),423kbOpts: {424weight: KeybindingWeight.EditorContrib + 99,425kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, ContextKeyExpr.not('isComposing')),426primary: KeyCode.Enter427}428}));429430registerEditorCommand(new RenameCommand({431id: 'acceptRenameInputWithPreview',432precondition: ContextKeyExpr.and(CONTEXT_RENAME_INPUT_VISIBLE, ContextKeyExpr.has('config.editor.rename.enablePreview')),433handler: x => x.acceptRenameInput(true),434kbOpts: {435weight: KeybindingWeight.EditorContrib + 99,436kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, ContextKeyExpr.not('isComposing')),437primary: KeyMod.CtrlCmd + KeyCode.Enter438}439}));440441registerEditorCommand(new RenameCommand({442id: 'cancelRenameInput',443precondition: CONTEXT_RENAME_INPUT_VISIBLE,444handler: x => x.cancelRenameInput(),445kbOpts: {446weight: KeybindingWeight.EditorContrib + 99,447kbExpr: EditorContextKeys.focus,448primary: KeyCode.Escape,449secondary: [KeyMod.Shift | KeyCode.Escape]450}451}));452453registerAction2(class FocusNextRenameSuggestion extends Action2 {454constructor() {455super({456id: 'focusNextRenameSuggestion',457title: {458...nls.localize2('focusNextRenameSuggestion', "Focus Next Rename Suggestion"),459},460precondition: CONTEXT_RENAME_INPUT_VISIBLE,461keybinding: [462{463primary: KeyCode.DownArrow,464weight: KeybindingWeight.EditorContrib + 99,465}466]467});468}469470override run(accessor: ServicesAccessor): void {471const currentEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor();472if (!currentEditor) { return; }473474const controller = RenameController.get(currentEditor);475if (!controller) { return; }476477controller.focusNextRenameSuggestion();478}479});480481registerAction2(class FocusPreviousRenameSuggestion extends Action2 {482constructor() {483super({484id: 'focusPreviousRenameSuggestion',485title: {486...nls.localize2('focusPreviousRenameSuggestion', "Focus Previous Rename Suggestion"),487},488precondition: CONTEXT_RENAME_INPUT_VISIBLE,489keybinding: [490{491primary: KeyCode.UpArrow,492weight: KeybindingWeight.EditorContrib + 99,493}494]495});496}497498override run(accessor: ServicesAccessor): void {499const currentEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor();500if (!currentEditor) { return; }501502const controller = RenameController.get(currentEditor);503if (!controller) { return; }504505controller.focusPreviousRenameSuggestion();506}507});508509// ---- api bridge command510511registerModelAndPositionCommand('_executeDocumentRenameProvider', function (accessor, model, position, ...args) {512const [newName] = args;513assertType(typeof newName === 'string');514const { renameProvider } = accessor.get(ILanguageFeaturesService);515return rename(renameProvider, model, position, newName);516});517518registerModelAndPositionCommand('_executePrepareRename', async function (accessor, model, position) {519const { renameProvider } = accessor.get(ILanguageFeaturesService);520const skeleton = new RenameSkeleton(model, position, renameProvider);521const loc = await skeleton.resolveRenameLocation(CancellationToken.None);522if (loc?.rejectReason) {523throw new Error(loc.rejectReason);524}525return loc;526});527528529//todo@jrieken use editor options world530Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({531id: 'editor',532properties: {533'editor.rename.enablePreview': {534scope: ConfigurationScope.LANGUAGE_OVERRIDABLE,535description: nls.localize('enablePreview', "Enable/disable the ability to preview changes before renaming"),536default: true,537type: 'boolean'538}539}540});541542543