未验证 提交 f76ca9f6 编写于 作者: P Pine 提交者: GitHub

Merge pull request #92597 from microsoft/octref/live-rename

On Type Rename for #88424
......@@ -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.") }
......
......@@ -789,6 +789,20 @@ export interface DocumentHighlightProvider {
provideDocumentHighlights(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult<DocumentHighlight[]>;
}
/**
* 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<IRange[]>;
}
/**
* Value-object that contains additional information when
* requesting references.
......@@ -1642,6 +1656,11 @@ export const DocumentSymbolProviderRegistry = new LanguageFeatureRegistry<Docume
*/
export const DocumentHighlightProviderRegistry = new LanguageFeatureRegistry<DocumentHighlightProvider>();
/**
* @internal
*/
export const OnTypeRenameProviderRegistry = new LanguageFeatureRegistry<OnTypeRenameProvider>();
/**
* @internal
*/
......
......@@ -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
}
/**
......
/*---------------------------------------------------------------------------------------------
* 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;
}
/*---------------------------------------------------------------------------------------------
* 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<boolean>('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>(OnTypeRenameContribution.ID);
}
private readonly _editor: ICodeEditor;
private _enabled: boolean;
private readonly _visibleContextKey: IContextKey<boolean>;
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<void> {
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<void> {
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<void> {
const controller = OnTypeRenameContribution.get(editor);
if (controller) {
return Promise.resolve(controller.run(editor.getPosition(), true));
}
return Promise.resolve();
}
}
const OnTypeRenameCommand = EditorCommand.bindToContribution<OnTypeRenameContribution>(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);
/*---------------------------------------------------------------------------------------------
* 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<void>,
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: '<ooo></ooo>',
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' });
}, '<iooo></iooo>');
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' });
}, '<oioo></oioo>');
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' });
}, '<oooi></oooi>');
/**
* 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' });
}, '<iooo></iooo>');
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' });
}, '<oioo></oioo>');
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' });
}, '<oooi></oooi>');
/**
* 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<ooo></ooo>');
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' });
}, '<ooo>i</ooo>');
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' });
}, '<ooo><i/ooo>');
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' });
}, '<ooo></ooo>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' });
}, '<iiooo></iiooo>');
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' });
}, '<ioioo></ioioo>');
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' });
}, '<iooo>i</iooo>');
/**
* 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' });
}, '<ioo></ioo>');
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' });
}, '<i></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></oo>');
/**
* @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' });
// }, '<ooioo>');
/**
* 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: ' ' });
}, '<ooo ></ooo>');
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);
}, '<ooo></ooo>');
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: ' ' });
}, '<oo o></ooo>');
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"' });
}, '<ooo i="i"></ooo>');
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);
}, '<ooo></ooo>');
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' });
}, '<oo io></ooo>');
/**
* 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' });
}, '<iooo></iooo>');
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' });
}, '<sooo></ooo>');
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' });
}, '<sooo></ooo>');
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' });
}, '<soooo></ooo>');
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' });
}, '<ooos></ooo>');
/**
* 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', {});
}, '<oo></oo>');
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);
}, '<ooo></ooo>');
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);
}, '<ooo></ooo>');
/**
* 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);
// }, '></ooo>');
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);
}, '<ooo></ooo>');
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', {});
}, '<oo></oo>');
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></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);
}, '<ooo></ooo>');
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);
}, '<iooo></iooo>');
/**
* Multi line
*/
const state2 = {
text: [
'<ooo>',
'</ooo>'
],
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' });
}, [
'<iooo>',
'</iooo>'
]);
});
......@@ -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';
......
......@@ -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: <any>registerHoverProvider,
registerDocumentSymbolProvider: <any>registerDocumentSymbolProvider,
registerDocumentHighlightProvider: <any>registerDocumentHighlightProvider,
registerOnTypeRenameProvider: <any>registerOnTypeRenameProvider,
registerDefinitionProvider: <any>registerDefinitionProvider,
registerImplementationProvider: <any>registerImplementationProvider,
registerTypeDefinitionProvider: <any>registerTypeDefinitionProvider,
......
......@@ -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<EditorOption.acceptSuggestionOnCommitCharacter, boolean>;
......@@ -3998,6 +4004,7 @@ declare namespace monaco.editor {
quickSuggestions: IEditorOption<EditorOption.quickSuggestions, ValidQuickSuggestionsOptions>;
quickSuggestionsDelay: IEditorOption<EditorOption.quickSuggestionsDelay, number>;
readOnly: IEditorOption<EditorOption.readOnly, boolean>;
renameOnType: IEditorOption<EditorOption.renameOnType, boolean>;
renderControlCharacters: IEditorOption<EditorOption.renderControlCharacters, boolean>;
renderIndentGuides: IEditorOption<EditorOption.renderIndentGuides, boolean>;
renderFinalNewline: IEditorOption<EditorOption.renderFinalNewline, boolean>;
......@@ -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<DocumentHighlight[]>;
}
/**
* 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<IRange[]>;
}
/**
* Value-object that contains additional information when
* requesting references.
......
......@@ -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<Range[]>;
}
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:
......
......@@ -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, <modes.OnTypeRenameProvider>{
stopPattern: revivedStopPattern,
provideOnTypeRenameRanges: (model: ITextModel, position: EditorPosition, token: CancellationToken): Promise<IRange[] | undefined> => {
return this._proxy.$provideOnTypeRenameRanges(handle, model.uri, position, token);
}
}));
}
// --- references
$registerReferenceSupport(handle: number, selector: IDocumentFilterDto[]): void {
......
......@@ -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);
},
......
......@@ -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<modes.Hover | undefined>;
$provideEvaluatableExpression(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise<modes.EvaluatableExpression | undefined>;
$provideDocumentHighlights(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise<modes.DocumentHighlight[] | undefined>;
$provideOnTypeRenameRanges(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise<IRange[] | undefined>;
$provideReferences(handle: number, resource: UriComponents, position: IPosition, context: modes.ReferenceContext, token: CancellationToken): Promise<ILocationDto[] | undefined>;
$provideCodeActions(handle: number, resource: UriComponents, rangeOrSelection: IRange | ISelection, context: modes.CodeActionContext, token: CancellationToken): Promise<ICodeActionListDto | undefined>;
$releaseCodeActions(handle: number, cacheId: number): void;
......
......@@ -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<IRange[] | undefined> {
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<IRange[] | undefined> {
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 {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册