UNPKG

monaco-editor-core

Version:

A browser based code editor

480 lines (479 loc) • 23.2 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var RenameController_1; import { alert } from '../../../../base/browser/ui/aria/aria.js'; import { raceCancellation } from '../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { CancellationError, onUnexpectedError } from '../../../../base/common/errors.js'; import { isMarkdownString } from '../../../../base/common/htmlContent.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { assertType } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { EditorAction, EditorCommand, registerEditorAction, registerEditorCommand, registerEditorContribution, registerModelAndPositionCommand } from '../../../browser/editorExtensions.js'; import { IBulkEditService } from '../../../browser/services/bulkEditService.js'; import { ICodeEditorService } from '../../../browser/services/codeEditorService.js'; import { Position } from '../../../common/core/position.js'; import { Range } from '../../../common/core/range.js'; import { EditorContextKeys } from '../../../common/editorContextKeys.js'; import { NewSymbolNameTriggerKind } from '../../../common/languages.js'; import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js'; import { ITextResourceConfigurationService } from '../../../common/services/textResourceConfiguration.js'; import { EditorStateCancellationTokenSource } from '../../editorState/browser/editorState.js'; import { MessageController } from '../../message/browser/messageController.js'; import * as nls from '../../../../nls.js'; import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { Extensions } from '../../../../platform/configuration/common/configurationRegistry.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IEditorProgressService } from '../../../../platform/progress/common/progress.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { CONTEXT_RENAME_INPUT_VISIBLE, RenameWidget } from './renameWidget.js'; class RenameSkeleton { constructor(model, position, registry) { this.model = model; this.position = position; this._providerRenameIdx = 0; this._providers = registry.ordered(model); } hasProvider() { return this._providers.length > 0; } async resolveRenameLocation(token) { const rejects = []; for (this._providerRenameIdx = 0; this._providerRenameIdx < this._providers.length; this._providerRenameIdx++) { const provider = this._providers[this._providerRenameIdx]; if (!provider.resolveRenameLocation) { break; } const res = await provider.resolveRenameLocation(this.model, this.position, token); if (!res) { continue; } if (res.rejectReason) { rejects.push(res.rejectReason); continue; } return res; } // we are here when no provider prepared a location which means we can // just rely on the word under cursor and start with the first provider this._providerRenameIdx = 0; const word = this.model.getWordAtPosition(this.position); if (!word) { return { range: Range.fromPositions(this.position), text: '', rejectReason: rejects.length > 0 ? rejects.join('\n') : undefined }; } return { range: new Range(this.position.lineNumber, word.startColumn, this.position.lineNumber, word.endColumn), text: word.word, rejectReason: rejects.length > 0 ? rejects.join('\n') : undefined }; } async provideRenameEdits(newName, token) { return this._provideRenameEdits(newName, this._providerRenameIdx, [], token); } async _provideRenameEdits(newName, i, rejects, token) { const provider = this._providers[i]; if (!provider) { return { edits: [], rejectReason: rejects.join('\n') }; } const result = await provider.provideRenameEdits(this.model, this.position, newName, token); if (!result) { return this._provideRenameEdits(newName, i + 1, rejects.concat(nls.localize('no result', "No result.")), token); } else if (result.rejectReason) { return this._provideRenameEdits(newName, i + 1, rejects.concat(result.rejectReason), token); } return result; } } export async function rename(registry, model, position, newName) { const skeleton = new RenameSkeleton(model, position, registry); const loc = await skeleton.resolveRenameLocation(CancellationToken.None); if (loc?.rejectReason) { return { edits: [], rejectReason: loc.rejectReason }; } return skeleton.provideRenameEdits(newName, CancellationToken.None); } // --- register actions and commands let RenameController = class RenameController { static { RenameController_1 = this; } static { this.ID = 'editor.contrib.renameController'; } static get(editor) { return editor.getContribution(RenameController_1.ID); } constructor(editor, _instaService, _notificationService, _bulkEditService, _progressService, _logService, _configService, _languageFeaturesService, _telemetryService) { this.editor = editor; this._instaService = _instaService; this._notificationService = _notificationService; this._bulkEditService = _bulkEditService; this._progressService = _progressService; this._logService = _logService; this._configService = _configService; this._languageFeaturesService = _languageFeaturesService; this._telemetryService = _telemetryService; this._disposableStore = new DisposableStore(); this._cts = new CancellationTokenSource(); this._renameWidget = this._disposableStore.add(this._instaService.createInstance(RenameWidget, this.editor, ['acceptRenameInput', 'acceptRenameInputWithPreview'])); } dispose() { this._disposableStore.dispose(); this._cts.dispose(true); } async run() { const trace = this._logService.trace.bind(this._logService, '[rename]'); // set up cancellation token to prevent reentrant rename, this // is the parent to the resolve- and rename-tokens this._cts.dispose(true); this._cts = new CancellationTokenSource(); if (!this.editor.hasModel()) { trace('editor has no model'); return undefined; } const position = this.editor.getPosition(); const skeleton = new RenameSkeleton(this.editor.getModel(), position, this._languageFeaturesService.renameProvider); if (!skeleton.hasProvider()) { trace('skeleton has no provider'); return undefined; } // part 1 - resolve rename location const cts1 = new EditorStateCancellationTokenSource(this.editor, 4 /* CodeEditorStateFlag.Position */ | 1 /* CodeEditorStateFlag.Value */, undefined, this._cts.token); let loc; try { trace('resolving rename location'); const resolveLocationOperation = skeleton.resolveRenameLocation(cts1.token); this._progressService.showWhile(resolveLocationOperation, 250); loc = await resolveLocationOperation; trace('resolved rename location'); } catch (e) { if (e instanceof CancellationError) { trace('resolve rename location cancelled', JSON.stringify(e, null, '\t')); } else { trace('resolve rename location failed', e instanceof Error ? e : JSON.stringify(e, null, '\t')); if (typeof e === 'string' || isMarkdownString(e)) { MessageController.get(this.editor)?.showMessage(e || nls.localize('resolveRenameLocationFailed', "An unknown error occurred while resolving rename location"), position); } } return undefined; } finally { cts1.dispose(); } if (!loc) { trace('returning early - no loc'); return undefined; } if (loc.rejectReason) { trace(`returning early - rejected with reason: ${loc.rejectReason}`, loc.rejectReason); MessageController.get(this.editor)?.showMessage(loc.rejectReason, position); return undefined; } if (cts1.token.isCancellationRequested) { trace('returning early - cts1 cancelled'); return undefined; } // part 2 - do rename at location const cts2 = new EditorStateCancellationTokenSource(this.editor, 4 /* CodeEditorStateFlag.Position */ | 1 /* CodeEditorStateFlag.Value */, loc.range, this._cts.token); const model = this.editor.getModel(); // @ulugbekna: assumes editor still has a model, otherwise, cts1 should've been cancelled const newSymbolNamesProviders = this._languageFeaturesService.newSymbolNamesProvider.all(model); const resolvedNewSymbolnamesProviders = await Promise.all(newSymbolNamesProviders.map(async (p) => [p, await p.supportsAutomaticNewSymbolNamesTriggerKind ?? false])); const requestRenameSuggestions = (triggerKind, cts) => { let providers = resolvedNewSymbolnamesProviders.slice(); if (triggerKind === NewSymbolNameTriggerKind.Automatic) { providers = providers.filter(([_, supportsAutomatic]) => supportsAutomatic); } return providers.map(([p,]) => p.provideNewSymbolNames(model, loc.range, triggerKind, cts)); }; trace('creating rename input field and awaiting its result'); const supportPreview = this._bulkEditService.hasPreviewHandler() && this._configService.getValue(this.editor.getModel().uri, 'editor.rename.enablePreview'); const inputFieldResult = await this._renameWidget.getInput(loc.range, loc.text, supportPreview, newSymbolNamesProviders.length > 0 ? requestRenameSuggestions : undefined, cts2); trace('received response from rename input field'); if (newSymbolNamesProviders.length > 0) { // @ulugbekna: we're interested only in telemetry for rename suggestions currently this._reportTelemetry(newSymbolNamesProviders.length, model.getLanguageId(), inputFieldResult); } // no result, only hint to focus the editor or not if (typeof inputFieldResult === 'boolean') { trace(`returning early - rename input field response - ${inputFieldResult}`); if (inputFieldResult) { this.editor.focus(); } cts2.dispose(); return undefined; } this.editor.focus(); trace('requesting rename edits'); const renameOperation = raceCancellation(skeleton.provideRenameEdits(inputFieldResult.newName, cts2.token), cts2.token).then(async (renameResult) => { if (!renameResult) { trace('returning early - no rename edits result'); return; } if (!this.editor.hasModel()) { trace('returning early - no model after rename edits are provided'); return; } if (renameResult.rejectReason) { trace(`returning early - rejected with reason: ${renameResult.rejectReason}`); this._notificationService.info(renameResult.rejectReason); return; } // collapse selection to active end this.editor.setSelection(Range.fromPositions(this.editor.getSelection().getPosition())); trace('applying edits'); this._bulkEditService.apply(renameResult, { editor: this.editor, showPreview: inputFieldResult.wantsPreview, label: nls.localize('label', "Renaming '{0}' to '{1}'", loc?.text, inputFieldResult.newName), code: 'undoredo.rename', quotableLabel: nls.localize('quotableLabel', "Renaming {0} to {1}", loc?.text, inputFieldResult.newName), respectAutoSaveConfig: true }).then(result => { trace('edits applied'); if (result.ariaSummary) { alert(nls.localize('aria', "Successfully renamed '{0}' to '{1}'. Summary: {2}", loc.text, inputFieldResult.newName, result.ariaSummary)); } }).catch(err => { trace(`error when applying edits ${JSON.stringify(err, null, '\t')}`); this._notificationService.error(nls.localize('rename.failedApply', "Rename failed to apply edits")); this._logService.error(err); }); }, err => { trace('error when providing rename edits', JSON.stringify(err, null, '\t')); this._notificationService.error(nls.localize('rename.failed', "Rename failed to compute edits")); this._logService.error(err); }).finally(() => { cts2.dispose(); }); trace('returning rename operation'); this._progressService.showWhile(renameOperation, 250); return renameOperation; } acceptRenameInput(wantsPreview) { this._renameWidget.acceptInput(wantsPreview); } cancelRenameInput() { this._renameWidget.cancelInput(true, 'cancelRenameInput command'); } focusNextRenameSuggestion() { this._renameWidget.focusNextRenameSuggestion(); } focusPreviousRenameSuggestion() { this._renameWidget.focusPreviousRenameSuggestion(); } _reportTelemetry(nRenameSuggestionProviders, languageId, inputFieldResult) { const value = typeof inputFieldResult === 'boolean' ? { kind: 'cancelled', languageId, nRenameSuggestionProviders, } : { kind: 'accepted', languageId, nRenameSuggestionProviders, source: inputFieldResult.stats.source.k, nRenameSuggestions: inputFieldResult.stats.nRenameSuggestions, timeBeforeFirstInputFieldEdit: inputFieldResult.stats.timeBeforeFirstInputFieldEdit, wantsPreview: inputFieldResult.wantsPreview, nRenameSuggestionsInvocations: inputFieldResult.stats.nRenameSuggestionsInvocations, hadAutomaticRenameSuggestionsInvocation: inputFieldResult.stats.hadAutomaticRenameSuggestionsInvocation, }; this._telemetryService.publicLog2('renameInvokedEvent', value); } }; RenameController = RenameController_1 = __decorate([ __param(1, IInstantiationService), __param(2, INotificationService), __param(3, IBulkEditService), __param(4, IEditorProgressService), __param(5, ILogService), __param(6, ITextResourceConfigurationService), __param(7, ILanguageFeaturesService), __param(8, ITelemetryService) ], RenameController); // ---- action implementation export class RenameAction extends EditorAction { constructor() { super({ id: 'editor.action.rename', label: nls.localize('rename.label', "Rename Symbol"), alias: 'Rename Symbol', precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasRenameProvider), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: 60 /* KeyCode.F2 */, weight: 100 /* KeybindingWeight.EditorContrib */ }, contextMenuOpts: { group: '1_modification', order: 1.1 } }); } runCommand(accessor, args) { const editorService = accessor.get(ICodeEditorService); const [uri, pos] = Array.isArray(args) && args || [undefined, undefined]; if (URI.isUri(uri) && Position.isIPosition(pos)) { return editorService.openCodeEditor({ resource: uri }, editorService.getActiveCodeEditor()).then(editor => { if (!editor) { return; } editor.setPosition(pos); editor.invokeWithinContext(accessor => { this.reportTelemetry(accessor, editor); return this.run(accessor, editor); }); }, onUnexpectedError); } return super.runCommand(accessor, args); } run(accessor, editor) { const logService = accessor.get(ILogService); const controller = RenameController.get(editor); if (controller) { logService.trace('[RenameAction] got controller, running...'); return controller.run(); } logService.trace('[RenameAction] returning early - controller missing'); return Promise.resolve(); } } registerEditorContribution(RenameController.ID, RenameController, 4 /* EditorContributionInstantiation.Lazy */); registerEditorAction(RenameAction); const RenameCommand = EditorCommand.bindToContribution(RenameController.get); registerEditorCommand(new RenameCommand({ id: 'acceptRenameInput', precondition: CONTEXT_RENAME_INPUT_VISIBLE, handler: x => x.acceptRenameInput(false), kbOpts: { weight: 100 /* KeybindingWeight.EditorContrib */ + 99, kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, ContextKeyExpr.not('isComposing')), primary: 3 /* KeyCode.Enter */ } })); registerEditorCommand(new RenameCommand({ id: 'acceptRenameInputWithPreview', precondition: ContextKeyExpr.and(CONTEXT_RENAME_INPUT_VISIBLE, ContextKeyExpr.has('config.editor.rename.enablePreview')), handler: x => x.acceptRenameInput(true), kbOpts: { weight: 100 /* KeybindingWeight.EditorContrib */ + 99, kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, ContextKeyExpr.not('isComposing')), primary: 2048 /* KeyMod.CtrlCmd */ + 3 /* KeyCode.Enter */ } })); registerEditorCommand(new RenameCommand({ id: 'cancelRenameInput', precondition: CONTEXT_RENAME_INPUT_VISIBLE, handler: x => x.cancelRenameInput(), kbOpts: { weight: 100 /* KeybindingWeight.EditorContrib */ + 99, kbExpr: EditorContextKeys.focus, primary: 9 /* KeyCode.Escape */, secondary: [1024 /* KeyMod.Shift */ | 9 /* KeyCode.Escape */] } })); registerAction2(class FocusNextRenameSuggestion extends Action2 { constructor() { super({ id: 'focusNextRenameSuggestion', title: { ...nls.localize2('focusNextRenameSuggestion', "Focus Next Rename Suggestion"), }, precondition: CONTEXT_RENAME_INPUT_VISIBLE, keybinding: [ { primary: 18 /* KeyCode.DownArrow */, weight: 100 /* KeybindingWeight.EditorContrib */ + 99, } ] }); } run(accessor) { const currentEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor(); if (!currentEditor) { return; } const controller = RenameController.get(currentEditor); if (!controller) { return; } controller.focusNextRenameSuggestion(); } }); registerAction2(class FocusPreviousRenameSuggestion extends Action2 { constructor() { super({ id: 'focusPreviousRenameSuggestion', title: { ...nls.localize2('focusPreviousRenameSuggestion', "Focus Previous Rename Suggestion"), }, precondition: CONTEXT_RENAME_INPUT_VISIBLE, keybinding: [ { primary: 16 /* KeyCode.UpArrow */, weight: 100 /* KeybindingWeight.EditorContrib */ + 99, } ] }); } run(accessor) { const currentEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor(); if (!currentEditor) { return; } const controller = RenameController.get(currentEditor); if (!controller) { return; } controller.focusPreviousRenameSuggestion(); } }); // ---- api bridge command registerModelAndPositionCommand('_executeDocumentRenameProvider', function (accessor, model, position, ...args) { const [newName] = args; assertType(typeof newName === 'string'); const { renameProvider } = accessor.get(ILanguageFeaturesService); return rename(renameProvider, model, position, newName); }); registerModelAndPositionCommand('_executePrepareRename', async function (accessor, model, position) { const { renameProvider } = accessor.get(ILanguageFeaturesService); const skeleton = new RenameSkeleton(model, position, renameProvider); const loc = await skeleton.resolveRenameLocation(CancellationToken.None); if (loc?.rejectReason) { throw new Error(loc.rejectReason); } return loc; }); //todo@jrieken use editor options world Registry.as(Extensions.Configuration).registerConfiguration({ id: 'editor', properties: { 'editor.rename.enablePreview': { scope: 5 /* ConfigurationScope.LANGUAGE_OVERRIDABLE */, description: nls.localize('enablePreview', "Enable/disable the ability to preview changes before renaming"), default: true, type: 'boolean' } } });