Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable renaming of matching jsx tags #179806

Merged
merged 1 commit into from
Apr 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions extensions/typescript-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
1 change: 1 addition & 0 deletions extensions/typescript-language-features/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
120 changes: 91 additions & 29 deletions extensions/typescript-language-features/src/languageFeatures/rename.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
) { }
Expand All @@ -24,74 +35,124 @@ 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(
document: vscode.TextDocument,
position: vscode.Position,
newName: string,
token: vscode.CancellationToken
): Promise<vscode.WorkspaceEdit | null> {
const response = await this.execRename(document, position, token);
if (!response || response.type !== 'response' || !response.body) {
return null;
): Promise<vscode.WorkspaceEdit | undefined> {
const file = this.client.toOpenTsFilePath(document);
if (!file) {
return undefined;
}

const renameInfo = response.body.info;
if (!renameInfo.canRename) {
return Promise.reject<vscode.WorkspaceEdit>(renameInfo.localizedErrorMessage);
const response = await this.execRename(document, position, token);
if (!response || token.isCancellationRequested) {
return undefined;
}

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"));
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"));
}
}

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
Expand Down Expand Up @@ -138,13 +199,14 @@ class TypeScriptRenameProvider implements vscode.RenameProvider {

export function register(
selector: DocumentSelector,
language: LanguageDescription,
client: ITypeScriptServiceClient,
fileConfigurationManager: FileConfigurationManager,
) {
return conditionalRegistration([
requireSomeCapability(client, ClientCapability.Semantic),
], () => {
return vscode.languages.registerRenameProvider(selector.semantic,
new TypeScriptRenameProvider(client, fileConfigurationManager));
new TypeScriptRenameProvider(language, client, fileConfigurationManager));
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))),
Expand Down