mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-24 12:19:20 +00:00
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:
@@ -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": [
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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))),
|
||||
|
||||
Reference in New Issue
Block a user