/*--------------------------------------------------------------------------------------------- * 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 __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import * as arrays from '../../../../base/common/arrays.js'; import { createCancelablePromise, Delayer, first } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Color } from '../../../../base/common/color.js'; import { isCancellationError, onUnexpectedError, onUnexpectedExternalError } from '../../../../base/common/errors.js'; import { Event } from '../../../../base/common/event.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import * as strings from '../../../../base/common/strings.js'; import { URI } from '../../../../base/common/uri.js'; import { EditorAction, EditorCommand, registerEditorAction, registerEditorCommand, registerEditorContribution, registerModelAndPositionCommand } from '../../../browser/editorExtensions.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 { ModelDecorationOptions } from '../../../common/model/textModel.js'; import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js'; import * as nls from '../../../../nls.js'; import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { registerColor } from '../../../../platform/theme/common/colorRegistry.js'; import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js'; import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js'; import { ILanguageFeatureDebounceService } from '../../../common/services/languageFeatureDebounce.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; export const CONTEXT_ONTYPE_RENAME_INPUT_VISIBLE = new RawContextKey('LinkedEditingInputVisible', false); const DECORATION_CLASS_NAME = 'linked-editing-decoration'; let LinkedEditingContribution = class LinkedEditingContribution extends Disposable { constructor(editor, contextKeyService, languageFeaturesService, languageConfigurationService, languageFeatureDebounceService) { super(); this.languageConfigurationService = languageConfigurationService; this._syncRangesToken = 0; this._localToDispose = this._register(new DisposableStore()); this._editor = editor; this._providers = languageFeaturesService.linkedEditingRangeProvider; this._enabled = false; this._visibleContextKey = CONTEXT_ONTYPE_RENAME_INPUT_VISIBLE.bindTo(contextKeyService); this._debounceInformation = languageFeatureDebounceService.for(this._providers, 'Linked Editing', { min: 200 }); this._currentDecorations = this._editor.createDecorationsCollection(); this._languageWordPattern = null; this._currentWordPattern = null; this._ignoreChangeEvent = false; this._localToDispose = this._register(new DisposableStore()); this._rangeUpdateTriggerPromise = null; this._rangeSyncTriggerPromise = null; this._currentRequest = null; this._currentRequestPosition = null; this._currentRequestModelVersion = null; this._register(this._editor.onDidChangeModel(() => this.reinitialize(true))); this._register(this._editor.onDidChangeConfiguration(e => { if (e.hasChanged(64 /* EditorOption.linkedEditing */) || e.hasChanged(84 /* EditorOption.renameOnType */)) { this.reinitialize(false); } })); this._register(this._providers.onDidChange(() => this.reinitialize(false))); this._register(this._editor.onDidChangeModelLanguage(() => this.reinitialize(true))); this.reinitialize(true); } static get(editor) { return editor.getContribution(LinkedEditingContribution.ID); } reinitialize(forceRefresh) { const model = this._editor.getModel(); const isEnabled = model !== null && (this._editor.getOption(64 /* EditorOption.linkedEditing */) || this._editor.getOption(84 /* EditorOption.renameOnType */)) && this._providers.has(model); if (isEnabled === this._enabled && !forceRefresh) { return; } this._enabled = isEnabled; this.clearRanges(); this._localToDispose.clear(); if (!isEnabled || model === null) { return; } this._localToDispose.add(Event.runAndSubscribe(model.onDidChangeLanguageConfiguration, () => { this._languageWordPattern = this.languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).getWordDefinition(); })); const rangeUpdateScheduler = new Delayer(this._debounceInformation.get(model)); const triggerRangeUpdate = () => { var _a; this._rangeUpdateTriggerPromise = rangeUpdateScheduler.trigger(() => this.updateRanges(), (_a = this._debounceDuration) !== null && _a !== void 0 ? _a : this._debounceInformation.get(model)); }; const rangeSyncScheduler = new Delayer(0); const triggerRangeSync = (token) => { this._rangeSyncTriggerPromise = rangeSyncScheduler.trigger(() => this._syncRanges(token)); }; this._localToDispose.add(this._editor.onDidChangeCursorPosition(() => { triggerRangeUpdate(); })); this._localToDispose.add(this._editor.onDidChangeModelContent((e) => { if (!this._ignoreChangeEvent) { if (this._currentDecorations.length > 0) { const referenceRange = this._currentDecorations.getRange(0); if (referenceRange && e.changes.every(c => referenceRange.intersectRanges(c.range))) { triggerRangeSync(this._syncRangesToken); return; } } } triggerRangeUpdate(); })); this._localToDispose.add({ dispose: () => { rangeUpdateScheduler.dispose(); rangeSyncScheduler.dispose(); } }); this.updateRanges(); } _syncRanges(token) { // dalayed invocation, make sure we're still on if (!this._editor.hasModel() || token !== this._syncRangesToken || this._currentDecorations.length === 0) { // nothing to do return; } const model = this._editor.getModel(); const referenceRange = this._currentDecorations.getRange(0); if (!referenceRange || referenceRange.startLineNumber !== referenceRange.endLineNumber) { return this.clearRanges(); } const referenceValue = model.getValueInRange(referenceRange); if (this._currentWordPattern) { const match = referenceValue.match(this._currentWordPattern); const matchLength = match ? match[0].length : 0; if (matchLength !== referenceValue.length) { return this.clearRanges(); } } const edits = []; for (let i = 1, len = this._currentDecorations.length; i < len; i++) { const mirrorRange = this._currentDecorations.getRange(i); if (!mirrorRange) { continue; } if (mirrorRange.startLineNumber !== mirrorRange.endLineNumber) { edits.push({ range: mirrorRange, text: referenceValue }); } else { let oldValue = model.getValueInRange(mirrorRange); let newValue = referenceValue; let rangeStartColumn = mirrorRange.startColumn; let rangeEndColumn = mirrorRange.endColumn; const commonPrefixLength = strings.commonPrefixLength(oldValue, newValue); rangeStartColumn += commonPrefixLength; oldValue = oldValue.substr(commonPrefixLength); newValue = newValue.substr(commonPrefixLength); const commonSuffixLength = strings.commonSuffixLength(oldValue, newValue); rangeEndColumn -= commonSuffixLength; oldValue = oldValue.substr(0, oldValue.length - commonSuffixLength); newValue = newValue.substr(0, newValue.length - commonSuffixLength); if (rangeStartColumn !== rangeEndColumn || newValue.length !== 0) { edits.push({ range: new Range(mirrorRange.startLineNumber, rangeStartColumn, mirrorRange.endLineNumber, rangeEndColumn), text: newValue }); } } } if (edits.length === 0) { return; } try { this._editor.popUndoStop(); this._ignoreChangeEvent = true; const prevEditOperationType = this._editor._getViewModel().getPrevEditOperationType(); this._editor.executeEdits('linkedEditing', edits); this._editor._getViewModel().setPrevEditOperationType(prevEditOperationType); } finally { this._ignoreChangeEvent = false; } } dispose() { this.clearRanges(); super.dispose(); } clearRanges() { this._visibleContextKey.set(false); this._currentDecorations.clear(); if (this._currentRequest) { this._currentRequest.cancel(); this._currentRequest = null; this._currentRequestPosition = null; } } updateRanges(force = false) { return __awaiter(this, void 0, void 0, function* () { if (!this._editor.hasModel()) { this.clearRanges(); return; } const position = this._editor.getPosition(); if (!this._enabled && !force || this._editor.getSelections().length > 1) { // disabled or multicursor this.clearRanges(); return; } const model = this._editor.getModel(); const modelVersionId = model.getVersionId(); if (this._currentRequestPosition && this._currentRequestModelVersion === modelVersionId) { if (position.equals(this._currentRequestPosition)) { return; // same position } if (this._currentDecorations.length > 0) { const range = this._currentDecorations.getRange(0); if (range && range.containsPosition(position)) { return; // just moving inside the existing primary range } } } this._currentRequestPosition = position; this._currentRequestModelVersion = modelVersionId; const request = createCancelablePromise((token) => __awaiter(this, void 0, void 0, function* () { try { const sw = new StopWatch(false); const response = yield getLinkedEditingRanges(this._providers, model, position, token); this._debounceInformation.update(model, sw.elapsed()); if (request !== this._currentRequest) { return; } this._currentRequest = null; if (modelVersionId !== model.getVersionId()) { return; } let ranges = []; if (response === null || response === void 0 ? void 0 : response.ranges) { ranges = response.ranges; } this._currentWordPattern = (response === null || response === void 0 ? void 0 : response.wordPattern) || this._languageWordPattern; let foundReferenceRange = false; for (let i = 0, len = ranges.length; i < len; i++) { if (Range.containsPosition(ranges[i], position)) { foundReferenceRange = true; if (i !== 0) { const referenceRange = ranges[i]; ranges.splice(i, 1); ranges.unshift(referenceRange); } break; } } if (!foundReferenceRange) { // Cannot do linked editing if the ranges are not where the cursor is... this.clearRanges(); return; } const decorations = ranges.map(range => ({ range: range, options: LinkedEditingContribution.DECORATION })); this._visibleContextKey.set(true); this._currentDecorations.set(decorations); this._syncRangesToken++; // cancel any pending syncRanges call } catch (err) { if (!isCancellationError(err)) { onUnexpectedError(err); } if (this._currentRequest === request || !this._currentRequest) { // stop if we are still the latest request this.clearRanges(); } } })); this._currentRequest = request; return request; }); } }; LinkedEditingContribution.ID = 'editor.contrib.linkedEditing'; LinkedEditingContribution.DECORATION = ModelDecorationOptions.register({ description: 'linked-editing', stickiness: 0 /* TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges */, className: DECORATION_CLASS_NAME }); LinkedEditingContribution = __decorate([ __param(1, IContextKeyService), __param(2, ILanguageFeaturesService), __param(3, ILanguageConfigurationService), __param(4, ILanguageFeatureDebounceService) ], LinkedEditingContribution); export { LinkedEditingContribution }; export class LinkedEditingAction extends EditorAction { constructor() { super({ id: 'editor.action.linkedEditing', label: nls.localize('linkedEditing.label', "Start Linked Editing"), alias: 'Start Linked Editing', precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasRenameProvider), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: 2048 /* KeyMod.CtrlCmd */ | 1024 /* KeyMod.Shift */ | 60 /* KeyCode.F2 */, weight: 100 /* KeybindingWeight.EditorContrib */ } }); } 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 controller = LinkedEditingContribution.get(editor); if (controller) { return Promise.resolve(controller.updateRanges(true)); } return Promise.resolve(); } } const LinkedEditingCommand = EditorCommand.bindToContribution(LinkedEditingContribution.get); registerEditorCommand(new LinkedEditingCommand({ id: 'cancelLinkedEditingInput', precondition: CONTEXT_ONTYPE_RENAME_INPUT_VISIBLE, handler: x => x.clearRanges(), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, weight: 100 /* KeybindingWeight.EditorContrib */ + 99, primary: 9 /* KeyCode.Escape */, secondary: [1024 /* KeyMod.Shift */ | 9 /* KeyCode.Escape */] } })); function getLinkedEditingRanges(providers, model, position, token) { const orderedByScore = providers.ordered(model); // in order of score ask the linked editing range provider // until someone response with a good result // (good = not null) return first(orderedByScore.map(provider => () => __awaiter(this, void 0, void 0, function* () { try { return yield provider.provideLinkedEditingRanges(model, position, token); } catch (e) { onUnexpectedExternalError(e); return undefined; } })), result => !!result && arrays.isNonEmptyArray(result === null || result === void 0 ? void 0 : result.ranges)); } export const editorLinkedEditingBackground = registerColor('editor.linkedEditingBackground', { dark: Color.fromHex('#f00').transparent(0.3), light: Color.fromHex('#f00').transparent(0.3), hcDark: Color.fromHex('#f00').transparent(0.3), hcLight: Color.white }, nls.localize('editorLinkedEditingBackground', 'Background color when the editor auto renames on type.')); registerThemingParticipant((theme, collector) => { const editorLinkedEditingBackgroundColor = theme.getColor(editorLinkedEditingBackground); if (editorLinkedEditingBackgroundColor) { collector.addRule(`.monaco-editor .${DECORATION_CLASS_NAME} { background: ${editorLinkedEditingBackgroundColor}; border-left-color: ${editorLinkedEditingBackgroundColor}; }`); } }); registerModelAndPositionCommand('_executeLinkedEditingProvider', (_accessor, model, position) => { const { linkedEditingRangeProvider } = _accessor.get(ILanguageFeaturesService); return getLinkedEditingRanges(linkedEditingRangeProvider, model, position, CancellationToken.None); }); registerEditorContribution(LinkedEditingContribution.ID, LinkedEditingContribution); registerEditorAction(LinkedEditingAction);