diff --git a/packages/language-server/src/plugins/typescript/features/RenameProvider.ts b/packages/language-server/src/plugins/typescript/features/RenameProvider.ts index 2b0a6a3c8..f6ba55816 100644 --- a/packages/language-server/src/plugins/typescript/features/RenameProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/RenameProvider.ts @@ -58,7 +58,7 @@ export class RenameProviderImpl implements RenameProvider { const docs = new Map([[tsDoc.filePath, fragment]]); let convertedRenameLocations: (ts.RenameLocation & { range: Range; - })[] = await this.mapRenameLocationsToParent(renameLocations, docs); + })[] = await this.mapAndFilterRenameLocations(renameLocations, docs); // eslint-disable-next-line max-len const additionalRenameForPropRenameInsideComponentWithProp = await this.getAdditionLocationsForRenameOfPropInsideComponentWithProp( document, @@ -127,7 +127,7 @@ export class RenameProviderImpl implements RenameProvider { if ( !renameInfo.canRename || renameInfo.kind === ts.ScriptElementKind.jsxAttribute || - renameInfo.fullDisplayName?.includes("JSX.IntrinsicElements") + renameInfo.fullDisplayName?.includes('JSX.IntrinsicElements') ) { return null; } @@ -189,7 +189,7 @@ export class RenameProviderImpl implements RenameProvider { rename.fileName !== updatePropLocation.fileName || this.isInSvelte2TsxPropLine(fragment, rename), ); - return await this.mapRenameLocationsToParent(replacementsForProp, fragments); + return await this.mapAndFilterRenameLocations(replacementsForProp, fragments); } /** @@ -222,7 +222,7 @@ export class RenameProviderImpl implements RenameProvider { const idx = (match.index || 0) + match[0].lastIndexOf(match[1]); const replacementsForProp = lang.findRenameLocations(updatePropLocation.fileName, idx, false, false) || []; - return await this.mapRenameLocationsToParent(replacementsForProp, fragments); + return await this.mapAndFilterRenameLocations(replacementsForProp, fragments); } // --------> svelte2tsx? @@ -278,12 +278,13 @@ export class RenameProviderImpl implements RenameProvider { * The rename locations the ts language services hands back are relative to the * svelte2tsx generated code -> map it back to the original document positions. * Some of those positions could be unmapped (line=-1), these are handled elsewhere. + * Also filter out wrong renames. */ - private async mapRenameLocationsToParent( + private async mapAndFilterRenameLocations( renameLocations: readonly ts.RenameLocation[], fragments: Map, ): Promise<(ts.RenameLocation & { range: Range })[]> { - return Promise.all( + const mappedLocations = await Promise.all( renameLocations.map(async (loc) => { let doc = fragments.get(loc.fileName); if (!doc) { @@ -297,6 +298,27 @@ export class RenameProviderImpl implements RenameProvider { }; }), ); + return this.filterWrongRenameLocations(mappedLocations); + } + + private filterWrongRenameLocations( + mappedLocations: (ts.RenameLocation & { range: Range })[], + ): (ts.RenameLocation & { range: Range })[] { + return mappedLocations.filter((loc) => { + const snapshot = this.getSnapshot(loc.fileName); + if (!(snapshot instanceof SvelteDocumentSnapshot)) { + return true; + } + + const content = snapshot.getText(0, snapshot.getLength()); + const svelteInstanceOfFn = '__sveltets_instanceOf('; + // When the user renames a Svelte component, ts will also want to rename + // `__sveltets_instanceOf(TheComponentToRename)`. Prevent that. + return ( + content.lastIndexOf(svelteInstanceOfFn, loc.textSpan.start) !== + loc.textSpan.start - svelteInstanceOfFn.length + ); + }); } private mapRangeToOriginal(doc: SnapshotFragment, textSpan: ts.TextSpan): Range { diff --git a/packages/language-server/test/plugins/typescript/features/RenameProvider.test.ts b/packages/language-server/test/plugins/typescript/features/RenameProvider.test.ts index eb5751666..542fcf28c 100644 --- a/packages/language-server/test/plugins/typescript/features/RenameProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/RenameProvider.test.ts @@ -27,7 +27,8 @@ describe('RenameProvider', () => { const renameDoc1 = await openDoc('rename.svelte'); const renameDoc2 = await openDoc('rename2.svelte'); const renameDoc3 = await openDoc('rename3.svelte'); - return { provider, renameDoc1, renameDoc2, renameDoc3, docManager }; + const renameDoc4 = await openDoc('rename4.svelte'); + return { provider, renameDoc1, renameDoc2, renameDoc3, renameDoc4, docManager }; async function openDoc(filename: string) { const filePath = getFullPath(filename); @@ -234,6 +235,57 @@ describe('RenameProvider', () => { }); }); + it('should do rename of svelte component', async () => { + const { provider, renameDoc4 } = await setup(); + const result = await provider.rename(renameDoc4, Position.create(1, 12), 'ChildNew'); + + assert.deepStrictEqual(result, { + changes: { + [getUri('rename4.svelte')]: [ + { + newText: 'ChildNew', + range: { + start: { + line: 1, + character: 11, + }, + end: { + line: 1, + character: 16, + }, + }, + }, + { + newText: 'ChildNew', + range: { + start: { + line: 7, + character: 5, + }, + end: { + line: 7, + character: 10, + }, + }, + }, + { + newText: 'ChildNew', + range: { + start: { + line: 8, + character: 5, + }, + end: { + line: 8, + character: 10, + }, + }, + }, + ], + }, + }); + }); + it('should allow rename of variable', async () => { const { provider, renameDoc1 } = await setup(); const result = await provider.prepareRename(renameDoc1, Position.create(1, 25)); diff --git a/packages/language-server/test/plugins/typescript/testfiles/rename4.svelte b/packages/language-server/test/plugins/typescript/testfiles/rename4.svelte new file mode 100644 index 000000000..fe697c741 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/rename4.svelte @@ -0,0 +1,10 @@ + + +
+ + +
\ No newline at end of file