onTypeRename: wordPattern (for #104823)

This commit is contained in:
Martin Aeschlimann 2020-08-21 13:54:31 +02:00
parent 0817b70ca9
commit fae07df7c3
9 changed files with 105 additions and 54 deletions

View file

@ -28,7 +28,7 @@ namespace TagCloseRequest {
export const type: RequestType<TextDocumentPositionParams, string, any, any> = new RequestType('html/tag');
}
namespace OnTypeRenameRequest {
export const type: RequestType<TextDocumentPositionParams, Range[] | null, any, any> = new RequestType('html/onTypeRename');
export const type: RequestType<TextDocumentPositionParams, LspRange[] | null, any, any> = new RequestType('html/onTypeRename');
}
// experimental: semantic tokens
@ -172,9 +172,14 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua
disposable = languages.registerOnTypeRenameProvider(documentSelector, {
async provideOnTypeRenameRanges(document, position) {
const param = client.code2ProtocolConverter.asTextDocumentPositionParams(document, position);
const response = await client.sendRequest(OnTypeRenameRequest.type, param);
return response || [];
return client.sendRequest(OnTypeRenameRequest.type, param).then(response => {
if (response) {
return {
ranges: response.map(r => client.protocol2CodeConverter.asRange(r))
};
}
return undefined;
});
}
});
toDispose.push(disposable);

View file

@ -813,12 +813,12 @@ export interface DocumentHighlightProvider {
*/
export interface OnTypeRenameProvider {
stopPattern?: RegExp;
wordPattern?: RegExp;
/**
* Provide a list of ranges that can be live-renamed together.
*/
provideOnTypeRenameRanges(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult<IRange[]>;
provideOnTypeRenameRanges(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult<{ ranges: IRange[]; wordPattern?: RegExp; }>;
}
/**

View file

@ -29,6 +29,7 @@ import * as strings from 'vs/base/common/strings';
import { registerColor } from 'vs/platform/theme/common/colorRegistry';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { Color } from 'vs/base/common/color';
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
export const CONTEXT_ONTYPE_RENAME_INPUT_VISIBLE = new RawContextKey<boolean>('onTypeRenameInputVisible', false);
@ -58,7 +59,8 @@ export class OnTypeRenameContribution extends Disposable implements IEditorContr
private _currentRequestModelVersion: number | null;
private _currentDecorations: string[]; // The one at index 0 is the reference one
private _stopPattern: RegExp;
private _languageWordPattern: RegExp | null;
private _currentWordPattern: RegExp | null;
private _ignoreChangeEvent: boolean;
private readonly _localToDispose = this._register(new DisposableStore());
@ -73,7 +75,8 @@ export class OnTypeRenameContribution extends Disposable implements IEditorContr
this._visibleContextKey = CONTEXT_ONTYPE_RENAME_INPUT_VISIBLE.bindTo(contextKeyService);
this._currentDecorations = [];
this._stopPattern = /\s/;
this._languageWordPattern = null;
this._currentWordPattern = null;
this._ignoreChangeEvent = false;
this._localToDispose = this._register(new DisposableStore());
@ -113,6 +116,11 @@ export class OnTypeRenameContribution extends Disposable implements IEditorContr
return;
}
this._languageWordPattern = LanguageConfigurationRegistry.getWordDefinition(model.getLanguageIdentifier().id);
this._localToDispose.add(model.onDidChangeLanguageConfiguration(() => {
this._languageWordPattern = LanguageConfigurationRegistry.getWordDefinition(model.getLanguageIdentifier().id);
}));
const rangeUpdateScheduler = new Delayer(200);
const triggerRangeUpdate = () => {
this._rangeUpdateTriggerPromise = rangeUpdateScheduler.trigger(() => this.updateRanges());
@ -160,8 +168,12 @@ export class OnTypeRenameContribution extends Disposable implements IEditorContr
}
const referenceValue = model.getValueInRange(referenceRange);
if (this._stopPattern.test(referenceValue)) {
return this.clearRanges();
if (this._currentWordPattern) {
const match = referenceValue.match(this._currentWordPattern);
const matchLength = match ? match[0].length : 0;
if (matchLength !== referenceValue.length) {
return this.clearRanges();
}
}
let edits: IIdentifiedSingleEditOperation[] = [];
@ -281,9 +293,8 @@ export class OnTypeRenameContribution extends Disposable implements IEditorContr
if (response?.ranges) {
ranges = response.ranges;
}
if (response?.stopPattern) {
this._stopPattern = response.stopPattern;
}
this._currentWordPattern = response?.wordPattern || this._languageWordPattern;
let foundReferenceRange = false;
for (let i = 0, len = ranges.length; i < len; i++) {
@ -403,7 +414,7 @@ registerEditorCommand(new OnTypeRenameCommand({
export function getOnTypeRenameRanges(model: ITextModel, position: Position, token: CancellationToken): Promise<{
ranges: IRange[],
stopPattern?: RegExp
wordPattern?: RegExp
} | undefined | null> {
const orderedByScore = OnTypeRenameProviderRegistry.ordered(model);
@ -412,16 +423,16 @@ export function getOnTypeRenameRanges(model: ITextModel, position: Position, tok
// (good = none empty array)
return first<{
ranges: IRange[],
stopPattern?: RegExp
wordPattern?: RegExp
} | undefined>(orderedByScore.map(provider => () => {
return Promise.resolve(provider.provideOnTypeRenameRanges(model, position, token)).then((ranges) => {
if (!ranges) {
return Promise.resolve(provider.provideOnTypeRenameRanges(model, position, token)).then((res) => {
if (!res) {
return undefined;
}
return {
ranges,
stopPattern: provider.stopPattern
ranges: res.ranges,
wordPattern: res.wordPattern || provider.wordPattern
};
}, (err) => {
onUnexpectedExternalError(err);

View file

@ -55,22 +55,21 @@ suite('On type rename', () => {
function testCase(
name: string,
initialState: { text: string | string[], stopPattern?: RegExp },
initialState: { text: string | string[], responseWordPattern?: RegExp, providerWordPattern?: RegExp },
operations: (editor: TestEditor) => Promise<void>,
expectedEndText: string | string[]
) {
test(name, async () => {
disposables.add(modes.OnTypeRenameProviderRegistry.register(mockFileSelector, {
stopPattern: initialState.stopPattern || /\s/,
wordPattern: initialState.providerWordPattern,
provideOnTypeRenameRanges(model: ITextModel, pos: IPosition) {
const wordAtPos = model.getWordAtPosition(pos);
if (wordAtPos) {
const matches = model.findMatches(wordAtPos.word, false, false, true, USUAL_WORD_SEPARATORS, false);
assert.ok(matches.length > 0);
return matches.map(m => m.range);
return { ranges: matches.map(m => m.range), wordPattern: initialState.responseWordPattern };
}
return [];
return { ranges: [], wordPattern: initialState.responseWordPattern };
}
}));
@ -294,12 +293,12 @@ suite('On type rename', () => {
}, '<oo io></ooo>');
/**
* Break out with custom stopPattern
* Break out with custom provider wordPattern
*/
const state3 = {
...state,
stopPattern: /s/
providerWordPattern: /[a-yA-Y]+/
};
testCase('Breakout with stop pattern - insert', state3, async (editor) => {
@ -311,26 +310,38 @@ suite('On type rename', () => {
testCase('Breakout with stop pattern - insert stop char', state3, async (editor) => {
const pos = new Position(1, 2);
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: 's' });
}, '<sooo></ooo>');
await editor.trigger('keyboard', Handler.Type, { text: 'z' });
}, '<zooo></ooo>');
testCase('Breakout with stop pattern - paste char', state3, async (editor) => {
const pos = new Position(1, 2);
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Paste, { text: 's' });
}, '<sooo></ooo>');
await editor.trigger('keyboard', Handler.Paste, { text: 'z' });
}, '<zooo></ooo>');
testCase('Breakout with stop pattern - paste string', state3, async (editor) => {
const pos = new Position(1, 2);
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Paste, { text: 'so' });
}, '<soooo></ooo>');
await editor.trigger('keyboard', Handler.Paste, { text: 'zo' });
}, '<zoooo></ooo>');
testCase('Breakout with stop pattern - insert at end', state3, async (editor) => {
const pos = new Position(1, 5);
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: 's' });
}, '<ooos></ooo>');
await editor.trigger('keyboard', Handler.Type, { text: 'z' });
}, '<oooz></ooo>');
const state4 = {
...state,
providerWordPattern: /[a-yA-Y]+/,
responseWordPattern: /[a-eA-E]+/
};
testCase('Breakout with stop pattern - insert stop char, respos', state4, async (editor) => {
const pos = new Position(1, 2);
await editor.setPosition(pos);
await editor.trigger('keyboard', Handler.Type, { text: 'i' });
}, '<iooo></ooo>');
/**
* Delete

7
src/vs/monaco.d.ts vendored
View file

@ -5793,11 +5793,14 @@ declare namespace monaco.languages {
* the live-rename feature.
*/
export interface OnTypeRenameProvider {
stopPattern?: RegExp;
wordPattern?: RegExp;
/**
* Provide a list of ranges that can be live-renamed together.
*/
provideOnTypeRenameRanges(model: editor.ITextModel, position: Position, token: CancellationToken): ProviderResult<IRange[]>;
provideOnTypeRenameRanges(model: editor.ITextModel, position: Position, token: CancellationToken): ProviderResult<{
ranges: IRange[];
wordPattern?: RegExp;
}>;
}
/**

View file

@ -1064,9 +1064,11 @@ declare module 'vscode' {
* @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.
* identical length and contain identical text content. The ranges cannot overlap. Optional a word pattern
* that overrides the word pattern defined when registering the provider. Live rename stops as soon as the renamed content
* no longer matches the word pattern.
*/
provideOnTypeRenameRanges(document: TextDocument, position: Position, token: CancellationToken): ProviderResult<Range[]>;
provideOnTypeRenameRanges(document: TextDocument, position: Position, token: CancellationToken): ProviderResult<{ ranges: Range[]; wordPattern?: RegExp; }>;
}
namespace languages {
@ -1079,10 +1081,10 @@ declare module 'vscode' {
*
* @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`.
* @param wordPattern Word pattern for this provider.
* @return A [disposable](#Disposable) that unregisters this provider when being disposed.
*/
export function registerOnTypeRenameProvider(selector: DocumentSelector, provider: OnTypeRenameProvider, stopPattern?: RegExp): Disposable;
export function registerOnTypeRenameProvider(selector: DocumentSelector, provider: OnTypeRenameProvider, wordPattern?: RegExp): Disposable;
}
//#endregion

View file

@ -263,12 +263,19 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha
// --- on type rename
$registerOnTypeRenameProvider(handle: number, selector: IDocumentFilterDto[], stopPattern?: IRegExpDto): void {
const revivedStopPattern = stopPattern ? MainThreadLanguageFeatures._reviveRegExp(stopPattern) : undefined;
$registerOnTypeRenameProvider(handle: number, selector: IDocumentFilterDto[], wordPattern?: IRegExpDto): void {
const revivedWordPattern = wordPattern ? MainThreadLanguageFeatures._reviveRegExp(wordPattern) : 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);
wordPattern: revivedWordPattern,
provideOnTypeRenameRanges: async (model: ITextModel, position: EditorPosition, token: CancellationToken): Promise<{ ranges: IRange[]; wordPattern?: RegExp; } | undefined> => {
const res = await this._proxy.$provideOnTypeRenameRanges(handle, model.uri, position, token);
if (res) {
return {
ranges: res.ranges,
wordPattern: res.wordPattern ? MainThreadLanguageFeatures._reviveRegExp(res.wordPattern) : undefined
};
}
return undefined;
}
}));
}

View file

@ -1350,7 +1350,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>;
$provideOnTypeRenameRanges(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise<{ ranges: IRange[]; wordPattern?: IRegExpDto; } | 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;

View file

@ -324,14 +324,17 @@ class OnTypeRenameAdapter {
private readonly _provider: vscode.OnTypeRenameProvider
) { }
provideOnTypeRenameRanges(resource: URI, position: IPosition, token: CancellationToken): Promise<IRange[] | undefined> {
provideOnTypeRenameRanges(resource: URI, position: IPosition, token: CancellationToken): Promise<{ ranges: IRange[]; wordPattern?: RegExp; } | 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));
if (value && Array.isArray(value.ranges)) {
return {
ranges: coalesce(value.ranges.map(typeConvert.Range.from)),
wordPattern: value.wordPattern
};
}
return undefined;
});
@ -1549,15 +1552,24 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF
// --- on type rename
registerOnTypeRenameProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.OnTypeRenameProvider, stopPattern?: RegExp): vscode.Disposable {
registerOnTypeRenameProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.OnTypeRenameProvider, wordPattern?: 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);
const serializedWordPattern = wordPattern ? ExtHostLanguageFeatures._serializeRegExp(wordPattern) : undefined;
this._proxy.$registerOnTypeRenameProvider(handle, this._transformDocumentSelector(selector), serializedWordPattern);
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);
$provideOnTypeRenameRanges(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise<{ ranges: IRange[]; wordPattern?: extHostProtocol.IRegExpDto; } | undefined> {
return this._withAdapter(handle, OnTypeRenameAdapter, async adapter => {
const res = await adapter.provideOnTypeRenameRanges(URI.revive(resource), position, token);
if (res) {
return {
ranges: res.ranges,
wordPattern: res.wordPattern ? ExtHostLanguageFeatures._serializeRegExp(res.wordPattern) : undefined
};
}
return undefined;
}, undefined);
}
// --- references