mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 13:00:58 +00:00
onTypeRename: wordPattern (for #104823)
This commit is contained in:
parent
0817b70ca9
commit
fae07df7c3
|
@ -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);
|
||||
|
|
|
@ -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; }>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
7
src/vs/monaco.d.ts
vendored
|
@ -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;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
10
src/vs/vscode.proposed.d.ts
vendored
10
src/vs/vscode.proposed.d.ts
vendored
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue