Enable renaming of matching jsx tags (#179806)

Fixes #159534

Uses the new linked editing api to make f2 rename matching jsx tags
This commit is contained in:
Matt Bierner
2023-04-12 12:42:59 -07:00
committed by GitHub
parent 3cf3ef8897
commit 26ccce443f
4 changed files with 105 additions and 30 deletions

View File

@@ -1007,6 +1007,18 @@
"description": "%typescript.preferences.useAliasesForRenames%",
"scope": "language-overridable"
},
"javascript.preferences.renameMatchingJsxTags": {
"type": "boolean",
"default": true,
"description": "%typescript.preferences.renameMatchingJsxTags%",
"scope": "language-overridable"
},
"typescript.preferences.renameMatchingJsxTags": {
"type": "boolean",
"default": true,
"description": "%typescript.preferences.renameMatchingJsxTags%",
"scope": "language-overridable"
},
"typescript.updateImportsOnFileMove.enabled": {
"type": "string",
"enum": [

View File

@@ -165,6 +165,7 @@
"configuration.tsserver.watchOptions.synchronousWatchDirectory": "Disable deferred watching on directories. Deferred watching is useful when lots of file changes might occur at once (e.g. a change in node_modules from running npm install), but you might want to disable it with this flag for some less-common setups.",
"typescript.preferences.renameShorthandProperties.deprecationMessage": "The setting 'typescript.preferences.renameShorthandProperties' has been deprecated in favor of 'typescript.preferences.useAliasesForRenames'",
"typescript.preferences.useAliasesForRenames": "Enable/disable introducing aliases for object shorthand properties during renames.",
"typescript.preferences.renameMatchingJsxTags": "When on a JSX tag, try to rename the matching tag instead of renaming the symbol. Requires using TypeScript 5.1+ in the workspace.",
"typescript.workspaceSymbols.scope": "Controls which files are searched by [Go to Symbol in Workspace](https://code.visualstudio.com/docs/editor/editingevolved#_open-symbol-by-name).",
"typescript.workspaceSymbols.scope.allOpenProjects": "Search all open JavaScript or TypeScript projects for symbols.",
"typescript.workspaceSymbols.scope.currentProject": "Only search for symbols in the current JavaScript or TypeScript project.",

View File

@@ -6,16 +6,27 @@
import * as path from 'path';
import * as vscode from 'vscode';
import { DocumentSelector } from '../configuration/documentSelector';
import * as languageIds from '../configuration/languageIds';
import { API } from '../tsServer/api';
import type * as Proto from '../tsServer/protocol/protocol';
import * as typeConverters from '../typeConverters';
import { ClientCapability, ITypeScriptServiceClient, ServerResponse } from '../typescriptService';
import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService';
import FileConfigurationManager from './fileConfigurationManager';
import { conditionalRegistration, requireSomeCapability } from './util/dependentRegistration';
import { LanguageDescription } from '../configuration/languageDescription';
type RenameResponse = {
readonly type: 'rename';
readonly body: Proto.RenameResponseBody;
} | {
readonly type: 'jsxLinkedEditing';
readonly spans: readonly Proto.TextSpan[];
};
class TypeScriptRenameProvider implements vscode.RenameProvider {
public constructor(
private readonly language: LanguageDescription,
private readonly client: ITypeScriptServiceClient,
private readonly fileConfigurationManager: FileConfigurationManager
) { }
@@ -24,22 +35,30 @@ class TypeScriptRenameProvider implements vscode.RenameProvider {
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise<vscode.Range | null> {
): Promise<vscode.Range | undefined> {
if (this.client.apiVersion.lt(API.v310)) {
return null;
return undefined;
}
const response = await this.execRename(document, position, token);
if (response?.type !== 'response' || !response.body) {
return null;
if (!response) {
return undefined;
}
const renameInfo = response.body.info;
if (!renameInfo.canRename) {
return Promise.reject<vscode.Range>(renameInfo.localizedErrorMessage);
switch (response.type) {
case 'rename': {
const renameInfo = response.body.info;
if (!renameInfo.canRename) {
return Promise.reject<vscode.Range>(renameInfo.localizedErrorMessage);
}
return typeConverters.Range.fromTextSpan(renameInfo.triggerSpan);
}
case 'jsxLinkedEditing': {
return response.spans
.map(typeConverters.Range.fromTextSpan)
.find(range => range.contains(position));
}
}
return typeConverters.Range.fromTextSpan(renameInfo.triggerSpan);
}
public async provideRenameEdits(
@@ -47,51 +66,93 @@ class TypeScriptRenameProvider implements vscode.RenameProvider {
position: vscode.Position,
newName: string,
token: vscode.CancellationToken
): Promise<vscode.WorkspaceEdit | null> {
): Promise<vscode.WorkspaceEdit | undefined> {
const file = this.client.toOpenTsFilePath(document);
if (!file) {
return undefined;
}
const response = await this.execRename(document, position, token);
if (!response || response.type !== 'response' || !response.body) {
return null;
if (!response || token.isCancellationRequested) {
return undefined;
}
const renameInfo = response.body.info;
if (!renameInfo.canRename) {
return Promise.reject<vscode.WorkspaceEdit>(renameInfo.localizedErrorMessage);
}
switch (response.type) {
case 'rename': {
const renameInfo = response.body.info;
if (!renameInfo.canRename) {
return Promise.reject<vscode.WorkspaceEdit>(renameInfo.localizedErrorMessage);
}
if (renameInfo.fileToRename) {
const edits = await this.renameFile(renameInfo.fileToRename, newName, token);
if (edits) {
return edits;
} else {
return Promise.reject<vscode.WorkspaceEdit>(vscode.l10n.t("An error occurred while renaming file"));
if (renameInfo.fileToRename) {
const edits = await this.renameFile(renameInfo.fileToRename, newName, token);
if (edits) {
return edits;
} else {
return Promise.reject<vscode.WorkspaceEdit>(vscode.l10n.t("An error occurred while renaming file"));
}
}
return this.updateLocs(response.body.locs, newName);
}
case 'jsxLinkedEditing': {
return this.updateLocs([{
file,
locs: response.spans.map((span): Proto.RenameTextSpan => ({ ...span })),
}], newName);
}
}
return this.updateLocs(response.body.locs, newName);
}
public async execRename(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken
): Promise<ServerResponse.Response<Proto.RenameResponse> | undefined> {
): Promise<RenameResponse | undefined> {
const file = this.client.toOpenTsFilePath(document);
if (!file) {
return undefined;
}
// Prefer renaming matching jsx tag when available
if (this.client.apiVersion.gte(API.v510) &&
vscode.workspace.getConfiguration(this.language.id).get('preferences.renameMatchingJsxTags', true) &&
this.looksLikePotentialJsxTagContext(document, position)
) {
const args = typeConverters.Position.toFileLocationRequestArgs(file, position);
const response = await this.client.execute('linkedEditingRange', args, token);
if (response.type !== 'response' || !response.body) {
return undefined;
}
return { type: 'jsxLinkedEditing', spans: response.body.ranges };
}
const args: Proto.RenameRequestArgs = {
...typeConverters.Position.toFileLocationRequestArgs(file, position),
findInStrings: false,
findInComments: false
};
return this.client.interruptGetErr(() => {
return this.client.interruptGetErr(async () => {
this.fileConfigurationManager.ensureConfigurationForDocument(document, token);
return this.client.execute('rename', args, token);
const response = await this.client.execute('rename', args, token);
if (response.type !== 'response' || !response.body) {
return undefined;
}
return { type: 'rename', body: response.body };
});
}
private looksLikePotentialJsxTagContext(document: vscode.TextDocument, position: vscode.Position): boolean {
if (![languageIds.typescriptreact, languageIds.javascript, languageIds.javascriptreact].includes(document.languageId)) {
return false;
}
const prefix = document.getText(new vscode.Range(position.line, 0, position.line, position.character));
return /\<\/?\s*[\w\d_$.]*$/.test(prefix);
}
private updateLocs(
locations: ReadonlyArray<Proto.SpanGroup>,
newName: string
@@ -138,6 +199,7 @@ class TypeScriptRenameProvider implements vscode.RenameProvider {
export function register(
selector: DocumentSelector,
language: LanguageDescription,
client: ITypeScriptServiceClient,
fileConfigurationManager: FileConfigurationManager,
) {
@@ -145,6 +207,6 @@ export function register(
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
return vscode.languages.registerRenameProvider(selector.semantic,
new TypeScriptRenameProvider(client, fileConfigurationManager));
new TypeScriptRenameProvider(language, client, fileConfigurationManager));
});
}

View File

@@ -80,7 +80,7 @@ export default class LanguageProvider extends Disposable {
import('./languageFeatures/quickFix').then(provider => this._register(provider.register(selector, this.client, this.fileConfigurationManager, this.commandManager, this.client.diagnosticsManager, this.telemetryReporter))),
import('./languageFeatures/refactor').then(provider => this._register(provider.register(selector, this.client, this.fileConfigurationManager, this.commandManager, this.telemetryReporter))),
import('./languageFeatures/references').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/rename').then(provider => this._register(provider.register(selector, this.client, this.fileConfigurationManager))),
import('./languageFeatures/rename').then(provider => this._register(provider.register(selector, this.description, this.client, this.fileConfigurationManager))),
import('./languageFeatures/semanticTokens').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/signatureHelp').then(provider => this._register(provider.register(selector, this.client))),
import('./languageFeatures/smartSelect').then(provider => this._register(provider.register(selector, this.client))),