diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 63ac7bf213380..e6217a6527587 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -135,6 +135,11 @@ export interface IEditorOptions { * Defaults to false. */ readOnly?: boolean; + /** + * Rename matching regions on type. + * Defaults to false. + */ + renameOnType?: boolean; /** * Should the editor render validation decorations. * Defaults to editable. @@ -3374,6 +3379,7 @@ export const enum EditorOption { quickSuggestions, quickSuggestionsDelay, readOnly, + renameOnType, renderControlCharacters, renderIndentGuides, renderFinalNewline, @@ -3790,6 +3796,10 @@ export const EditorOptions = { readOnly: register(new EditorBooleanOption( EditorOption.readOnly, 'readOnly', false, )), + renameOnType: register(new EditorBooleanOption( + EditorOption.renameOnType, 'renameOnType', false, + { description: nls.localize('renameOnType', "Controls whether the editor auto renames on type.") } + )), renderControlCharacters: register(new EditorBooleanOption( EditorOption.renderControlCharacters, 'renderControlCharacters', false, { description: nls.localize('renderControlCharacters', "Controls whether the editor should render control characters.") } diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index d892eee8f62b7..de9a075803645 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -789,6 +789,20 @@ export interface DocumentHighlightProvider { provideDocumentHighlights(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult; } +/** + * The rename provider interface defines the contract between extensions and + * the live-rename feature. + */ +export interface OnTypeRenameProvider { + + stopPattern?: RegExp; + + /** + * Provide a list of ranges that can be live-renamed together. + */ + provideOnTypeRenameRanges(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult; +} + /** * Value-object that contains additional information when * requesting references. @@ -1642,6 +1656,11 @@ export const DocumentSymbolProviderRegistry = new LanguageFeatureRegistry(); +/** + * @internal + */ +export const OnTypeRenameProviderRegistry = new LanguageFeatureRegistry(); + /** * @internal */ diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 418fe492a04c8..30200a230eadb 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -238,47 +238,48 @@ export enum EditorOption { quickSuggestions = 70, quickSuggestionsDelay = 71, readOnly = 72, - renderControlCharacters = 73, - renderIndentGuides = 74, - renderFinalNewline = 75, - renderLineHighlight = 76, - renderValidationDecorations = 77, - renderWhitespace = 78, - revealHorizontalRightPadding = 79, - roundedSelection = 80, - rulers = 81, - scrollbar = 82, - scrollBeyondLastColumn = 83, - scrollBeyondLastLine = 84, - scrollPredominantAxis = 85, - selectionClipboard = 86, - selectionHighlight = 87, - selectOnLineNumbers = 88, - showFoldingControls = 89, - showUnused = 90, - snippetSuggestions = 91, - smoothScrolling = 92, - stopRenderingLineAfter = 93, - suggest = 94, - suggestFontSize = 95, - suggestLineHeight = 96, - suggestOnTriggerCharacters = 97, - suggestSelection = 98, - tabCompletion = 99, - useTabStops = 100, - wordSeparators = 101, - wordWrap = 102, - wordWrapBreakAfterCharacters = 103, - wordWrapBreakBeforeCharacters = 104, - wordWrapColumn = 105, - wordWrapMinified = 106, - wrappingIndent = 107, - wrappingStrategy = 108, - editorClassName = 109, - pixelRatio = 110, - tabFocusMode = 111, - layoutInfo = 112, - wrappingInfo = 113 + renameOnType = 73, + renderControlCharacters = 74, + renderIndentGuides = 75, + renderFinalNewline = 76, + renderLineHighlight = 77, + renderValidationDecorations = 78, + renderWhitespace = 79, + revealHorizontalRightPadding = 80, + roundedSelection = 81, + rulers = 82, + scrollbar = 83, + scrollBeyondLastColumn = 84, + scrollBeyondLastLine = 85, + scrollPredominantAxis = 86, + selectionClipboard = 87, + selectionHighlight = 88, + selectOnLineNumbers = 89, + showFoldingControls = 90, + showUnused = 91, + snippetSuggestions = 92, + smoothScrolling = 93, + stopRenderingLineAfter = 94, + suggest = 95, + suggestFontSize = 96, + suggestLineHeight = 97, + suggestOnTriggerCharacters = 98, + suggestSelection = 99, + tabCompletion = 100, + useTabStops = 101, + wordSeparators = 102, + wordWrap = 103, + wordWrapBreakAfterCharacters = 104, + wordWrapBreakBeforeCharacters = 105, + wordWrapColumn = 106, + wordWrapMinified = 107, + wrappingIndent = 108, + wrappingStrategy = 109, + editorClassName = 110, + pixelRatio = 111, + tabFocusMode = 112, + layoutInfo = 113, + wrappingInfo = 114 } /** diff --git a/src/vs/editor/contrib/rename/media/onTypeRename.css b/src/vs/editor/contrib/rename/media/onTypeRename.css new file mode 100644 index 0000000000000..2c80c5957b1a6 --- /dev/null +++ b/src/vs/editor/contrib/rename/media/onTypeRename.css @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-editor .on-type-rename-decoration { + background: rgba(255, 0, 0, 0.3); + border-left: 1px solid rgba(255, 0, 0, 0.3); + /* So border can be transparent */ + background-clip: padding-box; +} diff --git a/src/vs/editor/contrib/rename/onTypeRename.ts b/src/vs/editor/contrib/rename/onTypeRename.ts new file mode 100644 index 0000000000000..8a3177f139f60 --- /dev/null +++ b/src/vs/editor/contrib/rename/onTypeRename.ts @@ -0,0 +1,367 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/onTypeRename'; +import * as nls from 'vs/nls'; +import { registerEditorContribution, registerModelAndPositionCommand, EditorAction, EditorCommand, ServicesAccessor, registerEditorAction, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; +import * as arrays from 'vs/base/common/arrays'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { Position, IPosition } from 'vs/editor/common/core/position'; +import { ITextModel, IModelDeltaDecoration, TrackedRangeStickiness, IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import { OnTypeRenameProviderRegistry } from 'vs/editor/common/modes'; +import { first, createCancelablePromise, CancelablePromise, RunOnceScheduler } from 'vs/base/common/async'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { ContextKeyExpr, RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { URI } from 'vs/base/common/uri'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors'; +import * as strings from 'vs/base/common/strings'; + +export const CONTEXT_ONTYPE_RENAME_INPUT_VISIBLE = new RawContextKey('onTypeRenameInputVisible', false); + +export class OnTypeRenameContribution extends Disposable implements IEditorContribution { + + public static readonly ID = 'editor.contrib.onTypeRename'; + + private static readonly DECORATION = ModelDecorationOptions.register({ + stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges, + className: 'on-type-rename-decoration' + }); + + static get(editor: ICodeEditor): OnTypeRenameContribution { + return editor.getContribution(OnTypeRenameContribution.ID); + } + + private readonly _editor: ICodeEditor; + private _enabled: boolean; + + private readonly _visibleContextKey: IContextKey; + + private _currentRequest: CancelablePromise<{ + ranges: IRange[], + stopPattern?: RegExp + } | null | undefined> | null; + private _currentDecorations: string[]; // The one at index 0 is the reference one + private _stopPattern: RegExp; + private _ignoreChangeEvent: boolean; + private _updateMirrors: RunOnceScheduler; + + constructor( + editor: ICodeEditor, + @IContextKeyService contextKeyService: IContextKeyService + ) { + super(); + this._editor = editor; + this._enabled = this._editor.getOption(EditorOption.renameOnType); + this._visibleContextKey = CONTEXT_ONTYPE_RENAME_INPUT_VISIBLE.bindTo(contextKeyService); + this._currentRequest = null; + this._currentDecorations = []; + this._stopPattern = /^\s/; + this._ignoreChangeEvent = false; + this._updateMirrors = this._register(new RunOnceScheduler(() => this._doUpdateMirrors(), 0)); + + this._register(this._editor.onDidChangeModel((e) => { + this.stopAll(); + this.run(); + })); + + this._register(this._editor.onDidChangeConfiguration((e) => { + if (e.hasChanged(EditorOption.renameOnType)) { + this._enabled = this._editor.getOption(EditorOption.renameOnType); + this.stopAll(); + this.run(); + } + })); + + this._register(this._editor.onDidChangeCursorPosition((e) => { + // no regions, run + if (this._currentDecorations.length === 0) { + this.run(e.position); + } + + // has cached regions, don't run + if (!this._editor.hasModel()) { + return; + } + if (this._currentDecorations.length === 0) { + return; + } + const model = this._editor.getModel(); + const currentRanges = this._currentDecorations.map(decId => model.getDecorationRange(decId)!); + + // just moving cursor around, don't run again + if (Range.containsPosition(currentRanges[0], e.position)) { + return; + } + + // moving cursor out of primary region, run + this.run(e.position); + })); + + this._register(OnTypeRenameProviderRegistry.onDidChange(() => { + this.run(); + })); + + this._register(this._editor.onDidChangeModelContent((e) => { + if (this._ignoreChangeEvent) { + return; + } + if (!this._editor.hasModel()) { + return; + } + if (this._currentDecorations.length === 0) { + // nothing to do + return; + } + if (e.isUndoing || e.isRedoing) { + return; + } + if (e.changes[0] && this._stopPattern.test(e.changes[0].text)) { + this.stopAll(); + return; + } + this._updateMirrors.schedule(); + })); + } + + private _doUpdateMirrors(): void { + if (!this._editor.hasModel()) { + return; + } + if (this._currentDecorations.length === 0) { + // nothing to do + return; + } + + const model = this._editor.getModel(); + const currentRanges = this._currentDecorations.map(decId => model.getDecorationRange(decId)!); + + const referenceRange = currentRanges[0]; + if (referenceRange.startLineNumber !== referenceRange.endLineNumber) { + return this.stopAll(); + } + + const referenceValue = model.getValueInRange(referenceRange); + if (this._stopPattern.test(referenceValue)) { + return this.stopAll(); + } + + let edits: IIdentifiedSingleEditOperation[] = []; + for (let i = 1, len = currentRanges.length; i < len; i++) { + const mirrorRange = currentRanges[i]; + 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._ignoreChangeEvent = true; + const prevEditOperationType = this._editor._getCursors().getPrevEditOperationType(); + this._editor.executeEdits('onTypeRename', edits); + this._editor._getCursors().setPrevEditOperationType(prevEditOperationType); + } finally { + this._ignoreChangeEvent = false; + } + } + + public dispose(): void { + super.dispose(); + this.stopAll(); + } + + stopAll(): void { + this._visibleContextKey.set(false); + this._currentDecorations = this._editor.deltaDecorations(this._currentDecorations, []); + } + + async run(position: Position | null = this._editor.getPosition(), force = false): Promise { + if (!position) { + return; + } + if (!this._enabled && !force) { + return; + } + if (!this._editor.hasModel()) { + return; + } + + if (this._currentRequest) { + this._currentRequest.cancel(); + this._currentRequest = null; + } + + const model = this._editor.getModel(); + + this._currentRequest = createCancelablePromise(token => getOnTypeRenameRanges(model, position, token)); + try { + const response = await this._currentRequest; + + let ranges: IRange[] = []; + if (response?.ranges) { + ranges = response.ranges; + } + if (response?.stopPattern) { + this._stopPattern = response.stopPattern; + } + + 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 on type rename if the ranges are not where the cursor is... + this.stopAll(); + return; + } + + const decorations: IModelDeltaDecoration[] = ranges.map(range => ({ range: range, options: OnTypeRenameContribution.DECORATION })); + this._visibleContextKey.set(true); + this._currentDecorations = this._editor.deltaDecorations(this._currentDecorations, decorations); + } catch (err) { + onUnexpectedError(err); + this.stopAll(); + } + } +} + +export class OnTypeRenameAction extends EditorAction { + constructor() { + super({ + id: 'editor.action.onTypeRename', + label: nls.localize('onTypeRename.label', "On Type Rename Symbol"), + alias: 'On Type Rename Symbol', + precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasRenameProvider), + kbOpts: { + kbExpr: EditorContextKeys.editorTextFocus, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.F2, + weight: KeybindingWeight.EditorContrib + } + }); + } + + runCommand(accessor: ServicesAccessor, args: [URI, IPosition]): void | Promise { + 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: ServicesAccessor, editor: ICodeEditor): Promise { + const controller = OnTypeRenameContribution.get(editor); + if (controller) { + return Promise.resolve(controller.run(editor.getPosition(), true)); + } + return Promise.resolve(); + } +} + +const OnTypeRenameCommand = EditorCommand.bindToContribution(OnTypeRenameContribution.get); +registerEditorCommand(new OnTypeRenameCommand({ + id: 'cancelOnTypeRenameInput', + precondition: CONTEXT_ONTYPE_RENAME_INPUT_VISIBLE, + handler: x => x.stopAll(), + kbOpts: { + kbExpr: EditorContextKeys.editorTextFocus, + weight: KeybindingWeight.EditorContrib + 99, + primary: KeyCode.Escape, + secondary: [KeyMod.Shift | KeyCode.Escape] + } +})); + + +export function getOnTypeRenameRanges(model: ITextModel, position: Position, token: CancellationToken): Promise<{ + ranges: IRange[], + stopPattern?: RegExp +} | undefined | null> { + const orderedByScore = OnTypeRenameProviderRegistry.ordered(model); + + // in order of score ask the occurrences provider + // until someone response with a good result + // (good = none empty array) + return first<{ + ranges: IRange[], + stopPattern?: RegExp + } | undefined>(orderedByScore.map(provider => () => { + return Promise.resolve(provider.provideOnTypeRenameRanges(model, position, token)).then((ranges) => { + if (!ranges) { + return undefined; + } + + return { + ranges, + stopPattern: provider.stopPattern + }; + }, (err) => { + onUnexpectedExternalError(err); + return undefined; + }); + + }), result => !!result && arrays.isNonEmptyArray(result?.ranges)); +} + + +registerModelAndPositionCommand('_executeRenameOnTypeProvider', (model, position) => getOnTypeRenameRanges(model, position, CancellationToken.None)); + +registerEditorContribution(OnTypeRenameContribution.ID, OnTypeRenameContribution); +registerEditorAction(OnTypeRenameAction); diff --git a/src/vs/editor/contrib/rename/test/onTypeRename.test.ts b/src/vs/editor/contrib/rename/test/onTypeRename.test.ts new file mode 100644 index 0000000000000..f57996f48c315 --- /dev/null +++ b/src/vs/editor/contrib/rename/test/onTypeRename.test.ts @@ -0,0 +1,451 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { Handler } from 'vs/editor/common/editorCommon'; +import * as modes from 'vs/editor/common/modes'; +import { OnTypeRenameContribution } from 'vs/editor/contrib/rename/onTypeRename'; +import { createTestCodeEditor, TestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; +import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; + +const mockFile = URI.parse('test:somefile.ttt'); +const mockFileSelector = { scheme: 'test' }; +const timeout = 30; + +suite('On type rename', () => { + const disposables = new DisposableStore(); + + setup(() => { + disposables.clear(); + }); + + teardown(() => { + disposables.clear(); + }); + + function createMockEditor(text: string | string[]) { + const model = typeof text === 'string' + ? createTextModel(text, undefined, undefined, mockFile) + : createTextModel(text.join('\n'), undefined, undefined, mockFile); + + const editor = createTestCodeEditor({ model }); + disposables.add(model); + disposables.add(editor); + + return editor; + } + + + function testCase( + name: string, + initialState: { text: string | string[], ranges: Range[], stopPattern?: RegExp }, + operations: (editor: TestCodeEditor, contrib: OnTypeRenameContribution) => Promise, + expectedEndText: string | string[] + ) { + test(name, async () => { + disposables.add(modes.OnTypeRenameProviderRegistry.register(mockFileSelector, { + stopPattern: initialState.stopPattern || /^\s/, + + provideOnTypeRenameRanges() { + return initialState.ranges; + } + })); + + const editor = createMockEditor(initialState.text); + const ontypeRenameContribution = editor.registerAndInstantiateContribution( + OnTypeRenameContribution.ID, + OnTypeRenameContribution + ); + + await operations(editor, ontypeRenameContribution); + + return new Promise((resolve) => { + setTimeout(() => { + if (typeof expectedEndText === 'string') { + assert.equal(editor.getModel()!.getValue(), expectedEndText); + } else { + assert.equal(editor.getModel()!.getValue(), expectedEndText.join('\n')); + } + resolve(); + }, timeout); + }); + }); + } + + const state = { + text: '', + ranges: [ + new Range(1, 2, 1, 5), + new Range(1, 8, 1, 11), + ] + }; + + /** + * Simple insertion + */ + testCase('Simple insert - initial', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 2); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Type, { text: 'i' }); + }, ''); + + testCase('Simple insert - middle', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 3); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Type, { text: 'i' }); + }, ''); + + testCase('Simple insert - end', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 5); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Type, { text: 'i' }); + }, ''); + + /** + * Simple insertion - end + */ + testCase('Simple insert end - initial', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 8); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Type, { text: 'i' }); + }, ''); + + testCase('Simple insert end - middle', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 9); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Type, { text: 'i' }); + }, ''); + + testCase('Simple insert end - end', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 11); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Type, { text: 'i' }); + }, ''); + + /** + * Boundary insertion + */ + testCase('Simple insert - out of boundary', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 1); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Type, { text: 'i' }); + }, 'i'); + + testCase('Simple insert - out of boundary 2', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 6); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Type, { text: 'i' }); + }, 'i'); + + testCase('Simple insert - out of boundary 3', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 7); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Type, { text: 'i' }); + }, ''); + + testCase('Simple insert - out of boundary 4', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 12); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Type, { text: 'i' }); + }, 'i'); + + /** + * Insert + Move + */ + testCase('Continuous insert', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 2); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Type, { text: 'i' }); + editor.trigger('keyboard', Handler.Type, { text: 'i' }); + }, ''); + + testCase('Insert - move - insert', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 2); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Type, { text: 'i' }); + editor.setPosition(new Position(1, 4)); + editor.trigger('keyboard', Handler.Type, { text: 'i' }); + }, ''); + + testCase('Insert - move - insert outside region', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 2); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Type, { text: 'i' }); + editor.setPosition(new Position(1, 7)); + editor.trigger('keyboard', Handler.Type, { text: 'i' }); + }, 'i'); + + /** + * Selection insert + */ + testCase('Selection insert - simple', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 2); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.setSelection(new Range(1, 2, 1, 3)); + editor.trigger('keyboard', Handler.Type, { text: 'i' }); + }, ''); + + testCase('Selection insert - whole', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 2); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.setSelection(new Range(1, 2, 1, 5)); + editor.trigger('keyboard', Handler.Type, { text: 'i' }); + }, ''); + + testCase('Selection insert - across boundary', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 2); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.setSelection(new Range(1, 1, 1, 3)); + editor.trigger('keyboard', Handler.Type, { text: 'i' }); + }, 'ioo>'); + + /** + * @todo + * Undefined behavior + */ + // testCase('Selection insert - across two boundary', state, async (editor, ontypeRenameContribution) => { + // const pos = new Position(1, 2); + // editor.setPosition(pos); + // await ontypeRenameContribution.run(pos, true); + // editor.setSelection(new Range(1, 4, 1, 9)); + // editor.trigger('keyboard', Handler.Type, { text: 'i' }); + // }, ''); + + /** + * Break out behavior + */ + testCase('Breakout - type space', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 5); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Type, { text: ' ' }); + }, ''); + + testCase('Breakout - type space then undo', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 5); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Type, { text: ' ' }); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + }, ''); + + testCase('Breakout - type space in middle', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 4); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Type, { text: ' ' }); + }, ''); + + testCase('Breakout - paste content starting with space', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 5); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Paste, { text: ' i="i"' }); + }, ''); + + testCase('Breakout - paste content starting with space then undo', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 5); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Paste, { text: ' i="i"' }); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + }, ''); + + testCase('Breakout - paste content starting with space in middle', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 4); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Paste, { text: ' i' }); + }, ''); + + /** + * Break out with custom stopPattern + */ + + const state3 = { + ...state, + stopPattern: /^s/ + }; + + testCase('Breakout with stop pattern - insert', state3, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 2); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Type, { text: 'i' }); + }, ''); + + testCase('Breakout with stop pattern - insert stop char', state3, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 2); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Type, { text: 's' }); + }, ''); + + testCase('Breakout with stop pattern - paste char', state3, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 2); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Paste, { text: 's' }); + }, ''); + + testCase('Breakout with stop pattern - paste string', state3, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 2); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Paste, { text: 'so' }); + }, ''); + + testCase('Breakout with stop pattern - insert at end', state3, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 5); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Type, { text: 's' }); + }, ''); + + /** + * Delete + */ + testCase('Delete - left char', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 5); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', 'deleteLeft', {}); + }, ''); + + testCase('Delete - left char then undo', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 5); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', 'deleteLeft', {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + }, ''); + + testCase('Delete - left word', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 5); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', 'deleteWordLeft', {}); + }, '<>'); + + testCase('Delete - left word then undo', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 5); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', 'deleteWordLeft', {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + }, ''); + + /** + * Todo: Fix test + */ + // testCase('Delete - left all', state, async (editor, ontypeRenameContribution) => { + // const pos = new Position(1, 3); + // editor.setPosition(pos); + // await ontypeRenameContribution.run(pos, true); + // editor.trigger('keyboard', 'deleteAllLeft', {}); + // }, '>'); + + /** + * Todo: Fix test + */ + // testCase('Delete - left all then undo', state, async (editor, ontypeRenameContribution) => { + // const pos = new Position(1, 5); + // editor.setPosition(pos); + // await ontypeRenameContribution.run(pos, true); + // editor.trigger('keyboard', 'deleteAllLeft', {}); + // CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + // }, '>'); + + testCase('Delete - left all then undo twice', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 5); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', 'deleteAllLeft', {}); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + }, ''); + + testCase('Delete - selection', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 5); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.setSelection(new Range(1, 2, 1, 3)); + editor.trigger('keyboard', 'deleteLeft', {}); + }, ''); + + testCase('Delete - selection across boundary', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 3); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.setSelection(new Range(1, 1, 1, 3)); + editor.trigger('keyboard', 'deleteLeft', {}); + }, 'oo>'); + + /** + * Undo / redo + */ + testCase('Undo/redo - simple undo', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 2); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Type, { text: 'i' }); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + }, ''); + + testCase('Undo/redo - simple undo/redo', state, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 2); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Type, { text: 'i' }); + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + CoreEditingCommands.Redo.runEditorCommand(null, editor, null); + }, ''); + + /** + * Multi line + */ + const state2 = { + text: [ + '', + '' + ], + ranges: [ + new Range(1, 2, 1, 5), + new Range(2, 3, 2, 6), + ] + }; + + testCase('Multiline insert', state2, async (editor, ontypeRenameContribution) => { + const pos = new Position(1, 2); + editor.setPosition(pos); + await ontypeRenameContribution.run(pos, true); + editor.trigger('keyboard', Handler.Type, { text: 'i' }); + }, [ + '', + '' + ]); +}); diff --git a/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index 90f3ab505f177..0f3f9e0202018 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -32,6 +32,7 @@ import 'vs/editor/contrib/linesOperations/linesOperations'; import 'vs/editor/contrib/links/links'; import 'vs/editor/contrib/multicursor/multicursor'; import 'vs/editor/contrib/parameterHints/parameterHints'; +import 'vs/editor/contrib/rename/onTypeRename'; import 'vs/editor/contrib/rename/rename'; import 'vs/editor/contrib/smartSelect/smartSelect'; import 'vs/editor/contrib/snippet/snippetController2'; diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index a6022052c2b3b..b95826e915603 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -391,6 +391,13 @@ export function registerDocumentHighlightProvider(languageId: string, provider: return modes.DocumentHighlightProviderRegistry.register(languageId, provider); } +/** + * Register an on type rename provider. + */ +export function registerOnTypeRenameProvider(languageId: string, provider: modes.OnTypeRenameProvider): IDisposable { + return modes.OnTypeRenameProviderRegistry.register(languageId, provider); +} + /** * Register a definition provider (used by e.g. go to definition). */ @@ -559,6 +566,7 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages { registerHoverProvider: registerHoverProvider, registerDocumentSymbolProvider: registerDocumentSymbolProvider, registerDocumentHighlightProvider: registerDocumentHighlightProvider, + registerOnTypeRenameProvider: registerOnTypeRenameProvider, registerDefinitionProvider: registerDefinitionProvider, registerImplementationProvider: registerImplementationProvider, registerTypeDefinitionProvider: registerTypeDefinitionProvider, diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 08918576d0e56..fea311268e3b2 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2673,6 +2673,11 @@ declare namespace monaco.editor { * Defaults to false. */ readOnly?: boolean; + /** + * Rename matching regions on type. + * Defaults to false. + */ + renameOnType?: boolean; /** * Should the editor render validation decorations. * Defaults to editable. @@ -3882,47 +3887,48 @@ declare namespace monaco.editor { quickSuggestions = 70, quickSuggestionsDelay = 71, readOnly = 72, - renderControlCharacters = 73, - renderIndentGuides = 74, - renderFinalNewline = 75, - renderLineHighlight = 76, - renderValidationDecorations = 77, - renderWhitespace = 78, - revealHorizontalRightPadding = 79, - roundedSelection = 80, - rulers = 81, - scrollbar = 82, - scrollBeyondLastColumn = 83, - scrollBeyondLastLine = 84, - scrollPredominantAxis = 85, - selectionClipboard = 86, - selectionHighlight = 87, - selectOnLineNumbers = 88, - showFoldingControls = 89, - showUnused = 90, - snippetSuggestions = 91, - smoothScrolling = 92, - stopRenderingLineAfter = 93, - suggest = 94, - suggestFontSize = 95, - suggestLineHeight = 96, - suggestOnTriggerCharacters = 97, - suggestSelection = 98, - tabCompletion = 99, - useTabStops = 100, - wordSeparators = 101, - wordWrap = 102, - wordWrapBreakAfterCharacters = 103, - wordWrapBreakBeforeCharacters = 104, - wordWrapColumn = 105, - wordWrapMinified = 106, - wrappingIndent = 107, - wrappingStrategy = 108, - editorClassName = 109, - pixelRatio = 110, - tabFocusMode = 111, - layoutInfo = 112, - wrappingInfo = 113 + renameOnType = 73, + renderControlCharacters = 74, + renderIndentGuides = 75, + renderFinalNewline = 76, + renderLineHighlight = 77, + renderValidationDecorations = 78, + renderWhitespace = 79, + revealHorizontalRightPadding = 80, + roundedSelection = 81, + rulers = 82, + scrollbar = 83, + scrollBeyondLastColumn = 84, + scrollBeyondLastLine = 85, + scrollPredominantAxis = 86, + selectionClipboard = 87, + selectionHighlight = 88, + selectOnLineNumbers = 89, + showFoldingControls = 90, + showUnused = 91, + snippetSuggestions = 92, + smoothScrolling = 93, + stopRenderingLineAfter = 94, + suggest = 95, + suggestFontSize = 96, + suggestLineHeight = 97, + suggestOnTriggerCharacters = 98, + suggestSelection = 99, + tabCompletion = 100, + useTabStops = 101, + wordSeparators = 102, + wordWrap = 103, + wordWrapBreakAfterCharacters = 104, + wordWrapBreakBeforeCharacters = 105, + wordWrapColumn = 106, + wordWrapMinified = 107, + wrappingIndent = 108, + wrappingStrategy = 109, + editorClassName = 110, + pixelRatio = 111, + tabFocusMode = 112, + layoutInfo = 113, + wrappingInfo = 114 } export const EditorOptions: { acceptSuggestionOnCommitCharacter: IEditorOption; @@ -3998,6 +4004,7 @@ declare namespace monaco.editor { quickSuggestions: IEditorOption; quickSuggestionsDelay: IEditorOption; readOnly: IEditorOption; + renameOnType: IEditorOption; renderControlCharacters: IEditorOption; renderIndentGuides: IEditorOption; renderFinalNewline: IEditorOption; @@ -4954,6 +4961,11 @@ declare namespace monaco.languages { */ export function registerDocumentHighlightProvider(languageId: string, provider: DocumentHighlightProvider): IDisposable; + /** + * Register an on type rename provider. + */ + export function registerOnTypeRenameProvider(languageId: string, provider: OnTypeRenameProvider): IDisposable; + /** * Register a definition provider (used by e.g. go to definition). */ @@ -5712,6 +5724,18 @@ declare namespace monaco.languages { provideDocumentHighlights(model: editor.ITextModel, position: Position, token: CancellationToken): ProviderResult; } + /** + * The rename provider interface defines the contract between extensions and + * the live-rename feature. + */ + export interface OnTypeRenameProvider { + stopPattern?: RegExp; + /** + * Provide a list of ranges that can be live-renamed together. + */ + provideOnTypeRenameRanges(model: editor.ITextModel, position: Position, token: CancellationToken): ProviderResult; + } + /** * Value-object that contains additional information when * requesting references. diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index c9bf51adfa5ef..720a82e06103f 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1230,6 +1230,43 @@ declare module 'vscode' { //#endregion + //#region OnTypeRename: https://github.com/microsoft/vscode/issues/88424 + + /** + * The rename provider interface defines the contract between extensions and + * the live-rename feature. + */ + export interface OnTypeRenameProvider { + /** + * Provide a list of ranges that can be live renamed together. + * + * @param document The document in which the command was invoked. + * @param position The position at which the command was invoked. + * @param token A cancellation token. + * @return A list of ranges that can be live-renamed togehter. The ranges must have + * identical length and contain identical text content. The ranges cannot overlap. + */ + provideOnTypeRenameRanges(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; + } + + namespace languages { + /** + * Register a rename provider that works on type. + * + * Multiple providers can be registered for a language. In that case providers are sorted + * by their [score](#languages.match) and the best-matching provider is used. Failure + * of the selected provider will cause a failure of the whole operation. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider An on type rename provider. + * @param stopPattern Stop on type renaming when input text matches the regular expression. Defaults to `^\s`. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + export function registerOnTypeRenameProvider(selector: DocumentSelector, provider: OnTypeRenameProvider, stopPattern?: RegExp): Disposable; + } + + //#endregion + //#region Custom editors: https://github.com/microsoft/vscode/issues/77131 // TODO: diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index c079a43ef3657..061b30226df69 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -261,6 +261,18 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha })); } + // --- on type rename + + $registerOnTypeRenameProvider(handle: number, selector: IDocumentFilterDto[], stopPattern?: IRegExpDto): void { + const revivedStopPattern = stopPattern ? MainThreadLanguageFeatures._reviveRegExp(stopPattern) : undefined; + this._registrations.set(handle, modes.OnTypeRenameProviderRegistry.register(selector, { + stopPattern: revivedStopPattern, + provideOnTypeRenameRanges: (model: ITextModel, position: EditorPosition, token: CancellationToken): Promise => { + return this._proxy.$provideOnTypeRenameRanges(handle, model.uri, position, token); + } + })); + } + // --- references $registerReferenceSupport(handle: number, selector: IDocumentFilterDto[]): void { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 9866e80b3a446..e7a3f40a091f8 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -362,6 +362,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerDocumentHighlightProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentHighlightProvider): vscode.Disposable { return extHostLanguageFeatures.registerDocumentHighlightProvider(extension, checkSelector(selector), provider); }, + registerOnTypeRenameProvider(selector: vscode.DocumentSelector, provider: vscode.OnTypeRenameProvider, stopPattern?: RegExp): vscode.Disposable { + checkProposedApiEnabled(extension); + return extHostLanguageFeatures.registerOnTypeRenameProvider(extension, checkSelector(selector), provider, stopPattern); + }, registerReferenceProvider(selector: vscode.DocumentSelector, provider: vscode.ReferenceProvider): vscode.Disposable { return extHostLanguageFeatures.registerReferenceProvider(extension, checkSelector(selector), provider); }, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index e156349df4cc5..7bf429a3d82a9 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -362,6 +362,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerHoverProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerEvaluatableExpressionProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerDocumentHighlightProvider(handle: number, selector: IDocumentFilterDto[]): void; + $registerOnTypeRenameProvider(handle: number, selector: IDocumentFilterDto[], stopPattern: IRegExpDto | undefined): void; $registerReferenceSupport(handle: number, selector: IDocumentFilterDto[]): void; $registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto, displayName: string): void; $registerDocumentFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void; @@ -1286,6 +1287,7 @@ export interface ExtHostLanguageFeaturesShape { $provideHover(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; $provideEvaluatableExpression(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; $provideDocumentHighlights(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; + $provideOnTypeRenameRanges(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; $provideReferences(handle: number, resource: UriComponents, position: IPosition, context: modes.ReferenceContext, token: CancellationToken): Promise; $provideCodeActions(handle: number, resource: UriComponents, rangeOrSelection: IRange | ISelection, context: modes.CodeActionContext, token: CancellationToken): Promise; $releaseCodeActions(handle: number, cacheId: number): void; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index a2d7ca79b982f..b854a5cbf1543 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -318,6 +318,26 @@ class DocumentHighlightAdapter { } } +class OnTypeRenameAdapter { + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.OnTypeRenameProvider + ) { } + + provideOnTypeRenameRanges(resource: URI, position: IPosition, token: CancellationToken): Promise { + + const doc = this._documents.getDocument(resource); + const pos = typeConvert.Position.to(position); + + return asPromise(() => this._provider.provideOnTypeRenameRanges(doc, pos, token)).then(value => { + if (Array.isArray(value)) { + return coalesce(value.map(typeConvert.Range.from)); + } + return undefined; + }); + } +} + class ReferenceAdapter { constructor( @@ -1350,7 +1370,8 @@ type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | Hov | RangeFormattingAdapter | OnTypeFormattingAdapter | NavigateTypeAdapter | RenameAdapter | SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter | TypeDefinitionAdapter | ColorProviderAdapter | FoldingProviderAdapter | DeclarationAdapter - | SelectionRangeAdapter | CallHierarchyAdapter | DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter | EvaluatableExpressionAdapter; + | SelectionRangeAdapter | CallHierarchyAdapter | DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter | EvaluatableExpressionAdapter + | OnTypeRenameAdapter; class AdapterData { constructor( @@ -1594,6 +1615,19 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return this._withAdapter(handle, DocumentHighlightAdapter, adapter => adapter.provideDocumentHighlights(URI.revive(resource), position, token), undefined); } + // --- on type rename + + registerOnTypeRenameProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.OnTypeRenameProvider, stopPattern?: RegExp): vscode.Disposable { + const handle = this._addNewAdapter(new OnTypeRenameAdapter(this._documents, provider), extension); + const serializedStopPattern = stopPattern ? ExtHostLanguageFeatures._serializeRegExp(stopPattern) : undefined; + this._proxy.$registerOnTypeRenameProvider(handle, this._transformDocumentSelector(selector), serializedStopPattern); + return this._createDisposable(handle); + } + + $provideOnTypeRenameRanges(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise { + return this._withAdapter(handle, OnTypeRenameAdapter, adapter => adapter.provideOnTypeRenameRanges(URI.revive(resource), position, token), undefined); + } + // --- references registerReferenceProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.ReferenceProvider): vscode.Disposable {